微前端源码精讲
第9章 Module Federation 设计哲学
第9章 Module Federation 设计哲学
“微前端的终局不是更好的沙箱,而是让沙箱变得不再必要。”
本章要点
- 理解从”独立构建独立部署”到”运行时共享模块”的范式跃迁
- 掌握 Module Federation 四大核心概念:Host、Remote、Shared、Exposes
- 深入 remoteEntry.js 的加载机制与模块注册流程
- 理解 Shared 依赖的版本协商算法:singleton、requiredVersion、eager 的设计权衡
- 对比 Module Federation 1.0 与 2.0 的架构跃迁
- 厘清 Module Federation 与运行时加载方案(乾坤/single-spa)的本质区别
2020 年 10 月,Webpack 5 正式发布。在长达两年的开发周期中,Webpack 团队悄然引入了一个不起眼的新特性——ModuleFederationPlugin。发布说明中只有寥寥数行描述。没有铺天盖地的营销,没有”改变前端未来”的宣言。
但那行不起眼的代码,掀起了一场静默的革命。
在 Module Federation 之前,前端模块的共享边界是构建产物。无论你的代码拆得多细,最终都要打包成一个或多个 bundle,以完整的构建产物为单位进行部署和加载。乾坤加载的是一个完整的子应用 HTML,single-spa 加载的是一个完整的子应用入口 JS——粒度再小也是”应用”级别。
Module Federation 打破了这个边界。它让不同的构建产物之间可以在运行时共享任意粒度的模块——一个组件、一个函数、一个配置对象。就像 Node.js 的 require 可以引用任何本地模块一样,Module Federation 让你的浏览器端代码可以”require”另一个独立构建、独立部署的应用中的任何导出模块。
这不是渐进式改良。这是范式转换。
要理解这场转换的深意,我们需要从头开始——从微前端最基本的架构假设说起。
9.1 从”独立构建独立部署”到”运行时共享模块”
先用一个业内典型场景作为 Module Federation 的切入点。2019 年前后、基于乾坤/single-spa 的微前端架构在中大型项目上普遍遇到一个棘手问题:多个子应用重复打包同一批基础依赖。以一个拆成六个子应用的后台管理系统为例,每个子应用都独立打包 React(~130KB)、Ant Design(gzip 前 ~600KB 量级)、lodash(~70KB),用户访问主应用时浏览器要下载六份同版本的这些库,重复体积可达数 MB。社区当时主要用两类方案规避:把公共依赖外部化到 CDN、或用 webpack externals 在构建时剔除 peer 依赖——但这两种方案都要求所有子应用锁到同一个依赖版本,牺牲了”团队独立升级”这个微前端的初衷。
Tobias Koppers(webpack 作者)在 2020 年公开的 Module Federation 设计文档中给出了另一条路:把依赖协商从”构建时”挪到”运行时”——每个子应用不再锁定版本、而是声明自己能提供/能消费的版本区间,运行时协商出一个各方都可用的版本并只加载一份。这种从”构建时外部化”到”运行时共享”的思路转变,把微前端从”把大项目拆小”的防守型价值,扩展到了”独立团队在同一页面内共享运行时资源”的进攻型价值。
9.1.1 传统微前端的架构假设
乾坤和 single-spa 共享一个基本假设:每个子应用是一个独立的、完整的前端应用。 它有自己的 package.json,自己的构建流程,自己的入口文件,自己的路由系统。主应用通过某种机制(HTML Entry 或 JS Entry)在运行时加载这个完整的应用,然后将其挂载到页面上的某个 DOM 容器中。
// 乾坤的架构模型:以"应用"为加载单元
interface QiankunApp {
name: string;
entry: string; // 子应用的完整入口 URL
container: string; // 挂载的 DOM 节点
activeRule: string; // 路由匹配规则
}
// 注册子应用
registerMicroApps([
{
name: 'order-app',
entry: 'https://order.example.com', // 加载一个完整的应用
container: '#subapp-container',
activeRule: '/order',
},
{
name: 'product-app',
entry: 'https://product.example.com', // 又一个完整的应用
container: '#subapp-container',
activeRule: '/product',
},
]);
这个架构模型有三个隐含前提:
- 加载粒度 = 应用:你无法只加载订单应用中的
OrderList组件,必须加载整个订单应用 - 依赖各自独立:每个子应用自带全部依赖,React 可能被加载多次
- 隔离是必需的:因为多个完整应用共享同一个页面,必须通过沙箱防止相互污染
// 传统微前端的依赖加载示意
// 主应用
import React from 'react'; // React 实例 #1 (18.2.0)
import ReactDOM from 'react-dom';
// 订单子应用(独立构建)
import React from 'react'; // React 实例 #2 (18.2.0) —— 重复!
import ReactDOM from 'react-dom';
// 商品子应用(独立构建)
import React from 'react'; // React 实例 #3 (18.3.0) —— 又一份!
import ReactDOM from 'react-dom';
// 三份 React,三份 ReactDOM
// gzipped 后约 42KB × 3 = 126KB 的冗余
乾坤试图通过 externals 和全局变量约定来缓解这个问题——让所有子应用使用主应用加载的 React。但这种方案本质上是一种运行时约定:没有编译器参与、没有版本检查、没有类型安全。一旦某个子应用忘记配置 externals,或者需要一个不同版本的 React,整个约定就会崩溃。
9.1.2 Module Federation 的范式转换
Module Federation 的核心洞见可以用一句话概括:模块共享不应该是运行时的约定,而应该是编译时的契约。
// webpack.config.js - 订单应用
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
output: {
publicPath: 'https://order.example.com/',
},
plugins: [
new ModuleFederationPlugin({
name: 'orderApp',
filename: 'remoteEntry.js',
// 暴露模块:声明哪些模块可以被其他应用引用
exposes: {
'./OrderList': './src/components/OrderList',
'./OrderDetail': './src/components/OrderDetail',
'./useOrder': './src/hooks/useOrder',
},
// 共享依赖:声明哪些依赖应该在运行时共享
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
'react-router-dom': { requiredVersion: '^6.0.0' },
},
}),
],
};
// webpack.config.js - 商品应用(消费方)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
output: {
publicPath: 'https://product.example.com/',
},
plugins: [
new ModuleFederationPlugin({
name: 'productApp',
filename: 'remoteEntry.js',
// 远程模块:声明要从哪里引用模块
remotes: {
orderApp: 'orderApp@https://order.example.com/remoteEntry.js',
},
exposes: {
'./ProductCard': './src/components/ProductCard',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
现在,在商品应用的任何代码中,你可以这样写:
// 商品应用的某个页面
import OrderList from 'orderApp/OrderList';
import { useOrder } from 'orderApp/useOrder';
const ProductPage: React.FC = () => {
const { orders } = useOrder();
return (
<div>
<h1>商品详情</h1>
{/* 直接使用订单应用暴露的组件 */}
<OrderList orders={orders} />
</div>
);
};
注意这段代码——import OrderList from 'orderApp/OrderList'。在编译时,Webpack 知道 orderApp 是一个远程模块,它不会试图从本地 node_modules 中解析这个路径,而是生成一段运行时加载逻辑,在浏览器中动态获取订单应用暴露的 OrderList 模块。
这里发生了三个根本性的变化:
- 加载粒度从”应用”变为”模块”:你只加载需要的
OrderList组件,不需要加载整个订单应用 - 依赖从”各自独立”变为”协商共享”:React 只加载一份,版本由运行时协商决定
- 隔离从”必需”变为”不需要”:模块之间不存在全局变量污染的问题,因为它们通过正式的模块接口交互
9.1.3 一个类比:从集装箱运输到管道网络
传统微前端像集装箱运输——每个子应用是一个密封的集装箱,里面装满了它需要的所有东西(包括重复的公共依赖)。主应用是港口,负责接收和卸载集装箱。为了防止集装箱之间的货物相互污染,你需要一套复杂的仓储隔离系统(沙箱)。
Module Federation 像管道网络——每个应用是一个节点,它们通过管道(模块接口)连接。任何节点可以向网络中输出特定的产品(exposes),也可以从网络中获取需要的产品(remotes)。公共的基础设施(shared dependencies)只在网络中存在一份,所有节点共用。不需要隔离,因为管道本身就是清晰的边界。
// 架构模型对比
interface TraditionalMicroFE {
unit: 'application'; // 加载单位:应用
sharing: 'convention'; // 共享方式:约定(externals)
isolation: 'required'; // 隔离:必需(沙箱)
communication: 'event-bus'; // 通信:事件总线
overhead: 'high'; // 额外开销:高
}
interface ModuleFederation {
unit: 'module'; // 加载单位:模块
sharing: 'negotiated'; // 共享方式:协商(版本对齐)
isolation: 'unnecessary'; // 隔离:不需要
communication: 'import'; // 通信:标准 import/export
overhead: 'minimal'; // 额外开销:极低
}
深度洞察:为什么”不需要沙箱”是一个巨大的进步
沙箱不是免费的。乾坤的
ProxySandbox在每次全局变量访问时都要经过 Proxy 拦截,这在高频调用场景下会产生可测量的性能开销。更重要的是,沙箱的存在本身就说明了一个问题:你的模块边界不清晰。 如果两个模块需要一堵墙来防止相互污染,说明它们的交互方式出了问题——依赖全局变量、修改全局 CSS、覆盖原型链方法。Module Federation 的模块通过标准的import/export交互,就像你在同一个项目中引用不同文件一样自然。这不是”隔离得更好”,而是”从根本上消除了需要隔离的场景”。
9.2 核心概念:Host、Remote、Shared、Exposes
Module Federation 的四个核心概念、刻意借用了电信和计算机网络领域已有的术语——Host、Remote、Shared——这种术语选择本身就传递了一个信号:MF 作者看待”模块”的心智模型、和网络工程师看待”节点”的心智模型、是同构的。在网络世界里、每台机器既可以当 host(主动发起请求)、也可以当 remote(提供服务);机器之间通过共享协议(shared protocol)通信;每台机器暴露一组端点(exposes endpoints)。MF 只是把这套网络化思维、带到了前端模块世界——这种”跨领域借鉴成熟思维”的设计能力、是一流工程师和普通工程师的分水岭——一流工程师能看到”不同领域里的同构问题”、把一个领域的成熟解法迁移到另一个领域;普通工程师只看到眼前问题、不知道这个问题在别的领域早就被解决过。
9.2.1 四个核心角色
Module Federation 的架构围绕四个核心概念展开。要理解它们,最好的方式是跟踪一个模块从被暴露到被消费的完整生命周期。
- Host(宿主):主动发起模块请求的一方,通过
remotes字段声明依赖的远程应用,运行时加载remoteEntry.js并获取模块 - Remote(远程):提供模块的一方,通过
exposes字段声明可用的模块,构建时生成remoteEntry.js作为模块注册清单 - Shared(共享依赖):避免依赖重复加载的协商机制,通过
shared字段声明哪些依赖参与共享,运行时根据版本规则决定使用哪个版本 - Exposes(暴露模块):定义 Remote 的公共 API,可以是组件、函数、常量、类型——任何 JS 模块
一个应用可以同时扮演 Host 和 Remote 两个角色——它既消费别人的模块,也暴露自己的模块。这种双向关系形成了一个去中心化的模块联邦网络。
下图展示了 Module Federation 的去中心化网络架构,每个节点可以同时是模块的提供者和消费者:
flowchart TB
subgraph AppA["应用 A (Host + Remote)"]
A_Expose["exposes:\n./SharedLayout\n./AuthContext"]
A_Remote["remotes:\nappB"]
A_Shared["shared:\nreact, react-dom"]
end
subgraph AppB["应用 B (Remote)"]
B_Expose["exposes:\n./DataGrid\n./useDataFetch"]
B_Shared["shared:\nreact, react-dom"]
end
subgraph AppC["应用 C (Host)"]
C_Remote["remotes:\nappA"]
C_Shared["shared:\nreact, react-dom"]
end
subgraph SharedScope["运行时 Shared Scope"]
React["react@18.2.0\n(singleton)"]
ReactDOM["react-dom@18.2.0\n(singleton)"]
end
AppA -->|"加载 remoteEntry.js\n消费 DataGrid"| AppB
AppC -->|"加载 remoteEntry.js\n消费 SharedLayout"| AppA
AppA --- SharedScope
AppB --- SharedScope
AppC --- SharedScope
style SharedScope fill:#e8f5e9,stroke:#2e7d32
style AppA fill:#e3f2fd,stroke:#1565c0
style AppB fill:#fff3e0,stroke:#e65100
style AppC fill:#f3e5f5,stroke:#7b1fa2
// 应用 A:既是 Host(消费 B 的模块),也是 Remote(暴露模块给 C)
// webpack.config.js
new ModuleFederationPlugin({
name: 'appA',
filename: 'remoteEntry.js',
remotes: {
appB: 'appB@https://b.example.com/remoteEntry.js',
},
exposes: {
'./SharedLayout': './src/components/SharedLayout',
'./AuthContext': './src/contexts/AuthContext',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
});
// 应用 B:Remote,暴露模块
new ModuleFederationPlugin({
name: 'appB',
filename: 'remoteEntry.js',
exposes: {
'./DataGrid': './src/components/DataGrid',
'./useDataFetch': './src/hooks/useDataFetch',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
});
// 应用 C:Host,消费 A 暴露的模块
new ModuleFederationPlugin({
name: 'appC',
filename: 'remoteEntry.js',
remotes: {
appA: 'appA@https://a.example.com/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
},
});
9.2.2 remoteEntry.js:模块联邦的入口契约
remoteEntry.js 是 Module Federation 里最关键的一个文件——它是”模块联邦的握手协议载体”。当一个 Host 想要使用某个 Remote 的模块时、第一件事就是加载这个文件——里面包含了”这个 Remote 暴露了哪些模块、需要哪些共享依赖、这些共享依赖支持什么版本”这些信息**。这种”用一个入口文件承载元数据”的设计、在软件工程里有很长的谱系——npm 的 package.json、Java 的 MANIFEST.MF、OSGi 的 Bundle Manifest、Kubernetes 的 PodSpec、Docker 的 Dockerfile——这些文件都是”告诉外部系统、如何理解和使用这个组件”的契约载体。Module Federation 的 remoteEntry.js、只是把这种古老的思想、应用到了 JavaScript 运行时动态加载的场景。理解了这个谱系、你就不会把 remoteEntry.js 当成一个”神秘的黑盒”、而会把它看成”一份自描述的模块元数据文档”——它的每一个字段都有清晰的用途、每一个约定都能被独立推理。
下图展示了 Host 应用加载远程模块的完整时序,从 remoteEntry.js 加载到模块实例化:
sequenceDiagram
participant Host as Host 应用
participant Script as script 标签加载
participant RE as remoteEntry.js
participant SS as Shared Scope
participant Chunk as 远程 Chunk
Host->>Script: 动态创建 script 加载 remoteEntry.js
Script-->>RE: 全局变量注册 (var remoteApp = ...)
Host->>RE: remoteApp.init(sharedScope)
RE->>SS: 注册共享依赖到 __webpack_require__.S
SS-->>RE: 版本协商完成
Host->>RE: remoteApp.get('./OrderList')
RE->>RE: 查找 moduleMap['./OrderList']
RE->>Chunk: __webpack_require__.e() 按需加载 Chunk
Chunk-->>RE: Chunk 加载完成
RE->>RE: __webpack_require__(moduleId)
RE-->>Host: 返回模块工厂 () => module
Host->>Host: const OrderList = factory().default
Note over Host: 像本地模块一样使用远程组件
remoteEntry.js 是 Module Federation 架构中最关键的产物。它不包含任何业务逻辑——它是一个模块注册清单,告诉消费方”我有哪些模块可用,以及如何加载它们”。
当 Webpack 构建一个配置了 exposes 的应用时,会生成这个文件。让我们深入分析它的结构:
// remoteEntry.js 的简化结构(真实产物经过 Webpack 编译,这里展示核心逻辑)
var orderApp;
// 全局注册:将自己注册到全局作用域
(function () {
// moduleMap:暴露模块的映射表
var moduleMap = {
'./OrderList': function () {
// 返回一个 Promise,懒加载对应的 chunk
return import('./src_components_OrderList_tsx.js');
},
'./OrderDetail': function () {
return import('./src_components_OrderDetail_tsx.js');
},
'./useOrder': function () {
return import('./src_hooks_useOrder_ts.js');
},
};
// get 方法:Host 通过这个方法获取指定模块
var get = function (module) {
return moduleMap[module]
? moduleMap[module]()
: Promise.reject(new Error('Module "' + module + '" does not exist'));
};
// init 方法:初始化共享作用域
var init = function (shareScope) {
// 将 Host 的共享依赖注册到 Remote 的共享作用域
// 这是版本协商的关键步骤
initSharedScope(shareScope);
};
// 注册到全局
orderApp = {
get: get,
init: init,
};
})();
关键在于两个方法:
init(shareScope):接收 Host 传入的共享作用域(包含所有 shared 依赖的版本信息和工厂函数),完成依赖版本的协商get(moduleName):根据模块名返回一个 Promise,resolve 为模块的实际内容
9.2.3 模块加载的完整流程
让我们追踪一个完整的模块加载过程——当商品应用(Host)引用订单应用(Remote)的 OrderList 组件时,发生了什么:
// 第 1 步:Host 的 Webpack 运行时加载 remoteEntry.js
// 当代码中出现 import('orderApp/OrderList') 时
// Webpack 运行时首先检查:orderApp 的 remoteEntry.js 是否已加载?
async function loadRemoteEntry(remoteName: string, url: string): Promise<void> {
// 创建 <script> 标签加载 remoteEntry.js
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url; // https://order.example.com/remoteEntry.js
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// 第 2 步:初始化共享作用域
async function initRemote(remoteName: string): Promise<void> {
const container = window[remoteName]; // 获取全局注册的 Remote 容器
// 确保共享作用域已初始化
if (!__webpack_share_scopes__['default']) {
__webpack_init_sharing__('default');
}
// 将 Host 的共享作用域传递给 Remote
// 这一步完成版本协商
await container.init(__webpack_share_scopes__['default']);
}
// 第 3 步:获取具体模块
async function loadRemoteModule(remoteName: string, moduleName: string): Promise<any> {
const container = window[remoteName];
// 调用 Remote 的 get 方法,获取模块工厂函数
const factory = await container.get(moduleName);
// 执行工厂函数,获取模块导出
const module = factory();
return module;
}
// 完整流程
async function resolveRemoteModule(): Promise<React.ComponentType> {
// 1. 加载 remoteEntry.js(如果尚未加载)
await loadRemoteEntry('orderApp', 'https://order.example.com/remoteEntry.js');
// 2. 初始化共享作用域(版本协商)
await initRemote('orderApp');
// 3. 获取模块
const module = await loadRemoteModule('orderApp', './OrderList');
return module.default; // React 组件
}
整个过程可以归纳为六步:1)Host 通过 <script> 加载 remoteEntry.js;2)调用 container.init() 传递共享作用域;3)调用 container.get('./OrderList') 请求模块;4)Remote 返回模块工厂函数;5)工厂函数触发动态 import() 加载对应的 chunk 文件;6)返回模块导出(React 组件)。
9.2.4 Shared 依赖的版本协商机制
“版本协商”是 MF 最复杂、也是最容易让人头晕的部分——因为它牵涉到一个根本性的矛盾:我们希望多个应用”共享一份依赖”(减少加载量)、但每个应用可能又”需要不同版本”(依赖演化自由)。这个矛盾无法完美解决、只能在”强制统一”和”各自使用”之间做工程取舍。**MF 的做法是——通过 singleton、requiredVersion、strictVersion 三个配置、让开发者自己决定这个取舍应该偏向哪一边——singleton 偏向”强制统一”、一个版本赢者通吃;requiredVersion 偏向”语义版本兼容”、允许小版本差异;strictVersion 偏向”严格匹配”、不兼容直接报错。**这种”把取舍决策权交还给使用者”的设计、是一流框架的共同特征——不要假设自己比使用者更懂他们的业务、而是提供清晰的 knob 让使用者根据自己的场景调节。npm 和 yarn 的 resolutions、Rust 的 Cargo.lock 解析策略、都是同一种”把复杂度暴露给使用者”的设计哲学——看似增加了学习成本、实际上赋予了使用者真正的控制力。
下图展示了 Shared 依赖版本协商的决策流程:
flowchart TB
Start["Host 和 Remote 都声明了\nshared: react"] --> InitScope["Host 调用 container.init(sharedScope)\n注册自身的 react 版本信息"]
InitScope --> Check{"共享作用域中是否\n已有 react?"}
Check -->|"否"| Register["注册当前版本\nreact@18.2.0"]
Check -->|"是"| Singleton{"singleton: true?"}
Singleton -->|"是"| CompareVersion{"比较版本号"}
Singleton -->|"否"| BothLoad["两个版本共存\n各用各的"]
CompareVersion --> Higher{"当前版本更高?"}
Higher -->|"是"| UseNew["使用更高版本\n(满足 semver 兼容)"]
Higher -->|"否"| KeepOld["保留已有版本"]
UseNew --> SatisfyCheck{"满足 requiredVersion?"}
KeepOld --> SatisfyCheck
SatisfyCheck -->|"是"| Done["协商完成,使用该版本"]
SatisfyCheck -->|"否, strictVersion=false"| Warn["console.warn 版本警告\n仍然使用"]
SatisfyCheck -->|"否, strictVersion=true"| Error["抛出运行时错误"]
style Start fill:#e3f2fd,stroke:#1565c0
style Done fill:#e8f5e9,stroke:#2e7d32
style Error fill:#ffebee,stroke:#c62828
Shared 依赖的版本协商是 Module Federation 中最精妙的设计之一。它解决了一个看似简单但实际极其棘手的问题:当 Host 和 Remote 依赖了同一个库的不同版本,应该使用哪一个?
三个关键配置项控制着这个行为:
// webpack.config.js
new ModuleFederationPlugin({
shared: {
react: {
// singleton: 整个页面只允许存在一个实例
// React 必须是 singleton,因为多个 React 实例会导致 Hooks 失效
singleton: true,
// requiredVersion: 声明这个应用需要的版本范围
// 遵循 semver 规范
requiredVersion: '^18.2.0',
// eager: 是否在初始 bundle 中包含这个依赖
// false(默认):异步加载,减小初始 bundle 体积
// true:同步包含,避免异步加载的时序问题
eager: false,
// strictVersion: 版本不兼容时是否抛出错误
// false(默认):打印警告
// true:抛出运行时错误
strictVersion: false,
},
// 简写形式:只指定包名,所有配置使用默认值
lodash: {},
// 带版本的简写
axios: { requiredVersion: '^1.6.0' },
},
});
版本协商的具体过程如下:
// 版本协商算法的简化实现
interface SharedModuleInfo {
version: string; // 实际版本
factory: () => Promise<Module>; // 模块工厂函数
eager: boolean; // 是否同步加载
singleton: boolean; // 是否单例
requiredVersion: string; // 要求的版本范围
strictVersion: boolean; // 是否严格版本检查
}
// 共享作用域:存储所有应用注册的共享依赖信息
type ShareScope = Record<string, Record<string, SharedModuleInfo>>;
function resolveSharedModule(
shareScope: ShareScope,
packageName: string,
requiredVersion: string,
singleton: boolean,
strictVersion: boolean
): SharedModuleInfo {
const versions = shareScope[packageName];
// versions 是一个以版本号为 key 的映射
// 例如:{ '18.2.0': {...}, '18.3.1': {...} }
if (singleton) {
// 单例模式:选择已注册的最高版本
const highestVersion = findHighestVersion(versions);
const selected = versions[highestVersion];
// 检查版本兼容性
if (!satisfies(highestVersion, requiredVersion)) {
if (strictVersion) {
throw new Error(
`Unsatisfied version ${highestVersion} of shared ` +
`singleton module ${packageName} ` +
`(required ${requiredVersion})`
);
} else {
console.warn(
`Unsatisfied version ${highestVersion} from ${packageName}. ` +
`Required: ${requiredVersion}`
);
}
}
return selected;
}
// 非单例模式:选择满足版本要求的最高版本
const compatibleVersions = Object.keys(versions)
.filter(v => satisfies(v, requiredVersion))
.sort(compareVersions);
if (compatibleVersions.length > 0) {
return versions[compatibleVersions[compatibleVersions.length - 1]];
}
// 没有兼容版本,使用本地 fallback
return localFallback(packageName);
}
用一个具体场景来说明:
// 场景:三个应用共享 React
// App A: react@18.2.0, singleton: true, requiredVersion: '^18.0.0'
// App B: react@18.3.1, singleton: true, requiredVersion: '^18.2.0'
// App C: react@17.0.2, singleton: true, requiredVersion: '^17.0.0'
// 协商结果:
// 1. singleton = true → 只允许一个 React 实例
// 2. 可用版本:18.2.0, 18.3.1, 17.0.2
// 3. 选择最高版本:18.3.1
// 4. 兼容性检查:
// - App A (^18.0.0) ← 18.3.1 ✓ 满足
// - App B (^18.2.0) ← 18.3.1 ✓ 满足
// - App C (^17.0.0) ← 18.3.1 ✗ 不满足!
// → strictVersion = false → 打印警告
// → strictVersion = true → 抛出错误
// ⚠️ 这就是为什么在 Module Federation 中
// 所有应用的主要依赖版本必须大致对齐
9.2.5 eager 的设计权衡
在框架的众多配置里、eager 这种”一个 boolean 却背后藏着深刻权衡”的配置是最值得重点学习的——因为它们往往比那些”看似复杂其实只是有更多字段”的配置、蕴含更多设计智慧。eager 配置的表象是”这个模块是立即加载还是延迟加载”、它的深层是”我们要怎么在启动性能和运行时确定性之间做取舍”。启动时同步加载的好处是”代码执行到哪里都知道模块已经就绪”、坏处是”首屏下载量增大”;运行时异步加载的好处是”首屏轻量”、坏处是”必须等模块加载完才能执行”。MF 不强求开发者选一边、而是让开发者根据”这个模块有多重要、首屏能不能少了它”来自己决定。这种”暴露核心权衡、让使用者决定”的设计哲学、比”我们帮你决定了”要尊重开发者、也更能适配多样化场景。好的框架是”提供强大的工具 + 清晰的文档 + 合理的默认值”、让使用者能快速上手但又可以随着理解加深做更精细的调节——而不是把所有决策都封装到黑盒里让使用者失去控制力。MF 的整套配置体系、都体现了这种尊重使用者智力的设计姿态。
eager 看似是一个简单的布尔配置,但它背后反映了 Module Federation 设计中一个深层的张力:初始加载性能 vs 运行时确定性。
// eager: false(默认)
// 共享模块在运行时异步加载
shared: {
react: {
singleton: true,
eager: false, // React 不包含在初始 bundle 中
},
}
// 好处:初始 bundle 更小
// 风险:如果应用入口处同步使用了 React,会报错
// 因为 React 还没被加载
// eager: true
// 共享模块包含在初始 bundle 中
shared: {
react: {
singleton: true,
eager: true, // React 包含在初始 bundle 中
},
}
// 好处:无异步加载时序问题
// 代价:如果 Host 已经加载了 React,这份 eager 的 React 就浪费了
在实践中,通常只有入口应用(最先加载的 Host)需要设置 eager: true,其他 Remote 应用设置 eager: false 以复用 Host 提供的共享模块。
// 推荐的配置模式
// Host 应用(最先加载)
new ModuleFederationPlugin({
name: 'shell',
shared: {
react: { singleton: true, eager: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, eager: true, requiredVersion: '^18.0.0' },
},
});
// Remote 应用
new ModuleFederationPlugin({
name: 'orderApp',
shared: {
react: { singleton: true, eager: false, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, eager: false, requiredVersion: '^18.0.0' },
},
});
但这里有一个常见的陷阱——入口应用必须使用异步引导模式:
// ❌ 错误:同步入口,shared 模块还没初始化
// index.ts
import React from 'react'; // 此时 shared scope 未就绪
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
// ✅ 正确:异步引导
// index.ts(bootstrap 入口)
import('./bootstrap');
// bootstrap.ts(真正的入口)
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<App />);
// 为什么?因为那个 import('./bootstrap') 创建了一个异步边界
// Webpack 运行时利用这个边界来完成 shared scope 的初始化
// 所有 shared 模块的加载和版本协商都在这个异步间隙中完成
深度洞察:异步边界是 Module Federation 的”隐性契约”
很多开发者在初次使用 Module Federation 时都会踩到同步入口的坑。这不是一个 bug,而是一个有意为之的设计决策。异步边界给了 Webpack 运行时一个”喘息”的机会——在业务代码执行之前,完成所有远程模块的发现、共享依赖的协商、模块工厂的注册。这和浏览器的
DOMContentLoaded类似——你需要一个事件来标记”准备就绪”。Module Federation 用import()的 Promise 来实现了同样的效果。理解这一点,你就理解了 Module Federation 为什么要求入口是异步的——不是技术限制,是架构必然。
9.3 Module Federation 1.0 → 2.0 的架构跃迁
任何一个”开创性但不完美”的技术、都会经历一次或多次重大重构——MF 1.0 到 2.0、就是这种重构的一个典型案例。MF 1.0 作为 Webpack 5 的内置特性、在 2020 年问世时引起了前端圈的巨大轰动——但随着越来越多团队在生产环境真正落地、一些设计局限逐渐暴露:和 Webpack 深度耦合、类型系统缺失、运行时行为难以扩展。这些问题不是 MF 1.0 的”bug”、而是”初版设计没有充分考虑到的场景”——任何开创性技术都难免有这种局限。MF 2.0 的意义、不在于它加了什么新功能、而在于它敢于把 1.0 的核心假设重新审视一遍——把运行时从 Webpack 中剥离、独立为一个可被任何构建工具使用的库;引入 Manifest 规范让不同构建工具的产物能互相识别;设计插件系统让扩展不再需要改核心代码。这种”敢于重构自己”的勇气、是一个技术能够持续进化的关键——如果 MF 团队满足于 1.0 的成就、整个生态可能就会停滞;正是因为他们愿意承担重构的成本、MF 才能从”Webpack 内置特性”升级为”跨构建工具的通用基础设施”。
9.3.1 MF 1.0 的三个硬伤
Module Federation 1.0(Webpack 5 内置版本)虽然开创了模块共享的范式,但在生产环境中暴露出了三个显著的局限性:
硬伤一:构建工具绑定
MF 1.0 是 Webpack 5 的内置特性。这意味着如果你的项目使用 Vite、Rspack 或任何其他构建工具,你无法使用 Module Federation。在 2024-2025 年 Vite 快速崛起的背景下,这成了一个越来越大的限制。
// MF 1.0 的架构约束
interface MF1Limitation {
buildTool: 'Webpack 5 only'; // 构建工具绑定
runtime: 'Webpack runtime'; // 运行时与 Webpack 耦合
protocol: 'implicit'; // 没有显式的模块协议
}
// 如果团队 A 用 Webpack,团队 B 用 Vite
// 在 MF 1.0 下,它们无法建立联邦关系
// 这与 Module Federation "让不同团队独立选择技术栈"的愿景相矛盾
硬伤二:类型安全缺失
当你写 import OrderList from 'orderApp/OrderList' 时,TypeScript 完全不知道这个模块的类型。你需要手动编写类型声明文件,而且每次 Remote 的接口变更,Host 的类型声明都需要同步更新。
// MF 1.0 下的类型声明:手动维护,容易过时
// types/orderApp.d.ts
declare module 'orderApp/OrderList' {
import { FC } from 'react';
interface OrderListProps {
orders: Array<{ id: string; total: number }>;
onSelect?: (orderId: string) => void;
}
const OrderList: FC<OrderListProps>;
export default OrderList;
}
// 问题:如果订单团队给 OrderListProps 加了一个 filterBy 属性
// 商品团队的类型声明不会自动更新
// TypeScript 不会报错——但运行时可能出问题
硬伤三:版本管理薄弱
MF 1.0 没有内建的模块版本管理机制。当 Remote 应用更新了暴露的模块接口(比如修改了组件的 props),所有消费方的代码可能在运行时崩溃——没有任何编译时或部署时的检查。
// MF 1.0 的版本问题
// v1: OrderList 接受 { orders: Order[] }
// v2: OrderList 接受 { orders: Order[], pageSize: number }(pageSize 必填)
// Host 仍然按 v1 的方式使用:
<OrderList orders={orders} />
// 运行时:TypeError: Cannot read property 'xxx' of undefined
// 没有任何提前预警
9.3.2 MF 2.0 的架构革新
Module Federation 2.0(由 Zack Jackson 主导的独立项目)针对上述硬伤进行了系统性的架构升级。核心理念从”Webpack 的内置特性”转变为”跨构建工具的模块共享基础设施”。
MF 2.0 的核心架构升级体现在三个方面:独立运行时(不绑定构建工具)、跨构建工具支持(Webpack/Rspack/Vite)、自动类型生成与基于 Manifest 的版本管理。
革新一:独立运行时
MF 2.0 将模块联邦的运行时从 Webpack 中抽离为一个独立的包 @module-federation/runtime。这个运行时可以在任何构建工具的产物中运行。
// MF 2.0:使用独立运行时
import { init, loadRemote } from '@module-federation/runtime';
// 初始化联邦运行时
init({
name: 'productApp',
remotes: [
{
name: 'orderApp',
entry: 'https://order.example.com/remoteEntry.js',
},
],
shared: {
react: {
version: '18.3.1',
lib: () => require('react'),
shareConfig: { singleton: true, requiredVersion: '^18.0.0' },
},
},
});
// 动态加载远程模块
const OrderList = await loadRemote('orderApp/OrderList');
// MF 2.0 的 Rspack 配置
// rspack.config.js
const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'orderApp',
filename: 'remoteEntry.js',
exposes: {
'./OrderList': './src/components/OrderList',
},
shared: {
react: { singleton: true },
},
}),
],
};
// 注意:API 与 Webpack 版本几乎完全一致
// 但底层运行时是独立的 @module-federation/runtime
革新二:Manifest 与类型同步
MF 2.0 引入了 manifest.json——一个描述 Remote 应用所有暴露模块及其元信息的清单文件。
{
"id": "orderApp",
"name": "orderApp",
"metaData": {
"name": "orderApp",
"buildInfo": {
"buildVersion": "1.2.3",
"buildTime": "2026-03-15T10:30:00Z"
},
"remoteEntry": {
"name": "remoteEntry.js",
"path": "https://order.example.com/remoteEntry.js"
},
"types": {
"path": "https://order.example.com/@mf-types.zip",
"name": "@mf-types.zip"
}
},
"shared": [
{
"sharedName": "react",
"version": "18.3.1",
"singleton": true
}
],
"exposes": [
{
"path": "./OrderList",
"name": "OrderList",
"assets": {
"js": ["static/js/OrderList.chunk.js"],
"css": ["static/css/OrderList.chunk.css"]
}
}
]
}
这个 Manifest 有三重作用:
- 资源发现:Host 通过 Manifest 知道 Remote 有哪些模块可用,以及它们对应的 chunk 文件
- 类型同步:
types.path指向自动生成的类型定义文件,Host 的构建流程可以自动下载并应用 - 版本追踪:
buildInfo.buildVersion提供了显式的版本信息
// MF 2.0 的类型同步流程
// 1. Remote 构建时,自动生成类型声明并打包为 @mf-types.zip
// 2. Host 构建时,自动下载并解压到 @mf-types/ 目录
// 3. TypeScript 通过 paths 配置自动解析
// tsconfig.json(Host 端,自动配置)
{
"compilerOptions": {
"paths": {
"orderApp/*": ["./@mf-types/orderApp/*"]
}
}
}
// 现在 TypeScript 自动知道 OrderList 的类型
import OrderList from 'orderApp/OrderList';
// ✓ 自动补全
// ✓ 类型检查
// ✓ 接口变更时编译报错
革新三:运行时插件系统
MF 2.0 引入了一个强大的插件系统,允许在模块加载的各个阶段插入自定义逻辑。
import { init, type FederationRuntimePlugin } from '@module-federation/runtime';
// 自定义插件:添加模块加载监控
const monitorPlugin: () => FederationRuntimePlugin = () => ({
name: 'monitor-plugin',
// 在 remoteEntry 加载前
beforeRequest(args) {
console.log(`[MF] Loading remote: ${args.id}`);
performance.mark(`mf-load-start:${args.id}`);
return args;
},
// 在模块加载完成后
afterResolve(args) {
performance.mark(`mf-load-end:${args.id}`);
performance.measure(
`mf-load:${args.id}`,
`mf-load-start:${args.id}`,
`mf-load-end:${args.id}`
);
return args;
},
// 加载错误时降级处理
errorLoadRemote(args) {
console.error(`[MF] Failed to load: ${args.id}`, args.error);
// 返回一个降级组件
return {
default: () => <div>模块加载失败,请刷新重试</div>,
};
},
});
init({
name: 'productApp',
remotes: [
{ name: 'orderApp', entry: 'https://order.example.com/remoteEntry.js' },
],
plugins: [monitorPlugin()],
});
9.3.3 MF 1.0 与 2.0 的对比总结
| 维度 | MF 1.0 | MF 2.0 |
|---|---|---|
| 运行时 | 嵌入 Webpack 运行时 | 独立运行时 @module-federation/runtime |
| 构建工具 | 仅 Webpack 5 | Webpack / Rspack / Vite |
| 类型安全 | 手动声明 .d.ts | 自动生成并同步类型 |
| 版本管理 | 仅 shared 版本协商 | Manifest + 构建版本号 |
| 错误处理 | 基础的 Promise reject | 插件式错误处理 + 降级策略 |
| 开发体验 | 无专用工具 | Chrome DevTools 扩展 + CLI 工具 |
| 动态加载 | remotes 编译时固定 | loadRemote() 支持完全动态化 |
| 部署 | URL 硬编码在构建产物中 | Manifest + 运行时配置 |
深度洞察:从”构建工具特性”到”模块共享协议”
MF 1.0 到 2.0 的跃迁,本质上是从”一个构建工具的特性”升级为”一个模块共享协议”。MF 1.0 的
remoteEntry.js格式、init/get接口、共享作用域结构都是 Webpack 的内部实现。MF 2.0 将这些内部实现标准化为一套跨构建工具的协议。这就像 HTTP 从一个 CERN 的内部协议演变为互联网的基础设施一样——当一个机制从”某个工具的特性”升级为”多个工具共同遵守的协议”时,它的网络效应和生态价值会发生指数级跃迁。 这就是为什么 MF 2.0 的意义远超过一次版本升级——它标志着模块联邦从 Webpack 生态的专属方案,升级为前端生态的基础设施。
9.3.4 横向对标:MF vs SystemJS vs Bit vs iframe vs npm
很多读者对 MF 的位置感到模糊——它既像动态加载器(SystemJS)、又像组件分发平台(Bit)、又像运行时外挂(iframe)、又像包管理器(npm)。把这五条赛道摊开来看、才能真正理解 MF 到底占据了哪块生态位。下面这张表以 2025 Q1 各方案生产版本(SystemJS 6.15、Bit 1.10、MF 2.0)为基准做横向对比:
| 维度 | npm / yarn | SystemJS | iframe | Bit | Module Federation 2.0 |
|---|---|---|---|---|---|
| 共享时机 | 构建前(node_modules) | 运行时(纯动态) | 运行时(子页面) | 构建前(依赖平台) | 运行时(编译时声明) |
| 共享粒度 | 整个 npm 包 | 任意 ESM/AMD 模块 | 整个页面 | 组件/模块 | 任意 ESM 模块 |
| 版本协商 | 构建时 semver 解析 | 无,按 URL 区分 | 无(各自独立) | 构建时 Bit 平台负责 | 运行时 shared scope 协商 |
| 依赖去重 | hoisting / PnP | 需手动 importmap 配置 | 零去重(重复加载) | 平台侧 tree-shake | singleton + requiredVersion |
| 类型同步 | .d.ts 随包发布 | 无原生支持 | 完全不共享 | 自动生成并同步 | @mf-types.zip + Manifest |
| 隔离强度 | 无(同一进程) | 无(同一全局) | 最强(跨 Document) | 无(同一进程) | 无(同一全局) |
| 部署独立性 | ✗ 消费方需重新构建 | ✓ URL 换新即生效 | ✓ 子页面独立部署 | ✓ 通过 Bit 平台热更新 | ✓ remoteEntry 换新即生效 |
| 体积开销 | 0(构建进产物) | ~3KB 运行时 | 每个 iframe 全量 | 依赖 Bit runtime | ~5–15KB 运行时 |
| 典型代表 | 所有 npm 包 | single-spa 配套 | 网易云音乐/淘宝天猫 | bit.dev、Lerna Next | Shopify、BytePlus、字节内部中台 |
五条赛道的核心差异可以用一句话概括:npm 是”构建期静态链接”,SystemJS 是”运行期纯动态加载但无协商”,iframe 是”运行期强隔离但零共享”,Bit 是”平台化组件分发”,MF 是”编译期声明 + 运行期协商”。 MF 的独特位置在于——它是唯一同时解决”独立部署”+“依赖去重”+“类型同步”三角问题的方案。SystemJS 能独立部署但不去重(importmap 需手动维护);npm 去重但不能独立部署(改一行要全链路发版);iframe 独立部署但零共享(每个 React 都要加载一份);Bit 三者都能做但要求绑定 Bit 平台。
与本书第 11 章将详细拆解的 Rspack + MF2 集成对应——那一章实测 Rspack 0.7+ 接入 @module-federation/enhanced/rspack 后、remoteEntry 构建耗时比 Webpack 5 快约 6–8 倍(同一业务,Webpack 5 平均 42s → Rspack 0.7 平均 6s 左右,2026-02 实测),这正是 MF 2.0 “独立运行时 + 跨构建工具”设计带来的直接红利——如果 MF 还像 1.0 那样绑死 Webpack、Rspack 就必须重新实现一整套 federation 运行时、而不是直接复用 @module-federation/runtime。这种”协议从工具中独立出来”的设计决策、直接打开了整个构建工具生态(Rspack、Rsbuild、Vite、esbuild)接入 MF 的通道。
9.4 与运行时加载方案(乾坤/single-spa)的本质区别
这一节是本章的”压轴戏”、也是最容易被误解的部分。网上有太多”MF 取代了乾坤”或”乾坤要被 MF 淘汰”这样的二元对立言论——这些说法大多出自没有真正在生产环境同时用过两种方案的人。真实的情况要微妙得多——两种方案在问题域上是”垂直的、互补的”、不是”水平的、替代的”。如果你只看功能列表、会觉得它们有重叠——都能”加载远程代码”、都能”共享依赖”——但如果你看使用场景、就会发现差异巨大。MF 最擅长的场景是”一个大前端系统内部的模块共享”——比如一个金融 SaaS 把”图表组件库”抽出来作为 Remote、让所有前端团队共享;乾坤最擅长的场景是”老系统 + 新系统共存的渐进式迁移”——比如一个老 jQuery 后台 + 一个新 React 后台共用同一个外壳。用错了方案、不是”用不了”、而是”用得很难受”——就像用锤子去拧螺丝、不是不能拧、是事倍功半。
9.4.1 问题域的根本差异
乾坤/single-spa 和 Module Federation 经常被放在一起比较。但如果你深入理解了两者的设计哲学,你会发现它们解决的不是同一个问题。
- 乾坤/single-spa 的核心问题:如何在一个页面中运行多个独立应用?解法:运行时加载 + 沙箱隔离 + 生命周期管理
- Module Federation 的核心问题:如何在不同构建产物之间共享模块?解法:编译时声明 + 运行时协商 + 模块级加载
乾坤的核心关注点是应用的运行时隔离:如何让子应用 A 的全局变量不会影响子应用 B?如何让子应用 A 的 CSS 不会泄漏到子应用 B?如何在子应用切换时清理副作用?
Module Federation 的核心关注点是模块的跨构建共享:如何让构建产物 A 在运行时引用构建产物 B 的某个模块?如何在不同版本的依赖之间进行协商?如何最小化重复加载?
这两个问题域有交集——两者都涉及”多个独立部署的前端代码在同一个页面中协作”。但出发点和解法路径完全不同。
9.4.2 五个维度的对比
维度一:加载粒度
// 乾坤:加载粒度 = 应用
// 加载订单子应用 → 获取整个应用的 HTML → 解析 JS/CSS → 创建沙箱 → 挂载
registerMicroApps([{
name: 'order-app',
entry: 'https://order.example.com', // 一个完整的应用
container: '#container',
activeRule: '/order',
}]);
// Module Federation:加载粒度 = 模块
// 只加载需要的 OrderList 组件
const OrderList = React.lazy(() => import('orderApp/OrderList'));
// 不需要加载订单应用的路由、布局、其他页面
维度二:隔离策略
// 乾坤:运行时沙箱隔离
// 每个子应用运行在独立的 Proxy 沙箱中
// 全局变量、定时器、事件监听器都被拦截和隔离
const sandbox = new ProxySandbox('order-app');
// window.xxx → sandbox.proxy.xxx
// 性能开销:每次全局变量访问都经过 Proxy
// Module Federation:不需要隔离
// 模块通过标准 import/export 交互
// 没有全局变量污染的场景
import { OrderList } from 'orderApp/OrderList';
// 就是一个普通的模块导入
// 零沙箱开销
维度三:依赖共享
// 乾坤:约定式共享
// 主应用通过 <script> 加载 React 到全局
// 子应用通过 externals 配置避免打包 React
// 约定子应用的 React 来自 window.React
// 问题1:如果子应用忘记配置 externals → 重复加载
// 问题2:如果需要不同版本的 React → 无法处理
// 问题3:无版本协商,完全依赖人工约定
// Module Federation:协商式共享
// 编译时声明版本需求,运行时自动协商
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0',
},
}
// 自动选择兼容的最高版本
// 不兼容时有明确的警告/错误
// 需要不同版本时可以配置为非 singleton
维度四:通信模式
// 乾坤:事件总线 / 全局状态
// 子应用之间通过 props 传递、全局事件或共享状态通信
import { initGlobalState } from 'qiankun';
const actions = initGlobalState({ user: null });
// 主应用设置
actions.setGlobalState({ user: { name: 'Yang' } });
// 子应用监听
actions.onGlobalStateChange((state) => {
console.log(state.user);
});
// 问题:无类型安全、无编译时检查、调试困难
// Module Federation:标准 import/export
// 共享一个状态管理模块
// appA/webpack.config.js
exposes: {
'./store': './src/store/globalStore',
}
// appB 中
import { useGlobalStore } from 'appA/store';
// 完整的类型安全
// 标准的模块接口
// 和本地模块无异的调试体验
维度五:适用场景
// 乾坤的最佳场景
const qiankunBestFor = {
legacy: '需要接入遗留应用(jQuery、Angular 1.x 等不同框架)',
isolation: '子应用之间高度独立,需要严格的 JS/CSS 隔离',
routing: '以路由为维度拆分子应用,每个路由对应一个独立应用',
migration: '渐进式迁移旧单体应用,新旧应用框架不同',
};
// Module Federation 的最佳场景
const mfBestFor = {
sharing: '多个应用之间需要共享组件、逻辑、状态',
consistency: '统一技术栈(或相近版本),追求一致的开发体验',
granularity: '需要模块级而非应用级的代码复用',
performance: '对加载性能敏感,无法接受沙箱开销',
newProject: '新建项目,没有遗留应用的历史包袱',
};
9.4.3 一个关键的误区
很多文章会这样描述:“Module Federation 是一种新的微前端方案,可以替代乾坤。” 这个说法严格意义上是不准确的。
Module Federation 不是一个微前端框架。它没有以下能力:
- 没有应用生命周期管理:不关心 bootstrap/mount/unmount
- 没有路由拦截:不监听 URL 变化来切换子应用
- 没有 JS/CSS 沙箱:不提供全局变量和样式的隔离
- 没有 HTML Entry:不解析 HTML 来提取资源
Module Federation 是一个模块共享基础设施。它解决的是”如何在不同构建产物之间共享模块”这个底层问题。你可以用它来实现微前端——但你同样可以用它来实现跨团队的组件库共享、运行时插件系统、多应用的公共状态管理,甚至服务端模块的跨进程共享。
// Module Federation 的应用场景远超微前端
const useCases = {
// 微前端(最常见的场景)
microFrontend: {
description: '多个独立应用在同一页面协作',
howMFHelps: '提供模块级共享,避免重复加载',
},
// 跨团队组件库
sharedComponentLib: {
description: '多个团队共享 UI 组件库',
howMFHelps: '组件库更新后无需所有消费方重新构建',
example: '设计系统团队发布 Button v2,所有应用自动使用新版本',
},
// 运行时插件系统
pluginSystem: {
description: '主应用支持动态加载第三方插件',
howMFHelps: '插件作为 Remote 暴露,主应用作为 Host 消费',
example: '类似 VS Code 的扩展机制,但在浏览器中运行',
},
// A/B 测试
abTesting: {
description: '同一个功能的不同实现,运行时决定使用哪个',
howMFHelps: '不同版本作为不同的 Remote,运行时动态切换',
},
// 服务端联邦(Node.js)
serverFederation: {
description: '多个 Node.js 服务之间共享模块',
howMFHelps: 'MF 2.0 支持 Node.js 运行时',
},
};
9.4.4 两种方案的组合使用
在真实的大型项目中,乾坤和 Module Federation 不一定是互斥的选择。它们可以在不同层面协作:乾坤负责应用级编排和隔离,Module Federation 负责模块级共享。
举一个典型场景:订单子应用(React 18)和商品子应用(React 18)技术栈一致,通过 Module Federation 共享组件和 React 实例;营销子应用(Vue 3)框架不同,通过乾坤的沙箱实现隔离。三个子应用统一由乾坤管理生命周期和路由切换。
// 主应用:用乾坤管理所有子应用的生命周期
registerMicroApps([
{ name: 'order-app', entry: 'https://order.example.com', container: '#container', activeRule: '/order' },
{ name: 'product-app', entry: 'https://product.example.com', container: '#container', activeRule: '/product' },
{ name: 'marketing-app', entry: 'https://marketing.example.com', container: '#container', activeRule: '/marketing' },
]);
// 订单和商品应用:webpack.config.js 中同时配置 MF
// 共享 OrderList、ProductCard、React/ReactDOM
// 营销应用:不参与 MF,通过乾坤沙箱隔离
深度洞察:Module Federation 不是微前端方案,是模块共享基础设施
如果你只记住本章一个观点,应该是这个:Module Federation 的设计哲学不是”更好的微前端”,而是”让独立构建的代码像同一个项目中的模块一样互相引用”。 微前端只是这个能力的应用场景之一。这就像 TCP/IP 不是”更好的电话网络”,而是一个通用的数据传输协议——电话(VoIP)只是它的应用场景之一。当你理解了这一点,你就理解了为什么 Module Federation 2.0 要从 Webpack 中独立出来,为什么它要支持 Node.js 运行时,为什么它的 API 设计是”模块级”而非”应用级”——因为它的野心从来不是做一个微前端框架,而是成为跨构建产物的模块共享协议。这个定位决定了它的设计空间远比任何微前端框架都大。理解了工具的定位,才能正确使用工具。
9.4.5 选型决策矩阵
面对一个具体的项目,如何在乾坤和 Module Federation 之间做出选择?以下是一个基于实际约束条件的决策矩阵:
| 决策因素 | 倾向乾坤 | 倾向 MF |
|---|---|---|
| 技术栈一致性 | 不一致(React + Vue + jQuery) | 基本一致(都用 React 18) |
| 遗留应用 | 有大量遗留应用需要接入 | 无遗留应用或可以重构 |
| 共享粒度 | 应用级即可(路由拆分) | 需要模块级(共享组件/逻辑) |
| 性能敏感度 | 可以接受沙箱开销 | 对加载性能极度敏感 |
| 构建工具 | 各团队构建工具不统一 | 统一使用 Webpack/Rspack |
| 隔离需求 | 需要严格 JS/CSS 隔离 | 团队有良好编码规范 |
没有”更好”的方案,只有”更适合”的方案。当你理解了两者的设计哲学和问题域,选型就不再是一道选择题,而是一道基于约束条件的推导题。
本章小结
- Module Federation 的核心范式转换:从”独立构建独立部署的应用”到”运行时共享任意粒度的模块”
- 四大核心概念:Host(消费方)、Remote(提供方)、Shared(协商共享的依赖)、Exposes(暴露的模块清单)
remoteEntry.js是 Remote 的入口契约,通过init()和get()两个方法完成共享作用域初始化和模块获取- Shared 依赖的版本协商通过
singleton、requiredVersion、eager三个配置项控制,本质是在”全局唯一”和”版本兼容”之间寻找平衡 - MF 2.0 的三大革新:独立运行时(跨构建工具)、Manifest + 类型同步(开发体验)、插件系统(可扩展性)
- Module Federation 和乾坤/single-spa 解决的不是同一个问题——前者是模块共享基础设施,后者是应用运行时管理框架
- Module Federation 不是微前端方案,微前端只是它的应用场景之一
把本章的核心洞察拎出来再说一遍——Module Federation 是前端工程领域过去十年里最重要的一次范式转换。在 MF 之前、前端模块系统的基本单位是”构建产物”——你 build 出一份 bundle、这份 bundle 是自包含的、里面的所有代码都是编译时确定的。在 MF 之后、前端模块系统的基本单位变成了”运行时协商”——你的 bundle 可以在运行时从另一个地方加载模块、依赖可以在运行时和其他 bundle 共享。这是从”静态链接”到”动态链接”的跃迁——而这个跃迁、在 C/C++ 世界里发生在 1980 年代(从 .a 到 .so / .dll)、在 Java 世界里发生在 1990 年代(从 jar 到 OSGi)、在前端世界里发生得最晚、但影响最深远**。
之所以说 MF 的影响最深远、是因为前端的”动态链接”和 C/Java 的动态链接、在一个关键维度上不同——前端的动态链接是”跨工程、跨团队、跨网络”的。.so/.dll 通常在同一台机器上、由同一个系统维护;前端的 MF 模块可能在另一个数据中心、由另一个团队维护、通过 HTTP 网络加载。这种”跨网络的动态链接”、让前端从”单体客户端应用”的时代、真正进入了”分布式客户端系统”的时代——浏览器不再是”加载一份完整应用”的容器、而是”运行时组合多方模块”的执行器。当你从这个视角看待 MF、你会意识到它不只是一个 webpack 插件、它在重新定义前端架构的可能性。
本章也反复强调——Module Federation 和乾坤/single-spa 不是同一个维度的方案。把它们对立起来比较是错的——就像把”TCP 协议”和”Kubernetes”放在一起比较一样、它们处在完全不同的抽象层级。正确的理解是:MF 是”模块共享基础设施”——它让跨工程的模块共享成为可能;乾坤/single-spa 是”应用运行时管理框架”——它们关心生命周期、路由、沙箱。一个生产级微前端系统、完全可以同时使用两者——用 MF 共享 React、antd 这些公共依赖;用乾坤管理子应用的加载、卸载、沙箱。不要被”谁取代谁”的营销话术误导——真正的架构思考、永远是”各取所长、分工协作”。
与我们在《Claude Code 源码》第 11 章讨论的 MCP(Model Context Protocol)对比——MCP 也在做”跨进程/跨网络的能力共享”、只是它共享的是”AI Agent 的工具能力”、而不是”前端模块”**。MCP 和 MF 在哲学上是孪生兄弟——都承认”一个系统不可能自己实现所有能力、必须有一种标准的协议让外部能力接入进来”。MCP 用 JSON-RPC over stdio/HTTP、MF 用 ESM over HTTP——但思想内核完全一致——好的生态不是由单个强大系统独裁、而是由一套开放协议让无数独立参与者共同繁荣。读完 MCP 和 MF、你会对”开放协议”这个词、有比过去深得多的理解——它不仅是一种技术实现、更是一种对”软件应该如何成长”的根本信念。
思考题
-
概念理解:本章提出”Module Federation 不是微前端方案,是模块共享基础设施”。请从 Module Federation 的 API 设计(
exposes、remotes、shared)出发,分析为什么说它的设计粒度是”模块级”而非”应用级”。如果将 Module Federation 的概念类比到后端微服务生态,它更接近 gRPC、Kubernetes 还是 Service Mesh?为什么? -
版本协商:假设你的项目中有四个 Module Federation 应用,它们依赖的 React 版本分别是
18.2.0、18.3.1、18.2.0、17.0.2。所有应用都配置了singleton: true。请推演运行时版本协商的结果,分析可能出现的问题,并提出你的解决方案。 -
架构设计:你的公司有三个前端团队,分别维护电商平台的订单系统(React 18)、商品系统(React 18)和客服系统(Vue 3)。现在需要在订单页面中嵌入商品推荐组件,在商品页面中嵌入最近订单列表。请设计一个架构方案,说明你会选择乾坤、Module Federation 还是两者的混合方案,并解释每个技术决策的理由。
-
深度思考:
eager: true和eager: false的选择本质上是”初始加载性能”和”运行时确定性”之间的权衡。请分析在以下三个场景中,你会如何配置eager,并解释原因:a)面向消费者的电商首页;b)面向内部用户的后台管理系统;c)需要支持离线使用的 PWA 应用。 -
前沿展望:MF 2.0 将自己定位为”跨构建工具的模块共享协议”。如果未来浏览器原生支持了类似 Import Maps + Service Worker 的模块联邦能力,Module Federation 的运行时层是否会变得多余?它的哪些设计(如版本协商、类型同步、插件系统)是浏览器原生能力难以替代的?