現在位置を記録するWebアプリを作って、Cloudflare D1の使い方を学ぶ
Webアプリ開発にデータベースは欠かせませんので、その練習としてLocation Logger という現在位置を記録するWebアプリを作ってみました。 サーバーレス環境のCloudflare WorkersとCloudflare D1を使っています。 ソースコードはGitHubに公開してあります。
アプリ開発を通じて学びたかったこと
Section titled “アプリ開発を通じて学びたかったこと”Location Loggerの仕組みは以下のとおりです。
flowchart LR
ui[**Vueアプリ**
地図・地名表示、
現在位置の取得]
api[**Workers**
Vueアプリの配信、
REST API]
db[**D1**
位置情報の記録]
subgraph client [Webブラウザ]
direction LR
ui
end
subgraph server [Cloudflare]
direction LR
api <--> db
end
ui <--> api
Vueを使ったSPA(シングルページアプリケーション)の開発とCloudflare Workersを使ったデプロイのやり方は習得済みでしたので、今回の開発で学びたかったことは以下になります。
- Cloudflare Workersを使って、サーバーレスJavaScriptにREST APIを実装する方法を習得する
- Cloudflare D1上にデータベースを作成し、それにアクセスするJavaScriptの実装方法を習得する
C3 CLIを使ってアプリ開発を開始する
Section titled “C3 CLIを使ってアプリ開発を開始する”Cloudflareが提供しているC3 CLIを使って、Cloudflare D1を使ったWorkersアプリのプロジェクトを作成します。
npm create cloudflare@latest -- location-logger-example質問事項(Yes / Noなど)はEnterキーで続行し、選択肢は以下の項目を選びます。
- What would you like to start with? →
Framework Starter - Which development framework do you want to use? →
Vue - Would you like to use TypeScript? →
TypeScript
プロジェクトのサブフォルダー構成は以下のようになっています。
location-logger-example├── node_modules # npmで導入したパッケージが格納される├── public # favicon.icoなどのファイルを格納する├── server # REST APIの実装を格納する└── src # Vueアプリの実装を格納するパッケージのインストール
Section titled “パッケージのインストール”まずは、Vueアプリ側で使用するパッケージをインストールします。
Vueアプリは数年前にLaravel上で作ったものを流用しているので、使用しているパッケージもそのときのままで少し古いです。
# 不要なパッケージのアンインストールnpm uninstall vue-router
# パッケージのインストールnpm i bootstrap axios leaflet geographiclib-geodesic nosleep.js \ datatables.net-dt datatables.net-select datatables.net-vue3次に、WorkersとD1で使うパッケージをインストールします。HonoはCloudflare WorkersをサポートしているWebフレームワークです。また、drizzleはCloudflare D1をサポートしているORMです。
# Honoのインストールnpm i hono
# drizzleのインストールnpm i drizzle-ormnpm i -D drizzle-kit本アプリでは、REST APIの実装にHonoを、データベースにアクセスするORMにdrizzleを使用しています。本アプリの規模ではこれらを使う必要はありませんが、 これらのパッケージの使い方も習得しておきたいと考えました。
D1データベースの準備
Section titled “D1データベースの準備”Location Loggerの位置情報を記録するためのデータベースをCloudflare D1に作成します。
CloudflareのダッシュボードからGUIを使ってデータベースを作ることもできますが、今回はWranlgerコマンドを使います。初めて使用する際はCloudflareへのログインが必要です。
npx wrangler d1 create location_logger_example_db --binding DB --use-remoteCloudflareのダッシュボードでD1 SQL データベースを表示すると、location_logger_example_dbという名前のデータベースが作られていることを確認できます。
また、wrangler.jsoncを見ると、以下のようにd1_databasesのバインディングが追加されます。
"d1_databases": [ { "binding": "DB", "database_name": "location_logger_example_db", "database_id": "<D1データベースのdatabaseId>", "remote": true } ]"remote": trueを設定すると、npm run devでローカル起動した場合もD1データベースにアクセスするようになります。
デバッグ時はローカルにDBを作ることもできますが、今回はD1データベースの練習も兼ねて、ローカルにDBを作らずに進めます。
サーバーレスのJavaScriptからd1_databasesのバインディングを利用できるように型を生成します。
npx wrangler types型がworker-configuration.d.tsに追加されます。
declare namespace Cloudflare { interface GlobalProps { mainModule: typeof import("./server/index"); } interface Env { DB: D1Database; }}drizzle経由でD1データベースにアクセスするため、./drizzle.config.tsファイルを作成します。
import { defineConfig } from 'drizzle-kit'export default defineConfig({ schema: './drizzle/schema.ts', out: './drizzle/migrations', dialect: 'sqlite', driver: 'd1-http', dbCredentials: { accountId: process.env.CLOUDFLARE_ACCOUNT_ID!, databaseId: process.env.CLOUDFLARE_DATABASE_ID!, token: process.env.CLOUDFLARE_D1_TOKEN!, },})dbCredentialsには環境変数を使っています。.envで環境変数の値を設定します。
CLOUDFLARE_ACCOUNT_IDは、Cloudflareのダッシュボードで確認できます。CLOUDFLARE_D1_TOKENは、Cloudflareのダッシュボードからプロフィールを開き、サイドバーのAPIトークンを選択してトークンを作成するボタンを押して、カスタムトークンを作成してください。
CLOUDFLARE_ACCOUNT_ID=<Workers & PagesのAccount Detailsに記載されているAccount ID>CLOUDFLARE_DATABASE_ID=<D1データベースのdatabaseId>CLOUDFLARE_D1_TOKEN=<D1の編集権を備えたカスタムAPIトークン>./drizzle/schema.tsを作成して、D1データベースに作成するテーブルのモデルを実装します。
import { sql } from 'drizzle-orm'import { sqliteTable, int, text, real } from 'drizzle-orm/sqlite-core'
export const clients = sqliteTable('clients', { id: int('id').primaryKey({ autoIncrement: true }), cid: text('cid').notNull().unique(), ua: text('ua').notNull(), createdAt: text('created_at') .notNull() .default(sql`CURRENT_TIMESTAMP`),})
export const locations = sqliteTable('locations', { id: int('id').primaryKey({ autoIncrement: true }), timestamp: int('timestamp').notNull(), latitude: real('latitude').notNull(), longitude: real('longitude').notNull(), distance: real('distance'), address: text('address').notNull(), clientId: int('client_id').references(() => clients.id), createdAt: text('created_at') .notNull() .default(sql`CURRENT_TIMESTAMP`),})./drizzle/schema.tsで定義したテーブルのマイグレーションファイルを生成します。マイグレーションファイルの保存先は./drizzle.config.tsのoutで設定した./drizzle/migrationsです。
npx drizzle-kit generateマイグレーションファイルが正常に生成できたら、D1データベースにマイグレーションを実行します。
npx drizzle-kit migrate./drizzle/schema.tsを変更する度に、マイグレーションファイルの生成と実行を都度行います。
テーブルの確認はCloudflareのダッシュボードからも確認できますが、テーブル数だけ表示されます。D1データベース内のテーブルの一覧を得るためにSQLを実行します。
npx wrangler d1 execute location_logger_example_db \ --remote --command \ "SELECT tbl_name, sql FROM sqlite_schema WHERE type ='table';"REST APIの実装
Section titled “REST APIの実装”Location LoggerのREST APIを./server/index.tsに実装します。以下は一部省略しています。詳細はGitHubのindex.tsでご覧いただけます。
import { Hono, Context } from 'hono'import { HTTPException } from 'hono/http-exception'import { getCookie, setCookie } from 'hono/cookie'import { drizzle, DrizzleD1Database } from 'drizzle-orm/d1'import { clients, locations } from '../drizzle/schema'import { Client, Location } from '../src/shared/model'import { eq, desc } from 'drizzle-orm'
const VERSION = '0.0.1'const MAX_LOCATION_COUNT = 300
type ClientRow = typeof clients.$inferSelecttype LocationRow = typeof locations.$inferSelect
const app = new Hono<{ Bindings: Env }>()
app.onError((err: Error | HTTPException, c: Context) => { /* エラーハンドリング: 省略 */})
app.get('/api/v1/misc/version', async (c: Context) => { return c.json({ version: VERSION })})
app.get('/api/v1/client', async (c: Context) => { const db = drizzle(c.env.DB) const client = await getClient(db, c) return c.json({ cid: client.cid })})
app.get('/api/v1/location', async (c: Context) => { const db = drizzle(c.env.DB) const client = await getClient(db, c) const locationRows: LocationRow[] = await db.select() /* 位置情報の一覧取得: 省略 */ return c.json( /* 中略 */ )})
app.post('/api/v1/location', async (c: Context) => { const db = drizzle(c.env.DB) const client = await getClient(db, c) const location = await c.req.json<Location>() const locationRow = await db .insert(locations) /* 省略 */ .returning() return c.json({ id: locationRow[0].id })})
async function getClient( db: DrizzleD1Database<Record<string, never>>, c: Context): Promise<Client> { /* 省略 */}
export default app./src/shared/model.d.tsというファイルを作成して、REST APIの型を定義しています。
export interface Client { id: number cid: string ua: string createdAt: string}
export interface Location { timestamp: number latitude: number longitude: number distance: number | null address: string clientId: number createdAt: string}Vueアプリの移植
Section titled “Vueアプリの移植”Vueアプリは./index.htmlと./srcフォルダーの中身で実装します。
以下は、数年前に作ったVueアプリのコンポーネントを移植したときの手順です。本記事の主旨ではありませんので、省略します。
Vueアプリの移植作業の手順
./index.htmlは、<title>だけ変更し、他はデフォルトのままです。
./src/main.tsは、app.use(router)などのVue Routerに関する行を削除して、import 'bootstrap'を追加します。これに伴い、./src/viewsと./src/routerは削除します。
./src/App.vueには、作成済みのVueアプリのコンポーネントを<template>の中に組み込みます。./src/components配下のファイル・フォルダーは全て削除して、作成済みのVueアプリのコンポーネントをコピーします。
REST APIのエンドポイントが少し変わったので、API呼び出し周りを修正しています。
./src/assets/はbase.cssとmain.cssだけを残しておき、main.cssの中を数年前に作ったCSSで置き換えます。
なお、数年前に作ったVueアプリのコンポーネントはTypeScriptを使っていません。このため、TypeScriptで実装されているApp.vueでコンポーネントの型が解決できないとエラーになります。
そのため、./src/shims.d.tsにJavaScript Vueコンポーネントの型を定義しています。
declare module '*.vue' { import type { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component}ビルドとデバッグ
Section titled “ビルドとデバッグ”npm run buildでビルドエラーが発生しないか確認したところ、以下のメッセージが表示されます。
(!) Some chunks are larger than 500 kB after minification. Consider:...ビルドしたVueアプリが500KBを超えてしまうため、/.vite.config.tsに以下の設定を追加します。
export default defineConfig({ build: { chunkSizeWarningLimit: 1000, }, plugins: [ // 以下略これで再度ビルドを実行すると、エラーが無くなりました。
ローカル環境でLocation Loggerが動くことを確認します。
npm run dev
右側にCID: xxxxxxxxと表示されている部分は、Cookieに設定しているUUIDです。このUUIDはD1データベースのclientsテーブルに登録されていることを示しています。SQLでclientsテーブルの内容を確認したところ、画面に表示されているUUIDはclientsテーブルに登録されていました。
npx wrangler d1 execute location_logger_example_db \ --remote --command "SELECT * FROM clients;"REST APIをデバッグするために、./.vscode/launch.jsonを作成します。
{ "version": "0.2.0", "configurations": [ { "name": "devサーバにアタッチする", "type": "node", "request": "attach", "port": 9229 } ]}これでnpm run devで動かしている間にデバッガをアタッチできるようになります。
Cloudflare Workersにデプロイする
Section titled “Cloudflare Workersにデプロイする”GitHub経由でのデプロイ方法は、以前の記事を参考にしてただければと思います。今回はC3 CLIを使ってCloudflare Workersにデプロイします。
npm run deployターミナルには以下のようなメッセージが表示され、問題なくデプロイできました。(***はアカウント名)
Uploaded location-logger-example (8.07 sec)Deployed location-logger-example triggers (1.05 sec) https://location-logger-example.***.workers.devCurrent Version ID: adc9e8c4-64c2-4645-a14a-a982d7af163aCloudflareのダッシュボードを見ると、デプロイしたことを確認できます。
workers.devのURLを開くと、Cloudflare Workersが配信するLocation Loggerの画面が開いて、現在位置を取得・記録することができます。Workers上でREST APIが正常に動作しているかどうかは、以下のコマンドでも確認できます。
curl https://location-logger-example.***.workers.dev/api/v1/misc/version{"version":"0.0.1"}が返ってくれば正常に動作しています。
今回学ぶことが出来たのは、下図の斜体文字の部分になります。
flowchart LR
ui["`**Vueアプリ**`"]
api["`**Workers**
REST API: _Hono_
ORM: _Drizzle ORM_`"]
db["`**D1**
DB操作: _Wrangler_
マイグレーション: _Drizzle Kit_`"]
subgraph client [Webブラウザ]
direction LR
ui
end
subgraph server [Cloudflare]
direction LR
api <--> db
end
ui <--> api
ユーザー認証やリレーションなど、アプリ開発に必要な知識は沢山ありますので、引き続き精進しようと思います。