王者技能之最新Axios + TS + Element Plus 企业级二次封装(完整版)

大家好,我是鱼樱!!!

关注公众号【鱼樱AI实验室】持续每天分享更多前端和AI辅助前端编码新知识~~喜欢的就一起学反正开源至上,无所谓被诋毁被喷被质疑文章没有价值~

一个城市淘汰的自由职业-农村前端程序员(虽然不靠代码挣钱,写文章就是为爱发电),兼职远程上班目前!!!热心坚持分享~~~

今天大家分享一份企业级axios二次封装~ 并且推荐一个双越老师的划水AI项目 有需要的可以私我走优惠通道~

你是否平时封装axios应该如何封装,需不需要封装(基本上需要的简单封装也是需要不然代码乱的不堪入目),封装哪些功能合适无从下手,网上都是一些最简单基础的请求拦截,响应拦截,哈哈哈看了非常无趣不如看官网,今天基于本人多年经验总结的一个参考方向,不喜勿喷给大家分享一个个人思路。。。

本方案基于 axios 最新版 实现完整的企业级 HTTP 请求库封装,支持各种高级功能和优化配置。

完整项目结构

md 复制代码
src/
  └── utils/
      └── request/
          ├── index.ts           // 导出请求实例
          ├── request.ts         // 核心请求类
          ├── types.ts           // 类型定义
          ├── cancel.ts          // 取消请求控制器
          ├── loading.ts         // loading 管理
          ├── toast.ts           // 消息提示管理
          ├── error-handler.ts   // 错误处理
          ├── token-manager.ts   // token 管理
          └── network.ts         // 网络状态监测

1. 类型定义 (types.ts)

typescript 复制代码
import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';

/**
 * 请求配置选项接口,提供了更细粒度的控制
 */
export interface RequestOptions {
  // 是否显示loading,默认:true
  showLoading?: boolean;
  // loading的文本,默认:'加载中...'
  loadingText?: string;
  // loading的配置
  loadingOptions?: {
    lock?: boolean;
    background?: string;
    fullscreen?: boolean;
  };
  // 是否允许取消重复请求,默认:true
  cancelRepeatRequest?: boolean;
  // 是否自动刷新token,默认:true
  autoRefreshToken?: boolean;
  // 是否需要处理状态码,默认:true
  handleErrorStatus?: boolean;
  // 是否显示错误提示,默认:true
  showErrorMessage?: boolean;
  // 下载文件时的文件名
  fileName?: string;
  // 自定义错误处理函数
  errorHandler?: (error: any) => void;
  // 请求超时时间(ms),默认:15000
  timeout?: number;
  // 是否在断网时加入请求队列,默认:true
  queueWhenOffline?: boolean;
  // 在断网恢复后是否自动重试,默认:true
  retryWhenOnline?: boolean;
  // 失败自动重试次数,默认:0(不重试)
  retryCount?: number;
  // 重试间隔(ms),默认:1000
  retryDelay?: number;
}

/**
 * 扩展AxiosRequestConfig,添加自定义配置
 */
export interface CustomAxiosRequestConfig extends AxiosRequestConfig {
  requestOptions?: RequestOptions;
}

/**
 * 扩展InternalAxiosRequestConfig,添加内部使用的字段
 */
export interface CustomInternalAxiosRequestConfig extends InternalAxiosRequestConfig {
  requestOptions?: RequestOptions;
  requestId?: string; // 用于标识请求,取消重复请求时使用
  retryCount?: number; // 当前已重试次数
}

/**
 * 基础响应格式
 */
export interface BaseResponse<T = any> {
  code: number;
  data: T;
  message: string;
}

/**
 * 网络状态类型
 */
export type NetworkStatus = 'online' | 'offline' | 'unknown';

/**
 * 待处理的离线请求
 */
export interface PendingRequest {
  config: CustomAxiosRequestConfig;
  resolve: (value: any) => void;
  reject: (reason?: any) => void;
}

2. 取消请求模块 (cancel.ts)

typescript 复制代码
import type { CustomInternalAxiosRequestConfig } from './types';
import axios from 'axios';

/**
 * 请求取消控制器类
 * 用于管理和取消重复或未完成的请求
 */
class AxiosCanceler {
  // 存储请求与取消令牌的Map
  private pendingMap = new Map<string, AbortController>();

  /**
   * 生成请求的唯一标识
   * @param config 请求配置
   * @returns 唯一标识字符串
   */
  private generateRequestKey(config: CustomInternalAxiosRequestConfig): string {
    const { method, url, params, data } = config;
    return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&');
  }

  /**
   * 添加请求到pendingMap
   * @param config 请求配置
   */
  public addPending(config: CustomInternalAxiosRequestConfig): void {
    // 先尝试移除同样的请求
    this.removePending(config);
    const requestKey = this.generateRequestKey(config);
    const controller = new AbortController();
    
    config.signal = controller.signal;
    config.requestId = requestKey;
    
    if (!this.pendingMap.has(requestKey)) {
      this.pendingMap.set(requestKey, controller);
    }
  }

  /**
   * 移除请求从pendingMap
   * @param config 请求配置 
   */
  public removePending(config: CustomInternalAxiosRequestConfig): void {
    const requestKey = this.generateRequestKey(config);
    if (this.pendingMap.has(requestKey)) {
      const controller = this.pendingMap.get(requestKey);
      controller?.abort('Request canceled due to duplicate request');
      this.pendingMap.delete(requestKey);
    }
  }

