写一个Axios的基础封装与使用

238 阅读7分钟

前言

Axios 也是用的比较多的一个库了,其属于 XMLHttpRequests 类型请求,与 fetch 不一样,XMLHttpRequests比较友好的是,支持上传下载进度条,而 fetch 不支持,因此也有不少人更倾向于,基于 XMLHttpRequestsAxio(基于 fetch 就是前面用的 umi-request 了)

也可以理解为,如果原生不支持的基础功能,基本上三方组件也不支持了

Axios参考文档

案例demo

安装 axios

安装 axios 库

yarn add axios
//npm install axios
//pnpm add axios

axios 基础使用与拦截

基础使用

axios 可以直接使用,也可以使用 .create 创建,还有其他参数,则可以点击进去查看,这里不多介绍了,.get .post后面也是可以直接跟 url 等信息的

import axios from 'axios'

axios.get(url)

//我们可以创建一个更独立的axios对象,方便我们特殊时候使用多套基础配置
export const instance = axios.create({
  baseURL: '',
  timeout: 15000,
})

instance.get(url, data)

请求拦截

请求拦截(.interceptors.request.use),一般处理身份认证 token 使用,也有隐晦放置一些参数处理的

//拦截器处理请求体 request
instance.interceptors.request.use((config) => {
  //可以做token处理,也可以做其他的
  config = handleAuth(config)
  return config
})

const handleAuth = <T>(config: T) => {
  //处理token,当然 token第一次获取到保存起来就行,没必要每次都 getItem
  token = token || localStorage.getItem('token') || ''
  return {
    ...config,
    'X-Auth-Token': token,
    // 'token': token,
  }
}

响应拦截

响应拦截(.interceptors.response.use),我们一般统一处理数据,或者做前面一些文章提到的 双 token 无感刷新,这里就做一个无感刷新吧,对于参数处理,后面在介绍,有不同的思路

//拦截器处理响应体 response,我们顺便做一个无感刷新的
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const { data, config } = error.response

    if (data.statusCode === 401 && !config.url.includes('/refresh')) {
      const res = await refreshToken()
      //使用config重发
      if (res.status === 200) {
        return api(config)
      } else {
        alert(data.message || '登录过期,请重新登录')
      }
    } else {
      //如果有需要处理其他的也可以直接罗列,非正常的错误,都不给用户展示,可以直接打印
      return error.response
    }
  },
)

const refreshToken = async () => {
  const res = await api.get('/refresh', {
    params: {
      token: localStorage.getItem('refresh_token'),
    },
  })
  localStorage.setItem('token', res.data.accessToken)
  localStorage.setItem('refresh_token', res.data.refreshToken)
  return res
}

封装方案一(返回数据和错误信息同时返回)

这种方案也是以前看过的一篇文章写到的,主要是将返回结果、错误结果,放置到一个元组中,同时返回,如果没有错误信息,则成功,有错误信息看情况处理错误信息即可

这种方案个人也是比较喜欢的,能够节省不少代码,并且大家代码开发思路结构一样,也很舒服,缺点也很明显,不是很灵活,比较适合后端规范性比较强的情况

封装get请求

我们直接使用 axios 或者 自己创建一个 axios 对象即可,直接调用其 get 方法,然后无论成功或者失败都是 resolve,这样就不会走 .catch 了,直接以元组的方式,同时返回错误和结果,没问题时,错误返回 null 即可,可以通过 err 参数判断是否正确

ps:一般元组返回为 [err, res],也就是错误信息和正确数据,可能是一般要优先判断 err 的原因吧

import { api } from '.'

export const get = <T = unknown>(
  url: string,
  params: Record<string, unknown> = {},
  options: AxiosRequestConfig<unknown> = {},
): Promise<[ResponseType<T> | null, unknown]> => {
  return new Promise((resolve) => {
    api
      .get(url, { params, ...options })
      .then((result) => {
        const res: ResponseType<T> = result.data
        if (res.code === 200) {
          resolve([null, res])
        } else if (res.code === -1) {
          //这种一般是有效,可以直接提示,此时res就是错误err了
          resolve([res, null])
        } else {
          //仍然是错误的,需要提示,这种一般统一提示,此时res就是错误err了
          resolve([res, null])
        }
      })
      .catch((err) => {
        //仍然是错误的,需要提示,这种一般统一提示
        resolve([err, null])
      })
  })
}

使用如下所示

const [err, res] = await get('userinfo')
if (!err) {
    //这就是正确的结果
}
//实际上错误,可以这里处理,也可以统一弹窗处理

封装post(带常见的form、json类型参数)

而 post 使用还是不太一样,因为我们平时开发过程中,不同的人使用的 Content-Type 他可能不一样,这里 umi-request 分成了两个 form、json,当然 json 也是最常见的(我们开始的开发就是 form)

  • form: Content-Type: application/x-www-form-urlencoded;
  • json: Content-Type: application/json;

