在实际开发中,我们需要对用户发起的重复请求进行拦截处理,比如用户快速点击提交按钮。
对于重复的 get 请求,会导致页面更新多次,发生页面抖动的现象,影响用户体验;对于重复的 post 请求,会导致在服务端生成两次记录(例如生成两条订单记录)。
无论从用户体验或者从业务严谨方面来说,取消无用的请求是需要避免的。
一、一般处理方式
我们可以在用户即将发送请求,但还未发送请求时给页面添加一个 loading 效果,提示数据正在加载,loading 会阻止用户继续操作。
这种方式在大部分情况下是可行的,但是在某些情况下却不奏效,比如在 loading 显示之前,用户就已经触发了两次请求的情况。
二、Axios 拦截器统一处理
重复发送的请求的场景很多,我们需要在一个公共的地方对请求响应进行处理,Axios 拦截器就闪亮登场了。
Axios 拦截器包括请求拦截器和响应拦截器,可以在请求发送前或响应后进行拦截处理,用法如下:
// 添加请求拦截器
axios.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
return config;
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error);
}
);
// 添加响应拦截器
axios.interceptors.response.use(
function (response) {
// 对响应数据做点什么
return response;
},
function (error) {
// 对响应错误做点什么
return Promise.reject(error);
}
);
那么,如何进行拦截呢?也就是如何取消用户的请求,将它扼杀在摇篮里...
2.1、如何取消请求
众所周知,浏览器是通过 XMLHttpRequest 对象进行 http 通信的,如果要取消请求的话,我们可以通过调用 XMLHttpRequest 对象上的 abort 方法来取消请求。
let xhr = new XMLHttpRequest();
xhr.open("GET", "http://www.shanzhonglei.com/", true);
xhr.send();
setTimeout(() => xhr.abort(), 300);
Axios是一个主流的http请求库,它提供了两种取消请求的方式。
第一种,通过axios.CancelToken.source生成取消令牌token和取消方法cancel。
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
第二种,通过axios.CancelToken构造函数生成取消函数。
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c;
})
});
// cancel the request
cancel();
需要注意的是在catch中捕获异常时,应该使用axios.isCancel()判断当前请求是否是主动取消的,以此来区分普通的异常逻辑。
知道了如何取消请求就好办了,如果两个请求是相同的,那么我们就可以对后一个请求进行拦截操作。
2.2、判断重复请求
我们可以把每个请求的方法、url 和参数组合成一个字符串,作为该请求的唯一标识 key,与此同时,为对应的 key 生成一个 CancelToken 以备取消当前的请求。把 key 和对应的 cancel 函数以键值对的形式保存在 Map 对象中。
const pendingRequest = new Map();
const requestKey = [
method,
url,
JSON.stringify(params),
JSON.stringify(data),
].join("&");
const cancelToken = new CancelToken(function executor(cancel) {
if (!pendingRequest.has(requestKey)) {
pendingRequest.set(requestKey, cancel);
}
});
定义pendingRequests 为 map 对象的目的是为了方便我们查询它是否包含某个 key,以及添加和删除 key。
在请求拦截器中,会检查pendingRequests 对象中是否包含当前请求的 requestKey,如果重复,就cancel拦截掉当前请求,如果不重复,则将requestKey 添加到 pendingRequests 对象中。
2.3 具体实现
我们先来生成几个辅助函数:
generateReqKey
:用于根据当前请求的信息,生成请求 Key
function generateReqKey(config) {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.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);
}
}
clearPending
清空 pending 中的请求(在路由跳转时调用)
function clearPending() {
for (const [requestKey, cancelToken] of pendingRequest) {
cancelToken(requestKey)
}
pendingRequest.clear()
}
实操来了...
请求拦截器
axios.interceptors.request.use(
function (config) {
removePendingRequest(config); // 检查是否存在重复请求,若存在则取消已发的请求
addPendingRequest(config); // 把当前请求信息添加到pendingRequest对象中
return config;
},
(error) => {
// 这里出现错误可能是网络波动造成的,清空 pendingRequests 对象
pendingRequests.clear();
return Promise.reject(error);
}
);
响应拦截器
在这里,说明请求已经结束了,状态已经变成pending,这时需要把它从pendingRequests删除。
axios.interceptors.response.use(
(response) => {
removePendingRequest(response.config); // 从pendingRequest对象中移除请求
return response;
},
(error) => {
removePendingRequest(error.config || {}); // 从pendingRequest对象中移除请求
if (axios.isCancel(error)) {
console.warn(error);
return Promise.reject(error);
} else {
// 添加其它异常处理
}
return Promise.reject(error);
}
);
最后,我们要在页面切换之前取消上一个路由中未完成的请求,清空缓存的pendingRequest对象。
router.beforeEach((to, from, next) => {
clearPending();
// ...
next();
});
最后
关注公众号【前端技术驿站】让我们共同进步吧!我整理了一些项目实战视频,欢迎来学习!