背景
- 项目比较庞大,需要把几大功能模块当成独立应用拎出来管理,减轻整体项目压力,实现微前端效果。
- 使用webpack5的模块联邦可以实现独立项目(模块)之间的组件共用,A项目可以使用B项目抛出来的组件,实现项目之间的代码共享。
模块联邦参数介绍
ModuleFederationPlugin
- name: 应用名称,需要唯一性;
- filename: 入口文件名称,用于对外提供模块时候的入口文件名;
- exposes: 暴露出去的文件名称,被引用的;
- remotes: 依赖的远程模块,用于引入外部其他模块;
- shared: 配置共享的组件,一般是对第三方库做共享使用;
new ModuleFederationPlugin({
// 应用名称
name: "myApp",
// 暴露出去生成的文件名称,被引用的
filename: "remoteEntry.js",
// 暴露出去的模块,被其他应用引用
exposes: {
'./Bootstrap': './src/App.tsx',
},
// 远程模块,引用外部的模块
remotes: {
app1: "myAppChild@http://localhost:3009/remoteEntry.js",
},
shared: {
react: { singleton: true, eager: true, requiredVersion: deps.react },
"react-dom": {
singleton: true,
eager: true,
requiredVersion: deps["react-dom"],
},
"react-router-dom": {
singleton: true,
eager: true,
requiredVersion: deps["react-router-dom"],
}
}
})
- 模块联邦 shared
// 公共依赖shared的配置项
Shared = string[] | {
[string]: {
eager?: boolean; // 是否立即加载模块而不是异步加载
import?: false | SharedItem; // 应该提供给共享作用域的模块。如果在共享范围中没有发现共享模块或版本无效,还充当回退模块。默认为属性名
packageName?: string; // 设置包名称以查找所需的版本。只有当包名不能根据请求自动确定时,才需要这样做(如要禁用自动推断,请将requiredVersion设置为false)。
requiredVersion?: false | string; // 共享范围内模块的版本要求
shareKey?: string; // 用这个名称在共享范围中查找模块
shareScope?: string; // 共享范围名称
singleton?: boolean; // 是否在共享作用域中只允许共享模块的一个版本 (单例模式).
strictVersion?: boolean; // 如果版本无效则不接受共享模块(默认为true,如果本地回退模块可用且共享模块不是一个单例,否则为false,如果没有指定所需的版本则无效)
version?: false | string; // 所提供模块的版本,将替换较低的匹配版本
}[]
}
主应用
- modulefederation配置
const moduleName = 'myApp'
const fileName = "remoteEntry.js"
const deps = require("./package.json").dependencies;
// 静态导入远程模块
const remotesModules = {
app1: "myAppChild@http://localhost:3009/remoteEntry.js",
app2: "myAppChild2@http://localhost:3010/remoteEntry.js"
}
module.exports = {
// 模块名代表整个子应用
name: moduleName,
// 暴露出去的文件名称
filename: fileName,
// 暴露出去的模块
// exposes: exportModules,
// 引入外部模块
remotes: remotesModules,
shared: {
...deps,
react: { singleton: true, eager: true, requiredVersion: deps.react },
"react-dom": {
singleton: true,
eager: true,
requiredVersion: deps["react-dom"],
},
"react-router-dom": {
singleton: true,
eager: true,
requiredVersion: deps["react-router-dom"],
},
}
}
- 动态链接远程容器
App.tsx
// 加载组件
function loadComponent(scope: any, module: string) {
return async () => {
// 初始化共享作用域。这将使用此构建中提供的已知模块和所有远程
await __webpack_init_sharing__('default');
const container = window[scope]; // or get the container somewhere else
// 初始化容器,它可以提供共享模块
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
const Module = factory();
return Module;
};
}
const urlCache = new Set();
const useDynamicScript = (url: string) => {
const [ready, setReady] = React.useState(false);
const [errorLoading, setErrorLoading] = React.useState(false);
React.useEffect(() => {
if (!url) return;
if (urlCache.has(url)) {
setReady(true);
setErrorLoading(false);
return;
}
setReady(false);
setErrorLoading(false);
const element = document.createElement('script');
element.src = url;
element.type = 'text/javascript';
element.async = true;
element.onload = () => {
urlCache.add(url);
setReady(true);
};
element.onerror = () => {
setReady(false);
setErrorLoading(true);
};
document.head.appendChild(element);
return () => {
urlCache.delete(url);
document.head.removeChild(element);
};
}, [url]);
return {
errorLoading,
ready,
};
};
const componentCache = new Map();
export const useFederatedComponent = (remoteUrl: string, scope: string, module: string) => {
const key = `${remoteUrl}-${scope}-${module}`;
const [Component, setComponent] = React.useState(null);
const { ready, errorLoading } = useDynamicScript(remoteUrl);
React.useEffect(() => {
if (Component) setComponent(null);
// Only recalculate when key changes
}, [key]);
React.useEffect(() => {
if (ready && !Component) {
const Comp = React.lazy(loadComponent(scope, module));
componentCache.set(key, Comp);
setComponent(Comp as any);
}
// key includes all dependencies (scope/module)
}, [Component, ready, key]);
return { errorLoading, Component };
};
// 加载模块hook
// 切换url, scope, module状态进行动态导入FederatedComponent模块
const { Component: FederatedComponent, errorLoading }: any = useFederatedComponent(url, scope, module);
子应用
- app1: modulefederation配置
const moduleName = 'myAppChild'
const fileName = "remoteEntry.js"
const exportModules = {
'./Bootstrap': './src/App.tsx',
}
const deps = require("./package.json").dependencies;
module.exports = {
// 模块名代表整个子应用
name: moduleName,
// 暴露出去的文件名称
filename: fileName,
// 暴露出去的模块
exposes: exportModules,
// 引入外部模块
// remotes: remotesModules,
shared: {
...deps,
react: { singleton: true, eager: true, requiredVersion: deps.react },
"react-dom": {
singleton: true,
eager: true,
requiredVersion: deps["react-dom"],
},
"react-router-dom": {
singleton: true,
eager: true,
requiredVersion: deps["react-router-dom"],
},
}
}
- app2: modulefederation配置
const moduleName = 'myAppChild2'
const fileName = "remoteEntry.js"
const exportModules = {
'./Bootstrap': './src/App.tsx',
}
const deps = require("./package.json").dependencies;
module.exports = {
// 模块名代表整个子应用
name: moduleName,
// 暴露出去的文件名称
filename: fileName,
// 暴露出去的模块
exposes: exportModules,
// 引入外部模块
// remotes: remotesModules,
shared: {
...deps,
react: { singleton: true, eager: true, requiredVersion: deps.react },
"react-dom": {
singleton: true,
eager: true,
requiredVersion: deps["react-dom"],
},
"react-router-dom": {
singleton: true,
eager: true,
requiredVersion: deps["react-router-dom"],
},
}
}
微前端的好处
1.支持独立打包,独立升级,对比单体项目的部署更方便,粒度更小,独立打包构建的速度也快,维护更方便。
微前端的缺点
1.要搞几套项目代码进行配置,比单体项目略微复杂。
遇到的坑
- Uncaught Error: createRoot(…): Target container is not a DOM element.
// 由于react18后使用createRoot
// 子应用expose出去时要注意,不是把root.render组件暴露出去,而是把内容组件export default出去,支持export class或者function
const exportModules = {
'./Bootstrap': './src/App.tsx',
}
demo用的lerna进行依赖包整合,按照lerna文档操作即可,如果觉得繁琐,可以直接把my-app\my-app-child\my-app-child2独立安装使用即可