Workers Static Assets 靜態託管實作 | Cloudflare 完整教學

2026/09/13
Workers Static Assets 靜態託管實作 | Cloudflare 完整教學

這一篇要帶你動手實作 Workers Static Assets 的靜態託管。上一篇我們攤開了「Pages 還是 Workers」的選型地圖,這一篇就把 Workers Static Assets 拆開來,一行一行講清楚 wrangler.jsoncassets 的每個選項:directorybindinghtml_handlingnot_found_handlingrun_worker_first,並實作純靜態站、SPA fallback、靜態與 Worker API 共存,以及 env.ASSETS.fetch 的用法,順帶說清楚為什麼靜態請求免費、快取怎麼運作。

前言

上一篇《Pages 與 Static Assets 總覽》,我們把 Cloudflare 前端與全端託管的地圖攤開來看:Cloudflare Pages 進入維護模式、Workers Static Assets 成為官方主推的新選擇,並用最小設定對照了兩者的差別。那一篇是「選哪一台機器」;這一篇,我們要真正把機器打開來操作。

先給一句話定義:

Workers Static Assets 是 Cloudflare Worker 的一項能力,讓你在 wrangler.jsonc 裡透過 assets 設定把一整個目錄的靜態檔案(HTML、CSS、JS、圖片)上傳到 Cloudflare 的資源儲存系統,由全球邊緣節點自動快取、壓縮、附上 ETag 後直接提供;需要後端時,再補上一個 Worker,透過 env.ASSETS 綁定與這些靜態資源共存。

打個比方會更好懂。你可以把 Workers Static Assets 想成一間「有前台自助區 + 後台廚房」的餐廳。前台自助區(靜態資源)擺滿了做好的餐點(HTML、CSS、JS、圖片),客人自己拿、拿了就走,不用麻煩廚師——這就是「靜態請求免費、不進 Worker」。後台廚房(Worker 程式碼)只在客人「點需要現做的菜」(打 /api 這種動態請求)時才啟動。而 run_worker_first 就是門口那位服務生的規則:哪些請求直接放行去自助區、哪些要先送進廚房。設對了,廚房只做該做的事;設錯了(例如 run_worker_first: true),連拿一瓶水都要進廚房排隊——又慢又貴。

讀完這篇你會掌握:

  • assets 的五個核心選項——directorybindinghtml_handlingnot_found_handlingrun_worker_first 各自管什麼。
  • 三種實作型態——純靜態站、SPA fallback、靜態 + Worker API 共存,以及各自的最小設定。
  • env.ASSETS.fetch 的用法——什麼時候一定要手動呼叫、它有什麼限制。
  • 成本與快取——為什麼靜態請求免費、Cloudflare 自動幫你做了哪些快取與壓縮。

核心概念

一、請求怎麼走:靜態優先 vs Worker 優先

理解 Workers Static Assets,關鍵是先看懂「一個請求進來後怎麼被路由」。整體流程如下:

Request 進入
     ↓
是否符合 run_worker_first 設定?
├─ 是 → 執行 Worker(你的 fetch)→ 可呼叫 env.ASSETS.fetch() 取靜態資源
└─ 否 → 先查找靜態資源
         ├─ 找到 → 直接回傳靜態檔案(自動壓縮、快取,不計 Worker 調用 → 免費)
         └─ 找不到 → 依 not_found_handling 處理
                     ├─ single-page-application → 回 /index.html(200)
                     ├─ 404-page → 回 /404.html(若存在,404)
                     └─ none → 若有 Worker 則進 Worker,否則直接 404

這裡有兩條分岔要記牢:

  1. 預設是「靜態優先」。沒被 run_worker_first 命中的請求,Cloudflare 會先嘗試靜態資源;找到就直接回,你的 Worker 根本不會被觸發——這就是靜態請求免費的來源(不算一次 Worker 調用)。
  2. run_worker_first 命中的路由是「Worker 優先」。這些請求會先進你的 fetch,由你決定回什麼;需要靜態資源時,再由你手動呼叫 env.ASSETS.fetch()

