实现带重试机制的接口请求(原生js方案&Axios方案)

1,158 阅读8分钟

背景

在实际的软件开发过程中,网络请求是我们经常需要处理的异步任务。但由于网络波动或服务端的不稳定,请求可能会偶然失败,即使没有明显的错误。在这种情况下,一个常见的解决策略是实施一个重试机制,这样,在网络请求失败时,系统会自动尝试再次进行请求,而不是直接返回错误。

前置知识

要理解本文的代码实现,读者需要具备以下前置知识:

  • Promise: JavaScript 的 Promise 对象用于表示异步操作的最终完成(或失败)及其结果值。
  • setTimeout: setTimeout 是一个定时器函数,它可以在指定的毫秒数后执行函数。
  • Math.random: Math.random 方法返回一个0到1之间的随机数。
  • Async/await: 这是一个用于异步函数的 JavaScript 新特性,使得处理 Promises 更加简洁易懂。

思路

我们希望创建一个函数,当一个 Promise 返回的异步操作失败时,可以自动重试指定次数。如果在所有重试尝试后操作仍然失败,则最终返回一个失败的 Promise。成功则立即返回成功结果。

实现

我们创建两种不同的重试实现。retry1 使用 .then.catch 方法链对 Promise 进行处理,retry2 使用 async/await 进行更现代的异步处理。在每种实现中,我们使用 setTimeout 来实现重试延迟,并且保证延迟时间是递增的。

这两个函数 retry1retry2 的目的是封装了一个简单的重试逻辑,允许对任意返回 Promise 的函数进行重试。如果函数 fn 成功执行(即 Promise 被解决),则立即解决整个 Promise。如果 fn 失败(即 Promise 被拒绝),根据指定的重试次数 times 和每次重试之间的延迟 delay 进行重试。如果所有尝试都失败,最终 Promise 会被拒绝,附带错误信息 '多次尝试失败'

重要的区别在于实现方法:retry1 采用 Promise 链的方式,而 retry2 则采用了更现代的 async/await 语法。尽管两者在功能上相似,但 async/await 的方式通常被认为在阅读和理解上更直观。

// 定义一个带有重试逻辑的函数 retry1
function retry1(fn, times = 0, delay = 0) {
  // 返回一个新的 Promise
  return new Promise((resolve, reject) => {
    // 定义内部函数 inner 用于执行传入的函数 fn
    let inner = function () {
      return fn() // 执行函数 fn
        .then(resolve) // 如果成功,直接使用外部 Promise 的 resolve 方法解决这个 Promise
        .catch((error) => { // 如果失败,进入 catch
          if (times-- > 0) { // 检查是否还有剩余的重试次数
            setTimeout(inner, delay * 2); // 如果有,等待 delay*2 ms 后重新调用 inner 以重试
          } else { 
            reject('多次尝试失败', error); // 如果没有剩余重试次数,使用外部 Promise 的 reject 方法拒绝这个 Promise
          }
        });
    };
    inner() // 初次调用 inner 函数以启动逻辑
  });
}

// 定义第二个带有重试逻辑的函数 retry2,与 retry1 类似,但使用 async/await 实现
function retry2(fn, times = 0, delay = 0) {
  // 返回一个新的 Promise
  return new Promise((resolve, reject) => {
    // 定义异步的内部函数 inner
    let inner = async function () {
      try {
        const result = await fn(); // 尝试执行 fn,并等待其结果
        resolve(result); // 如果 fn 执行成功,使用 resolve 方法解决外部 Promise
      } catch (error) { // 如果执行失败,捕获到异常
        if (times-- > 0) { // 检查剩余重试次数
          setTimeout(inner, delay * 2); // 如果还有剩余次数,等待 delay*2 ms 后调用 inner 以进行重试
        } else {
          reject('多次尝试失败', error); // 如果没有剩余次数,用 reject 拒绝外部 Promise 并提供错误信息
        }
      }
    };
    inner(); // 初次调用 inner 函数以启动逻辑
  });
}

Demo

为了演示这一点,我们首先定义一个 req 函数,它以50%的概率模拟异步操作的成功或失败。这是通过比较 Math.random() 生成的随机数和预设的概率 rate 来实现的。

const rate = 0.8; // 设定失败率
// 封装一个50%失败可能的方法
function req() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let res = Math.random();
      console.log('res', res);
      res > rate ? resolve(res) : reject(res);
    });
  });
}

// 首先尝试 `retry1` 函数
retry1(req, 3, 1000)
  .then((data) => console.log('成功', data))
  .catch((err) => console.log('失败', err));

// 可以类似地测试 `retry2` 函数

axios

Axios 是一个流行的 HTTP 客户端,它基于 Promise,用于在浏览器和 node.js 中发送 HTTP 请求。Axios 本身并不直接提供重连(重试)机制,但是因为它是基于 Promise 的,你可以很容易地为它添加自定义的重试逻辑。

