module-federation是webpack5更新的一项新特性, 可以更容易的共享前端代码, 本文将介绍@module-federation/webpack-4实现原理及其与webpack5的差异
在公司内我们搭建了微前端包管理平台, 由于有大量webpack4的项目, 我们使用umd规范来共享资源, 也产出了和mf同等作用的插件import-http-webpack-plugin, 但是精力有限我们不打算在umd规范下建立微前端生态, 转而投入到有同样作用的mf, 借助其已有的各领域能力来继续搭建微前端生态。
简单的解释下实现原理, webpack4和webpack5是怎么实现互通的呢? 有三个关键点
// container
{
async init(shareScope){},
get(name){ return async factory() }
}
// shareScopes example
{
[default]: {
[react]: {
[18.0.2]: {
get() {
return async function factory() {
return module
}
},
...other
},
[17.0.2]: {
get() {
return async function factory() {
return module
}
},
...other
}
}
}
}
通过插件实现上述流程(图示)
其中介绍图中两处红色部分, 如何改变webpack4加载流程使其支持加载远程模块
https://github.com/module-federation/webpack-4
// module-federation/webpack-4/lib/plugin.js
apply(compiler) {
// 1. 生成唯一的jsonpFunction全局变量防止冲突
compiler.options.output.jsonpFunction = `mfrename_webpackJsonp__${this.options.name}`
// 2. 生成4个虚拟模块备用
this.genVirtualModule(compiler)
// 3. 在entry chunks初始化远程模块映射关系
// 4. 在entry chunks加载所有的container初始化依赖集合(shareScopes)
this.watchEntryRecord(compiler)
this.addLoader(compiler)
// 5. 生成mf的入口文件(一般是remoteEntry.js)
this.addEntry(compiler)
this.genRemoteEntry(compiler)
// 6. 拦截remotes、shared模块的webpack编译
this.convertRemotes(compiler)
this.interceptImport(compiler)
// 7. 使webpack jsonp chunk等待远程依赖加载
this.patchJsonpChunk(compiler)
this.systemParse(compiler)
}
compiler.options.output.jsonpFunction = `mfrename_webpackJsonp__${this.options.name}`
只是将这4个文件代码模块作为webpack虚拟模块来注册, 可被后续流程引入使用
初始化所有container(其他mf模块), 并将加载过程以promise形式导出, 以标识初始化阶段的完成(所有的jsonp chunk需要等待初始化阶段完成)
// 1. 使用singleEntry添加mf入口
new SingleEntryPlugin(compiler.options.context, virtualExposesPath, "remoteEntry").apply(compiler)
// 2. 复制remoteEntry入口最后生成的文件, 并重命名
entryChunks.forEach(chunk => {
this.eachJsFiles(chunk, (file) => {
if (file.indexOf("$_mfplugin_remoteEntry.js") > -1) {
compilation.assets[file.replace("$_mfplugin_remoteEntry.js", this.options.filename)] = compilation.assets[file]
// delete compilation.assets[file]
}
})
})
`
/* eslint-disable */
...
const {setInitShared} = require("${virtualSetSharedPath}")
// 此处使用dynamic-import预设了所有exposes module
const exposes = {
[moduleName]: async () {}
}
// 1. 在全局以类似global的方式注册container
module.exports = window["${options.name}"] = {
async get(moduleName) {
// 2. 使用代码分割来暴露导出的模块
const module = await exposes[moduleName]()
return function() {
return module
}
},
// 此处是某个scope之内的shared
async init(shared) {
// 4. 合并share、等待init阶段完成
setInitShared(shared)
await window["__mfplugin__${options.name}"].initSharedPromise
return 1
}
}
`
const { remotes, shared } = this.options
Object.keys(remotes).forEach(key => {
compiler.options.resolve.alias[key] = `wpmjs/$/${key}`
compiler.options.resolve.alias[`${key}$`] = `wpmjs/$/${key}`
})
Object.keys(shared).forEach(key => {
// 不存在的文件才能拦截
compiler.options.resolve.alias[key] = `wpmjs/$/mfshare:${key}`
compiler.options.resolve.alias[`${key}$`] = `wpmjs/$/mfshare:${key}`
})
compiler.resolverFactory.plugin('resolver normal', resolver => {
resolver.hooks.resolve.tapAsync(pluginName, (request, resolveContext, cb) => {
if (是来自remotes、shared的别名) {
// 携带pkgName参数转发至import-wpm-loader
cb(null, {
path: emptyJs,
request: "",
query: `?${query.replace('?', "&")}&wpm&type=wpmPkg&mfName=${this.options.name}&pkgName=${encodeURIComponent(pkgName + query)}`,
})
} else {
// 请求本地模块
cb()
}
});
});
module.exports = function() {
`
/* eslint-disable */
if (window.__wpm__importWpmLoader__garbage) {
// 1. 留下代码标识, 标识依赖的远程模块, 用于让chunk等待远程依赖加载
window.__wpm__importWpmLoader__garbage = "__wpm__importWpmLoader__wpmPackagesTag${pkgName}__wpm__importWpmLoader__wpmPackagesTag";
}
// 2. 进入此模块代码时, 远程模块已经加载完毕, 可以使用get获取模块的同步值, 并返回
module.exports = window["__mfplugin__${mfName}"].get("${decodeURIComponent(pkgName)}")
`
}
@module-federation/webpack-4插件已经实现了module-federation的主要能力, 并且可以在webpack4和webpack5互相引用 , 下面说明下哪些参数是插件是未支持的
此参数优先级不是很高, 在webpack4种实现较为复杂, 在webpack5中使用也仍有问题, 详见https://github.com/webpack/webpack/issues/16236 , 故在webpack4中的实现类似于设置了library.type = “global”
同一个mf container只可以用一个shareScope初始化, 如果被多次使用shareScope设置的不一致webpack会报错, 并且shareScope可设置处过多比较混乱, 即使在纯webpack5中使用表现也不可预估, 建议使用options.shared.xxx.shareScope、options.shareScope替代
webpack-4插件暂未集成webpack-5相关包的能力(ssr、typescript、hmr、dashboard等), 但已实现4、5互通, 可以助您可以放心的使用webpack5实现新项目, 而无需重构已有项目