Module Federation 提供了一套轻量的运行时插件系统,用以实现自身的大多数功能,并允许用户进行扩展。
开发者编写的插件能够修改 Module Federation
的默认行为,并添加各类额外功能,包括但不限于:
插件提供类似 () => FederationRuntimePlugin
的函数。
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const runtimePlugin: () => FederationRuntimePlugin = function () {
return {
name: 'my-runtime-plugin',
beforeInit(args) {
console.log('beforeInit: ', args);
return args;
},
beforeRequest(args) {
console.log('beforeRequest: ', args);
return args;
},
afterResolve(args) {
console.log('afterResolve', args);
return args;
},
onLoad(args) {
console.log('onLoad: ', args);
return args;
},
async loadShare(args) {
console.log('loadShare:', args);
},
async beforeLoadShare(args) {
console.log('beforeloadShare:', args);
return args;
},
};
};
export default runtimePlugin;
注册插件(两种方式选择一种即可):
const path = require('path');
module.exports = {
plugins: [
new ModuleFederation({
// ...
runtimePlugins: [path.resolve(__dirname, './custom-runtime-plugin.ts')],
}),
],
};
import { registerPlugins } from '@module-federation/enhanced/runtime'
import runtimePlugin from 'custom-runtime-plugin.ts';
registerPlugins([runtimePlugin()]);
函数形式的插件可以 接受选项对象 并 返回插件实例,并通过闭包机制管理内部状态。
其中各部分的作用分别为:
name
属性用于标注插件名称。fn
各类钩子。插件的命名规范如下:
name
采用 xxx-plugin
格式。下面是一个例子:
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const pluginFooBar = (): FederationRuntimePlugin => ({
name: 'xxx-plugin',
//...
});
export default pluginFooBar;
当然,这里是上述内容的中文翻译:
SyncWaterfallHook
在远程容器的初始化过程之前更新联合主机配置。
function beforeInit(args: BeforeInitOptions): BeforeInitOptions;
type BeforeInitOptions = {
userOptions: UserOptions;
options: FederationRuntimeOptions;
origin: FederationHost;
shareInfo: ShareInfos;
};
interface FederationRuntimeOptions {
id?: string;
name: string;
version?: string;
remotes: Array<Remote>;
shared: ShareInfos;
plugins: Array<FederationRuntimePlugin>;
inBrowser: boolean;
}
SyncHook
在远程容器初始化期间调用。
function init(args: InitOptions): void;
type InitOptions = {
options: FederationRuntimeOptions;
origin: FederationHost;
};
AsyncWaterfallHook
在解析远程容器之前调用,用于注入容器或在查找之前更新某些内容。
async function beforeRequest(
args: BeforeRequestOptions,
): Promise<BeforeRequestOptions>;
type BeforeRequestOptions = {
id: string;
options: FederationRuntimeOptions;
origin: FederationHost;
};
AsyncWaterfallHook
在解析容器后调用,允许重定向或修改已解析的信息。
async function afterResolve(
args: AfterResolveOptions,
): Promise<AfterResolveOptions>;
type AfterResolveOptions = {
id: string;
pkgNameOrAlias: string;
expose: string;
remote: Remote;
options: FederationRuntimeOptions;
origin: FederationHost;
remoteInfo: RemoteInfo;
remoteSnapshot?: ModuleInfo;
};
AsyncHook
联合模块加载完毕时触发,允许访问和修改已加载文件的导出内容。
async function onLoad(args: OnLoadOptions): Promise<void>;
type OnLoadOptions = {
id: string;
expose: string;
pkgNameOrAlias: string;
remote: Remote;
options: ModuleOptions;
origin: FederationHost;
exposeModule: any;
exposeModuleFactory: any;
moduleInstance: Module;
};
type ModuleOptions = {
remoteInfo: RemoteInfo;
host: FederationHost;
};
interface RemoteInfo {
name: string;
version?: string;
buildVersion?: string;
entry: string;
type: RemoteEntryType;
entryGlobalName: string;
shareScope: string;
}
SyncHook
处理联合模块预加载逻辑。
function handlePreloadModule(args: HandlePreloadModuleOptions): void;
type HandlePreloadModuleOptions = {
id: string;
name: string;
remoteSnapshot: ModuleInfo;
preloadConfig: PreloadRemoteArgs;
};
AsyncHook
当联合模块加载失败时,此钩子将被触发,允许自定义错误处理策略。
它被设计为在模块加载的各个生命周期阶段失败时触发。
利用 args.lifecycle
来识别调用 errorLoadRemote
的具体生命周期阶段,从而实现适当的错误处理或回退机制。
async function errorLoadRemote(
args: ErrorLoadRemoteOptions,
): Promise<void | unknown>;
type ErrorLoadRemoteOptions = {
id: string;
error: unknown;
options?: any;
from: 'build' | 'runtime';
lifecycle: 'onLoad' | 'beforeRequest';
origin: FederationHost;
};
import { init, loadRemote } from '@module-federation/enhanced/runtime';
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const fallbackPlugin: () => FederationRuntimePlugin = function () {
return {
name: 'fallback-plugin',
errorLoadRemote(args) {
if(args.lifecycle === 'onLoad') {
const fallback = 'fallback';
return fallback;
} else if (args.lifecycle === 'beforeRequest') {
return args
}
}
};
};
init({
name: '@demo/app-main',
remotes: [
{
name: '@demo/app2',
entry: 'http://localhost:3006/remoteEntry.js',
alias: 'app2'
}
],
plugins: [fallbackPlugin()]
});
loadRemote('app2/un-existed-module').then((mod) => {
expect(mod).toEqual('fallback');
});
AsyncWaterfallHook
在尝试加载或协商联合应用之间的共享模块之前调用。
async function beforeLoadShare(
args: BeforeLoadShareOptions,
): Promise<BeforeLoadShareOptions>;
type BeforeLoadShareOptions = {
pkgName: string;
shareInfo?: Shared;
shared: Options['shared'];
origin: FederationHost;
};
SyncWaterfallHook
允许手动解析共享模块请求。
function resolveShare(args: ResolveShareOptions): ResolveShareOptions;
type ResolveShareOptions = {
shareScopeMap: ShareScopeMap;
scope: string;
pkgName: string;
version: string;
GlobalFederation: Federation;
resolver: () => Shared | undefined;
};
import { init, loadRemote } from '@module-federation/enhanced/runtime';
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const customSharedPlugin: () => FederationRuntimePlugin = function () {
return {
name: 'custom-shared-plugin',
resolveShare(args) {
const { shareScopeMap, scope, pkgName, version, GlobalFederation } = args;
if (pkgName !== 'react') {
return args;
}
// set lib
args.resolver = function () {
shareScopeMap[scope][pkgName][version] = {
lib: ()=>window.React,
loaded:true,
loading: Promise.resolve(()=>window.React)
}; // Manually replace the local share scope with the desired module
return shareScopeMap[scope][pkgName][version];
};
// set get
args.resolver = function () {
shareScopeMap[scope][pkgName][version] = {
get: async ()=>()=>window.React,
}; // Manually replace the local share scope with the desired module
return shareScopeMap[scope][pkgName][version];
};
return args;
},
};
};
init({
name: '@demo/app-main',
shared: {
react: {
version: '17.0.0',
scope: 'default',
lib: () => React,
shareConfig: {
singleton: true,
requiredVersion: '^17.0.0',
},
},
},
plugins: [customSharedPlugin()],
});
window.React = () => 'Desired Shared';
loadShare('react').then((reactFactory) => {
expect(reactFactory()).toEqual(window.React());
});
AsyncHook
在预加载处理程序执行任何预加载逻辑之前调用。
async function beforePreloadRemote(
args: BeforePreloadRemoteOptions,
): BeforePreloadRemoteOptions;
type BeforePreloadRemoteOptions = {
preloadOps: Array<PreloadRemoteArgs>;
options: Options;
origin: FederationHost;
};
AsyncHook
根据配置生成预加载资产。
async function generatePreloadAssets(
args: GeneratePreloadAssetsOptions,
): Promise<PreloadAssets>;
type GeneratePreloadAssetsOptions = {
origin: FederationHost;
preloadOptions: PreloadOptions[number];
remote: Remote;
remoteInfo: RemoteInfo;
remoteSnapshot: ModuleInfo;
globalSnapshot: GlobalModuleInfo;
};
interface PreloadAssets {
cssAssets: Array<string>;
jsAssetsWithoutEntry: Array<string>;
entryAssets: Array<EntryAssets>;
}
SyncHook
function createScript(args: CreateScriptOptions): HTMLScriptElement | {script?: HTMLScriptElement, timeout?: number } | void;
type CreateScriptOptions = {
url: string;
attrs?: Record<string, any>;
};
import { init } from '@module-federation/enhanced/runtime';
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () {
return {
name: 'change-script-attribute',
createScript({ url }) {
if (url === testRemoteEntry) {
let script = document.createElement('script');
script.src = testRemoteEntry;
script.setAttribute('loader-hooks', 'isTrue');
script.setAttribute('crossorigin', 'anonymous');
return script;
}
},
};
};
import { init } from '@module-federation/enhanced/runtime';
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () {
return {
name: 'change-script-attribute',
createScript({ url }) {
if (url === testRemoteEntry) {
let script = document.createElement('script');
script.src = testRemoteEntry;
script.setAttribute('loader-hooks', 'isTrue');
script.setAttribute('crossorigin', 'anonymous');
return { script, timeout: 1000 }
}
},
};
};
可以完全自定义remote, 可以扩展新的remote类型。 下面两个简单的例子分别实现了加载json数据和模块代理
asyncHook
function createScript(args: LoadEntryOptions): HTMLScriptElement | {script?: HTMLScriptElement, timeout?: number } | void;
type LoadEntryOptions = {
createScriptHook: SyncHook,
remoteEntryExports?: RemoteEntryExports,
remoteInfo: RemoteInfo
};
interface RemoteInfo {
name: string;
version?: string;
buildVersion?: string;
entry: string;
type: RemoteEntryType;
entryGlobalName: string;
shareScope: string;
}
export type RemoteEntryExports = {
get: (id: string) => () => Promise<Module>;
init: (
shareScope: ShareScopeMap[string],
initScope?: InitScope,
remoteEntryInitOPtions?: RemoteEntryInitOptions,
) => void | Promise<void>;
};
// load-json-data-plugin.ts
import { init } from '@module-federation/enhanced/runtime';
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () {
return {
name: 'load-json-data-plugin',
loadEntry({ remoteInfo }) {
if (remoteInfo.jsonA === "jsonA") {
return {
init(shareScope, initScope, remoteEntryInitOPtions) {},
async get(path) {
const json = await fetch(remoteInfo.entry + ".json").then(res => res.json())
return () => ({
path,
json
})
}
}
}
},
};
};
// module-federation-config
{
remotes: {
jsonA: "jsonA@https://cdn.jsdelivr.net/npm/@module-federation/runtime/package"
}
}
// src/bootstrap.js
import jsonA from "jsonA"
jsonA // {...json data}
// delegate-modules-plugin.ts
import { init } from '@module-federation/enhanced/runtime';
import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const changeScriptAttributePlugin: () => FederationRuntimePlugin = function () {
return {
name: 'delegate-modules-plugin',
loadEntry({ remoteInfo }) {
if (remoteInfo.name === "delegateModulesA") {
return {
init(shareScope, initScope, remoteEntryInitOPtions) {},
async get(path) {
path = path.replace("./", "")
const {[path]: factory} = await import("./delegateModulesA.js")
const result = await factory()
return () => result
}
}
}
},
};
};
// ./src/delegateModulesA.js
export async function test1() {
return new Promise(resolve => {
setTimeout(() => {
resolve("test1 value")
}, 3000)
})
}
export async function test2() {
return new Promise(resolve => {
setTimeout(() => {
resolve("test2 value")
}, 3000)
})
}
// module-federation-config
{
remotes: {
delegateModulesA: "delegateModulesA@https://delegateModulesA.js"
}
}
// src/bootstrap.js
import test1 from "delegateModulesA/test1"
import test2 from "delegateModulesA/test2"
test1 // "test1 value"
test2 // "test2 value"