用 Pages + Functions 部署全端應用
一個 git repo、一次部署:靜態網頁和 /api/* 後端一起上線。
我們要串什麼?
Cloudflare Pages 負責放你的前端(瀏覽器會下載的 HTML、CSS、JavaScript)。而 Pages Functions(Pages 函式)讓「同一個專案」也能跑後端程式碼——也就是你的 API。你只要把檔案丟進 functions/ 資料夾,每個檔案就會自動變成一條 API 路由,再綁定 D1 資料庫(SQL)或 KV 儲存(鍵值對),後端就能讀寫資料了。
重點在於:靜態網站和 API 住在同一個 repository(程式碼倉庫)裡、一起部署。打 / 會回傳靜態檔案;打 /api/hello 則會執行一個 Function。不用另外架伺服器,也不用維護第二條部署流程。
把它想成…
一間「前面是店面、後面是辦公室」的店,全在同一個屋簷下。店面(靜態網頁)是客人看到的門面;後台辦公室(Functions)處理訂單、去翻檔案櫃(D1/KV)。一棟建築、一把鑰匙、一個地址。
各層的職責
這是你實際會建立的資料夾。放在「建置輸出資料夾」(這裡是 public/)的東西,都會被當成靜態檔案直接送出;放在 functions/ 裡的,則會變成對應到某個網址的後端程式碼。
my-fullstack-app/
├── public/ # static front-end (served as-is)
│ └── index.html
├── functions/ # each file = one API route
│ └── api/
│ └── hello.js # -> /api/hello
├── schema.sql # D1 table + seed data
└── wrangler.toml # bindings + build output dir靜態前端
public/ 裡的檔案(HTML、CSS、JS、圖片)會被快取,並從 Cloudflare 全球邊緣節點直接送出。
functions/ 就是你的 API
functions/api/hello.js 會自動回應 /api/hello。檔案的路徑,就等於網址的路徑。
綁定(Bindings)
綁定就是一個有名字的把手(例如 env.DB、env.CACHE),把 Function 接到 D1 或 KV——不用連線字串、不用密碼。
一次部署
推上 git(或跑一行指令),靜態網站和 Functions 就會一起上線、掛在同一個網域下。
從 git push 到請求上線
當你把 repo 連上 Pages 後,每次 push 都會觸發一次建置(build)。Pages 會編譯前端、把 functions/ 資料夾打包起來,然後一起部署。之後邊緣節點會「逐一請求」判斷:要回傳靜態檔案,還是執行一個 Function。
一步步動手做
我們會做一個小網頁去抓 /api/hello、一個從 D1 讀名字並用 KV 計數的 Function、把綁定設好、再部署。前端、Function、資料層三邊,全部塞進同一個專案。
寫前端
把這段放進 public/index.html。fetch('/api/hello') 是同源請求(same-origin)——不用處理 CORS、不用寫完整網址——因為 API 就住在同一個專案裡。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Hello Pages</title> </head> <body> <h1 id="msg">Loading…</h1> <script> // Same origin: the Function is at /api/hello in the SAME project fetch("/api/hello") .then((r) => r.json()) .then((data) => { document.getElementById("msg").textContent = data.message; }) .catch((err) => { document.getElementById("msg").textContent = "Error: " + err; }); </script> </body> </html>寫 Function
建立 functions/api/hello.js。匯出的 onRequest 會處理所有 HTTP 方法;context.env 就握著你的綁定。(想只處理單一方法,就用 onRequestGet/onRequestPost。)
// functions/api/hello.js -> GET/POST /api/hello export async function onRequest(context) { // context.env carries your bindings (DB, CACHE) + vars + secrets const { env } = context; // Read one row from the D1 database bound as DB const { results } = await env.DB .prepare("SELECT name FROM greetings WHERE id = ?") .bind(1) .all(); const name = results[0]?.name ?? "world"; // Count visits in the KV namespace bound as CACHE const hits = Number(await env.CACHE.get("hits")) + 1; await env.CACHE.put("hits", String(hits)); return Response.json({ message: `Hello from ${name}!`, hits }); }建立資料層
建立一個 D1 資料庫和一個 KV 命名空間,然後把資料表灌進去。先把下面的 SQL 存成 schema.sql。
-- schema.sql: create the table the Function reads, then seed one row CREATE TABLE IF NOT EXISTS greetings ( id INTEGER PRIMARY KEY, name TEXT NOT NULL ); INSERT INTO greetings (id, name) VALUES (1, 'Taipei');開好 D1 + KV
Wrangler 會各印出一組 id——把它們複製到下一步的 wrangler.toml。--remote 代表對雲端上真正的資料庫執行。
npx wrangler d1 create my-app-db npx wrangler kv namespace create CACHE # seed the D1 table from your SQL file npx wrangler d1 execute my-app-db --remote --file=./schema.sql把 D1 + KV 綁到專案
wrangler.toml 告訴 Pages:要把哪些儲存以 env.DB、env.CACHE 的名字交給 Functions,以及哪個資料夾是靜態輸出。pages_build_output_dir 這一行,正是它被視為 Pages 設定的關鍵。
name = "my-fullstack-app" pages_build_output_dir = "./public" compatibility_date = "2025-01-01" # D1 -> reachable in Functions as env.DB [[d1_databases]] binding = "DB" database_name = "my-app-db" database_id = "<paste-your-d1-id>" # KV -> reachable in Functions as env.CACHE [[kv_namespaces]] binding = "CACHE" id = "<paste-your-kv-id>"設定建置
純 HTML 沒東西要編譯,所以建置指令留空、輸出設成 public/。如果是框架(Vite/React),就設好建置指令、把輸出指向 dist/。這些設定在 Pages 儀表板的 Settings > Builds,或寫在 wrangler.toml 裡。
# Pages build settings (dashboard, or wrangler.toml) Build command: (blank for plain static, or: npm run build) Build output directory: public # plain HTML/CSS/JS Root directory: / # For a Vite / React app instead: # Build command: npm run build # Build output directory: dist兩種部署方式
直接部署(direct deploy)會立刻把資料夾上傳。Git 連動才是正式做法:在儀表板把 repo 連一次、設好建置指令與輸出資料夾,之後每次 push 到正式分支,都會自動建置並部署。
# Option A: direct deploy (great for a quick first try) npx wrangler pages deploy ./public # Option B: git-connected (recommended) # 1. Push your repo to GitHub/GitLab # 2. Cloudflare dashboard > Workers & Pages > Create > Pages > Connect to Git # 3. Set build command + output dir, add D1/KV bindings # 4. Every push to main now builds & deploys automatically git push origin main
重點概念
最常見的疑問:到底該用 Pages Functions,還是獨立的 Worker?下面這張圖給你一個快速判斷準則。
Pages 與 Workers 的差別
Pages 本質是「前端網站托管」,再外掛 Functions 當後端;Worker 則是「純程式碼」、本身不附帶靜態托管。其實 Pages Functions 底層就是跑在 Workers 上的。
檔案即路由
functions/api/hello.js 對應 /api/hello。[id].js 只接一段路徑(例如 /users/42);[[route]].js 是 catch-all,後面接幾層都吃得下。
onRequest 處理器
匯出 onRequest 處理所有方法;或用 onRequestGet/onRequestPost/onRequestPut/onRequestDelete 各自對應一個 HTTP 動詞。
context 物件
每個處理器都會拿到 context,裡面有 env(綁定)、params(動態路由的值)、request、next(中介層 middleware)和 waitUntil(背景工作)。
用綁定,不用密碼
綁定讓 Function 透過 env.NAME 直接、已驗證地存取 D1/KV/R2。程式碼裡不會出現連線字串或密碼。
用 _middleware.js 做中介層
資料夾裡的 _middleware.js 會在該層路由之前先跑——很適合做驗證或記錄。呼叫 context.next() 就會繼續往下交給命中的 Function。
小提示、陷阱與計費
functions/ 資料夾是特別的
別把 functions/ 放進「建置輸出資料夾」裡。Pages 會「另外」讀 functions/ 來產生路由;如果你的前端建置流程清空或搬動了它,你的 /api/* 路由就會無聲無息地消失。
- 綁定要在儀表板裡幫「正式(Production)」和「預覽(Preview)」兩個環境都各加一次,否則預覽部署在 env.DB 會炸出 undefined。
- 用這行在本機整包跑起來:npx wrangler pages dev ./public——它會把靜態檔案、Functions 和你的綁定一起服務。
- Function 的呼叫次數會算進你的 Workers 額度:免費方案每天含 100,000 次請求。
- 免費方案:每月 500 次建置、每次部署最多 20,000 個檔案、單一檔案上限 25 MiB。
- 關聯式/SQL 資料用 D1;快速鍵值查找與快取用 KV;大型檔案則交給 R2。
先靜態,再長成全端
你今天可以先上一個純靜態網站,之後想加第一個後端,只要新增 functions/api/hello.js 就好。其他什麼都不用改——同一個專案、同一條部署。