Skip to content

Cloudflare R2 物件儲存入門:零 egress 費用,工具站的檔案怎麼放?

Cloudflare R2 物件儲存入門:零 egress 費用,工具站的檔案怎麼放?

最近在維護自己那一系列小工具站(像是郵遞區號查詢、QRCode 產生器這些),常常遇到一個很現實的問題:「這個檔案到底要放哪裡?」

我的工具站幾乎都是 vite-ssg 預渲染後丟到 Cloudflare Workers 的 Static Assets 上跑,純靜態、超便宜。但有些檔案它就是不太適合塞進 dist/:使用者上傳的圖片、社群分享用的 OG 圖、或是像郵遞區號原始資料那種好幾 MB 的大檔。這時候就輪到今天的主角登場——Cloudflare R2

這篇我會用聊天的方式帶你認識 R2 是什麼、它最殺的「零 egress 費用」到底香在哪,然後手把手用 wrangler 建一個 bucket、從 Worker 讀寫它。最後再聊一個我自己一開始也搞混的問題:R2 跟 Static Assets 到底差在哪、什麼時候該用誰。

R2 是什麼?一句話:Cloudflare 版的 S3

如果你聽過 AWS S3,那 R2 你三秒就懂了——它就是 Cloudflare 提供的**物件儲存(object storage)**服務。

「物件儲存」聽起來很玄,但你可以把它想成一個放在雲端、可以無限長大的資料夾。你往裡面丟一個個「物件」(其實就是檔案 + 一點 metadata),每個物件都有一把專屬的 key(就是它的路徑名,例如 og/zipcode-100.png),之後用這把 key 就能把它撈回來。它不像資料庫要設 schema,也不像硬碟有容量上限,特別適合放圖片、影片、備份檔、大型資料集這種非結構化的東西。

R2 最貼心的一點是:它的 API 跟 S3 相容。所以你現有那一堆 aws-sdk 的程式碼、或是任何支援 S3 的工具(像 rclone),幾乎都能直接接上 R2,只要換個 endpoint 跟金鑰就好。對轉職者來說這超友善——你在別的地方學的 S3 知識完全沒白費。

真正的殺手鐧:零 egress 費用

來講重點。S3 那套儲存服務,最讓人心痛的不是存放費,而是 egress(流量出口)費用——也就是「資料被讀出去」要收錢。你的圖片越紅、越多人載,帳單就越可怕,像是你開店生意越好、房東抽成抽越兇。

R2 直接把這條砍掉:egress 完全免費。你只付「存放容量」跟「操作次數(Class A 寫入 / Class B 讀取)」的錢。而且還有一層佛心的免費額度:每月 10 GB 儲存、百萬次 Class A、千萬次 Class B 操作免費。

對我這種做工具站、又不想被流量帳單嚇到的人來說,這個模式根本是量身打造。一張 OG 圖被幾十萬人預覽?流量費 0 元。郵遞區號的大檔被爬蟲狂抓?還是 0 元。我終於可以放心把檔案開出去,不用半夜醒來檢查帳單。

用 wrangler 建一個 bucket

實作開始。先確認你裝了 wrangler(Cloudflare 的官方 CLI),然後建一個 bucket:

bash
# 登入(第一次才需要)
npx wrangler login

# 建立一個叫 my-tool-assets 的 bucket
npx wrangler r2 bucket create my-tool-assets

想丟個檔案上去測試、或把現有的清單列出來,也都有對應指令:

bash
# 上傳本機檔案到 bucket 的某個 key
npx wrangler r2 object put my-tool-assets/og/zipcode-100.png \
  --file ./assets/og/zipcode-100.png

# 列出帳號下所有 bucket
npx wrangler r2 bucket list

從 Worker 綁定讀寫(重點來了)

R2 真正的威力,是它能跟 Worker **綁定(binding)**在一起,這樣你在 Worker 程式裡就能直接讀寫,完全不用管金鑰、不用發 HTTP 請求繞一圈。

先在 wrangler.jsonc 裡加上綁定。以我的工具站來說,平常我的 config 大概長這樣(純靜態 assets,沒有 Worker 進入點):

jsonc
{
  "name": "taiwan-zipcode",
  "compatibility_date": "2025-05-30",
  "assets": {
    "directory": "./dist",
    "not_found_handling": "404-page"
  }
}

