Open4

2023なWebアプリの開発についてちょこちょこ試す

kazuphkazuph

Hono + htmx + Cloudflare で簡単なTodoアプリの作成

これ試したい。
https://zenn.dev/yusukebe/articles/e8ff26c8507799

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

テーブルを作成する。
ドキュメントは以下。
https://developers.cloudflare.com/d1/get-started/#4-run-a-query-against-your-d1-database

-- 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の追加

ここを見て合わせにいく。

https://github.com/yusukebe/hono-htmx/blob/main/package.json

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

https://github.com/yusukebe/hono-htmx/tree/main/src

これをコピペする。

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を消しておくのを忘れずに。

以上です。

kazuphkazuph

https://zenn.dev/yu7400ki/articles/58091688063734
やってるところ。

Googleで認証を試してみたいので、こちらの記事を参照する。
https://zenn.dev/activecore/articles/6b9f883f147107

できた。追加で画像を表示してみる。

sessionの取り回しも追加で実装するのが良さそう。

layout.ts
...
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が使えるようになる。

page.ts
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" />
    </>
  );

kazuphkazuph

alpine.jsだけで、簡単な計算問題ツールを作成する。

index.html
<!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>