axios请求取消、请求竞态、请求重试

发布于:2024-04-26 ⋅ 阅读:(21) ⋅ 点赞:(0)

请求取消

我们在处理前后端交互的过程中,有时需要仔细斟酌接口的请求时机(例:频繁的Tab切换、树节点切换、数据录入时,请求什么时候发?)或接口返回数据的处理时机(例:接口还没返回时就要切换路由,路由都切换走了,之前请求的数据怎么办?),避免一些无用的请求或者接口返回顺序的差异(例如:同一个按钮点了多次,如果后点的先返回,先点的后返回,怎么办?)。 常见的处理方式有: 加防抖:控制请求时机。对于频繁操作,只在最后一次动作时,发出请求。 锁状态:控制请求时机。直接禁止很频繁的操作,必须一个接一个。 取消请求:控制请求处理时机。取消之前没返回的请求,不再处理了。

AbortController

AbortController 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。 我们先使用 AbortController() 构造函数创建一个控制器,然后使用 AbortController.signal 属性获取其关联 AbortSignal 对象的引用。 当 fetch 请求初始化时,我们将 AbortSignal 作为一个选项传递进入请求的选项对象中(下面的 {signal})。这将 signal 和 controller 与 fetch 请求相关联,并且允许我们通过调用 AbortController.abort() 去中止它。

const controller = new AbortController();

fetch('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});
// 取消请求
controller.abort()

// 当 abort() 被调用时,这个 fetch() promise 将 reject 一个名为 AbortError 的 DOMException。

Axios

Axios 也支持以 fetch API 方式通过 AbortController 取消请求:

const controller = new AbortController();

axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});
// 取消请求
controller.abort()
// 如果取消了请求,那么不管这个请求服务端有没有在处理,浏览器端都不会再去关注这个请求,通常情况
下是不会接受到服务端的任何请求的,但是如果网络状态不好的话,服务端可能还在处理这个请求并最终完成
响应并发送数据,这些数据也将被丢弃,是无效的。

使用 Axios 您还可以使用 cancel token 取消一个请求。 (不推荐)

import axios from 'axios'
 
// 注意:AbortController是fetch API提供的,不需要从axios引入
// isCancel用于判断请求是不是被AbortController取消的
const { isCancel } = axios
 
// 请求队列,缓存发出的请求
const cacheRequest = {}
// 不进行重复请求拦截的白名单
const cacheWhiteList = ['/foo/bar&get']
 
// axios实例
const service = axios.create({
  baseURL: '/api',
  headers: {
    'Content-Type': 'application/json; charset=UTF-8'
  },
})
 
// 请求拦截
service.interceptors.request.use((config) => {
    const { url, method } = config
    // 请求地址和请求方式组成唯一标识,将这个标识作为取消函数的key,保存到请求队列中
    const reqKey = `${url}&${method}`
    // 如果存在重复请求,删除之前的请求
    if (cacheWhiteList.indexOf(reqKey) === -1) {
      removeCacheRequest(reqKey)
      // 将请求加入请求队列,通过AbortController来进行手动取消
      const controller = new AbortController()
      config.signal = controller.signal
      cacheRequest[reqKey] = controller
    }
})
 
// 响应拦截
service.interceptors.response.use(
    (response) => {
        // 请求成功,从队列中移除
        const { url, method } = response.config
        removeCacheRequest(`${url}&${method}`)
    },
    (error) => {
        // 请求失败,使用isCancel来区分是被CancelToken取消,还是常规的请求失败
        if (isCancel(error)) {
          // 通过CancelToken取消的请求不做任何处理
          return Promise.reject({
            message: '重复请求,自动拦截并取消'
          })
        } else{
            // 正常请求发生错误,抛出异常等统一提示
            console.log(error.response, 'errMsg')
        }
    }
)
 
/**
 * @desc 删除缓存队列中的请求
 * @param {String} reqKey 本次请求的唯一标识 url&method
 */
function removeCacheRequest(reqKey) {
  if (cacheRequest[reqKey]) {
    // 通过AbortController实例上的abort来进行请求的取消
    cacheRequest[reqKey].abort()
    delete cacheRequest[reqKey]
  }
}

请求重试

为什么需要请求重试

项目中,经常会有很多用户的网络抽风或者各种原因造成偶发性的网络异常请求错误,如果没有重试机制,有时候体验就比较糟糕。这个时候实现网络错误请求错误重试也能比较好的解决这种偶发场景。