  /**
   * 清除所有pending请求
   */
  public clearPending(): void {
    this.pendingMap.forEach((controller) => {
      controller.abort('Request canceled due to application state change');
    });
    this.pendingMap.clear();
  }
  
  /**
   * 获取当前pending请求数量
   */
  public getPendingCount(): number {
    return this.pendingMap.size;
  }
}

export default new AxiosCanceler();

3. Loading 管理 (loading.ts)

typescript 复制代码
import { ElLoading } from 'element-plus';
import type { LoadingInstance } from 'element-plus/es/components/loading/src/loading';
import type { RequestOptions } from './types';

/**
 * Loading管理类
 * 支持配置化和计数功能,防止多个请求导致的闪烁问题
 */
class LoadingManager {
  private loadingInstance: LoadingInstance | null = null;
  private count = 0;
  private defaultOptions = {
    text: '加载中...',
    background: 'rgba(0, 0, 0, 0.7)',
    fullscreen: true,
    lock: true
  };

  /**
   * 显示loading
   * @param options 自定义配置选项
   */
  public show(options?: RequestOptions): void {
    if (this.count === 0) {
      const loadingOptions = {
        text: options?.loadingText || this.defaultOptions.text,
        background: options?.loadingOptions?.background || this.defaultOptions.background,
        fullscreen: options?.loadingOptions?.fullscreen !== undefined 
          ? options.loadingOptions.fullscreen 
          : this.defaultOptions.fullscreen,
        lock: options?.loadingOptions?.lock !== undefined 
          ? options.loadingOptions.lock 
          : this.defaultOptions.lock
      };
      
      this.loadingInstance = ElLoading.service(loadingOptions);
    }
    this.count++;
  }

  /**
   * 隐藏loading
   */
  public hide(): void {
    if (this.count <= 0) return;
    
    this.count--;
    if (this.count === 0) {
      setTimeout(() => {
        // 延迟关闭,防止闪烁
        if (this.count === 0 && this.loadingInstance) {
          this.loadingInstance.close();
          this.loadingInstance = null;
        }
      }, 300);
    }
  }

  /**
   * 强制隐藏所有loading
   */
  public forceHide(): void {
    if (this.loadingInstance) {
      this.count = 0;
      this.loadingInstance.close();
      this.loadingInstance = null;
    }
  }
  
  /**
   * 获取当前loading状态
   */
  public isLoading(): boolean {
    return this.count > 0;
  }
}

export default new LoadingManager();

4. 消息提示管理 (toast.ts)

typescript 复制代码
import { ElMessage, ElMessageBox } from 'element-plus';
import type { MessageProps } from 'element-plus';

/**
 * Toast消息管理类
 * 统一管理消息提示,防止重复提示,支持配置化
 */
class ToastManager {
  private messageInstance: { close: () => void } | null = null;
  
  /**
   * 显示消息
   * @param message 消息内容
   * @param type 消息类型 
   * @param options 配置选项
   */
  public show(
    message: string, 
    type: 'success' | 'warning' | 'info' | 'error' = 'info',
    options: Partial<MessageProps> = {}
  ): void {
    // 关闭已存在的消息,避免堆叠
    this.clear();
    
    const defaultOptions: Partial<MessageProps> = {
      duration: type === 'error' ? 5000 : 3000,
      showClose: true,
      grouping: true
    };
    
    this.messageInstance = ElMessage({
      message,
      type,
      ...defaultOptions,
      ...options
    });
  }
  
  /**
   * 显示成功消息
   */
  public success(message: string, options?: Partial<MessageProps>): void {
    this.show(message, 'success', options);
  }
  
  /**
   * 显示错误消息
   */
  public error(message: string, options?: Partial<MessageProps>): void {
    this.show(message, 'error', options);
  }
  
  /**
   * 显示警告消息
   */
  public warning(message: string, options?: Partial<MessageProps>): void {
    this.show(message, 'warning', options);
  }
  
  /**
   * 显示信息消息
   */
  public info(message: string, options?: Partial<MessageProps>): void {
    this.show(message, 'info', options);
  }
  
  /**
   * 显示确认对话框
   */
  public async confirm(
    message: string, 
    title: string = '提示', 
    options: any = {}
  ): Promise<boolean> {
    try {
      await ElMessageBox.confirm(message, title, {
        confirmButtonText: '确认',
        cancelButtonText: '取消',
        type: 'warning',
        ...options
      });
      return true;
    } catch (error) {
      return false;
    }
  }
  
  /**
   * 清除当前显示的消息
   */
  public clear(): void {
    if (this.messageInstance) {
      this.messageInstance.close();
      this.messageInstance = null;
    }
  }
}

export default new ToastManager();

5. 网络状态监测 (network.ts)

typescript 复制代码
import type { NetworkStatus, PendingRequest } from './types';
import toastManager from './toast';

/**
 * 网络状态管理类
 * 用于监测网络状态变化,管理离线请求队列
 */
class NetworkManager {
  // 当前网络状态
  private status: NetworkStatus = navigator.onLine ? 'online' : 'offline';
  // 离线请求队列
  private offlineQueue: PendingRequest[] = [];
  // 网络状态变化监听器集合
  private listeners: Set<(status: NetworkStatus) => void> = new Set();
  // 是否已初始化
  private initialized = false;
  
  constructor() {
    this.init();
  }

