独立使用 MFSU:把 umi 的依赖预构建加速方案接入你的 webpack 项目
【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi
本文面向所有在非 umi 项目(如自建 webpack 项目、CRA 改造项目)中使用 webpack 的开发者,完整讲解如何独立安装、配置和运行
@umijs/mfsu,涵盖实例初始化、devServer 中间件、源码转换器(Babel / esbuild 两种方案)与 webpack 配置注入的四个关键步骤。读完本文,你将能够在自己的 webpack 4 / webpack 5 项目中复现 umi 的 MFSU 体验——用 esbuild 完成依赖的快速预构建,并享受二次启动时的缓存加速。
MFSU(Module Federation Speed Up)是 umi 生态中基于 webpack Module Federation 的依赖预构建方案:它把node_modules中的第三方依赖预先打包成独立的联邦模块,业务代码在开发时只编译自身,从而显著缩短冷启动与热重启时间。在 umi 项目中它开箱即用,而本文要讲的是它的另一种能力——脱离 umi 独立使用,将其接入任何基于 webpack 的工程。
示例项目与安装
官方仓库在 examples/mfsu-independent 提供了一个可直接运行的独立示例:一个使用antd+framer-motion+react@18的最小项目,README 描述其目标是在无缓存的情况下“一秒内启动”项目。示例包含两份 webpack 配置——webpack.config.js(Babel 方案)与 webpack.config.esbuild.js(esbuild 方案),分别对应下面第 3 步的两种转换器选择。
安装只需一个命令,把@umijs/mfsu作为开发依赖加入项目:
pnpm add -D @umijs/mfsu示例项目的 package.json 中,@umijs/mfsu与webpack、webpack-dev-server、babel-loader、esbuild并列放在devDependencies,并提供了两个启动脚本:
"dev": "webpack serve --config webpack.config.js", "dev:esbuild": "webpack serve --config webpack.config.esbuild.js"即pnpm dev走 Babel 转换器、pnpm dev:esbuild走 esbuild 转换器,二者共用同一套 MFSU 实例初始化逻辑,方便对照两种方案的效果。
配置 MFSU:完整四步
配置 MFSU 一共需要四步操作。有一点需要始终牢记:请确保以下所有行为都只在开发环境生效,生产构建不应被 MFSU 侵入(具体做法见文末“常见问题”)。
第 1 步:初始化实例
第一步是初始化一个MFSU实例,它是后续所有能力的基础:
// webpack.config.js const { MFSU } = require('@umijs/mfsu'); const webpack = require('webpack'); // [mfsu] 1. init instance const mfsu = new MFSU({ implementor: webpack, buildDepWithESBuild: true, });从 MFSU 类实现 可以看出,构造函数会完成一系列默认值装配:mfName默认取常量DEFAULT_MF_NAME(值为'mf',见 constants.ts),缓存目录tmpBase默认是${process.cwd()}/.mfsu,模式默认development,并依据strategy参数选择“编译期收集”或“静态分析”两种依赖收集策略。implementor是必须传的——它指定与项目内实际使用完全一致的webpack 唯一实例(通常就是require('webpack')),MFSU 会用它来构造依赖侧的联邦构建。
第 2 步:添加 devServer 中间件
第二步,把 MFSU 的中间件挂到webpack-dev-server上。这些中间件负责托管 MFSU 打包后产出的远程资源(remoteEntry.js与各依赖 chunk)。
webpack 5
// webpack.config.js module.exports = { devServer: { // [mfsu] 2. add mfsu middleware setupMiddlewares(middlewares, devServer) { middlewares.unshift(...mfsu.getMiddlewares()); return middlewares; }, }, };webpack 4
// webpack.config.js module.exports = { devServer: { // [mfsu] 2. add mfsu middleware onBeforeSetupMiddleware(devServer) { for (const middleware of mfsu.getMiddlewares()) { devServer.app.use(middleware); } }, }, };getMiddlewares()的底层实现在 mfsu.ts#L306-L354:它会拦截请求路径前缀为mf-va_、mf-dep_、mf-static/(对应常量MF_VA_PREFIX、MF_DEP_PREFIX、MF_STATIC_PREFIX)的资源请求,等待依赖构建完成后从tmpBase目录读取产物并返回,非 MFSU 请求则直接放行交给next()。其中remoteEntry.js之外的文件还会被设置cache-control: max-age=31536000,immutable长缓存,这也是二次启动加速的重要来源;中间件末尾还有一个express.static(tmpBase)兜底,用于处理依赖构建产物中指定了 chunk 名的场景。
第 3 步:配置源码转换器
转换器的作用是收集并改写业务代码中的依赖导入路径,把它们替换为 MFSU 模块联邦的地址(也就是第 2 步中间件所提供的资源)。官方提供两种方案:babel plugins与esbuild handler,一般情况下选择babel plugins即可。
方案 A:Babel Plugins
向babel-loader的plugins中展开mfsu.getBabelPlugins()即可:
// webpack.config.js module.exports = { module: { rules: [ // handle javascript source loader { test: /\.[jt]sx?$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { plugins: [ // [mfsu] 3. add mfsu babel plugins ...mfsu.getBabelPlugins(), ], }, }, }, ], }, };getBabelPlugins()在 mfsu.ts#L356-L358 中返回[this.strategy.getBabelPlugin()],即当前策略(编译期收集或静态分析)生成的 babel 插件。对应的插件实现位于 packages/mfsu/src/babelPlugins/awaitImport,核心逻辑是把import x from 'antd'这类依赖导入改写为对联邦远端模块的访问。
方案 B:Esbuild handler
另一种方案是用@umijs/mfsu内置导出的esbuildLoader处理js/ts资源。该方案仅用于开发环境。
info:使用这种方案的好处是,在开发环境获得比
babel更快的编译和启动速度。
// webpack.config.js const { esbuildLoader } = require('@umijs/mfsu'); const esbuild = require('esbuild'); module.exports = { module: { rules: [ { test: /\.[jt]sx?$/, exclude: /node_modules/, use: { loader: esbuildLoader, options: { handler: [ // [mfsu] 3. add mfsu esbuild loader handlers ...mfsu.getEsbuildLoaderHandler(), ], target: 'esnext', implementation: esbuild, }, }, }, ], }, };这里的关键参数语义如下:
esbuildLoader:由 packages/mfsu/src/index.ts 显式导出,专供“独立使用”场景,其 loader 实现见 packages/mfsu/src/loader/esbuild.ts,会根据文件扩展名(.js/.jsx/.ts/.tsx等)自动映射 esbuild 的 loader,target未指定时默认es2015;handler:mfsu.getEsbuildLoaderHandler()返回的处理器数组,会在 esbuild 转换完成后拿到code / imports / exports / filePath(类型定义见 types.ts 中的IEsbuildLoaderHandlerParams),负责执行与 babel 插件等价的导入路径改写;implementation:传入项目中已安装的esbuild实例,避免 loader 内部引入重复版本。
warning:什么时候我不应该使用 esbuild 方案?
- 我有自定义的
babel plugins必须在开发环境使用;- 我需要显示
css-in-js的开发环境友好类名(一般由 babel plugin 提供支持);- 在开发环境多适配一套
esbuild-loader的成本大于配置babel plugins的成本。
第 4 步:注入 webpack 配置
第四步调用mfsu.setWebpackConfig来改变你的 webpack 配置。它的行为是增量的——只做添加和改写,不会破坏你原有的配置内容。由于这是一个异步方法,需要把原始配置抽成对象config,再导出调用后的返回值:
// webpack.config.js const config = { // origin webpack config }; const depConfig = { // webpack config for dependencies }; // [mfsu] 4. inject mfsu webpack config const getConfig = async () => { await mfsu.setWebpackConfig({ config, depConfig, }); return config; }; module.exports = getConfig();从源码看,setWebpackConfig(mfsu.ts#L125-L261)内部做了这些事:
- 收集 alias 与 externals:把
config.resolve.alias和config.externals快照到 MFSU 实例上,供依赖构建与转换器复用; - 改写入口:把原始 entry 拆解为虚拟入口(
mfsu-virtual-entry/目录下的虚拟模块),通过await import()异步加载真实入口,并透传其具名导出,确保业务代码的导出语义不变;该虚拟模块机制还依赖 webpack 的experiments.topLevelAwait: true,代码中会显式lodash.set打开; - 注入插件:追加
WebpackVirtualModules、ModuleFederationPlugin(name 为__,remote 指向mf@${publicPath}/mf-va_remoteEntry.js,其中REMOTE_FILE_FULL = 'mf-va_remoteEntry.js')以及依赖构建插件BuildDepPlugin; - 解析 publicPath:
resolvePublicPath会把'auto'归一为/,作为联邦远端地址的基础。
而depConfig是依赖构建侧独立的 webpack 配置。在示例项目中,它给出了依赖构建所需的resolve.extensions与babel-loader规则(不需要exclude和 MFSU 插件,因为依赖构建由 MFSU 自己驱动):
const depConfig = { output: {}, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], }, module: { rules: [ { test: /\.[jt]sx?$/, use: { loader: 'babel-loader', options: { presets: [ '@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript', ], }, }, }, ], }, plugins: [], };到此为止,MFSU 配置完毕,可以启动项目了。
一份完整的可运行配置
把上述四步串起来,并补齐 devServer 端口、mode: 'development'、publicPath: '/'等常规字段,就是示例 webpack.config.js 的完整形态:
const path = require('path'); const webpack = require('webpack'); const { MFSU } = require('@umijs/mfsu'); // [mfsu] 1. init instance const mfsu = new MFSU({ implementor: webpack, buildDepWithESBuild: true, }); const config = { entry: path.join(__dirname, './src'), mode: 'development', output: { path: path.join(__dirname, './dist'), filename: 'bundle.js', publicPath: '/', }, devServer: { // [mfsu] 2. add mfsu middleware setupMiddlewares(middlewares, devServer) { middlewares.unshift(...mfsu.getMiddlewares()); return middlewares; }, }, resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], }, module: { rules: [ { test: /\.[jt]sx?$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: [ '@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript', ], plugins: [ // [mfsu] 3. add mfsu babel plugins ...mfsu.getBabelPlugins(), ], }, }, }, ], }, plugins: [ new (require('html-webpack-plugin'))({ template: path.resolve(__dirname, './index.html'), }), ], stats: { assets: false, moduleAssets: false, runtime: false, runtimeModules: false, modules: false, entrypoints: false, }, }; const depConfig = { /* 见上文 */ }; // [mfsu] 4. inject mfsu webpack config const getConfig = async () => { await mfsu.setWebpackConfig({ config, depConfig }); return config; }; module.exports = getConfig();esbuild 版本与它的唯一差别在第 3 步——把babel-loader规则整体替换为esbuildLoader(见 webpack.config.esbuild.js),其余四步结构完全一致。
使用:缓存目录与启动验证
完成四步配置后启动项目,你会在项目根目录看到.mfsu文件夹,这就是MFSU 缓存文件夹,里面存放着依赖预构建的产物与缓存元数据。这些是构建缓存,不应该提交到版本库,请在.gitignore中加入:
# .gitignore .mfsu对应源码中的常量DEFAULT_TMP_DIR_NAME = '.mfsu'(constants.ts)——这也是tmpBase选项的默认值来源。
符合预期时,你可以立刻感受到 MFSU 带来的好处:esbuild 对依赖的快速打包(依赖构建走DepBuilder.buildWithESBuild,见 depBuilder.ts,构建完成后会输出[mfsu] compiled with esbuild successfully in X ms的时间日志)以及二次热启动的提速(.mfsu缓存命中后,buildDeps会打印[MFSU] skip buildDeps并直接复用既有产物,见 mfsu.ts#L263-L304)。
其他配置项
除了implementor和buildDepWithESBuild,MFSU实例还支持以下常用配置:
const mfsu = new MFSU({ cwd: process.cwd(), });完整 Options 说明:
| option | default | description |
|---|---|---|
cwd | process.cwd() | 项目根目录 |
getCacheDependency | () => {} | 用返回值来对比,使 MFSU cache 无效的函数 |
tmpBase | ${process.cwd()}/.mfsu | MFSU 缓存存放目录 |
unMatchLibs | [] | 手动排除某些不需要被 MFSU 处理的依赖 |
runtimePublicPath | undefined | 同 umi 的runtimePublicPath |
implementor | undefined | webpack 实例,需要和项目内使用的唯一实例一致 |
buildDepWithESBuild | false | 是否使用esbuild打包依赖 |
onMFSUProgress | undefined | 获取 MFSU 编译进度的回调 |
结合源码对其中几个选项做进一步说明:
cwd/tmpBase/getCacheDependency:三者共同决定缓存的“位置”与“有效性”。构造函数中tmpBase默认取join(process.cwd(), '.mfsu'),getCacheDependency默认返回{}(mfsu.ts#L80-L94)。当依赖版本或构建环境变化时,你可以通过getCacheDependency返回一个变化的值(比如依赖锁文件的 hash)来主动使缓存失效、触发重新构建;unMatchLibs:接受字符串或正则数组,命中项将不走 MFSU 处理,保持普通依赖的打包方式,用于处理与联邦化不兼容的库(例如依赖动态require、需要物理隔离的场景)。仓库中也有对应的 mfsu-un-match-libs 示例可供参考;runtimePublicPath:与 umi 的runtimePublicPath语义一致,设置为true后,MFSU 会改用 promise-based 动态 remote(见 mfsu.ts#L212-L241),运行时通过window.publicPath拼接mf-va_remoteEntry.js的加载地址并注入<script>标签。适合部署在 CDN、子路径或 publicPath 运行时才能确定的场景;若依赖需要被其他站点引用,则需要显式指定publicPath以保证远端入口可被正确访问;onMFSUProgress:回调会收到形如{ done, ...progress }的进度对象。在 worker 构建路径下,构建进度消息通过worker.postMessage回传并汇聚到onProgress(depBuilder.ts#L80-L118),可用于在开发界面渲染依赖构建进度条;buildDepWithESBuild:为true时依赖侧使用 esbuild 打包(对应DepBuilder.buildWithESBuild),启动更快;为false时退化为用implementor(webpack)构建依赖(DepBuilder.buildWithWebpack)。
常见问题
如何保证我的 MFSU 配置只在开发环境生效?
使用环境标识隔离,避免所有 MFSU 配置侵入生产构建。核心思路是:仅在development环境下创建实例,并在 loader 的 plugins 中做条件展开:
const isDev = process.env.NODE_ENV === 'development' const mfsu = isDev ? new MFSU({ implementor: webpack, buildDepWithESBuild: true, }) : undefined // e.g. { test: /\.[jt]sx?$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { plugins: [ ...(isDev ? [] : mfsu.getBabelPlugins()) ] } } }注意这里的要点:mfsu.getBabelPlugins()只有在isDev时才展开,生产构建的 babel 配置完全保持原样。同理,devServer 中间件、setWebpackConfig注入等步骤也应包裹在isDev分支内,确保 MFSU 只存在于开发链路。示例项目 examples/mfsu-independent 的build脚本(webpack,生产模式)与dev脚本(webpack serve,开发模式)正是按这一约定划分的。
如何在 webpack 4 与 webpack 5 之间选择接入方式?
两者唯一的差异在第 2 步中间件挂载 API:webpack 5 使用devServer.setupMiddlewares(注意要unshift到最前,确保先于其他中间件拦截 MFSU 请求),webpack 4 使用devServer.onBeforeSetupMiddleware手动app.use。其余三步配置与源码层面的行为完全一致,依赖构建同样交由implementor指定的 webpack 实例驱动。
冷启动时如何观察 MFSU 是否生效?
观察两点即可:一是项目根目录是否生成.mfsu目录,二是终端日志——首次构建会输出[MFSU] buildDeps since ...与 esbuild 编译耗时,缓存命中时会输出[MFSU] skip buildDeps。依赖变更或getCacheDependency返回值变化时,则会触发依赖重建并写入新缓存。
小结
独立使用 MFSU 的本质,是把 umi 中“依赖预构建 + Module Federation 远端模块 + 开发中间件”这一整套机制,通过@umijs/mfsu暴露的MFSU类、esbuildLoader、getMiddlewares、getBabelPlugins/getEsbuildLoaderHandler、setWebpackConfig五个 API 移植到任何 webpack 工程。四步配置——初始化实例、挂中间件、配转换器、注入配置——彼此职责清晰且均为增量行为,配合.mfsu缓存目录与buildDepWithESBuild选项,即可在非 umi 项目中复现依赖快速打包与二次启动提速的开发体验。若需更完整的落地参考,可直接对照仓库内的 examples/mfsu-independent 示例项目阅读其两份 webpack 配置。
【免费下载链接】umiA framework in react community ✨项目地址: https://gitcode.com/GitHub_Trending/um/umi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考