最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

前端必看之超優(yōu)雅的Axios封裝實(shí)踐

 更新時(shí)間:2026年03月30日 09:21:52   作者:前端優(yōu)雅哥  
封裝axios可以提升代碼復(fù)用性、統(tǒng)一錯(cuò)誤處理和請(qǐng)求攔截等,這篇文章主要介紹了前端必看之超優(yōu)雅Axios封裝的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下

經(jīng)過一上午的反復(fù)打磨,終于寫出了這個(gè)讓我滿意的 HTTP 客戶端封裝。當(dāng)我把代碼發(fā)給朋友時(shí),他竟然挑出了一堆問題!還好我都一一解決了。

為什么要重新封裝 Axios?

我們都知道 Axios 是 JavaScript 世界里最受歡迎的 HTTP 客戶端,但在實(shí)際項(xiàng)目中,我們總是需要:

  • ?? 統(tǒng)一的錯(cuò)誤處理 - 不想在每個(gè)請(qǐng)求里寫重復(fù)的 try-catch
  • ?? 請(qǐng)求取消機(jī)制 - 用戶快速切換頁(yè)面時(shí)取消無用請(qǐng)求
  • ?? 自動(dòng)重試功能 - 網(wǎng)絡(luò)不穩(wěn)定時(shí)自動(dòng)重試
  • ?? TypeScript 完美支持 - 類型安全,IDE 智能提示
  • ??? 多實(shí)例管理 - 不同 API 服務(wù)需要不同配置

市面上的封裝要么太簡(jiǎn)單,要么太復(fù)雜。所以我決定寫一個(gè)既優(yōu)雅又實(shí)用的版本。

設(shè)計(jì)理念

這個(gè)封裝的設(shè)計(jì)遵循幾個(gè)核心原則:

  1. ?? 漸進(jìn)式增強(qiáng) - 可以像原生 Axios 一樣簡(jiǎn)單使用,也可以啟用高級(jí)功能
  2. ?? 類型安全 - 完整的 TypeScript 支持,編譯時(shí)發(fā)現(xiàn)問題
  3. ?? 靈活擴(kuò)展 - 支持多實(shí)例、自定義攔截器、業(yè)務(wù)定制
  4. ? 性能優(yōu)先 - 自動(dòng)去重、智能重試、內(nèi)存管理
  5. ?? 文檔友好 - 豐富的示例和注釋,上手即用

完整源碼

import Axios, { type AxiosInstance, type AxiosRequestConfig, type CustomParamsSerializer, type AxiosResponse, type InternalAxiosRequestConfig, type Method, type AxiosError } from "axios";
import { stringify } from "qs";
// 基礎(chǔ)配置
const defaultConfig: AxiosRequestConfig = {
  timeout: 6000,
  headers: {
    "Content-Type": "application/json;charset=utf-8"
  },
  paramsSerializer: {
    serialize: stringify as unknown as CustomParamsSerializer
  }
};
// 響應(yīng)數(shù)據(jù)基礎(chǔ)結(jié)構(gòu)
export interface BaseResponse {
  code: number;
  message?: string;
}
// 去除與BaseResponse沖突的字段
type OmitBaseResponse<T> = Omit<T, keyof BaseResponse>;
// 響應(yīng)數(shù)據(jù)類型定義 - 避免屬性沖突,確保BaseResponse優(yōu)先級(jí)
export type ResponseData<T = any> = BaseResponse & OmitBaseResponse<T>;
// 響應(yīng)數(shù)據(jù)驗(yàn)證函數(shù)類型
export type ResponseValidator<T = any> = (data: ResponseData<T>) => boolean;
// 重試配置
export interface RetryConfig {
  retries?: number; // 重試次數(shù)
  retryDelay?: number; // 重試延遲(毫秒)
  retryCondition?: (error: AxiosError) => boolean; // 重試條件
}
// 攔截器配置類型
interface InterceptorsConfig {
  requestInterceptor?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig;
  requestErrorInterceptor?: (error: AxiosError) => Promise<any>;
  responseInterceptor?: (response: AxiosResponse<ResponseData<any>>) => any;
  responseErrorInterceptor?: (error: AxiosError) => Promise<any>;
}
// 請(qǐng)求唯一鍵
type RequestKey = string | symbol;
/**
 * 增強(qiáng)型 HTTP 客戶端,基于 Axios 封裝
 * 支持?jǐn)r截器配置、請(qǐng)求取消、多實(shí)例管理等功能
 */
