Axios取消请求 - AbortController和CancelToken

1,589 阅读4分钟

一、现象

最近突然群里报障,说统计数据不对,看日志发现请求的时间和前端显示的时间不同,但无法复现这个情况。

5b3a41d37a7a31311d339260ea86d8fd.jpg

分析问题

仔细查看日志发现,有多个请求,原来当选择不同的时间范围时,如果用户连续操作,发起多个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()

兼容性

image.png

很明显,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
  • 连续点击,出现多次请求,点击取消请求,请求终止
  • 再次点击发起请求,请求失效

request.gif

三、在项目中有几种场景如何实现

由于上面两种操作方式基本一样,下面的操作我们以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,当点击取消时,没有请求返回的接口会同时取消。

cg40e-0os3j.gif

四、原理

我们查看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,则终止请求

五、参考