TS 封装 Axios

前言

Axios 的二次封装是一项基础工作,主要目的就是将一些常用的功能进行封装,简化后续网络请求的发送。

JS 版本的封装大家都已经非常熟悉了,可以信手拈来。但是使用 TypeScript 对 Axios 进行封装,稍微就复杂了些。主要是由于 TS 引入了类型系统,带来了一些类型的束缚。对于 TS 不太熟悉的小伙伴就容易绕晕。

因此本文适合的阅读对象:熟悉 axios 封装,但在 TypeScript 中不知该如何下手。

明确我们的封装目标:能用上TypeScript 带来的好处,类型检查(安全性) 和语法提示(便捷性) 。

本文将从泛型入手,然后了解 Axios 中的部分类型,延续 JS 版本的极简风,教你封装出一个可用的清爽版 Axios。

初始化项目

使用 create-vue 脚手架初始化项目:

  1.  
    npm create vue ts-axios
  2.  
    // 或者
  3.  
    pnpm create vue ts-axios
  4.  
    复制代码

选择添加 TypeScript :

cfc493c04238995f80e43236a1b03828.jpegimage-20221015234134695

之后进入项目目录并安装依赖:

  1.  
    npm install
  2.  
    // 或者
  3.  
    pnpm install
  4.  
    复制代码

安装其他依赖

示例会用到一些组件,所以安装下组件库:

  1.  
    pnpm add axios
  2.  
    pnpm add ant-design-vue
  3.  
    复制代码

接口 mock

使用插件 mock 一个登录接口和一个用户信息接口,方便测试请求。

上篇文章《一个登录案例包学会 Pinia》[2]介绍了在 vite 中 mock 数据的简单用法,本文不再赘述。

安装插件并进行配置:

  1.  
    pnpm add vite-plugin-mock mockjs -D
  2.  
    复制代码
  1.  
    // vite.config.ts
  2.  
     
  3.  
    import vue from '@vitejs/plugin-vue'
  4.  
    import { viteMockServe } from 'vite-plugin-mock'
  5.  
     
  6.  
    export default defineConfig({
  7.  
      plugins: [
  8.  
        vue(),
  9.  
        viteMockServe()
  10.  
      ],
  11.  
     
  12.  
    })
  13.  
    复制代码

建立 mock 文件,一个登录接口返回 token,一个用户信息接口返回用户名等信息,还有一个模拟出现业务错误的接口。

  1.  
    // mock/user.ts
  2.  
     
  3.  
     
  4.  
    export default [
  5.  
      // 用户登录
  6.  
      {
  7.  
        url: "/api/user/login",
  8.  
        method: "post",
  9.  
        response: (res) => {
  10.  
          return {
  11.  
            code: 0,
  12.  
            message: 'success',
  13.  
            data: {
  14.  
              token: "Token"
  15.  
            }
  16.  
          }
  17.  
        }
  18.  
      },
  19.  
      // 获取用户信息
  20.  
      {
  21.  
        url: "/api/user/info",
  22.  
        method: "get",
  23.  
        response: (res) => {
  24.  
          return {
  25.  
            code: 0,
  26.  
            message: 'success',
  27.  
            data: {
  28.  
              id: "2467751560226270",
  29.  
              username: "昆吾kw",
  30.  
              avatar: "https://p3-passport.byteimg.com/img/user-avatar/3745b7eb198f2357155cd88eb7930f35~180x180.awebp",
  31.  
              description: "前端开发",
  32.  
            }
  33.  
          }
  34.  
        }
  35.  
      },
  36.  
     
  37.  
      // 一个失败的请求
  38.  
      {
  39.  
        url: "/api/error",
  40.  
        method: "get",
  41.  
        response: (res) => {
  42.  
          return {
  43.  
            code: 1,
  44.  
            message: '密码错误',
  45.  
            data: null
  46.  
          }
  47.  
        }
  48.  
      }
  49.  
    ]
  50.  
    复制代码

泛型

泛型的英文是Generic,意思是“通用的”,“一般的“。

在程序设计,泛型中可以理解为 “泛指的类型”,不确定的类型,或者通用的类型。

理解泛型

泛型的概念很像函数中的参数。为了更方便的理解什么是泛型,可以用函数参数做一个类比。

