webpack 5 新增 Module Federation(模块联邦)功能,他可以帮助将多个独立的构建组成一个应用程序,不同的构建可以独立的开发与部署。
借助模块联邦我们可以一定程度的实现微前端
// webpack.config.js
export default {
plugins: [
new ModuleFederationPlugin({
name: "app_two", // 当前应用名
filename: "remoteEntry.js", // 外部应用引用当前应用模块时的加载入口
exposes: {
Search: "./src/Search" // 输出给外部应用使用的模块
},
remotes: {
appOne: 'appOne@http://localhost:3003/remoteEntry.js' // 当前应用会用到的远程应用地址
}
shared: [ // 与远程模块共享的模块,与远程模块共同配置,这样在页面中就只会加载一次这个library, 用来避免重复加载第三方依赖
"react",
"react-dom"
]
})
]
};
// /index.js 注意,入口一定要动态引入模块
import('./bootstrap');
// /bootstrap.js
ReactDOM.render(
<App/>
document.getElementById('root'),
);
// /App.js 中使用远程应用的模块 import('远程应用/远程应用模块')
const AppOne = React.lazy(() => import('appOne/Button'));
shared 说明
/ **
*应该在共享范围内共享的模块的高级配置。
* /
declare interface SharedConfig {
/ **
*直接在异步请求后面包含提供的和后备模块。这也允许在初始加载中使用此共享模块。所有可能的共享模块也都需要急切。
* /
eager?: boolean;
/ **
*应提供共享范围的提供的模块。如果在共享作用域中找不到共享模块或版本无效,则还充当回退模块。默认为属性名称。
* /
import?: DevTool;
/ **
*软件包名称,用于从描述文件中确定所需的版本。仅当无法根据请求自动确定包名称时才需要。
* /
packageName?: string;
/ **
*共享范围中来自模块的版本要求。
* /
requiredVersion?: DevTool;
/ **
*在共享范围内的此键下查找模块。
* /
shareKey?: string;
/ **
*共享范围名称。
* /
shareScope?: string;
/ **
*在共享范围内仅允许共享模块的单个版本(默认情况下处于禁用状态)。
* /
singleton?: boolean;
/ **
*如果版本无效,则不接受共享模块(默认为是,如果本地后备模块可用并且共享模块不是单例,否则为no,如果未指定所需的版本,则无效)。
* /
strictVersion?: boolean;
/ **
*所提供模块的版本。将替换较低的匹配版本,但不会替换较高的版本。
* /
version?: DevTool;
}
由于 关于“远程依赖应用的引用”是在 build 打包时,打包到代码中的固定值(url),为了通过文件名区分是否有更新,我们需要给remoteEntry.[contenthash].js 加上 hash,;这个时候如果, “远程依赖应用”有版本更新,那么使用这个“远程依赖应用“的应用也要更新(否则拿到的时是期的资源),如此一来我们 “只发布有改动的应用” 这个目标就没办法达成了。
为了实现这个目标,最好是将 remoteEntry 的确切地址(url)在项目运行时注入,这样就避免了改动一个应用,其他应用也跟着更新的窘境
参考案例
除此之外还可以借助
__webpack_init_sharing__ __webpack_share_scopes__
实现 “动态远程容器” 参考 通过这套方案我们可以实现:项目线上运行时,动态决定要渲染哪些远程应用模块
module.noParse
来处理工具库的解析,否则会引发模块引用的 报错如果我们的项目使用 lerna 做一个monorepo 的仓库,每个子 package 代表着一个 应用,预期这些应用都会用 TS 编写,而且 TS 的配置应该是一样的,所以,我按照如下配置
// tsconfig.json 配置
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@shell/*": ["./shells/*"],
"@module/*": ["./modules/*"]
},
}
}
// remote 模块 /modules/library/antd/index.tsx
export {
Button } from 'antd';
export {
Row } from 'antd';
// host 中调用 remote 模块
import {
Button,Row } from '@module/library/antd';
// Error1: 会导致不输出样式文件
// export { Button, Row } from 'antd';
// Error2: 不能直接在 import {Button} 后 export {Button}; webpack 会报错
// import { Button, Row } from 'antd';
// export { Button, Row };
// Error3: export default 可以实现, 但是使用时不方便
// 例如只能这样使用:
// import antd from '@module/library/antd';
// const {Button} = antd;
// export default { Button, Row };
// 可实现方案,编写库应用时有点繁琐,但是使用时简单 例: import {Button} from '@module/library/antd';
import {
Button as _Button, Row as _Row } from 'antd';
export const Button = _Button;
export const Row = _Row;
// 1. 前提:所有构建的 webpacCofnig 都要禁用 webpack.HotModuleReplacementPlugin()因为现阶段的 HMR 只能支持一个构建,多个构建会出错
// webpack.config.js
const cofnig = {
plugins: [
// new webpack.HotModuleReplacementPlugin(), // 禁用
]
}
// 2. 主应用开启 hmr
// /shells/admin/bootstrap.js 中加入如下内容
if (module.hot) {
module.hot.accept();
}
// 3. 除主应用外,其他应用通过 MultiCompiler 一起构建, 并通过 webpack watch 监控源码变化实时编译到指定目录(buildPath)
const compiler = webpack([module1Config, module2config, ...]);
compiler.watch({
}, (err, stats) => {
console.log(stats);
});
// 4. 主应用借助 webpack-dev-server 来实现 源码监控、实时编译、文件映射到页面、页面实时刷新
const server = new WebpackDevServer(webpack(hostConfig), {
transportMode:'ws',
hot: true,
// 5. 借助主应用 webpack-dev-server 的 static 配置,将其他应用的构建结果纳入“监控及映射到页面”的范围
static: [
{
directory: path.join(buildPath, module1Static), publicPath: path.join(publicPath, module1Static)},
{
directory: path.join(buildPath, module2Static), publicPath: path.join(publicPath, module2Static)},
],
});
server.listen(customPort, customHost);