后端接口不响应该怎么办??axios-retry是一个很不错的选择,附带源码解析!

415 阅读13分钟

前言

挺久没写文章,最近下班后都在打瓦罗兰特,一直在黄金一和白银三徘徊,感觉已经要废了,所以也没啥时间写文章。工作上最近也是换了一个组,之前主要是干web,现在是在写sass中台和h5,然后也是负责一个小迭代,整体其实就是一个curd,但是也是遇到一些奇奇怪怪的坑,有一个我觉得还是很有含金量的,然后我是用了一个第三包解决的,然后也顺带去看了一下这个包的源码,也学到不少的东西,记录分享一下。

请求不响应后重新请求

在我现在这个项目中,对于一些请求,他的生命周期会比较长。正常来说,我们只需要和一个服务端请求,服务端收到后就返回。但是这里是,前端对服务端a请求后,服务端a还要向服务端b去请求,服务器a只能等待服务器b响应后再给我们前端响应,所以就会存在请求不响应超时的问题,如果是偶发性的还好,但是频率好像还挺高的,就是可能调同一个接口10次,有3次是不响应的。然后我这是将网络禁用去模拟的一个效果。

12345.gif

解决方案

解决方案也挺简单的,就是服务端a在5s内如果收不到服务端b的响应,就会给前端报timeout的错误,我这边如果收到timeout的错误就是重新请求,指数型去重试请求5次,如果还是不成功就只能给用户提示“请求超时,请重新提交了”。

代码

axios-retry地址 www.npmjs.com/package/axi…

这里使用了axios-retry,这个包就是可以二次封装axios实例去实现重新请求。正常来说,我们的项目中都会对axios进行封装,如下代码,去对请求拦截器和响应拦截器做一些公共处理。

import axios from 'axios'
const http = axios.create({
  headers: {},
  timeout: 5 * 1000 // 请求超时时间
})
// 请求拦截器
http.interceptors.request.use(
  (config) => {
    console.log(config)
    return config
  },
  (err) => {
    console.log(err)
    return Promise.reject(err)
  }
)
// 响应拦截器
http.interceptors.response.use(
  (response) => {
    return response
  },
  (error) => {
    return Promise.reject(error)
  }
)
export default http

这里我们就可以得到一个axios实例http,axios-retry就可以对这个实例进行封装实现重新请求

import axios from 'axios'
import axiosRetry from 'axios-retry'   
const api = axios.create({
  headers: {},
  timeout: 5 * 1000 // 请求超时时间
})
// 请求拦截器
api.interceptors.request.use(
  (config) => {
    console.log(config)
    return config
  },
  (err) => {
    console.log(err)
    return Promise.reject(err)
  }
)
// 响应拦截器
api.interceptors.response.use(
  (response) => {
    // 将index变回0 
    index = 0                         
    return response
  },
  (error) => {
    return Promise.reject(error)
  }
)
axiosRetry(api, {
  retries: 5,
  shouldResetTimeout: true,
  retryDelay: (retryCount) => {
    // retryCount为重试的次数
    return retryCount * 1000
  },
  retryCondition: err => {
    console.log(err)
    index++
    if (index === 5) {
      // 超过五次进行提示就不进行请求
      Toast('请求超时,请重新提交')
      index = 0
      return false
    } else {
      if (err.message.includes('timeout')) return true
      return false
    }
  }
})
export default api

axiosRetry

对于axiosRetry来说,我们只需要去配置下面参数就行

  1. retries 重试次数
  2. retryCondition 重试条件,返回ture就允许重试,返回false就不允许重试
  3. shouldResetTimeout 是否重置超时,ture代表每次重试都重置超时时间 false则相反
  4. retryDelay 延迟重试时间,需要返回一个时间
  5. onRetry 每次重试时执行的回调函数
  6. onMaxRetryTimesExceeded 当达到最大重试次数后执行的回调函数
  7. validateResponse 用于验证响应是否有效的函数

可以看到,我代码中定义闭包了一个index变量,在retryCondition中去判断是否为等于5,如果等于5就不进行重试并提示给用户进行重新提交请求,这里要注意的是要维护好index这个变量,在请求成功后变回0。本来我是想在onMaxRetryTimesExceeded 这个配置项去写逻辑的,但是不知道为什么没有执行这里面的逻辑,我也没去研究了,能实现效果就行了。

源码

