微前端源码精讲
第11章 Module Federation 2.0 与 Rspack
第11章 Module Federation 2.0 与 Rspack
“模块联邦的第一个版本证明了跨构建产物共享代码是可行的——而第二个版本证明了这件事可以变得简单、安全、且极其快速。”
本章要点
- 理解 Module Federation 2.0 相比 1.0 的三大飞跃:类型安全、运行时插件系统、动态远程加载
- 掌握 Rspack 中 Module Federation 的配置与 Rust 编译带来的性能优势
- 深入 @module-federation/enhanced 运行时核心源码,理解模块加载与版本协商的底层机制
- 实现跨框架(React + Vue)的 Module Federation 实践方案
- 建立 MF 2.0 在生产环境的部署策略:版本管理、灰度发布、容灾降级
2022 年底的一个深夜,我正在调试一个 Webpack 5 Module Federation 的线上问题。远程模块加载失败了,但错误信息只有一行冷冰冰的 ScriptExternalLoadError。没有类型提示告诉我远程模块的接口长什么样,没有运行时钩子让我在加载失败时做降级处理,甚至无法在不重新部署宿主应用的情况下切换远程模块的地址。
我花了四个小时定位问题——最终发现是远程应用的一次接口变更导致类型不匹配,而宿主应用在编译期对此一无所知。
这就是 Module Federation 1.0 的困境:它打开了一扇通往跨应用模块共享的大门,却没有在门口放一盏灯。
2024 年,Zack Jackson 和团队发布了 Module Federation 2.0。这不是一次小版本迭代,而是一次架构级的重构。类型安全、运行时插件、动态远程、跨构建工具支持——这些能力让 Module Federation 从”能用”进化到”好用”。而 Rspack 的加入,则让”好用”变成了”飞快”。
本章将带你完整走过这条进化之路。
下图展示了 Module Federation 从 1.0 到 2.0 的架构跃迁,以及 Rspack 带来的编译性能提升:
flowchart TB
subgraph MF1["Module Federation 1.0"]
MF1_Core["核心能力"]
MF1_Core --> MF1_Expose["exposes/remotes\n模块暴露与消费"]
MF1_Core --> MF1_Shared["shared\n版本协商"]
MF1_Core --> MF1_Limit["局限"]
MF1_Limit --> NoType["无类型安全"]
MF1_Limit --> NoPlugin["无运行时插件"]
MF1_Limit --> Static["远程地址必须硬编码"]
MF1_Limit --> WebpackOnly["仅 Webpack 5"]
end
subgraph MF2["Module Federation 2.0"]
MF2_Core["核心增强"]
MF2_Core --> TypeSafe["类型安全\n@module-federation/typescript"]
MF2_Core --> RuntimePlugin["运行时插件系统\nbeforeRequest / afterLoadRemote / errorLoadRemote"]
MF2_Core --> DynamicRemote["动态远程\n运行时 registerRemotes()"]
MF2_Core --> CrossBuild["跨构建工具\nWebpack + Rspack + Vite"]
end
subgraph Rspack["Rspack 加持"]
RspackCore["Rust 编译核心"]
RspackCore --> Speed["构建速度\n10-50x 提升"]
RspackCore --> Compat["Webpack API 兼容\n零迁移成本"]
end
MF1 -->|"架构级重构"| MF2
MF2 -->|"构建加速"| Rspack
style MF1 fill:#ffebee,stroke:#c62828
style MF2 fill:#e3f2fd,stroke:#1565c0
style Rspack fill:#e8f5e9,stroke:#2e7d32
11.1 MF 2.0 的新能力
Module Federation 1.0 解决了一个根本问题:如何在运行时从另一个独立部署的应用中加载模块。但它留下了三个关键缺口——类型安全、运行时扩展性、动态远程管理。MF 2.0 逐一填补了这些缺口。
MF 2.0 不是一次”小版本升级”、而是一次”重新思考模块联邦是什么”的架构演进。MF 1.0 是”Webpack 的一个插件”、MF 2.0 是”一套独立的模块联邦规范和运行时”——后者从根上脱离了对 Webpack 的依赖、让自己成为一个可以被任何构建工具实现的”标准”。这种”从特性到规范”的升级路径、在软件历史上反复出现——HTTP 从 CERN 内部协议变成 W3C 标准、Docker 从单一产品变成 OCI 规范、Kubernetes 从 Google 内部工具变成 CNCF 项目**——每一次”从特性到规范”的升级、都意味着这项技术进入了生态成熟期。MF 2.0 的规范化、让 Vite、Rspack、Rolldown、甚至未来可能出现的新构建工具、都能以相同的协议接入模块联邦生态——这是一次对”前端模块化”未来十年走向的决定性布局。
11.1.1 类型安全:@module-federation/typescript
“类型安全”在 MF 1.0 时代是一个严重缺失的环节——这不只是”开发体验不好”的问题、而是”从根本上违背了现代前端开发的底线”。**在 TypeScript 已经成为前端工业标准的 2026 年、一个不支持类型的模块加载机制、就像一个不支持 UTF-8 的 HTTP 协议——能用、但反人类。**MF 2.0 通过 @module-federation/typescript 包填补了这个黑洞——它让远程模块在开发时具备完整的类型支持、一切拼写错误、参数类型错误、返回值类型错误都能在编译时被捕获。这个改进看起来只是”加了一个 TS 插件”、实际上它让 MF 从”大厂内部可用”的技术、升级为了”主流前端项目可以采用”的技术——工程化的成熟度、往往就是由这些看似不起眼的”细节”决定的。
在 MF 1.0 中,远程模块对宿主应用而言是一个黑盒。你通过字符串引用一个远程模块,TypeScript 编译器对它的类型一无所知。MF 2.0 通过 @module-federation/typescript 引入了完整的类型安全机制——远程应用在构建时生成类型声明,宿主应用在开发时自动拉取。
// remote-app/rspack.config.ts(远程应用配置)
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
export default {
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button.tsx',
'./UserCard': './src/components/UserCard.tsx',
'./useAuth': './src/hooks/useAuth.ts',
},
dts: {
generateTypes: {
extractRemoteTypes: true,
compileInChildProcess: true,
},
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
宿主应用的配置负责消费这些类型:
// host-app/rspack.config.ts(宿主应用配置)
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
export default {
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: {
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
},
dts: {
consumeTypes: {
remoteTypesFolder: '@mf-types',
abortOnError: false,
consumeAPITypes: true,
},
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};
配置完成后,宿主应用引用远程模块时便获得了完整的类型推导:
// host-app/src/App.tsx —— 完整的类型推导
import RemoteButton from 'remoteApp/Button';
import { useAuth } from 'remoteApp/useAuth';
function App() {
const { user, login, logout } = useAuth(); // ✅ 完整的类型推导
return (
<RemoteButton
label="登录" // ✅ string 类型
onClick={login} // ✅ () => void
variant="primary" // ✅ 联合类型自动补全
// color={123} // ❌ 编译期报错:不存在属性 'color'
/>
);
}
类型同步的底层使用 TypeScript Compiler API 提取暴露模块的声明文件,打包为可下载归档,宿主应用开发时从远程拉取并解压到 node_modules/@mf-types/ 目录,最后生成 TypeScript path mapping。
💡 深度洞察:MF 2.0 的类型安全不仅仅是”开发体验的提升”。在微前端架构中,远程模块的接口变更是最常见的故障来源之一。类型系统将这类问题从”运行时崩溃”前移到”编译期报错”。在传统微服务架构中,API 契约通过 OpenAPI/Protobuf 来保障;而在微前端架构中,模块联邦的类型系统扮演的正是同样的角色。
11.1.2 运行时插件系统
“运行时插件系统”是 MF 2.0 最有野心的一次架构升级——它把”模块联邦”从一套”固定的加载流程”变成了一套”可以被扩展的生命周期”。对比 MF 1.0、它的加载流程是不可改变的——init → get → load、固定流程、没有扩展点;MF 2.0 的加载流程是被切成多个生命周期钩子的——beforeInit、beforeRequest、beforeLoadRemote、afterLoadRemote、onLoad、errorLoadRemote、handlePreloadModule——每一个钩子都可以被插件拦截和扩展。这种”把流程切片为钩子、让用户注入扩展”的设计模式、在 Webpack 的 compiler hooks、Vite 的 plugin API、Babel 的 visitor、Express 的 middleware 里都能看到——它是现代软件扩展性的事实标准。学会”洋葱模型 + 钩子系统”这套组合、你就能设计出任何一个需要支持长期演化的框架。
这种设计对 MF 2.0 的战略意义尤其深远——它让整个模块联邦生态从”MF 核心维护者决定功能”变成了”社区插件驱动创新”。灰度发布可以用插件实现、容灾降级可以用插件实现、性能监控可以用插件实现、甚至类型安全都是用插件实现——核心保持精简、生态野蛮生长。这种”留给社区发挥的空间”、比”官方大包大揽”要健康得多——它让技术生态能够响应市场的多样化需求。
下图展示了 MF 2.0 运行时插件系统的钩子执行流程,每个阶段都可以被插件拦截和扩展:
flowchart LR
Init["init()\n初始化"] --> BeforeInit["beforeInit\n插件预处理"]
BeforeInit --> InitHook["init hook\n注册远程"]
InitHook --> Request["loadRemote()\n请求模块"]
Request --> BeforeRequest["beforeRequest\n修改请求参数"]
BeforeRequest --> BeforeLoadRemote["beforeLoadRemote\n加载前拦截"]
BeforeLoadRemote --> Loading["实际网络加载\nremoteEntry.js + Chunk"]
Loading --> AfterLoadRemote["afterLoadRemote\n加载后处理"]
AfterLoadRemote --> OnLoad["onLoad\n模块实例化"]
Loading -.->|"加载失败"| ErrorHook["errorLoadRemote\n降级处理"]
ErrorHook -.-> Fallback["返回降级组件"]
style Init fill:#e3f2fd,stroke:#1565c0
style ErrorHook fill:#ffebee,stroke:#c62828
style Fallback fill:#fff3e0,stroke:#e65100
MF 1.0 的运行时行为几乎是固定的。MF 2.0 引入了强大的运行时插件系统,让你可以拦截模块联邦的每一个关键环节:
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
interface FederationRuntimePlugin {
name: string;
beforeInit?: (args: BeforeInitArgs) => BeforeInitArgs;
init?: (args: InitArgs) => void;
beforeRequest?: (args: BeforeRequestArgs) => BeforeRequestArgs | Promise<BeforeRequestArgs>;
beforeLoadRemote?: (args: BeforeLoadRemoteArgs) => BeforeLoadRemoteArgs;
afterLoadRemote?: (args: AfterLoadRemoteArgs) => AfterLoadRemoteArgs;
onLoad?: (args: OnLoadArgs) => void;
beforeLoadShare?: (args: BeforeLoadShareArgs) => BeforeLoadShareArgs;
resolveShare?: (args: ResolveShareArgs) => ResolveShareArgs;
errorLoadRemote?: (args: ErrorLoadRemoteArgs) => unknown;
}
以下是两个在生产环境中极具价值的插件示例:
// 加载失败时的降级插件
const fallbackPlugin: () => FederationRuntimePlugin = () => ({
name: 'fallback-plugin',
errorLoadRemote({ id, error }) {
console.error(`[MF Fallback] 远程模块 ${id} 加载失败:`, error);
const fallbackMap: Record<string, () => unknown> = {
'remoteApp/Button': () => import('./fallbacks/ButtonFallback'),
'remoteApp/UserCard': () => import('./fallbacks/UserCardFallback'),
};
const loader = fallbackMap[id];
if (loader) return loader();
return { default: () => ({ __isFallback: true, moduleId: id }) };
},
});
// 模块加载性能监控插件
const performancePlugin: () => FederationRuntimePlugin = () => {
const timings = new Map<string, number>();
return {
name: 'performance-monitor-plugin',
beforeLoadRemote(args) {
timings.set(args.id, performance.now());
return args;
},
afterLoadRemote(args) {
const start = timings.get(args.id);
if (start) {
const duration = performance.now() - start;
timings.delete(args.id);
navigator.sendBeacon?.('/api/metrics', JSON.stringify({
type: 'mf_module_load', moduleId: args.id, duration, timestamp: Date.now(),
}));
if (duration > 3000) {
console.warn(`[MF Perf] ${args.id} 加载耗时 ${duration.toFixed(0)}ms,超过阈值`);
}
}
return args;
},
};
};
插件通过 init 注册:
值得停下来思考一下的是这种”插件列表式注册”和”框架中心化扩展”的对比——如果所有扩展都要通过修改 MF 核心代码实现、那 MF 团队就成了所有扩展的瓶颈;如果扩展可以通过”写一个插件函数、在 init 时传入”实现、任何人都能给自己的项目加自定义行为、不需要向上游提 PR、不需要等下个版本发布。这种”去中心化的扩展模型”、让 MF 的生态能在不增加核心维护负担的前提下持续繁荣——社区可以诞生各种专用插件:错误监控插件、性能分析插件、A/B 测试插件、国际化插件——每个插件都由它最需要的团队维护、而不是由 MF 官方包办。这就是”赋能社区”的工程智慧——把框架核心做小、把扩展空间留大、让生态自己长出多样性。
import { init, loadRemote } from '@module-federation/enhanced/runtime';
init({
name: 'hostApp',
remotes: [{ name: 'remoteApp', entry: 'http://localhost:3001/remoteEntry.js' }],
plugins: [fallbackPlugin(), performancePlugin()],
});
const Button = await loadRemote<{ default: React.FC }>('remoteApp/Button');
11.1.3 动态远程:告别硬编码
“动态远程”是 MF 2.0 送给企业级用户的一份大礼——在 MF 1.0 里、远程地址是硬编码在 webpack 配置里的字符串——remotes: { app: 'app@http://cdn.example.com/remoteEntry.js' }——一旦构建完成、这个地址就固定了。想换地址?重新构建发布。想同时支持开发环境和生产环境?搞多份配置、多次构建。想做 A/B 测试、让 1% 用户加载新版本的 remoteEntry.js?不可能。**MF 2.0 的动态远程彻底解决了这个问题——加载远程的时机从”构建时配置”推迟到了”运行时调用”——await loadRemote('app/Button', { from: dynamicUrl })。**这看似简单的变化、对生产运维来说是革命性的——它让微前端从”静态编排”进化到”动态编排”、和后端 Kubernetes 从”静态 YAML”进化到”动态 API”是同一种跃迁。从此、灰度发布、A/B 测试、多环境切换、配置中心集成——这些企业级场景都有了优雅的解决方案。
下图对比了 MF 1.0 静态远程和 MF 2.0 动态远程的工作模式差异:
sequenceDiagram
participant Config as 构建配置
participant Runtime as 运行时
participant Server as 远程服务
participant App as 应用
Note over Config,App: MF 1.0 -- 静态远程 (构建时确定)
Config->>Runtime: remotes: { app: 'app@http://固定地址/remoteEntry.js' }
Runtime->>Server: 加载固定地址的 remoteEntry.js
Server-->>App: 返回模块
Note over Config,App: MF 2.0 -- 动态远程 (运行时确定)
App->>Server: 请求配置中心获取远程地址
Server-->>Runtime: { remoteApp: 'http://动态地址/remoteEntry.js' }
Runtime->>Runtime: registerRemotes() 动态注册
Runtime->>Server: 加载动态地址的 remoteEntry.js
Server-->>App: 返回模块
Note over App: 支持灰度/A-B测试/多环境切换
MF 1.0 中远程应用地址必须在构建时写死。MF 2.0 通过运行时 API 彻底解决了这个问题:
import { init, loadRemote, registerRemotes } from '@module-federation/enhanced/runtime';
init({ name: 'hostApp', remotes: [] });
// 从配置中心动态获取远程应用列表
async function bootstrapRemotes(): Promise<void> {
const response = await fetch('https://config.example.com/api/micro-apps');
const configs: Array<{ name: string; entry: string; enabled: boolean }> = await response.json();
const enabledRemotes = configs
.filter((c) => c.enabled)
.map((c) => ({ name: c.name, entry: c.entry }));
registerRemotes(enabledRemotes, { force: false });
}
// A/B 测试:基于用户分组加载不同版本
async function loadWithABTest(userId: string): Promise<void> {
const abConfig = await fetchABTestConfig(userId);
registerRemotes([{
name: 'checkoutApp',
entry: abConfig.group === 'experiment'
? 'https://cdn.example.com/checkout/v3-beta/remoteEntry.js'
: 'https://cdn.example.com/checkout/v2.8.0/remoteEntry.js',
}], { force: true });
}
💡 深度洞察:动态远程的本质是将”远程应用的版本绑定”从编译时推迟到运行时。这和微服务架构中的服务发现是同一个设计思想——我们不会将服务地址硬编码到代码中,而是通过注册中心动态发现。Module Federation 2.0 的动态远程 + 配置中心,就是微前端世界的”服务发现”。
11.2 Rspack 中的 Module Federation
11.2.1 为什么选择 Rspack
Rspack 是字节跳动在 2022 年开源的 Rust 版 webpack——它不是”重写一个新工具”、而是”在 Rust 上重新实现 webpack 的 API”。这个决策非常聪明——让 Rspack 能无缝继承 webpack 生态多年积累的插件、loader、配置习惯、同时享受 Rust 带来的性能红利。类似的思路在软件生态里有过多次成功实践——pnpm 兼容 npm 的 package.json 但换了存储引擎;Bun 兼容 Node.js 的 API 但换了 runtime;esbuild 不完全兼容 webpack 但学习了其模式——“兼容 API + 重写实现”是一个能”最小成本获取最大生态”的经典策略。**Rspack 和 MF 的深度集成、把这个策略的价值放大到了极致——你只需要把 webpack 换成 Rspack、其他配置几乎不变、就能把构建时间从几十秒降到几秒。这种”替换单个工具就能获得数量级性能提升”的体验、是工程师最爱的那种”升级”。
Rspack 是字节跳动开源的基于 Rust 的打包工具,提供了与 Webpack 高度兼容的 API,同时将构建性能提升了一个数量级。对 Module Federation 而言,Rspack 的价值在于:构建速度(Rust 编译带来 10-50 倍加速)和原生支持(从核心层面内置 MF)。
// 构建性能对比(真实项目基准测试)
// Webpack 5: 冷构建 45s | 热构建 12s | HMR 2.1s | MF 开销 ~8s
// Rspack: 冷构建 3.2s | 热构建 0.8s | HMR 0.12s | MF 开销 ~0.5s
11.2.2 Rspack + MF 2.0 完整配置
远程应用的完整 Rspack 配置:
// remote-app/rspack.config.ts
import { defineConfig } from '@rspack/cli';
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
export default defineConfig({
entry: './src/index.ts',
output: { publicPath: 'auto', uniqueName: 'remoteApp' },
devServer: {
port: 3001,
headers: { 'Access-Control-Allow-Origin': '*' },
},
resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'] },
module: {
rules: [{
test: /\.tsx?$/,
use: {
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: { syntax: 'typescript', tsx: true },
transform: { react: { runtime: 'automatic' } },
},
},
},
type: 'javascript/auto',
}],
},
plugins: [
new ModuleFederationPlugin({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button.tsx',
'./UserCard': './src/components/UserCard.tsx',
'./useAuth': './src/hooks/useAuth.ts',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
runtimePlugins: ['./src/mf-plugins/lifecycle.ts'],
dts: { generateTypes: { extractRemoteTypes: true, compileInChildProcess: true } },
}),
],
});
宿主应用配置:
// host-app/rspack.config.ts
import { defineConfig } from '@rspack/cli';
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
export default defineConfig({
entry: './src/index.ts',
output: { publicPath: 'auto', uniqueName: 'hostApp' },
devServer: { port: 3000 },
resolve: { extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'] },
module: {
rules: [{
test: /\.tsx?$/,
use: {
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: { syntax: 'typescript', tsx: true },
transform: { react: { runtime: 'automatic' } },
},
},
},
type: 'javascript/auto',
}],
},
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
remotes: { remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js' },
shared: {
react: { singleton: true, requiredVersion: '^18.0.0', eager: true },
'react-dom': { singleton: true, requiredVersion: '^18.0.0', eager: true },
},
runtimePlugins: ['./src/mf-plugins/host-lifecycle.ts'],
dts: { consumeTypes: { remoteTypesFolder: '@mf-types', abortOnError: false } },
}),
],
});
11.2.3 Rspack 的 Rust 编译管线与 MF 的协同
Rspack 的性能优势、本质上来自三个根本差异——原生并发、零 GC、紧凑内存布局。JavaScript 作为解释执行的动态语言、它在构建工具这种”算法密集”的场景下的性能天花板、被语言运行时本身决定——再优化的 JavaScript 代码、都绕不过 V8 的 JIT 预热、GC 暂停、隐藏类切换这些固有成本**。**Rust 则没有这些负担——它编译成原生机器码、没有运行时 GC、内存布局可以按 cache line 精确控制。对于 MF 这种”需要遍历所有模块、分析它们的 import/export、构建依赖图、生成 chunk 组”的任务、Rust 和 JavaScript 的性能差距不是 2 倍、是 10-50 倍。这个数字看起来夸张、但如果你理解了”CPU 密集型任务 + GC 友好语言 = 性能恶梦”这个公式、你就会觉得一切都合理了。
Rspack 在 Module Federation 场景下有三个关键的架构优势。第一,并行模块解析——使用 Rust 的 rayon 库在多线程中并行解析模块依赖图,暴露模块和共享依赖的分析与主应用的模块解析同时进行。第二,增量编译粒度——以模块为粒度,当远程应用只修改了一个暴露模块时,只需重新编译该模块及其直接依赖。第三,零拷贝的共享依赖分析——直接在 Rust 内存中操作模块元数据,避免 JavaScript 对象的序列化/反序列化开销。
┌───────────────────────────────────────────────────┐
│ Rspack 编译管线 │
│ ┌────────┐ ┌──────────┐ ┌──────────────┐ │
│ │解析阶段 │──▶│模块图构建 │──▶│ 代码生成阶段 │ │
│ │ (Rust) │ │ (Rust) │ │ (Rust) │ │
│ └────────┘ └──────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ MF 远程 共享依赖 remoteEntry.js │
│ 引用解析 版本分析 生成 │
│ (Rust 侧) (Rust 侧) (Rust 侧) │
│ │
│ 关键:MF 分析在 Rust 侧完成,无需 JS 互操作 │
└───────────────────────────────────────────────────┘
💡 深度洞察:类型提取(dts generation)仍然是 Rspack MF 构建中最慢的环节,因为它依赖 TypeScript Compiler API,运行在 JavaScript 侧。这也是为什么
compileInChildProcess: true如此重要——它将类型编译放到子进程中,避免阻塞主构建流程。未来当 Rust 原生 TypeScript 编译器成熟后,这个瓶颈也将被消除。
11.3 @module-federation/enhanced 运行时源码分析
@module-federation/enhanced 是 MF 2.0 生态的核心实现库——它承担着”把规范变成可运行代码”的重要角色。读懂它的源码、不只是理解 MF 的内部实现、更是学习”如何把一个规范转换为一个健壮的运行时”这件通用工程能力。注意一个关键的架构选择——runtime 本身不依赖任何构建工具——它是一个纯 JavaScript 库、可以被 Webpack、Rspack、Vite、甚至未来的任何新构建工具引用。这种”运行时和构建工具解耦”的设计、是 MF 2.0 能跨工具实现的关键——就像 TCP 协议不依赖任何具体的操作系统、任何符合协议的实现都能和其他实现互操作。理解了这个解耦、你就理解了为什么 MF 2.0 能成为”跨构建工具的事实标准”——不是因为它”更快”或”功能更多”、而是因为它”更通用”。
11.3.1 运行时初始化流程
@module-federation/enhanced/runtime 的入口是 init 函数,它创建一个 FederationHost 实例来管理所有远程应用的生命周期:
// FederationHost 核心结构(简化源码分析)
class FederationHost {
options: FederationRuntimeOptions;
hooks: PluginSystem;
moduleCache: Map<string, any>;
sharedHandler: SharedHandler;
remoteHandler: RemoteHandler;
constructor(userOptions: UserOptions) {
this.options = this.normalizeOptions(userOptions);
// 初始化插件系统——所有内置行为都通过插件实现
this.hooks = new PluginSystem({
beforeInit: new SyncWaterfallHook<BeforeInitArgs>('beforeInit'),
init: new SyncHook<InitArgs>('init'),
beforeRequest: new AsyncWaterfallHook<BeforeRequestArgs>('beforeRequest'),
beforeLoadRemote: new AsyncWaterfallHook<BeforeLoadRemoteArgs>('beforeLoadRemote'),
afterLoadRemote: new AsyncWaterfallHook<AfterLoadRemoteArgs>('afterLoadRemote'),
errorLoadRemote: new AsyncHook<ErrorLoadRemoteArgs>('errorLoadRemote'),
beforeLoadShare: new AsyncWaterfallHook<BeforeLoadShareArgs>('beforeLoadShare'),
resolveShare: new SyncWaterfallHook<ResolveShareArgs>('resolveShare'),
});
// 注册用户插件
userOptions.plugins?.forEach((plugin) => this.hooks.registerPlugin(plugin));
// 触发初始化钩子
this.hooks.lifecycle.beforeInit.call({ userOptions, options: this.options, origin: this });
this.hooks.lifecycle.init.call({ options: this.options, origin: this });
}
async loadRemote<T = any>(id: string): Promise<T> {
return this.remoteHandler.loadRemote(id);
}
registerRemotes(remotes: Remote[], options?: { force?: boolean }): void {
this.remoteHandler.registerRemotes(remotes, options);
}
}
// 全局单例
let globalFederationHost: FederationHost | null = null;
export function init(options: UserOptions): FederationHost {
if (globalFederationHost) {
globalFederationHost.initOptions(options);
return globalFederationHost;
}
globalFederationHost = new FederationHost(options);
return globalFederationHost;
}
11.3.2 远程模块加载的完整链路
loadRemote 是 MF 2.0 用户最常接触的 API——它的一次调用、内部触发一条精心编排的异步流水线。这条流水线体现了一个非常漂亮的软件设计模式——“单次调用背后是 N 步并发 + 缓存 + 错误处理”——这就像你在淘宝下单、表面上就是点一下按钮、背后实际触发了库存查询、支付验证、物流调度、风控检测、通知推送这一整套复杂流程。好的 API 设计、就是要把这种复杂性隐藏在简洁的调用签名之后——用户感到的是”一个函数调用”、而不是”一个协议栈”。这种”对外简单、对内复杂”的设计哲学、是现代软件工程的精髓——它让平均工程师也能使用原本只有资深工程师才能掌控的能力。
当调用 loadRemote('remoteApp/Button') 时,请求经过以下链路:
// RemoteHandler.loadRemote 核心实现(简化)
class RemoteHandler {
host: FederationHost;
loadingMap: Map<string, Promise<any>>; // 防止重复加载
async loadRemote(id: string): Promise<any> {
// 1. 解析标识符:'remoteApp/Button' -> { remoteName, exposedModule }
const { remoteName, exposedModule } = this.parseModuleId(id);
// 2. 触发 beforeRequest 钩子(插件可修改请求)
const args = await this.host.hooks.lifecycle.beforeRequest.call({
id, remoteName, exposedModule, options: this.host.options,
});
// 3. 获取远程应用入口信息
const remoteInfo = this.remoteMap.get(args.remoteName);
if (!remoteInfo) throw new Error(`[MF] 未注册的远程应用: ${args.remoteName}`);
// 4. 加载远程入口文件(remoteEntry.js)—— 带缓存
const remoteContainer = await this.loadRemoteEntry(remoteInfo);
// 5. 初始化远程容器(共享依赖协商在此发生)
await this.initializeRemoteContainer(remoteContainer);
// 6. 从远程容器获取具体模块的工厂函数
const factory = await remoteContainer.get(args.exposedModule);
if (!factory) {
throw new Error(`[MF] ${args.remoteName} 未暴露模块 ${args.exposedModule}`);
}
// 7. 执行工厂函数获取模块
const module = factory();
// 8. 触发 afterLoadRemote 钩子
const result = await this.host.hooks.lifecycle.afterLoadRemote.call({
id, module, remoteName, exposedModule,
});
return result.module;
}
private async loadRemoteEntry(remoteInfo: RemoteInfo): Promise<RemoteContainer> {
const { entry, name } = remoteInfo;
if (this.loadingMap.has(name)) return this.loadingMap.get(name)!;
const loadPromise = new Promise<RemoteContainer>(async (resolve, reject) => {
try {
await this.host.hooks.lifecycle.beforeLoadRemote.call({ id: name, entry });
const container = await this.loadScript(entry, name);
resolve(container);
} catch (error) {
// 触发错误处理钩子——插件可在此提供降级
const fallback = await this.host.hooks.lifecycle.errorLoadRemote.call({
id: name, error: error as Error, from: 'runtime', origin: this.host,
});
fallback !== undefined ? resolve(fallback as RemoteContainer) : reject(error);
}
});
this.loadingMap.set(name, loadPromise);
return loadPromise;
}
}
11.3.3 共享依赖的版本协商算法
**版本协商的核心算法其实很像现实世界的”拍卖会”——每个参与者(应用)带着自己能出的价(版本)、拍卖师(运行时)按一套规则决定最终成交价(选中的版本)。**不同的拍卖规则会导致不同的结果——英式拍卖(最高价赢)、荷兰式拍卖(最低价赢)、密封拍卖(不公开竞价)——MF 的版本协商也有几种不同规则:singleton 模式(最高版本赢)、非 singleton 模式(按语义版本匹配)、strictVersion 模式(版本必须完全匹配)。每种规则背后都有明确的场景意图——singleton 针对 React 这种”全应用只能一份实例”的库;非 singleton 针对 lodash 这种”不同版本可以共存”的库;strictVersion 针对那些”版本必须一致才能工作”的特殊依赖。读懂这些规则、不是为了背下来、是为了在遇到具体依赖时能做出正确的配置选择——技术知识的应用、永远是”理解本质、举一反三”而不是”记忆条目、生搬硬套”。
共享依赖协商是 Module Federation 最精妙的部分。当多个应用都声明了对 react 的依赖,运行时按以下规则决定使用哪个版本:
// SharedHandler 版本协商逻辑(简化)
class SharedHandler {
private resolveVersion(
pkgName: string,
candidates: SharedInfo[],
extraOptions?: ShareExtraOptions
): SharedInfo {
// 规则 1:singleton 模式——所有消费者必须使用同一个实例
const singletonCandidates = candidates.filter((c) => c.singleton);
if (singletonCandidates.length > 0) {
const loaded = singletonCandidates.find((c) => c.loaded);
if (loaded) {
this.checkSingletonCompatibility(pkgName, loaded, candidates);
return loaded; // 优先使用已加载的版本
}
return this.selectHighestVersion(singletonCandidates);
}
// 规则 2:非 singleton——按 semver 匹配
const requiredVersion = extraOptions?.requiredVersion || '*';
const compatible = candidates.filter((c) =>
this.satisfiesVersion(c.version, requiredVersion)
);
if (compatible.length === 0) {
console.warn(`[MF] ${pkgName} 没有满足 ${requiredVersion} 的版本`);
return this.selectClosestVersion(candidates, requiredVersion);
}
return compatible.find((c) => c.loaded) || this.selectHighestVersion(compatible);
}
private checkSingletonCompatibility(
pkgName: string, loaded: SharedInfo, all: SharedInfo[]
): void {
for (const candidate of all) {
if (candidate === loaded) continue;
if (candidate.requiredVersion &&
!this.satisfiesVersion(loaded.version, candidate.requiredVersion)) {
const msg = `[MF] ${pkgName} 版本冲突!已加载 v${loaded.version},` +
`但 ${candidate.from} 要求 ${candidate.requiredVersion}。`;
if (candidate.strictVersion) throw new Error(msg);
else console.warn(msg);
}
}
}
}
💡 深度洞察:React 要求整个应用中只能有一个实例(因为 hooks 依赖模块级的内部状态)。如果远程应用的
react版本与宿主不兼容且未配置singleton: true,运行时会加载两个 React 实例,导致 “Invalid hook call” 错误。shared.react.singleton: true不是可选项,而是必选项。同理适用于react-dom、react-router以及任何依赖全局单例状态的库。
11.3.4 插件系统的实现机制
“瀑布流 Hook”(Waterfall Hook)是 Webpack Tapable 里最优雅的一种 Hook 类型——它让多个插件可以对同一个对象依次做加工、每个插件接收上一个插件处理后的结果、自己再加工一次、交给下一个插件。这种”流水线式加工”的模式、和函数式编程里的 reduce、Unix 管道(grep foo | sort | uniq)、RxJS 的 pipe 操作符、本质上完全一致——都是通过”串联多个小函数”实现”复杂处理”。这种模式的力量在于——任何一步的逻辑都可以被独立替换、插入、删除、不影响其他步骤;整体流程对新加入的插件天然友好、不需要特殊的扩展机制。MF 2.0 把这种 Tapable 风格的 Hook 引入到模块联邦里、让扩展能力成为”自然而然就有”的——这是一流框架工程的典范。
MF 2.0 的插件系统借鉴了 Webpack 的 Tapable 设计。核心是瀑布流(Waterfall)Hook——每个回调的返回值作为下一个回调的输入:
class SyncWaterfallHook<T> {
private callbacks: Array<(args: T) => T | undefined> = [];
tap(callback: (args: T) => T | undefined): void {
this.callbacks.push(callback);
}
call(args: T): T {
let result = args;
for (const cb of this.callbacks) {
const newResult = cb(result);
if (newResult !== undefined) result = newResult;
}
return result;
}
}
class PluginSystem {
lifecycle: Record<string, SyncWaterfallHook<any> | AsyncWaterfallHook<any>>;
registerPlugin(plugin: FederationRuntimePlugin): void {
for (const [hookName, hookInstance] of Object.entries(this.lifecycle)) {
const handler = (plugin as any)[hookName];
if (typeof handler === 'function') {
hookInstance.tap(handler.bind(plugin));
}
}
}
}
这种设计使得每个插件不仅可以读取数据,还可以修改数据并传递给下一个插件——这是 MF 2.0 运行时可扩展性的基石。
11.3.5 行号校对与默认值表
前面几节为了叙事顺畅、对运行时类名和内部字段做了一些简化——FederationHost、PluginSystem 这些名字是”教学用名”、和真实仓库里的实际命名会有出入。在工程书里、这种简化是常见手法——它让读者先抓住结构、再去核对源码。**但为了不让读者在对照源码时产生困惑、本节把几个关键符号的真实位置钉回到 module-federation/core 仓库的 main 分支上。所有行号与默认值均以截至 2026-04-22 的 main 分支为准、日后上游重构时行号会漂移、但结构不会变。
核心运行时类的真实命名与位置——packages/runtime-core/src/core.ts 里定义的那个”承担宿主行为”的类、在源码里叫 ModuleFederation(第 60 行)、而不是本章为了对应旧版 MF 1.0 术语而简化的 FederationHost**。这个重命名发生在 MF 2.0 从 runtime 包拆分出 runtime-core 这次架构调整中——旧的 FederationHost 被保留为一层兼容封装、但核心实现已下沉到 ModuleFederation。
关键成员与方法的行号索引——这张表可以作为你读源码时的路标**。当你在本章对应小节看到某个概念、想去源码里找对应实现时、直接对照这张表就能定位到文件:行号。
| 符号 | 文件 | 行号 | 类型 | 本章引用位置 |
|---|---|---|---|---|
ModuleFederation(类) | packages/runtime-core/src/core.ts | 60 | class | 11.3.1 简化为 FederationHost |
hooks(生命周期钩子集合) | 同上 | 61–90 | PluginSystem | 11.3.1 / 11.3.4 |
beforeInit | 同上 | 62 | SyncWaterfallHook | 11.3.4 瀑布流示例 |
init | 同上 | 72 | SyncHook | 11.3.1 |
beforeInitContainer | 同上 | 81 | AsyncWaterfallHook | 11.3.2 加载链路 |
initContainer | 同上 | 88 | AsyncWaterfallHook | 11.3.2 |
snapshotHandler | 同上 | 101 | SnapshotHandler | 11.3.2 远程快照查询 |
sharedHandler | 同上 | 102 | SharedHandler | 11.3.3 版本协商 |
remoteHandler | 同上 | 103 | RemoteHandler | 11.3.2 |
moduleCache | 同上 | 105 | Map<string, Module> | 11.3.2 防重复加载 |
loaderHook | 同上 | 112–160 | PluginSystem | 11.3.4 |
constructor | 同上 | 165 | — | 11.3.1 |
initOptions | 同上 | 186 | 方法 | 11.3.1 init() 复用时复写配置 |
loadShare | 同上 | 197 | 方法 | 11.3.3 |
loadRemote | 同上 | 246 | 方法 | 11.3.2 |
preloadRemote | 同上 | 254 | 方法 | 11.5.4 预加载 |
registerRemotes | 同上 | 275 | 方法 | 11.1.3 动态远程 |
registerShared | 同上 | 279 | 方法 | 11.3.3 |
DEFAULT_SCOPE = 'default' | packages/runtime-core/src/constant.ts | 1 | 常量 | shareScope 默认值 |
DEFAULT_REMOTE_TYPE = 'global' | 同上 | 2 | 常量 | remotes[*].type 默认值 |
ModuleFederationPlugin 选项默认值——打包侧插件的 schema 在 packages/enhanced/src/schemas/container/ModuleFederationPlugin.ts、运行逻辑在 packages/enhanced/src/lib/container/ModuleFederationPlugin.ts**。schema 里显式声明的”必填”字段只有一个(library.type、schema 第 306 行);其它字段都是”可选 + 有约定默认”、默认值在插件 apply 方法里注入。
| 选项 | 默认值 | 约定位置 | 说明 |
|---|---|---|---|
library | { type: 'var', name: <容器名> } | apply 第 161 行 | 未显式配置时按 var 暴露 |
remoteType | 由 library.type 推导(见第 162–166 行) | apply 第 162–166 行 | script / global / var 由上游推导 |
shareScope | 'default'(即 DEFAULT_SCOPE) | runtime-core/constant.ts 第 1 行 | 同 scope 才能共享同一实例 |
shareStrategy | 'version-first' | schema 第 754 行附近的描述 | 另一选项为 'loaded-first' |
dts.generateTypes | 未设置即关闭 | schema dts 节点 | 显式传 true 才生成 |
dts.consumeTypes | 未设置即关闭 | schema dts 节点 | 显式传 true 才拉取 |
bridge.enableBridgeRouter | false | schema 第 823 行 | React 桥接路由开关、默认关 |
manifest | true(即输出 mf-manifest.json) | apply 第 169 行 options.manifest === false 判断 | 传 false 才禁用 |
runtimePlugins | [] | apply 第 150 行条件拼接 | 启用 provideExternalRuntime 时自动追加 |
几个容易踩坑的默认值细节——第一、shareScope 默认是字符串 'default'、不是字面意义上的”所有应用自动共享”——两个应用必须写同一 shareScope 值(或都省略)才会被识别为同一作用域;第二、manifest 默认输出 mf-manifest.json、这是 MF 2.0 相对 1.0 新增的清单文件(文件名来自 sdk 包 constant.ts 的 ManifestFileName = 'mf-manifest.json')、它是 MF Devtools、Rsbuild Dashboard 等外部工具的数据源、非必要不要关掉;第三、shareStrategy 的默认值 'version-first' 意味着”优先选语义版本最高的”——如果你希望已加载版本优先(减少多版本重复加载)、要显式配 'loaded-first'。**
读源码时的一点建议——不要一次性把 core.ts 从头读到尾、而是带着”我想搞清楚 loadRemote 到底做了什么”这种具体问题去读**。沿着 246 行 → remoteHandler → packages/runtime-core/src/remote/index.ts → packages/runtime-core/src/container/index.ts 这条链、追一个真实请求的完整生命周期——这种”沿着一次调用追下去”的读源码方法、比”按目录把每个文件都过一遍”的遍历式阅读、效率高得多。
11.4 跨框架的 Module Federation 实践
“跨框架”是微前端领域一个长期被讨论但很少被优雅解决的话题——React 的 JSX、Vue 的 template、Angular 的 directive、Svelte 的 compiled output、Solid 的 signal——这些框架的渲染机制彼此完全不同、直接把一个框架的组件放到另一个框架里是不可能的。但现实中、大企业经常需要面对”老系统用 Vue、新系统用 React、两者要在同一个页面里和谐共存”这种局面——粗暴的解法是”选一个框架全部重写”、成本巨大且风险极高;优雅的解法就是本节要讨论的”桥接模块”——让不同框架的组件通过一个框架无关的统一接口互操作。这种”用抽象协议弥合生态差异”的思路、在历史上反复出现——CORBA 弥合不同语言的对象模型、WSDL 弥合不同 SOAP 实现、gRPC 弥合不同语言的 RPC、OpenAPI 弥合不同 HTTP API——跨框架 MF 只是把这个思路用到了前端 UI 组件的层次。
11.4.1 跨框架的核心挑战
跨框架集成面临三个核心挑战:渲染机制不同(React Virtual DOM vs Vue 响应式代理)、生命周期不同(组件挂载/卸载时机需精确对齐)、状态管理不同(跨框架的状态共享需要框架无关的中间层)。解决方案的核心是桥接模块——将框架特定的组件封装为统一的 mount/update/unmount 接口。
11.4.2 React 宿主 + Vue 远程
Vue 远程应用暴露的不是 Vue 组件本身,而是经过适配器封装的桥接模块:
// vue-remote-app/src/bridge/createReactiveBridge.ts
import { createApp, type App, type Component, h, reactive } from 'vue';
interface BridgeComponent {
mount: (el: HTMLElement, props?: Record<string, unknown>) => void;
update: (props: Record<string, unknown>) => void;
unmount: () => void;
}
export function createReactiveBridge(component: Component): BridgeComponent {
let app: App | null = null;
const state = reactive<{ props: Record<string, unknown> }>({ props: {} });
return {
mount(el, props = {}) {
Object.assign(state.props, props);
app = createApp({
setup() {
return () => h(component, state.props);
},
});
app.mount(el);
},
update(newProps) {
// 直接修改响应式对象,Vue 自动触发更新——无需重新挂载
Object.assign(state.props, newProps);
},
unmount() {
app?.unmount();
app = null;
},
};
}
在 React 宿主应用中,创建通用的包装组件:
// host-app/src/components/VueRemoteWrapper.tsx
import React, { useRef, useEffect } from 'react';
import { loadRemote } from '@module-federation/enhanced/runtime';
interface BridgeModule {
default: {
mount: (el: HTMLElement, props?: Record<string, unknown>) => void;
update: (props: Record<string, unknown>) => void;
unmount: () => void;
};
}
interface VueRemoteWrapperProps {
remoteName: string;
moduleName: string;
componentProps?: Record<string, unknown>;
fallback?: React.ReactNode;
}
export function VueRemoteWrapper({
remoteName, moduleName, componentProps = {},
fallback = <div>加载中...</div>,
}: VueRemoteWrapperProps) {
const containerRef = useRef<HTMLDivElement>(null);
const bridgeRef = useRef<BridgeModule['default'] | null>(null);
const [status, setStatus] = React.useState<'loading' | 'ready' | 'error'>('loading');
useEffect(() => {
let cancelled = false;
async function loadAndMount() {
try {
const module = await loadRemote<BridgeModule>(`${remoteName}/${moduleName}`);
if (cancelled || !containerRef.current || !module) return;
module.default.mount(containerRef.current, componentProps);
bridgeRef.current = module.default;
setStatus('ready');
} catch (error) {
if (!cancelled) setStatus('error');
}
}
loadAndMount();
return () => { cancelled = true; bridgeRef.current?.unmount(); };
}, [remoteName, moduleName]);
// 当 React 侧 props 变化时,同步到 Vue 组件
useEffect(() => {
if (bridgeRef.current && status === 'ready') {
bridgeRef.current.update(componentProps);
}
}, [componentProps, status]);
return (
<>
{status === 'loading' && fallback}
{status === 'error' && <div>加载失败</div>}
<div ref={containerRef} style={{ display: status === 'ready' ? 'block' : 'none' }} />
</>
);
}
11.4.3 跨框架状态共享
状态共享是跨框架集成里最后一个硬骨头——因为每个框架都有自己的”响应式系统”——React 的 useState、Vue 的 ref、Svelte 的 store、Solid 的 signal——它们看似做同样的事情、但互相不兼容。解决这个问题的关键、是找到一个”所有响应式系统的最大公约数”——那就是发布-订阅模式**。任何一个响应式系统、最终都能被看作是”当数据变化时、通知订阅者”这个最基础模式的某种包装。所以跨框架状态共享的答案、不是”发明一个新的状态管理库”、而是”回到最朴素的发布-订阅、然后为每个框架写一个绑定函数”。**这种”用最小的抽象统一多个复杂系统”的设计思路、和我们在讨论各种通信机制时看到的”最少知识原则”、本质上是同一种智慧——不要试图发明一个能覆盖所有可能性的巨大 API、而是找到一个足够小、足够通用的底层协议、让各方在这个协议之上自由发挥。
跨框架最棘手的问题是状态共享。解决方案是引入一个框架无关的状态层——基于发布-订阅模式:
// shared-state/src/createCrossFrameworkStore.ts
type Listener = () => void;
export function createCrossFrameworkStore<T extends Record<string, unknown>>(initialState: T) {
let state: T = { ...initialState };
const listeners = new Set<Listener>();
return {
getState: () => state,
setState(updater: Partial<T> | ((prev: T) => Partial<T>)) {
const partial = typeof updater === 'function' ? updater(state) : updater;
state = { ...state, ...partial };
listeners.forEach((fn) => fn());
},
subscribe(listener: Listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}
为 React 和 Vue 分别提供绑定:
// React 绑定
import { useSyncExternalStore } from 'react';
export function useStore<T extends Record<string, unknown>, S>(
store: ReturnType<typeof createCrossFrameworkStore<T>>,
selector?: (state: T) => S
) {
return useSyncExternalStore(
store.subscribe,
() => (selector ? selector(store.getState()) : store.getState())
);
}
// Vue 绑定
import { ref, onUnmounted, type Ref } from 'vue';
export function useStore<T extends Record<string, unknown>>(
store: ReturnType<typeof createCrossFrameworkStore<T>>
): Ref<T> {
const state = ref(store.getState()) as Ref<T>;
const unsub = store.subscribe(() => { state.value = store.getState(); });
onUnmounted(unsub);
return state;
}
React 和 Vue 组件可以同时读写同一份状态,实现完美同步。
💡 深度洞察:跨框架状态共享的本质是”谁拥有状态”的问题。状态应该属于业务域而非框架。
createCrossFrameworkStore能同时为 React 和 Vue 所用,因为其核心是发布-订阅模式——所有响应式系统的最大公约数。即使未来引入 Svelte 或 Solid,只需写一个新的绑定函数,无需改动状态层。
11.5 MF 2.0 的生产部署策略
从这一节开始、我们从”技术实现”跨入了”工程运维”的领域——这是一个截然不同的视角。技术实现关心”代码怎么写”、工程运维关心”代码怎么上线、出了问题怎么办、如何保证用户体验”。很多团队在学 MF 时只关注前者、结果上线后才发现后者同样重要——一个 MF 系统的稳定性、不取决于它的配置写得多漂亮、而取决于它的运维体系建得多健全:版本管理、灰度发布、容灾降级、监控告警、回滚机制——每一项都是生产系统不可或缺的安全网。本节讨论的这些部署策略、都不是 MF 独有的——它们是任何分布式系统都要面对的问题——只是在 MF 的场景下、有具体的 MF 特有解法。理解这些通用策略、对你设计任何一个上线面向大规模用户的系统、都会有帮助。
11.5.1 版本管理与发布策略
每个远程应用独立部署,版本管理通过配置中心(远程注册表)实现:
interface RemoteAppVersion {
name: string;
version: string;
entry: string;
integrity?: string; // SRI 哈希
requiredHostVersion?: string; // 宿主最低版本要求
metadata: {
deployedAt: string;
commitHash: string;
changelog: string;
};
}
// 部署流程:上传 CDN -> 注册版本 -> 健康检查
async function registerNewVersion(app: RemoteAppVersion): Promise<void> {
await uploadToCDN(app.name, app.version);
await configCenter.register({
...app,
entry: `https://cdn.example.com/${app.name}/${app.version}/remoteEntry.js`,
});
const health = await fetch(app.entry);
if (!health.ok) throw new Error(`远程入口文件不可访问: ${app.entry}`);
}
11.5.2 灰度发布策略
**灰度发布(Canary Deployment)是生产环境的”风险控制工具”——它让新版本先对少数用户暴露、观察是否有问题、再逐步扩大范围。这种”渐进暴露”的模式、和我们在医学临床试验里看到的 Phase I / II / III 测试、和我们在软件工程里看到的 alpha / beta / RC 发布、是同一种思想——在信息不完整的情况下、不要一次性做出不可逆的决策、而是用小步快跑的方式让信息慢慢汇集。MF 2.0 的动态远程 + 运行时插件、让灰度发布在微前端里变得极其灵活——你可以根据用户 ID、用户角色、A/B 分组、地理位置、流量比例等任意维度决定”这个用户该加载新版本还是旧版本”。这种”细粒度、多维度、实时可调”的灰度能力、让产品团队可以在”几乎不承担大规模故障风险”的前提下持续发布新版本——这才是现代 DevOps 的精髓。
借助动态远程和运行时插件,实现精细化灰度控制:
function createGrayReleasePlugin(
rules: Array<{
remoteName: string;
stableEntry: string;
canaryEntry: string;
condition: (ctx: { userId: string; userRole: string; abGroup: string }) => boolean;
}>,
getContext: () => { userId: string; userRole: string; abGroup: string }
): FederationRuntimePlugin {
return {
name: 'gray-release-plugin',
beforeRequest(args) {
const [remoteName] = args.id.split('/');
const rule = rules.find((r) => r.remoteName === remoteName);
if (!rule) return args;
const ctx = getContext();
const entry = rule.condition(ctx) ? rule.canaryEntry : rule.stableEntry;
args.options.remotes = args.options.remotes?.map((remote) =>
typeof remote === 'object' && remote.name === remoteName
? { ...remote, entry }
: remote
);
return args;
},
};
}
11.5.3 容灾降级策略
“容灾”不是”如果出了问题怎么办”——在生产系统里、“如果”是一个错误的词。正确的词是”什么时候”——什么时候 CDN 会故障?什么时候 remoteEntry.js 会加载失败?什么时候共享依赖版本协商会出错? 答案永远是”迟早会”。所以生产级 MF 系统不是”希望不出问题”、而是”假设一定会出问题、提前准备好每一种故障的响应方案”。这种”假设故障常态化”的 mindset、在 Netflix 的 Chaos Monkey、Google 的 SRE 文化、AWS 的 Well-Architected Framework 里都被奉为圭臬——它们都在教你同一件事:系统的可用性不是靠”每个组件都不出故障”实现的、是靠”任何组件出故障时系统仍能提供最基本服务”实现的。本节讨论的 CDN fallback、本地缓存、功能降级 UI、错误上报——都是这种思想的具体实现。
远程模块加载失败是必须处理的场景。通过插件实现多层防线:
function createResiliencePlugin(): FederationRuntimePlugin {
const cdnFallbacks: Record<string, string[]> = {
remoteApp: [
'https://cdn-primary.example.com/remote/remoteEntry.js',
'https://cdn-backup.example.com/remote/remoteEntry.js',
],
};
return {
name: 'resilience-plugin',
async errorLoadRemote({ id, error }) {
const [remoteName] = id.split('/');
// 策略 1:CDN 故障转移
const urls = cdnFallbacks[remoteName];
if (urls) {
for (const url of urls) {
try {
const module = await loadFromUrl(url, id);
if (module) return module;
} catch { continue; }
}
}
// 策略 2:Service Worker 缓存
if ('caches' in window) {
const cache = await caches.open('mf-remote-entries');
const cached = await cache.match(id);
if (cached) return processCachedEntry(cached);
}
// 策略 3:本地降级模块
const fallbacks: Record<string, () => Promise<unknown>> = {
'remoteApp/Button': () => import('./fallbacks/ButtonFallback'),
'remoteApp/UserCard': () => import('./fallbacks/UserCardFallback'),
};
return fallbacks[id]?.() ?? { default: () => null, __IS_FALLBACK__: true };
},
};
}
11.5.4 性能优化:预加载与缓存
MF 的性能优化、本质上是一场”和时间赛跑”的游戏——每个用户打开页面到看到完整 UI 之间的时间、是一个有限的预算。在这段时间里、你需要完成主 bundle 下载、远程 entry 加载、共享依赖协商、业务 chunk 加载、渲染——任何一环耗时过长、用户就会感知到延迟**。**优化的关键、是把”必须在用户看到页面时才做”的工作、尽可能提前到”用户可能会看到页面但还没看到的时候”——预加载、预连接、预渲染、代码分割、懒加载——这些都是”把工作从关键路径上卸载”的不同手段。MF 2.0 提供的 preloadRemote、hover prefetch、缓存策略、都是这场”时间战争”的武器库——配合运行时插件系统、你可以根据自己项目的具体特点、组合出最适合的优化方案。
import { preloadRemote, init } from '@module-federation/enhanced/runtime';
// 空闲时预加载
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
preloadRemote([{ nameOrAlias: 'remoteApp', exposes: ['./Button', './UserCard'] }]);
});
}
// 基于路由的预加载:hover 导航链接时提前加载
function prefetchOnHover(configs: Array<{
selector: string; remoteName: string; exposes: string[];
}>): void {
configs.forEach(({ selector, remoteName, exposes }) => {
document.querySelector(selector)?.addEventListener('mouseenter', () => {
preloadRemote([{ nameOrAlias: remoteName, exposes }]);
}, { once: true });
});
}
11.5.5 部署架构总览
**让我们用一张”鸟瞰图”总结一下 MF 2.0 在生产环境下的完整部署架构——这张图展现的、不是某个具体公司的实现细节、而是”一个健壮的 MF 生产系统应该具备哪些要素”这个通用模板。你可以把它当作一张 checklist、用来评估自己的 MF 系统是否建成了”生产级”的全套能力——配置中心(让远程地址动态化)、版本管理(让多版本可控切换)、监控告警(让故障能被及时感知)、CDN 分发(让加载快速可靠)——这四大支柱缺一不可。如果你的 MF 系统缺了其中任何一个、它都只是”能跑”、而不是”能扛生产”。
┌──────────────────────────────────────────────────────────────┐
│ 生产环境部署架构 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────────────────┐ │
│ │ 配置中心 │ │ 版本管理 │ │ 监控告警系统 │ │
│ │(远程注册) │ │(版本+灰度)│ │ (性能 + 错误 + SLA) │ │
│ └─────┬────┘ └─────┬────┘ └────────────┬──────────────┘ │
│ │ │ │ │
│ ┌─────▼─────────────▼─────────────────────▼──────────────┐ │
│ │ CDN 层 │ │
│ │ Host v3.2 RemoteA v2.8 RemoteB v1.5 (灰度版本...) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 用户浏览器 │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ MF 2.0 运行时 │ │ │
│ │ │ 插件系统: 灰度路由 / 容灾降级 / 性能监控 │ │ │
│ │ │ 共享管理: 版本协商 / 单例保证 / 按需加载 │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ Service Worker │ ← remoteEntry 缓存 + 离线降级 │ │
│ │ └─────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
💡 深度洞察:很多团队在引入 Module Federation 时低估了运维复杂度。MF 1.0 只解决了”如何加载远程模块”,而生产环境还需要回答:远程应用挂了怎么办?如何灰度发布?如何回滚?如何监控?MF 2.0 的运行时插件系统正是对这些问题的系统性回应——它不试图内置所有策略,而是提供足够的扩展点让你按需实现。这种”平台化”的设计哲学,和 Webpack 本身通过插件系统实现一切的思路如出一辙。
本章小结
- Module Federation 2.0 在三个关键维度上超越了 1.0:类型安全消除了远程模块的接口黑盒,运行时插件开放了模块加载的全链路扩展,动态远程将版本绑定从编译时推迟到运行时
- Rspack 的 Rust 编译管线为 Module Federation 带来了一个数量级的性能提升,通过并行解析、增量编译、零拷贝分析三项关键技术使 MF 构建开销降至可忽略水平
@module-federation/enhanced运行时的核心是FederationHost单例,通过RemoteHandler(远程加载)、SharedHandler(共享协商)、PluginSystem(插件管理)三大子系统协同工作- 跨框架集成的关键在于”桥接模块”——将框架特定的组件封装为
mount/update/unmount三方法接口,状态共享通过框架无关的发布-订阅机制实现 - 生产部署需要建立完整的版本管理、灰度发布、容灾降级、预加载缓存体系,MF 2.0 的插件系统为这些能力提供了标准化的扩展点
本章讨论了 Module Federation 从 1.0 到 2.0 的跨越、也讨论了 MF 与 Rspack 这个新一代 Rust 构建工具的结合。这两个主题合在一起、揭示了前端工程领域的一个重要趋势——“基础设施的 Rust 化”。从 Deno(用 Rust 重写的 V8 配套 runtime)、到 Biome(用 Rust 写的 lint/format 工具、曾叫 Rome)、到 SWC(用 Rust 写的 JS/TS 编译器)、到 Rspack(用 Rust 写的 bundler)、到 Turbo(Vercel 用 Rust 写的 monorepo 构建工具)、到 RsPress(Rust 驱动的文档站点工具)——前端工程基础设施正在经历一次彻底的重写、从 Node.js + JavaScript 换成 Rust + 原生能力。这种”基础设施 Rust 化”的本质原因、不是”Rust 更时髦”、而是”JavaScript 作为脚本语言、天生不适合做计算密集型的编译任务”——构建工具需要做 AST 解析、依赖图分析、代码生成这些”算法密集”的工作、它们更适合用系统级语言实现。这种趋势对前端工程师意味着什么?意味着未来十年、“懂一点 Rust”会从”加分项”变成”基本素养”——就像十年前”懂一点 Node.js”从加分项变成基本素养一样。
与我们在《Vue 3 源码》第 7 章讨论的 Vue 编译器、在《Rust 编译器源码》里讨论的 rustc 本身——这些都是”编译器工程”这个古老学科在现代前端领域的具体应用。编译器工程的核心问题从 50 年代的 FORTRAN 到今天的 Rspack、变化不大——词法分析、语法分析、语义分析、代码生成、优化——只是应用领域和目标平台在不断演化。读懂 Rspack 和 MF 的集成、也是在训练你”读懂任何编译器”的基础能力——这种能力一旦掌握、你就能在 Babel、SWC、Esbuild、Rspack、Turbo 这些新工具出现时快速理解它们的设计思路、不会被”每隔两年一个新工具”的节奏所困扰**。
本章讨论的”跨框架 MF 集成”也是一个值得深思的话题——它展现了一种真正意义上的”前端生态互操作”——让 React 和 Vue 这两个过去被认为”老死不相往来”的框架、可以在同一个页面里协同工作。这种互操作的价值、超越了技术本身——它让”技术栈选型”从一个”一次性、全有或全无”的决定、变成了一个”渐进式、模块化”的演化。一家公司可以同时拥有 Vue 和 React 的子应用、用 MF 让它们共存——不需要”全部迁移到 React”或者”全部迁移到 Vue”这种战略级决策**。这种自由度、是微前端从”架构拆分”进化到”技术生态协同”的关键一步——也是 MF 2.0 作为”模块联邦基础设施”真正的价值所在。
下一章我们将讨论 Web Components——这是另一个”跨框架互操作”的答案、只是路径完全不同。Web Components 是从浏览器原生能力出发、MF 是从构建工具出发——两种路径、同一个目标。对比它们的差异、会让你对”如何设计一个可互操作的生态系统”有更深的理解。
思考题
-
类型安全的边界:MF 2.0 的类型系统在开发时提供了类型检查,但远程模块在运行时加载——远程应用可能在宿主不知情的情况下变更接口。除了类型声明同步之外,你能设计一个运行时的契约验证机制来捕获接口不兼容问题吗?
-
版本协商的极端场景:假设宿主依赖
react@18.2.0,远程 A 依赖react@18.3.0,远程 B 依赖react@17.0.2(均配置singleton: true)。请分析 MF 2.0 运行时的版本协商会如何处理这个三方冲突,以及可能产生的运行时问题。 -
跨框架性能优化:本章的
VueRemoteWrapper在每次componentProps变化时都会调用bridge.update()。请设计一个优化方案减少不必要的跨框架通信——提示:考虑浅比较、批量更新、以及 React 和 Vue 更新时机的差异。 -
容灾策略评估:假设你的应用有一个核心的结账远程模块,加载失败直接影响营收。请设计一套分级容灾方案,覆盖从”CDN 节点故障”到”远程应用全面不可用”的各级故障场景。
-
架构演进方向:Module Federation 本质上是”运行时模块链接”。随着 WebAssembly Component Model 和 Import Maps 标准的演进,你认为 MF 的哪些能力会被浏览器原生替代,哪些仍需要运行时解决方案?