CloudflareWorks + KVでカウンターウェブアプリを作る



# 事前準備（hello worldまで）

環境
OS: macOS Monterey 12.3.1
node -v: v18.0.0. 
yarn -v: 1.22.18

```sh
# ディレクトリ作成
mkdir cloudflare-works-counter
cd $_

# 初期化
% npx wrangler init -y
Need to install the following packages:
  wrangler
Ok to proceed? (y) y
 ⛅️ wrangler 2.0.5 
-------------------
Using npm as package manager.
✨ Created wrangler.toml
npm WARN deprecated rollup-plugin-inject@3.0.2: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.

added 57 packages, and audited 58 packages in 10s

1 package is looking for funding
  run `npm fund` for details

found 0 vulnerabilities
✨ Created package.json

added 2 packages, and audited 60 packages in 2s

1 package is looking for funding
  run `npm fund` for details

found 0 vulnerabilities
✨ Created tsconfig.json, installed @cloudflare/workers-types into devDependencies
✨ Created src/index.ts

To start developing your Worker, run `npm start`
To publish your Worker to the Internet, run `npm run publish`
```

こんな感じで雛形ができました
```
% tree -L 1
.
├── node_modules
├── package-lock.json
├── package.json
├── src
│   └── index.ts
├── tsconfig.json
└── wrangler.toml
```

