如何用ts搭建一个vue3通用项目底座 | 第六篇:axios封装

前言

本篇来封装一个axios,这部分很重要。

1、createAxios函数

新建/@/utils/http/index.ts文件,这里面用来存放核心的createAxios函数。

// /@/utils/http/index.ts
function createAxios(opt?: Partial<CreateAxiosOptions>) {
  return new VAxios(
    // 深度合并
    deepMerge(
      {
        // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
        // authentication schemes,e.g: Bearer
        // authenticationScheme: 'Bearer',
        authenticationScheme: '',
        timeout: 10 * 1000,
        // 基础接口地址
        // baseURL: globSetting.apiUrl,

        headers: { 'Content-Type': ContentTypeEnum.JSON },
        // 如果是form-data格式
        // headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
        // 数据处理方式
        transform: clone(transform),
        // 配置项,下面的选项都可以在独立的接口请求中覆盖
        requestOptions: {
          // 默认将prefix 添加到url
          joinPrefix: true,
          // 是否返回原生响应头 比如:需要获取响应头时使用该属性
          isReturnNativeResponse: false,
          // 需要对返回数据进行处理
          isTransformResponse: true,
          // post请求的时候添加参数到url
          joinParamsToUrl: false,
          // 格式化提交参数时间
          formatDate: true,
          // 消息提示类型
          errorMessageMode: 'message',
          // 接口地址
          apiUrl: globSetting.apiUrl,
          // 接口拼接地址
          urlPrefix: urlPrefix,
          //  是否加入时间戳
          joinTime: true,
          // 忽略重复请求
          ignoreCancelToken: true,
          // 是否携带token
          withToken: true,
          retryRequest: {
            isOpenRetry: true,
            count: 5,
            waitTime: 100,
          },
        },
      },
      opt || {},
    ),
  );
}
export const defHttp = createAxios();

有很多爆红,下面逐个解析。

1.1、CreateAxiosOptions类型

新建/@/utils/http/axios/axiosTransform.ts文件作为数据处理类。

// /@/utils/http/axios/axiosTransform.ts
/**
 * 数据处理类,可根据项目配置
 */
import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import type { RequestOptions, Result } from '/#/axios';

export interface CreateAxiosOptions extends AxiosRequestConfig {
  authenticationScheme?: string;
  transform?: AxiosTransform;
  requestOptions?: RequestOptions;
}

export abstract class AxiosTransform {
  /**
   * @description: 请求前的流程配置
   */
  beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;

  /**
   * @description: 处理响应数据
   */
  transformResponseHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any;

  /**
   * @description: 请求失败处理
   */
  requestCatchHook?: (e: Error, options: RequestOptions) => Promise<any>;

  /**
   * @description: 请求之前的拦截器
   */
  requestInterceptors?: (
    config: InternalAxiosRequestConfig,
    options: CreateAxiosOptions,
  ) => InternalAxiosRequestConfig;

  /**
   * @description: 请求之后的拦截器
   */
  responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;

  /**
   * @description: 请求之前的拦截器错误处理
   */
  requestInterceptorsCatch?: (error: Error) => void;

  /**
   * @description: 请求之后的拦截器错误处理
   */
  responseInterceptorsCatch?: (axiosInstance: AxiosResponse, error: Error) => void;
}

从axiosTransform数据处理类里面可以看到后续要封装的各种函数处理。
接着去全局类型里补充缺失的类型,新建/@/types/axios.d.ts文件。

// /@/types/axios.d.ts
export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined;
export type SuccessMessageMode = ErrorMessageMode;

export interface RequestOptions {
  // 将请求参数拼接到url
  joinParamsToUrl?: boolean;
  // 格式化请求参数时间
  formatDate?: boolean;
  // 是否处理请求结果
  isTransformResponse?: boolean;
  // 是否返回本机响应标头
  // 例如:当您需要获取响应标头时,请使用此属性
  isReturnNativeResponse?: boolean;
  // 是否加入url
  joinPrefix?: boolean;
  // 接口地址,如果保留为空,请使用默认的apiUrl
  apiUrl?: string;
  // 请求拼接路径
  urlPrefix?: string;
  // 错误消息提示类型
  errorMessageMode?: ErrorMessageMode;
  // 成功消息提示类型
  successMessageMode?: SuccessMessageMode;
  // 是否添加时间戳
  joinTime?: boolean;
  ignoreCancelToken?: boolean;
  // 是否在标头中发送token
  withToken?: boolean;
  // 请求重试机制
  retryRequest?: RetryRequest;
}

