📙

Honoの新しいCloudflare Pagesスターターについて

2023/10/30に公開11

先日リリースしたv3.9.0でHonoの「Cloudflare Pages」のスターターテンプレートが新しくなりました。

https://github.com/honojs/hono/releases/tag/v3.9.0

これがなかなか面白いので詳しく紹介します。

使ってみる

使ってみてください。create honoコマンドを使います。C3(Create Cloudflare CLI)コマンドでもHonoを選べますが今のところそれだとWorkersのテンプレートになるのでcreate honoで。npmの場合は以下です。

npm create hono@latest

選択肢がでてくるのでcloudflare-pagesを選びます。

SS

そしたら、ディレクトリに入って、npm installしてnpm run devすれば開発サーバーが立ち上がり、npm run deployすればデプロイできます。

Viteベース

で、以前からCloudflare Pages、もしくはWorkersも含み、Honoでアプリ開発をしていた人はこのスターターの特徴に気づくと思います。なんと開発サーバーがWranglerではないのです。Viteで動いています。npm run devするとこうなります。そして、起動ポートもデフォルトでは「http://localhost:5173/」になります。

SS

この開発サーバー上でHonoアプリの開発をするわけです。で、デプロイの時にWranglerを使います。

SS

これも、ビルドにはViteを使っていて、ビルドされたスクリプトをアップロードするため、つまり純粋にデプロイだけにWranglerが使われています。

Bindings

勘のよい方は疑問に思われるかもしれません。「Bindingsはどうするの?」。実はローカルに限っては対応しています。というかPagesはwrangler --remoteできませんので、Pagesでできる範疇はすべてできます。

例えば、KVなんかこのようにvite.config.tsを編集すると使えるようになります。

export default defineConfig({
  plugins: [
    devServer({
      entry: 'src/index.tsx',
      cf: {
        bindings: {
          NAME: 'Hono',
        },
        kvNamespaces: ['MY_KV'],
      },
    }),
  ],
})

D1もちょっと工夫すれば、ローカルのSQLiteを参照するようにして使えます。

デプロイ先で使いたければ、ダッシュボードからBindingsを有効にすればOKです。

Viteだと何がいいのか?

では、WranglerではなくなぜViteを開発サーバーにするのか?それはViteの開発サーバーの方が快適だからです。

速い

ビルド、再起動が速いの一番大きなアドバンテージです。Wranglerと比べると差は明らかだと思います。

SS

クライアントのビルドサポート

後ほど紹介しますが、クライアント用のスクリプトも一緒に開発&ビルドできます。HMR = Hot Module Replacementを効かせることができます。

クロスプラットフォーム

これが面白い点で、Viteで開発できるのは、特にCloudflare Pagesのアプリに限ったものではありません。ビルドをうまいことしちゃえば、他のプラットフォーム、ランタイムで動かすことができます。現在はPagesだけ提供していますが、

  • Cloudflare Pages
  • Cloudflare Workers
  • Vercel

はすでに対応できます。他のプラットフォームもいけると思います。Denoも工夫次第でいけると思います。

WorkersとPages

ここでまた疑問。「Workersじゃだめなの?」

Workersでもできます。でも、Pagesにしましょう。というのも、特に問題になるがアセットファイルのサーブです。Workersではserve-staticの実装がそうであるように、静的ファイルはWorkers Sitesから配信するようになります。ただし、このWorkers Sitesは現在非推奨になっています。

Use Cloudflare Pages for hosting fullstack applications instead of Workers Sites. Do not use Workers Sites for new projects.

ですので、WorkersではなくPagesを使いましょう。Pagesの場合アセットファイルの扱いもKVベースのWorkers Sitesよりうまくやってくれます。そしてWorkersと同じコードが動きます。

コード

スターターのコードはこうなっています。エントリーファイルはsrc/index.tsxとTSXファイルです。

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

const app = new Hono()

app.get('*', renderer)

app.get('/', (c) => {
  return c.render(<h1>Hello!</h1>)
})

export default app

最近のHonoのアップデートを追っていない方は見慣れないc.render()に驚くかもしれません。これはrendererで設定したレイアウトを適応しつつHTMLを返却するというメソッドです。renderersrc/renderer.tsxで定義されています。

src/renderer.tsx
import { jsxRenderer } from 'hono/jsx-renderer'

export const renderer = jsxRenderer(
  ({ children, title }) => {
    return (
      <html>
        <head>
          <link href="/static/style.css" rel="stylesheet" />
          <title>{title}</title>
        </head>
        <body>{children}</body>
      </html>
    )
  },
  {
    docType: true
  }
)

ここでも最新のhono/jsx-renderer = JSX Rendererを使っています。これだとレイアウトを適応できて、かつ、タイトルを設定する時に

app.get('/', (c) => {
  return c.render(<h1>Hello!</h1>, {
    title: 'Hello Hono'
  })
})

みたいに書けば、個別のエンドポイントごとにタイトルを簡単に設定できます。

また、useRequestContext()を使えば、ファンクションコンポーネント内でContextを取ることもできるようになります。

import { useRequestContext } from 'hono/jsx-renderer'

// ...
const RequestUrlBadge: FC = () => {
  const c = useRequestContext()
  return <b>{c.req.url}</b>
}

