vite3-vue3-ts 封装axios

536 阅读7分钟

前言

最后更新时间:2022.8.29(新增api请求分类,集成后一起导出,接口定义文件位置统一)

我的项目各版本

image.png

image.png

我不确定我所封装是否没有bug,所以这个文章今后说不定会修改

很多说明习惯性的放在注释里了就不多说了

本文章是对一碗周大佬一篇ts封装axios文章有所领悟写的, 我把大佬的封装小小改动了一下,这是大佬链接:juejin.cn/post/707151… 本文章现在所有的功能:

1.三次请求拦截与响应拦截 2.取消请求 3.阻止重复请求 4.请求失败,再次发起请求 5.可以手动配置官方一些api 6.可以多路径(虽然没啥用)

1. 封装准备

需要的:

  • pinia
pnpm i pinia -S
  • axios
pnpm i axios -S
  • ts-md5
pnpm i ts-md5 -S

2. 工具函数

先在src下面创建一个utils文件夹,用来装工具函数, 再创建一个md5.ts用来管理ts-md5这个插件,这样以后需要更换或者维护只用到这个文件进行修改

image.png

里面目前只有一个请求使用的md5函数,上代码

import { Md5 } from "ts-md5/dist/md5";

// 请求所用转化md5值方法
const requestMd5 = (arr: any[]): string => {
    let str = "";
    for (let i = 0, length = arr.length; i < length; i++) {
        str += JSON.stringify(arr[i])
    }
    let newStr = Md5.hashStr(str)
    return newStr
}

export { requestMd5 }

pinia配置

本人不习惯用pinia所以没做模块化

用意说明:pinia来保存请求的方法,方便单个取消或者一起取消

src文件夹下

image.png

代码

import { defineStore } from "pinia";

export const mainStore = defineStore("main", {
    state: () => {
        return {
            // 请求取消方法容器(用途:取消请求或取消重复请求),开始请求时将请求参数放入容器,
            // 请求结束后在此容器中删除相应参数
            requestMap: new Map(),
            // 当前语言
            language: "",
            // 用户登陆后的token
            token: "",
            // 当前登陆用户信息
            userInfo: ""
        };
    },
    getters: {},
    actions: {
        // 不能使用箭头函数,this指向会绑定外部的this
        // 取消请求方法
        cancelRequest(data: string | string[] | "all" | "请传入MD5值") {
            if (data === "请传入MD5值") return Error("禁止传入'请传入MD5值'")
            // 删除一个
            if (typeof data === "string" && data !== "all" && this.requestMap.has(data)) {
                this.requestMap.get(data)() && this.requestMap.delete(data)
                return
            }
            // 删除一堆
            if (Array.isArray(data) && data.length > 0) {
                for (let i = 0, length = data.length; i < length; i++)
                    if (this.requestMap.has(data[i]))
                        this.requestMap.get(data[i])() && this.requestMap.delete(data[i])
                return
            }
            // 删除所有,最后一个了不用return节省性能了
            if (data === "all") {
                this.requestMap.forEach(item => item())
                this.requestMap.clear()
            }
        }
    },
})

3. 封装axios

封装的本意是方便使用\维护,复用率高,减少本来需要重复写的代码,但是我现在并不能全部做到

我只能尽力尽我所能

现在需要创建一个这样的目录结构

image.png

注意了,最后一个index.ts是api文件夹下第一级的,与其他文件夹同级

接口定义文件夹

image.png

从上往下慢慢来

我对一碗周大佬的结构做了一些修改,拦截器的触发顺序为

image.png

common/index.ts

import axios from "axios";
import type { AxiosInstance, AxiosResponse } from 'axios';
import type { RequestConfig, RequestInterceptors, GlobalRequestConfig } from '@/typeInterface/api/types';
import { mainStore } from "@/store";
import { requestMd5 } from "@/utils/md5";

// 实例化
const store = mainStore()
// 取消请求令牌
const CancelToken = axios.CancelToken

class Request {
    // axios 实例
    instance: AxiosInstance
    // 拦截器对象
    interceptorsObj?: RequestInterceptors

    constructor(config: RequestConfig) {
        this.instance = axios.create(config)
        this.interceptorsObj = config.interceptors

        // 全局请求拦截器
        this.instance.interceptors.request.use(
            (res: GlobalRequestConfig) => {
                // console.log('全局请求拦截器', res)
                return res
            },
            err => { return Promise.reject(err) }
        )

        // 给实例添加请求拦截器
        this.instance.interceptors.request.use(
            this.interceptorsObj?.requestInterceptors,
            this.interceptorsObj?.requestInterceptorsCatch
        )

        // 全局响应拦截器
        this.instance.interceptors.response.use(
            // 接口的数据都在res.data下,直接返回res.data
            (res: AxiosResponse) => {
                // console.log('全局响应拦截器', res)
                return res.data
            },
            err => { return Promise.reject(err) }
        )

        // 给实例添加响应拦截器
        this.instance.interceptors.response.use(
            this.interceptorsObj?.responseInterceptors,
            this.interceptorsObj?.responseInterceptorsCatch
        )
    }

