基于类的axios完整封装

特点:

1、集成了网络重连、防止重复网络请求、取消网络请求等功能

2、集成了JSON、application/x-www-form-urlencoded;charset=UTF-8、multipart/form-data;charset=UTF-8(文件上传)三种类型上传;

3、为一个项目使用多个服务端请求地址提供便利,通过生成多个实例对象实现

4、与TS深度结合


Tips:

这里贴一下对三种content-type的处理方式(代码片段,详情看下面的完整实现):

// interface.ts
/**
 * @description:  contentType
 */
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',
}


// axios.ts
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" }),
    };
  }
/*
   * @description:  上传文件  FormData
   */
  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,
      },
    });
  }

完整的代码实现:

index.ts
import { VAxios } from "./axios";
import { CreateAxiosOptions } from "./axiosTransform";
import { ContentTypeEnum} from "./interface";
import { deepMerge } from './utils'
import { useGlobSetting } from "./globalSetting";

const globSetting = useGlobSetting()

function createAxios(opt?: Partial<CreateAxiosOptions>) {
    return new VAxios(
      // 深度合并
      deepMerge(
        {
          authenticationScheme: 'Bearer',
          timeout: 10 * 1000,
          headers: { 'Content-Type': ContentTypeEnum.JSON },
          // 配置项,下面的选项都可以在独立的接口请求中覆盖
          requestOptions: {
            // 默认将prefix 添加到url
            joinPrefix: true,
            // 接口地址
            apiUrl: globSetting.apiUrl,
            // 接口拼接地址
            urlPrefix: globSetting.urlPrefix,
            // 忽略重复请求
            ignoreCancelToken: true,
            // 是否携带token
            withToken: true,
            // 接口错误重试(针对http错误码)
            retryRequest: {
              isOpenRetry: true,
              count: 5,
              waitTime: 100,
            },
          },
        },
        opt || {},
      ),
    );
  }
  export const defHttp = createAxios();
  
axios.ts
import type {
  AxiosRequestConfig,
  AxiosInstance,
  AxiosResponse,
  AxiosError,
} from "axios";
import axios from "axios";
import qs from "qs";
import type { CreateAxiosOptions } from "./axiosTransform";
import {
  ContentTypeEnum,
  RequestEnum,
  RequestOptions,
  Result,
  UploadFileParams,
} from "./interface";
import { isFunction, isString } from "./utils";
import { cloneDeep } from "lodash-es";
import { AxiosRetry } from "./AxiosRetry";
import { AxiosCanceler } from "./AxiosCancel";
declare type Recordable<T = any> = Record<string, T>;
export class VAxios {
  private axiosInstance: AxiosInstance;
  private readonly options: CreateAxiosOptions;

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

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

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

