做獨立工作室最容易踩的雷,就是在技術選擇上花太多時間討論「用什麼才對」,最後一行程式碼都沒寫。我花了一段時間實際把工作室官網、14 款瀏覽器遊戲、後端 API 全部上線,累積下來的架構非常簡單,全部加起來月固定成本不到台幣 200 元,一個人週末就能 debug 完。這篇把全站技術堆疊完整攤開,當成給未來想做類似規模專案的朋友(以及半年後忘記自己用什麼的我)的一份備忘錄。
為什麼要寫這篇
坊間的「獨立工作室技術堆疊」文章通常不是太抽象(「選你熟的就好」),就是太 enterprise(「先導入 Kubernetes、Terraform、OpenTelemetry」)。我想寫一篇夾在中間的 — 具體到 每個組件選什麼、為什麼選它、實際跑起來花多少錢、一個人怎麼維護。這份架構撐起了:
- 主站
hao0321.com(工作室作品集、關於頁、部落格) - 遊戲大廳
game.hao0321.com(14 款原創小遊戲) - 後端 API
api.hao0321.com(帳號、排行榜、簽到、成就) - 幾個獨立工具(Social Analytics、動畫 Pipeline 展示頁)
全站架構地圖
三個獨立但互相串接的層:
[Browser]
↓
[ Cloudflare CDN / Pages ] ← 靜態內容(HTML / JSX / 圖片)
↓
[ Cloudflare Workers ] ← API 端點(JS 邊緣運算)
↓
[ Cloudflare D1 ] ← SQLite on the edge
沒有 Kubernetes、沒有 Docker、沒有自己架的 VPS、沒有 MongoDB 或 PostgreSQL。每個請求的生命週期大概是:使用者在瀏覽器開啟頁面 → Cloudflare Pages 從邊緣節點吐靜態檔 → JS 呼叫 Workers API → Worker 讀寫 D1 → 一路 SSL 終端都在 Cloudflare 做掉。在台北,API 回應時間大約 50~90ms。
前端(Frontend)
主站:純 HTML + Vanilla JS
主站 hao0321.com/index.html 是一個 1600 行的單檔 HTML。沒有 React、沒有 build step、沒有 bundler。理由很樸素:
- 作品集不需要狀態管理。滾動觸發動畫用 IntersectionObserver 就夠,粒子星空用 Canvas 原生 API 就能做。
- Zero build = zero debt。三年後打開這個檔案還是能直接跑,不會因為某個 webpack 版本或 Node.js LTS 結束而爆炸。
- 首頁 Lighthouse 分數 95+。沒有 bundle 意味著 JS payload 極小,初次載入速度是 SPA 做不到的。
缺點當然有:CSS 和 JS 都寫在同一個檔案裡、重複結構要手動複製、跨頁共用邏輯得手動同步。但站的規模是 6 頁,這些成本可以忍。
遊戲大廳與遊戲本體:React via Babel(在瀏覽器裡)
每款遊戲是一個 .jsx 檔案,由 play.html 動態 fetch 後用 Babel Standalone 即時轉譯再 eval。完整流程:
fetch('cat-battle.jsx')
.then(r => r.text())
.then(raw => {
const { code, defaultExport } = stripModuleSyntax(raw);
const transformed = Babel.transform(code, { presets: ['react'] }).code;
eval(finalCode);
});
這個做法看起來很粗暴(「Babel 在瀏覽器裡跑 = 慢」),但實際延遲可以接受:React + Babel 加起來約 200KB gzipped,第一次載入花 300ms,切換遊戲時的 Babel 轉譯大約 50~150ms。換取的好處是:
- 不需要任何 build pipeline。改
.jsx→ 直接 git push → Cloudflare Pages 自動部署 → 完成。 - 每款遊戲獨立。要加一款遊戲,只要新增一個
.jsx檔再登記到play.html的GAMES物件即可,不用重新 build 整站。 - 新進貢獻者零門檻。任何會 React 的朋友打開檔案就能看懂,不用先搞懂 Vite 或 esbuild 設定。
權衡:production 流量大時這不是最佳解,應該 pre-compile。但當前工作室規模,這個成本可以吸收。
字型與視覺一致性
全站用 Plus Jakarta Sans(標題、英文)、Noto Sans TC(中文)、Playfair Display(特殊強調)、JetBrains Mono(等寬)。色彩系統統一由 CSS custom properties 管理,每個頁面都複製同一套 :root 變數表 — 醜但穩定。
後端(Backend)
Cloudflare Workers
整個後端是一個 203 行的 worker.js 檔案。API 涵蓋:
| Method | Path | 用途 |
|---|---|---|
| POST | /auth/google | Google ID token → JWT |
| GET / POST | /checkin | 每日簽到 |
| POST | /play | 匿名 +1 遊玩次數 |
| POST | /score | 提交分數、更新 best_scores |
| GET | /leaderboard[/:game] | 全域或單款遊戲排行榜 |
| GET | /profile | 個人檔案 + 最佳紀錄 + 成就 |
| GET | /achievements | 成就清單 |
選 Workers 的理由:
- 邊緣運算:API 端點在全球 300+ 個 Cloudflare 節點執行,使用者在哪延遲都低。台灣使用者連到台北節點大約 20ms RTT。
- 零冷啟動:V8 isolates 不是 container,沒有 Lambda 那種「第一次 request 慢」的問題。
- Free tier 極大方:每天 10 萬次 request 免費。我目前的流量用 0.1% 都不到。
- 與 Pages 無縫整合:同一個 wrangler CLI 管理,同一個 dashboard,同一套 DNS。
資料庫:Cloudflare D1
D1 是 Cloudflare 的 SQLite-on-edge 產品。Schema 只有 5 張表:users、checkins、scores、best_scores、play_counts、achievements。沒有 ORM,直接寫 parameterised SQL:
await env.DB.prepare( 'SELECT b.score, u.name, u.avatar FROM best_scores b ' + 'JOIN users u ON b.user_id = u.id ' + 'WHERE b.game_id = ? ORDER BY b.score DESC LIMIT 20' ).bind(game).all();
對於遊戲後端這種讀多寫少的場景,SQLite 完全夠用。D1 自動處理備份、複製、版本化遷移。整個資料庫目前佔 57KB,距離 10GB 的 free tier 上限還有一大段。
認證(Auth)
不自己管密碼,全靠 Google Sign-In。流程:
- 前端載入 Google Identity Services SDK,渲染「Sign in with Google」按鈕。
- 使用者授權後,Google 回傳一個 ID token(JWT 格式)。
- 前端把 ID token 打到
/auth/google。 - Worker 呼叫 Google tokeninfo 端點驗證 token 有效,並檢查
aud(必須等於我們的 GOOGLE_CLIENT_ID)、exp(未過期)、email_verified。 - Worker 寫入 D1
users表(若是新使用者),用自己的JWT_SECRET簽一個 30 天期限的 session JWT 回給前端。 - 前端把 session JWT 存 localStorage,之後所有 API 請求 header 帶
Authorization: Bearer <jwt>。
JWT_SECRET 用 wrangler secret put 存在 Cloudflare Workers 的加密環境變數,從不寫進原始碼或 wrangler.toml。先前踩過一次把 placeholder secret 寫進 wrangler.toml 推到 GitHub 的坑,立刻 rotate 換掉。
部署(Deploy)
三個獨立的 Cloudflare Pages / Workers 專案:
| 專案 | 網域 | 部署指令 |
|---|---|---|
| hao0321-studio | hao0321.com | wrangler pages deploy . --project-name=hao0321-studio |
| hao0321-games | game.hao0321.com | wrangler pages deploy ./game --project-name=hao0321-games |
| hao-games-api | api.hao0321.com | wrangler deploy(在 game-api/ 內) |
Pages 是「直接上傳」模式(不是 Git 整合),所以 git push 不會自動部署,要額外手動執行 wrangler。這在實務上有個好處:push 到 main 可以當成「階段性備份」,真正 production 上線要經過一次額外的手動動作,減少誤推的機會。
成本實算
目前所有 Cloudflare 服務都在免費額度內:
| 服務 | 用量 | 免費上限 |
|---|---|---|
| Pages 請求 | 每月約 3000 | 無上限(fair use) |
| Workers 請求 | 每月約 1500 | 100 萬 / 月 |
| D1 讀取 | 每月約 5000 | 500 萬 / 月 |
| D1 寫入 | 每月約 200 | 10 萬 / 月 |
唯一付費的是 hao0321.com 網域年費(Cloudflare Registrar,約 US$10 / 年 ≈ 台幣 310 / 年 ≈ 每月 26 元)。其他全免費。所以實際月成本 新台幣 26 元。當流量增加需要開始付費時,100 萬次 Workers 請求的下一階是 $5 USD — 到那時候再煩惱。
一個人怎麼維護
關鍵在於降低「變更」的摩擦:
- 前端完全靜態 → 改檔案直接重新部署,不用跑 CI。
- 後端是單檔 200 行 → 一眼看完整個 API 邏輯,不會迷路。
- 資料庫 schema 簡單 → 大多數功能用 JOIN 就能組出來,不用寫複雜的 query builder。
- 沒有 staging 環境 → 有風險的改動直接在 Cloudflare Pages 的 preview URL(每次部署都會生一個)驗證,OK 了再把 custom domain 切過去。
- 監控靠 Cloudflare 內建的 Workers Analytics — 不需要自己架 Grafana。
不適合這套架構的情境
誠實說這套不是銀彈:
- 需要 WebSocket / SSE 長連線:Workers 對 WebSocket 支援有限,要用 Durable Objects 才行,複雜度會立刻上升。
- 需要大量背景任務:Workers 的 CPU 時間有上限(免費版 10ms 每請求),跑 ML 推理或大型資料處理不合適。
- 團隊人數 > 3:沒有 CI/CD、staging、代碼審查流程,團隊協作會很痛。
- 嚴格的資料主權需求:D1 目前是 SQLite 多區複製,不是強一致 transactional DB。
結語
技術選擇的第一性原則應該是「這個決定把問題變簡單了還是變複雜了」。對一個要做作品集、遊戲、內容網站的獨立工作室,Cloudflare 全家桶的答案是「變簡單很多」。省下來的時間用來做遊戲、寫文章、把視覺做細緻,比多花時間調 infra 有意義得多。
如果你也在設計小型個人專案或獨立工作室的技術架構,歡迎參考這份地圖。有問題隨時寄信 lo246179268@gmail.com 聊。