  /**
   * 初始化网络监听
   */
  private init(): void {
    if (this.initialized) return;
    
    // 添加网络状态监听
    window.addEventListener('online', this.handleOnline.bind(this));
    window.addEventListener('offline', this.handleOffline.bind(this));
    
    this.initialized = true;
  }

  /**
   * 处理网络恢复在线
   */
  private handleOnline(): void {
    const prevStatus = this.status;
    this.status = 'online';
    
    // 网络状态变化时通知订阅者
    if (prevStatus !== this.status) {
      this.notifyStatusChange();
      
      // 恢复在线后,处理离线请求队列
      if (this.offlineQueue.length > 0) {
        toastManager.info(`网络已恢复,正在处理 ${this.offlineQueue.length} 个待处理请求...`);
        this.processOfflineQueue();
      } else {
        toastManager.success('网络已恢复连接');
      }
    }
  }

  /**
   * 处理网络离线
   */
  private handleOffline(): void {
    const prevStatus = this.status;
    this.status = 'offline';
    
    // 网络状态变化时通知订阅者
    if (prevStatus !== this.status) {
      this.notifyStatusChange();
      toastManager.error('网络已断开,请检查您的网络连接', {
        duration: 0 // 不自动关闭
      });
    }
  }

  /**
   * 通知所有状态变化监听器
   */
  private notifyStatusChange(): void {
    this.listeners.forEach(listener => {
      listener(this.status);
    });
  }

  /**
   * 处理离线请求队列
   */
  private async processOfflineQueue(): void {
    if (this.status !== 'online' || this.offlineQueue.length === 0) return;
    
    const queue = [...this.offlineQueue];
    this.offlineQueue = [];
    
    // 逐个处理队列中的请求
    for (const item of queue) {
      try {
        // 这里不直接使用axios,而是通过resolve回调将控制权还给原请求处理流程
        const response = await fetch(item.config.url || '', {
          method: item.config.method,
          headers: item.config.headers as any,
          body: item.config.data ? JSON.stringify(item.config.data) : undefined
        }).then(res => res.json());
        
        item.resolve(response);
      } catch (error) {
        item.reject(error);
      }
    }
  }

  /**
   * 添加网络状态变化监听器
   * @param listener 状态变化回调函数
   */
  public addStatusChangeListener(listener: (status: NetworkStatus) => void): void {
    this.listeners.add(listener);
  }

  /**
   * 移除网络状态变化监听器
   * @param listener 要移除的监听器
   */
  public removeStatusChangeListener(listener: (status: NetworkStatus) => void): void {
    this.listeners.delete(listener);
  }

  /**
   * 获取当前网络状态
   */
  public getStatus(): NetworkStatus {
    return this.status;
  }

  /**
   * 添加请求到离线队列
   * @param request 请求对象
   */
  public addToOfflineQueue(request: PendingRequest): void {
    this.offlineQueue.push(request);
    toastManager.warning(`网络离线中,请求已加入队列,恢复网络后将自动发送 (${this.offlineQueue.length})`);
  }

  /**
   * 清空离线请求队列
   */
  public clearOfflineQueue(): void {
    this.offlineQueue.forEach(item => {
      item.reject(new Error('Offline request queue cleared'));
    });
    this.offlineQueue = [];
  }
  
  /**
   * 获取离线队列长度
   */
  public getOfflineQueueLength(): number {
    return this.offlineQueue.length;
  }
}

export default new NetworkManager();

6. Token 管理 (token-manager.ts)

typescript 复制代码
import { CustomAxiosRequestConfig, BaseResponse } from './types';
import toastManager from './toast';

export interface TokenInfo {
  accessToken: string;
  refreshToken: string;
  expiresIn: number;
  tokenType?: string;
}

/**
 * Token管理类
 * 处理token的存储、刷新和过期处理
 */
class TokenManager {
  private TOKEN_KEY = 'AUTH_TOKEN';
  private REFRESH_TOKEN_KEY = 'REFRESH_TOKEN';
  private TOKEN_EXPIRES_KEY = 'TOKEN_EXPIRES_AT';
  
  private isRefreshing = false;
  private refreshSubscribers: Array<(token: string) => void> = [];
  
  /**
   * 设置token信息
   * @param tokenInfo 令牌信息对象
   */
  public setToken(tokenInfo: TokenInfo): void {
    localStorage.setItem(this.TOKEN_KEY, tokenInfo.accessToken);
    localStorage.setItem(this.REFRESH_TOKEN_KEY, tokenInfo.refreshToken);
    
    // 计算过期时间并存储
    const expiresAt = Date.now() + tokenInfo.expiresIn * 1000;
    localStorage.setItem(this.TOKEN_EXPIRES_KEY, expiresAt.toString());
  }

  /**
   * 获取访问令牌
   */
  public getAccessToken(): string {
    return localStorage.getItem(this.TOKEN_KEY) || '';
  }

  /**
   * 获取刷新令牌
   */
  public getRefreshToken(): string {
    return localStorage.getItem(this.REFRESH_TOKEN_KEY) || '';
  }

  /**
   * 清除令牌
   */
  public clearToken(): void {
    localStorage.removeItem(this.TOKEN_KEY);
    localStorage.removeItem(this.REFRESH_TOKEN_KEY);
    localStorage.removeItem(this.TOKEN_EXPIRES_KEY);
  }
  
