Skip to content
⚙️
@tuvix.js/core
コアオーケストレーター
マイクロアプリを登録し、そのエントリーを読み込み、URLの変化に合わせてマウントまたはアンマウントします。組み込みルーターまたは外部ルーターに従い、指定に応じてアプリのCSSをShadow Rootに隔離し、ライフサイクルイベントを共有イベントバスで通知します。

インストール ​

bash
npm install @tuvix.js/core
# or pnpm add @tuvix.js/core

@tuvix.js/coreは@tuvix.js/router、@tuvix.js/event-bus、@tuvix.js/loader、@tuvix.js/sandboxに依存します。パッケージマネージャーがこれらも一緒にインストールします。

使いどころ ​

個別にビルドされた複数のアプリが1つのページを共有し、どのアプリを表示するかをURLで決める場合に、ホストページ(シェル)でオーケストレーターを使います。各アプリを、エントリーURL、コンテナ、担当するパスとともに一度だけ登録します。オーケストレーターは初回使用時にエントリーを読み込み、アプリのbootstrap、mount、unmountを呼び出し、ナビゲーションのたびにページを同期させます。

次の場合は別の手段を選んでください。

  • ページごとにフレームワークのアイランドを1つ描画し、表示内容をすでにサーバーが決めている場合。アダプター(@tuvix.js/react、@tuvix.js/vue、@tuvix.js/svelte、@tuvix.js/angular)と@tuvix.js/viteは、オーケストレーターなしでアイランドをマウントします。
  • リモートスクリプトを読み込み、そのライフサイクルを自分で呼び出すだけの場合。@tuvix.js/loaderを使います。
  • すべてを1つのimportで使いたい場合。このパッケージを再エクスポートする統合パッケージtuvix.jsをインストールします。

クイックスタート ​

次のシェルは、#mainを共有する2つのアプリを登録し、組み込みルーターでブラウザのURLに従い、同一オリジンのリンククリックをルーターのナビゲーションに変換します。

ts
import { createOrchestrator, interceptLinks } from '@tuvix.js/core';

const orchestrator = createOrchestrator({
  router: {
    mode: 'history',
    routes: [{ path: '/settings/*', app: 'settings' }],
  },
  loader: { timeout: 8000, retries: 1 },
  onError: (error, appName) => console.error(`[${appName}]`, error),
});

// Active on /dashboard and every path below it.
orchestrator.register({
  name: 'dashboard',
  entry: 'https://cdn.example.com/dashboard/main.js',
  container: '#main',
  activeWhen: '/dashboard',
  props: { theme: 'dark' },
});

// No activeWhen: the router route above activates it on /settings/*.
orchestrator.register({
  name: 'settings',
  entry: {
    scripts: ['https://cdn.example.com/settings/main.js'],
    styles: ['https://cdn.example.com/settings/main.css'],
  },
  container: '#main',
});

await orchestrator.start();

interceptLinks({ navigate: (path) => orchestrator.navigateTo(path) });

各リモートは、シェルが登録した名前でライフサイクルを公開します。素のDOMコードではdefineMicroAppがこれを行い、React、Vue、Svelte、Angularのコンポーネントではフレームワークアダプターが行います。

ts
// dashboard/main.ts, built to https://cdn.example.com/dashboard/main.js
import { defineMicroApp } from '@tuvix.js/core';

export default defineMicroApp({
  name: 'dashboard',
  mount({ container, props, bus }) {
    container.innerHTML = `<h1>Dashboard (${String(props?.theme ?? 'light')})</h1>`;
    bus?.emit('dashboard:ready', { at: Date.now() });
  },
  unmount({ container }) {
    container.replaceChildren();
  },
});

アプリがアクティブになる条件 ​

登録されたアプリは、5つのアクティベーションルールのいずれかに従います。オーケストレーターはアプリの設定からルールを選びます。

設定アプリがアクティブになるときアンマウントするのは
activeWhen: '/path'matchPath('/path', currentPath).matchedがtrueパスが一致しなくなった次のリコンサイル
activeWhen: (path) => boolean関数がtrueを返す関数がfalseを返す次のリコンサイル
activeWhenなし、router.routesで名前を指定app: nameを持つルートがパス全体に一致するそのルートが一致しなくなった次のリコンサイル
activeWhenなし、ルートなし自動ではアクティブにならないmountApp / unmountAppを使う開発者
mountWhenVisible: trueコンテナの10 %以上がビューポート内に入る開発者。リコンサイルはこのアプリをスキップし、activeWhenは無視されます

