TangYi Studio

瑭宜觀點

邊緣上的派對

林政賢 ·

這不是「AI 幫我寫了 hello world」的故事。這是一個上架中的 App:1,428 張卡牌、22 個頁面、17 張資料表、即時多人連線、內購訂閱、通過 App Store 審查。這篇寫的是這個組合到底行不行,以及沒人會先告訴你的那些坑。

先講清楚規模,不然後面的話沒有重量。

紫色微醺夜X · 現況

1,428張問答卡牌,分四個系列、三種難度、七個限定牌組
39張結局卡,含六階稀有度與陀螺儀 3D 效果
22個前端頁面,其中 16 個按需載入
17張業務資料表,15 個 migration
2–10人即時連線的多人包廂,含聊天、評分、配對、檢舉封鎖

整套跑在 Cloudflare Workers 上:一支 Worker 同時是 API 與靜態網站,資料在 D1,圖檔在 R2,即時訊息走 Ably,登入走 better-auth,訂閱走 RevenueCat。iOS App 是 Capacitor 殼包住同一個網站 —— 也就是說,改畫面、改規則、改卡牌、改資料庫,都不用出新 build

開發過程幾乎都是我跟 Claude 對話:我描述要什麼、看實機、指出哪裡不對;它讀程式碼、找原因、改、部署、驗證。這篇不打算說服你這樣很神奇,我想講的是哪些部分真的省下大量時間,哪些部分它會很自信地弄錯

為什麼是 Cloudflare

不是因為便宜(雖然確實便宜)。是因為一個 Worker 就是全部

傳統做法要拆成前端託管、API 伺服器、資料庫、CDN、檔案儲存,五個地方各自設定、各自付費、各自有自己的部署流程。在 Workers 上,這些是同一份 wrangler.json 裡的幾行綁定:

"d1_databases":  [{ "binding": "DB",     "database_name": "ppnx-db" }],
"r2_buckets":    [{ "binding": "MEDIA",  "bucket_name": "ppnx-media" }],
"assets":        { "directory": "./dist/client", "binding": "ASSETS" }

然後 npm run deploy,一次上完。這件事對「一個人做完整產品」的意義比省錢大得多 —— 你要維護的心智模型只有一個。而當你的協作者是 AI 時,這一點更關鍵:整個系統能被一次讀完,它才有機會真的理解你在做什麼,而不是每次都在猜另外四個服務現在是什麼狀態。

為什麼是 Claude

不是「會寫程式」—— 那個門檻早就過了。真正拉開差距的是它會去查證,而不是憑印象回答

舉一個這週實際發生的例子。使用者回報:「房間裡什麼都很慢,按鈕沒反應。」

憑印象的回答是「網路慢,加個快取吧」。實際做的是:先量從台灣打到 Worker 的往返時間(0.43–0.70 秒,確實慢,因為台灣線路被導到聖荷西而資料庫在亞太),做了優化 —— 然後使用者說還是慢

網路慢不會讓按鈕沒反應。

這句話推翻了原本的方向。第二輪去量的是完全不同的東西:頁面閒置時的 DOM 變動次數。

同一個畫面,完全不操作,10 秒內的 DOM 變動

78 次修好之前 —— 也就是閒著沒事也在每秒重畫八次
0 次修好之後

原因是一支每秒執行的計時器(用來判斷聊天泡泡有沒有過期)無條件在跑,而它一動就讓整個房間元件重新渲染。泡泡只有出現後那六秒需要這個時鐘,其餘時間完全不需要。

這個 bug 存在很久了。找到它靠的不是更聰明的猜測,是換一個東西去量。而「先量再修」這件事,AI 做起來比人有耐心得多 —— 它不會因為覺得自己已經知道答案就跳過。

四個沒人先告訴你的 Cloudflare 陷阱

這一節是這篇文章的重點。以下每一條都真的擋了我至少半天,而且第一眼看起來完全是別的原因

01找不到的 JS 檔會回 200,不是 404

症狀:部署之後,已經開著 App 的使用者按進某一頁會壞掉,錯誤訊息完全看不出原因。

