前言
最后更新时间:2022.8.29(新增api请求分类,集成后一起导出,接口定义文件位置统一)
我的项目各版本
我不确定我所封装是否没有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这个插件,这样以后需要更换或者维护只用到这个文件进行修改
里面目前只有一个请求使用的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文件夹下
代码
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
封装的本意是方便使用\维护,复用率高,减少本来需要重复写的代码,但是我现在并不能全部做到
我只能尽力尽我所能
现在需要创建一个这样的目录结构
注意了,最后一个index.ts是api文件夹下第一级的,与其他文件夹同级
接口定义文件夹
从上往下慢慢来
我对一碗周大佬的结构做了一些修改,拦截器的触发顺序为
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
最后在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打开看看
这里我后期把打印数据删除了
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我没有试过,包括上传下载之类的