Skip to content
💚
@tuvix.js/vue
Vueバインディング
Vue 3コンポーネントを、リアクティブなprops、コンテナごとに1つのVueアプリ、SSRハイドレーションを備えたTuvix.jsのマイクロアプリに変換します。サーバーではrenderVueToStringでレンダリングします。

インストール ​

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

サーバーレンダリングを行う場合は、オプションのピア依存であるVueのサーバーレンダラーもインストールします。

bash
npm install @vue/server-renderer
# or pnpm add @vue/server-renderer

このパッケージにはVue 3が必要です(ピア範囲は>=3.0.0)。@tuvix.js/loaderと@tuvix.js/event-busに依存します。

使用する場面 ​

Tuvixのシェル内のマイクロアプリの1つがVue 3アプリである場合に@tuvix.js/vueを使います。createVueMicroAppはルートコンポーネントを、オーケストレーターとローダーが呼び出す{ bootstrap, mount, unmount, update }モジュールでラップします。オーケストレーターはそれをコンテナにマウントし、updateAppPropsで新しいpropsを渡し、ルート変更時にアンマウントします。

SEOのため、またはJavaScriptの実行前に空白の領域ができるのを避けるために、Vueアプリを最初のHTMLレスポンスに含める必要がある場合は、サーバーレンダリングを追加します。サーバー関数内で@tuvix.js/vue/serverのrenderVueToStringを使ってHTMLをレンダリングし、それをコンテナに入れ、createSsrVueMicroAppでハイドレートします。ReactのホストページではそのHTMLをTuvixIslandに渡します。

ページ全体が1つのVueアプリであれば、このパッケージは不要です。Vue自身のcreateAppまたはNuxtを使います。

クイックスタート ​

ルートコンポーネントは、シェルが渡すpropsを宣言します。

vue
<!-- src/micro-apps/settings/App.vue -->
<script setup lang="ts">
defineProps<{ userId: string; theme: 'light' | 'dark' }>();
</script>

<template>
  <section :class="`settings theme-${theme}`">Settings for {{ userId }}</section>
</template>

それをモジュールでラップします。

ts
// src/micro-apps/settings/index.ts
import { createVueMicroApp } from '@tuvix.js/vue';
import App from './App.vue';

export default createVueMicroApp({ name: 'settings', App });

シェルに登録します。

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

const orchestrator = createOrchestrator();

orchestrator.register({
  name: 'settings',
  entry: () => import('./micro-apps/settings'),
  container: '#app',
  activeWhen: '/settings/*',
  props: { userId: 'u_42', theme: 'light' },
});

await orchestrator.start();

// Re-renders App with theme="dark"; local component state survives.
await orchestrator.updateAppProps('settings', { theme: 'dark' });

API ​

createVueMicroApp ​

ts
function createVueMicroApp(config: VueMicroAppConfig): MicroAppModule;
オプション型デフォルト説明
namestring必須@tuvix.js/loaderのregisterModuleを通じてモジュールを登録する名前です。ライフサイクルイベントはこの名前を持ちます。
AppComponent必須ルートのVueコンポーネントです。
pluginsVuePluginEntry[]なし各Vueアプリにインストールするプラグインです。各エントリーはプラグイン、またはapp.use(plugin, ...options)に渡される[plugin, ...options]タプルです。
bootstrap() => void | Promise<void>なし一度だけ実行するセットアップです。モジュールのbootstrap()はこれをawaitします。
ssrbooleanfalseコンテナが要素または空白以外のテキストを含む場合、createSSRAppでサーバーのマークアップをハイドレートします。空のコンテナにはcreateAppでレンダリングします。

返されるモジュール:

  • mount({ container, props })はそのコンテナ用にVueアプリを作成し、プラグインをインストールしてマウントします。Vueがサーバーのマークアップを引き継いだ場合はhydrated: trueを付けて、コンテナ上でtuvix:mountedをディスパッチします。
  • update({ props, container })はそのコンテナのリアクティブなpropsオブジェクトにpropsを書き込み、Vueが再レンダリングをフラッシュした後(nextTick)に解決します。新しいpropsにないキーは削除されます。containerを省略すると、モジュールがマウントしたすべてのコンテナを更新します。
  • unmount({ container })はVueアプリをアンマウントし、replaceChildren()でコンテナを空にしてtuvix:unmountedをディスパッチします。
  • bootstrap()はconfig.bootstrapを実行します。