この時点で　`yarn start` すると動きますが、`package.json` にscriptをひとつ追加します。
エミュレータの[Miniflare](https://github.com/cloudflare/miniflare)で起動するコマンドです。

```diff
  "scripts": {
    "start": "wrangler dev",
+   "local": "wrangler dev --local",
    "publish": "wrangler publish"
  }
```

b でブラウザを開くと「Hello World!」が表示されます
`/src/index.js` を編集してリロードすると表示内容が変わりますね

```
% yarn local          
yarn run v1.22.18
$ wrangler dev --local
 ⛅️ wrangler 2.0.5 
-------------------
⎔ Starting a local server...
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ [b] open a browser, [d] open Devtools, [l] turn off local mode, [c] clear console, [x] to exit                                                                                 │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
[mf:inf] Worker reloaded! (180B)
[mf:inf] Listening on localhost:8787
[mf:inf] - http://localhost:8787
[mf:inf] Updated Request cf object cache!
GET / 200 OK (7.21ms)
```

`yarn start` でも動きます
（初回はCloudflareをブラウザで開いて認証する必要があったかも）

# カウンターを作る

## hono導入

まずは[hono](https://github.com/honojs/hono)を使って書き換えます
[「Hono[炎]」はCloudflare Workersに特化した「Webアプリを作るためのフレームワーク」です](https://zenn.dev/yusukebe/articles/0c7fed0949e6f7)

```sh
% npm add hono                        

added 1 package, and audited 61 packages in 2s
1 package is looking for funding
  run `npm fund` for details
found 0 vulnerabilities
```
```ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.text('Hello! Cloudflare Works!'))

app.fire()
```
表示される文言が変わっただけで、やってることは同じ
```sh
% curl -X GET "http://localhost:8787"               
Hello! Cloudflare Works!
```

## counterのエンドポイントを作る
ユーザーごとにカウントできるように動的なurlにしておきます
```diff_typescript
+ app.get('/counter/:username', (c) => {
+   const username = c.req.param('username')
+   const count = 0
+ 
+   return c.text(`${username}, count: ${count}`)
+ })

app.fire()
```

```sh
% curl -X GET "http://localhost:8787/counter/yamada"
yamada, count: 0
```

## KVの追加
for production
```sh
% npx wrangler kv:namespace create COUNTER
 ⛅️ wrangler 2.0.5 
-------------------
🌀 Creating namespace with title "cloudflare-works-counter-COUNTER"
✨ Success!
Add the following to your configuration file in your kv_namespaces array:
{ binding = "COUNTER", id = "feoifeojeoijfaeojfo" }
```

`wrangler.toml` に設定を追加
```diff_toml
compatibility_date = "2022-05-14"

+ kv_namespaces = [
+   { binding = "COUNTER", id = "feoifeojeoijfaeojfo" }
+ ]
```

for preview
```sh
% npx wrangler kv:namespace create COUNTER --preview
 ⛅️ wrangler 2.0.5 
-------------------
🌀 Creating namespace with title "cloudflare-works-counter-COUNTER_preview"
✨ Success!
Add the following to your configuration file in your kv_namespaces array:
{ binding = "COUNTER", preview_id = "feuhvbiuehfiuaheoige" }
```

`wrangler.toml` に設定を追加
```diff_toml
compatibility_date = "2022-05-14"

kv_namespaces = [
-  { binding = "COUNTER", id = "feoifeojeoijfaeojfo" }
+  { binding = "COUNTER", id = "feoifeojeoijfaeojfo", preview_id = "feuhvbiuehfiuaheoige" }
]
```

## カウンターのコードの追加

```diff_typescript
+ declare const COUNTER: KVNamespace

+ const addCount = async (username: string) => {
+   const value = await COUNTER.get(username)
+   let count = value != null ? parseInt(value) : 0
+   count = count + 1
+   await COUNTER.put(username, `${count}`)
+   return count
+ }

- app.get('/counter/:username', (c) => {
+ app.get('/counter/:username', async (c) => {
  const username = c.req.param('username')
+  const count = await addCount(username)

-  return c.text(`${username}, count: 0`)
+  return c.text(`${username}, count: ${count}`)
})

app.fire()
```

KVから値を取得し、加算する関数`addCount`を追加します
WorksはKVと統合されているので、`COUNTER.get(key)` `COUNTER.put(key, value)`と書くだけで取得と登録ができます
初回のためにnull→0の変換、登録済みの値は文字列で返ってくるので数値への変換を行い、加算したのちKVに保存しています
型エラーを解消するために`declare const COUNTER: KVNamespace`を書いていますが、なくても動きます

## 動作確認

```sh
% yarn local
略）
```

別のターミナルから
```sh
 % curl -X GET "http://localhost:8787/counter/tanaka"
tanaka, count: 1

% curl -X GET "http://localhost:8787/counter/tanaka"
tanaka, count: 2

% curl -X GET "http://localhost:8787/counter/tanaka"
tanaka, count: 3

% curl -X GET "http://localhost:8787/counter/tanaka"
tanaka, count: 4

% curl -X GET "http://localhost:8787/counter/tanaka"
tanaka, count: 5
```

# Production へ deploy
```sh
% npx wrangler publish                              
 ⛅️ wrangler 2.0.5 
-------------------
Uploaded cloudflare-works-counter (0.79 sec)
Published cloudflare-works-counter (3.64 sec)
  cloudflare-works-counter.tyakatyaka3390.workers.dev
```
4秒！速い！

```sh
% curl -X GET "https://cloudflare-works-counter.tyakatyaka3390.workers.dev/"
Hello! Cloudflare Works!

curl -X GET "https://cloudflare-works-counter.tyakatyaka3390.workers.dev/counter/tanaka"
tanaka, count: 1
```
prviewとproductionでKVが分かれているので、ちゃんとtanakaさんのカウンターが初期値からになってますね

Worksの上限に達するまでは置いておくので、試してみてください

https://cloudflare-works-counter.tyakatyaka3390.workers.dev/counter/tanaka

# おまけ

```sh
# KVの一覧
% npx wrangler kv:namespace list        
[
  {
    "id": "",
    "title": "cloudflare-works-counter-COUNTER",
    "supports_url_encoding": true
  },
  {
    "id": "",
    "title": "cloudflare-works-counter-COUNTER_preview",
    "supports_url_encoding": true
  }
]

# 指定のKVに保存されたkeyの一覧
% npx wrangler kv:key list --binding=COUNTER
[
  {
    "name": "tanaka"
  }
]

# 指定のKV, 指定keyの値を取得
% npx wrangler kv:key get tanaka --binding=COUNTER            
2
```

/src/index.ts
```typescript
import { Hono } from 'hono'

const app = new Hono()

declare const COUNTER: KVNamespace

const addCount = async (username: string) => {
  const value = await COUNTER.get(username)
  let count = value != null ? parseInt(value) : 0
  count = count + 1
  await COUNTER.put(username, `${count}`)
  return count
}

app.get('/', (c) => c.text('Hello! Cloudflare Works!'))

app.get('/counter/:username', async (c) => {
  const username = c.req.param('username')
  const count = await addCount(username)

  return c.text(`${username}, count: ${count}`)
})

app.fire()
```