基于AMD与ESM,实现远程组件加载(手写一下)

2024-10-14 1,166 10/14

前面已经讲了 远程组件加载的几种方式,这里具体的实现一下(这里就不具体的去陈述异步模块加载了,默认已经懂了)

首先我们先观察下,基于amd打包之后的代码

其实就是一个define方法

define(name,['dep1','dep2'],(dep1,dep2)=>{
// 打包之后的代码
return // 入口文件 export default
})

我们要做的就是实现这个define方法,这里其实可以去建议看下require.js是如何实现的,不过我们还是自己写一个吧

简易的define方法

import React from 'react';
import * as ReactDOM from 'react-dom';
import _ from 'lodash';

export const modules = {}; // 各模块
export const moduleDependencies = {}; // 模块与其依赖的模块的对应关系
export const dependencyModules = {}; // 被依赖的模块与依赖此模块的对应关系

/** 加载 JS 文件 */
const loadModule = (name) => {
  if (name.includes('@')) {
    const script = document.createElement('script');
    const [moduleName, moduleVersion] = name.split('@');
    if (moduleName && moduleVersion) {
        script.src = `.......${moduleName}.js`; // (路径自己拼接)
      try {
        document.getElementsByTagName('head')[0].appendChild(script);
        script.onerror = function () {
          console.error('组件加载失败')
        };
      } catch (err) {
        console.info('components err ->', err);
      }
    }
  }
};

/** 激活当前模块 */
const runModule = (name, dependencies, tenantId) => {
  moduleDependencies[name] = dependencies;
  // 所有依赖是否完成
  if (checkDependencies(name, dependencies, tenantId)) {
    const module = modules[name];
    module.fired = true;
    const res = module.factory.apply(window, getDependencies(name));
    if (res) {
      if (res.default) {
        module.exports = res.default;
      } else {
        // return attributes if exist
        let moduleName = name;
        if (moduleName.includes('@')) {
          moduleName = moduleName.split('@')[0];
        }

        if (res[moduleName]) {
          module.exports = res[moduleName];
        } else {
          const upName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
          if (res[upName]) {
            module.exports = res[upName];
          } else {
            module.exports = res;
          }
        }
      }
    }
    // 是否有依赖此模块的其他模块
    if (dependencyModules[name]) {
      dependencyModules[name].forEach((item) => {
        if (modules[item] && !modules[item].fired) {
          runModule(item, moduleDependencies[item], tenantId);
        }
      });
    }
  }
};

/** 获取所有依赖 */
const getDependencies = (name) => {
  if (moduleDependencies[name]) {
    return moduleDependencies[name].map((item) => modules[item].exports);
  }
  return [];
};

/** 检查所有依赖加载情况 */
const checkDependencies = (name, dependencies, tenantId) => {
  let flag = true;
  dependencies.map((depName) => {
    if (!modules[depName] || !modules[depName].fired) {
      loadModule(depName, tenantId);
      flag = false;
    }

    if (!dependencyModules[depName]) {
      dependencyModules[depName] = [];
    }

    if (!dependencyModules[depName].includes(name)) {
      dependencyModules[depName].push(name);
    }
  });

  return flag;
};

/** 创建 define 方法 */
const define = (name: string, dependencies: any, factory: any, tenantId?: number) => {
  if (typeof name === 'string') {
    if (!modules[name]) {
      modules[name] = { exports: {}, loaded: false, fired: false };
    }
    modules[name].factory = factory;
    modules[name].loaded = true;
    runModule(name, dependencies, tenantId);
  }
};

// 将公共库引入amd体系
define('react', [], () => ({
  default: React,
}));

define('lodash', [], () => ({
  default: _,
}));
define('react-dom', [], () => ({
  default: ReactDOM,
}));

window.define = define;
export { define }

组件的打包处理,公共依赖剔除(依赖外部化),剔除的公共依赖会在define中通过外部在回调函数中注入,不剔除公共依赖的话,打包后的组件代码就很冗余,且这里去尝试了下,如果没有,会error

第二,依赖是由谁决定的,由外部主动的注入还是打包组件的deps,这里肯定是由打包的组件决定的(重点在与define方法的实现)

示例代码

vite配置(注意剔除公共依赖)

import { defineConfig,loadEnv } from 'vite'
import { resolve } from "path"
import react from '@vitejs/plugin-react'

export default defineConfig((mode)=>{
  return {
    plugins: [react()],
    build: {
      rollupOptions: {
        external: ['react', 'react-dom'],
        output: {
          globals: {
            react: 'React',
            'react-dom': 'ReactDOM'
          },
          amd: {
            id: 'my-module' // 模块唯一的id
          }
        },
      } ,
      outDir: 'lib',
      lib: {
        entry: resolve('./src/App.tsx'),
        name: 'ESDrager',
        fileName: format => `index.${format}.js`,
        formats: ['amd'],
      },
    }
  }
})

示例代码

import React from "react";
import define from './utils'

export default ()=>{
    const [num,setNum] = React.useState(0)
    const [TestComponent,setTestComponent] = React.useState(null)
    const click =async ()=>{
        // 这里传入的name存在问题,因为没有使用cdn的方式引入,就直接更改的define方法,这个细节不重要,理解实现就行
        define('./index.amd.js',['react,reactDom'],(fac)=>{
            setTestComponent(()=>fac)
        })
}
    return <>
        <button onClick={()=>click()}>点击加一11</button>
        <button onClick={()=>setNum(v => v+1)}>{num}</button>
        {TestComponent &&  <TestComponent/>}
    </>
}

效果

基于AMD与ESM,实现远程组件加载(手写一下)

 

第二种,基于esm实现,,思路其实就是使用的import,知道为什么import可以吗,可以看一下,前面的一篇文章,webpack如何支持的懒加载

这里打包的处理是一样的,需要剔除公共依赖

vite配置

import { defineConfig,loadEnv } from 'vite'
import { resolve } from "path"
import react from '@vitejs/plugin-react'

export default defineConfig((mode)=>{
  return {
    plugins: [react()],
    build: {
      rollupOptions: {
        external: ['react', 'react-dom'],
        output: {
          globals: {
            react: 'React',
            'react-dom': 'ReactDOM'
          }
        }
      } ,
      outDir: 'lib',
      lib: {
        entry: resolve('./src/App.tsx'),
        name: 'ESDrager',
        fileName: format => `index.${format}.js`,
        formats: ['esm'],
      },
    }
  }
})

示例代码

import React from "react";

export default ()=>{
    const [num,setNum] = React.useState(0)
    const [TestComponent,setTestComponent] = React.useState(null)
    const click =async ()=>{
        // 这里可以引cdn上的,因为import的实现本就是script去加载的
       const module = await import('./index.esm.js')
        console.dir(module)
       setTestComponent(()=>module.default)
}
    return <>
        <button onClick={()=>click()}>点击加一11</button>
        <button onClick={()=>setNum(v => v+1)}>{num}</button>
        {TestComponent &&  <TestComponent/>}
    </>
}
注意一个点 setCom的时候不要直接 setCom(module.default) 可以看下前面的 setCom导致崩溃的问题

效果

基于AMD与ESM,实现远程组件加载(手写一下)

基于AMD与ESM,实现远程组件加载(手写一下)

 

这里总结下要注意的几个点:

组件打包的配置

拿到组件后的setState的逻辑

 

两种方案的对比

amd:能够很好的解决组件依赖的问题和版本的问题。

esm:不能很好的解决依赖的问题,依赖完全由项目本身提供

- THE END -
5

非特殊说明,本博所有文章均为博主原创。

共有 0 条评论