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 │ │ 物件儲存 │ │
│ └─────────┘ └─────────┘ │
└─────────────────────────────┘
三個關鍵決策:
- 靜態优先、動態最小化:頁面骨架由 Astro 預先建置成靜態檔,只有
/api/*會執行 Functions。大多數請求根本不會碰到程式碼。 - Bindings 而非 HTTP 呼叫:Functions 用
env.DB(D1)、env.BUCKET(R2)綁定存取,不走公網,零延遲、零流量費。 - 同一個 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 的施工圖。
練習
- 幫 API 加一條
DELETE /api/comments/[id]想想:需要什麼額外的身份驗證才能安全開放?(提示:第 7 章) - 如果文章要支援「草稿狀態」,
posts表要怎麼改?加上status欄位並讓列表查詢過濾status = 'published'。 - 畫出「使用者從首頁點進一篇文章並留言」的完整時序圖,標出每一步快取命中與否。