see: axios-demo-vue

see: axios-demo-java

直接上代码

1 axios封装

  • axios封装/api/HttpHelper.ts
import axios, { type AxiosRequestConfig, type AxiosResponse, type InternalAxiosRequestConfig, type ResponseType } from "axios";

import { handleGlobalBizError, handleGlobalHttpStatusError } from "./handler";
import type { IHttpApiResponse } from "./types";

const helper = {
  isDownload: function (response: AxiosResponse<IHttpApiResponse<any> | Blob>): boolean {
    return response.data instanceof Blob;
  },

  hasBizError: function (response: AxiosResponse<IHttpApiResponse<any> | Blob>): boolean {
    if (this.isDownload(response)) {
      return response.data.type === "application/json";
    }

    return (response.data as IHttpApiResponse<any>)?.code !== 200;
  },

  download: function (response: any, defaultFileName?: string): void {
    const url = window.URL.createObjectURL(response.data);
    const link = document.createElement("a");
    link.href = url;
    link.download = this.getFileName(response, defaultFileName);
    document.body.appendChild(link);
    link.click();

    document.body.removeChild(link);
    window.URL.revokeObjectURL(url);
  },

  getFileName: function (response: any, defaultFileName?: string) {
    if (defaultFileName) {
      return defaultFileName;
    }

    let fileName = "download";
    const contentDisposition = response.headers["content-disposition"];
    if (!contentDisposition) {
      return fileName;
    }

    const rfc5987Match = contentDisposition.match(/filename\*=(.*''.+)/); // Rfc5987
    if (rfc5987Match?.length === 2) {
      let tmp = rfc5987Match[1].split("''");
      if (tmp.length === 1) {
        fileName = decodeURI(tmp[0]);
      } else if (tmp.length === 2) {
        fileName = decodeURI(tmp[1]);
      }
    }

    if (!fileName) {
      let match = contentDisposition.match(/filename="(.+)"/); // other
      if (match?.length === 2) {
        fileName = decodeURI(match[1]);
      }
    }

    return fileName;
  },
};

const axiosInstance = axios.create({
  // baseURL: import.meta.env.BASE_URL, // 基础请求地址
  baseURL: "http://localhost:8080", // 基础请求地址
  timeout: 10000, // 请求超时设置
  withCredentials: false, // 跨域请求是否需要携带 cookie
});

axiosInstance.interceptors.request.use(
  function (config: InternalAxiosRequestConfig) {
    // config.headers.set({
    //   'Content-Type': 'application/json; charset=utf-8',
    // });

    // const token = localStorage.getItem('token');
    // if (token) {
    //   config.headers.set('Authorization', `Bearer ${token}`);
    // }
    return config;
  },
  function (error: any) {
    return Promise.reject(error);
  }
);

axiosInstance.interceptors.response.use(
  function (response: AxiosResponse<IHttpApiResponse<any> | Blob>) {
    // Any status code that lie within the range of 2xx cause this function to trigger
    if (response.status !== 200) {
      return Promise.reject({
        isBizError: false,
        data: response,
      });
    }

    if (!helper.hasBizError(response)) {
      return response;
    }

    if (helper.isDownload(response)) {
      return new Promise((resolve, reject) => {
        let fileReader = new FileReader();
        fileReader.onload = function (e) {
          // BizError
          return reject({
            isBizError: true,
            data: fileReader.result,
          });
        };
        fileReader.readAsText(response.data as Blob);
      });
    }

    handleGlobalBizError(response.data as IHttpApiResponse<any>);
    return Promise.reject({
      isBizError: true,
      data: response.data,
    });
  },
  function (error) {
    // Any status codes that falls outside the range of 2xx cause this function to trigger
    handleGlobalHttpStatusError(error.status);
    return Promise.reject({
      isBizError: false,
      data: error.response ?? error,
    });
  }
);

