微前端源码精讲

第11章 Module Federation 2.0 与 Rspack

作者 杨艺韬 · 12,614 字

第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-domreact-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 行号校对与默认值表

前面几节为了叙事顺畅、对运行时类名和内部字段做了一些简化——FederationHostPluginSystem 这些名字是"教学用名"、和真实仓库里的实际命名会有出入。在工程书里、这种简化是常见手法——它让读者先抓住结构、再去核对源码。但为了不让读者在对照源码时产生困惑、本节把几个关键符号的真实位置钉回到 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.typeschema 第 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.tsManifestFileName = 'mf-manifest.json')、它是 MF Devtools、Rsbuild Dashboard 等外部工具的数据源、非必要不要关掉;第三、shareStrategy 的默认值 'version-first' 意味着"优先选语义版本最高的"——如果你希望已加载版本优先(减少多版本重复加载)、要显式配 'loaded-first'

读源码时的一点建议——不要一次性把 core.ts 从头读到尾、而是带着"我想搞清楚 loadRemote 到底做了什么"这种具体问题去读。沿着 loadRemote(在 80e9597 上是 core.ts 第 391 行)→ remoteHandler.loadRemotepackages/runtime-core/src/remote/index.tspackages/runtime-core/src/module/index.ts 里的 Module.get 这条链、追一个真实请求的完整生命周期——这种"沿着一次调用追下去"的读源码方法、比"按目录把每个文件都过一遍"的遍历式阅读、效率高得多。

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 是从构建工具出发——两种路径、同一个目标对比它们的差异、会让你对"如何设计一个可互操作的生态系统"有更深的理解

思考题

  1. 类型安全的边界:MF 2.0 的类型系统在开发时提供了类型检查,但远程模块在运行时加载——远程应用可能在宿主不知情的情况下变更接口。除了类型声明同步之外,你能设计一个运行时的契约验证机制来捕获接口不兼容问题吗?

  2. 版本协商的极端场景:假设宿主依赖 react@18.2.0,远程 A 依赖 react@18.3.0,远程 B 依赖 react@17.0.2(均配置 singleton: true)。请分析 MF 2.0 运行时的版本协商会如何处理这个三方冲突,以及可能产生的运行时问题。

  3. 跨框架性能优化:本章的 VueRemoteWrapper 在每次 componentProps 变化时都会调用 bridge.update()。请设计一个优化方案减少不必要的跨框架通信——提示:考虑浅比较、批量更新、以及 React 和 Vue 更新时机的差异。

  4. 容灾策略评估:假设你的应用有一个核心的结账远程模块,加载失败直接影响营收。请设计一套分级容灾方案,覆盖从"CDN 节点故障"到"远程应用全面不可用"的各级故障场景。

  5. 架构演进方向:Module Federation 本质上是"运行时模块链接"。随着 WebAssembly Component Model 和 Import Maps 标准的演进,你认为 MF 的哪些能力会被浏览器原生替代,哪些仍需要运行时解决方案?