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

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

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

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

配置项速览

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

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

不同 shareScope 组合的效果

当消费者初始化某个生产者时,会先根据双方的 shareScope 配置把「共享池」对齐。让生产者知道哪些共享池要复用的,随后再按自己的 shareScope 初始化共享依赖。

为了更简单地说明对齐与初始化的关系,我们用:

  • HostShareScope 表示消费者侧为某个生产者配置的 remotes[remote].shareScope
  • RemoteShareScope 表示生产者侧配置的 shareScope
注意

不要将 shareScope / remotes[remote].shareScope 配置为 ['default'][]

  • 单个 Scope:使用字符串,不要使用数组。两者的内部实现在「共享池对齐 / 初始化」的分支上是不同的:当消费者配的是数组而生产者配的是字符串时,生产者会按消费者的列表去对齐 Scope;而生产者配成数组时,则只会按生产者的列表处理。
  • 空数组 []:会导致没有任何 Scope 被初始化(既不会初始化 default,也不会对齐其它 Scope),是错误配置。
HostShareScopeRemoteShareScope共享池行为
'default''default'default 完全共用。
['default','scope1']'default'default 共用;scope1 不会被生产者初始化(需要生产者也配置多共享池)。
'default'['default','scope1']default 共用;scope1 消费者未提供(被补成 {}),其下的 shared 无法从消费者复用,会回退到本地依赖。
['scope1','default']['scope1','scope2']scope1 共用;scope2 消费者未提供(被补成 {}),其下的 shared 无法从消费者复用。
规则速记

共享池由消费者提供,由生产者初始化 —— 消费者没列出的 Scope 会被补成 {}(不会因找不到而崩溃),生产者没列出的 Scope 则不会被初始化。两边都列出,shared 才能真正在该 Scope 下复用。

Playground

构建插件如何配置

生产者

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 决定消费者在运行时初始化生产者时,会把哪些 Scope 作为 shareScopeKeys 传入生产者。
  • 如果消费者配了多 Scope,但生产者仍是单 Scope,生产者会把 scopeMap 对齐好,但只会初始化单 Scope 的 sharing(见上面的组合表)。因此多共享池要想「真正生效」,通常需要消费者和生产者两侧都配置一致。

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

如果你不通过构建插件声明生产者和共享依赖(例如希望在运行时动态注册),可以改用 Runtime API 实现相同的多共享池效果。两个关键 API:

  • 注册生产者registerRemotescreateInstance({ remotes }),在每条生产者配置里用 shareScope: string | string[] 声明要对齐的共享池。
  • 注册共享依赖registerSharedcreateInstance({ shared }),在每条依赖配置里用 scope: string | string[] 决定它落在哪个共享池。
字段名差异

注册共享依赖时字段名是 scope,不是 shareScope(与构建插件的 shared[pkg].shareScope 不一致)。

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 在 init 阶段或共享解析阶段介入。

1. 按生产者动态改写 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:在生产者初始化共享池过程中,对每个 Scope 的 shareScope 映射做改写。
  • resolveShare:通过改写 args.resolver 来改写最终选择结果。按现在的运行时实现,只返回 { ...args, 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 current =
        args.shareScopeMap[args.scope]?.[args.pkgName]?.[args.version];
      if (current) return args;

      args.resolver = () => {
        const fallbackVersionMap = args.shareScopeMap.default?.[args.pkgName];
        if (!fallbackVersionMap) {
          return undefined;
        }

        const fallbackShared =
          fallbackVersionMap[args.version] ??
          Object.values(fallbackVersionMap)[0];

        if (!fallbackShared) {
          return undefined;
        }

        return {
          shared: fallbackShared,
          useTreesShaking: false,
        };
      };

      return args;
    },
  };
}

你也可以在 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,
      };
    },
  };
}