项目使用axios+ts封装 配合业务使用风味更佳

2,746 阅读7分钟

前言

大家好!我又好久没有写文章了,最近比较有空,又想输出一些东西了,由于我在公司日常的业务都是写一些系统什么的,但是之前的前端留下来的项目里面的axios封装都不够通用,每次新的项目或者需要修改的时候都要修改大量的地方,所以再看了很多axios的封装之后在日常开发中使用的一套,欢迎提意见。

项目搭建

npm init vite@latest

这里选择用vite没有特别的要求,只是搭得快, 如果没有安装的可以去安装一下。

接着安装我们需要的依赖

npm install axios
npm install lodash-es
npm install qs

创建实例

安装完成后我们在src下面新建utils/axios/Axios.ts,并创建我们axios的实例

//axios 声明的接口类型 AxiosRequestConfig
import type {AxiosRequestConfig,AxiosInstance,AxiosResponse} from "axios";

import axios from 'axios'

/**
 * @description: axios的模块
 * **/
//声明2个私有属性 axios的请求方法 和 配置
export class VAxios {
    private axiosInstance:AxiosInstance;
    private  options:AxiosRequestConfig;

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

配置声明

目前基本的实例已经成功了,实际当然没有这么简单,我们既然用了axios.create创建一个axios实例,当然是希望在原本的基础上扩展出符合我们业务的复用性强的拦截器或者配置。

在此目录下新建types.ts,用来存放我们扩展的类型声明


//axios Config声明
import { AxiosRequestConfig } from 'axios';
//扩展拦截器方法
import { AxiosTransform } from './axiosTransform';

//为了保留原来axios实例拥有的配置,新声明的配置接口需要基础原来的接口
export interface CreateAxiosOptions extends AxiosRequestConfig {
    prefixUrl?: string;
    transform?: AxiosTransform;
    requestOptions?: RequestOptions;
}

//需要增加的options 用来控制功能
export interface RequestOptions {
    // 请求参数拼接到url
    joinParamsToUrl?: boolean;
    // 是否显示提示信息
    isShowMessage?: boolean;
    // 是否解析成JSON
    isParseToJson?: boolean;
    // 成功的文本信息
    successMessageText?: string;
    // 是否显示成功信息
    isShowSuccessMessage?: boolean;
    // 是否显示失败信息
    isShowErrorMessage?: boolean;
    // 错误的文本信息
    errorMessageText?: string;
    // 是否加入url
    joinPrefix?: boolean;
    // 接口地址, 不填则使用默认apiUrl
    apiUrl?: string;
    // 错误消息提示类型
    errorMessageMode?: 'none' | 'modal';
    // 不进行任何处理,直接返回
    isTransformResponse?: boolean;
    // 是否返回原生响应头
    isReturnNativeResponse?: boolean;
}

//后端返回数据的格式,根据自己的后端规范进行修改
export interface Result<T = any> {
    code: number;
    type?: 'success' | 'error' | 'warning';
    message: string;
    result?: T;
}

数据处理方法类声明

拦截器方法声明单独抽出一个文件专门维护,在同级目录下新建axiosTransform.ts,写入我们配置的方法

/**
 * 数据处理类,可以根据项目自行配置
 */
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import type { RequestOptions, Result } from './types';

export abstract class AxiosTransform {
    /**
     * @description: 请求之前处理配置
     * @description: Process configuration before request
     */
    beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;

    /**
     * @description: 请求成功处理
     */
    transformRequestData?: (res: AxiosResponse<Result>, options: RequestOptions) => any;

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

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

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

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

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

防止重复请求

还有防止重复发送请求,在上一个同样的请求未响应之前不能重复的发送请求,减轻服务端的压力。 在同等级目录下新建 axiosCancel.ts,编写axiosCancel实例用于取消重复请求(非常有用)

import axios, { AxiosRequestConfig, Canceler } from 'axios';
// @ts-ignore
import qs from 'qs';

const toString = Object.prototype.toString;
/**
 * @description: 判断值是否未某个类型
 */
export function is(val: unknown, type: string) {
    return toString.call(val) === `[object ${type}]`;
}
/**
 * @description:  是否为函数
 */
export function isFunction<T = Function>(val: unknown): val is T {
    return is(val, 'Function');
}

// 声明一个 Map 用于存储每个请求的标识 和 取消函数
let pendingMap = new Map<string, Canceler>();

export const getPendingUrl = (config: AxiosRequestConfig) =>
    [config.method, config.url, qs.stringify(config.data), qs.stringify(config.params)].join('&');

//axiosCancel实例,取消重复请求

export class AxiosCanceler {
    /**
     * 添加请求
     * @param {Object} config
     */
    addPending(config: AxiosRequestConfig) {
        this.removePending(config);
        const url = getPendingUrl(config);
        config.cancelToken =
            config.cancelToken ||
            new axios.CancelToken((cancel) => {
                if (!pendingMap.has(url)) {
                    // 如果 pending 中不存在当前请求,则添加进去
                    pendingMap.set(url, cancel);
                }
            });
    }

    /**
     * @description: 清空所有pending
     */
    removeAllPending() {
        pendingMap.forEach((cancel) => {
            cancel && isFunction(cancel) && cancel();
        });
        pendingMap.clear();
    }

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

        if (pendingMap.has(url)) {
            // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
            const cancel = pendingMap.get(url);
            cancel && cancel(url);
            pendingMap.delete(url);
        }
    }