函数在定义时,我们并不知道参数的值到底是多少。只有当函数调用时,才能确定。

  1.  
    function print(a) {
  2.  
        console.log(a)
  3.  
    }
  4.  
     
  5.  
    print('hello')
  6.  
    print('world')
  7.  
    复制代码

泛型的含义也是如此。只不过,参数不确定的是值,泛型不确定的是类型。

泛型的使用场景有很多种,比如泛型函数,泛型类,泛型接口。我们只看泛型函数。

如何定义泛型函数呢?如下:

  1.  
    function add<T>(a:T, b:T): T {
  2.  
        return a + b
  3.  
    }
  4.  
    复制代码

这段代码定义了一个 add 函数,在函数名后来使用了一对尖括号来声明泛型,使用 T(Type 的缩写)来表示一个泛型。之后参数的类型,就不再具体指定是 string 还是 number ,亦或是其他具体的类型,而是用 T 表示。函数的返回值类型同样使用 T 表示。

这样就声明了一个泛型函数。

接着就是泛型函数的调用:

  1.  
    add<number>(1, 2)
  2.  
     
  3.  
    add<string>('a', 'b')
  4.  
    复制代码

和普通函数的调用一样,只不过在函数名后面再次使用一对尖括号,来传递了具体的类型。

通过和参数的类比,详细大家能体会到泛型的含义了。所谓泛型,就是在定义时不确定,而在使用时确定的一种类型。泛型的好处就是提高了代码的复用性,减少了代码的冗余。

有了对泛型的初步认识后,下面就可以去进入正题,去看看 TypeScript 如何封装 Axios 了。

本文的思路

我们不再按照创建实例、配置参数、设置拦截器、封装公共请求方法、封装 API 的顺序去实现封装代码。而是打破常规,从类型入手。

Axios 中的重要类型

查看第三方库的类型声明

Axios 提供了完备的类型声明。声明文件可以直接去看 node_modules/axios/index.d.ts 这个文件:

31371802fcd0b60e1d612439fdad9e2f.jpegimage-20221015222400669

也可以在编辑器中,按 ctrl,同时鼠标点击 axios 模块跳转过去:

3830c028a59e957b6b69d9b4e7f5e93b.jpegimage-20221015222331509a891d437f6f037153b759e1e3efab91d.jpegimage-20221015222440898

要封装好 axios,需要先明白这几个类型是干啥用的。

从一个请求方法入手,看常用的类型

TypeScript 中 class 不仅是类,还可以是类类型。作为前者,它是一个值。作为后者,它是一个类型。

下面的 Axios 就是一个类类型,它声明了一些成员的类型。我们看其中的核心请求方法,requestget ,post :

  1.  
    export class Axios {
  2.  
      // ......
  3.  
      request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
  4.  
        
  5.  
      get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
  6.  
        
  7.  
      post<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
  8.  
      
  9.  
      // ......
  10.  
    }
  11.  
    复制代码

熟悉 axios 的朋友都知道,axios.getaxios.post 这些方法其实是对 axios.request 的一层封装。所以我们就看 request 方法的声明。

request 方法有三个泛型,T ,R 和 D,接收一个 AxiosRequestConfig 类型的参数作为配置对象,返回值的类型是一个接收泛型R 的 Promise 类型。

  1.  
    request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
  2.  
    复制代码

这三个泛型,T 的含义是什么?要去看 R。R 的含义是什么?要去看它的默认类型 AxiosResponse :

  1.  
    export interface AxiosResponse<T = any, D = any> {
  2.  
     data: T;
  3.  
     status: number;
  4.  
     statusText: string;
  5.  
     headers: RawAxiosResponseHeaders | AxiosResponseHeaders;
  6.  
     config: AxiosRequestConfig<D>;
  7.  
     request?: any;
  8.  
    }
  9.  
    复制代码

看到这个结构,我们知道了,原来 AxiosResponse 就是在设置响应拦截器中用到的那个 response 对象的类型。

同时也就知道了泛型 T 就是服务器返回的数据的类型。因为服务器究竟返回什么类型,代码是不知道的,所以它的默认类型是 any。