換個角度理解這個設計:Cloudflare 讓「大多數請求(靜態檔案)走最短、最便宜的路徑」,只有你明確指定要動態處理的路由才付出 Worker 執行的代價。這跟傳統伺服器「每個請求都進應用程式再判斷」的模型剛好相反,也是它成本效率高的根本原因。所以設計 run_worker_first 時的心法是:清單越短越好,只列真正需要後端的路徑,其餘全部留給「靜態優先」的免費快車道。

另外要注意 run_worker_first 的 glob 語法:* 比對單層任意字元、** 比對跨層路徑,而 ! 前綴是「排除」且優先於正向規則。例如 ["/admin/*", "!/admin/assets/*"] 的意思是「/admin 底下都先進 Worker,但 /admin/assets/ 的靜態資源例外、走免費快車道」——這在後台頁面需要動態驗證、但其 CSS/JS 想省成本時很實用。

二、assets 的五個核心選項

所有設定都寫在 wrangler.jsoncassets 物件裡。逐一拆解:

選項型別作用預設
directorystring必填。靜態資源目錄,指向你的建置輸出(如 ./dist)
bindingstringWorker 中存取資源用的綁定名稱(env.ASSETS)"ASSETS"
html_handlingstringHTML 頁面的 URL 尾部斜線行為"auto-trailing-slash"
not_found_handlingstring找不到靜態資源時的處理模式"none"
run_worker_firstboolean | string[]哪些路由「先進 Worker」false

其中 directory 是唯一必填。純靜態站只需要它;要接後端才需要 binding(其實有預設值,通常不用寫)、run_worker_first,以及一個 main 入口。

值得特別說明的是 binding:它決定你在 Worker 程式碼裡用什麼名字存取靜態資源。預設是 "ASSETS",所以你寫 env.ASSETS.fetch(...);如果你把 binding 改成 "STATIC",那就得寫 env.STATIC.fetch(...)。多數情況維持預設即可,除非你的環境變數命名有衝突。另外提醒:純靜態站(沒有 main)時,binding 寫不寫都無所謂,因為根本沒有 Worker 程式碼會用到它。

三、not_found_handling:三種模式

not_found_handling 決定「找不到對應靜態檔案」時怎麼辦,是靜態託管最常設錯的一個:

模式行為適用場景
"single-page-application"所有非資源路徑回 /index.html(HTTP 200)React / Vue / Angular 等 SPA
"404-page"若有 /404.html 就回它(HTTP 404),否則 404SSG 靜態站(Hugo、Astro static)
"none"直接回 404(有 Worker 則交給 Worker)API 優先、完全自訂路由

SPA 一定要用 single-page-application,否則使用者重新整理 /dashboard 這種前端路由(client-side routing)子頁面時會 404;每頁都預先產好 HTML 的靜態站則用 404-page

四、html_handling:URL 斜線行為

html_handling 控制 HTML 頁面的尾部斜線如何處理,影響 URL 外觀與 SEO:

模式說明
"auto-trailing-slash"預設。依檔案結構自動決定(/aboutabout/index.html 就導向 /about/)
"force-trailing-slash"一律加上尾部斜線
"drop-trailing-slash"一律去掉尾部斜線(乾淨 URL)
"none"完全不改寫,交給你自己控制

大多數情況用預設即可,不必特別設定。

實作範例

理論看完,我們用三種由簡到繁的型態,把設定與程式碼實際寫出來。

1. 純靜態站:零 Worker 程式碼

最單純的起手式——一個沒有後端的靜態網站。連 Worker 程式碼、main 入口都不用寫:

// wrangler.jsonc — 純靜態站,只需要 directory
{
  "name": "my-static-site",
  "compatibility_date": "2026-01-01",
  "assets": {
    "directory": "./dist"   // 指向建置輸出目錄即可
  }
}
# 建置前端(以 Vite 為例),再部署
npm run build
npx wrangler deploy
# ↑ 上線後所有請求由邊緣直接提供靜態檔案,不計入 Worker 調用次數(免費)

