Axios 封装:那么多的Vue3 后台模版,能不能结合下各自的优点?

596 阅读6分钟

现有的像是vben-admin,pure-admin,这些的后台模版,都有各自的 Axios 封装,这两个框架我都使用了很久,在使用的过程中,虽然已经很便利了;VbenAdmin 对请求的状态控制划分的比较细、清晰明了;PureAdmin 又比较简洁;能不能综合下各自的优势呢?

需求

  • 整合 VbenAdminPureAdmin 中的 Axios 的封装

用法

export interface PureHttpResponse extends AxiosResponse<Result> {
  config: PureHttpRequestConfig;
}

export interface PureHttpRequestConfig extends AxiosRequestConfig {
  beforeRequestCallback?: (request: PureHttpRequestConfig) => void;
  beforeResponseCallback?: (response: PureHttpResponse) => void;
}

export default class PureHttp {
  request<T>(
    method: RequestMethods,
    url: string,
    param?: AxiosRequestConfig,
    axiosConfig?: PureHttpRequestConfig
  ): Promise<T>;
  post<T, P>(
    url: string,
    params?: T,
    config?: PureHttpRequestConfig
  ): Promise<P>;
  get<T, P>(
    url: string,
    params?: T,
    config?: PureHttpRequestConfig
  ): Promise<P>;
  delete<T, P>(
    url: string,
    params?: T,
    config?: PureHttpRequestConfig
  ): Promise<P>;
  put<T, P>(
    url: string,
    params?: T,
    config?: PureHttpRequestConfig
  ): Promise<P>;
  patch<T, P>(
    url: string,
    params?: T,
    config?: PureHttpRequestConfig
  ): Promise<P>;
}

实现效果

import { http } from "@/utils/http";
import { prefix } from "./prefix";

import type { ConfigVO, SmsConfig, DigitalVirtualParams,  ReturnDigitalVirtualData } from './props'

// 查询平台配置
export const findConfig = () =>
  http.request<ConfigVO>("get", `${prefix.sys}/config`);

// 修改短信平台配置
export const updateSmsConfig = (data: SmsConfig) =>
  http.post<SmsConfig, any>(`${prefix.sys}/config/sms`, {
    data
  });
  
// 数字虚拟人总览接口
export const getDigitalVirtualData = (params: DigitalVirtualParams) =>
  http.get<DigitalVirtualParams, ReturnDigitalVirtualData>(
    `${prefix.robot}/digital-human/overview/total`,
    {
      params
    }
);

代码

  1. 目录结构

image.png

  1. index.ts
import Axios, {
  AxiosInstance,
  AxiosRequestConfig,
  CustomParamsSerializer
} from "axios";
import {
  PureHttpError,
  RequestMethods,
  PureHttpResponse,
  PureHttpRequestConfig
} from "./types.d";
import { stringify } from "qs";
import NProgress from "../progress";
import { getToken, formatToken } from "@/utils/auth";
import { handleSystemStatus, handlerAbnormalCode } from "./status";

// 相关配置请参考:www.axios-js.com/zh-cn/docs/#axios-request-config-1
const defaultConfig: AxiosRequestConfig = {
  baseURL: import.meta.env.MODE === "production" ? window.API : "",
  // 请求超时时间
  timeout: 10000,
  headers: {
    Accept: "application/json, text/plain, */*",
    "Content-Type": "application/json",
    "X-Requested-With": "XMLHttpRequest"
  },
  // 数组格式参数序列化(https://github.com/axios/axios/issues/5142)
  paramsSerializer: {
    serialize: stringify as unknown as CustomParamsSerializer
  }
};

const whiteList = ["/refreshToken", "/api-sys/oauth2/web/token"];

class PureHttp {
  [x: string]: any;
  constructor() {
    this.httpInterceptorsRequest();
    this.httpInterceptorsResponse();
  }

  /** token过期后,暂存待执行的请求 */
  private static requests = [];

  /** 防止重复刷新token */
  private static isRefreshing = false;

  /** 初始化配置对象 */
  private static initConfig: PureHttpRequestConfig = {};

  /** 保存当前Axios实例对象 */
  private static axiosInstance: AxiosInstance = Axios.create(defaultConfig);

  /** 重连原始请求 */
  private static retryOriginalRequest(config: PureHttpRequestConfig) {
    return new Promise(resolve => {
      PureHttp.requests.push((token: string) => {
        config.headers["Authorization"] = formatToken(token);
        resolve(config);
      });
    });
  }

