sell整合實戰
web

用 Pages + Functions 部署全端應用

一個 git repo、一次部署:靜態網頁和 /api/* 後端一起上線。

1個 repo 跑全端
500每月免費建置
100自訂網域
25 MiB單檔上限
insights

我們要串什麼?

Cloudflare Pages 負責放你的前端(瀏覽器會下載的 HTML、CSS、JavaScript)。而 Pages Functions(Pages 函式)讓「同一個專案」也能跑後端程式碼——也就是你的 API。你只要把檔案丟進 functions/ 資料夾,每個檔案就會自動變成一條 API 路由,再綁定 D1 資料庫(SQL)或 KV 儲存(鍵值對),後端就能讀寫資料了。

重點在於:靜態網站和 API 住在同一個 repository(程式碼倉庫)裡、一起部署。打 / 會回傳靜態檔案;打 /api/hello 則會執行一個 Function。不用另外架伺服器,也不用維護第二條部署流程。

schema專案結構與請求路由

靜態路徑

/api/*

呼叫 /api/hello

瀏覽器

Cloudflare 邊緣節點

靜態檔案 (HTML/CSS/JS)

functions/api/[[route]].js

D1 資料庫 (env.DB)

KV 命名空間 (env.CACHE)

home_work

把它想成…

一間「前面是店面、後面是辦公室」的店,全在同一個屋簷下。店面(靜態網頁)是客人看到的門面;後台辦公室(Functions)處理訂單、去翻檔案櫃(D1/KV)。一棟建築、一把鑰匙、一個地址。

account_tree

各層的職責

這是你實際會建立的資料夾。放在「建置輸出資料夾」(這裡是 public/)的東西,都會被當成靜態檔案直接送出;放在 functions/ 裡的,則會變成對應到某個網址的後端程式碼。

text資料夾結構
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
html

靜態前端

public/ 裡的檔案(HTML、CSS、JS、圖片)會被快取,並從 Cloudflare 全球邊緣節點直接送出。

function

functions/ 就是你的 API

functions/api/hello.js 會自動回應 /api/hello。檔案的路徑,就等於網址的路徑。

cable

綁定(Bindings)

綁定就是一個有名字的把手(例如 env.DB、env.CACHE),把 Function 接到 D1 或 KV——不用連線字串、不用密碼。

merge

一次部署

推上 git(或跑一行指令),靜態網站和 Functions 就會一起上線、掛在同一個網域下。

swap_vert

從 git push 到請求上線

當你把 repo 連上 Pages 後,每次 push 都會觸發一次建置(build)。Pages 會編譯前端、把 functions/ 資料夾打包起來,然後一起部署。之後邊緣節點會「逐一請求」判斷:要回傳靜態檔案,還是執行一個 Function。

schema推送、建置、部署、回應
"瀏覽器""Cloudflare 邊緣""Pages 建置"GitHub"開發者""瀏覽器""Cloudflare 邊緣""Pages 建置"GitHub"開發者"git pushwebhook 觸發建置執行建置指令部署靜態檔案 + FunctionsGET /index.html (靜態)GET /api/helloFunction 回傳 JSON
construction

一步步動手做

我們會做一個小網頁去抓 /api/hello、一個從 D1 讀名字並用 KV 計數的 Function、把綁定設好、再部署。前端、Function、資料層三邊,全部塞進同一個專案。

  1. 寫前端

    把這段放進 public/index.html。fetch('/api/hello') 是同源請求(same-origin)——不用處理 CORS、不用寫完整網址——因為 API 就住在同一個專案裡。

    html
    <!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>
  2. 寫 Function

    建立 functions/api/hello.js。匯出的 onRequest 會處理所有 HTTP 方法;context.env 就握著你的綁定。(想只處理單一方法,就用 onRequestGet/onRequestPost。)

    js
    // 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 });
    }
  3. 建立資料層

    建立一個 D1 資料庫和一個 KV 命名空間,然後把資料表灌進去。先把下面的 SQL 存成 schema.sql。

    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');
  4. 開好 D1 + KV

    Wrangler 會各印出一組 id——把它們複製到下一步的 wrangler.toml。--remote 代表對雲端上真正的資料庫執行。

    bash
    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
  5. 把 D1 + KV 綁到專案

    wrangler.toml 告訴 Pages:要把哪些儲存以 env.DB、env.CACHE 的名字交給 Functions,以及哪個資料夾是靜態輸出。pages_build_output_dir 這一行,正是它被視為 Pages 設定的關鍵。

    toml
    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>"
  6. 設定建置

    純 HTML 沒東西要編譯,所以建置指令留空、輸出設成 public/。如果是框架(Vite/React),就設好建置指令、把輸出指向 dist/。這些設定在 Pages 儀表板的 Settings > Builds,或寫在 wrangler.toml 裡。

    text
    # 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
  7. 兩種部署方式

    直接部署(direct deploy)會立刻把資料夾上傳。Git 連動才是正式做法:在儀表板把 repo 連一次、設好建置指令與輸出資料夾,之後每次 push 到正式分支,都會自動建置並部署。

    bash
    # 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
school

重點概念

最常見的疑問:到底該用 Pages Functions,還是獨立的 Worker?下面這張圖給你一個快速判斷準則。

schemaPages Functions 與獨立 Worker 怎麼選

要:前端 + API

不用:只要 API

也要同時架網站嗎?

用 Pages + Functions

用獨立的 Worker

一個 repo、一次部署

純後端服務

compare_arrows

Pages 與 Workers 的差別

Pages 本質是「前端網站托管」,再外掛 Functions 當後端;Worker 則是「純程式碼」、本身不附帶靜態托管。其實 Pages Functions 底層就是跑在 Workers 上的。

route

檔案即路由

functions/api/hello.js 對應 /api/hello。[id].js 只接一段路徑(例如 /users/42);[[route]].js 是 catch-all,後面接幾層都吃得下。

api

onRequest 處理器

匯出 onRequest 處理所有方法;或用 onRequestGet/onRequestPost/onRequestPut/onRequestDelete 各自對應一個 HTTP 動詞。

deployed_code

context 物件

每個處理器都會拿到 context,裡面有 env(綁定)、params(動態路由的值)、request、next(中介層 middleware)和 waitUntil(背景工作)。

vpn_key

用綁定,不用密碼

綁定讓 Function 透過 env.NAME 直接、已驗證地存取 D1/KV/R2。程式碼裡不會出現連線字串或密碼。

layers

用 _middleware.js 做中介層

資料夾裡的 _middleware.js 會在該層路由之前先跑——很適合做驗證或記錄。呼叫 context.next() 就會繼續往下交給命中的 Function。

tips_and_updates

小提示、陷阱與計費

report

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。
bolt

先靜態,再長成全端

你今天可以先上一個純靜態網站,之後想加第一個後端,只要新增 functions/api/hello.js 就好。其他什麼都不用改——同一個專案、同一條部署。