axios + ts + class的二次封装(全面封装)
每天对自己的代码多问几个为什么,总是有着想象不到的收获。一个菜鸟小白的成长之路(copyer)1、理解promise的类型限定Promise本身是可以有类型的, 对resolve,reject参数类型限定new Promise<string>((resolve, reject) => {// resolve(321)报错: number类型resolve('james')//字
每天对自己多问几个为什么,总是有着想象不到的收获。 一个菜鸟小白的成长之路(copyer)
1、理解promise的类型限定
Promise本身是可以有类型的, 对resolve,reject参数类型限定
new Promise<string>((resolve, reject) => {
// resolve(321) 报错: number类型
resolve('james') //字符串类型
}).then(res => {
console.log(res) //这里的res就是字符串的形式
})
2、理解axios提供的几种类型
以get为例
get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>;
一:参数
get()
是一个函数,接收两个参数,返回一个Promise对象
- url: 字符串: 路径地址
- config: 配置对象,类型为
AxiosRequestConfig
, 看下面
二:返回值的类型
上面解释了Promise的对象,本身是有类型的,类型为 R
R是卅呢? 泛型 R = AxiosResponse<T>
, 泛型接口, AxiosResponse
,看下面
T
也是泛型, 看了AxiosResponse的接口之后,很明显知道 T 就是data的类型
1、对象配置的类型
export interface AxiosRequestConfig {
url?: string;
method?: Method;
baseURL?: string;
headers?: any;
params?: any;
data?: any;
timeout?: number;
withCredentials?: boolean;
responseType?: ResponseType;
...
}
2、axios的返回值类型
export interface AxiosResponse<T = any> {
data: T;
status: number;
statusText: string;
headers: any;
config: AxiosRequestConfig;
request?: any;
}
3、实例
//http.get()返回一个promise对象, promise返回值的类型为 string[]
http.get<string[]>('http://httpbin.org/get').then(res => {
// res.data: string[]
})
3、class + ts二次封装axios
在这里主要是使用ES6提供的类来对axios进行封装,在使用的时候,通过实例来进行使用。
1、封装基本类(初级)
封装类:
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
class Http {
public instance: AxiosInstance
constructor(config: AxiosRequestConfig) {
this.instance = axios.create(config)
}
}
export default Http
可以根据类来进行创建实例:
import Http from './service/index'
//配置基本路径和请求时间 (当然这里也可以写在配置文件)
const BASE_URL = 'http://127.0.0.1'
const TIME_OUT = 1000
const http = new Http({
baseURL: BASE_URL,
timeout: TIME_OUT
})
export default http
2、拦截器配置(中级)
拦截器主要分为以下三种的形式,但是主要的形式,第一种和第三种是不常见的(了解即可),最主要的还是同统一的拦截器(掌握)
针对不同实例之间的进行不同的拦截(了解)
对所有实例进行统一拦截(掌握)
针对不同接口进行不同的拦截(了解)
尽管上面三种情况,两种需要了解,但是还是需要理解他的封装原理
先对ts接口进行实现(接下来会使用到)
//自定义拦截器的类型限定(针对于第一种情况和第三种情况)
interface customInterceptorType {
//请求拦截(接口请求成功)
requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
requestInterceptorCatch?: (error: any) => any //请求拦截(捕获接口失败)
resInterceptor?: (res: AxiosResponse) => AxiosResponse //响应拦截(接口响应成功)
resInterceptorCatch?: (error: any) => any //响应拦截(捕获响应失败)
}
//自定义的axios的config的配置(使用接口继承)
interface customRequest extends AxiosRequestConfig {
interceptor?: customInterceptorType //是否传入拦截器
}
对axios中的config的类型,进行重写
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
class Http {
public instance: AxiosInstance
constructor(config: customRequest) { //使用自定义的axios的config的配置
this.instance = axios.create(config)
}
}
export default Http
2.1、不同实例之间的拦截器
不同的实例,其实就是存在两个项目之间。(主要是为了让封装更加的具有扩展性)
使用class封装的axios,通过new关键字进行实例化,来进行使用的。那么创建实例就会存在多个实例,那么他们之间的拦截可能是想要不一样的功能。
样例:
假如存在两个实例, http1
和 http2
, http1
发送请求之前,在url前面加上专属的字段,如:/http1/getSearchData
; http2
在发送之前修改url加上专属的字段,如: http2/getSearchData
, 当然这种情况很少见,这里只是简单的举个例子
类实例初始化:
import Http from './request/request'
const http1 = new Http({
baseURL: BASE_URL,
timeout: TIME_OUT,
interceptor: {
requestInterceptor: (config) => {
config.url = '/http1' + config.url
return config
},
//其他的三种拦截器可选
}
})
const http2 = new Http({
baseURL: BASE_URL,
timeout: TIME_OUT,
interceptor: {
requestInterceptor: (config) => {
config.url = '/http2' + config.url
return config
},
//其他的三种拦截器可选
}
})
在上面的代码中,在初始化的时候, 就传入了一个请求拦截器,对url字符串的修改,这样就达到了,对不同的实例之间的不同拦截。
类中处理实例传入的拦截器
class Http {
public instance: AxiosInstance
public interceptor?: customInterceptorType
constructor(config: customRequest) {
this.instance = axios.create(config)
this.interceptor = config.interceptor
//如果存在this.interceptor, 就处理实例中传递过来的拦截器
this.instance.interceptor.request.use(
this.interceptor?.requestInterceptor,
this.interceptor?.requestInterceptorCatch
)
this.instance.interceptors.response.use(
this.interceptor?.resInterceptor,
this.interceptor?.resInterceptorCatch
)
}
}
export default Http
上面代码,就单独处理实例传递过来的拦截器,注入。
2.2 、对所有实例进行统一拦截
这个是非常常用的拦截器,这里可以大部分想要的实现功能拦截,所以我认为这是必须要掌握的。比如:
- 在请求中,对token的拦截
- 在请求中,对loading图标的显示
- 在响应中,对数据的处理
当然,还有很多的情况处理。这样的代码逻辑,也是比较好处理的
class Http {
public instance: AxiosInstance
...
this.instance.interceptors.request.use(
(config) => {
//请求拦截:对token的拦截
const token = localStorage.getItem('token')
token && config.headers.common['token'] = token
return config
},
(error) => {
return error
}
)
this.instance.interceptors.response.use(
(res) => {
//响应拦截: 接口返回的数据处理
return res.data
},
(error) => {
return error
}
)
}
2.3、针对不同接口进行不同的拦截
在某些场景下,需要对个别接口进行单独的拦截处理。那么在封装类中,我们就需要单独对这种拦截器处理。
样例:
针对大部分的接口,请求超时的时间时统一的,但是对于处于复杂的接口,请求的时间肯定会长一些。比如登陆接口
,如果时间超出2s,那么就说明请求接口失败。但是如果针对于人脸识别的接口
,如果时间在1分钟左右,我们也是可以接受的。所以这种情况下,就需要对接口拦截器进行处理。
类中处理接口拦截器
class Http {
request(config: customRequest): any {
if (config.interceptor?.requestInterceptor) {
//调用接口传递过来的拦截器函数,接收最新的config
config = config.interceptor.requestInterceptor(config)
}
this.instance.request(config).then((res) => {
if (config.interceptor?.resInterceptor) {
config = config.interceptor.resInterceptor(res)
}
})
}
}
接口的使用拦截器:
import http from './service/index'
//登陆接口
http.request({
url: '/login',
method: 'POST',
interceptor: {
requestInterceptor: config => {
config.timeout = 2000
return config
},
//其他三种也可以写的
}
})
//人脸识别接口
http.request({
url: '/recognitionFace
method: 'POST',
interceptor: {
requestInterceptor: config => {
config.timeout = 60000
return config
},
//其他三种也可以写的
}
})
这里就单独对接口的超时时间进行了处理,当然还有其他的应用场景。
在上面的类中处理接口拦截器这个代码中,有个bug,就会类型会报错,因为我们一般会在统一接口中,在响应拦截的时候,就返回了res.data
,这个类型我们传入过去的promise类型了,但是在resInterceptor
这个接口函数中类型为:AxiosResponse
,所以要想处理这个问题(就简单粗暴解决吧),改为any类型
interface customInterceptorType {
//请求拦截(接口请求成功)
requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
requestInterceptorCatch?: (error: any) => any //请求拦截(捕获接口失败)
resInterceptor?: (res: any) => any //响应拦截(接口响应成功)
resInterceptorCatch?: (error: any) => any //响应拦截(捕获响应失败)
}
小节总结:上面的拦截情况分类三种,但是第一种和第三种,我们只需要了解,然后扩宽我们的封装思路,但是第二种的统一处理,是必须要掌握,使用起来也很简单。
3、封装loading图标
在请求接口的时候,想要用户体验感好的话,就需要给用户的提示,所以需要加上一个loading图标来提示用户,说明接口真正请求中。
这里我主要使用element-plus中的加载动画。
实现的效果:
http.request({
url: '/get',
method: 'GET'
}).then((res:any) => {
console.log(res);
})
//默认情况下,就是有loading图标
http.request({
url: '/get',
method: 'GET',
showLoading: false
}).then((res:any) => {
console.log(res);
})
//设置showLoading为false, 就不展示loading图标
封装代码如下:
类型接口:
interface customRequest extends AxiosRequestConfig {
interceptor?: customInterceptorType,
showLoading?: boolean //config配置文件的类型上,添加一个showLoading属性,展示是否需要展示
}
//导入ElLoading的动画组件
import { ElLoading } from 'element-plus'
//导入ElLoading中的service返回值的类型
import { ILoadingInstance } from 'element-plus/lib/el-loading/src/loading.type'
const DEFAULT_LOADING = true //loading 的默认值
class Http {
protected instance: AxiosInstance
interceptor?: customInterceptorType
loading?: ILoadingInstance //定义loading
showLoading?: boolean //是否显示loading
constructor(config: customRequest) {
this.instance = axios.create(config)
this.interceptor = config?.interceptor
//初始化默认值,如果config里面有,就使用config里面的,但是如果没有就使用默认值
this.showLoading = config.showLoading || DEFAULT_LOADING
this.instance.interceptors.request.use(
(config) => {
//loading图标加载
if(this.showLoading) { //当为true的时候,显示loading图标
this.loading = ElLoading.service({
lock: true,
text: '正在加载中...'
})
}
return config
},
(error) => {
return error
}
)
this.instance.interceptors.response.use(
(res) => {
//当响应成功的时候,关闭loading
setTimeout(() => {
this.loading?.close()
}, 1000)
return res.data
},
(error) => {
//当响应失败的时候,关闭loading
this.loading?.close()
return error
}
)
}
//request方法
request(config: customRequest): any {
return new Promise((resolve, reject) => {
//单独处理接口配置传递showLoading, 为false就是显示图标
if(config.showLoading === false) {
this.showLoading = false
}
//调用接口之后,无论成功还是失败,都需要把实例的showLoading改为true
this.instance.request(config).then((res) => {
resolve(res)
//为什么要true, 是为了不影响下一次的接口的loading加载动画
this.showLoading = DEFAULT_LOADING
}).cathc((err) => {
this.showLoading = DEFAULT_LOADING
return err
})
})
}
}
export default Http
4、暴露请求的方法
对request方法详细说明
request<T>(config: customRequest): Promise<T> {
return new Promise((resolve) => {
//这里的instance中的request,看源码知道,这里接收两个泛型,第二个泛型才是Promise返回的对象
this.instance.request<any, T>(config).then((res) => {
resolve(res) //所以这里res的类型就为 T 了
})
})
}
在上面的代码中,request接收一个泛型,该方法返回一个Promise
对象,这个泛型就是对象Promise的类型设定,对最后的返回执行进行设置。
对其他的方法封装:
get<T>(config: customRequest): Promise<T>{
return this.request({...config, method: 'GET'})
}
post<T>(config: customRequest): Promise<T>{
return this.request({...config, method: 'POST'})
}
使用封装的方法时
interface IResType {
data: any,
status: number,
success: boolean
}
http.get<IResType>({
url: '/get'
}).then(res => {
console.log(res);
})
在上面的代码中,data为any
,但是我们想对data中的返回类型进行限定,那么该怎么处理呢?
interface IResType<T> {
data: T,
status: number,
success: boolean
}
http.request<IResType<any>>({
method: 'GET',
url: '/get'
}).then(res => {
console.log(res);
})
当然,要明确知道data的类型, 必须要跟后端统一,不然一方改变就会报错的,接口类型行不通
4、完整的Http类的代码封装
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
import { ElLoading } from 'element-plus'
import { ILoadingInstance } from 'element-plus/lib/el-loading/src/loading.type'
//自定义拦截器类型
interface customInterceptorType {
requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
requestInterceptorCatch?: (error: any) => any
resInterceptor?: (res: any) => any
resInterceptorCatch?: (error: any) => any
}
//定义自己的实例类型
interface customRequest extends AxiosRequestConfig {
interceptor?: customInterceptorType,
showLoading?: boolean
}
const DEFAULT_LOADING = true //loading 的默认值
class Http {
protected instance: AxiosInstance
interceptor?: customInterceptorType
loading?: ILoadingInstance
showLoading: boolean
constructor(config: customRequest) {
this.instance = axios.create(config)
this.interceptor = config?.interceptor
this.showLoading = config.showLoading || DEFAULT_LOADING
//从config里面取出每个实例的不同拦截器 (可以删除,如果没有单独实例处理)
this.instance.interceptors.request.use(
this.interceptor?.requestInterceptor,
this.interceptor?.requestInterceptorCatch
)
this.instance.interceptors.response.use(
this.interceptor?.resInterceptor,
this.interceptor?.resInterceptorCatch
)
//统一,所以实例都会拦截
this.instance.interceptors.request.use(
(config) => {
//loading图标加载
if(this.showLoading) {
this.loading = ElLoading.service({
lock: true,
text: '正在加载中...'
})
}
//token拦截
const token = localStorage.getItem('token')
if(token) {
config.headers.Authorization = token
}
return config
},
(error) => {
return error
}
)
this.instance.interceptors.response.use(
(res) => {
//当响应成功的时候,关闭loading
setTimeout(() => {
this.loading?.close()
}, 1000)
return res.data
},
(error) => {
//当响应失败的时候,关闭loading
this.loading?.close()
return error
}
)
}
// request<T>(params: customRequest): Promise<T>
// request<T>(...args: any[]): Promise<T>
//对部分接口的进行拦截配置
request<T>(params: customRequest): Promise<T> {
let config:customRequest
if(typeof params === 'string') { //字符串
// config = arguments[1] || {}
// config.url = arguments[0] //处理url
// if(arguments[2]) {
// config.showLoading = arguments[2] || {} //处理showLoading
// }
} else { //对象
config = params || {}
}
return new Promise((resolve) => {
if (config.interceptor?.requestInterceptor) { //可以删除,没有接口拦截器
config = config.interceptor.requestInterceptor(config)
}
if(config.showLoading === false) {
this.showLoading = false
}
this.instance.request<any, T>(config).then((res) => {
if (config.interceptor?.resInterceptor) { //可以删除,没有接口拦截器
config = config.interceptor.resInterceptor(res)
}
resolve(res)
this.showLoading = DEFAULT_LOADING
})
})
}
get<T>(config: customRequest): Promise<T>{
return this.request({...config, method: 'GET'})
}
post<T>(config: customRequest): Promise<T>{
return this.request({...config, method: 'POST'})
}
}
export default Http
这里的代码唯一美中不足的就是,get等方法还不支持传递字符串形式,只能以对象的形式作为参数。因为目前我不知道怎么在ts使用剩余运算符,直接使用arguments,好像直接报错;第二次测试,打算用函数的重载,但是好像也是需要使用剩余参数,还没解决出来。如果以后有思路了,再来进行补充(脚步是一个一个的走的)。
5、总结
在这次二次封装axios的过程中,用到了太多的ts的类型限定了,各种泛型的使用,对Promise的类型全面认识,大大的加强了我对ts的使用,对ts的安全检测类型有着全面的认识;而且这次还加强了自己的封装能力,让自己封装的函数,更加的具有扩展性,安全性,感觉这次真的收获巨大。
如果其中有什么不对地方,可以提出来,虚心接受(一位菜鸟小白的成长之路)。
最后想说,站在大佬(coderwhy老师)的肩膀上学习,一切不可能就能变为可能。
更多推荐
所有评论(0)