Skip to content
🌐
@tuvix.js/module-federation
Module Federation
webpackのModule FederationのリモートをTuvix.jsのエントリーに変換します。remoteEntry.jsを読み込み、共有スコープを1回だけ初期化し、公開されたマイクロアプリを返します。

インストール ​

bash
npm install @tuvix.js/module-federation
# or pnpm add @tuvix.js/module-federation

このパッケージは@tuvix.js/loaderに依存し、そのloadScriptを使います。

使いどころ ​

マイクロアプリがwebpackのModuleFederationPluginでビルドされ、それぞれがremoteEntry.jsを公開している構成です。シェルが実行時にしかリモートのURLを知らない場合や、シェルが別のバンドラー(Vite、Rollup)でビルドされている場合、シェルは自分のwebpack設定にリモートを列挙できません。

federatedEntryは、オーケストレーターが登録できるエントリーを返します。最初のマウント時にremoteEntry.jsを読み込み、共有スコープを渡してコンテナのinitを呼び出し、get('./App')を呼び出して、公開されたモジュールをオーケストレーターに渡します。

シェルがwebpackでビルドされていて、ビルド時にリモートを列挙できる場合は、webpack自身のremotesオプションとentry: () => import('dashboard/App')を使えば、このパッケージなしで動作します。

クイックスタート ​

リモートは、自身のエクスポートまたはdefaultエクスポートがTuvix.jsのマイクロアプリであるモジュールを公開します:

js
// dashboard/webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  output: { publicPath: 'auto' },
  devServer: { port: 3001, headers: { 'Access-Control-Allow-Origin': '*' } },
  plugins: [
    new ModuleFederationPlugin({
      name: 'dashboard',
      filename: 'remoteEntry.js',
      exposes: { './App': './src/App' },
    }),
  ],
};
tsx
// dashboard/src/App.tsx
import { createReactMicroApp } from '@tuvix.js/react';

function Dashboard() {
  return <h2>Dashboard</h2>;
}

export default createReactMicroApp({ name: 'dashboard', App: Dashboard });

シェルはリモートをエントリーとして登録します:

ts
// shell/src/main.ts
import { createOrchestrator } from '@tuvix.js/core';
import { federatedEntry } from '@tuvix.js/module-federation';

const orchestrator = createOrchestrator();

orchestrator.register({
  name: 'dashboard',
  entry: federatedEntry({ name: 'dashboard', url: 'http://localhost:3001/remoteEntry.js' }, './App'),
  container: '#main',
  activeWhen: '/dashboard/*',
});

await orchestrator.start();

オーケストレーターのローダーはsignalを付けてエントリーを呼び出すため、そのtimeoutとretriesオプションはremoteEntry.jsにも適用されます。

API ​

federatedEntry ​

ts
function federatedEntry(remote: RemoteConfig, modulePath?: string, options?: FederatedEntryOptions): EntryLoader;
オプション型デフォルト説明
remoteRemoteConfig必須リモート:name、url、および任意のintegrityとscope(下の表)。
modulePathstring'./App'リモートのexposesのキー。
options.shareScopeShareScopewebpackのdefaultスコープ、なければページ全体で共有するオブジェクトコンテナのinitに渡す共有スコープ。

orchestrator.register({ entry })またはModuleLoader.load(name, entry)用のEntryLoader(({ signal }) => Promise<MicroAppModule>)を返します。呼び出しごとに専用の内部ローダーが作られますが、同じリモートに対する2つのエントリーは、1つのremoteEntry.jsタグと1回のinit呼び出しを共有します。

RemoteConfig:

オプション型デフォルト説明
namestring必須リモート名。リモートのModuleFederationPlugin設定のnameです。
urlstring必須remoteEntry.jsのURL。
integritystringなしremoteEntry.jsのscriptタグに付けるSRIハッシュ。
scopestringnameremoteEntry.jsが定義するwindowグローバルの名前。nameと異なる場合に指定します。

createFederatedLoader ​

