记录axios的一次过渡封装

我正在参加「掘金·启航计划」

首先了解axios的作用

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

从axios的作用上来说,对请求的封装已经很到位,但是我们有时候,就是要针对一些请求拦截和响应拦截进行一系列的封装,简单来说,封装相当于自己的项目来说,够用就行,但是我就偏不,下面我就要对axios进行一些很变态的操作,那大概实现的就是以下功能

  • 无处不在的代码提示
  • 灵活可配置的请求拦截器和响应拦截器
  • 可取消请求和避免重复请求
  • 可灵活配置请求头,超时时间等
  • 可实现请求失败后的重新请求发送功能
  • 可实现无感刷新token

基础的封装

首先我们实现一个基础的版本,实例代码如下

// axios.ts
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig } from 'axios'

export class VAxios {
  private axiosInstance: AxiosInstance
  private readonly options: CreateAxiosOptions

  constructor(options: CreateAxiosOptions) {
    this.options = options
    this.axiosInstance = axios.create(options)
  }
  requeset(config: AxiosRequestConfig){
    return this.instance.request(config)
  }
}

拦截器封装

首先我们封装一下拦截器,拦截器分为两种

  • 实例拦截器
  • 接口拦截器

那我们先提出来一个transform对象,来描述所有的拦截器方法

const transform: AxiosTransform = {
  /**
   * 处理响应数据,如果数据不是预期的格式,可直接抛出错误
   * */
  transformResponseHook(res, options) {},
  //请求之前处理config
  beforeRequestHook: (config, options) => {},
  //请求拦截器
  requestInterceptors: (config, options) => {},
  //请求错误拦截器
  requestInterceptorsCatch: (error) => {},
  //响应拦截器
  responseInterceptors: (res) => { },
  /**
   * 响应的错误处理
   * */
  responseInterceptorsCatch: (axiosInstance: AxiosResponse, error: any) => {}
    
}

类拦截器的实现

类拦截器是比较好实现的,只要对axios的实例调用interceptors调用响应拦截器和请求拦截器即可,调用setupInterceptors来调用拦截器方法

export class VAxios {
  private axiosInstance: AxiosInstance
  private readonly options: CreateAxiosOptions

  constructor(options: CreateAxiosOptions) {
    this.options = options
    this.axiosInstance = axios.create(options)
    this.setupInterceptors()
  }
  setupInterceptors(){
    //
    const {
      requestInterceptors,
      requestInterceptorsCatch,
      responseInterceptors,
      responseInterceptorsCatch
    } = transform
  }
     //请求拦截器部分
    this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
      // @ts-ignore
      if (requestInterceptors && isFunction(requestInterceptors)) {
        config = requestInterceptors(config, this.options)
      }
      return config
    }, undefined)

  	  //请求拦截器错误处理
    requestInterceptorsCatch &&
      isFunction(requestInterceptorsCatch) &&
      this.axiosInstance.interceptors.request.use(undefined, (error) => {
        return requestInterceptorsCatch(error)
      })
  	 //响应拦截器
    this.axiosInstance.interceptors.response.use((res) => {
      if (responseInterceptors && isFunction(responseInterceptors)) {
        res = responseInterceptors(res)
      }
      return res
    }, undefined)

    //响应拦截器的错误捕获
    responseInterceptorsCatch &&
      isFunction(responseInterceptorsCatch) &&
      this.axiosInstance.interceptors.response.use(undefined, (error) => {
        return responseInterceptorsCatch(this.axiosInstance, error)
      })
}
  • 在这里我们就是在对类的拦截器的方法进行了逻辑抽离,把我们需要对请求拦截器和响应拦截器的处理方法都放在了transform的方法类中

requestInterceptors请求拦截器处理

  • 在这里只是进行了一个简单的token加入,还可以加入其他逻辑
  //请求拦截器
  requestInterceptors: (config, options) => {
    const token = '123456'
    // @ts-ignore
    if (token) {
      // jwt token
      config.headers.Authorization = "Bearer" +  token
    }
    return config
  },
  //请求错误拦截器
  requestInterceptorsCatch: (error) => {
    console.log(error)
  },
    

responseInterceptorsCatch响应拦截器错误处理

  • 这里主要针对的就是对错误进行错误,包括采用是弹窗模式的错误提醒,还是log形式的错误提示
  responseInterceptors: (res) => {
    return res
  },
     /**
   * 响应的错误处理
   * */
  responseInterceptorsCatch: (axiosInstance: AxiosResponse, error: any) => {
    const { response, code, message, config } = error || {}
    const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none'
    const msg: string = response?.data?.error?.message ?? ''
    const err: string = error?.toString() ?? ''
    let errMessage = ''
    try {
      if (code === 'ECONNABORTED' && message.indexOf('timeout') != -1) {
        errMessage = '接口请求超时,请刷新页面充实'
      }
      if (err.includes('Network Error')) {
        errMessage = '网络异常,请检查您的网络连接是否正常'
      }
      if (errMessage) {
        if (errorMessageMode === 'modal') {
          console.log('model 提示错误')
        } else if (errorMessageMode === 'message') {
          console.log('message 提示错误')
        }
        return Promise.reject(error)
      }
      return Promise.reject(error)
    } catch (error) {
      throw new Error(error as unknown as string)
    }
    //检查各状态码,并进行对应的处理
    checkStatus(error?.response?.status, msg, errorMessageMode)
  } 
  • checkStatus方法,对各状态码的报错进行处理,可以自行错误进行提示
