跳轉到

4.4 Pages Functions:檔案式路由與全棧頁面#

本節目標#

  • 認識 Cloudflare Pages:把靜態前端與後端函式合併在同一個專案的「全棧」單元
  • 掌握 functions/ 資料夾慣例:檔名即路由、匯出名即 HTTP 方法
  • 學會 context.params、context.env 與 context.data 的使用
  • 建立「Pages vs Worker」的權衡框架,知道何時該選哪個

Pages 是什麼:靜態網站 + 邊緣函式#

Cloudflare Pages 把兩件事打包:

  1. 靜態資源託管:Vite/Next/Astro 產物放進全球 CDN。
  2. Functions:functions/ 目錄下寫 Workers 程式碼,檔名即路由,零路由設定。

整個站是一疊可原子化部署的檔案,前後端同源同網域。

functions/ 資料夾:檔名即路由#

核心慣例可以用一條同構式描述:

\[ \text{檔案路徑} \;\cong\; \text{URL 路徑}, \qquad \text{匯出的函式名} \;\cong\; \text{HTTP 方法} \]
functions/
├── index.ts               →  GET/POST /
├── about.ts               →  GET /about
├── api/
│   ├── todos.ts           →  GET/POST /api/todos
│   └── todos/
│       └── [id].ts        →  GET/PUT/DELETE /api/todos/:id
└── blog/
    └── [...slug].ts       →  /blog/*(多重匹配)

一個具體檔案:

// functions/api/todos.ts
type Env = { TODOS: KVNamespace };   // 'onRequestXxx' 的 context 泛型

export const onRequestGet: PagesFunction<Env> = async (context) => {
  const data = await context.env.TODOS.list();
  return Response.json(data.keys.map((k) => k.name));
};

export const onRequestPost: PagesFunction<Env> = async (context) => {
  const body = await context.request.json() as { title: string };
  const id = crypto.randomUUID();
  await context.env.TODOS.put(id, JSON.stringify(body));
  return Response.json({ id }, { status: 201 });
};

規則細節:

匯出名 語意
onRequest 攔截所有方法
onRequestGet / onRequestPost / onRequestPut / onRequestDelete 對應單一方法
onRequestHead / onRequestOptions 決定是否讓請求穿透到靜態資源,預設沒有匹配才會交給 asset

params 與中間件#

[id].ts 的方括號捕獲從 context.params 取:

// functions/api/todos/[id].ts
export const onRequestGet: PagesFunction<Env, "id"> = async ({ params, env }) => {
  const value = await env.TODOS.get(params.id as string);
  return value === null
    ? new Response("Not Found", { status: 404 })
    : Response.json(JSON.parse(value));
};

functions/_middleware.ts 是一個約定:它會先於同層檔案的 handler 執行,且必須呼叫 next()(等同 Axum 的 next.run(req)):

// functions/_middleware.ts
export const onRequest: PagesFunction = async ({ request, next }) => {
  const key = request.headers.get("authorization");
  if (!key) return new Response("Unauthorized", { status: 401 });
  return next();   // 交給下一個 handler 或靜態資源
};
客戶端請求 → functions/_middleware.ts ─next()→ 對應檔案的 onRequestGet/…→ 回應

context 的完整形狀#

PagesFunction 的 context 型別拆開來看:

interface EventContext<Env, P, Data> {
  request: Request;         // 與 Worker 相同的 Fetch API Request
  env: Env;                 // 綁定:vars、secrets、KV、D1…
  params: P;                // 方括號路由的捕獲,如 { id: "17" }
  data: Data;               // 前一層 middleware 塞進來的結果
  next(): Promise<Response>;
  waitUntil(p: Promise<any>): void;
}

與 4.2 的 Worker env 同源,但多了 params 與 data — 這兩者是「檔名即路由 + middleware 呼叫 next()」模型的自然推論。

Pages 项目的 bindings 從 Dashboard 設#

Pages 的 vars/secrets 不由 wrangler.toml 主導,在 Dashboard → Settings → Functions 分兩區:明文變數與 Secrets。本地開發用 .dev.vars(加入 .gitignore),原則同 4.2:機密絕不進 git。

Pages vs Worker:取捨框架#

兩者後端執行時完全相同(都是 Isolate 上的 Worker),差別在專案結構與自動化範圍:

面向 Pages Worker(獨立)
部署單位 前端資源 + functions 原子化一起上 後端 API 或單一服務
路由 檔名慣例,零設定 自己寫(或用 Hono)
預覽環境 每 branch 自動出 *.pages.dev 要自己配 --env
彈性 限於 HTTP + 靜態網站模型 cron/scheduled、email、queues、Durable Objects 等全事件
適合 內容站、公司官網、全棧小產品 純 API、bot、cron、重後端

形式上,Pages ≈ Worker +「檔案系統即路由」語法糖 + 前端 asset 綁定。能被 Pages 涵蓋的就不必自己寫路由;需要 scheduled、Durable Objects 或獨立服務邊界時,才上 Worker。

全棧頁面:一個目錄完整產品#

my-app/          # Vite + React 前端在 src/
functions/api/count.ts

後端 function:

// functions/api/count.ts
export const onRequestGet: PagesFunction<{ COUNTER: KVNamespace }> = async ({ env }) => {
  const n = Number((await env.COUNTER.get("n")) ?? 0) + 1;
  await env.COUNTER.put("n", String(n));
  return Response.json({ count: n });
};
// src/App.tsx — 前端呼叫同源 API
export function App() {
  const [count, setCount] = useState<number | null>(null);
  useEffect(() => { fetch("/api/count").then(r => r.json()).then(d => setCount(d.count)); }, []);
  return <h1>訪客人數:{count}</h1>;   // 同源,無 CORS 問題
}

同源這一點在邊緣部署上意味著:前端與 API 同住一個子網域,DNS 距離、CORS、cookie 皆最簡。

小結與練習#

  • Pages 把「靜態資源 + functions」合成一個原子單位,檔名即路由,匯出名即方法。
  • context.params 取路徑捕獲、context.env 取綁定、_middleware.ts 支持 next() 串接。
  • Pages 適合全棧小產品;Worker 適合非 HTTP 事件與重後端。兩者底層執行模型相同。

練習:

  1. 在 Pages 專案中建立 functions/api/users/[id].ts 與 functions/api/users/index.ts,分別支援 GET /api/users/:id 與 GET /api/users。
  2. 寫一層 _middleware.ts,把一個 user 物件放進 data,在 handler 中取出使用,體驗 middleware → handler 的資料傳遞。
  3. 用 4.3 的 Vitest pool 在 Pages 專案中測一個 function,說明 4.3 的測試策略為何同樣適用(提示:Functions 本質就是 Worker)。