就這樣。沒有 main、沒有 fetch,Cloudflare 自動幫你壓縮(brotli / gzip)、快取、附上 ETag。這時候根本沒有 Worker 在跑,所有靜態請求都免費。

2. SPA fallback:讓前端路由不再 404

前端框架(React、Vue、Angular)的建置輸出是單頁應用(Single Page Application,SPA):只有一個 index.html,所有路由由前端 JS 接手。這時你必須設 not_found_handling,否則使用者一重新整理子頁面就會 404:

// wrangler.jsonc — 純 SPA(仍無後端)
{
  "name": "my-spa",
  "compatibility_date": "2026-01-01",
  "assets": {
    "directory": "./dist",
    "not_found_handling": "single-page-application"  // 找不到就回 index.html(200)
  }
}

有了這一行,使用者直接開 /dashboard/users/42 這種前端路由,伺服器上雖然沒有對應檔案,但會回傳 index.html(HTTP 200),瀏覽器載入 JS 後由前端路由渲染正確畫面。這一步是 SPA 部署最容易漏掉、也最常見的坑。

3. 靜態 + Worker API 共存(最常見的全端模式)

現在加上一個 /api 後端。只要補上 main 入口與 run_worker_first,同一個專案就升級成全端應用,不用換平台:

// wrangler.jsonc — SPA + API 全端設定
{
  "name": "my-app",
  "main": "src/index.ts",             // 補上 Worker 入口
  "compatibility_date": "2026-01-01",
  "compatibility_flags": ["nodejs_compat"],
  "assets": {
    "directory": "./dist",
    "binding": "ASSETS",                              // 可省略(預設就是 ASSETS)
    "not_found_handling": "single-page-application",  // SPA fallback
    "run_worker_first": ["/api/*"]                    // 只有 /api/* 先進 Worker
  },
  "d1_databases": [
    { "binding": "DB", "database_id": "your-db-id" }
  ]
}

對應的 Worker 入口。注意「API 自己處理、其餘全部交還給 env.ASSETS」這個模式:

// src/index.ts — API 由 Worker 處理,其餘 fallback 給靜態資源
interface Env {
  ASSETS: Fetcher;   // 由 assets.binding 提供
  DB: D1Database;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // 只有 /api/* 會進到這裡(因為 run_worker_first: ["/api/*"])
    if (url.pathname.startsWith("/api/")) {
      const { results } = await env.DB.prepare("SELECT * FROM users").all();
      return Response.json(results);
    }

    // 其餘請求交還靜態資源系統
    // SPA 模式下,找不到檔案時這一步會回傳 index.html
    return env.ASSETS.fetch(request);
  },
};