export interface RetryRequest {
  isOpenRetry: boolean;
  count: number;
  waitTime: number;
}
export interface Result<T = any> {
  code: number;
  type: 'success' | 'error' | 'warning';
  message: string;
  result: T;
}

// multipart/form-data: upload file
// 文件上传
export interface UploadFileParams {
  // 其他参数
  data?: Recordable;
  // 文件参数接口字段名称
  name?: string;
  // 文件名
  file: File | Blob;
  // 文件名
  filename?: string;
  [key: string]: any;
}

import { CreateAxiosOptions } from ‘./axios/axiosTransform’;
这样CreateAxiosOptions类型就ok了。

1.2、VAxios类

新建/@/utils/http/axios/index.ts文件作为axios模块类。

// /@/utils/http/axios/index.ts
import type {
  AxiosRequestConfig,
  InternalAxiosRequestConfig,
  AxiosInstance,
  AxiosResponse,
  AxiosError,
} from 'axios';
import type { RequestOptions, Result, UploadFileParams } from '/#/axios';
import type { CreateAxiosOptions } from './axiosTransform';
import axios from 'axios';
import qs from 'qs';
import { AxiosCanceler } from './axiosCancel';
import { isFn } from '/@/utils/is';
import { cloneDeep } from 'lodash-es';
import { ContentTypeEnum } from '/@/enums/httpEnum';
import { RequestEnum } from '/@/enums/httpEnum';

export * from './axiosTransform';

/**
 * @description:  axios模块
 */
export class VAxios {
  private axiosInstance: AxiosInstance;
  private readonly options: CreateAxiosOptions;

  constructor(options: CreateAxiosOptions) {
    this.options = options;
    this.axiosInstance = axios.create(options);
    this.setupInterceptors();
  }

  /**
   * @description:  创建axios实例
   */
  private createAxios(config: CreateAxiosOptions): void {
    this.axiosInstance = axios.create(config);
  }

  private getTransform() {
    const { transform } = this.options;
    return transform;
  }

  getAxios(): AxiosInstance {
    return this.axiosInstance;
  }

  /**
   * @description: 重新配置axios
   */
  configAxios(config: CreateAxiosOptions) {
    if (!this.axiosInstance) {
      return;
    }
    this.createAxios(config);
  }

  /**
   * @description: 设置通用header
   */
  setHeader(headers: any): void {
    if (!this.axiosInstance) {
      return;
    }
    Object.assign(this.axiosInstance.defaults.headers, headers);
  }

