1.场景
- 站点管理代客下单计算运费,弱网环境下,多次调用计算运费接口,后一次请求比前几次请求响应快,拿到的运费值不是最新的,导致运费不正确。
- 联想组件,多次调用接口响应时序问题。
2.解决方案
在发起后一次请求的时候,将上一次未完成的请求cancel掉。
3.官网例子
可以使用 CancelToken.source 工厂方法创建 cancel token,像这样:
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 {
// 处理错误
}
});
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
还可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token:
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});
// cancel the request
cancel();
那我们如何确保取消的是上一次请求,而不是本次请求呢。
4.实例
import React, { useRef } from 'react';
import axios from 'axios';
const Test = () => {
/**存放取消的请求方法*/
const ref = useRef<any>(null);
/**取消请求*/
const cancelQuest = () => {
if (typeof ref.current === 'function') {
ref.current('终止请求'); // 取消请求
}
};
/** 请求方法 */
const loadData = (value: string): Promise<any[]> => {
return new Promise((resolve, reject) => {
/**执行请求操作*/
cancelQuest();
axios.get('/waybill/xxx', {
cancelToken: new axios.CancelToken((c) => {
ref.current = c;
}),
})
.then((res: any) => {
if (res.code === 200) {
const { list = [] } = res?.data;
resolve(list);
} else {
resolve([]);
}
})
.catch((e) => {
if (axios.isCancel(e)) {
console.log('请求取消', e.message);
} else {
reject(e);
}
});
});
};
return(<>xxxx</>)
}
5.如何在拦截器中配置
- 设置一个列表pendingList,用于存储当前处于pending的请求
const pendingList = new Map();
- 提供getFetchKey方法,用于生成各个请求的标识,当为GET请求时,因为只用于获取数据,因此只要当method和url都一致时,我们就可以认为这是同一请求,而其他请求则还需要加上请求的参数。
const getFetchKey = (config) => {
const { useCancelToken } = config;
//useCancelToken 用于配置该接口是否需要检测查复请求
if (useCancelToken) {
const { headers, url, data, method, params } = config;
let token;
if (method === 'GET') {
token = [method, url].join('&');
} else {
token = [method, url, JSON.stringify(data), JSON.stringify(params)].join('&');
}
return token;
}
};
- 添加请求到pendingList
const addPending = (config) => {
const fetchKey = getFetchKey(config);
if (fetchKey) {
config.cancelToken =
config.cancelToken ||
new axios.CancelToken((cancel) => {
if (!pendingList.has(fetchKey)) {
pendingList.set(fetchKey, cancel);
}
});
}
};
- 移除请求从pendingList
const removePending = (config) => {
const fetchKey = getFetchKey(config);
if (fetchKey) {
if (pendingList.has(fetchKey)) {
pendingList.delete(fetchKey);
}
}
};
- 移除并取消该请求从pendingList
const cancelPending = (config) => {
const fetchKey = getFetchKey(config);
if (fetchKey) {
if (pendingList.has(fetchKey)) {
const cancel = pendingList.get(fetchKey);
cancel(fetchKey);
pendingList.delete(fetchKey);
}
}
};
- 在拦截器中添加以上方法
axios.interceptors.request.use((config) => {
//发送请求前首先检查该请求是否已经重复,重复则进行取消并移除
cancelPending(config);
//添加该请求到pendingList中
addPending(config);
return config;
});
axios.interceptors.response.use((response) => {
const config = response.config;
//请求完成后移除该请求
removePending(config);
return response;
});
- 最后,因取消请求抛出的error我们不应该返回给用户,使用axios.isCancel()判断当前请求是否是主动取消的
axios.(options).then(...)..catch((error) => {
if (axios.isCancel(error)) {
console.warn('repeated request: ' + error.message);
return;
}
reject(error);
});
6.效果
更详细的api文档请前往axios