axios 封装 http 请求详解

风雨同行  金牌会员 | 2024-6-24 00:50:53 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 800|帖子 800|积分 2400


前言

Axios 是一个基于 Promise 的 HTTP 库,它的概念及利用方法本文不过多赘述,请参考:axios传送门
本文重点讲述下在项目中是如何利用 axios 封装 http 请求。

一、预设全局变量

在 /const/preset.js 中配置预先设置一些全局变量
  1. window.$env = process.env.NODE_ENV === 'development' ? 'DEV' : 'PROD'
  2. // 默认开发环境
  3. let config = {
  4.   baseURL: location.origin,
  5.   httpBaseURL: location.origin + '/api',
  6.   webBaseURL: location.origin + location.pathname,
  7.   vipAddress: '/necp/mapp/sc', // 后端微服务的统一入口
  8. }
  9. // 生产环境
  10. if (window.$env !== 'DEV') {
  11.   if (location.href.indexOf('/ecs/') > -1) {
  12.     config.baseURL = location.href.replace(/\/ecs.+/, '')
  13.     config.httpBaseURL = config.baseURL
  14.   }
  15. }
  16. // 文件资源请求路径
  17. config.fileUrl = config.httpBaseURL + config.vipAddress + 'file/download'
  18. window.$globals = config
复制代码
在 main.js 中引入
  1. import Vue from 'vue'
  2. import './const/preset'
  3. // ...
  4. // 把 vue 示例挂载到 window 下
  5. window.$vm = new Vue({
  6.   render: h => h(App),
  7.   router
  8. }).$mount('#app')
复制代码
由于生产环境摆设的差异,http 请求的 baseURL 并非都是同一的,以是不但独配置默认的 axios.defaults.baseURL,而是通过此文件预设的变量进行设置。
全局预设变量中的 config.httpBaseURL 将添加到请求的 URL 中,对于代码中的 location.href.indexOf(‘/ecs/’) > -1 判定只是举例,可根据实际需求决定是否需要。
二、http 请求封装

1.配置全局 axios 默认值

  1. axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8'
  2. axios.defaults.timeout = 60000
  3. axios.defaults.crossDomain = true
复制代码
此三条配置分别对应以下作用:


  • 发送POST请求时,设置请求头的 Content-Type 字段为 ‘application/json;charset=UTF-8’ ,以便服务器精确解析请求的数据。
  • 发送请求默认的超时时间为 60s。
  • 允许跨域请求。
   提示:覆盖默认超时时间,可在 axios 发送请求的参数 config 对象中设置 timeout 属性即可
  2.配置请求拦截器

请求拦截器是在发送请求前实验的函数,它可以用于修改请求的配置或者在请求发送前进行一些操作。最常用的功能就是利用请求拦截器实现身份验证
一个常见的实现是用户登录之后,服务端会响应用户的登录信息,并且把用户的身份认证 token 存储到 cookie 中,然后在请求拦截器中将 cookie 中获取到的 token 设置到请求头中,每次发送请求都会携带上此 token 发送到服务端,服务端再获取请求头的 token 来判定用户是否登录状态或者登录已过期,作出不同的响应。
  1. axios.interceptors.request.use(
  2.   config => {
  3.     const token = cookie.get(TOKEN_COOKIE_KEY)
  4.     if (token) {
  5.       config.headers[TOKEN_REQ_KEY] = token
  6.     }
  7.     return config
  8.   },
  9.   error => {
  10.     return Promise.reject(error)
  11.   }
  12. )
复制代码
3.配置响应拦截器

响应拦截器是在吸取到响应后实验的函数,它可以用于修改响应的数据或者在吸取到响应后进行一些操作。
响应拦截器主要作用包括修改响应数据、错误处置惩罚、同一处置惩罚响应等功能,因把响应数据及错误的处置惩罚都放在了发送请求的回调中,以是只定义了最简单的响应拦截器。
  1. axios.interceptors.response.use(response => {
  2.   return response
  3. }, error => {
  4.   return Promise.reject(error)
  5. })
