共享依赖隔离:多共享池(Share Scope)

shared 分到多个命名的“共享池”(例如 defaultscope1),不同池之间互不共享。

背景

Module Federation 中,shared 默认都注册在 default 这个 share scope 下。当你的系统里存在以下情况时,单一 scope 往往不够用:

  • 希望把一部分共享依赖“隔离”出来,避免与默认共享池互相影响(例如两套 React 生态并存、灰度升级、微前端子域隔离)。
  • 希望同一个包在不同业务域使用不同版本/策略,但仍保持在各自域内共享(每个域内仍可 singleton / 复用)。

多共享池(share scope)的核心价值是:把共享依赖的注册与解析切到不同的命名空间(scope)里,从而实现共享池隔离与策略分层。

配置项速览

理解多共享池最简单的方法:只看“生产者怎么配”、“消费者怎么配”、“shared 条目落在哪个池里”,不需要了解运行时内部数据结构与变量名。

  • 生产者(remote):用 shareScope 声明该 remote 会初始化哪些共享池(默认 default,支持 string | string[])。
  • 消费者(host):用 remotes[remote].shareScope 声明 host 与该 remote 需要对齐哪些共享池(默认 default)。
  • shared 条目:用 shared[pkg].shareScope 决定某个依赖注册/解析在哪个共享池(见 shared.shareScope)。

不同 shareScope 组合的效果

host 初始化某个 remote 时,会先根据双方的 shareScope 配置把“共享池”对齐(让 remote 知道哪些共享池要复用 host 的),随后 remote 再按自己的 shareScope 初始化共享依赖。

下面用 HostShareScope 表示 host(消费者)侧为某个 remote 配置的 remotes[remote].shareScope,用 RemoteShareScope 表示 remote(生产者)侧配置的 shareScope

HostShareScope(remotes[remote].shareScope)RemoteShareScope(shareScope)共享池对齐(伪代码)最终效果(关键点)
'default''default'remote['default'] = host['default']remote 的 shareScopeMap['default'] 指向 host 的 shareScopeMap['default'],共享池完全一致。
['default','scope1']'default'remote['default'] = host['default']; remote['scope1'] = host['scope1'] ?? {}remote 会把 default/scope1 都准备好,但 只会按 RemoteShareScope 初始化共享依赖(因此只会初始化 default 共享池)。要让 remote 真正使用 scope1 去解析 shared,需要把 remote(生产者)也配置为多共享池。
'default'['default','scope1']remote['default'] = host['default']; remote['scope1'] = {}remote 会初始化 default/scope1 两个 scope;但 host 只提供 defaultscope1 会被补成空对象 {},因此 remote 中声明在 scope1 下的 shared 无法从 host 复用(通常会回退到自身提供/本地依赖)。
['scope1','default']['scope1','scope2']remote['scope1'] = host['scope1']; remote['scope2'] = host['scope2'] ?? {}remote 会对齐 scope1(复用 host 的 scope1),scope2 若 host 没有会被补空 {};remote 会按 RemoteShareScope 初始化共享依赖(因此会尝试初始化 scope1/scope2 两个共享池)。

补充说明:

  • 当某个 scope 在 host 的 shareScopeMap 中不存在时,host 会先补齐为 {} 后再参与 init(因此不会出现 scopeName 找不到导致崩溃)。
  • 多共享池下,remote 会尝试把 RemoteShareScope 中列出的 scope 都初始化到位。

构建插件如何配置

构建插件需要关注三个层面的 scope:

  1. Remote(生产者)侧:remote 自身要初始化哪些 scope(shareScope)。
  2. Host(消费者)侧:host 与某个 remote 对齐哪些 scope(remotes[remote].shareScope)。
  3. Shared(依赖条目)层:某个 shared 依赖注册在哪个 scope 下(shared[pkg].shareScope)。

生产者

remote/rspack.config.ts
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';

export default {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app_remote',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/Button',
      },
      shareScope: ['default', 'scope1'],
      shared: {
        react: {
          singleton: true,
          requiredVersion: false,
          shareScope: 'default',
        },
        'react-dom': {
          singleton: true,
          requiredVersion: false,
          shareScope: 'default',
        },
        '@company/design-system': {
          singleton: true,
          requiredVersion: false,
          shareScope: 'scope1',
        },
      },
    }),
  ],
};

要点:

  • shareScope: ['default','scope1'] 决定 remoteEntry 在运行时会初始化哪些 scope。
  • shared[pkg].shareScope 决定该包最终注册/解析时使用哪个 scope;如果你把 @company/design-system 放在 scope1,它将只在 scope1 的共享池中参与版本选择与复用。

