为什么需要 Module Federation?
传统微前端方案(iframe、路由分发、乾坤 qiankun)各有痛点:
| 方案 |
缺点 |
| iframe |
上下文隔离强但通信麻烦,SEO 不友好 |
| 路由分发 |
多应用需独立部署,体验割裂 |
| qiankun |
侵入性强,子应用改造成本高 |
Webpack 5 Module Federation 提供了一种全新的思路:在构建时定义共享模块,运行时动态加载远程代码,子应用和主应用共享同一个运行时,通信零成本。
基础架构
一个典型的 Module Federation 架构包含两种角色:
- Host(宿主):加载远程模块的主应用,负责路由分发、公共 Layout
- Remote(远程):独立开发的子应用,暴露出可消费的组件/页面
1
2
3
4
|
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Host App │────▶│ Remote App A │ │ Remote App B │
│ (container) │◀────│ (商品页) │ │ (用户页) │
└─────────────┘ └──────────────┘ └──────────────┘
|
实战:搭建 Host + Remote
1. Remote 应用配置
Remote 应用通过 ModuleFederationPlugin 暴露自身组件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// remote/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'product_remote', // 唯一标识
filename: 'remoteEntry.js', // 远程入口文件名
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/pages/ProductDetail',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true },
},
}),
],
};
|
关键点:
name 全局唯一,不能与其他 remote 冲突
exposes 暴露的模块路径:key 是远程路径,value 是本地的模块路径
shared 共享的依赖库:singleton: true 确保整个应用只加载一个 React 实例
2. Host 应用配置
Host 声明需要从哪些 remote 加载模块:
1
2
3
4
5
6
7
8
9
10
11
|
// host/webpack.config.js
new ModuleFederationPlugin({
name: 'host_app',
remotes: {
ProductApp: 'product_remote@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true },
},
});
|
remotes 字段的格式:{别名}: {remoteName}@{remoteEntryUrl}。
3. 在 Host 中引用远程组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// host/src/App.jsx
import React, { Suspense, lazy } from 'react';
// 使用 lazy 加载远程组件
const ProductList = lazy(() => import('ProductApp/ProductList'));
function App() {
return (
<div>
<h1>主应用</h1>
<Suspense fallback={<div>加载中...</div>}>
<ProductList />
</Suspense>
</div>
);
}
|
注意:必须用 React.lazy 或动态 import(),因为远程模块是异步加载的。
共享依赖策略
Module Federation 最关键也最容易出错的是共享依赖配置:
singleton + 版本锁定
1
2
3
4
5
6
7
8
9
|
shared: {
react: {
singleton: true, // 仅一个实例
requiredVersion: '^18.0.0', // 版本约束
eager: false, // 首次访问时异步加载
},
// 不带版本的全局工具库,用 eager 避免白屏
'lodash-es': { singleton: true, eager: true },
}
|
- React/Next.js 等框架库:设
singleton: true,否则会有多个 React 实例导致 hooks 报错
- 轻量工具库(lodash、dayjs):可
singleton: true + eager: true,直接内联
- 组件库(antd、MUI):建议
singleton: true,否则样式和 context 冲突
版本不匹配的处理
当 host 和 remote 依赖版本不同时,Webpack 默认使用声明版本更高的那个:
1
2
3
4
5
6
7
|
// 通过 shared 的 version 字段也可以手动指定
shared: {
dayjs: {
singleton: true,
version: '1.11.0', // 降级到统一版本
},
}
|
动态加载与路由集成
项目中更常见的是按路由粒度加载远程模块:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// host/src/router.jsx
const routes = [
{
path: '/products',
component: lazy(() => import('ProductApp/ProductList')),
},
{
path: '/products/:id',
component: lazy(() => import('ProductApp/ProductDetail')),
},
{
path: '/user',
component: lazy(() => import('UserApp/UserProfile')),
},
];
function AppRouter() {
return (
<BrowserRouter>
<Layout>
<Suspense fallback={<PageSkeleton />}>
<Routes>
{routes.map(r => (
<Route key={r.path} path={r.path} element={<r.component />} />
))}
</Routes>
</Suspense>
</Layout>
</BrowserRouter>
);
}
|
生产部署实战
统一版本号管理
1
2
3
4
5
6
7
|
// 每个 remote 发布时携带版本号
// product_remote@1.2.3 -> remoteEntry.1.2.3.js
new ModuleFederationPlugin({
filename: `remoteEntry.${pkg.version}.js`,
// ... 其余配置
})
|
用 CI 自动注入 remote 地址
开发和生产环境的 remote 地址不同,用环境变量动态注入:
1
2
3
4
5
6
7
8
|
// host/webpack.config.js
const REMOTE_URL = process.env.REMOTE_URL || 'http://localhost:3001';
new ModuleFederationPlugin({
remotes: {
ProductApp: `product_remote@${REMOTE_URL}/remoteEntry.js`,
},
})
|
Nginx 缓存策略
remoteEntry.js 会被频繁引用,建议短缓存或 no-cache:
1
2
3
4
5
6
7
8
9
10
|
# 远程入口文件不缓存,保证 host 总是获取最新版本
location /remoteEntry {
add_header Cache-Control 'no-cache, must-revalidate';
}
# 业务 JS 可以强缓存(文件名含 hash)
location /static {
expires 1y;
add_header Cache-Control 'public, immutable';
}
|
常见坑点
1. 样式冲突
不同 remote 使用不同的 CSS-in-JS 方案或 UI 库版本,可能导致全局样式覆盖。
解决方案:使用 CSS Modules 或 CSS-in-JS 的命名空间隔离:
1
2
3
4
5
6
7
8
|
// webpack.config.js
{
test: /\.module\.css$/,
use: ['style-loader', {
loader: 'css-loader',
options: { modules: { localIdentName: '[name]__[local]--[hash:base64:5]' } }
}],
}
|
2. Context 丢失
如果 host 和 remote 各有一个 React 实例,useContext 将无法共享数据。
解决方案:
- 确保
react 和 react-dom 在 shared 中设为 singleton: true
- 全局数据用 Vite/Rspack 的
define 或 window 对象传递
- 复杂场景使用
React.createContext 结合 shared 策略
3. TypeScript 支持
远程组件在 host 中没有类型定义。推荐使用 @module-federation/typescript 插件:
1
|
npm install @module-federation/typescript -D
|
1
2
3
4
5
6
7
|
// webpack.config.js
const { FederatedTypesPlugin } = require('@module-federation/typescript');
plugins: [
new ModuleFederationPlugin({ /* ... */ }),
new FederatedTypesPlugin(),
],
|
4. 回退策略
当 remote 服务宕机时,host 应提供降级体验:
1
2
3
4
5
6
7
8
9
10
11
12
|
// 封装远程加载组件
function RemoteWrapper({ fallback = <div>服务暂不可用</div>, children }) {
const [hasError, setHasError] = useState(false);
return hasError ? fallback : (
<ErrorBoundary onError={() => setHasError(true)}>
<Suspense fallback={<Skeleton />}>
{children}
</Suspense>
</ErrorBoundary>
);
}
|
总结
Module Federation 的核心优势在于:
- 运行时集成:无需 CI 协调即可独立部署各模块
- 共享运行时:只加载一次 React,避免重复下载
- 渐进迁移:老应用可以逐步暴露出模块,无需大重构
适合的场景:大型后台管理系统、B2B 门户、多团队协作的中台项目。如果你的项目小于 3 个页面或团队只有 1-2 人,用 Vite 的单体应用更省心。