app.get('/page/info', (c) => {
  return c.render(
    <div>
      You are accessing: <RequestUrlBadge />
    </div>
  )
})

素晴らしい。

クライアントサイド

クライアントのTypeScript/JavaScript、もしくはCSS等をViteの機能を使って取り込むことができます。

例えば、src/client.tsを用意します。次にrendererを以下のように書き換えます。

src/renderer.tsx
export const renderer = jsxRenderer(
  ({ children, title }) => {
    return (
      <html>
        <head>
          {import.meta.env.PROD ? (
            <script type="module" src="/static/client.js"></script>
          ) : (
            <script type="module" src="/src/client.ts"></script>
          )}
          <title>{title}</title>
        </head>
        <body>{children}</body>
      </html>
    )
  },
  {
    docType: true
  }
)

こうすればTypeScriptでクライアントを書いて、ビルド後は/static/client.jsをエントリポイントとしてデプロイ先で使うというのができます。ちょっとまだ例を作れてないですが、工夫によってはTailwind CSSみたいなのをsrc/client.tsで参照させることもできます。

フルスタック

というようにViteによる高速サーバーや、Pagesのアセット配信、Renderer機能、クライアントへの対応などを使えばいわゆるフルスタックなアプリケーションをHonoだけで作れてしまいます!

背景を話すと長くなるしそのつもりはないので、簡単に書くと、HonoはもともとWorkersのような軽量エッジ環境で動かして、アプリケーションはWeb APIがベストかと思っていました。ところがJSXが組み込まれていることも相まって、HTMLをレンダリングするケースが多くなりました。またhtmxやAlpine.js、Hotwireなど、HTMLのタグでインタラクティブを表現するライブラリがここへ来て人気なこともあり、HonoとそれらでStackを作ることが増えました。それは以下に書いてます。

https://zenn.dev/yusukebe/articles/e8ff26c8507799

となると、もうフルスタックにしてしまおうというのが今やってるところです。

僕も「Honoでフルスタックは違う。他のフレームワーク使え」って思ってましたが、自分で使ってフィーリングを確かめてみると悪くないです。ってかむしろHono自体が軽量でViteも高速なので、軽快に動いてDXがとてもよい。Bindingsも使える。ちなみに、ViteでCloudflareのBindingsが使えるのは現在、このHonoだけです(Clouflare社内ではこの対応を急いでおり、近々いくつかのフレームワークが対応する予定)。

JSXの進化

JSXはrenderToString()相当があれば、組み込みのJSXじゃなくてもPreactかReact、もしくはSolidなどが使えます。

ただ、みんな組み込みのJSXが好きみたいなので、HonoのJSXを進化させています。

3.9.0ではタグに補完が効くようになりました。

SS

SS

また、今後、Async Componentsに対応する他、usualomaさんが今日、Suspenseuseという機能を実装していたので、React SSR Streamingのようなことができるかもです。これはすごい。

どうやっているか

で、このViteの仕組みどうやってるかというと、今のところ、以下の2つのVite Pluginを使っています。

dev-serverがキモで、HonoなどのfetchベースのアプリケーションをViteで動かすためのカスタムサーバーをつくってそれがPluginになっています。面白いのはそれを実現するために、@hono/node-serverが使えたということです。これはHonoのアプリケーションをNode.jsで動かすためのアダプタですので、Viteカスタムサーバーのreq/res変換に使えたのです。最初、Miniflareやworkeredで動かしていたら、遅かったのですが、Node.jsネイティブだとビルド、再起動が速いのです。

Honoの「フルスタック対応」を見越して、Viteのプラグインを作っていたわけです。今後例えば、@hono/vite-vercelなんかすぐ作ることができます。

課題

書いてて思ったのですが、JSXにrenderToReadableStream()が実装されたとしてそれのハンドリングをNode.jsアダプタでどうやるか、Viteのプラグインでどうやるかは感がなくてはいけないんですね。できるとは思うけど、課題です。

まとめ

話が長くなってしまいましたが、HonoのCloudflare Pagesスターターを使ってみてください!Next.jsやRemixとはまた違ったフィーリングでアプリを作れます。レッツcreate hono

npm create hono@latest

Discussion

tomotomo

ご質問させて頂きたいのですが、ローカルで.envファイルを読み込む場合、変数名にVITE_プリフィックスを付与してimport.meta.envからアクセスするしかないのでしょうか?

tomotomo

Bindingsはwrangler secret putコマンドを使って設定されることを想定されているのでしょうか?どこでbindingsの値をセットすべきなのかが分からず..。wrangler.tomlのvarsもセットしてみましたが、取得できずでした。

tomotomo

ご回答ありがとうございます!例えばAPI_KEYという値をセットしたい場合、どうやってセットすればc.env.API_KEYで取得できるようになりますでしょうか。

tomotomo

あぁ、vite.config.tsのbindingsのハッシュにc.envでアクセスできるようになっているのですね。ようやく理解できました。

tomotomo

vite.config.tsはgitコミットしているので、ここにコンフィデンシャルな情報を書きたくない気持ちもありますね。

tomotomo

度々すみません。上記手順で作成したプロジェクトでHTTPリクエスト受信時にCloudflare Queuesにenqueueすることは可能でしょうか?

tomotomo

やはりそうなのですね。ご回答ありがとうございます!