Skip to content

5分钟教你封装一个好用的请求类

约 2033 字大约 7 分钟

axioshttp工具类

2024-11-29

一、为什么要封装请求类

在前端开发中,封装请求方法是一种常见的实践,主要是为了提高代码的可维护性、可读性和复用性。以下是一些具体的原因:

  1. 代码复用

    • 在应用程序中,你可能会多次调用相同的 API 端点。通过封装请求方法,可以避免重复代码,提高代码的复用性。
  2. 简化调用

    • 封装请求方法可以简化调用过程,使得代码更简洁。例如,可以通过一个简单的函数调用来处理复杂的请求配置(如设置头信息、处理参数等)。
  3. 统一错误处理

    • 通过封装,可以在一个地方集中处理请求错误。这使得错误处理逻辑更加一致,减少遗漏和错误。
  4. 方便的请求配置管理

    • 可以在封装中统一管理请求的配置,比如设置默认的请求头、超时时间、基础 URL 等。这使得全局配置更容易管理和修改。
  5. 增强可测试性

    • 封装请求方法可以使得代码更容易测试。你可以在封装层进行模拟和拦截请求,以便在测试中使用。
  6. 支持不同的请求库

    • 如果未来需要更换请求库(例如从axios换到fetch),只需要在封装层进行修改,而不需要更改所有使用请求的地方。
  7. 添加中间件或拦截器

    • 可以在封装中添加请求和响应的拦截器,以便进行日志记录、认证、缓存等操作。

通过封装请求方法,开发者能够更好地管理和控制应用程序中的网络请求,提升代码的质量和维护效率。

二、如何封装

2.1 基础版本

提供的能力设置默认配置、请求和响应拦截器、常用的 HTTP 方法(GET、POST、PUT、DELETE)以及文件下载功能

import axios from "axios";

class HttpService {
  /**
   * @description 默认axios配置
   * @type {Object}
   * @property {string} baseURL 请求基础路径
   * @property {number} timeout 请求超时时间
   */
  static defaultAxiosOptions = {
    baseURL: "",
    timeout: 1000 * 60,
  };

  /**
   * @param {*} axiosOption axios相关的配置项
   */
  constructor(axiosOptions = {}) {
    this.axiosOptions = { ...HttpService.defaultAxiosOptions, ...axiosOptions };
    this.axiosInstance = axios.create(this.axiosOptions);
    this.#initInterceptors();
  }

  #initInterceptors() {
    this.axiosInstance.interceptors.request.use(
      (config) => {
        return config;
      },
      (error) => {
        return Promise.reject(error);
      }
    );

    this.axiosInstance.interceptors.response.use(
      (response) => {
        return response;
      },
      (error) => {
        return Promise.reject(error);
      }
    );
  }

  /**
   * @description 请求拦截器
   * @param {Function} onFulfilled 请求成功回调
   * @param {Function} onRejected  请求失败回调
   */
  setRequestInterceptor(onFulfilled, onRejected) {
    this.axiosInstance.interceptors.request.use(onFulfilled, onRejected);
  }

  /**
   * @description 响应拦截器
   * @param {Function} onFulfilled 响应成功回调
   * @param {Function} onRejected  响应失败回调
   */
  setResponseInterceptor(onFulfilled, onRejected) {
    this.axiosInstance.interceptors.response.use(onFulfilled, onRejected);
  }

  /**
   *
   * @param {Object } config
   * @returns {Function}
   */
  async request(config) {
    const executeRequest = async () => {
      try {
        const response = await this.axiosInstance({ ...config });
        loadingCallback?.onEnd?.();
        return response;
      } catch (error) {
        loadingCallback?.onEnd?.();
        throw error;
      }
    };
    return executeRequest();
  }

  get(url, params, config = {}) {
    return this.request({ url, method: "GET", params, ...config });
  }

  post(url, data, config = {}) {
    return this.request({ url, method: "POST", data, ...config });
  }

  put(url, data, config = {}) {
    return this.request({ url, method: "PUT", data, ...config });
  }

  delete(url, config = {}) {
    return this.request({ url, method: "DELETE", ...config });
  }

  // 下载文件
  async downloadFile(url, method, params, config = {}) {
    let response;
    if (method === "GET") {
      response = await this.get(url, params, {
        responseType: "blob",
        ...config,
      });
    }
    if (method === "POST") {
      response = await this.post(url, params, {
        responseType: "blob",
        ...config,
      });
    }

    let fileName = "downloaded_file";
    const contentDisposition = response.headers["content-disposition"];
    if (contentDisposition) {
      const fileNameMatch = contentDisposition.match(/filename="?(.+)"?/);
      if (fileNameMatch && fileNameMatch.length === 2) {
        fileName = fileNameMatch[1];
      }
    }

    const urlBlob = window.URL.createObjectURL(new Blob([response.data]));
    const link = document.createElement("a");
    link.href = urlBlob;
    link.setAttribute("download", fileName);
    document.body.appendChild(link);
    link.click();
    link.remove();
  }

  // GET方式下载文件
  async downloadFileGet(url, params, config = {}) {
    return this.downloadFile(url, "GET", params, config);
  }

  // POST方式下载文件
  async downloadFilePost(url, data, config = {}) {
    return this.downloadFile(url, "POST", data, config);
  }
}