回到头来再来看 request 方法的定义,现在就能明白了,它接收的第一个泛型 T 就是将来服务器返回数据的类型,R 就是这个数据经过 axios 包装一层得到的 response 对象的类型,而 request 方法的返回值是一个 Promise,其值就是成功态的 R,也就是 response对象。

第三个泛型 D,这里就不再说了。你可以用同样的方法,找出它又是谁的类型,然后发在评论区。

AxiosRequestConfig 类型

先看下它的类型声明:

  1.  
    export interface AxiosRequestConfig<D = any> {
  2.  
      url?: string;
  3.  
      method?: Method | string;
  4.  
      baseURL?: string;
  5.  
      headers?: RawAxiosRequestHeaders;
  6.  
      // .....
  7.  
    }
  8.  
    复制代码

相信大家能看出来,它其实就是 axios 的配置对象的类型。在设置请求拦截器和封装公共请求方法时会用到。

AxiosInstance

该类型为 axios.create 方法创建出的实例的类型。后面直接用到。

AxiosError

该类型为请求发送过程中出现错误产生的错误对象的类型:

  1.  
    export class AxiosError<T = unknown, D = any> extends Error {
  2.  
      constructor(
  3.  
          message?: string,
  4.  
          code?: string,
  5.  
          config?: AxiosRequestConfig<D>,
  6.  
          request?: any,
  7.  
          response?: AxiosResponse<T, D>
  8.  
      );
  9.  
     
  10.  
      config?: AxiosRequestConfig<D>;
  11.  
      code?: string;
  12.  
      request?: any;
  13.  
      response?: AxiosResponse<T, D>;
  14.  
      isAxiosError: boolean;
  15.  
      status?: number;
  16.  
      toJSON: () => object;
  17.  
      cause?: Error;
  18.  
      // ......
  19.  
    }
  20.  
    复制代码

我们会用到其中的 response 属性,它表示响应对象,需要根据它的 HTTP 状态码做一些处理。

知道了上面这几个类型,接下来就可以着手封装 Axios 了。

开始封装

先提前打个预防针,因为封装的很简单,没有用到类,没有过多的处理业务逻辑。估计看完后你会感觉索然无味。

毕竟我们最初的目的就是用上 TypeScript 的类型能力:类型检查和代码提示。

而且我相信,太复杂的封装,可能会阻碍对类型系统的学习,反而得不偿失。

新建一个 src/utils/request.ts 文件,在这个文件中对 Axios 进行封装。

Result 类型

从服务器返回的数据的类型通常长这样子:

  1.  
    {
  2.  
       code: 0,
  3.  
       message: 'ok',
  4.  
       data: {} 
  5.  
    }
  6.  
    复制代码

定义一个接口 Result 来表示它,其中 data 的类型不确定,所以就要用到泛型了:

  1.  
    /* 服务器返回数据的的类型,根据接口文档确定 */
  2.  
    export interface Result<T=any> {
  3.  
      code: number,
  4.  
      message: string,
  5.  
      data: T
  6.  
    }
  7.  
    复制代码

这是一个泛型接口。和泛型函数差不多,在定义时声明一个泛型,用这个泛型去规范成员的类型。将来使用该接口的时候,再去确定泛型的具体类型。

接下来就可以写代码了。

创建实例

这个大家都非常熟悉了,不再赘述。

  1.  
    import axios from 'axios'
  2.  
    import type { AxiosInstance } from 'axios'
  3.  
     
  4.  
    const service: AxiosInstance = axios.create({
  5.  
      baseURL: '/api',
  6.  
      timeout: 30000
  7.  
    })
  8.  
    复制代码

请求拦截器

这个大家也很熟悉,主要在这里处理请求发送前的一些工作,比如给 HTTP Header 添加 token ,开启 Loading 效果,设置取消请求等。

  1.  
    import type { AxiosError, AxiosRequestConfig } from 'axios'
  2.  
    import { message as Message } from 'ant-design-vue'
  3.  
     
  4.  
    /* 请求拦截器 */
  5.  
    service.interceptors.request.use((config: AxiosRequestConfig) => {
  6.  
      //  伪代码
  7.  
      // if (token) {
  8.  
      //   config.headers.Authorization = `Bearer ${token}`;
  9.  
      // }
  10.  
      return config
  11.  
    }, (error: AxiosError) => {
  12.  
      Message.error(error.message);
  13.  
      return Promise.reject(error)
  14.  
    })
  15.  
    复制代码

