Runtime API
MF 的运行时 API 围绕 ModuleFederation 实例展开。除 createInstance 会创建并返回新的实例外,其他 API 都会基于运行时中的已有实例执行对应操作。
默认实例通常由构建插件自动创建,也可以通过 init 手动初始化。调用运行时 API 时,无需显式传入 ModuleFederation 实例,它们会自动使用运行时中的默认实例。
需要注意的是,createInstance 创建的新实例不会替换默认实例,但会被加入全局实例列表。后续可以通过 getInstance 配合 finder 回调重新获取该实例。
createInstance
用于创建 ModuleFederation 实例。
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
init 用于初始化运行时实例,但它并不总是创建新的 ModuleFederation 实例。
调用 init 时,运行时会先根据 name 和 version 查找已有实例。若找到匹配实例,则复用该实例,并通过 instance.initOptions(options) 合并本次传入的配置;若未找到匹配实例,才会创建新的实例。
因此,init 更适合用于同一个 host 多次初始化,并希望复用、扩展同一个运行时实例的场景。
- Type:
init(options: InitOptions): ModuleFederation
- 用于初始化或复用
ModuleFederation 运行时实例
- 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(使用构建插件)
- 移除
init API 的调用
- 使用 registerShared 代替
init 中的 shared 配置
- 使用 registerRemotes 代替
init 中的 remotes 配置
- 使用 registerPlugins 代替
init 中的 plugins 配置
- 使用 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()]
});
如果没有使用构建插件,你可以从 init 切换到 createInstance,因为两者接收的配置形状是一致的。但要注意实例语义不同:createInstance 总是创建新实例,而 init 可能会复用同一个 name / version 对应的已有实例,并把新配置合并进去。
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');
通过 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;
}
info: 请谨慎设置 force:true !
如果设置 force: true, 这会覆盖已经注册(且加载的模块, 并且自动删除已经加载过的模块缓存(如果已经加载过),同时在控制台输出警告,告知这操作存在风险性。
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 });
import { createInstance } from '@module-federation/enhanced/runtime';
const mf = createInstance({
name: 'mf_host',
remotes: [
{
name: 'sub1',
entry: 'http://localhost:2001/mf-manifest.json',
}
]
});
// 增加新的 remote sub2
mf.registerRemotes([
{
name: 'sub2',
entry: 'http://localhost:2002/mf-manifest.json',
}
]);
// 覆盖之前的 remote sub1
mf.registerRemotes([
{
name: 'sub1',
entry: 'http://localhost:2003/mf-manifest.json',
}
],{ force:true });
registerPlugins
function registerPlugins(plugins: ModuleFederationRuntimePlugin[]) {}
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;
},
}
]);
import { createInstance } from '@module-federation/enhanced/runtime';
import runtimePlugin from './custom-runtime-plugin';
const mf = createInstance({
name: 'mf_host',
remotes: [
{
name: 'sub1',
entry: 'http://localhost:2001/mf-manifest.json',
}
]
});
// 增加新的运行时插件
mf.registerPlugins([runtimePlugin()]);
mf.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 状态,而不是只注册到某一个当前实例。
适合这类场景:
全局插件会按 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 实例之前注册全局插件。
getRemoteInfo
function getRemoteInfo(remote: Remote): RemoteInfo {}
把 remote 配置归一化为 runtime 内部使用的 RemoteInfo 结构。
这个 helper 会补齐一些默认值,比如:
type
entryGlobalName
shareScope
import { getRemoteInfo } from '@module-federation/enhanced/runtime';
const remoteInfo = getRemoteInfo({
name: 'sub1',
entry: 'http://localhost:2001/mf-manifest.json',
});
console.log(remoteInfo.entryGlobalName);
console.log(remoteInfo.shareScope);
这是一个偏底层的 helper。大多数应用仍然应该优先使用 createInstance、registerRemotes 或构建插件去管理 remotes。
getRemoteEntry
function getRemoteEntry(params: {
origin: ModuleFederation;
remoteInfo: RemoteInfo;
remoteEntryExports?: RemoteEntryExports;
getEntryUrl?: (url: string) => string;
}): Promise<RemoteEntryExports | false | void> {}
用于加载 remote entry,并返回对应的 entry exports。
这个 API 更适合底层工具、调试、自定义 loader 或高级 runtime 集成。大多数应用应该优先使用 loadRemote。
import {
createInstance,
getRemoteInfo,
getRemoteEntry,
} from '@module-federation/enhanced/runtime';
const mf = createInstance({
name: 'mf_host',
remotes: [],
});
const remoteInfo = getRemoteInfo({
name: 'sub1',
entry: 'http://localhost:2001/mf-manifest.json',
});
const remoteEntryExports = await getRemoteEntry({
origin: mf,
remoteInfo,
});
loadScript
function loadScript(
url: string,
info: {
attrs?: Record<string, any>;
createScriptHook?: CreateScriptHookDom;
},
): Promise<void> {}
浏览器侧的底层 helper,用于插入并加载 remote entry script。
只有在你需要绕过 loadRemote / getRemoteEntry、自己控制 script 加载流程时才需要直接使用它。
import { loadScript } from '@module-federation/enhanced/runtime';
await loadScript('http://localhost:2001/remoteEntry.js', {
attrs: {
crossorigin: 'anonymous',
},
});
loadScriptNode
function loadScriptNode(
url: string,
info: {
attrs?: Record<string, any>;
loaderHook?: {
createScriptHook?: CreateScriptHookNode;
};
},
): Promise<void> {}
Node.js 侧的底层 helper,用于把 remote entry 加载到当前进程。
适合 Node 端集成或自定义 loader。浏览器环境请使用 loadScript。
import { loadScriptNode } from '@module-federation/enhanced/runtime';
await loadScriptNode('http://localhost:2001/remoteEntry.js', {
attrs: {
name: 'sub1',
globalName: 'sub1',
},
});
registerShared
详情
- shared:
{ [pkgName]: ShareArgs | ShareArgs[] }
- ShareArgs 参数:
- lib:
() => Module 同步工厂函数;不应返回 Promise。
- get:
() => () => Module 或 () => Promise<() => Module>,用于懒加载/异步加载。
- 其他:scope、shareConfig、deps 可选;version 建议填写以便版本区分。
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>);
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),
}
});
import { createInstance } from '@module-federation/enhanced/runtime';
import React from 'react';
import ReactDom from 'react-dom';
const mf = createInstance({
name: 'mf_host',
remotes: [
{
name: 'sub1',
entry: 'http://localhost:2001/mf-manifest.json',
}
]
});
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;})
- 获取
share 依赖,当全局环境有符合当前 host 的 share 依赖时,将优先复用当前已存在且满足 share 条件的依赖,否则将加载自身的依赖并存入全局缓存
- 该
API 一般不由用户直接调用,用于构建插件转换自身依赖时使用
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())
});
import { createInstance } from '@module-federation/enhanced/runtime';
import React from 'react';
import ReactDom from 'react-dom';
const mf = createInstance({
name: 'mf_host',
remotes: [
{
name: 'remote',
entry: 'http://localhost:2001/mf-manifest.json',
alias: 'app1'
}
]
});
mf.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"
}
}
});
mf.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)
- 加载远程模块
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));
import { createInstance } from '@module-federation/enhanced/runtime';
const mf = createInstance({
name: 'mf_host',
remotes: [
{
name: 'remote',
entry: 'http://localhost:2001/mf-manifest.json',
alias: 'app1'
}
]
});
// remoteName + expose
mf.loadRemote("remote/util").then((m)=> m.add(1,2,3));
// alias + expose
mf.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 可以预加载哪些内容:
remote 的 remoteEntry
remote 的 expose
remote 的同步资源还是异步资源
remote 依赖的 remote 资源
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'],
},
]);
import { createInstance } from '@module-federation/enhanced/runtime';
const mf = createInstance({
name: 'mf_host',
remotes: []
});
mf.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 模块
mf.preloadRemote([
{
nameOrAlias: 'sub1',
filter(assetUrl) {
return assetUrl.indexOf('ignore') === -1;
},
depsRemote: [{ nameOrAlias: 'sub1-button' }],
},
]);
// 预加载 sub2 模块
// 预加载 sub2 下的所有 expose
// 预加载 sub2 的同步资源和异步资源
mf.preloadRemote([
{
nameOrAlias: 'sub2',
resourceCategory: 'all',
},
]);
// 预加载 sub3 模块的 add expose
mf.preloadRemote([
{
nameOrAlias: 'sub3',
resourceCategory: 'all',
exposes: ['add'],
},
]);