前端请求层封装 重试、取消与并发控制
滑板少年阿野
7 分钟阅读
前端请求层封装 重试、取消与并发控制
裸调 fetch 在真实项目里很快会失控:超时怎么处理、重复提交怎么拦、列表几十个请求怎么不挤爆连接。封装一层请求层,目标是把"重试、取消、并发"做成默认能力,业务代码不再逐个处理。很多项目前期图省事直接 fetch,等到白屏、重复下单、列表卡死等问题集中爆发,再回头补这一层成本极高。
为什么不能裸调
分散在各处的请求逻辑会带来三类重复问题:超时和重试每家自己写一套,实现不一致;组件卸载后响应回来仍 setState 导致告警;首屏并发几十个请求把同域连接数打满,后面的请求排队等到超时。统一封装后,这些变成请求层的配置项,业务只传参数。更深层的好处是可观测:所有请求都经过同一入口,打点、错误上报、鉴权注入都能集中做,而非散落在几十个调用点。
超时与重试
超时靠 AbortController 在到达时限时中断,重试只对幂等请求有意义:
async function request(url, { retries = 2, timeout = 5000 } = {}) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), timeout);
for (let i = 0; i <= retries; i++) {
try {
const res = await fetch(url, { signal: ctrl.signal });
clearTimeout(timer);
return await res.json();
} catch (e) {
if (i === retries) throw e;
await new Promise(r => setTimeout(r, 2 ** i * 300)); // 指数退避
}
}
}
边界:重试不要对写操作盲目重试。POST 创建订单这类非幂等请求自动重试可能造成重复单据,只有 GET、幂等 PUT 才适合加入自动重试白名单。退避用指数而非固定间隔,避免所有客户端在同一时刻一起重发形成惊群。还要区分错误类型:网络层超时值得重试,而 4xx 业务错误重试也没用,应在重试逻辑里跳过。经验上,重试次数设 2 到 3 次足够,过多只是延长用户等待。
取消:组件卸载不报错
AbortController 同时是取消的手段,组件卸载时主动中断,避免竞态写状态:
const ctrl = new AbortController();
fetch('/api/list', { signal: ctrl.signal });
// 组件卸载时
useEffect(() => () => ctrl.abort(), []);
被 abort 的请求会在 catch 里收到 AbortError,请求层应识别并吞掉这类错误,不向上抛成"网络异常"干扰业务。这也是避免"快速切换 tab 时旧请求覆盖新结果"的核心手段——新请求发出后,旧请求的 controller 立即 abort,保证只有最新的结果会落地。在搜索联想这类场景里,取消比重试更常用。
并发控制:限制同时进行的请求数
浏览器同域连接数有上限,经验上约 6 个。超过会排队,但需要主动控量避免瞬间洪峰把队列堵死。用令牌式队列实现:
function createPool(limit) {
let running = 0;
const queue = [];
const next = () => {
if (running >= limit || !queue.length) return;
running++;
const { fn, resolve, reject } = queue.shift();
fn().then(resolve, reject).finally(() => { running--; next(); });
};
return (fn) => new Promise((resolve, reject) => {
queue.push({ fn, resolve, reject });
next();
});
}
这个池子把并行数锁在 limit 内,多余的请求在队列里等待。首屏需要并发拉取多个独立接口时,用它既加速又不击穿连接上限。边界:limit 设太小会拖慢首屏,太大又失去控制意义,按经验取 4 到 6 较稳;队列里的 Promise 要能响应外部取消,否则组件卸载后任务还在排队白白占用。
去重与防抖
连续触发相同请求(如快速输入联想)可做 key 去重,命中在途请求直接复用 Promise,既省流量又避免结果互相覆盖。防抖则用于把高频输入合并成一次请求,避免每次按键都打接口。两者配合:防抖负责"何时发",去重负责"发了别重复发"。还要注意请求层应当统一注入鉴权头、统一错误码映射、统一 loading 计数,让业务只关心"拿数据",其余交给这一层。
错误与鉴权的统一收口
请求层还应当是错误处理和鉴权注入的唯一出口。所有请求统一加鉴权头,避免每个调用点重复拼 token;错误码也在这一层映射成业务可读的文案,下游组件只关心成功数据和友好错误。登录态过期这类全局错误,更应该由请求层统一拦截并触发重新登录,而不是散落在几十个 catch 里各写一遍。把"网络异常、鉴权失效、限流"这些横切关注点收口到一层,业务代码才能保持干净。经验上,请求层越往上长(重试、取消、并发、鉴权、错误、上报),上层业务就越薄,这是健康的依赖方向。
还有一点常被遗漏:请求层应当暴露可观测钩子,比如每个请求的开始与结束都触发回调,便于在界面上统一展示全局 loading,或统计接口耗时分布。把这些横切能力集中,业务侧才能真正只写业务逻辑。配置上建议把超时、重试、并发上限都提成可覆盖的默认值,不同接口按需传入,而不是写死在封装里。
经验上,请求层是前端工程里性价比最高的一层抽象。它把网络的不确定性(超时、重试、取消、并发)收敛成一个稳定接口,业务代码从此不必为每个接口单独兜底。
浏览 0
点赞 0
评论 0