//checkStatus.ts
import type { ErrorMessageMode } from '#/axios'
import { useUserStoreWithOut } from '/@/store/modules/user'
export function checkStatus(status: number, msg: string, errorMessageMode: ErrorMessageMode) {
  let errMsg = ''
  const userStore = useUserStoreWithOut()
  switch (status) {
    case 400:
      errMsg = `${msg}`
      break
    case 401:
      //没有权限,清除token,退出登陆
      errMsg = '用户没有权限'
      userStore.setToken(undefined)
      ---逻辑自行完善--
      break
    case 403:
      errMsg = '用户得到授权,但是访问是被禁止的'
      break
    case 404:
      errMsg = '网络请求错误,未找到该资源'
      break
    case 405:
      errMsg = '网络请求错误,请求方法未允许'
      break
    case 408:
      errMsg = '网络请求超时'
      break
    case 500:
      errMsg = '服务器错误,请联系管理员!'
      break
    case 501:
      errMsg = '网络未实现'
      break
    case 502:
      errMsg = '网络错误!'
      break
    case 503:
      errMsg = '服务不可用,服务器暂时过载或维护!'
      break
    case 504:
      errMsg = '网络超时!'
      break
    case 505:
      errMsg = 'http版本不支持该请求!'
      break
    default:
  }
  if (errMsg) {
    if (errorMessageMode === 'modal') {
      console.log('model', errMsg)
    } else {
      console.log('log', errMsg)
    }
  }
}

接口拦截器

现在我们对单一接口进行拦截操作

  • 包括请求钱参数的处理
  • 返回后参数的处理
  request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
    let conf: CreateAxiosOptions = cloneDeep(config)
 		const { requestOptions } = this.options

    const opt: RequestOptions = Object.assign({}, requestOptions, options)

    const { beforeRequestHook, requestCatchHook, transformResponseHook } = transform || {}

    if (beforeRequestHook && isFunction(beforeRequestHook)) {
      conf = beforeRequestHook(conf, opt)
    }
    conf.requestOptions = opt
  	
    return new Promise((resolve, reject) => {
      this.axiosInstance
        .request(conf)
        .then((res) => {
          if (transformResponseHook && isFunction(transformResponseHook)) {
            try {
              const ret = transformResponseHook(res, opt)
              resolve(ret)
            } catch (error) {
              reject(error)
            }
            return
          }
          resolve(res as unknown as Promise<T>)
        })
        .catch((e: Error) => {
          if (requestCatchHook && isFunction(requestCatchHook)) {
            reject(requestCatchHook(e, opt))
            return
          }
          reject(e)
        })
    })
  }

beforeRequestHook请求钱config处理

  • 在请求之前处理config
  • 下面只是做了一个简单的配置项处理
//在初始化的配置里面有
let options = {
    //接口地址
    apiUrl: '',
    //接口拼接地址
    urlPrefix: '',
    //默认将 prefix,添加到url
    joinPrefix: true,
}
beforeRequestHook: (config, options) => {
  
  const { apiUrl, joinPrefix,urlPrefix } = options
  // 是否添加前缀
  if (joinPrefix) {
    config.url = `${urlPrefix}${config.url}`
  }
  //添加接口地址
  if (apiUrl && isString(apiUrl)) {
    config.url = `${apiUrl}${config.url}`
  }
  //还可以自定义添加对config的处理
  return config
},

transformResponseHook统一处理返回数据,想要的格式

  • 根据requestOptions的配置项,来处理判断逻辑
  transformResponseHook(res, options) {
    const { isReturnNativeResponse, isTransformResponse } = options
    //是否返回原生响应头 比如:需要获取响应头时使用该属性
    if (isReturnNativeResponse) {
      return res
    }
    //不进行任何处理,直接返回
    //用于页面代码可能需要直接获取code,data,message这些信息时开启
    if (!isTransformResponse) {
      return res.data
    }
    //错误是返回
    const { data } = res
    if (!data) {
      throw new Error('请求出错,请稍后重试')
    }
    //这里code,result,message为后台同意字段,需要在type.ts内修改为项目自己的接口返回格式
    const { code, result, message } = data
    //根据自己项目逻辑自己修改
    const hasSuccess = data && Reflect.has(data, 'code')
    if (hasSuccess) {
      return result
    }

    //在此处根据自己项目的实际情况对不同的code执行不同的操作
    //如果不希望中断当前请求,请return数据,否则直接抛出异常即可
    let timeoutMsg = ''
    switch (code) {
      case ResultEnum.TIMEOUT:
        timeoutMsg = '登陆超时,请重新登陆'
        //退出系统的逻辑
        break
      default:
        if (message) {
          timeoutMsg = message
        }
    }
    if (options.errorMessageMode === 'model') {
      console.log('model', timeoutMsg)
    } else if (options.errorMessageMode === 'message') {
      console.log('message', timeoutMsg)
    }

    throw new Error(timeoutMsg)
  },