class HttpClient {
  private instance: AxiosInstance;
  private requestInterceptorId?: number;
  private responseInterceptorId?: number;
  private abortControllers: Map<RequestKey, AbortController> = new Map();
  /**
   * 創(chuàng)建 HTTP 客戶端實(shí)例
   * @param customConfig 自定義 Axios 配置
   * @param interceptors 自定義攔截器配置
   */
  constructor(customConfig?: AxiosRequestConfig, interceptors?: InterceptorsConfig) {
    this.instance = Axios.create({ ...defaultConfig, ...customConfig });
    this.initInterceptors(interceptors);
  }
  /** 初始化攔截器 */
  private initInterceptors(interceptors?: InterceptorsConfig): void {
    this.initRequestInterceptor(interceptors?.requestInterceptor, interceptors?.requestErrorInterceptor);
    this.initResponseInterceptor(interceptors?.responseInterceptor, interceptors?.responseErrorInterceptor);
  }
  /** 初始化請(qǐng)求攔截器 */
  private initRequestInterceptor(customInterceptor?: InterceptorsConfig["requestInterceptor"], customErrorInterceptor?: InterceptorsConfig["requestErrorInterceptor"]): void {
    // 默認(rèn)請(qǐng)求攔截器
    const defaultInterceptor = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
      /* 在這里寫請(qǐng)求攔截器的默認(rèn)業(yè)務(wù)邏輯 */
      // 示例: 添加token
      // const token = localStorage.getItem('token');
      // if (token) {
      //   config.headers.Authorization = `Bearer ${token}`;
      // }
      // 示例: 添加時(shí)間戳防止緩存
      // if (config.method?.toUpperCase() === 'GET') {
      //   config.params = { ...config.params, _t: Date.now() };
      // }
      return config;
    };
    // 默認(rèn)請(qǐng)求錯(cuò)誤攔截器
    const defaultErrorInterceptor = (error: AxiosError): Promise<any> => {
      /* 在這里寫請(qǐng)求錯(cuò)誤攔截器的默認(rèn)業(yè)務(wù)邏輯 */
      // 示例: 處理請(qǐng)求前的錯(cuò)誤
      // console.error('請(qǐng)求配置錯(cuò)誤:', error);
      return Promise.reject(error);
    };
    // 優(yōu)先使用自定義攔截器,否則使用默認(rèn)攔截器
    this.requestInterceptorId = this.instance.interceptors.request.use(customInterceptor || defaultInterceptor, customErrorInterceptor || defaultErrorInterceptor);
  }
  /** 初始化響應(yīng)攔截器 */
  private initResponseInterceptor(customInterceptor?: InterceptorsConfig["responseInterceptor"], customErrorInterceptor?: InterceptorsConfig["responseErrorInterceptor"]): void {
    // 默認(rèn)響應(yīng)攔截器
    const defaultInterceptor = (response: AxiosResponse<ResponseData<any>>) => {
      const requestKey = this.getRequestKey(response.config);
      if (requestKey) this.abortControllers.delete(requestKey);
      /* 在這里寫響應(yīng)攔截器的默認(rèn)業(yè)務(wù)邏輯 */
      // 示例: 處理不同的響應(yīng)碼
      // const { code, message } = response.data;
      // switch(code) {
      //   case 200:
      //     return response.data;
      //   case 401:
      //     // 未授權(quán)處理
      //     break;
      //   case 403:
      //     // 權(quán)限不足處理
      //     break;
      //   default:
      //     // 其他錯(cuò)誤處理
      //     console.error('請(qǐng)求錯(cuò)誤:', message);
      // }
      return response.data;
    };
    // 默認(rèn)響應(yīng)錯(cuò)誤攔截器
    const defaultErrorInterceptor = (error: AxiosError): Promise<any> => {
      if (error.config) {
        const requestKey = this.getRequestKey(error.config);
        if (requestKey) this.abortControllers.delete(requestKey);
      }
      // 處理請(qǐng)求被取消的情況
      if (Axios.isCancel(error)) {
        console.warn("請(qǐng)求已被取消:", error.message);
        return Promise.reject(new Error("請(qǐng)求已被取消"));
      }
      // 網(wǎng)絡(luò)錯(cuò)誤處理
      if (!(error as AxiosError).response) {
        if ((error as any).code === "ECONNABORTED" || (error as AxiosError).message?.includes("timeout")) {
          return Promise.reject(new Error("請(qǐng)求超時(shí),請(qǐng)稍后重試"));
        }
        return Promise.reject(new Error("網(wǎng)絡(luò)錯(cuò)誤,請(qǐng)檢查網(wǎng)絡(luò)連接"));
      }
      // HTTP狀態(tài)碼錯(cuò)誤處理
      const status = (error as AxiosError).response?.status;
      const commonErrors: Record<number, string> = {
        400: "請(qǐng)求參數(shù)錯(cuò)誤",
        401: "未授權(quán),請(qǐng)重新登錄",
        403: "權(quán)限不足",
        404: "請(qǐng)求的資源不存在",
        408: "請(qǐng)求超時(shí)",
        500: "服務(wù)器內(nèi)部錯(cuò)誤",
        502: "網(wǎng)關(guān)錯(cuò)誤",
        503: "服務(wù)暫不可用",
        504: "網(wǎng)關(guān)超時(shí)"
      };
      const message = commonErrors[status] || `請(qǐng)求失?。顟B(tài)碼:${status})`;
      return Promise.reject(new Error(message));
    };
    // 優(yōu)先使用自定義攔截器,否則使用默認(rèn)攔截器
    this.responseInterceptorId = this.instance.interceptors.response.use(customInterceptor || defaultInterceptor, customErrorInterceptor || defaultErrorInterceptor);
  }
  /** 生成請(qǐng)求唯一標(biāo)識(shí) */
  private getRequestKey(config: AxiosRequestConfig): RequestKey | undefined {
    if (!config.url) return undefined;
    return `${config.method?.toUpperCase()}-${config.url}`;
  }
  /** 設(shè)置取消控制器 - 用于取消重復(fù)請(qǐng)求或主動(dòng)取消請(qǐng)求 */
  private setupCancelController(config: AxiosRequestConfig, requestKey?: RequestKey): AxiosRequestConfig {
    const key = requestKey || this.getRequestKey(config);
    if (!key) return config;
    // 如果已有相同key的請(qǐng)求,先取消它
    this.cancelRequest(key);
    const controller = new AbortController();
    this.abortControllers.set(key, controller);
    return {
      ...config,
      signal: controller.signal
    };
  }
  /** 移除請(qǐng)求攔截器 */
  public removeRequestInterceptor(): void {
    if (this.requestInterceptorId !== undefined) {
      this.instance.interceptors.request.eject(this.requestInterceptorId);
      this.requestInterceptorId = undefined; // 重置ID,避免重復(fù)移除
    }
  }
  /** 移除響應(yīng)攔截器 */
  public removeResponseInterceptor(): void {
    if (this.responseInterceptorId !== undefined) {
      this.instance.interceptors.response.eject(this.responseInterceptorId);
      this.responseInterceptorId = undefined; // 重置ID,避免重復(fù)移除
    }
  }
  /** 動(dòng)態(tài)設(shè)置請(qǐng)求攔截器 */
  public setRequestInterceptor(customInterceptor?: InterceptorsConfig["requestInterceptor"], customErrorInterceptor?: InterceptorsConfig["requestErrorInterceptor"]): void {
    this.removeRequestInterceptor();
    this.initRequestInterceptor(customInterceptor, customErrorInterceptor);
  }
  /** 動(dòng)態(tài)設(shè)置響應(yīng)攔截器 */
  public setResponseInterceptor(customInterceptor?: InterceptorsConfig["responseInterceptor"], customErrorInterceptor?: InterceptorsConfig["responseErrorInterceptor"]): void {
    this.removeResponseInterceptor();
    this.initResponseInterceptor(customInterceptor, customErrorInterceptor);
  }
  /** 獲取 Axios 實(shí)例 */
  public getInstance(): AxiosInstance {
    return this.instance;
  }
  /**
   * 取消某個(gè)請(qǐng)求
   * @param key 請(qǐng)求唯一標(biāo)識(shí)
   * @param message 取消原因
   * @returns 是否成功取消
   */
  public cancelRequest(key: RequestKey, message?: string): boolean {
    const controller = this.abortControllers.get(key);
    if (controller) {
      controller.abort(message || `取消請(qǐng)求: ${String(key)}`);
      this.abortControllers.delete(key);
      return true;
    }
    return false;
  }
  /**
   * 取消所有請(qǐng)求
   * @param message 取消原因
   */
  public cancelAllRequests(message?: string): void {
    this.abortControllers.forEach((controller, key) => {
      controller.abort(message || `取消所有請(qǐng)求: ${String(key)}`);
    });
    this.abortControllers.clear();
  }
  /**
   * 判斷是否為取消錯(cuò)誤
   * @param error 錯(cuò)誤對(duì)象
   * @returns 是否為取消錯(cuò)誤
   */
  public static isCancel(error: unknown): boolean {
    return Axios.isCancel(error);
  }
  /**
   * 睡眠函數(shù)
   * @param ms 毫秒數(shù)
   */
  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
  /**
   * 通用請(qǐng)求方法
   * @param method 請(qǐng)求方法
   * @param url 請(qǐng)求地址
   * @param config 請(qǐng)求配置
   * @returns 響應(yīng)數(shù)據(jù)
   */
  public async request<T = any>(method: Method, url: string, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }): Promise<ResponseData<T>> {
    const { requestKey, retry, ...restConfig } = config || {};
    // 設(shè)置合理的默認(rèn)重試條件
    const defaultRetryCondition = (error: AxiosError) => {
      // 默認(rèn)只重試網(wǎng)絡(luò)錯(cuò)誤或5xx服務(wù)器錯(cuò)誤
      return !error.response || (error.response.status >= 500 && error.response.status < 600);
    };
    const retryConfig = {
      retries: 0,
      retryDelay: 1000,
      retryCondition: defaultRetryCondition,
      ...retry
    };
    let lastError: any;
    const key = requestKey || this.getRequestKey({ ...restConfig, method, url });
    for (let attempt = 0; attempt <= retryConfig.retries; attempt++) {
      try {
        // 重試前清除舊的AbortController(避免重試請(qǐng)求被誤取消)
        if (attempt > 0 && key) {
          this.abortControllers.delete(key);
        }
        const requestConfig = this.setupCancelController({ ...restConfig, method, url }, requestKey);
        /* 在這里寫通用請(qǐng)求前的業(yè)務(wù)邏輯 */
        // 示例: 記錄請(qǐng)求日志
        // console.log(`[${method.toUpperCase()}] ${url}:`, restConfig);
        const response = await this.instance.request<ResponseData<T>>(requestConfig);
        /* 在這里寫通用請(qǐng)求后的業(yè)務(wù)邏輯 */
        // 示例: 記錄響應(yīng)日志
        // console.log(`[${method.toUpperCase()}] ${url} 響應(yīng):`, response.data);
        return response.data;
      } catch (error) {
        lastError = error;
        // 如果是最后一次嘗試或不滿足重試條件或請(qǐng)求被取消,直接拋出錯(cuò)誤
        if (attempt === retryConfig.retries || !retryConfig.retryCondition(error as AxiosError) || HttpClient.isCancel(error)) {
          break;
        }
        // 延遲后重試
        if (retryConfig.retryDelay > 0) {
          await this.sleep(retryConfig.retryDelay);
        }
      }
    }
    /* 在這里寫請(qǐng)求異常的通用處理邏輯 */
    // 示例: 統(tǒng)一錯(cuò)誤提示
    // if (lastError instanceof Error) {
    //   console.error('請(qǐng)求失敗:', lastError.message);
    // }
    return Promise.reject(lastError);
  }
  /**
   * GET 請(qǐng)求
   * @param url 請(qǐng)求地址
   * @param config 請(qǐng)求配置
   * @returns 響應(yīng)數(shù)據(jù)
   */
  public get<T = any>(url: string, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }): Promise<ResponseData<T>> {
    return this.request<T>("get", url, config);
  }
  /**
   * POST 請(qǐng)求
   * @param url 請(qǐng)求地址
   * @param data 請(qǐng)求數(shù)據(jù)
   * @param config 請(qǐng)求配置
   * @returns 響應(yīng)數(shù)據(jù)
   */
  public post<T = any>(url: string, data?: any, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }): Promise<ResponseData<T>> {
    return this.request<T>("post", url, { ...config, data });
  }
  /**
   * PUT 請(qǐng)求
   * @param url 請(qǐng)求地址
   * @param data 請(qǐng)求數(shù)據(jù)
   * @param config 請(qǐng)求配置
   * @returns 響應(yīng)數(shù)據(jù)
   */
  public put<T = any>(url: string, data?: any, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }): Promise<ResponseData<T>> {
    return this.request<T>("put", url, { ...config, data });
  }
  /**
   * DELETE 請(qǐng)求
   * @param url 請(qǐng)求地址
   * @param config 請(qǐng)求配置
   * @returns 響應(yīng)數(shù)據(jù)
   */
  public delete<T = any>(url: string, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }): Promise<ResponseData<T>> {
    return this.request<T>("delete", url, config);
  }
  /**
   * PATCH 請(qǐng)求
   * @param url 請(qǐng)求地址
   * @param data 請(qǐng)求數(shù)據(jù)
   * @param config 請(qǐng)求配置
   * @returns 響應(yīng)數(shù)據(jù)
   */
  public patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }): Promise<ResponseData<T>> {
    return this.request<T>("patch", url, { ...config, data });
  }
}
// 默認(rèn)導(dǎo)出實(shí)例 - 可直接使用
export const http = new HttpClient();
export default HttpClient;

