Honoの新しいCloudflare Pagesスターターについて
先日リリースしたv3.9.0でHonoの「Cloudflare Pages」のスターターテンプレートが新しくなりました。
これがなかなか面白いので詳しく紹介します。
使ってみる
使ってみてください。create hono
コマンドを使います。C3(Create Cloudflare CLI)コマンドでもHonoを選べますが今のところそれだとWorkersのテンプレートになるのでcreate hono
で。npm
の場合は以下です。
npm create hono@latest
選択肢がでてくるのでcloudflare-pages
を選びます。
そしたら、ディレクトリに入って、npm install
してnpm run dev
すれば開発サーバーが立ち上がり、npm run deploy
すればデプロイできます。
Viteベース
で、以前からCloudflare Pages、もしくはWorkersも含み、Honoでアプリ開発をしていた人はこのスターターの特徴に気づくと思います。なんと開発サーバーがWranglerではないのです。Viteで動いています。npm run dev
するとこうなります。そして、起動ポートもデフォルトでは「http://localhost:5173/
」になります。
この開発サーバー上でHonoアプリの開発をするわけです。で、デプロイの時にWranglerを使います。
これも、ビルドには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と比べると差は明らかだと思います。
クライアントのビルドサポート
後ほど紹介しますが、クライアント用のスクリプトも一緒に開発&ビルドできます。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ファイルです。
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を返却するというメソッドです。renderer
は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
を以下のように書き換えます。
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を作ることが増えました。それは以下に書いてます。
となると、もうフルスタックにしてしまおうというのが今やってるところです。
僕も「Honoでフルスタックは違う。他のフレームワーク使え」って思ってましたが、自分で使ってフィーリングを確かめてみると悪くないです。ってかむしろHono自体が軽量でViteも高速なので、軽快に動いてDXがとてもよい。Bindingsも使える。ちなみに、ViteでCloudflareのBindingsが使えるのは現在、このHonoだけです(Clouflare社内ではこの対応を急いでおり、近々いくつかのフレームワークが対応する予定)。
JSXの進化
JSXはrenderToString()
相当があれば、組み込みのJSXじゃなくてもPreactかReact、もしくはSolidなどが使えます。
ただ、みんな組み込みのJSXが好きみたいなので、HonoのJSXを進化させています。
3.9.0ではタグに補完が効くようになりました。
また、今後、Async Componentsに対応する他、usualomaさんが今日、Suspense
とuse
という機能を実装していたので、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
ご質問させて頂きたいのですが、ローカルで.envファイルを読み込む場合、変数名にVITE_プリフィックスを付与してimport.meta.envからアクセスするしかないのでしょうか?
Bindingsはwrangler secret putコマンドを使って設定されることを想定されているのでしょうか?どこでbindingsの値をセットすべきなのかが分からず..。wrangler.tomlのvarsもセットしてみましたが、取得できずでした。
ローカルだとこの方法で設定してもらえると!
デプロイ環境だとダッシュボードから設定することになります。
ご回答ありがとうございます!例えばAPI_KEYという値をセットしたい場合、どうやってセットすればc.env.API_KEYで取得できるようになりますでしょうか。
あぁ、vite.config.tsのbindingsのハッシュにc.envでアクセスできるようになっているのですね。ようやく理解できました。
vite.config.tsはgitコミットしているので、ここにコンフィデンシャルな情報を書きたくない気持ちもありますね。
その場合は、試してないですが、以下のような方法などを試して
.env
から読むようにしてください。度々すみません。上記手順で作成したプロジェクトでHTTPリクエスト受信時にCloudflare Queuesにenqueueすることは可能でしょうか?
ローカルの環境では不可能です。これはWranglerを使ったPagesの開発でも無理で、それに従う形になります。
リモートのプレビュー、プロダクションだったら
c.env.MY_QUEUE
などでオブジェクトが取得可能です。やはりそうなのですね。ご回答ありがとうございます!
いえいえ!