🔥

Honoのv4が2月9日にリリースされます

2024/01/30に公開1

X dayは2月9日です!

https://github.com/honojs/hono/issues/1844

ということで、Honoの現在のバージョンはv3系なのですが、v4を2月9日にリリースする予定です。偶然にもYAPC::Hiroshima 2024の前夜祭の日ですね。

当初はdeprecatedな機能を廃止したいという「ポジティブではない」理由でメジャーバージョンアップをしたかったのですが、大きな機能が入ることになりました。ずばりこの3つです。

  1. Static Site Generation
  2. Client Components
  3. File-based Routing

お分かりの通り、よりフルスタックなフレームワークに進化します。今回は2月9日に先駆けてこの3つの機能を軽くオーバービューしてみましょう。

RC版

v4のRC版が出てます。現在は4.0.0-rc.3が最新なので以下のコマンドでインストールできます。

npm i hono@4.0.0-rc.3

npm create honoで作ったプロジェクト内で実行するとよいでしょう。

Static Site Generation

Honoのアプリケーションから静的ページ、主にHTMLを書き出してサイトを作る機能です。@watany-devさんのPRで提案されました。

https://github.com/honojs/hono/pull/1904

例えば、以下のような超シンプルなアプリがあるとします。

src/index.tsx
import { Hono } from 'hono'
import { renderer } from './renderer'

const app = new Hono()

app.use(renderer)

app.get('/', (c) => {
  return c.render(
    <div>
      <h2>ようこそ!</h2>
      <p>現在の時刻は {new Date().toLocaleString()}</p>
    </div>
  )
})

app.get('/about', (c) => {
  return c.render(
    <div>
      <h2>私について</h2>
      <p>秘密です😘</p>
    </div>
  )
})

export default app

これを今まで通り、wrangler devとかbun runで立ち上げるとJSXがサーバーサイドでアクセスのたびに都度レンダリングされることになります。これをSSGしちゃおうというわけです。

SSGをするためのbuild.tsというファイルを作りましょう。中身はとっても簡単です。以下はNode.js上で動かす場合です。

build.ts
import fs from 'node:fs/promises'
import { toSSG } from 'hono/ssg'
import app from './src/index'

toSSG(app, fs)

toSSGという関数がページを書き出してくれるわけです。Bunの場合はアダプタがあるのでもっと簡単です。

build.ts
import { toSSG } from 'hono/bun'
import app from './src/index'

toSSG(app)

ではこれを実行してみましょう。

bun ./build.ts

すると./staticというディレクトリにHTMLページが生成されています!

$ ls static
about.html  index.html

これをこのままWranglerでCloudflare Pagesへデプロイできます。

wrangler pages deploy static

おおー、素晴らしい。これまでHonoのアプリをCloudflare Pagesにデプロイする場合は、_worker.jsというひとつファイルにアプリを全てバンドルしてアップしてサーバーサイドでレンダリングさせていたのですが、SSGという選択肢が増えました。もちろんダイナミックな挙動はさせられませんが、有効な場合はたくさんあるでしょう。

Viteと一緒に使う

さらにViteのSSGビルドをするためのプラグイン@hono/vite-ssgを作りました。これを活用すれば、viteコマンドだけでDevサーバーでの開発とSSGビルドができることになります。

設定はこれです。

vite.config.ts
import build from '@hono/vite-ssg'
import devServer from '@hono/vite-dev-server'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    build(),
    devServer({
      entry: 'src/index.tsx'
    })
  ]
})

開発をしたければ、

$ vite

とだけ打てばデフォルトでhttp://localhost:5173/にサーバーが立って、Honoアプリケーションを開発できます。また、ビルドしたければこうします。

$ vite build

するとdist以下にページが生成されます。

上記したCloudflare Pagesへのデプロイを組み合わせると、開発〜SSGのビルド〜デプロイがノンストップできます。そしてそれぞれがめっちゃ速いという(動画は2倍速です)。

SC

Client Components

