🔥

Honoのv3.10とv3.11について

2023/12/05に公開

昨日、Honoのv3.11.0を出しました。

https://github.com/honojs/hono/releases/tag/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>
  )
})

SuspenserenderToReadableStream()

JSXの話題が続きます。

上記の例のようなAsyncコンポーネントの場合、fetchが終わるのを待ってから結果の描画が始まります。fetchしている間になにかを表示したい場合、Suspenseが使えます。

SuspenserenderToReadableStream()を使えば、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の内容が描画されています。

SC

いわゆる遅延ロードですが、すごいのはサーバーサイドの実装だけで実現できてる点です。フロントエンドはなにも書いてません!

JSX Rendererのstreamサポート

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)

Deno用の@jsx precompileのサポート

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のコンポーネントが導入されました。これを使うとコンポーネントの中のエラーをハンドリングして代替コンテンツを表示することができます。

例えば以下の例だと、ErrorBoundaryfallbackで指定したコンテンツが表示されることになります。

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>
  )
})

ErrorBoundarySuspenseに対しても使うことができます。

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モードが使えるようになります。

SS

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関連のアップデートが多いですね。各機能を作った人は以下の通りです。敬称略。

Hono Advent Calendar

この記事はHono Advent Calendar 2023の5日の記事です。このHono Advent Calendarはまだ空きがあるので、ぜひ!

https://qiita.com/advent-calendar/2023/hono

Discussion