开发过程中,我们有几种场景的比较容易忽视。
重复请求:
- 用户提交表单时按钮重复点击。
- 移动端的列表下拉和上拉加载时,意味着两个相同路径的请求(仅页码参数不同)先后发出。我们必须保留后一个请求,否则就可能出现,上拉的数据和下拉的数据先后返回并渲染在列表里,且可能二者顺序错误。
请求需要取消:
- 页面请求尚未结束,路由已切换,此时请求仍在发送中。
针对1这种按钮重复点击问题,有三种方案解决:
- 最简单粗暴的方法,在代码中对提交过程加锁,按钮点击后加锁知道请求结束才能重新点击。
- 利用防抖,对短时间内产生的点击只响应其最后一次。具体可参见 防抖和节流 。
防抖虽然可以解决短时间内的连击问题,但是考虑一种场景:假设请求由于各种原因响应非常慢,那么依然可以在防抖时间后再次发出请求,依然可以制造出重复请求,所以根本上还是要解决重复请求的问题。
3.利用请求拦截器,对重复请求进行拦截。这里以最常用的axios
来举例子。
// 使用map来保存每个请求的CancelToken
const pendingRequest = new Map();
const getRequestKey = (config) => {
// let key = {
// method: config.method,
// url: config.url,
// params: config.params,
// data: config.data
// }
// key = qs.stringify(key)
// 注意这里是用来判断重复请求的标准(比如路径相同且请求方法相同即视为重复请求),也可以根据业务需要进行更改。
const key = `${config.url}/request_type=${config.method}`;
return key;
};
const isRequestExist = (config) => {
const requestKey = getRequestKey(config);
return pendingRequest.has(requestKey);
};
const addPendingRequest = (config) => {
const requestKey = getRequestKey(config);
config.requestKey = requestKey;
config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
if (!isRequestExist(config)) {
pendingRequest.set(requestKey, cancel);
}
});
};
const cancelRequest = (requestKey) => {
const cancel = pendingRequest.get(requestKey);
cancel(requestKey);
};
const removePendingRequest = (config) => {
const requestKey = config.requestKey;
if (pendingRequest.has(requestKey)) {
pendingRequest.delete(requestKey);
}
// cancelRequest(requestKey)
};
const clearPending = () => {
for (const [requestKey, cancel] of pendingRequest) {
cancel(requestKey);
}
pendingRequest.clear();
};
const allowRepeatRequest = (url) => {
const result = false;
for (const path of allowRepeatRequestPaths) {
if (url.indexOf(path) > -1) {
return true;
}
}
return result;
};
const service = axios.create({
baseURL: process.env.VUE_APP_BASIC_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 60000, // request timeout
});
// HTTPrequest拦截
service.interceptors.request.use((config) => {
if (config.showLoading) {
context.$showLoading();
}
if (!allowRepeatRequest(config.url)) {
// 注意这里是取消旧的重复请求,重新发送新的,根据业务需要也可以取消新请求,保留旧请求。
if (isRequestExist(config)) {
cancelRequest(getRequestKey(config));
removePendingRequest(config);
}
addPendingRequest(config);
}
const isToken = (config.headers || {}).isToken === false;
const token = getToken();
if (token && !isToken) {
config.headers.Authorization = `Bearer ${token}`;// token
}
return config;
}, (error) => {
// 网络波动导致错误
console.warn('request error');
clearPending();
return Promise.reject(error);
});
service.interceptors.response.use((response) => {
removePendingRequest(response.config);
if (response.config && response.config.showLoading) {
context.$closeLoading();
}
return response;
}, (error) => {
if (axios.isCancel(error)) {
console.warn(`repeated request: ${error.message}`);
return Promise.reject(error);
}
if (error && error.response && error.response.status === 401) {
EventBus.$emit(EventType.UNAUTHORIZED);
}
clearPending();
return Promise.reject(error);
});
所以,对于重复请求,我们可以简单粗暴采取方案1进行处理,但是缺点是重复代码比较多,代码侵入性比较强,不推荐使用。更推荐方案2+3结合来使用,其实单单3已经可以拦截所有重复请求了,2可以降低3的拦截次数。 至于请求取消,已经在上述代码中包含了,有需要的话直接在路由守卫里进行处理就好。