1、简单的 axios 请求和响应拦截配置

2、取消重复请求

import axios from 'axios'
import debounce from 'lodash/debounce'
import {
  Message
} from 'element-ui'

class Ajax {
  constructor() {
    // 初始化值
    this.init()
    // 初始化token
    this.getToken()
    // 初始化axios
    this.initAxios()
    // 打开请求拦截
    this.startReqInterceptors()
    // 打开响应拦截
    this.startRespInsterceptors()
    // 返回 axios 实例对象
    return this.ajax
  }

  /**
   * @method 初始化值
   */
  init() {
    this.overTimeValue = 30
    this.baseURL = '/new-trade-bank'
    this.pending = []
    this.CancelToken = axios.CancelToken
  }
  /**
  * 获取token
  */
  getToken() {
    this.token = localStorage.getItem('token')
    this._token = localStorage.getItem('ac_token')
  }

  /**
   * @method 初始化axios实例
   */
  initAxios() {
    this.ajax = axios.create({
      timeout: this.overTimeValue * 1000
    })
  }

  /**
   * @method 通过config获取到参数
   * @returns 对应参数的字符串
   */
  getParamFormConfig(config) {
    const paramsMethodMap = {
      get: 'params',
      // delete 请求 data和params都可以
      delete: 'data',
      post: 'data',
      put: 'data',
      patch: 'data'
    }
    // 参数
    const param = config[paramsMethodMap[config.method]]

    // 参数进行格式化
    const format = {
      '[object Undefined]': undefined,
      '[object Array]': JSON.stringify(param),
      '[object Object]': JSON.stringify(param),
      '[object Number]': param,
      '[object String]': param
    }
    // 返回字符串格式
    return format[Object.prototype.toString.call(param)]
  }

  /**
   * @method 打开请求拦截
   */
  startReqInterceptors() {
    const {
      token,
      _token
    } = this
    this.ajax.interceptors.request.use(config => {
      if (config.url) {
        // 包含/special开头的api做特殊处理,不用baseURL
        config.url = /^(\/special)/.test(config.url) ? config.url : this.baseURL + config.url
        // 发送ajax前,执行取消操作,便利数组 看是否重复发送
        this.removePending(config)
        // 设置token和请求头authorization
        if (token) {
          config.headers.token = token
        } else if (_token) {
          config.headers.Authorization = `bearer ${_token}`
        }
        // 将新请求添加进队列
        this.addPending(config)
      }

      // 做全局加载中
      return config
    })
  }

  /**
   * @method 打开响应拦截
   */
  startRespInsterceptors() {
    this.ajax.interceptors.response.use(resp => {
      // 当前请求已经响应结束,从pending队列中移除
      this.removePending(resp.config)
      let { data } = resp

      if (resp.status === 200) {
        const {
          filename
        } = resp.headers

        if (!filename && data.type && data.type !== 'success') {
          this.alertMsg(data)
        }
        if (data && filename) {
          // 对文件下载特殊处理
          const str = data
          data = {
            fileStream: str,
            filename
          }
        }

        return data || resp
      }
    }, err => {
      if (!err.response) {
        /* 重复发送请求,请求被取消了 */
        const {
          message
        } = err
        // 超时
        if (/^(timeout of)/.test(message)) {
          // 移除超时请求
          this.removeOverTimeRequest()
          this.alertMsg({
            type: 'error',
            text: '接口请求超时'
          })
        } else {
          console.warn('Warning: 重复发送了请求:' + err.message)
        }
      } else if (err.response.status === 401) {
        // 移除token
        const {
          token
        } = this

        window.localStorage.clear()
        // 没有权限跳转到权限认证页面
        window.location.href = token ? `/new-trade-bank/authorize?token=${token}` : '/new-trade-bank/authorize'
      } else if (err.response.status === 403) {
        this.alertMsg(err.response.data)
      } else if (err.response.status === 404) {
        console.warn('%c404-后台接口未找到', 'font-weight: bold;color: red;font-size: 16px;')
      } else if (err.response.status === 500) {
        this.alertMsg({
          type: 'error',
          text: '500-连接服务器出错'
        })
      }

      /**
       * 在这里对状态吗不是200的进行统一错误捕获
       * 注意: 这里代码执行完了 还是会走Promise.resolve
       *      也就是还是会走请求的.then函数
       * 返回可以按照自己需要进行返回,可以减少请求出错时的控制台报错
       */
      return Promise.reject(err)
    })
  }

  /**
   * @method 请求成功,移除队列中的请求
   */
  removePending(target) {
    // target的param需要经过格式化成相同字符串才能比较
    const targetParam = this.getParamFormConfig(target)
    for (const [index, p] of Object.entries(this.pending)) {
      // url,method,param需要完全一致才能确定是重复请求
      if (p.url === target.url && p.method === target.method && targetParam === p.param) {
        // 当当前请求在数组中存在执行函数体,取消队列中的请求并删除,执行cancel函数会传递一个
        // cancel对象给promise.reject, cancel里面的参数会传递到response中的err对象的message中
        p.cancel(`${p.method} ${p.url}`)
        // 从队列中删除
        this.pending.splice(index, 1)
      }
    }
  }
  /**
   * @method 将请求添加进队列中
   */
  addPending(config) {
    config.cancelToken = new axios.CancelToken(c => {
      this.pending.push({
        url: config.url,
        method: config.method,
        cancel: c,
        param: this.getParamFormConfig(config),
        createTime: Date.now()
      })
    })
  }

  /**
   * @method 通过element的message组件进行统一弹窗告警
   */
  alertMsg(data) {
    // 关闭现在有的告警信息
    Message.closeAll()
    Message({
      showClose: true,
      message: data.text,
      type: data.type
    })
  }
}

/**
 * @method 移除pending队列中的超时请求
 * @description 通过防抖避免频繁调取
 */
Ajax.prototype.removeOverTimeRequest = debounce(function () {
  const nowDate = Date.now()

  for (const p in this.pending) {
    const {
      createTime
    } = this.pending[p]
    const time = nowDate - createTime
    if (time >= (this.overTimeValue * 1000)) {
      this.pending.splice(p, 1)
    }
  }
}, 100)

const ajax = new Ajax()

export default ajax