Vue-vben-admin Vue3+TS Axios的封装源码分析
前言
一、近期再用Vue3+TS 重构之前Vue2的项目,因此想着借鉴一下业界较为优秀的代码,在Git上面找了好久,经过同事推荐,我发现由anncwd出品的Vue-vben-admin是很不错的,截止目前在git上面已经有5.9k个star了,目前将这个模板看完了,我觉得里面写的最好的莫过于对于Axios的封装了,在此我简单对这个源码进行一个分析,里面也涉及一些TS+Vue3的知识点,最重要的是,能够发现其中写的特别优秀的地方,我们也可以运用在自己的项目当中。
1.我们先来看一下源码中的目录结构吧
2.目录分析,这个文件夹当中分为6个文件,其中index.ts是入口文件,Axios是主要的对于axios二次封装的类,axiosCancel是对于取消请求类canceler的封装,axiosTransform.ts是定义了一个类,涵盖所有对于数据处理无论错误还是失败的钩子函数,checkStatus.ts是对于后端返回code非200时的处理函数,helper是一个处理时间的函数,这6个文件相辅相成,构建了一个强大的axios的二次封装,接下来我们就来看一看其中每个文件的源码吧
二、index.ts
1.在看index.ts的源码之前我必须先带着大家补充几个知识点,以免其中有几个地方比较卡顿
(1)ts中的Partial
它可以将接受一个T泛型,可以将T中的所有属性变为可选的,换句话说,任意类型的机构,经过Partial处理后,所有的内部属性就变为可选的了。
interface A {
a:string
b:number
}
const a:A = { // error 因为A类型必须具备两个属性
a:'张三'
}
const b:Partial = { // successful 经过Partial处理后,所有属性值变为可选
a:'张三'
}
//看一下他的源码
type Partial = {
[P in keyof T]?: T[P];
};
//其实就是给每一个属性加了一个?操作符而已,将所有属性变为非必须的了而已
(2)encodeURIComponent
//这个是JavaScript的原生Api ,可以把字符串作为 URI 组件进行编码。字母数字保持原来的样子,当时汉字或者其他所有符号都会将其编码为数字,字母或者%,
var uri="http://w3cschool.cc/my test.php?name=ståle&car=saab";
encodeURIComponent(uri)
// 输出:http%3A%2F%2Fw3cschool.cc%2Fmy%20test.php%3Fname%3Dst%C3%A5le%26car%3Dsaab
有了上面的补充,接下来就直接来看一下代码吧,我都做了注释,但是下面的代码会依赖一些类型或者函数,大家先过一下,我们主要看一下结构,开始吧
import { AxiosTransform } from './axiosTransform'
import { AxiosResponse, AxiosRequestConfig } from 'axios'
import { Result, RequestOptions, CreateAxiosOptions } from './types'
import { errorResult } from './const'
import { ResultEnum, RequestEnum, ContentTypeEnum,} from '../../../enums/httpEnum'
import { useMessage } from '@/hooks/web/useMessage'
import { isString, isObject, isBlob } from '@/utils/is'
import { formatRequestDate } from './helper'
import { setObjToUrlParams, deepMerge } from '@/utils'
import { getToken } from '@/utils/auth'
import { checkStatus } from './checkStatus'
import VAxios from './Axios'
const { createMessage, createErrorModal } = useMessage()
/**
* 请求处理
*/
const transform: AxiosTransform = { // 所谓transform 本质上就是 transform 这个对象中拥有 多个处理数据的钩子
/**
* 请求成功处理
* 但依然要根据后端返回code码进行判断
*/
transformRequestData: ( // 对后端返回的数据做处理, 这是在http状态码为200的时候
res: AxiosResponse,
options: RequestOptions
) => {
const { isTransformRequestResult } = options
if (!isTransformRequestResult) return res.data // 不处理 直接返回res.data
const { data } = res
if (!data) return errorResult // 错误处理
const { code, msg: message } = data
if (code === ResultEnum.UNLOGIN) {
const msg = '请重新登陆!'
createMessage.error(msg)
Promise.reject(new Error(msg))
location.replace('/login')
return errorResult
}
if (code === ResultEnum.BACKERROR) {
const msg = '操作失败,系统异常!'
createMessage.error(msg)
Promise.reject(new Error(msg))
return errorResult
}
const hasSuccess =
data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS // 200
if (!hasSuccess) { // 非200 非 401 非500 的情况 直接返回后端提示的错误
if (message) {
if (options.errorMessageMode === 'modal') {
createErrorModal({ title: '错误提示', content: message })
} else {
createMessage.error(message)
}
}
Promise.reject(new Error(message))
return errorResult
}
if (code === ResultEnum.SUCCESS) return data // 200 返回data
if (code === ResultEnum.TIMEOUT) {
const timeoutMsg = '登录超时,请重新登录!'
createErrorModal({
title: '操作失败',
content: timeoutMsg,
})
Promise.reject(new Error(timeoutMsg))
return errorResult
}
return errorResult
},
/*
* 请求发送之前的钩子 说白了,本质上这个函数就是在处理发送之前的参数 用户永远只需要传params就可以传参数了
*/
beforeRequestHook: (config: AxiosRequestConfig, options: RequestOptions) => {
const { apiUrl, joinParamsToUrl, formatDate } = options
if (apiUrl && isString(apiUrl)) {
config.url = `${apiUrl}${config.url}`
}
if (config.method?.toUpperCase() === RequestEnum.GET) { // 对get方法做处理,避免浏览器缓存数据,导致数刷新不及时
const now = new Date().getTime()
if (!isString(config.params)) {
config.params = Object.assign(config.params || {}, { _t: now })
} else {
config.url = config.url + '/' + encodeURIComponent(config.params)
config.params = undefined
}
} else {
// 这个是post 或者 其他的非get方式的固定写法 必须将参数放在data当中
if (!isString(config.params)) {
formatDate && formatRequestDate(config.params)
if (joinParamsToUrl) {
config.url = setObjToUrlParams(config.url as string, config.params)
} else {
config.data = config.params
}
config.params = undefined
} else {
config.url = config.url + '/' + encodeURIComponent(config.params)
config.params = undefined
}
}
return config
},
// 请求拦截, 添加token 没什么说的
requestInterceptors: (config) => {
const token = getToken()
if (token) {
config.headers.Authorization = token
}
return config
},
// 当http状态码非200时的错误处理
responseInterceptorsCatch: (error: any) => {
//todo
const { response, code, message } = error || {}
const err: string = error.toString()
try {
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
createMessage.error('接口请求超时,请刷新页面重试!')
}
if (err && err.includes('Network Error')) {
createErrorModal({
title: '网络异常',
content: '网络异常,请检查您的网络连接是否正常!',
})
}
} catch (error) {
throw new Error(error)
}
let msg = `服务器发生异常!`
// 这是处理http状态码发生异常时的可能,使用服务器返回的msg提示,但状态取决于http状态码
if (
response &&
response.data &&
isObject(response.data) && // 假如data是个一般对象,说明可以判断
response.data.code === ResultEnum.ERROR
) {
msg = response.data.msg
checkStatus(error.response && error.response.status, msg)
} else if (response && response.data && isBlob(response.data)) {
const text = new FileReader()
text.readAsText(response.data)
text.onload = function () { // 本质还是解析再进行处理
const obj = JSON.parse(text.result as string)
msg = obj.code === ResultEnum.ERROR ? obj.msg : '服务器发生异常!'
checkStatus(error.response && error.response.status, msg)
}
} else {
// 使用默认的message提示
checkStatus(error.response && error.response.status, msg)
}
return Promise.reject(error)
},
}
function createAxios(opt?: Partial) { // 把CreateAxiosOptions 中的每个属性变为可选的
return new VAxios( // 这个http会返回一个Vaxios的实例对象 在不传任何参数的情况下 默认传递以下的参数
deepMerge(
{
timeout: 6 * 10 * 1000,
headers: { 'Content-type': ContentTypeEnum.JSON },
transform,
requestOptions: {
isTransformRequestResult: true, //是否转换结果
joinParamsToUrl: false, //是否将参数添加到url
formatDate: true, //是否格式化时间参数
errorMessageMode: 'none', //错误消息的提示模式
apiUrl: process.env.VUE_APP_API_URL, //api前缀
},
},
opt || {}
)
)
}
export const http = createAxios()
我大概总结一下这个index到底做了什么
1.导出了一个函数的调用,实际上是导出了一个VAxios的实例对象,并且 默认情况下传递了一大堆的参数,参数就是这些东西
{
timeout: 6 * 10 * 1000,
headers: { 'Content-type': ContentTypeEnum.JSON },
transform,
requestOptions: {
isTransformRequestResult: true, //是否转换结果
joinParamsToUrl: false, //是否将参数添加到url
formatDate: true, //是否格式化时间参数
errorMessageMode: 'none', //错误消息的提示模式
apiUrl: process.env.VUE_APP_API_URL, //api前缀
},
}
后面它会根据这些参数的值,来决定如何处理数据,我们最重要还是要看一下,transform的内容,这个是重点。
2.tansform 这个是一个对象,里面实际上包含了多个钩子函数,请求前处理数据的 beforeRequestHook,请求拦截器requestInterceptors添加token,请求成功后的transformRequestData,也就是对于响应成功回来的时候如何返回数据,因为有的时候我们需要返回整个response,比如下载文件的时候,有的时候只需要返回response.data就好了,responseInterceptorsCatch ,当http状态码非200时的错误处理 。通俗来讲,可以理解为,transform对象中就是一堆钩子函数,用于处理数据。或者根据数据情况来做不同逻辑的操作。
3.最终实际上这个时候我们就知道要去看看VAxios到底是个什么东西了,也就是最核心的文件。
一起来看一下!!!!
二、Axios.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import {
CreateAxiosOptions,
UploadFileParams,
RequestOptions,
Result,
} from './types'
import { isFunction } from '@/utils/is'
import { ContentTypeEnum } from '../../../enums/httpEnum'
import { cloneDeep } from 'lodash-es'
import { errorResult } from './const'
import { AxiosCanceler } from './axiosCancel'
export default class VAxios {
private axiosInstance: AxiosInstance // axios实例本身
private readonly options: CreateAxiosOptions // 传递进来的 options
constructor(options: CreateAxiosOptions) {
this.options = options
this.axiosInstance = axios.create(options)
this.setupInterceptors() // 将当前实例添加响应拦截
}
private createAxios(config: CreateAxiosOptions): void { // 更新实例
this.axiosInstance = axios.create(config)
}
getAxios(): AxiosInstance {
return this.axiosInstance
}
//重新配置axios
configAxios(config: CreateAxiosOptions) {
if (!this.axiosInstance) {
return
}
this.createAxios(config)
}
setHeader(headers: any): void {
if (!this.axiosInstance) return
Object.assign(this.axiosInstance.defaults.headers, headers)
}
private getTransform() {
const { transform } = this.options
return transform
}
/**
* 拦截器配置
*/
private setupInterceptors() {
const transform = this.getTransform()
if (!transform) return
const {
requestInterceptors,
requestInterceptorsCatch,
responseInterceptors,
responseInterceptorsCatch,
} = transform
const axiosCanceler = new AxiosCanceler()
//请求拦截器
this.axiosInstance.interceptors.request.use(
// 本项目中这里实际上是在添加token
(config: AxiosRequestConfig) => {
const {
headers: { ignoreCancelToken } = { ignoreCancelToken: false },
} = config
!ignoreCancelToken && axiosCanceler.addPending(config)
if (requestInterceptors && isFunction(requestInterceptors)) {
config = requestInterceptors(config)
}
return config
},
undefined
)
//请求拦截器错误捕获
requestInterceptorsCatch &&
isFunction(requestInterceptorsCatch) &&
// 本项目中这里其实没有任何处理
this.axiosInstance.interceptors.request.use(
undefined,
requestInterceptorsCatch
)
//响应拦截器
// 本项目中没啥用
this.axiosInstance.interceptors.response.use((res: AxiosResponse) => {
res && axiosCanceler.removePending(res.config)
if (responseInterceptors && isFunction(responseInterceptors)) {
res = responseInterceptors(res)
}
return res
}, undefined)
//响应拦截器错误捕获
// http状态码为非200时的错误捕获
responseInterceptorsCatch &&
isFunction(responseInterceptorsCatch) &&
this.axiosInstance.interceptors.response.use(
undefined,
responseInterceptorsCatch
)
}
//文件上传
uploadFile(config: AxiosRequestConfig, params: UploadFileParams) {
const formData = new window.FormData()
const { useBean = true } = params
if (useBean && params.data) {
formData.append('bean', JSON.stringify(params.data))
} else if (!useBean && params.data) {
Object.keys(params.data).forEach((key) => {
if (!params.data) return
const value = params.data[key]
if (Array.isArray(value)) {
value.forEach((item) => {
formData.append(`${key}[]`, item)
})
return
}
formData.append(key, params.data[key])
})
}
params.file =
Object.prototype.toString.call(params.file) === '[object File]'
? params.file
: ''
formData.append(params.name || 'file', params.file)
const { requestOptions } = this.options
if (requestOptions?.apiUrl) {
config.url = requestOptions.apiUrl + config.url
}
const opt: RequestOptions = Object.assign({}, requestOptions)
const transform = this.getTransform()
const { requestCatch, transformRequestData } = transform || {}
return new Promise((resolve, reject) => {
this.axiosInstance
.request>({
...config,
data: formData,
headers: {
'Content-type': ContentTypeEnum.FORM_DATA,
},
})
.then((res: AxiosResponse) => {
if (transformRequestData && isFunction(transformRequestData)) {
const ret = transformRequestData(res, opt)
ret !== errorResult ? resolve(ret) : reject(new Error(ret))
return
}
resolve((res as unknown) as Promise)
})
.catch((e: Error) => {
if (requestCatch && isFunction(requestCatch)) {
reject(requestCatch(e))
return
}
reject(e)
})
})
}
request(
config: AxiosRequestConfig,
options?: RequestOptions
): Promise {
let conf: AxiosRequestConfig = cloneDeep(config)
const transform = this.getTransform()
const { requestOptions } = this.options
const opt: RequestOptions = Object.assign({}, requestOptions, options)
const { beforeRequestHook, requestCatch, transformRequestData } =
transform || {}
if (beforeRequestHook && isFunction(beforeRequestHook)) {
conf = beforeRequestHook(conf, opt) // conf 就是真正发送请求的url对象 || opt 就是原始对象和传递进来的对象的合集
}
return new Promise((resolve, reject) => {
this.axiosInstance
.request>(conf)
.then((res: AxiosResponse) => {
if (transformRequestData && isFunction(transformRequestData)) {
const ret = transformRequestData(res, opt)
ret !== errorResult ? resolve(ret) : reject(new Error(ret))
return
}
resolve((res as unknown) as Promise)
})
.catch((e: Error) => {
if (requestCatch && isFunction(requestCatch)) {
reject(requestCatch(e))
return
}
reject(e)
})
})
}
}
我也大概来总结一下这到底做了什么事情
1.一句话总结,首先定义了一个类VAxios , 这个类拥有一些私有属性,一些方法,结束。
2.这些属性分别是 基本配置项,也就是上面我提到的那个Options, 第二个属性就是根据这些配置项创建的axios实例对象,所有的请求发送,实际上都是用这个实例对象来进行发送的。
3.有一部分方法基本不用看,比如获取当前实例,改变options配置,基本用不到的,也很简单,最终要就是那个setInterceptor方法,request方法和upload方法要看一下。其中最核心的就是之前在transform中配置的那些方法,都会一一运用在这个axios发送请求的这一套流程之中。因此我们可以将其理解为扩展了一些钩子函数。
三、 总结
这个axios封装的思想我总结下来就是它暴露出去的东西是比较简单的,因此使用起来很好用,将上传都做了一个封装,而且扩展了多个钩子,这个目录结构就比较请求,维护起来也就比较清晰方便了,当然前提是要看懂,总之一起加油。