  /**
   * @description: 拦截器配置
   */
  private setupInterceptors() {
    const transform = this.getTransform();
    if (!transform) {
      return;
    }
    const {
      requestInterceptors,
      requestInterceptorsCatch,
      responseInterceptors,
      responseInterceptorsCatch,
    } = transform;

    const axiosCanceler = new AxiosCanceler();

    // 请求拦截器配置处理
    this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
      // 如果取消重复请求已打开,则禁止取消重复请求
      // @ts-ignore
      const { ignoreCancelToken } = config.requestOptions;
      const ignoreCancel =
        ignoreCancelToken !== undefined
          ? ignoreCancelToken
          : this.options.requestOptions?.ignoreCancelToken;

      !ignoreCancel && axiosCanceler.addPending(config);
      if (requestInterceptors && isFn(requestInterceptors)) {
        config = requestInterceptors(config, this.options);
      }
      return config;
    }, undefined);

    // 请求拦截器错误捕获
    requestInterceptorsCatch &&
      isFn(requestInterceptorsCatch) &&
      this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch);

    // 响应结果拦截器处理
    this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => {
      res && axiosCanceler.removePending(res.config);
      if (responseInterceptors && isFn(responseInterceptors)) {
        res = responseInterceptors(res);
      }
      return res;
    }, undefined);

    // 响应结果拦截器错误捕获
    responseInterceptorsCatch &&
      isFn(responseInterceptorsCatch) &&
      this.axiosInstance.interceptors.response.use(undefined, (error) => {
        // @ts-ignore
        return responseInterceptorsCatch(this.axiosInstance, error);
      });
  }

  /**
   * @description:  文件上传
   * 根据实际接口需要加以更改
   */
  uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
    const formData = new window.FormData();
    const customFilename = params.name || 'file';

    if (params.filename) {
      formData.append(customFilename, params.file, params.filename);
    } else {
      formData.append(customFilename, params.file);
    }

    if (params.data) {
      Object.keys(params.data).forEach((key) => {
        const value = params.data![key];
        if (Array.isArray(value)) {
          value.forEach((item) => {
            formData.append(`${key}[]`, item);
          });
          return;
        }

        formData.append(key, params.data![key]);
      });
    }

    return this.axiosInstance.request<T>({
      ...config,
      method: 'POST',
      data: formData,
      headers: {
        'Content-type': ContentTypeEnum.FORM_DATA,
        // @ts-ignore
        ignoreCancelToken: true,
      },
    });
  }

  // 支持form-data数据格式
  supportFormData(config: AxiosRequestConfig) {
    const headers = config.headers || this.options.headers;
    const contentType = headers?.['Content-Type'] || headers?.['content-type'];

    if (
      contentType !== ContentTypeEnum.FORM_URLENCODED ||
      !Reflect.has(config, 'data') ||
      config.method?.toUpperCase() === RequestEnum.GET
    ) {
      return config;
    }

    return {
      ...config,
      data: qs.stringify(config.data, { arrayFormat: 'brackets' }),
    };
  }

  get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
    return this.request({ ...config, method: 'GET' }, options);
  }

  post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
    return this.request({ ...config, method: 'POST' }, options);
  }

  put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
    return this.request({ ...config, method: 'PUT' }, options);
  }

  delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
    return this.request({ ...config, method: 'DELETE' }, options);
  }

  request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
    let conf: CreateAxiosOptions = cloneDeep(config);
    // cancelToken 如果被深拷贝,会导致最外层无法使用cancel方法来取消请求
    if (config.cancelToken) {
      conf.cancelToken = config.cancelToken;
    }

    const transform = this.getTransform();

    const { requestOptions } = this.options;

    const opt: RequestOptions = Object.assign({}, requestOptions, options);

    const { beforeRequestHook, requestCatchHook, transformResponseHook } = transform || {};
    if (beforeRequestHook && isFn(beforeRequestHook)) {
      conf = beforeRequestHook(conf, opt);
    }
    conf.requestOptions = opt;

    conf = this.supportFormData(conf);

    return new Promise((resolve, reject) => {
      this.axiosInstance
        .request<any, AxiosResponse<Result>>(conf)
        .then((res: AxiosResponse<Result>) => {
          if (transformResponseHook && isFn(transformResponseHook)) {
            try {
              const ret = transformResponseHook(res, opt);
              resolve(ret);
            } catch (err) {
              reject(err || new Error('request error!'));
            }
            return;
          }
          resolve(res as unknown as Promise<T>);
        })
        .catch((e: Error | AxiosError) => {
          if (requestCatchHook && isFn(requestCatchHook)) {
            reject(requestCatchHook(e, opt));
            return;
          }
          if (axios.isAxiosError(e)) {
            // 在此处重写axios的错误消息
          }
          reject(e);
        });
    });
  }
}

可以看到这个类做了axios.create()函数创建axios实例,引入axios配置参数,注册了请求拦截器、响应拦截器,支持form-data数据格式,封装get、post、put、delete这些Promise请求,基本的request函数这些。里面还有文件上传这部分,不过是最基本的配置,后面需要根据实际情况修改。
安装一下qs插件。

// package.json
"qs": "^6.11.2",

import { VAxios } from ‘./axios’;
这样VAxios类就ok了。

1.3、deepMerge递归合并函数

这个函数在前面utils函数篇添加过。

// 
/**
 递归合并两个对象。
 @param target 目标对象,合并后结果存放于此。
 @param source 要合并的源对象。
 @returns 合并后的对象。
 */
export function deepMerge<T extends object | null | undefined, U extends object | null | undefined>(
  target: T,
  source: U,
): T & U {
  return mergeWith(cloneDeep(target), source, (objValue, srcValue) => {
    if (isObj(objValue) && isObj(srcValue)) {
      return mergeWith(cloneDeep(objValue), srcValue, (prevValue, nextValue) => {
        return isArr(prevValue) ? prevValue.concat(nextValue) : undefined;
      });
    }
  });
}

