HonoでSPA(Single Page Application)のウェブアプリを作ろうと思い、公式ガイド Client Components にたどり着きました。試行錯誤で何とかデモを動かせるようになったので、その結果を共有します!
デモとソースコード
作ったデモは次の2つの記事のものと同じです。useStateを使ったカウンターと、API経由での日付の取得が動きます。
上記はReactで作ってあるのですが、それをわざわざJSXのClient Componentsを使う形に戻しました。
デモアプリはCloudflare Pagesで公開しています(利用停止するかもしれません。あらかじめご容赦ください)。
作成したコードは次のリポジトリーで公開しています。
ファイル構成
フロントエンドはsrc/client.tsx
に、バックエンドはsrc/index.tsx
に実装します。
プロジェクトのファイル構成を展開
.
├── README.md
├── package-lock.json
├── package.json
├── public
│ └── static
│ └── style.css
├── src
│ ├── client.tsx
│ └── index.tsx
├── tsconfig.json
├── vite.config.ts
└── wrangler.toml
手順
1. create-hono
でCloudflare Pagesのプロジェクトを作る
に従って、Cloudflare Pagesのプロジェクトを作ります。なお、記事執筆時は次のバージョンで動かしています。
- create-hono 0.14.3
- vite 5.4.11
npm create hono@latest <プロジェクト名>
実行結果を展開
> npx
> create-hono cf-pages-jsx-client-component
create-hono version 0.14.3
✔ Using target directory … cf-pages-jsx-client-component
? Which template do you want to use? cloudflare-pages
? Do you want to install project dependencies? yes
? Which package manager do you want to use? npm
✔ Cloning the template
✔ Installing project dependencies
🎉 Copied project files
Get started with: cd <プロジェクト名>
作ったディレクトリーに移動して、開発用サーバーを起動します。
cd <プロジェクト名>
npm run dev
実行結果を展開
> dev
> vite
VITE v5.4.11 ready in 685 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
(!) Could not auto-determine entry point from rollupOptions or html files and there are no explicit optimizeDeps.include patterns. Skipping dependency pre-bundling.
はじめて開発用サーバーを起動したときだけ、警告((!) Could not auto-determine entry point...)が表示されます。
2. 設定を変更する
設定ファイルを書き換えていきます。
tsconfig.json
compileOptions.lib
に"DOM"
と"DOM.Iterable"
を追加します。
"lib": [
- "ESNext"
+ "DOM", "DOM.Iterable", "ESNext"
],
tsconfig.json を展開
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"lib": [
"DOM", "DOM.Iterable", "ESNext"
],
"types": [
"@cloudflare/workers-types/2023-07-01",
"vite/client"
],
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
},
}
変更し忘れていると、次のエラーが表示されます。
- Cannot find name 'document'. Do you need to change your target library? Try changing the 'lib' compiler option to include 'dom'.ts(2584)
- Property 'entries' does not exist on type 'Headers'.ts(2339)
vite.config.ts と package.json
Cloudflare Pagesへのデプロイ前に、client modeのビルドを追加します。
- export default defineConfig({
+ export default defineConfig(({ mode }) => {
+ if (mode === "client")
+ return {
+ esbuild: {
+ jsxImportSource: "hono/jsx/dom",
+ },
+ build: {
+ rollupOptions: {
+ input: "./src/client.tsx",
+ output: {
+ entryFileNames: "static/client.js",
+ },
+ },
+ },
+ };
+ return {
plugins: [
build(),
devServer({
adapter,
entry: 'src/index.tsx'
})
]
+ }
})
vite.config.tsを展開
import build from '@hono/vite-build/cloudflare-pages'
import devServer from '@hono/vite-dev-server'
import adapter from '@hono/vite-dev-server/cloudflare'
import { defineConfig } from 'vite'
export default defineConfig(({ mode }) => {
if (mode === "client")
return {
esbuild: {
jsxImportSource: "hono/jsx/dom", // Optimized for hono/jsx/dom
},
build: {
rollupOptions: {
input: "./src/client.tsx",
output: {
entryFileNames: "static/client.js",
},
},
},
};
return {
plugins: [
build(),
devServer({
adapter,
entry: 'src/index.tsx'
})
]
}
})
"scripts": {
"dev": "vite",
- "build": "vite build",
+ "build": "vite build --mode client && vite build",
"preview": "wrangler pages dev",
"deploy": "npm run build && wrangler pages deploy"
},
package.jsonを展開
{
"name": "cf-pages-jsx-client-component",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build --mode client && vite build",
"preview": "wrangler pages dev",
"deploy": "npm run build && wrangler pages deploy"
},
"dependencies": {
"hono": "^4.6.15"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20241218.0",
"@hono/vite-build": "^1.2.0",
"@hono/vite-dev-server": "^0.17.0",
"vite": "^5.2.12",
"wrangler": "^3.96.0"
}
}
変更し忘れていると、dist/client.js
が生成されず、デプロイに成功しても空白のページが表示されます。
参考
3. 実装する
フロントエンドとバックエンドを実装していきます。
フロントエンドsrc/client.tsx
Reactを使う部分を、JSXに書き換えます。
- import { useState } from 'react'
- import { createRoot } from 'react-dom/client'
+ import { useState } from 'hono/jsx/dom'
+ import { createRoot } from 'hono/jsx/dom/client'
src/client.tsxを展開
import { useState } from 'hono/jsx/dom'
import { createRoot } from 'hono/jsx/dom/client'
function App() {
return (
<>
<h1>Hello, Hono with JSX!</h1>
<h2>Example of useState()</h2>
<Counter />
<h2>Example of API fetch()</h2>
<ClockButton />
</>
)
}
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
const ClockButton = () => {
const [response, setResponse] = useState<string | null>(null)
const handleClick = async () => {
const response = await fetch('/api/clock')
const data = await response.json()
const headers = Array.from(response.headers.entries()).reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
const fullResponse = {
url: response.url,
status: response.status,
headers,
body: data
}
setResponse(JSON.stringify(fullResponse, null, 2))
}
return (
<div>
<button onClick={handleClick}>Get Server Time</button>
{response && <pre>{response}</pre>}
</div>
)
}
const domNode = document.getElementById('root')!
const root = createRoot(domNode)
root.render(<App />)
バックエンドsrc/index.tsx
フロントエンドから呼び出すAPI/api/clock
と、フロントエンド本体のHTMLを返却する処理をそれぞれ実装します。
import { Hono } from 'hono'
const app = new Hono()
app.get('/api/clock', (c) => {
return c.json({
time: new Date().toLocaleTimeString()
})
})
app.get('/', (c) => {
return c.html(
<html>
<head>
<link href="/static/style.css" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
{import.meta.env.PROD ? (
<script type="module" src="/static/client.js" />
) : (
<script type="module" src="/src/client.tsx" />
)}
</body>
</html>
)
})
export default app
Cloudflare Pagesへのデプロイ前にclient modeでbuildすることで、/src/client.tsx
を/static/client.js
に変換します。デプロイしたコードはimport.meta.env.PROD
がtrue
になり、/static/client.js
を参照します。
まとめ
この記事では、HonoとJSXを使って簡単なSPA(Single Page Application)を作る手順を解説しました。ポイントは次の通りです:
-
Honoを使ったプロジェクトのセットアップ
create-hono
を使ってCloudflare Pages用のプロジェクトを簡単に構築しました。 -
JSXでのフロントエンド開発
React風の記法を活用して、カウンターやAPIを使った日付取得のコンポーネントを作成しました。 -
バックエンドとの連携
フロントエンドからバックエンドAPIを呼び出し、データを動的に表示する仕組みを構築しました。 -
開発とデプロイのプロセス
Viteを活用したローカル開発から、Wranglerを使ったCloudflare Pagesへのデプロイまでの流れを体験しました。
この記事が、HonoとJSXを使ったウェブアプリ開発の第一歩となれば幸いです!