API de runtime

MF runtime APIs são construídas em torno do ModuleFederation instance. A instância padrão é geralmente criada automaticamente pelo plugin de build, so você não precisa passar um instance explicitamente ao chamar runtime APIs — they automatically apply para a instância padrão no runtime.

createInstance

Usado para criar uma nova ModuleFederation instance. Diferentemente de outras runtime APIs, createInstance não atua sobre a instância padrão; ele retorna uma nova instância esse é isolada do padrão.

Quando usar

A instância padrão pattern já cobre uma maioria dos cenários. Você só precisa de createInstance nos seguintes casos:

  • No plugin de build é usado (cenário de runtime puro)
  • Você precisa criar várias ModuleFederation instances com configurações diferentes na mesma aplicação
  • Você quer aproveitar Module Federation's partitioning feature para encapsulate uma definir de APIs para uso por outros projetos

Behavior

  • Cada chamada cria uma nova instância e uma adiciona à o lista global de instâncias
  • It não procura instância existentes by name / version, nem mescla o novo opções em uma instância existente
  • O nova instância does não replace a instância padrão; se precisar recuperá-la em outro lugar, use getInstance

Exemplo

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 Use com cautela

Usado para inicializar ou reutilizar uma ModuleFederation instância de runtime.

init nem sempre cria uma nova instância: quando chamada, o runtime primeiro procura uma instância existente by name e version. Se uma correspondência for encontrada, ela é reutilizada e o novo opções são merged via instance.initOptions(options). Somente quando nenhuma correspondência é encontrada uma nova instância é criada.

Portanto, init é mais adequado para cenários em que o mesmo host é inicializado várias vezes e você quiser reutilizar e estender o mesmo instância de runtime.

Warning
  • Se você precisa crie uma completamente independent nova instância, use createInstance.
  • Se você apenas want para acessar o instance criado pelo plugin de build, use getInstance.
  • Tipo: init(options: InitOptions): ModuleFederation
  InitOptions Type
type InitOptions {
  // Name of the current host
  name: string;
  // List of dependent remote modules
  // tip: The remotes configured at runtime are not completely consistent in type and data with those passed in by the build plugin.
  remotes: Array<RemoteInfo>;
  // List of dependencies that the current host needs to share
  // When using the build plugin, users can configure the dependencies to be shared in the build plugin, and the build plugin will inject the dependencies to be shared into the shared configuration at runtime.
  // When shared is passed in at runtime, the version instance reference must be passed in manually, because it cannot be directly obtained at runtime.
  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 = {
  // Package name and basic information of the dependency, sharing strategy
  [pkgName: string]: Share;
};

type Share = {
  // Version of the shared dependency
  version: string;
  // Which modules consume the current shared dependency
  useIn?: Array<string>;
  // Which module the shared dependency comes from
  from?: string;
  // Factory function to get the instance of the shared dependency. When the cached shared instance cannot be loaded, it will load its own shared dependency.
  lib: () => Module;
  // Sharing strategy, which strategy will be used to determine the reuse of shared dependencies
  shareConfig?: SharedConfig;
  // Dependencies between shares
  deps?: Array<string>;
  // Under which scope the current shared dependency is placed, the default is default
  scope?: string | Array<string>;
};
  Example
import { init, loadRemote } from '@module-federation/enhanced/runtime';

init({
  name: "mf_host",
  remotes: [
    {
      name: "remote",
      // After configuring an alias, it can be loaded directly through the alias
      alias: "app1",
      // Decide which module to load by specifying the address of the module's manifest.json file
      entry: "http://localhost:2001/mf-manifest.json"
    }
  ],
});
Recommended migration

Se your code currently usa init, você pode migrate para o recomendado approach baseado em your scenario.

Opção de migração - usando o plugin de build

Remove o init call, use getInstance para retrieve o instance, e use registerRemotes / registerShared / registerPlugins para registrar o opções esse were previously passed para 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()]);

Opção de migração - runtime puro

Switch diretamente para createInstance; o opção shape stays o mesmo. But note o diferente semantics: createInstance always cria uma nova instância, enquanto init may reuse uma instância existente com o mesmo name / version e merge o novo opções em it.

- 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

  • Tipo: getInstance(): ModuleFederation | null
  • Tipo: getInstance(finder: (instance: ModuleFederation) => boolean): ModuleFederation | null
  • Retrieves o padrão ModuleFederation instance, ou o primeiro registered instance esse matches uma finder callback

Once a instância padrão has been criado pelo plugin de build ou by init, você pode call getInstance() para retrieve it.

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

Se o plugin de build é não usado, chamar getInstance vai lança um exceção. Nesse caso você precisa use createInstance para criar uma nova instância.

Instances criado via createInstance não replace a instância padrão, but they são still registered no lista global de instâncias. So even se você did não keep o retornado reference, você pode still find them later by passing uma finder callback para getInstance.

O callback de busca se comporta como Array.prototype.find: o runtime percorre o currently instâncias registradas e retorna o primeiro correspondência. Se nenhuma instância correspondente for encontrada, getInstance retorna 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

Quando force: true é definir, o newly registered módulos vai overwrite já-registered e carregado módulos, e o cache do carregado módulos vai ser automatically deleted. Uma aviso vai também ser printed para o console para inform você esse este operation é risky.

Build Plugin (Use build plugin)
Pure Runtime (Not use build plugin)
import { registerRemotes } from '@module-federation/enhanced/runtime';

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

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

