前言

axios 是前端开发的基本工具之一,它的封装早就不新鲜了
本文分为两部分:一是 axios 基本封装示例;二是非必要封装,列举个人开发中遇到的一些较为实用的封装需求(自定义方法、监听上传/下载进度、中断请求、接口loading)。
本文示例基于 axios@0.21.1


一、基本封装

axios的基本封装网上有很多,内容大差不差。这里,参考axios官方文档以及GitHub高星开源项目的axios封装:

axios - Interceptors - github
vue-element-admin

import axios from 'axios'
import store from '@/store'
import { getToken } from '@/utils/auth'

// create an axios instance
const service = axios.create({
  // baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  // withCredentials: true, // send cookies when cross-domain requests
  timeout: 5000 // request timeout
})

// Add a request interceptor
service.interceptors.request.use(
  config => {
    // do something before request is sent
    if (store.getters.token) {
      // let each request carry token
      // ['X-Token'] is a custom headers key
      // please modify it according to the actual situation
      config.headers['X-Token'] = getToken()
    }
    return config
  },
  error => {
    // do something with request error
    return Promise.reject(error)
  }
)

// Add a response interceptor
service.interceptors.response.use(
  /**
   * If you want to get http information such as headers or status
   * Please return  response => response
   */

  /**
   * Determine the request status by custom code
   * Here is just an example
   * You can also judge the status by HTTP Status Code
   */
  response => {
    // do something with response data
    const res = response.data

    // if the custom code is not 20000, it is judged as an error.
    if (res.code !== 20000) {
      // TODO: Message prompt
      console.error(res.message || 'Error')

      // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
        // TODO: to re-login
      }
      // reject
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res
    }
  },
  error => {
    // do something with response error
    return Promise.reject(error)
  }
)

export default service

示例中,baseURL、消息提示、消息确认、token获取需要结合具体项目进行替换。
封装非常简洁,就是创建一个axios实例,设置baseURL/timeout,添加请求拦截器、响应拦截器。
请求拦截器中带上用户登录令牌,响应拦截器中根据响应数据中的code(同后端约定好)识别特殊响应(登录失效/超时),并作对应的处理
使用封装后的方法:

import request from '@/utils/request'

export function fetchList(query) {
  return request({
    url: '/vue-element-admin/article/list',
    method: 'get',
    params: query
  })
}

上例中,request就是在request.js中创建并导出的axios实例,和直接导入并使用axios默认实例相比,两者的参数类型是一致的,也可以使用 .get .post 等别名。
上面的封装完成了最基础且重要的功能,抛出的实例与axios用法一样,但每一个使用该实例的,都会自动在请求头中添加登录令牌,自动拦截请求与响应。完美!

小结

最基本的封装就是这样,可以理解成:

import axios from 'axios'

const service = axios.create(config)
service.interceptors.request.use(requestHandler, requestErrorHandler)
service.interceptors.response.use(responseHandler, responseErrorHandler)

export default service

其中,config为默认的配置,请求拦截器、响应拦截器中分别设置正确处理与错误处理方法,上面的示例仅供参考,实现细节可根据具体项目需求调整。

请参考 axios 官网文档
axios - Request Config
axios - Interceptors


二、其它非必要封装

基本封装上一节就够了,本节的内容都是在基本封装的基础上,对一些非必要的需求作出的补充,而这些非必要的需求在有些项目中可能永远也用不上。实现过程因人而异

使用实例基于 vue@3.2.37

1. 自定义方法

如果存在某类需要固定添加/调整 axios 配置的接口,可能会造成代码冗余,我们希望方法仅包含与接口相关的url和数据。此时,可以如下封装:

// ...

const axiosBlob = (url, data, otherConfigs = {}) => {
  otherConfigs.responseType = 'blob'
  otherConfigs.timeout = 5000
  return new Promise((resolve, reject) => {
    service({
      method: 'get',
      url,
      params: data,
      ...otherConfigs
    }).then(resolve, reject)
  })
}

