アプリページに client:only を使わない:静的シェルと明示的なマウント

ゲームとツールのページは「シェル + createApp」で、client:only ではありません。読み込み・成功・失敗の三状態を自分で持ち、再試行ボタンも本当に動きます。

ゲームとツールのページには共通点があります。どちらも [slug] の動的ルートで、コンポーネントが URL によって変わります。まさに <Component client:only> が役に立たなくなる場面です。

client:only が与えてくれないもの

client:only がやることは一つ、SSR を飛ばしてブラウザで描画すること。読み込み状態も、失敗状態も、再試行も与えてくれません。

結果として三つを自分で作ることになります。chunk の取得中はページが空白で、読み込み中か壊れているか分かりません。マウントが例外を投げてもコンソールに行くだけで画面は空のままです。再試行したければ、結局は自前の状態機械を書くことになります。

しかも動的ルートではコンポーネントは変数で、ハイドレーション指令はコンパイル時に確定します。逆らうほうが手間です。

シェルの形

AppStage.astro は状態を持つコンテナを一つ出します。

<div data-app-stage data-state="loading">
  <div data-app-root data-app-kind="games" data-app-slug="snake" data-app-locale="zh"></div>
</div>

マウンターは data-* 属性を読むだけで、個々のアプリを知りません。

const loader = loaders[loaderKey(kind, slug)];
const mod = await loader();
createApp(mod.default, { locale }).mount(root);

三状態は CSS が持つ

data-state が変われば、どのブロックを表示するかはスタイルが決めます。読み込み中の文言もエラーパネルも再試行ボタンも静的な HTML として出荷され、JS に依存しません。スクリプトが落ちても、ユーザーは起動できなかったこととボタンを見られます。

各アプリは独立した chunk のまま

レジストリはリテラルの相対パスで import.meta.glob を使います。

const loaders = {
  ...import.meta.glob('../games/*/App.vue'),
  ...import.meta.glob('../tools/*/App.vue'),
};

Vite はビルド時にこれを「ファイルパス → 動的 import」のマップへ展開し、各 App.vue が個別の chunk になります。開いたアプリだけがダウンロードされます。ここでエイリアスは使えません。Vite はリテラルの相対パスしか受け付けません。

代償

アプリページは内容ページのようなゼロ JS ではありません。マウンター本体と最低一回の import() を運びます。内容ページは Vue ランタイムを一切読み込みません。線は「このページはアプリか」で引かれています。

読み込み中と失敗は、フレームワークが与えるものではなく、そのページがもともと持っている二つの状態です。手放すことは、設計からその二つを消すことと同じです。

← 記事一覧に戻る

コメント

…