Axios封装

实现功能:针对不同请求设置token、code不为成功时的信息提示、code异常情况下的异常处理、屏蔽某些接口、简洁的数据结构响应

缺点:1.status为200时,code为401时不会进行无感刷新,在首页游客登录状态下缺失此功能是致命缺陷,对只可以登录状态才能进行首页的页面有效。

   2.登录状态是不可以进行无感刷新的,token失效直接跳转到登录页(考虑需求,一般都不需要此功能)

import axios from 'axios'
import { DialogAlert } from '@/components/ui/dialog/index'
import { storage } from '@/utils/storage'  //封装获取token
import Router from '@/router'
import store from '@/store'

// const pendingMap = new Map()
const dialogMsg = DialogAlert.Msg
const dialogAlert = DialogAlert.Alert
let showExpireLoginAlert = false

// 无感刷新的参数
let isRefreshToken = false // 是否已经发出刷新请求
let overTimer = 0 // 最多请求5次
const requestList = [] // token过期的api列表

const noHandleResonseUrlList = [   //10.需要屏蔽的接口数组
  '/outer/monitor/list/data',
  '/outer/monitor/view/data/'
]

const service = axios.create({   1.创建实例
  baseURL: process.env.VUE_APP_BASE_URL, // 设置统一的请求前缀
  timeout: 10000 // 设置统一的超时时长
})

// 自定义配置
let initCustomOptions = Object.assign({    //7.配置化功能需求
  repeatRequestCancel: false, // 是否开启取消重复请求, 默认为 true
  loading: false, // 是否开启loading层效果, 默认为false
  reductDataFormat: true, // 是否开启简洁的数据结构响应, 默认为true
  errorMessageShow: true, // 是否开启接口错误信息展示,默认为true
  codeMessageShow: false, // 是否开启code不为成功时的信息提示, 默认为false
  dataCodeErrorHandle: true // 是否开启code异常情况下的异常处理
})

// 请求拦截
service.interceptors.request.use(  2.设置请求拦截
  config => {
    // 每次请求前,先检查请求是否重复了,重复了就取消上一次的请求
    // removePending(config)
    // 开启了重复请求,就把请求添加到队列中
    // initCustomOptions.repeatRequestCancel && addPending(config)
    // 携带token
    settingToken(config)   3.设置token
    // 白名单处理,有些接口不携带token     4.针对某些接口不需要设置token的进行操作
    if (!whiteListNoTokenApi(config.url)) { // 不需要携带token的接口
      delete config.headers.Authorization
    }
    return config
  },
  error => {
    return Promise.reject(error)  5.出现出错返回一个reject
  }
)

// 响应拦截
service.interceptors.response.use(   //6.设置响应拦截
  response => {
    const { codeMessageShow,   //8.开启对应的功能需求
      dataCodeErrorHandle } = initCustomOptions
    // 请求完成之后要删除请求map中的请求key
    // removePending(response.config)
    if (codeMessageShow && response.status !== 200) {  //9.针对所有失败的请求进行拦截,实际不会执行该段代码,status非200直接进行下面的error
      dialogMsg(response.data.msg, {icon: 3})
      return Promise.reject(response.data) // status不等于200, 页面具体逻辑就不执行了
    }
    const requestUrl = response.config.url
    // 请求是成功请求,但是结果不对
    let hasPath = false
    noHandleResonseUrlList.forEach(item => { //11.屏蔽的某些接口
      if (requestUrl.includes(item)) hasPath = true
    })
    if (dataCodeErrorHandle &&     //12.针对status为200,但是实际后端code报错
          response.status === 200 &&
          response.data.code !== 200 &&
          !hasPath) {
      const { code } = response.data
      if (code === 401) {  //13.针对token失效情况
        crushLoginTips()  
      }else{
        dialogMsg(response.data.msg, {icon: 3}) //15.针对其他code报错情况
    }
    return Promise.reject(response.data) //16.code不等于200, 直接页面具体逻辑就不执行了
    }
    return initCustomOptions.reductDataFormat ? response.data : response //17.简洁数据结构功能
  },
  error => {
    // error.config && removePending(error.config)
    initCustomOptions.errorMessageShow && httpErrorStatusHandle(error) //18.针对status报错,处理错误状态码  
    return Promise.reject(error) // 错误继续返回给到具体页面
  }
)