我们需要添加一个 requestType 参数,用于统一处理我们的 post请求 的 content-type 类型

import axios, { type AxiosRequestConfig } from 'axios'
import { api } from '.'

//目前就前两种主流 Content-Type:application/x-www-form-urlencoded;  Content-Type:application/json;
//上传文件一般不用通用接口,忽略
export type RequestType = 'form' | 'json'

//设置默认类型为 json
const defaultRequestType = 'json'

type ResponseType<T = unknown> = {
  code?: number
  data?: T
  msg?: string
}

//分装请求,逻辑和 get 差不多,只不过加入了 Content-Type 的处理
export const post = <T = unknown>(
  url: string,
  data: Record<string, unknown> = {},
  options: AxiosRequestConfig = {},
  requestType: RequestType = defaultRequestType,
): Promise<[ResponseType<T> | null, unknown]> => {
  return new Promise((resolve) => {
    const headers = options.headers || {}
    if (requestType === 'json') {
      headers['Content-Type'] = 'application/json'
    } else if (requestType === 'form') {
      headers['Content-Type'] = 'application/x-www-form-urlencoded'
    }
    api
      .post(url, data, {
        ...options,
        headers,
      })
      .then((result) => {
        const res: ResponseType<T> = result.data
        if (res.code === 200) {
          resolve([null, res])
        } else if (res.code === -1) {
          //这种一般是有效,可以直接提示
          resolve([null, res])
        } else {
          //仍然是错误的,需要提示,这种一般统一提示
          resolve([null, res])
        }
      })
      .catch((err) => {
        //仍然是错误的,需要提示,这种一般统一提示
        resolve([null, err])
      })
  })
}

upload上传

upload上传实际上走的也是 post,只不过是 form-data 数据罢了,其对应的 content-type也不一样,为 Content-Type: multipart/form-data,我们单独使用即可,一般不会走我们的通用业务接口,大多是走的云服务器,基本不需要额外配置参数(可能需要配置 timeout 哈)

///options.onUploadProgress进度条
export const upload = (url: string, data: FormData, options: AxiosRequestConfig = {}) => {
  // Content-Type:multipart/form-data
  return axios.post(url, data, {
    headers: {
      'Content-Type': 'multipart/form-data',
    },
    ...options,
  })
}

封装方案二(放弃细节封装)

这个方案比较简单,就是我们放弃我们上面的封装格式,直接使用默认的返回结果即可

为什么这么写,看后面的图

import type { AxiosRequestConfig } from 'axios'
import { api } from ".";

//可以写成这样,为何写成这样,可以看后面的图
export const request = <T>(url: string, options: AxiosRequestConfig) => {
  return api.request<T>({
    url,
    ...options,
  })
}

//当然我们可以再改装一下,可以进一步处理数据,也可以不处理
export const request = <T>(url: string, options: AxiosRequestConfig) => {
  return new Promise((resolve, reject) => {
    api
      .request<T>({
        url,
        ...options,
      })
      .then((result) => {
        resolve(result.data)
      })
      .catch(reject)
  })
}

//如果很多接口非常统一,那么我们在改进一下,特殊情况使用,这也是项目统一时,本人比较懒的使用方式
//单独使用可以额外加上一个静默通用也行,不一定使用 silent 0 不静默都提示,1仅仅提示有效,2全都不提示
export const request = <T extends ResponseType<T>>(
  url: string,
  options: AxiosRequestConfig,
  //silent = 0,
): Promise<ResponseType<T> | undefined> => {
  return new Promise((resolve, reject) => {
    api
      .request<T>({
        url,
        ...options,
      })
      .then((result) => {
        const res: ResponseType<T> = result.data
        if (res.code === 200) {
          resolve(res)
          return
        } else if (res.code === -1) {
          //可以做一个提示
          return Promise.reject(res)
        } else {
          return Promise.reject()
        }
      })
      .catch((err) => {
        //存在有效提示就有效提示,不存在且静默,那就不提示
        //if (silent === 2) {
          //return
        //}
        const msg = err?.msg || '似乎断开了与网络的连接'
        alert(msg)
        reject(msg)
      })
  })
}

下面的图,是之前使用 umi-request 的时候,找到的 openapi 自动生成的接口数据,只要 swagger 文档写好了,那么直接一键生成,很方便,为了保证格式一致,直接封装成上面那样子,参考这里

实际上在一些项目多的情况下,这种方案能节省不少时间,即使是方案一,实际上也就是节省部分代码或者规范部分判断,实际上并没有节省太多

image.png

最后

就介绍到这里吧,实际使用也挺简单的,当然直接用也没毛病

对于统一处理的情况,实际上个人觉得,第一种挺好用的,但是还不够懒,第二种懒得就可以了,openapi + 统一处理,懒到极点,挺好

ps:再懒请求头的 token 还是要处理的,毕竟再懒也不能完不成功能😂

第一种,偏向于传统,需要处理的东西少,使用也比较简单,仍然要写判断和处理罢了😂