86a536fc339d03e39c6dfbba688f53f.jpg 大家可以npm上这个位置去看源码,大家如果感兴趣,最好还是自己去看一下源码,我的分析可能比较片面,而且我本身技术也就那样。这个第三方包的源码相比vue的源码其实还算简单的,没有那么复杂。虽然简单,我看的也很头大。如果你要继续往下面看,就需要你对axios本身有一丢丢了解才行,就比如请求拦截器,响应拦截器这些。当然也能继续看,就是可能会有点迷迷糊糊的,但是肯定是能学到东西的!然后我是将代码的逻辑通过注释是写在代码里面了,所以要先看一下代码块里面的东西

axiosRetry

这个方法就是这个第三方包的主函数,我先说下这个包整体上的实现逻辑,在请求拦截器和响应拦截器中维护一个对象,在响应拦截器中,通过这个对象中一些信息去判断要不要重新请求。可以看到这个函数接收两个参数,一个是axiosInstanceaxios实例,一个defaultOptions也就是我在使用axios-retry配置的配置项。可以看下面的代码,从大的方面来看就是一个请求拦截器,一个响应拦截器,最后将这两个拦截器给return了。

const axiosRetry = (axiosInstance, defaultOptions) => {
    
    // 请求拦截器
    const requestInterceptorId = axiosInstance.interceptors.request.use((config) => {
        
        setCurrentState(config, defaultOptions, true);
        
        // 这一段代码可以向不看,这个是为了实现配置项上的validateResponse的功能
        // =====1
        if (config[namespace]?.validateResponse) {
            config.validateStatus = () => false;
        }
        // =====1
        return config;
    });
    
    // 响应拦截器
    const responseInterceptorId = axiosInstance.interceptors.response.use(null, async (error) => {
        const { config } = error;
        // 如果没有config,无法判断是否需要重新请求,直接返回错误
        if (!config) {
            return Promise.reject(error);
        }
        const currentState = setCurrentState(config, defaultOptions);
        
        // 这一段可以先不看,为了实现配置项上的validateResponse的功能
        // =====2
        if (error.response && currentState.validateResponse?.(error.response)) {
            // 如果响应没问题(通过 validateResponse 验证)则直接返回响应
            return error.response;
        }
        // =====2
            
        // 根据是否满足重试条件来决定是执行重试操作(调用 handleRetry 函数)
        if (await shouldRetry(currentState, error)) {
            return handleRetry(axiosInstance, currentState, error, config);
        }
        
        // 这一段可以先不看,为了实现配置项上的 onMaxRetryTimesExceeded 的功能
        // =====3 
        // 在达到最大重试次数后执行相应回调(调用 handleMaxRetryTimesExceeded 函数)
        await handleMaxRetryTimesExceeded(currentState, error);
        // =====3
            
        return Promise.reject(error);
    });
    
    
    return { requestInterceptorId, responseInterceptorId };
};

除了===之间的内容不看后,拦截器里面剩下的就很简单了,在请求拦截器中,就是调用了setCurrentState这个方法,要想理解setCurrentState这个方法,我们得先知道对于一个axois,发起请求是有一个config对象,这个对象里面包括像请求头,请求方式等等的一些字段,所以这个我们可以理解成一个给config对象中添加属性的方法,源码如下。前面我们说过,这个包的整体思路就是在请求拦截器和响应拦截器维护一个对象,而这个对象就是config中的某一个属性,也就是config[namespace]。namespace是一个变量,变量值为axios-retry,也就是config中叫axios-retry的属性

setCurrentState

这个方法接收三个参数,一个是axois请求的配置,一个用户的配置,一个是否需要重置上次请求时间。可以看下面的代码,一开始是调用了getRequestOptions的这个方法,这个方法就一个合并对象的方法,合并的对象就是我们前面所说的在请求拦截器和响应拦截器维护的那个对象。它是将,我们axios-retry默认配置用户的配置以及config[namespace](也就是维护的那个对象)合并成一个对象。整体去看setCurrentState这个方法,可以分为1,2,3步,分别对应着拿变量,改变量,存变量,就和维护变量的操作一模一样。