const HttpHelper = {
  get: function <T>(
    url: string,
    options?: {
      config?: AxiosRequestConfig<any>; // request配置
      isThrow?: boolean; // 是否使用reject(error)外抛错误。默认为false
    }
  ): Promise<IHttpApiResponse<T>> {
    return new Promise((resolve, reject) => {
      axiosInstance
        .get(url, options?.config)
        .then(function (response) {
          resolve(response.data);
        })
        .catch(function (error) {
          if (options?.isThrow) {
            reject(error);
          }
        })
        .finally(function () {
          // always executed
        });
    });
  },

  post: function <T>(
    url: string,
    data?: any,
    options?: {
      config?: AxiosRequestConfig<any>; // request配置
      isThrow?: boolean; // 是否使用reject(error)外抛错误。默认为false
    }
  ): Promise<IHttpApiResponse<T>> {
    return new Promise((resolve, reject) => {
      axiosInstance
        .post(url, data, options?.config)
        .then(function (response) {
          resolve(response.data);
        })
        .catch(function (error) {
          if (options?.isThrow) {
            reject(error);
          }
        })
        .finally(function () {
          // always executed
        });
    });
  },

  put: function <T>(
    url: string,
    data?: any,
    options?: {
      config?: AxiosRequestConfig<any>; // request配置
      isThrow?: boolean; // 是否使用reject(error)外抛错误。默认为false
    }
  ): Promise<IHttpApiResponse<T>> {
    return new Promise((resolve, reject) => {
      axiosInstance
        .put(url, data, options?.config)
        .then(function (response) {
          resolve(response.data);
        })
        .catch(function (error) {
          if (options?.isThrow) {
            reject(error);
          }
        })
        .finally(function () {
          // always executed
        });
    });
  },

  delete: function <T>(
    url: string,
    options?: {
      config?: AxiosRequestConfig<any>; // request配置
      isThrow?: boolean; // 是否使用reject(error)外抛错误。默认为false
    }
  ): Promise<IHttpApiResponse<T>> {
    return new Promise((resolve, reject) => {
      axiosInstance
        .delete(url, options?.config)
        .then(function (response) {
          resolve(response.data);
        })
        .catch(function (error) {
          if (options?.isThrow) {
            reject(error);
          }
        })
        .finally(function () {
          // always executed
        });
    });
  },

  /**
   * post JSON数据
   *
   * @param url url
   * @param data JSON格式数据
   * @param isThrow  是否使用reject(error)外抛错误。默认为false
   * @returns
   */
  postJson: function <T>(url: string, data: {}, isThrow?: boolean): Promise<IHttpApiResponse<T>> {
    const config = {
      headers: {
        "Content-Type": "application/json;charset:utf-8;",
      },
    };

    return this.post(url, data, {
      config: config,
      isThrow: isThrow,
    });
  },

  /**
   * post HTML form作为JSON数据
   * @param url url
   * @param formId form表单的id
   * @param isThrow  是否使用reject(error)外抛错误。默认为false
   * @returns
   */
  postFormAsJson: function <T>(url: string, formId: string, isThrow?: boolean): Promise<IHttpApiResponse<T>> {
    const config = {
      headers: {
        "Content-Type": "application/json;charset:utf-8;",
      },
    };

    const data = document.querySelector("#" + formId);
    return this.post(url, data, {
      config: config,
      isThrow: isThrow,
    });
  },

  /**
   * 使用post 'Content-Type': 'application/x-www-form-urlencoded'
   *
   * @param url url
   * @param data JSON格式数据
   * @param isThrow  是否使用reject(error)外抛错误。默认为false
   * @returns
   */
  postUrlencoded: function <T>(url: string, data: {}, isThrow?: boolean): Promise<IHttpApiResponse<T>> {
    const config = {
      headers: {
        "Content-Type": "application/x-www-form-urlencoded;charset:utf-8;",
      },
    };
    return this.post(url, data, {
      config: config,
      isThrow: isThrow,
    });
  },

  /**
   * 使用post 'Content-Type': 'multipart/form-data'
   *
   * @param url url
   * @param data JSON格式数据。包含文件信息<br/>示例
   * @param isThrow  是否使用reject(error)外抛错误。默认为false
   * {
   *    userId: 1,
   *    avatars: document.querySelector('#fileInput').files
   *  }
   *
   * input示例:
   * <input id="fileInput" type="file" name="avatars" multiple />
   * @returns
   */
  postMultipart: function <T>(url: string, data: {}, isThrow?: boolean): Promise<IHttpApiResponse<T>> {
    const config = {
      headers: {
        "Content-Type": "multipart/form-data",
      },
    };

    return this.post(url, data, {
      config: config,
      isThrow: isThrow,
    });
  },

  /**
   * 上传多个文件(可包含其他数据字段)
   *
   * @param url url
   * @param data JSON格式数据。包含文件信息<br/>示例
   * @param isThrow  是否使用reject(error)外抛错误。默认为false
   * {
   *    userId: 1,
   *    avatars: document.querySelector('#fileInput').files
   *  }
   *
   * input示例:
   * <input id="fileInput" type="file" name="avatars" multiple />
   * @returns
   * @see HttpHelper.postMultipart()
   */
  uploadFiles: function <T>(url: string, data: {}, isThrow?: boolean): Promise<IHttpApiResponse<T>> {
    return this.postMultipart(url, data, isThrow);
  },

  /**
   * 使用GET请求下载
   *
   * @param url url
   * @param options 其他参数
   */
  getDownload: function (
    url: string,
    options?: {
      filename?: string; // 默认文件名
      isThrow?: boolean; // 是否使用reject(error)外抛错误。默认为false
    }
  ): Promise<void> {
    const config = {
      responseType: "blob" as ResponseType,
    };

    return new Promise((resolve, reject) => {
      axiosInstance
        .get(url, config)
        .then((response) => {
          helper.download(response, options?.filename);
          resolve();
        })
        .catch(function (error) {
          if (options?.isThrow) {
            reject(error);
          }
        });
    });
  },

  /**
   * 使用POST请求下载
   *
   * @param url url
   * @param data 请求参数
   * @param options 其他参数
   */
  postDownload: function (
    url: string,
    data: {},
    options?: {
      filename?: string; // 默认文件名
      isThrow?: boolean; // 是否使用reject(error)外抛错误。默认为false
    }
  ): Promise<void> {
    const config = {
      responseType: "blob" as ResponseType,
    };

    return new Promise((resolve, reject) => {
      axiosInstance
        .post(url, data, config)
        .then((response) => {
          helper.download(response, options?.filename);
          resolve();
        })
        .catch(function (error) {
          if (options?.isThrow) {
            reject(error);
          }
        });
    });
  },
};

