Axios 请求竞态处理:如何通过 AbortController 实现“取消重复请求”?

725 阅读3分钟

Axios 的 AbortController 竞态处理方案

背景

在前端开发中,高频触发的网络请求(如搜索框联想、分页切换)可能导致 竞态问题(Race Condition)

为什么会想去研究解决这个竞态问题?

因为在工作遇到这样一个需求:平台中的所有表格筛选搜索改为动态的,去掉搜索按钮,即将搜索功能分散到各个筛选变化中进行

这样一来,短时间对同一查询接口发送多次请求的场景就会频繁出现,而接口的返回次序并不严格按照请求的次序。比如在切换下拉框时,先发送A,然后又切换发送B,(A,B属于同一接口请求),如果A的响应比B慢,就会导致筛选条件与实际显示内容不符的情况。简单演示如下: image.png

而导致竞态问题,虽然对输入框做了防抖处理,也不能解决某些查询接口返回过慢。

由于项目中请求主要是使用ahook库的useReqest钩子以及部分直接使用axios。经过测试发现,useReqest内置了竞态处理逻辑(具体实现方式待学习),所以不需要特殊处理。所以本次竞态处理只针对axios。

解决方案

一开始从交互层面入手,也是讨巧的办法,直接在查询的时候让loading覆盖整个查询区域,这样可以直接制止高频请求。但是查询时无法进行其它操作,交互不友好。

查阅相关文章,发现axios官网已给出解决方案:地址。Axios 支持以 fetch API 方式—— AbortController 取消请求

结合项目实际,具体的实现方案如下:

  1. 请求标识:为每个请求生成唯一标识(如 method + path)。
  2. 自动取消:发起新请求时,自动取消相同标识的未完成请求。
  3. 灵活控制:通过请求配置参数 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的请求,以保持数据的新鲜。 image.png

总结

AbortController是一个内建的Web API,用于取消异步操作,比如fetch请求。它的工作方式是:首先创建一个AbortController实例,并将其signal属性传给请求。在调用abort()方法时,它会在signal内部设置一个标志,这会触发abort事件。如果异步操作监听到了这个信号,就会中止操作。现代版本的axios也支持AbortController,当abort被调用时,返回的promise会因为“ERR_CANCELED”等错误而被拒绝。

通过 AbortController + 请求标识管理,实现了灵活、高效的竞态处理方案,代码侵入性低,适用于大多数前端项目。

问题遗留

  1. 竞态处理之后,高频请求下,loading显示异常
  2. 接口报错情况下对于pendingRequests队列的处理