前言
最近有一个朋友问我,他想在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并发执行,在命令加上--parallel
options即可
项目搭建完成之后,分别在项目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 新特性 - 模块联邦》