registerPlugins

function registerPlugins(plugins: ModuleFederationRuntimePlugin[]) {}
Build Plugin (Use build plugin)
Pure Runtime (Not use build plugin)
import { registerPlugins } from '@module-federation/enhanced/runtime';
import runtimePlugin from './custom-runtime-plugin';

// add new 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 {}

Registers plugins no global federation estado instead de on uma single current instance. Este é suitable for scenarios such as:

  • instrumentação compartilhada
  • environment-wide policy
  • plugins padrão em todo o host

Plugins globais são deduplicados por 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',
    },
  ],
});

Para um comportamento previsível, register global plugins antes criação ou usando runtime instances.

registerShared

Registra dependências compartilhadas para o host. O runtime vai prefer para reuse uma dependência compartilhada esse já exists globally e satisfies o conditions; caso contrário it falls back para o dependência registrada aqui.

function registerShared(shared: Shared): void;

type Shared = {
  // Mapping of shared dependency package names; the same package can be registered with multiple versions (array form)
  [pkgName: string]: ShareArgs | ShareArgs[];
};

/**
 * ShareArgs must provide either lib or get
 *
 * lib: synchronous factory function that returns the module immediately when called; should not return a Promise.
 * Suitable when the module is already available at the time `registerShared` is called, e.g. `import React from 'react'`
 *
 * get: used for async / lazy loading scenarios
 * */
type ShareArgs =
  | (SharedBaseArgs & { lib: () => Module })
  | (SharedBaseArgs & { get: SharedGetter })
  | SharedBaseArgs;

type SharedBaseArgs = {
  // Version number being registered; recommended for version differentiation
  version?: string;
  // Sharing strategy, controlling singleton, requiredVersion, etc.
  shareConfig?: SharedConfig;
  // Scope it belongs to, defaults to 'default'; can be placed under multiple scopes simultaneously
  scope?: string | Array<string>;
  // Names of other shared dependencies that this one depends on
  deps?: Array<string>;
  // Version selection strategy: prefer by version number / prefer already loaded instance
  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 (Use build plugin)
Pure Runtime (Not use build plugin)
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>;

Gets uma share dependência. Quando there é uma share dependência no global environment esse meets o requirements do current host, o existente dependência esse meets o share conditions vai ser reused primeiro. Caso contrário, its own dependência vai ser carregado e stored no global cache for later reuse.

Este API é generally não chamada diretamente pelo user, but é usado pelo plugin de build quando transforming its own dependências.

Build Plugin (Use build plugin)
Pure Runtime (Not use build plugin)
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());
});

Se multiple versões de shared são definidas, por padrão o item carregado com o versão mais alta é retornado. Esse comportamento pode ser alterado definindo 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

Esta API é usado para carregar um módulo remoto em runtime.

type loadRemote = (remoteNameOrAlias: string) => Promise<RemoteModule>;
Build Plugin (Use build plugin)
Pure Runtime (Not use build plugin)
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 = {
  // Name or alias of the remote to be preloaded
  nameOrAlias: string;
  // The exposes to be preloaded
  // By default, all exposes are preloaded
  // When exposes are provided, only the required exposes will be loaded
  exposes?: Array<string>; // Default request
  // The default is sync, which only loads the synchronous code referenced in expose
  // When set to all, both synchronous and asynchronous references will be loaded
  resourceCategory?: 'all' | 'sync';
  // When no value is configured, all dependencies are loaded by default
  // After configuring dependencies, only the configuration options will be loaded
  depsRemote?: boolean | Array<depsPreloadArg>;
  // When not configured, resources are not filtered
  // After configuration, unnecessary resources will be filtered
  filter?: (assetUrl: string) => boolean;
};

Com preloadRemote, você pode começa preloading módulo recursos at um earlier stage para avoid waterfall requests. preloadRemote pode preload:

  • o remote's remoteEntry
  • o remote's expose
  • o remote's synchronous ou asynchronous recursos
  • o remote's dependent remote recursos

preloadRemote waits for all recursos involved no current preload call para finish. Se every recurso é carregado successfully ou hits o cache, o Promise resolves; se any recurso fails ou times out, o Promise rejects, e o erro objeto carries o recurso results de este preload call.

Se você apenas care about o final result, você pode use await ou .then/.catch diretamente:

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

Se você precisa track que specific recursos bem-sucedidos, falhou, expiraram, ou vieram do cache, read results do erro objeto:

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

Quando exposes é não specified, o recurso id for este preload é remoteName/*. Quando exposes é specified, o runtime generates recursos per exponha, e o recurso id é remoteName/expose.

Build Plugin (Use build plugin)
Pure Runtime (Not use build plugin)
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',
  },
]);

// preload sub1 module
// filter the resource information that carries ignore in the resource name
// only preload sub-dependent sub1-button module
preloadRemote([
  {
    nameOrAlias: 'sub1',
    filter(assetUrl) {
      return assetUrl.indexOf('ignore') === -1;
    },
    depsRemote: [{ nameOrAlias: 'sub1-button' }],
  },
]);

// preload sub2 module
// preload all exposes under sub2
// preload synchronous and asynchronous resources of sub2
preloadRemote([
  {
    nameOrAlias: 'sub2',
    resourceCategory: 'all',
  },
]);

// preload add expose of sub3 module
preloadRemote([
  {
    nameOrAlias: 'sub3',
    resourceCategory: 'all',
    exposes: ['add'],
  },
]);