2023なWebアプリの開発についてちょこちょこ試す
Hono + htmx + Cloudflare で簡単なTodoアプリの作成
これ試したい。
Setup
まだな場合は
npm install -g wrangler
wrangler login
雛形を作成する。
npm create hono@latest cf-hono-htmx-app # workerを選ぶ
cd cf-hono-htmx-app
npm install
DBの作成
TodoのDBをクリエイトする
wrangler d1 create todo-app
→
[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "todo-app"
database_id = "..."
出力された上記の設定情報をwrangler.tomlに追記しておく。
vi wrangler.toml
テーブルを作成する。
ドキュメントは以下。
-- Todoアプリケーションのデータベース
DROP TABLE IF EXISTS Todos;
CREATE TABLE IF NOT EXISTS Todos (
id INTEGER PRIMARY KEY,
title TEXT
);
実行する
wrangler d1 execute todo-app --local --file=./db/init.sql
適当に確認します。
wrangler d1 execute todo-app --local --command='INSERT INTO Todos (title) VALUES ("タスク1");'
wrangler d1 execute todo-app --local --command='INSERT INTO Todos (title) VALUES ("タスク2");'
wrangler d1 execute todo-app --local --command='INSERT INTO Todos (title) VALUES ("タスク3");'
wrangler d1 execute todo-app --local --command='SELECT * FROM Todos'
┌────┬───────┐
│ id │ title │
├────┼───────┤
│ 1 │ タスク1 │
├────┼───────┤
│ 2 │ タスク2 │
├────┼───────┤
│ 3 │ タスク3 │
└────┴───────┘
正常に作成できてそうです。
Zodの追加
ここを見て合わせにいく。
npm i zod @hono/zod-validator
live-reloadの追加
あとpackage.jsonのscriptsのdevに --live-reload
を追加する。また拡張子をts
からtsx
に修正する。
"scripts": {
- "dev": "wrangler dev src/index.ts",
+ "dev": "wrangler dev --live-reload src/index.tsx",
- "deploy": "wrangler deploy --minify src/index.ts"
+ "deploy": "wrangler deploy --minify src/index.tsx"
},
index.tsとcomponents.tsx
これをコピペする。
SQLの部分だけ自分が作成したテーブル名に修正する。
自分の場合は、todoのidをintにしたので、その部分を修正する。
async (c) => {
const { title } = c.req.valid('form')
// const id = crypto.randomUUID()
const { results } = await c.env.DB.prepare(`INSERT INTO Todos(title) VALUES(?);`).bind(title).run()
return c.html(<Item title={title} id={results.id} />)
}
run
npm run dev
実行するとエラーになる。
✘ [ERROR] Could not resolve "hono/jsx-renderer"
src/components.tsx:2:28:
2 │ import { jsxRenderer } from 'hono/jsx-renderer'
╵ ~~~~~~~~~~~~~~~~~~~
The path "./jsx-renderer" is not exported by package "hono":
なるほど、そういえば本家だとRC版のhono使ってましたね…。
ということでhonoのバージョンアップ
npm i hono@3.8.0-rc.2
そしてまた run dev
すると…
npm run dev
...
[mf:inf] Ready on http://0.0.0.0:8787
これで簡単ですが、正常に動作するTodoアプリができました。
Workerにデプロイする
wrangler.tomlを確認してください。 name
の部分がそのままWorker名になるので、デフォルトのままの人はここを適当な名前に修正してください。
name = "my-todo-app"
で以下を実行する。
npm run deploy
が、これだとまだremote側のD1にデータベースが設定されてなくて500になります。
なので先程ローカルでやったことをremoteでやります。--local
を抜くだけです。
wrangler d1 execute todo-app --file=./db/init.sql
これでWorker上でデプロイできたので誰でもアクセスできるようになりました。
終わったらWorkerとD1を消しておくのを忘れずに。
以上です。
やった。ほぼまんま。
やってるところ。
Googleで認証を試してみたいので、こちらの記事を参照する。
できた。追加で画像を表示してみる。
sessionの取り回しも追加で実装するのが良さそう。
↓
...
import { NextAuthProvider } from "../Providers/NextAuthProvider";
import { Session } from "next-auth";
import { auth } from "./api/auth/[...nextauth]/auth";
...
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const session = await auth(); // next-auth v5(beta)の書き方
return (
<html lang="ja">
<body className={inter.className}>
<NextAuthProvider session={session as Session ?? null}>
{children}
</NextAuthProvider>
</body>
</html>
)
}
これで useSession
が使えるようになる。
export default function Home() {
const { data: session } = useSession();
...
return (
<>
<nav>
{ session? (
<>
<button onClick={() => signOut()}>Sign Out</button>
</>
) : (
<>
<button onClick={() => signIn()}>Sign In</button>
</>
)}
</nav>
<p>{typeof name !== "undefined" ? `Hello ${name}!` : "Loading..."}</p>
<img src={image} alt="image" />
</>
);
alpine.jsだけで、簡単な計算問題ツールを作成する。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>計算問題</title>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.16/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="flex items-center justify-center h-screen bg-gray-200">
<div class="p-8 bg-white rounded shadow mt-8">
<h2 class="text-2xl font-bold mb-4">計算問題</h2>
<div x-data="app()" x-init="generateProblems()">
<ul class="mb-2">
<li>
<span x-text="currentProblem.num1"></span>
<span> + </span>
<span x-text="currentProblem.num2"></span>
<span> = </span>
<input type="number" x-model="currentProblem.answer" x-ref="input" class="ml-2 border rounded w-16">
<span x-text="currentProblem.message"></span>
</li>
</ul>
<div class="grid grid-cols-3 gap-2">
<template x-for="number in [1, 2, 3]">
<button x-on:click="currentProblem.answer += number"
class="px-4 py-2 font-bold text-white bg-blue-500 rounded mb-2" x-text="number"></button>
</template>
</div>
<div class="grid grid-cols-3 gap-2">
<template x-for="number in [4, 5, 6]">
<button x-on:click="currentProblem.answer += number"
class="px-4 py-2 font-bold text-white bg-blue-500 rounded mb-2" x-text="number"></button>
</template>
</div>
<div class="grid grid-cols-3 gap-2">
<template x-for="number in [7, 8, 9]">
<button x-on:click="currentProblem.answer += number"
class="px-4 py-2 font-bold text-white bg-blue-500 rounded mb-2" x-text="number"></button>
</template>
</div>
<div class="grid grid-cols-3 gap-2">
<button x-on:click="currentProblem.answer = ''"
class="px-4 py-2 font-bold text-white bg-blue-500 rounded mb-2">C</button>
<button x-on:click="currentProblem.answer += '0'"
class="px-4 py-2 font-bold text-white bg-blue-500 rounded mb-2">0</button>
<button x-on:click="checkAnswer()" class="px-4 py-2 font-bold text-white bg-green-500 rounded mb-2">確認</button>
</div>
</div>
<script>
function app() {
return {
problems: [],
currentProblemIndex: 0,
get currentProblem() {
return this.problems[this.currentProblemIndex];
},
generateProblems() {
this.problems = Array.from({ length: 10 }, () => ({
num1: Math.floor(Math.random() * 90 + 10),
num2: Math.floor(Math.random() * 90 + 10),
answer: '',
message: '',
}));
},
checkAnswer() {
const correctAnswer = this.currentProblem.num1 + this.currentProblem.num2;
if (this.currentProblem.answer == correctAnswer) {
this.currentProblem.message = '⭕';
setTimeout(() => {
this.currentProblemIndex++;
// this.$refs.input.focus();
}, 1200); // 1.2-second delay
} else {
this.currentProblem.message = '❌';
}
},
};
}
</script>
</body>
</html>