  /**
   * @description: 拦截器配置
   */
  private setupInterceptors() {
    const axiosCanceler = new AxiosCanceler();
    // 请求拦截器
    this.axiosInstance.interceptors.request.use(
      (config: AxiosRequestConfig) => {
        // 取消重复请求
        // @ts-ignore
        const { ignoreCancelToken } = config.requestOptions;
        const ignoreCancel =
          ignoreCancelToken !== undefined
            ? ignoreCancelToken
            : this.options.requestOptions?.ignoreCancelToken;
        !ignoreCancel && axiosCanceler.addPending(config);

        // 请求之前处理config
        const token = "";
        if (
          token &&
          (config as Recordable)?.requestOptions?.withToken !== false
        ) {
          // jwt token
          (config as Recordable).headers.Authorization = this.options
            .authenticationScheme
            ? `${this.options.authenticationScheme} ${token}` : token;
        }

        return config;
      },
      undefined
    );

    // 响应拦截器
    this.axiosInstance.interceptors.response.use(
      (res: AxiosResponse<any>) => {
        res && axiosCanceler.removePending(res.config);
        const responseData = res.data;

        const { rtnCode } = responseData;
        if (rtnCode !== "10000") {
          return Promise.reject(responseData);
        }
        // 正常流程
        return Promise.resolve(responseData);
      },
      (error) => {
        const { config } = error || {};

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

  // support form-data Content-Type= application/x-www-form-urlencoded;charset=UTF-8
  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<[Error | AxiosError | undefined, Result<T> | undefined]> {
    return this.request({ ...config, method: "GET" }, options);
  }

  post<T = any>(
    config: AxiosRequestConfig,
    options?: RequestOptions
  ): Promise<[Error | AxiosError | undefined, Result<T> | undefined]> {
    return this.request({ ...config, method: "POST" }, options);
  }

  put<T = any>(
    config: AxiosRequestConfig,
    options?: RequestOptions
  ): Promise<[Error | AxiosError | undefined, Result<T> | undefined]> {
    return this.request({ ...config, method: "PUT" }, options);
  }

  delete<T = any>(
    config: AxiosRequestConfig,
    options?: RequestOptions
  ): Promise<[Error | AxiosError | undefined, Result<T> | undefined]> {
    return this.request({ ...config, method: "DELETE" }, options);
  }
  /*
   * @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,
      },
    });
  }

  request<T = any>(
    config: AxiosRequestConfig,
    options?: RequestOptions
  ): Promise<[Error | AxiosError | undefined, Result<T> | undefined]> {
    let conf: CreateAxiosOptions = cloneDeep(config);
    const { requestOptions } = this.options;

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

    const { apiUrl, joinPrefix, urlPrefix } = opt;
    if (joinPrefix) {
      conf.url = `${urlPrefix}${conf.url}`;
    }

    if (apiUrl && isString(apiUrl)) {
      conf.url = `${apiUrl}${conf.url}`;
    }

    conf.requestOptions = opt;

    conf = this.supportFormData(conf);

    return new Promise((resolve) => {
      this.axiosInstance
        .request<any, AxiosResponse<Result>>(conf)
        .then((res: AxiosResponse<Result>) => {
          const response = res as unknown as Result<T>;
          resolve([undefined, response]);
        })
        .catch((e: Error | AxiosError) => {
          //   if (axios.isAxiosError(e)) {
          // rewrite error message from axios in here
          //   }
          resolve([e, undefined]);
        });
    });
  }
}

axiosCancel.ts
import type { AxiosRequestConfig } from 'axios';


// 用于存储请求等待队列
let pendingMap = new Map<string, AbortController>();
// 生成map的key值(get&api形式)
export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&');


export class AxiosCanceler {
  /**
   * 新增请求
   * @param {Object} config
   */
  addPending(config: AxiosRequestConfig) {
    this.removePending(config);
    const url = getPendingUrl(config);
    const controller = new AbortController()
    config.signal = 
        config.signal ||
        controller.signal
        if (!pendingMap.has(url)) {
            // If there is no current request in pending, add it
            pendingMap.set(url, controller);
        }
  }


  /**
   * @description: 清除所有请求
   */
  removeAllPending() {
    pendingMap.forEach((cancel) => {
      cancel  && cancel.abort();
    });
    pendingMap.clear();
  }


  /**
   * 清除目标请求
   * @param {Object} config
   */
  removePending(config: AxiosRequestConfig) {
    const url = getPendingUrl(config);


    if (pendingMap.has(url)) {
      //   取消在等待队列中的请求
      const cancel = pendingMap.get(url);
      cancel && cancel.abort();
      pendingMap.delete(url);
    }
  }


  /**
   * @description: 重置
   */
  reset(): void {
    pendingMap = new Map<string, AbortController>();
  }
}
axiostRetry.ts
import { AxiosError, AxiosInstance } from 'axios';
/**
 *  请求重试机制
 */


export class AxiosRetry {
  /**
   * 重试
   */
  retry(axiosInstance: AxiosInstance, error: AxiosError) {
    // @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));
  }
}

globSetting.ts

interface IGlobSetting {
    apiUrl: string,
    urlPrefix: string,
    uploadUrl?: string,
}
const globSetting:Readonly<{development:IGlobSetting,production:IGlobSetting}> = Object.freeze({
    development: {
        apiUrl: '',
        urlPrefix: '',
    },
    production: {
        apiUrl: '',
        urlPrefix: '',
    }
})


export const useGlobSetting = ():Readonly<IGlobSetting> => {
    // vite 提供的环境变量
    // @ts-ignore
    const env = import.meta.env.MODE as string
    return globSetting[env]
}

interface.ts

export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined;
export type SuccessMessageMode = ErrorMessageMode;


export interface RequestOptions {
  // 默认将prefix 添加到url
  joinPrefix?: boolean;
  // 接口地址
  apiUrl?: string;
  // 请求拼接路径
  urlPrefix?: string;


  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;
}
declare type Recordable<T = any> = Record<string, T>;


// multipart/form-data: upload file
export interface UploadFileParams {
  // 其他数据
  data?: Recordable;
  // File parameter interface field name
  name?: string;
  // 文件
  file: File | Blob;
  // 文件名
  filename?: string;
  [key: string]: any;
}



/**
 * @description:  contentType
 */
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',
}

/**
 * @description: request method
 */
export enum RequestEnum {
  GET = 'GET',
  POST = 'POST',
  PUT = 'PUT',
  DELETE = 'DELETE',
}
utils.ts
import { cloneDeep } from 'lodash-es';


export function is(val: unknown, type: string) {
  return toString.call(val) === `[object ${type}]`;
}


export function isFunction(val: unknown): val is Function {
    return typeof val === 'function';
  }


  export function isObject(val: any): val is Record<any, any> {
    return val !== null && is(val, 'Object');
  }
  export function isString(val: unknown): val is string {
    return is(val, 'String');
  }
  // 深度合并
export function deepMerge<T = any>(src: any = {}, target: any = {}): T {
  let key: string;
  const res: any = cloneDeep(src)
  for (key in target) {
    res[key] = isObject(res[key]) ? deepMerge(res[key], target[key]) : (res[key] = target[key]);
  }
  return res;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值