要加上 R2,就補一個 main(Worker 進入點)跟 r2_buckets

jsonc
{
  "name": "taiwan-zipcode",
  "main": "./src/worker.ts",
  "compatibility_date": "2025-05-30",
  "assets": {
    "directory": "./dist",
    "binding": "ASSETS",
    "not_found_handling": "404-page"
  },
  "r2_buckets": [
    {
      "binding": "BUCKET",
      "bucket_name": "my-tool-assets"
    }
  ]
}

這個 binding: "BUCKET" 就是你在程式裡會用到的變數名。接著在 Worker 裡,env.BUCKET 就是那顆 bucket 的操控桿:

ts
interface Env {
  BUCKET: R2Bucket;
  ASSETS: Fetcher;
}

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

    // 範例一:讀取——把 /files/ 底下的請求導去 R2
    if (url.pathname.startsWith("/files/")) {
      const key = url.pathname.slice("/files/".length);
      const object = await env.BUCKET.get(key);

      if (object === null) {
        return new Response("Not Found", { status: 404 });
      }

      const headers = new Headers();
      object.writeHttpMetadata(headers); // 自動帶上 content-type 等
      headers.set("etag", object.httpEtag);
      return new Response(object.body, { headers });
    }

    // 範例二:寫入——使用者用 PUT 上傳檔案
    if (request.method === "PUT" && url.pathname.startsWith("/upload/")) {
      const key = url.pathname.slice("/upload/".length);
      await env.BUCKET.put(key, request.body);
      return new Response(`已存到 ${key}`, { status: 201 });
    }

    // 其他路徑交回給靜態網站
    return env.ASSETS.fetch(request);
  },
};

你看,getput 就這麼直覺。沒有 access key、沒有 secret,綁定本身就是授權。本機開發時跑 npx wrangler dev,它還會給你一個本地模擬的 R2,不用連線到雲端也能測。

公開存取與自訂網域

預設情況下 bucket 是私有的——這很合理,你不會希望使用者上傳的東西被誰都能亂翻。如果某個 bucket 真的需要對外公開(例如純放 OG 圖),有兩條路:

  1. r2.dev 子網域:在 Cloudflare 後台一鍵開啟,會給你一個 https://pub-xxxx.r2.dev/... 的網址,適合測試。
  2. 自訂網域(推薦):把 bucket 接到自己 zone 底下的網域,例如 assets.pocketool.app。這樣不只網址漂亮,還能吃到 Cloudflare 的 CDN 快取與 cache 規則,速度更快。

這跟我在 QRCode 工具站綁自訂網域是同一套思路。在我的 wrangler.jsonc 裡,Worker 路由就是這樣綁網域的:

jsonc
"routes": [
  { "pattern": "assets.pocketool.app", "custom_domain": true }
]

只要 zone 在同一個 Cloudflare 帳號下,custom_domain: true 會自動幫你建好 DNS 記錄跟憑證,省事很多。

R2 vs. Workers Static Assets:誰負責什麼?

最後回答開頭那個讓我一開始很混亂的問題。兩個都能「放檔案、發給使用者」,但定位完全不同:

  • Static Assets 放的是「建置時就確定」的東西——你的 HTML、CSS、JS、預渲染頁面。它跟你的程式碼一起部署、一起進版控,每次 wrangler deploy 整包上去。我那些 vite-ssg 站的 dist/ 就屬於這類。
  • R2 放的是「執行期才產生或變動」的東西——使用者上傳的圖、跑批次生成的 OG 圖、那份好幾 MB、不該塞進 git 也不該每次部署都重傳的郵遞區號資料檔。

一個簡單的判斷法:這檔案會跟著我的程式碼一起 commit 嗎? 會,就放 Static Assets;不會、或它太大、或它是使用者給的,就放 R2。

兩者還能漂亮地搭配——像上面的 Worker 範例,靜態頁面走 env.ASSETS,動態檔案走 env.BUCKET,同一個 Worker 各司其職,互不打架。

好了,今天就先聊到這。R2 對做工具站、又怕流量帳單的我們來說真的是好朋友,下次有檔案不知道往哪塞的時候,不妨想想它。我們下篇見!