前言
最近学习了vue3又学习了ts,学完准备搭建一个项目练练手时。突然想起现在封装的网络请求貌似是vue2和原生js以及axios结合使用的,所以当我们使用vue3和ts进行开发的时候那肯定也得根据当前的技术来重新封装一个结构性更强的基于axios的网络请求体系。封装好了我们以后搭建项目可以说直接采用就好了,学习此文能让我们体会ts在封装网络请求时所给予更加完善的安全性,让我们来看看ts的独特魅力吧。
结构预览及说明
- src下新建service文件夹为主文件夹
- request为封装请求的文件夹
- config.ts 为一些请求的固定配置及不同的开发环境中的值
- errCode.ts 为错误请求码的对应状态信息
- index.ts 为主要构造请求类的关键配置
- type.ts 为导出需要用到的一些自定义接口的类型
- service.ts中的index.ts 则是实例化构造类生成真正的网络请求配置并返回
现在我们结构已经了解清楚了,让我开始搭建吧!
构造类 ZJRequest
我们主要的工作都是在构造类ZJRequest中进行配置,配置好后,只需要实例化ZJRequest类并传入对应配置即可得到一个完整的网络请求配置对象。这里的命名推荐大家使用自己独有的昵称加Request来进行命名,这也是一种开发习惯。 我这边搭建的网络请求配置包含了拦截器完整配置及响应遮罩完整配置。 先看一看最标准的基本配置吧。
import type { AxiosInstance,AxiosRequestConfig } from 'axios'
import axios from 'axios'
class ZJRequest {
instance: AxiosInstance
constructor(config: AxiosRequestConfig) {
this.instance = axios.create(config) //根据传入配置手动创建实例
}
先简单说下上面的类型问题吧,构造ZJRequest类其中的instance或是传入的参数config现在我们使用ts肯定是需要对其进行类型规范的,而他们的类型其实在axios是有给出的,我们只需要引用即可。
很简洁的一段代码只需要根据传入的配置调用axios创建新的Instance实例就可以得到基本的网络配置了,显然我们并不需要这么简单的封装,这样我们的封装变得意义不大,所以我们也知道拦截器和响应遮罩才是重点封装对象,下面让我们来看看这是如何实现的吧。
拦截器实现解读
先贴一份完整的请求代码吧,方便我们进行讲解它的实现过程。
import type { AxiosInstance } from 'axios'
import type { KZJRequestInterceptors, KZJRequestConfig } from './type'
import { getErrMessage } from './errCode'
import axios from 'axios'
import { ElLoading } from 'element-plus'
import type { ILoadingInstance } from 'element-plus/lib/el-loading/src/loading.type'
const DEFAULT_LOADING = true
class ZJRequest {
instance: AxiosInstance
interceptors?: KZJRequestInterceptors
showLoading: boolean
loading?: ILoadingInstance
constructor(config: KZJRequestConfig) {
//KZJRequestConfig继承自AxiosRequestConfig并进行了拦截器接口扩展
this.instance = axios.create(config) //根据传入配置手动创建实例
this.interceptors = config.interceptors //添加拦截器
this.showLoading = config.showLoading ?? DEFAULT_LOADING //控制loading显示
// 使用拦截器
// 1.从config中取出的拦截器是对应的实例的拦截器
this.instance.interceptors.request.use(
this.interceptors?.requestInterceptor, //请求成功拦截
this.interceptors?.requestInterceptorCatch //请求失败拦截
)
this.instance.interceptors.response.use(
this.interceptors?.responseInterceptor, //响应成功拦截
this.interceptors?.responseInterceptorCatch //响应失败拦截
)
//全局请求拦截器
this.instance.interceptors.request.use(
(res) => {
console.log('全局拦截请求成功')
if (this.showLoading) {
this.loading = ElLoading.service({
lock: true,
text: '正在加载中...'
})
}
return res
},
(err) => {
return err
}
)
//全局响应拦截器
this.instance.interceptors.response.use(
(res) => {
console.log('全局拦截响应成功')
//移除Loading
setTimeout(() => {
this.loading?.close()
}, 2000)
return res.data
},
(err) => {
//移除Loading
this.loading?.close()
//错误状态码解析信息
err = getErrMessage(err)
return err
}
)
}
request<T>(config: KZJRequestConfig<T>): Promise<T> {
return new Promise((resolve, reject) => {
if (config.interceptors?.requestInterceptor) {
//判断单独请求是否存在请求成功拦截 是则执行其拦截并返回请求接口的配置信息
// 返回 config/res 的目的是拦截器中可能会对其改变所以需要拿到最新的结果返回过来
config = config.interceptors.requestInterceptor(config)
//判断单独请求是否需要显示loading 与配置不一致时才修改
if (config.showLoading === !this.showLoading) {
this.showLoading = config.showLoading
}
}
//这里以及上面的request传入泛型T的目的是返回的结果的类型可以由发送时定义
//这时默认下面的responseInterceptor(res)中的res是AxiosResponse
//但是我们想用我们发送定义的接口类型这时候就需要传入泛型来解决
this.instance.request<any, T>(config).then(
(res) => {
if (config.interceptors?.responseInterceptor) {
//判断单独请求是否存在响应成功拦截 是则执行其拦截并返回响应成功的数据
res = config.interceptors.responseInterceptor(res)
//无论响应是否成功失败都需要初始化showliadong的值以便响应下次请求的配置
this.showLoading = DEFAULT_LOADING
}
resolve(res)
},
(err) => {
this.showLoading = DEFAULT_LOADING
reject(err)
return err
}
)
})
}
get<T>(config: KZJRequestConfig<T>): Promise<T> {
return this.request({ ...config, method: 'GET' })
}
post<T>(config: KZJRequestConfig<T>): Promise<T> {
return this.request({ ...config, method: 'POST' })
}
delete<T>(config: KZJRequestConfig<T>): Promise<T> {
return this.request({ ...config, method: 'DELETE' })
}
patch<T>(config: KZJRequestConfig<T>): Promise<T> {
return this.request({ ...config, method: 'PATCH' })
}
}
export default ZJRequest
我们从头开始,先谈一谈引用type文件夹中的接口类型: type.ts
import type { AxiosResponse, AxiosRequestConfig } from 'axios'
//定义拦截器接口类型
//这里的responseInterceptor类型用泛型的目的是可能后面我们会采用自己定义的接口类型
export interface KZJRequestInterceptors<T = AxiosResponse> {
requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
requestInterceptorCatch?: (error: any) => any
responseInterceptor?: (res: T) => T
responseInterceptorCatch?: (error: any) => any
}
//扩展AxiosRequestConfig类型加上自己写的拦截器接口
export interface KZJRequestConfig<T = AxiosResponse>
extends AxiosRequestConfig {
interceptors?: KZJRequestInterceptors<T>
showLoading?: boolean
}
这里我们可以看到我们定义了两个接口,一个是拦截器的体系接口,一个是扩展AxiosRequestConfig类型。而且,在下面这个图中我们也运用上去了,这个扩展AxiosRequestConfig类型就是专门为config而配置的。为什么要进行扩展呢?主要是我们想加入自己定义的拦截器,所以需要在原本的AxiosRequestConfig类型上进行扩展。
添加拦截器后我们就可以使用其中有的拦截器并将其配置到请求对象中。值得一提的是,这里的config是指实例化该类时传入的配置,所以这里定义的拦截器将会在该实例化化对象中使用起来。换句话说,当我们实例化多个请求对象时,每个传入的配置不同时也会生成不同的拦截器。有的对象内包含全部的拦截器,而有的可能只有请求成功拦截的拦截器等。所以在我们实例化对象的时候就可以确定并配置想要的拦截器,这就是是第一种拦截器 默认拦截器。
也有这样的一种情况,不管我们实例化多少个对象,我们都想他们都共有一个或多个拦截器。也就是我们要讲的第二种拦截器 全局拦截器。全局拦截器最主要的用处之一就是配合来实现我们后面的响应遮罩,因为每一次发送请求前我们需要进行拦截,而当响应成功后我们也需要进行拦截。这样的话我们最好在全局拦截器中进行配置来实现该功能。
还有第三种拦截器 单个请求拦截器。有时候我们可能只需要对某个接口进行单独的拦截器处理,这时候我们用上面的两种显然都不合适,所以我们采用单独的单个请求拦截器。这里需要注意的是这里的config是request请求方法时传入的配置和默认实例化的config不一样,注意不要搞混了。
第一种拦截器用法:直接在实例化对象时配置上去即可
第二种拦截器就不用多说了,直接在全局拦截器配置即可 第三种拦截器用法:在传入的请求中添加interceptors属性并配置
让我们再来探讨一下这三种拦截器的执行顺序吧,首先我们都启用这三种拦截器,让我们来看看结果如何。
由此可得出我们拦截器的顺序是
第三种单独接口请求成功拦截
第二种全局接口请求成功拦截
第一种默认配置请求成功拦截
第一种默认配置响应成功拦截
第二种全局接口响应成功拦截
第三种单独接口响应成功拦截
响应遮罩动画实现解读
showLoading的用处是控制是否需要响应遮罩动画 loading则是loading组件创建的实例对象,我们可以调用其中的close来进行关闭遮罩 我们先根据实例化对象时传入的showloading属性的值来判断再合理的修改类中showloading的值,再来决定要不要显示响应遮罩。这里有一个优先级,因为我们上面探讨了拦截器的执行顺序,所以由此而来我们得出。优先单独接口配置中的showLoading,然后是实例化对象中的showLoading,最后两者都没有配置则采用默认的DEFAULT_LOADING。 这里可能部分同学会有疑问,为什么单独接口最先执行还是采用它的showLoading呢?不应该会被后面的覆盖吗? 首先大家要搞清楚逻辑关系,下面这段代码是关键。它是什么时候执行的呢?是实例化的时候,它就会根据实例化时的配置来决定showLoading的值,如果没有配置则采用默认的DEFAULT_LOADING。这个时候都还没有启用,那肯定是他们俩是最先决定showLoading的值,但是如果发送请求成功后启用拦截器的时候若是单独接口中有配置showLoading,那毫无疑问showLoading肯定是会覆盖前面showLoading的值,所以说单独接口的优先决定。 this.showLoading = config.showLoading ?? DEFAULT_LOADING //控制loading显示
然后在全局拦截器中,请求成功且showLoading为真时启用响应遮罩动画,等请求响应无论成功失败时我们都要取消遮罩动画。这里的定时器是方便测试,毕竟接口的请求速度过快看不到效果。
如果单独请求配置中有showLoading的值且和当前类中的showLoading的值不一致时我们优先采用单独接口配置的showLoading,在我们响应完成后无论成功或者是失败我们都需要把它还原成最开始的默认值DEFAULT_LOADING。 以防下个接口没有值而采用上次的值造成错误,若是有值则依然会执行再覆盖。
完善接口类型
get<T>(config: KZJRequestConfig<T>): Promise<T> {
return this.request({ ...config, method: 'GET' })
}
post<T>(config: KZJRequestConfig<T>): Promise<T> {
return this.request({ ...config, method: 'POST' })
}
delete<T>(config: KZJRequestConfig<T>): Promise<T> {
return this.request({ ...config, method: 'DELETE' })
}
patch<T>(config: KZJRequestConfig<T>): Promise<T> {
return this.request({ ...config, method: 'PATCH' })
}
这就不多说了,相当于复用上面的request方法,只是换了个名字及修改了method请求方式
其他文件
config.ts
let BASE_URL = ''
const TIME_OUT = 10000
if (process.env.NODE_ENV === 'development') {
//开发模式 进行了跨域配置参看vue.config.js
BASE_URL = '/api'
} else if (process.env.NODE_ENV === 'production') {
//生产模式
BASE_URL = 'http://kzj.org/prod'
} else {
//测试环境
BASE_URL = 'http://kzj.org/test'
}
export { BASE_URL, TIME_OUT }
errCode.ts
export function getErrMessage(err: any): void {
switch (err.response.status) {
case 400:
err.message = '请求错误'
break
case 401:
err.message = '未授权,请登录'
break
case 403:
err.message = '拒绝访问'
break
case 404:
err.message = `请求地址出错: ${err.response.config.url}`
break
case 408:
err.message = '请求超时'
break
case 500:
err.message = '服务器内部错误'
break
case 501:
err.message = '服务未实现'
break
case 502:
err.message = '网关错误'
break
case 503:
err.message = '服务不可用'
break
case 504:
err.message = '网关超时'
break
case 505:
err.message = 'HTTP版本不受支持'
break
default:
}
return err
}
service下的 index.ts
import ZJRequest from './request'
import { BASE_URL, TIME_OUT } from './request/config'
import localCache from '@/utils/cache'
const zjRequest = new ZJRequest({
baseURL: BASE_URL,
timeout: TIME_OUT,
showLoading: false, //默认接口没有Loading动画效果
interceptors: {
//请求成功的拦截
requestInterceptor: (config) => {
//携带token拦截
console.log('默认配置的请求成功拦截')
const token = localCache.getCache('token') ?? ''
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
//请求失败的拦截
requestInterceptorCatch: (err) => {
return err
},
//响应成功的拦截
responseInterceptor: (res) => {
console.log('默认配置的相应成功拦截')
return res
},
//响应失败的拦截
responseInterceptorCatch: (err) => {
return err
}
}
})
export default zjRequest
开始使用
这里我们用登录接口来示范下使用
type.ts
export interface TAccount {
name: string
password: string
}
export interface ILoginResult {
id: number
name: string
token: string
}
export interface IDataType<T = any> {
code: number
data: T
}
login.ts
import KZJRequest from '../index'
import { TAccount, ILoginResult, IDataType } from './type'
enum LoginApi {
AccountLogin = '/login',
LoginUserInfo = '/users/', // 用法: /users/1
UserMenus = '/role/' // 用法: role/1/menu
}
export function accountLogin(account: TAccount) {
return KZJRequest.post<IDataType<ILoginResult>>({
url: LoginApi.AccountLogin,
data: account,
showLoading: true
})
}
export function requestUserInfoById(id: number) {
return KZJRequest.get<IDataType>({
url: LoginApi.LoginUserInfo + id
})
}
export function requestUserMenusByRoleId(id: number) {
return KZJRequest.get<IDataType>({
url: LoginApi.UserMenus + id + '/menu'
})
}
定义好后,直接引入对应接口即可
结语
希望对大家有帮助,需要文件的可以私我。创作不易,望大家能点个赞,万分感谢!今天的分享就到这里了,我们下次见。
|