前端数据请求治理
前端和后端打交道,绕不开数据请求这一层。这篇文章梳理了我在实际项目里遇到的几个高频问题,这些问题很容易在真实项目里反复出现。本文围绕下面四个问题展开:
- token 无感刷新:401 触发时静默换新 token,不打断用户操作;
- 并发请求控制:浏览器同源并发有限,列表/批量场景要排队;
- 请求缓存与去重:相同请求不重复执行,必要时按 TTL 失效;
- 请求重试:网络抖动场景下用指数退避兜底。
token 过期后,如何实现无感刷新
实际场景
- 登录态过期:Access Token 已经过期但 Refresh Token 还有效,需要拿到新 Token 后继续请求;
- 并发请求雪崩:多个请求同时触发 401,不能每个请求都去刷新一次 Token;
- 用户体验:把用户踢到登录页会打断正在进行的操作,应该静默重试。
实现思路(双令牌 + 单点刷新)
双令牌机制:短期的访问令牌(Access Token)+ 长期的刷新令牌(Refresh Token)。
- Access Token:每次 API 请求携带,有效期短(如 15 分钟),泄露风险低。更稳妥的做法是放在后端 HttpOnly Cookie 中;如果业务需要前端手动携带 Token,则至少避免以明文形式存储;
- Refresh Token:仅用于刷新 Access Token,有效期长(如 7 天)。
请求流程(登录之后的循环):
当多个请求同时触发 401,用 isRefreshing 标志位保证只有一个请求去刷新 token,其他挂起等待;新 token 拿到后再批量唤醒队列里的请求。实现如下:
注意事项
- 401 拦截要避免死循环:用
_retry标志位标记已重试过的请求; - Refresh Token 本身刷新失败(如过期)才跳转到登录页。
如何批量控制并发请求
实际场景
- 列表页一次加载 100 张图片,浏览器对同源最大并发只有 6 个(HTTP/1.1),多余的请求会被阻塞;
- 批量调用查询接口,后端有限流时不能一次性把所有请求都发出去;
- 大文件上传或下载,并发过大容易触发浏览器内存或网络问题。
实现思路(任务池排队)
核心是维护一个运行中的任务池,限制同时执行的数量。任务来了先入队,池子有空位再出队执行:
调用方只需要把请求包进任务池:
注意事项
- max 数值不是越大越好:图片加载 6~8 比较合适,API 调用要根据后端限流策略调;
- HTTP/2 及以上通过多路复用缓解了同源连接数限制,但浏览器、服务端并发流和后端处理能力仍然有上限;
- 长时间排队的任务要加超时,避免请求堆积。
如何控制请求缓存
实际场景
- 用户基本信息、字典数据等“基本不变”或“偶尔变化”的数据,每次都重新请求会造成浪费;
- 短时间内对同一接口发起多次相同请求(搜索框抖动、列表页快速切换);
- 多个组件同时请求同一份数据,重复请求浪费带宽和后端资源。
实现思路(LRU + 请求去重)
数据更新频率差异很大,先按变化频率选策略更稳妥:
缓存可以存最终数据,也可以短时间存正在进行的 Promise。前者减少重复读取,后者主要用来合并并发请求。
LRUCache 实现:
基于 LRUCache 可以封装一个支持请求去重的 useFetch hook,让短时间内相同 key 的请求复用同一个 Promise:
使用示例(仅作演示,省略了部分边界判断):
注意事项
- TTL 要按数据更新频率设置:基本不变的数据 TTL 长一些,频繁变化的不缓存;
- LRU 容量不能太大:缓存本身占内存,几百到几千条比较合理。
如何控制请求重试
实际场景
- 网络抖动导致偶发的请求失败(5xx、超时),重试一次就能成功;
- 表单提交、订单创建等场景不适合盲目重试,要区分错误类型;
- 后端短暂过载,重试几次能拿到正确结果。
实现思路(指数退避 + 随机抖动)
指数退避 + 随机抖动:失败后等 1s、2s、4s 再重试,避免雪崩重试把后端打挂。
调用时显式包一层 withRetry,instance 本身不会自动重试:
注意事项
- 一般重试 3 次以内就行,超过 3 次基本属于业务层面兜底而不是网络重试能解决的问题;
- 4xx 通常是业务/客户端错误,不要盲目重试:参数不变重试大概率还是同样的 4xx;
- 优先只对幂等请求重试;POST / PUT 这类写请求要有幂等键或服务端去重保护,否则可能导致重复写入。
总结
四个问题看起来独立,实际在生产环境里经常要组合使用:
- token 刷新 + 缓存:token 换了之后,缓存里基于旧身份拉到的数据也该失效。刷新成功后要清理相关缓存,或者把用户身份纳入缓存 key,避免用旧身份读到数据;
- 并发控制 + 重试:
pool.run(() => withRetry(...))时,pool控制同时飞的请求数,withRetry负责单次请求的退避,两层互不干扰; - 缓存 + 重试:缓存层负责命中和去重,重试层只包真实请求;缓存命中时不发网络请求,也就不会进入失败后的退避重试。
工程上没有银弹,关键是看具体业务对"延迟"、"成功率"和"后端压力"的取舍。