Honoのv4が2月9日にリリースされます
X dayは2月9日です!
ということで、Honoの現在のバージョンはv3系なのですが、v4を2月9日にリリースする予定です。偶然にもYAPC::Hiroshima 2024の前夜祭の日ですね。
当初はdeprecatedな機能を廃止したいという「ポジティブではない」理由でメジャーバージョンアップをしたかったのですが、大きな機能が入ることになりました。ずばりこの3つです。
- Static Site Generation
- Client Components
- 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で提案されました。
例えば、以下のような超シンプルなアプリがあるとします。
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上で動かす場合です。
import fs from 'node:fs/promises'
import { toSSG } from 'hono/ssg'
import app from './src/index'
toSSG(app, fs)
toSSG
という関数がページを書き出してくれるわけです。Bunの場合はアダプタがあるのでもっと簡単です。
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ビルドができることになります。
設定はこれです。
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倍速です)。
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.json
でhono/jsx
の代わりにhono/jsx/dom
を指定すればよいです。
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx/dom"
また、うまくやれば、サーバーではhono/jsx
、クライアントではhono/jsx/dom
を使うとできます。
上記のカウンターのexampleはBrotli圧縮で、2.8KBになります。
エディタで開くとこれだけです。
対してReactは、同じことをやると47.3KBです。
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>
</>
)
}
簡単にアニメーションを作れます。
他にもuseViewTransition()
フックとviewTransition()
ヘルパーがあります。
File-based Routing
最後がFile-based Routingです。これはhono
パッケージには含まれず、別パッケージで提供されます。現在、インターナルなレポジトリで開発されていて、2月9日に公開される予定です。とはいえ、僕が以前から開発しているSonikというフレームワークの後継となり、似ているAPIになります。
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
オブジェクトと全く同じです。
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のミドルウェアがそのまま使えるのがいいですね。
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つの機能は以下でした。
- Static Site Generation
- Client Components
- File-based Routing
この3つが組み合わさるのやばくないっすか?File-based Routingでアプリケーションを作って、IslandsにClient Componentsを置いてインタラクションを追加して、SSGにも書き出せるのです。
X dayはもうすぐ!
以上、Hono v4で導入される注目の機能3つを紹介しました。X dayの2月9日はもうすぐです!
Discussion
テンプレートエンジンの大体 → テンプレートエンジンの代替 ?