Appearance
第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 带来的编译性能提升:
11.1 MF 2.0 的新能力
Module Federation 1.0 解决了一个根本问题:如何在运行时从另一个独立部署的应用中加载模块。但它留下了三个关键缺口——类型安全、运行时扩展性、动态远程管理。MF 2.0 逐一填补了这些缺口。
11.1.1 类型安全:@module-federation/typescript
在 MF 1.0 中,远程模块对宿主应用而言是一个黑盒。你通过字符串引用一个远程模块,TypeScript 编译器对它的类型一无所知。MF 2.0 通过 @module-federation/typescript 引入了完整的类型安全机制——远程应用在构建时生成类型声明,宿主应用在开发时自动拉取。
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' },
},
}),
],
};宿主应用的配置负责消费这些类型:
typescript
// 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' },
},
}),
],
};配置完成后,宿主应用引用远程模块时便获得了完整的类型推导:
typescript
// 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 的运行时行为几乎是固定的。MF 2.0 引入了强大的运行时插件系统,让你可以拦截模块联邦的每一个关键环节:
typescript
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;
}以下是两个在生产环境中极具价值的插件示例:
typescript
// 加载失败时的降级插件
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 注册:
typescript
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 静态远程和 MF 2.0 动态远程的工作模式差异:
MF 1.0 中远程应用地址必须在构建时写死。MF 2.0 通过运行时 API 彻底解决了这个问题:
typescript
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 的动态远程 + 配置中心,就是微前端世界的"服务发现"。