🍋Freshについて
FreshはDeno社製のWebフレームワークです。
Next.jsのようなファイルシステムベースのルーティングを採用していますが、デフォルトではPreactによるSSRとなっており、必要な場合のみクライアントサイドJSが配信される「アイランドアーキテクチャ」を採用しています。
また、ルーティングにはWeb標準のRequest/Responseオブジェクトを使用します。
🔥Honoについて
Honoはexpressライクなサーバーサイドフレームワークです。Web標準Request/Responseに対応しており、サーバー/クライアント間で型を共有できる「RPCモード」に対応しています。
FreshとHonoを組み合わせる
FreshではJSONなどのレスポンスを返す際、通常routes/
ディレクトリ内のファイルにhandler
関数を設定します。
import { Handler } from "$fresh/server.ts";
// この部分をHonoに置き換えたい
export const handler: Handler = (req, ctx) => {
return Response.json({ hello: "world" });
};
このhandler
関数の部分をHonoに処理させて、ルーティングをカスタマイズしていきたいと思います。
1. import-mapの設定
Honoを使う前に、import_map
にHonoの読み込み先とバージョンを設定しておきます。
この設定をしておくと、import文にhttps://deno.land/x/hono@v3.2.1/mod.ts
と書く代わりに$hono/mod.ts
と書くことができます。
{
"imports": {
"$fresh/": "https://deno.land/x/fresh@1.1.5/",
"preact": "https://esm.sh/preact@10.13.1",
"preact/": "https://esm.sh/preact@10.13.1/",
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@5.2.6",
"@preact/signals": "https://esm.sh/*@preact/signals@1.1.3",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3",
"twind": "https://esm.sh/twind@0.16.19",
"twind/": "https://esm.sh/twind@0.16.19/",
+ "$hono/": "https://deno.land/x/hono@v3.2.1/"
}
}
2. Honoで処理したいパスのルーティングを設定する
Freshでは、角括弧([]
)を用いることで、ワイルドカードを使用したルーティングを設定することができます。
routes/index.tsx --> / (ルート)にアクセスした時に実行される
routes/hello.tsx --> /helloにアクセスした時に実行される
routes/users/[name].tsx --> /users/bob や /users/alice にアクセスした時に実行される
routes/hello/[...path].ts --> /hello/foo や /hello/foo/bar など、hello/以下にアクセスした時に実行される
[...path]
のような形式のワイルドカードを使用すると、特定のディレクトリ以下へのリクエストをまとめて受け取ることができます。
これを利用して、今回は/api/
以下のパスへのリクエストをHonoで受け取るようにしてみます。コードは以下のようになります。
import { Handler } from "$fresh/server.ts";
import { Hono } from "$hono/mod.ts";
// /api/以下へのリクエストは全てこのファイルで受け取る
const app = new Hono().basePath("/api");
// ルーティングの設定
const route = app
.get("/status", (c) => c.jsonT({ status: "ok" }))
.get("/hello", (c) => c.jsonT({ hello: "world" }));
export const handler: Handler = (req) => app.fetch(req);
export type AppType = typeof route; // rpcモードを使用するときはこのAppTypeをexportする
ポイントは.basePath("/api")
でどのパスが処理の対象になっているのか明示することです。
また、後でRPCモードを使用するため、レスポンスの作成を.jsonT()
で行い、最後の行でAppType
をexportしています。
上のコードを保存した状態でサーバーを起動し、/api/hello
にアクセスすると、レスポンスが返ってくることが確認できると思います。
3. Honoで定義したAPIをRPCモードでクライアント側から呼び出す
Honoにはサーバー側で定義したエンドポイントの型定義を使用して、フロントエンド側のAPI呼び出しに型を付けることができるRPCモードが存在します。今回はこれを使ってみたいと思います。
HonoのRPCモードについては、詳しくは公式ドキュメントをご覧ください。
Freshではクライアント側のコードはislands
ディレクトリかcomponents
ディレクトリに入れる必要があります。
今回は、islands
ディレクトリにStatus
コンポーネントを作り、先ほどHonoで作成したAPIのエンドポイントを呼び出していきたいと思います。
RPCモードを使うには、以下のようなコードになります。
import { useEffect, useState } from "preact/hooks";
+ import { hc } from "$hono/mod.ts";
+ // サーバー側から必要な型をimportする
+ import type { AppType } from "../routes/api/[...path].ts";
+ // クライアント作成時に`AppType`をジェネリクスで渡す
+ const client = hc<AppType>("/");
+ async function getStatus() {
+ // API呼び出し
+ const res = await client.api.status.$get();
+ return await res.json();
+ }
export default function Hello() {
const [message, setMessage] = useState("");
+ useEffect(() => {
+ getStatus().then((data) => setMessage(data.status));
+ }, []);
return <div>{message}</div>;
}
コードの説明です。
まず、先ほどroutes/api/[...path].ts
で作成したAppType
型をimportし、hc<AppType>("/")
のようにジェネリクスで渡します。こうすることでクライアントとサーバーで共通の型定義を使用することができます。
+ import { hc } from "$hono/mod.ts";
+ import type { AppType } from "../routes/api/[...path].ts";
+ const client = hc<AppType>("/");
その後、client.api.status.$get()
関数を呼び出すとAPI呼び出しが行われます。
+ async function getStatus() {
+ const res = await client.api.status.$get();
+ return await res.json();
+ }
vscodeで確認すると、ちゃんとres.json()
の戻り値にサーバー側で定義したものと同じ型が付いていることが分かります。
以上、FreshとHonoを組み合わせてRPCモードを使う方法の解説でした。
まとめ
- FreshとHonoはどちらもWeb標準のRequest/Responseオブジェクトに対応しているため、組み合わせることができる
- Freshの特定のパス以下のルーティングを、Honoに処理させることができる
- RPCモードを使用して、クライアント側とサーバー側でAPI呼び出しの型定義を共有できる