vue3 admin 保姆教学指南|关于使用typescript二次封装Axios的特别说明
如果对axios用法不熟悉的,可以参考axios官方文档去学习。
可能很多人在封装axios的时候,不知道后端返回的一大坨数据到底应该如何处理,难道每个字段都声明一下?那不把我们前端累吐血了?
今天我们就来讲讲怎么优雅的处理这个数据格式。
axios二次封装
首先我们引入axios
import axios from 'axios
我们从axios
点击去,看它的index.d.ts
,
可以看到axios暴露了这么几个方法:
export interface AxiosStatic extends AxiosInstance {
create(config?: CreateAxiosDefaults): AxiosInstance;
Cancel: CancelStatic;
CancelToken: CancelTokenStatic;
Axios: typeof Axios;
AxiosError: typeof AxiosError;
HttpStatusCode: typeof HttpStatusCode;
readonly VERSION: string;
isCancel: typeof isCancel;
all: typeof all;
spread: typeof spread;
isAxiosError: typeof isAxiosError;
toFormData: typeof toFormData;
formToJSON: typeof formToJSON;
CanceledError: typeof CanceledError;
AxiosHeaders: typeof AxiosHeaders;
}
create方法
这里我们需要的是create
方法,来生成axios创建的实例,然后它需要的参数我们可以在CreateAxiosDefaults
找到,它是继承自AxiosRequestConfig
,可以看到这里面是所有它的配置参数:
export interface AxiosRequestConfig<D = any> {
url?: string;
method?: Method | string;
baseURL?: string;
transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[];
transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[];
headers?: (RawAxiosRequestHeaders & MethodsHeaders) | AxiosHeaders;
params?: any;
paramsSerializer?: ParamsSerializerOptions;
data?: D;
timeout?: Milliseconds;
timeoutErrorMessage?: string;
withCredentials?: boolean;
adapter?: AxiosAdapterConfig | AxiosAdapterConfig[];
auth?: AxiosBasicCredentials;
responseType?: ResponseType;
responseEncoding?: responseEncoding | string;
xsrfCookieName?: string;
xsrfHeaderName?: string;
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;
maxContentLength?: number;
validateStatus?: ((status: number) => boolean) | null;
maxBodyLength?: number;
maxRedirects?: number;
maxRate?: number | [MaxUploadRate, MaxDownloadRate];
beforeRedirect?: (options: Record<string, any>, responseDetails: {headers: Record<string, string>}) => void;
socketPath?: string | null;
httpAgent?: any;
httpsAgent?: any;
proxy?: AxiosProxyConfig | false;
cancelToken?: CancelToken;
decompress?: boolean;
transitional?: TransitionalOptions;
signal?: GenericAbortSignal;
insecureHTTPParser?: boolean;
env?: {
FormData?: new (...args: any[]) => object;
};
formSerializer?: FormSerializerOptions;
}
我们使用axios.create
先创建一个实例,然后配置它的baseUrl
和timeout
,
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: ResultEnum.TIMEOUT as number,
})
实例的类型为AxiosInstance
,可以在create
声明的地方看到。
请求拦截
接下来做个请求拦截器,这里需要处理一下token:
service.interceptors.request.use(
(config) => {
const userStore = useUserStore()
const token = userStore.token
if (token) {
config.headers.token = token
}
return config
},
(error: AxiosError) => {
ElMessage.error(error.message)
return Promise.reject(error)
},
)
这里的token是从store中拿过来的,由于我们的store做了持久化缓存,所以只要是缓存中有token,这里就能获取到了。
响应拦截
接下来就是响应拦截器,这里我们需要对响应返回的数据做处理。
首先成请求成功的情况,我们需要对成功分成不同的情况处理。第一种token失效,当data.code === 203
的时候,就意味着token过期或者不正确,需要清空store和缓存数据,然后跳转到登陆页。第二种是data.code
非200的情况,直接返回给用户错误信息即可,第三种就是data.code=== 200
,直接把data
返回即可。
service.interceptors.response.use(
(response: AxiosResponse) => {
const { data } = response
// * 登陆失效(code == 203)
if (data.code === ResultEnum.EXPIRE) {
RESEETSTORE()
ElMessage.error(data.message || ResultEnum.ERRMESSAGE)
router.replace(LOGIN_URL)
return Promise.reject(data)
}
if (data.code && data.code !== ResultEnum.SUCCESS) {
ElMessage.error(data.message || ResultEnum.ERRMESSAGE)
return Promise.reject(data)
}
return data
}
)
对于响应错误的情况,我们也需要单独处理,然后提示不同的错误消息。
这里需要根据不同状态码来判断,也就是response.status
,下面是我对于常见的错误码的处理
service.interceptors.response.use(
(error: AxiosError) => {
// 处理 HTTP 网络错误
let message = ''
// HTTP 状态码
const status = error.response?.status
switch (status) {
case 403:
message = '拒绝访问'
break
case 404:
message = '请求地址错误'
break
case 500:
message = '服务器故障'
break
default:
message = '网络连接故障'
}
ElMessage.error(message)
return Promise.reject(error)
},
)
封装的请求方法
然后我们封装一下常见的几种请求方法,get、post、delete、put
const http = {
get<T>(
url: string,
params?: object,
config?: AxiosRequestConfig,
): Promise<ResultData<T>> {
return service.get(url, { params, ...config })
},
post<T>(
url: string,
data?: object,
config?: AxiosRequestConfig,
): Promise<ResultData<T>> {
return service.post(url, data, config)
},
put<T>(
url: string,
data?: object,
config?: AxiosRequestConfig,
): Promise<ResultData<T>> {
return service.put(url, data, config)
},
delete<T>(
url: string,
data?: object,
config?: AxiosRequestConfig,
): Promise<ResultData<T>> {
return service.delete(url, { data, ...config })
},
}
对于这几种方法,他们的接收参数的形式不一样的,拿get
和post
距离,一个接收的是params
,一个接收的data
,get
方法要想传递query
参数,就需要这样写:
service.get(url, { params, ...config })
因为它跟config是一体的,而post
传递参数,跟config是分开的,就需要这样传:
service.post(url, data, config)
类型约束
接下来我们来看看它们的类型是如何约束的。拿get
方法来举例:
get<T>(
url: string,
params?: object,
config?: AxiosRequestConfig,
): Promise<ResultData<T>> {
return service.get(url, { params, ...config })
},
get
方法接受了一个范型T,那这个T是从哪来的?
我们再去看看在调用get
方法的时候是怎么使用的:
import http from '@/utils/http'
import type { UserRes } from './types'
/**
* @description 获取后台用户分页列表(带搜索)
* @param page
* @param limit
* @param username
* @returns {<PageRes<AclUser.ResAclUserList>>}
*/
/**
* 获取登录用户信息
*/
export function getUserInfo() {
return http.get<UserRes>('/admin/acl/index/info')
}
这个接口是用来获取用户登陆信息的,可以看到它传了一个UserRes
,而UserRes
是这样声明的:
export interface UserRes {
userId?: string
name: string
avatar: string
buttons: string[]
roles: string[]
routes: string[]
}
再回过头去看看,T
就是UserRes
,它约束了接口返回的data
里面的字段信息。再接着看,http.get()
方法返回信息的时候这样约束的:
get<T>(): Promise<ResultData<T>> {},
它返回了一个Promise
,然后通过范型的方式,接收了ResultData<T>
,这里的ResultData
长这样:
// * 请求响应参数(不包含data)
export interface Result {
code: number
message: string
ok?: boolean
}
// * 请求响应参数(包含data)
export interface ResultData<T = any> extends Result {
data: T
}
这样一来是不是就清楚了?原来层层传递的范型最终约束的就是后端返回的整个json对象,我们再看看后端返回的数据格式就一目了然了:
现在你对后端返回的数据如何进行约束是不是有了一定的了解了?
接下来我们在看一个分页列表的接口如何进行约束的。
import http from '@/utils/http'
import type { PageRes } from '../types'
import type { AclUser } from './types'
/**
* @description 获取后台用户分页列表(带搜索)
* @param page
* @param limit
* @param username
* @returns {<PageRes<AclUser.ResAclUserList>>}
*/
export function getAclUserList(params: AclUser.ReqAclUserListParams) {
return http.get<PageRes<AclUser.ResAclUserList>>(
`/admin/acl/user/${params.pageNum}/${params.pageSize}`,
{ username: params.username },
)
}
首先我们看看后台返回的数据:
data
外面的我们前面已经约束了,现在data里面的这个分页格式我们是不是可以统一约束一下?也就是上面的PageRes
,它长这样:
// * 分页响应参数
export interface PageRes<T> {
records: T[]
pageNum?: number
pageSize?: number
total: number
}
这样我们就对一个分页格式的数据做了统一的约束,至于里面的records
数据列表,我们就要单独进行约束,通过范型的形式传进去就可以了。
我们再看看records
是如何约束的:
// * 分页请求参数
export interface ReqPage {
pageNum: number
pageSize: number
}
// * 用户管理模块
export namespace AclUser {
export interface ReqAclUserListParams extends ReqPage {
username?: string
}
export interface ResAclUserList {
deleted: boolean
gmtCreate: string
gmtModified: string
id: string
nickName: string
password: string
roleName: string
salt: null
token: null
username: string
}
}
我们通过ResAclUserList
来对里面的字段进行了约束。同样的接受分页参数的时候也进行了这样的约束。
注意:不需要约束所有的后端字段,只需要把我们前端使用到的字段约束即可。
上面所提到的data格式需要跟后端提前约定好,跟自己自己公司的实际情况做修改,这个不是统一标准的。
OK,讲完了类型约束,下面我们再来说一下接口规范。
接口规范
1.数据格式
数据格式需要跟后端进行约定,比如我这里的就是:
{
code: number
data: {}
message: string
ok: boolean
}
2.code规范
200 // 成功
201 // 失败
203 // token失效
3.分页列表规范
{
data: {
pageNum: number
pageSize: number
total: number
records: []
}
}
有的records叫做list,items都可以。
4.api目录规范
项目所有的接口都放在api/
目录下面,按照分类放在不同的文件夹下。统一在api/index.ts
中导出。
共公类型放在api/types.ts
中,非公共类型放在各自的文件夹下的types.ts
中。下面是我的api
目录划分:
这样我们在使用的时候,只需要从import { xxx } from "@/api"
引入即可。
文章教程系列
转载自:https://juejin.cn/post/7214146630467305530