为什么需要模块联邦
随着前端项目规模膨胀,单个应用(巨石应用)的构建、部署和团队协作成本越来越高。微前端架构将一个大应用拆分为多个独立子应用,每个子应用可以由不同团队独立开发、独立部署,最后在运行时组合成一个完整的产品。
传统的微前端方案(如 qiankun、iframe)通常需要一个中心化的“基座”应用来加载子应用,并且子应用之间共享依赖困难,容易导致重复加载和版本冲突。模块联邦(Module Federation) 是 Webpack 5 推出的核心特性,它允许一个应用在运行时动态加载另一个应用的模块,无需中心化编排,并且可以精细控制依赖的共享和版本。
模块联邦的核心概念
- Host(宿主):消费远程模块的应用。
- Remote(远程):暴露模块供其他应用消费的应用。
- Shared(共享依赖):多个应用间共享的库(如 react、react-dom),可以配置为单例,避免重复加载和版本冲突。
- Exposes(暴露模块):Remote 应用中向外暴露的模块路径。
它的本质是:每个应用都可以既是 Host 又是 Remote,在运行时通过异步 chunk 加载其他应用的代码,如同使用本地模块一样。
在 React 项目中使用模块联邦
假设我们有两个独立 React 项目:host-app 和 remote-app。remote-app 暴露一个 Header 组件,host-app 在运行时加载并使用它。
第一步:在 remote-app 中配置暴露
使用 Webpack 5 的 ModuleFederationPlugin,在 webpack.config.js 中添加:
// remote-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
// ...其他配置
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp', // 远程应用的唯一名称
filename: 'remoteEntry.js', // 远程入口文件
exposes: {
// 暴露组件:键是外部使用时的路径,值是本地文件路径
'./Header': './src/components/Header',
},
shared: {
react: { singleton: true, eager: false, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, eager: false, requiredVersion: '^18.0.0' },
},
}),
],
};
singleton: true确保整个页面只有一个 React 实例。eager: false表示异步加载共享依赖,避免初始包体积过大。
第二步:在 host-app 中配置消费
// host-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
// ...
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
// 名字:remoteName@remoteEntry地址
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, eager: false, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, eager: false, requiredVersion: '^18.0.0' },
},
}),
],
};
remotes 中的 remoteApp@http://localhost:3001/remoteEntry.js 告诉 webpack 在运行时从该 URL 加载远程模块。注意开发环境通常需要启动远程应用的服务。
第三步:在 Host 中使用远程组件
// host-app/src/App.jsx
import React, { Suspense } from 'react';
// 使用动态 import 加载远程模块
const RemoteHeader = React.lazy(() => import('remoteApp/Header'));
function App() {
return (
<div>
<Suspense fallback={<div>加载中...</div>}>
<RemoteHeader title="微前端标题" />
</Suspense>
<main>这是宿主应用的主体内容</main>
</div>
);
}
export default App;
import('remoteApp/Header') 会触发加载远程入口文件,然后异步获取组件。React.lazy 配合 Suspense 处理加载状态。
使用 Vite 的模块联邦
Vite 生态中可以使用 @originjs/vite-plugin-federation 插件实现类似功能,配置略有差异但思想一致:
// vite.config.js (remote)
import federation from '@originjs/vite-plugin-federation';
export default {
plugins: [
federation({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Header': './src/components/Header',
},
shared: ['react', 'react-dom'],
}),
],
build: {
target: 'esnext', // 必须,因为模块联邦使用原生 ES 模块
},
};
// vite.config.js (host)
import federation from '@originjs/vite-plugin-federation';
export default {
plugins: [
federation({
name: 'hostApp',
remotes: {
remoteApp: 'http://localhost:3001/assets/remoteEntry.js',
},
shared: ['react', 'react-dom'],
}),
],
};
同理,在代码中用动态 import 引入远程组件。
微前端架构的最佳实践
- 独立仓库与独立部署
每个子应用应放在独立 Git 仓库,有独立的 CI/CD 管道。部署后更新 remoteEntry.js 的地址即可,宿主无需重新部署。
- 共享依赖的统一管理
将 react、react-dom、react-router 等核心库设为 singleton,避免多个实例导致的 Hooks 报错或状态不一致。可以考虑使用一个共享配置包来维护依赖版本。
- 样式隔离
默认情况下,远程组件的 CSS 会注入到全局,可能污染宿主样式。建议使用 CSS Modules 或 CSS-in-JS 方案做到样式隔离,或配置模块联邦的 shared 对样式作用域的控制。
- 通信方式
子应用间应尽量松耦合,推荐使用自定义事件、共享状态库(如 zustand 的 global store)或通过宿主传递回调 Props。避免直接依赖子应用的内部状态。
- 路由集成
如果远程应用有自己的路由,宿主可以选择将部分路径委托给远程应用(模块联邦暴露一个路由配置或页面组件),宿主通过 React Router 动态嵌套。
- 降级与容灾
远程模块加载失败时,应当显示降级 UI,避免整个宿主崩溃:
<ErrorBoundary fallback={<div>组件加载失败</div>}>
<Suspense fallback={<div>loading...</div>}>
<RemoteHeader />
</Suspense>
</ErrorBoundary>
模块联邦 vs 传统微前端方案
| 方案 | 特点 | 缺点 |
|------|------|------|
| qiankun / single-spa | 基于路由分发,主子应用生命周期管理,成熟度高 | 需要改造子应用,JS 沙箱和样式隔离有性能损耗,依赖中心基座 |
| iframe | 完美的隔离性,简单 | 通信麻烦,URL 不同步,性能较差,用户体验割裂 |
| 模块联邦 | 运行时按需加载模块,极度灵活,可共享依赖避免重复加载,无中心基座 | 需要 webpack 5 / vite 插件,样式隔离需自行处理,调试复杂,对构建配置要求高 |
模块联邦更适合组件级别的跨应用共享,特别适合技术栈统一、团队自治且需要高度复用的场景(如大型 SaaS 平台中的公共组件库按需加载)。它能做到真正的“去中心化”微前端,每个应用都能提供能力并被消费。
总结
模块联邦为 React 项目的微前端实践提供了原生级的模块共享能力。它改变了传统的构建方式,让不同团队的代码可以在运行时无缝融合。结合 React 的组件化特性和动态 import,你可以构建出高度可扩展、独立演进的前端架构。然而,这也对团队的工程规范、依赖管理和监控能力提出了更高要求。在引入之前,务必评估项目的实际规模和拆分需求,避免过度工程化。