在实际前端项目中,我们经常会遇到重复请求的问题:比如用户频繁点击按钮、关键字改变实时搜索、页面快速切换等,导致同一请求被多次发送,不仅浪费资源,还可能引起数据错乱。本文将介绍如何使用 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();