复制代码
4.发送请求的 request 函数

此函数吸取四个参数:请求方法,请求的 api 接口,请求参数,请求的 config 配置项,返回一个 Promise 的实例。此函数完成了正常响应处置惩罚、异常处置惩罚、重复请求取消等功能。
4.1 拼接完备的请求 url

  1. const apiInterceptor = api => {
  2.   if (api.startsWith('http')) { // 自定义请求路径
  3.     return api.slice(4)
  4.   }
  5.   if (api.startsWith('_SC_')) { // 项目统一的api前缀
  6.     api = $globals.vipAddress + api.slice(4)
  7.   }
  8.   return $globals.httpBaseURL + api
  9. }
  10. const request = async (method = 'post', api, params = {}, config = {}) => {
  11.   // 省略...
  12.   let url = apiInterceptor(api)
  13.   let opts = {
  14.     method,
  15.     url,
  16.     headers: config.headers || {},
  17.     withCredentials: config.withCredentials || true // 跨域请求时是否需要使用凭证
  18.   }
  19.   // 省略...
  20. }
复制代码
调用 apiInterceptor 函数来拼接完备的请求 url,假如 api 是以 http 开头,则表现自定义 api 的请求路径,否则请求路径利用 preset.js 中预设的全局变量来拼接完备的 url。
4.2 参数处置惩罚

  1. const jsonObj2FormData = jsonObj => {
  2.   let formData = new FormData()
  3.   Object.keys(jsonObj).forEach(key => {
  4.     if (jsonObj[key] instanceof Array) {
  5.       formData.append(key, JSON.stringify(jsonObj[key]))
  6.     } else {
  7.       formData.append(key, jsonObj[key])
  8.     }
  9.   })
  10.   return formData
  11. }
  12. // 省略...
  13. if (config.formDataFormat) {
  14.   opts.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
  15.   params = jsonObj2FormData(params)
  16. }
  17. if (method == 'post') {
  18.   opts.data = params
  19. } else {
  20.   opts.params = params
  21. }
复制代码


  • 服务端有部分接口吸取的参数要求 FormData 格式,这时候需要将参数序列化,并且修改请求头的 Content-Type。
  • 发送 get/post 请求时,吸取参数的对象的 key 不一样。
4.3 正常响应处置惩罚

利用 axios(opts) 发起请求,得到的是一个 Promise,在 then 的第一个参数中传入一个正常的响应处置惩罚函数,这个函数吸取响应拦截器中返回的 response 作为参数。
  1. return new Promise((resolve, reject) => {
  2.   axios(opts).then(response => {
  3.      let res = response.data
  4.      if (config.customHandler) { // 自定义响应处理
  5.        if (config.responseAll) return resolve(response)
  6.        return resolve(res)
  7.      }
  8.      if (res) {
  9.        if (res.code === 000) { // 登录超时
  10.           $vm.$toast.error(res.message)
  11.           $vm.$store.dispatch('REMOVE_USER') // 移除 cookie、session、storage 存储的信息
  12.           reject(res.message)
  13.           if (window.self === window.top) {
  14.             $vm.$router.push('/login') // 跳转登录页
  15.           }
  16.         } else if (res.code === 200) {
  17.           resolve(res.data)
  18.         } else {
  19.           $vm.$toast.error(res.message || '接口异常, 请稍后重试')
  20.           reject(res)
  21.         }
  22.      } else {
  23.         $vm.$toast.error('接口无返回内容')
  24.      }
  25.    })
  26. })
复制代码
  提示:$vm 指向全局的 Vue 实例,$toast 则是将 element 的 Message 组件实例挂载到了 Vue 的原型上
  

  • 假如调用 request 函数传入了 config.customHandler = true,表现自定义响应处置惩罚,并且 config.responseAll = true 时,会把响应拦截器中得到的 response 直接返回,这个参数主要用于调用服务端响应字节省的接口时利用。
  • 后端响应的数据结构如下图,并且登录过期接口的 http 响应状态码是 200,但是响应的数据格式中的 code 值为特定值,以是要特殊处置惩罚此类环境,清空存储在客户端的客户信息,跳转到登录页。
  • 当响应的数据中与服务端约定响应正常的 code 为 200,此时把 data 作为 Promise.resolve 的值