export default HttpService;

2.2 基础版本使用示例

以下是使用 HttpService类实现的请求

import HttpService from "./HttpService";

// 整体设置axios配置
const httpService = new HttpService({
  baseURL: "https://api.example.com",
  timeout: 1000 * 60,
});

httpService.setResponseInterceptor(
  (response) => {
    return response.data;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// get
async function fetchUserData(userId) {
  try {
    const response = await httpService.get(`/users/${userId}`);
    console.log("User Data:", response.data);
  } catch (error) {
    console.error("Error fetching user data:", error);
  }
}

fetchUserData(1);

// post
async function loginUser(credentials) {
  try {
    // 请求内部单个设置axios配置
    const response = await httpService.post("/login", credentials, {
      headers: { custom: "example" },
    });
    console.log("Login Response:", response.data);
  } catch (error) {
    console.error("Error logging in:", error);
  }
}

const credentials = {
  username: "exampleUser",
  password: "examplePassword",
};

loginUser(credentials);

// downloadFileGet下载文件
async function downloadUserReport(userId) {
  try {
    await httpService.downloadFileGet(`/users/${userId}/report`, null, {
      responseType: "blob",
    });
    console.log("File downloaded successfully");
  } catch (error) {
    console.error("Error downloading file:", error);
  }
}

downloadUserReport(1);

2.3 高级版本

提供的能力请求重试、取消重复请求、请求前后回调

import axios from "axios";

class HttpService {
  /**
   * @description 默认axios配置
   * @type {Object}
   * @property {string} baseURL 请求基础路径
   * @property {number} timeout 请求超时时间
   */
  static defaultAxiosOptions = {
    baseURL: "",
    timeout: 1000 * 60,
  };

  /**
   * @description 默认自定义配置
   * @type {Object}
   * @property {number} retry 请求重试次数
   * @property {number} retryDelay 请求重试间隔 ,使用指数退避算法
   * @property {Object} loadingCallback 请求前后回调
   * @property {boolean} cancelDuplicate 是否取消重复请求
   * @property {number} cancelDuplicateDelay 请求重复间隔
   */
  static defaultCustomOptions = {
    retry: 0,
    retryDelay: 200,
    loadingCallback: {
      onStart: () => {},
      onEnd: () => {},
    },
    cancelDuplicate: false,
    cancelDuplicateDelay: 500,
  };

  /**
   *
   * @param {*} axiosOption axios相关的配置项
   * @param {*} customOption 自定义的配置项:包含请求重试、请求拒绝、请求前后回调
   */
  constructor(axiosOptions = {}, customOptions = {}) {
    this.axiosOptions = { ...HttpService.defaultAxiosOptions, ...axiosOptions };
    this.customOptions = {
      ...HttpService.defaultCustomOptions,
      ...customOptions,
    };
    this.axiosInstance = axios.create(this.axiosOptions);
    this.pendingRequests = new Map();
    this.#initInterceptors();
  }

  #initInterceptors() {
    this.axiosInstance.interceptors.request.use(
      (config) => {
        return config;
      },
      (error) => {
        return Promise.reject(error);
      }
    );

    this.axiosInstance.interceptors.response.use(
      (response) => {
        return response;
      },
      (error) => {
        return Promise.reject(error);
      }
    );
  }

  /**
   * @description 请求拦截器
   * @param {Function} onFulfilled 请求成功回调
   * @param {Function} onRejected  请求失败回调
   */
  setRequestInterceptor(onFulfilled, onRejected) {
    this.axiosInstance.interceptors.request.use(onFulfilled, onRejected);
  }

  /**
   * @description 响应拦截器
   * @param {Function} onFulfilled 响应成功回调
   * @param {Function} onRejected  响应失败回调
   */
  setResponseInterceptor(onFulfilled, onRejected) {
    this.axiosInstance.interceptors.response.use(onFulfilled, onRejected);
  }

  /**
   *
   * @param {Object } config
   * @param {Object} customOptions
   * @returns {Function}
   */
  async request(config, customOptions = {}) {
    const finalCustomOptions = { ...this.customOptions, ...customOptions };
    const {
      retry,
      retryDelay,
      loadingCallback,
      cancelDuplicate,
      cancelDuplicateDelay,
    } = finalCustomOptions;
    const requestKey =
      config.method + config.url + JSON.stringify(config.params);

    if (cancelDuplicate && this.pendingRequests.has(requestKey)) {
      const controller = this.pendingRequests.get(requestKey);
      controller.abort();
      this.pendingRequests.delete(requestKey);
      await this.#delay(cancelDuplicateDelay);
    }

    const controller = new AbortController();
    this.pendingRequests.set(requestKey, controller);

    let retries = 0;

    const executeRequest = async () => {
      loadingCallback?.onStart?.();
      try {
        const response = await this.axiosInstance({
          ...config,
          signal: controller.signal,
        });
        this.pendingRequests.delete(requestKey);
        loadingCallback?.onEnd?.();
        return response;
      } catch (error) {
        if (axios.isCancel(error)) {
          throw error;
        } else if (retry && retries < retry) {
          retries++;
          await this.#delay(retryDelay * retries);
          return executeRequest();
        } else {
          this.pendingRequests.delete(requestKey);
          loadingCallback?.onEnd?.();
          throw error;
        }
      }
    };
    return executeRequest();
  }

  #delay(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  get(url, params, config = {}, customOptions = {}) {
    return this.request(
      { url, method: "GET", params, ...config },
      customOptions
    );
  }

  post(url, data, config = {}, customOptions = {}) {
    return this.request(
      { url, method: "POST", data, ...config },
      customOptions
    );
  }

  put(url, data, config = {}, customOptions = {}) {
    return this.request({ url, method: "PUT", data, ...config }, customOptions);
  }

  delete(url, config = {}, customOptions = {}) {
    return this.request({ url, method: "DELETE", ...config }, customOptions);
  }

  // 下载文件
  async downloadFile(url, method, params, config = {}, customOptions = {}) {
    let response;
    if (method === "GET") {
      response = await this.get(
        url,
        params,
        { responseType: "blob", ...config },
        customOptions
      );
    }
    if (method === "POST") {
      response = await this.post(
        url,
        params,
        { responseType: "blob", ...config },
        customOptions
      );
    }

    let fileName = "downloaded_file";
    const contentDisposition = response.headers["content-disposition"];
    if (contentDisposition) {
      const fileNameMatch = contentDisposition.match(/filename="?(.+)"?/);
      if (fileNameMatch && fileNameMatch.length === 2) {
        fileName = fileNameMatch[1];
      }
    }

    const urlBlob = window.URL.createObjectURL(new Blob([response.data]));
    const link = document.createElement("a");
    link.href = urlBlob;
    link.setAttribute("download", fileName);
    document.body.appendChild(link);
    link.click();
    link.remove();
  }

  // GET方式下载文件
  async downloadFileGet(url, params, config = {}, customOptions = {}) {
    return this.downloadFile(url, "GET", params, config, customOptions);
  }

  // POST方式下载文件
  async downloadFilePost(url, data, config = {}, customOptions = {}) {
    return this.downloadFile(url, "POST", data, config, customOptions);
  }
}

