Skip to content
Anonymous

實戰 Cloudflare D1 + Astro:打造個人物品期限追蹤系統

將 Astro 網站升級為 SSR 模式,整合 Cloudflare D1 (SQLite) 資料庫,並實作一個隱私優先的個人物品管理系統。

#Astro #Cloudflare D1 #SSR #SQLite #Fullstack

作為一個數位遊牧的 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 (圖形介面)

  1. 登入 Cloudflare Dashboard
  2. 左側選單進入 Workers & Pages > D1
  3. 點擊 Create Database
  4. 命名為 personal-app,點擊 Create。
  5. 重要:記下 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 時,最穩健的做法是多重身分檢查

  1. Header 檢查:優先讀取 CF-Access-Authenticated-User-Email,這是最直接的方式。
  2. JWT 備援:如果因某些網路配置導致 Header 遺失,我們可以解碼 CF_Authorization Cookie (JWT)。即便 Header 不見了,Token 通常都還在,從 Payload 中能解析出 Email。
  3. 本地開發:本地沒有 Cloudflare Access 層,所以我們設計了 user@example.com 作為預設 fallback,或透過 X-Dev-User Header 模擬特定用戶。

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_constructors flag,否則 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