要实现重连机制,你可以利用 Axios 的拦截器功能或者直接封装请求函数来添加重试逻辑。通过拦截器,你可以在请求发生错误时(如4xx、5xx状态码或网络错误)进行拦截,并实现自定义的重试逻辑。

下面是一个示例代码,说明了如何使用 Axios 拦截器为请求添加简单的重试机制:

import axios from 'axios';

// 创建一个带有重试机制的 Axios 实例
const axiosInstance = axios.create();

// 设置重试次数和重试延迟
const MAX_RETRIES = 3;   // 最大重试次数
const RETRY_DELAY = 1000; // 重试之间的延迟,单位ms

// 添加响应拦截器
axiosInstance.interceptors.response.use(undefined, (err) => {
  const config = err.config;
  // 如果请求未设置不重试,或未达到最大重试次数,则重试请求
  if (!config._retry && (!config.maxRetries || config.maxRetries > 0)) {
    config._retry = true; // 设置重试标志
    config.maxRetries = config.maxRetries || MAX_RETRIES; // 设置或获取最大重试次数
    // 根据重试次数和设定的延迟进行重试
    let backoff = new Promise((resolve) => {
      setTimeout(() => {
        resolve();
      }, RETRY_DELAY || 1);
    });
    // 返回重试请求的 Promise
    return backoff.then(() => {
      return axiosInstance(config);
    });
  }
  // 如果不需要重试,则reject错误
  return Promise.reject(err);
});

在这个拦截器中,我们捕获了错误响应,并根据设定的重试策略决定是否重试请求。如果错误响应满足重试条件,我们将在指定延时后再次发送请求。

使用这个 Axios 实例进行请求,它将自动进行重试,直到达到最大重试次数或请求成功。

axiosInstance.get('/example')
  .then(response => {
    console.log('Success:', response);
  })
  .catch(error => {
    console.log('Error after max retries:', error);
  });

此外,还有第三方库如 axios-retry 可以为 Axios 请求提供自动重试功能,它提供更灵活的配置选项,包括重试次数、重试条件和指数退避策略等。

总之,虽然 Axios 不自带重试机制,但通过自定义代码或第三方库的帮助,为 Axios 请求添加重试是相对简单的。

指数退避是一种网络协议中常见的错误恢复策略,它在重试请求时采用递增的延迟,常用于处理临时性的问题,以给予系统或网络恢复的时间。

要在拦截器中应用递增的延迟重试策略,需要记录重试的次数,并计算延迟值。下面是如何修改你的代码来实现指数退避的例子:

import axios from 'axios';
// 创建一个带有重试机制的 Axios 实例
const axiosInstance = axios.create();
// 设置重试次数
const MAX_RETRIES = 3;   // 最大重试次数

// 添加响应拦截器
axiosInstance.interceptors.response.use(undefined, (err) => {
  const config = err.config;
  // 确保重试次数是配置中的属性
  if (!config._retryCount) {
    config._retryCount = 0;
  }
  
  // 如果未达到最大重试次数,则重试请求
  if (config._retryCount < MAX_RETRIES) {
    config._retryCount++; // 增加重试次数
    // 计算重试延迟,采用指数退避策略
    const retryDelay = Math.pow(2, config._retryCount) * 1000; // retryDelay成倍增加

    // 创建一个等待指定延迟的 Promise
    const backoff = new Promise((resolve) => {
      setTimeout(() => {
        resolve();
      }, retryDelay);
    });
    
    // 返回重试请求的 Promise
    return backoff.then(() => {
      return axiosInstance(config);
    });
  }
  
  // 如果达到最大重试次数,reject错误
  return Promise.reject(err);
});

在这段拦截器代码中,我们在配置对象中添加了一个 _retryCount 属性,用来记录当前已重试的次数。每次重试发生时,我们会递增这个计数,并按照指数函数计算延迟(例如,2 的次数方乘以 1000 毫秒)。

请注意,这里设置的初始延迟时间应根据实际的应用案例进行调整。如果指数增长可能导致过长的等待时间,可以通过设置上限值来防止延迟时间变得过长。

这种增加延迟的策略能有效避免在网络波动或服务短暂不可用时的冲击,帮助服务有机会在后续的重试中成功恢复。

总结

在现代 web 开发中,面对不稳定的网络环境,合理使用重试机制能显著提升用户体验。本文介绍了如何利用原生 JavaScript 搭配 Promise 构建一个简单的重试逻辑,同时也演示了使用 async/await 以更清晰的方式来处理异步操作。所有的代码都通过浏览器的控制台日志进行了演示,并且可以根据实际需求进行调整和增强。

通过构建这样的重试机制,我们可以为用户提供更加稳定和可靠的应用服务,进而改善他们的总体体验。记住,良好的错误处理和重试策略是任何健壮的网络服务必须考虑的。