activeWhenの文字列は@tuvix.js/routerのmatchPathで評価されます。ルーターと@tuvix.js/serverが使うものと同じマッチャーです。

  • ?queryと#hashは無視されるため、/dashboard?tab=2でもダッシュボードはマウントされたままです。
  • 通常のパターンは、セグメント境界でサブパスにも一致します。'/dashboard'は/dashboardと/dashboard/statsに一致し、/dashboardsには一致しません。
  • '/dashboard/*'は'/dashboard'と同じパスに一致します。
  • :nameは1つのセグメントに一致します。'/users/:id'は/users/42に一致します。
  • '/'はすべてのパスに一致します。
  • .や(などのその他の文字は、文字どおりに一致します。

ルーターのroutesは完全一致を使います。{ path: '/settings' }は/settingsにのみ一致します。サブパスを含めるには'/settings/*'と書きます。'/'ルートはホームページでのみアクティブになり、'/*'はすべてのパスでアクティブになります。ここでもクエリ文字列とハッシュは無視されます。

現在のパスの取得元 ​

オーケストレーターは、最初に存在するソースからパスを読み取ります。

  1. connectRouterに渡したルーター(そのgetPath())。
  2. config.routerから作られた組み込みルーター(そのcurrentPath)。
  3. window.location.pathname。

config.routerまたはconnectRouterがある場合、ナビゲーションのたびにリコンサイルが実行されます。どちらもない場合、start()はwindow.location.pathnameに対して一度だけリコンサイルし、reconcile()を呼び出すまで以降のナビゲーションには反応しません。

リコンサイルは1つずつ実行される ​

リコンサイルは、ルートで管理される各アプリをパスと比較し、DOMを一致させます。

  1. 非アクティブになるべきアプリが先にすべて並列でアンマウントされます。そのため、コンテナを共有する2つのアプリが重なることはありません。
  2. 次に、アクティブになるべきアプリが並列で読み込まれ(初回のみ)、ブートストラップされ(bootstrapの呼び出しが一度成功するまで)、マウントされます。

リコンサイル中にナビゲーションが発生すると、オーケストレーターはそれをキューに入れます。3つのナビゲーションがキューに溜まった場合、次に実行されるのは最新のパスだけで、すべての呼び出し元のPromiseはキューが空になった時点で解決されます。エントリーの読み込み中に非アクティブになったアプリはマウントをスキップします。mount()の途中で非アクティブになったアプリは、そのマウントが完了した直後にアンマウントされます。/a → /b → /aと素早く移動すると、マウントされたまま残るのはaだけです。

API ​

createOrchestrator ​

ts
function createOrchestrator(config?: OrchestratorConfig): Orchestrator;

Orchestratorを作成します。new Orchestrator(config)と同じです。

オプション型デフォルト意味
routerRouterConfigなし@tuvix.js/routerの組み込みルーターを、mode('history'または'hash'、デフォルトは'history')、base、routes(必須、[]も可)で作成します。navigateToとROUTE_CHANGEイベントに必要です。
eventBusEventBusOptionsなし専用バスのオプション(maxListeners、debug、logger)。省略すると、オーケストレーターはページ上のすべてのアプリが共有するバスであるgetGlobalBus()を使います。
loaderLoaderOptions{ timeout: 10000, retries: 2, retryDelay: 1000 }内部のModuleLoaderに渡されます。timeoutは試行ごとのミリ秒です(0で無効)。globalsはエントリーのスクリプトが実行される前にwindowへ書き込まれます。
prefetch{ strategy: 'immediate' | 'idle' | 'hover' | 'none' }{ strategy: 'none' }必要になる前にエントリーをダウンロードするタイミング。下記を参照してください。
onError(error: Error, appName: string) => voidなしアプリの読み込み、ブートストラップ、マウント、更新、アンマウントが失敗したときに呼び出されます。OrchestratorEvent.APP_ERRORは同じ失敗をフェーズ付きで通知します。
onStatusChange(appName: string, status: AppStatus) => voidなしすべてのアプリのステータスが変わるたびに呼び出されます。

プリフェッチ戦略は、スクリプトとスタイルのURLを<link rel="prefetch">または<link rel="modulepreload">で、実行せずにダウンロードします。ローダー関数のエントリーはスキップされます。関数を呼び出すとモジュールが実行されてしまうためです。

  • 'immediate':start()の実行中に、登録済みのすべてのアプリ。
  • 'idle':start()後の最初のrequestIdleCallbackで、登録済みのすべてのアプリ(このAPIがない環境では200 msのsetTimeout)。
  • 'hover':ポインターが同一オリジンのリンクに入るか、リンクにフォーカスが移ると、オーケストレーターはそのリンクのパスでアクティブになるアプリをプリフェッチします。各アプリのプリフェッチは一度だけです。hashモードとrouter.baseも考慮されます。

destroy()は保留中のidleおよびhoverのプリフェッチをキャンセルします。

Orchestrator ​

ts
class Orchestrator {
  constructor(config?: OrchestratorConfig);
  register(appConfig: MicroAppConfig): void;
  unregister(name: string): Promise<void>;
  start(): Promise<void>;
  mountApp(name: string): Promise<void>;
  unmountApp(name: string): Promise<void>;
  updateAppProps(name: string, props: Record<string, unknown>): Promise<void>;
  reconcile(path?: string): Promise<void>;
  connectRouter(source: RouterSource): () => void;
  navigateTo(path: string): Promise<void>;
  getAppStatus(name: string): AppStatus | null;
  getRegisteredApps(): string[];
  getMountedApps(): string[];
  getEventBus(): IEventBus;
  getRouter(): IRouter | null;
  destroy(): Promise<void>;
}

register(appConfig) ​

アプリの設定を保存します。読み込み、マウント、リコンサイルは行いません。それらはstart()と以降のナビゲーションが行います。

オプション型デフォルト意味
namestring必須一意のアプリ名。リモートモジュールは同じ名前で登録する(defineMicroApp({ name })またはアダプターのname)か、mount関数を持つ新しいwindowグローバルを1つ公開する必要があります。
entryEntry必須スクリプトURL、EntryConfig({ scripts, styles?, type?, integrity? })、または() => import('./dashboard')のようなローダー関数。@tuvix.js/loaderを参照してください。
containerstring | HTMLElement必須CSSセレクターまたは要素。セレクターはマウントのたびにdocument.querySelectorで解決されます。
activeWhenstring | ((path: string) => boolean)なしアクティベーションルール。アプリがアクティブになる条件を参照してください。
propsRecord<string, unknown>なしmount()に渡されます。updateAppPropsはここにマージします。
sandboxboolean | { css?: boolean }falsetrueまたは{ css: true }を指定すると、アプリはコンテナ上のShadow Root内に描画されます。CSSの隔離を参照してください。
fallbackstringなし読み込み、ブートストラップ、マウントが失敗したときにinnerHTMLでコンテナへ書き込まれるHTML。更新とアンマウントの失敗ではコンテナに手を加えません。
mountWhenVisiblebooleanfalseIntersectionObserverを使い、コンテナが10 %見えた時点でマウントします。リコンサイルはこのアプリとそのactiveWhenを無視します。

[Tuvix] App name is required.、[Tuvix] App "<name>" is already registered.、[Tuvix] App "<name>" requires an entry.、または[Tuvix] App "<name>" requires a container.をスローします。

ts
import { createOrchestrator } from '@tuvix.js/core';

const orchestrator = createOrchestrator();

orchestrator.register({
  name: 'profile',
  entry: () => import('./profile'),
  container: '#main',
  activeWhen: '/users/:id',
  fallback: '<p>The profile is unavailable.</p>',
});

orchestrator.register({
  name: 'comments',
  entry: 'https://cdn.example.com/comments/main.js',
  container: '#comments',
  mountWhenVisible: true,
});

await orchestrator.start();

start() ​

組み込みルーター(あれば)を購読し、現在のパスに対してリコンサイルし、プリフェッチを設定し、mountWhenVisibleのコンテナの監視を開始します。OrchestratorEvent.STARTEDを発行します。最初のリコンサイルの後に解決されます。そのリコンサイルでアプリが失敗した場合も解決されます。失敗はonError、APP_ERROR、コンソールに通知されます。2回目の呼び出しは[Tuvix] Orchestrator is already started.をログに出力して戻ります。

mountApp(name) / unmountApp(name) ​

1つのアプリを手動でマウントまたはアンマウントします。activeWhenもルートもないアプリに使います。ルートで管理されるアプリでは、次のリコンサイルがこの操作を上書きします。

mountAppは進行中のアンマウントを待ってから、読み込みとマウントを行います。fallbackを描画した後、読み込みまたはマウントのエラーでrejectします。unmountAppは進行中の読み込みまたはマウントを待ってからアンマウントします。読み込み中に呼び出された場合、マウント自体をスキップします。

ts
import { createOrchestrator } from '@tuvix.js/core';

const orchestrator = createOrchestrator();
orchestrator.register({ name: 'chat', entry: '/chat/main.js', container: '#chat' });
await orchestrator.start();

document.querySelector('#open-chat')?.addEventListener('click', () => {
  orchestrator.mountApp('chat').catch(() => {
    // fallback HTML is already in #chat
  });
});

updateAppProps(name, props) ​

propsをアプリの現在のプロパティに浅くマージします。update関数を持つマウント済みのアプリは、マージ後のオブジェクトをupdate({ props, container })として受け取ります。それ以外の場合、オーケストレーターはマージ後のプロパティを保存し、次のmount()に渡します。保留中のマウントを先に待つため、更新は新しいインスタンスに届きます。updateがスローすると、ステータスがerrorになり、onErrorが実行され、Promiseがrejectされ、保存済みのプロパティは変更されません。

ts
import { createOrchestrator } from '@tuvix.js/core';

const orchestrator = createOrchestrator();
orchestrator.register({ name: 'cart', entry: '/cart/main.js', container: '#cart', activeWhen: '/' });
await orchestrator.start();

await orchestrator.updateAppProps('cart', { currency: 'EUR' });

reconcile(path?) ​

path、または省略時は現在のパスに対するリコンサイルをキューに入れます。ナビゲーションをオーケストレーターに伝えるルーターがない場合に使います。リコンサイルは1つずつ実行されるのキューのルールが適用されます。

ts
import { createOrchestrator } from '@tuvix.js/core';

const orchestrator = createOrchestrator();
await orchestrator.reconcile('/dashboard?tab=stats');

connectRouter(source) ​

組み込みルーターの代わりに外部ルーター(TanStack Router、React Router、独自のもの)に従います。source.subscribe(callback)は、ナビゲーションのたびにcallbackを呼び出し、購読解除関数を返す必要があります。source.getPath()は現在のパスを返します。start()の後、各コールバックはgetPath()に対してリコンサイルします。start()もgetPath()を読み取ります。すでに開始しているオーケストレーターに接続すると、ただちにリコンサイルします。disconnect()を返します。

ts
import { createOrchestrator } from '@tuvix.js/core';

// TanStack Router instance created elsewhere in your shell.
declare const router: {
  subscribe(event: 'onResolved', callback: () => void): () => void;
  state: { location: { pathname: string } };
};

const orchestrator = createOrchestrator();
orchestrator.register({ name: 'billing', entry: '/billing/main.js', container: '#main', activeWhen: '/billing' });

const disconnect = orchestrator.connectRouter({
  subscribe: (callback) => router.subscribe('onResolved', callback),
  getPath: () => router.state.location.pathname,
});
await orchestrator.start();

// Later, when the shell tears down:
disconnect();

React Routerのデータルーター(createBrowserRouter)も同じ形に当てはまります。subscribe: (callback) => router.subscribe(() => callback())とgetPath: () => router.state.location.pathnameを使います。

組み込みルーターのpush(path)を呼び出します。PromiseはURLが変わった後に解決され、それによって起動するリコンサイルはバックグラウンドで実行されます。config.routerがない場合は[Tuvix] No router configured.をスローします。

getAppStatus(name), getRegisteredApps(), getMountedApps() ​

getAppStatusはアプリのAppStatusを返し、不明な名前にはnullを返します。getRegisteredAppsは登録済みのすべての名前を返します。getMountedAppsはステータスが'mounted'の名前を返します('updating'のアプリは含まれません)。

getEventBus(), getRouter() ​

getEventBusは、オーケストレーターがOrchestratorEventを発行しmount()に渡すバスを返します。これはgetGlobalBus()、またはeventBusを設定した場合は専用バスです。getRouterは組み込みルーター、またはnullを返します。

unregister(name) ​

アプリをアンマウントし(進行中のマウントを待ちます)、コンテナの監視を止め、エントリーのscriptタグとstyleタグを削除し、キャッシュされたモジュールを破棄し、アプリの登録を消去します。不明な名前ではrejectします。ブラウザはモジュールスクリプトをURLごとに一度しか実行しないため、type: 'module'のURLエントリーを再登録するには、新しいURLまたはローダー関数が必要です。

destroy() ​

プリフェッチを止め、組み込みルーターと接続済みのすべてのルーターの購読を解除し、実行中のリコンサイルを待ち、すべてのアプリをアンマウントし、ローダーのキャッシュをクリアして、OrchestratorEvent.DESTROYEDを発行します。専用バスは破棄し、グローバルバスは残します。繰り返し呼び出すと同じPromiseを返します。その後、register、start、mountApp、unmountApp、unregister、reconcile、connectRouterは[Tuvix] Cannot use a destroyed Orchestrator. Create a new one.をスローします。

defineMicroApp ​

ts
function defineMicroApp(definition: MicroAppDefinition): MicroAppModule;

ライフサイクルオブジェクトを検証し、@tuvix.js/loaderのregisterModule(definition.name, module)で公開して返します。リモートアプリのエントリーで呼び出します。エントリーがURLでもローダー関数でも、オーケストレーターはエントリーのスクリプトが実行された後に名前でモジュールを見つけます。

オプション型デフォルト意味
namestring必須シェルが登録したnameと一致する必要があります。
bootstrap() => void | Promise<void>なし初回の読み込み後、初回のマウント前に実行されます。スローするとマウントは失敗し、次のマウントで再び呼び出されます。一度成功すると、そのオーケストレーターでは二度と実行されません。
mount(context: MountContext) => void | Promise<void>必須context.containerに描画します。
unmount(context: UnmountContext) => void | Promise<void>必須後片付けをしてcontext.containerを空にします。
update(context: UpdateContext) => void | Promise<void>なし再マウントせずにcontext.propsを適用します。これがない場合、新しいプロパティは次のマウントまで待ちます。
ssrbooleanfalsemountがサーバーレンダリングされたマークアップを引き継ぐ場合に設定します。その場合、mountの実行前にコンテナにマークアップがあれば、tuvix:mountedイベントはhydrated: trueを通知します。

MountContextの内容:

フィールド型意味
containerHTMLElement描画先の要素。CSSを隔離したアプリでは、Shadow Root内の<div data-tuvix-sandbox-root>です。
propsRecord<string, unknown> | undefined現在のプロパティ。
namestring | undefined登録されたアプリ名。
busMicroAppBus | undefinedオーケストレーターのイベントバス(on、once、emit、off)。

返されるモジュールはmountとunmountをラップし、フレームワークアダプターと同じDOMライフサイクルイベントをディスパッチします。そのため、@tuvix.js/loaderのwhenMounted(name)、isMounted(name)、onLifecycleはこれらのアプリでも動作します。

  • tuvix:mounted:mountが解決した後、{ name, container, hydrated }とともに。
  • tuvix:unmounted:unmountが解決した後。
  • tuvix:error:どちらかがスローしたとき。その後、ラッパーはエラーを再スローします。

イベントはコンテナからdocumentへバブリングします。ドキュメントに接続されていないコンテナの場合、ランタイムはイベントをdocumentに直接ディスパッチします。同じコンテナでmountの実行中に届いたunmountは、先にそのマウントを待ちます。

[Tuvix] defineMicroApp requires a "name" property.、[Tuvix] defineMicroApp "<name>" requires a "mount" function.、または[Tuvix] defineMicroApp "<name>" requires an "unmount" function.をスローします。

ts
import { defineMicroApp } from '@tuvix.js/core';

let offCart: (() => void) | undefined;

export default defineMicroApp({
  name: 'header',
  bootstrap() {
    console.log('header loaded');
  },
  mount({ container, props, bus }) {
    container.innerHTML = `<header>Hello, ${String(props?.user ?? 'guest')} <span id="count">0</span></header>`;
    offCart = bus?.on<{ count: number }>('cart:changed', ({ count }) => {
      const badge = container.querySelector('#count');
      if (badge) badge.textContent = String(count);
    });
  },
  update({ props, container }) {
    const header = container?.querySelector('header');
    if (header?.firstChild) header.firstChild.textContent = `Hello, ${String(props.user ?? 'guest')} `;
  },
  unmount({ container }) {
    offCart?.();
    container.replaceChildren();
  },
});

OrchestratorEvent ​

オーケストレーターがバスに発行するイベントの文字列enumです。デフォルトのバスはgetGlobalBus()なので、ページ上のどのアプリでもリッスンできます。

メンバーイベント名ペイロード
APP_REGISTERED'tuvix:app:registered'{ name }
APP_BOOTSTRAP'tuvix:app:bootstrap'{ name }
APP_MOUNT'tuvix:app:mount'{ name }
APP_UNMOUNT'tuvix:app:unmount'{ name }
APP_ERROR'tuvix:app:error'AppErrorEvent:{ name, error, phase }。phaseは'load'、'bootstrap'、'mount'、'update'、'unmount'のいずれか
APP_STATUS_CHANGE'tuvix:app:status-change'{ name, status }
ROUTE_CHANGE'tuvix:route:change'組み込みルーターのNavigationEvent(from、to、fromRoute、toRoute)
STARTED'tuvix:started'{}
DESTROYED'tuvix:destroyed'{}

phase: 'load'は、エントリーの読み込みに加えてコンテナの解決も対象にします。

ts
import { createOrchestrator, getGlobalBus, OrchestratorEvent, type AppErrorEvent } from '@tuvix.js/core';

const orchestrator = createOrchestrator();

getGlobalBus().on<AppErrorEvent>(OrchestratorEvent.APP_ERROR, ({ name, error, phase }) => {
  console.error(`${name} failed during ${phase}`, error);
});

await orchestrator.start();

CSSの隔離 ​

sandbox: trueまたはsandbox: { css: true }は、アプリのCSSを隔離します。

  1. 初回のマウント時、オーケストレーターは@tuvix.js/sandboxのCssSandboxでコンテナにopenなShadow Rootをアタッチし、既存の子要素(サーバーレンダリングされたマークアップ)をその中の<div data-tuvix-sandbox-root="<name>">に移動します。アプリはそのdivにマウントされます。
  2. エントリーのstylesは、document.headではなくShadow Root内の<link>要素として読み込まれます。マウントは、それらの読み込みが完了するか失敗するまで、最大loader.timeoutミリ秒待ちます(デフォルトは10000。0の場合は無制限に待ちます)。
  3. アンマウントしてもShadow Rootは残り、次のマウントで再利用されます。

ページのスタイルシートはアプリ内の要素に一致しなくなり、アプリのスタイルシートはページに届かなくなります。colorやfont-familyなどの継承プロパティは、引き続きShadow境界を越えます。{ css: false }とfalseはLight DOMに描画します。

ts
import { createOrchestrator } from '@tuvix.js/core';

const orchestrator = createOrchestrator();

orchestrator.register({
  name: 'checkout',
  entry: { scripts: ['/checkout/main.js'], styles: ['/checkout/main.css'] },
  container: '#checkout',
  activeWhen: '/checkout',
  sandbox: { css: true },
});

await orchestrator.start();

オーケストレーターはJavaScriptをサンドボックス化しません。@tuvix.js/sandboxのJsSandboxは、自分で呼び出す独立したユーティリティです。

型 ​

型意味
OrchestratorConfigcreateOrchestratorとnew Orchestratorの引数。createOrchestratorの表を参照してください。
MicroAppConfigregisterの引数。registerの表を参照してください。
MicroAppDefinitiondefineMicroAppの引数:name、bootstrap?、mount、unmount、update?。
AppSandboxOptionboolean | { css?: boolean }。sandboxフィールドです。
RouterSource{ subscribe(callback: () => void): () => void; getPath(): string }。connectRouterの引数です。
AppErrorEvent{ name: string; error: Error; phase: AppErrorPhase }。APP_ERRORのペイロードです。
AppErrorPhase'load' | 'bootstrap' | 'mount' | 'update' | 'unmount'。
AppStatus'registered' | 'bootstrapping' | 'bootstrapped' | 'mounting' | 'mounted' | 'updating' | 'unmounting' | 'unmounted' | 'error'。初回のマウントはregistered → bootstrapping → bootstrapped → mounting → mountedと進み、再マウントはunmounted → mounting → mountedと進みます。
RegisteredApp1つのアプリに対するオーケストレーターのレコードの形:config、status、module、container、error、lastUpdated。0.5.0では、これを返すOrchestratorのメソッドはありません。

再エクスポート ​

@tuvix.js/coreは依存パッケージから次のものを再エクスポートするため、シェルはすべてを1つのパッケージからimportできます。各リンク先に完全なAPIが記載されています。

エクスポート種類ソース
createEventBus, getGlobalBusfunction@tuvix.js/event-bus
IEventBus, EventHandler, Unsubscribetype@tuvix.js/event-bus
createRouter, matchPath, interceptLinksfunction@tuvix.js/router
RouterConfig, IRouter, RouteConfig, PathMatch, InterceptLinksOptionstype@tuvix.js/router
createLoaderfunction@tuvix.js/loader
Entry, MicroAppModuletype@tuvix.js/loader

getGlobalBus()はインスタンスをwindow.__TUVIX_GLOBAL_BUS__に保持します。そのため、@tuvix.js/event-busの独自のコピーをバンドルしたマイクロアプリも、シェルと同じバスを取得します。

エッジケース ​

  • start()前のregister()は設定を保存するだけです。start()後に、ルートで管理されるアプリ(activeWhenを持つか、ルーターのルートで名前が指定されているアプリ)を登録すると、現在のパスに対してリコンサイルが実行されます。そのため、パスがアプリをアクティブにする場合はすぐにマウントされます。
  • start()後に登録されたmountWhenVisibleアプリは、すぐに監視されます。その時点で(またはstart()の時点で)DOMにないコンテナはスキップされ、監視されることはありません。IntersectionObserverがない環境(サーバー、古いブラウザ)では、ビューポートアプリはマウントされません。
  • マウントに失敗したビューポートアプリはerrorのままです。オブザーバーは再試行しません。
  • error状態のルート管理アプリは、そのアプリをアクティブにする次のリコンサイルで再試行されます。読み込みに失敗した場合は再度読み込みます。bootstrapが失敗した場合は、再試行時にbootstrapを再実行します。成功したbootstrapは繰り返されないため、mountが失敗した場合はmountだけを再試行します。
  • コンテナが見つからない場合、マウントは[Tuvix] Container "#main" not found in DOM.で失敗します。初回のマウントでは、fallbackを描画する場所がありません。
  • リコンサイルの失敗は、start()、reconcile()、ナビゲーションをrejectしません。オーケストレーターは[Tuvix] Failed to mount "<name>":(またはunmount)をコンソールに出力し、onErrorを呼び出します。mountAppとunmountAppはrejectします。
  • APP_ERRORはすべてのフェーズの失敗で発火し、phaseがそのフェーズを示します。fallbackはload、bootstrap、mountの失敗でのみ描画されます。
  • アンマウントがスローしたアプリはerrorで終わり、アンマウント済みとして扱われます。
  • fallbackはinnerHTMLを通ります。信頼できるマークアップのみを渡してください。
  • アンマウントは、他の読み込み済みアプリが同じURLを使っていない限り、アプリのエントリーのスタイルシートをdocument.headから削除します。次のマウントでそれらを再び追加し、読み込みを待ちます。
  • 1つのページ上の2つのオーケストレーターはgetGlobalBus()を共有し、互いのOrchestratorEventを受け取ります。一方に専用バスを持たせるにはeventBus: {}を渡します。
  • @tuvix.js/event-busのresetGlobalBus()は、実行中のオーケストレーターが保持するバスを破棄します。オーケストレーターは破棄されたバスを持ち続けます。resetGlobalBus()はdestroy()の後にのみ呼び出してください(たとえばテストの間)。
  • start()前に発火したconnectRouterのコールバックは無視されます。connectRouterを2回呼び出すと、両方の購読が保持されます。start()とreconcile()のパスは最新のソースが提供します。
  • config.routerとconnectRouterの両方がある場合、各ソースはそれぞれのパスに対してリコンサイルします。どちらか一方を使ってください。
  • activeWhen関数はソースから生のパスを受け取ります。組み込みルーターまたはconnectRouterでは、そのパスにクエリ文字列が含まれることがあります。文字列パターンの場合はmatchPathがそれを取り除きます。
  • updateAppPropsはネストしたオブジェクトを置き換えます。{ user: { id: 2 } }はuserプロパティ全体を上書きします。

対象外の機能 ​

  • JavaScriptをサンドボックス化しません。アプリが作成したグローバル、タイマー、リスナーはwindowに残ります。@tuvix.js/sandboxのJsSandboxを使ってください。
  • リンクのクリックをインターセプトしません。interceptLinks(ここで再エクスポートされています)を呼び出すか、フレームワークのルーターにナビゲーションを任せてください。
  • サーバーでレンダリングしません。@tuvix.js/serverとアダプターの./serverエントリーを使ってください。
  • アプリ間で依存関係を共有しません。@tuvix.js/module-federationまたはimport mapsを使ってください。
  • DOMライフサイクルイベント(tuvix:mounted、tuvix:unmounted、tuvix:error)を自身では発行しません。defineMicroAppのモジュールとフレームワークアダプターが発行します。手書きのMicroAppModuleは、@tuvix.js/loaderのemitLifecycleを呼び出さない限り何も発行しません。
  • アンマウント時にアプリを生かしたままにしません。アプリが状態を別の場所に保存しない限り、アプリ内の状態は失われます。

0.4からの移行 ​

0.5.0の破壊的変更:

  • 共有イベントバス。 eventBusを渡さない限り、オーケストレーターのバスはgetGlobalBus()です。0.4では、オーケストレーターごとに独自のバスを作成していました。destroy()はグローバルバスを残すようになりました。

    ts
    import { createOrchestrator } from '@tuvix.js/core';
    
    // 0.5: keep a private bus as in 0.4
    const orchestrator = createOrchestrator({ eventBus: {} });
  • 手動のアプリはマウントされたままになります。 リコンサイルは、activeWhenもルーターのルートも持たないアプリや、mountWhenVisibleアプリをアンマウントしなくなりました。0.4では、手動でマウントしたアプリは次のルート変更でアンマウントされていました。その動作に依存していた箇所ではunmountApp(name)を呼び出してください。

  • sandbox: trueはShadow Rootを使います。 ページのCSSはアプリに届かなくなり、エントリーのstylesもページに届かなくなりました。0.4の描画を維持するには、sandboxを削除するか{ css: false }を設定してください。

  • **prefetch: { strategy: 'hover' }**は、ホバーまたはフォーカスされたリンクがアクティブにするアプリだけをプリフェッチします。0.4では、ページ上のどこかで最初にマウスを動かすと、すべてのアプリがプリフェッチされていました。

  • activeWhenはmatchPathを使います。 クエリ文字列やハッシュでアプリがアンマウントされなくなり、:paramセグメントが機能し、.や(などの文字は文字どおりに一致します。

  • sharedは削除されました。 OrchestratorConfig.sharedは一度も読み取られておらず、型から削除されました。削除して、依存関係は@tuvix.js/module-federationまたはimport mapsで共有してください。

  • ルーターのroutesは完全一致になります。 { path: '/', app: 'home' }ルートは/でのみアクティブになります。すべてのパスで表示するアプリには'/*'、またはプレフィックス一致を維持するactiveWhen: '/'を使ってください。RouteConfig.exactは削除されたので、指定を外してください。

  • APP_ERRORのペイロード。 phaseを含むようになり、更新とアンマウントの失敗でも発火します。マウントの失敗を前提としていたハンドラーはphaseを確認してください。

外部ルーター向けの0.4の回避策は不要になります。

ts
import { createOrchestrator } from '@tuvix.js/core';

declare const router: {
  subscribe(event: 'onResolved', callback: () => void): () => void;
  state: { location: { pathname: string } };
};

const orchestrator = createOrchestrator();

// 0.4:
// router.subscribe('onResolved', () => setTimeout(() => orchestrator.reconcile(location.pathname)));

// 0.5:
orchestrator.connectRouter({
  subscribe: (callback) => router.subscribe('onResolved', callback),
  getPath: () => router.state.location.pathname,
});
await orchestrator.start();

このページの以前のバージョンには、Orchestratorに存在しないメソッドとオプションが記載されていました。実際の名前を使ってください。

以前のページ実際のAPI
createOrchestrator({ container })containerはregister()で各アプリに指定します
register(name, options)register({ name, entry, container, ... })
mount(name, props)updateAppProps(name, props)の後にmountApp(name)
unmount(name)unmountApp(name)
update(name, props)updateAppProps(name, props)
stop()destroy()
onBeforeMount, onAfterMountonStatusChange(name, status)またはOrchestratorEvent.APP_MOUNT
sandbox: { js: true }@tuvix.js/sandboxのJsSandbox