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

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

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

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

為什么要重新封裝 Axios?

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

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

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

設(shè)計理念

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

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

使用指南

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

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

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

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

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

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

關(guān)鍵特性

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

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

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

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

適用場景

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

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

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

最常見的場景是處理認證邏輯:

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

攔截器的典型用途

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

4. 動態(tài)修改攔截器:運行時靈活調(diào)整請求行為

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

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

實用場景

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

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

在用戶快速操作或頁面切換時,取消無用的請求可以顯著提升性能和用戶體驗。HttpClient 提供了多種靈活的請求取消方式。

5.1 主動取消單個請求

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

5.2 自動取消重復請求

async function cancelDuplicate() {
  // 連續(xù)發(fā)起相同參數(shù)的請求
  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 頁面卸載時取消所有請求

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

帶來的好處

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

6. 文件上傳:大文件處理的最佳實踐

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

// 定義上傳結(jié)果接口
interface UploadResult {
  url: string;      // 上傳后的文件URL
  filename: string; // 文件名
  size: number;     // 文件大小
}
// 處理文件上傳
async function uploadFile(file: File) {
  // 創(chuàng)建FormData對象
  const formData = new FormData();
  formData.append('file', file);
  // 可選:添加其他表單字段
  formData.append('category', 'document');
  formData.append('description', '用戶上傳的文檔');
  // 創(chuàng)建上傳專用實例(配置更長的超時)
  const uploadHttp = new HttpClient({
    timeout: 60000,  // 上傳超時設(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è)置較長超時
  • 配合進度條展示上傳進度(可通過 Axios 的 onUploadProgress 實現(xiàn))
  • 考慮分片上傳大文件(超過100MB的文件)
  • 重要文件上傳可配置重試機制

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

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

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

并發(fā)處理技巧

  • 使用 Promise.all 處理相互依賴的并發(fā)請求(一失敗全失敗)
  • 使用 Promise.allSettled 處理可以獨立失敗的請求
  • 對大量并發(fā)請求進行分批處理,避免瀏覽器限制
  • 結(jié)合請求取消機制,在某個關(guān)鍵請求失敗時取消其他請求

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

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

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

// 使用默認重試條件
async function basicRetryWithDefaults() {
  try {
    const result = await http.get<User>('/api/users/1', {
      retry: {
        retries: 3,       // 最多重試3次
        retryDelay: 1000  // 每次重試間隔1秒
        // 默認策略:只重試網(wǎng)絡(luò)錯誤或5xx服務(wù)器錯誤
      }
    });
    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ò)錯誤、超時或5xx錯誤才重試
          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)需謹慎
  • 重試次數(shù)不宜過多(通常2-3次),避免加重服務(wù)器負擔
  • 使用指數(shù)退避策略(retryDelay 逐漸增加)
  • 對明確的客戶端錯誤(如400、401、403)不重試

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

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

// 獲取原始Axios實例
function getOriginalAxiosInstance() {
  const axiosInstance = http.getInstance();
  // 示例1:使用Axios的cancelToken(舊版取消方式)
  const CancelToken = Axios.CancelToken;
  const source = CancelToken.source();
  axiosInstance.get('/api/special', {
    cancelToken: source.token
  });
  // 取消請求
  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);
}

適用場景

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

插曲:朋友的無情嘲笑

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

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

朋友: "你這代碼有點問題啊 ?? 你這個 ResponseData 類型擴展性不足:"

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

: "不可能!絕對不可能!(曹操.gif)"

然后我測試了一下,果然報錯了... ??

解決方案

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

第二輪攻擊:請求取消邏輯缺陷

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

: "這... 這應該不會吧?"

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

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

如果有重復key怎么辦?建議加個警告。"

: "但是我這里直接取消重復請求不是挺好的嗎?這是防重復機制啊!"

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

第三輪攻擊:重試機制邊界問題

朋友: "但是你這個重試邏輯有問題!重試時未重置 AbortController:"

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

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

朋友: "邊緣情況也是情況?。∵€有你的 retryCondition 默認值缺失,當用戶未配置時,會默認重試所有錯誤(包括400等客戶端錯誤),不符合預期。"

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

解決方案

// 修復后的版本
const defaultRetryCondition = (error: AxiosError) => {
  // 默認只重試網(wǎng)絡(luò)錯誤或5xx服務(wù)器錯誤
  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);
}

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

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

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

: "這... 好吧,確實應該重置一下。"

解決方案

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

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

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

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

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

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

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

朋友: "當然是 code review 啦,不過后面這幾個確實比較雞蛋里挑骨頭,前面幾個確實需要修復。"

我的反擊與分析

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

確實有價值的問題(必須修復):

  • ? ResponseData 類型沖突 - 很重要!確實會導致 never 類型問題
  • ? 重試機制的默認條件缺失 - 重要!應該有合理的默認重試條件
  • ? 攔截器ID重置問題 - 中等重要,確實應該重置ID
  • ? 重試時AbortController重置 - 中等重要,理論上存在問題

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

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

主觀性問題:

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

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

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

寫在最后

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

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

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

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

核心優(yōu)勢:

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

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

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

相關(guān)文章

  • JS實現(xiàn)異步上傳壓縮圖片

    JS實現(xiàn)異步上傳壓縮圖片

    這篇文章主要為大家詳細介紹了JS實現(xiàn)異步上傳壓縮圖片,并立即顯示圖片,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-04-04
  • JavaScript定義函數(shù)的三種實現(xiàn)方法

    JavaScript定義函數(shù)的三種實現(xiàn)方法

    這篇文章主要介紹了JavaScript定義函數(shù)的三種實現(xiàn)方法的相關(guān)資料,希望通過本文大家能夠掌握三種定義函數(shù)的方法,需要的朋友可以參考下
    2017-09-09
  • javascript實現(xiàn)base64 md5 sha1 密碼加密

    javascript實現(xiàn)base64 md5 sha1 密碼加密

    本篇文章給大家介紹了javascript實現(xiàn)密碼加密,通過base64、md5、sha1文件,調(diào)用相關(guān)方法實現(xiàn)密碼加密,非常簡單,需要的朋友可以參考下
    2015-09-09
  • JS實現(xiàn)點擊復選框變更DIV顯示狀態(tài)的示例代碼

    JS實現(xiàn)點擊復選框變更DIV顯示狀態(tài)的示例代碼

    下面小編就為大家分享一篇JS實現(xiàn)點擊復選框變更DIV顯示狀態(tài)的示例代碼,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2017-12-12
  • javascript實現(xiàn)3D切換焦點圖

    javascript實現(xiàn)3D切換焦點圖

    一款用JavaScript模仿3D立體切換效果的js焦點幻燈片特效,使用方法很簡單:用鼠標拖拽圖片向左右方向就好~
    2015-10-10
  • JavaScript實現(xiàn)簡單進度條效果

    JavaScript實現(xiàn)簡單進度條效果

    這篇文章主要為大家詳細介紹了JavaScript實現(xiàn)簡單進度條效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-03-03
  • 微信小程序視頻彈幕位置隨機

    微信小程序視頻彈幕位置隨機

    這篇文章主要介紹了微信小程序視頻彈幕位置隨機,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-04-04
  • 常見的瀏覽器Hack技巧整理

    常見的瀏覽器Hack技巧整理

    這篇文章主要介紹了常見的瀏覽器Hack技巧整理的相關(guān)資料,需要的朋友可以參考下
    2017-06-06
  • JSONP原理及簡單實現(xiàn)

    JSONP原理及簡單實現(xiàn)

    這篇文章主要介紹了JSONP原理及簡單實現(xiàn)的相關(guān)資料,非常不錯具有參考借鑒價值,需要的朋友可以參考下
    2016-06-06
  • 微信小程序?qū)崿F(xiàn)的一鍵連接wifi功能示例

    微信小程序?qū)崿F(xiàn)的一鍵連接wifi功能示例

    這篇文章主要介紹了微信小程序?qū)崿F(xiàn)的一鍵連接wifi功能,結(jié)合實例形式分析了微信小程序操作WiFi連接的模塊初始化、配置、連接等相關(guān)操作技巧,需要的朋友可以參考下
    2019-04-04

最新評論

黄大仙区| 盐山县| 南安市| 丽江市| 平度市| 运城市| 金门县| 讷河市| 林芝县| 巴里| 固安县| 盖州市| 毕节市| 金昌市| 麻江县| 远安县| 扎鲁特旗| 察隅县| 扎鲁特旗| 泽州县| 兴宁市| 廊坊市| 大同市| 栾川县| 财经| 湖口县| 隆化县| 漳浦县| 淮北市| 台江县| 中方县| 盐亭县| 措美县| 龙口市| 武乡县| 迁安市| 香河县| 忻城县| 贵港市| 雷州市| 鹤壁市|