  /** 请求拦截 */
  private httpInterceptorsRequest(): void {
    PureHttp.axiosInstance.interceptors.request.use(
      async (config: PureHttpRequestConfig): Promise<any> => {
        // 开启进度条动画
        NProgress.start();
        // 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
        if (typeof config.beforeRequestCallback === "function") {
          config.beforeRequestCallback(config);
          return config;
        }
        if (PureHttp.initConfig.beforeRequestCallback) {
          PureHttp.initConfig.beforeRequestCallback(config);
          return config;
        }
        /** 请求白名单,放置一些不需要token的接口(通过设置请求白名单,防止token过期后再请求造成的死循环问题) */
        return whiteList.find(url => url === config.url)
          ? config
          : new Promise(resolve => {
              const token = getToken();
              config.headers["Authorization"] = formatToken(token);
              resolve(config);
            });
      },
      error => {
        return Promise.reject(error);
      }
    );
  }

  /** 响应拦截 */
  private httpInterceptorsResponse(): void {
    const instance = PureHttp.axiosInstance;
    instance.interceptors.response.use(
      (response: PureHttpResponse) => {
        const $config = response.config;
        // 关闭进度条动画
        NProgress.done();
        // 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
        if (typeof $config.beforeResponseCallback === "function") {
          $config.beforeResponseCallback(response);
          return response.data;
        }
        if (PureHttp.initConfig.beforeResponseCallback) {
          PureHttp.initConfig.beforeResponseCallback(response);
          return response.data;
        }
        const { code, msg, data: result } = response.data;

        const isWhite = whiteList.find(url => url === response.config.url);
        // 白名单直接处理且须有返回
        if (isWhite) {
          handlerAbnormalCode(code, msg);
          return {
            ...result,
            status: code === "000000" ? 1 : 0
          };
        }
        if (code !== "000000") {
          handlerAbnormalCode(code, msg);
          return Promise.reject("异常!");
        }
        // 返回结果
        return result;
      },
      (error: PureHttpError) => {
        // 关闭进度条动画
        NProgress.done();
        const $error = error;
        $error.isCancelRequest = Axios.isCancel($error);
        const { code, message } = error;
        const err: string = error?.toString?.() ?? "";
        // 网络超时
        if (code === "ECONNABORTED" && message.indexOf("timeout") !== -1) {
          handleSystemStatus("ECONNABORTED");
        } else if (err?.includes("Network Error")) {
          // 网络错误
          handleSystemStatus("Network Error");
        } else {
          handleSystemStatus(error?.response?.status);
        }
        // 所有的响应异常 区分来源为取消请求/非取消请求
        return Promise.reject($error);
      }
    );
  }

  /** 通用请求工具函数 */
  public request<T>(
    method: RequestMethods,
    url: string,
    param?: AxiosRequestConfig,
    axiosConfig?: PureHttpRequestConfig
  ): Promise<T> {
    const config = {
      method,
      url,
      ...param,
      ...axiosConfig
    } as PureHttpRequestConfig;

    // 单独处理自定义请求/响应回调
    return new Promise((resolve, reject) => {
      PureHttp.axiosInstance
        .request(config)
        .then((response: undefined) => {
          resolve(response);
        })
        .catch(error => {
          reject(error);
        });
    });
  }

  /** 单独抽离的post工具函数 */
  public post<T, P>(
    url: string,
    params?: AxiosRequestConfig<T>,
    config?: PureHttpRequestConfig
  ): Promise<P> {
    return this.request<P>("post", url, params, config);
  }

  /** 单独抽离的get工具函数 */
  public get<T, P>(
    url: string,
    params?: AxiosRequestConfig<T>,
    config?: PureHttpRequestConfig
  ): Promise<P> {
    return this.request<P>("get", url, params, config);
  }

  /** 单独抽离的delete工具函数 */
  public delete<T, P>(
    url: string,
    params?: AxiosRequestConfig<T>,
    config?: PureHttpRequestConfig
  ): Promise<P> {
    return this.request<P>("delete", url, params, config);
  }

  /** 单独抽离的put工具函数 */
  public put<T, P>(
    url: string,
    params?: AxiosRequestConfig<T>,
    config?: PureHttpRequestConfig
  ): Promise<P> {
    return this.request<P>("put", url, params, config);
  }

  /** 单独抽离的patch工具函数 */
  public patch<T, P>(
    url: string,
    params?: AxiosRequestConfig<T>,
    config?: PureHttpRequestConfig
  ): Promise<P> {
    return this.request<P>("patch", url, params, config);
  }
}

export const http = new PureHttp();
  1. status.ts
import sys from "./enum";
import { removeToken } from "../auth";
import { ElMessageBox } from "element-plus";
import { useUserStoreHook } from "@/store/modules/user";
import { message } from "../message";