const axiosPostFormData = (url, data, otherConfigs = {}) => {
  otherConfigs.headers = { 'Content-Type': 'multipart/form-data; charset=UTF-8' }
  return new Promise((resolve, reject) => {
    service({
      method: 'post',
      url,
      data,
      ...otherConfigs
    }).then(resolve, reject)
  })
}

export { service, axiosBlob, axiosPostFormData }
export default service

使用

import request, { axiosBlob } from '@/utils/request'

export function fetchFile1(params) {
  return request({
    url: '/vue-element-admin/article/file',
    method: 'get',
    params,
    responseType: 'blob',
    timeout: 5000
  })
}

export function fetchFile2(params) {
  return axiosBlob('/vue-element-admin/article/file', params)
}

如上,可导出自定义方法,免去特定请求下反复填写固定的配置信息
如果偏好这种风格,可以统一封装 get/post/patch/put/delete 类请求,其它如上例中的两种特殊请求,可自行添加。

// ...

const axiosCustomFuncHandler = (method, url, data, otherConfigs = {}) => {
  return new Promise((resolve, reject) => {
    service({
      method,
      url,
      [method === 'get' ? 'params' : 'data']: data ? data : {},
      ...otherConfigs,
    }).then(resolve, reject)
  })
}
const axiosGet = (url, data, otherConfigs) => axiosCustomFuncHandler('get', url, data, otherConfigs)
const axiosPost = (url, data, otherConfigs) => axiosCustomFuncHandler('post', url, data, otherConfigs)
const axiosPut = (url, data, otherConfigs) => axiosCustomFuncHandler('put', url, data, otherConfigs)
const axiosPatch = (url, data, otherConfigs) => axiosCustomFuncHandler('patch', url, data, otherConfigs)
const axiosDelete = (url, otherConfigs) => axiosCustomFuncHandler('delete', url, undefined, otherConfigs)
const axiosPostFormData = (url, data, otherConfigs = {}) => {
  otherConfigs.headers = { 'Content-Type': 'multipart/form-data; charset=UTF-8' }
  return axiosCustomFuncHandler('post', url, data, otherConfigs)
}
const axiosBlob = (url, data, otherConfigs = {}) => {
  otherConfigs.responseType = 'blob'
  otherConfigs.timeout = 5000
  return axiosCustomFuncHandler('get', url, data, otherConfigs)
}

export {
  service,
  axiosGet,
  axiosPost,
  axiosPut,
  axiosPatch,
  axiosDelete,
  axiosBlob,
  axiosPostFormData,
}
export default service

请注意自定义方法与实例方法别名的区别:
axios实例方法:request(config)
axios实例方法别名:request.get(url[, config])
自定义方法:axiosGet(url, params, config)

config 优先级

axios - Config Defaults

在自定义方法中,设置了固定的请求配置到axios实例上。axios默认实例也可以设置默认配置,而axios实例方法中,同样可以传递请求配置。他们之间存在优先级:
Global axios defaults < Custom instance defaults < Config argument for the request

2. 监听上传/下载进度

axios提供了监听上传/下载进度的事件: axios - Request Config

  // `onUploadProgress` allows handling of progress events for uploads
  // browser only
  onUploadProgress: function (progressEvent) {
    // Do whatever you want with the native progress event
  },

  // `onDownloadProgress` allows handling of progress events for downloads
  // browser only
  onDownloadProgress: function (progressEvent) {
    // Do whatever you want with the native progress event
  },

