Axios 取消重复请求:AbortController vs CancelToken 全面对比

78 阅读3分钟

在实际前端项目中,我们经常会遇到重复请求的问题:比如用户频繁点击按钮、关键字改变实时搜索、页面快速切换等,导致同一请求被多次发送,不仅浪费资源,还可能引起数据错乱。本文将介绍如何使用 Axios 的两种方式AbortController 和 CancelToken来自动取消重复请求,并对比它们的实现与适用场景。

特性AbortController (现代标准)CancelToken (Axios传统方式)
机制浏览器原生提供的API,通过 signal/abort() 工作Axios库自建的机制,通过 cancel() 函数工作
状态推荐使用,是现代的Web标准已弃用,但现有项目仍在广泛使用

方案一:使用 CancelToken(传统方式)

取消未完成的重复请求(当具有相同 method 和 URL)。该行为可通过 config.clearBefore 配置项灵活控制是否启用,适用于如关键字改变搜索列表等场景,有效避免因请求响应时序问题造成的数据错乱。

config.cancelToken =
    new axios.CancelToken(cancel => {
        pendingRequests.set(requestKey, cancel)
     })
cancel('取消重复请求')
import axios from 'axios'

// 使用Map存储请求标识和取消函数,性能更好
const pendingRequests = new Map()

// 生成请求的唯一标识 - 只使用method和url
const generateReqKey = config => {
  const { method, url } = config
  return `${method}&${url}`
}

// 取消相同请求(相同method和url)
const cancelSameRequest = config => {
  const requestKey = generateReqKey(config)
  if (pendingRequests.has(requestKey)) {
    const cancel = pendingRequests.get(requestKey)
    cancel('取消重复请求')
    pendingRequests.delete(requestKey)
  }
}

// 添加请求到pending中
const addPendingRequest = config => {
  const requestKey = generateReqKey(config)
  config.cancelToken =
    config.cancelToken ||
    new axios.CancelToken(cancel => {
      if (!pendingRequests.has(requestKey)) {
        pendingRequests.set(requestKey, cancel)
      }
    })
}

// 移除请求从pending中
const removePendingRequest = config => {
  const requestKey = generateReqKey(config)
  if (pendingRequests.has(requestKey)) {
    pendingRequests.delete(requestKey)
  }
}

// 请求拦截器
service.interceptors.request.use(
  config => {
    if (config.clearBefore) {
      // 取消相同请求(相同method和url)
      cancelSameRequest(config)
      // 添加当前请求到pending中
      addPendingRequest(config)
    }

    // ... 其他配置
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    // 请求成功后移除对应pending记录
    removePendingRequest(response.config)
    return response
  },
  error => {
    if (axios.isCancel(error)) {
      console.log('请求已被取消:', error.message)
    } else {
      // 对于非取消错误的请求,移除对应pending记录
      if (error.config) {
        removePendingRequest(error.config)
      }
    }
    return Promise.reject(error)
  }
)

方案二:使用 AbortController(推荐)

const controller = new AbortController();
config.signal = controller.signal; 
// 取消重复请求
controller.abort();
import axios from 'axios';

// 存储请求的Map
const pendingRequests = new Map();

// 生成请求的key,这里使用方法和URL来标识同一个请求
// 注意:如果同一个URL和方法,但参数不同,可能需要考虑参数,这里简单处理
const getRequestKey = (config) => {
  return [config.method, config.url].join('&');
};

// 添加请求到Map
const addPendingRequest = (config) => {
  const requestKey = getRequestKey(config);
  // 如果已经有该请求,则先取消之前的请求
  if (pendingRequests.has(requestKey)) {
    const cancelController = pendingRequests.get(requestKey);
    cancelController.abort();
    pendingRequests.delete(requestKey);
  }
  const controller = new AbortController();
  config.signal = controller.signal; // 将controller的signal添加到config中
  pendingRequests.set(requestKey, controller);
};

// 移除请求
const removePendingRequest = (config) => {
  const requestKey = getRequestKey(config);
  if (pendingRequests.has(requestKey)) {
    pendingRequests.delete(requestKey);
  }
};

// 创建axios实例
const instance = axios.create({
  // 可以在这里设置一些默认配置
});

// 请求拦截器
instance.interceptors.request.use(
  (config) => {
    // 在发送请求之前,将请求添加到pendingRequests
    addPendingRequest(config);
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 响应拦截器
instance.interceptors.response.use(
  (response) => {
    // 请求完成,移除该请求
    removePendingRequest(response.config);
    return response;
  },
  (error) => {
    // 请求失败,也移除该请求
    if (error.config) {
      removePendingRequest(error.config);
    }
    return Promise.reject(error);
  }
);

// 取消所有请求的函数
export const cancelAllRequests = () => {
  for (const [requestKey, controller] of pendingRequests) {
    controller.abort();
    pendingRequests.delete(requestKey);
  }
};

// 取消指定请求的函数
export const cancelRequest = (config) => {
  const requestKey = getRequestKey(config);
  if (pendingRequests.has(requestKey)) {
    const controller = pendingRequests.get(requestKey);
    controller.abort();
    pendingRequests.delete(requestKey);
  }
};

export default instance;
import axiosInstance, { cancelRequest, cancelAllRequests } from './axiosInstance';

const config = {
  method: 'get',
  url: '/api/data'
};

// 发送一个请求
axiosInstance(config)
  .then(response => {
    console.log(response);
  })
  .catch(error => {
 });

// 取消这个请求
 cancelRequest(config);

// 取消所有请求
 cancelAllRequests();