消费者

host/rspack.config.ts
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';

export default {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app_host',
      remotes: {
        app_remote: {
          external: 'app_remote@http://localhost:2001/remoteEntry.js',
          shareScope: ['default', 'scope1'],
        },
      },
    }),
  ],
};

要点:

  • remotes[remote].shareScope 决定 host 运行时在 init 这个 remote 时,会把哪些 scope 作为 shareScopeKeys 传入 remote。
  • 如果 host 配了多 scope,但 remote 仍是单 scope,remote 会把 scopeMap 对齐好,但只会初始化单 scope 的 sharing(见上面的组合表)。因此多共享池要想“真正生效”,通常需要 host/remote 两侧都配置一致。

纯运行时(Runtime API)如何配置

当你不依赖构建插件 remotes(或希望在运行时动态注册 remotes/shared)时,可以用 runtime API 来完成同样的事情:

  • remotes 的多 scope:使用 registerRemotes / createInstance({ remotes }),在 remote 配置里设置 shareScope: string | string[]
  • shared 的 scope:使用 registerShared / createInstance({ shared }),在 shared 配置里设置 scope: string | string[](注意这里字段名是 scope)。
host/runtime.ts
import React from 'react';
import { registerRemotes, registerShared } from '@module-federation/enhanced/runtime';

registerRemotes([
  {
    name: 'app_remote',
    alias: 'remote',
    entry: 'http://localhost:2001/mf-manifest.json',
    shareScope: ['default', 'scope1'],
  },
]);

registerShared({
  react: {
    version: '18.0.0',
    scope: 'default',
    lib: () => React,
    shareConfig: {
      singleton: true,
      requiredVersion: '^18.0.0',
    },
  },
  '@company/design-system': {
    version: '1.2.3',
    scope: 'scope1',
    lib: () => require('@company/design-system'),
    shareConfig: {
      singleton: true,
      requiredVersion: false,
    },
  },
});

用 Runtime Hook 做更精细的控制

多 scope 的本质是“把共享池按名称分组”。当你希望更细粒度地控制 scope 的选择、对齐与回退策略时,可以使用 runtime hooks(runtime plugin)在 init 阶段或共享解析阶段介入。

1) 按 remote 动态改写 shareScopeKeys(beforeInitContainer)

下面的例子会让 legacy_remote 永远使用 legacy scope(即使它在构建期或运行时注册时写的是别的 shareScope):

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

export function multiScopePolicyPlugin(): ModuleFederationRuntimePlugin {
  return {
    name: 'multi-scope-policy',
    async beforeInitContainer(args) {
      if (args.remoteInfo.name !== 'legacy_remote') return args;

      const hostShareScopeMap = args.origin.shareScopeMap;
      if (!hostShareScopeMap.legacy) hostShareScopeMap.legacy = {};

      args.remoteEntryInitOptions.shareScopeKeys = ['legacy'];

      return {
        ...args,
        shareScope: hostShareScopeMap.legacy,
      };
    },
  };
}

2) scope 缺失时做别名/回退(initContainerShareScopeMap / resolveShare)

  • initContainerShareScopeMap:在 remote 初始化共享池过程中,对每个 scope 的 shareScope 映射做改写。
  • resolveShare:在实际选择某个 shared 版本时介入,可用于“当前 scope 没有就回退到 default”等策略。

示例:如果 scope1 中找不到某个包,则回退用 default scope:

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

export function scopeFallbackPlugin(): ModuleFederationRuntimePlugin {
  return {
    name: 'scope-fallback',
    resolveShare(args) {
      const hasPkg =
        Boolean(args.shareScopeMap[args.scope]?.[args.pkgName]);
      if (hasPkg) return args;
      return { ...args, scope: 'default' };
    },
  };
}

你也可以在 initContainerShareScopeMap 中把某个 scope 直接别名到另一个 scope(让两个 scope 共用同一个共享池):

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

export function scopeAliasPlugin(): ModuleFederationRuntimePlugin {
  return {
    name: 'scope-alias',
    initContainerShareScopeMap(args) {
      if (args.scopeName !== 'scope1') return args;
      if (!args.hostShareScopeMap?.default) return args;

      args.hostShareScopeMap.scope1 = args.hostShareScopeMap.default;
      return {
        ...args,
        shareScope: args.hostShareScopeMap.default,
      };
    },
  };
}