    request<T>(config: GlobalRequestConfig): Promise<T> {
        let str = requestMd5([config.url, config.method, config.params])
        //  重复请求处理
        if (config.cancelDuplicate && store.requestMap.has(str)) return Promise.reject("Request repeated, cancelled")

        return new Promise((resolve, reject) => {
            // 这里使用单个请求的拦截器
            if (config.interceptors?.requestInterceptors)
                config = config.interceptors.requestInterceptors(config)
            // 为pinia添加取消请求方法
            if (config.cancelRequest) config.cancelToken = new CancelToken(function (cancelFn) {
                store.requestMap.set(str, cancelFn)
            })
            this.instance.request<any, T>(config)
                .then(res => {
                    // 这里使用单个响应的拦截器
                    if (config.interceptors?.responseInterceptors)
                        res = config.interceptors.responseInterceptors<T>(res)
                    resolve(res)
                })
                .catch(err => reject(err))
                .finally(() => {
                    if (config.cancelRequest && store.requestMap.has(str)) store.requestMap.delete(str);
                })
        })
    }
}

export { Request, CancelToken } 

我希望全局响应拦截器应该是第一个先响应的

4. 封装axios所需要的ts接口

当然你可以全用any,只不过之后写啥是不会有提示的

typeInterface/api/types.ts

import type { AxiosRequestConfig, AxiosResponse } from 'axios';

export interface RequestInterceptors {
    // 请求拦截
    requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfig
    requestInterceptorsCatch?: (err: any) => any
    // 响应拦截
    responseInterceptors?: <T = AxiosResponse>(config: T) => T
    responseInterceptorsCatch?: (err: any) => any
}

// 自定义传入的参数
export interface RequestConfig extends AxiosRequestConfig {
    interceptors?: RequestInterceptors
}

// params:请求参数
// ask:是否开启取消请求,true开启后pinia中requestArr会有取消方法
// repeat: 是否开启阻止重复请求
export interface Data {
    // 自定义属性
    // 是否开启获取取消请求
    cancelRequest?: 0 | 1,
    // 是否开启拦截重复请求
    cancelDuplicate?: 0 | 1,
    // 开启请求失败重新发送请求的次数
    requestsNum?: number,
    // 自定义拦截器,官方单独请求拦截器后返回的数据不一样(不推荐)
    interceptors?: {
        requestInterceptors?: Function,
        responseInterceptors?: Function
    },
    // 官方配置属性
    url?: string,
    method?: "get" | "post" | "put" | "delete" | "patch" | string,
    baseURL?: string,
    transformRequest?: Function[],
    transformResponse?: Function[],
    headers?: object,
    params?: any,
    paramsSerializer?: object,
    data?: object | string,
    timeout?: number,
    withCredentials?: boolean,
    adapter?: Function,
    auth?: object,
    responseType?: string,
    responseEncoding?: string,
    xsrfCookieName?: string,
    xsrfHeaderName?: string,
    onUploadProgress?: Function,
    onDownloadProgress?: Function,
    maxContentLength?: number,
    maxBodyLength?: number,
    validateStatus?: Function,
    maxRedirects?: number,
    beforeRedirect?: Function,
    socketPath?: any,
    httpAgent?: any,
    httpsAgent?: any,
    proxy?: object,
    cancelToken?: any,
    signal?: any,
    decompress?: boolean,
    insecureHTTPParser?: any,
    transitional?: object,
    env?: object,
    formSerializer?: object
}

// 全局请求拦截器参数接口
export interface GlobalRequestConfig extends RequestConfig {
    cancelRequest?: 0 | 1,
    cancelDuplicate?: 0 | 1
}

我上github看着官方api配置的,大部分没试过,请相中这篇文章的大佬们帮我踩坑咯

5. axios封装完成,使用实例,再对实例进行封装

integrated/http.ts

import { Request } from "@/api/common";
import type { Data } from "@/typeInterface/api/types";

// 实例化
const request = new Request({
    // 设置默认参数
    timeout: 5000,
    // 实例拦截器
    interceptors: {
        requestInterceptors: (res) => {
            // console.log("实例请求拦截器", res);
            return res
        },
        responseInterceptors: (res: any) => {
            // console.log("实例响应拦截器", res);
            return res
        },
        requestInterceptorsCatch: err => Promise.reject(err),
        responseInterceptorsCatch: err => Promise.reject(err)
    }
})

