前言
Axios 的二次封装是一项基础工作,主要目的就是将一些常用的功能进行封装,简化后续网络请求的发送。
JS 版本的封装大家都已经非常熟悉了,可以信手拈来。但是使用 TypeScript 对 Axios 进行封装,稍微就复杂了些。主要是由于 TS 引入了类型系统,带来了一些类型的束缚。对于 TS 不太熟悉的小伙伴就容易绕晕。
因此本文适合的阅读对象:熟悉 axios 封装,但在 TypeScript 中不知该如何下手。
明确我们的封装目标:能用上TypeScript 带来的好处,类型检查(安全性) 和语法提示(便捷性) 。
本文将从泛型入手,然后了解 Axios 中的部分类型,延续 JS 版本的极简风,教你封装出一个可用的清爽版 Axios。
初始化项目
使用 create-vue 脚手架初始化项目:
- npm create vue ts-axios
- // 或者
- pnpm create vue ts-axios
- 复制代码
选择添加 TypeScript :
image-20221015234134695之后进入项目目录并安装依赖:
- npm install
- // 或者
- pnpm install
- 复制代码
安装其他依赖
示例会用到一些组件,所以安装下组件库:
- pnpm add axios
- pnpm add ant-design-vue
- 复制代码
接口 mock
使用插件 mock 一个登录接口和一个用户信息接口,方便测试请求。
上篇文章《一个登录案例包学会 Pinia》[2]介绍了在 vite 中 mock 数据的简单用法,本文不再赘述。
安装插件并进行配置:
- pnpm add vite-plugin-mock mockjs -D
- 复制代码
- // vite.config.ts
- import vue from '@vitejs/plugin-vue'
- import { viteMockServe } from 'vite-plugin-mock'
- export default defineConfig({
- plugins: [
- vue(),
- viteMockServe()
- ],
- })
- 复制代码
建立 mock 文件,一个登录接口返回 token,一个用户信息接口返回用户名等信息,还有一个模拟出现业务错误的接口。
- // mock/user.ts
- export default [
- // 用户登录
- {
- url: "/api/user/login",
- method: "post",
- response: (res) => {
- return {
- code: 0,
- message: 'success',
- data: {
- token: "Token"
- }
- }
- }
- },
- // 获取用户信息
- {
- url: "/api/user/info",
- method: "get",
- response: (res) => {
- return {
- code: 0,
- message: 'success',
- data: {
- id: "2467751560226270",
- username: "昆吾kw",
- avatar: "https://p3-passport.byteimg.com/img/user-avatar/3745b7eb198f2357155cd88eb7930f35~180x180.awebp",
- description: "前端开发",
- }
- }
- }
- },
- // 一个失败的请求
- {
- url: "/api/error",
- method: "get",
- response: (res) => {
- return {
- code: 1,
- message: '密码错误',
- data: null
- }
- }
- }
- ]
- 复制代码
泛型
泛型的英文是Generic
,意思是“通用的”,“一般的“。
在程序设计,泛型中可以理解为 “泛指的类型”,不确定的类型,或者通用的类型。
理解泛型
泛型的概念很像函数中的参数。为了更方便的理解什么是泛型,可以用函数参数做一个类比。
函数在定义时,我们并不知道参数的值到底是多少。只有当函数调用时,才能确定。
- function print(a) {
- console.log(a)
- }
- print('hello')
- print('world')
- 复制代码
泛型的含义也是如此。只不过,参数不确定的是值,泛型不确定的是类型。
泛型的使用场景有很多种,比如泛型函数,泛型类,泛型接口。我们只看泛型函数。
如何定义泛型函数呢?如下:
- function add<T>(a:T, b:T): T {
- return a + b
- }
- 复制代码
这段代码定义了一个 add 函数,在函数名后来使用了一对尖括号来声明泛型,使用 T(Type 的缩写)来表示一个泛型。之后参数的类型,就不再具体指定是 string 还是 number ,亦或是其他具体的类型,而是用 T 表示。函数的返回值类型同样使用 T 表示。
这样就声明了一个泛型函数。
接着就是泛型函数的调用:
- add<number>(1, 2)
- add<string>('a', 'b')
- 复制代码
和普通函数的调用一样,只不过在函数名后面再次使用一对尖括号,来传递了具体的类型。
通过和参数的类比,详细大家能体会到泛型的含义了。所谓泛型,就是在定义时不确定,而在使用时确定的一种类型。泛型的好处就是提高了代码的复用性,减少了代码的冗余。
有了对泛型的初步认识后,下面就可以去进入正题,去看看 TypeScript 如何封装 Axios 了。
本文的思路
我们不再按照创建实例、配置参数、设置拦截器、封装公共请求方法、封装 API 的顺序去实现封装代码。而是打破常规,从类型入手。
Axios 中的重要类型
查看第三方库的类型声明
Axios 提供了完备的类型声明。声明文件可以直接去看 node_modules/axios/index.d.ts
这个文件:
也可以在编辑器中,按 ctrl,同时鼠标点击 axios 模块跳转过去:
image-20221015222331509image-20221015222440898要封装好 axios,需要先明白这几个类型是干啥用的。
从一个请求方法入手,看常用的类型
TypeScript 中 class 不仅是类,还可以是类类型。作为前者,它是一个值。作为后者,它是一个类型。
下面的 Axios 就是一个类类型,它声明了一些成员的类型。我们看其中的核心请求方法,request
,get
,post
:
- export class Axios {
- // ......
- request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
- get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
- post<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
- // ......
- }
- 复制代码
熟悉 axios 的朋友都知道,axios.get
,axios.post
这些方法其实是对 axios.request
的一层封装。所以我们就看 request
方法的声明。
request
方法有三个泛型,T ,R 和 D,接收一个 AxiosRequestConfig 类型的参数作为配置对象,返回值的类型是一个接收泛型R 的 Promise 类型。
- request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
- 复制代码
这三个泛型,T 的含义是什么?要去看 R。R 的含义是什么?要去看它的默认类型 AxiosResponse :
- export interface AxiosResponse<T = any, D = any> {
- data: T;
- status: number;
- statusText: string;
- headers: RawAxiosResponseHeaders | AxiosResponseHeaders;
- config: AxiosRequestConfig<D>;
- request?: any;
- }
- 复制代码
看到这个结构,我们知道了,原来 AxiosResponse 就是在设置响应拦截器中用到的那个 response 对象的类型。
同时也就知道了泛型 T 就是服务器返回的数据的类型。因为服务器究竟返回什么类型,代码是不知道的,所以它的默认类型是 any。
回到头来再来看 request
方法的定义,现在就能明白了,它接收的第一个泛型 T 就是将来服务器返回数据的类型,R 就是这个数据经过 axios 包装一层得到的 response 对象的类型,而 request 方法的返回值是一个 Promise,其值就是成功态的 R,也就是 response对象。
第三个泛型 D,这里就不再说了。你可以用同样的方法,找出它又是谁的类型,然后发在评论区。
AxiosRequestConfig 类型
先看下它的类型声明:
- export interface AxiosRequestConfig<D = any> {
- url?: string;
- method?: Method | string;
- baseURL?: string;
- headers?: RawAxiosRequestHeaders;
- // .....
- }
- 复制代码
相信大家能看出来,它其实就是 axios 的配置对象的类型。在设置请求拦截器和封装公共请求方法时会用到。
AxiosInstance
该类型为 axios.create 方法创建出的实例的类型。后面直接用到。
AxiosError
该类型为请求发送过程中出现错误产生的错误对象的类型:
- export class AxiosError<T = unknown, D = any> extends Error {
- constructor(
- message?: string,
- code?: string,
- config?: AxiosRequestConfig<D>,
- request?: any,
- response?: AxiosResponse<T, D>
- );
- config?: AxiosRequestConfig<D>;
- code?: string;
- request?: any;
- response?: AxiosResponse<T, D>;
- isAxiosError: boolean;
- status?: number;
- toJSON: () => object;
- cause?: Error;
- // ......
- }
- 复制代码
我们会用到其中的 response 属性,它表示响应对象,需要根据它的 HTTP 状态码做一些处理。
知道了上面这几个类型,接下来就可以着手封装 Axios 了。
开始封装
先提前打个预防针,因为封装的很简单,没有用到类,没有过多的处理业务逻辑。估计看完后你会感觉索然无味。
毕竟我们最初的目的就是用上 TypeScript 的类型能力:类型检查和代码提示。
而且我相信,太复杂的封装,可能会阻碍对类型系统的学习,反而得不偿失。
新建一个 src/utils/request.ts
文件,在这个文件中对 Axios 进行封装。
Result 类型
从服务器返回的数据的类型通常长这样子:
- {
- code: 0,
- message: 'ok',
- data: {}
- }
- 复制代码
定义一个接口 Result
来表示它,其中 data
的类型不确定,所以就要用到泛型了:
- /* 服务器返回数据的的类型,根据接口文档确定 */
- export interface Result<T=any> {
- code: number,
- message: string,
- data: T
- }
- 复制代码
这是一个泛型接口。和泛型函数差不多,在定义时声明一个泛型,用这个泛型去规范成员的类型。将来使用该接口的时候,再去确定泛型的具体类型。
接下来就可以写代码了。
创建实例
这个大家都非常熟悉了,不再赘述。
- import axios from 'axios'
- import type { AxiosInstance } from 'axios'
- const service: AxiosInstance = axios.create({
- baseURL: '/api',
- timeout: 30000
- })
- 复制代码
请求拦截器
这个大家也很熟悉,主要在这里处理请求发送前的一些工作,比如给 HTTP Header 添加 token ,开启 Loading 效果,设置取消请求等。
- import type { AxiosError, AxiosRequestConfig } from 'axios'
- import { message as Message } from 'ant-design-vue'
- /* 请求拦截器 */
- service.interceptors.request.use((config: AxiosRequestConfig) => {
- // 伪代码
- // if (token) {
- // config.headers.Authorization = `Bearer ${token}`;
- // }
- return config
- }, (error: AxiosError) => {
- Message.error(error.message);
- return Promise.reject(error)
- })
- 复制代码
响应拦截器
响应拦截器大家也很熟悉了。简单说一下做了哪些事情:
-
根据自定义错误码判断请求是否成功,然后只将组件会用到的数据,也就是上面的 Result 中的 data 返回
-
如果错误码判断请求失败,此时为业务错误,比如用户名不存在等,在这里进行提示
-
如果网络错误,则进入第二个回调函数中,根据不同的状态码设置不同的提示消息进行提示
- import type { AxiosError, AxiosResponse } from 'axios'
- import { message as Message } from 'ant-design-vue'
- /* 响应拦截器 */
- service.interceptors.response.use((response: AxiosResponse) => {
- const { code, message, data } = response.data
- // 根据自定义错误码判断请求是否成功
- if (code === 0) {
- // 将组件用的数据返回
- return data
- } else {
- // 处理业务错误。
- Message.error(message)
- return Promise.reject(new Error(message))
- }
- }, (error: AxiosError) => {
- // 处理 HTTP 网络错误
- let message = ''
- // HTTP 状态码
- const status = error.response?.status
- switch (status) {
- case 401:
- message = 'token 失效,请重新登录'
- // 这里可以触发退出的 action
- break;
- case 403:
- message = '拒绝访问'
- break;
- case 404:
- message = '请求地址错误'
- break;
- case 500:
- message = '服务器故障'
- break;
- default:
- message = '网络连接故障'
- }
- Message.error(message)
- return Promise.reject(error)
- })
- 复制代码
公共请求方法
这里是本文的重点,也是 TS 封装 Axios 的重点。使用 TS 无非要获得良好的代码提示,我们在调用接口时编辑器能提示出该接口的返回值有哪些。
- /* 导出封装的请求方法 */
- export const http = {
- get<T=any>(url: string, config?: AxiosRequestConfig) : Promise<T> {
- return service.get(url, config)
- },
- post<T=any>(url: string, data?: object, config?: AxiosRequestConfig) :Promise<T> {
- return service.post(url, data, config)
- },
- put<T=any>(url: string, data?: object, config?: AxiosRequestConfig) :Promise<T> {
- return service.put(url, data, config)
- },
- delete<T=any>(url: string, config?: AxiosRequestConfig) : Promise<T> {
- return service.delete(url, config)
- }
- }
- 复制代码
我们以 get
方法为例,看下为什么这么封装?
- get<T>(url: string, config?: AxiosRequestConfig) : Promise<T> {
- return service.get(url, config)
- }
- 复制代码
上文说过,axios 的 requet,get 这些方法,返回的都是一个 AxiosResponse 类型的数据。
默认的响应拦截器返回的是完整的 response
对象,类型为 AxiosResponse 。
经过我们的修改,现在响应拦截器只返回 response.data.data
,对应的类型就是 AxiosResponse<Result> 中的 T。所以这里的 service.get
方法,其实际返回类型应为 T,但是编辑器并不知道,它仍然认为它返回的是:
所以我们要手动帮助编辑器“修正”类型提示,也就是不依靠 TS 的类型推导,而是主动设置返回类型为 Promise :
- get<T>(url: string, config?: AxiosRequestConfig) : Promise<T> {
- return service.get(url, config)
- }
- 复制代码
这样编辑器就准确识别类型了:
image-20221016134438155这里的泛型 T 表示的服务器返回数据的类型,具体返回什么类型,要到接口调用时才知道。所以下面来到 API 层,去封装请求接口。
为什么要封装公共请求方法
之所以封装这一层公共请求方法,主要目的就是为了帮编译器正确识别类型。方法就是手动设置返回类型。如果直接使用 axios 实例的请求方法,就需要在每次调用时都指定一遍,当接口有几十上百个时,多少会在增加一些工作量。所以就在这里多加一层,提前将类型制定好。
API 层
准备两个文件,一个写类型,一个写接口。
image-20221016135035587在 api/user/types.ts
文件中,写接口需要的参数的类型,和接口返回数据的类型。这里的类型,要根据接口文档而定。
- /* 登录接口参数类型 */
- export interface LoginData {
- username: string,
- password: string,
- }
- /* 登录接口返回值类型 */
- export interface LoginRes {
- token: string
- }
- /* 用户信息接口返回值类型 */
- export interface UserInfoRes {
- id: string,
- username: string,
- avatar: string,
- description: string,
- }
- 复制代码
在 api/user/index.ts
文件中,封装组件用到的接口。
- import request, { http } from '@/utils/request'
- import type { LoginData, LoginRes, UserInfoRes} from './types'
- /**
- * 登录
- */
- export function login(data: LoginData) {
- return http.post<LoginRes>('/user/login', data);
- }
- /**
- * 获取登录用户信息
- */
- export function getUserInfo() {
- return http.get<UserInfoRes>('/user/info')
- }
- 复制代码
以 login 方法为例进行说明。它接收 LoginData 类型的参数作为请求参数。在调用接口时,通过泛型指定了该接口的返回值类型:
image-20221016135528697测试
Axios 代码提示测试
App.vue 组件中有一个登录表单,在这里测试登录接口是否正常。
方法login
可以推导出当前返回值的类型:
返回值 res
的类型也能推导出:
也可以正确使用类型提示:
image-20221016002233899然后再测试下调用请求用户信息的接口:
image-20221016002441196如我们所愿,现在 Axios 经过 TypeScript 的封装,可以给出友好的类型提示,这对于开发体验和效率都有很好的提升。
响应拦截器提示测试
测试业务处理错误的请求,假设此次请求用户密码输入错误:
image-20221016132121313关闭开发服务,测试无网络连接下的请求发送:
image-20221016132156024小结
本文示例代码已上传到仓库[3]。
开始只是想说说 TypeScript 封装 Axios 的一个简单实现版本。但没想到最后说了不老少内容,包括:
-
TypeScript 泛型的概念
-
第三方库如何看类型声明
-
如何利用类型封装 Axios
https://blog.csdn.net/lunahaijiao/article/details/128246274