简单解释就是,deepMerge可以把{ a: 1,b: 2 }和{ c: 3 }合并为{ a: 1,b: 2,c: 3 }。
import { deepMerge } from ‘/@/utils’;

1.4、ContentTypeEnum枚举。

这个枚举在前面vite配置篇里面配置过。
import { ContentTypeEnum,} from ‘/@/enums/httpEnum’;

1.5、useGlobSetting函数

useXXX这种格式的被称作hook函数,新建/@/hooks/settings/index.ts文件存放。

// /@/hooks/settings/index.ts
import type { GlobConfig } from '/#/config';

import { warn } from '/@/utils/log';
import { getENV } from '/@/utils/env';

// 全局环境变量
export const useGlobSetting = (): Readonly<GlobConfig> => {
  const ENV = getENV();

  const {
    VITE_GLOB_APP_TITLE,
    VITE_GLOB_API_URL,
    VITE_GLOB_APP_SHORT_NAME,
    VITE_GLOB_API_URL_PREFIX,
    VITE_GLOB_UPLOAD_URL,
  } = ENV;

  if (!/[a-zA-Z\_]*/.test(VITE_GLOB_APP_SHORT_NAME)) {
    warn(`VITE_GLOB_APP_SHORT_NAME变量只能是字符/下划线,请在环境变量中修改并重新运行.`);
  }

  // 采用全局配置
  const glob: Readonly<GlobConfig> = {
    title: VITE_GLOB_APP_TITLE,
    apiUrl: VITE_GLOB_API_URL,
    shortName: VITE_GLOB_APP_SHORT_NAME,
    urlPrefix: VITE_GLOB_API_URL_PREFIX,
    uploadUrl: VITE_GLOB_UPLOAD_URL,
  };
  return glob as Readonly<GlobConfig>;
};

这里补充一下GlobConfig类型,去config.d.ts文件里添加。

// config.d.ts
export interface GlobConfig {
  // 站点名称
  title: string;
  // 服务接口url
  apiUrl: string;
  // 上传url
  uploadUrl?: string;
  // 服务接口url前缀
  urlPrefix?: string;
  // 项目简称
  shortName: string;
}

从useGlobSetting函数里可以取到整个项目的glob配置。

// /@/utils/http/index.ts
...
import { useGlobSetting } from '/@/hooks/setting';
const globSetting = useGlobSetting();
const urlPrefix = globSetting.urlPrefix;
...

这样globSetting和urlPrefix就可以使用了。

2、AxiosTransform配置参数

前面createAxios创建axios实例函数里的transform还没有添加,这里实际上使用的就是AxiosTransform配置参数。

// /@/utils/http/index.ts
...
/**
 * @description: 数据处理,方便区分多种处理方式
 */