    /**
     * @description: 重置
     */
    reset(): void {
        pendingMap = new Map<string, Canceler>();
    }
}

完善

好了 所有的配置,数据处理,请求防重,都准备好了 接下来会到我们的Axios.ts,完善我们的VAxios实例,参考axios文档添加拦截器方法

1651047041(1).png

//axios 声明的接口类型 AxiosRequestConfig
import type {AxiosRequestConfig,AxiosInstance,AxiosResponse} from "axios";
import type { RequestOptions, CreateAxiosOptions, Result } from "./types";
import axios from 'axios'
import {AxiosCanceler, isFunction} from "./axiosCancel"
import { cloneDeep } from 'lodash-es';
/**
 * @description: axios的模块
 * **/
//声明2个私有属性 axios的请求方法 和 配置
export class VAxios {
    private axiosInstance:AxiosInstance;
    private  options:CreateAxiosOptions;

    constructor(options:CreateAxiosOptions) {
        this.options = options;
        this.axiosInstance = axios.create(options)
    }
    /**
     * @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:AxiosRequestConfig) => {
            //发送请求时headers忽略取消
            const { headers: {ignoreCancelToken} = { ignoreCancelToken:false } } = config;
            !ignoreCancelToken && axiosCanceler.addPending(config);//发送请求添加进map
            if (requestInterceptors && isFunction(requestInterceptors)) {
                config = requestInterceptors(config)
            }
            return config;
        },undefined)//成功和错误的处理分开写,传入undefined

        // 请求拦截器错误捕捉
        requestInterceptorsCatch &&
            isFunction(requestInterceptorsCatch) &&
            this.axiosInstance.interceptors.request.use(undefined,responseInterceptorsCatch);
        // 响应结果拦截器处理
        this.axiosInstance.interceptors.response.use((res: AxiosResponse) => {
            res && axiosCanceler.removePending(res.config);
            if (responseInterceptors && isFunction(responseInterceptors)) {
                res = responseInterceptors(res);
            }
            return res;
        },undefined);

        // 响应结果拦截器错误捕获
        responseInterceptorsCatch &&
            isFunction(responseInterceptorsCatch) &&
            this.axiosInstance.interceptors.response.use(undefined,responseInterceptorsCatch);
    }
    /**
     * @description:   请求方法封装
     */

    request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
        let conf: AxiosRequestConfig = cloneDeep(config);
        const transform = this.getTransform();

        const { requestOptions } = this.options;

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

        const { beforeRequestHook, requestCatch, transformRequestData } = transform || {};
        if (beforeRequestHook && isFunction(beforeRequestHook)) {
            conf = beforeRequestHook(conf, opt);
        }
        return new Promise((resolve, reject) => {
            this.axiosInstance
                .request<any, AxiosResponse<Result>>(conf)
                .then((res: AxiosResponse<Result>) => {
                    // 请求是否被取消
                    const isCancel = axios.isCancel(res);
                    if (transformRequestData && isFunction(transformRequestData) && !isCancel) {
                        const ret = transformRequestData(res, opt);

                        return resolve(ret);
                    }
                    reject(res as unknown as Promise<T>);
                })
                .catch((e: Error) => {
                    if (requestCatch && isFunction(requestCatch)) {
                        reject(requestCatch(e));
                        return;
                    }
                    reject(e);
                });
        });
    }
}

使用

新建index.ts

// axios配置  可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
import { VAxios } from './Axios';
import { AxiosTransform } from './axiosTransform';
import axios from 'axios';
import { checkStatus } from './checkStatus';
import { RequestEnum, ResultEnum, ContentTypeEnum } from '@/enums/httpEnum';
import { RequestOptions } from './types';
/**
 * @description: 数据处理,方便区分多种处理方式
 */
const transform: AxiosTransform = {
  /**
   * @description: 处理请求数据
   */
  transformRequestData: (res, options: RequestOptions) => {
   
  //xx这里写数据的处理逻辑根据你自己的喜好或者团队规范来
  
    return data;
  },

  // 请求之前处理config
  beforeRequestHook: (config, options) => {
     
    return config;
  },

  /**
   * @description: 请求拦截器处理
   */
  requestInterceptors: (config) => {
    // 请求之前处理config

    if (token) {
      // jwt token
      console.log(token);
      config.headers.token = token;
    }
    return config;
  },

  /**
   * @description: 响应错误处理
   */
  responseInterceptorsCatch: (error: any) => {
  
      }
    } catch (error) {
      throw new Error(error);
    }
    // 请求是否被取消
    const isCancel = axios.isCancel(error);
    if (!isCancel) {
      checkStatus(error.response && error.response.status, msg, Message);
    } else {
      console.warn(error, '请求被取消!');
    }
    return error;
  },
};

const Axios = new VAxios({
  timeout: 10 * 2000,
  // 接口前缀
  prefixUrl: xxxx,
  headers: { 'Content-Type': ContentTypeEnum.JSON },
  // 数据处理方式
  transform,
  // 配置项,下面的选项都可以在独立的接口请求中覆盖
  requestOptions: {
    // 默认将prefix 添加到url
    joinPrefix: true,
    // 是否返回原生响应头 比如:需要获取响应头时使用该属性
    isReturnNativeResponse: false,
    // 需要对返回数据进行处理
    isTransformResponse: true,
    // post请求的时候添加参数到url
    joinParamsToUrl: false,
    // 格式化提交参数时间
    formatDate: true,
    // 消息提示类型
    errorMessageMode: 'none',
    // 是否加入时间戳
    joinTime: false,
    // 接口地址
    apiUrl: globSetting.apiUrl as string,
  },
  withCredentials: false,
});

export default Axios;

我们在index.ts里初始化了实例并导出了,因为一般后台管理系统的接口都比较多,我们需要新建一个文件进行集中管理,新建src/api/user.ts。

import http from "../utils/axios"

export function login(params:any) {
    return http.request(
        {
            url: '/user/login',
            method: 'POST',
            params,
        },
        {
            isTransformResponse: false,
        }
    );
}

之后在项目内引入结合async和await就可以非常简洁的使用了