4.4 Pages Functions:檔案式路由與全棧頁面#
本節目標#
- 認識 Cloudflare Pages:把靜態前端與後端函式合併在同一個專案的「全棧」單元
- 掌握
functions/資料夾慣例:檔名即路由、匯出名即 HTTP 方法 - 學會
context.params、context.env與context.data的使用 - 建立「Pages vs Worker」的權衡框架,知道何時該選哪個
Pages 是什麼:靜態網站 + 邊緣函式#
Cloudflare Pages 把兩件事打包:
- 靜態資源託管:Vite/Next/Astro 產物放進全球 CDN。
- 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 事件與重後端。兩者底層執行模型相同。
練習:
- 在 Pages 專案中建立
functions/api/users/[id].ts與functions/api/users/index.ts,分別支援GET /api/users/:id與GET /api/users。 - 寫一層
_middleware.ts,把一個user物件放進data,在 handler 中取出使用,體驗 middleware → handler 的資料傳遞。 - 用 4.3 的 Vitest pool 在 Pages 專案中測一個 function,說明 4.3 的測試策略為何同樣適用(提示:Functions 本質就是 Worker)。