Runtime API

若使用构建插件,项目启动时将自动创建 ModuleFederation 实例并存储于内存中。此时可直接调用 API,API 会自动从内存中获取构建运行时创建的 ModuleFederation 实例。

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

loadRemote('remote1');

若未使用构建插件,则需手动创建 ModuleFederation 实例,之后调用相应 API。

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

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

mf.loadRemote('remote1');
  • 什么是 ModuleFederation 实例 ?

ModuleFederation 实例是 ModuleFederation 类的实例,它包含了 ModuleFederation 运行时的所有功能。

你可以在控制台输入 __FEDERATION__.__INSTANCES__ 来查看已经创建好的实例。

createInstance

用于创建 ModuleFederation 实例。

  • 什么时候使用 createInstance

为了保证 ModuleFederation 实例的唯一性,我们在构建插件创建实例后,会将其存储到内存中,导出的 API 都是先从内存中获取 ModuleFederation 实例,然后再调用 ModuleFederation 实例的 API。这也是为什么 loadRemote 等 API 可以直接使用的原因。

这种单例模式适用于大多数场景,但如果在以下场景,你需要使用 createInstance

  • 没有使用构建插件(纯运行时场景)
  • 需要创建多个 ModuleFederation 实例,每个实例的配置不同
  • 希望使用 ModuleFederation 分治特性,封装相应的 API 提供给其他项目使用
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 废弃

Warning

此 API 将被废弃。如果需要获取已创建的实例,可以使用 getInstance 获取已创建的实例。

如果需要创建新的实例,请使用 createInstance

  • Type: init(options: InitOptions): void
  • 用于运行时动态注册 Vmok 模块
  • InitOptions:
  Type declaration
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"
        }
    ],
});

如何迁移

Build Plugin(使用构建插件)

  1. 移除 init API 的调用
  2. 使用 registerShared 代替 init 中的 shared 配置
  3. 使用 registerRemotes 代替 init 中的 remotes 配置
  4. 使用 registerPlugins 代替 init 中的 plugins 配置
  5. 使用 getInstance 获取构建插件创建的 ModuleFederation 实例
- 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({
+ const instance = getInstance();
- name: 'mf_host',
- remotes: [
-   {
-     name: 'remote',
-     entry: 'http://localhost:2001/mf-manifest.json',
-   }
- ],
+ registerRemotes([
+   {
+     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"
-     }
-   },
- },
+ registerShared({
+   react: {
+     version: "18.0.0",
+     scope: "default",
+     lib: ()=> React,
+     shareConfig: {
+       singleton: true,
+       requiredVersion: "^18.0.0"
+     }
+   }
+ });
- plugins: [mfRuntimePlugin()]
+ registerPlugins([mfRuntimePlugin()]);
-});

Pure Runtime(未使用构建插件)

- 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()]
});

如果没有使用构建插件,你可以使用 createInstance替换 init ,其参数完全一致。

getInstance

  • Type: getInstance(): ModuleFederation
  • 获取构建插件创建的 ModuleFederation 实例

当使用了构建插件后,可以调用 getInstance 获取构建插件创建的 ModuleFederation 实例。

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

const mfInstance = getInstance();
mfInstance.loadRemote('remote/util');

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

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;
}
  • Details

info: 请谨慎设置 force:true !

如果设置 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

  • type
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;
      },
    }
  ]);

registerShared

  Type declaration
function registerShared(shared: Shared) {}

type Shared = {
  [pkgName: string]: ShareArgs | ShareArgs[];
}
type ShareArgs = (SharedBaseArgs & {
    get: SharedGetter;
}) | (SharedBaseArgs & {
    lib: () => Module;
}) | SharedBaseArgs;
type SharedBaseArgs = {
    version?: string;
    shareConfig?: SharedConfig;
    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"
          }
      }
  });

loadShare

  • Type: loadShare(pkgName: string, extraOptions?: { customShareInfo?: Partial<Shared>;resolver?: (sharedOptions: ShareInfos[string]) => Shared;})
  • 获取 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,默认会返回已加载且最高版本的 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

  • Type: loadRemote(id: string)
  • 加载远程模块
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;
  // 是否需要预加载模块的接口,默认值为 false,具体参考<性能优化>中<Interface Prefetch 接口预请求>章节,@vmok/kit 版本需要大于 1.7.6
  prefetchInterface?: boolean;
  // 需要预加载的 expose
  // 默认预加载所有的 expose
  // 提供了 expose 时只会加载所需要的 expose
  exposes?: Array<string>; // 默认请求
  // 默认为 sync,只会加载 expose 中引用的同步代码
  // 设置为 all 时将加载同步引用和异步引用
  resourceCategory?: 'all' | 'sync';
  // 未配置值时默认加载所有的依赖
  // 配置了依赖后仅会加载配置选项
  depsRemote?: boolean | Array<depsPreloadArg>;
  // 未配置时不过滤资源
  // 配置了后将会过滤不需要的资源
  filter?: (assetUrl: string) => boolean;
};
  • Details

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

  • remoteremoteEntry
  • remoteexpose
  • remote 的同步资源还是异步资源
  • remote 依赖的 remote 资源
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'],
      },
  ]);