✍️

Honoハンズオン2024年3月沖縄

2024/03/05に公開

編集中

ここにコードを置くと思います。

https://github.com/yusukebe/workshop-okinawa


このイベントが今週末で、その中で「Honoハンズオン」をやるので超絶ネタバレですが、そこで話す内容を書きます。

https://cfm-cts.connpass.com/event/310916/

方針

ハンズオンと言いつつ「みんなで一斉にやりましょう」ってやると合わせるのに時間がかかるので、僕がどんどん進めます。なので、ついてきたい人だけついてきてください。そうじゃない人は僕がコード書くのを見てください。むしろその方がよくわかっていいと思います。

Honoとは?

ウェブサイトを見てください。

https://hono.dev/

あと正直、僕はブログ書くのとか好きなんですが、いわゆるちゃんとした「ドキュメント」を書くのが正直苦手でそれは英語に限らずなんですが、なので、よくしている方いたら貢献してください。コントリビューションウェルカム!

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について

ここに書いてあるのでみて。

https://yusukebe.com/posts/2024/cloudflare-workers-updates/

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種類あります。

  1. ビルトインミドルウェア
  2. カスタムミドルウェア
  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を使う方法があります。こちらを参考にしてください。

https://hono.dev/getting-started/cloudflare-workers#using-variables-in-middleware

カスタムミドルウェア

ミドルウェアは自分でつくれます。これが醍醐味ですね。

たとえば、レスポンスタイムを測るミドルウェアはこちらです。といいつつ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種類あります。

  1. createRoute()
  2. Honoインスタンス(Classicスタイル)
  3. ファンクション

この辺はこの記事が詳しいね。

https://zenn.dev/yusukebe/articles/724940fa3f2450

ああ、ここにも書いてあるとおりHonoXはまだアルファってステータスだから、いきなりブレイキングチェンジが入ったりするし、未熟なところがあるから多めに見てください。これから良くしていきますね。

MDXを使う

HonoXでは.mdxという拡張子のMDXファイルも対象になります。これは何かというとMarkdownの拡張ですね。依存ライブラリをインストールして、vite.config.tsに細工をします。

bun add -D @mdx-js/rollup remark-frontmatter remark-mdx-frontmatter
vite.config.ts
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のプラグインがあるので、それを使うと便利です。

https://github.com/honojs/vite-plugins/tree/main/packages/ssg

ブログを作る

さて、ここまできたらHonoXでブログができますね。hono/cssと合わせたりしてもいいです。

クライアントコンポーネント

これは時間があまったら。HonoXはクライアントクライアントコンポーネントをIslandに配置できるんです。Reactと結構互換があるので、親しみやすいんじゃないでしょうか。といっても、HonoXの場合、まさしくIslandというのを目指していて、MPAのページの一部分にインタラクションを足すという方針なので、あんま多用するって感じじゃないんですよね。

その代わりといってはあれです、IslandをもってないページではJavaScriptが配信されないんです。なのでパフォーマンスはいいはず。

まとめ

さあ、駆け足でHonoについて喋ってきました。だいぶ飛ばしましたが、結構Honoについて伝えられたんじゃないでしょうか。

HonoとCloudflareを触った人は一様に「開発者体験がよい!」って言ってくれるので、それを体験してほしいですね。

今日はありがとうございました。

Discussion