コンテナごとに独自のVueアプリと独自のpropsを持つため、1つのモジュールを複数の場所へ同時にマウントできます。

ts
import { createVueMicroApp } from '@tuvix.js/vue';
import Badge from './Badge.vue';

const badge = createVueMicroApp({ name: 'badge', App: Badge });

const left = document.createElement('div');
const right = document.createElement('div');
document.body.append(left, right);

await badge.mount({ container: left, props: { label: 'New' } });
await badge.mount({ container: right, props: { label: 'Sale' } });

// Only the right badge changes.
await badge.update?.({ props: { label: 'Sold out' }, container: right });

await badge.unmount({ container: left });

コンポーネントでpropsを読む ​

アダプターはpropsをshallowReactiveオブジェクトに保持し、それらをコンポーネントのpropsとして渡すルートラッパーを通してAppをレンダリングします。ラッパー自体はDOMをレンダリングしません。propsは次の2つの方法で読み取ります。

  • ルートコンポーネントでの**defineProps**。updateは新しい値でルートコンポーネントを再レンダリングします。
  • ツリー内の任意のコンポーネントでの**inject('tuvixProps')**(またはinject(TUVIX_PROPS_KEY))。同じリアクティブオブジェクトが得られるため、それを読むcomputedはupdateの後に更新されます。propsを下へ受け渡す代わりに、深い子コンポーネントで使います。
vue
<!-- src/micro-apps/settings/ThemeLabel.vue -->
<script setup lang="ts">
import { computed, inject } from 'vue';
import { TUVIX_PROPS_KEY } from '@tuvix.js/vue';

const tuvixProps = inject<Record<string, unknown>>(TUVIX_PROPS_KEY, {});
const theme = computed(() => String(tuvixProps.theme ?? 'light'));
</script>

<template>
  <span>Theme: {{ theme }}</span>
</template>

オプション付きのプラグイン ​

ts
import { createVueMicroApp } from '@tuvix.js/vue';
import type { VuePluginEntry } from '@tuvix.js/vue';
import type { App as VueApp } from 'vue';
import App from './App.vue';

const analytics = {
  install(app: VueApp, options: { siteId: string }) {
    app.provide('analyticsSiteId', options.siteId);
  },
};

const plugins: VuePluginEntry[] = [[analytics, { siteId: 'site-42' }]];

export default createVueMicroApp({ name: 'checkout', App, plugins });

コンテナごとに独自のVueアプリを持つため、マウントのたびにすべてのプラグインについてapp.useが再度呼び出されます。createPinia()の結果のような状態を持つプラグインオブジェクトは、モジュールのすべてのコンテナで共有されます。

createSsrVueMicroApp ​

ts
function createSsrVueMicroApp(config: VueSsrMicroAppConfig): MicroAppModule;

createVueMicroApp({ ...config, ssr: true })と同じです。オプションは上の表と同じくname、App、plugins、bootstrapです。

コンテナがrenderVueToStringのマークアップを含む場合、VueはcreateSSRAppを通してそのDOMノードを引き継ぎ、リスナーを付けます。サーバーとクライアントは同じルートラッパーを使うため、コンポーネントツリーは一致します。サーバーがレンダリングに使ったのと同じpropsでマウントします。

ts
// src/micro-apps/contact/client.ts
import { createSsrVueMicroApp } from '@tuvix.js/vue';
import ContactApp from './App.vue';

const contact = createSsrVueMicroApp({ name: 'contact', App: ContactApp });

await contact.mount({ container: document.getElementById('contact')!, props: { lang: 'en' } });

renderVueToString ​

@tuvix.js/vue/serverサブパスからインポートします。

ts
function renderVueToString(
  App: Component,
  props?: Record<string, unknown>,
  plugins?: VuePluginEntry[],
  ssrContext?: VueSsrContext
): Promise<string>;
引数型デフォルト説明
AppComponent必須ルートコンポーネントです。createSsrVueMicroAppに渡すものと同じです。
propsRecord<string, unknown>{}Appに渡すpropsです。SSR中もinject('tuvixProps')はこれを参照します。
pluginsVuePluginEntry[]なしインストールするプラグインです。createVueMicroAppと同じタプル形式を使えます。
ssrContextVueSsrContext{}レンダリング中にVueが書き込むオブジェクトです。Promiseが解決した後にteleportsとmodulesを読みます。