// 创建队列

export function handleSystemStatus(
  status: number | string,
  msg?: string
): void {
  if (status !== 401) {
    let errMessage = "";
    switch (status) {
      case 400:
        errMessage = `${msg}`;
        break;
      case 403:
        errMessage = sys.api.errMsg403;
        break;
      // 404请求不存在
      case 404:
        errMessage = sys.api.errMsg404;
        break;
      case 405:
        errMessage = sys.api.errMsg405;
        break;
      case 408:
        errMessage = sys.api.errMsg408;
        break;
      case 500:
        errMessage = sys.api.errMsg500;
        break;
      case 501:
        errMessage = sys.api.errMsg501;
        break;
      case 502:
        errMessage = sys.api.errMsg502;
        break;
      case 503:
        errMessage = sys.api.errMsg503;
        break;
      case 504:
        errMessage = sys.api.errMsg504;
        break;
      case 505:
        errMessage = sys.api.errMsg505;
        break;
      case "Network Error":
        errMessage = sys.api.networkExceptionMsg;
        break;
      case "ECONNABORTED":
        errMessage = sys.api.apiTimeoutMessage;
        break;
      case "ABNORMAL":
        errMessage = sys.api.apiRequestFailed;
        break;
      default:
    }
    // 存在错误信息就提示
    errMessage &&
      message(errMessage, {
        type: "error"
      });
    return;
  }
  ElMessageBox.alert("用户登录已过期, 确定跳转登录页吗?", "过期提示", {
    confirmButtonText: "确定",
    callback: () => {
      removeToken();
      location.reload();
    }
  });
}

// 异常状态码
export const CodeEnum = ["103107", "103104", "103105", "103109", "103102"];

// 异常状态码处理
export const handlerAbnormalCode = (code: string, msg?: string): void => {
  // 用户名密码错误多次提交需要多次提示 '103111', '291003'
  const userStore = useUserStoreHook();
  switch (code) {
    case "103107":
      ElMessageBox.alert(
        "该用户已在其他地方登录, 确定强制登录吗?",
        "登录提示",
        {
          confirmButtonText: "确定",
          callback: () => {
            userStore.handleForceLogin();
          }
        }
      );
      break;
    case "103104":
      ElMessageBox.alert(
        "该用户已在其他地方登录, 确定重新登录吗?",
        "登录提示",
        {
          confirmButtonText: "确定",
          callback: () => {
            userStore.expiredLogin();
          }
        }
      );
      break;
    case "103105":
      ElMessageBox.alert(
        "该用户密码已过期, 确定跳转修改密码页吗?",
        "过期提示",
        {
          confirmButtonText: "确定",
          callback: async () => {
            userStore.handleJumpToModifyPage();
          }
        }
      );
      break;
    case "103109":
      ElMessageBox.alert("该用户权限已变更, 确定跳转登录页吗?", "变更提示", {
        confirmButtonText: "确定",
        callback: () => {
          userStore.expiredLogin();
        }
      });
      break;
    case "103102":
      message("登录已失效,请重新登录!", {
        type: "error"
      });
      userStore.expiredLogin();
      break;
    default:
      msg &&
        message(msg, {
          type: "error"
        });
  }
};
  1. enum.ts
