微前端源码精讲
第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 在生产环境的部署策略:版本管理、灰度发布、容灾降级
本章 Module Federation 源码以 module-federation/core 仓库 commit
80e9597(2026-08-17)为准,行号与默认值都对照这一提交;Rspack 相关描述按其公开文档与仓库结构核对。
2022 年底的一个深夜,我正在调试一个 Webpack 5 Module Federation 的线上问题。远程模块加载失败了,但错误信息只有一行冷冰冰的 ScriptExternalLoadError。没有类型提示告诉我远程模块的接口长什么样,没有运行时钩子让我在加载失败时做降级处理,甚至无法在不重新部署宿主应用的情况下切换远程模块的地址。
我花了四个小时定位问题——最终发现是远程应用在一次重构里顺手改了 ModuleFederationPlugin 的 name:remoteEntry.js 照常加载成功,却不再挂出宿主期待的那个全局变量,webpack 的运行时只能报 ScriptExternalLoadError(错误类型是 missing)。而宿主应用在编译期对此一无所知。
这就是 Module Federation 1.0 的困境:它打开了一扇通往跨应用模块共享的大门,却没有在门口放一盏灯。
2024 年 4 月,字节跳动 Web Infra 团队与 MF 作者 Zack Jackson 共同发布了 Module Federation 2.0(见第 9 章)。这不是一次小版本迭代,而是一次架构级的重构。类型安全、运行时插件、动态远程、跨构建工具支持——这些能力让 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/dts-plugin"]
MF2_Core --> RuntimePlugin["运行时插件系统\nbeforeRequest / onLoad / errorLoadRemote"]
MF2_Core --> DynamicRemote["动态远程\n运行时 registerRemotes()"]
MF2_Core --> CrossBuild["跨构建工具\nWebpack + Rspack + Vite"]
end
subgraph Rspack["Rspack 加持"]
RspackCore["Rust 编译核心"]
RspackCore --> Speed["构建速度\n明显提升"]
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 逐一填补了这些缺口。
11.1.1 类型安全:@module-federation/dts-plugin
在 MF 1.0 中,远程模块对宿主应用而言是一个黑盒。你通过字符串引用一个远程模块,TypeScript 编译器对它的类型一无所知。MF 2.0 通过 @module-federation/dts-plugin 引入了完整的类型安全机制——远程应用在构建时生成类型声明,宿主应用在开发时自动拉取。顺带澄清一个容易混淆的名字:仓库里另有一个 @module-federation/typescript 包,那是 MF 1.x 时代的 Federated Types webpack 插件,不是 2.0 的类型方案。
// 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 提取暴露模块的声明文件,打包为可下载归档,宿主应用开发时从远程拉取 @mf-types.zip(归档名取自 dts.consumeTypes.remoteTypesFolder),解压到项目目录下的 @mf-types/(dts.consumeTypes.typesFolder;两者默认值都是 @mf-types,见 packages/dts-plugin/src/core/configurations/hostPlugin.ts)。path mapping 不会自动生成:消费方要在 tsconfig.json 的 compilerOptions.paths 里自己加上 "*": ["./@mf-types/*"],上面的类型推导才生效。
💡 深度洞察:MF 2.0 的类型安全不仅仅是"开发体验的提升"。在微前端架构中,远程模块的接口变更是常见的故障来源。类型系统将这类问题从"运行时崩溃"前移到"编译期报错"。在传统微服务架构中,API 契约通过 OpenAPI/Protobuf 来保障;而在微前端架构中,模块联邦的类型系统扮演的正是同样的角色。
11.1.2 运行时插件系统
"运行时插件系统"是 MF 2.0 最有野心的一次架构升级——它把"模块联邦"从一套"固定的加载流程"变成了一套"可以被扩展的生命周期"。对比 MF 1.0、它的加载流程是不可改变的——加载 remoteEntry → init → get、固定流程、没有扩展点;MF 2.0 的加载流程是被切成多个生命周期钩子的——beforeInit、init、beforeRequest、onLoad、afterLoadRemote、errorLoadRemote、handlePreloadModule、beforeLoadShare、resolveShare——每一个钩子都可以被插件拦截和扩展。这种"把流程切片为钩子、让用户注入扩展"的设计模式、在 Webpack 的 compiler hooks、Vite 的 plugin API、Babel 的 visitor、Express 的 middleware 里都能看到——它是现代软件扩展性的事实标准。学会"洋葱模型 + 钩子系统"这套组合、你就能设计出任何一个需要支持长期演化的框架。
下图展示了 MF 2.0 运行时插件系统的钩子执行流程,每个阶段都可以被插件拦截和扩展:
flowchart LR
Init["init()\n初始化"] --> BeforeInit["beforeInit\n改写 options(瀑布流)"]
BeforeInit --> InitHook["init hook\n远程/共享已注册"]
InitHook --> Request["loadRemote()\n请求模块"]
Request --> BeforeRequest["beforeRequest\n改写 id / options"]
BeforeRequest --> Match["匹配 remote + expose\nafterResolve"]
Match --> Loading["加载 remoteEntry\ninit(shareScope) → get(expose)"]
Loading --> OnLoad["onLoad\n可替换返回的模块"]
OnLoad --> AfterLoad["afterLoadRemote\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 引入了强大的运行时插件系统,让你可以拦截模块联邦的每一个关键环节:
// 示意,不是源码:真实类型从 '@module-federation/enhanced/runtime' 导入 ModuleFederationRuntimePlugin 即可,
// 真实类型是各个 handler 上 hooks.lifecycle 的并集(packages/runtime-core/src/type/plugin.ts)
// 这里按"谁拥有这个钩子"分组列出常用的一部分:
interface ModuleFederationRuntimePlugin {
name: string;
apply?: (instance: ModuleFederation) => void;
// ModuleFederation.hooks(core.ts)
beforeInit?: (args: BeforeInitArgs) => BeforeInitArgs; // SyncWaterfallHook
init?: (args: InitArgs) => void; // SyncHook
beforeInitContainer?: (args: InitContainerArgs) => Promise<InitContainerArgs>;
initContainer?: (args: InitContainerArgs) => Promise<InitContainerArgs>;
// remoteHandler.hooks(remote/index.ts)
beforeRequest?: (args: BeforeRequestArgs) => BeforeRequestArgs | Promise<BeforeRequestArgs>;
onLoad?: (args: OnLoadArgs) => unknown; // 返回函数则替换模块
afterLoadRemote?: (args: AfterLoadRemoteArgs) => void; // AsyncHook,只读通知
errorLoadRemote?: (args: ErrorLoadRemoteArgs) => unknown; // 返回值即降级结果
handlePreloadModule?: (args: HandlePreloadModuleArgs) => void;
// sharedHandler.hooks(shared/index.ts)
beforeLoadShare?: (args: BeforeLoadShareArgs) => Promise<BeforeLoadShareArgs>;
resolveShare?: (args: ResolveShareArgs) => ResolveShareArgs; // SyncWaterfallHook
}
以下是两个在生产环境中极具价值的插件示例:
// 加载失败时的降级插件
const fallbackPlugin: () => ModuleFederationRuntimePlugin = () => ({
name: 'fallback-plugin',
errorLoadRemote({ id, error, lifecycle }) {
// 这个钩子还会在 beforeRequest / afterResolve / beforeLoadShare 阶段触发(例如 manifest 拉取失败时,
// afterResolve 阶段期望的返回值是 manifest 而不是模块),这里只兜底"加载模块"这一步
if (lifecycle !== 'onLoad') return;
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: () => null, __isFallback: true, moduleId: id };
},
});
// 模块加载性能监控插件
const performancePlugin: () => ModuleFederationRuntimePlugin = () => {
const timings = new Map<string, number>();
return {
name: 'performance-monitor-plugin',
beforeRequest(args) {
timings.set(args.id, performance.now());
return args; // 瀑布流钩子必须把 args 传下去
},
afterLoadRemote({ id, error }) {
// AsyncHook:无返回值、成功与失败都会触发(失败时带 error)
const start = timings.get(id);
if (start === undefined) return;
const duration = performance.now() - start;
timings.delete(id);
navigator.sendBeacon?.('/api/metrics', JSON.stringify({
type: 'mf_module_load', moduleId: id, duration, failed: Boolean(error), timestamp: Date.now(),
}));
if (duration > 3000) {
console.warn(`[MF Perf] ${id} 加载耗时 ${duration.toFixed(0)}ms,超过阈值`);
}
},
};
};
插件通过 init 注册:
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 1.0 里,远程地址是写在 webpack 配置里的字符串——remotes: { app: 'app@http://cdn.example.com/remoteEntry.js' }——一旦构建完成,这个地址就固定了。想换地址?重新构建发布。想同时支持开发环境和生产环境?搞多份配置,多次构建。想做 A/B 测试,让 1% 用户加载新版本的 remoteEntry.js?只能手写 promise 形式的 remote,或自己加载脚本再调容器的 init/get。MF 2.0 的动态远程把这件事做成了官方 API:远程地址从"构建时配置"推迟到"运行时注册"——registerRemotes([{ name: 'app', entry: dynamicUrl }]) 之后再 await loadRemote('app/Button')。灰度发布、A/B 测试、多环境切换、接入配置中心,都可以建立在这个 API 上。
下图对比了 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 中远程应用地址默认在构建时写死(要动态化得自己写 promise 形式的 remote)。MF 2.0 把它做成了运行时 API:
import { init, loadRemote, registerRemotes } from '@module-federation/enhanced/runtime';
declare function fetchABTestConfig(userId: string): Promise<{ group: 'control' | 'experiment' }>; // 业务侧实现,从略
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 是字节跳动开源的基于 Rust 的打包工具,提供了与 Webpack 高度兼容的 API,构建速度通常明显快于 webpack。对 Module Federation 而言,Rspack 的价值在于:构建速度(Rust 实现带来的加速,具体倍数随项目规模与缓存状态变化很大,以自己项目的实测为准)和原生支持(container / sharing 相关插件内置在 Rust 核心里)。
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 的性能优势主要来自语言层面的差异:JavaScript 构建工具绕不过 JIT 预热、GC 暂停这些运行时成本,而 Rust 编译成原生机器码,没有运行时 GC,还能用多线程并行处理。对 MF 这种"遍历所有模块、分析 import/export、构建依赖图、生成 chunk"的 CPU 密集型任务,这些差异会直接体现在构建时间上。
Rspack 在 Module Federation 场景下有三个关键的架构优势。第一,并行模块解析——使用 Rust 的 rayon 库在多线程中并行解析模块依赖图,暴露模块和共享依赖的分析与主应用的模块解析同时进行。第二,增量编译粒度——以模块为粒度,当远程应用只修改了一个暴露模块时,只需重新编译该模块及其直接依赖。第三,零拷贝的共享依赖分析——直接在 Rust 内存中操作模块元数据,避免 JavaScript 对象的序列化/反序列化开销。
┌───────────────────────────────────────────────────┐
│ Rspack 编译管线 │
│ ┌────────┐ ┌──────────┐ ┌──────────────┐ │
│ │解析阶段 │──▶│模块图构建 │──▶│ 代码生成阶段 │ │
│ │ (Rust) │ │ (Rust) │ │ (Rust) │ │
│ └────────┘ └──────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ MF 远程 共享依赖 remoteEntry.js │
│ 引用解析 版本分析 生成 │
│ (Rust 侧) (Rust 侧) (Rust 侧) │
│ │
│ 关键:container/sharing 分析在 Rust 侧完成; │
│ manifest 与 dts 生成仍在 JS 侧 │
└───────────────────────────────────────────────────┘
💡 深度洞察:类型提取(dts generation)往往是 Rspack MF 构建中最慢的环节,因为它依赖 TypeScript Compiler API,运行在 JavaScript 侧。这也是为什么
compileInChildProcess: true如此重要——它将类型编译放到子进程中,避免阻塞主构建流程。未来原生实现的 TypeScript 编译器(官方正在用 Go 移植)成熟后,这个瓶颈有望缓解。
11.3 @module-federation/enhanced 运行时源码分析
11.3.1 运行时初始化流程
@module-federation/enhanced/runtime(转发到 @module-federation/runtime)的入口是 init 函数,它创建一个 ModuleFederation 实例来管理所有远程应用的生命周期——下面为了和 1.0 的"宿主"术语对应、把它简称为 FederationHost,真实类名与位置见 11.3.5:
// 核心结构(按 packages/runtime-core/src/core.ts 与 packages/runtime/src/index.ts 精简)
class FederationHost /* 源码里叫 ModuleFederation */ {
options: Options;
moduleCache: Map<string, Module> = new Map(); // 每个 remote 一个 Module 实例
snapshotHandler: SnapshotHandler;
sharedHandler: SharedHandler; // 自带 beforeLoadShare / resolveShare 等钩子
remoteHandler: RemoteHandler; // 自带 beforeRequest / onLoad / errorLoadRemote 等钩子
// hooks 里只有四个钩子(实例上另有 loaderHook / bridgeHook 两组,见 11.3.5);加载/共享相关的钩子分别挂在两个 handler 上
hooks = new PluginSystem({
beforeInit: new SyncWaterfallHook<BeforeInitArgs>('beforeInit'),
init: new SyncHook<[InitArgs]>(),
beforeInitContainer: new AsyncWaterfallHook<InitContainerArgs>('beforeInitContainer'),
initContainer: new AsyncWaterfallHook<InitContainerArgs>('initContainer'),
});
constructor(userOptions: UserOptions) {
this.snapshotHandler = new SnapshotHandler(this);
this.sharedHandler = new SharedHandler(this);
this.remoteHandler = new RemoteHandler(this);
// 内置插件(snapshot、preload assets)先于用户插件注册
this.registerPlugins([snapshotPlugin(), generatePreloadAssetsPlugin(), ...(userOptions.plugins || [])]);
this.options = this.formatOptions(defaultOptions, userOptions);
}
formatOptions(globalOptions: Options, userOptions: UserOptions): Options {
// beforeInit 是瀑布流:插件可以改写 userOptions / options
const { userOptions: u, options: o } =
this.hooks.lifecycle.beforeInit.emit({ origin: this, userOptions, options: globalOptions, shareInfo });
const remotes = this.remoteHandler.formatAndRegisterRemote(o, u);
const { allShareInfos } = this.sharedHandler.registerShared(o, u);
const optionsRes = { ...globalOptions, ...userOptions, remotes, shared: allShareInfos /* ... */ };
this.hooks.lifecycle.init.emit({ origin: this, options: optionsRes });
return optionsRes;
}
initOptions(userOptions: UserOptions): Options { // init() 复用实例时走这里
this.registerPlugins(userOptions.plugins);
return (this.options = this.formatOptions(this.options, userOptions));
}
async loadRemote<T>(id: string, options?: { loadFactory?: boolean; from: 'build' | 'runtime' }) {
return this.remoteHandler.loadRemote<T>(id, options);
}
registerRemotes(remotes: Remote[], options?: { force?: boolean }): void {
return this.remoteHandler.registerRemotes(remotes, options);
}
}
// packages/runtime/src/index.ts:按 name(+version)在全局 __FEDERATION__.__INSTANCES__ 里找同名实例
let FederationInstance: ModuleFederation | null = null;
export function init(options: UserOptions): ModuleFederation {
const instance = getGlobalFederationInstance(options.name, options.version);
if (!instance) {
FederationInstance = createInstance({ ...options, id: options.id || '' });
return FederationInstance;
}
instance.initOptions({ ...options, id: options.id || '' }); // 已存在则合并配置
FederationInstance ||= instance;
return instance;
}
11.3.2 远程模块加载的完整链路
当调用 loadRemote('remoteApp/Button') 时,请求经过以下链路:
// RemoteHandler.loadRemote 核心链路(按 packages/runtime-core/src/remote/index.ts 与 module/index.ts 精简)
class RemoteHandler {
host: ModuleFederation;
async loadRemote<T>(id: string, options?: { loadFactory?: boolean; from: CallFrom }): Promise<T | null> {
const { host } = this;
try {
// 1. beforeRequest 钩子(瀑布流,插件可改写 id / options)
const { id: idRes } = await this.hooks.lifecycle.beforeRequest.emit({ id, options: host.options, origin: host });
// 2. 解析标识符:'remoteApp/Button' -> { remote, expose },支持 name 或 alias 前缀
const remoteSplitInfo = matchRemoteWithNameAndExpose(host.options.remotes, idRes);
if (!remoteSplitInfo) error(RUNTIME_004 );
// 3. afterResolve(sharedHandler 的瀑布流钩子,snapshot 插件在这里补全 entry 等信息)
const { remote, expose, remoteSnapshot } =
await host.sharedHandler.hooks.lifecycle.afterResolve.emit({ id: idRes, ...remoteSplitInfo, options: host.options, origin: host });
// 4. 每个 remote 只有一个 Module 实例,缓存在 host.moduleCache 里
let module = host.moduleCache.get(remote.name);
if (!module) {
module = new Module({ host, remoteInfo: getRemoteInfo(remote) });
host.moduleCache.set(remote.name, module);
}
// 5. Module.get:拉取 remoteEntry -> 初始化容器(共享依赖协商在此发生)-> 取 expose 的工厂函数
const moduleOrFactory = await module.get(idRes, expose, options, remoteSnapshot);
// 6. onLoad 钩子:返回函数则用它替换模块
const moduleWrapper = await this.hooks.lifecycle.onLoad.emit({
id: idRes, expose, remote, exposeModule: moduleOrFactory, moduleInstance: module, origin: host, /* ... */
});
return typeof moduleWrapper === 'function' ? (moduleWrapper as T) : moduleOrFactory;
} catch (error) {
// 7. errorLoadRemote 钩子:插件返回的非 undefined 值即降级结果
const failOver = await this.hooks.lifecycle.errorLoadRemote.emit({
id, error, from: options?.from ?? 'runtime', lifecycle: 'onLoad', origin: host,
});
if (!failOver) throw error;
return failOver as T;
} finally {
// afterLoadRemote 是只读的 AsyncHook,成功、失败都会带上 id / error 通知一次
}
}
}
// Module.get / Module.init(module/index.ts):容器协议 init(shareScope, initScope, opts) -> get(expose)
class Module {
async init(id, remoteSnapshot, rawInitScope, expose) {
const remoteEntryExports = await this.getEntry(expose); // loadScript 拿到容器对象
if (this.inited) return remoteEntryExports; // 已初始化直接复用
const { remoteEntryInitOptions, shareScope, initScope } =
createRemoteEntryInitOptions(this.remoteInfo, this.host.shareScopeMap, rawInitScope);
const opts = await this.host.hooks.lifecycle.beforeInitContainer.emit({ shareScope, remoteEntryInitOptions, initScope, remoteInfo: this.remoteInfo, origin: this.host });
await remoteEntryExports.init(opts.shareScope, opts.initScope, opts.remoteEntryInitOptions); // 共享作用域在此合并
await this.host.hooks.lifecycle.initContainer.emit({ ...opts, id, remoteSnapshot, remoteEntryExports });
this.inited = true;
return remoteEntryExports;
}
async get(id, expose, options, remoteSnapshot) {
const remoteEntryExports = await this.init(id, remoteSnapshot, undefined, expose);
const moduleFactory = await remoteEntryExports.get(expose); // 容器的 get 返回工厂
return options?.loadFactory === false ? moduleFactory : moduleFactory();
}
}
11.3.3 共享依赖的版本协商算法
共享依赖协商是 Module Federation 最精妙的部分。它整个发生在运行时——构建期只把 requiredVersion 这样的版本区间写进产物,挑哪一份实例是下面这个函数在浏览器里算出来的(packages/runtime-core/src/utils/share.ts:354-474)。当多个应用都声明了对 react 的依赖,运行时按以下规则决定使用哪个版本:
// 版本协商核心:getRegisteredShare(packages/runtime-core/src/utils/share.ts,精简)
// shareScopeMap[scope][pkgName] 是 { [version]: Shared } —— 同一包的多个版本并存在作用域里
function getRegisteredShare(shareScopeMap, pkgName, shareInfo, resolveShare) {
const { shareConfig, scope = DEFAULT_SCOPE, strategy } = shareInfo; // scope 也可以是数组,源码逐个 scope 尝试
const versions = shareScopeMap[scope]?.[pkgName];
if (!shareConfig || !versions) return;
const { requiredVersion } = shareConfig;
// 规则 1:先按 shareStrategy 从作用域里挑出一个"候选版本"
// 'version-first'(默认):版本号最高者胜,但已经 loaded 的版本不会被更高的未加载版本顶掉
// 'loaded-first':已加载/加载中的优先,其次才比版本号
const findShare = strategy === 'loaded-first'
? findSingletonVersionOrderByLoaded
: findSingletonVersionOrderByVersion;
const { version: picked } = findShare(shareScopeMap, scope, pkgName);
const defaultResolver = () => {
const shared = versions[picked];
// 规则 2:singleton —— 无论满不满足 requiredVersion 都用这一份
if (shareConfig.singleton) {
if (typeof requiredVersion === 'string' && !satisfy(picked, requiredVersion)) {
const msg = `Version ${picked} from ${shared.from} of shared singleton module ${pkgName} ` +
`does not satisfy the requirement of ${shareInfo.from} which needs ${requiredVersion})`;
shareConfig.strictVersion ? error(msg) : warn(msg); // strictVersion 只改变"警告 vs 报错"
}
return { shared };
}
// 规则 3:非 singleton —— 候选版本满足就用;不满足就按注册顺序找第一个满足的
if (requiredVersion === false || requiredVersion === '*' || satisfy(picked, requiredVersion)) {
return { shared };
}
for (const [version, value] of Object.entries(versions)) {
if (satisfy(version, requiredVersion)) return { shared: value };
}
return; // 一个都不满足:返回 undefined,调用方退回用自己打包的那份
};
// 规则 4:resolveShare 瀑布流钩子可以整个替换 resolver
const params = { shareScopeMap, scope, pkgName, version: picked, shareInfo, resolver: defaultResolver, GlobalFederation };
return (resolveShare.emit(params) || params).resolver();
}
💡 深度洞察:react.dev 的 Invalid Hook Call Warning 页写明,组件代码里 import 的
react必须和react-dom内部用的是同一个模块——渲染时 react-dom 只把 Hooks 的 dispatcher 写进它自己依赖的那一份react(§10.3.4 有展开)。如果远程应用的react版本与宿主不兼容且未配置singleton: true,按上面的规则 3 运行时会各用各的那份,页面上就有两份react,Hooks 报 "Invalid hook call"。所以对 React 来说,shared.react.singleton: true实际上是必选项;react-dom、react-router以及任何依赖模块级单例状态的库同理。
11.3.4 插件系统的实现机制
MF 2.0 的插件系统借鉴了 Webpack 的 Tapable 设计。核心是瀑布流(Waterfall)Hook——每个回调的返回值作为下一个回调的输入:
// packages/runtime-core/src/utils/hooks/syncWaterfallHook.ts:21(精简)
class SyncWaterfallHook<T extends Record<string, any>> extends SyncHook<[T], T | void> {
emit(data: T): T {
for (const fn of this.listeners) {
const tempData = fn(data);
if (tempData === undefined) continue; // 不返回 = 不修改,继续传原对象
if (checkReturnData(data, tempData)) { // 返回值必须是对象、且保留原有全部字段
data = tempData;
} else {
this.onerror(`A plugin returned an unacceptable value for the "${this.type}" type.`);
break;
}
}
return data;
}
}
// packages/runtime-core/src/utils/hooks/pluginSystem.ts(精简)
class PluginSystem<T extends Record<string, any>> {
lifecycle: T;
registerPlugins: Record<string, Plugin<T>> = {};
applyPlugin(plugin: Plugin<T>, instance: ModuleFederation): void {
assert(plugin.name, 'A name must be provided by the plugin.');
if (this.registerPlugins[plugin.name]) return; // 同名插件只注册一次
this.registerPlugins[plugin.name] = plugin;
plugin.apply?.(instance);
for (const key of Object.keys(this.lifecycle)) { // 插件对象上与钩子同名的方法自动挂上去
if (plugin[key]) this.lifecycle[key].on(plugin[key]);
}
}
}
这种设计使得每个插件不仅可以读取数据,还可以修改数据并传递给下一个插件——这是 MF 2.0 运行时可扩展性的基石。
11.3.5 行号校对与默认值表
前面几节为了叙事顺畅,对运行时类名和内部字段做了一些简化——FederationHost 这个名字是"教学用名",和真实仓库里的实际命名会有出入。这种简化让读者先抓住结构,再去核对源码。但为了不让读者在对照源码时产生困惑,本节把几个关键符号的真实位置钉回到 module-federation/core 仓库上。所有行号与默认值均以 commit 80e9597(2026-08-17)为准,日后上游重构时行号会漂移。
核心运行时类的真实命名与位置——packages/runtime-core/src/core.ts 里定义的那个"承担宿主行为"的类,在源码里叫 ModuleFederation(第 67 行),而不是本章为了对应旧版 MF 1.0 术语而简化的 FederationHost。它在源码史上确实叫过 FederationHost:runtime 拆成 runtime-core + runtime 两个包是 2024-12-31 的 #3154,把类名改成 ModuleFederation 则是 2025-07-15 的 #3874(同一个 PR 加了 createInstance)。在 80e9597 上 runtime / runtime-core / enhanced 的源码里已经没有 FederationHost 这个名字,也没有兼容别名——搜不到是正常的。
关键成员与方法的行号索引——这张表可以作为你读源码时的路标。在本章某一小节看到一个概念、想去源码里找实现时,直接查这张表。
| 符号 | 文件 | 行号 | 类型 | 本章引用位置 |
|---|---|---|---|---|
ModuleFederation(类) |
packages/runtime-core/src/core.ts |
67 | class | 11.3.1 简化为 FederationHost |
hooks(核心生命周期钩子集合) |
同上 | 69–107 | PluginSystem | 11.3.1 / 11.3.4 |
beforeInit |
同上 | 70 | SyncWaterfallHook | 11.3.4 瀑布流示例 |
init |
同上 | 79 | SyncHook | 11.3.1 |
beforeInitContainer |
同上 | 89 | AsyncWaterfallHook | 11.3.2 加载链路 |
initContainer |
同上 | 97 | AsyncWaterfallHook | 11.3.2 |
moduleCache |
同上 | 110 | Map<string, Module> | 11.3.2 防重复加载 |
snapshotHandler |
同上 | 111 | SnapshotHandler | 11.3.2 远程快照查询 |
sharedHandler |
同上 | 112 | SharedHandler | 11.3.3 版本协商 |
remoteHandler |
同上 | 113 | RemoteHandler | 11.3.2 |
loaderHook |
同上 | 115–277 | PluginSystem | 11.3.4 |
bridgeHook |
同上 | 278–295 | PluginSystem | React/Vue bridge 钩子 |
constructor |
同上 | 298 | — | 11.3.1 |
initOptions |
同上 | 333 | 方法 | 11.3.1 init() 复用时复写配置 |
loadShare |
同上 | 345 | 方法 | 11.3.3 |
loadRemote |
同上 | 391 | 方法 | 11.3.2 |
preloadRemote |
同上 | 399 | 方法 | 11.5.4 预加载 |
formatOptions |
同上 | 411 | 方法 | 11.3.1 触发 beforeInit / init |
registerRemotes |
同上 | 470 | 方法 | 11.1.3 动态远程 |
registerShared |
同上 | 474 | 方法 | 11.3.3 |
beforeRequest / onLoad / afterLoadRemote / errorLoadRemote |
packages/runtime-core/src/remote/index.ts |
76 / 95 / 111 / 141 | RemoteHandler.hooks | 11.1.2 / 11.3.2 |
beforeLoadShare / resolveShare |
packages/runtime-core/src/shared/index.ts |
58 / 95 | SharedHandler.hooks | 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 顶层没有任何必填字段——name 的必填是插件 apply 里手动 throw 出来的(第 173–176 行);子结构里标了 required 的主要是这几处:library.type(schema 第 275 行)、exposes 条目的 import(第 87 行)、remotes 条目的 external(第 367 行);另有 shared 的 include/exclude 条件要求 request / version / fallbackVersion 三选一(第 613–623 行)、dts.consumeTypes.remoteTypeUrls 条目要求 api 与 zip(第 929 行)。其它字段都是"可选 + 有约定默认"、默认值在插件 apply 方法里注入。
| 选项 | 默认值 | 约定位置 | 说明 |
|---|---|---|---|
library |
{ type: 'var', name: <容器名> } |
apply 第 218 行 |
未显式配置时按 var 暴露 |
remoteType |
library.type 是合法的 externals type 就取它、否则 'script' |
apply 第 219–223 行 |
显式传 remoteType 优先 |
shareScope |
'default'(即 DEFAULT_SCOPE) |
runtime-core/constant.ts 第 1 行 | 同 scope 才能共享同一实例 |
shareStrategy |
'version-first' |
schema 第 782–786 行;运行时 utils/share.ts 第 68 行兜底 |
另一选项为 'loaded-first' |
dts |
TS 项目(能找到 tsconfig)默认开启 | apply 第 211–215 行 dts !== false;dts-plugin/src/plugins/DtsPlugin.ts 第 27–30 行 |
传 false 才整体关闭 |
dts.generateTypes |
{ generateAPITypes: true, compileInChildProcess: true, abortOnError: false, extractRemoteTypes: false, ... } |
dts-plugin/src/plugins/GenerateTypesPlugin.ts 第 19–25 行 DEFAULT_GENERATE_TYPES |
不是"不传就关" |
dts.consumeTypes |
{ abortOnError: false, consumeAPITypes: true, typesOnBuild: false }、remoteTypesFolder: '@mf-types' |
dts-plugin/src/plugins/ConsumeTypesPlugin.ts 第 15–19 行;core/configurations/hostPlugin.ts 第 13 行 |
同上 |
bridge.enableBridgeRouter |
false |
schema 第 1013–1018 行 | React 桥接路由开关、默认关 |
manifest |
true(即输出 mf-manifest.json) |
apply 第 227 行 options.manifest === false 判断 |
传 false 才禁用 |
runtimePlugins |
[] |
apply 第 189–192 行条件拼接 |
启用 experiments.provideExternalRuntime 时自动追加 |
几个容易踩坑的默认值细节——第一、shareScope 默认是字符串 'default',不是字面意义上的"所有应用自动共享"——两个应用必须写同一 shareScope 值(或都省略)才会被识别为同一作用域;第二、manifest 默认输出 mf-manifest.json,这是 MF 2.0 相对 1.0 新增的清单文件(文件名来自 sdk 包 constant.ts 的 ManifestFileName = 'mf-manifest.json'),remote 的 entry 可以直接写成这个 json(运行时据此做快照与预加载),它也是 MF Devtools 等外部工具的数据源,非必要不要关掉;第三、shareStrategy 的默认值 'version-first' 意味着"优先选语义版本最高的"(已经加载的那份不会被更高但未加载的版本顶掉)——如果你希望已加载版本优先(减少多版本重复加载),要显式配 'loaded-first';第四、dts 在 TS 项目里是默认开启的,CI 里嫌慢要显式 dts: false。
读源码时的一点建议——不要一次性把 core.ts 从头读到尾,而是带着"我想搞清楚 loadRemote 到底做了什么"这种具体问题去读。沿着 loadRemote(core.ts 第 391 行)→ remoteHandler.loadRemote(packages/runtime-core/src/remote/index.ts 第 239 行)→ packages/runtime-core/src/module/index.ts 里的 Module.get(第 284 行)这条链,追一个真实请求的完整生命周期。
11.4 跨框架的 Module Federation 实践
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 跨框架状态共享
跨框架最棘手的问题是状态共享。解决方案是引入一个框架无关的状态层——基于发布-订阅模式:
// 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 组件读写的是同一份状态,任何一方 setState 后,两边的订阅者都会收到通知。
💡 深度洞察:跨框架状态共享的本质是"谁拥有状态"的问题。状态应该属于业务域而非框架。
createCrossFrameworkStore能同时为 React 和 Vue 所用,因为其核心是发布-订阅模式——所有响应式系统的最大公约数。即使未来引入 Svelte 或 Solid,只需写一个新的绑定函数,无需改动状态层。
11.5 MF 2.0 的生产部署策略
11.5.1 版本管理与发布策略
每个远程应用独立部署,版本管理通过配置中心(远程注册表)实现:
interface RemoteAppVersion {
name: string;
version: string;
entry: string;
integrity?: string; // SRI 哈希
requiredHostVersion?: string; // 宿主最低版本要求
metadata: {
deployedAt: string;
commitHash: string;
changelog: string;
};
}
// 部署流程:上传 CDN -> 注册版本 -> 健康检查
declare function uploadToCDN(name: string, version: string): Promise<void>; // 业务侧实现,从略
declare const configCenter: { register(app: RemoteAppVersion): Promise<void> }; // 同上
async function registerNewVersion(app: RemoteAppVersion): Promise<void> {
await uploadToCDN(app.name, app.version);
const entry = `https://cdn.example.com/${app.name}/${app.version}/remoteEntry.js`;
await configCenter.register({ ...app, entry });
const health = await fetch(entry);
if (!health.ok) throw new Error(`远程入口文件不可访问: ${entry}`);
}
11.5.2 灰度发布策略
借助动态远程和运行时插件,实现精细化灰度控制:
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 }
): ModuleFederationRuntimePlugin {
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;
},
};
}
这个插件有一个限制:运行时按 remote 名把 Module 实例缓存在 host.moduleCache 里(packages/runtime-core/src/remote/index.ts:613-622),同一个 remote 第一次加载之后,后面的请求即使在 beforeRequest 里换了 entry,拿到的仍是已经加载的那个容器。所以它适合"进入页面时就定下走哪个版本、整个会话不再变"的分流;要在同一个页面里切换版本,得用 registerRemotes([...], { force: true }) 覆盖注册——force 会走 removeRemote,删掉该 remote 的 moduleCache 条目和全局容器变量(同文件 :697-832),已经渲染出去的旧版组件则要业务自己卸载重挂。
11.5.3 容灾降级策略
远程模块加载失败是必须处理的场景。通过插件实现多层防线:
declare function loadFromUrl(url: string, id: string): Promise<unknown>; // 业务侧实现,从略
declare function processCachedEntry(res: Response, id: string): Promise<unknown>; // 同上
function createResiliencePlugin(): ModuleFederationRuntimePlugin {
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, lifecycle, remote }) {
if (lifecycle !== 'onLoad') return; // 只兜底模块加载,其他阶段交回默认流程
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:CacheStorage 里的旧版 remoteEntry(通常由 Service Worker 预先写入)
// CacheStorage 按请求 URL 做 key,要用 remoteEntry 的地址去查,不能用 'remoteApp/Button' 这种模块 id
const entryUrl = remote?.entry ?? urls?.[0];
if (entryUrl && 'caches' in window) {
const cache = await caches.open('mf-remote-entries');
const cached = await cache.match(entryUrl);
if (cached) return processCachedEntry(cached, id);
}
// 策略 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 性能优化:预加载与缓存
import { preloadRemote, init } from '@module-federation/enhanced/runtime';
// 空闲时预加载
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
// 当前版本 preloadRemote 有资源失败会 reject,预加载失败不该影响页面,吞掉即可
preloadRemote([{ nameOrAlias: 'remoteApp', exposes: ['./Button', './UserCard'] }]).catch(() => {});
});
}
// 基于路由的预加载: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 }]).catch(() => {});
}, { once: true });
});
}
11.5.5 部署架构总览
下图把前面几节的部署要素放在一起:配置中心让远程地址动态化,版本管理让多版本可控切换,监控告警让故障能被及时感知,CDN 负责分发。它是一张通用的检查清单,不是某家公司的具体实现。
┌──────────────────────────────────────────────────────────────┐
│ 生产环境部署架构 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────────────────┐ │
│ │ 配置中心 │ │ 版本管理 │ │ 监控告警系统 │ │
│ │(远程注册) │ │(版本+灰度)│ │ (性能 + 错误 + 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 的构建明显提速(幅度随项目规模与缓存状态而异),container/sharing 分析在 Rust 侧完成,但 dts 类型生成仍在 JS 侧、往往是最慢的一环
@module-federation/enhanced运行时的核心是ModuleFederation实例(本章简称FederationHost,按 name 复用),通过RemoteHandler(远程加载)、SharedHandler(共享协商)、PluginSystem(插件管理)三大子系统协同工作- 跨框架集成的关键在于"桥接模块"——将框架特定的组件封装为
mount/update/unmount三方法接口,状态共享通过框架无关的发布-订阅机制实现 - 生产部署需要建立完整的版本管理、灰度发布、容灾降级、预加载缓存体系,MF 2.0 的插件系统为这些能力提供了标准化的扩展点
下一章讨论 Web Components——它同样是跨框架互操作的一种答案,只是从浏览器原生能力出发,而不是从构建工具出发。
思考题
-
类型安全的边界: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 的哪些能力会被浏览器原生替代,哪些仍需要运行时解决方案?