使用指南

1. 基礎(chǔ)請(qǐng)求操作:GET/POST 等常用方法

任何 HTTP 客戶端最核心的功能都是處理基礎(chǔ)請(qǐng)求,HttpClient 對(duì)常用的 HTTP 方法進(jìn)行了友好封裝,同時(shí)提供了完整的 TypeScript 類型支持。

首先定義我們需要用到的數(shù)據(jù)類型:

// 用戶信息接口
interface User {
  id: number;
  name: string;
  email: string;
}
// 分頁(yè)響應(yīng)通用接口
interface PageResponse<T> {
  list: T[];       // 數(shù)據(jù)列表
  total: number;   // 總條數(shù)
  page: number;    // 當(dāng)前頁(yè)碼
  size: number;    // 每頁(yè)條數(shù)
}

基礎(chǔ)請(qǐng)求操作示例:

async function basicRequests() {
  try {
    // 1. GET 請(qǐng)求(帶查詢參數(shù))
    const userPage = await http.get<PageResponse<User>>('/api/users', {
      params: { page: 1, size: 10 }
    });
    console.log(`第${userPage.page}頁(yè),共${userPage.total}個(gè)用戶:`, userPage.list);
    // 2. POST 請(qǐng)求(提交數(shù)據(jù))
    const newUser = await http.post<{ id: number }>('/api/users', {
      name: '張三',
      email: 'zhangsan@example.com'
    });
    console.log('新增用戶ID:', newUser.id);
    // 3. PUT 請(qǐng)求(更新數(shù)據(jù))
    const updatedUser = await http.put<User>('/api/users/1', {
      id: 1,
      name: '張三三',
      email: 'zhangsansan@example.com'
    });
    // 4. DELETE 請(qǐng)求
    const deleteRes = await http.delete<{ success: boolean; message?: string }>('/api/users/1');
    console.log('刪除結(jié)果:', deleteRes.success);
  } catch (error) {
    // 統(tǒng)一錯(cuò)誤處理
    console.error('請(qǐng)求失敗:', error instanceof Error ? error.message : error);
  }
}

