アーキテクチャ
03. ルート定義からリクエストへ
ルート定義からリクエストの振り分け、ページのパラメーター検証までの実装を読み解きます。
描画先はPage、完全なパス、middleware、周囲のLayout/Loadingスコープをまとめたものです。 Effrontはリクエストを受け付ける前に描画先を組み立て、不正な宣言と、ルートには一致してもPageのパラメーター検証に失敗するURLを区別します。
サーバーが受け取る描画先から読む
Routes はPageとマウントした子Routesの木であり、動作中のHTTP matcherではありません。EFFRONT.make の実行時に、application/route-graph.ts の compileRouteGraph がこれを描画先の配列に変換します。
packages/core/src/application/route-graph.ts の抜粋export type RouteScope<Services> = {
readonly id: string;
readonly layout: LayoutComponent<Services> | null;
readonly loading: LoadingComponent<Services> | null;
};
export type CompiledDestination<Services> = {
readonly middleware: ReadonlyArray<AnyMiddleware<Services>>;
readonly page: PageImplementationState<Services>;
readonly pattern: AbsolutePath;
readonly scopes: ReadonlyArray<RouteScope<Services>>;
};
export type CompiledRouteGraph<Services> = readonly [
CompiledDestination<Services>,
...Array<CompiledDestination<Services>>,
];/:id にある子Pageを /items の下にマウントすると、/items/:id になります。 走査では親のスコープを引き継ぎ、子RoutesにLayoutかLoadingがある場合だけ新しいスコープを加えます。 パスをまとめるだけでは描画の境界は増えません。 スコープIDは宣言の scopeId とマウント先の接頭辞を組み合わせ、同じ宣言の複数回のマウントを区別します。
packages/core/src/application/route-graph.ts の抜粋 for (const route of currentState.pages) {
const pattern = joinRoutePaths(prefix, route.path);
validateUnreservedPath(pattern);
destinations.push(
Object.freeze({ middleware, page: getPageState(route.page), pattern, scopes }),
);
}
for (const mount of currentState.mounts) {
visit(mount.routes, joinRoutePaths(prefix, mount.path), scopes, middleware);
}
};ループは現在のノードのPage、次にマウント先を、それぞれの登録順でたどります。page と mount を交互に呼んでも、全体の呼び出し順は残りません。 これは照合の優先順位ではなく走査順です。照合はEffect HTTPが担当し、このコンパイラーは静的パスと動的パスを並べ替えません。
resolveRouteMiddleware は継承した鎖を残し、共通接頭辞を除いた現在の宣言の残りを加えます。 重複が残れば TypeError になります。 コンパイラーはrootのLayoutと少なくとも一つのPageも要求し、空でない描画先の配列をfreezeして返します。 リクエストはRoutesを再走査せず、この配列を使います。
URLから取り出した値をPageの入力につなぐ
application/routes.ts の page と mount は、既存の定義を変更せずに新しい定義を返します。RoutesDefinition はLayoutの有無、登録済みパス、照合上の形を保持し、後の追加を先の定義と照らし合わせて検査します。
MatchingPageParams はURLのパラメーター名とPage Schemaの Encoded 側のキーを比較します。/items/:id ではエンコード側のキーが id と過不足なく一致する必要があり、描画にはデコード後の Type を渡します。 変換で値や出力キーを変えても、URLのパラメーター名は変わりません。 静的パスにはパラメーターSchemaのないPageを使います。
実行時の登録処理は、文法、アプリケーションidentity、パラメーターとSchemaの有無の対応を検査します。 エンコード側の全キーを比べる型レベルの検査までは繰り返しません。
application/route-path.ts の ValidRoutePath と analyzeRoutePath が、型と実行時の文法検査を対にします。
- パスは
/で始め、:idのような名前付きセグメントを使います。末尾には*pathのような名前付きcatch-allを置けます。 - rootの
/を除き、空セグメントと末尾のスラッシュは無効です。ドットセグメント、パラメーター名の重複、クエリー文字列、その他のワイルドカード形式も無効です。 - マウントの接頭辞は静的である必要があります。子Routesは空でなく、アプリケーションidentityを共有する必要があります。
アプリケーションの組み立て時の失敗を見分ける
衝突検査は静的セグメントを小文字にし、パラメーター名を除いて照合上の形を比較します。 拒否するのは同じ形であり、一致範囲が重なるすべての組み合わせではありません。
/items/:idと/items/:slugは衝突します。/Aboutと/aboutは衝突します。/manual/*pathは、取得値が空になる/manualも予約します。/items/newと/items/:idは形が異なるため共存できます。
マウントは接頭辞を結合した子のパスを検査するため、親に直接登録したPageとの衝突も検出します。joinRoutePaths は / を特別扱いし、余分なスラッシュを防ぎます。 型の検査に加え、実行時の衝突検査も TypeError を投げます。
validateUnreservedPath は結合後の最終パスを検査し、/_effront を保護します。 大文字小文字を問わず先頭セグメントが _effront の場合と、その値を取得できる動的な先頭セグメントを拒否します。 したがって最終パスの /:slug は無効ですが、/items/:slug はこの検査を通ります。 失敗は組み立て時に起き、リクエストURLの照合より前です。
ルートの照合とパラメーターの検証を分ける
server/application.ts の makeRouteLayer が、HttpRouter.add でGETとPOSTのhandlerを登録します。 URLの照合と取得値のデコードはEffect HTTPが担当します。 Effrontは * の取得値を宣言したcatch-all名へ移し、値がなければ空文字列を使います。 描画処理は HttpRouter.params を読み、PageのSchemaを適用します。
packages/core/src/server/application.ts の抜粋 if (request.method !== "POST" && destination.page.paramsSchema !== null) {
return yield* Schema.decodeEffect(destination.page.paramsSchema)(encodedParams).pipe(
Effect.matchEffect({
onFailure: () =>
Effect.succeed(
HttpServerResponse.empty({ status: 404, headers: DynamicResponseHeaders }),
),
onSuccess: (value) => renderResponse({ _tag: "Decoded", value }),
}),
);
}
return yield* renderResponse({ _tag: "Encoded", value: encodedParams });
});非POSTリクエストでは、Schemaデコードの型付き失敗を、private, no-store が付いた本文なしの404に変換します。 成功時は Decoded パラメーターを渡し、Pageでの二重デコードを防ぎます。 たとえば /items/not-a-number は /items/:id に一致しても、IDを数値にデコードするSchemaには失敗します。
ルートの不一致やURL取得値の読み取り失敗は、このSchema handlerの外で起きます。 POSTもここを通らず、描画に Encoded パラメーターを渡し、Pageコンポーネントでデコードします。 そのため、GETのSchema失敗を404にする規則は、Server Functionの入力やrefreshの失敗には当てはまりません。
リクエスト処理 は選ばれた描画先のContextと寿命を追います。描画 はスコープをUIに変換し、Server Functions はPOST経路を追います。