一、为什么要对axios封装?多此一举?
本文将基于 typescript 对 axios 进行优雅封装,同时会将每部分内容细化,尽最大努力带大家弄明白每一处知识和细节。
其实要对 axios 进行封装并不是一个简单的过程,也有许多人认为这是化简为繁,但其实封装本身就是麻烦自己,方便所有人。对 axios 进行二次封装有助于使项目的网络请求方式更加规范化,可复用性和定制化程度更高,减少重复劳动。
"如何对 axios 进行封装"、"前端短时间内发送多个http请求,如何确保获取最后发送请求的响应?"这些是我们在面试中常聊到的话题,相信跟着本文来一点点敲出属于自己的 AxiosMax,在回答这些问题的时候也能更随心应手。
本文基于 vue-vben-admin 项目对 axios 的封装方式,全文代码仓库在文末。
二、项目结构
AbortAxios.ts:
取消请求实体类Axios.ts:
请求实体类axiosRetry.ts:
重复请求方法checkErrorStatus:
错误状态码处理config.ts:
静态配置index.ts:
实例创建、拦截器实现type.ts:
类型定义
三、类型定义
如果项目并没有使用 ts 的需求,或者还不了解 ts 的,也可以直接采用 js 进行封装,把相关类型删除就行。
type.ts
中
typescript
import type { AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse, AxiosInstance, AxiosError } from 'axios'
/**
* axios实例配置选项,继承AxiosRequestConfig
*/
export interface AxiosOptions extends AxiosRequestConfig {
// 是否直接返回data数据
directlyGetData?: boolean
// 定义拦截器
interceptors?: RequstInterceptors
// 是否取消重复请求
abortRepetitiveRequest?: boolean
// 重连配置
retryConfig?: {
// 重连次数
count: number
// 每次请求间隔时间
waitTime: number
}
}
/**
* 定义拦截器抽象类,后续在index.ts文件中继承实现
*/
export abstract class RequstInterceptors {
// 请求拦截器
abstract requestInterceptors?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
// 请求错误拦截器
abstract requestInterceptorsCatch: (err: Error) => Error
// 响应拦截器
abstract responseInterceptor?: (res: AxiosResponse) => AxiosResponse
// 响应错误拦截器
abstract responseInterceptorsCatch?: (axiosInstance: AxiosInstance, error: AxiosError) => void;
}
/**
* 定义返回类型
*/
export interface Respones<T = any> {
code: number
result: T
}
四. 具体封装步骤
1. 创建请求实体类
在 Axios.ts
文件中,创建一个实体类 AxiosMax
后续可通过 const useRequest = new AxiosMax(...)
直接使用。
typescript
class AxiosMax {
// axios实例, 通过axios.create()方法创建
private axiosInstance: AxiosInstance
// 传入的配置
private options: AxiosOptions
// 拦截器
private interceptors: RequstInterceptors | undefined
constructor(options: AxiosOptions) {
this.axiosInstance = axios.create(options)
this.options = options
this.interceptors = options.interceptors
// 对拦截器进行初始化注册
this.setInterceptors()
}
...
}
2. 在 AxiosMax 实体类中创建统一请求方法
后续通过 ueRequest.get({ url: '/a' })
直接调用
typescript
class AxiosMax {
...
/**
* 统一请求方法
*/
request<T = any>(config: AxiosRequestConfig): Promise<T> {
return new Promise((resolve, reject) => {
this.axiosInstance.request<any, AxiosResponse<Respones>>(config).then((res) => {
return resolve(res as unknown as Promise<T>)
}).catch((err) => {
return reject(err)
})
})
}
get<T = any>(config: AxiosRequestConfig): Promise<T> {
return this.request<T>({ ...config, method: 'GET' })
}
post<T = any>(config: AxiosRequestConfig): Promise<T> {
return this.request<T>({ ...config, method: 'POST' })
}
put<T = any>(config: AxiosRequestConfig): Promise<T> {
return this.request<T>({ ...config, method: 'PUT' })
}
delete<T = any>(config: AxiosRequestConfig): Promise<T> {
return this.request<T>({ ...config, method: 'DELETE' })
}
...
}
3. 注册拦截器方法
这部分的内容相对于较多,但是关键知识较多,希望大家能够耐心看完。
本小节主要是讲述创建注册拦截器的方法。
typescript
class AxiosMax {
...
/**
* 注册拦截器方法
*/
setInterceptors() {
// 如果配置中并没有传入拦截器,则直接返回
if (!this.interceptors) return
// 解构出各个拦截器
const {
requestInterceptors,
requestInterceptorsCatch,
responseInterceptor,
responseInterceptorsCatch
} = this.interceptors
// 挂载请求拦截器
this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
if (requestInterceptors) {
// 如果存在请求拦截器,则将 config 先交给 requestInterceptors 做对应的配置。
config = requestInterceptors(config)
}
return config
}, requestInterceptorsCatch ?? undefined)
// 挂载响应拦截器
this.axiosInstance.interceptors.response.use((res: AxiosResponse) => {
if (responseInterceptor) {
// 如果存在响应拦截器,则将返回值先交给 responseInterceptor 做处理
res = responseInterceptor(res)
}
// 根据 options.directlyGetData 配置选项判断是否直接取得data值
if (this.options.directlyGetData) {
res = res.data
}
return res
}, (err: AxiosError) => {
if (responseInterceptorsCatch) {
// 如果存在响应错误拦截器,则将返回值交给 responseInterceptorsCatch 做处理
return responseInterceptorsCatch(this.axiosInstance, err)
}
return err
})
}
...
}
内容到这里,我们已经实现了 axios 二次封装的雏形,直接拿去使用也不是不行,但总感觉还是缺了点什么,下面就将带大家继续完善我们的 AxiosMax,坚持就是胜利!
4. 错误状态码统一判断
经历了上面复杂的内容,我们来点简单的放松一下!
checkErrorStatus.ts
中:
typescript
/**
* 对错误状态码进行检测
*/
export function checkErrorStatus(status: number | undefined, message: string | undefined, callback: (errorMessage: string) => any) {
let errorMessage = message ?? ''
switch (status) {
case 400:
errorMessage = '客户端错误,请求格式或参数有误!'
break
case 401:
errorMessage = '身份认证不通过'
break
case 403:
errorMessage = '用户得到授权,但是访问是被禁止的!'
break
case 404:
errorMessage = '未找到目标资源!'
break
case 500:
errorMessage = '服务器错误!'
break
case 503:
errorMessage = '服务器错误!'
break
}
if (errorMessage.length > 0) {
callback(`checkErrorStatus:${errorMessage}`)
}
}
后续将在响应错误拦截器当中调用 checkErrorStatus
函数,来对错误状态进行统一判断。
5. 取消重复请求
"前端短时间内发送多个http请求,如何确保获取最后发送请求的响应?"这是面试官常问的一道题,接下来,我就将带大家实现一遍相似功能,其核心是通过 AbortController
API来取消请求。
AbortAxios.ts
中:
typescript
import { AxiosRequestConfig } from "axios"
// 用于存储控制器
const pendingMap = new Map<string, AbortController>()
// 创建各请求唯一标识, 返回值类似:'/api:get',后续作为pendingMap的key
const getUrl = (config: AxiosRequestConfig) => {
return [config.url, config.method].join(':')
}
class AbortAxios {
// 添加控制器
addPending(config: AxiosRequestConfig) {
this.removePending(config)
const url = getUrl(config)
// 创建控制器实例
const abortController = new AbortController()
// 定义对应signal标识
config.signal = abortController.signal
if (!pendingMap.has(url)) {
pendingMap.set(url, abortController)
}
}
// 清除重复请求
removePending(config: AxiosRequestConfig) {
const url = getUrl(config)
if (pendingMap.has(url)) {
// 获取对应请求的控制器实例
const abortController = pendingMap.get(url)
// 取消请求
abortController?.abort()
// 清除出pendingMap
pendingMap.delete(url)
}
}
}
export default AbortAxios
在 AxiosMax
类的 setInterceptors
方法中补上下面标识的内容:
至此,我们的注册拦截器函数才完成,我们也实现了取消重复请求的功能。
6. 超时报错重发
本小节将带大家实现请求超时报错重发的功能。
axiosRetry.ts
中:
typescript
import type { AxiosError, AxiosInstance } from "axios";
export function retry(instance: AxiosInstance, err: AxiosError) {
const config: any = err.config
// 获取配置项内容(请求间隔时间,请求次数)
const { waitTime, count } = config.retryConfig ?? {}
// 当前重复请求的次数
config.currentCount = config.currentCount ?? 0
console.log(`第${config.currentCount}次重连`)
// 如果当前的重复请求次数已经大于规定次数,则返回Promise
if (config.currentCount >= count) {
return Promise.reject(err)
}
config.currentCount++
// 等待间隔时间结束后再执行请求
return wait(waitTime).then(() => instance(config))
}
function wait(waitTime: number) {
return new Promise(resolve => setTimeout(resolve, waitTime))
}
我们将在下一小节中展示 retry
函数的具体用法。
7. 实现拦截器
index.ts
中:
typescript
// 继承了我们在最开始实现的抽象类RequstInterceptors,主要关心responseInterceptorsCatch内容
const _RequstInterceptors: RequstInterceptors = {
requestInterceptors(config) {
return config
},
requestInterceptorsCatch(err) {
return err
},
responseInterceptor(config) {
return config
},
responseInterceptorsCatch(axiosInstance, err: AxiosError) {
let message = err.code === 'ECONNABORTED' ? '请求超时' : undefined
// 判断本次请求是否已经被取消
if (axios.isCancel(err)) {
return Promise.reject(err);
}
// 检查响应状态码
checkErrorStatus((err as AxiosError).response?.status, message, (message) => console.log(message))
// 响应错误实现重连功能
return retry(axiosInstance, err as AxiosError)
},
}
8. 创建实例
这是我们的最后一步,就是创建 AxiosMax
的实例,然后就可以尽情使用由我们自己封装的axios了。
typescript
const useRequest = new AxiosMax({
directlyGetData: true,
baseURL: staticAxiosConfig.baseUrl,
timeout: 3000,
interceptors: _RequstInterceptors,
abortRepetitiveRequest: true,
retryConfig: {
count: 5,
waitTime: 500
}
})
export default useRequest
9. 测试
通过简单的测试,封装的 axios 已经实现了我们既定的功能。
代码仓库地址:github.com/windlil/axi...
感谢大家的阅读!