ts
function createFederatedLoader(config: FederatedLoaderConfig): {
  createFederatedEntry(remoteName: string, modulePath?: string): EntryLoader;
  createFederatedApp(remoteName: string, modulePath?: string, signal?: AbortSignal): Promise<MicroAppModule>;
  loadModule(remoteName: string, modulePath: string, signal?: AbortSignal): Promise<unknown>;
  loadRemoteContainer(remote: RemoteConfig, signal?: AbortSignal): Promise<FederatedContainer>;
};
オプション型デフォルト説明
remotesRemoteConfig[]必須このローダーが読み込む可能性のあるすべてのリモート。
shareScopeShareScopewebpackのdefaultスコープ、なければページ全体で共有するオブジェクト各コンテナのinitに渡す共有スコープ。

複数のアプリがリモートのリストから提供される場合や、公開されたモジュールそのものが必要な場合に使います。

ts
import { createOrchestrator } from '@tuvix.js/core';
import { createFederatedLoader } from '@tuvix.js/module-federation';

const federation = createFederatedLoader({
  remotes: [
    { name: 'remote1', url: 'http://localhost:3001/remoteEntry.js' },
    { name: 'remote2', url: 'http://localhost:3002/remoteEntry.js' },
  ],
});

const orchestrator = createOrchestrator();

orchestrator.register({
  name: 'remote1',
  entry: federation.createFederatedEntry('remote1', './App'),
  container: '#remote1-container',
  activeWhen: '/',
});

orchestrator.register({
  name: 'remote2',
  entry: federation.createFederatedEntry('remote2', './App'),
  container: '#remote2-container',
  activeWhen: '/',
});

await orchestrator.start();

createFederatedEntry(remoteName, modulePath = './App') ​

createFederatedApp(remoteName, modulePath, signal)を呼び出すEntryLoaderを返します。remoteNameがremotesにない場合はその場で例外を投げるため、タイプミスは最初のマウント時ではなくregisterの時点で失敗します。

createFederatedApp(remoteName, modulePath = './App', signal?) ​

コンテナを読み込み、get(modulePath)を呼び出し、ファクトリーを実行してマイクロアプリを返します。公開されたモジュールがmount関数を持つ場合はそのモジュール自体を、そうでなくdefaultエクスポートがmount関数を持つ場合はそのエクスポートを返します。どちらでもなければ[Tuvix MF] Module "<modulePath>" from remote "<remoteName>" does not export mount/unmount.を投げます。

loadModule(remoteName, modulePath, signal?) ​

共有コンポーネントライブラリや設定オブジェクトなど、任意の公開モジュールを返します。モジュールにdefaultエクスポートがある場合、loadModuleはそのエクスポートを返します。

ts
import { createFederatedLoader } from '@tuvix.js/module-federation';

const federation = createFederatedLoader({
  remotes: [{ name: 'design', url: 'https://cdn.example.com/design/remoteEntry.js' }],
});

const tokens = (await federation.loadModule('design', './tokens')) as Record<string, string>;
document.documentElement.style.setProperty('--brand', tokens.brand ?? '#f05454');

loadRemoteContainer(remote, signal?) ​

(名前ではなく)RemoteConfigを受け取り、そのremoteEntry.jsをクラシックスクリプトとして読み込み、window[remote.scope ?? remote.name]からコンテナを読み取り、初期化して返します。ローダーはコンテナをリモート名でキャッシュします。

共有スコープ ​

shareScopeを指定しない場合、パッケージは次の順序でスコープを選びます:

  1. webpackがバンドルするシェルでは、__webpack_init_sharing__('default')を呼び出して__webpack_share_scopes__.defaultを渡します。これにより、リモートはホストが自身のshared設定で宣言した依存関係を共有します。
  2. それ以外のシェルでは、ページ上のすべてのリモートが受け取る1つのプレーンなオブジェクトを渡します。リモート同士はこれを通じて依存関係を共有できますが、ホスト自身のコピーは関与しません。

自分で管理するスコープを使うにはshareScopeを渡します。

型 ​

ts
type ShareScope = Record<string, unknown>;

interface FederatedContainer {
  init(shareScope: ShareScope): void | Promise<void>;
  get(module: string): (() => unknown) | Promise<() => unknown>;
}
型説明
ShareScopeコンテナのinitに渡すオブジェクト。
FederatedContainerremoteEntry.jsが定義する{ init, get }グローバル。
RemoteConfig{ name, url, integrity?, scope? }。
FederatedLoaderConfigcreateFederatedLoader用の{ remotes, shareScope? }。
FederatedEntryOptionsfederatedEntry用の{ shareScope? }。