  /**
   * 检查令牌是否即将过期 (提前5分钟)
   */
  public isTokenExpiringSoon(): boolean {
    const expiresAt = Number(localStorage.getItem(this.TOKEN_EXPIRES_KEY) || 0);
    // 如果token将在5分钟内过期,则视为即将过期
    return expiresAt ? Date.now() > expiresAt - 5 * 60 * 1000 : false;
  }

  /**
   * 添加token刷新订阅者
   * @param callback token刷新后的回调
   */
  public subscribeTokenRefresh(callback: (token: string) => void): void {
    this.refreshSubscribers.push(callback);
  }
  
  /**
   * 通知所有订阅者token已刷新
   * @param token 新token
   */
  private onRefreshed(token: string): void {
    this.refreshSubscribers.forEach(callback => callback(token));
    this.refreshSubscribers = [];
  }

  /**
   * 刷新token
   * @param refreshRequest 刷新token的请求函数
   */
  public async refreshToken(
    refreshRequest: (token: string) => Promise<BaseResponse<TokenInfo>>
  ): Promise<string> {
    // 如果已经在刷新中,返回一个promise等待刷新完成
    if (this.isRefreshing) {
      return new Promise((resolve) => {
        this.subscribeTokenRefresh(resolve);
      });
    }

    this.isRefreshing = true;
    
    try {
      const refreshToken = this.getRefreshToken();
      if (!refreshToken) {
        throw new Error('No refresh token available');
      }

      // 使用传入的刷新token方法
      const response = await refreshRequest(refreshToken);
      
      if (response.code === 200 && response.data) {
        this.setToken(response.data);
        
        // 通知所有等待token刷新的请求
        this.onRefreshed(response.data.accessToken);
        return response.data.accessToken;
      } else {
        throw new Error(response.message || 'Token refresh failed');
      }
    } catch (error) {
      // 刷新失败,清除token
      this.clearToken();
      
      // 显示错误提示并跳转登录
      toastManager.error('登录已过期,请重新登录');
      
      // 延迟跳转,让用户看到提示
      setTimeout(() => {
        window.location.href = '/login';
      }, 1500);
      
      return '';
    } finally {
      this.isRefreshing = false;
    }
  }
}

export default new TokenManager();

7. 错误处理 (error-handler.ts)

typescript 复制代码
import type { AxiosError } from 'axios';
import toastManager from './toast';
import networkManager from './network';

/**
 * 错误处理类
 * 统一处理各种HTTP错误、业务错误和网络错误
 */
class ErrorHandler {
  /**
   * 处理HTTP状态码错误
   * @param status HTTP状态码
   * @param message 错误消息
   * @param showErrorMessage 是否显示错误提示
   */
  public handleStatusError(
    status: number, 
    message: string, 
    showErrorMessage: boolean = true
  ): void {
    // HTTP状态码对应的默认错误消息
    const statusMap: Record<number, string> = {
      400: '请求参数错误',
      401: '未授权,请重新登录',
      403: '拒绝访问',
      404: '请求地址不存在',
      405: '请求方法不允许',
      408: '请求超时',
      409: '资源冲突',
      410: '资源已删除',
      413: '请求实体过大',
      429: '请求过于频繁',
      500: '服务器内部错误',
      501: '服务未实现',
      502: '网关错误',
      503: '服务不可用',
      504: '网关超时',
    };
    
    const errorMessage = statusMap[status] || message || `Unknown error (${status})`;
    
    // 根据配置决定是否显示错误提示
    if (showErrorMessage) {
      toastManager.error(errorMessage);
    }
    
    // 记录错误到控制台
    console.error(`[HTTP Error ${status}]: ${errorMessage}`);
  }

  /**
   * 处理业务错误码
   * @param code 业务错误码
   * @param message 错误消息
   * @param showErrorMessage 是否显示错误提示
   */
  public handleBusinessError(
    code: number, 
    message: string, 
    showErrorMessage: boolean = true
  ): void {
    // 业务错误码处理
    const businessErrorMap: Record<number, string> = {
      10001: '用户不存在',
      10002: '密码错误',
      10003: '账号已被禁用',
      10004: '权限不足',
      20001: '数据不存在',
      20002: '操作失败',
      30001: '系统繁忙,请稍后再试',
      // ... 其他业务错误码
    };
    
    const errorMessage = businessErrorMap[code] || message || `未知业务错误 (${code})`;
    
    if (showErrorMessage) {
      toastManager.error(errorMessage);
    }
    
    console.error(`[Business Error ${code}]: ${errorMessage}`);
  }
  
  /**
   * 处理网络错误
   * @param error Axios错误对象
   * @param showErrorMessage 是否显示错误提示
   */
  public handleNetworkError(
    error: AxiosError, 
    showErrorMessage: boolean = true
  ): void {
    // 如果已知网络离线,不再显示错误
    if (networkManager.getStatus() === 'offline') {
      return;
    }
    
    let message = '网络连接异常,请检查您的网络';
    
    if (error.message.includes('timeout')) {
      message = '网络请求超时,请稍后重试';
    } else if (error.message.includes('Network Error')) {
      message = '网络连接异常,请检查您的网络';
    } else if (error.message.includes('canceled')) {
      // 请求被取消,通常不需要提示用户
      return;
    } else if (error.message.includes('aborted')) {
      // 请求被中止,通常不需要提示用户
      return;
    }
    
    if (showErrorMessage) {
      toastManager.error(message);
    }
    
    console.error('[Network Error]:', error);
  }
  