共有のルートラッパーでSSRアプリを作成し、プラグインをインストールして、@vue/server-rendererのrenderToStringが返すHTMLを返します。このパッケージをインストールしておいてください。renderVueToStringは最初の呼び出し時にこれをインポートします。

renderVueToStringはサーバー専用です。アイソモーフィックなルートローダーからではなく、サーバー関数(TanStack StartのcreateServerFnハンドラー、APIルート、Expressハンドラー)から呼び出します。ローダーはクライアントでのナビゲーション中にブラウザでも実行され、そこではこの関数は''を返します。また、ローダーがVueのサーバーレンダラーをインポートするため、バンドラーはそれをクライアントにも含めてしまいます。

ts
// src/server/contact.ts
import { createServerFn } from '@tanstack/react-start';
import { renderVueToString } from '@tuvix.js/vue/server';
import type { VueSsrContext } from '@tuvix.js/vue/server';
import ContactApp from '../micro-apps/contact/App.vue';

export const renderContact = createServerFn().handler(async () => {
  const ssrContext: VueSsrContext = {};
  const html = await renderVueToString(ContactApp, { lang: 'en' }, [], ssrContext);
  return { html, teleports: ssrContext.teleports ?? {} };
});

ページ内のコンテナ要素(<div id="contact">…</div>)にhtmlを入れ、上のとおりクライアントでcreateSsrVueMicroAppをマウントします。<Teleport>のターゲットのHTMLは、返される文字列ではなくssrContext.teleports['#target']に入ります。ターゲットがある場所に出力してください。

TUVIX_PROPS_KEY ​

ts
const TUVIX_PROPS_KEY = 'tuvixProps';

リアクティブなpropsオブジェクトのインジェクションキーです。inject(TUVIX_PROPS_KEY)とinject('tuvixProps')は、クライアントでもrenderVueToStringの実行中でも同じオブジェクトを返します。

ts
import { inject } from 'vue';
import { TUVIX_PROPS_KEY } from '@tuvix.js/vue';

export function useTheme(): string {
  const props = inject<Record<string, unknown>>(TUVIX_PROPS_KEY, {});
  return String(props.theme ?? 'light');
}

useTuvixBus ​

ts
function useTuvixBus<T = unknown>(
  bus: IEventBus,
  event: string,
  handler: EventHandler<T>
): Unsubscribe;

onMountedでeventにhandlerを購読し、onUnmountedで購読を解除します。返される関数を呼ぶと早期に購読を解除できます。setup中に呼び出します。サーバーレンダリング中はonMountedが実行されないため、購読はクライアントにのみ存在します。

アダプターはオーケストレーターのバスをAppに渡しません。eventBusオプションを指定しない限り、オーケストレーターはgetGlobalBus()を使います。

vue
<script setup lang="ts">
import { ref } from 'vue';
import { useTuvixBus } from '@tuvix.js/vue';
import { getGlobalBus } from '@tuvix.js/event-bus';

const count = ref(0);

useTuvixBus<{ items: unknown[] }>(getGlobalBus(), 'cart:updated', ({ items }) => {
  count.value = items.length;
});
</script>

<template>
  <span>{{ count }}</span>
</template>

useTuvixProps ​

ts
function useTuvixProps<T extends Record<string, unknown>>(
  initialProps: T,
  bus?: IEventBus,
  updateEvent?: string
): ShallowRef<T>;
引数型デフォルト説明
initialPropsT必須初期値で、一度だけコピーされます。
busIEventBusなしリッスンするバスです。省略するとrefはinitialPropsのままです。
updateEventstring'tuvix:props:update'このイベントのペイロードを、コンポーザブルがrefにマージします({ ...prev, ...payload })。

オーケストレーターもアダプターもtuvix:props:updateを発行しません。自分で発行します。オーケストレーターからのpropsには、バスなしでupdateに追従するdefinePropsまたはinject(TUVIX_PROPS_KEY)を使います。

vue
<script setup lang="ts">
import { useTuvixProps } from '@tuvix.js/vue';
import { getGlobalBus } from '@tuvix.js/event-bus';

const settings = useTuvixProps({ theme: 'light' }, getGlobalBus());
// Elsewhere: getGlobalBus().emit('tuvix:props:update', { theme: 'dark' });
</script>

<template>
  <p>Theme: {{ settings.theme }}</p>
</template>

型 ​

