インストール
npm install @tuvix.js/vue vue
# or pnpm add @tuvix.js/vue vueサーバーレンダリングを行う場合は、オプションのピア依存であるVueのサーバーレンダラーもインストールします。
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を宣言します。
<!-- 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>それをモジュールでラップします。
// src/micro-apps/settings/index.ts
import { createVueMicroApp } from '@tuvix.js/vue';
import App from './App.vue';
export default createVueMicroApp({ name: 'settings', App });シェルに登録します。
// 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
function createVueMicroApp(config: VueMicroAppConfig): MicroAppModule;| オプション | 型 | デフォルト | 説明 |
|---|---|---|---|
name | string | 必須 | @tuvix.js/loaderのregisterModuleを通じてモジュールを登録する名前です。ライフサイクルイベントはこの名前を持ちます。 |
App | Component | 必須 | ルートのVueコンポーネントです。 |
plugins | VuePluginEntry[] | なし | 各Vueアプリにインストールするプラグインです。各エントリーはプラグイン、またはapp.use(plugin, ...options)に渡される[plugin, ...options]タプルです。 |
bootstrap | () => void | Promise<void> | なし | 一度だけ実行するセットアップです。モジュールのbootstrap()はこれをawaitします。 |
ssr | boolean | false | コンテナが要素または空白以外のテキストを含む場合、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つのモジュールを複数の場所へ同時にマウントできます。
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を下へ受け渡す代わりに、深い子コンポーネントで使います。
<!-- 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>オプション付きのプラグイン
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
function createSsrVueMicroApp(config: VueSsrMicroAppConfig): MicroAppModule;createVueMicroApp({ ...config, ssr: true })と同じです。オプションは上の表と同じくname、App、plugins、bootstrapです。
コンテナがrenderVueToStringのマークアップを含む場合、VueはcreateSSRAppを通してそのDOMノードを引き継ぎ、リスナーを付けます。サーバーとクライアントは同じルートラッパーを使うため、コンポーネントツリーは一致します。サーバーがレンダリングに使ったのと同じpropsでマウントします。
// 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サブパスからインポートします。
function renderVueToString(
App: Component,
props?: Record<string, unknown>,
plugins?: VuePluginEntry[],
ssrContext?: VueSsrContext
): Promise<string>;| 引数 | 型 | デフォルト | 説明 |
|---|---|---|---|
App | Component | 必須 | ルートコンポーネントです。createSsrVueMicroAppに渡すものと同じです。 |
props | Record<string, unknown> | {} | Appに渡すpropsです。SSR中もinject('tuvixProps')はこれを参照します。 |
plugins | VuePluginEntry[] | なし | インストールするプラグインです。createVueMicroAppと同じタプル形式を使えます。 |
ssrContext | VueSsrContext | {} | レンダリング中にVueが書き込むオブジェクトです。Promiseが解決した後にteleportsとmodulesを読みます。 |
共有のルートラッパーでSSRアプリを作成し、プラグインをインストールして、@vue/server-rendererのrenderToStringが返すHTMLを返します。このパッケージをインストールしておいてください。renderVueToStringは最初の呼び出し時にこれをインポートします。
renderVueToStringはサーバー専用です。アイソモーフィックなルートローダーからではなく、サーバー関数(TanStack StartのcreateServerFnハンドラー、APIルート、Expressハンドラー)から呼び出します。ローダーはクライアントでのナビゲーション中にブラウザでも実行され、そこではこの関数は''を返します。また、ローダーがVueのサーバーレンダラーをインポートするため、バンドラーはそれをクライアントにも含めてしまいます。
// 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
const TUVIX_PROPS_KEY = 'tuvixProps';リアクティブなpropsオブジェクトのインジェクションキーです。inject(TUVIX_PROPS_KEY)とinject('tuvixProps')は、クライアントでもrenderVueToStringの実行中でも同じオブジェクトを返します。
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
function useTuvixBus<T = unknown>(
bus: IEventBus,
event: string,
handler: EventHandler<T>
): Unsubscribe;onMountedでeventにhandlerを購読し、onUnmountedで購読を解除します。返される関数を呼ぶと早期に購読を解除できます。setup中に呼び出します。サーバーレンダリング中はonMountedが実行されないため、購読はクライアントにのみ存在します。
アダプターはオーケストレーターのバスをAppに渡しません。eventBusオプションを指定しない限り、オーケストレーターはgetGlobalBus()を使います。
<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
function useTuvixProps<T extends Record<string, unknown>>(
initialProps: T,
bus?: IEventBus,
updateEvent?: string
): ShallowRef<T>;| 引数 | 型 | デフォルト | 説明 |
|---|---|---|---|
initialProps | T | 必須 | 初期値で、一度だけコピーされます。 |
bus | IEventBus | なし | リッスンするバスです。省略するとrefはinitialPropsのままです。 |
updateEvent | string | 'tuvix:props:update' | このイベントのペイロードを、コンポーザブルがrefにマージします({ ...prev, ...payload })。 |
オーケストレーターもアダプターもtuvix:props:updateを発行しません。自分で発行します。オーケストレーターからのpropsには、バスなしでupdateに追従するdefinePropsまたはinject(TUVIX_PROPS_KEY)を使います。
<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/vue | createVueMicroAppの設定:name、App、plugins?、bootstrap?、ssr?。 |
VueSsrMicroAppConfig | @tuvix.js/vue | ssrを除いたVueMicroAppConfigで、createSsrVueMicroAppの設定です。 |
VuePluginEntry | @tuvix.js/vue | Plugin | [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で読みます。
<!-- Before (0.4) -->
<template>
<span>{{ $tuvixProps.theme }}</span>
</template><!-- 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ガイドを参照してください。