webpack5 的 Module Federation 相信大家都不陌生了,它为前端的模块共享,带来了全新的解决方案
这里不再对其基础的使用做说明了,还没使用的过的同学可以先去了解一下,官方提供的这里示例仓库覆盖了绝大部分使用场景,有兴趣的同学建议看一下。
本文主要是对其中的 Shared 的原理做下解析,此前我一直对它是怎么实现的十分好奇
shared: 主要是用来避免项目出现多个公共依赖。例如 react、react-dom。一般 shared 都会在共享模块的服务都 配置了对应的模块,他们打包时会将其单独抽成一个 chunk,如果远程的模块没有提供或者还没有加载,那么就加载自己服务的资源。
个人觉得就算不用于共享, shared 也可以用于一种的优化手段,对公共的模块都单独抽成一个 chunk,降低 js 体积,并充分利用缓存
先贴一下示例的代码,部署两个服务 app1 app2, 共享模块 semver 和 lodash, app2 还作为 remote 提供了 一个 Button 组件
app1
# app1 config
new ModuleFederationPlugin({
name: 'app1',
remotes: {
app2: `app2@${getRemoteEntryUrl(3002)}`,
},
shared: { semver: { singleton: true }, 'lodash': { singleton: true } },
}),
# app1 index.js
import('./bootstrap');
# app1 bootstrap.js
import semver from 'semver';
import lodash from 'lodash';
const obj = {
v: '1.2.3'
}
console.log('demo', semver.valid(lodash.get(obj, 'v')))
import('app2/Button').then((res) => {
console.log('res', res.default())
})
app2
new ModuleFederationPlugin({
name: 'app2',
library: { type: 'var', name: 'app2' },
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button',
},
shared: { semver: { singleton: true }, 'lodash': { singleton: true } },
}),
// app2 index.js
import('./bootstrap');
// app2 button.js
import semver from 'semver';
import lodash from 'lodash';
const Button = () => {
const obj = {
v: '1.2.3'
}
return semver.valid(lodash.get(obj, 'v'))
}
export default Button;
怎么判断加载远程服务,还是自己服务的 shared 模块?
当我们异步加载 bootstrap.js 时,会编译成如下代码
// src_bootstrap 会单独抽取为一个 chunk, 当__webpack_require__.e 的 Promise 状态变为 resolve 时
// 表示 src_bootstrap 已经在 __webpack_modules__ 中添加完毕了,就可以用__webpack_require__去执行了
__webpack_require__.e("src_bootstrap_js").then(
__webpack_require__.bind(__webpack_require__, "./src/bootstrap.js"));
当使用 webpack_require.e 加载模块的时候,其实会经过下面三个方法的处理
// 会去加载 remote 的 entry,并加载配置的 shared
__webpack_require__.f.consumes = (chunkId, promises) = {}
// 判断是否已经加载,会创建 script 标签 去加载
__webpack_require__.f.j = (chunkId, promises) = {}
// 加载远程的模块,比如我们这里的 Button,这里逻辑比较简单,原理跟加载异步的 js 一样,可参考:https://juejin.cn/post/6918281070026686478
__webpack_require__.f.remotes = (chunkId, promises) = {}
从一开始的代码中,可以知道在 app1 中,我们的 bootstrap.js 是依赖于 semver 和 lodash 这两个库的,在打包后,会用这里一个结构来表示依赖关系
var chunkMapping = {
"src_bootstrap_js": [
"webpack/sharing/consume/default/semver/semver",
"webpack/sharing/consume/default/lodash/lodash"
]
};
在调用 __webpack_require__.e("src_bootstrap_js")时,根据这个可以知道 boostrap 的依赖模块,这里,会等待 boostrap 以及它的依赖 semver 、 lodash 都注册到了 webpack_modules 中后,才认为src_bootstrap_js 完成了加载,可以认为:
当我们的模块中,import 了对应的 shared 配置的模块,那么会等待其全部加载完毕后,才会执行模块代码
那么它是如何判断,shared 配置的模块,是消费本服务的,还是消费 remote 服务的? 只要你在 ModuleFederationPlugin 中配置了 shared, 都会根据你的配置生成这样的结构
{
'lodash': {
'4.17.21': {
get: factory, // get 方法会去创建 script 加载,并注册到 __webpack_modules__
from: uniqueName,
eager: !!eager
}
}
}
其实就是在 webpack_require.f.consumes 加载 remote (App2)的 remoteEntry.js 时,会调用 app2 中暴露出来的 window.app2.init 方法, 并将 app1 的 shared 结构作为参数传递过去,如果名称、版本都能匹配上,那么 app2 会重写这里的 get 方法,让其加载 app2 中的 shared 模块