取消请求的封装

我们需要将所有的请求保存到一个集合(也可以使用数组,也可以用Map,我使用的是Map)中,然后具体需要去调用这个集合中的某个取消方法

  • 先参考axios中使用 cancel token 取消请求的方法
  • 简单说明实现思路,就是在请求拦截器时,将请求添加到集合中,在响应拦截器时,将其取消,如果判断出来method,url拼接出来的key值相同的请求,则取消重复发送
  • config中的配置说明,可见AxiosRequestConfig这个接口中的配置
//
import type { Canceler, AxiosRequestConfig } from 'axios'
import axios from 'axios'
import { isFunction } from 'lodash-es'

let pendingMap = new Map<string, Canceler>()
//创建key值作为唯一值
export const getPendingUrl = (config: AxiosRequestConfig) => 
  [config.method, config.url].join('&')

export class AxiosCanceler {
  addPending(config: AxiosRequestConfig) {
    this.removePending(config)
    const url = getPendingUrl(config)
    // @ts-ignore
    config.cancelToken =
      config.cancelToken ||
      new axios.Cancel((cancel) => {
        if (!pendingMap.has(url)) {
          pendingMap.set(url, cancel)
        }
      })
  }
  //取消全部请求
  removeAllPending() {
    pendingMap.forEach((cancel) => {
      cancel && isFunction(cancel) && cancel()
    })
  }

  removePending(config: AxiosRequestConfig) {
    const url = getPendingUrl(config)
    if (pendingMap.has(url)) {
      const cancel = pendingMap.get(url)
      cancel && cancel()
      pendingMap.delete(url)
    }
  }

  /**
   * @description: reset
   */
  reset(): void {
    pendingMap = new Map<string, Canceler>()
  }
}

改造请求拦截器的方法

    // @ts-ignore
    //请求拦截器部分
    this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
      //将请求添加到队列中
      axiosCanceler.addPending(config)

      if (requestInterceptors && isFunction(requestInterceptors)) {
        config = requestInterceptors(config, this.options)
      }
      return config
    }, undefined)

改造响应拦截器

   //响应拦截器
  this.axiosInstance.interceptors.response.use((res) => {
    //如果响应了,那么就清除掉请求
    res && axiosCanceler.removePending(res.config)
    if (responseInterceptors && isFunction(responseInterceptors)) {
      res = responseInterceptors(res)
    }
    return res
  }, undefined)

添加请求失败自动重发功能

改造responseInterceptorsCatch

  /**
   * 响应的错误处理
   * */
	let retryRequest  = {
      isOpenRetry: true,
      count: 5,
      waitTime: 100
    }
  responseInterceptorsCatch: (axiosInstance: AxiosResponse, error: any) => {

    //添加自动重试机制 保险起见 只针对于GET请求
    const retryRequest = new AxiosRetry()
    const { isOpenRetry } = retryRequest
    config.method?.toUpperCase() === 'GET' && isOpenRetry 
      && retryRequest.retry(axiosInstance, error)
    return Promise.reject(error)
  }
  • AxiosRetry.ts
import type { AxiosInstance, AxiosError } from 'axios'

export class AxiosRetry {
  /**
   * 重试
   * */
  retry(AxiosInstance: AxiosInstance, error: AxiosError) {
    // @ts-ignore
    const { config } = error.response
    // eslint-disable-next-line no-unsafe-optional-chaining
    const { waitTime, count } = retryRequest
    config.__retryCount = config.__retryCount || 0
    if (config.__retryCount >= count) {
      return Promise.reject(error)
    }
    config.__retryCount += 1
    return this.delay(waitTime).then(() => AxiosInstance(config))
  }
  /**
   * 延迟
   */
  private delay(waitTime: number) {
    return new Promise((resolve) => setTimeout(resolve, waitTime))
  }
}

写在最后

我们其实还可以针对于axios封装无感刷新token,对上传文件方法进行封装,但是本人感觉,太多的封装导致代码的阅读成本太高,相较于而言,本人更喜欢vue-element-admin的axios的封装方法,以上的封装方式就是为了练习而言,可以参考,完善自己的思维

代码参考地址:github.com/vbenjs/vue-…

原文链接:https://juejin.cn/post/7231921877467316261 作者:前端胡汉三

(0)
上一篇 2023年5月11日 上午11:13
下一篇 2023年5月12日 上午10:05

相关推荐

发表回复

登录后才能评论