Runtime 插件

Runtime 插件用于改写 运行时行为,而不是改写 runtime 本身。

适合这类场景:

  • 运行时改写 remote URL
  • 自定义 manifest 请求
  • 改写 script 注入逻辑
  • 覆盖 shared 最终命中结果
  • 增加容错、回退、恢复逻辑
  • 扩展新的 remote 加载方式

如果你只是想在构建配置里注册插件路径,见 runtimePlugins。如果你需要完整 hook 列表,见 Runtime Hooks

心智模型

一个 runtime 插件本质上是一个返回 ModuleFederationRuntimePlugin 的函数:

my-runtime-plugin.ts
import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime';

export default function myRuntimePlugin(): ModuleFederationRuntimePlugin {
  return {
    name: 'my-runtime-plugin',
  };
}

用函数返回插件实例,而不是直接导出对象,主要有几个好处:

  • 可以接收 options
  • 可以通过闭包保存状态
  • 同一份插件逻辑可以复用到不同环境

注册方式

Runtime 插件可以在两个阶段注册:

  1. 构建期,通过 runtimePlugins
  2. 运行时,通过 registerPlugins(...)instance.registerPlugins(...)

构建期注册

如果某个 host 在所有环境都需要这个插件,优先用构建期注册。

module-federation.config.ts
const path = require('path');

export default {
  name: 'host',
  remotes: {
    catalog: 'catalog@https://registry.example.com/mf-manifest.json',
  },
  runtimePlugins: [
    [
      path.resolve(__dirname, './plugins/rewrite-remote-entry.ts'),
      {
        fromHost: 'registry.example.com',
        toHost: 'cdn.example.com',
      },
    ],
  ],
};

运行时注册

如果插件依赖用户态、环境变量、特性开关、启动后拿到的数据,适合运行时注册。

bootstrap.ts
import { createInstance } from '@module-federation/enhanced/runtime';
import rewriteRemoteEntryPlugin from './plugins/rewrite-remote-entry';

const mf = createInstance({
  name: 'host',
  remotes: [
    {
      name: 'catalog',
      entry: 'https://registry.example.com/mf-manifest.json',
    },
  ],
});

mf.registerPlugins([
  rewriteRemoteEntryPlugin({
    fromHost: 'registry.example.com',
    toHost: 'cdn.example.com',
  }),
]);

怎么选 hook

目标Hook适用场景
改写 remote 查找输入beforeRequest需要在 remote 解析开始前改写请求内容
改写解析后的 entry URLafterResolve需要在 runtime 解析出 remoteInfo.entry 之后改写最终加载地址
自定义 manifest 请求fetch需要加 credentials、headers、重试或自定义 fetch 行为
自定义 script 注入createScript需要加 crossorigin、timeout、特殊 script 属性
在 remote init 前对齐 / 改写共享池beforeInitContainerinitContainerShareScopeMap需要控制 remote 初始化时使用哪个 share scope
改写 shared 最终命中结果resolveShare需要强制命中某个 shared,而不是用 runtime 默认选择
加载失败时兜底errorLoadRemote需要离线兜底、fallback module、分层恢复策略
扩展新的 remote 加载方式loadEntry需要完整接管 remote entry 加载过程,或实现新的 remote 类型

配方:改写解析后的 remote entry

afterResolve 适合“remote 已经解析完成,但真正加载前还要改一下最终 URL”这种场景。

典型用途:

  • CDN 切流
  • 环境切换
  • 基于注册中心改写地址
  • 域名归一化
plugins/rewrite-remote-entry.ts
import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime';

interface RewriteRemoteEntryOptions {
  fromHost: string;
  toHost: string;
}

export default function rewriteRemoteEntryPlugin(
  options: RewriteRemoteEntryOptions,
): ModuleFederationRuntimePlugin {
  return {
    name: 'rewrite-remote-entry',
    async afterResolve(args) {
      const entry = args.remoteInfo?.entry;
      if (!entry) {
        return args;
      }

      try {
        const currentUrl = new URL(entry);
        if (currentUrl.hostname !== options.fromHost) {
          return args;
        }

        const nextUrl = new URL(entry);
        nextUrl.hostname = options.toHost;
        args.remoteInfo.entry = nextUrl.toString();
      } catch {
        // 非 URL 场景直接忽略,保留原始解析结果。
      }

      return args;
    },
  };
}

为什么选这个 hook:

  • beforeRequest 太早,此时还拿不到解析后的 entry
  • afterResolve 已经给出了 remoteInfo.entry
  • 在这里改 args.remoteInfo.entry,可以保留原始 path / query / hash
只影响运行时

afterResolve 改写的是运行时加载地址。它不会自动改写类型生成或其他构建期工具使用的 remote URL。
如果你依赖远程类型生成,这条链路需要单独保持一致。

配方:自定义 manifest fetch

当你需要改 manifest 请求本身时,使用 fetch

plugins/fetch-manifest-with-credentials.ts
import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime';

export default function fetchManifestWithCredentials(): ModuleFederationRuntimePlugin {
  return {
    name: 'fetch-manifest-with-credentials',
    fetch(manifestUrl, requestInit) {
      const headers = new Headers(requestInit?.headers);
      headers.set('x-mf-host', 'host');

      return fetch(manifestUrl, {
        ...requestInit,
        credentials: 'include',
        headers,
      });
    },
  };
}

fetch 常见用途:

  • 带凭证请求
  • 增加鉴权头
  • manifest 重试
  • 自定义代理逻辑

如果你想直接复用现成的传输层恢复策略,可以看内置的 retry 插件。

配方:优先使用 host 自己的 shared

resolveShare 用来改写 shared 最终选择结果。

关键点:只改 args.scopeargs.version 这类字段,并不会自动改变最终命中项。要真正改掉结果,必须替换 args.resolver

plugins/prefer-host-react.ts
import type { ModuleFederationRuntimePlugin } from '@module-federation/enhanced/runtime';

export default function preferHostReact(): ModuleFederationRuntimePlugin {
  return {
    name: 'prefer-host-react',
    resolveShare(args) {
      if (args.pkgName !== 'react') {
        return args;
      }

      const hostVersionMap = args.shareScopeMap.default?.react;
      if (!hostVersionMap) {
        return args;
      }

      const preferredShared =
        hostVersionMap[args.version] ?? Object.values(hostVersionMap)[0];
      if (!preferredShared) {
        return args;
      }

      args.resolver = () => ({
        shared: preferredShared,
        useTreesShaking: false,
      });

      return args;
    },
  };
}

容错与 shareStrategy

做运行时容错时,errorLoadRemote 很关键。

但有个经常踩坑的点:它和 shareStrategy 强相关。

  • version-first 会在启动阶段提前加载 remote entry,用来初始化 shared
  • loaded-first 则会把 remote 加载延迟到真正访问 remote 时

这意味着:

  • version-first 时,remote 离线可能会在启动期就以 lifecycle: 'beforeLoadShare' 失败
  • loaded-first 时,通常会等到真正访问 remote 时才暴露失败

如果你的 remote 存在离线、波动、跨网络访问等情况,建议把 errorLoadRemote 和明确的 shareStrategy 一起设计。

对版本敏感的 hook

高级 hook 的参数和行为会随 runtime 演进。
afterResolvefetchcreateScriptloadEntry 这类 hook,如果你依赖比较新的参数,最好以你本地安装版本的类型定义为准。

尤其要注意这些场景:

  • createScript 里依赖新增 script attrs
  • fetch / loadEntry 里依赖更多 loader hook 上下文
  • 使用内置插件里那些文档没完全展开的生命周期 hook

相关文档