先日、「DenoとHonoでWebAuthnを使ったログインを実装する」というLTを見せていただいた。
Fresh x Hono の組み合わせについて興味を持ったので、自分でも試してみた。
参考
実装
基本
標準 Request => 標準 Response の形式をFreshとHonoが取るので、相性いいことは先のLTで学んだ。
これを使うと、次のようなAPIとIslandsを構築できる。
deno.json(抜粋)1 2 3 4 5 6 7 8 9
| { "imports": { "$fresh/": "https://deno.land/x/fresh@1.6.5/", "preact": "https://esm.sh/preact@10.19.2", "preact/": "https://esm.sh/preact@10.19.2/", "@hono/": "https://deno.land/x/hono@v4.0.10/", }, }
|
routes/api/[...path].ts1 2 3 4 5 6 7 8 9 10
| import { Handler } from "$fresh/server.ts"; import { Hono } from "@hono/mod.ts";
const app = new Hono().basePath("/api");
export const appRoute = app.get("/hello_world", (c) => { return c.json({ text: "Hello World!" }); });
export const handler: Handler = app.fetch;
|
islands\HelloWorld.tsx1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import { useEffect, useState } from "preact/hooks"; import { hc } from "@hono/mod.ts"; import { appRoute } from "../routes/api/[...path].ts"; const client = hc<typeof appRoute>("/");
async function getStatus() { const res = await client.api.hello_world.$get(); return await res.json(); }
export default function Hello() { const [message, setMessage] = useState("");
useEffect(() => { getStatus().then((data) => setMessage(data.text)); }, []);
return <div>{message}</div>; }
|
とこんな感じで、APIからメッセージを拾って表示できる。
hono(RPC)は、hono/openapiを噛ませてもできる様子だが、エディタがうまく解釈できなかった(自分が悪いはある)。
なので、ここではスキップする。
FreshのcontextからHonoへのデータ渡し
APIはHonoの領分として処理するのは楽だが、現状Freshミドルウェアで設定したContextのStateをHonoに渡すことはできていない。
それは、ContextはRequestにデータが乗らないから。
これは、以下のように実装できた。
routes/_middleware.ts1 2 3 4 5 6 7 8 9 10 11 12 13
| import { FreshContext } from "$fresh/server.ts";
interface State { data: string; }
export async function handler( _req: Request, ctx: FreshContext<State>, ) { ctx.state.data = "middleware data"; return await ctx.next(); }
|
routes/api/[...path].ts1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { Handler,FreshContext } from "$fresh/server.ts"; import { Hono } from "@hono/mod.ts";
interface FromFreshState { data:string } const app = new Hono().basePath("/api");
export const appRoute = app.get("/hello_world_with_message", (c) => { return c.json({ text: `Hello World with '${c.env?.data}'` }); })
export const handler: Handler = async (req, context: FreshContext<Record<string, unknown>>) => { return await app.fetch(req, {data: context.state.data}); };
|
このようにすると、返却される文字列は、Hello World with 'middleware data'
になる。
なお、env へのデータ設定が適切なものであるという資料は見当たらなかったのであしからず。
これができれば、Freshのレイヤーでログイン管理処理して、Honoでその他クエリを処理に使用できる。
HonoからFreshのcontextからへのデータ渡し
また、Hono => Fresh にContextで何かを返すなら、honoの処理の返却はResponse なのでHeaderに詰めて返すことができる。
ResponseからFreshのContextに値を渡し直す。
routes/_middleware.ts1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { FreshContext } from "$fresh/server.ts";
interface State { data: string; honoData: string; }
export async function handler( _req: Request, ctx: FreshContext<State>, ) { ctx.state.data = "middleware data"; const res = await ctx.next(); res.headers.set("server", ctx.state.honoData);
return res; }
|
routes/api/[...path].ts1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import { FreshContext, Handler } from "$fresh/server.ts"; import { Hono } from "@hono/mod.ts";
interface FromFreshState { data: string; } const app = new Hono().basePath("/api");
export const appRoute = app .get("/hello_world_with_context", (c) => { c.header("honoData", "hono data"); return c.json({ text: `Hello World with '${c.env?.data}'` }); });
export const handler: Handler = async ( req, context: FreshContext<Record<string, unknown>>, ) => { const res = await app.fetch(req, { data: context.state.data }); context.state.honoData = res.headers.get("honoData"); res.headers.delete("honoData");
return res; };
|
というわけで、Freshに組み込んだHonoとFreshの間での情報やり取りについてメモがてらまとめた。
ボイラープレートのような形で、各ファイルに書く必要が低減してスッキリ書けるという良さがあった。
とてもいい。
先のLTで、Fresh x Hono の熱が高まったので次に開発したアプリでも採用した。
では。