请求超时重试封装

请求超时重试封装

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 {
        // 创建超时 Promise
        const 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 signal
        if (typeof requestFn === 'function') {
          const requestResult = requestFn();
          
          // 检查是否是 fetch 风格的请求
          if (requestResult && typeof (requestResult as any).catch === 'function') {
            // 普通 Promise
            result = 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>
  );
}

主要特性

  1. 多种重试策略: 指数退避、固定间隔、线性增长
  2. 智能重试条件: 网络错误、超时、5xx状态码自动重试
  3. 超时控制: 单次请求超时和总超时控制
  4. 事件监听: 重试、成功、失败事件回调
  5. 随机抖动: 避免惊群效应
  6. TypeScript 支持: 完整的类型定义
  7. 框架集成: 支持 Axios、Fetch、React Hook

这个封装提供了灵活的重试机制,可以根据具体需求调整重试策略和参数,有效处理网络不稳定的情况。

posted @ 2025-10-14 12:18  阿木隆1237  阅读(14)  评论(0)    收藏  举报