這裡有兩個關鍵細節:

  • run_worker_first: ["/api/*"] 讓「只有 /api/* 的請求先進 Worker」,其餘靜態檔案(HTML、CSS、JS、圖片)請求直接由邊緣提供、不進 Worker、不計費。這對成本控制至關重要。
  • return env.ASSETS.fetch(request) 是必要的 fallback。進到 fetch 後,凡是不屬於你 API 邏輯的請求,都要交還給 env.ASSETS,否則靜態檔案不會自動出現。

4. env.ASSETS.fetch:用法與限制

env.ASSETS 實作了標準的 Fetcher 介面,你可以用三種方式呼叫:

// 一、直接轉發原始請求(最常見)
return env.ASSETS.fetch(request);

// 二、指定路徑(hostname 會被忽略,只看 pathname)
return env.ASSETS.fetch(new URL("/index.html", request.url));

// 三、明確指定某個靜態檔案當作回退(例如 SSR 找不到資料時)
return env.ASSETS.fetch(new URL("/404.html", request.url));

實際的 SSR 範例——動態頁面由 Worker 渲染,找不到資料時回退到靜態 404 頁:

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // 動態商品頁,由 Worker 即時渲染
    if (url.pathname.startsWith("/products/")) {
      const slug = url.pathname.replace("/products/", "");
      const product = await env.DB
        .prepare("SELECT * FROM products WHERE slug = ?")
        .bind(slug)
        .first();

      // 找不到商品 → 回退到靜態 404 頁面
      if (!product) {
        return env.ASSETS.fetch(new URL("/404.html", request.url));
      }

      return new Response(renderProductPage(product), {
        headers: {
          "Content-Type": "text/html; charset=utf-8",
          "Cache-Control": "public, max-age=60, stale-while-revalidate=3600",
        },
      });
    }

    // 其餘靜態資源
    return env.ASSETS.fetch(request);
  },
};

env.ASSETS.fetch 的重要限制:

  • 只支援 GETHEAD,其他 HTTP 方法會回 405。因此表單提交、資料寫入這類 POST / PUT / DELETE 都得由你自己的 Worker 邏輯處理,不能丟給 env.ASSETS
  • 回應會自動附上 Content-Type(依副檔名推斷)、ETag(內容雜湊),客戶端支援時還會 Content-Encoding: br(brotli 壓縮)。
  • 傳入路徑時 hostname 會被忽略,只看 pathname——所以 env.ASSETS.fetch("https://any/index.html")env.ASSETS.fetch(new URL("/index.html", request.url)) 效果相同,重點是路徑對不對。

5. .assetsignore:排除不需上傳的檔案

assets.directory 目錄放一個 .assetsignore(語法同 .gitignore),排除不該上傳的檔案:

# .assetsignore
_worker.js
*.map
*.md
.DS_Store
node_modules/

為什麼要排除?一來省儲存與上傳時間,二來避免把不該公開的檔案(例如 source map、Markdown 原稿)暴露在網路上。要特別注意的是,.assetsignore 只影響「上傳」,不影響請求路由——被排除的檔案根本不存在於資源系統,請求它就會走 not_found_handling 流程。

6. 免費靜態請求與快取:平台幫你做了什麼

「靜態請求免費」是 Workers Static Assets 相對於「什麼都塞進 Worker」的最大成本優勢,值得單獨講清楚。

先看計費模型。Cloudflare 的 Worker 計費以「調用次數(invocation)」為單位,免費方案每天 10 萬次、付費方案每月 1000 萬次。只有真正執行到你 Worker 程式碼的請求才算一次調用;由邊緣直接提供靜態檔案、沒有進 Worker 的請求則完全不計費。這代表:

  • 一個頁面載入 50 個圖片、CSS、JS 檔——只要它們沒被 run_worker_first 命中,這 50 個請求全部免費
  • 只有打到 /api/*(被 run_worker_first 指定)、真正跑進你 fetch 的請求才計入調用次數。

這就是為什麼「把 run_worker_first 設成 true」是災難級的錯誤:它讓每一個靜態請求都變成一次計費調用。

再看快取與壓縮。這些 Cloudflare 全自動處理,你什麼都不用寫:

  • 邊緣快取——靜態資源快取在全球 300+ 個資料中心,使用者從最近的節點取得。
  • 內容壓縮——客戶端支援時自動用 brotli 或 gzip,不必自己壓。
  • ETag——每個資源附上內容雜湊,瀏覽器可用 If-None-Match 做條件式請求,內容沒變就回 304、省流量。

若你需要覆寫預設的快取策略(例如替 content-hash 檔名的資源設超長快取),可以在 Worker 回應裡自訂 Cache-Control:

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const res = await env.ASSETS.fetch(request);

    // 帶內容雜湊的檔名(如 app.a3b2c1d4.js)→ 長期不可變快取
    if (/\.[a-f0-9]{8,}\.(js|css|woff2|png|webp)$/.test(url.pathname)) {
      const r = new Response(res.body, res);
      r.headers.set("Cache-Control", "public, max-age=31536000, immutable");
      return r;
    }
    return res;
  },
};

注意:要在 Worker 裡覆寫 header,就得讓請求進 Worker,這會產生調用計費——所以只在真的需要細緻快取控制時才這麼做,否則交給平台預設即可。

常見錯誤與最佳實踐

靜態託管的坑,大多不在「部署失敗」,而在「路由設定漏一行」或「白白燒掉成本」。以下是最高頻的幾個。

坑一:SPA 沒設 not_found_handling,子頁面一重新整理就 404。

這是新手部署 React / Vue 專案最常見的問題:首頁正常,但直接開 /dashboard 或在子頁面按重新整理就 404。原因是伺服器上沒有 dashboard.html 這個檔案。正確做法:SPA 一律設 not_found_handling: "single-page-application",讓找不到的路徑都回 index.html(200),交由前端路由接手。

坑二:run_worker_first: true 燒光調用額度。

為了省事把 run_worker_first 設成布林 true,結果連載入圖片、CSS、JS 都先進 Worker,每個靜態請求都算一次 Worker 調用,額度飛快耗盡、延遲也上升。正確做法:用陣列模式,只讓真正需要後端的路由先進 Worker,例如 ["/api/*", "/auth/*"],其餘靜態資源維持「免費直送」。需要在某個前綴下排除子路徑時,用 ! 前綴,例如 "!/admin/assets/*"

坑三:進了 fetch 卻忘了 return env.ASSETS.fetch(request)

一旦某路由被 run_worker_first 命中進了 Worker,靜態資源就不會自動出現——你必須在 fetch 結尾明確 return env.ASSETS.fetch(request) 當作 fallback。忘了這行,那些請求就會掉進你 API 邏輯的「查無此路由」,回傳錯誤或空白。正確做法:fetch 的最後一行永遠是把非 API 請求交還 env.ASSETS

坑四:用 env.ASSETS.fetch 處理 POST,結果 405。

有人想把表單 POST 也丟給 env.ASSETS,結果回 405——因為它只接受 GET / HEAD正確做法:所有需要寫入、需要動態處理的請求(POST / PUT / DELETE)都應由你的 Worker 邏輯處理,只有讀取靜態檔案才走 env.ASSETS

最佳實踐小結:

  • 純靜態站 → 只設 assets.directory,零 Worker 程式碼,享受全部免費靜態請求。
  • SPA → 一律加 not_found_handling: "single-page-application"
  • 全端 → 補 main + run_worker_first: ["/api/*"](陣列模式),fetch 結尾 fallback 給 env.ASSETS
  • 快取交給平台 → 靜態資源的壓縮、ETag、邊緣快取 Cloudflare 自動處理;需要自訂快取策略時,再在 Worker 回應裡覆寫 Cache-Control

小結

上一篇《Pages 與 Static Assets 總覽》,我們把「Pages 還是 Workers」的選型地圖攤開來看,建立了整體視角。這一篇《Workers Static Assets 靜態託管》,我們把主角拆開來實作:

  • assets 五選項——directory(必填,指向建置輸出)、binding(預設 ASSETS)、html_handling(URL 斜線)、not_found_handling(找不到怎麼辦)、run_worker_first(哪些路由先進 Worker)。
  • 三種型態——純靜態站(零程式碼)、SPA fallback(single-page-application)、靜態 + API 共存(main + run_worker_first + env.ASSETS fallback)。
  • env.ASSETS.fetch——進了 Worker 就要手動呼叫它交還靜態資源;只支援 GET / HEAD,自動附上壓縮與 ETag。
  • 成本與快取——沒進 Worker 的靜態請求免費,壓縮、快取、ETag 由平台自動處理。

掌握了單一 Worker 的靜態託管,下一步就是把它接上真實框架。下一篇《框架整合:Next.js、Astro、SvelteKit 部署到 Workers》,我們會把今天學到的 assets 設定,套用到主流框架的官方適配器上——看 @opennextjs/cloudflare@astrojs/cloudflare@sveltejs/adapter-cloudflare 如何自動幫你產生 assets.directorymain,讓你用熟悉的框架語法,享受 Workers Static Assets 的全部好處。我們下一篇見。

想查閱 Cloudflare 官方對 assets 各設定欄位的完整說明與最新選項,可以參考 Cloudflare Workers Static Assets 官方文件(設定欄位與官方建議以當下官方文件為準)。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →