使用TS对axios1+版本进行二次封装

为什么要进行二次封装?

axios是一个非常优秀的框架,那为什么我们还需要进行二次封装呢?

  • 封装后使用更方便,不需要每个页面导入axios后再进行请求
  • 封装后方便管理,修改baseURL、超时时间、请求头等更加方便
  • 相比于在页面进行请求,封装后接口能更加方便去管理及调用

详细步骤(以React项目为例)

一、项目中安装axios

目前推荐使用pnpm,其通过软连接和硬连接能够极快的提高依赖安装速度并节省大量硬盘空间

pnpm add axios

二、创建目录来管理封装和接口文件

react-app
├── ...
├── src
│ ├── service
│ │ ├── config.ts 储存请求的配置变量(url、超时时间等)
│ │ ├── index.ts 封装主文件
│ │ ├── types.ts 类型文件
│ │ └── modules
│ │ ├── ...
│ │ ├── login.ts
│ │ └── ...
│ └── pages
└── ...

三、进行封装

  • 首先在types.ts中创建一些基础的通用类型
  // 定义统一的数据类型,避免使用 any
  export type HttpData = Record<string, unknown>;
  export type HttpParams = Record<string, string | number | boolean | null | undefined>;
  
  // 统一api响应格式(根据后端按照什么规则返回的来修改,此类型文件按照若依数据返回规则来定义)
  export interface IApiResponse {
    code: number;
    msg: string;
  }
  // 分页响应类型
  export interface IPageResp<T = unknown> extends IApiResponse {
    total: number;
    rows: T[];
  }
  // 列表响应类型
  export interface IListResp<T = unknown> extends IApiResponse {
    data: T[];
  }
  // 单数据响应类型
  export interface IDataResp<T = unknown> extends IApiResponse {
    data: T;
  }
  • 在config.ts中配置通用的请求
export const BASE_URL = import.meta.env.VITE_API_BASE_URL;

/*
    import.meta.env.VITE_API_BASE_URL 
    是vite中获取env文件中对应环境变量的方式,使用之前需要在.dev.development 和 .dev.production中创建好VITE_API_BASE_URL这个变量(命名可以改但是一定要VITE开头)
*/

export const HTTP_CONFIG = {
  timeout: 10000,
  headers: {
    "Content-Type": "application/json;charset=UTF-8"
  }
};
  • 在index.ts中通过class进行封装
import type { AxiosInstance } from "axios";
import axios from "axios";
import { BASE_URL, HTTP_CONFIG } from "./config";
import type { IRequestRecord } from "./types";

class HttpRequest {
  // axios 实例
  private instance: AxiosInstance;
  // 基础URL,可以从环境变量获取
  private baseURL: string;
  // 请求记录,用于防止重复请求
  private pendingRequests: Map<string, IRequestRecord> = new Map();
  // 防抖时间,单位毫秒
  private debounceTime = 200;

  constructor(baseURL = "") {
    this.baseURL = baseURL;
    this.instance = axios.create({
      baseURL: this.baseURL,
      ...HTTP_CONFIG
    });
  }
}

export default new HttpRequest(BASE_URL || "");
  • 在上述封装的class中,通过constructor中创建请求和响应拦截器和处理重复请求问题