hono/jsxは元々、MustacheやHandlebarsなどのテンプレートエンジンの大体として登場しました。サーバーサイドでHTMLを描画するためのものです。ReactのようにrenderToStringを意図的にせずとも、タグがそのまま文字列になるのが特徴です。以下のコードは動きます。

console.log(<h1>Hello</h1>.toString())

このサーバーサイドのJSXは思わぬ人気を博し、Honoのかかせない特徴になりました。HTMXやAlpine.jsなどクライアントのJavaScriptなしで、インタラクションを追加する方法も登場し、スタックが完成したかに思えました。「hono/jsxはサーバーサイドだけでいい」と思っていました。ところが…

hono/jsxがクライントサイドでも動くようになりました。それをClient Componentsもしくはhono/jsx/domと呼んでいます。フックも搭載し、Reactでよくみるカウンターのサンプルがそのまま動きます。

import { useState } from 'hono/jsx'
import { render } from 'hono/jsx/dom'

function Counter() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  )
}

function App() {
  return (
    <html>
      <body>
        <Counter />
      </body>
    </html>
  )
}

const root = document.getElementById('root')
render(<App />, root)

冗談ではありません。例えばこれをindex.htmlから以下のように呼び出し、Viteで立ち上げると動きます。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>hono/jsx</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

すごくないですか?

Hooks

フックは独自のものも合わせて以下があります。

  • useContext
  • useEffect
  • useState
  • useCalllback
  • use
  • useViewTransition
  • useDeferredValue

Reactと同じような使い心地なので移行できるのではないでしょうか。なお、このhono/jsxは@usualomaさんがほぼ全て実装していて、すごいです。俺たちは雰囲気でReact互換のrenderToDOMを実装しています。

めちゃくちゃ小さい

hono/jsx/domは速くてそしてめちゃくちゃ小さいです。面白いのは、サーバー、クライアント共通のものとは別にDOM専用のより小さくなるJSXのランタイムを持っています。tsconfig.jsonhono/jsxの代わりにhono/jsx/domを指定すればよいです。

"jsx": "react-jsx",
"jsxImportSource": "hono/jsx/dom"

また、うまくやれば、サーバーではhono/jsx、クライアントではhono/jsx/domを使うとできます。

上記のカウンターのexampleはBrotli圧縮で、2.8KBになります。

SS

エディタで開くとこれだけです。

SS

対してReactは、同じことをやると47.3KBです。

SS

startViewTransition

独自実装にstartViewTransitionというのがあります。これが面白いです。hono/cssと合わせて、View Transition APIを楽に扱えます。

例えば一部の機能だけでシンプルなのだとこんなコードが書けます。

import { useState, startViewTransition } from 'hono/jsx'
import { Style, css, keyframes } from 'hono/css'

const fadeIn = keyframes`
  from { opacity: 0; }
  to { opacity: 1; }
`

const App = () => {
  const [showTitleImage, setShowTitleImage] = useState(false)

  return (
    <>
      <button onClick={() => startViewTransition(() => setShowTitleImage((state) => !state))}>Click!</button>
      <div>
        {!showTitleImage ? (
          <img src="https://avatars.githubusercontent.com/u/98495527?s=48&v=4" />
        ) : (
          <div
            class={css`
              animation: ${fadeIn} 1s;
              background: url('https://github.com/honojs/hono/blob/main/docs/images/hono-title.png?raw=true');
              background-size: contain;
              background-repeat: no-repeat;
              background-position: center;
              width: 500px;
              height: 200px;
            `}
          />
        )}
      </div>
    </>
  )
}

簡単にアニメーションを作れます。

SC

他にもuseViewTransition()フックとviewTransition()ヘルパーがあります。

File-based Routing

最後がFile-based Routingです。これはhonoパッケージには含まれず、別パッケージで提供されます。現在、インターナルなレポジトリで開発されていて、2月9日に公開される予定です。とはいえ、僕が以前から開発しているSonikというフレームワークの後継となり、似ているAPIになります。

