微前端源码精讲

第11章 Module Federation 2.0 与 Rspack

作者 杨艺韬 · 9,540 字 · 发布于 · 更新于

第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——它同样是跨框架互操作的一种答案,只是从浏览器原生能力出发,而不是从构建工具出发。

思考题

  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 的哪些能力会被浏览器原生替代,哪些仍需要运行时解决方案?