微前端与模块联邦
微前端架构将一个大型前端应用拆分为多个独立的小型应用,每个子应用可以独立开发、独立部署、独立运行。Webpack 5 引入的 Module Federation(模块联邦)是当前最优雅的微前端实现方案之一。它允许不同的 Webpack 构建产物在运行时互相消费对方的模块,无需重新打包或加载完整的远程应用。
模块联邦的核心思想是:一个应用既可以是 Host(消费者),也可以是 Remote(提供者),甚至两者兼有。这种对称的设计让微前端的组合变得极其灵活。
核心配置解析
Module Federation 通过 ModuleFederationPlugin 插件配置,其关键参数如下:
// webpack.config.js (Remote 端 — 提供模块的应用)
const { ModuleFederationPlugin } = require('webpack').container
module.exports = {
// ... 其他配置
plugins: [
new ModuleFederationPlugin({
// name: 当前应用的唯一标识,Remote 端和 Host 端引用时需要一致
name: 'remoteApp',
// filename: 远程入口文件的名称
filename: 'remoteEntry.js',
// exposes: 暴露给其他应用消费的模块
exposes: {
'./Button': './src/components/Button',
'./UserService': './src/services/UserService',
'./utils': './src/utils/index',
},
// shared: 声明共享依赖,避免重复加载
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
lodash: { requiredVersion: '^4.17.0' },
},
}),
],
}
// webpack.config.js (Host 端 — 消费远程模块的应用)
const { ModuleFederationPlugin } = require('webpack').container
module.exports = {
// ... 其他配置
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
// remotes: 声明要消费的远程应用
remotes: {
// 格式: '内部引用名: 远程应用name@远程入口URL'
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
},
// Host 也应该声明 shared,确保共享依赖协商正确
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
}
Host 端使用远程模块
Host 端的入口文件需要使用异步边界(dynamic import),因为远程模块的加载是异步过程:
// src/index.js (Host 端入口)
import('./bootstrap')
// src/bootstrap.js
import React from 'react'
import { createRoot } from 'react-dom/client'
import RemoteButton from 'remoteApp/Button'
function App() {
return (
<div>
<h1>Host Application</h1>
<RemoteButton onClick={() => alert('来自远程模块')}>
远程按钮
</RemoteButton>
</div>
)
}
createRoot(document.getElementById('root')).render(<App />)
共享依赖深入理解
共享依赖是模块联邦的关键特性。当多个应用声明了相同的 shared 依赖时,运行时只会加载版本最高的一份。配置选项的含义:
- singleton — 设为
true时,确保全局只加载一个实例。React 等库必须设为 singleton,否则会报错。 - requiredVersion — 声明需要的版本范围。如果 Remote 提供的版本不满足要求,会回退加载自己的版本。
- eager — 设为
true时,共享依赖会打包进初始 bundle 而不是异步加载。谨慎使用,会增加包体积。 - strictVersion — 设为
true时,如果版本不匹配会抛出警告。
shared: {
react: {
singleton: true, // 全局唯一实例
requiredVersion: '^18.0.0',
strictVersion: true, // 版本不匹配时警告
eager: false, // 异步加载(推荐)
},
vuex: {
requiredVersion: '^4.0.0',
singleton: true,
}
}
动态远程加载
除了在配置中静态声明 remotes,还可以在运行时动态加载远程模块,这适合需要根据环境或用户身份决定加载哪些子应用的场景:
// 动态加载远程组件
import React, { lazy, Suspense } from 'react'
function useDynamicScript(url) {
const [ready, setReady] = React.useState(false)
React.useEffect(() => {
const element = document.createElement('script')
element.src = url
element.type = 'text/javascript'
element.async = true
element.onload = () => setReady(true)
document.head.appendChild(element)
return () => { document.head.removeChild(element) }
}, [url])
return ready
}
function DynamicRemoteComponent({ url, scope, module }) {
const ready = useDynamicScript(url)
const Component = React.useMemo(() => {
if (!ready) return null
return lazy(async () => {
// 初始化远程容器的共享作用域
await __webpack_init_sharing__('default')
const container = window[scope]
await container.init(__webpack_share_scopes__.default)
const factory = await window[scope].get(module)
return factory()
})
}, [ready, scope, module])
if (!Component) return <div>加载中...</div>
return (
<Suspense fallback={<div>加载组件中...</div>}>
<Component />
</Suspense>
)
}
// 使用
function App() {
return (
<DynamicRemoteComponent
url="http://localhost:3002/remoteEntry.js"
scope="dashboardApp"
module="./Dashboard"
/>
)
}
一个完整的双应用示例
下面给出一个更贴近实际的项目结构。假设有一个主框架应用(Shell)和一个业务子应用(Dashboard):
// dashboard/webpack.config.js (Remote)
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { ModuleFederationPlugin } = require('webpack').container
module.exports = {
entry: './src/index.js',
output: { publicPath: 'auto' },
devServer: { port: 3002 },
module: {
rules: [
{ test: /\.jsx?$/, exclude: /node_modules/, use: 'babel-loader' },
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
],
},
plugins: [
new ModuleFederationPlugin({
name: 'dashboard',
filename: 'remoteEntry.js',
exposes: {
'./Dashboard': './src/Dashboard',
'./Charts': './src/components/Charts',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
recharts: { singleton: true },
},
}),
new HtmlWebpackPlugin({ template: './public/index.html' }),
],
}
// shell/webpack.config.js (Host)
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { ModuleFederationPlugin } = require('webpack').container
module.exports = {
entry: './src/index.js',
output: { publicPath: 'auto' },
devServer: { port: 3000 },
module: {
rules: [
{ test: /\.jsx?$/, exclude: /node_modules/, use: 'babel-loader' },
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
],
},
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
dashboard: 'dashboard@http://localhost:3002/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
new HtmlWebpackPlugin({ template: './public/index.html' }),
],
}
与其他微前端方案的对比
| 方案 | 接入成本 | 技术栈限制 | 沙箱隔离 | 适用场景 |
|---|---|---|---|---|
| Module Federation | 低 | Webpack 项目 | 无(共享运行时) | 同技术栈团队协作 |
| qiankun (umi) | 中 | 无限制 | JS 沙箱 + CSS 隔离 | 异构技术栈整合 |
| iframe | 极低 | 无限制 | 完全隔离 | 完全独立的子系统 |
| Web Components | 高 | 无限制 | Shadow DOM | 跨框架组件复用 |
注意事项与最佳实践
- publicPath 设为 'auto' — 让 Webpack 自动推断资源路径,避免部署时静态资源 404。
- 共享依赖必须声明 singleton — React、Vue 等框架不允许多实例共存,务必设置
singleton: true。 - 异步边界 — Host 入口必须使用
import('./bootstrap')的形式,为共享依赖协商争取时间。 - 类型安全 — 对于 TypeScript 项目,可以为远程模块编写
.d.ts类型声明文件。 - 错误边界 — 远程模块加载可能失败,务必在 Host 端用 Error Boundary 包裹远程组件。
- 版本对齐 — 使用
pnpm的 workspace 或renovate工具保持各子应用共享依赖版本一致。
小结
Webpack Module Federation 以最小的侵入性实现了微前端的核心诉求:独立开发、独立部署、运行时集成。相比 iframe 和 JS 沙箱方案,它更轻量、性能更好,但代价是缺乏隔离性。如果你的团队技术栈统一(都是 Webpack + React 或 Webpack + Vue),模块联邦是最推荐的微前端方案。对于异构技术栈混合的场景,可以考虑 qiankun 等提供完整沙箱的框架。