spa-routes について
LobeHub のコードベースで SPA のルートと機能を整理する開発スキルです。ルートの配置、Web/Electron 共通ルーティング、遅延読み込み時のスケルトン、プラットフォーム別実装を案内します。
主な機能
- 01ルートと機能実装の分離
- 02共通デスクトップルートの定義
- 03遅延ルートのスケルトン登録
- 04Web と Electron の差分処理
- 05段階的なルート移行の指針
活用シーン
- 01SPA ルートの追加
- 02ルートファイルから業務ロジックを移動
- 03Web と Electron の共通パスを更新
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ lobehub/ lobehub --skill spa-routesインストール後、Claude Code で/spa-routesを入力して読み込まれたか確認します。
詳細
SPA ルートと機能のガイド
SPA の構成:
src/spa/– エントリーポイント(entry.web.tsx、entry.mobile.tsx、entry.desktop.tsx)とルーター設定(router/)です。src/routes/との混同を避けるため、ルーターはここに置きます。src/routes/– ページセグメントのみ(ルート)。src/features/– ドメインごとのビジネスロジックと UI。
このプロジェクトでは、ルートと機能を分離しています。src/routes/ にはページセグメントのみを置き、ビジネスロジックと UI はドメインごとに src/features/ に置きます。
エージェント向けの制約 — 共有デスクトップルーター: Web/Electron 共通のパス、ネスト、メタデータ、遅延ローダー、プリロードグループは src/spa/router/desktopRouter.shared.tsx に置きます。2 つの desktopRouter.config* ファイルは薄いプラットフォームアダプターです。実行環境による真の違いがある場合にのみ変更してください。共通ルートを両方のアダプターに重複して定義しないでください。
このスキルを使用する場面
- 新しい SPA ルートまたはルートセグメントの追加
src/routes/以下のレイアウト/ページファイルの定義またはリファクタリング- ルート固有のコンポーネントやロジックを
src/features/に移動 - 新しいコンポーネントの配置先(ルートフォルダーか機能フォルダーか)の判断
1. src/routes/ に配置するもの(ルート)
各ルートディレクトリには、以下のみを含めます。
| ファイル/フォルダー | 目的 |
|---|---|
_layout/index.tsx または layout.tsx | このセグメントのレイアウトです。<Outlet /> でラップし、任意でシェル(例:サイドバー+メイン領域)を追加します。薄く保ち、@/features/* から再エクスポートまたは合成することを推奨します。 |
index.tsx または page.tsx | このセグメントのページエントリーです。機能からのみインポートしてレンダリングし、ビジネスロジックは含めません。 |
[param]/index.tsx(例:[id]、[cronId]) | 動的セグメントのページです。同じルールに従い、薄く保って機能に委譲します。 |
ルール: ルートファイルではインポートと合成のみを行います。src/routes/ 内に新しい features/ フォルダーや大きなコンポーネントを作成しないでください。
2. src/features/ に配置するもの
ドメイン指向の UI とロジックをここに配置します。
- レイアウトの構成要素:サイドバー、ヘッダー、ボディパネル、ドロワー
- そのドメインのフックとストアの使用
- ドメイン固有のフォーム、リスト、モーダルなど
ルートパスではなく、ドメイン(例:Pages、Home、Agent、PageEditor)ごとに整理します。1 つのルートで複数の機能を使用でき、1 つの機能を複数のルートで使用できます。
各機能は次の要件を満たす必要があります。
src/features/<FeatureName>/以下に配置するindex.tsまたはindex.tsxを介して明確な公開 API をエクスポートする- 必要に応じて、内部インポートには
@/features/<FeatureName>/...を使用する
3. 新しい SPA ルートの追加方法
-
ルートグループを選択する
(main)/– デスクトップのメインアプリ(mobile)/– モバイル(desktop)/– Electron 固有onboarding/、share/– 特殊なフロー
-
src/routes/以下にはセグメントファイルのみを作成する- 例:
src/routes/(main)/my-feature/_layout/index.tsxとsrc/routes/(main)/my-feature/index.tsx(任意で[id]/index.tsxも追加)。
- 例:
-
レイアウトとページコンテンツを
src/features/に実装する- ドメインを新規作成するか再利用します(例:
src/features/MyFeature/)。 - レイアウト(サイドバー、ヘッダー、ボディ)とページ UI を配置し、機能の
indexからエクスポートします。
- ドメインを新規作成するか再利用します(例:
-
ルートファイルを薄く保つ
- レイアウト:
export { default } from '@/features/MyFeature/MyLayout'を使用するか、いくつかの機能コンポーネントと<Outlet />を合成します。 - ページ:
@/features/MyFeature(または特定のサブパス)からインポートしてレンダリングし、ルートファイルにビジネスロジックを含めません。
- レイアウト:
-
正しい定義レイヤーにルートを登録する
- Web/Electron 共通ルート:
desktopRouter.shared.tsxにdynamicElement/dynamicLayoutを使ってセグメントを一度だけ追加します。共有の遅延ローダー定義の一部として、そこでpreloadIdを指定します。 - Web 専用または Electron 専用のルート: 対応する薄い
desktopRouter.config.tsxアダプターに追加します。プラットフォーム固有の違いは明示し、最小限に抑えます。 - モバイル専用のフロー:
mobileRouter.config.tsxを使用します。モバイルは共有デスクトップツリーを使用しません。
- Web/Electron 共通ルート:
-
ルートスケルトンを登録する(すべての遅延ルートで必須)
- メイン領域内のすべての遅延ルートは、チャンクの読み込み中に
RouteSegmentSkeleton(src/components/Skeleton/RouteSegment.tsx)をレンダリングします。これはhandle.meta.Skeletonのみを通じて解決されます(一致する中で最も深いルートが優先され、親ルートをたどります)。Skeletonを省略するとペインが空白になるため、それが意図的な場合はNoRouteSkeletonを使用します。 - ルートの追加や再構成に合わせてスケルトンを選択します。
- 近い形状の汎用スケルトンがある場合 →
@/components/Skeleton/SurfaceのSkeleton: createSurfaceSkeleton('list' | 'form' | 'grid' | 'editor' | 'detail')を使用します。 - ページに特徴的なレイアウト(ダッシュボード、マルチパネル、会話)がある場合 →
src/components/Skeleton/に専用コンポーネントを作成して登録します(ResourceHome.tsx、Generation.tsx、Conversation/を参照)。
- 近い形状の汎用スケルトンがある場合 →
- 登録場所:ルートの
routeMeta(機能のrouteMeta.tsまたはdesktopRouter.shared.tsx内)に設定します。同じ形状を共有するサブツリー全体では、親/レイアウトルートのhandleに一度だけ登録できます。独自のSkeletonを持つ子ルートは、それを引き続き優先します。 - ページのレイアウトを変更する場合は、同じ PR で登録済みのスケルトンも更新します。ページに合わなくなった古いスケルトンは回帰です。
- スケルトンのみを持つ親のハンドルは、タイトルについても安全です。タイトル/アイコンの解決も最も深いルートから順にたどり、リーフのメタデータが引き続き優先されます。
- 同じ最も深いスケルトンが、チェーン内のすべての保留中境界(レイアウトのチャンク、ページのチャンク、さらにページ自身のデータ待機時)でレンダリングされます。ページ内の読み込み状態を登録済みのスケルトンにするか、それを引き継ぐ本文にしてください。ここで異なる形状を使うと、別個の無関係なスケルトンに見えてしまいます。
- ルートスケルトンは常に静的インポートにします。フォールバックは初回レンダリングで表示する必要があり、
lazy()のスケルトンは(プリロード済みかどうかにかかわらず)チャンクが読み込まれるまで空白になります。そのため、スケルトンのインポートグラフ全体がエントリチャンクに含まれます。参照できるのは UI プリミティブ、他のスケルトン、定数、import typeのみにしてください。ストア、サービス、機能コンポーネント、バレルからの値インポート(@/types/*は zod スキーマを再エクスポートします)は含めず、代わりに最小限の要素をスケルトンの近くに移動します(ResourceHome/SkeletonおよびExplorer/viewMode.tsを参照)。 - カスタムフォールバック(
fallback:オプション、ページ内のSuspense、または早期のreturn <XSkeleton />)で切り替えを連続させるには、delayed()/DelayedFallbackまたはRouteLoadingを使用してスケルトンを表示中として登録する必要があります(retainSkeleton)。それ以外の方法でスケルトンをレンダリングすると、次の境界で 200ms の遅延が終わるまでペインが空白になります。
- メイン領域内のすべての遅延ルートは、チャンクの読み込み中に
3a. 共通のデスクトップルート定義とプラットフォームアダプター
| ファイル | 役割 |
|---|---|
desktopRouter.shared.tsx | 共通パス、ネスト構造、メタデータ、遅延インポート、優先順位付きルートプリロードグループの唯一の信頼できる情報源です。 |
desktopRouter.config.tsx | 薄い Web アダプターです。共通コンテンツツリーを / にマウントし、Web 専用ルートを追加します。 |
desktopRouter.config.desktop.tsx | 薄い Electron アダプターです。タブごとの Home 動作、TabHost のルートスタブ、Electron 専用のオンボーディングを注入します。 |
共通ルートの追加や削除は desktopRouter.shared.tsx でのみ行ってください。desktopRouter.sync.test.tsx をパスの動作、遅延読み込みの境界、プリロードの所有関係、意図されたプラットフォーム間の差異を検証するテストとして、常に通過する状態に保ってください。
3b. src/routes/ 内のその他の .desktop.{ts,tsx} バリアント
薄いルーターアダプターはツリーを重複して持つものではありません。ほかのルートモジュールでは、ベースの <name>.{ts,tsx} の隣に <name>.desktop.{ts,tsx} を配置する場合があります。Vite のリゾルバーは、Electron ビルドでは .desktop ファイルに差し替えます。こうした対になるモジュールの実装にも、ずれが生じるリスクがあります。
現在確認されているバリアント:
| ベースファイル(Web) | デスクトップファイル(Electron) | 目的 |
|---|---|---|
src/features/Settings/features/componentMap.ts | src/features/Settings/features/componentMap.desktop.ts | Settings タブからコンポーネントマップへの対応付けです。Web では動的な import() を使用し、デスクトップでは同期インポートを使用します。componentMap.sync.test.ts がキーの一致を検証します。 |
src/routes/(main)/agent/index.tsx | src/routes/(main)/agent/index.desktop.tsx | ページのエントリーポイントです。デスクトップのバリアントは Web ページ全体を上書きします(例:追加のポップアップガード)。 |
src/routes/(main)/group/index.tsx | src/routes/(main)/group/index.desktop.tsx | agent と同じパターンです。 |
ルール:
src/routes/内の任意の.ts/.tsxを編集した後、同じディレクトリで<filename>.desktop.{ts,tsx}の兄弟ファイルを glob で検索してください。存在する場合は、同等の変更を同じコミットで適用してください。- 新しい SettingsTab を追加する場合は、
componentMap.ts(dynamic(...)を使用)とcomponentMap.desktop.ts(同期importを使用)の両方に登録してください。そうしないと、componentMap.sync.test.tsによってビルドが失敗します。 - デスクトップ専用ページで全体を上書きするものを新たに追加する場合は、新しい
.desktop.tsxバリアントを導入するのではなく、プラットフォームに応じたコードを含む単一のベースファイルを優先してください。新しいバリアントを追加するのは、2 つのツリーが実際に異なる場合(異なるストア接続、異なるポップアップガードなど)に限ってください。 - 削除する場合は、両方のファイルをまとめて削除してください。
4. ファイルの分け方(route と feature)
| 質問 | src/routes/ に配置するもの | src/features/ に配置するもの |
|---|---|---|
| ルートのレイアウトラッパーまたはページのエントリーポイントですか? | はい — _layout/index.tsx、index.tsx、[id]/index.tsx | いいえ |
| ビジネスロジックまたは複雑な UI が含まれていますか? | いいえ | はい — 適切なドメインの下 |
| 再利用可能なレイアウト要素(サイドバー、ヘッダー、本文)ですか? | いいえ | はい |
| フック、ストアの使用、またはドメインロジックですか? | いいえ | はい |
| feature コンポーネントの再エクスポートまたは組み合わせだけを行っていますか? | はい | いいえ |
例
- ルート(薄い構成):
src/routes/(main)/page/_layout/index.tsx→export { default } from '@/features/Pages/PageLayout' - feature(実際の実装):
src/features/Pages/PageLayout/→ サイドバー、データ同期、本文、ヘッダー、スタイルなど - ルート(薄い構成):
src/routes/(main)/page/index.tsx→@/features/Pagesと@/features/PageExplorerからPageTitle、PageExplorerPlaceholderをインポートし、<PageTitle />とプレースホルダーをレンダリングします。 - feature:
ページ一覧、アクション、ドロワー、フックはsrc/features/Pages/の下に配置します。
5. 段階的な移行(既存コード)
既存のルートは、この構成に一貫して従っているわけではありません。すでに薄いセグメントファイルになっているもの(例:src/routes/(main)/page/、src/routes/(main)/settings/)もあれば、src/routes/ の下にロジックや features/ フォルダーが残っているものもあります。思い込みで判断せず、ルートの現在の状態を確認してください。
src/routes/ 内にロジックや features/ が残っている古いルートを変更する場合:
- 新しいコードは
src/features/<Domain>/に追加し、ルートからインポートすることを優先してください。 - 大規模なリファクタリングでは、既存のルート専用ロジックを適切な feature に移してから、ルートファイルを薄くしてください(feature から再エクスポートするか、組み合わせます)。
- ファイルを移動する際は、履歴を保持するために
git mvを使用してください。
6. 参照構成(フェーズ 1 完了後)
ルート(薄い構成):
src/routes/(main)/page/
├── _layout/index.tsx → re-export or compose from @/features/Pages/PageLayout
├── index.tsx → import from @/features/Pages, @/features/PageExplorer
└── [id]/index.tsx → import from @/features/Pages, @/features/PageExplorer
feature(実装):
src/features/Pages/
├── index.ts → export PageLayout, PageTitle
├── PageTitle.tsx
└── PageLayout/
├── index.tsx → Sidebar + Outlet + DataSync
├── DataSync.tsx
├── Sidebar.tsx
├── style.ts
├── Body/ → list, actions, drawer, etc.
└── Header/ → breadcrumb, add button, etc.
ルーターの設定は引き続きルートパス(例:@/routes/(main)/page、@/routes/(main)/page/_layout)を参照します。ルートファイルはその後、feature に処理を委譲します。