如何去做呢

我们可以使用这个库去实现重试。用法也非常简单

import axiosRetry from 'axios-retry';
axiosRetry(axios, {});

直接执行axiosRetry传递axios实例即可。同时它会支持几个配置参数

  • retries: 重试次数,默认是3次
  • retryCondition:一个函数判断发生错误时是否重试。默认是5xxhttp 错误或者网络异常或者是幂等请求(GET/HEAD/ OPTIONS/PUT/DELETE)才会重试。
  • shouldResetTimeout:重试的时候是否重置超时时间。默认不重置。也就是说多次重试请求必须在timeout内结束
  • retryDelay每个请求之间的重试延迟时间,默认为0

例如,如果我想定制,重试4次、除了默认情况重试外,404也重试、重置超时时间、重试延迟时间50ms,则这样即可

import axiosRetry from 'axios-retry';
axiosRetry(axios, {
    retries: 4,
    retryCondition: (err) => axiosRetry.isNetworkOrIdempotentRequestError(err) || error.response.status === 404,
    shouldResetTimeout: true,
    retryDelay: 50
});

实现原理

axios-retry实现重试的原理也比较简单

  • axios-retry会在axios的config的axios-retry字段中保存当前已经重试的次数(retryCount)
  • axios会在http异常/网络异常的情况下抛出错误。axios-retry则在响应拦截器中注册错误处理函数,执行retryCondition判断是否需要进行重试。如果需要重试则对retryCount进行++操作,然后返回一个Prommise使用当前的config重新发起一次新的请求new Promise(resolve => setTimeout(() => resolve(axios(config)), delay));。如果当前不需要重试(retryCondition返回false或者已经超过重试次数的场景,直接reject这个错误对象)
  axios.interceptors.response.use(null, error => {
    const config = error.config;
    // ....
    const currentState = getCurrentState(config);
    const shouldRetry = retryCondition(error) && currentState.retryCount < retries;
​
    if (shouldRetry) {
      currentState.retryCount += 1;
        //.....
​
      return new Promise(resolve => setTimeout(() => resolve(axios(config)), delay));
    }
​
    return Promise.reject(error);
  });

业务场景

在实际场景中,很多时候http请求成功并不说明我们的请求就符合预期的。有以下子几种场景,如果直接使用axios-retry是无法触发重试的

  • 业务code异常 以笔者实际项目为例,后端返回异常时,http code为200,但是返回code非0的错误,如{code:1,msg:'some err'}。有的时候可能是一些偶发错误,这个时候可能也需要重试
  • 异步接口返回不符合预期 假设以下场景。首先操作a先上传了视频;紧接这b操作去查询这个视频的信息,可能刚上传完,后端一些信息落db的时候有延迟。偶发的我们马上查询的时候可能查不到这个信息,需要延迟个几ms才能查到。例如查不到返回{code:0,data:null} 查到返回code:0,data:'some thing'。这个时候重试也是很重要了

如何优雅重试

上文提到axios-retry的重试原理是通过响应拦截器的错误处理函数去实现的,那么我们在响应拦截器的正常处理函数中抛出这个这个错误是否可以呢?当然是可以的。

  • 给axios的config加一个自定义选项函数判断是否需要重试
  • 在响应拦截器中调用判断函数,若需要重试,设置一个标志位,Promise.reject抛出一个错误
  instance.interceptors.response.use((response) => {
    const { data, config, request } = response
​
    if (config?.[namespace]?.shouldRetry?.(data)) {
      config[namespace].needRetry = true
      return Promise.reject(
        createError(
          `Axios retry enhance error`,
          config,
          null,
          request,
          response
        )
      )
    }
    return response
  })
  • axios-retry的retryCondition读取到上一步的属性返回true,即可利用axios-retry进行重试
  axiosRetry(instance, {
    ...config,
    retryCondition: (error) => {
      const {
        retryCondition = axiosRetry.isNetworkOrIdempotentRequestError,
      } = config
      return retryCondition(error) || error.config?.[namespace]?.needRetry
    },
  })

于是,代码调用的时候只需如下即可

client.get<Result>('http://example.com/test', {
  retry: {
    // The request will retry when the code isn't 0 even the http code is 200
    shouldRetry: (res: Result) => res.code !== 0,
  },
})

请求竞态

视频内容: