Webpack Module Federation 原理与实现
一、解决了什么问题
传统多应用架构的痛点:
| 问题 | 场景 |
|---|---|
| 重复加载依赖 | A、B 两个应用都用 React,用户访问时加载了两份 |
| 跨应用共享代码困难 | 公共组件只能发 npm 包,改一次发布一次 |
| 巨石应用难拆分 | 一个大 repo,构建慢、团队协作难 |
| 微前端集成复杂 | iframe、single-spa 各有局限,运行时共享难 |
核心解法:让独立部署的多个 JS 应用,在运行时互相共享代码和依赖,而不是构建时打包在一起。
二、核心概念
Host(宿主) Remote(远程)
┌──────────────┐ ┌──────────────┐
│ 主应用 │ ──加载── │ 子应用 │
│ │ │ exposes: │
│ remotes: { │ │ ./Button │
│ app2: URL │ │ ./Header │
│ } │ │ │
└──────────────┘ └──────────────┘
↑ ↑
└──── shared: [react] ──┘
(运行时协商,只加载一份)
| 角色 | 职责 |
|---|---|
| Host(宿主) | 消费其他应用暴露的模块 |
| Remote(远程) | 暴露自己的模块给别人用 |
| Shared(共享) | 声明哪些依赖在运行时共享(如 React) |
一个应用可以同时是 Host 和 Remote。
三、配置示例
Remote 端(被消费方)
// webpack.config.js
new ModuleFederationPlugin({
name: 'app2',
filename: 'remoteEntry.js', // 模块清单入口文件
exposes: {
'./Button': './src/Button', // 暴露的模块
'./Header': './src/Header',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
})
Host 端(消费方)
// webpack.config.js
new ModuleFederationPlugin({
name: 'app1',
remotes: {
app2: 'app2@https://cdn.com/remoteEntry.js', // 运行时才加载
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
})
Host 中使用远程模块
// 像普通 import 一样写,实际是运行时异步加载
const Button = React.lazy(() => import('app2/Button'))
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<Button />
</Suspense>
)
}
四、React.lazy + import() 的含义
React.lazy(() => import('app2/Button'))
两层嵌套,各自独立:
import('app2/Button'):动态 import,返回 Promise。在 MF 场景下,app2是 remotes 配置里的别名,运行时去远程 URL 拉取代码。React.lazy(...):接收一个返回 Promise 的函数,首次渲染时才触发加载。
完整时序:
渲染 <Button />
↓
React.lazy 触发 import()
↓
浏览器请求 remoteEntry.js(模块清单)
↓
协商 shared 依赖(react 已有则复用)
↓
拉取 Button 的 chunk 文件
↓
Suspense fallback 消失,Button 渲染
五、运行时协商原理(shared 依赖)
没有 shared 时的问题
Host 打包: [react@18.2] + [自己的代码]
Remote 打包:[react@18.2] + [Button 组件]
用户访问:
加载 Host → 下载一份 react
加载 Remote → 又下载一份 react ← 重复!
内存里两个 React 实例 → Hooks 报错
加了 shared 之后
构建时:两边的 react 不打进 bundle,而是在 remoteEntry.js 里写入声明:"我提供/需要 react@18.2"。
运行时:MF runtime 执行协商:
Host 说: 我有 react@18.2,放进 shared scope
Remote 说: 我需要 react,版本要求 ^18.0.0
runtime 检查:18.2 满足 ^18.0.0 ✓
结论:Remote 复用 Host 已加载的 react,不重新下载
shared scope(全局共享空间)
┌─────────────────────────────┐
│ react@18.2 ← 只有这一份 │
│ ↑ ↑ │
│ Host 放入 Remote 复用 │
└─────────────────────────────┘
版本冲突处理
| 场景 | singleton: true | singleton: false |
|---|---|---|
| 版本兼容 | 复用高版本 | 各自加载 |
| 版本冲突 | 强制用高版本 + 警告 | 各自加载两份 |
**React 必须设 **singleton: true,否则两个 React 实例并存,Hooks 校验失败报错。
六、shared 关键配置项
shared: {
react: {
singleton: true, // 全局只允许一个实例
requiredVersion: '^18.0.0', // 可接受的版本范围
eager: false, // true = 打进入口 chunk(增大体积,避免懒加载)
strictVersion: false, // true = 版本不匹配直接报错(不降级)
}
}
七、构建产物说明
Remote 构建后会生成:
dist/
├── remoteEntry.js ← 模块清单 + runtime(Host 首先加载这个)
├── src_Button_js.chunk.js ← Button 组件的实际代码
└── ...
remoteEntry.js 是 MF 的核心文件,记录了:
- 该 Remote 暴露了哪些模块
- 每个模块对应哪个 chunk 文件
- 声明了哪些 shared 依赖及版本
八、与其他方案对比
| 方案 | 共享依赖 | 独立部署 | 运行时通信 | 复杂度 |
|---|---|---|---|---|
| npm 包 | 构建时合并 | 需重新发布 | 无 | 低 |
| iframe | 完全隔离 | 是 | postMessage | 高 |
| single-spa | 需手动协调 | 是 | 自定义 | 高 |
| Module Federation | 运行时共享 | 是 | 原生 import | 中 |
九、适用场景
- 多团队维护的微前端架构
- 需要跨应用复用组件/工具库,但不想每次都发 npm 包
- 大型应用拆分,独立部署、独立发布
- 需要在运行时动态加载远程模块(插件化系统)

浙公网安备 33010602011771号