跳到主要内容

Module Federation 实战拆解:远程模块、依赖共享与 MF 2.0

· 阅读需 8 分钟

微前端绕不开 Module Federation(MF)。它和 qiankun 那类方案常被放在一起比,但两者其实抽象层级不同:qiankun 管的是「应用怎么隔离地拼在一起」,MF 管的是「模块怎么跨应用加载与共享」。

这篇不空谈:我搭了两个最小应用(host / remote),用 webpack 5.111.0 + @module-federation/enhanced 2.9.0 真跑了一遍构建,把产物、stats、manifest 全抓出来给你看——MF 这套东西「到底生成了什么」,看完就有底了。

声明:本文配置与构建产物均为本机真实运行结果(含 mf-manifest.json 原文)。运行时(浏览器里加载远程模块)未实测,涉及运行时的部分我标明了依据,不编造效果。

一、一句话理解 MF

把「另一个独立构建、独立部署的应用」里的模块,当成本地模块一样 import 进来,并且让它们共用同一份依赖(如 React、dayjs)。

关键点:host 的构建产物里并没有 remote 的代码——只有一个「远程地址」。真正的加载发生在运行期

二、五个核心概念(一次记全)

概念属于谁作用
exposesremote对外提供哪些模块('./Button': './src/Button.js'
remoteshost消费哪些远程(remote_app@http://localhost:3002/remoteEntry.js
shared两边哪些依赖要跨应用共享(避免重复打包/双实例)
filenameremote远程入口文件名,约定 remoteEntry.js
uniqueName两边运行时全局容器名,避免多个应用互相踩

再加一个现代概念:manifest(MF 2.0 默认产出 mf-manifest.json)——把「暴露了什么、共享了什么、远程是谁」写成机器可读的元数据,运行时可以基于它加载,而不只是硬编码 remoteEntry.js 地址。

三、最小可跑配置(真实代码)

remote 端(提供方):

// remote/webpack.config.cjs
const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');

module.exports = {
mode: 'development',
output: { publicPath: 'http://localhost:3002/', uniqueName: 'remote_app', clean: true },
plugins: [
new ModuleFederationPlugin({
name: 'remote_app',
filename: 'remoteEntry.js',
exposes: { './Button': './src/Button.js' }, // 对外提供
shared: { dayjs: { singleton: true, requiredVersion: '^1.11.0' } },
}),
],
};

host 端(消费方):

// host/webpack.config.cjs
plugins: [
new ModuleFederationPlugin({
name: 'host_app',
remotes: { remote_app: 'remote_app@http://localhost:3002/remoteEntry.js' }, // 只是地址
shared: { dayjs: { singleton: true, requiredVersion: '^1.11.0' } },
}),
],

消费方代码就是普通的动态 import(这点最舒服):

const { Button, fmt } = await import('remote_app/Button');

四、构建出来的是什么(本机真实产物)

remote 的 dist/

remoteEntry.js ← 远程入口(供 host 加载)
mf-manifest.json ← 元数据(暴露/共享/远程)
mf-stats.json ← 构建统计
__federation_expose_Button.js ← 被暴露模块的产物
node_modules_dayjs_dayjs_min_js.js ← 共享依赖被单独拆出

host 的 dist/

main.js ← host 自己
mf-manifest.json
node_modules_dayjs_dayjs_min_js.js

注意 host 没有 remoteEntry.js(它不对外暴露),但同样有 dayjs 的独立 chunk——这就是 shared 在构建期的体现。

stats 里的关键证据(真实构建输出)

provide shared module (default) dayjs@1.11.23 = ../node_modules/dayjs/dayjs.min.js
consume shared module (default) dayjs@!=1...1.1...0 (singleton) (fallback: ../node_modules/dayjs/dayjs.min.js)
remote remote_app/Button 6 bytes (remote) 6 bytes (share-init)
external "remote_app@http://localhost:3002/remoteEntry.js" 42 bytes

四行说明四件事

  1. provide:我提供 dayjs 1.11.23 给共享池;
  2. consume:我要消费 dayjs,带 singletonrequiredVersion 约束,并准备了 fallback(共享池没有时用自带的那份);
  3. remoteremote_app/Button 被登记为远程模块(构建期只占 6 字节的“引用”);
  4. external:那个远程地址在产物里是个 external——构建期不下载、运行期才拉

mf-manifest.json 写了什么(真实文件,节选)

{
"id": "remote_app",
"metaData": { "globalName": "remote_app", "remoteEntry": { "name": "remoteEntry.js" },
"pluginVersion": "2.9.0", "publicPath": "http://localhost:3002/" },
"shared": [{ "name": "dayjs", "version": "1.11.23", "singleton": true,
"requiredVersion": "^1.11.0",
"assets": { "js": { "sync": ["node_modules_dayjs_dayjs_min_js.js"] } } }],
"exposes": [{ "id": "remote_app:Button", "name": "Button", "path": "./Button",
"assets": { "js": { "sync": ["__federation_expose_Button.js"] } } }],
"remotes": []
}

host 侧的 manifest 则反过来(exposes 为空,remotes 有值):

"remotes": [{ "federationContainerName": "remote_app", "moduleName": "Button",
"alias": "remote_app", "entry": "http://localhost:3002/remoteEntry.js" }],
"exposes": []

这份 manifest 是 MF 2.0 相对 1.0 最大的“可运维性”升级:远程模块不再只靠一串 URL,而是有可枚举、可校验、可预加载的元数据。

五、运行时契约:remoteEntry 里有什么

我把 remoteEntry.js 拆开看了一眼(grep 真实产物),里面出现这些 API 名:

get init initializeSharing loadRemote registerRemotes (分别出现 27 / 16 / 6 / 4 / 3 次)

对应的运行时心智:

API干什么
init初始化容器(传入共享作用域)
get取某个暴露的模块(get('./Button')
initializeSharing建立/接入共享依赖池
loadRemoteMF 2.0 的便捷入口loadRemote('remote_app/Button') 直接拿到模块
registerRemotes运行期动态注册远程(不用写死在构建配置里)

loadRemote + registerRemotes 让「远程是谁」变成运行时数据——这也是为什么 manifest 驱动加载、微前端平台化管理成为可能。

顺带一个真实数字:dev 模式下我的 remoteEntry.js382 KB(未压缩、含 MF 运行时与共享逻辑),mf-manifest.json 只有 1.2 KB。生产构建会小很多,但体积要心里有数

六、MF vs qiankun:不是替代,是不同层

Module Federationqiankun 类方案
抽象层级模块(import 级)应用(HTML entry 级)
依赖共享(同一份 dayjs/React)各自打包(易双实例)
隔离不做沙箱(共享即耦合)JS 沙箱 + 样式隔离
技术栈构建器插件(webpack/Rspack/Vite)框架无关的运行时容器
适用同构技术栈、要共享依赖(如多个 React 应用)异构/老旧应用隔离接入

实践里两者常叠用:qiankun 负责「应用级别接入与隔离」(尤其是老应用、Vue2+React 混布),MF 负责「新应用之间的模块与依赖共享」。本站 qiankun 系列 讲的是前者的落地细节。

七、MF 2.0 带来了什么

从本机实测的包和官方资料看,2.x 的主要变化:

  • @module-federation/enhanced(2.9.0):一个插件把「暴露/消费/共享/类型/manifest/devtools」都带上,不再手拼一堆插件;
  • 框架无关 runtime@module-federation/runtime 2.9.0):不绑定 webpack,可在任意环境驱动加载;
  • manifest 与 mf-stats.json:可枚举、可托管、可做预加载与健康检查;
  • 运行时插件体系(runtime plugins):重试、降级、日志、缓存策略可以插件化;
  • 多构建器支持:Rspack(@rspack/core 实测版本 2.2.4)与 Vite(@module-federation/vite 1.21.6)都有官方方案;
  • 类型支持:配合 dts 插件可以给远程模块生成/注入 TS 类型(不然 import('remote_app/Button') 全是 any)。

八、五个真实的坑(先想清楚再上)

  1. 依赖双实例:React 这类库没共享成功 → hooks 报错、context 不通用。核心解法就是 singleton: true + 严格的 requiredVersion,并理解「版本协商失败会 fallback 到各自那份」这件事(上面 stats 里那条 consume shared module ... (fallback: ...) 就是它)。
  2. 远程挂了怎么办:MF 默认不会帮你优雅降级。生产要自己加超时、重试、兜底 UI(remoteEntry 拉不到时整块区域降级),这也是 runtime plugin 的用武之地。
  3. 远程地址不是无限的remoteEntry.js运行期拉的,意味着跨域 CORS、CDN 缓存策略、版本发布顺序都要设计(典型做法:remoteEntry 短缓存/不缓存,带 hash 的 chunk 长缓存)。
  4. 样式没有隔离:MF 只共享模块,不做样式沙箱——CSS 命名冲突、全局样式污染要自己用 CSS Modules / 命名空间 / Shadow DOM 解决。
  5. 本地开发体验:remote 没起时 host 直接报错。约定好本地端口、或用 manifest 指向测试环境远程,能省很多扯皮。

九、选型清单

你的情况建议
多个同构应用(都 React/Vue3)要共享组件与依赖Module Federation
要接异构/老旧应用、强隔离qiankun 类方案(也可叠 MF)
只是把页面拼起来、几乎不共享代码iframe / Web Components 可能更省事
团队与发布完全统一单体应用 / monorepo,别上微前端

一句话:MF 解决的是「模块与依赖的跨应用复用」,它的收益来自共享,它的风险也来自共享——共享得越深,团队之间的构建、版本、发布节奏就越需要对齐。

参考

  • Module Federation 官方站(2.0 文档):module-federation.io
  • webpack 官方 Module Federation 指南:webpack.js.org/concepts/module-federation
  • 本站对照:qiankun 微前端系列
  • 版本事实(本机 npm view 实测,2026-09):webpack 5.111.0、@module-federation/enhanced 2.9.0、@module-federation/runtime 2.9.0、@module-federation/vite 1.21.6、@rspack/core 2.2.4
  • 构建产物与 manifest 节选:本机 /tmp/mf-lab 实测(webpack 5.111.0 + MF enhanced 2.9.0,dayjs 1.11.23)