Runtime API

MF 的运行时 API 围绕 ModuleFederation 实例展开。默认实例通常由构建插件自动创建,调用运行时 API 时无需显式传入实例,它们会自动作用于运行时中的默认实例。

createInstance

用于创建一个新的 ModuleFederation 实例。与其他运行时 API 不同,createInstance 不会作用于默认实例,而是返回一个全新的、与默认实例隔离的实例。

什么时候使用

默认实例模式已经覆盖大多数场景。以下情况才需要使用 createInstance

  • 没有使用构建插件(纯运行时场景)
  • 需要在同一应用中创建多个配置不同的 ModuleFederation 实例
  • 希望基于 Module Federation 的分治特性,封装一组 API 提供给其他项目使用

行为说明

  • 每次调用都会创建一个全新的实例,并加入全局实例列表
  • 不会根据 name / version 查找已有实例,也不会把新配置合并到已有实例上
  • 新实例不会替换默认实例;如需在其他地方拿到它,可通过 getInstance 取回

示例

import { createInstance } from '@module-federation/enhanced/runtime';

const mf = createInstance({
  name: 'host',
  remotes: [
    {
      name: 'sub1',
      entry: 'http://localhost:8080/mf-manifest.json'
    }
  ]
});

mf.loadRemote('sub1/util').then((m) => m.add(1, 2, 3));

init 谨慎使用

用于初始化或复用 ModuleFederation 运行时实例。

init 并不总是创建新实例:调用时运行时会先根据 nameversion 查找已有实例,若找到则复用,并通过 instance.initOptions(options) 合并本次传入的配置;若未找到,才会创建新的实例。

因此,init 更适合用于同一个 host 多次初始化,并希望复用、扩展同一个运行时实例的场景。

Warning
  • 如果你需要创建一个完全独立的新实例,请使用 createInstance
  • 如果你只是想获取由构建插件创建的实例,请使用 getInstance
  • Type: init(options: InitOptions): ModuleFederation
  InitOptions Type
type InitOptions {
  // 当前 host 的名称
  name: string;
  // 依赖远程模块的列表
  // tip: 在运行时配置的 remotes 和构建插件传入的类型和数据并不完全一致
  remotes: Array<RemoteInfo>;
  // 当前 host 需要共享出去的依赖列表
  // 在使用构建插件时,用户可以在构建插件中配置需要共享的依赖,构建插件将会将需要共享的依赖注入到运行时的 shared 配置中
  // shared 在运行时传入时必须要要手动传入版本实例引用,因为在运行时无法直接
  shared?: {
    [pkgName: string]: ShareArgs | ShareArgs[];
  };
};

type ShareArgs =
  | (SharedBaseArgs & { get: SharedGetter })
  | (SharedBaseArgs & { lib: () => Module });

type SharedBaseArgs = {
  version: string;
  shareConfig?: SharedConfig;
  scope?: string | Array<string>;
  deps?: Array<string>;
  strategy?: 'version-first' | 'loaded-first';
};

type SharedGetter = (() => () => Module) | (() => Promise<() => Module>);

type RemoteInfo = {
  alias?: string;
};

interface RemotesWithEntry {
  name: string;
  entry: string;
}

type ShareInfos = {
  // 依赖的包名和依赖的基础信息、共享策略
  [pkgName: string]: Share;
};

type Share = {
  // 共享依赖的版本
  version: string;
  // 当前的共享依赖被哪些模块消费了
  useIn?: Array<string>;
  // 共享的依赖来自哪个模块
  from?: string;
  // 获取共享依赖的实例的工厂函数,当无法加载缓存的共享实例时将加载自身的共享依赖
  lib: () => Module;
  // 共享策略,将以一个什么策略来决定共享依赖的复用
  shareConfig?: SharedConfig;
  // share 间的依赖
  deps?: Array<string>;
  // 当前共享依赖放在什么 scope 下面,默认为 default
  scope?: string | Array<string>;
};
  示例
import { init, loadRemote } from '@module-federation/enhanced/runtime';