export default HttpHelper;
  • 数据类型/api/types.ts
export interface IHttpApiResponse<T> {
    type: "IHttpApiResponse";
    /** 请求的唯一id */
    requestId: string;
    /** 响应编码, 200-成功;非200-业务异常码 */
    code: number;
    /** 提示信息 */
    message: string;
    /** 应答消息体 */
    data: T;
}

export interface IHttpApiError<T> {
    type: "IHttpApiError";
    /** 是否是业务异常 */
    isBizError: boolean;
    /** 错误详细数据 */
    data: any | IHttpApiResponse<T>;
}
  • 全局错误处理/api/handler.ts
import type { IHttpApiResponse } from "./types";

const handleGlobalHttpStatusError = (status: number): void => {
  let message = "未知错误";
  if (status) {
    switch (status) {
      case 400:
        message = "错误的请求";
        break;
      case 401:
        message = "未授权,请重新登录";
        break;
      case 403:
        message = "拒绝访问";
        break;
      case 404:
        message = "请求错误,未找到该资源";
        break;
      case 405:
        message = "请求方法未允许";
        break;
      case 408:
        message = "请求超时";
        break;
      case 500:
        message = "服务器端出错";
        break;
      case 501:
        message = "网络未实现";
        break;
      case 502:
        message = "网络错误";
        break;
      case 503:
        message = "服务不可用";
        break;
      case 504:
        message = "网络超时";
        break;
      case 505:
        message = "http版本不支持该请求";
        break;
      default:
        message = `其他错误 --${status}`;
    }
  } else {
    message = `无法连接到服务器!`;
  }

  console.log(message);
};