function setCurrentState(config, defaultOptions, resetLastRequestTime = false) {
    // 合并配置参数 getRequestOptions方法在下面----------------------------1
    const currentState = getRequestOptions(config, defaultOptions || {});
    
    // 初始化或更新重试次数,retryCount就是记录当前重试的次数
    // 如果currentState中没有这个变量,就是第一次请求,有就使用这个变量--------2
    currentState.retryCount = currentState.retryCount || 0;
    // 更新上次请求时间
    if (!currentState.lastRequestTime || resetLastRequestTime) {
        currentState.lastRequestTime = Date.now();
    }
    
    // 赋值给config配置项 namespace就是一个变量,在下面的代码,这就是维护变量的操作---------3
    config[namespace] = currentState;
    
    return currentState;
}
​
// 合并默认配置,就是将默认的,用户设置的,和config中的配置合并
function getRequestOptions(config, defaultOptions) {
    return { ...DEFAULT_OPTIONS, ...defaultOptions, ...config[namespace] };
}
​
// 下面这些代码可以先不看
//===============================================================
// 定义添加config对象中的属性名
export const namespace = 'axios-retry';
​
// 默认配置对象 isNetworkOrIdempotentRequestError和noDelay是一个默认方法,
// 大家感兴趣可以去看源码,因为如果用户有配置的话,就是使用用户配置的回调函数
export const DEFAULT_OPTIONS = {
    retries: 3,
    retryCondition: isNetworkOrIdempotentRequestError,
    retryDelay: noDelay,
    shouldResetTimeout: false,
    onRetry: () => { },
    onMaxRetryTimesExceeded: () => { },
    validateResponse: null
};

我们结合上面的axiosRetry来看,在请求拦截器和响应拦截器都使用了这个方法,也就是说在每一次请求的时候都去更新维护config[namespace]对象。这也就是为啥一直在说核心就是请求拦截器和响应拦截器维护一个对象,那为什么要维护这个对象呢?别急,马上就来了!我们再回去看响应拦截器,除开1,2,3段可以先不看,就剩下下面两行代码,这段代码也就是这个包的核心代码,这段代码主要使用了shouldRetryhandleRetry两个方法,可以看到这两个方法都使用了currentState这个变量,这个变量就是我们一直强调的那个'维护的对象'。shouldRetry方法是用来判断要不要重新的请求,而handleRetry是用来重新请求的方法。

// 根据是否满足重试条件来决定是执行重试操作(调用 handleRetry 函数)
if (await shouldRetry(currentState, error)) {
   return handleRetry(axiosInstance, currentState, error, config);
}

维护的对象

说这么多,这个'维护的对象'到底是什么,我们在请求拦截器中打印一下config这个对象,可能大家已经忘了config是啥,config就是我们在请求拦截器回调接收的那个参数,也就是axois发起请求的配置。在控制台可以看到其中会有axios-retry这样的一个属性,也就是namespace变量的值。我们一直在维护的也是这个axios-retry的值。这个对象里面有重试次数,上次请求时间,重试条件,重试回调等等,也就是我们所配置的那些东西。也就是形参currentState需要的值。

4c1e7f499eb2cf8f57c6f81c317b5d1.png

shouldRetry

async function shouldRetry(currentState, error) {
    // 从currentState拿到retries, retryCondition
    const { retries, retryCondition } = currentState;
    // 如果没超过重试次数,然后通过retryCondition去判断,根据这两个去判断要不要重新请求
    const shouldRetryOrPromise = (currentState.retryCount || 0) < retries && retryCondition(error);
    
    
    // 这一段代码是为了兼容retryCondition可能是promise的值,就要去等待他执行完成
    // =========1
    if (typeof shouldRetryOrPromise === 'object') {  // 这可能是一个promise
        try {
            const shouldRetryPromiseResult = await shouldRetryOrPromise;
            // 保持 return true,除非 shouldRetryPromiseResult 返回 false 以实现兼容性
            return shouldRetryPromiseResult !== false;
        }
        catch (_err) {
            return false;
        }
    }
    // ========1
    return shouldRetryOrPromise;
}

这个方法其实很简单,就是通过重试次数,以及用户配置的retryCondition回调,去得到一个布尔值。整体逻辑大家应该都能看得懂,这里需要给大家讲一下error是什么,error就是在响应拦截器中请求失败的回调的传参,也就是当axios请求失败报错的那个值。下面这张图可以看到这个error中也是有config属性的,也有axios-retry的,这很重要!

0d1965952bb44c759b3f1602cc3b155.png

handleRetry

这个方法就是实现重试的方法,接收四个参数,分别是axiosInstance axios实例,currentState就是config中的axios-retry属性,也就是维护的那个对象,error就是上面那个error,config就是那个config,之前都有提过。