關(guān)鍵特性

  • 泛型參數(shù)直接指定響應(yīng)數(shù)據(jù)類型,獲得完整的類型提示
  • 統(tǒng)一的錯(cuò)誤處理機(jī)制,無需在每個(gè)請(qǐng)求中重復(fù)編寫 try-catch
  • 自動(dòng)處理請(qǐng)求參數(shù)序列化和響應(yīng)數(shù)據(jù)解析

2. 多實(shí)例管理:為不同 API 服務(wù)創(chuàng)建專屬客戶端

在復(fù)雜項(xiàng)目中,我們常常需要與多個(gè) API 服務(wù)交互,每個(gè)服務(wù)可能有不同的基礎(chǔ)路徑、超時(shí)設(shè)置或認(rèn)證方式。HttpClient 支持創(chuàng)建多個(gè)獨(dú)立實(shí)例,完美解決這個(gè)問題。

// 定義商品數(shù)據(jù)接口
interface Product {
  id: string;
  title: string;
  price: number;
  stock: number;
}
// 1. 創(chuàng)建用戶服務(wù)專用實(shí)例
const userApi = new HttpClient({
  baseURL: 'https://api.example.com/user',  // 用戶服務(wù)基礎(chǔ)路徑
  timeout: 10000                           // 超時(shí)設(shè)置為10秒
});
// 2. 創(chuàng)建商品服務(wù)專用實(shí)例(帶特殊請(qǐng)求頭)
const productApi = new HttpClient({
  baseURL: 'https://api.example.com/product',  // 商品服務(wù)基礎(chǔ)路徑
  headers: { 'X-Product-Token': 'special-token' }  // 商品服務(wù)需要的特殊頭部
});
// 使用多實(shí)例處理不同服務(wù)的請(qǐng)求
async function useMultiInstances() {
  // 調(diào)用用戶服務(wù)API
  const userPage = await userApi.get<PageResponse<User>>('/list');
  // 調(diào)用商品服務(wù)API
  const productPage = await productApi.get<PageResponse<Product>>('/list');
  // 兩個(gè)實(shí)例的配置完全隔離,不會(huì)相互影響
}

適用場(chǎng)景

  • 前后端分離項(xiàng)目中對(duì)接多個(gè)微服務(wù)
  • 同時(shí)需要訪問內(nèi)部API和第三方API
  • 不同接口有不同的超時(shí)需求(如普通接口5秒,文件上傳60秒)