init({
  name: "mf_host",
  remotes: [
    {
      name: "remote",
      // 配置别名后可直接通过别名加载
      alias: "app1",
      // 通过指定模块的 manifest.json 文件地址来决定加载的模块
      entry: "http://localhost:2001/mf-manifest.json"
    }
  ],
});
推荐迁移

如果你的代码当前在用 init,可以按场景迁移到推荐方案。

迁移方案 - 使用构建插件

移除 init 调用,改用 getInstance 取实例,并用 registerRemotes / registerShared / registerPlugins 分别注册原本传给 init 的配置。

- import { init } from '@module-federation/enhanced/runtime';
+ import { registerShared, registerRemotes, registerPlugins, getInstance } from '@module-federation/enhanced/runtime';
  import React from 'react';
  import mfRuntimePlugin from 'mf-runtime-plugin';

- const instance = init({
-   name: 'mf_host',
-   remotes: [
-     {
-       name: 'remote',
-       entry: 'http://localhost:2001/mf-manifest.json',
-     },
-   ],
-   shared: {
-     react: {
-       version: '18.0.0',
-       scope: 'default',
-       lib: () => React,
-       shareConfig: {
-         singleton: true,
-         requiredVersion: '^18.0.0',
-       },
-     },
-   },
-   plugins: [mfRuntimePlugin()],
- });
+ const instance = getInstance();
+ registerRemotes([
+   {
+     name: 'remote',
+     entry: 'http://localhost:2001/mf-manifest.json',
+   },
+ ]);
+ registerShared({
+   react: {
+     version: '18.0.0',
+     scope: 'default',
+     lib: () => React,
+     shareConfig: {
+       singleton: true,
+       requiredVersion: '^18.0.0',
+     },
+   },
+ });
+ registerPlugins([mfRuntimePlugin()]);

迁移方案 - 纯运行时

直接换成 createInstance,配置形状不变。但要注意语义不同:createInstance 始终创建新实例,而 init 可能会复用同一个 name / version 对应的已有实例,并把新配置合并进去。

- import { init } from '@module-federation/enhanced/runtime';
+ import { createInstance } from '@module-federation/enhanced/runtime';

