Axios 封装:实现 503 自动重试与 Retry-After 支持

封装Axios,处理 503 Service Unavailable 错误,并包含自动重试逻辑,同时会尝试遵循 Retry-After 响应头。

javascript 复制代码
import axios from 'axios';

// --- 配置常量 ---
const MAX_RETRIES = 3; // 最大重试次数
const INITIAL_RETRY_DELAY_MS = 1000; // 初始重试延迟(毫秒),用于指数退避
const MAX_RETRY_DELAY_MS = 10000; // 最大重试延迟(毫秒),例如 10 秒

/**
 * 创建一个延迟指定毫秒数的 Promise
 * @param {number} ms - 延迟的毫秒数
 * @returns {Promise<void>}
 */
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

/**
 * 计算重试延迟时间 (毫秒)
 * 优先使用 Retry-After 头,否则使用指数退避
 * @param {number} retryCount - 当前是第几次重试 (从 1 开始)
 * @param {object | null | undefined} responseHeaders - Axios 响应头对象 (通常是小写 key)
 * @returns {number} - 计算出的延迟毫秒数
 */
const calculateRetryDelay = (retryCount, responseHeaders) => {
  let delayMs = 0;
  const retryAfterHeader = responseHeaders?.['retry-after']; // Axios header keys are often lowercase

  if (retryAfterHeader) {
    // 尝试解析秒数
    const retryAfterSeconds = parseInt(retryAfterHeader, 10);
    if (!isNaN(retryAfterSeconds)) {
      delayMs = retryAfterSeconds * 1000;
      console.log(`[Axios Retry] Using Retry-After header (seconds): ${retryAfterSeconds}s`);
    } else {
      // 尝试解析 HTTP 日期格式
      try {
        const retryDate = new Date(retryAfterHeader);
        if (!isNaN(retryDate.getTime())) {
          const waitTime = retryDate.getTime() - Date.now();
          delayMs = Math.max(0, waitTime); // 确保延迟非负
          console.log(`[Axios Retry] Using Retry-After header (date): ${retryAfterHeader}. Wait ${delayMs}ms`);
        }
      } catch (e) {
        console.warn('[Axios Retry] Could not parse Retry-After header date format:', retryAfterHeader);
      }
    }
  }

  // 如果没有有效的 Retry-After,或者计算出的延迟为 0 或负数,则使用指数退避
  if (delayMs <= 0) {
    delayMs = Math.min(
      INITIAL_RETRY_DELAY_MS * (2 ** (retryCount - 1)), // 指数增长
      MAX_RETRY_DELAY_MS // 不超过最大延迟
    );
    console.log(`[Axios Retry] Using Exponential Backoff. Wait ${delayMs}ms`);
  }

  // 再次确保不超过最大延迟
  return Math.min(delayMs, MAX_RETRY_DELAY_MS);
};

// --- 创建并配置 Axios 实例 ---
const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_URL || '/api', // 设置你的基础 URL
  timeout: 15000, // 设置一个合理的请求超时时间
  headers: {
    'Content-Type': 'application/json',
    // 你可以在这里添加其他默认请求头
  }
});

// --- 请求拦截器:初始化重试计数 ---
apiClient.interceptors.request.use(
  (config) => {
    // 为每个新请求初始化重试计数器
    config._retryCount = 0;
    // 你也可以在这里添加通用的请求头,如 Auth Token
    // const token = localStorage.getItem('token');
    // if (token) {
    //   config.headers.Authorization = `Bearer ${token}`;
    // }
    return config;
  },
  (error) => {
    // 请求配置出错,直接拒绝
    console.error('[Axios Request Interceptor Error]', error);
    return Promise.reject(error);
  }
);

// --- 响应拦截器:处理 503 和重试逻辑 ---
apiClient.interceptors.response.use(
  (response) => {
    // 对于成功的响应 (2xx),直接返回
    return response;
  },
  async (error) => { // 错误处理函数改为 async
    const { config, response } = error;

    // 1. 检查是否需要重试:
    //    - 请求配置存在
    //    - 响应存在且状态码为 503
    //    - 当前重试次数小于最大次数
    if (config && response?.status === 503 && config._retryCount < MAX_RETRIES) {
      // 增加重试计数
      config._retryCount += 1;

      // 计算延迟时间
      const delayMs = calculateRetryDelay(config._retryCount, response.headers);

      console.log(`[Axios Retry] Attempt ${config._retryCount}/${MAX_RETRIES} for ${config.url} after ${delayMs}ms delay due to 503.`);

      // 等待延迟
      await delay(delayMs);

      // 重新发起请求,传递原始配置 (带有更新后的 _retryCount)
      // 返回这个 Promise,使得调用者能接收到重试成功的结果或最终的失败
      console.log(`[Axios Retry] Retrying request: ${config.url}`);
      return apiClient(config); // 使用同一个 apiClient 实例重新请求
    }

    // 2. 如果不需要重试(非 503、达到最大次数、或其他错误),则拒绝 Promise
    if (response?.status === 503) {
        console.error(`[Axios Retry] Max retries (${MAX_RETRIES}) reached for 503 on ${config?.url}. Rejecting.`, error);
    } else {
        console.error('[Axios Response Interceptor Error]', error.message); // 记录其他错误
    }
    // 将原始错误传递下去
    return Promise.reject(error);
  }
);