const transform: AxiosTransform = {
  /**
   * @description: 处理响应数据。如果数据不是预期格式,可直接抛出错误
   */
  transformResponseHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
    const { isTransformResponse, isReturnNativeResponse } = options;
    // 是否返回原生响应头 比如:需要获取响应头时使用该属性
    if (isReturnNativeResponse) {
      return res;
    }
    // 不进行任何处理,直接返回
    // 用于页面代码可能需要直接获取code,data,message这些信息时开启
    if (!isTransformResponse) {
      return res.data;
    }
    const { data } = res;
    if (!data) {
      throw new Error('[HTTP] Request has no return value');
    }
    //  这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
    const { result } = data;

    return result;
  },

  // 请求之前处理config
  beforeRequestHook: (config, options) => {
    const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;

    if (joinPrefix) {
      config.url = `${urlPrefix}${config.url}`;
    }

    if (apiUrl && isStr(apiUrl)) {
      config.url = `${apiUrl}${config.url}`;
    }
    const params = config.params || {};
    const data = config.data || false;
    formatDate && data && !isStr(data) && formatRequestDate(data);
    if (config.method?.toUpperCase() === RequestEnum.GET) {
      if (!isStr(params)) {
        // 给 get 请求加上时间戳参数,避免从缓存中拿数据。
        config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
      } else {
        // 兼容restful风格
        config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;
        config.params = undefined;
      }
    } else {
      if (!isStr(params)) {
        formatDate && formatRequestDate(params);
        if (
          Reflect.has(config, 'data') &&
          config.data &&
          (Object.keys(config.data).length > 0 || config.data instanceof FormData)
        ) {
          config.data = data;
          config.params = params;
        } else {
          // 非GET请求如果没有提供data,则将params视为data
          config.data = params;
          config.params = undefined;
        }
        if (joinParamsToUrl) {
          config.url = setObjToUrlParams(
            config.url as string,
            Object.assign({}, config.params, config.data),
          );
        }
      } else {
        // 兼容restful风格
        config.url = config.url + params;
        config.params = undefined;
      }
    }
    return config;
  },

  /**
   * @description: 请求拦截器处理
   */
  requestInterceptors: (config, _options) => {
    return config;
  },

  /**
   * @description: 响应拦截器处理
   */
  responseInterceptors: (res: AxiosResponse<any>) => {
    return res;
  },

  /**
   * @description: 响应错误处理
   */
  responseInterceptorsCatch: (axiosInstance: AxiosResponse, error: any) => {
    const errorLogStore = useErrorLogStoreWithOut();
    errorLogStore.addAjaxErrorInfo(error);
    const { response, code, message, config } = error || {};
    const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none';
    const msg: string = response?.data?.error?.message ?? '';
    const err: string = error?.toString?.() ?? '';
    let errMessage = '';

    if (axios.isCancel(error)) {
      return Promise.reject(error);
    }

    try {
      if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
        errMessage = '[HTTP] Timeout';
      }
      if (err?.includes('Network Error')) {
        errMessage = '[HTTP] network exception';
      }

      // 后续根据需要添加message组件
      if (errMessage) {
        if (errorMessageMode === 'modal') {
          throw new Error(errMessage);
        } else if (errorMessageMode === 'message') {
          throw new Error(errMessage);
        }
        return Promise.reject(error);
      }
    } catch (error) {
      throw new Error(error as unknown as string);
    }

	// 根据请求状态做出对应的提示
    checkStatus(error?.response?.status, msg, errorMessageMode);

    // 添加自动重试机制 保险起见 只针对GET请求
    const retryRequest = new AxiosRetry();
    const { isOpenRetry } = config.requestOptions.retryRequest;
    config.method?.toUpperCase() === RequestEnum.GET &&
      isOpenRetry &&
      // @ts-ignore
      retryRequest.retry(axiosInstance, error);

    return Promise.reject(error);
  },
};
...

引入也更新一下。

// /@/utils/http/index.ts
import { AxiosTransform, CreateAxiosOptions } from './axios/axiosTransform';
import { VAxios } from './axios';
import { deepMerge, setObjToUrlParams } from '/@/utils';
import { ContentTypeEnum, RequestEnum } from '/@/enums/httpEnum';
import { useGlobSetting } from '/@/hooks/setting';
import { isStr } from '../is';
import { AxiosResponse } from 'axios';
import { RequestOptions, Result } from '/#/axios';
import { useErrorLogStoreWithOut } from '/@/stores/modules/errorLog';
import axios from 'axios';

此时还是有一些爆红,接下来逐项解析。

2.1、helper工具函数

AxiosTransform里面使用了两个工具函数joinTimestamp和formatRequestDate,它们都是用来处理参数拼接的函数。
新建/@/utils/http/helper.ts文件。

// /@/utils/http/helper.ts
import { isObj, isStr } from '/@/utils/is';

const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';

/**
 * @description: 当前时间拼接
 */
export function joinTimestamp<T extends boolean>(
  join: boolean,
  restful: T,
): T extends true ? string : object;

export function joinTimestamp(join: boolean, restful = false): string | object {
  if (!join) {
    return restful ? '' : {};
  }
  const now = new Date().getTime();
  if (restful) {
    return `?_t=${now}`;
  }
  return { _t: now };
}

/**
 * @description: 格式化请求参数时间
 */
export function formatRequestDate(params: Recordable) {
  if (Object.prototype.toString.call(params) !== '[object Object]') {
    return;
  }

  for (const key in params) {
    const format = params[key]?.format ?? null;
    if (format && typeof format === 'function') {
      params[key] = params[key].format(DATE_TIME_FORMAT);
    }
    if (isStr(key)) {
      const value = params[key];
      if (value) {
        try {
          params[key] = isStr(value) ? value.trim() : value;
        } catch (error: any) {
          throw new Error(error);
        }
      }
    }
    if (isObj(params[key])) {
      formatRequestDate(params[key]);
    }
  }
}

