インストール
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のマイクロアプリであるモジュールを公開します:
// 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' },
}),
],
};// dashboard/src/App.tsx
import { createReactMicroApp } from '@tuvix.js/react';
function Dashboard() {
return <h2>Dashboard</h2>;
}
export default createReactMicroApp({ name: 'dashboard', App: Dashboard });シェルはリモートをエントリーとして登録します:
// 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
function federatedEntry(remote: RemoteConfig, modulePath?: string, options?: FederatedEntryOptions): EntryLoader;| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
remote | RemoteConfig | 必須 | リモート:name、url、および任意のintegrityとscope(下の表)。 |
modulePath | string | './App' | リモートのexposesのキー。 |
options.shareScope | ShareScope | webpackのdefaultスコープ、なければページ全体で共有するオブジェクト | コンテナのinitに渡す共有スコープ。 |
orchestrator.register({ entry })またはModuleLoader.load(name, entry)用のEntryLoader(({ signal }) => Promise<MicroAppModule>)を返します。呼び出しごとに専用の内部ローダーが作られますが、同じリモートに対する2つのエントリーは、1つのremoteEntry.jsタグと1回のinit呼び出しを共有します。
RemoteConfig:
| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
name | string | 必須 | リモート名。リモートのModuleFederationPlugin設定のnameです。 |
url | string | 必須 | remoteEntry.jsのURL。 |
integrity | string | なし | remoteEntry.jsのscriptタグに付けるSRIハッシュ。 |
scope | string | name | remoteEntry.jsが定義するwindowグローバルの名前。nameと異なる場合に指定します。 |
createFederatedLoader
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>;
};| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
remotes | RemoteConfig[] | 必須 | このローダーが読み込む可能性のあるすべてのリモート。 |
shareScope | ShareScope | webpackのdefaultスコープ、なければページ全体で共有するオブジェクト | 各コンテナのinitに渡す共有スコープ。 |
複数のアプリがリモートのリストから提供される場合や、公開されたモジュールそのものが必要な場合に使います。
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はそのエクスポートを返します。
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を指定しない場合、パッケージは次の順序でスコープを選びます:
- webpackがバンドルするシェルでは、
__webpack_init_sharing__('default')を呼び出して__webpack_share_scopes__.defaultを渡します。これにより、リモートはホストが自身のshared設定で宣言した依存関係を共有します。 - それ以外のシェルでは、ページ上のすべてのリモートが受け取る1つのプレーンなオブジェクトを渡します。リモート同士はこれを通じて依存関係を共有できますが、ホスト自身のコピーは関与しません。
自分で管理するスコープを使うにはshareScopeを渡します。
型
type ShareScope = Record<string, unknown>;
interface FederatedContainer {
init(shareScope: ShareScope): void | Promise<void>;
get(module: string): (() => unknown) | Promise<() => unknown>;
}| 型 | 説明 |
|---|---|
ShareScope | コンテナのinitに渡すオブジェクト。 |
FederatedContainer | remoteEntry.jsが定義する{ init, get }グローバル。 |
RemoteConfig | { name, url, integrity?, scope? }。 |
FederatedLoaderConfig | createFederatedLoader用の{ remotes, shareScope? }。 |
FederatedEntryOptions | federatedEntry用の{ 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」で失敗していました。現在は公開モジュールのパスを受け取り、ローダー関数を返します:tsimport { 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 })を紹介していました。どちらも存在しませんでした。