请求超时重试封装
1. 基础版本 - 带指数退避的重试机制
interface RetryConfig {maxRetries?: number; // 最大重试次数baseDelay?: number; // 基础延迟时间(ms)timeout?: number; // 单次请求超时时间(ms)retryCondition?: (error: any) => boolean; // 重试条件
}class RetryableRequest {private config: Required<RetryConfig>;constructor(config: RetryConfig = {}) {this.config = {maxRetries: config.maxRetries ?? 3,baseDelay: config.baseDelay ?? 1000,timeout: config.timeout ?? 10000,retryCondition: config.retryCondition ?? this.defaultRetryCondition};}private defaultRetryCondition(error: any): boolean {// 网络错误、超时、5xx 状态码时重试return !error.response || error.code === 'ECONNABORTED' || error.response.status >= 500;}private sleep(ms: number): Promise<void> {return new Promise(resolve => setTimeout(resolve, ms));}private calculateDelay(retryCount: number): number {// 指数退避算法:baseDelay * 2^retryCount + 随机抖动const exponentialDelay = this.config.baseDelay * Math.pow(2, retryCount);const jitter = Math.random() * 1000; // 0-1000ms 随机抖动return exponentialDelay + jitter;}async request<T>(requestFn: () => Promise<T>,customConfig?: Partial<RetryConfig>): Promise<T> {const config = { ...this.config, ...customConfig };let lastError: any;for (let retryCount = 0; retryCount <= config.maxRetries; retryCount++) {try {// 创建超时 Promiseconst timeoutPromise = new Promise<never>((_, reject) => {setTimeout(() => reject(new Error('Request timeout')), config.timeout);});// 竞速:请求 vs 超时const result = await Promise.race([requestFn(), timeoutPromise]);return result;} catch (error: any) {lastError = error;// 检查是否应该重试const shouldRetry = retryCount < config.maxRetries && config.retryCondition(error);if (shouldRetry) {const delay = this.calculateDelay(retryCount);console.warn(`请求失败,${delay}ms后重试 (${retryCount + 1}/${config.maxRetries}):`, error.message);await this.sleep(delay);continue;}break;}}throw lastError;}
}
2. 增强版本 - 支持不同策略和事件监听
enum RetryStrategy {EXPONENTIAL_BACKOFF = 'exponential', // 指数退避FIXED = 'fixed', // 固定间隔LINEAR = 'linear' // 线性增长
}interface EnhancedRetryConfig {maxRetries?: number;baseDelay?: number;timeout?: number;strategy?: RetryStrategy;retryCondition?: (error: any) => boolean;onRetry?: (retryCount: number, error: any, delay: number) => void;onSuccess?: (result: any, retryCount: number) => void;onFailure?: (error: any, retryCount: number) => void;
}class EnhancedRetryableRequest {private config: Required<EnhancedRetryConfig>;constructor(config: EnhancedRetryConfig = {}) {this.config = {maxRetries: config.maxRetries ?? 3,baseDelay: config.baseDelay ?? 1000,timeout: config.timeout ?? 10000,strategy: config.strategy ?? RetryStrategy.EXPONENTIAL_BACKOFF,retryCondition: config.retryCondition ?? this.defaultRetryCondition,onRetry: config.onRetry ?? (() => {}),onSuccess: config.onSuccess ?? (() => {}),onFailure: config.onFailure ?? (() => {})};}private defaultRetryCondition(error: any): boolean {// 可重试的错误类型const retryableErrors = ['ECONNABORTED', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNRESET', 'ECONNREFUSED'];if (error.code && retryableErrors.includes(error.code)) {return true;}if (error.response) {// 5xx 服务器错误或 429 太多请求return error.response.status >= 500 || error.response.status === 429;}// 网络错误、超时等return error.message?.includes('timeout') || error.message?.includes('network');}private calculateDelay(retryCount: number): number {const baseDelay = this.config.baseDelay;switch (this.config.strategy) {case RetryStrategy.FIXED:return baseDelay;case RetryStrategy.LINEAR:return baseDelay * (retryCount + 1);case RetryStrategy.EXPONENTIAL_BACKOFF:default:const exponentialDelay = baseDelay * Math.pow(2, retryCount);const jitter = Math.random() * baseDelay * 0.1; // 10% 随机抖动return exponentialDelay + jitter;}}private sleep(ms: number): Promise<void> {return new Promise(resolve => setTimeout(resolve, ms));}async request<T>(requestFn: () => Promise<T>,customConfig?: Partial<EnhancedRetryConfig>): Promise<T> {const config = { ...this.config, ...customConfig };let lastError: any;let retryCount = 0;for (; retryCount <= config.maxRetries; retryCount++) {try {// 创建超时控制器const controller = new AbortController();const timeoutId = setTimeout(() => controller.abort(), config.timeout);let result: T;// 如果请求函数支持 signal,传递 abort signalif (typeof requestFn === 'function') {const requestResult = requestFn();// 检查是否是 fetch 风格的请求if (requestResult && typeof (requestResult as any).catch === 'function') {// 普通 Promiseresult = await Promise.race([requestResult,new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Request timeout')), config.timeout))]);} else {result = await requestResult;}} else {throw new Error('Invalid request function');}clearTimeout(timeoutId);// 请求成功if (retryCount > 0) {config.onSuccess(result, retryCount);}return result;} catch (error: any) {clearTimeout(timeoutId);lastError = error;// 检查是否应该重试const shouldRetry = retryCount < config.maxRetries && config.retryCondition(error);if (shouldRetry) {const delay = this.calculateDelay(retryCount);// 触发重试事件config.onRetry(retryCount + 1, error, delay);console.warn(`请求失败,${delay}ms后第${retryCount + 1}次重试:`, error.message);await this.sleep(delay);continue;}break;}}// 所有重试都失败了config.onFailure(lastError, retryCount);throw lastError;}// 便捷方法:创建不同策略的实例static createExponential(config: Omit<EnhancedRetryConfig, 'strategy'> = {}) {return new EnhancedRetryableRequest({ ...config, strategy: RetryStrategy.EXPONENTIAL_BACKOFF });}static createFixed(config: Omit<EnhancedRetryConfig, 'strategy'> = {}) {return new EnhancedRetryableRequest({ ...config, strategy: RetryStrategy.FIXED });}static createLinear(config: Omit<EnhancedRetryConfig, 'strategy'> = {}) {return new EnhancedRetryableRequest({ ...config, strategy: RetryStrategy.LINEAR });}
}
3. Axios 集成版本
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';class AxiosRetryAdapter {private axiosInstance: AxiosInstance;private retryableRequest: EnhancedRetryableRequest;constructor(axiosInstance?: AxiosInstance, retryConfig?: EnhancedRetryConfig) {this.axiosInstance = axiosInstance || axios.create();this.retryableRequest = new EnhancedRetryableRequest(retryConfig);this.setupInterceptors();}private setupInterceptors() {// 请求拦截器 - 添加重试逻辑this.axiosInstance.interceptors.request.use((config) => {// 如果配置了重试,使用重试逻辑if (config.retryConfig) {const originalRequest = () => this.axiosInstance.request(config);return this.retryableRequest.request(originalRequest, config.retryConfig).then(response => {// 重试逻辑已经处理,直接返回结果throw new axios.Cancel('Request handled by retry logic');}).catch(error => {if (axios.isCancel(error) && error.message === 'Request handled by retry logic') {// 这是预期的取消,实际结果在重试逻辑中处理return Promise.reject(error);}throw error;});}return config;},(error) => Promise.reject(error));}// 扩展 AxiosRequestConfig 类型request<T = any>(config: AxiosRequestConfig & { retryConfig?: Partial<EnhancedRetryConfig> }): Promise<AxiosResponse<T>> {if (config.retryConfig) {const requestFn = () => this.axiosInstance.request(config);return this.retryableRequest.request(requestFn, config.retryConfig);}return this.axiosInstance.request(config);}get<T = any>(url: string, config?: AxiosRequestConfig & { retryConfig?: Partial<EnhancedRetryConfig> }): Promise<AxiosResponse<T>> {return this.request({ ...config, method: 'GET', url });}post<T = any>(url: string, data?: any, config?: AxiosRequestConfig & { retryConfig?: Partial<EnhancedRetryConfig> }): Promise<AxiosResponse<T>> {return this.request({ ...config, method: 'POST', url, data });}// 其他 HTTP 方法...
}
4. 使用示例
// 1. 基础使用
const retryRequest = new EnhancedRetryableRequest({maxRetries: 3,baseDelay: 1000,timeout: 5000,onRetry: (retryCount, error, delay) => {console.log(`第${retryCount}次重试,原因: ${error.message}`);}
});// 2. 使用 fetch
async function fetchWithRetry() {try {const response = await retryRequest.request(() => fetch('https://api.example.com/data').then(res => {if (!res.ok) throw new Error(`HTTP ${res.status}`);return res.json();}));console.log('获取数据成功:', response);} catch (error) {console.error('所有重试都失败了:', error);}
}// 3. 使用 Axios
const axiosRetry = new AxiosRetryAdapter(axios, {maxRetries: 3,strategy: RetryStrategy.EXPONENTIAL_BACKOFF
});async function axiosWithRetry() {try {const response = await axiosRetry.get('https://api.example.com/data', {retryConfig: {maxRetries: 2,retryCondition: (error) => {// 只在网络错误和 500 状态码时重试return !error.response || error.response.status >= 500;}}});console.log('请求成功:', response.data);} catch (error) {console.error('请求失败:', error);}
}// 4. 不同策略的实例
const fixedRetry = EnhancedRetryableRequest.createFixed({maxRetries: 5,baseDelay: 2000
});const linearRetry = EnhancedRetryableRequest.createLinear({maxRetries: 3,baseDelay: 1000
});// 5. 模拟测试
async function testRetry() {let attempt = 0;const result = await retryRequest.request(async () => {attempt++;console.log(`第${attempt}次尝试`);if (attempt < 3) {throw new Error('模拟失败');}return { data: '成功数据' };});console.log('最终结果:', result);
}
5. React Hook 版本
import { useState, useCallback } from 'react';export function useRetryableRequest(config?: EnhancedRetryConfig) {const [loading, setLoading] = useState(false);const [error, setError] = useState<any>(null);const [data, setData] = useState<any>(null);const [retryCount, setRetryCount] = useState(0);const retryableRequest = new EnhancedRetryableRequest({...config,onRetry: (count, error, delay) => {setRetryCount(count);config?.onRetry?.(count, error, delay);},onSuccess: (result, count) => {setRetryCount(0);config?.onSuccess?.(result, count);},onFailure: (error, count) => {setRetryCount(count);config?.onFailure?.(error, count);}});const execute = useCallback(async <T>(requestFn: () => Promise<T>) => {setLoading(true);setError(null);try {const result = await retryableRequest.request(requestFn);setData(result);return result;} catch (err) {setError(err);throw err;} finally {setLoading(false);}}, [retryableRequest]);return {execute,loading,error,data,retryCount,reset: () => {setLoading(false);setError(null);setData(null);setRetryCount(0);}};
}// React 使用示例
function MyComponent() {const { execute, loading, error, data, retryCount } = useRetryableRequest({maxRetries: 3});const fetchData = async () => {try {await execute(() => fetch('/api/data').then(res => res.json()));} catch (err) {// 错误处理}};return (<div><button onClick={fetchData} disabled={loading}>{loading ? `加载中... (重试 ${retryCount})` : '获取数据'}</button>{error && <div>错误: {error.message}</div>}{data && <div>数据: {JSON.stringify(data)}</div>}</div>);
}
主要特性
- 多种重试策略: 指数退避、固定间隔、线性增长
- 智能重试条件: 网络错误、超时、5xx状态码自动重试
- 超时控制: 单次请求超时和总超时控制
- 事件监听: 重试、成功、失败事件回调
- 随机抖动: 避免惊群效应
- TypeScript 支持: 完整的类型定义
- 框架集成: 支持 Axios、Fetch、React Hook
这个封装提供了灵活的重试机制,可以根据具体需求调整重试策略和参数,有效处理网络不稳定的情况。