  /**
   * 全局错误处理函数
   * @param error 任何类型的错误
   * @param showErrorMessage 是否显示错误提示
   */
  public handleError(error: any, showErrorMessage: boolean = true): void {
    if (axios.isAxiosError(error)) {
      // Axios错误
      if (error.response) {
        // 服务器返回了错误状态码
        this.handleStatusError(
          error.response.status,
          (error.response.data as any)?.message || error.message,
          showErrorMessage
        );
      } else {
        // 网络错误
        this.handleNetworkError(error, showErrorMessage);
      }
    } else {
      // 普通JS错误
      const message = error?.message || '发生未知错误';
      if (showErrorMessage) {
        toastManager.error(message);
      }
      console.error('[Error]:', error);
    }
  }
}

export default new ErrorHandler();

8. 核心请求类 (request.ts)

typescript 复制代码
import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios';
import { 
  CustomAxiosRequestConfig, 
  CustomInternalAxiosRequestConfig, 
  BaseResponse, 
  PendingRequest 
} from './types';
import axiosCanceler from './cancel';
import loadingManager from './loading';
import errorHandler from './error-handler';
import tokenManager from './token-manager';
import toastManager from './toast';
import networkManager from './network';

/**
 * HTTP请求类
 * 企业级axios二次封装
 */
class Request {
  private axiosInstance: AxiosInstance;
  
  // 默认配置
  private defaultConfig: CustomAxiosRequestConfig = {
    // 基础URL,优先使用环境变量
    baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
    // 超时时间
    timeout: 15000,
    // 默认头信息
    headers: {
      'Content-Type': 'application/json;charset=utf-8',
    },
    // 自定义请求选项
    requestOptions: {
      showLoading: true,
      loadingText: '加载中...',
      cancelRepeatRequest: true,
      autoRefreshToken: true,
      handleErrorStatus: true,
      showErrorMessage: true,
      queueWhenOffline: true,
      retryWhenOnline: true,
      retryCount: 0,
      retryDelay: 1000,
    },
  };

  /**
   * 创建HTTP请求实例 
   * @param config 自定义配置,会与默认配置合并
   */
  constructor(config: CustomAxiosRequestConfig = {}) {
    // 创建axios实例并合并配置
    this.axiosInstance = axios.create({
      ...this.defaultConfig,
      ...config,
      // 合并requestOptions
      requestOptions: {
        ...this.defaultConfig.requestOptions,
        ...config.requestOptions,
      }
    });
    
    // 初始化拦截器
    this.setupInterceptors();
    
    // 监听网络状态变化
    this.setupNetworkListener();
  }

  /**
   * 设置网络状态监听
   */
  private setupNetworkListener(): void {
    networkManager.addStatusChangeListener((status) => {
      if (status === 'online') {
        // 网络恢复时可以执行一些操作,比如重试失败的请求
        console.log('Network is back online');
      } else if (status === 'offline') {
        // 网络断开时可以执行一些操作
        console.log('Network is offline');
        // 可以选择是否关闭所有loading
        loadingManager.forceHide();
      }
    });
  }

