一、现象
最近突然群里报障,说统计数据不对,看日志发现请求的时间和前端显示的时间不同,但无法复现这个情况。
分析问题
仔细查看日志发现,有多个请求,原来当选择不同的时间范围时,如果用户连续操作,发起多个API请求,API数据量不同,响应的时间不同,造成当前数据被覆盖。
出现这个问题,有两个优化方案:
- 当点击搜索后,需要增加loading效果,禁止在没有返回数据之前,再次点击搜索。但这样有个问题,就是如果筛选的时间段太长时,接口响应的时间长,用户体验是很不好的。
- 所以此时就需要增加优化,允许用户在长时间请求没有响应时,主动触发取消当前请求。
二、解决问题
axios配置重复请求,取消下发,取消下发有两种方式:
Axios在v0.22.0之前推出CancelToken实现取消请求,在21年10月后,推荐用AbortController来实现取消请求,两者用法基本一致。
1、AbortController实现取消请求
const controller = new AbortController();
axios.get('/foo/bar',
{ signal: controller.signal }
).then(
function(response) { //... }
);
// cancel the request
controller.abort()
兼容性
很明显,AbortController不支持IE,Chorme的最低版本也要66。
2、CancelToken(从v0.22.0被废弃)
虽然已经被废弃了,但针对兼容旧浏览器时,还是要用CancelToken。
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{
// error
}
}))
axios.post('/usr/12345', {
name: 'new name'
},{
cancelToken: source.token
})
// 取消请求
source.cancel('Operation canceled by the user.');
我们按照上面的代码来实现,调整网络为低速3G,效果如下:
- 当点击调用接口时,正常请求API
- 连续点击,出现多次请求,点击取消请求,请求终止
- 再次点击发起请求,请求失效
三、在项目中有几种场景如何实现
由于上面两种操作方式基本一样,下面的操作我们以AbortController为例实现
那么如何解决取消请求后无法再次请求?很简单,下面我们看看最简单的方式是什么,面对实际工作中的场景,我们应该怎么去处理?
如果按上一步的做,当我取消请求时,由于对应的signal已经被使用,发起的请求会直接取消,所以我们需要给每次请求都生成不同的signal。
1、取消请求、允许再次发起
对应的场景包括:
- 长时间请求,允许用户手动取消
- 同一个请求,如果用户发出了多次,只保留最后一次
- 取消一批请求
假设有个请求,我们封装的时候,预留一下signal参数。
// request.js
export const getProject = ({ signal }) => {
return axios.get('http://xxx/v1/project', {
params: {},
signal
}).then(function(response) {
return response.data;
}).catch(error => {
if (axios.isCancel(error)) {
console.log('Request canceled', error.message);
} else {
console.error('Error:', error);
}
});
}
在实际请求时,为每一个请求创建new AbortController(),
signal唯一标识,方便后续取消请求。
// CancelRequest.js
import { Button } from 'antd';
import { getProject } from '../../api/request';
let ProjectController = {}
const getController = () => {
const controller = new AbortController();
const signal = controller.signal
return {
controller,
signal
}
}
function CancelRequest() {
const handleClick = () => {
// 如果需要在重复点击时,取消之前的请求,可以在请求之前调用handleCancel
// handleCancel();
const { controller, signal } = getController()
const uniqueKey = Date.now(); // 使用当前时间戳作为唯一键
ProjectController[uniqueKey] = controller;
getProject({ signal }).then(res => {
console.log("res", res)
})
}
const handleCancel = () => {
// 取消请求
if(Object.keys(ProjectController).length){
Object.keys(ProjectController).forEach(key => {
ProjectController[key].abort()
});
ProjectController = {};
}
}
return (
<div className="App">
<Button onClick={handleClick}>获取项目</Button>
<Button onClick={handleCancel}>取消请求</Button>
</div>
);
}
export default CancelRequest
2、取消一批请求
有时候,我们等待的接口是多个组合在一起的,取消的时候要同时取消,此时可以给这一批接口相同的signal,当点击取消时,没有请求返回的接口会同时取消。
四、原理
我们查看axios的源码,浏览器的请求可以在lib/adapters/xhr.js中查看,我们可以看到cancelToken和signal是如何取消请求的。
if (_config.cancelToken || _config.signal) {
// Handle cancellation
// eslint-disable-next-line func-names
onCanceled = cancel => {
if (!request) {
return;
}
reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
request.abort();
request = null;
};
_config.cancelToken && _config.cancelToken.subscribe(onCanceled);
if (_config.signal) {
_config.signal.aborted ? onCanceled() : _config.signal.addEventListener('abort', onCanceled);
}
}
- axios判断cancelToken存在,则会订阅onCanceled方法,当onCanceled触发后,将会取消请求,这里的取消请求,不会影响向服务器发起的请求,只会在浏览器侧终止。
- axios会判断signal是否存在,如果存在则会判断aborted是否为true,如果是true,则终止请求