型エクスポート元説明
VueMicroAppConfig@tuvix.js/vuecreateVueMicroAppの設定:name、App、plugins?、bootstrap?、ssr?。
VueSsrMicroAppConfig@tuvix.js/vuessrを除いたVueMicroAppConfigで、createSsrVueMicroAppの設定です。
VuePluginEntry@tuvix.js/vuePlugin | [Plugin, ...any[]]:プラグイン、またはプラグインとそのapp.useオプションです。
VueSsrContext@tuvix.js/vue/server{ teleports?: Record<string, string>; modules?: Set<string>; [key: string]: unknown }。renderVueToStringの実行中にVueとそのプラグインが書き込みます。@vitejs/plugin-vueはレンダリングされたコンポーネントのモジュールをmodulesに記録します。

エッジケース ​

  • マウント時のエラー。 mount中のsetup、レンダリング、ライフサイクルフックのエラーは、開発ビルドでも本番ビルドでもPromiseをrejectします。アダプターは失敗したアプリをアンマウントし、tuvix:errorをディスパッチします。プラグイン自身のapp.config.errorHandlerも引き続きエラーを受け取ります。
  • 同じコンテナへの2回のマウント。 モジュールがすでに保持しているコンテナへの2回目のmountは、まず古いアプリをアンマウントし(tuvix:unmountedをディスパッチしてコンテナを空にする)、新しいアプリをマウントします。ssr: trueの場合、コンテナが空になっているため、この2回目のマウントは新規にレンダリングします。
  • 未知のコンテナ。 モジュールがマウントしていないコンテナに対するunmountは何もしません。そのようなcontainerを指定したupdateは何も変更しません。
  • propsは置き換えられます。 モジュールのupdateはpropsを渡したオブジェクトに設定し、存在しないキーを削除します。orchestrator.updateAppPropsはupdateを呼び出す前に、新しいpropsを登録済みのpropsにマージします。
  • 宣言されていないprops。 ルートコンポーネントがdefinePropsで宣言していないpropsは属性として渡され、Vueが任意のコンポーネントで行うのと同様にルート要素へフォールスルーします。langを宣言するかinheritAttrs: falseを設定しない限り、{ lang: 'en' }はルート要素にlang="en"をレンダリングします。
  • ssr: trueで空白のみのコンテナ。 hasServerMarkupが空白テキストとコメントを無視するため、アダプターはcreateAppでレンダリングします。Vueは空のコンテナのハイドレーションについて警告しません。
  • ブラウザでのrenderVueToString。 windowが存在する場合は''を返します。
  • アンマウント時のエラー。 app.unmount()が例外を投げた場合、アダプターはコンテナを空にし、tuvix:errorをディスパッチして再スローします。
  • 同じ名前を2回使う場合。 registerModuleは、その名前で最後に作成されたモジュールを保持します。

対象外の機能 ​

  • Vue 2はサポートしません。
  • URLからモジュールを読み込みません。モジュールをインポートするか、@tuvix.js/coreにentry経由で読み込ませます。
  • MountContext.busやMountContext.nameをコンポーネントに渡しません。@tuvix.js/event-busのgetGlobalBus()を使います。
  • renderVueToStringはストリーミングせず、CSSも収集しません。サーバーレンダリングされたアイランドのスタイルシートリンクを出力するには@tuvix.js/viteを使います。
  • テレポートされたHTMLを配置しません。ssrContext.teleportsを読んで出力します。

0.4からの移行 ​

$tuvixPropsは削除されました。 update()はapp.config.globalProperties.$tuvixPropsに書き込まなくなりました。propsはdefinePropsまたはinjectで読みます。

vue
<!-- Before (0.4) -->
<template>
  <span>{{ $tuvixProps.theme }}</span>
</template>
vue
<!-- After (0.5) -->
<script setup lang="ts">
defineProps<{ theme: string }>();
</script>

<template>
  <span>{{ theme }}</span>
</template>

未知のコンテナに対するunmountは何もしません。 0.4ではコンテナを空にしていました。その動作に依存していた場合は、container.replaceChildren()を自分で呼び出します。

VueSsrMicroAppConfigにはssrフィールドがありません。 createSsrVueMicroAppに渡す設定からssrを削除します。

設定オブジェクト。 このページの以前の版ではcreateVueMicroApp(App)と示していました。この関数は設定オブジェクトを受け取ります:createVueMicroApp({ name: 'settings', App })。

その他の解説はVueガイドを参照してください。