// 给在封装请求api的人一个提示,不要传第四个参数
const http = (url: string, data?: object | string, config?: Data, requestsOBJ?: Data | "重复请求数据占位,勿填") => {
    // 笨蛋勿入
    if (requestsOBJ === "重复请求数据占位,勿填") return Promise.reject("第四个参数不能填")
    // 处理数据 // 请求需要的数据容器
    let requestObj: any
    // 赋值给数据容器,并判断无相关属性时赋值默认值
    // 如果是重复请求,数据已经处理好了,直接用
    if (requestsOBJ) { requestObj = requestsOBJ }
    else requestObj = {
        ...config,
        url: url || config?.url,
        method: config?.method || 'POST',
        params: data || config?.data,
        data: data || config?.data,
        cancelRequest: config?.cancelRequest === undefined ? 1 : config.cancelRequest,
        cancelDuplicate: config?.cancelDuplicate === undefined ? 1 : config.cancelDuplicate,
        requestsNum: config?.requestsNum === undefined ? 0 : config.requestsNum
    }
    // 参数已经调整完毕,开始请求
    return new Promise((resolve, reject) => {
        request.request(requestObj)
            .then((res: any) => {
                resolve(res)
            })
            .catch(err => {
                // 失败重新发送请求,如果请求依然失败,只会抛出最后一次错误
                if (err.code !== "ERR_CANCELED" && requestObj.requestsNum && requestObj.requestsNum > 0) {
                    requestObj.requestsNum--
                    console.log(requestObj)
                    http(url, data, {}, requestObj)
                    return
                }
                reject(err)
            })
    })
}

export default http

封装好这个http了,我在把他集成一下,保证请求使用文件的唯一性

6. 集成各种请求实例,应对不同需求

integrated/index

// 获取文件夹下面所有ts文件导出的default,声明modules循坏引入一起导出
const allModules: any = import.meta.glob(["../integrated/*.ts", '!**/index.ts'], { eager: true }), modules: any = {}

for (let key in allModules) modules[key.replace(/(\.\/|\.ts)/g, '')] = allModules[key].default

export default modules

每次创建一个实例我都会把它引入此文件一起导出

**7. 将请求分类,集成后导出 **

api文件夹下新建一个文件夹为ask,里面为你分好类的api

image.png

最后在api下的index.ts里

// 获取文件夹下面所有ts文件导出的default,声明modules循坏引入一起导出
const allModules: any = import.meta.glob("./ask/*.ts", { eager: true }), modules: any = {}

for (let key in allModules) modules[key.replace(/(\.\/ask\/|\.ts)/g, '')] = allModules[key].default

export default modules

8. 实战

api/ask/user.ts

import api from "@/api/integrated";

export default {
    login: {
        url: "/api",
        method: "get",
        fn: (data?: any) => {
            return api.http("/api", data, {
                method: "get",
                requestsNum: 5,
                // 自定义
                interceptors: {
                    requestInterceptors: (config: any) => {
                        console.log("接口请求拦截", config)
                        return config
                    },
                    responseInterceptors: (res: any) => {
                        console.log("接口响应拦截", res)
                        return res
                    }
                },
                // 官方
                transformRequest: [
                    function (config: any) {
                        console.log("官方api接口请求拦截", config)
                        return config
                    }
                ],
                transformResponse: [
                    function (res: any) {
                        console.log("官方api接口响应拦截", res)
                        return res
                    }
                ]
            })
        }
    }
}

这里我后期修改了一下路径

这个路径你可以随意修改,没有基础路径限制

说明一下,我配置的对象有路径和请求方法是为了方便取消单个请求,要是不需要单独手动取消可以直接return请求也可以

然后把所有console打开看看

image.png

这里我后期把打印数据删除了

9. 实战,取消方法

home.vue

<template>
  <div class="h">
    hhh
  </div>
</template>

<script setup lang="ts">
// 集成后的请求方法
import api from "@/api/index";
import { mainStore } from "@/store/index";
import { onMounted } from "vue";

const store = mainStore()
onMounted(() => {
  api.user.login.fn({ sss: "hhh" }).then((res: any) => {
    console.log("使用页面");
    // console.log(res);
  }).catch((err: any) => { })
  
  //取消全部
  // store.cancelRequest("all")
})
</script>

<style lang="scss" scoped>
.h {
  font-size: calc(12rem / 36);
}
</style>

亲测取消方法可用

10. 最后

最后我想说,感谢一碗周大佬的思路,更应该感谢公司大佬的帮助

大部分官方api我没有试过,包括上传下载之类的