  /**
   * 设置请求和响应拦截器
   */
  private setupInterceptors(): void {
    // 请求拦截器
    this.axiosInstance.interceptors.request.use(
      (config: CustomInternalAxiosRequestConfig) => {
        const requestOptions = config.requestOptions || this.defaultConfig.requestOptions;
        
        // 检查网络状态,如果离线且配置了离线队列,则加入队列
        if (networkManager.getStatus() === 'offline' && requestOptions?.queueWhenOffline) {
          // 返回一个新的Promise,将请求放入离线队列
          return new Promise((resolve, reject) => {
            const pendingRequest: PendingRequest = {
              config,
              resolve,
              reject
            };
            networkManager.addToOfflineQueue(pendingRequest);
          }) as any;
        }
        
        // 显示loading
        if (requestOptions?.showLoading) {
          loadingManager.show(requestOptions);
        }
        
        // 取消重复请求
        if (requestOptions?.cancelRepeatRequest) {
          axiosCanceler.addPending(config);
        }
        
        // 添加token到请求头
        const token = tokenManager.getAccessToken();
        if (token) {
          config.headers = config.headers || {};
          config.headers['Authorization'] = `Bearer ${token}`;
        }

        // 禁用浏览器缓存
        if (config.method?.toUpperCase() === 'GET') {
          config.params = { 
            ...config.params, 
            _t: new Date().getTime() 
          };
        }
        
        // 如果token即将过期,尝试刷新
        if (tokenManager.isTokenExpiringSoon() && requestOptions?.autoRefreshToken) {
          // 这里不等待,让刷新token在后台进行
          this.refreshTokenInBackground();
        }
        
        return config;
      },
      (error: AxiosError) => {
        loadingManager.forceHide();
        return Promise.reject(error);
      }
    );

    // 响应拦截器
    this.axiosInstance.interceptors.response.use(
      (response: AxiosResponse) => {
        const config = response.config as CustomInternalAxiosRequestConfig;
        const requestOptions = config.requestOptions || this.defaultConfig.requestOptions;
        
        // 在响应处理完成后,移除pending中的请求
        if (requestOptions?.cancelRepeatRequest) {
          axiosCanceler.removePending(config);
        }
        
        // 请求完成,隐藏loading
        if (requestOptions?.showLoading) {
          loadingManager.hide();
        }
        
        // 处理文件下载
        const contentType = response.headers['content-type'];
        if (
          contentType?.includes('application/octet-stream') || 
          contentType?.includes('application/vnd.ms-excel') ||
          contentType?.includes('application/vnd.openxmlformats-officedocument') ||
          contentType?.includes('application/pdf')
        ) {
          return response;
        }
        
        // 业务状态码处理
        if (response.data && requestOptions?.handleErrorStatus) {
          const { code, message } = response.data as BaseResponse;
          // 假设业务状态码200代表成功
          if (code !== 200 && code !== 0) {
            errorHandler.handleBusinessError(
              code, 
              message, 
              requestOptions.showErrorMessage
            );
            return Promise.reject(new Error(message));
          }
        }

        return response.data;
      },
      async (error: AxiosError) => {
        const config = error.config as CustomInternalAxiosRequestConfig;
        
        // 如果没有config,说明请求没有发出去
        if (!config) {
          loadingManager.forceHide();
          return Promise.reject(error);
        }
        
        const requestOptions = config.requestOptions || this.defaultConfig.requestOptions;
        
        // 移除pending中的请求
        if (config && requestOptions?.cancelRepeatRequest) {
          axiosCanceler.removePending(config);
        }
        
        // 隐藏loading
        if (requestOptions?.showLoading) {
          loadingManager.hide();
        }
        
        // 处理请求重试逻辑
        const retryCount = config.retryCount || 0;
        const maxRetries = requestOptions?.retryCount || 0;
        
        // 如果配置了重试且未达到最大重试次数
        if (maxRetries > 0 && retryCount < maxRetries) {
          // 增加重试计数
          const newConfig = {
            ...config,
            retryCount: retryCount + 1,
          };
          
          // 延迟指定时间后重试
          const delay = requestOptions?.retryDelay || 1000;
          await new Promise(resolve => setTimeout(resolve, delay));
          
          console.log(`Retrying request (${retryCount + 1}/${maxRetries}): ${config.url}`);
          return this.axiosInstance(newConfig);
        }
        
        // 处理401错误,尝试刷新Token
        if (
          error.response?.status === 401 &&
          requestOptions?.autoRefreshToken &&
          tokenManager.getRefreshToken()
        ) {
          // 尝试刷新token并重试请求
          try {
            const newToken = await this.refreshToken();
            if (newToken && config) {
              config.headers = config.headers || {};
              config.headers['Authorization'] = `Bearer ${newToken}`;
              // 重新发送请求
              return this.axiosInstance(config);
            }
          } catch (refreshError) {
            return Promise.reject(refreshError);
          }
        }

        if (error.response && requestOptions?.handleErrorStatus) {
          // HTTP状态码错误处理
          errorHandler.handleStatusError(
            error.response.status,
            (error.response.data as any)?.message || error.message,
            requestOptions.showErrorMessage
          );
        } else if (error.request) {
          // 请求已发送但没有收到响应
          errorHandler.handleNetworkError(error, requestOptions?.showErrorMessage);
        } else {
          // 其他错误
          if (requestOptions?.showErrorMessage) {
            toastManager.error(error.message || '发生未知错误');
          }
          console.error('[Request Error]:', error);
        }
        
        return Promise.reject(error);
      }
    );
  }
  
  /**
   * 在后台刷新token,不影响当前请求
   */
  private async refreshTokenInBackground(): Promise<void> {
    try {
      await this.refreshToken();
    } catch (error) {
      console.error('Background token refresh failed:', error);
    }
  }
  
  /**
   * 刷新token的实现
   */
  private async refreshToken(): Promise<string> {
    return tokenManager.refreshToken(async (refreshToken) => {
      // 使用axios直接发送请求,而不是通过this.axiosInstance
      // 避免触发拦截器导致的死循环
      const response = await axios.post<BaseResponse<any>>(
        '/api/auth/refresh-token',
        { refreshToken },
        {
          baseURL: this.defaultConfig.baseURL,
          headers: {
            'Content-Type': 'application/json',
          }
        }
      );
      return response.data;
    });
  }
  
  /**
   * 发送请求的通用方法
   * @param config 请求配置
   */
  public async request<T = any>(config: CustomAxiosRequestConfig): Promise<T> {
    return this.axiosInstance.request<any, T>(config);
  }

  /**
   * GET请求
   * @param url 请求地址
   * @param params 请求参数
   * @param config 其他配置
   */
  public async get<T = any>(
    url: string,
    params?: any,
    config?: CustomAxiosRequestConfig
  ): Promise<T> {
    return this.request<T>({
      method: 'GET',
      url,
      params,
      ...config,
    });
  }

  /**
   * POST请求
   * @param url 请求地址
   * @param data 请求数据
   * @param config 其他配置
   */
  public async post<T = any>(
    url: string,
    data?: any,
    config?: CustomAxiosRequestConfig
  ): Promise<T> {
    return this.request<T>({
      method: 'POST',
      url,
      data,
      ...config,
    });
  }

  /**
   * PUT请求
   * @param url 请求地址
   * @param data 请求数据
   * @param config 其他配置
   */
  public async put<T = any>(
    url: string,
    data?: any,
    config?: CustomAxiosRequestConfig
  ): Promise<T> {
    return this.request<T>({
      method: 'PUT',
      url,
      data,
      ...config,
    });
  }

  /**
   * DELETE请求
   * @param url 请求地址
   * @param data 请求数据
   * @param config 其他配置
   */
  public async delete<T = any>(
    url: string,
    data?: any,
    config?: CustomAxiosRequestConfig
  ): Promise<T> {
    return this.request<T>({
      method: 'DELETE',
      url,
      data,
      params: config?.params,
      ...config,
    });
  }