const handleGlobalBizError = (resp: IHttpApiResponse<any>): void => {
  switch (resp.code) {
    case 200:
      break;
    case 30000:
      console.log(`Business Error: ${resp.message}`);
      break;
    default:
  }
};

export { handleGlobalHttpStatusError, handleGlobalBizError };

export default {};

2 模块API

以test模块为示例

  • API文件/api/modules/test/index.ts
import HttpHelper from "@/api/HttpHelper";
import type { IHttpApiError, IHttpApiResponse } from "@/api/types";
import type { CreateRequestV1, CreateResponseV1 } from "./types";

const Api = {
  v1: {
    get200: `/v1/test/get200`,
    get200BizError: "/v1/test/get200BizError",
    get2xx: "/v1/test/get2xx",
    get5xx: "/v1/test/get5xx",
    create: "/v1/test/create",
    upload: "/v1/test/upload",
    getDownload: "/v1/download/getDownload",
    getImage: "/v1/download/getImage",
    getDownloadBizError: "/v1/download/getDownloadBizError",
    postDownload: "/v1/download/postDownload",
  },
};

const test = {
  get200: function (): Promise<IHttpApiResponse<string>> {
    return HttpHelper.get(Api.v1.get200);
  },

  get200BizError: function (isThrow: boolean = false): Promise<IHttpApiResponse<string>> {
    return HttpHelper.get(Api.v1.get200BizError, { isThrow });
  },

  get2xx: function (isThrow: boolean = false): Promise<IHttpApiResponse<string>> {
    return HttpHelper.get(Api.v1.get2xx, { isThrow });
  },

  get5xx: function (isThrow: boolean = false): Promise<IHttpApiResponse<string>> {
    return HttpHelper.get(Api.v1.get5xx, { isThrow });
  },

  create: function (params: CreateRequestV1): Promise<IHttpApiResponse<CreateResponseV1>> {
    return HttpHelper.postJson(Api.v1.create, params);
  },

  upload: function (): Promise<IHttpApiResponse<Array<string>>> {
    return HttpHelper.uploadFiles(Api.v1.upload, {
      testId: 8001,
      files: (document.querySelector("#files") as HTMLInputElement).files,
    });
  },

  getDownload: function (): Promise<void> {
    return HttpHelper.getDownload(Api.v1.getDownload, { filename: "1.jpg" });
  },

  getJpgImage: function (): Promise<void> {
    return HttpHelper.getDownload(Api.v1.getImage + "?type=jpg");
  },

  getPngImage: function (): Promise<void> {
    return HttpHelper.getDownload(Api.v1.getImage + "?type=png");
  },

  getDownloadBizError: function (): Promise<void> {
    return HttpHelper.getDownload(Api.v1.getDownloadBizError, { isThrow: true });
  },

  postDownload: function (): Promise<void> {
    return HttpHelper.postDownload(
      Api.v1.postDownload,
      {
        fileName: "中文.jpg",
      },
      {
        filename: "自定义名称.jpg",
      }
    );
  },
};

export default test;
  • 数据类型/api/modules/test/types.ts
export interface CreateRequestV1 {
  testId: number;
  name: string;
}
export interface CreateResponseV1 {
  message: string;
}

3 统一API export

  • /api/HttpApi.ts
import test from "./modules/test";

