![]()
一個按鈕點下去,下午用戶就開始 rage-clicking。界面顯示的數據亂序、更新丟失、隨機報錯——你本地復現不了,線上卻真實發生。
這就是玩具級 fetch() 和生產級網絡層的差距。Netflix 工程團隊在 2019 年的技術博客中披露,他們的客戶端曾因請求競態導致播放記錄錯亂,用戶看完一集后進度條回退到上一集。問題根源不是后端,而是前端沒做請求排序控制。
本文用一個票務排隊系統演示:從單請求到完整生產方案,逐步疊加排序、失敗處理、重試、取消等機制。所有代碼在 GitHub 倉庫 js-fetch-production-demo 可運行,包含 Express 后端和原生 JS 前端。
慢網絡和亂序響應:為什么先發的請求會后到
后端接口 GET /tickets/:id/nextNumber 每次返回遞增的票號。理想情況下,請求 A 先發出,返回 1;請求 B 后發出,返回 2。但網絡不保證順序。
模擬延遲后,問題暴露:請求 A 耗時 200ms,請求 B 耗時 50ms。用戶先點 A 再點 B,界面卻先顯示 2,后顯示 1。這就是競態條件(race condition)。
解決方案:為每個請求附加序列號,只接受最新請求的結果。代碼層面,維護一個遞增的 requestId,響應返回時比對:if (responseId !== currentId) return;。老響應直接丟棄,界面永遠顯示最新數據。
Netflix 的 RxJS 實現中,這個模式叫 switchMap——新請求發出時自動取消對老響應的訂閱。原生 JS 里用 AbortController 也能做到,但更簡單的方式是版本號比對。
關鍵細節:requestId 必須在發送前遞增,而不是收到響應后。否則兩個請求幾乎同時發出時,仍會拿到相同的 ID。
HTTP 錯誤和不可靠響應:200 不等于成功
fetch() 不會為 HTTP 錯誤碼拋出異常。404、500 都走正常 resolve,只有網絡完全斷開才會 reject。這是設計上的坑,很多開發者踩過。
生產級代碼需要手動檢查:if (!res.ok) throw new Error(res.status)。但還不夠——后端可能返回 200,但 body 是 HTML 錯誤頁(比如 CDN 回源失敗)。
更隱蔽的是超時。fetch() 沒有內置超時,瀏覽器默認等待時間長達幾分鐘。用戶早就關閉頁面了,請求還在后臺掛起。
用 AbortController 封裝超時:
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
const res = await fetch(url, { signal: controller.signal });
超時后 fetch 拋出 AbortError,需要單獨捕獲處理。注意:abort 后的請求無法復用,必須新建 controller。
自動重試:瞬態故障的自救
移動網絡切換 WiFi、CDN 邊緣節點重啟、數據庫主從延遲——這些故障通常幾秒后自愈。直接報錯太粗暴,無腦重試又會壓垮已過載的服務。
指數退避(exponential backoff)是標準答案:第 1 次等 100ms,第 2 次等 200ms,第 3 次等 400ms。但純指數增長在分布式系統中會引發"驚群效應"——所有客戶端同時重試,瞬間打爆服務。
加入隨機抖動(jitter):實際延遲 = base * 2^attempt + random(0, 100)。AWS SDK 和 Kubernetes 的 client-go 都采用類似策略。
重試次數要有上限,且只對特定錯誤重試。4xx 客戶端錯誤重試無意義,5xx 和超時才可以。429 Too Many Requests 要特殊處理:響應頭里的 Retry-After 優先級高于退避算法。
關鍵細節:重試必須搭配冪等性設計。POST 創建資源時,服務端需要支持去重鍵(idempotency key),否則重試會導致重復創建。
生產級模式:從可用到優雅
基礎功能補齊后,還有四個進階課題:
請求合并(coalescing):100 個組件同時請求同一資源,只發 1 個請求,結果共享。React Query 和 SWR 的緩存層做這個,原生實現需要維護 in-flight promise 的 Map。
熔斷(circuit breaker):連續失敗 5 次后,直接拒絕后續請求 30 秒,給后端恢復窗口。Netflix 的 Hystrix 庫帶火了這個模式,現在瀏覽器端也可用 opossum 等庫實現。
速率限制(rate limiting):用戶瘋狂點擊時,前端先限流,而不是把壓力傳導到后端。令牌桶算法適合客戶端:每秒生成 2 個令牌,每個請求消耗 1 個,沒令牌就排隊或拒絕。
緩存策略:HTTP 緩存頭(Cache-Control、ETag)是第一道防線,但 SPA 中更常用內存緩存。關鍵決策:緩存多久?如何失效?樂觀更新(optimistic update)還是等響應確認?
這些不是都要做。票務系統需要排序和重試,但可能不需要熔斷;實時聊天需要取消和合并,但緩存策略完全不同。工具箱里的選項,根據場景挑選。
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.