  /**
   * 上传文件
   * @param url 上传地址
   * @param file 文件对象或FormData
   * @param config 其他配置
   */
  public async upload<T = any>(
    url: string,
    file: File | FormData,
    config?: CustomAxiosRequestConfig
  ): Promise<T> {
    let formData: FormData;
    
    if (file instanceof FormData) {
      formData = file;
    } else {
      formData = new FormData();
      formData.append('file', file);
    }

    return this.request<T>({
      method: 'POST',
      url,
      data: formData,
      headers: {
        'Content-Type': 'multipart/form-data',
      },
      ...config,
    });
  }

  /**
   * 批量上传文件
   * @param url 上传地址
   * @param files 文件数组
   * @param fileField 文件字段名
   * @param config 其他配置
   */
  public async uploadMultiple<T = any>(
    url: string,
    files: File[],
    fileField: string = 'files',
    config?: CustomAxiosRequestConfig
  ): Promise<T> {
    const formData = new FormData();
    
    files.forEach((file, index) => {
      formData.append(`${fileField}[${index}]`, file);
    });

    return this.upload<T>(url, formData, config);
  }

  /**
   * 下载文件
   * @param url 下载地址
   * @param params 请求参数
   * @param config 其他配置
   */
  public async download(
    url: string,
    params?: any,
    config?: CustomAxiosRequestConfig
  ): Promise<Blob> {
    const fileName = config?.requestOptions?.fileName;
    
    const response = await this.axiosInstance.request({
      method: 'GET',
      url,
      params,
      responseType: 'blob',
      ...config,
    });

    const blob = new Blob([response.data]);
    
    // 获取文件名
    let downloadFileName = fileName;
    if (!downloadFileName) {
      const contentDisposition = response.headers['content-disposition'];
      if (contentDisposition) {
        const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
        const matches = filenameRegex.exec(contentDisposition);
        if (matches != null && matches[1]) {
          downloadFileName = matches[1].replace(/['"]/g, '');
          // 解码文件名
          try {
            downloadFileName = decodeURIComponent(downloadFileName);
          } catch (e) {
            // 解码失败时使用原始文件名
          }
        }
      }
      downloadFileName = downloadFileName || 'download';
    }

    // 创建下载链接
    const downloadUrl = window.URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.href = downloadUrl;
    link.download = downloadFileName;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    
    // 释放URL对象
    setTimeout(() => {
      window.URL.revokeObjectURL(downloadUrl);
    }, 100);

    return blob;
  }
  
  /**
   * 直接导出Excel文件
   * @param url 导出地址
   * @param params 请求参数
   * @param fileName 文件名
   */
  public async exportExcel(
    url: string,
    params?: any,
    fileName: string = 'export.xlsx'
  ): Promise<Blob> {
    return this.download(url, params, {
      requestOptions: {
        fileName: fileName,
        showLoading: true,
        loadingText: '正在导出数据,请稍候...'
      }
    });
  }
  
  /**
   * 取消所有请求
   * @param message 取消原因
   */
  public cancelAllRequests(message: string = 'Request canceled'): void {
    axiosCanceler.clearPending();
  }
  
  /**
   * 清空离线请求队列
   */
  public clearOfflineQueue(): void {
    networkManager.clearOfflineQueue();
  }
}

export default Request;

9. 导出请求实例 (index.ts)

typescript 复制代码
import Request from './request';
import type { CustomAxiosRequestConfig, RequestOptions } from './types';
import loadingManager from './loading';
import toastManager from './toast';
import networkManager from './network';
import tokenManager from './token-manager';

// 创建默认实例
const request = new Request();

// 导出请求实例、类、工具类和类型
export { 
  request,
  Request,
  loadingManager,
  toastManager,
  networkManager,
  tokenManager,
  type CustomAxiosRequestConfig,
  type RequestOptions 
};

export default request;

使用示例

1. 基本使用

typescript 复制代码
import { request } from '@/utils/request';

// 获取用户列表
const getUserList = async () => {
  try {
    const res = await request.get('/api/users', { page: 1, limit: 10 });
    console.log(res.data);
    return res.data;
  } catch (error) {
    console.error('获取用户列表失败:', error);
    return [];
  }
};

// 创建用户(不显示loading)
const createUser = async (userData: any) => {
  try {
    const res = await request.post('/api/users', userData, {
      requestOptions: {
        showLoading: false
      }
    });
    return res.data;
  } catch (error) {
    return null;
  }
};

// 上传文件(自定义loading文本)
const uploadAvatar = async (file: File) => {
  try {
    const res = await request.upload('/api/upload/avatar', file, {
      requestOptions: {
        loadingText: '正在上传头像...'
      }
    });
    return res.data.url;
  } catch (error) {
    return '';
  }
};

// 下载文件(自定义文件名)
const downloadReport = async () => {
  try {
    await request.download('/api/reports/export', { id: 123 }, {
      requestOptions: {
        fileName: '报表.xlsx',
        loadingText: '正在生成报表...'
      }
    });
  } catch (error) {
    console.error('下载报表失败:', error);
  }
};

2. 自定义配置

typescript 复制代码
import { Request } from '@/utils/request';

// 创建具有不同配置的请求实例
const customRequest = new Request({
  baseURL: 'https://api.example.com',
  timeout: 30000,
  requestOptions: {
    showLoading: false,      // 默认不显示loading
    showErrorMessage: true,  // 显示错误提示
    retryCount: 3,           // 失败自动重试3次
    retryDelay: 1000,        // 重试间隔1秒
    queueWhenOffline: true   // 离线时加入队列
  }
});

// 使用自定义实例
const fetchData = async () => {
  const res = await customRequest.get('/data');
  return res.data;
};

3. 高级功能使用

typescript 复制代码
import { request, loadingManager, toastManager, tokenManager } from '@/utils/request';

// 手动控制loading
const complexOperation = async () => {
  loadingManager.show();
  try {
    // 执行多个请求
    const [result1, result2] = await Promise.all([
      request.get('/api/data1', {}, { requestOptions: { showLoading: false } }),
      request.get('/api/data2', {}, { requestOptions: { showLoading: false } })
    ]);
    
    // 处理成功情况
    toastManager.success('操作成功完成');
    return { result1, result2 };
  } catch (error) {
    toastManager.error('操作失败,请重试');
    return null;
  } finally {
    loadingManager.hide();
  }
};

// 自行处理token刷新
const refreshMyToken = async () => {
  try {
    await tokenManager.refreshToken(async (refreshToken) => {
      const response = await request.post(
        '/api/auth/refresh-token',
        { refreshToken },
        { requestOptions: { autoRefreshToken: false } } // 避免循环
      );
      return response;
    });
    toastManager.success('Token已更新');
    return true;
  } catch (error) {
    return false;
  }
};

// 批量上传文件并显示进度
const uploadFiles = async (files: File[]) => {
  const formData = new FormData();
  files.forEach((file, index) => {
    formData.append(`files[${index}]`, file);
  });
  
  try {
    const result = await request.request({
      method: 'POST',
      url: '/api/upload/batch',
      data: formData,
      headers: {
        'Content-Type': 'multipart/form-data'
      },
      onUploadProgress: (progressEvent) => {
        const percentCompleted = Math.round(
          (progressEvent.loaded * 100) / (progressEvent.total || 100)
        );
        console.log(`上传进度: ${percentCompleted}%`);
        // 这里可以更新UI进度条
      },
      requestOptions: {
        showLoading: false // 使用自定义进度条,不显示loading
      }
    });
    
    toastManager.success(`成功上传 ${files.length} 个文件`);
    return result.data;
  } catch (error) {
    toastManager.error('文件上传失败');
    return null;
  }
};

核心功能亮点

这个企业级 axios 封装实现了以下核心特性

  1. 完全 TypeScript 支持:提供完整类型定义,开发时获得类型检查和代码提示

  2. 精细化 Loading 控制

    • 可配置的 loading 显示:全局和单个请求级别控制
    • 防闪烁:使用计数器管理并发请求的 loading 状态
    • 自定义 loading 文本和样式
  3. 智能的错误处理

    • 可配置的错误提示:支持全局和单个请求控制是否显示错误
    • HTTP 状态码统一处理:为不同的 HTTP 状态码提供友好提示
    • 业务错误码统一处理:自定义业务错误码映射
    • 网络错误友好提示:针对网络异常提供明确提示
  4. 完善的 Token 管理

    • 自动刷新 Token:检测 Token 即将过期并主动刷新
    • 响应 401 自动刷新:遇到未授权错误时尝试刷新 Token
    • 队列化处理并发请求:Token 刷新期间,其他请求进入等待队列
  5. 强大的请求控制

    • 取消重复请求:自动取消重复的请求,避免资源浪费
    • 请求超时控制:可为不同接口设置不同超时时间
    • 自动重试机制:请求失败后可自动重试指定次数
  6. 断网处理方案

    • 网络状态监听:实时监测网络状态变化
    • 离线请求队列:断网时自动将请求加入队列
    • 网络恢复自动重试:网络恢复时自动发送队列中的请求
  7. 全面的文件处理

    • 文件上传:支持单文件和多文件上传,自动设置正确的 Content-Type
    • 上传进度监控:支持显示上传进度
    • 文件下载:自动处理文件名和内容类型
    • Excel 导出:便捷的表格导出功能
  8. 防缓存机制

    • GET 请求自动添加时间戳参数,确保每次获取最新数据

通过这个完整的二次封装,您可以极大提高开发效率,同时提供一致的用户体验和错误处理。

看到这了,点赞收藏+关注一波~水文我一般比较少些,能帮一个是一个,能不内卷一个是一个~~~~

相关推荐
import_random3 分钟前
[python]conda
前端
亲亲小宝宝鸭4 分钟前
写了两个小需求,终于搞清楚了表格合并
前端·vue.js
BUG收容所所长6 分钟前
栈的奇妙世界:从冰棒到算法的华丽转身
前端·javascript·算法
令狐寻欢12 分钟前
JavaScript中常用的设计模式
javascript
xingba14 分钟前
重写IE的showModalDialog模态框以兼容现代浏览器
前端·javascript·google
前端小巷子15 分钟前
Promise 静态方法:轻松处理多个异步任务
前端·面试·promise
梨子同志20 分钟前
JavaScript Set 和 Map 数据结构
前端·javascript
令狐寻欢22 分钟前
JavaScript中常用的数据结构与算法
javascript
初辰ge25 分钟前
做个大屏既要不留白又要不变形还要没滚动条,我直接怒斥领导,大屏适配就这四种模式
前端·javascript
Face27 分钟前
路由Vue-router 及 异步组件
前端·javascript·vue.js