/assets/ 的檔名帶內容雜湊,每次部署都換一組。舊的分頁請求舊檔名,而 not_found_handling: "single-page-application" 會讓任何找不到的路徑都回首頁的 HTML —— 包含 .js。於是瀏覽器拿到「HTTP 200 + text/html」卻要把它當 JS module 解析,直接丟 MIME 錯誤。

解法:動態載入失敗時自動重載一次頁面(重載會拿到新的 HTML 與新檔名),用 sessionStorage 記住避免無限迴圈。這是 SPA + 邊緣靜態託管的通病,不是 Cloudflare 獨有,但這個設定讓它從「明顯的 404」變成「詭異的 MIME 錯誤」。

02Zone 的快取設定會蓋掉你 Worker 回的 header

症狀:在 Worker 裡設了 Cache-Control,改了內容卻要等好幾小時才生效。

Cloudflare 主控台 zone 層級的 Browser Cache TTL(預設四小時)優先於 Worker 回應的 Cache-Control。另外一個對稱的誤會:Worker 的回應預設不會被邊緣快取,你以為它幫你快取了,其實沒有 —— 要自己用 caches.default

解法:要繞過 zone TTL,在網址加一個會變的參數(例如分鐘級的 ?v=)。要邊緣快取就明確寫 caches.default。兩件事都不會自動發生。

03OAuth 回呼被靜態資源層攔截

症狀:Google 登入回呼 404、Apple 登入回呼 405。而你用 curl 測那個網址,完全正常

靜態資源層會攔截「導覽請求」,SPA fallback 讓回呼根本進不到 Worker。curl 測不出來,因為它送的不是導覽請求 —— 要加 Sec-Fetch-Mode: navigate 才會重現。

解法:把那些路徑放進 run_worker_first。另外如果你把 Worker 掛在子路徑上,記得設 html_handling: "none",否則訪客會被 307 轉回主站首頁。

04wrangler 的登入權限比你以為的小

症狀:某些 wrangler 指令一律回 Unauthorized [code: 2036],怎麼改指令都沒用。

wrangler login 拿到的 OAuth token 沒有 DNS 寫入權限,也沒有 Email 權限。所以「幫 Pages 自訂子網域建 CNAME」「啟用 Email Sending」這類操作一定失敗,跟你指令對不對無關。

解法:要嘛去主控台開一把有對應權限的 API token,要嘛繞路 —— 我的做法是 DNS 改用 Worker 路徑掛載,寄信改走已經在用的第三方服務。先確認是權限問題再花時間 debug 指令,這一條省下的時間最多。

邊緣的地理現實

Workers 跑在全球邊緣節點,聽起來哪裡都快。但你的資料庫只有一個地方

實測從台灣的機器打到我們的 API,一趟往返 0.43 到 0.70 秒。原因是台灣線路被 Cloudflare 導到聖荷西(colo=SJC),而 D1 在亞太。請求先橫跨太平洋到 Worker,Worker 再橫跨回來查資料庫。

診斷方法很簡單:wrangler tail 會同時給你 cpuTimewallTime。CPU 6 毫秒而牆上時間 470 毫秒 —— 那就證明不是你的程式慢。

知道這件事之後,設計原則就只剩兩條:

但樂觀更新會咬你一口

這是我這週最喜歡的一個 bug,因為它是修好一個問題直接製造出另一個的教科書範例。

樂觀更新上線之後,使用者回報畫面「跳來跳去」:抽卡會先閃回牌組選單再跳到卡牌,送出答案後畫面會倒退再往前。

原因是輪詢是獨立在跑的。一個在你寫入之前就送出的請求,回來時帶的是舊資料,直接套用就把剛做好的畫面推回去;下一次輪詢才又往前。你以為你在修延遲,其實你在製造閃爍。

解法不需要時間戳,也不怕時鐘不同步 —— 用單調遞增的計數器判斷誰比較新就好:

function mergeRoom(prev, incoming){
  const pT = prev.turn_count, iT = incoming.turn_count;
  const pC = prev.current_turn, iC = incoming.current_turn;
  if (iC > pC || iT > pT) return incoming;   // 伺服器比較新 → 整包接受
  if (iC < pC || iT < pT) return prev;       // 輪詢落後了 → 整包丟掉
  // 同一回合:伺服器還沒收到我們剛送出的動作 → 只保留那幾個欄位
  if (prev.last_pick && !incoming.last_pick) return { ...incoming,
        last_pick: prev.last_pick, last_by: prev.last_by };
  return incoming;
}

只要你的狀態裡有一個「只會往前不會往後」的數字(回合數、版本號、序號都行),這個問題就有一個很便宜的解。樂觀更新沒有配套的調和策略,等於把延遲換成閃爍。

Claude 會在哪裡弄錯

這一節寫給打算這樣做的人。它不是萬能的,而且錯的方式有規律。

一、它會很有自信地說「修好了」

最常見的失敗模式:改完程式、部署、然後看自己瀏覽器裡的舊快取,或看本機的建置產物,回報「已修好」。這在這個專案發生過不只一次。

唯一有效的紀律是驗證線上實際被服務的檔案:用 curl 抓部署後的 chunk,比對關鍵字串。而且要確認 grep 真的有抓到 —— 壓縮之後空白會消失,帶空格的比對字串會落空,你會得到一個看起來像「沒上去」的假警報。

二、它會為自己的選擇編出漂亮的理由

如果你問「為什麼這樣做」,它幾乎一定給得出一套聽起來很專業的說法 —— 即使真正的原因只是「上一步剛好這樣寫」。這在寫 README 或技術文件時特別危險,因為那些文字會被別人當成設計決策讀。

我的做法是:重要的東西發布前跑一次獨立的對抗式稽核 —— 開另一個乾淨的對話,只給它程式碼,請它挑錯,不要給它原本的說法。

三、它需要你當那個說「還是不對」的人

前面那個「什麼都很慢」的例子,第一輪的優化是對的、量測是真的、方向是錯的。把它推回正軌的不是更好的提示詞,是一句「還是慢」

這大概是整篇最實用的一句:你的價值不在於會不會寫那段程式,而在於你知道它還沒好。實機測、講具體症狀、不接受「應該可以了」——這件事目前還沒有東西能代替你。


那,這能複製嗎

能,但要挑對題目。

這個組合最強的地方是「一個人要負責全部」的專案:沒有前端後端維運之分,沒有跨團隊溝通成本,改一行到上線只有一個指令。Workers 把基礎設施壓縮到你一個人扛得動,Claude 把「讀懂三萬行程式再改對一行」的成本壓下來 —— 兩件事必須同時成立才有意義,少一個都不夠。

反過來說,如果你的系統本來就分散在五個雲、三個團隊、兩套部署流程,那瓶頸不在寫程式,這個組合幫不上什麼忙。

至於「AI 會不會取代工程師」—— 從這幾週的經驗看,它取代的是查文件、寫樣板、追蹤誰改壞了什麼這些事。留給人的是判斷什麼值得做、以及看著實機說「這裡不對」。後面那件事的份量,只有變重沒有變輕。

關於這篇:文中所有數字都是實際量測,不是估計。0.43–0.70 秒是從台灣的機器對線上 API 實測;78→0 是同一個畫面閒置十秒的 DOM 變動次數;卡牌與資料表數量是直接查資料庫。四個 Cloudflare 陷阱都附了成因與解法,因為只講「有雷」對讀的人沒有用。

紫色微醺夜X 是瑭宜工作室的 16+ 派對問答卡牌 App。工作室的開源專案在 github.com/tangyistudio

白話版

不用懂技術也想看這件事的重點?

同一個專案還有一篇寫給非工程師的版本,講的是另一個重點:AI 這麼強之後,人還剩下什麼。主線是那句把整個方向推翻的「還是慢」。

讀白話版:那句「還是慢」 →

新文章

兩盞 AI 神燈,該擦哪一盞?

Claude Fable 5.1 與 GPT-6 Astra 兩天內先後上市,價格一模一樣。這篇不比跑分,只回答一個問題:想開始 vibe coding,該把錢付給誰。

讀:AI 界的神燈大戰 →

作者:林政賢(導演 · Gen AI 創作者兼工程師 · 瑭宜影像創意工作室創辦人)