joinTimestamp函数是将当前时间拼接到axios参数里面,formatRequestDate是将时间转为YYYY-MM-DD HH:mm:ss格式,这里根据后续实际情况修改。
import { formatRequestDate, joinTimestamp } from ‘./helper’;

2.2、根据接口状态做出对应提示

新建/@/utils/http/axios/checkStatus.ts文件。

// /@/utils/http/axios/checkStatus.ts
import type { ErrorMessageMode } from '/#/axios';
import projectSetting from '/@/settings/projectSetting';
import { SessionTimeoutProcessingEnum } from '/@/enums/appEnum';

const stp = projectSetting.sessionTimeoutProcessing;

export function checkStatus(
  status: number,
  msg: string,
  errorMessageMode: ErrorMessageMode = 'message',
): void {
  let errMessage = '';

  switch (status) {
    case 400:
      errMessage = `${msg}`;
      break;

    case 401:
      // 401: 未登录
      if (stp === SessionTimeoutProcessingEnum.PAGE_COVERAGE) {
        // 如果超时,则根据实际情况处理超时的情况
      } else {
        // 如果未登录,则跳转到登录页面,并携带当前页面的路径
        // 登录成功后返回当前页面。此步骤需要在登录页面上进行操作.
      }
      break;
    // 下面可以根据实际情况处理
    case 403:
      errMessage = '[HTTP] 403';
      break;
    case 404:
      errMessage = '[HTTP] 404';
      break;
    case 500:
      errMessage = '[HTTP] 407';
      break;
    default:
  }

  // 后续根据需要添加message组件
  if (errMessage) {
    if (errorMessageMode === 'modal') {
      throw new Error(errMessage);
    } else if (errorMessageMode === 'message') {
      throw new Error(errMessage);
    }
  }
}

2.3、axios自动重试机制

保险起见 只针对GET请求。
新建/@/utils/http/axios/axiosRetry.ts文件。

// /@/utils/http/axios/axiosRetry.ts
import { AxiosInstance } from 'axios';
/**
 *  请求重试机制
 */

export class AxiosRetry {
  /**
   * 重试
   */
  retry(axiosInstance: AxiosInstance, error: any) {
    // @ts-ignore
    const { config } = error.response;
    const { waitTime, count } = config?.requestOptions?.retryRequest ?? {};
    config.__retryCount = config.__retryCount || 0;
    if (config.__retryCount >= count) {
      return Promise.reject(error);
    }
    config.__retryCount += 1;
    //请求返回后config的header不正确造成重试请求失败,删除返回headers采用默认headers
    delete config.headers;
    return this.delay(waitTime).then(() => axiosInstance(config));
  }

  /**
   * 延迟
   */
  private delay(waitTime: number) {
    return new Promise((resolve) => setTimeout(resolve, waitTime));
  }
}

这里逻辑比较复杂,实际上就是count控制重试次数,waitTime控制重试间隔,isOpenRetry控制是否重试。

完整代码

// /@/utils/http/index.ts
import { AxiosTransform, CreateAxiosOptions } from './axios/axiosTransform';
import { VAxios } from './axios';
import { deepMerge, setObjToUrlParams } from '/@/utils';
import { ContentTypeEnum, RequestEnum } from '/@/enums/httpEnum';
import { useGlobSetting } from '/@/hooks/setting';
import { isStr } from '../is';
import { AxiosResponse } from 'axios';
import { RequestOptions, Result } from '/#/axios';
import { useErrorLogStoreWithOut } from '/@/stores/modules/errorLog';
import axios from 'axios';
import { formatRequestDate, joinTimestamp } from './helper';
import { checkStatus } from './axios/checkStatus';
import { clone } from 'lodash-es';
import { AxiosRetry } from './axios/axiosRetry';

const globSetting = useGlobSetting();
const urlPrefix = globSetting.urlPrefix;

/**
 * @description: 数据处理,方便区分多种处理方式
 */