- const instance = init({
+ const instance = createInstance({
    name: 'mf_host',
    remotes: [
      {
        name: 'remote',
        entry: 'http://localhost:2001/mf-manifest.json',
      },
    ],
    shared: {
      react: {
        version: '18.0.0',
        scope: 'default',
        lib: () => React,
        shareConfig: {
          singleton: true,
          requiredVersion: '^18.0.0',
        },
      },
    },
    plugins: [mfRuntimePlugin()],
  });

getInstance

  • Type: getInstance(): ModuleFederation | null
  • Type: getInstance(finder: (instance: ModuleFederation) => boolean): ModuleFederation | null
  • 获取默认的 ModuleFederation 实例,或者返回首个匹配 finder 回调的已注册实例

当使用构建插件或 init 创建默认实例后,可以调用 getInstance() 获取这个默认实例。

import { getInstance } from '@module-federation/enhanced/runtime';

const mfInstance = getInstance();
if (!mfInstance) {
  throw new Error('Module Federation instance is not initialized');
}

mfInstance.loadRemote('remote/util');

如果没有使用构建插件,调用 getInstance 会抛出异常,此时你需要使用 createInstance 来创建一个新的实例。

通过 createInstance 创建的实例不会替换默认实例,但仍然会注册到全局实例列表中。所以即使你没有保存它的返回值,也可以通过给 getInstance 传入 finder 回调把它找回来。

finder 回调的行为和 Array.prototype.find 类似:运行时会遍历当前已注册的实例,并返回第一个匹配项。如果没有找到匹配实例,getInstance 会返回 null

const targetInstance = getInstance(
  (instance) => instance.name === 'remote-host',
);

if (targetInstance) {
  targetInstance.loadRemote('remote/util');
}

registerRemotes

  Type declaration
function registerRemotes(remotes: Remote[], options?: { force?: boolean }) {}

type Remote = (RemoteWithEntry | RemoteWithVersion) & RemoteInfoCommon;

interface RemoteInfoCommon {
  alias?: string;
  shareScope?: string;
  type?: RemoteEntryType;
  entryGlobalName?: string;
}

interface RemoteWithEntry {
  name: string;
  entry: string;
}

interface RemoteWithVersion {
  name: string;
  version: string;
}
Warning

设置 force: true 后,新注册的模块会覆盖已经注册且加载的模块,并且自动删除已经加载过的模块缓存。此时在控制台会输出警告,告知操作存在风险性。

Build Plugin(使用构建插件)
Pure Runtime(未使用构建插件)
import { registerRemotes } from '@module-federation/enhanced/runtime';

// 增加新的 remote sub2
registerRemotes([
  {
    name: 'sub2',
    entry: 'http://localhost:2002/mf-manifest.json',
  }
]);

// 覆盖之前的 remote sub1
registerRemotes([
  {
    name: 'sub1',
    entry: 'http://localhost:2003/mf-manifest.json',
  }
], { force: true });

registerPlugins

function registerPlugins(plugins: ModuleFederationRuntimePlugin[]) {}
Build Plugin(使用构建插件)
Pure Runtime(未使用构建插件)
import { registerPlugins } from '@module-federation/enhanced/runtime';
import runtimePlugin from './custom-runtime-plugin';

// 增加新的运行时插件
registerPlugins([runtimePlugin()]);

registerPlugins([
  {
    name: 'custom-plugin-runtime',
    beforeInit(args) {
      const { userOptions, origin } = args;
      if (origin.options.name && origin.options.name !== userOptions.name) {
        userOptions.name = origin.options.name;
      }
      console.log('[build time inject] beforeInit: ', args);
      return args;
    },
    beforeLoadShare(args) {
      console.log('[build time inject] beforeLoadShare: ', args);
      return args;
    },
    createLink({ url }) {
      const link = document.createElement('link');
      link.setAttribute('href', url);
      link.setAttribute('rel', 'preload');
      link.setAttribute('as', 'script');
      link.setAttribute('crossorigin', 'anonymous');
      return link;
    },
  }
]);

registerGlobalPlugins

function registerGlobalPlugins(plugins: ModuleFederationRuntimePlugin[]): void {}

用于把插件注册到全局 federation 状态,而不是只注册到某一个当前实例。适合这类场景:

  • 通用埋点
  • 环境级统一策略
  • host 级默认插件

全局插件会按 plugin.name 去重。

import { registerGlobalPlugins, createInstance } from '@module-federation/enhanced/runtime';

import runtimePlugin from './runtime-plugin';

registerGlobalPlugins([runtimePlugin()]);

const mf = createInstance({
  name: 'mf_host',
  remotes: [
    {
      name: 'sub1',
      entry: 'http://localhost:2001/mf-manifest.json',
    },
  ],
});

为了保证行为可预期,建议在创建或使用 runtime 实例之前注册全局插件。

registerShared

注册供 host 使用的共享依赖。运行时会优先复用全局已存在且满足条件的共享依赖,否则使用本次注册的依赖。

function registerShared(shared: Shared): void;

type Shared = {
  // 共享依赖的包名映射;同一个包可以注册多个版本(数组形式)
  [pkgName: string]: ShareArgs | ShareArgs[];
};

/**
 * ShareArgs 必须提供 lib 或 get 之一
 *
 * lib:同步工厂函数,调用时立即返回模块,不应返回 Promise。
 * 适合调用 registerShared 时已能直接拿到模块的场景,例如已 `import React from 'react'`
 *
 * get:异步 / 懒加载场景使用
 * */
type ShareArgs =
  | (SharedBaseArgs & { lib: () => Module })
  | (SharedBaseArgs & { get: SharedGetter })
  | SharedBaseArgs;

type SharedBaseArgs = {
  // 当前注册的版本号;建议填写以便版本区分
  version?: string;
  // 共享策略,控制 singleton、requiredVersion 等
  shareConfig?: SharedConfig;
  // 所属 scope,默认 'default',可同时挂在多个 scope 下
  scope?: string | Array<string>;
  // 依赖的其它共享依赖名称
  deps?: Array<string>;
  // 版本选择策略:优先按版本号 / 优先用已加载的实例
  strategy?: 'version-first' | 'loaded-first';
  loaded?: boolean;
};

interface SharedConfig {
  singleton?: boolean;
  requiredVersion: false | string;
  eager?: boolean;
  strictVersion?: boolean;
  layer?: string | null;
}

type SharedGetter = (() => () => Module) | (() => Promise<() => Module>);
Build Plugin(使用构建插件)
Pure Runtime(未使用构建插件)
import { registerShared } from '@module-federation/enhanced/runtime';
import React from 'react';
import ReactDom from 'react-dom';

registerShared({
  react: {
    version: '18.0.0',
    scope: 'default',
    lib: () => React,
    shareConfig: {
      singleton: true,
      requiredVersion: '^18.0.0',
    },
  },
  'react-dom': {
    version: '18.0.0',
    scope: 'default',
    lib: () => ReactDom,
    shareConfig: {
      singleton: true,
      requiredVersion: '^18.0.0',
    },
  },
  antd: {
    version: '1.0.0',
    scope: 'default',
    get: () => import('antd').then((m) => () => m),
  },
});

loadShare

type loadShare = (
  pkgName: string,
  extraOptions?: {
    customShareInfo?: Partial<Shared>;
    resolver?: (sharedOptions: ShareInfos[string]) => Shared;
  }
) => Promise<() => ShareModule>;

获取 share 依赖,当全局环境有符合当前 hostshare 依赖时,将优先复用当前已存在且满足 share 条件的依赖,否则将加载自身的依赖并存入全局缓存中供后续复用。

API 一般不由用户直接调用,用于构建插件转换自身依赖时使用

Build Plugin(使用构建插件)
Pure Runtime(未使用构建插件)
import { registerShared, loadShare } from '@module-federation/enhanced/runtime';
import React from 'react';
import ReactDom from 'react-dom';

registerShared({
  react: {
    version: '17.0.0',
    scope: 'default',
    lib: () => React,
    shareConfig: {
      singleton: true,
      requiredVersion: '^17.0.0',
    },
  },
  'react-dom': {
    version: '17.0.0',
    scope: 'default',
    lib: () => ReactDom,
    shareConfig: {
      singleton: true,
      requiredVersion: '^17.0.0',
    },
  },
});

loadShare('react').then((reactFactory) => {
  console.log(reactFactory());
});

如果设置了多个版本 shared,默认会返回已加载且最高版本。可以通过设置 extraOptions.resolver 来改变这个行为:

loadShare('react', {
  resolver: (sharedOptions) => {
    return (
      sharedOptions.find((i) => i.version === '17.0.0') ?? sharedOptions[0]
    );
  },
}).then((reactFactory) => {
  console.log(reactFactory()); // { version: '17.0.0' }
});

loadRemote

该 API 用于运行时加载远程模块。

type loadRemote = (remoteNameOrAlias: string) => Promise<RemoteModule>;
Build Plugin(使用构建插件)
Pure Runtime(未使用构建插件)
import { loadRemote } from '@module-federation/enhanced/runtime';

// remoteName + expose
loadRemote('remote/util').then((m) => m.add(1, 2, 3));

// alias + expose
loadRemote('app1/util').then((m) => m.add(1, 2, 3));

preloadRemote

  Type declaration
async function preloadRemote(preloadOptions: Array<PreloadRemoteArgs>){}

type depsPreloadArg = Omit<PreloadRemoteArgs, 'depsRemote'>;
type PreloadRemoteArgs = {
  // 预加载 remote 的名称和别名
  nameOrAlias: string;
  // 需要预加载的 expose
  // 默认预加载所有的 expose
  // 提供了 expose 时只会加载所需要的 expose
  exposes?: Array<string>; // 默认请求
  // 默认为 sync,只会加载 expose 中引用的同步代码
  // 设置为 all 时将加载同步引用和异步引用
  resourceCategory?: 'all' | 'sync';
  // 未配置值时默认加载所有的依赖
  // 配置了依赖后仅会加载配置选项
  depsRemote?: boolean | Array<depsPreloadArg>;
  // 未配置时不过滤资源
  // 配置了后将会过滤不需要的资源
  filter?: (assetUrl: string) => boolean;
};

通过 preloadRemote 可以在更早的阶段开始预加载模块资源,避免出现瀑布请求,preloadRemote 可以预加载哪些内容:

  • remoteremoteEntry
  • remoteexpose
  • remote 的同步资源还是异步资源
  • remote 依赖的 remote 资源

preloadRemote 会等待本次预加载涉及的资源完成。如果资源全部成功加载或命中缓存,Promise 会 resolve;如果有资源加载失败或超时,Promise 会 reject,并在错误对象上携带本次预加载的资源结果。

如果只关心最终结果,可以直接用 await.then/.catch 判断:

import { preloadRemote } from '@module-federation/enhanced/runtime';

try {
  await preloadRemote([
    {
      nameOrAlias: 'sub1',
      exposes: ['add'],
      resourceCategory: 'all',
    },
  ]);

  console.log('sub1/add preload success');
} catch (error) {
  console.error('sub1/add preload failed', error);
}

如果需要统计具体哪些资源成功、失败、超时或命中缓存,可以读取错误对象上的 results

type PreloadRemoteError = Error & {
  results?: Array<{
    id: string;
    results: Array<{
      url: string;
      status: 'success' | 'error' | 'timeout' | 'cached';
      resourceType: 'manifest' | 'remoteEntry' | 'js' | 'css';
      error?: unknown;
    }>;
  }>;
};

preloadRemote([
  {
    nameOrAlias: 'sub1',
    exposes: ['add'],
    resourceCategory: 'all',
  },
]).catch((error: PreloadRemoteError) => {
  const failedResources =
    error.results
      ?.flatMap((remoteResult) =>
        remoteResult.results.map((resource) => ({
          id: remoteResult.id,
          ...resource,
        })),
      )
      .filter(
        (resource) =>
          resource.status === 'error' || resource.status === 'timeout',
      ) ?? [];

  failedResources.forEach((resource) => {
    console.error(
      `[preloadRemote] ${resource.id} ${resource.resourceType} failed`,
      resource.url,
      resource.error,
    );
  });
});

如果没有指定 exposes,本次预加载的资源 id 是 remoteName/*。如果指定了 exposes,运行时会按 expose 单独生成资源,资源 id 是 remoteName/expose

Build Plugin(使用构建插件)
Pure Runtime(未使用构建插件)
import { registerRemotes, preloadRemote } from '@module-federation/enhanced/runtime';

registerRemotes([
  {
    name: 'sub1',
    entry: 'http://localhost:2001/mf-manifest.json',
  },
  {
    name: 'sub2',
    entry: 'http://localhost:2002/mf-manifest.json',
  },
  {
    name: 'sub3',
    entry: 'http://localhost:2003/mf-manifest.json',
  },
]);

// 预加载 sub1 模块
// 过滤资源名称中携带 ignore 的资源信息
// 只预加载子依赖的 sub1-button 模块
preloadRemote([
  {
    nameOrAlias: 'sub1',
    filter(assetUrl) {
      return assetUrl.indexOf('ignore') === -1;
    },
    depsRemote: [{ nameOrAlias: 'sub1-button' }],
  },
]);

// 预加载 sub2 模块
// 预加载 sub2 下的所有 expose
// 预加载 sub2 的同步资源和异步资源
preloadRemote([
  {
    nameOrAlias: 'sub2',
    resourceCategory: 'all',
  },
]);

// 预加载 sub3 模块的 add expose
preloadRemote([
  {
    nameOrAlias: 'sub3',
    resourceCategory: 'all',
    exposes: ['add'],
  },
]);