模块联邦,这个名字也挺奇怪的,我感觉叫动态引用 更合适,有点像 动态库 .so
的这种感觉。
直接依赖远程 js,其映射关系为供给方生成的一个 js
,用于查找其库的真实地址。
我们一直也这么用的,比如 vconsole
这种工具,一种方式就是直接加载线上脚本使用。
但这种方式不够工程化,引用、打包、使用时,需要特殊对待,不像本地包那样可以直接 import 'xxx'
。
ModuleFederationPlugin
就是提供一种方式,可以将想共享的内容发布到线上,使用方再通过类似本地包的形式去使用它。
供给方 A 的配置:
目标是生成用于共享的库 (font.js
, theme.js
...),和用于查找库地址的清单 (entry.js
),
js
new ModuleFederationPlugin({
name: 'app_frame',
// version 号主要是怕升级多台机器过程中,半半拉拉的时候,有 gap,线上一些依赖找不到了,保留老的清单映射
// filename 就是 库对应清单 js 了
filename: 'static/js/remote-entry-v2.js',
// 这是 entry.js 暴露方式,直接在 window 上暴露变量了
library: { type: 'var', name: 'app_frame' },
// 需要共享的内容
exposes: {
'./font.css': path.resolve(paths.appSrc, './css/font.css'),
'./theme.css': path.resolve(paths.appSrc, './css/theme/index.css'),
},
// 一些可以共享的库,先按下不讲
shared: {
react: {
// 这个库(比如react)在整个页面上只能有一个实例。如果 B 发现 A 提供的React版本不兼容,它不会加载自己的版本,而是会直接报错
singleton: true,
requiredVersion: deps.react,
},
'react-dom': {
singleton: true,
requiredVersion: deps['react-dom'],
},
'react/jsx-runtime': {
singleton: true,
requiredVersion: deps.react,
},
},
}),
需求方 B 的配置:
目的是引用上面生成的 entry.js
,然后找到对应共享库
js
new ModuleFederationPlugin({
// 库清单 entry.js 的地址
remotes: {
// key 在项目内自行定义, name 为提供容器内 name 字段
app_frame: `app_frame@https://xxx.com/static/js/remote-entry-v2.js`,
},
// shared 这里和上面重复了发现没?其实说的就是:
// 如果上面清单里有这些库的地址,我就不用我自己生成的了,直接复用清单里的
shared: {
react: {
requiredVersion: reactVersion,
},
'react-dom': {
requiredVersion: reactDomVersion,
},
'react/jsx-runtime': {
requiredVersion: reactVersion,
},
},
}),
B 在页面中使用的时候,就这么用:
js
import 'app_frame/font.css';
import 'app_frame/theme.css';
流程
-
A 先 build 生成
entry.js
,font.[hash].js
,theme.[hash].js
,abc.[hash].js
... 放到自己的网站上 -
B 打开了,先加载
entry.js
(因为它是入口,地址也是固定的)-
找到
font.css
/theme.css
的地址,加载它们 -
entry.js
中定义的shared
的库- 如果满足自己的版本需求,就复用
entry.js
里的shared
库(直接加载entry.js
中的对应地址) - 如果不满足,就不看它了,加载自己的
.js
- 如果满足自己的版本需求,就复用
-
更进一步
其实上面已经看出端倪了,shared 的都是一些不会经常变的库,这几个要是整合到一起,然后正好也满足 B 的需求,B 就不用加载自己的那份了嘛:
给 A 加上一个提取这几个库的功能:
js
optimization: {
splitChunks: {
cacheGroups: {
firmware: {
test: new RegExp(`node_modules${stp}(react|react-dom)([${stp}]|$)`),
name: 'firmware',
chunks: 'all',
enforce: true,
priority: 30,
},
}
}
}
这样把这三个 react 相关的,放到了 firmware.[hash].js
中,A 当然本身就会加载它,在 B 里我们也做类似的配置:
js
optimization: {
splitChunks: {
cacheGroups: {
firmware: {
test: new RegExp(`node_modules${stp}(react|react-dom)([${stp}]|$)`),
// 注意这里换了个名字
name: 'contained-firmware',
chunks: 'all',
enforce: true,
priority: 30,
},
}
}
}
这里换了个名字,主要是为了方便验证,到底加载的是哪个 js。
- 如果复用成功,只会加载
firmware.[hash].js
,两次(第二次就有浏览器缓存了) - 如果复用失败,A 加载
firmware.[hash].js
,B 加载contained-firmware.[hash].js
如何保证复用成功?
其实很简单,只要保证版本一致就行了,在上面 ModuleFederationPlugin
的配置里,已经写上了。
刷新下,可以观察到,只去加载 firmware.[hash].js
的。