Axios 如何取消重复请求?
? 在 Web 项目开发过程中,我们经常会遇到重复请求的场景,如果系统不对重复的请求进行处理,则可能会导致系统出现各种问题。比如重复的 post 请求可能会导致服务端产生两笔记录。那么重复请求是如何产生的呢?这里我们举 2 个常见的场景: 假设页面中有一个按钮,用户点击按钮后会发起一个 AJAX 请求。如果未对该按钮进行控制,当用户快速点击按钮时,则会发出重复请求。
既然已经知道重复请求是如何产生的,也知道了它会带来一些问题。接下来,将以 Axios 为例,带大家来一起解决重复请求的问题。
一、如何取消请求
Axios 是一个基于 Promise 的 HTTP 客户端,同时支持浏览器和 Node.js 环境。它是一个优秀的 HTTP 客户端,被广泛地应用在大量的 Web 项目中。对于浏览器环境来说,Axios 底层是利用 XMLHttpRequest 对象来发起 HTTP 请求。如果要取消请求的话,我们可以通过调用 XMLHttpRequest 对象上的 abort 方法来取消请求:
let xhr = new XMLHttpRequest();
xhr.open("GET", "https://developer.mozilla.org/", true);
xhr.send();
setTimeout(() => xhr.abort(), 300);
而对于 Axios 来说,我们可以通过 Axios 内部提供的 CancelToken 来取消请求:
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.post('/user/12345', {
name: 'semlinker'
}, {
cancelToken: source.token
})
source.cancel('Operation canceled by the user.');
此外,你也可以通过调用 CancelToken 的构造函数来创建 CancelToken,具体如下所示:
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
cancel = c;
})
});
cancel();
现在我们已经知道在 Axios 中如何使用 CancelToken 来取消请求了,那么 CancelToken内部是如何工作的呢?这里我们先记住这个问题,后面将为你们揭开 CancelToken 背后的秘密。接下来,我们来分析一下如何判断重复请求。
二、如何判断重复请求
当请求方式、请求 URL 地址和请求参数都一样时,我们就可以认为请求是一样的。因此在每次发起请求时,我们就可以根据当前请求的请求方式、请求 URL 地址和请求参数来生成一个唯一的 key,同时为每个请求创建一个专属的 CancelToken,然后把 key 和 cancel 函数以键值对的形式保存到 Map 对象中,使用 Map 的好处是可以快速的判断是否有重复的请求:
import qs from 'qs'
const pendingRequest = new Map();
const requestKey = [method, url, qs.stringify(params), qs.stringify(data)].join('&');
const cancelToken = new CancelToken(function executor(cancel) {
if(!pendingRequest.has(requestKey)){
pendingRequest.set(requestKey, cancel);
}
})
当出现重复请求的时候,我们就可以使用 cancel 函数来取消前面已经发出的请求,在取消请求之后,我们还需要把取消的请求从 pendingRequest 中移除。现在我们已经知道如何取消请求和如何判断重复请求,下面我们来介绍如何取消重复请求。
三、如何取消重复请求
因为我们需要对所有的请求都进行处理,所以我们可以考虑使用 Axios 的拦截器机制来实现取消重复请求的功能。Axios 为开发者提供了请求拦截器和响应拦截器,它们的作用如下:
3.1 定义辅助函数
在配置请求拦截器和响应拦截器前,先来定义 3 个辅助函数: generateReqKey:用于根据当前请求的信息,生成请求 Key;
function generateReqKey(config) {
const { method, url, params, data } = config;
return [method, url, Qs.stringify(params), Qs.stringify(data)].join("&");
}
addPendingRequest:用于把当前请求信息添加到pendingRequest对象中;
const pendingRequest = new Map();
function addPendingRequest(config) {
const requestKey = generateReqKey(config);
config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
if (!pendingRequest.has(requestKey)) {
pendingRequest.set(requestKey, cancel);
}
});
}
removePendingRequest:检查是否存在重复请求,若存在则取消已发的请求。
function removePendingRequest(config) {
const requestKey = generateReqKey(config);
if (pendingRequest.has(requestKey)) {
const cancelToken = pendingRequest.get(requestKey);
cancelToken(requestKey);
pendingRequest.delete(requestKey);
}
}
创建好 generateReqKey、addPendingRequest 和 removePendingRequest 函数之后,我们就可以设置请求拦截器和响应拦截器了。
设置请求拦截器
axios.interceptors.request.use(
function (config) {
removePendingRequest(config);
addPendingRequest(config);
return config;
},
(error) => {
return Promise.reject(error);
}
);
设置响应拦截器
axios.interceptors.response.use(
(response) => {
removePendingRequest(response.config);
return response;
},
(error) => {
removePendingRequest(error.config || {});
if (axios.isCancel(error)) {
console.log("已取消的重复请求:" + error.message);
} else {
}
return Promise.reject(error);
}
);
四、CancelToken 的工作原理
在前面的示例中,我们是通过调用 CancelToken 构造函数来创建 CancelToken 对象:
new axios.CancelToken((cancel) => {
if (!pendingRequest.has(requestKey)) {
pendingRequest.set(requestKey, cancel);
}
})
所以接下来,我们来分析 CancelToken 构造函数,该函数被定义在 lib/cancel/CancelToken.js 文件中:
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
由以上代码可知,cancel 对象是一个函数,当我们调用该函数后,会创建 Cancel 对象并调用 resolvePromise 方法。该方法执行后,CancelToken 对象上 promise 属性所指向的 promise 对象的状态将变为 resolved。那么这样做的目的是什么呢?这里我们从 lib/adapters/xhr.js 文件中找到了答案:
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) { return; }
request.abort();
reject(cancel);
request = null;
});
}
|