ts 封装 axios 技巧:充分利用类型检查与提示

25,328 阅读7分钟

前言

Vue3的发布大肆推广了一波typescript,现在ts的使用也越来越广泛了。而axios作为目前最流行的http库,常驻于Vue、React和Angular三大前端框架开发的代码之中。在当前的开发环境中,ts与axios并肩作战的机会愈加频繁。本文主要就是介绍如何充分利用ts的特性简洁、高效地在代码中使用axios。

开始之前

本文会从零开始,由浅入深递进式地对axios进行封装,小伙伴可根据自身条件选择慢慢阅读或是直接跳到 完整代码

接口准备

既然要使用axios,肯定需要准备相应的后端环境。自己手撸后端接口、使用Mock.js或者在线的Mock平台等都可以,这里就不详述该过程了,笔者用的是fastmock(在线平台),下面是接口相关内容:

// GET /api/success
{
  "code": 0,
  "message": "请求成功",
  "data": {
    "name": "管理员"
  }
}
// GET /api/fail
{
  "code": -1,
  "message": "请求失败:XXX错误!",
  "data": null
}

实验环境

首先对axios做一个最基础的封装,并编写调用上述两个测试接口的api,从js转型过来的小伙们都可以轻松地写出以下代码:

// @/utils/request.ts
import axios from 'axios'

const request = axios.create({
  baseURL: '/api'
})

export default request

// @/api/test.ts
export const successApi = () => {
  return request({
    url: '/success',
    method: 'get'
  })
}

export const failApi = () => {
  return request({
    url: '/fail',
    method: 'get'
  })
}

最后简单的写下调用上述接口的页面,笔者用的是Vue3,这里用其它任意框架都行:

注:这里为简化代码,就不编写消息提示的组件,直接使用 console.logconsole.error来替代

// App.tsx
import { successApi, failApi } from '@/api/test'

export default {
  name: 'App',
  setup () {
    // 处理点击事件
    const handleClick = async (isSuccess: boolean) => {
      const api = isSuccess ? successApi : failApi
      const res = await api()
      if (res.data.code === 0) {
        console.log(res.data.message) // 成功消息提示
        console.log(res.data.data.name)
      } else {
        console.error(res.data.message) // 失败消息提示
      }
    }
    // render 函数
    return () => (
      <div>
        <button onClick={ () => handleClick(true) }>成功</button>
        <button onClick={ () => handleClick(false) }>失败</button>
      </div>
    )
  }
}

实验环境搭建完成,测试下两个接口的是通的就可以继续啦。

引入 Response 拦截器

上述代码实现了功能逻辑,但是却有两个明显的问题:

  1. 很多接口都会有消息提示,直接在组件中写消息提示会重复大量代码逻辑。
  2. res.data.data.name调用链太长,实际上,在组件中我们往往只会用到res.data中的部分。 这两个问题都可以通过引入axios自带的 Response 拦截器来解决, 如果用js的话,以下代码就解决问题了:
// @/utils/request.ts
request.interceptors.response.use((response) => {
  const { data } = response
  data.code === 0
    ? console.log(data.message) // 成功消息提示
    : console.error(data.message) // 失败消息提示
  return data
})

// App.tsx
import { successApi, failApi } from '@/api/test'

export default {
  name: 'App',
  setup () {
    // 处理点击事件
    const handleClick = async (isSuccess: boolean) => {
      const api = isSuccess ? successApi : failApi
      const data = await api()
      if (data.code === 0) {
        console.log(data.data.name)
      }
    }
    // render 函数
    return () => (
      <div>
        <button onClick={ () => handleClick(true) }>成功</button>
        <button onClick={ () => handleClick(false) }>失败</button>
      </div>
    )
  }
}

写完之后发现编译器报错了,原因是const data = await api()中的data被编译器解析为AxiosResponse<any>,而该类型中没有code这个属性,执行到if (data.code === 0)这句时就会报错。看过axios源码或是对axios比较熟悉的小伙伴应该都知道AxiosResponse<any>类型对应的是const { data } = response中response的类型,在没有拦截器的时候的确返回的也是这个response。

总结一下原因:axios并不会根据传入的Response拦截器的函数类型去对自身的返回类型进行变动,所以当Response拦截器的返回类型不是AxiosResponse<any>时就会出现类似的编译器问题。

简单粗暴的解决方案

对于这个问题,一行代码也能解决:

// App.tsx
const data: any = await api()

编译器不再报错,所有逻辑也正常进行。

自定义Response操作

但俗话说 一入any深似海,从此类型是路人,虽说目前data的类型的确是any,但是上述做法过于简单粗暴,不利于后续的深入封装,这里可采用自定义Response操作来解决:

// @/utils/request.ts
import axios, { AxiosRequestConfig } from 'axios'

const instance = axios.create({
  baseURL: '/api'
})

const request = async (config: AxiosRequestConfig) => {
  const { data } = await instance(config)
  data.code === 0
    ? console.log(data.message) // 成功消息提示
    : console.error(data.message) // 失败消息提示
  return data
}

export default request

修改之后,可以将App.tsx中的any给去掉了:

// App.tsx
const data = await api()

OK,编译器不会再报错了。

使用泛型 - 返回类型声明

上面一通操作猛如虎,但是data的类型还是any,当我们对其进行操作时,也没有享受到ts带来的类型检查与只能提示,在我们与后端进行通信的时候,一般都会有固定的格式,所以我们可以再对返回类型进行声明:

// @/types/index.ts
interface MyResponseType {
  code: number;
  message: string;
  data: any;
}

axios也为我们提供了一个非常友好的泛型方法:AxiosInstance.request,可指定response.data的返回类型:

// @/utils/request
import { MyResponseType } from '@/types'

const request = async (config: AxiosRequestConfig): Promise<MyResponseType> => {
  const { data } = await instance.request<MyResponseType>(config)
  data.code === 0
    ? console.log(data.message) // 成功消息提示
    : console.error(data.message) // 失败消息提示
  return data
}

现在,在data上调用code、message等属性时已经能享受到ts带来的智能提示。如果需要与多个后端通信,也只需要相应地声明多个返回类型并编写对应的请求函数即可。

双层泛型 - 更进一步封装

经过上述操作,封装已经基本完善了,不过还是略有不足。在真实场景下,我们已经根据后端提供的接口文档,知道了接口/api/success必定是返回一个带有name属性的类型,这个类型也是MyResponseType中data属性的类型,但是我们在App.tsx中使用data.data时,依旧是一个any类型,不能有效地为我们生成name属性的智能提示。可以在发起请求的时候就声明返回结果中内层data的类型来解决这个问题

首先定义内层data类型(这里定义为User),并将MyResponseType改为泛型类型:

// @/types/index.ts
export interface MyResponseType<T = any> {
  code: number;
  message: string;
  data: T;
}

export interface User {
  name: string;
}

然后将request函数也改造成泛型函数:

// @/utils/request
const request = async <T = any>(config: AxiosRequestConfig): Promise<MyResponseType<T>> => {
  const { data } = await instance.request<MyResponseType<T>>(config)
  data.code === 0
    ? console.log(data.message) // 成功消息提示
    : console.error(data.message) // 失败消息提示
  return data
}

在发起请求时指定泛型类型:

// @/api/test.ts
export const successApi = () => {
  return request<User>({
    url: '/success',
    method: 'get'
  })
}

export const failApi = () => {
  return request<User>({
    url: '/fail',
    method: 'get'
  })
}

回到App.tsx中,发现外层data被解析为MyResponseType<User>类型,内层data被解析为User类型,使用data.data.name时,可以享受到智能提示了。

// App.tsx
const data = await api()
if (data.code === 0) {
    console.log(data.data.name)
}

错误处理 - 最后的完善

ES6为我们带来的async await语法可以帮助我们摆脱then catch的多层回调地狱,不过执行异步操作的代码块还是需要包裹在try catch块中,为了精简代码、减少嵌套,避免每次调用异步请求都用try catch来进行包裹,笔者这里并没有使用try catch块,而是直接通过Response中的code属性来判断请求是否成功(0表示成功,-1表示失败),为了进一步减少代码嵌套可以这样修改(请求成功后往往都会有后续操作,请求失败已在自定义请求中进行消息提示,一般不会再有后续操作):

// App.tsx
const data = await api()
if (data.code !== 0) {
    // 如果有请求失败的逻辑,在此执行
    return
}
// 执行请求成功的逻辑
console.log(data.data.name)

不过如果完全不使用try catch块的话,遇上错误码为4xx,5xx的错误,就无法捕获,且用户收不到消息提示,这样的体验显然是不好的,于是我们再在拦截器中对这些错误进行统一捕获并处理:

// @/utils/request
const request = async <T = any>(config: AxiosRequestConfig): Promise<MyResponseType<T>> => {
  try {
    const response = await instance(config)
    const data: MyResponseType<T> = response.data
    data.code === 0
      ? console.log(data.message) // 成功消息提示
      : console.error(data.message) // 失败消息提示
    return data
  } catch (err) {
    const message = err.message || '请求失败'
    console.error(message) // 网络错误消息提示
    return {
      code: -1,
      message,
      data: null as any
    }
  }
}

注:在catch代码块中将data属性强制转化为any只是为了规避MyResponseType中data的类型检查,在请求已经报错的情况下,data中的内容不应该再被使用

完整代码

附上完整代码以供参考:

axios封装代码

// @/types/index.ts
export interface MyResponseType<T = any> {
  code: number;
  message: string;
  data: T;
}

// @/utils/request.ts
import axios, { AxiosRequestConfig } from 'axios'
import { MyResponseType } from '@/types'

const instance = axios.create({
  baseURL: '/api'
})

const request = async <T = any>(config: AxiosRequestConfig): Promise<MyResponseType<T>> => {
  try {
    const { data } = await instance.request<MyResponseType<T>>(config)
    data.code === 0
      ? console.log(data.message) // 成功消息提示
      : console.error(data.message) // 失败消息提示
    return data
  } catch (err) {
    const message = err.message || '请求失败'
    console.error(message) // 失败消息提示
    return {
      code: -1,
      message,
      data: null as any
    }
  }
}

export default request

使用示例

// @/types/index.ts
export interface User {
  name: string;
}

// @/api/test.ts
import { User } from '@/types'
import request from '@/utils/request'

export const successApi = () => {
  return request<User>({
    url: '/success',
    method: 'get'
  })
}

export const failApi = () => {
  return request<User>({
    url: '/fail',
    method: 'get'
  })
}

深入学习

本文对axios的封装可以满足最常用的应用场景,如果业务场景需要更深层次的封装,笔者推荐参考下 vue-vben-admin,该仓库是一个基于Vue3+TS的后台管理系统模板,其对axios进行了深层次的封装。