const HttpApi = {
  test,
};

export default HttpApi;

4 示例

  • App.vue
<script setup lang="ts">
import HttpApi from "./api/HttpApi";
import type { CreateResponseV1 } from "./api/modules/test/types";
import type { IHttpApiResponse } from "./api/types";

function get200() {
  HttpApi.test.get200().then(function (response: IHttpApiResponse<string>) {
    console.log(response);
  });
}
function get200BizError() {
  HttpApi.test.get200BizError().then(function (response: IHttpApiResponse<string>) {
    console.log(response);
  });
}
function get200BizErrorThrow() {
  HttpApi.test
    .get200BizError(true)
    .then(function (response: IHttpApiResponse<string>) {
      console.log(response);
    })
    .catch((error: any) => {
      if (error.isBizError) {
        console.log("BizError", error.data);
      } else {
        console.log("SystemError", error.data);
      }
    });
}

function get2xx() {
  HttpApi.test.get2xx().then(function (response: IHttpApiResponse<string>) {
    console.log(response);
  });
}
function get2xxThrow() {
  HttpApi.test
    .get2xx(true)
    .then(function (response: IHttpApiResponse<string>) {
      console.log(response);
    })
    .catch((error: any) => {
      if (error.isBizError) {
        console.log("BizError", error.data);
      } else {
        console.log("SystemError", error.data);
      }
    });
}

function get5xx() {
  HttpApi.test.get5xx().then(function (response: IHttpApiResponse<string>) {
    console.log(response);
  });
}
function get5xxThrow() {
  HttpApi.test
    .get5xx(true)
    .then(function (response: IHttpApiResponse<string>) {
      console.log(response);
    })
    .catch((error: any) => {
      if (error.isBizError) {
        console.log("BizError", error.data);
      } else {
        console.log("SystemError", error.data);
      }
    });
}

function post() {
  HttpApi.test
    .create({
      testId: 3001,
      name: "Tom",
    })
    .then(function (response: IHttpApiResponse<CreateResponseV1>) {
      console.log(response);
    });
}
function uploadFiles() {
  HttpApi.test.upload().then(function (response: IHttpApiResponse<Array<string>>) {
    console.log(response);
  });
}

function getDownloadBizError() {
  HttpApi.test.getDownloadBizError().catch((error: any) => {
    if (error.isBizError) {
      console.log("BizError", error.data);
    } else {
      console.log("SystemError", error.data);
    }
  });
}
</script>

<template>
  <button @click="get200">GET (200)</button>
  <button @click="get200BizError">GET (200, BizError)</button>
  <button @click="get200BizErrorThrow">GET (200, BizError, Throw)</button><br />

  <button @click="get2xx">GET (2xx)</button>
  <button @click="get2xxThrow">GET (2xx, Throw)</button><br />

  <button @click="get5xx">GET (5xx)</button>
  <button @click="get5xxThrow">GET (5xx, Throw)</button>

  <button @click="post">POST</button>
  <button @click="uploadFiles">UPLOAD FILES</button><br />

  <button @click="HttpApi.test.getDownload">GET DOWNLOAD</button>
  <button @click="HttpApi.test.getJpgImage">GET JPG IMAGE</button>
  <button @click="HttpApi.test.getPngImage">GET PNG IMAGE</button>
  <button @click="getDownloadBizError">GET DOWNLOAD (BizError, Throw)</button>
  <button @click="HttpApi.test.postDownload">POST DOWNLOAD</button>
  <fieldset>
    <legend>Form</legend>
    <form method="post" enctype="multipart/form-data" action="http://localhost:8080/v1/test/upload">
      testId: <input name="testId" value="8001" /><br />
      files: <input id="files" type="file" name="files" multiple /><br />
      <input type="submit" value="Submit" />
    </form>
  </fieldset>
</template>

<style scoped>
button,
input {
  margin: 5px;
}
</style>