3. 自定義攔截器:業(yè)務(wù)邏輯與請(qǐng)求處理的解耦

攔截器是處理請(qǐng)求/響應(yīng)公共邏輯的最佳方式,HttpClient 允許在初始化時(shí)配置自定義攔截器,將認(rèn)證、日志等橫切關(guān)注點(diǎn)與業(yè)務(wù)邏輯分離。

最常見的場(chǎng)景是處理認(rèn)證邏輯:

// 創(chuàng)建帶權(quán)限驗(yàn)證的HTTP客戶端實(shí)例
const authHttp = new HttpClient(
  { baseURL: 'https://api.example.com/auth' },  // 基礎(chǔ)配置
  {
    // 請(qǐng)求攔截器:添加認(rèn)證Token
    requestInterceptor: (config) => {
      const token = localStorage.getItem('token');
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    },
    // 響應(yīng)攔截器:處理認(rèn)證失敗
    responseInterceptor: (response) => {
      // 未授權(quán),自動(dòng)跳轉(zhuǎn)到登錄頁(yè)
      if (response.code === 401) {
        localStorage.removeItem('token');
        window.location.href = '/login';
      }
      return response;
    }
  }
);

攔截器的典型用途

  • 添加全局認(rèn)證信息(Token、API Key等)
  • 統(tǒng)一處理錯(cuò)誤碼(如401未授權(quán)、403權(quán)限不足)
  • 實(shí)現(xiàn)請(qǐng)求/響應(yīng)日志記錄
  • 添加請(qǐng)求時(shí)間戳防止緩存

4. 動(dòng)態(tài)修改攔截器:運(yùn)行時(shí)靈活調(diào)整請(qǐng)求行為

有時(shí)候我們需要在運(yùn)行時(shí)根據(jù)業(yè)務(wù)場(chǎng)景動(dòng)態(tài)改變請(qǐng)求/響應(yīng)處理邏輯,HttpClient 提供了動(dòng)態(tài)修改攔截器的能力。

// 定義日志數(shù)據(jù)接口
interface LogData {
  id: number;
  timestamp: string;
  content: string;
}
async function dynamicInterceptors() {
  // 場(chǎng)景1:臨時(shí)添加日志攔截器
  const logInterceptor = (response: AxiosResponse<ResponseData<PageResponse<LogData>>>) => {
    console.log(`請(qǐng)求[${response.config.url}]返回${response.data.total}條日志`);
    return response;
  };
  // 設(shè)置新的響應(yīng)攔截器
  http.setResponseInterceptor(logInterceptor);
  // 發(fā)送請(qǐng)求時(shí)會(huì)執(zhí)行新的攔截器
  await http.get<PageResponse<LogData>>('/api/logs');
  // 場(chǎng)景2:完成日志收集后,恢復(fù)默認(rèn)攔截器
  http.setResponseInterceptor();
  // 場(chǎng)景3:動(dòng)態(tài)更新認(rèn)證信息(如Token刷新后)
  const newToken = 'new-auth-token';
  http.setRequestInterceptor((config) => {
    config.headers.Authorization = `Bearer ${newToken}`;
    return config;
  });
}

實(shí)用場(chǎng)景

  • 臨時(shí)開啟調(diào)試日志
  • Token過期后動(dòng)態(tài)更新認(rèn)證信息
  • 特定頁(yè)面需要特殊的請(qǐng)求頭
  • A/B測(cè)試時(shí)切換不同的API處理邏輯

5. 請(qǐng)求取消:優(yōu)化用戶體驗(yàn)的關(guān)鍵技巧

在用戶快速操作或頁(yè)面切換時(shí),取消無用的請(qǐng)求可以顯著提升性能和用戶體驗(yàn)。HttpClient 提供了多種靈活的請(qǐng)求取消方式。

5.1 主動(dòng)取消單個(gè)請(qǐng)求

async function cancelSingleRequest() {
  const requestKey = 'user-list';  // 定義唯一標(biāo)識(shí)
  try {
    // 發(fā)起請(qǐng)求時(shí)指定requestKey
    const promise = http.get<PageResponse<User>>('/api/users', { requestKey });
    // 模擬:200ms后取消請(qǐng)求(例如用戶快速切換了頁(yè)面)
    setTimeout(() => {
      http.cancelRequest(requestKey, '數(shù)據(jù)已過時(shí)');
    }, 200);
    const result = await promise;
  } catch (error) {
    // 判斷是否為取消錯(cuò)誤
    if (HttpClient.isCancel(error)) {
      console.log('請(qǐng)求已取消:', error.message);
    }
  }
}

5.2 自動(dòng)取消重復(fù)請(qǐng)求

async function cancelDuplicate() {
  // 連續(xù)發(fā)起相同參數(shù)的請(qǐng)求
  http.get<PageResponse<User>>('/api/users', { params: { page: 1 } }); // 被取消
  http.get<PageResponse<User>>('/api/users', { params: { page: 1 } }); // 被取消
  const latestData = await http.get<PageResponse<User>>('/api/users', { params: { page: 1 } }); // 最終生效
}

5.3 頁(yè)面卸載時(shí)取消所有請(qǐng)求

// 在React/Vue等框架的組件卸載鉤子中調(diào)用
function onPageUnmount() {
  http.cancelAllRequests('頁(yè)面已關(guān)閉');
}
// 或者監(jiān)聽頁(yè)面關(guān)閉事件
window.addEventListener('beforeunload', () => {
  http.cancelAllRequests('用戶離開頁(yè)面');
});