// --- 导出配置好的 Axios 实例 ---
export default apiClient;

使用示例:

js 复制代码
// --- 使用示例 ---
/*
import apiClient from './apiClient'; // 导入封装好的实例

async function fetchData() {
  try {
    const response = await apiClient.get('/your/endpoint');
    console.log('Data fetched successfully:', response.data);
  } catch (error) {
    if (error.response) {
      // 服务器返回了响应,但状态码不是 2xx
      console.error(`API Error ${error.response.status}:`, error.response.data);
      // 这里捕获的是所有重试都失败后的最终错误
      if (error.response.status === 503) {
          alert('服务器暂时不可用,请稍后再试。');
      } else {
          // 处理其他错误...
          alert(`请求失败: ${error.message}`);
      }
    } else if (error.request) {
      // 请求已发出,但没有收到响应 (网络错误等)
      console.error('Network Error:', error.message);
      alert('网络错误,请检查您的连接。');
    } else {
      // 设置请求时发生了一些事情,触发了错误
      console.error('Request Setup Error:', error.message);
      alert(`请求设置错误: ${error.message}`);
    }
  }
}

fetchData();
*/

这个封装做了什么:

  1. 创建独立实例: 使用 axios.create 创建了一个新的 Axios 实例 apiClient,避免污染全局 axios
  2. 设置默认值: 可以方便地设置 baseURL, timeout, headers 等默认配置。
  3. 请求拦截器: 在每次请求发出前,自动初始化一个 _retryCount 属性到请求配置 config 中,用于跟踪重试次数。
  4. 响应拦截器 (核心):
    • 成功响应 (status 2xx): 直接返回响应。
    • 错误响应:
      • 检查错误是否包含 responseconfig 对象。
      • 检查 response.status 是否为 503
      • 检查当前 config._retryCount 是否小于 MAX_RETRIES
      • 如果满足重试条件:
        • 增加 _retryCount
        • 调用 calculateRetryDelay 计算延迟时间(优先 Retry-After,后备指数退避)。
        • 使用 await delay() 等待。
        • 使用 apiClient(config) 重新发起原始请求 ,并将返回的 Promise 传递下去
      • 如果不满足重试条件(非 503、达到最大次数等):直接拒绝 原始的 error
  5. 延迟计算 (calculateRetryDelay): 封装了计算延迟时间的逻辑,包括解析 Retry-After (秒数和日期格式) 和实现指数退避。
  6. 导出实例: 导出配置好的 apiClient 实例供其他模块使用。

如何使用:

在你的项目中,导入这个 apiClient 实例,然后像使用普通 axios 一样使用它即可(例如 apiClient.get('/users'), apiClient.post('/login', data))。当遇到 503 错误时,它会自动在后台进行重试。你的业务代码只需要处理最终成功的结果或最终(所有重试都失败后)的错误。

相关推荐
_一条咸鱼_1 小时前
深入解析 Vue API 模块原理:从基础到源码的全方位探究(八)
前端·javascript·面试
患得患失9491 小时前
【前端】【难点】前端富文本开发的核心难点总结与思路优化
前端·富文本
执键行天涯1 小时前
在vue项目中package.json中的scripts 中 dev:“xxx“中的xxx什么概念
前端·vue.js·json
雯0609~1 小时前
html:文件上传-一次性可上传多个文件,将文件展示到页面(可删除
前端·html
涵信1 小时前
2024年React最新高频面试题及核心考点解析,涵盖基础、进阶和新特性,助你高效备战
前端·react.js·前端框架
mmm.c1 小时前
应对多版本vue,nvm,node,npm,yarn的使用
前端·vue.js·npm
混血哲谈1 小时前
全新电脑如何快速安装nvm,npm,pnpm
前端·npm·node.js
天天扭码1 小时前
项目登录注册页面太丑?试试我“仿制”的丝滑页面(全源码可复制)
前端·css·html
桂月二二2 小时前
Vue3服务端渲染深度实战:SSR架构优化与企业级应用
前端·vue.js·架构
萌萌哒草头将军2 小时前
🚀🚀🚀 这六个事半功倍的 Pinia 库,你一定要知道!
前端·javascript·vue.js