async function handleRetry(axiosInstance, currentState, error, config) {
    
    // 重试次数加1
    currentState.retryCount += 1;
    const { retryDelay, shouldResetTimeout, onRetry } = currentState;
    
    // 执行retryDelay,也就是用户配置的那个retryDelay
    const delay = retryDelay(currentState.retryCount, error);
    
    // 修复config======可以不看,为了兼容,感兴趣的可以去细看源码
    fixConfig(axiosInstance, config);
    
    // 这一段代码是为实现用户配置shouldResetTimeout是否重置超时时间的功能
    // 如果是false,也就是不进行重置超时时间,所以这里要去更新config中的timeout。
    // 如果是ture就不进入这个if,不对timeout做处理
    if (!shouldResetTimeout && config.timeout && currentState.lastRequestTime) {
        const lastRequestDuration = Date.now() - currentState.lastRequestTime;
        const timeout = config.timeout - lastRequestDuration - delay;
        if (timeout <= 0) {
            return Promise.reject(error);
        }
        config.timeout = timeout;
    }
    
    // config.transformRequest是对请求数据进行处理,这里的意思就是传入了什么,就用什么数据。
    // 这行代码是为了重置转换函数。
    config.transformRequest = [(data) => data];
    
    // 执行onRetry,也就是用户配置的onRetry
    await onRetry(currentState.retryCount, error, config);
    
    // config.signal是AbortController产生的,AbortController是提供取消异步操作的一个js接口。
    // 这里所有关于config.signal都是为了兼容,兼容用户对请求进行主动取消的情况下。
    // 他是去监听abort事件,因为如果用户需要主动取消请求,会去触发abort事件
    // 这里是做了一个防抖以及监听事件和取消监听。
    // 如果没有接触过,可以直接把这些相关代码(1,2,3,4)先删了再去看,
    // 把这些删了之后发现就只剩下了一个定时器和axiosInstance(config)。
    // axiosInstance(config)就是重新请求。
    
    // =============1
    if (config.signal?.aborted) {
        return Promise.resolve(axiosInstance(config));
    }
    // =============1
    
    return new Promise((resolve) => {
        // =============2
        const abortListener = () => {
            clearTimeout(timeout);
            resolve(axiosInstance(config));
        };
        // =============2
        
        // delay是上面retryDelay得出的东西
        const timeout = setTimeout(() => {
            resolve(axiosInstance(config));
            
            // =============3
            if (config.signal?.removeEventListener) {
                config.signal.removeEventListener('abort', abortListener);
            }
            // =============3
        }, delay);
        
        // =============4
        if (config.signal?.addEventListener) {
            config.signal.addEventListener('abort', abortListener, { once: true });
        }
        //  =============4
    });
}

梳理

看到这里大家可能明白了,可能很懵。因为我这是对核心源码一行一行的去注释,可能并不能将整条线连起来,所以我这用文字去总结一下。首先,我们先将重试作为主线,去看重试是怎么实现的。还是那个'维护的对象',这个对象串联了整条线,这个对象包括我们的重试条件,重试回调等等这些方法。我们先在请求拦截器中和响应拦截器中都是使用了setCurrentState去维护这个对象,然后再响应拦截器中去通过shouldRetry去判断该不该重试,再通过handleRetry去重试。而这两个方法实现的前提就是这个'维护的对象'。比如该不该重试,是通过用户配置的retryCondition和重试次数去判断的,再比如怎么去重试,是通过axios实例配合config参数再次请求。其次,我们再通过我们配置的参数,去看重试这条主线的支线,也就是retryDelay重试延时时间,shouldResetTimeout是否重置超时时间,onRetry重试回调,onMaxRetryTimesExceeded最大重试次数后执行的回调,validateResponse验证响应内容。这些在上面的代码注释中,我都有标明在哪里实现的。

总结

axios-retry这个包是很不错的,可以在无响应报错的时候进行重新请求。在前后端交互的时候,或多或少都会遇到接口不响应超时的问题。而在一些很需要的接口响应的场景,是很实用的,然后源码看不看懂其实都无所谓,会用就行,而且我们这种底层前端的工作内容基本都是curd,根本不用去造轮子。不过看源码也是有很多好处的,比如怎么封装包能让用户有更多的扩展性,像这里的retryCondition和onRetry就不错。还有可以增加自己的自信心的,这包一周是有三百万人在使用的,感觉也没有多复杂,我又觉得我行了(手动狗头)。最后,来个赋能哥带我上分呗,我真打不上去啊。