插件系统

Module Federation 提供了一套轻量的运行时插件系统,用以实现自身的大多数功能,并允许用户进行扩展。

开发者编写的插件能够修改 Module Federation 的默认行为,并添加各类额外功能,包括但不限于:

  • 获取上下文信息
  • 注册生命周期钩子
  • 修改 Module Federation 配置
  • ...

开发插件

插件提供类似 () => FederationRuntimePlugin 的函数。

插件示例

custom-runtime-plugin.ts
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;

注册插件(两种方式选择一种即可):

  • 构建配置中注册插件
rspack.config.ts
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 各类钩子。

命名规范

插件的命名规范如下:

  • 插件的函数通过 default 导出。
  • 插件的 name 采用 xxx-plugin 格式。

下面是一个例子:

import type { FederationRuntimePlugin } from '@module-federation/enhanced/runtime';
const pluginFooBar = (): FederationRuntimePlugin => ({
  name: 'xxx-plugin',
  //...
});

export default pluginFooBar;

hooks

当然,这里是上述内容的中文翻译:

beforeInit

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

init

SyncHook

在远程容器初始化期间调用。

  • 类型
function init(args: InitOptions): void;

type InitOptions = {
  options: FederationRuntimeOptions;
  origin: FederationHost;
};

beforeRequest

AsyncWaterfallHook

在解析远程容器之前调用,用于注入容器或在查找之前更新某些内容。

  • 类型
async function beforeRequest(
  args: BeforeRequestOptions,
): Promise<BeforeRequestOptions>;

type BeforeRequestOptions = {
  id: string;
  options: FederationRuntimeOptions;
  origin: FederationHost;
};

afterResolve

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

onLoad

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

handlePreloadModule

SyncHook

处理联合模块预加载逻辑。

  • 类型
function handlePreloadModule(args: HandlePreloadModuleOptions): void;

type HandlePreloadModuleOptions = {
  id: string;
  name: string;
  remoteSnapshot: ModuleInfo;
  preloadConfig: PreloadRemoteArgs;
};

errorLoadRemote

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');
});

beforeLoadShare

AsyncWaterfallHook

在尝试加载或协商联合应用之间的共享模块之前调用。

  • 类型
async function beforeLoadShare(
  args: BeforeLoadShareOptions,
): Promise<BeforeLoadShareOptions>;

type BeforeLoadShareOptions = {
  pkgName: string;
  shareInfo?: Shared;
  shared: Options['shared'];
  origin: FederationHost;
};

resolveShare

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

beforePreloadRemote

AsyncHook

在预加载处理程序执行任何预加载逻辑之前调用。

  • 类型
async function beforePreloadRemote(
  args: BeforePreloadRemoteOptions,
): BeforePreloadRemoteOptions;

type BeforePreloadRemoteOptions = {
  preloadOps: Array<PreloadRemoteArgs>;
  options: Options;
  origin: FederationHost;
};

generatePreloadAssets

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

createScript

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 }
      }
    },
  };
};

loadEntry

可以完全自定义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>;
};
  • 示例(加载json数据)
// 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"