// 无感刷新请求
async function refreshVisiterToken(config, isRefreshMiddleToken) {
  if (!isRefreshToken) {   //24.首次401则进入,如果在游客登录失败时则isRefreshToken不为false,进入添加request队列
    // pendingMap.clear()
    isRefreshToken = true  //25.token已经重新请求了
    await store.dispatch('userInfo/visitorLogin') // 26.游客登录,如果此处登录失败则退出进去到response.error?
    while (requestList.length > 0) {
      requestList.shift()() // shift()返回的是被删除的数组最后一个元素,按顺序执行回调
    }
    overTimer++
    isRefreshToken = false
    return service(config)
  } else {
    return new Promise((resolve) => {
      requestList.push(() => {
        settingToken(config) // 给config加上token
        resolve(service(config))
      })
    })
  }
}

// 属于不需要刷新的请求
const isNoRefresh = function(url) {
  const list = []
  if (list.includes(url)) return true
  return false
}

// 不需要携带token的api
const whiteListNoTokenApi = function(url) {
  const list = ['/api/captchaImage', '/api/login']
  return !list.includes(url)
}

// 设置token
function settingToken(config) {
  const token = storage.token || storage.visitor || ''
  if (token && typeof window !== 'undefined') {
    config.headers.Authorization = `bearer ${token}`
  }
}

// 401错误无感处理
function handleError401(errorConfig) {
  const { config } = errorConfig
  if (overTimer >= 30) return // 21.重复请求超过上限30次   
  if (overTimer < 2 && !isNoRefresh(config.url)) {   //22.非不需要刷新的url
    if (!storage.token) { // 23.如果是原本没有token,则刷新游客的token
      return refreshVisiterToken(config)
    }
    if (storage.token) { // 已经登录的用户,弹窗提示
      crushLoginTips()
    }
  }
}

// 挤登处理
function crushLoginTips() {  //14.弹出token失效提示框
  if (showExpireLoginAlert) return
  showExpireLoginAlert = true
  dialogAlert('登录过期,或此账号已在其他设备登录,请重新登录!', {
    closeModal: false,
    showClose: false,
    callback: function(cb) {
      Router.push({name: 'login'})
      cb()
      showExpireLoginAlert = false
    }
  })
}

/**
 * 生成唯一的每个请求的唯一key
 * @param {*} config
 * @returns
 */
// function getPendingKey(config) {
//   let {url, method, params, data} = config
//   if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象
//   return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')
// }

/**
 * 处理异常
 * @param {*} error
 */
function httpErrorStatusHandle(error) {
  // 处理被取消的请求
  // if (axios.isCancel(error)) return console.error('请求的重复请求:' + error.message)
  let message = ''
  if (error && error.response) {
    switch (error.response.status) {
      case 302: message = '接口重定向了'; break
      case 400: message = '参数不正确'; break
        // case 401: message = '您未登录,或者登录已经超时,请先登录'; break  // 401不拦截,让外部自己处理
      case 401:   //20.针对401情况特殊处理
        handleError401(error)
        break
      case 403: message = '您没有权限操作'; break
      case 404: message = `请求地址出错: ${error.response.config.url}`; break // 在正确域名下
      case 408: message = '请求超时'; break
      case 409: message = '系统已存在相同数据'; break
      case 500: message = '服务器内部错误'; break
      case 501: message = '服务未实现'; break
      case 502: message = '网关错误'; break
      case 503: message = '服务不可用'; break
      case 504: message = '服务暂时无法访问,请稍后再试'; break
      case 505: message = 'HTTP版本不受支持'; break
      default: message = '异常问题,请联系管理员'; break
    }
  }
  if (error.message.includes('timeout')) message = '网络请求超时'
  if (error.message.includes('Network')) message = window.navigator.onLine ? '服务端异常' : '您断网了'

  dialogMsg(message, { icon: 3 })
}

/**
 * 储存每个请求的唯一cancel回调, 以此为标识
 * @param {*} config
 */
// function addPending(config) {
//   const pendingKey = getPendingKey(config)
//   config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
//     // 如果当前的请求是首次请求,那就把key添加到map中存储
//     if (!pendingMap.has(pendingKey)) {
//       pendingMap.set(pendingKey, cancel)
//     }
//   })
// }

/**
 * 删除重复的请求
 * @param {*} config
 */
// function removePending(config) {
//   const pendingKey = getPendingKey(config)
//   if (pendingMap.has(pendingKey)) {
//     const cancelToken = pendingMap.get(pendingKey)
//     // 传入 pendingKey主要的作用是,让axios在错误的时候通过catch可以捕获到错误信息
//     // 用于后续,使用者可以快速定位是哪个请求导致的
//     cancelToken(pendingKey)
//     pendingMap.delete(pendingKey)
//   }
// }

export default service

posted on 2023-02-16 13:47  ChoZ  阅读(70)  评论(0编辑  收藏  举报

导航