const transform: AxiosTransform = {
  /**
   * @description: 处理响应数据。如果数据不是预期格式,可直接抛出错误
   */
  transformResponseHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
    const { isTransformResponse, isReturnNativeResponse } = options;
    // 是否返回原生响应头 比如:需要获取响应头时使用该属性
    if (isReturnNativeResponse) {
      return res;
    }
    // 不进行任何处理,直接返回
    // 用于页面代码可能需要直接获取code,data,message这些信息时开启
    if (!isTransformResponse) {
      return res.data;
    }
    const { data } = res;
    if (!data) {
      throw new Error('[HTTP] Request has no return value');
    }
    //  这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
    const { result } = data;

    return result;
  },

  // 请求之前处理config
  beforeRequestHook: (config, options) => {
    const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;

    if (joinPrefix) {
      config.url = `${urlPrefix}${config.url}`;
    }

    if (apiUrl && isStr(apiUrl)) {
      config.url = `${apiUrl}${config.url}`;
    }
    const params = config.params || {};
    const data = config.data || false;
    formatDate && data && !isStr(data) && formatRequestDate(data);
    if (config.method?.toUpperCase() === RequestEnum.GET) {
      if (!isStr(params)) {
        // 给 get 请求加上时间戳参数,避免从缓存中拿数据。
        config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
      } else {
        // 兼容restful风格
        config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;
        config.params = undefined;
      }
    } else {
      if (!isStr(params)) {
        formatDate && formatRequestDate(params);
        if (
          Reflect.has(config, 'data') &&
          config.data &&
          (Object.keys(config.data).length > 0 || config.data instanceof FormData)
        ) {
          config.data = data;
          config.params = params;
        } else {
          // 非GET请求如果没有提供data,则将params视为data
          config.data = params;
          config.params = undefined;
        }
        if (joinParamsToUrl) {
          config.url = setObjToUrlParams(
            config.url as string,
            Object.assign({}, config.params, config.data),
          );
        }
      } else {
        // 兼容restful风格
        config.url = config.url + params;
        config.params = undefined;
      }
    }
    return config;
  },

  /**
   * @description: 请求拦截器处理
   */
  requestInterceptors: (config, _options) => {
    return config;
  },

  /**
   * @description: 响应拦截器处理
   */
  responseInterceptors: (res: AxiosResponse<any>) => {
    return res;
  },

  /**
   * @description: 响应错误处理
   */
  responseInterceptorsCatch: (axiosInstance: AxiosResponse, error: any) => {
    const errorLogStore = useErrorLogStoreWithOut();
    errorLogStore.addAjaxErrorInfo(error);
    const { response, code, message, config } = error || {};
    const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none';
    const msg: string = response?.data?.error?.message ?? '';
    const err: string = error?.toString?.() ?? '';
    let errMessage = '';

    if (axios.isCancel(error)) {
      return Promise.reject(error);
    }

    try {
      if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
        errMessage = '[HTTP] Timeout';
      }
      if (err?.includes('Network Error')) {
        errMessage = '[HTTP] network exception';
      }

      // 后续根据需要添加message组件
      if (errMessage) {
        if (errorMessageMode === 'modal') {
          throw new Error(errMessage);
        } else if (errorMessageMode === 'message') {
          throw new Error(errMessage);
        }
        return Promise.reject(error);
      }
    } catch (error) {
      throw new Error(error as unknown as string);
    }

    // 根据请求状态做出对应的提示
    checkStatus(error?.response?.status, msg, errorMessageMode);

    // 添加自动重试机制 保险起见 只针对GET请求
    const retryRequest = new AxiosRetry();
    const { isOpenRetry } = config.requestOptions.retryRequest;
    config.method?.toUpperCase() === RequestEnum.GET &&
      isOpenRetry &&
      // @ts-ignore
      retryRequest.retry(axiosInstance, error);

    return Promise.reject(error);
  },
};

function createAxios(opt?: Partial<CreateAxiosOptions>) {
  return new VAxios(
    // 深度合并
    deepMerge(
      {
        // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
        // authentication schemes,e.g: Bearer
        // authenticationScheme: 'Bearer',
        authenticationScheme: '',
        timeout: 10 * 1000,
        // 基础接口地址
        // baseURL: globSetting.apiUrl,

        headers: { 'Content-Type': ContentTypeEnum.JSON },
        // 如果是form-data格式
        // headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
        // 数据处理方式
        transform: clone(transform),
        // 配置项,下面的选项都可以在独立的接口请求中覆盖
        requestOptions: {
          // 默认将prefix 添加到url
          joinPrefix: true,
          // 是否返回原生响应头 比如:需要获取响应头时使用该属性
          isReturnNativeResponse: false,
          // 需要对返回数据进行处理
          isTransformResponse: true,
          // post请求的时候添加参数到url
          joinParamsToUrl: false,
          // 格式化提交参数时间
          formatDate: true,
          // 消息提示类型
          errorMessageMode: 'message',
          // 接口地址
          apiUrl: globSetting.apiUrl,
          // 接口拼接地址
          urlPrefix: urlPrefix,
          //  是否加入时间戳
          joinTime: true,
          // 忽略重复请求
          ignoreCancelToken: true,
          // 是否携带token
          withToken: true,
          retryRequest: {
            isOpenRetry: true,
            count: 5,
            waitTime: 100,
          },
        },
      },
      opt || {},
    ),
  );
}
export const defHttp = createAxios();