帶來的好處

  • 減少不必要的網(wǎng)絡(luò)請(qǐng)求和服務(wù)器負(fù)載
  • 避免過時(shí)數(shù)據(jù)覆蓋最新數(shù)據(jù)
  • 防止頁(yè)面跳轉(zhuǎn)后仍彈出錯(cuò)誤提示
  • 減少內(nèi)存占用和潛在的內(nèi)存泄漏

6. 文件上傳:大文件處理的最佳實(shí)踐

文件上傳是前端開發(fā)中的常見需求,尤其需要注意超時(shí)設(shè)置和數(shù)據(jù)格式。HttpClient 可以輕松配置適合文件上傳的參數(shù)。

// 定義上傳結(jié)果接口
interface UploadResult {
  url: string;      // 上傳后的文件URL
  filename: string; // 文件名
  size: number;     // 文件大小
}
// 處理文件上傳
async function uploadFile(file: File) {
  // 創(chuàng)建FormData對(duì)象
  const formData = new FormData();
  formData.append('file', file);
  // 可選:添加其他表單字段
  formData.append('category', 'document');
  formData.append('description', '用戶上傳的文檔');
  // 創(chuàng)建上傳專用實(shí)例(配置更長(zhǎng)的超時(shí))
  const uploadHttp = new HttpClient({
    timeout: 60000,  // 上傳超時(shí)設(shè)為60秒
    headers: { 'Content-Type': 'multipart/form-data' }
  });
  try {
    const result = await uploadHttp.post<UploadResult>('/api/upload', formData);
    console.log('文件上傳成功,訪問地址:', result.url);
    return result.url;
  } catch (error) {
    console.error('文件上傳失敗:', error);
    throw error;
  }
}

上傳優(yōu)化建議

  • 大文件上傳使用專門的實(shí)例,設(shè)置較長(zhǎng)超時(shí)
  • 配合進(jìn)度條展示上傳進(jìn)度(可通過 Axios 的 onUploadProgress 實(shí)現(xiàn))
  • 考慮分片上傳大文件(超過100MB的文件)
  • 重要文件上傳可配置重試機(jī)制

7. 并發(fā)請(qǐng)求處理:高效獲取多源數(shù)據(jù)

實(shí)際開發(fā)中經(jīng)常需要同時(shí)請(qǐng)求多個(gè)接口,然后匯總處理數(shù)據(jù)。HttpClient 結(jié)合 Promise API 可以優(yōu)雅地處理并發(fā)請(qǐng)求。

// 使用Promise.all處理并發(fā)請(qǐng)求
async function handleConcurrentRequests() {
  try {
    // 同時(shí)發(fā)起多個(gè)請(qǐng)求
    const [userRes, productRes] = await Promise.all([
      http.get<User>('/api/users/1'),                // 獲取用戶詳情
      http.get<PageResponse<Product>>('/api/products') // 獲取商品列表
    ]);
    // 所有請(qǐng)求成功后處理數(shù)據(jù)
    console.log('用戶詳情:', userRes);
    console.log('商品列表:', productRes.list);
    // 可以在這里進(jìn)行數(shù)據(jù)整合
    return {
      user: userRes,
      products: productRes.list
    };
  } catch (error) {
    // 任何一個(gè)請(qǐng)求失敗都會(huì)進(jìn)入這里
    console.error('并發(fā)請(qǐng)求失敗:', error);
    throw error;
  }
}