可以看到他们的参数类型是相同的(ProgressEvent
最直接的使用方式就是导入封装好的service实例,定义该监听方法

<script setup>
  import request from '@/utils/request'
  import { ref, onMounted } from 'vue'

  let progress = ref(0)

  onMounted(() => {
    request({
      url: '/vue-element-admin/article/list',
      method: 'get',
      params: query,
      onDownloadProgress: function (progressEvent) {
        if (progressEvent.lengthComputable) {
          const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
          progress.value = percentCompleted
        } else {
          progress.value = 100
        }
      }
    }).then(res => {
      // ...
    })
  })
</script>
<template>
  <div>Loading...{{ progress }}%</div>
</template>

那每个需要监听下载进度的都这样写一遍的话,一方面会产生很多冗余代码,另一方面也不方便统一维护监听方法

思路:
将一个响应式变量(下载/上传进度)通过 request config 传给 axios 实例,在请求拦截器中绑定监听事件。监听事件会更改响应式变量的值

// 下载进度监听事件(更新封装方法传入的响应式变量——进度)
const handleDownloadProcess = (progressEvent, progress) => {
  if (progressEvent.lengthComputable) {
    const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
    progress.value = percentCompleted
  } else {
    progress.value = 100
  }
}
// 上传进度监听事件,同 handleDownloadProcess
const handleUploadProcess = handleDownloadProcess

// request interceptor
service.interceptors.request.use(
  config => {
    // ...

    // set download/upload progress' event listeners
    if (config.downloadProgress) {
      config.onDownloadProgress = progressEvent =>
        handleDownloadProcess(progressEvent, config.downloadProgress)
    }
    if (config.uploadProgress) {
      config.onUploadProgress = progressEvent =>
        handleUploadProcess(progressEvent, config.uploadProgress)
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

上面的示例中,约定了两个配置名(downloadProgress, uploadProgress),通过判断各自对应的变量是否存在来绑定监听事件。
比如,想绑定下载进度监听事件,需要在 request config 中传递 downloadProgress 变量。严谨一点的话,请求拦截器中最好检测下它是否是响应式变量。
由于是在实例的请求拦截器中处理的,无论是直接调用实例还是封装后的方法,都可以实现下载进度监听。同手动绑定监听事件相比,写法如下:

request({
  url: '/vue-element-admin/article/list',
  method: 'get',
  params: query,
  downloadProgress: progress
}).then(res => {
  // ...
})

下载大小未知时的处理

如果下载大小未知,那上面的监听方法中,会直接将进度置为100,而实际上并不是,仍在下载中。

可以作假进度。但监听下载进度就是为了知道进度,并在前端页面上作下载进度提示,假进度毫无意义。在服务器未返回大小的情况下,可以将进度置为一个特定值,在对应页面监听到该特定进度值时,不作下载进度提示,转为普通loading提示。

// 下载进度监听事件(更新封装方法传入的响应式变量——进度)
const handleDownloadProcess = (progressEvent, progress) => {
  if (progressEvent.lengthComputable) {
    const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
    progress.value = percentCompleted
  } else {
    progress.value = -1
  }
}
// response handler
const respHandler = response => {
  if (response.config.downloadProgress) {
    response.config.downloadProgress.value = 100
  }
  // ...
}

如果要作假进度提示的话,参考如下:

const handleDownloadProcess = (progressEvent, progress) => {
  if (progressEvent.lengthComputable) {
    const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total)
    progress.value = percentCompleted
  } else {
    let tmp = progress.value + (100 - progress.value) / 50
    tmp = +tmp.toFixed(2)
    if (tmp >= 100) tmp = 99.99
    progress.value = tmp
  }
}

注意,都需要在响应完成时,将进度值置为100。仅作参考,具体实现因人而异。

3. 接口loading

掘金上看到的一篇文章,针对接口loading状态的一种封装:axios和loading不得不说的故事
它针对的业务场景如下:

const loading = ref(false)

function getData () {
  loading.value = true
  axios.get('/vue-element-admin/article/list').then(res => {
    // ...
  }).finally(() => {
    loading.value = false
  })
}

之前从来没想过封装接口loading,可能是它所能抽离的公共代码很少。
思路:
将一个响应式变量(loading)通过 request config 传给 axios 实例,在请求拦截器更改它为true(表示开始请求接口),在响应拦截器中更改它为false(表示接口响应完毕)

// request interceptor
service.interceptors.request.use(
  config => {
    // ...
    if (config.loading) {
      config.loading.value = true
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)
// response interceptor
service.interceptors.response.use(
  response => {
    if (response.config?.loading) {
      response.config.loading.value = false
    }
    // ...
  },
  error => {
    if (error.config?.loading) {
      error.config.loading.value = false
    }
    return Promise.reject(error)
  }
)

使用:

const loading = ref(false)

function getData () {
  axios.get('/vue-element-admin/article/list', { loading }).then(res => {
    // ...
  })
}

4. 中断请求

有时候,出于性能方面的考虑,我们希望能主动中断axios正进行的请求,例如路由跳转
axios提供了两种方法中断请求,详见文档:axios - Cancellation

  • signal
  • cancelToken(deprecated since v0.22.0)

由于本人使用的axios版本低于v0.22.0,这里使用后者进行封装

// ...

// request interceptor
service.interceptors.request.use(
  config => {
    // ...

    // set cancel token
    if (config.useCancelToken) {
      const CancelToken = axios.CancelToken
      config.cancelToken = new CancelToken(cancel => {
        config.useCancelToken.value = cancel
      })
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

在请求拦截器中检测是否存在 useCancelToken 属性,存在则添加 cancelToken 属性方法到配置中,方法内将响应式变量 useCancelToken 的值指向 cancel 方法。
使用:

const cancelToken = ref()
function getAllData() {
  axios.get('/demo', { useCancelToken: cancelToken })
}

onBeforeUnmount(() => {
  cancelToken.value?.()
})

通过 useCancelToken 属性开启 axios Cancellation,组件销毁前中断当前组件内的请求。
axios的中断封装到此结束。

手动中断后的提示

当请求被手动中断后,会触发响应拦截器的错误处理方法(respErrorHandler):

import axios from 'axios'
import { ElMessage } from 'element-plus'

// ...

service.interceptors.response.use(
  response => {
    // ...
  },
  error => {
    // do something with response error
  	if (error instanceof axios.Cancel) ElMessage(error.message || 'Request cancelled')
    return Promise.reject(error)
  }
)

当手动中断时,此error的类型为 axios.Cancel,如果有需求,可添加手动中断后的提示

中断功能的使用封装

上例中可以看到使用该中断功能时,有些繁琐,对于组件内的每一个需要使用中断功能的接口,都需要:

  • 定义一个响应式中断方法变量
  • 添加到请求配置中
  • 添加 onBeforeUnmount 方法,并在其内调用前面的每个中断方法

这里依据个人风格提供一个axios中断功能的使用hooks,仅供参考:

import { ref, onBeforeUnmount } from 'vue'

/**
 * @description: 自动取消axios请求
 * @example
 * // import:
 * import autoCancelAxios from '@/use/auto-cancel-axios'
 *
 * const { addCancelToken } = autoCancelAxios()
 *
 * function getAllData() {
 *   axiosGet('/demo', { useCancelToken: addCancelToken() })
 * }
 */
export default () => {
  // cancelToken 列表
  const cancels = []
	// 添加 cancelToken
  function addCancelToken() {
    const currAxiosCancelToken = ref()
    cancels.push(currAxiosCancelToken)
    return currAxiosCancelToken
  }

  onBeforeUnmount(() => {
    try {
      cancels.forEach(cancelToken => {
        cancelToken.value?.()
      })
    } catch (error) {
      console.error('Failed to cancel axios', error)
    }
  })

  return { addCancelToken }
}

使用示例:

import autoCancelAxios from '@/use/auto-cancel-axios'

const { addCancelToken } = autoCancelAxios()

function getAllData() {
  axios.get('/demo', { useCancelToken: addCancelToken() })
}
function getData1() {
  axios.get('/demo1', { useCancelToken: addCancelToken() })
}
function getData2() {
  axios.get('/demo2', { useCancelToken: addCancelToken() })
}

总结

封装的目的在于方便自己使用,较少代码冗余、方便维护、提高开发效率,所以并不存在标准答案。
本文仅供参考,如有错误,望指正!

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