响应拦截器

响应拦截器大家也很熟悉了。简单说一下做了哪些事情:

  • 根据自定义错误码判断请求是否成功,然后只将组件会用到的数据,也就是上面的 Result 中的 data 返回

  • 如果错误码判断请求失败,此时为业务错误,比如用户名不存在等,在这里进行提示

  • 如果网络错误,则进入第二个回调函数中,根据不同的状态码设置不同的提示消息进行提示

  1.  
    import type { AxiosError, AxiosResponse } from 'axios'
  2.  
    import { message as Message } from 'ant-design-vue'
  3.  
     
  4.  
    /* 响应拦截器 */
  5.  
    service.interceptors.response.use((response: AxiosResponse) => {
  6.  
      const { code, message, data } = response.data
  7.  
     
  8.  
      // 根据自定义错误码判断请求是否成功
  9.  
      if (code === 0) {
  10.  
        // 将组件用的数据返回
  11.  
        return data
  12.  
      } else {
  13.  
        // 处理业务错误。
  14.  
        Message.error(message)
  15.  
        return Promise.reject(new Error(message))
  16.  
      }
  17.  
    }, (error: AxiosError) => {
  18.  
      // 处理 HTTP 网络错误
  19.  
      let message = ''
  20.  
      // HTTP 状态码
  21.  
      const status = error.response?.status
  22.  
      switch (status) {
  23.  
        case 401:
  24.  
          message = 'token 失效,请重新登录'
  25.  
          // 这里可以触发退出的 action
  26.  
          break;
  27.  
        case 403:
  28.  
          message = '拒绝访问'
  29.  
          break;
  30.  
        case 404:
  31.  
          message = '请求地址错误'
  32.  
          break;
  33.  
        case 500:
  34.  
          message = '服务器故障'
  35.  
          break;
  36.  
        default:
  37.  
          message = '网络连接故障'
  38.  
      }
  39.  
      
  40.  
      Message.error(message) 
  41.  
      return Promise.reject(error)
  42.  
    })
  43.  
    复制代码

公共请求方法

这里是本文的重点,也是 TS 封装 Axios 的重点。使用 TS 无非要获得良好的代码提示,我们在调用接口时编辑器能提示出该接口的返回值有哪些。

  1.  
    /* 导出封装的请求方法 */
  2.  
    export const http = {
  3.  
      get<T=any>(url: string, config?: AxiosRequestConfig) : Promise<T> {
  4.  
        return service.get(url, config)
  5.  
      },
  6.  
     
  7.  
      post<T=any>(url: string, data?: object, config?: AxiosRequestConfig) :Promise<T> {
  8.  
        return service.post(url, data, config)
  9.  
      },
  10.  
     
  11.  
      put<T=any>(url: string, data?: object, config?: AxiosRequestConfig) :Promise<T> {
  12.  
        return service.put(url, data, config)
  13.  
      },
  14.  
     
  15.  
      delete<T=any>(url: string, config?: AxiosRequestConfig) : Promise<T> {
  16.  
        return service.delete(url, config)
  17.  
      }
  18.  
    }
  19.  
    复制代码

我们以 get 方法为例,看下为什么这么封装?

  1.  
    get<T>(url: string, config?: AxiosRequestConfig) : Promise<T> {
  2.  
        return service.get(url, config)
  3.  
    }
  4.  
    复制代码

上文说过,axios 的 requet,get 这些方法,返回的都是一个 AxiosResponse 类型的数据。

默认的响应拦截器返回的是完整的 response 对象,类型为 AxiosResponse 。

经过我们的修改,现在响应拦截器只返回 response.data.data,对应的类型就是 AxiosResponse<Result> 中的 T。所以这里的 service.get 方法,其实际返回类型应为 T,但是编辑器并不知道,它仍然认为它返回的是:

47e9c66a7ad20a76b1fe9fddfd735002.jpegimage-20221016134207024

所以我们要手动帮助编辑器“修正”类型提示,也就是不依靠 TS 的类型推导,而是主动设置返回类型为 Promise :

  1.  
    get<T>(url: string, config?: AxiosRequestConfig) : Promise<T> {
  2.  
        return service.get(url, config)
  3.  
    }
  4.  
    复制代码

