Honoハンズオン2024年3月沖縄
編集中
ここにコードを置くと思います。
このイベントが今週末で、その中で「Honoハンズオン」をやるので超絶ネタバレですが、そこで話す内容を書きます。
方針
ハンズオンと言いつつ「みんなで一斉にやりましょう」ってやると合わせるのに時間がかかるので、僕がどんどん進めます。なので、ついてきたい人だけついてきてください。そうじゃない人は僕がコード書くのを見てください。むしろその方がよくわかっていいと思います。
Honoとは?
ウェブサイトを見てください。
あと正直、僕はブログ書くのとか好きなんですが、いわゆるちゃんとした「ドキュメント」を書くのが正直苦手でそれは英語に限らずなんですが、なので、よくしている方いたら貢献してください。コントリビューションウェルカム!
create-hono
さてこれからやっていくわけですが、Honoのプロジェクトをつくるときにはcreate-honoというCLIを使います。そのテンプレートはいくつかあるわけですが、今回は以下の3つを使っていきましょう。
-
cloudflare-workers
- Cloudflare Workers -
cloudflare-pages
- Cloudflare Pages -
x-basic
- HonoX
ではガンガンいきますね。
Cloudflare Workers
まずはCloudflare Workersです。
あ、その前にWorkersとPagesの違いを解説したほうがいいですかね。
WorkersとPagesについて
ここに書いてあるのでみて。
3分でデプロイ
さて、Honoのプロジェクトを作るのは簡単です。それにCloudflare Workersへのデプロイも一瞬です。3分で全部やってみましょう。もしかして3分以内かもしれないし、超えるかもしれないけど、3分くらいでしょう。
まずcreate-hono。ちなみに今回はBunのbun
コマンドをパッケージマネージャーとして使います。気に入らない人はnpm
とかyarn
でやってください。これについて詳しいことは書きません。
bun create hono my-app-okinawa
cd my-app-okinawa
bun run dev
はい。これでプロジェクトの初期化、そして開発サーバが立ち上がったわけです。ではデプロイしてみましょう。
bun run deploy
できました!3分かかりましたかね?(時計を見る)
レスポンスを返す、リクエストをハンドリングする
先ほどはいわゆる「Hello World」だったので、もうちょっと工夫してみましょう。
- JSONを返してみましょう
- HTMLを返してみましょう
- リダイレクトをする
- 生のResponseを返す
- ヘッダーを追加する
さて、今度はリクエストを扱ってみましょう。このc.req
というやつですが、これはWeb StandardのRequestとは違いHono特有の「HonoRequest」というオブジェクトです。純粋なRequestにアクセスしたければ「c.req.raw」を使ってください。ちょっと応用編ですが、Cloudflareの場合「c.req.raw.cf」でCloudflare特有のプロパティにアクセスできるので試してみてください。
HonoRequestの扱いについてですね。これをやってみます。
- クエリを取得する
- パスパラメータを取得する
- ヘッダーを取得する
- ボディを取得する、JSON、フォーム
ルーティング
あ、ルーティングについての説明がまだでしたね。ルーティングとはこのパスにこのメソッドできたらどのハンドラを実行するかどうかというものです。Honoでは基本的なルーティングをサポートしていてだいたいできます。
- 基本
- パスパラメータ
- 正規表現
- チェーン
- グループ、
app.route()
ミドルウェアを使う
ではいよいよHonoっぽい使い方をしていきましょう。ミドルウェアを使います。ミドルウェアには3種類あります。
- ビルトインミドルウェア
- カスタムミドルウェア
- 3rd-party ミドルウェア
わかりやすいビルトインミドルウェアの利用からやってみます。
Pretty JSON
これ、地味なんですが、僕は結構好きなミドルウェアです。
import { prettyJSON } from 'hono/pretty-json'
//...
app.use(prettyJSON())
こうやると、URLに?pretty
を使えるだけで整形されるようになります。地味ですね。他にもビルトインのミドルウェアは以下があります。
- Basic Authentication
- Bearer Authentication
- Cache
- Compress
- CORS
- CSRF Protection
- ETag
- JSX Renderer
- JWT
- Timing
- Logger
- Secure Headers
認証系なんかは便利です。Basic認証をやってみますかね。
Basic認証
(やってみる)
ほら簡単ですね。これだとIDとパスワードをハードコードしてますが、Cloudflareの場合だとBindingsのVariablesを使う方法があります。こちらを参考にしてください。
カスタムミドルウェア
ミドルウェアは自分でつくれます。これが醍醐味ですね。
たとえば、レスポンスタイムを測るミドルウェアはこちらです。といいつつCloudlfareではDateの扱いにクセがあるのでうまく動くか保証できませんが、まあみてください。
app.use(async (c, next) => {
const start = Date.now()
await next()
const end = Date.now()
c.res.headers.set('X-Response-Time', `${end - start}`)
})
next()
がハンドラだと思ってください。その前後で実行さるわけです。で、おもにnext()
の前でリクエストを扱い、next()
のあとでレスポンスを加工します。
例えば、わかりやすいのだと、レスポンスヘッダーの追加です。
応用編だと、HTMLRewriterというCloudflare特有のAPIを使ったものです。これだとリンクを一気に書き換えることができて、リンク先の引っ越しなどに便利です。
app.get('/pages/*', async (c, next) => {
await next()
class AttributeRewriter {
constructor(attributeName) {
this.attributeName = attributeName
}
element(element) {
const attribute = element.getAttribute(this.attributeName)
if (attribute) {
element.setAttribute(this.attributeName, attribute.replace('oldhost', 'newhost'))
}
}
}
const rewriter = new HTMLRewriter().on('a', new AttributeRewriter('href'))
const contentType = c.res.headers.get('Content-Type')
if (contentType!.startsWith('text/html')) {
c.res = rewriter.transform(c.res)
}
})
ミドルウェアの実行順
ミドルウェアを使うのは超カンタンなのですが、実行順だけ気をつけてください。上に書いたものが先に実行されます。まぁ正しくは上に書いたもののnext()
の前が先に実行され、そのnext()
のあとが最後に実行されます。これをみるとよくわかると思います。
app.use(async (_, next) => {
console.log('middleware 1 start')
await next()
console.log('middleware 1 end')
})
app.use(async (_, next) => {
console.log('middleware 2 start')
await next()
console.log('middleware 2 end')
})
app.use(async (_, next) => {
console.log('middleware 3 start')
await next()
console.log('middleware 3 end')
})
app.get('/', (c) => {
console.log('handler')
return c.text('Hello!')
})
これを実行するとですね、出力はこうなります。
middleware 1 start
middleware 2 start
middleware 3 start
handler
middleware 3 end
middleware 2 end
middleware 1 end
3rd-partyミドルウェア
これはその名の通り、第三者、つまり外部のライブラリに依存する、もしくは「していい」ミドルウェアです。というのも、Honoは外部のライブラリに依存しない、という大原則がありまして、外部ライブラリを使ったとたんいコアに入れられないんですね。でもこれがまあいいガイドラインになっています。3rd-partyミドルウェアには以下があります。codehex君のFirebase Authもありますね。
- GraphQL Server
- Sentry
- Firebase Auth
- Zod Validator
- Qwik City
- tRPC Server
- TypeBox Validator
- Typia Validator
- Valibot Validator
- Zod OpenAPI
- Clerk Auth
- Swagger UI
- esbuild Transpiler
- Prometheus Metrics
- Auth.js(Next Auth)
Zod Validatorをあとで使ってみましょう。
ヘルパーを使う
ミドルウェアの他にヘルパーってのがあります。
Honoのコアはすごく小さくて、一番小さいプリセットのhono/tiny
だと12KB以下なんです。これはすごい小さい。Expressは560KBとかかな。で、でもこれだと基本的なことしかできない。なので、ミドルウェアとヘルパーで機能を拡張するわけです。
ヘルパーには以下がありますね。
- Accepts
- Adapter
- Cookie
- css
- Dev
- Factory
- html
- JWT
- SSG
- Streaming
- Testing
Devヘルパー
結構使うのがDevヘルパーのshowRoutes()
だったりします。使ってみましょう。
登録されているルートが一目瞭然でしょ。これ便利ですよ。
Streamingヘルパー
今っぽいのがStreamingヘルパーです。今っぽいっていいながらSSEなんてやっちゃってるわけですから、どうなのって感じですが。AIの時代だと時間がかかる処理をちょろちょろと少しずつ返すってことがよくあるので、これを使う場面があるかもしれませんね。
Zod Validatorを使う
Zod Validatorというミドルウェアはお気に入りです。というかみんな大好きです。これをやるとZodでバリデーションできて、型がしっかりつく。そのうえRPCモードなんかも使えちゃう。まぁ見てみてください。
CloudflareのBindingsを使う
Cloudflareのミドルウェア、つまり、KV、R2、D1とかそういうの。それにアクセスするのを「Bindings」と呼んでいます。ああと変数もそうですね。HonoではBindingsにアクセスするのも簡単です。「c.env.FOO」でアクセスできます。ただこれだとTypeScriptの型がつかないので、Honoをインスタンス化する時に、ジェネリクスを渡すことを推奨してます。こんな感じ。
type Bindings = {
TOKEN: string
MY_KV: KVNamespace
}
const app = new Hono<{
Bindings: Bindings
}>()
これで型がついたでしょ?
KVを使う
Bindingsを使ってみましょう。D1もいいですが、KVが一番カンタンなので、それやってみますか。
Workers AIを使う
続いてWorkers AI。これはいいですよ。Cloudflareは今めちゃくちゃAIに力を入れていて、最近、うちのチームにAI専門のEducatorが2人はいったくらいです。彼らはめちゃくちゃ精力的に活動してて面白いですね。
さて、AIを利用するには、wrangler.toml
に以下を追加します。
[ai]
binding = "AI"
こうすると他のBindingsと同じにc.env.AI
でアクセスできます。AIの場合はこれに加えて、Cloudflareが提供している@cloudflare/ai
というライブラリを使います。
bun add @cloudflare/ai
そしてコード例はこれです。
import { Ai } from '@cloudflare/ai'
//...
app.get('/', (c) => {
const ai = new Ai(c.env.AI);
const response = await ai.run('@cf/meta/llama-2-7b-chat-int8', {
prompt: "What is the origin of the phrase Hello, World"
}
)
//...
})
このai.run
ってやった時にモデル名が補完されるのいいでしょ。実はこれ、僕がコード書いてます!TypeScriptの型の推論ってやつですね。このresponse
には残念ながら型がついてないのですが、先日型を追加するPR作りまして、もうすぐそれがマージされたのがリリースされますね。はは。
テストをする
Honoを使っているとテストが簡単にできます。こんな感じ。
it('should return 200 response', async () => {
const res = await app.request('/')
expect(res.status).toBe(200)
})
これがいいんですよ。Web StandardのAPIってサーバーレイヤーをブラックボックス化するので、Request/Responseのレベルでテストを書けばいいのですね。Honoのテストは全部で2万行あるんですが、その中のかなりの部分がこの書き方と同じ書き方です。これで十分なんですよね。このレイヤーのテストをするだけで、おかしなことが起こったことはほとんどないですね。いやないですかね。
あとはBindingsのテストですが、これはcodehex君が詳しいかな。まあアツいAPIが出てきたのですが、今回はちょっと省きます。
ベストプラクティス
Honoのアプリを作るにあたってベストプラクティスがいくつかあります。
app.route()
一番引っかかるのがこれね。大きなアプリを作ろうとすると、どうしてもRuby on Railsモデルっていうのかな。パスとコントローラーみたいにわけてしまう。
const booksList = (c: Context) => {
return c.json('list books')
}
app.get('/books', booksList)
これでもいいんですが、型の推論がききにくくなるんで、app.route()
で拡張するスタイルをおすすめしてます。
// books.ts
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.json('list books'))
app.post('/', (c) => c.json('create a book', 201))
app.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))
export default app
// index.ts
import { Hono } from 'hono'
import authors from './authors'
import books from './books'
const app = new Hono()
app.route('/authors', authors)
app.route('/books', books)
export default app
でもまあどうしてもというのもあるから、その場合はファクトリのヘルパーを用意したのでそれを使うといいでしょう。
import { createFactory } from 'hono/factory'
import { logger } from 'hono/logger'
// ...
// 😃
const factory = createFactory()
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)
Cloudflare Pages
さあ、Workers編が終わりました。はぁ。じゃあ次はCloudflare Pages編です。create-honoでcloudflare-pages
を選びましょう。
JSXを使う
JSXを使ってみましょう。HonoはこのJSXが使えるのが大きな武器です。拡張子を.tsx
にすればすぐ使えます。このスターターテンプレートはもう対応してますね。
JSX Rendererミドルウェア
JSXレンダラーミドルウェアも僕のお気に入りです。これを使うとJSXのページが書きやすくなります。これもこのスターターではデフォルトで導入されています。
hono/css
を使う
実はHonoはJSX以外にもCSS in JSを実装しているのですね。styled-componentsってライブラリがありますが、それと同じようなのをスクラッチで実装したのですね。これはすごい。ちなみに、styled-componentsの作者はグレンといって、僕をCloudflareに誘った人ですね。はは。
まぁこれが基本的な使い方です。class名を渡すってのが面白いですね。
app.get('/', (c) => {
const headerClass = css`
background-color: orange;
color: white;
padding: 1rem;
`
return c.html(
<html>
<head>
<Style />
</head>
<body>
<h1 class={headerClass}>Hello!</h1>
</body>
</html>
)
})
Pagesへデプロイする
Cloudflare Pagesへのデプロイも超簡単です。やってみましょう。
HonoX
さていよいよHonoXです。そうこれ「ほのおえっくす」って読みます。
create-honoコマンドを実行したあとx-basic
を実行してみましょう。
基本的なルーティング
基本的なルーティングはこれですね。プリザーブとして_error.tsx
と_404.tsx
あと、_renderer.tsx
があります。
ルートとハンドラ
じゃあ各ルートファイルの中には何を書くかというとレスポンスの返し方は3種類あります。
createRoute()
- Honoインスタンス(Classicスタイル)
- ファンクション
この辺はこの記事が詳しいね。
ああ、ここにも書いてあるとおりHonoXはまだアルファってステータスだから、いきなりブレイキングチェンジが入ったりするし、未熟なところがあるから多めに見てください。これから良くしていきますね。
MDXを使う
HonoXでは.mdx
という拡張子のMDXファイルも対象になります。これは何かというとMarkdownの拡張ですね。依存ライブラリをインストールして、vite.config.ts
に細工をします。
bun add -D @mdx-js/rollup remark-frontmatter remark-mdx-frontmatter
import devServer from '@hono/vite-dev-server'
import mdx from '@mdx-js/rollup'
import honox from 'honox/vite'
import remarkFrontmatter from 'remark-frontmatter'
import remarkMdxFrontmatter from 'remark-mdx-frontmatter'
import { defineConfig } from 'vite'
const entry = './app/server.ts'
export default defineConfig(() => {
return {
plugins: [
honox(),
devServer({ entry }),
mdx({
jsxImportSource: 'hono/jsx',
remarkPlugins: [remarkFrontmatter, remarkMdxFrontmatter],
}),
],
}
})
これで、app/routes/foo.mdx
とかやると/foo
にアクセスした時にいつの間にかレンダリングされてるのですね。これは便利。あとフロントマターにも対応しているから、タイトルとかメタデータを簡単に追加できます。
あと記事一覧なんかはこう書けますね。Meta
ってのはメタデータを書いておけばいいね。
import type { Meta } from '../types'
export default function Top() {
const posts = import.meta.glob<{ frontmatter: Meta }>('./posts/*.mdx', {
eager: true,
})
return (
<div>
<h2>Posts</h2>
<ul class='article-list'>
{Object.entries(posts).map(([id, module]) => {
if (module.frontmatter) {
return (
<li>
<a href={`${id.replace(/\.mdx$/, '')}`}>{module.frontmatter.title}</a>
</li>
)
}
})}
</ul>
</div>
)
}
SSGヘルパーを使う
これまではいわゆるSSR、というかまあフロントエンドの人に言わせるとハイドレートしてねーからそれはSSRとは言わないとか言われそうですけど、ダイナミックにレンダリングしてたわけです。ところがv4からSSGヘルパーってのができまして、これを使うとHonoのアプリを静的なHTMLに書き出せるんです。いいですねー。
例えば、ブログを作ってCloudflare Pagesへデプロイしたいって時にこまるのは、コンテンツもまるごとバンドルしなくちゃいけないことで、これはさすがに限界がある。なので、このSSGはすごくいいんですね。
実装するにはビルド用のファイルをつくってそれを実行してもいいですが、Viteのプラグインがあるので、それを使うと便利です。
ブログを作る
さて、ここまできたらHonoXでブログができますね。hono/cssと合わせたりしてもいいです。
クライアントコンポーネント
これは時間があまったら。HonoXはクライアントクライアントコンポーネントをIslandに配置できるんです。Reactと結構互換があるので、親しみやすいんじゃないでしょうか。といっても、HonoXの場合、まさしくIslandというのを目指していて、MPAのページの一部分にインタラクションを足すという方針なので、あんま多用するって感じじゃないんですよね。
その代わりといってはあれです、IslandをもってないページではJavaScriptが配信されないんです。なのでパフォーマンスはいいはず。
まとめ
さあ、駆け足でHonoについて喋ってきました。だいぶ飛ばしましたが、結構Honoについて伝えられたんじゃないでしょうか。
HonoとCloudflareを触った人は一様に「開発者体験がよい!」って言ってくれるので、それを体験してほしいですね。
今日はありがとうございました。
Discussion