![]()
一个按钮点下去,下午用户就开始 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.