1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Hono × JSXではじめる!Cloudflare Pagesを使ったSPAウェブアプリ開発

Posted at

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"を追加します。

tsconfig.json
    "lib": [
-      "ESNext"
+      "DOM", "DOM.Iterable", "ESNext"
    ],
tsconfig.json を展開
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のビルドを追加します。

vite.config.ts
- 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を展開
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'
      })
    ]
  }    
})
package.json
  "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を展開
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に書き換えます。

src/client.tsx
- 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を展開
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を返却する処理をそれぞれ実装します。

src/index.tsx
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.PRODtrueになり、/static/client.jsを参照します。

まとめ

この記事では、HonoとJSXを使って簡単なSPA(Single Page Application)を作る手順を解説しました。ポイントは次の通りです:

  • Honoを使ったプロジェクトのセットアップ
    create-honoを使ってCloudflare Pages用のプロジェクトを簡単に構築しました。

  • JSXでのフロントエンド開発
    React風の記法を活用して、カウンターやAPIを使った日付取得のコンポーネントを作成しました。

  • バックエンドとの連携
    フロントエンドからバックエンドAPIを呼び出し、データを動的に表示する仕組みを構築しました。

  • 開発とデプロイのプロセス
    Viteを活用したローカル開発から、Wranglerを使ったCloudflare Pagesへのデプロイまでの流れを体験しました。

この記事が、HonoとJSXを使ったウェブアプリ開発の第一歩となれば幸いです!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?