Honoのv3.10とv3.11について
昨日、Honoのv3.11.0を出しました。
v3.10.0とあわせて導入された代表的な新機能を紹介します。
Asyncコンポーネントサポート
Hono組み込みのJSXでAsyncコンポーネントがサポートされました。コンポーネントの中でasync
/await
が使えます。
const AsyncComponent = async () => {
const res = await fetch('https://ramen-api.dev/shops/yoshimuraya')
const data = await res.json<{ shop: { name: string } }>()
return <div>{data.shop.name} 🍜</div>
}
app.get('/', (c) => {
return c.html(
<html>
<body>
<h1>My favorite ramen shop</h1>
<AsyncComponent />
</body>
</html>
)
})
Suspense
とrenderToReadableStream()
JSXの話題が続きます。
上記の例のようなAsyncコンポーネントの場合、fetch
が終わるのを待ってから結果の描画が始まります。fetch
している間になにかを表示したい場合、Suspense
が使えます。
Suspense
とrenderToReadableStream()
を使えば、fetch
している間、fallback
内で指定したローディング画面などを表示しておくことができます。そして、Promiseが解決されるとコンポーネントのコンテンツが描画されます。
import { renderToReadableStream, Suspense } from 'hono/jsx/streaming'
// ...
app.get('/', (c) => {
const stream = renderToReadableStream(
<html>
<body>
<h1>My favorite ramen shop</h1>
<Suspense fallback={<div>loading...</div>}>
<AsyncComponent />
</Suspense>
</body>
</html>
)
return c.body(stream, {
headers: {
'Content-Type': 'text/html; charset=UTF-8',
'Transfer-Encoding': 'chunked'
}
})
})
以下の動画はわざと2秒待ってからfetch
をしている例です。最初にloadingが表示され、2秒と少し経ってからfetch
の内容が描画されています。
いわゆる遅延ロードですが、すごいのはサーバーサイドの実装だけで実現できてる点です。フロントエンドはなにも書いてません!
stream
サポート
JSX RendererのJSX Rendererミドルウェアにstream
オプションが入ってSuspense
が使えるようなりました。これを使うとわざわざrenderToReadableStream()
とTransfer-Encoding: chunked
のようなヘッダを書かなくてもストリーミングレスポンスが返せるようになります。
import { jsxRenderer } from 'hono/jsx-renderer'
// ...
app.get(
'*',
jsxRenderer(
({ children }) => {
return (
<html>
<body>
<h1>My favorite ramen shop</h1>
{children}
</body>
</html>
)
},
{
stream: true
}
)
)
app.get('/', (c) => {
return c.render(
<Suspense fallback={<div>loading...</div>}>
<AsyncComponent />
</Suspense>
)
})
AWS LambdaアダプタのStreamingサポート
AWS LambdaアダプタにstreamHandle()
ができました。これを使うとAWS Lambdaでもストリーミングレスポンスを返すことができます。
import { Hono } from 'hono'
import { streamHandle } from 'hono/aws-lambda'
const app = new Hono()
app.get('/stream', async (c) => {
return c.streamText(async (stream) => {
for (let i = 0; i < 3; i++) {
await stream.writeln(`${i}`)
await stream.sleep(1)
}
})
})
const handler = streamHandle(app)
@jsx precompile
のサポート
Deno用のDenoのv1.38ではサーバーサイドのJSXレンダリングを高速化するための@jsx precompile
という機能が入りました。
This release introduces a new JSX transform that is optimized for server-side rendering. It works by serializing the HTML parts of a JSX template into static string arrays at compile time, instead of creating hundreds of short lived objects.
HonoのJSXではこれにいち早く対応しました。
deno.json
を以下のように設定すればOKです。
{
"compilerOptions": {
"jsx": "precompile",
"jsxImportSource": "hono/jsx"
},
"imports": {
"hono/jsx/jsx-runtime": "https://deno.land/x/hono@v3.10.0/jsx/jsx-runtime.ts"
}
}
ErrorBoundary
ここからはv3.11の機能です。
ErrorBoundary
というJSXのコンポーネントが導入されました。これを使うとコンポーネントの中のエラーをハンドリングして代替コンテンツを表示することができます。
例えば以下の例だと、ErrorBoundary
のfallback
で指定したコンテンツが表示されることになります。
import { ErrorBoundary } from 'hono/jsx'
// ...
function SyncComponent() {
throw new Error('Error')
return <div>Hello</div>
}
app.get('/sync', async (c) => {
return c.html(
<html>
<body>
<ErrorBoundary fallback={<div>Out of Service</div>}>
<SyncComponent />
</ErrorBoundary>
</body>
</html>
)
})
ErrorBoundary
はSuspense
に対しても使うことができます。
async function AsyncComponent() {
await new Promise((resolve) => setTimeout(resolve, 2000))
throw new Error('Error')
return <div>Hello</div>
}
app.get('/with-suspense', async (c) => {
return c.html(
<html>
<body>
<ErrorBoundary fallback={<div>Out of Service</div>}>
<Suspense fallback={<div>Loading...</div>}>
<AsyncComponent />
</Suspense>
</ErrorBoundary>
</body>
</html>
)
})
createFactory()
とcreateHandlers()
FactoryヘルパーがcreateFactory()
を提供するようになりました。これはFactoryクラスのインスタンスを返します。
import { createFactory } from 'hono/factory'
const factory = createFactory()
このインスタンスから利用できるcreateHandlers()
を使うと型解決をいい感じにしつつハンドラの定義ができます。
import { createFactory } from 'hono/factory'
import { logger } from 'hono/logger'
// ...
const factory = createFactory<Env>()
const middleware = factory.createMiddleware(async (c, next) => {
c.set('foo', 'bar')
await next()
})
const handlers = factory.createHandlers(logger(), middleware, (c) => {
return c.json(c.var.foo)
})
app.get('/api', ...handlers)
Ruby on Railsのようにルート定義とハンドラ定義を分けたい場合、このcreateHandlers()
を使ってください。
Devヘルパー
新しくDevヘルパーができました。
今までアプリに登録されいてるルーティングをデバグ用に表示するのにapp.showRoutes()
が便利だったのですが、それがdeprecated
になり、DevヘルパーのshowRoutes()
を使うようになります。
例えば、以下のようなアプリがあったとします。
import { showRoutes } from 'hono/dev'
// ...
const app = new Hono().basePath('/v1')
app.get('/posts', (c) => {
// ...
})
app.get('/posts/:id', (c) => {
// ...
})
app.post('/posts', (c) => {
// ...
})
showRoutes(app)
すると、以下がコンソールに表示されます。
GET /v1/posts
GET /v1/posts/:id
POST /v1/posts
verbose
オプションもあります。
GET /v1/*
cors
GET /v1/posts
[handler]
GET /v1/posts/:id
[handler]
POST /v1/posts
[handler]
c.json()
のRPCサポート
c.json()
がRPCをサポートしました。これによりc.jsonT()
と書かずともRPCモードが使えるようになります。
c.req.routePath
c.req.routePath
を使うと、ハンドラの中でルート定義を取得することができます。
app.get('/posts/:id', (c) => {
return c.json({ path: c.req.routePath })
})
もしGET /posts/123
というアクセスが来たら/posts/:id
という値になります。ロギングなどに便利です。
{ "path": "/posts/:id" }
コントリビューター
以上、v3.10とv3.11で導入さた新機能をみてきました。JSX関連のアップデートが多いですね。各機能を作った人は以下の通りです。敬称略。
- Asyncコンポーネントサポート - @usualoma
-
Suspense
とrenderToReadableStream()
- @usualoma - JSX Rendererの
stream
サポート - @usualoma - AWS LambdaアダプタのStreamingサポート - @watany-dev
- Deno用の
@jsx precompile
のサポート - @usualoma -
ErrorBoundary
- @usualoma -
createFactory()
とcreateHandlers()
- @yusukebe - Devヘルパー - @usualoma
-
c.json()
のRPCサポート - @usualoma -
c.req.routePath
- @usualoma
Hono Advent Calendar
この記事はHono Advent Calendar 2023の5日の記事です。このHono Advent Calendarはまだ空きがあるので、ぜひ!
Discussion