export default {
  api: {
    operationFailed: "操作失败",
    errorTip: "错误提示",
    timeoutMessage: "登录超时,请重新登录!",
    apiTimeoutMessage: "接口请求超时,请刷新页面重试!",
    apiRequestFailed: "请求出错,请稍候重试",
    networkException: "网络异常",
    networkExceptionMsg: "网络异常,请检查您的网络连接是否正常!",

    errMsg401: "用户没有权限(令牌、用户名、密码错误)!",
    errMsg403: "用户得到授权,但是访问是被禁止的。!",
    errMsg404: "网络请求错误,未找到该资源!",
    errMsg405: "网络请求错误,请求方法未允许!",
    errMsg408: "网络请求超时!",
    errMsg500: "服务器错误,请联系管理员!",
    errMsg501: "网络未实现!",
    errMsg502: "网络错误!",
    errMsg503: "服务不可用,服务器暂时过载或维护!",
    errMsg504: "网络超时!",
    errMsg505: "http版本不支持该请求!"
  },
  app: {
    logoutTip: "温馨提醒",
    logoutMessage: "是否确认退出系统?",
    menuLoading: "菜单加载中..."
  },
  errorLog: {
    tableTitle: "错误日志列表",
    tableColumnType: "类型",
    tableColumnDate: "时间",
    tableColumnFile: "文件",
    tableColumnMsg: "错误信息",
    tableColumnStackMsg: "stack信息",
    tableActionDesc: "详情",
    modalTitle: "错误详情",
    fireVueError: "点击触发vue错误",
    fireResourceError: "点击触发资源加载错误",
    fireAjaxError: "点击触发ajax错误",
    enableMessage:
      "只在`/src/settings/projectSetting.ts` 内的useErrorHandle=true时生效."
  },
  exception: {
    backLogin: "返回登录",
    backHome: "返回首页",
    subTitle403: "抱歉,您无权访问此页面。",
    subTitle404: "抱歉,您访问的页面不存在。",
    subTitle500: "抱歉,服务器报告错误。",
    noDataTitle: "当前页无数据",
    networkErrorTitle: "网络错误",
    networkErrorSubTitle: "抱歉,您的网络连接已断开,请检查您的网络!"
  },
  lock: {
    unlock: "点击解锁",
    alert: "锁屏密码错误",
    backToLogin: "返回登录",
    entry: "进入系统",
    placeholder: "请输入锁屏密码或者用户密码"
  },
  login: {
    backSignIn: "返回",
    signInFormTitle: "登录",
    mobileSignInFormTitle: "手机登录",
    qrSignInFormTitle: "二维码登录",
    signUpFormTitle: "注册",
    forgetFormTitle: "重置密码",

    signInTitle: "开箱即用的中后台管理系统",
    signInDesc: "输入您的个人详细信息开始使用!",
    policy: "我同意xxx隐私政策",
    scanSign: `扫码后点击"确认",即可完成登录`,

    loginButton: "登录",
    registerButton: "注册",
    rememberMe: "记住我",
    forgetPassword: "忘记密码?",
    otherSignIn: "其他登录方式",

    // notify
    loginSuccessTitle: "登录成功",
    loginSuccessDesc: "欢迎回来",

    // placeholder
    accountPlaceholder: "请输入账号",
    passwordPlaceholder: "请输入密码",
    smsPlaceholder: "请输入验证码",
    mobilePlaceholder: "请输入手机号码",
    policyPlaceholder: "勾选后才能注册",
    diffPwd: "两次输入密码不一致",

    userName: "账号",
    password: "密码",
    confirmPassword: "确认密码",
    email: "邮箱",
    smsCode: "短信验证码",
    mobile: "手机号码"
  }
};

/**
 * @description: request method
 */
export enum RequestEnum {
  GET = "GET",
  POST = "POST",
  PUT = "PUT",
  DELETE = "DELETE"
}

/**
 * @description:  contentTyp
 */
export enum ContentTypeEnum {
  // json
  JSON = "application/json;charset=UTF-8",
  // form-data qs
  FORM_URLENCODED = "application/x-www-form-urlencoded;charset=UTF-8",
  // form-data  upload
  FORM_DATA = "multipart/form-data;charset=UTF-8"
}
  1. types.d.ts

import Axios, {
  Method,
  AxiosError,
  AxiosResponse,
  AxiosRequestConfig
} from "axios";

export type resultType = {
  accessToken?: string;
};

export type RequestMethods = Extract<
  Method,
  "get" | "post" | "put" | "delete" | "patch" | "option" | "head"
>;

export interface PureHttpError extends AxiosError {
  isCancelRequest?: boolean;
}

export interface Result<T = any> {
  code: string;
  data: T;
  msg: string;
}

export interface PureHttpResponse extends AxiosResponse<Result> {
  config: PureHttpRequestConfig;
}

export interface PureHttpRequestConfig extends AxiosRequestConfig {
  beforeRequestCallback?: (request: PureHttpRequestConfig) => void;
  beforeResponseCallback?: (response: PureHttpResponse) => void;
}

export default class PureHttp {
  request<T>(
    method: RequestMethods,
    url: string,
    param?: AxiosRequestConfig,
    axiosConfig?: PureHttpRequestConfig
  ): Promise<T>;
  post<T, P>(
    url: string,
    params?: T,
    config?: PureHttpRequestConfig
  ): Promise<P>;
  get<T, P>(
    url: string,
    params?: T,
    config?: PureHttpRequestConfig
  ): Promise<P>;
  delete<T, P>(
    url: string,
    params?: T,
    config?: PureHttpRequestConfig
  ): Promise<P>;
  put<T, P>(
    url: string,
    params?: T,
    config?: PureHttpRequestConfig
  ): Promise<P>;
  patch<T, P>(
    url: string,
    params?: T,
    config?: PureHttpRequestConfig
  ): Promise<P>;
}