likes
comments
collection
share

vue3 admin 保姆教学指南|关于使用typescript二次封装Axios的特别说明

作者站长头像
站长
· 阅读数 38

如果对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先创建一个实例,然后配置它的baseUrltimeout

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 })
  },
}

对于这几种方法,他们的接收参数的形式不一样的,拿getpost距离,一个接收的是params,一个接收的dataget方法要想传递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对象,我们再看看后端返回的数据格式就一目了然了:

vue3 admin 保姆教学指南|关于使用typescript二次封装Axios的特别说明

现在你对后端返回的数据如何进行约束是不是有了一定的了解了?

接下来我们在看一个分页列表的接口如何进行约束的。

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 },
  )
}

首先我们看看后台返回的数据:

vue3 admin 保姆教学指南|关于使用typescript二次封装Axios的特别说明

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目录划分:

vue3 admin 保姆教学指南|关于使用typescript二次封装Axios的特别说明

这样我们在使用的时候,只需要从import { xxx } from "@/api"引入即可。

文章教程系列

转载自:https://juejin.cn/post/7214146630467305530
评论
请登录