我正在参加「AdminJS·启航计划」
前言
最近在写admin
项目时,想对axios
方面进行一个彻底的重造,除了常规的错误信息拦截外,增加一些新的功能,目前已实现:loading加载、错误自动重试、错误日志记录、取消重复请求,中间也遇到过一些问题,这里记录下如何解决的,希望对你有所帮助。
ps:这里使用的vue3+ts+vite
基础配置
先安装axios:
# 选择一个你喜欢的包管理器
# NPM
$ npm install axios -s
# Yarn
$ yarn add axios
# pnpm
$ pnpm install axios -s
初始化axios
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import axios from "axios";
const service: AxiosInstance = axios.create({
baseURL: '/api',
timeout: 10 * 1000, // 请求超时时间
headers: { "Content-Type": "application/json;charset=UTF-8" }
});
区分不同环境
# 选择一个你喜欢的包管理器
# NPM
$ npm install axios -s
# Yarn
$ yarn add axios
# pnpm
$ pnpm install axios -s
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import axios from "axios";
const service: AxiosInstance = axios.create({
baseURL: '/api',
timeout: 10 * 1000, // 请求超时时间
headers: { "Content-Type": "application/json;charset=UTF-8" }
});
实际项目中,我们可能分开发环境、测试环境和生产环境,所以我们先在根目录建立三个文件:.env.development
,.env.production
, .env.test
其中 vite
内置了几个方法获取环境变量:
如果你想自定义一些变量,必须要以 VITE_
开头,这样才会被vite检测到并读取,例如我们可以在刚刚建立三种环境下定义title
,api
等字段,vite
会自动根据当前的环境去加载对应的文件。
# development
# app title
VITE_APP_Title=Vue3 Basic Admin Dev
# baseUrl
VITE_BASE_API= /dev
# public path
VITE_PUBLIC_PATH = /
# production
# app title
VITE_APP_Title=Vue3 Basic Admin
# baseUrl
VITE_BASE_API= /prod
# public path
VITE_PUBLIC_PATH = /
# test
# app title
VITE_APP_Title=Vue3 Basic Admin Test
# baseUrl
VITE_BASE_API= /test
# public path
VITE_PUBLIC_PATH = /
修改axios baseUrl:
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import axios from "axios";
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_BASE_API,
timeout: 10 * 1000, // 请求超时时间
headers: { "Content-Type": "application/json;charset=UTF-8" }
});
抽取hook
有些时候我们不希望项目中直接使用 import.meta.env
去读取env
配置,这样可能会不便于管理,这个时候可以抽取一个公共hook
文件,这个hook
文件主要就是去读取env
配置,然后页面中再通过读取这个hook
拿到对应的配置。
还有一个原因就是 如果env
配置中文件名字变换了,还得一个个的去项目中手动改,比较麻烦,在hook
中可以解构别名,然后return
出去即可。
建立 src/hooks/useEnv.ts:
export function useEnv() {
const { VITE_APP_TITLE, VITE_BASE_API, VITE_PUBLIC_PATH, MODE } = import.meta.env;
// 如果名字变换了,我们可以在这里解构别名
return {
MODE,
VITE_APP_NAME,
VITE_BASE_API,
VITE_PUBLIC_PATH,
VITE_BASE_UPLOAD_API
};
}
再更改axios
import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
import axios from "axios";
import { useEnv } from "@/hooks";
const { VITE_BASE_API } = useEnv();
const service: AxiosInstance = axios.create({
baseURL: VITE_BASE_API,
timeout: 10 * 1000, // 请求超时时间
headers: { "Content-Type": "application/json;charset=UTF-8" }
});
响应和拦截
这里就是老生常谈的axios请求和相应了,相关的例子有太多了,这里就简单描述下。
# 请求
service.interceptors.request.use((config: AxiosRequestConfig) => {
// 这里可以设置token: config!.headers!.Authorization = token
return config;
});
# 响应
service.interceptors.response.use((response: AxiosResponse) => {
const data = response.data;
if (data.code === 200) {
return data;
} else {
return Promise.reject(data);
}
},
(err) => {
return Promise.reject(err.response);
}
);
对外暴露方法和使用
外部使用时想通过xx.ge
t直接能请求,所以这里定义一个对外暴露的方法。
// ...前面的省略
const request = {
get<T = any>(url: string, data?: any): Promise<T> {
return request.request("GET", url, { params: data });
},
post<T = any>(url: string, data?: any): Promise<T> {
return request.request("POST", url, { data });
},
put<T = any>(url: string, data?: any): Promise<T> {
return request.request("PUT", url, { data });
},
delete<T = any>(url: string, data?: any): Promise<T> {
return request.request("DELETE", url, { params: data });
},
request<T = any>(method = "GET", url: string, data?: any): Promise<T> {
return new Promise((resolve, reject) => {
service({ method, url, ...data })
.then((res) => {
resolve(res as unknown as Promise<T>);
})
.catch((e: Error | AxiosError) => {
reject(e);
})
});
}
};
export default request;
外部使用:
api/user.ts文件中:
import request from "@/utils/request";
export const login = (data?: any) => request.post('/login', data);
到这里基础封装已经完成,接下来咱们给axios添加一些功能。
错误日志收集
需解决的问题:
- 收集接口错误信息
所以为了解决这个问题,我们可以给axios
扩展一个log
方法,去解决这个问题。
首先,先在axios
同级定义log.ts
文件:定义addErrorLog
方法
- 当页面进入响应拦截器的且进入
error
回调的时候,获取当前的url
,method
,params
,data
等参数,并请求添加日志接口。
export const addAjaxErrorLog = (err: any) => {
const { url, method, params, data, requestOptions } = err.config;
addErrorLog({
type:'ajax',
url: <string>url,
method,
params: ["get", "delete"].includes(<string>method) ? JSON.stringify(params) : JSON.stringify(data),
data: err.data ? JSON.stringify(err.data) : "",
detail: JSON.stringify(err)
});
};
axios中引入使用
import {addAjaxErrorLog} from "./log"
# 响应
service.interceptors.response.use((response: AxiosResponse) => {
// ...其他代码
},
(err) => {
// ...其他代码
addAjaxErrorLog(err.response)
}
);
效果展示:
添加loading功能
需解决的问题:
-
有时候我们可能需要
axios
请求时自动开启loading
,结束时关闭loading; -
初始化同时请求
n
个接口,请求完成后关闭loading
,但又不想页面中使用Primise.all
所以为了解决这两个问题,我们可以给axios
扩展一个loading
方法,去解决这两个问题。
首先,先在axios
同级定义loading.ts
文件:定义init
、add
和close
三个方法
- 当进入请求拦截器的时候,调用
addLoading
,开启loading
并loadingCount++
- 当页面进入响应拦截器的时候,调用
closeLoaidng
,loadingCount--
,如果loadingCount
数量为0
则关闭loading
import { ElLoading } from 'element-plus'
export class AxiosLoading {
loadingCount: number;
loading:any
constructor() {
this.loadingCount = 0;
}
initLoading(){
if(this.loading){
this.loading?.close?.()
}
this.loading=ElLoading.service({
fullscreen:true
})
}
addLoading() {
if (this.loadingCount === 0) {
initLoading()
}
this.loadingCount++;
}
closeLoading() {
if (this.loadingCount > 0) {
if (this.loadingCount === 1) {
loading.close();
}
this.loadingCount--;
}
}
}
axios中引入使用:
import {AxiosLoaing} from "./loading"
const axiosLoaing=new AxiosLoaing()
// ...其他代码
# 请求
service.interceptors.request.use((config: AxiosRequestConfig) => {
axiosLoaing.addLoading();
return config;
});
# 响应
service.interceptors.response.use((response: AxiosResponse) => {
// ...其他代码
axiosLoaing.closeLoading();
},
(err) => {
// ...其他代码
axiosLoaing.closeLoading();
}
);
效果展示:
取消重复请求
需解决的问题:
- 有时候可能会同时请求相同的接口(例如:分页的时候很快的切换几次页码),如果该接口比较慢就比较浪费性能
所以为了解决这个问题,我们可以给axios
扩展一个calcel
方法,去解决这个问题。
这里先了解下axios
给出的示例,请求接口的时候通过AbortController
拿到实例,请求时携带signal
,如果想取消可以通过返回实例的abort
方法。
首先,先在axios
同级定义calcel.ts
文件:定义addPending
和removePending
,removeAllPending
,reset
四个方法
-
当进入请求拦截器的时候,调用
addPending
,调用addPending
前,先执行removePedding
方法,终止掉请求队列中的请求,pendingMap set
当前请求,以method,url
参数当做key
,value
为AbortController
实例; -
当页面进入响应拦截器的时候,调用
remove
,清除pendingMap
中的当前key
-
需要注意的是,取消请求后会触发响应拦截器的
err
回调,所以需要做一下处理。
import type { AxiosRequestConfig } from "axios";
export class AxiosCancel {
pendingMap: Map<string, AbortController>;
constructor() {
this.pendingMap = new Map<string, AbortController>();
}
generateKey(config: AxiosRequestConfig): string {
const { method, url } = config;
return [ url || "", method || ""].join("&");
}
addPending(config: AxiosRequestConfig) {
this.removePending(config);
const key: string = this.generateKey(config);
if (!this.pendingMap.has(key)) {
const controller = new AbortController();
config.signal = controller.signal;
this.pendingMap.set(key, controller);
} else {
config.signal = (this.pendingMap.get(key) as AbortController).signal;
}
}
removePending(config: AxiosRequestConfig) {
const key: string = this.generateKey(config);
if (this.pendingMap.has(key)) {
(this.pendingMap.get(key) as AbortController).abort();
this.pendingMap.delete(key);
}
}
removeAllPending() {
this.pendingMap.forEach((cancel: AbortController) => {
cancel.abort();
});
this.reset();
}
reset() {
this.pendingMap = new Map<string, AbortController>();
}
}
axios中引入使用:
import {AxiosCancel} from "./cancel"
const axiosCancel=new AxiosCancel()
// ...其他代码
# 请求
service.interceptors.request.use((config: AxiosRequestConfig) => {
axiosCancel.addPending();
return config;
});
# 响应
service.interceptors.response.use((response: AxiosResponse) => {
// ...其他代码
axiosCancel.removePending();
},
(err) => {
if (err.code === "ERR_CANCELED") return;
// ...其他代码
axiosCancel.removePending();
}
);
效果展示:
错误重试机制
需解决的问题:
- 有时候接口可能突然出问题,所以允许错误自动重试 (慎用!!!)
所以为了解决这个问题,我们可以给axios
扩展一个retry
方法,去解决这个问题。
首先,先在axios
同级定义retry.ts
文件:定义retry
方法
- 当页面进入响应拦截器的且进入
error
回调的时候,判断当前接口的目前的重试次数是否大于规定的重试次数,如果小于,则执行retry
方法进行接口重新请求。
import type { AxiosError, AxiosInstance } from "axios";
export class AxiosRetry {
retry(service: AxiosInstance, err: AxiosError) {
const config = err?.config as any;
config._retryCount = config._retryCount || 0;
config._retryCount += 1;
delete config.headers; //删除config中的header,采用默认生成的header
setTimeout(() => {
service(config);
}, 100);
}
}
axios中引入使用:
import {AxiosRetry} from "./retry"
```ts
# 响应
service.interceptors.response.use((response: AxiosResponse) => {
// ...其他代码
},
(err) => {
if ((err.config._retryCount || 0) < 3) {
const axiosRetry = new AxiosRetry();
axiosRetry.retry(service, err);
return;
}
// ...其他代码
}
);
效果展示:
功能配置
需解决的问题:
- 有时候可能某个接口仅需要部分功能,例如仅某个接口需要重试,其他的不需要的情况。
所以为了解决这个问题,我们可以给axios
增加了一个默认配置axiosOptions
,去解决这个问题。
- 当页面进入需要使用某些参数的时候,先去读当前接口是否传递了,如果没有则去读取
axios
默认配置。
设置默认配置:
interface axiosConfig {
successMessage?: boolean; // 是否提示成功信息
errorMessage?: boolean; // 是否提示失败信息
cancelSame?: boolean; // 是否取消相同接口
retryCount?: number; // 失败重试次数
isRetry?: boolean; // 是否失败重试
}
const defaultConfig: axiosConfig = {
successMessage: false,
errorMessage: true,
cancelSame: false,
isRetry: false,
retryCount: 3
};
修改request,加上requestOptions参数:
# 修改request方法
const request = {
request<T = any>(method = "GET", url: string, data?: any, config?: axiosConfig): Promise<T> {
// 和默认配置合并
const options = Object.assign({}, defaultConfig, config);
return new Promise((resolve, reject) => {
service({ method, url, ...data, requestOptions: options })
.then((res) => {
// ...其他代码
})
.catch((e: Error | AxiosError) => {
// ...其他代码
})
});
}
};
请求拦截器:
service.interceptors.request.use((config: AxiosRequestConfig) => {
const { cancelSame } = config.requestOptions;
if (cancelSame) {
axiosCancel.addPending(config);
}
axiosLoading.addLoading();
return config;
});
响应拦截器:
service.interceptors.response.use((response: AxiosResponse) => {
const { cancelSame } = response.config.requestOptions;
if (cancelSame) {
axiosCancel.removePending(response.config);
}
// ...其他代码
},
(err) => {
const { isRetry, retryCount,cancelSame } = err.config.requestOptions;
if (isRetry && (err.config._retryCount || 0) < retryCount) {
//...其他代码
}
cancelSame && axiosCancel.removePending(err.config || {});
// ...其他代码
}
);
使用:
export const sameTestApi = (data?: any) => request.get('/test', data, { cancelSame: true });
效果展示:
最后
到这里axios集成已经完成了,完整代码存放在vue3-basic-admin里面,vue3-basic-admin
?是一款开源开箱即用的中后台管理系统。基于?Vue3
、Vite
、Element-Plus
、TypeScript
、Pinia
?等主流技术开发,内置许多开箱即用的组件,能快速构建中后台管理系统,目前决定完全开源。点击预览
如果觉得本文对你有帮助,可以点一下赞,如果好的意见可以评论区留言,大家共同进步。
其他文章
-
十五.vite搭建之优雅的使用icon图标
-
十四.使用verdaccio搭建私有组件库
-
十三.2022年了,你还不会vue3吗?
-
九. vue后台管理系统中权限控制
-
二. Echarts+Amap实现点击下钻功能? ? ??? ?
十五.vite搭建之优雅的使用icon图标
十四.使用verdaccio搭建私有组件库
十三.2022年了,你还不会vue3吗?
九. vue后台管理系统中权限控制
二. Echarts+Amap实现点击下钻功能? ? ??? ?
代办报建
本公司承接江浙沪报建代办施工许可证。
联系人:张经理,18321657689(微信同号)。
22条评论
东方不败还是灭绝师太啊?http://test.cqyiyou.net/test/
顶一下,收藏了!http://yeoy.surgreen.com.cn
态度决定一切,不错!http://kdf.wwggzp.com
你觉得该怎么做呢?http://yt5u.k3i6.com
这位作者的文笔极其出色,用词精准、贴切,能够形象地传达出他的思想和情感。http://o1m8x.nicetimes.cn
顶!顶!顶!http://p7iog.doytuan.com
收藏了,楼主加油!http://www.jiagu1.com/fei/aphcf5mh9/
被楼主的逻辑打败了!http://odq3k.ahkphb.com
突然觉得楼主说的很有道理,赞一个!http://vyk46k.yg3h.com
东方不败外加灭绝师太啊!http://www.3553km.com
看帖、回帖、拿分、走人http://q3u2.whhkzc.com
楼上的很有激情啊!http://www.3553km.com
世界末日我都挺过去了,看到楼主我才知道为什么上帝留我到现在!http://6e9.ibbxtx.net
看帖不回帖都是耍流氓!http://0zpk.nutrisunoil.com
楼主是在找骂么?http://87puom.etf9.com
这么好的帖子,应该加精华!http://yxxe5.hbjdco.com
我和我的小伙伴都惊呆了!http://ea1v4s.jssg888.com
对牛弹琴的人越来越多了!http://w6fm.cczcgs.net
楼主是我最崇拜的人!http://www.guangcexing.net/voddetail/tFRTnby.html
东方不败还是灭绝师太啊?http://www.guangcexing.net/voddetail/pGJZYcSTBW.html
突然觉得楼主说的很有道理,赞一个!https://www.skypeis.com/
白富美?高富帅?https://www.telegramlp.com/
发表评论