4.4 异常处置惩罚

异常处置惩罚在 axios(opts).then() 的第二个参数中传入处置惩罚函数,这个函数吸取响应拦截器中返回的 Promise.reject(error) 作为参数。
异常处置惩罚主要针对 http 响应状态码不即是 200 的环境,包括常见的请求超时,404请求资源不存在,50X 服务器异常等环境。
  1. axios(opts).then(response => {
  2.   // 省略...
  3. }, error => {
  4.       // 如果自定义处理
  5.       if (config.customHandler) {
  6.         reject(error)
  7.         return
  8.       }
  9.       // 请求超时
  10.       if (error.code == 'ECONNABORTED' && error.message.indexOf('timeout') > -1) {
  11.         $vm.$toast.error(`请求超时,接口地址:${url}`)
  12.         reject(error)
  13.         return
  14.       }
  15.       if (error.response) {
  16.         // 401未登录或登录失效
  17.         if (error.response.status === 401) {
  18.           reject(error)
  19.           if (window.self === window.top) {
  20.             $vm.$router.push('/login')
  21.           }
  22.           return
  23.         }
  24.         switch (error.response.status) {
  25.           case 404:
  26.             $vm.$toast.error(`请求的资源不存在,异常服务接口地址:${url}`)
  27.             break
  28.           case 408:
  29.             $vm.$toast.error('请求超时')
  30.             break
  31.           case 500:
  32.             $vm.$toast.error('服务异常')
  33.             break
  34.           case 502:
  35.             $vm.$toast.error(error.message || '服务未响应')
  36.             break
  37.           case 503:
  38.             $vm.$toast.error(error.message || '服务暂不可访问')
  39.             break
  40.           default:
  41.             $vm.$toast.error(error.response.statusText || '服务异常, 请稍后重试')
  42.         }
  43.       } else {
  44.         $vm.$toast.error(error.response.statusText || '未知错误, 请稍后重试')
  45.       }
  46.       reject(error)
  47.     })
复制代码
4.5 取消请求

在一些特定环境下,好比用户快速点击提交表单,短时间内同时触发同一个请求多次,我们可以借助 axios.cancelToken 来取消前几次请求,只保留最后一次请求。
主要实现的原理如下:

  • 每次调用 request 函数时,根据传入的 method + api + JSON.stringify(config) 作为当前请求的标识 key,假如配置了 config.cancelTokenWidthParams = true,时,在 key 背面拼接 JSON.stringify(params) 作为 key。
  • HTTP_CANCEL_MAP 每一项的 key 为每个请求的 ‘唯一标识 + _ + 时间戳’,每一项 value 设置为 axios.CancelToken 构造函数传入的 executor 函数的参数,也就是 cancel 函数,调用 checkHttpCancel 函数传入 key 判定是否为重复请求,是重复请求则调用 cancel() 取消请求。
  • 调用 request 函数时,配置 opts.cancelToken,利用 new 调用 CancelToken 的构造函数来创建 cancel token
  • 请求响应成功和失败时都需要从 HTTP_CANCEL_MAP 中删除 reqUniqueKey 对应的 cancelToken
  1. const CANCEL_TOKEN = axios.CancelToken
  2. const HTTP_CANCEL_MAP = $globals.httpCancelMap = new Map()
  3. const IS_CANCELED_MSG = 'canceled'
  4. const checkHttpCancel = reqKey => {
  5.   HTTP_CANCEL_MAP.forEach((v, k) => {
  6.     if (k.slice(0, -14) === reqKey) {
  7.       v()
  8.       HTTP_CANCEL_MAP.delete(k)
  9.     }
  10.   })
  11. }
  12. const request = async (method = 'post', api, params = {}, config = {}) => {
  13.   let reqKey = method + api + JSON.stringify(config)
  14.   if (config.cancelTokenWidthParams) reqKey += JSON.stringify(params)
  15.   let reqUniqueKey = reqKey + '_' + new Date().getTime()
  16.   checkHttpCancel(reqKey)
  17.   // 省略...
  18.   opts.cancelToken = new CANCEL_TOKEN(c => HTTP_CANCEL_MAP.set(reqUniqueKey, c))
  19.   // ...
  20.   axios(opts).then(response => {
  21.     HTTP_CANCEL_MAP.delete(reqUniqueKey)
  22.     // ...
  23.   }, error => {
  24.     HTTP_CANCEL_MAP.delete(reqUniqueKey)
  25.     if (axios.isCancel(error)) {
  26.       reject(new Error(IS_CANCELED_MSG))
  27.       return
  28.     }
  29.     // ...
  30.   })
  31. })
