我正在参加「掘金·启航计划」
首先了解axios的作用
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
从axios的作用上来说,对请求的封装已经很到位,但是我们有时候,就是要针对一些请求拦截和响应拦截进行一系列的封装,简单来说,封装相当于自己的项目来说,够用就行,但是我就偏不,下面我就要对axios进行一些很变态的操作,那大概实现的就是以下功能
- 无处不在的代码提示
- 灵活可配置的请求拦截器和响应拦截器
- 可取消请求和避免重复请求
- 可灵活配置请求头,超时时间等
- 可实现请求失败后的重新请求发送功能
- 可实现无感刷新token
基础的封装
首先我们实现一个基础的版本,实例代码如下
// axios.ts
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
export class VAxios {
private axiosInstance: AxiosInstance
private readonly options: CreateAxiosOptions
constructor(options: CreateAxiosOptions) {
this.options = options
this.axiosInstance = axios.create(options)
}
requeset(config: AxiosRequestConfig){
return this.instance.request(config)
}
}
拦截器封装
首先我们封装一下拦截器,拦截器分为两种
- 实例拦截器
- 接口拦截器
那我们先提出来一个transform对象,来描述所有的拦截器方法
const transform: AxiosTransform = {
/**
* 处理响应数据,如果数据不是预期的格式,可直接抛出错误
* */
transformResponseHook(res, options) {},
//请求之前处理config
beforeRequestHook: (config, options) => {},
//请求拦截器
requestInterceptors: (config, options) => {},
//请求错误拦截器
requestInterceptorsCatch: (error) => {},
//响应拦截器
responseInterceptors: (res) => { },
/**
* 响应的错误处理
* */
responseInterceptorsCatch: (axiosInstance: AxiosResponse, error: any) => {}
}
类拦截器的实现
类拦截器是比较好实现的,只要对axios
的实例调用interceptors
调用响应拦截器和请求拦截器即可,调用setupInterceptors
来调用拦截器方法
export class VAxios {
private axiosInstance: AxiosInstance
private readonly options: CreateAxiosOptions
constructor(options: CreateAxiosOptions) {
this.options = options
this.axiosInstance = axios.create(options)
this.setupInterceptors()
}
setupInterceptors(){
//
const {
requestInterceptors,
requestInterceptorsCatch,
responseInterceptors,
responseInterceptorsCatch
} = transform
}
//请求拦截器部分
this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
// @ts-ignore
if (requestInterceptors && isFunction(requestInterceptors)) {
config = requestInterceptors(config, this.options)
}
return config
}, undefined)
//请求拦截器错误处理
requestInterceptorsCatch &&
isFunction(requestInterceptorsCatch) &&
this.axiosInstance.interceptors.request.use(undefined, (error) => {
return requestInterceptorsCatch(error)
})
//响应拦截器
this.axiosInstance.interceptors.response.use((res) => {
if (responseInterceptors && isFunction(responseInterceptors)) {
res = responseInterceptors(res)
}
return res
}, undefined)
//响应拦截器的错误捕获
responseInterceptorsCatch &&
isFunction(responseInterceptorsCatch) &&
this.axiosInstance.interceptors.response.use(undefined, (error) => {
return responseInterceptorsCatch(this.axiosInstance, error)
})
}
- 在这里我们就是在对类的拦截器的方法进行了逻辑抽离,把我们需要对请求拦截器和响应拦截器的处理方法都放在了
transform
的方法类中
requestInterceptors请求拦截器处理
- 在这里只是进行了一个简单的token加入,还可以加入其他逻辑
//请求拦截器
requestInterceptors: (config, options) => {
const token = '123456'
// @ts-ignore
if (token) {
// jwt token
config.headers.Authorization = "Bearer" + token
}
return config
},
//请求错误拦截器
requestInterceptorsCatch: (error) => {
console.log(error)
},
responseInterceptorsCatch响应拦截器错误处理
- 这里主要针对的就是对错误进行错误,包括采用是弹窗模式的错误提醒,还是log形式的错误提示
responseInterceptors: (res) => {
return res
},
/**
* 响应的错误处理
* */
responseInterceptorsCatch: (axiosInstance: AxiosResponse, error: any) => {
const { response, code, message, config } = error || {}
const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none'
const msg: string = response?.data?.error?.message ?? ''
const err: string = error?.toString() ?? ''
let errMessage = ''
try {
if (code === 'ECONNABORTED' && message.indexOf('timeout') != -1) {
errMessage = '接口请求超时,请刷新页面充实'
}
if (err.includes('Network Error')) {
errMessage = '网络异常,请检查您的网络连接是否正常'
}
if (errMessage) {
if (errorMessageMode === 'modal') {
console.log('model 提示错误')
} else if (errorMessageMode === 'message') {
console.log('message 提示错误')
}
return Promise.reject(error)
}
return Promise.reject(error)
} catch (error) {
throw new Error(error as unknown as string)
}
//检查各状态码,并进行对应的处理
checkStatus(error?.response?.status, msg, errorMessageMode)
}
checkStatus
方法,对各状态码的报错进行处理,可以自行错误进行提示
//checkStatus.ts
import type { ErrorMessageMode } from '#/axios'
import { useUserStoreWithOut } from '/@/store/modules/user'
export function checkStatus(status: number, msg: string, errorMessageMode: ErrorMessageMode) {
let errMsg = ''
const userStore = useUserStoreWithOut()
switch (status) {
case 400:
errMsg = `${msg}`
break
case 401:
//没有权限,清除token,退出登陆
errMsg = '用户没有权限'
userStore.setToken(undefined)
---逻辑自行完善--
break
case 403:
errMsg = '用户得到授权,但是访问是被禁止的'
break
case 404:
errMsg = '网络请求错误,未找到该资源'
break
case 405:
errMsg = '网络请求错误,请求方法未允许'
break
case 408:
errMsg = '网络请求超时'
break
case 500:
errMsg = '服务器错误,请联系管理员!'
break
case 501:
errMsg = '网络未实现'
break
case 502:
errMsg = '网络错误!'
break
case 503:
errMsg = '服务不可用,服务器暂时过载或维护!'
break
case 504:
errMsg = '网络超时!'
break
case 505:
errMsg = 'http版本不支持该请求!'
break
default:
}
if (errMsg) {
if (errorMessageMode === 'modal') {
console.log('model', errMsg)
} else {
console.log('log', errMsg)
}
}
}
接口拦截器
现在我们对单一接口进行拦截操作
- 包括请求钱参数的处理
- 返回后参数的处理
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
let conf: CreateAxiosOptions = cloneDeep(config)
const { requestOptions } = this.options
const opt: RequestOptions = Object.assign({}, requestOptions, options)
const { beforeRequestHook, requestCatchHook, transformResponseHook } = transform || {}
if (beforeRequestHook && isFunction(beforeRequestHook)) {
conf = beforeRequestHook(conf, opt)
}
conf.requestOptions = opt
return new Promise((resolve, reject) => {
this.axiosInstance
.request(conf)
.then((res) => {
if (transformResponseHook && isFunction(transformResponseHook)) {
try {
const ret = transformResponseHook(res, opt)
resolve(ret)
} catch (error) {
reject(error)
}
return
}
resolve(res as unknown as Promise<T>)
})
.catch((e: Error) => {
if (requestCatchHook && isFunction(requestCatchHook)) {
reject(requestCatchHook(e, opt))
return
}
reject(e)
})
})
}
beforeRequestHook请求钱config处理
- 在请求之前处理config
- 下面只是做了一个简单的配置项处理
//在初始化的配置里面有
let options = {
//接口地址
apiUrl: '',
//接口拼接地址
urlPrefix: '',
//默认将 prefix,添加到url
joinPrefix: true,
}
beforeRequestHook: (config, options) => {
const { apiUrl, joinPrefix,urlPrefix } = options
// 是否添加前缀
if (joinPrefix) {
config.url = `${urlPrefix}${config.url}`
}
//添加接口地址
if (apiUrl && isString(apiUrl)) {
config.url = `${apiUrl}${config.url}`
}
//还可以自定义添加对config的处理
return config
},
transformResponseHook统一处理返回数据,想要的格式
- 根据requestOptions的配置项,来处理判断逻辑
transformResponseHook(res, options) {
const { isReturnNativeResponse, isTransformResponse } = options
//是否返回原生响应头 比如:需要获取响应头时使用该属性
if (isReturnNativeResponse) {
return res
}
//不进行任何处理,直接返回
//用于页面代码可能需要直接获取code,data,message这些信息时开启
if (!isTransformResponse) {
return res.data
}
//错误是返回
const { data } = res
if (!data) {
throw new Error('请求出错,请稍后重试')
}
//这里code,result,message为后台同意字段,需要在type.ts内修改为项目自己的接口返回格式
const { code, result, message } = data
//根据自己项目逻辑自己修改
const hasSuccess = data && Reflect.has(data, 'code')
if (hasSuccess) {
return result
}
//在此处根据自己项目的实际情况对不同的code执行不同的操作
//如果不希望中断当前请求,请return数据,否则直接抛出异常即可
let timeoutMsg = ''
switch (code) {
case ResultEnum.TIMEOUT:
timeoutMsg = '登陆超时,请重新登陆'
//退出系统的逻辑
break
default:
if (message) {
timeoutMsg = message
}
}
if (options.errorMessageMode === 'model') {
console.log('model', timeoutMsg)
} else if (options.errorMessageMode === 'message') {
console.log('message', timeoutMsg)
}
throw new Error(timeoutMsg)
},
取消请求的封装
我们需要将所有的请求保存到一个集合(也可以使用数组,也可以用Map,我使用的是Map)中,然后具体需要去调用这个集合中的某个取消方法
- 先参考axios中使用 cancel token 取消请求的方法
- 简单说明实现思路,就是在请求拦截器时,将请求添加到集合中,在响应拦截器时,将其取消,如果判断出来method,url拼接出来的key值相同的请求,则取消重复发送
- config中的配置说明,可见
AxiosRequestConfig
这个接口中的配置
//
import type { Canceler, AxiosRequestConfig } from 'axios'
import axios from 'axios'
import { isFunction } from 'lodash-es'
let pendingMap = new Map<string, Canceler>()
//创建key值作为唯一值
export const getPendingUrl = (config: AxiosRequestConfig) =>
[config.method, config.url].join('&')
export class AxiosCanceler {
addPending(config: AxiosRequestConfig) {
this.removePending(config)
const url = getPendingUrl(config)
// @ts-ignore
config.cancelToken =
config.cancelToken ||
new axios.Cancel((cancel) => {
if (!pendingMap.has(url)) {
pendingMap.set(url, cancel)
}
})
}
//取消全部请求
removeAllPending() {
pendingMap.forEach((cancel) => {
cancel && isFunction(cancel) && cancel()
})
}
removePending(config: AxiosRequestConfig) {
const url = getPendingUrl(config)
if (pendingMap.has(url)) {
const cancel = pendingMap.get(url)
cancel && cancel()
pendingMap.delete(url)
}
}
/**
* @description: reset
*/
reset(): void {
pendingMap = new Map<string, Canceler>()
}
}
改造请求拦截器的方法
// @ts-ignore
//请求拦截器部分
this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
//将请求添加到队列中
axiosCanceler.addPending(config)
if (requestInterceptors && isFunction(requestInterceptors)) {
config = requestInterceptors(config, this.options)
}
return config
}, undefined)
改造响应拦截器
//响应拦截器
this.axiosInstance.interceptors.response.use((res) => {
//如果响应了,那么就清除掉请求
res && axiosCanceler.removePending(res.config)
if (responseInterceptors && isFunction(responseInterceptors)) {
res = responseInterceptors(res)
}
return res
}, undefined)
添加请求失败自动重发功能
- 参考文章:blog.csdn.net/weixin_4743…
- 希望请求失败后,可以自动重发5次
改造responseInterceptorsCatch
/**
* 响应的错误处理
* */
let retryRequest = {
isOpenRetry: true,
count: 5,
waitTime: 100
}
responseInterceptorsCatch: (axiosInstance: AxiosResponse, error: any) => {
//添加自动重试机制 保险起见 只针对于GET请求
const retryRequest = new AxiosRetry()
const { isOpenRetry } = retryRequest
config.method?.toUpperCase() === 'GET' && isOpenRetry
&& retryRequest.retry(axiosInstance, error)
return Promise.reject(error)
}
- AxiosRetry.ts
import type { AxiosInstance, AxiosError } from 'axios'
export class AxiosRetry {
/**
* 重试
* */
retry(AxiosInstance: AxiosInstance, error: AxiosError) {
// @ts-ignore
const { config } = error.response
// eslint-disable-next-line no-unsafe-optional-chaining
const { waitTime, count } = retryRequest
config.__retryCount = config.__retryCount || 0
if (config.__retryCount >= count) {
return Promise.reject(error)
}
config.__retryCount += 1
return this.delay(waitTime).then(() => AxiosInstance(config))
}
/**
* 延迟
*/
private delay(waitTime: number) {
return new Promise((resolve) => setTimeout(resolve, waitTime))
}
}
写在最后
我们其实还可以针对于axios封装无感刷新token,对上传文件方法进行封装,但是本人感觉,太多的封装导致代码的阅读成本太高,相较于而言,本人更喜欢vue-element-admin的axios的封装方法,以上的封装方式就是为了练习而言,可以参考,完善自己的思维
代码参考地址:github.com/vbenjs/vue-…
原文链接:https://juejin.cn/post/7231921877467316261 作者:前端胡汉三