https://github.com/sonikjs/sonik

HonoとViteを組み合わせたメタフレームで、特徴は以下の5つです。

  • ファイルベースのルーティング - 関心事をわけてアプリケーションを作れます。
  • 速いSSR - Honoのおかげでサーバーサイドでとても速くレンダリングされます。
  • BYOR - Bring Your Own Renderer. hono/jsxだけではなく他のUIライブライを使ったレンダラーを使えます。
  • Islandsハイドレーション - もしインタラクションが欲しければ、Islandを作れば、そこだけクライントのJavaScriptが注入されます。
  • ミドルウェア - Honoそのものとして動くので、Honoのミドルウェアがそのまま使えます。

ファイルベースルーティング

app/routes以下に置いたファイルパスに応じてルーティングが決定します。

.
├── app
│   ├── global.d.ts // global type definitions
│   ├── routes
│   │   ├── _404.tsx // not found page
│   │   ├── _error.tsx // error page
│   │   ├── _renderer.tsx // renderer definition
│   │   ├── about
│   │   │   └── [name].tsx // matches `/about/:name`
│   │   └── index.tsx // matches `/`
│   └── server.ts // server entry file
├── package.json
├── tsconfig.json
└── vite.config.ts

一例を出すと、createRouteで定義したHandlerの配列をexportしたらそれがレンダリングされます。POSTをexportしたらPOSTリクエストをハンドリングすることになります。ポイントは、Honoのハンドラと全く同じ書き方ができることです。cはHonoのContextオブジェクトと全く同じです。

app/routes/index.tsx
import { getCookie, setCookie } from 'hono/cookie'

export const POST = createRoute(async (c) => {
  const { name } = await c.req.parseBody<{ name: string }>()
  setCookie(c, 'name', name)
  return c.redirect('/')
})

export default createRoute((c) => {
  const name = getCookie(c, 'name') ?? 'no name'
  return c.render(
    <div>
      <h1>Hello, {name}!</h1>
      <form method='POST'>
        <input type='text' name='name' placeholder='name' />
        <input type='submit' />
      </form>
    </div>
  )
})

BYOR - Bring Your Own Renderer

デフォルトでは、hono/jsxを使ったレンダラーがc.rednerに適応されます。しかし、_renderer.tsxに独自のレンダラーを定義することができます。なので、ReactやPreact、Solidのレンダラーを作れるのです。

Islandsハイドレーション

islands以下に置いたコンポーネントはクライントに自動的にハイドレートされます。このようなディレクトリ構造が考えられるでしょう。

.
├── app
│   ├── client.ts // client entry file
│   ├── global.d.ts
│   ├── islands
│   │   └── counter.tsx // island component
│   ├── routes
│   │   ├── _renderer.tsx
│   │   └── index.tsx
│   └── server.ts
├── package.json
├── tsconfig.json
└── vite.config.ts

ミドルウェア

Honoのミドルウェアがそのまま使えるのがいいですね。

app/routes/index.tsx
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

const schema = z.object({
  name: z.string().max(10),
})

export const POST = createRoute(zValidator('form', schema), async (c) => {
  const { name } = c.req.valid('form')
  setCookie(c, 'name', name)
  return c.redirect('/')
})

Tailwind CSS、MDX...

これはViteベースなので、設定次第でTailwind CSSを使ってスタイリングしたりやMDXベースのブログを作ったりもできます。

フルスタックへ

もう一度おさらいしましょう。3つの機能は以下でした。

  1. Static Site Generation
  2. Client Components
  3. File-based Routing

この3つが組み合わさるのやばくないっすか?File-based Routingでアプリケーションを作って、IslandsにClient Componentsを置いてインタラクションを追加して、SSGにも書き出せるのです。

X dayはもうすぐ!

以上、Hono v4で導入される注目の機能3つを紹介しました。X dayの2月9日はもうすぐです!

Discussion

asip2k25asip2k25

テンプレートエンジンの大体 → テンプレートエンジンの代替 ?