エッジケース ​

  • 初期化は1回だけ。 複数のローダーやエントリーが同じコンテナオブジェクトを使う場合でも、パッケージはページごとに1回だけ初期化します。失敗したinitは記録されないため、次の読み込みで再試行されます。
  • タイムアウトとリトライ。 オーケストレーターのtimeoutが発火すると、中断されたsignalがremoteEntry.jsの待機を止め、他の読み込みがまだ待っていない限りそのタグを削除します。リトライでは新しいタグが追加されます。中断された読み込みは中断理由でrejectされます。その他の読み込み失敗は[Tuvix MF] Failed to load remote entry: <url> (<error>)となります。
  • グローバルが見つからない。 remoteEntry.jsが読み込まれてもwindow[scope]にinitとgetがない場合、読み込みは[Tuvix MF] Remote container "<scope>" not found on window after loading <url>で失敗します。scopeがリモートのname(設定している場合はlibrary.name)と一致しているか確認してください。
  • 不明なモジュール。 getが例外を投げるか、ファクトリー以外を返した場合は[Tuvix MF] Module "<modulePath>" not found in remote "<remoteName>"になります。
  • 不明なリモート。 createFederatedEntry、createFederatedApp、loadModuleは[Tuvix MF] Remote "<name>" is not configured. Available: <names>を投げます。
  • CORS。 remoteEntry.jsはcrossorigin="anonymous"で読み込まれます。別オリジンのリモートはAccess-Control-Allow-Originを送信する必要があります。
  • アンロードと再マウント。 オーケストレーターのローダーは、ローダー関数のエントリーについては何も削除しません。次のマウントで再びget(modulePath)が呼ばれ、webpackはキャッシュ済みのモジュールを返します。
  • プリフェッチ。 ModuleLoader.prefetchはローダー関数のエントリーをスキップするため、オーケストレーターのprefetch設定でremoteEntry.jsが事前にダウンロードされることはありません。

対象外のこと ​

  • webpackは設定しません。各リモートが自分でModuleFederationPluginを追加します。
  • コンテナはwindowから読み取ります。library: { type: 'module' }やその他のグローバルではないライブラリタイプでビルドされたリモートは動作しません。
  • リモートのマニフェストを読み取ったり、リモートを検出したりはしません。各remoteEntry.jsのURLを自分で列挙します。
  • バージョンのネゴシエーションは行いません。それは各コンテナ内のwebpackランタイムが共有スコープを通じて行います。

0.4からの移行 ​

  • federatedEntry(remote)は{ scripts: [remote.url] }を返していたため、コンテナにはinitとgetしかないことから、ローダーは「no exports found」で失敗していました。現在は公開モジュールのパスを受け取り、ローダー関数を返します:

    ts
    import { federatedEntry } from '@tuvix.js/module-federation';
    
    // 0.4: federatedEntry({ name: 'dashboard', url }) returned an EntryConfig
    const entry = federatedEntry({ name: 'dashboard', url: 'https://cdn.example.com/dashboard/remoteEntry.js' }, './App');
  • createFederatedEntry(remoteName, modulePath)はEntryConfigではなくEntryLoaderを返します。createFederatedAppでリモートを事前に読み込み、entry: { scripts: [] }を登録していたシェルは、エントリーとしてfederation.createFederatedEntry('remote1', './App')を渡せます。

  • FederatedLoaderConfig.sharedはshareScopeに置き換えられました。パッケージがsharedを読み取ることはありませんでした。

  • loadRemoteContainerはanyではなくFederatedContainerを返します。

  • 0.4では、パッケージはwebpackが定義しないwindow.__webpack_share_scopes__を読み取っていたため、各リモートは専用の空のスコープを受け取っていました。現在、webpackのホストは実際のdefaultスコープを渡します。

  • このページの0.4版では、引数なしのcreateFederatedLoader()とloader.remote({ url, scope, module })を紹介していました。どちらも存在しませんでした。