...
constructor() {
  ...
  this.setupInterceptors();
}

  /**
   * 生成请求的唯一标识
   * 根据请求的URL、方法、参数和数据生成一个唯一的字符串
   */
  private generateRequestKey(config: AxiosRequestConfig): string {
    const { url = "", method = "", params, data } = config;
    return [url, method, params ? JSON.stringify(params) : "", data ? JSON.stringify(data) : ""].join("&");
  }

  /**
   * 检查是否是重复请求,如果是则取消前一个请求
   * @param config 请求配置
   */
  private checkDuplicateRequest(config: AxiosRequestConfig): void {
    const requestKey = this.generateRequestKey(config);
    const url = config.url || "";
    const method = config.method || "get";

    // 检查是否存在相同的请求
    const existingRequest = this.pendingRequests.get(requestKey);

    if (existingRequest) {
      const currentTime = Date.now();
      // 如果前一个请求在防抖时间内,取消它
      if (currentTime - existingRequest.timestamp < this.debounceTime) {
        existingRequest.cancelSource.cancel(`重复请求被取消: ${url}`);
        console.log(`取消重复请求: ${method.toUpperCase()} ${url}`);
      }
      this.pendingRequests.delete(requestKey);
    }

    // 创建新的取消令牌
    const cancelSource = axios.CancelToken.source();
    config.cancelToken = cancelSource.token;

    // 记录新请求
    this.pendingRequests.set(requestKey, {
      timestamp: Date.now(),
      cancelSource,
      url,
      method,
      requestKey
    });
  }

  /**
   * 从待处理请求中移除请求
   * @param config 请求配置
   */
  private removeRequest(config: AxiosRequestConfig): void {
    const requestKey = this.generateRequestKey(config);
    this.pendingRequests.delete(requestKey);
  }

  /**
   * 设置请求和响应拦截器
   */
  private setupInterceptors(): void {
    // 请求拦截器
    this.instance.interceptors.request.use(
      config => {
        // 检查重复请求
        this.checkDuplicateRequest(config);

        // 获取 token 并添加到请求头
        const token = localStorage.getItem("token");
        if (token && config.headers) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      error => Promise.reject(error)
    );

    // 响应拦截器
    this.instance.interceptors.response.use(
      response => {
        // 请求完成后,从待处理请求列表中移除
        if (response.config) {
          this.removeRequest(response.config);
        }

        console.log(`响应:`, response);

        const { code, msg } = response.data as IApiResponse;
        if (code === 200) {
          // return data; // 如果接口一定是以data字段返回那可以直接返回data,
          return response.data;
        } else {
          // 根据业务状态码处理错误
          return Promise.reject(new Error(msg || "请求失败"));
        }
      },
      (error: unknown) => {
        // 将错误转换为 AxiosError 类型
        const axiosError = error as AxiosError;

        // 请求出错,也需要从待处理请求列表中移除
        if (axiosError.config) {
          this.removeRequest(axiosError.config);
        }

        // 如果是取消请求,则不进行后续处理
        if (axios.isCancel(error)) {
          const cancelError = error as { message: string };
          console.log(`请求被取消: ${cancelError.message}`);
          return Promise.reject(error);
        }

        let emsg = "请求失败";
        if (axiosError.response) {
          const rpd = axiosError.response.data as { message?: string };
          // 服务器响应了状态码,但状态码超出了2xx的范围
          emsg = rpd.message || `服务器异常(${axiosError.response.status})`;

          // 处理特定状态码
          switch (axiosError.response.status) {
            case 401:
              // Token过期,可以在这里处理登出逻辑
              localStorage.removeItem("token");
              // 跳转登录页或其他处理
              break;
            case 403:
              emsg = "没有权限访问该资源";
              break;
            case 404:
              emsg = "请求的资源不存在";
              break;
            case 500:
              emsg = "服务器内部错误";
              break;
          }
        } else if (axiosError.request) {
          // 请求已发出,但没有收到响应
          emsg = "网络错误,请检查网络连接";
        } else {
          // 其他错误
          emsg = axiosError.message || "请求配置错误";
        }
        return Promise.reject(new Error(emsg));
      }
    );
  }
  • 创建get、post等请求方式的方法和特殊情况下要修改url、timeout等情况的方法
  /**
   * GET请求
   * @param url 请求路径
   * @param params URL参数
   * @param config 其他配置
   * @returns Promise<T>
   */
  public get<T = unknown>(url: string, params?: HttpParams, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.get(url, {
      params,
      ...config
    });
  }

  /**
   * POST请求
   * @param url 请求路径
   * @param data 请求体数据
   * @param config 其他配置
   * @returns Promise<T>
   */
  public post<T = unknown>(url: string, data?: HttpData, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.post(url, data, config);
  }

  /**
   * PUT请求
   * @param url 请求路径
   * @param data 请求体数据
   * @param config 其他配置
   * @returns Promise<T>
   */
  public put<T = unknown>(url: string, data?: HttpData, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.put(url, data, config);
  }

  /**
   * DELETE请求
   * @param url 请求路径
   * @param params URL参数
   * @param config 其他配置
   * @returns Promise<T>
   */
  public delete<T = unknown>(url: string, params?: HttpParams, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.delete(url, {
      params,
      ...config
    });
  }

  /**
   * 上传文件
   * @param url 请求路径
   * @param file 文件对象
   * @param name 文件参数名
   * @param data 其他参数
   * @returns Promise<T>
   */
  public upload<T = unknown>(
    url: string,
    file: File,
    name = "file",
    data?: Record<string, string>
  ): Promise<T> {
    const formData = new FormData();
    formData.append(name, file);

    if (data) {
      Object.entries(data).forEach(([key, value]) => {
        formData.append(key, value);
      });
    }

    return this.post<T>(url, formData as unknown as HttpData, {
      headers: {
        "Content-Type": "multipart/form-data"
      }
    });
  }

  /**
   * 下载文件
   * @param url 请求路径
   * @param params 参数
   * @param fileName 文件名(可选)
   * @returns Promise<void>
   */
  public async download(url: string, params?: HttpParams, fileName = "download"): Promise<void> {
    const response = await this.instance.get(url, {
      params,
      responseType: "blob"
    });

    // 创建下载链接
    const blob = new Blob([response.data]);
    const downloadUrl = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = downloadUrl;

    // 获取文件名
    // 尝试从响应头获取文件名
    const contentDisposition = response.headers?.["content-disposition"];
    if (contentDisposition) {
      const filenameMatch = contentDisposition.match(/filename="?(.+)"?/);
      if (filenameMatch && filenameMatch[1]) {
        fileName = filenameMatch[1];
      }
    }

    link.download = fileName;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(downloadUrl);
  }

  /**
   * 取消请求
   * @param url 可选,特定URL的请求,不提供则取消所有请求
   * @param message 取消消息
   */
  public cancelRequest(url?: string, message = "请求已取消"): void {
    if (url) {
      // 取消特定URL的请求
      this.pendingRequests.forEach((request, key) => {
        if (request.url === url) {
          request.cancelSource.cancel(message);
          this.pendingRequests.delete(key);
          console.log(`取消请求: ${request.method.toUpperCase()} ${request.url}`);
        }
      });
    } else {
      // 取消所有请求
      this.pendingRequests.forEach(request => {
        request.cancelSource.cancel(message);
        console.log(`取消请求: ${request.method.toUpperCase()} ${request.url}`);
      });
      this.pendingRequests.clear();
    }
  }

  /**
   * 设置请求头
   * @param headers 请求头配置
   */
  public setHeaders(headers: Record<string, string>): void {
    Object.assign(this.instance.defaults.headers, headers);
  }

  /**
   * 设置基础URL
   * @param baseURL 基础URL
   */
  public setBaseURL(baseURL: string): void {
    this.baseURL = baseURL;
    this.instance.defaults.baseURL = baseURL;
  }

  /**
   * 设置超时时间
   * @param timeout 超时时间(毫秒)
   */
  public setTimeout(timeout: number): void {
    this.instance.defaults.timeout = timeout;
  }

四、页面使用方式

首先是在modules文件夹中创建对应的接口文件,我这创建的是demo.ts

import http from "../index";
import type { IPageResp, IListResp } from "../types";

export const getPage = (params?: { key1: string; key2: string }) => {
  return http.get<IPageResp>("/v1/material/list", params);
};

export const getList = () => {
  return http.get<IListResp>("/system/user/noAuth/list");
};

在App.tsx中使用

import { memo } from "react";
import { getPage, getList } from "./service/modules/demo";

const App = memo(() => {
  const handleLoginClick = async () => {
    const res = await getPage();
    const res2 = await getList();
    console.log(res);
    console.log(res2);
  };

  return (
    <div className='App'>
      <button onClick={() => handleLoginClick()}>获取数据</button>
    </div>
  );
});

export default App;

正常情况下我们是可以在vscode中获得正确的代码提示的:

image-20250702140800948.png
image-20250702140815727.png

虽然直接通过await获取到的数据是有类型提示的,但是光标放到res变量上能发现,内部rows的类型还是不能提示,这时候就需要和后端沟通返回的数据格式,手动编写出对应的数据类型并通过泛型传递给IPageResp类型,这样就能获取到提示

  • 没显式添加类型前:
image-20250702141039394.png
  • 显式添加类型的方式:

    • type PageType = {
        key1: string;
        key2: string;
        key3: string;
        key4?: string;
      };
      
      export const getPage = (params?: { key1: string; key2: string }) => {
        return http.get<IPageResp<PageType>>("/v1/material/list", params);
      };
      
  • 显式添加类型后的提示:

image-20250702141326511.png
image-20250702141346947.png

这就是通过ts封装axios并使用的整个流程,特此记录一下便于后续使用,下面是全部代码:

  • Index.ts
import axios from "axios";
import type { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
import type { IApiResponse, IRequestRecord, HttpData, HttpParams } from "./types";
import { BASE_URL, HTTP_CONFIG } from "./config";

/**
 * HTTP请求封装类
 * 提供 get, post, put, delete 等方法
 */
class HttpRequest {
  // axios 实例
  private instance: AxiosInstance;
  // 基础URL,可以从环境变量获取
  private baseURL: string;
  // 请求记录,用于防止重复请求
  private pendingRequests: Map<string, IRequestRecord> = new Map();
  // 防抖时间,单位毫秒
  private debounceTime = 200;

  constructor(baseURL = "") {
    this.baseURL = baseURL;
    this.instance = axios.create({
      baseURL: this.baseURL,
      ...HTTP_CONFIG
    });

    this.setupInterceptors();
  }

  /**
   * 生成请求的唯一标识
   * 根据请求的URL、方法、参数和数据生成一个唯一的字符串
   */
  private generateRequestKey(config: AxiosRequestConfig): string {
    const { url = "", method = "", params, data } = config;
    return [url, method, params ? JSON.stringify(params) : "", data ? JSON.stringify(data) : ""].join("&");
  }

  /**
   * 检查是否是重复请求,如果是则取消前一个请求
   * @param config 请求配置
   */
  private checkDuplicateRequest(config: AxiosRequestConfig): void {
    const requestKey = this.generateRequestKey(config);
    const url = config.url || "";
    const method = config.method || "get";

    // 检查是否存在相同的请求
    const existingRequest = this.pendingRequests.get(requestKey);

    if (existingRequest) {
      const currentTime = Date.now();
      // 如果前一个请求在防抖时间内,取消它
      if (currentTime - existingRequest.timestamp < this.debounceTime) {
        existingRequest.cancelSource.cancel(`重复请求被取消: ${url}`);
        console.log(`取消重复请求: ${method.toUpperCase()} ${url}`);
      }
      this.pendingRequests.delete(requestKey);
    }

    // 创建新的取消令牌
    const cancelSource = axios.CancelToken.source();
    config.cancelToken = cancelSource.token;

    // 记录新请求
    this.pendingRequests.set(requestKey, {
      timestamp: Date.now(),
      cancelSource,
      url,
      method,
      requestKey
    });
  }

  /**
   * 从待处理请求中移除请求
   * @param config 请求配置
   */
  private removeRequest(config: AxiosRequestConfig): void {
    const requestKey = this.generateRequestKey(config);
    this.pendingRequests.delete(requestKey);
  }

  /**
   * 设置请求和响应拦截器
   */
  private setupInterceptors(): void {
    // 请求拦截器
    this.instance.interceptors.request.use(
      config => {
        // 检查重复请求
        this.checkDuplicateRequest(config);

        // 获取 token 并添加到请求头
        const token = localStorage.getItem("token");
        if (token && config.headers) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      error => Promise.reject(error)
    );

    // 响应拦截器
    this.instance.interceptors.response.use(
      response => {
        // 请求完成后,从待处理请求列表中移除
        if (response.config) {
          this.removeRequest(response.config);
        }

        console.log(`响应:`, response);

        const { code, msg } = response.data as IApiResponse;
        if (code === 200) {
          // return data; // 如果接口一定是以data字段返回那可以直接返回data,
          return response.data;
        } else {
          // 根据业务状态码处理错误
          return Promise.reject(new Error(msg || "请求失败"));
        }
      },
      (error: unknown) => {
        // 将错误转换为 AxiosError 类型
        const axiosError = error as AxiosError;

        // 请求出错,也需要从待处理请求列表中移除
        if (axiosError.config) {
          this.removeRequest(axiosError.config);
        }

        // 如果是取消请求,则不进行后续处理
        if (axios.isCancel(error)) {
          const cancelError = error as { message: string };
          console.log(`请求被取消: ${cancelError.message}`);
          return Promise.reject(error);
        }

        let emsg = "请求失败";
        if (axiosError.response) {
          const rpd = axiosError.response.data as { message?: string };
          // 服务器响应了状态码,但状态码超出了2xx的范围
          emsg = rpd.message || `服务器异常(${axiosError.response.status})`;

          // 处理特定状态码
          switch (axiosError.response.status) {
            case 401:
              // Token过期,可以在这里处理登出逻辑
              localStorage.removeItem("token");
              // 跳转登录页或其他处理
              break;
            case 403:
              emsg = "没有权限访问该资源";
              break;
            case 404:
              emsg = "请求的资源不存在";
              break;
            case 500:
              emsg = "服务器内部错误";
              break;
          }
        } else if (axiosError.request) {
          // 请求已发出,但没有收到响应
          emsg = "网络错误,请检查网络连接";
        } else {
          // 其他错误
          emsg = axiosError.message || "请求配置错误";
        }
        return Promise.reject(new Error(emsg));
      }
    );
  }

  /**
   * GET请求
   * @param url 请求路径
   * @param params URL参数
   * @param config 其他配置
   * @returns Promise<T>
   */
  public get<T = unknown>(url: string, params?: HttpParams, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.get(url, {
      params,
      ...config
    });
  }

  /**
   * POST请求
   * @param url 请求路径
   * @param data 请求体数据
   * @param config 其他配置
   * @returns Promise<T>
   */
  public post<T = unknown>(url: string, data?: HttpData, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.post(url, data, config);
  }

  /**
   * PUT请求
   * @param url 请求路径
   * @param data 请求体数据
   * @param config 其他配置
   * @returns Promise<T>
   */
  public put<T = unknown>(url: string, data?: HttpData, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.put(url, data, config);
  }

  /**
   * DELETE请求
   * @param url 请求路径
   * @param params URL参数
   * @param config 其他配置
   * @returns Promise<T>
   */
  public delete<T = unknown>(url: string, params?: HttpParams, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.delete(url, {
      params,
      ...config
    });
  }

  /**
   * 上传文件
   * @param url 请求路径
   * @param file 文件对象
   * @param name 文件参数名
   * @param data 其他参数
   * @returns Promise<T>
   */
  public upload<T = unknown>(
    url: string,
    file: File,
    name = "file",
    data?: Record<string, string>
  ): Promise<T> {
    const formData = new FormData();
    formData.append(name, file);

    if (data) {
      Object.entries(data).forEach(([key, value]) => {
        formData.append(key, value);
      });
    }

    return this.post<T>(url, formData as unknown as HttpData, {
      headers: {
        "Content-Type": "multipart/form-data"
      }
    });
  }

  /**
   * 下载文件
   * @param url 请求路径
   * @param params 参数
   * @param fileName 文件名(可选)
   * @returns Promise<void>
   */
  public async download(url: string, params?: HttpParams, fileName = "download"): Promise<void> {
    const response = await this.instance.get(url, {
      params,
      responseType: "blob"
    });

    // 创建下载链接
    const blob = new Blob([response.data]);
    const downloadUrl = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = downloadUrl;

    // 获取文件名
    // 尝试从响应头获取文件名
    const contentDisposition = response.headers?.["content-disposition"];
    if (contentDisposition) {
      const filenameMatch = contentDisposition.match(/filename="?(.+)"?/);
      if (filenameMatch && filenameMatch[1]) {
        fileName = filenameMatch[1];
      }
    }

    link.download = fileName;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    URL.revokeObjectURL(downloadUrl);
  }

  /**
   * 取消请求
   * @param url 可选,特定URL的请求,不提供则取消所有请求
   * @param message 取消消息
   */
  public cancelRequest(url?: string, message = "请求已取消"): void {
    if (url) {
      // 取消特定URL的请求
      this.pendingRequests.forEach((request, key) => {
        if (request.url === url) {
          request.cancelSource.cancel(message);
          this.pendingRequests.delete(key);
          console.log(`取消请求: ${request.method.toUpperCase()} ${request.url}`);
        }
      });
    } else {
      // 取消所有请求
      this.pendingRequests.forEach(request => {
        request.cancelSource.cancel(message);
        console.log(`取消请求: ${request.method.toUpperCase()} ${request.url}`);
      });
      this.pendingRequests.clear();
    }
  }

  /**
   * 设置请求头
   * @param headers 请求头配置
   */
  public setHeaders(headers: Record<string, string>): void {
    Object.assign(this.instance.defaults.headers, headers);
  }

  /**
   * 设置基础URL
   * @param baseURL 基础URL
   */
  public setBaseURL(baseURL: string): void {
    this.baseURL = baseURL;
    this.instance.defaults.baseURL = baseURL;
  }

  /**
   * 设置超时时间
   * @param timeout 超时时间(毫秒)
   */
  public setTimeout(timeout: number): void {
    this.instance.defaults.timeout = timeout;
  }
}

export { HttpRequest };
export default new HttpRequest(BASE_URL || "");

  • types.ts
import type { CancelTokenSource } from "axios";

// 定义统一的数据类型,避免使用 any
export type HttpData = Record<string, unknown>;
export type HttpParams = Record<string, string | number | boolean | null | undefined>;

// 统一api响应格式(根据后端按照什么规则返回的来修改,此类型文件按照若依数据返回规则来定义)
export interface IApiResponse {
  code: number;
  msg: string;
}
// 分页响应类型
export interface IPageResp<T = unknown> extends IApiResponse {
  total: number;
  rows: T[];
}
// 列表响应类型
export interface IListResp<T = unknown> extends IApiResponse {
  data: T[];
}
// 单数据响应类型
export interface IDataResp<T = unknown> extends IApiResponse {
  data: T;
}

// 定义请求记录的接口,用于取消重复请求是做判断的数据的类型
export interface IRequestRecord {
  timestamp: number;
  cancelSource: CancelTokenSource;
  url: string;
  method: string;
  requestKey: string;
}

  • config.ts
export const BASE_URL = import.meta.env.VITE_API_BASE_URL;

export const HTTP_CONFIG = {
  timeout: 10000,
  headers: {
    "Content-Type": "application/json;charset=UTF-8",
  }
};

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容