封装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();
*/
这个封装做了什么:
- 创建独立实例: 使用
axios.create
创建了一个新的 Axios 实例apiClient
,避免污染全局axios
。 - 设置默认值: 可以方便地设置
baseURL
,timeout
,headers
等默认配置。 - 请求拦截器: 在每次请求发出前,自动初始化一个
_retryCount
属性到请求配置config
中,用于跟踪重试次数。 - 响应拦截器 (核心):
- 成功响应 (
status 2xx
): 直接返回响应。 - 错误响应:
- 检查错误是否包含
response
和config
对象。 - 检查
response.status
是否为503
。 - 检查当前
config._retryCount
是否小于MAX_RETRIES
。 - 如果满足重试条件:
- 增加
_retryCount
。 - 调用
calculateRetryDelay
计算延迟时间(优先Retry-After
,后备指数退避)。 - 使用
await delay()
等待。 - 使用
apiClient(config)
重新发起原始请求 ,并将返回的 Promise 传递下去。
- 增加
- 如果不满足重试条件(非 503、达到最大次数等):直接拒绝 原始的
error
。
- 检查错误是否包含
- 成功响应 (
- 延迟计算 (
calculateRetryDelay
): 封装了计算延迟时间的逻辑,包括解析Retry-After
(秒数和日期格式) 和实现指数退避。 - 导出实例: 导出配置好的
apiClient
实例供其他模块使用。
如何使用:
在你的项目中,导入这个 apiClient
实例,然后像使用普通 axios
一样使用它即可(例如 apiClient.get('/users')
, apiClient.post('/login', data)
)。当遇到 503 错误时,它会自动在后台进行重试。你的业务代码只需要处理最终成功的结果或最终(所有重试都失败后)的错误。