本文是 UMI 4 源码主题阅读的第三篇。有些同学可能对 UMI 4 新特性还不太熟悉,推荐看一下:www.yuque.com/antfe/featu...
背景
这里需要说明的是,MFSU 在 UMI 3 就已经有了,只不过当时 bug 比较多,在 UMI 4 中很多问题都得到了解决。
什么是 MFSU?借用官方文档中的描述:
MFSU 是一种基于 webpack5 新特性 Module Federation 的打包提速方案。其核心的思路是通过分而治之,将应用源代码的编译和应用依赖的编译分离,将变动较小的应用依赖构建为一个 Module Federation 的 remote 应用,以免去应用热更新时对依赖的编译
顺带说一句,本人在看文档的时候,发现了很多 typo,还发现一个链接挂掉,实在看不下去了,顺手给官方提了两个 PR:
一提到 Module Federation 有些同学可能会觉得比较陌生,这里不过多涉及 Module Federation 相关介绍,但是可以做个简单的类比,一般前端工程里面我们会通过 Code-Splitting 拆分 Async Chunk,但是这个 Async Chunk 只能在工程内部消费,无法共享给其他工程(这是 Webpack 打包机制决定的);Module Federation 作用就是跨工程共享 Async Chunk,解决代码共享、复用问题。
MFSU 做的事情其实就是扫描依赖,对依赖预打包,然后通过 Module Federation 共享给业务工程消费。业务工程无需对依赖进行构建、打包,进而提升了构建效率。有没有觉得这种设计思路很熟悉了,没错,Vite 也用到了依赖预构建。
官方文档介绍 MFSU 有两种策略,一种是 normal 策略 (编译时分析)。该策略在编译期间通过 Babel 插件修改业务工程代码的 import
路径,让路径指向 MF 的模块地址,同时做依赖收集。等业务工程编译完成后,继续进行依赖部分的代码构建。该策略可以收集到完整的依赖,但缺点是整个过程是串行的,比较耗时。
另一种是 eager 策略 (扫描方式)。该策略会预先读取业务工程源码文件,通过静态分析方式获取项目依赖。分析完依赖信息后,Umi 会拿着这份依赖信息,并行的去进行业务工程代码构建和依赖构建。该策略效率较高,但缺点是会漏掉编译期间动态插入的依赖,这部分依赖最终和业务工程代码一起编译打包。
介绍完整体流程,下面来看源码分析。
源码分析
由于 MFSU 涉及的链路比较多,这篇文章我们换一种方式,通过对照文档 API 用法一点点分析源码实现。
首先 MFSU 提供了一个单独的包,直接安装这个包:
bash
pnpm add @umijs/mfsu -D
1. 初始化实例
首先需要初始化一个 MFSU
实例:
ts
// webpack.config.js
const { MFSU } = require('@umijs/mfsu')
const webpack = require('webpack')
const mfsu = new MFSU({
implementor: webpack,
buildDepWithESBuild: true,
});
从上例中可以看出,MFSU
实际上就是一个 class,当我们用 new 操作,会执行构造方法:
ts
// /packages/mfsu/src/mfsu/mfsu.ts
class MFSU {
constructor(opts: IOpts) {
// 此处省略一些代码
if (this.opts.strategy === 'eager') {
if (opts.srcCodeCache) {
// 当指定了 `strategy="eager"` 且指定了 `opts.srcCodeCache`
// 初始化一个 `StaticAnalyzeStrategy` 实例赋值给 `this.strategy`
logger.info('MFSU eager strategy enabled');
this.strategy = new StaticAnalyzeStrategy({
mfsu: this,
srcCodeCache: opts.srcCodeCache,
});
} else {
// 如果没有指定 `opts.srcCodeCache` 则 fallback 到 `strategy="normal"`
logger.warn(
'fallback to MFSU normal strategy, due to srcCache is not provided',
);
this.strategy = new StrategyCompileTime({ mfsu: this });
}
} else {
// `strategy="normal"` 模式
// 初始化 `StrategyCompileTime` 实例赋值给 `this.strategy`
this.strategy = new StrategyCompileTime({ mfsu: this });
}
// 加载缓存,反序列化创建上次构建的 snapshot
this.strategy.loadCache();
// 初始化 `DepBuilder` 实例
this.depBuilder = new DepBuilder({ mfsu: this });
}
}
2. 添加中间件
第二步,添加 MFSU
的 devServer
中间件到 webpack-dev-server 中,他将为你提供 MFSU
所需打包后的资源:
ts
// webpack.config.js
module.exports = {
devServer: {
setupMiddlewares(middlewares, devServer) {
middlewares.unshift(
...mfsu.getMiddlewares()
)
return middlewares
},
}
}
getMiddlewares()
是 MFSU
类的实例方法,我们来看一下实现:
ts
// /packages/mfsu/src/mfsu/mfsu.ts
class MFSU {
// 此处省略一些代码
getMiddlewares() {
return [
(req: Request, res: Response, next: NextFunction) => {
// 解析静态资源的 publicPath
const publicPath = this.publicPath;
const relativePublicPath = isAbsoluteUrl(publicPath)
? new URL(publicPath).pathname
: publicPath;
// 获取 `req.path`,判断是否请求 MF 的资源
const isMF =
req.path.startsWith(`${relativePublicPath}${MF_VA_PREFIX}`) ||
req.path.startsWith(`${relativePublicPath}${MF_DEP_PREFIX}`) ||
req.path.startsWith(`${relativePublicPath}${MF_STATIC_PREFIX}`);
if (isMF) {
// 当前请求是 MF 资源,重写服务端响应逻辑
// 监听 `depBuilder.onBuildComplete` 事件
// 等待依赖编译结束响应静态资源(编译完成之前请求是挂起状态)
this.depBuilder.onBuildComplete(() => {
// 资源不是 `remoteEntry.js`,设置强缓存
// 如果 `remoteEntry.js` 不可设置强缓存
if (!req.path.includes(REMOTE_FILE)) {
res.setHeader('cache-control', 'max-age=31536000,immutable');
}
// 根据资源类型设置 Content-Type
res.setHeader(
'content-type',
lookup(extname(req.path)) || 'text/plain',
);
// 将 `req.path` 转换为依赖构建产物的文件系统路径
const relativePath = req.path.replace(
new RegExp(`^${relativePublicPath}`),
'/',
);
const realFilePath = join(this.opts.tmpBase!, relativePath);
// 如果文件不存在,打印错误信息并响应 404
if (!existsSync(realFilePath)) {
logger.error(`MFSU dist file: ${realFilePath} not found`);
if (this.lastBuildError) {
logger.error(`MFSU latest build error: `, this.lastBuildError);
}
res.status(404);
return res.end();
}
// 文件存在,响应文件内容
const content = readFileSync(realFilePath);
res.send(content);
});
} else {
// 当前请求不是 MF,用默认响应逻辑
next();
}
},
// 兜底依赖构建时, 代码中有指定 chunk 名的情况
express.static(this.opts.tmpBase!),
];
}
}
3. 配置转换器
第三步,你需要配置一种源码转换器,他的作用是用来收集、转换依赖导入路径,替换为 MFSU
的模块联邦地址(中间件所提供的)。
此处提供两种方案:babel plugins
或 esbuild handler
,一般情况下选择 babel plugins
即可。
ts
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.[jt]sx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: [
...mfsu.getBabelPlugins()
]
}
}
}
]
}
}
getBabelPlugins
也是 MFSU
类的实例方法,简单看一下实现:
ts
// /packages/mfsu/src/mfsu/mfsu.ts
class MFSU {
// 此处省略一些代码
getBabelPlugins() {
// 根据不同策略,需要加载不同 Babel 插件
return [this.strategy.getBabelPlugin()];
}
}
4. 设定 webpack 配置
第四步,调用 MFSU
提供的方法改变你的 webpack 配置,在这里只有增量行为,你无需担心会影响到你原来的配置内容。
如下代码所示,mfsu.setWebpackConfig
是一个异步方法,为了调用他你需要将原来的 webpack 配置单独抽为一个对象 config
之后,再将调用此方法的返回值导出。
ts
// webpack.config.js
const config = {
// origin webpack config
}
const depConfig = {
// webpack config for dependencies
}
const getConfig = async () => {
await mfsu.setWebpackConfig({
config, depConfig
});
return config
}
module.exports = getConfig()
这里核心就是 mfsu.setWebpackConfig()
方法,里面逻辑比较多,我们分两块看。第一部分主要遍历 Webpack 入口配置,收集 import
、export
,生成新的虚拟模块和入口,替换原先 Webpack 的入口配置:
ts
// /packages/mfsu/src/mfsu/mfsu.ts
class MFSU {
async setWebpackConfig(opts: {
config: Configuration;
depConfig: Configuration;
}) {
const { mfName } = this.opts;
// 给 Babel 插件适配 `this.alias` 和 `this.externals`
// 有些 `import` 语句是基于 `alias` 配置实现的,Babel 插件需要解析正确路径
// 还有一些 `import` 配置了 `externals`,不需要收集依赖
Object.assign(this.alias, opts.config.resolve?.alias || {});
this.externals.push(...makeArray(opts.config.externals || []));
// 收集入口和虚拟模块
const entry: Record<string, string | string[]> = {};
const virtualModules: Record<string, string> = {};
// 将 `entry` 转为对象,方便进行遍历
const entryObject = lodash.isString(opts.config.entry)
? { default: [opts.config.entry] }
: (opts.config.entry as Record<string, string[]>);
for (const key of Object.keys(entryObject)) {
// 如果是项目导出的远端模块 不需要处理成动态加载的模块 以避免加载错误
if (key === this.opts.remoteName) {
entry[key] = entryObject[key];
continue;
}
const virtualPath = `./${VIRTUAL_ENTRY_DIR}/${key}.js`;
const virtualContent: string[] = [];
let index = 1;
let hasDefaultExport = false;
const entryFiles = lodash.isArray(entryObject[key])
? entryObject[key]
: ([entryObject[key]] as unknown as string[]);
const resolver = getResolver(opts.config);
for (let entry of entryFiles) {
entry = resolver(entry);
// 读取入口文件内容
const content = readFileSync(entry, 'utf-8');
// 解析入口文件的 `import` 和 `export`
// 个人认为前端业务工程入口文件一般只有 `import` 没有 `export`
const [_imports, exports] = await parseModule({ content, path: entry });
if (exports.length) {
// 生成 `import` 虚拟模块代码
virtualContent.push(`const k${index} = ${this.asyncImport(entry)}`);
for (const exportName of exports) {
// 如果存在 `export` 则生成 `export` 的虚拟模块代码
if (exportName === 'default') {
hasDefaultExport = true;
virtualContent.push(`export default k${index}.${exportName}`);
} else {
virtualContent.push(
`export const ${exportName} = k${index}.${exportName}`,
);
}
}
} else {
// 生成 `import` 虚拟模块代码
virtualContent.push(this.asyncImport(entry));
}
index += 1;
}
// 如果没有默认导出,则添加一个
if (!hasDefaultExport) {
virtualContent.push(`export default 1;`);
}
// 拼接虚拟模块代码,收集到 `virtualModules`
virtualModules[virtualPath] = virtualContent.join('\n');
// 收集虚拟模块入口
entry[key] = virtualPath;
}
// 将 Webpack 的入口配置替换为虚拟模块入口
opts.config.entry = entry;
// 此处省略一些代码
}
}
第二部分主要是配置 Webpack 插件,其中就包括 ModuleFederationPlugin
:
ts
// /packages/mfsu/src/mfsu/mfsu.ts
class MFSU {
async setWebpackConfig(opts: {
config: Configuration;
depConfig: Configuration;
}) {
// 此处省略一些代码
// 确保 plugins 是数组,方便下面进行遍历
opts.config.plugins = opts.config.plugins || [];
// 从 Webpack 配置中获取 `publicPath`
let publicPath = resolvePublicPath(opts.config);
this.publicPath = publicPath;
opts.config.plugins!.push(
...[
// 使用 `WebpackVirtualModules` 插件
// 将上面拼接的字符串代码生成虚拟模块
// https://www.npmjs.com/package/webpack-virtual-modules
new WebpackVirtualModules(virtualModules),
// 从初始化阶段传入的 `implementor` 拿到 Webpack 实例
// 基于 Webpack 实例创建 `ModuleFederationPlugin`
// 注意这边 `ModuleFederationPlugin` 只配置了 `remotes` 用于消费远程 Async Chunk
// 没有配置 `exposes` 将模块暴露给其他工程
new this.opts.implementor.container.ModuleFederationPlugin({
name: '__',
// 支持在初始化阶段传入 `ModuleFederationPlugin` 的 `shared` 配置
shared: this.opts.shared || {},
remotes: {
// 加载远程 remoteEntry.js
[mfName!]: this.opts.runtimePublicPath
? // ref:
// https://webpack.js.org/concepts/module-federation/#promise-based-dynamic-remotes
`
promise new Promise(resolve => {
const remoteUrlWithVersion = (window.publicPath || '/') + '${REMOTE_FILE_FULL}';
const script = document.createElement('script');
script.src = remoteUrlWithVersion;
script.onload = () => {
// the injected script has loaded and is available on window
// we can now resolve this Promise
const proxy = {
get: (request) => window['${mfName}'].get(request),
init: (arg) => {
try {
return window['${mfName}'].init(arg);
} catch(e) {
console.log('remote container already initialized');
}
}
}
resolve(proxy);
}
// inject this script with the src set to the versioned remoteEntry.js
document.head.appendChild(script);
})
`.trimLeft()
: `${mfName}@${publicPath}${REMOTE_FILE_FULL}`, // mfsu 的入口文件如果需要在其他的站点上被引用,需要显示的指定publicPath,以保证入口文件的正确访问
},
}),
new BuildDepPlugin(this.strategy.getBuildDepPlugConfig()),
],
);
// 给 Webpack 配置启用 topLevelAwait
lodash.set(opts.config, 'experiments.topLevelAwait', true);
// 从 `setWebpackConfig` 入参拿到依赖构建配置
this.depConfig = opts.depConfig;
// 注意这里的 `init` 方法只在 `StaticAnalyzeStrategy` 策略中用到
// 当启用 `StrategyCompileTime` 插件该方法为 noop
this.strategy.init(opts.config);
}
}
以上就是 MFSU
的整体配置流程。但是我们注意到,在 MFSU
类中还有一个非常重要的 buildDeps
方法,该方法在配置阶段并没有调用,那显然需要通过 Webpack 插件,在打包构建阶段监听特定事件钩子实现调用。下面来看依赖构建流程。
5. 依赖构建流程
这部分逻辑有点绕,花了点时间理清楚了。在上面的代码中,配置了一个自定义 Webpack 插件:
ts
new BuildDepPlugin(this.strategy.getBuildDepPlugConfig())
以 StrategyCompileTime
为例,我们来看下 getBuildDepPlugConfig
方法的逻辑:
ts
// /packages/mfsu/src/mfsu/strategyCompileTime.ts
export class StrategyCompileTime implements IMFSUStrategy {
// 此处省略一些代码
getBuildDepPlugConfig(): IBuildDepPluginOpts {
const mfsu = this.mfsu;
return {
onCompileDone: () => {
if (mfsu.depBuilder.isBuilding) {
mfsu.buildDepsAgain = true;
} else {
mfsu
.buildDeps()
.then(() => {
mfsu.onProgress({
done: true,
});
})
.catch((e: Error) => {
printHelp.runtime(e);
mfsu.onProgress({
done: true,
});
});
}
},
};
}
}
可以看到,该方法就是返回了一个配置对象,对象上带有 onCompileDone
事件钩子,当该事件钩子调用的时候,会执行 mfsu.buildDeps()
。因此我们猜测,当 Webpack 构建完成的时候,此时依赖收集也完成了,然后执行 mfsu.buildDeps()
进行依赖构建,依赖构建结束调用 mfsu.onProgress({ done: true })
说明整个 MFSU
流程已经完成。
我们先看一下 BuildDepPlugin
插件的代码,对于熟悉 Webpack 插件的同学来说这块应该非常简单:
ts
// /packages/mfsu/src/webpackPlugins/buildDepPlugin.ts
export class BuildDepPlugin {
private opts: IBuildDepPluginOpts;
constructor(opts: IBuildDepPluginOpts) {
this.opts = opts;
}
apply(compiler: Compiler): void {
compiler.hooks.watchRun.tapPromise(PLUGIN_NAME, (c: Compiler) => {
return this.opts.onFileChange?.(c) || Promise.resolve();
});
compiler.hooks.beforeCompile.tap(PLUGIN_NAME, () => {
if (this.opts.beforeCompile) {
return this.opts.beforeCompile?.();
} else {
return Promise.resolve();
}
});
// 监听 `done` 事件,触发 `onCompileDone` 钩子
compiler.hooks.done.tap(PLUGIN_NAME, (stats: Stats) => {
if (!stats.hasErrors()) {
this.opts.onCompileDone();
}
});
}
}
上例代码
beforeCompile
、onFileChange
用于StaticAnalyzeStrategy
策略,onCompileDone
用于StrategyCompileTime
策略
下面重点就是 MFSU
的 buildDeps
方法:
ts
// /packages/mfsu/src/mfsu/mfsu.ts
class MFSU {
// 此处省略一些代码
async buildDeps() {
try {
// 基于上次构建的 snapshot 判断依赖是否需要构建
// 主要基于两个信息:
// - `getCacheDependency` 为用户自定义函数,用返回值来对比,使 MFSU cache 无效的函数
// - `ModuleGraph` 用来保存编译期间收集的依赖信息,是内部实现的一个数据结构
// 首次创建 `MFSU` 实例的时候会通过 `loadCache` 方法从磁盘从恢复
// 如果不需要返回 false,需要则返回 string 类型 reason
const shouldBuild = this.strategy.shouldBuild();
if (!shouldBuild) {
logger.info('[MFSU] skip buildDeps');
return;
}
// 获取此次构建最新的 snapshot,用于下次构建进行比对
this.strategy.refresh();
// 获取编译期间收集的依赖信息
// `ModuleGraph` 是 `Map` 类型,前面调用 `refresh()` 方法获取 snapshot
// 进一步调用 `snapshotDeps()`,会将 `ModuleGraph` 转为普通对象存到 `depSnapshotModules`
// 调用 `getDepModules()` 方法直接返回 `depSnapshotModules`
const staticDeps = this.strategy.getDepModules();
// `buildDeps` 是静态方法,用于将每个依赖转换为 `Dep` 对象,返回数组
const deps = Dep.buildDeps({
deps: staticDeps,
cwd: this.opts.cwd!,
mfsu: this,
});
logger.info(`[MFSU] buildDeps since ${shouldBuild}`);
logger.debug(deps.map((dep) => dep.file).join(', '));
// 调用 `depBuilder.build` 方法实现依赖构建
await this.depBuilder.build({
deps,
});
this.lastBuildError = null;
// 写入缓存到 .mfsu/MFSU_CACHE.json
this.strategy.writeCache();
// 如果需要再次编译,则再调用 `this.buildDeps()` 进行编译
if (this.buildDepsAgain) {
logger.info('[MFSU] buildDepsAgain');
this.buildDepsAgain = false;
this.buildDeps().catch((e: Error) => {
printHelp.runtime(e);
});
}
} catch (e) {
this.lastBuildError = e;
throw e;
}
}
}
从上面代码可以看出,实现依赖构建核心方法是 depBuilder.build
,下面来看一下整体流程:
ts
// /packages/mfsu/src/depBuilder/depBuilder.ts
export class DepBuilder {
public opts: IOpts;
public completeFns: Function[] = [];
public isBuilding = false;
constructor(opts: IOpts) {
this.opts = opts;
}
// 此处省略一些代码
async build(opts: { deps: Dep[] }) {
this.isBuilding = true;
// 构造 `onBuildComplete` 事件回调,在依赖构建完成后调用
// 该方法会通知 devServer 响应 MF 静态资源请求
const onBuildComplete = () => {
this.isBuilding = false;
this.completeFns.forEach((fn) => fn());
this.completeFns = [];
};
try {
// 写入依赖构建所需的文件
await this.writeMFFiles({ deps: opts.deps });
const newOpts = {
...opts,
onBuildComplete,
};
if (this.opts.mfsu.opts.buildDepWithESBuild) {
// 如配置了 `buildDepWithESBuild` 则调用 ESBuild 进行编译
await this.buildWithESBuild(newOpts);
} else {
// 默认用 Webpack 进行编译
await this.buildWithWebpack(newOpts);
}
} catch (e) {
onBuildComplete();
throw e;
}
}
}
writeMFFiles
方法会将收集到的依赖写入 tmpBase
目录,用于后续依赖构建。先过一遍整体流程:
ts
// /packages/mfsu/src/depBuilder/depBuilder.ts
export class DepBuilder {
// 此处省略一些代码
async writeMFFiles(opts: { deps: Dep[] }) {
const tmpBase = this.opts.mfsu.opts.tmpBase!;
fsExtra.mkdirpSync(tmpBase);
// 将收集到的依赖写入 tmpBase 目录,用于后续依赖构建
for (const dep of opts.deps) {
const content = await dep.buildExposeContent();
writeFileSync(join(tmpBase, dep.filePath), content, 'utf-8');
}
// 这里是用于依赖构建的编译入口
writeFileSync(
join(tmpBase, 'index.js'),
// https://webpack.js.org/concepts/module-federation/#infer-publicpath-from-script
`__webpack_public_path__ = document.currentScript.src + '/../';`,
'utf-8',
);
}
}
这里以 Webpack 为例,看一下 buildWithWebpack
构建流程:
ts
// /packages/mfsu/src/depBuilder/depBuilder.ts
export class DepBuilder {
// 此处省略一些代码
async buildWithWebpack(opts: { onBuildComplete: Function; deps: Dep[] }) {
// 从 `getWebpackConfig` 方法获取用于依赖构建的 Webpack 配置
const config = this.getWebpackConfig({ deps: opts.deps });
return new Promise((resolve, reject) => {
// 创建一个新的 compiler 实例用于依赖构建
const compiler = this.opts.mfsu.opts.implementor(config);
// 启动构建
compiler.run((err, stats) => {
// 构建完成触发 `onBuildComplete` 构造
opts.onBuildComplete();
if (err || stats?.hasErrors()) {
if (err) {
reject(err);
}
if (stats) {
const errorMsg = stats.toString('errors-only');
// console.error(errorMsg);
reject(new Error(errorMsg));
}
} else {
resolve(stats);
}
// Webpack5 编译结束需要手动调用 `compiler.close()` 否则会继续监听文件编译
compiler.close(() => {});
});
});
}
}
上面还涉及到 getWebpackConfig
方法,简单过一下:
ts
// /packages/mfsu/src/depBuilder/depBuilder.ts
export class DepBuilder {
// 此处省略一些代码
getWebpackConfig(opts: { deps: Dep[] }) {
const mfName = this.opts.mfsu.opts.mfName!;
// 获取 `depConfig` 配置项
const depConfig = lodash.cloneDeep(this.opts.mfsu.depConfig!);
// 添加依赖构建的编译入口
// 注意这里用了 Infer publicPath from script 特性
// entry 配置需要与 `ModuleFederationPlugin` 的 `name` 配置保持一致
depConfig.entry = {
[mfName]: join(this.opts.mfsu.opts.tmpBase!, 'index.js'),
};
// 打包输出路径
depConfig.output!.path = this.opts.mfsu.opts.tmpBase!;
// 关闭 devtool 配置
depConfig.devtool = false;
// 关闭库模式
// library 会影响 external 的语法,导致报错
// ref: https://github.com/umijs/plugins/blob/6d3fc2d/packages/plugin-qiankun/src/slave/index.ts#L83
if (depConfig.output?.library) delete depConfig.output.library;
if (depConfig.output?.libraryTarget) delete depConfig.output.libraryTarget;
// 将所有依赖都打包到 vendor 里面
depConfig.optimization ||= {};
depConfig.optimization.splitChunks = {
chunks: (chunk) => {
// mf 插件中的 chunk 的加载并不感知到 mfsu 做了 chunk 的合并, 所以还是用原来的 chunk 名去加载
// 这样就会造成 chunk 加载不到的问题; 因此将 mf shared 相关的 chunk 不进行合并
const hasShared = chunk.getModules().some((m) => {
return (
m.type === 'consume-shared-module' ||
m.type === 'provide-module' ||
m.type === 'provide-shared-module'
);
});
return !hasShared;
},
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /.+/,
name(_module: any, _chunks: any, cacheGroupKey: string) {
return `${MF_DEP_PREFIX}___${cacheGroupKey}`;
},
},
},
};
depConfig.plugins = depConfig.plugins || [];
// `DepChunkIdPrefixPlugin` 给 chunkId 添加 `MF_DEP_PREFIX` 前缀
depConfig.plugins.push(new DepChunkIdPrefixPlugin());
// `StripSourceMapUrlPlugin` 用来给产物 JS 代码去掉 `sourceMappingURL`
depConfig.plugins.push(
new StripSourceMapUrlPlugin({
webpack: this.opts.mfsu.opts.implementor,
}),
);
// 应用 Webpack 内置 `ProgressPlugin`,实现 MFSU 编译进度回调
depConfig.plugins.push(
new this.opts.mfsu.opts.implementor.ProgressPlugin((percent, msg) => {
this.opts.mfsu.onProgress({ percent, status: msg });
}),
);
// 设置 `ModuleFederationPlugin` 插件的 `exposes` 选项
const exposes = opts.deps.reduce<Record<string, string>>((memo, dep) => {
memo[`./${dep.file}`] = join(this.opts.mfsu.opts.tmpBase!, dep.filePath);
return memo;
}, {});
// 注意这里创建了一个新的 `ModuleFederationPlugin` 配置实例
// 用于暴露依赖构建产物给业务工程消费
depConfig.plugins.push(
new this.opts.mfsu.opts.implementor.container.ModuleFederationPlugin({
library: {
type: 'global',
name: mfName,
},
name: mfName,
filename: REMOTE_FILE_FULL,
exposes,
shared: this.opts.mfsu.opts.shared || {},
}),
);
return depConfig;
}
}
以上就是 MFSU 整体流程,相信大家应该都有整体印象。如果对 MFSU
感兴趣的同学,可以进一步深入阅读源码。不过个人吐槽一句,跟其他模块相比,MFSU 整体代码质量不是很高,可以了解背后的原理,顺便学一些 Webpack 高级技巧。