跳轉到

6.1 專案規劃:架構圖、資料模型與 API 設計#

本節目標#

前三章我們分別學會了 Pages(第 3 章)、Functions(第 4 章)與 D1 / R2(第 5 章),但它們都是單獨的技術點。本章要把它們全部接起來,打造一個真實的三合一網站:

  • 部落格:文章列表、單篇文章頁(資料放在 D1)
  • 留言板:訪客可以留言、看留言(寫入 D1)
  • 檔案下載:提供講義、範例程式等檔案(存放在 R2)

本節不寫程式,只做三件工程師常常嫌麻煩卻最重要的事:需求清單、架構圖、資料與 API 設計。額外花 30 分鐘做規劃,後面 6.2、6.3 的實作就不會走回頭路。

需求清單#

先把需求寫成可驗收的條列,這是後續設計 API 的依據(也是第 10 章上線檢查清單的種子):

功能需求(Must have)

# 需求 使用到的服務
F1 首頁列出最新 10 篇文章(標題、摘要、日期) Pages + D1
F2 文章頁 /posts/[id] 顯示全文,網址可分享 Pages + D1
F3 文章頁下方顯示留言、可新增留言(暱稱 + 內容) Functions + D1
F4 檔案列表頁 /files,列出可下載檔案(名稱、大小) Functions + R2
F5 點擊檔案可下載,支援大檔案串流 Functions + R2

非功能需求(Should have)

# 需求 作法
N1 全球延遲低:頁面由 CDN 邊緣快取 靜態優先 + API 動態補資料(3.1 節 Jamstack)
N2 留言有基本防護:長度限制、防 XSS、CORS 白名單 6.3 節參數驗證
N3 每次推到 GitHub 自動建置部署,可一鍵回滾 6.4 節 CI/CD
N4 免費方案內可運作 D1 免費 5GB、R2 免費 10GB,綽綽有餘

明確不做(Not in scope,避免範圍蔓延)

  • 會員系統與登入(留給第 7 章 7.1 節)
  • 文章後台編輯介面(用 SQL 直接寫入 D1 即可)
  • 圖片上傳(本章只做「已放在 R2 的檔案」的下載)

整體架構圖#

回顧第 1 章的邊緣運算觀念:所有請求都先抵達離使用者最近的 Cloudflare 節點,再由邊緣分頭處理:

        瀏覽器
          │  https://blog.example.com
          ▼
┌─────────────────────────────┐
│      Cloudflare Edge        │
│  ┌───────────────────────┐  │   靜態頁(HTML/CSS/JS)
│  │  Pages(Astro 建置)   │  │   ← CDN 快取,命中即回傳
│  └──────────┬────────────┘  │
│             │ /api/* 沒快取  │
│             ▼               │
│  ┌───────────────────────┐  │
│  │  Pages Functions      │  │   functions/api/*.ts
│  │  (邊緣 V8 Isolate)   │  │   ← 第 4 章:冷啟動歸零
│  └────┬─────────────┬────┘  │
│       │ binding     │ binding│
│       ▼             ▼       │
│  ┌─────────┐   ┌─────────┐  │
│  │   D1    │   │   R2    │  │
│  │ SQLite  │   │ 物件儲存 │  │
│  └─────────┘   └─────────┘  │
└─────────────────────────────┘

三個關鍵決策:

  1. 靜態优先、動態最小化:頁面骨架由 Astro 預先建置成靜態檔,只有 /api/* 會執行 Functions。大多數請求根本不會碰到程式碼。
  2. Bindings 而非 HTTP 呼叫:Functions 用 env.DB(D1)、env.BUCKET(R2)綁定存取,不走公網,零延遲、零流量費。
  3. 同一個 Git repo:前端、Functions、migration 都在一起,CI/CD 一條流水線(6.4 節)。

D1 資料模型#

只要兩張表就夠了。用第 5 章學過的 SQLite 語法:

-- migrations/0001_init.sql
CREATE TABLE posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    slug TEXT NOT NULL UNIQUE,            -- 網址代稱,如 "hello-cloudflare"
    title TEXT NOT NULL,
    excerpt TEXT NOT NULL DEFAULT '',
    body TEXT NOT NULL,
    created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE comments (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
    author TEXT NOT NULL CHECK (length(author) BETWEEN 1 AND 30),
    body   TEXT NOT NULL CHECK (length(body)  BETWEEN 1 AND 500),
    created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX idx_comments_post ON comments(post_id, created_at DESC);

設計理由(對照第 3 章的防線觀念):

  • slug 唯一:網址用 /posts/hello-cloudflare 而非 /posts/3,SEO 與可讀性都好
  • ON DELETE CASCADE:刪文章時留言一起刪,避免孤兒資料
  • CHECK 約束:資料庫端的最後一道防線,就算程式有 bug 也塞不進 600 字的留言
  • 複合索引 (post_id, created_at DESC):查詢模式是「某文章的留言、由新到舊」

API 路由表#

遵循第 4 章的檔案式路由,functions/ 目錄結構直接對映網址:

Method 路徑 Functions 檔案 說明
GET /api/posts functions/api/posts.ts 文章列表(?limit=10)
GET /api/posts/[slug] functions/api/posts/[slug].ts 單篇文章全文
GET /api/posts/[slug]/comments .../[slug]/comments.ts 該文章的留言列表
POST /api/posts/[slug]/comments 同上(依 method 分支) 新增留言
GET /api/files functions/api/files.ts R2 檔案列表
GET /api/files/[key] functions/api/files/[key].ts 下載檔案(串流)

統一的回應格式,讓前端好處理:

// 成功
{ "data": { ... } }

// 失敗(HTTP status 對應 400 / 404 / 500)
{ "error": "留言長度超過上限" }

回頭檢查:需求到設計的映射#

每個需求都要能指出「由哪個設計承擔」,否則就是規劃有漏:

  • F1/F2 → 頁面 fetch('/api/posts') 系列 + posts 表 slug 索引
  • F3 → comments 表 + POST /api/posts/[slug]/comments
  • F4/F5 → R2 的 list() 與 get() + /api/files 兩條路由
  • N1 → 靜態頁快取;留言區用用戶端 JS 動態載入,不拖慢首屏
  • N2 → HTTP status 語意正確 + CHECK 約束 + CORS 白名單(6.3)

小結與練習#

本節完成了「動手前先想清楚」:需求表 → 架構圖 → 兩張表 → 六條路由。這份設計文件就是 6.2、6.3 的施工圖。

練習

  1. 幫 API 加一條 DELETE /api/comments/[id] 想想:需要什麼額外的身份驗證才能安全開放?(提示:第 7 章)
  2. 如果文章要支援「草稿狀態」,posts 表要怎麼改?加上 status 欄位並讓列表查詢過濾 status = 'published'。
  3. 畫出「使用者從首頁點進一篇文章並留言」的完整時序圖,標出每一步快取命中與否。