3、接口示例

正好之前使用了mock,来演示一下怎么使用mock。
新建/@/api/demo/index.ts文件。

// /@/api/demo/index.ts
import { DemoParams, DemoResultModel } from './model/listModel';
import { ErrorMessageMode } from '/#/axios';
import { defHttp } from '/@/utils/http';

enum Api {
  GETlISTINFO = '/getListInfo',
  POSTLISTINFO = '/postListInfo',
}

/**
 * @description: 示例get请求
 */
export function getListInfo(params: DemoParams, mode: ErrorMessageMode = 'message') {
  return defHttp.get<DemoResultModel>({ url: Api.GETlISTINFO, params }, { errorMessageMode: mode });
}
/**
 * @description: 示例post请求
 */
export function postListInfo(params: DemoParams, mode: ErrorMessageMode = 'message') {
  return defHttp.get<DemoResultModel>(
    { url: Api.POSTLISTINFO, params },
    { errorMessageMode: mode },
  );
}

新建/@/api/demo/model/listModel.ts文件

// /@/api/demo/model/listModel.ts
export interface ListType {
  id: number;
  name: string;
  createBy: string;
}

/**
 * @description: 示例接口返回值
 */
export interface DemoResultModel {
  content: ListType[];
  page: number;
  pageSize: number;
  total: number;
}

/**
 * @description: 示例接口参数
 */
export interface DemoParams {
  field1: string;
  field2: string;
}

修改一下mock文件。

// mock/demo/list.ts
import { resultSuccess, requestParams, baseUrl } from '../_util';
import { MockMethod } from 'vite-plugin-mock';

const listInfo = {
  content: [
    {
      id: 1,
      name: '示例数据1',
      createBy: '示例数据创建人1',
    },
    {
      id: 2,
      name: '示例数据2',
      createBy: '示例数据创建人2',
    },
    {
      id: 3,
      name: '示例数据3',
      createBy: '示例数据创建人3',
    },
    {
      id: 4,
      name: '示例数据4',
      createBy: '示例数据创建人4',
    },
    {
      id: 5,
      name: '示例数据5',
      createBy: '示例数据创建人5',
    },
    {
      id: 6,
      name: '示例数据6',
      createBy: '示例数据创建人6',
    },
  ],
  page: 1,
  pageSize: 10,
  total: 100,
};

export default [
  {
    url: `${baseUrl}/getListInfo`,
    timeout: 1000,
    method: 'get',
    response: (request: requestParams) => {
      const res: any = {
        ...request,
        ...listInfo,
      };
      return resultSuccess(res);
    },
  },
  {
    url: `${baseUrl}/postListInfo`,
    timeout: 1000,
    method: 'post',
    response: (request: requestParams) => {
      const res: any = {
        ...request,
        ...listInfo,
      };
      return resultSuccess(res);
    },
  },
] as MockMethod[];

然后随便找个文件调用一下, 顺便删除多余代码。

<script setup lang="ts">
  import { onMounted, reactive, ref } from 'vue';
  import { getListInfo } from '../api/demo';

  const res = ref();

  const demoParams = reactive({
    field1: 'field1',
    field2: 'field2',
  });

  async function getInfo() {
    const data = await getListInfo(demoParams);
    res.value = data;
  }

  onMounted(() => {
    getInfo();
  });
</script>

<template>
  <div>{{ res }}</div>
</template>

在这里插入图片描述
也可以mock一些error接口,get请求会重复5次,重复请求次数在上面的代码里就能找到。

结语

至此,整个vue3通用项目底座完成。源码参考common-template

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值