复制代码
  留意
  

  • 此项目利用的 axios 版本为 0.21.1,从 v0.22.0 开始,Axios 支持以 fetch API 方式—— AbortController 取消请求,CancelToken API被弃用
  • 可以利用同一个 cancel token 取消多个请求
  三、完备的 http.js

  1. import axios from 'axios'import { TOKEN_REQ_KEY, TOKEN_COOKIE_KEY } from '@/const/common'import { session, cookie, jsonObj2FormData } from '@/util/common'axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8'axios.defaults.timeout = 120000axios.defaults.crossDomain = trueaxios.interceptors.request.use(
  2.   config => {
  3.     const token = cookie.get(TOKEN_COOKIE_KEY)
  4.     if (token) {
  5.       config.headers[TOKEN_REQ_KEY] = token
  6.     }
  7.     return config
  8.   },
  9.   error => {
  10.     return Promise.reject(error)
  11.   }
  12. )
  13. axios.interceptors.response.use(response => {
  14.   return response
  15. }, error => {
  16.   return Promise.reject(error)
  17. })
  18. const CANCEL_TOKEN = axios.CancelTokenconst HTTP_CANCEL_MAP = $globals.httpCancelMap = new Map()const IS_CANCELED_MSG = 'canceled'const checkHttpCancel = reqKey => {  HTTP_CANCEL_MAP.forEach((v, k) => {    if (k.slice(0, -14) === reqKey) {      v()      HTTP_CANCEL_MAP.delete(k)    }  })}const apiInterceptor = api => {  if (api.startsWith('http')) { // 自定义请求路径    return api.slice(4)  }  if (api.startsWith('_SC_')) { // 项目同一的api前缀    api = $globals.vipAddress + api.slice(4)  }  return $globals.httpBaseURL + api}const request = async (method = 'post', api, params = {}, config = {}) => {  let reqKey = method + api + JSON.stringify(config)  if (config.cancelTokenWidthParams) reqKey += JSON.stringify(params)  let reqUniqueKey = reqKey + '_' + new Date().getTime()  checkHttpCancel(reqKey)  return new Promise((resolve, reject) => {    if (config.loading) $vm.$loading.show()    let url = apiInterceptor(api)    let opts = {      method,      url,      headers: config.headers || {},      withCredentials: config.withCredentials || true // 跨域请求时是否需要利用凭证    }    if (config.formDataFormat) {      opts.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'      params = jsonObj2FormData(params)    }    if (config.timeout) opts.timeout = config.timeout    if (config.extends) opts = Object.assign(opts, config.extends) // 假如有并列层级的参数扩展    if (method == 'post') {      opts.data = params    } else {      opts.params = params    }    opts.cancelToken = new CANCEL_TOKEN(c => HTTP_CANCEL_MAP.set(reqUniqueKey, c))    if (config.responseType) opts.responseType = config.responseType        // 发起 axios 请求    axios(opts).then(response => {      HTTP_CANCEL_MAP.delete(reqUniqueKey)      if (config.loading) $vm.$loading.close()      let res = response.data      if (config.customHandler) { // 自定义响应处置惩罚        if (config.responseAll) return resolve(response)        return resolve(res)      }      if (res) {        if (res.code === 000) { // 登录超时          $vm.$toast.error(res.message)          $vm.$store.dispatch('REMOVE_USER') // 移除 cookie、session、storage 存储的信息          reject(res.message)          if (window.self === window.top) {            $vm.$router.push('/login') // 跳转登录页          }        } else if (res.code === 200) {          resolve(res.data)        } else {          $vm.$toast.error(res.message || '接口异常, 请稍后重试')          reject(res)        }      } else {        $vm.$toast.error('接口无返回内容')      }    }, error => {      HTTP_CANCEL_MAP.delete(reqUniqueKey)      if (axios.isCancel(error)) {        reject(new Error(IS_CANCELED_MSG))        return      }      if (config.loading) $vm.$loading.close()      // 假如自定义处置惩罚      if (config.customHandler) {        reject(error)        return      }      // 请求超时      if (error.code == 'ECONNABORTED' && error.message.indexOf('timeout') > -1) {        $vm.$toast.error(`请求超时,接口地点:${url}`)        reject(error)        return      }      if (error.response) {        // 401未登录或登录失效        if (error.response.status === 401) {          reject(error)          if (window.self === window.top) {            $vm.$router.push('/login')          }          return        }        switch (error.response.status) {          case 404:            $vm.$toast.error(`请求的资源不存在,异常服务接口地点:${url}`)            break          case 408:            $vm.$toast.error('请求超时')            break          case 413:            $vm.$toast.error('请求实体大小凌驾服务器最大限定')            break          case 500:            $vm.$toast.error('服务异常')            break          case 502:            $vm.$toast.error(error.message || '服务未响应')            break          case 503:            $vm.$toast.error(error.message || '服务暂不可访问')            break          default:            $vm.$toast.error(error.response.statusText || '服务异常, 请稍后重试')        }      } else {        $vm.$toast.error(error.response.statusText || '未知错误, 请稍后重试')      }      reject(error)    })  })}export default {  get: (api, params = {}, config = {}) => {    return request('get', api, params, config)  },  post: (api, params = {}, config = {}) => {    return request('post', api, params, config)  },  image: id => {    return `${$globals.fileUrl}?fileId=${id}`  },  isCanceled: error => {    if (error && error.message === IS_CANCELED_MSG) return true    return false  }}
复制代码

  • http.image 方法仅用于返回文件的请求完备 url,利用场景为好比 <img> 标签中的 src 的值
  • http.isCanceled 方法用于判定当前请求是否取消,假如有请求未取消并且出现全局 loading 加载未关闭的环境,可根据此标记来判定是否关闭
四、封装成插件并挂载到原型

/plugins/http/install.js
  1. import httpService from '@/service/http'
  2. export default {
  3.   install: Vue => {
  4.     Vue.prototype.$http = httpService
  5.   }
  6. }
复制代码
五、管理 api

比方,根据业务可划分为文档,评论等模块,在 service 目录下分别创建对应的模块存放 api 的 js 文件,对 api 进行同一管理。
   猛烈建议给每个 api 备注功能,提高可维护性
  

/service/comment.js
  1. /**
  2. * @name 获取评论列表
  3. * @param {Object} params 请求参数对象
  4. */
  5. export const getCommentListPromise = params => {
  6.   params = Object.assign({
  7.     page: 0, // 页码
  8.     pageSize: 5, // 每页数量
  9.   }, params)
  10.   return $vm.$http.get('_SC_/comment/findCommentList', params)
  11. }
复制代码
在 Comment.vue 页面中利用
  1. import { getCommentListPromise } from '@/service/comment'
  2. async findCommentList() {
  3.   const data = await getCommentListPromise()
  4.   console.log(data)
  5. }
复制代码

总结

本文主要讲述了如何利用 axios 进行 http 封装的详细过程,及在项目中如何利用封装的 http 请求,请求拦截器和响应拦截器都是比较简单,没有处置惩罚许多的逻辑,逻辑处置惩罚基本是集中在 request 函数中。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

风雨同行

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表