export default HttpService;

2.4 高级版本使用示例

2.4.1 全局配置高级功能

该配置可以实现最多三次的请求失败重试, 重试间隔按照指数退避原则进行;同时提供在请求发起前的 hook 和请求结束后的 hook;同时对所有请求做 500ms 以内的防抖,防止短时间内的重复请求

import HttpService from "./HttpService";

// 整体设置axios配置
const httpService = new HttpService(
  {
    baseURL: "https://api.example.com",
    timeout: 1000 * 60,
  },
  {
    retry: 3,
    retryDelay: 200,
    loadingCallback: {
      onStart: () => {
        console.log("onStart");
      },
      onEnd: () => {
        console.log("onEnd");
      },
    },
    cancelDuplicate: true,
    cancelDuplicateDelay: 500,
  }
);

httpService.setResponseInterceptor(
  (response) => {
    return response.data;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// get
async function fetchUserData(userId) {
  try {
    const response = await httpService.get(`/users/${userId}`);
    console.log("User Data:", response.data);
  } catch (error) {
    console.error("Error fetching user data:", error);
  }
}

fetchUserData(1);

// post
async function loginUser(credentials) {
  try {
    // 请求内部单个设置axios配置
    const response = await httpService.post("/login", credentials, {
      headers: { custom: "example" },
    });
    console.log("Login Response:", response.data);
  } catch (error) {
    console.error("Error logging in:", error);
  }
}

const credentials = {
  username: "exampleUser",
  password: "examplePassword",
};

loginUser(credentials);

// downloadFileGet下载文件
async function downloadUserReport(userId) {
  try {
    await httpService.downloadFileGet(`/users/${userId}/report`, null, {
      responseType: "blob",
    });
    console.log("File downloaded successfully");
  } catch (error) {
    console.error("Error downloading file:", error);
  }
}

downloadUserReport(1);

2.4.2 单个请求配置高级功能

该示例实现对特定url请求的高配置功能。

import HttpService from "./HttpService";

// 整体设置axios配置
const httpService = new HttpService({
  baseURL: "https://api.example.com",
  timeout: 1000 * 60,
});

httpService.setResponseInterceptor(
  (response) => {
    return response.data;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// get
async function fetchUserData(userId) {
  try {
    const response = await httpService.get(`/users/${userId}`);
    console.log("User Data:", response.data);
  } catch (error) {
    console.error("Error fetching user data:", error);
  }
}

fetchUserData(1);

// post
async function loginUser(credentials) {
  try {
    // 请求内部单个设置axios配置
    const response = await httpService.post(
      "/login",
      credentials,
      {
        headers: { custom: "example" },
      },
      {
        retry: 3,
        retryDelay: 200,
        loadingCallback: {
          onStart: () => {
            console.log("onStart");
          },
          onEnd: () => {
            console.log("onEnd");
          },
        },
        cancelDuplicate: true,
        cancelDuplicateDelay: 500,
      }
    );
    console.log("Login Response:", response.data);
  } catch (error) {
    console.error("Error logging in:", error);
  }
}

const credentials = {
  username: "exampleUser",
  password: "examplePassword",
};

loginUser(credentials);

// downloadFileGet下载文件
async function downloadUserReport(userId) {
  try {
    await httpService.downloadFileGet(`/users/${userId}/report`, null, {
      responseType: "blob",
    });
    console.log("File downloaded successfully");
  } catch (error) {
    console.error("Error downloading file:", error);
  }
}

downloadUserReport(1);

© 2024 图图 📧 email