Axios 的 AbortController 竞态处理方案
背景
在前端开发中,高频触发的网络请求(如搜索框联想、分页切换)可能导致 竞态问题(Race Condition)
为什么会想去研究解决这个竞态问题?
因为在工作遇到这样一个需求:平台中的所有表格筛选搜索改为动态的,去掉搜索按钮,即将搜索功能分散到各个筛选变化中进行
这样一来,短时间对同一查询接口发送多次请求的场景就会频繁出现,而接口的返回次序并不严格按照请求的次序。比如在切换下拉框时,先发送A,然后又切换发送B,(A,B属于同一接口请求),如果A的响应比B慢,就会导致筛选条件与实际显示内容不符的情况。简单演示如下:
而导致竞态问题,虽然对输入框做了防抖处理,也不能解决某些查询接口返回过慢。
由于项目中请求主要是使用ahook库的useReqest钩子以及部分直接使用axios。经过测试发现,useReqest内置了竞态处理逻辑(具体实现方式待学习),所以不需要特殊处理。所以本次竞态处理只针对axios。
解决方案
一开始从交互层面入手,也是讨巧的办法,直接在查询的时候让loading覆盖整个查询区域,这样可以直接制止高频请求。但是查询时无法进行其它操作,交互不友好。
查阅相关文章,发现axios官网已给出解决方案:地址。Axios 支持以 fetch API 方式—— AbortController 取消请求
结合项目实际,具体的实现方案如下:
- 请求标识:为每个请求生成唯一标识(如
method + path)。 - 自动取消:发起新请求时,自动取消相同标识的未完成请求。
- 灵活控制:通过请求配置参数
raceHandling动态启用/关闭该功能。
定义请求管理器
// 保存所有待处理请求的取消令牌,键为请求的唯一标识
const pendingRequests = new Map<string, AbortController>();
/**
* 生成请求唯一标识(Key)
* 默认使用 method、请求路径 生成一个字符串
* @param config 请求配置
*/
function generateReqKey(config: InternalAxiosRequestConfig<any>): string {
const path = config.url?.split('?')?.[0];
return `${config.method?.toUpperCase()}:${path}`;
}
改造请求拦截器
为了减小影响面,默认不开启竞态处理
axiosInstance.interceptors.request.use((config) => {
// 默认关闭竞态处理,支持通过 raceHandling 配置覆盖
const enableRaceHandling = config.raceHandling ?? false;
if (enableRaceHandling) {
const requestKey = generateRequestKey(config);
// 取消前一个相同请求
if (pendingRequests.has(requestKey)) {
pendingRequests.get(requestKey)?.abort();
pendingRequests.delete(requestKey);
}
// 绑定 AbortController
const controller = new AbortController();
config.signal = controller.signal;
pendingRequests.set(requestKey, controller);
}
// 原有逻辑...
});
改造响应拦截器
axiosInstance.interceptors.response.use(
(response) => {
const config = response.config;
// 请求成功时清理缓存
if (config.raceHandling ?? true) {
const requestKey = generateRequestKey(config);
pendingRequests.delete(requestKey);
}
return response;
},
);
使用示例
apiService.get('/api/search', { query: 'test' }, { raceHandling: true });
还可以在请求拦截器中自定义请求的key的方法等等
if (enableRaceHandling) {
// 生成请求唯一标识
// 优先使用自定义生成 Key 的方法
const requestKey = generateCustomReqKey?.(reqConfig) || generateReqKey(reqConfig);
// 使用
async getVideoList(params: SearchParams): Promise<any> {
return await apiService.get(`/webssh/ssh/playlist/`, params, {
raceHandling:true,
generateCustomReqKey: (config) => {
// 自定义key
}
});
}
开启raceHandling后,请求会自动取消上一次正在进行的相同key的请求,以保持数据的新鲜。
总结
AbortController是一个内建的Web API,用于取消异步操作,比如fetch请求。它的工作方式是:首先创建一个AbortController实例,并将其signal属性传给请求。在调用abort()方法时,它会在signal内部设置一个标志,这会触发abort事件。如果异步操作监听到了这个信号,就会中止操作。现代版本的axios也支持AbortController,当abort被调用时,返回的promise会因为“ERR_CANCELED”等错误而被拒绝。
通过 AbortController + 请求标识管理,实现了灵活、高效的竞态处理方案,代码侵入性低,适用于大多数前端项目。
问题遗留
- 竞态处理之后,高频请求下,loading显示异常
- 接口报错情况下对于pendingRequests队列的处理