使用 Webpack 构建微前端应用
📋 v1 → v2 差异对照表
Module Federation 通常译作"模块联邦",是 Webpack 5 新引入的一种远程模块动态加载与运行时技术。MF 允许我们将原本单个巨大应用按理想方式拆分成多个体积更小、职责更内聚的小应用形式,理想情况下各个应用能够实现独立部署、独立开发(不同应用甚至允许使用不同技术栈)、团队自治,从而降低系统与团队协作的复杂度 —— 这正是所谓的微前端架构。
An architectural style where independently deliverable frontend applications are composed into a greater whole —— 摘自《Micro Frontends》。
自 Webpack 5 发布以来,Module Federation 经历了从实验性特性到生产级方案的演进。2024 年,字节跳动与 Module Federation 原作者 Zack Jackson 联合推出了 Module Federation 2.0,在原版基础上解决了大量边界问题,大幅提升了稳定性和开发体验。本章将系统性地介绍 Module Federation 的核心原理、MF 2.0 的新特性,以及如何在实际项目中构建可靠的微前端应用。
一、Module Federation 核心概念与架构
1.1 三角色模型
Module Federation 的架构围绕三个核心角色展开:
| 角色 | 英文 | 职责 | 类比 | |------|------|------|------| | 容器 | Container | 提供模块清单与运行时环境 | 图书馆 | | 宿主 | Host | 消费远程模块的应用 | 读者 | | 远程 | Remote | 暴露模块供其他应用使用 | 书籍提供者 |
关键点在于:在 MF 中,一个应用可以同时扮演多个角色。一个应用既可以作为 Remote 暴露模块,也可以作为 Host 消费其他应用的模块。这种去中心化的对等关系是 MF 区别于 qiankun 等传统方案的核心特征。
1.2 运行时架构全景
下图展示了 Module Federation 在运行时的完整数据流与组件交互关系:
架构要点解析:
-
Remote Entry File(
remoteEntry.js):这是整个 MF 的枢纽文件,由ModuleFederationPlugin自动生成,包含:- 所有
exposes模块的索引信息(模块名 → chunk 文件路径的映射) shared依赖的元数据(包名、版本、共享策略)- MF Runtime 的初始化代码
- 所有
-
Shared Scope(共享作用域):所有参与 MF 的应用通过一个全局的共享作用域来协商依赖复用。这是一个类似 Map 的数据结构,key 为包名,value 为模块加载/初始化的 Promise 链。
-
异步边界(Async Boundary):Host 应用的入口必须被包裹在异步操作中(通常使用动态
import()),确保 Shared Scope 初始化完成后才执行业务代码。
1.3 ModuleFederationPlugin 核心配置项
new ModuleFederationPlugin({
// === 身份标识 ===
name: "appName", // 应用唯一名称
// === 入口文件 ===
filename: "remoteEntry.js", // 远程入口文件名
// === Remote 端:暴露模块 ===
exposes: {
"./moduleName": "./src/path/to/module",
// 支持多种格式:
// "./Button": "./src/Button", // 相对路径
// "./utils": { import: "./src/utils", name: "utils" }, // 对象形式(MF 2.0 增强)
},
// === Host 端:消费远程模块 ===
remotes: {
// 格式: <别名>: <name>@<url>
RemoteApp: "app1@http://localhost:8081/remoteEntry.js",
// MF 2.0 支持 Promise 形式(动态加载)
DynamicRemote: "promise new Promise(resolve => { ... })",
},
// === 共享依赖 ===
shared: {
react: {
singleton: true, // 强制单例(React 必须开启)
requiredVersion: "^18.2",// 版本要求(semver)
eager: false, // 是否同步打包
shareKey: "react", // 自定义共享 key
shareScope: "default", // 共享作用域名称
},
// 简写形式: 'lodash' → 等同于 { lodash: { requiredVersion: '*' } }
},
// === MF 2.0 新增配置 ===
sharedScope: "default", // 默认共享作用域名
remoteType: "var", // 远程模块类型: var | module | script
implementation: undefined, // 自定义 MF runtime 实现
})二、基础示例:Host + Remote 双向通信
下面我们搭建一个完整的 MF 示例,展示 Host 与 Remote 之间的双向模块共享能力 —— 这是 MF 区别于传统主子架构的核心优势。
2.1 项目结构
MF-bidirectional
├─ app-host # 宿主应用 (端口 8082)
│ ├─ src
│ │ ├─ bootstrap.js # 异步入口(关键!)
│ │ └─ main.js # 业务入口
│ ├─ webpack.config.js
│ └─ package.json
├─ app-remote # 远程应用 (端口 8081)
│ ├─ src
│ │ ├── utils.js # 导出给 Host 的工具函数
│ │ ├── Button.jsx # 导出给 Host 的 UI 组件
│ │ └── main.js # 业务入口(也消费 Host 的模块)
│ ├─ webpack.config.js
│ └─ package.json
├─ lerna.json
└─ package.json2.2 Remote 应用配置(app-remote)
const path = require("path");
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
mode: "development",
devtool: "source-map",
entry: path.resolve(__dirname, "./src/main.js"),
output: {
path: path.resolve(__dirname, "./dist"),
publicPath: "http://localhost:8081/dist/",
clean: true,
},
plugins: [
new ModuleFederationPlugin({
name: "appRemote",
filename: "remoteEntry.js",
// === 暴露模块给其他应用 ===
exposes: {
"./utils": "./src/utils",
"./Button": "./src/Button",
},
// === 同时也作为 Host,消费其他应用的模块 ===
remotes: {
HostApp: "appHost@http://localhost:8082/dist/remoteEntry.js",
},
// === 共享依赖 ===
shared: {
react: {
singleton: true,
requiredVersion: "^18.2.0",
},
"react-dom": {
singleton: true,
requiredVersion: "^18.2.0",
},
lodash: {
singleton: false,
requiredVersion: "^4.17.0",
},
},
}),
],
devServer: {
port: 8081,
hot: true,
headers: {
"Access-Control-Allow-Origin": "*", // CORS 必须!
},
},
};2.3 Host 应用配置(app-host)
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
mode: "development",
devtool: "source-map",
entry: path.resolve(__dirname, "./src/main.js"),
output: {
path: path.resolve(__dirname, "./dist"),
publicPath: "auto", // MF 推荐设置
clean: true,
},
plugins: [
new ModuleFederationPlugin({
name: "appHost",
filename: "remoteEntry.js",
// === Host 也暴露模块(双向!) ===
exposes: {
"./theme": "./src/theme",
"./store": "./src/store",
},
// === 消费 Remote 的模块 ===
remotes: {
RemoteApp: "appRemote@http://localhost:8081/dist/remoteEntry.js",
},
// === 共享依赖(必须与 Remote 保持一致) ===
shared: {
react: {
singleton: true,
requiredVersion: "^18.2.0",
},
"react-dom": {
singleton: true,
requiredVersion: "^18.2.0",
},
lodash: {
singleton: false,
requiredVersion: "^4.17.0",
},
},
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
devServer: {
port: 8082,
hot: true,
open: true,
headers: {
"Access-Control-Allow-Origin": "*",
},
},
};2.4 异步入口(关键步骤)
⚠️ 这是 MF 最容易踩坑的地方:Host 和 Remote 的入口文件都必须是异步的,以确保 Shared Scope 在业务代码执行前完成初始化。
Host 端 — bootstrap.js + main.js 模式:
// app-host/src/main.js(真正的入口)
import("./bootstrap");
// app-host/src/bootstrap.js(异步边界)
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);Remote 端同样需要异步入口:
// app-remote/src/main.js
import("./bootstrap");2.5 双向模块消费
Host 消费 Remote 的模块:
// app-host/src/App.jsx
import React, { Suspense, lazy } from "react";
// 异步导入远程模块
const RemoteButton = lazy(() => import("RemoteApp/Button"));
const { formatMessage } = await import("RemoteApp/utils"); // 顶层 await 或在 useEffect 中
function App() {
return (
<div className="app">
<h1>Host Application</h1>
<Suspense fallback={<div>Loading Remote Button...</div>}>
<RemoteButton
label="来自 Remote 的按钮"
onClick={() => console.log(formatMessage("Hello from Remote!"))}
/>
</Suspense>
</div>
);
}
export default App;Remote 消费 Host 的模块(反向调用):
// app-remote/src/Button.jsx
import React from "react";
import { useTheme } from "HostApp/theme"; // 消费 Host 的主题!
export default function Button({ label, onClick }) {
const theme = useTheme();
return (
<button
onClick={onClick}
style={{
padding: "8px 16px",
backgroundColor: theme.primaryColor,
color: "#fff",
border: "none",
borderRadius: 4,
cursor: "pointer",
}}
>
{label}
</button>
);
}这个示例展示了 MF 的核心价值:没有主从之分,每个应用都是平等的参与者。Remote 可以调用 Host 的主题系统,Host 也可以调用 Remote 的 UI 组件和工具函数。
三、依赖共享机制深度解析
依赖共享是 Module Federation 最精妙的设计之一,它解决了一个根本性问题:如何在多个独立部署的应用之间避免重复加载同一份依赖?
3.1 共享策略详解
shared 配置支持以下策略选项:
| 选项 | 类型 | 默认值 | 说明 | |------|------|--------|------| | singleton | boolean | false | 强制单例模式:整个应用生命周期中只存在一份该模块实例。React/Vue/Angular 等框架必须设为 true,否则会出现多实例导致的 hooks/context 失效等严重 bug | | eager | boolean | false | 同步打包:将共享依赖直接打入初始 chunk,而非异步加载。适用于必须在入口同步执行的库 | | requiredVersion | string/false | *(匹配任意版本) | 版本约束,支持 semver 语法(如 ^18.2.0、~4.17.21)。设为 false 可禁用该共享 | | shareKey | string | 包名 | 自定义共享作用域中的 key,用于处理同一包的不同构建场景 | | shareScope | string | "default" | 共享作用域命名空间,可用于隔离不同团队的共享依赖 | | version | string | auto(从 package.json 读取) | 手动指定版本号,用于无法自动识别的场景 |
3.2 共享依赖协商流程
当 Host 加载 Remote 模块时,双方会对共享依赖进行一轮版本协商。以下是完整的协商时序:
关键阶段说明:
- register(注册阶段):各应用启动时将自己的共享依赖元数据注册到 Shared Scope,此时不会真正加载模块
- consume(消费阶段):当某个应用实际需要使用共享依赖时触发版本协商
- init(初始化阶段):协商通过后,调用模块的
get()函数获取真正的模块实例。所有消费者共享同一个 init 结果(Promise 缓存)
3.3 最佳实践配置模板
shared: {
// ===== 框架级依赖:必须 singleton =====
react: {
singleton: true, // React 必须单例!
requiredVersion: "^18.2.0",
eager: false, // React 不需要 eager
},
"react-dom": {
singleton: true,
requiredVersion: "^18.2.0",
},
"react-router-dom": {
singleton: true,
requiredVersion: "^6.8.0",
},
// ===== 工具库:推荐 singleton 但允许版本容差 =====
lodash: {
singleton: true,
requiredVersion: "^4.17.0", // 允许 4.17.x 范围内的任意版本
},
dayjs: {
singleton: false, // dayjs 无状态,允许多实例
requiredVersion: "^1.11.0",
},
// ===== 特殊场景:必须 eager =====
"core-js": {
singleton: true,
eager: true, // polyfill 必须在入口同步加载
},
"regenerator-runtime": {
singleton: true,
eager: true,
},
}3.4 常见陷阱与解决方案
陷阱 1:忘记异步入口导致共享失效
❌ 错误做法:直接同步导入
// main.js — 同步入口(错误!)
import React from "react"; // 此时 Shared Scope 尚未初始化
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));✅ 正确做法:异步边界
// main.js
import("./bootstrap"); // 先让 MF Runtime 初始化 Shared Scope
// bootstrap.js
import React from "react"; // 现在 Shared Scope 已就绪
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));陷阱 2:React 多实例导致 Hooks 失效
如果两个应用的 React 版本不兼容且 singleton: false,会导致页面中出现两份 React 实例,此时 Hooks、Context 等跨组件通信机制全部失效。
诊断方法:在浏览器控制台执行:
// 如果返回两个不同的对象,说明存在多实例
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers解决方案:确保所有应用对 React 设置 singleton: true + 合理的 requiredVersion。
陷阱 3:publicPath 配置错误导致资源 404
MF 的 Remote Entry 和后续 chunk 都是通过 HTTP(S) 加载的,output.publicPath 必须指向可访问的 URL:
// 开发环境
output: {
publicPath: "http://localhost:8081/dist/",
}
// 生产环境(建议使用变量或自动推断)
output: {
publicPath: "auto", // Webpack 5 支持根据当前脚本 URL 自动推导
}四、Module Federation 2.0 新特性详解
Module Federation 2.0 由字节跳动前端团队与 MF原作者 Zack Jackson 联合推出,解决了 1.x 版本在生产环境中遇到的诸多痛点。
4.1 MF 2.0 核心改进一览
| 改进领域 | 1.x 问题 | 2.0 解决方案 | |----------|----------|--------------| | 动态远程加载 | remotes 只能静态配置 | 支持 Promise-based 动态 Remotes | | 错误恢复 | 远程加载失败后无法恢复 | Error Boundary 集成支持 | | 类型安全 | 无 TypeScript 支持 | 完整的类型推导支持 | | 共享策略 | singleton/eager 配置复杂 | 更智能的默认值与策略推断 | | 边界情况 | 循环依赖、重复初始化等 | 大量边界 bug 修复 | | 开发体验 | HMR 不稳定 | 显著改善的热更新体验 | | 构建性能 | 大型项目构建慢 | 优化后的依赖图分析算法 |
4.2 动态远程加载(Dynamic Remotes)
这是 MF 2.0 最实用的特性之一。在 1.x 中,remotes 必须在编译时确定;2.0 允许你在运行时决定加载哪个远程应用。
使用场景:
- A/B 测试:动态切换不同版本的微应用
- 多环境:dev/stage/prod 使用不同的远程地址
- 权限控制:根据用户角色动态加载功能模块
- 灰度发布:渐进式发布新版本
实现方式一:webpack 配置中的 Promise 形式
// webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "host",
remotes: {
// 动态决定远程应用地址
microFrontend: `promise new Promise((resolve) => {
const remoteUrl = window.APP_CONFIG?.REMOTE_URL
| | "http://localhost:8081/remoteEntry.js";
const script = document.createElement("script");
script.src = remoteUrl;
script.type = "text/javascript";
script.async = true;
script.onload = () => {
// 脚本执行后会注册全局变量
const scope = window.appRemote; // 对应 Remote 的 name
resolve(scope);
};
script.onerror = () => {
reject(new Error(\`Failed to load remote: \${remoteUrl}\`));
};
document.head.appendChild(script);
})`,
},
}),
],
};实现方式二:代码中使用 getRemote(推荐)
import React, { useState, useEffect, Suspense } from "react";
function DynamicMicroFrontend() {
const [RemoteComponent, setRemoteComponent] = useState(null);
useEffect(() => {
async function loadRemote() {
// 动态获取远程入口 URL
const remoteUrl = `${process.env.REMOTE_BASE_URL}/remoteEntry.js`;
// 使用 import() 动态加载
const scope = await import(/* webpackIgnore: true */ remoteUrl);
const module = await scope.default.loadRemote("./Dashboard");
setRemoteComponent(() => module.default);
}
loadRemote().catch(console.error);
}, []);
if (!RemoteComponent) return <div>Loading...</div>;
return (
<ErrorBoundary fallback={<div>Micro frontend error</div>}>
<Suspense fallback={<div>Loading component...</div>}>
<RemoteComponent data={someData} />
</Suspense>
</ErrorBoundary>
);
}4.3 错误边界(Error Boundary)
微前端环境中,某个子应用崩溃不应影响整体系统。MF 2.0 与 React Error Boundary 天然契合:
class MicroFrontendErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback?: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error("[MF Error Boundary]", error, errorInfo);
// 上报监控
reportErrorToService(error, { type: "micro-frontend", ...errorInfo });
}
render() {
if (this.state.hasError) {
return this.props.fallback | | (
<div style={{ padding: 20, textAlign: "center" }}>
<h3>⚠️ 子应用加载异常</h3>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false, error: null })}>
重试
</button>
</div>
);
}
return this.props.children;
}
}
// 使用
<MicroFrontendErrorBoundary>
<Suspense fallback={<Skeleton />}>
<RemoteDashboard />
</Suspense>
</MicroFrontendErrorBoundary>4.4 MF 2.0 的 exposes 增强
// MF 2.0 支持更丰富的 exposes 格式
exposes: {
// 原有格式(保持兼容)
"./Button": "./src/Button",
// 对象形式(新增)
"./Dialog": {
import: "./src/Dialog",
name: "Dialog", // 用于调试和错误提示
},
// 多入口导出
"./components/*": "./src/components/*.js",
}五、微前端技术选型对比
在落地微前端架构时,技术选型是最关键的决策之一。下面对比当前主流的四套方案:
5.1 方案总览
5.2 详细对比矩阵
| 维度 | Module Federation | qiankun | single-spa | Micro App | |------|-----------------------|-------------|----------------|---------------| | 底层原理 | Webpack 5 运行时模块加载 | HTML Entry + Sandbox | 路由分发 + 生命周期 | WebComponent + Sandbox | | 技术栈限制 | ⚠️ 强依赖 Webpack 5 | 无限制(框架无关) | 无限制(框架无关) | 无限制(框架无关) | | 沙箱机制 | ❌ 无内置沙箱 | ✅ JS 沙箱 + 样式隔离 | ❌ 无(需自行实现) | ✅ JS/CSS 沙箱 | | 应用关系 | 🔥 对等(P2P) | 主子(Master-Slave) | 主子(Main-Parcel) | 主子(Base-Micro) | | 双向通信 | ✅ 原生支持 | ⚠️ 需借助 init/global | ⚠️ 需借助 custom props | ✅ 数据响应式通信 | | 依赖共享 | 🔥 原生 Shared Scope | ❌ 无(各自打包) | ❌ 无 | ❌ 无 | | 样式隔离 | ❌ 无(需 CSS Modules 等) | ✅ Shadow DOM / 严格模式 | ❌ 无 | ✅ Scoped CSS | | 预加载 | ✅ 按需加载 | ✅ 预加载 | ✅ 手动管理 | ✅ 自动预加载 | | TypeScript | ✅ MF 2.0 完整支持 | ✅ 良好 | ✅ 良好 | ✅ 良好 | | 社区成熟度 | 🌟🌟🌟🌟 快速增长 | 🌟🌟🌟🌟🌟 国内主流 | 🌟🌟🌟🌟 成熟稳定 | 🌟🌟🌟🌟 京东出品 | | 上手难度 | 🟢 中等(需理解 MF 概念) | 🟢 较低 | 🟡 中高 | 🟢 较低 | | 适用场景 | 全栈统一 Webpack 5 的团队 | 已有 legacy 系统改造 | 高度定制化需求 | 快速接入、京东生态 | | 维护方 | Webpack 官方 + 字节跳动 | 蚂蚁集团 | single-spa 社区 | 京东 |
5.3 选型决策树
你的团队是否全员使用 Webpack 5?
├─ 是 → Module Federation(首选)
│ ├─ 需要沙箱?→ MF + 手动 CSS Modules / Shadow DOM
│ └─ 不需要 → 直接用 MF
└─ 否 → 是否需要强沙箱隔离?
├─ 是 → qiankun(国内)/ Micro App(京东生态)
└─ 否 → single-spa(高度灵活)或 Micro App5.4 ESM-shared-stack:新兴方案
除了上述四大方案外,社区还涌现了一些基于 Native ESM(原生 ES Modules) 的新兴微前端方案:
- ESM-shared-stack:利用浏览器原生 ESM 的 import map 能力实现模块共享,无需构建工具
- native-fetch:纯运行时方案,零构建依赖
- 优势:极致轻量、无构建步骤、天然 tree-shaking
- 劣势:浏览器兼容性要求高(需支持 Import Maps)、生态尚不成熟
💡 趋势判断:随着浏览器对 ESM 和 Import Maps 支持的完善,基于 Native ESM 的微前端方案可能会在未来成为重要补充,但短期内 Module Federation 仍是 Webpack 生态下的最优解。
六、进阶话题
6.1 路由集成
在微前端架构中,路由管理是一个核心挑战。以下是几种常见的集成模式:
模式 A:集中式路由(推荐用于 MF)
由 Host 统一管理路由,Remote 只负责暴露路由配置或页面组件:
// host/src/App.jsx
import { HashRouter, Routes, Route, Navigate } from "react-router-dom";
import { lazy, Suspense } from "react";
import Navigation from "./Navigation";
// 远程路由配置(由各 Remote 应用暴露)
const remoteRoutes = await import("RemoteOrder/routes");
const remoteUserRoutes = await import("RemoteUser/routes");
const allRoutes = [
{ path: "/", element: <HomePage />, exact: true },
...remoteRoutes.default, // 订单模块的路由
...remoteUserRoutes.default, // 用户模块的路由
];
function App() {
return (
<HashRouter>
<Navigation />
<Routes>
{allRoutes.map(({ path, element, exact }) => (
<Route
key={path}
path={path}
element={
<Suspense fallback={<PageSkeleton />}>
{element}
</Suspense>
}
/>
))}
</Routes>
</HashRouter>
);
}// order/src/routes.js — Remote 暴露路由配置
import OrderList from "./OrderList";
import OrderDetail from "./OrderDetail";
export default [
{ path: "/orders", element: <OrderList /> },
{ path: "/orders/:id", element: <OrderDetail /> },
];模式 B:分布式路由
各 Remote 应用自主管理内部路由,Host 通过 <iframe> 或 Web Component 容器嵌套:
function MicroRouteContainer({ remoteName, basePath }) {
const RemoteApp = lazy(() => import(`${remoteName}/App`));
return (
<BrowserRouter basename={basePath}>
<Suspense fallback={<Loading />}>
<RemoteApp />
</Suspense>
</BrowserRouter>
);
}6.2 状态共享
微前端间的状态共享有多种粒度的方案:
方案一:通过共享模块传递 Store
// host/src/store.js — 暴露共享状态
import { createStore } from "redux";
const store = createStore(rootReducer);
export default store;
export const dispatch = store.dispatch;
export const getState = store.getState;
// remote/src/UserProfile.jsx — 消费共享状态
import store from "HostApp/store";
function UserProfile() {
const [user, setUser] = useState(store.getState().user);
useEffect(() => {
const unsub = store.subscribe(() => {
setUser(store.getState().user);
});
return unsub;
}, []);
return <div>Hello, {user.name}</div>;
}方案二:自定义事件总线(Event Bus)
// shared/eventBus.js — 轻量级事件总线
class EventBus {
constructor() {
this.events = new Map();
}
on(event, callback) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event).push(callback);
}
off(event, callback) {
const callbacks = this.events.get(event);
if (callbacks) {
this.events.set(event, callbacks.filter(cb => cb !== callback));
}
}
emit(event, data) {
const callbacks = this.events.get(event);
if (callbacks) {
callbacks.forEach(cb => cb(data));
}
}
}
export const eventBus = new EventBus();
// 将 eventBus 加入 shared 配置即可在所有应用间使用方案三:共享模块 + Context(React 生态)
对于 React 技术栈,可以通过 MF 共享 Context Provider:
// host/src/SharedContext.jsx
import { createContext, useContext } from "react";
const ThemeContext = createContext({ primaryColor: "#1890ff" });
const AuthContext = createContext(null);
export function useTheme() { return useContext(ThemeContext); }
export function useAuth() { return useContext(AuthContext); }
export function SharedProviders({ children }) {
return (
<ThemeContext.Provider value={themeConfig}>
<AuthContext.Provider value={authInfo}>
{children}
</AuthContext.Provider>
</ThemeContext.Provider>
);
}6.3 样式隔离
Module Federation 本身不提供样式隔离能力,但可以结合以下方案使用:
| 方案 | 实现方式 | 优点 | 缺点 | |------|----------|------|------| | CSS Modules | 编译时生成唯一类名 | 零运行时开销 | 需构建工具配合 | | CSS-in-JS | 运行时生成样式 | 动态样式能力强 | 性能开销、包体积增大 | | Shadow DOM | 浏览器原生隔离 | 完全隔离 | 事件冒泡受影响、兼容性 | | BEM 规范 | 命名约定 | 简单可靠 | 依赖团队纪律 | | scoped-css(Vue) | Vue 编译器添加属性选择器 | Vue 原生支持 | 仅限 Vue |
推荐实践:CSS Modules + 统一的设计 Token(Design Tokens),并通过 MF 共享:
// design-tokens.js — 共享设计令牌
export const tokens = {
colors: {
primary: "#1890ff",
success: "#52c41a",
warning: "#faad14",
error: "#ff4d4f",
},
spacing: {
xs: "4px",
sm: "8px",
md: "16px",
lg: "24px",
xl: "32px",
},
borderRadius: {
sm: "2px",
md: "4px",
lg: "8px",
},
};
// 将此文件加入 shared 配置,所有应用共享同一套设计规范6.4 性能优化策略
策略一:预加载 Remote Entry
<!-- 在 Host 的 index.html 中预加载 -->
<link rel="preload" href="http://localhost:8081/remoteEntry.js" as="script" />
<link rel="prefetch" href="http://localhost:8082/remoteEntry.js" as="script" />策略二:共享依赖 eager 化关键路径
shared: {
react: { singleton: true, eager: true }, // 关键路径依赖 eager 化
"react-dom": { singleton: true, eager: true },
}策略三:细粒度 Exposes
避免暴露过大的模块,按需拆分:
// ❌ 粗粒度:一次加载整个组件库
exposes: {
"./UI": "./src/components/index", // 包含几十个组件
}
// ✅ 细粒度:按需加载
exposes: {
"./Button": "./src/components/Button",
"./Input": "./src/components/Input",
"./Modal": "./src/components/Modal",
"./Table": "./src/components/Table",
}七、完整微前端实战案例
下面构建一个包含订单管理和用户中心两大模块的微前端系统,展示 MF 在真实项目中的应用。
7.1 项目结构
MF-micro-fe-v2
├─ packages
│ ├─ host/ # 主应用(布局 + 路由)
│ │ ├─ public/
│ │ │ └─ index.html
│ │ ├─ src/
│ │ │ ├─ bootstrap.js # 异步入口
│ │ │ ├─ main.js
│ │ │ ├─ App.jsx
│ │ │ ├─ Layout.jsx # 主布局
│ │ │ ├─ Navigation.jsx # 导航栏
│ │ │ ├─ ErrorBoundary.jsx # 错误边界
│ │ │ ├─ store/ # 共享状态
│ │ │ └─ theme/ # 设计令牌
│ │ ├─ webpack.config.js
│ │ └─ package.json
│ │
│ ├─ order/ # 订单微应用
│ │ ├─ src/
│ │ │ ├─ bootstrap.js
│ │ │ ├─ main.js
│ │ │ ├─ routes.js # 暴露路由配置
│ │ │ ├─ OrderList.jsx
│ │ │ ├─ OrderDetail.jsx
│ │ │ └─ OrderForm.jsx
│ │ ├─ webpack.config.js
│ │ └─ package.json
│ │
│ └─ user/ # 用户微应用
│ ├─ src/
│ │ ├─ bootstrap.js
│ │ ├─ main.js
│ │ ├─ routes.js
│ │ ├─ Profile.jsx
│ │ ├─ Settings.jsx
│ │ └─ AvatarUpload.jsx
│ ├─ webpack.config.js
│ └─ package.json
│
├─ lerna.json
└─ package.json7.2 Host 应用完整配置
// packages/host/webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
mode: "development",
devtool: "source-map",
entry: path.resolve(__dirname, "./src/main.js"),
output: {
path: path.resolve(__dirname, "./dist"),
publicPath: "auto",
clean: true,
},
resolve: {
extensions: [".js", ".jsx"],
},
plugins: [
new ModuleFederationPlugin({
name: "host",
filename: "remoteEntry.js",
// Host 暴露共享资源
exposes: {
"./store": "./src/store",
"./theme": "./src/theme/tokens",
"./EventBus": "./src/eventBus",
},
// 消费远程微应用
remotes: {
order: "order@http://localhost:8081/remoteEntry.js",
user: "user@http://localhost:8083/remoteEntry.js",
},
// 共享依赖
shared: {
react: { singleton: true, requiredVersion: "^18.2.0" },
"react-dom": { singleton: true, requiredVersion: "^18.2.0" },
"react-router-dom": { singleton: true, requiredVersion: "^6.8.0" },
lodash: { singleton: true, requiredVersion: "^4.17.0" },
antd: { singleton: false, requiredVersion: "^5.0.0" },
},
}),
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
devServer: {
port: 8082,
hot: true,
open: true,
headers: { "Access-Control-Allow-Origin": "*" },
},
};7.3 Host 应用核心代码
// packages/host/src/bootstrap.js
import React from "react";
import ReactDOM from "react-dom/client";
import { HashRouter } from "react-router-dom";
import App from "./App";
import { SharedProviders } from "./context";
ReactDOM.createRoot(document.getElementById("root")).render(
<SharedProviders>
<HashRouter>
<App />
</HashRouter>
</SharedProviders>
);// packages/host/src/App.jsx
import React, { useState, useEffect, Suspense } from "react";
import { Routes, Route, Navigate } from "react-router-dom";
import Layout from "./Layout";
import Navigation from "./Navigation";
import ErrorBoundary from "./ErrorBoundary";
import PageSkeleton from "./PageSkeleton";
function App() {
const [remoteRoutes, setRemoteRoutes] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function loadRemoteRoutes() {
try {
const [orderRoutes, userRoutes] = await Promise.all([
import("order/routes").then(m => m.default),
import("user/routes").then(m => m.default),
]);
setRemoteRoutes([...orderRoutes, ...userRoutes]);
} catch (err) {
console.error("Failed to load remote routes:", err);
setError(err);
} finally {
setLoading(false);
}
}
loadRemoteRoutes();
}, []);
if (loading) return <PageSkeleton />;
if (error) {
return (
<ErrorBoundary>
<div>部分微应用加载失败,但本地功能仍可用</div>
<Navigate to="/" replace />
</ErrorBoundary>
);
}
const localRoutes = [
{ path: "/", element: <HomePage /> },
];
const allRoutes = [...localRoutes, ...remoteRoutes];
return (
<Layout>
<Navigation />
<ErrorBoundary>
<Routes>
{allRoutes.map((route) => (
<Route
key={route.path}
path={route.path}
element={
<Suspense fallback={<PageSkeleton />}>
<ErrorBoundary>
{route.element}
</ErrorBoundary>
</Suspense>
}
/>
))}
</Routes>
</ErrorBoundary>
</Layout>
);
}
function HomePage() {
return (
<div style={{ padding: 24 }}>
<h2>🏠 首页</h2>
<p>欢迎来到微前端演示系统</p>
</div>
);
}
export default App;7.4 Order 微应用
// packages/order/webpack.config.js
const path = require("path");
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
mode: "development",
devtool: "source-map",
entry: path.resolve(__dirname, "./src/main.js"),
output: {
path: path.resolve(__dirname, "./dist"),
publicPath: "http://localhost:8081/dist/",
clean: true,
},
resolve: { extensions: [".js", ".jsx"] },
plugins: [
new ModuleFederationPlugin({
name: "order",
filename: "remoteEntry.js",
exposes: {
"./routes": "./src/routes",
"./OrderList": "./src/OrderList",
"./OrderDetail": "./src/OrderDetail",
},
remotes: {
host: "host@http://localhost:8082/remoteEntry.js",
},
shared: {
react: { singleton: true, requiredVersion: "^18.2.0" },
"react-dom": { singleton: true, requiredVersion: "^18.2.0" },
"react-router-dom": { singleton: true, requiredVersion: "^6.8.0" },
antd: { singleton: false, requiredVersion: "^5.0.0" },
lodash: { singleton: true, requiredVersion: "^4.17.0" },
},
}),
],
devServer: {
port: 8081,
hot: true,
headers: { "Access-Control-Allow-Origin": "*" },
},
};// packages/order/src/routes.js
import OrderList from "./OrderList";
import OrderDetail from "./OrderDetail";
const routes = [
{
path: "/orders",
element: <OrderList />,
label: "订单列表",
},
{
path: "/orders/:id",
element: <OrderDetail />,
label: "订单详情",
},
];
export default routes;// packages/order/src/OrderList.jsx
import React, { useState } from "react";
import { Table, Button, Tag, Space } from "antd";
import { useTheme } from "host/theme"; // 消费 Host 的设计令牌!
const mockOrders = [
{ id: "ORD-001", product: "MacBook Pro 16\"", amount: 19999, status: "completed" },
{ id: "ORD-002", product: "iPhone 15 Pro", amount: 8999, status: "pending" },
{ id: "ORD-003", product: "AirPods Pro 2", amount: 1899, status: "shipped" },
];
export default function OrderList() {
const tokens = useTheme(); // ← 来自 Host 的共享主题!
const [orders, setOrders] = useState(mockOrders);
const columns = [
{ title: "订单号", dataIndex: "id", key: "id" },
{ title: "商品", dataIndex: "product", key: "product" },
{
title: "金额",
dataIndex: "amount",
key: "amount",
render: (v) => `¥${v.toLocaleString()}`,
},
{
title: "状态",
dataIndex: "status",
key: "status",
render: (status) => {
const map = {
completed: { color: "green", text: "已完成" },
pending: { color: "orange", text: "待付款" },
shipped: { color: "blue", text: "配送中" },
};
const s = map[status] | | { color: "default", text: status };
return <Tag color={s.color}>{s.text}</Tag>;
},
},
];
return (
<div style={{ padding: 24 }}>
<h2 style={{ marginBottom: tokens.spacing.lg, color: tokens.colors.primary }}>
📦 订单管理
</h2>
<Table
dataSource={orders}
columns={columns}
rowKey="id"
pagination={{ pageSize: 10 }}
/>
</div>
);
}7.5 启动与验证
npm install
npx webpack serve --config packages/order/webpack.config.js
npx webpack serve --config packages/user/webpack.config.js
npx webpack serve --config packages/host/webpack.config.js访问 http://localhost:8082 即可看到完整的微前端系统,其中:
/orders和/orders/:id由 order 微应用渲染/profile和/settings由 user 微应用渲染/由 host 本地渲染- 所有应用共享同一份 React、React-DOM、Lodash
打开 DevTools → Network 面板,可以观察到:
remoteEntry.js仅加载一次(每个 Remote 应用一份)- 共享依赖(如 React)只加载一份到 Shared Scope
- 各微应用的业务 chunk 按需加载
八、总结
Module Federation 是 Webpack 5 引入的一项革命性特性,它从根本上改变了前端应用的组织方式和部署形态。通过本章的学习,我们梳理了以下核心知识点:
核心要点回顾
- 三角色架构:Container(容器)、Host(宿主)、Remote(远程端)构成了 MF 的基本骨架,一个应用可以同时承担多个角色
- 运行时加载:MF 通过
remoteEntry.js作为枢纽,实现了编译时独立构建、运行时动态组合的能力 - 依赖共享:Shared Scope 机制通过 register → consume → init 三阶段流程,在保证功能正确的前提下最大化减少冗余下载
- MF 2.0 进化:动态远程加载、错误边界集成、类型安全支持等新特性使 MF 从实验走向生产可用
- 技术选型:在全栈 Webpack 5 团队中,MF 是微前端的首选方案;在异构技术栈场景下,qiankun/Micro App 等方案更为合适
适用场景
✅ 适合使用 Module Federation 的场景:
- 大型单体前端应用的拆分与渐进式重构
- 多团队并行开发、独立交付的大型项目
- 需要在应用间共享组件库/工具函数
- 全栈统一使用 Webpack 5 的组织
⚠️ 需要慎重考虑的场景:
- 遗留系统改造(老项目可能难以升级至 Webpack 5)
- 需要强沙箱隔离的安全敏感场景
- 对首屏加载性能极度敏感的场景(MF 有额外的网络请求开销)
学习路线建议
入门:理解三角色模型 + Hello World 示例
↓
掌握:shared 配置 + 异步入口 + 双向通信
↓
进阶:MF 2.0 动态加载 + 错误边界 + 性能优化
↓
实战:完整微前端项目 + 路由集成 + 状态共享 + 样式策略
↓
精通:源码级理解 MF Runtime + 自定义共享策略 + 监控体系思考题
-
沙箱缺失问题:Module Federation 并未提供 JavaScript 沙箱能力,这在什么场景下可能导致问题?有哪些补偿方案?
-
版本回退策略:当 Remote 应用的共享依赖版本高于 Host 时,
singleton: true会强制使用 Host 的低版本。这种"向下兼容"策略是否总是合理的?有没有更好的方案? -
失败降级:如果某个 Remote 应用完全不可用(服务器宕机),Host 应用应该如何优雅降级?请设计一套完整的故障转移方案。
-
构建缓存:在 Monorepo + MF 的项目中,如何利用 Webpack 的持久化缓存(filesystem cache)加速构建?有哪些需要注意的坑?
-
ESM 未来:随着浏览器原生 ESM 和 Import Maps 的普及,你认为 Module Federation 这类基于 Bundle 的方案会被取代吗?为什么?