!
前面已经讲了 远程组件加载的几种方式,这里具体的实现一下(这里就不具体的去陈述异步模块加载了,默认已经懂了)
首先我们先观察下,基于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/>}
</>
}
效果

第二种,基于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导致崩溃的问题
效果


这里总结下要注意的几个点:
组件打包的配置
拿到组件后的setState的逻辑
两种方案的对比
amd:能够很好的解决组件依赖的问题和版本的问题。
esm:不能很好的解决依赖的问题,依赖完全由项目本身提供
- THE END -
共有 0 条评论