并發(fā)處理技巧

  • 使用 Promise.all 處理相互依賴的并發(fā)請(qǐng)求(一失敗全失?。?/li>
  • 使用 Promise.allSettled 處理可以獨(dú)立失敗的請(qǐng)求
  • 對(duì)大量并發(fā)請(qǐng)求進(jìn)行分批處理,避免瀏覽器限制
  • 結(jié)合請(qǐng)求取消機(jī)制,在某個(gè)關(guān)鍵請(qǐng)求失敗時(shí)取消其他請(qǐng)求

8. 請(qǐng)求重試:提升網(wǎng)絡(luò)不穩(wěn)定場(chǎng)景的可靠性

網(wǎng)絡(luò)波動(dòng)是前端請(qǐng)求失敗的常見原因,HttpClient 內(nèi)置的重試機(jī)制可以自動(dòng)處理這類問題,提升用戶體驗(yàn)。

8.1 基礎(chǔ)重試配置(使用默認(rèn)策略)

// 使用默認(rèn)重試條件
async function basicRetryWithDefaults() {
  try {
    const result = await http.get<User>('/api/users/1', {
      retry: {
        retries: 3,       // 最多重試3次
        retryDelay: 1000  // 每次重試間隔1秒
        // 默認(rèn)策略:只重試網(wǎng)絡(luò)錯(cuò)誤或5xx服務(wù)器錯(cuò)誤
      }
    });
    return result;
  } catch (error) {
    console.error('所有重試都失敗了:', error);
    throw error;
  }
}

8.2 自定義重試條件

// 自定義重試邏輯
async function customRetryCondition(userData: User) {
  try {
    const result = await http.post<User>('/api/users', userData, {
      retry: {
        retries: 2,       // 重試2次
        retryDelay: 500,  // 重試間隔500ms
        retryCondition: (error) => {
          // 自定義條件:網(wǎng)絡(luò)錯(cuò)誤、超時(shí)或5xx錯(cuò)誤才重試
          return !error.response || 
                 error.response.status === 408 || 
                 (error.response.status >= 500 && error.response.status < 600);
        }
      }
    });
    return result;
  } catch (error) {
    console.error('重試失敗:', error);
    throw error;
  }
}

重試策略建議

  • 讀操作(GET)適合重試,寫操作(POST/PUT)需謹(jǐn)慎
  • 重試次數(shù)不宜過多(通常2-3次),避免加重服務(wù)器負(fù)擔(dān)
  • 使用指數(shù)退避策略(retryDelay 逐漸增加)
  • 對(duì)明確的客戶端錯(cuò)誤(如400、401、403)不重試

9. 訪問原始 Axios 實(shí)例:兼容特殊需求

雖然 HttpClient 封裝了常用功能,但某些特殊場(chǎng)景可能需要直接使用 Axios 原生 API。HttpClient 提供了獲取原始實(shí)例的方法。

// 獲取原始Axios實(shí)例
function getOriginalAxiosInstance() {
  const axiosInstance = http.getInstance();
  // 示例1:使用Axios的cancelToken(舊版取消方式)
  const CancelToken = Axios.CancelToken;
  const source = CancelToken.source();
  axiosInstance.get('/api/special', {
    cancelToken: source.token
  });
  // 取消請(qǐng)求
  source.cancel('Operation canceled by the user.');
  // 示例2:使用Axios的攔截器API
  const myInterceptor = axiosInstance.interceptors.response.use(
    response => response,
    error => Promise.reject(error)
  );
  // 移除攔截器
  axiosInstance.interceptors.response.eject(myInterceptor);
}

適用場(chǎng)景

  • 使用一些 HttpClient 未封裝的 Axios 特性
  • 集成依賴原始 Axios 實(shí)例的第三方庫(kù)
  • 處理極特殊的請(qǐng)求場(chǎng)景
  • 平滑遷移現(xiàn)有基于 Axios 的代碼

插曲:朋友的無情嘲笑

在我們完成本次封裝前,還有一個(gè)小插曲:我得意洋洋地把這個(gè)"史上最優(yōu)雅"的封裝發(fā)給朋友炫耀,心想著他肯定會(huì)夸我兩句。結(jié)果他發(fā)來了一大段文字...

第一輪攻擊:類型定義問題

朋友: "你這代碼有點(diǎn)問題啊 ?? 你這個(gè) ResponseData 類型擴(kuò)展性不足:"

// 你的問題代碼
export type ResponseData<T = any> = BaseResponse & T;
// 當(dāng) T 中包含 code 或 message 字段時(shí)會(huì)沖突
interface UserWithCode {
  code: string; // 與BaseResponse沖突
  name: string;
}
type TestType = ResponseData<UserWithCode>; // code字段變成never類型!

: "不可能!絕對(duì)不可能?。ú懿?gif)"

然后我測(cè)試了一下,果然報(bào)錯(cuò)了... ??

解決方案

// 修復(fù)后的版本
type OmitBaseResponse<T> = Omit<T, keyof BaseResponse>;
export type ResponseData<T = any> = BaseResponse & OmitBaseResponse<T>;

第二輪攻擊:請(qǐng)求取消邏輯缺陷

朋友: "還有你這個(gè) setupCancelController 未處理自定義 requestKey 沖突,當(dāng)用戶傳入自定義 requestKey 時(shí),若與內(nèi)部生成的鍵重復(fù),會(huì)導(dǎo)致取消邏輯混亂。"

: "這... 這應(yīng)該不會(huì)吧?"

朋友: "你看,你的代碼是這樣的:

private setupCancelController(config: AxiosRequestConfig, requestKey?: RequestKey) {
  const key = requestKey || this.getRequestKey(config);
  // 直接取消,但沒有沖突警告
  this.cancelRequest(key);
}

如果有重復(fù)key怎么辦?建議加個(gè)警告。"

: "但是我這里直接取消重復(fù)請(qǐng)求不是挺好的嗎?這是防重復(fù)機(jī)制??!"

朋友: "嗯...這個(gè)倒是有道理。那算了,這個(gè)問題不大。"

第三輪攻擊:重試機(jī)制邊界問題

朋友: "但是你這個(gè)重試邏輯有問題!重試時(shí)未重置 AbortController:"

// 你的問題代碼
for (let attempt = 0; attempt <= retryConfig.retries; attempt++) {
  // 每次都會(huì)調(diào)用setupCancelController,創(chuàng)建新的controller
  // 但舊的還在Map中,可能導(dǎo)致重試請(qǐng)求被誤取消
  const requestConfig = this.setupCancelController({...restConfig, method, url}, requestKey);
}

: "這... 這是邊緣情況!"

朋友: "邊緣情況也是情況啊!還有你的 retryCondition 默認(rèn)值缺失,當(dāng)用戶未配置時(shí),會(huì)默認(rèn)重試所有錯(cuò)誤(包括400等客戶端錯(cuò)誤),不符合預(yù)期。"

: "好吧好吧,我改還不行嗎... ??"

解決方案

// 修復(fù)后的版本
const defaultRetryCondition = (error: AxiosError) => {
  // 默認(rèn)只重試網(wǎng)絡(luò)錯(cuò)誤或5xx服務(wù)器錯(cuò)誤
  return !error.response || (error.response.status >= 500 && error.response.status < 600);
};
for (let attempt = 0; attempt <= retryConfig.retries; attempt++) {
  // 重試前清除舊控制器
  if (attempt > 0 && key) {
    this.abortControllers.delete(key);
  }
  const requestConfig = this.setupCancelController({...restConfig, method, url}, requestKey);
}

第四輪攻擊:攔截器管理問題

朋友: "還有你的攔截器移除邏輯不嚴(yán)謹(jǐn),只通過 interceptorId 移除,但未重置 interceptorId,可能導(dǎo)致后續(xù)重復(fù)移除無效:"

// 你的問題代碼
public removeRequestInterceptor(): void {
  if (this.requestInterceptorId) {
    this.instance.interceptors.request.eject(this.requestInterceptorId);
    // 沒有重置ID!
  }
}

: "這... 好吧,確實(shí)應(yīng)該重置一下。"

解決方案

// 修復(fù)后的版本
public removeRequestInterceptor(): void {
  if (this.requestInterceptorId !== undefined) {
    this.instance.interceptors.request.eject(this.requestInterceptorId);
    this.requestInterceptorId = undefined; // 重置ID
  }
}

第五輪攻擊:其他細(xì)節(jié)問題

朋友: "哈哈,別急。不過你還有幾個(gè)問題:

  1. Content-Type 硬編碼問題 - 默認(rèn)強(qiáng)制設(shè)置為 application/json,但上傳文件時(shí)需要 multipart/form-data,需手動(dòng)覆蓋,不夠靈活。

  2. 錯(cuò)誤信息處理冗余 - 響應(yīng)錯(cuò)誤攔截器中對(duì)錯(cuò)誤信息的包裝會(huì)丟失原始錯(cuò)誤的詳細(xì)信息,不利于調(diào)試。

  3. requestKey 類型聲明不明確 - 定義為 string | symbol,但用戶傳入 symbol 時(shí),調(diào)試信息顯示不友好。"

: "停停停!你這是在 code review 還是在找茬?!"

朋友: "當(dāng)然是 code review 啦,不過后面這幾個(gè)確實(shí)比較雞蛋里挑骨頭,前面幾個(gè)確實(shí)需要修復(fù)。"

我的反擊與分析

經(jīng)過一番"友好"的討論(主要是我被教育),我冷靜分析了一下:

確實(shí)有價(jià)值的問題(必須修復(fù)):

  • ? ResponseData 類型沖突 - 很重要!確實(shí)會(huì)導(dǎo)致 never 類型問題
  • ? 重試機(jī)制的默認(rèn)條件缺失 - 重要!應(yīng)該有合理的默認(rèn)重試條件
  • ? 攔截器ID重置問題 - 中等重要,確實(shí)應(yīng)該重置ID
  • ? 重試時(shí)AbortController重置 - 中等重要,理論上存在問題

過于苛刻或設(shè)計(jì)選擇問題:

  • ? requestKey沖突警告 - 當(dāng)前設(shè)計(jì)已經(jīng)通過取消舊請(qǐng)求處理了,警告是多余的
  • ? Content-Type硬編碼 - 這是常見的默認(rèn)設(shè)置,Axios會(huì)自動(dòng)覆蓋FormData
  • ? 錯(cuò)誤信息包裝 - 保留原始錯(cuò)誤是好的,但當(dāng)前設(shè)計(jì)也合理
  • ? 攔截器組合模式 - 當(dāng)前的覆蓋模式是主流設(shè)計(jì),組合模式會(huì)增加復(fù)雜性

主觀性問題:

  • ?? requestKey類型限制 - symbol支持是特性,不是缺陷

最終我不得不承認(rèn):朋友的技術(shù)功底是不錯(cuò)的,提出了一些確實(shí)存在的邊緣問題。但有些建議過于"完美主義",可能會(huì)讓代碼變得過于復(fù)雜。

修復(fù)了前4個(gè)重要問題后,朋友終于點(diǎn)頭說:"現(xiàn)在看起來像個(gè)正經(jīng)的封裝了!不過你得承認(rèn),好的代碼不僅要能跑,還要經(jīng)得起同行的審視。" ??

寫在最后

經(jīng)過這一上午的"激情"編碼 + 朋友的"無情"嘲笑 + 我的"不服氣"修復(fù),這個(gè) HTTP 客戶端封裝終于變得更加健壯了。

從最初的自我感覺良好,到被朋友無情打臉,再到最后的虛心修復(fù),這個(gè)過程讓我深刻體會(huì)到:

  1. 沒有完美的代碼 - 總有你想不到的邊緣情況
  2. Code Review 很重要 - 別人的視角能發(fā)現(xiàn)你的盲點(diǎn)
  3. 保持開放心態(tài) - 被指出問題是好事,不是壞事
  4. 朋友很重要 - 能"嘲笑"你代碼的朋友才是真朋友 ??

現(xiàn)在這個(gè)封裝不僅解決了日常開發(fā)中的痛點(diǎn),還具備了企業(yè)級(jí)項(xiàng)目的穩(wěn)定性。

核心優(yōu)勢(shì):

  • ?? 開箱即用 - 無需復(fù)雜配置,默認(rèn)就很好用
  • ?? 高度可定制 - 支持各種業(yè)務(wù)場(chǎng)景的定制需求
  • ??? 類型安全 - TypeScript 完美支持,減少運(yùn)行時(shí)錯(cuò)誤
  • ? 性能出色 - 智能去重、重試、取消機(jī)制
  • ?? 文檔完善 - 詳細(xì)的示例和注釋

如果你也在為 HTTP 請(qǐng)求封裝而苦惱,不妨試試這個(gè)方案。記住,好的代碼是改出來的,不是寫出來的!

到此這篇關(guān)于前端必看之超優(yōu)雅的Axios封裝實(shí)踐的文章就介紹到這了,更多相關(guān)優(yōu)雅的Axios封裝內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論

香港| 建瓯市| 象山县| 花莲县| 淮北市| 芦山县| 芦山县| 中卫市| 茂名市| 上饶市| 乌鲁木齐市| 晋宁县| 建阳市| 佛山市| 象山县| 淮南市| 潮安县| 文水县| 郴州市| 泸水县| 奈曼旗| 邵阳县| 合作市| 中超| 岳西县| 临高县| 罗平县| 临湘市| 古丈县| 双江| 搜索| 洮南市| 上虞市| 庆安县| 姜堰市| 霞浦县| 鸡东县| 车险| 诏安县| 高阳县| 平果县|