这样编辑器就准确识别类型了:

6f76dbd5550fce1b4d3557e5235759df.jpegimage-20221016134438155

这里的泛型 T 表示的服务器返回数据的类型,具体返回什么类型,要到接口调用时才知道。所以下面来到 API 层,去封装请求接口。

为什么要封装公共请求方法

之所以封装这一层公共请求方法,主要目的就是为了帮编译器正确识别类型。方法就是手动设置返回类型。如果直接使用 axios 实例的请求方法,就需要在每次调用时都指定一遍,当接口有几十上百个时,多少会在增加一些工作量。所以就在这里多加一层,提前将类型制定好。

API 层

准备两个文件,一个写类型,一个写接口。

dac67da8cf42a50cd5414c8271e81a52.jpegimage-20221016135035587

在 api/user/types.ts 文件中,写接口需要的参数的类型,和接口返回数据的类型。这里的类型,要根据接口文档而定。

  1.  
    /* 登录接口参数类型 */
  2.  
    export interface LoginData {
  3.  
      username: string,
  4.  
      password: string,
  5.  
    }
  6.  
     
  7.  
    /* 登录接口返回值类型 */
  8.  
    export interface LoginRes {
  9.  
      token: string
  10.  
    }
  11.  
     
  12.  
    /* 用户信息接口返回值类型 */
  13.  
    export interface UserInfoRes {
  14.  
      id: string,
  15.  
      username: string,
  16.  
      avatar: string,
  17.  
      description: string,
  18.  
    }
  19.  
    复制代码

在 api/user/index.ts 文件中,封装组件用到的接口。

  1.  
    import request, { http } from '@/utils/request'
  2.  
     
  3.  
    import type { LoginData, LoginRes, UserInfoRes} from './types'
  4.  
     
  5.  
    /**
  6.  
     * 登录
  7.  
     */
  8.  
    export function login(data: LoginData) {
  9.  
      return http.post<LoginRes>('/user/login', data);
  10.  
    }
  11.  
     
  12.  
    /**
  13.  
     * 获取登录用户信息
  14.  
     */
  15.  
    export function getUserInfo() {
  16.  
      return http.get<UserInfoRes>('/user/info')
  17.  
    }
  18.  
    复制代码

以 login 方法为例进行说明。它接收 LoginData 类型的参数作为请求参数。在调用接口时,通过泛型指定了该接口的返回值类型:

79a25a394a0967958eeda059736f6329.jpegimage-20221016135528697

测试

Axios 代码提示测试

App.vue 组件中有一个登录表单,在这里测试登录接口是否正常。

方法login 可以推导出当前返回值的类型:

30e418bf5cfdf29282601ab6e97081d0.jpegimage-20221016002008786

返回值 res 的类型也能推导出:

423253db95356432be89d31b6e6107d0.jpegimage-20221016002133845

也可以正确使用类型提示:

c91c71d89295055891eab6255b69cbc7.jpegimage-20221016002233899

然后再测试下调用请求用户信息的接口:

d90e7966460116f692aa5ee4320165f1.jpegimage-20221016002441196

如我们所愿,现在 Axios 经过 TypeScript 的封装,可以给出友好的类型提示,这对于开发体验和效率都有很好的提升。

响应拦截器提示测试

测试业务处理错误的请求,假设此次请求用户密码输入错误:

030bf2c0e50cbaf9e25e60ebba71ea2a.jpegimage-20221016132121313

关闭开发服务,测试无网络连接下的请求发送:

1f165df7078ca028c2c07f2b3be71654.jpegimage-20221016132156024

小结

本文示例代码已上传到仓库[3]

开始只是想说说 TypeScript 封装 Axios 的一个简单实现版本。但没想到最后说了不老少内容,包括:

  • TypeScript 泛型的概念

  • 第三方库如何看类型声明

  • 如何利用类型封装 Axios

 https://blog.csdn.net/lunahaijiao/article/details/128246274

 

 

 

posted @ 2023-02-01 08:55  Haoyin-杰克  阅读(1329)  评论(0编辑  收藏  举报