前言
最近有一个朋友问我,他想在A项目里使用B项目的一个弹窗组件应该如何实现。当然最常见的就是将弹窗代码写成npm包的形式,项目A和B分别注入弹窗模块。那么还有其他方法,接下来就来探讨一下UMD共享机制和webpack5的模块联邦是如何实现这个需求的。推荐阅读<Webpack5 新特性 - 模块联邦>文章和YouTube视频教学
模块联邦 (微服务)
接下来将构造两个独立的SPA的应用,在runtime时使用模块联邦进行组件共享。
- 项目A包含
SayHelloFromA组件,这个SayHelloFromA组件也会共享到项目B中。 - 项目B包含
SayHelloFromB组件,这个SayHelloFromB组件也会共享到项目A中。

通过webpack模块联邦的模式,可以每个应用都可以单独开发和部署。并且通过零部署来实现共享模块更新(SayHelloFromA组件在A项目里更新了,B项目无需重新部署即可应用到最新的SayHelloFromA组件)
开始
使用pnpm workspace管理两个独立的react项目。模块联邦允许团队独立运行的,真实场景一般都是在独立的仓库体系中。但这里只是演示项目就放在一起好管理。
通过CRA创建的端口默认为3000,此时需要将项目B端口改为3001通过cross-env即可
ts
"scripts": {
"start": "cross-env PORT=3001 react-scripts start",
...
},
pnpm中Monorepo并发执行,在命令加上--paralleloptions即可 
项目搭建完成之后,分别在项目A创建SayHelloFromA组件、在项目B组件创建SayHelloFromB组件,并且再各自的APP.tsx引入这两个组件
ts
// a-project > SayHelloFromA.tsx
export default function SayHelloFromA() {
return <h1>Hello from Application A! </h1>
}
// a-project > SayHelloFromB.tsx
export default function SayHelloFromB() {
return <h1>Hello from Application B! </h1>
}
接下来通过customize-cra来修改create-react-app项目的webpack配置。当然还有其他方案,不过推荐使用customize-cra来实现
在根目录添加customize-cra和react-app-rewired包
ts
pnpm add customize-cra react-app-rewired -w
编写config-overrides.js文件要跟package.json文件放置在同一层级。customize-cra暴露了一些函数API。使用addWebpackPlugin方法扩展webpack的plugin参数,使用setWebpackPublicPath设置publicPath路径。 如下代码库的名称定义为application_a并且将SayHelloFromA组件暴露出去
typescript
// A项目 => config-overrides.js
const {
override,
addWebpackPlugin
} = require("customize-cra");
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
// 配置公共路径
const publicPath = 'http://localhost:3000/';
// 模块联邦相关配置
const ModuleFederationConfig = new ModuleFederationPlugin({
name: 'application_a',
library: { type: 'var', name: 'application_a' },
filename: 'remoteEntry.js',
exposes: {
'SayHelloFromA': './src/SayHelloFromA',
},
remotes: {
'application_b': 'application_b',
},
shared: ['react', 'react-dom'],
})
module.exports = override(
setWebpackPublicPath(publicPath),
addWebpackPlugin(ModuleFederationConfig)
)
// B项目 => 类似把publicPath端口改为3001,以及对应名称改为`application_b`暴露组件改为SayHelloFromB组件,这里就不在统一展示了
配置完config-overrides.js之后,在public/index.html添加script链接remoteEntry文件
ts
项目A
<head>
...
<script src="http://localhost:3001/remoteEntry.js"></script>
</head>
项目B
<head>
...
<script src="http://localhost:3000/remoteEntry.js"></script>
</head>
将package.jso中使用react-scripts的命令全部改为react-app-rewired启动
ts
"start": "cross-env PORT=3001 react-app-rewired start",
...
通过上面配置完之后,启动项目发现有如下报错。下面来看看这些报错如何解决
问题一: 当我配置完成运行报错了如下错误,那应该如何解决呢? webpack也提供了Troubleshooting来追击问题
根据webpack中Troubleshooting提供的解决方案
首先将原本index.tsx文件内容改为bootstrap.tsx,然后再新建一个index.js导入import('./bootstrap');
其次修改config-overrides.js的配置,将shared由原来的数组改为对象
arduino
// 修改前
shared:['react', 'react-dom']
// 修改后
shared: {
"react-dom": "^18.2.0",
react: {
eager: true,
},
},
通过上面的修改解决了问题一,之后我在App.tsx引入application_b/SayHelloFromB组件报了问题二的错误。
问题二: 当我在项目A中导入项目B的组件报错了,从下图不难看出项目B的remoteEntry.js加载成功。 Stack Overflow提供的问题解答
解决该问题也很简单,只需要在src目录新建index.d.ts声明这个模块即可
通过上面声明解决了问题二但此时又遇到了问题三
问题三: 报错提示说找不到SayHelloFromB,但我查看了remoteEntry.js文件是存在的SayHelloFromB组件的
在webpack的Troubleshooting提供的解决方案。将config-overrides.js的exposes的key在前面追加./即可
ts
// 修改之前
new ModuleFederationPlugin({
...
exposes: {
'SayHelloFromB': './src/SayHelloFromB',
},
})
// 修改之后
new ModuleFederationPlugin({
...
exposes: {
'./SayHelloFromB': './src/SayHelloFromB',
},
})
将上面的三个问题修改完成之后,就可以看到如下效果,在项目A里引用了项目B的SayHelloFromB组件
项目目录结构如下:
HTTP请求如下图: (第二次访问状态码为304,缓存机制) 
总结
- 通过如上步骤,使用
webpackv5的模块联邦属性实现了不同项目之间的组件共享。使用模块联邦的好处在于不需要向npm共享机制那样每个项目都需要安装公共模块,模块稍微改动各个项目之间需要更新然后重新部署。 比UMD共享机制的好处在于包体积得到了保证,我们都知道UMD格式是不会做tree shaking的。 UMD共享机制的实现机制在下一节进行探讨- 此次演示的完整代码git仓库
参考文献
Federation: A game-changer in JavaScript architecture
Getting Started With Federated Modules
Merge Proposal: Module federation and code sharing between bundles. Many builds act as one
精读《Webpack5 新特性 - 模块联邦》