兩個案例
我手上有兩個 side project 同時跑在 Cloudflare 上:
- you_are_right — Nuxt 4 + Drizzle,部署成 Cloudflare Pages。
- emdash-test(就是這個站)— Astro 6,部署成 Cloudflare Workers。
兩者業務差很多(前者 quiz app、後者 blog),這篇刻意忽略產品差異,只談三層架構:runtime(Workers vs Pages)、framework(Nuxt vs Astro)、平台介接(adapter / bindings 怎麼接到 Cloudflare)。
Layer 1: Workers vs Pages
最底層的差異是 Cloudflare 的兩個部署型態。逐項對照:
設定檔
- Workers:
wrangler.jsonc,含main入口(自己指定 worker 程式碼路徑) - Pages:
wrangler.toml,靠pages_build_output_dir+ Functions 目錄慣例
部署方式
- Workers:
wrangler deploy或 GitHub integration - Pages:Git push 自動 build,Cloudflare 端跑
新 binding 支援
- Workers:全部優先支援(Worker Loader、Containers、Hyperdrive…)
- Pages:慢半拍,部分新 binding 不會回填
靜態資產
- Workers:Workers Static Assets(2024 才加入)
- Pages:原生支援,最早期就有
心智模型
- Workers:純 Worker,static 是附加
- Pages:以靜態為主,function 是附加
emdash-test 這邊用了 worker_loaders binding,這是 Workers-only 的東西。如果要選 Pages,這個 binding 就拿不到。
youareright 沒用到任何 Workers-only binding,Pages 也夠用。
趨勢面:Cloudflare 自家把長期賭注押在 Workers,Pages 還在維護但新功能優先給 Workers。新專案沒特別理由就走 Workers。
Layer 2: Nuxt vs Astro
往上一層是 framework 哲學。這層決定你寫頁面的時候腦袋怎麼運作。
Nuxt 是 Vue 全家桶 — server route、middleware、composables、auto-import 一條龍。/server/api/foo.get.ts 寫一支 endpoint,前端 useFetch('/api/foo') 就有 RPC 可以用,型別跨進跨出都是 Vue 上下文一致。預設 hybrid SSR,靠 routeRules 在 config 裡集中宣告每條路由的渲染策略(SSR / ISG / SWR / 純 client)。
Astro 是 island architecture — 預設整頁 SSR HTML,互動的部分才宣告 client:load / client:visible 把 React/Vue/Svelte 元件 hydrate 成 island。同一頁可以混不同 framework 的 island。Output 模式是 page-level(output: 'server' 整站走 SSR、或單頁標 prerender),渲染策略寫在頁面檔本身、不集中在 config。
架構含意差在哪:
- Nuxt 假設你整站都需要 reactive 前後端整合,預設給最豐富的執行期
- Astro 假設你大部分頁面是 server HTML,互動是「插件」,所以 JS payload 預設極小
- Nuxt 的
routeRules跟 Astro 的 page-level output,是同一個問題(per-route 渲染策略)的兩種風格 — 集中式 vs 分散式
Layer 3: Adapter 跟 Bindings 怎麼接到 Cloudflare
這層是 framework 怎麼把自己塞進 Cloudflare runtime,最容易被忽略但客製化時就會碰到。
入口 / build 產物
Astro + `@astrojs/cloudflare`
astro.config.mjs寫adapter: cloudflare()- 入口檔案
src/worker.ts是專案內手控的,可以加自訂 fetch handler 邏輯(攔請求、改 response、接 cache provider…) - Build 產物是一個 Worker bundle
Nuxt + `nitro.preset = 'cloudflare-pages'`
nuxt.config.ts設 preset 就完事- 入口
_worker.js是 Nitro 自動生成、進dist/,不是專案內可編輯的檔案 - 要客製化 fetch 行為要走 Nitro plugin / hook,多一層抽象
Bindings 怎麼進來
兩邊都拿得到 D1 / KV / R2,但訪問路徑不同:
- Astro:
Astro.locals.runtime.env.DB/Astro.locals.runtime.env.MEDIA - Nuxt:
event.context.cloudflare.env.DB(在 server route handler 裡)
Env var 在 Astro 跟 bindings 同 namespace(都掛在 runtime.env);Nuxt 把 runtime config 抽成自己的 runtimeConfig 從 env 注入,跟 bindings 是分開兩條路。
Cache 怎麼接到邊緣
這個是我自己在 emdash-test 上踩過、印象最深的差別:
- Astro:要靠 experimental cache provider 自己接
caches.default(Edge Cache API)。我寫了src/cache-provider.ts把 Astro 的cacheHint翻譯成 Cache API put/match。完全可控,但要自己搭。 - Nuxt:
routeRules.headers設Cache-Control,靠 CDN 自動讀 header 觸發 Cloudflare cache。比較宣告式,但要更精細的快取邏輯(例如 cache key 根據 cookie 變、SWR 期間自訂、tag-based invalidation)就要回到 Nitro 層硬幹。
一句話收尾
- Astro:runtime 入口跟 fetch 行為對你開放,自由但要自己寫
- Nuxt:Nitro 幫你包好,舒服但客製要繞一圈
結論:架構選擇取決於你想 own 哪一層
從架構視角:
- 想對 runtime 入口、cache、binding 細節有完全控制 → Astro + Workers(你會直接寫到
worker.ts) - 想要 framework 幫你處理大半平台細節、寫業務邏輯為主 → Nuxt + Pages(Nitro 的抽象就是它的價值)
- 純內容、HTML-first、JS payload 越小越好 → Astro
- 整站 reactive、前後端緊耦合的 app → Nuxt
兩邊的 D1 / KV / R2 接得一樣好,差別不在 binding 能力,是在你願意把多少抽象託付給 framework。
Cloudflare 一片大平台、底層都共用,但 framework 跟 adapter 把同樣的能力包成完全不同的開發體驗。
No comments yet