實戰 Cloudflare D1 + Astro:打造個人物品期限追蹤系統
將 Astro 網站升級為 SSR 模式,整合 Cloudflare D1 (SQLite) 資料庫,並實作一個隱私優先的個人物品管理系統。
作為一個數位遊牧的 Solopreneur,將生活中的各種數據(3C 保固、食品效期、訂閱服務)數位化管理是必然的需求。與其依賴第三方 App 洩漏隱私,不如利用我們現有的 Personal Digital HQ 打造一個專屬的追蹤系統。
本文將記錄如何在現有 Astro 專案中,整合 Cloudflare D1 (Serverless SQLite),從零打造一個全端功能。
Phase 1: 環境升級 (Enable SSR)
要連接資料庫,我們必須讓 Astro 具備後端能力。
1. 安裝 Adapter
Cloudflare Adapter 讓我們能在 Edge Network 上執行 API。
npx astro add cloudflare
2. Adapter 設定 (重要更新)
在最新版的 Astro 中,output: 'hybrid' 已被棄用。現在我們只需要保持預設的 output: 'static',並安裝 Cloudflare Adapter 即可。
// astro.config.mjs
export default defineConfig({
output: 'static', // 預設值,支援混合渲染
adapter: cloudflare()
});
當我們在頁面或 API 中加入 export const prerender = false; 時,Astro 會自動將其切換為 SSR 模式。
3. 驗證 SSR
建立 src/pages/api/time.ts,如果每次重整時間都會變,代表 SSR 成功運作。
Phase 2: 設定 Cloudflare D1 資料庫
Cloudflare D1 是基於 SQLite 的邊緣資料庫,免費、快速且與 Workers 整合極佳。
方式一:使用 Dashboard (圖形介面)
- 登入 Cloudflare Dashboard。
- 左側選單進入 Workers & Pages > D1。
- 點擊 Create Database。
- 命名為
personal-app,點擊 Create。 - 重要:記下 Dashboard 顯示的
Database ID(例如xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)。
方式二:使用 CLI (Wrangler)
如果你喜歡駭客任務風格,或是需要自動化腳本:
# 1. 登入 (如果還沒)
npx wrangler login
# 2. 建立資料庫
npx wrangler d1 create personal-app
執行後,終端機回傳一段 JSON Config,這就是我們要填入 wrangler.jsonc 的內容。
配置專案
在專案根目錄建立(或修改)wrangler.jsonc:
{
"name": "personal-hq-db",
"d1_databases": [
{
"binding": "DB",
"database_name": "personal-app",
"database_id": "<YOUR_DATABASE_ID>"
}
]
}
初始化資料表 (Schema)
建立一個 SQL 檔案 db/schema.sql:
CREATE TABLE IF NOT EXISTS items (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
category TEXT,
expiry_date TEXT, -- ISO8601 string YYYY-MM-DD
created_at INTEGER DEFAULT (unixepoch())
);
接著執行遷移:
# 本地執行 (Local Development)
npx wrangler d1 execute personal-app --local --file=./db/schema.sql
# 遠端執行 (Production)
npx wrangler d1 execute personal-app --remote --file=./db/schema.sql
Phase 3: 實作 API
有了資料庫,我們需要建立 API 來與前端溝通。
1. 列表與新增 (GET/POST)
建立 src/pages/api/items/index.ts:
export const prerender = false;
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ locals }) => {
// 從 locals 取得 runtime 環境
const db = locals.runtime.env.DB;
const { results } = await db.prepare(
'SELECT * FROM items ORDER BY expiry_date ASC'
).all();
return new Response(JSON.stringify(results), {
headers: { 'Content-Type': 'application/json' }
});
};
export const POST: APIRoute = async ({ request, locals }) => {
const db = locals.runtime.env.DB;
const data = await request.json();
const id = crypto.randomUUID();
await db.prepare(
'INSERT INTO items (id, name, category, expiry_date, notify_days, note) VALUES (?, ?, ?, ?, ?, ?)'
).bind(id, data.name, data.category, data.expiry_date, data.notify_days || 7, data.note).run();
return new Response(JSON.stringify({ success: true, id }), {
status: 201,
headers: { 'Content-Type': 'application/json' }
});
};
2. 資料類型定義
為了讓 TypeScript 開心,別忘了在 env.d.ts 定義 DB 的類型(見專案代碼)。
3. 刪除資料 (DELETE)
建立 src/pages/api/items/[id].ts,利用動態路由捕捉 ID:
export const prerender = false;
import type { APIRoute } from 'astro';
export const DELETE: APIRoute = async ({ params, locals }) => {
const db = locals.runtime.env.DB;
const { id } = params;
await db.prepare('DELETE FROM items WHERE id = ?').bind(id).run();
return new Response(JSON.stringify({ success: true }));
};
下一步:前端介面
Phase 4: 前端介面 (Kanban Dashboard)
我們不想要無聊的 Excel 表格。為了更直觀地管理過期狀態,我們採用 Kanban (看板) 佈局,根據「剩餘天數」自動將物品分類:
- 🟢 Safe: > 30 天
- 🟡 Warning: < 30 天
- 🔴 Critical: < 7 天或已過期
1. 建立 React Components
我們會在 src/components/apps/inventory 建立相關組件(KanbanBoard, ItemCard, AddItemForm)。
利用 dayjs 或簡單的 JS Date 計算剩餘天數,動態分配顏色與欄位。
2. 建立頁面 (SSR)
建立 src/pages/apps/inventory.astro:
---
export const prerender = false;
import BaseLayout from '../../../layouts/BaseLayout.astro';
import { InventoryKanban } from '../../../components/apps/inventory/InventoryKanban';
// Server-side Fetching (SEO friendly & Fast)
const response = await fetch(new URL('/api/items', Astro.url));
const initialItems = await response.json();
---
<BaseLayout title="Personal Inventory">
<InventoryKanban client:load initialItems={initialItems} />
</BaseLayout>
這樣一來,使用者打開頁面時,資料已經由 Cloudflare D1 極速讀取並渲染好了 (SSR),後續的互動則由 React 接手 (Hydration)。
Phase 5: 部署與踩雷心得 (Troubleshooting)
在將這個系統部署到 Cloudflare Pages 的過程中,我們遇到了一些「坑」,這裡整理成珍貴的經驗分享:
1. D1 資料庫的多環境配置
Cloudflare Pages 支援 Production (正式) 與 Preview (預覽) 兩種環境,但 D1 的綁定設定需要特別注意:
- Wrangler 配置:在
wrangler.toml中,d1_databases陣列不會自動繼承到其他環境。如果你想在preview環境使用另一個資料庫,必須顯式定義[env.preview]區塊:
# 正式環境
[[d1_databases]]
binding = "DB"
database_name = "personal-app"
database_id = "xxx-prod-id"
# 預覽環境 (必須包含完整的 d1_databases 結構)
[env.preview]
[[env.preview.d1_databases]]
binding = "DB" # 綁定名稱必須相同,這樣程式碼才不用改
database_name = "personal-app_preview"
database_id = "xxx-preview-id"
- 控制台綁定:Wrangler 設定好後,別忘了在 Cloudflare Dashboard (Settings -> Functions) 中,也要分別為 Production 和 Preview 兩個區塊加入名為
DB的 D1 綁定。
2. 關於 Access Denied 與身份識別
我們使用 Cloudflare Access (Zero Trust) 作為身份驗證層。在實作 auth.ts 時,最穩健的做法是多重身分檢查:
- Header 檢查:優先讀取
CF-Access-Authenticated-User-Email,這是最直接的方式。 - JWT 備援:如果因某些網路配置導致 Header 遺失,我們可以解碼
CF_AuthorizationCookie (JWT)。即便 Header 不見了,Token 通常都還在,從 Payload 中能解析出 Email。 - 本地開發:本地沒有 Cloudflare Access 層,所以我們設計了
user@example.com作為預設 fallback,或透過X-Dev-UserHeader 模擬特定用戶。
3. 資料庫遷移 (Migrations)
本地開發很順利,但推上線後才發現 users 表格不存在?
記得對正式和預覽環境分別執行遷移指令:
# 正式環境
npx wrangler d1 migrations apply DB --remote
# 預覽環境 (使用剛才設定好的 env)
npx wrangler d1 migrations apply DB --remote --env preview
5. 500 Internal Server Error (Compatibility Flags)
如果你的 Astro SSR 頁面在部署後出現 500 Error,且日誌顯示 Error: To use the new ReadableStream() constructor...,這是因為 Cloudflare Workers Runtime 預設未啟用新的串流建構子。
解決方法:在 wrangler.toml 中更新 compatibility_date 至較新的日期(例如 2024-04-03 或之後)。
[!WARNING] 注意:如果你的
compatibility_date夠新,不要再手動加入streams_enable_constructorsflag,否則 Cloudflare 會因為「重複設定(該功能已變為預設)」而導致部署失敗。
Bonus: Wrangler 指令大全 (Cheatsheet)
在除錯 Serverless 應用時,熟練使用 CLI 是必備技能。以下是這專案最常用的指令:
🔍 線上除錯 (Live Logging)
看不到 console.log?用這個指令即時監控生產環境的日誌:
# 1. 先列出專案名稱 (以防忘記)
npx wrangler pages project list
# 2. 開始監控 (Tail logs)
npx wrangler pages deployment tail --project-name personal-hq
- 技巧:開著這個視窗,然後去刷新網頁,錯誤訊息就會直接噴在這個終端機裡。
💾 資料庫管理 (D1)
不一定要透過圖形介面,CLI 更快更直接:
# 檢查正式環境的 users 表格
npx wrangler d1 execute DB --remote --command "SELECT * FROM users"
# 檢查表格結構 (Schema)
npx wrangler d1 execute DB --remote --command "PRAGMA table_info(users)"
# 檢查預覽環境 (記得加 --env)
npx wrangler d1 execute DB --remote --env preview --command "SELECT * FROM users"
🚀 遷移 (Migrations)
# 建立新遷移檔案
npx wrangler d1 migrations create DB add_new_column
# 套用到正式環境
npx wrangler d1 migrations apply DB --remote
# 套用到預覽環境
npx wrangler d1 migrations apply DB --remote --env preview