请求调用封装

661 阅读4分钟

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

前言

网上有很多请求封装的文章,可以通过对请求的拦截,对请求参数、请求头、返回值等进行修改,甚至取消请求,或是重发请求,请求参数加密,鉴权等等,基本每个成熟的项目都会对请求进行封装,方案很多,个人比较喜欢的一种采用hook对请求的拦截做各种处理,这里就不多谈这个了,这里我们来谈谈对接口调用方式的封装。

常见调用方法

没做封装版

以axios为例,直接使用post\get\delete三种方式,调用接口,看看这段代码:

axios.post(url, {data}).then(res=>{})

这么写,url每个页面都得写一次,万一后台链接换了,改起不麻烦?即便是你能全局替换,但后面维护的人,维护同样的功能,不同页面时,得把这代码复制得到处都是。

进阶版

在前面的基础上将请求链接统一写在一个js文件里,在使用的时候通过引入的方式来使用

// static.js

export default {
    aLink: 'xxx',
    bLink: 'ccc'
}

// 调用的地方
import url from 'static.js'
axios.post(url.aLink, {data}).then(res=>{})

看代码,其实没多大变化,代码反而多了一点点

来看看这个版

其实每个请求都是一个promise,如果把这个promise统一弄到一处,这样调用的地方,只要引入就好,前面那段代码就不会重复了。来个例子:

// loginApi.js
const S_URL = {
   login: '/api/login',
   reg: '/api2/reg'
}
export login () {
    return axios.post(S_URL.login, {data})
}

// 调用的地方
import {login} from 'loginApi.js'
login.then(res=>{})

这版没毛病按需加载,也做到了统一管理,要改的时候改一处就好。但奈何本人有点懒,每个调用地方都得引入一次,一个页面接口一堆,不乐意一个个引入,所以个人爱好版来了。

个人爱好版

不喜欢一个个引入,那就一次性全局引入,然后把他们挂载到全局变量上去,可以是vue对象,也可以是window, global,我是挂到vue对象上。

来看代码

// storeApi.js
export default {
  // 详情查询
  storeCouponInfo: {
    method: 'post',
    url: '/stores/stores/coupon/storeCouponInfo',
    // 这里还可有一些其他配置,比如是否需要统一的报错处理等
  },
}

这里可以把所有apis文件夹下的通过js方法一起导入(不清楚的可以看看 require.context)

// api.js
import store from './apis/storeApi.js'
import MyHttp from './req'
const ALL_API = Object.assign({}, storeApi)
const api = new MyHttp({}, ALL_API)
export default api

这里有一些处理在req.js里面

// req.js

import axios from 'axios'
let axiosInstance
let typeOf = typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' ? function (obj) {
  return typeof obj
} : function (obj) {
  return obj && typeof Symbol === 'function' && obj.constructor === Symbol && obj !== Symbol.prototype ? 'symbol' : typeof obj
}

/*存储请求的数组*/
let refreshSubscribers = []
window.isRefreshing = false
/*将所有的请求都push到数组中,其实数组是[function(token){}, function(token){},...]*/
function subscribeTokenRefresh(cb) {
  refreshSubscribers.push(cb);
}
/*数组中的请求得到新的token之后自执行,用新的token去请求数据*/
function onRefreshed(token) {
  console.log('onRefreshedvvvvvv--->', refreshSubscribers)
  refreshSubscribers.map(cb => cb(token));
}

// 封装请求
class MyRequest {
  constructor() {
    axiosInstance = axios.create({
      baseURL: process.env.VUE_APP_BASE_API,
      timeout: 30000,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
      }
    })


    // 添加请求拦截器
    axiosInstance.interceptors.request.use(function (config) {
      console.log('config--->', config)
      let { isLoading } = config.options
      
      if (config.method === 'get' && config.data) {
        config.params = config.data
      }
      // 处理请求Loading
      if (isLoading) {
        global.toast.loading({
          message: '加载中...',
          forbidClick: true,
          duration: 30000
        })
      }
      return config
    }, function (error) {
      return Promise.reject(error)
    })
    // 添加响应拦截器
    axiosInstance.interceptors.response.use(
      (response) => {
      global.toast.clear()
      if (!response || !response.data) return Promise.reject(response)
      let res = response.data
      let _this = this
      switch (res.code) {
        case 0:
          return res
        case 2000:
        case 2007:
          switch (response.config.url) {
            case '/generateTokenByCustom':
              return Promise.reject(res)
            default:
              var userInfo = global.lStore.get(global.lStore.staticName.USER_INFO)
              if (userInfo && userInfo.id) {
                // 防止多个请求需要token的问题
                if (!window.isRefreshing) {
                  window.isRefreshing = true
                  global.api.generateTokenByCustom({ accountId: userInfo.id }, { dealException: true }).then(result => {
                    try {
                      //储存token
                      global.token = result.data.token
                      global.addon.refreshToken({
                        token: result.data.token
                      })
                      // 重新请求
                      onRefreshed(result.data.token)
                      window.isRefreshing = false
                    } catch (error) {
                      console.log(error)
                      global.toast('登录失败,请关闭APP重试')
                      return Promise.reject(error)
                    }
                  })
                }

                let retry = new Promise((resolve) => {
                  /*(token) => {...}这个函数就是cb*/
                  subscribeTokenRefresh((token) => {
                    let { url, method, data, headers, options } = response.config
                    data = JSON.parse(data)
                    headers.token = "Bearer " + token
                    /*将请求挂起*/
                    resolve(_this.sendRrquest(url, method, data, headers, options))
                  })
                })
                return retry
              }
              break
          }
          break
        default:
          if (res.message !== 'success') {
            console.log('err:', res)
            let { dealException } = response.config.options || {}
            if (!dealException) {
              global.toast(res.message || '服务忙,请稍后再试')
            }
            return Promise.reject(res)
          }
          break
      }
    }, error => {
      global.toast.clear()
      console.log('error: ', error)
      global.toast('服务忙,请稍后再试')
      return Promise.reject(error)
    })
  }
  // 是否是对象
  isObject(obj) {
    return (typeof obj === 'undefined' ? 'undefined' : typeOf(obj)) === 'object' && obj !== null
  }
  // 继承扩展
  extend(obj) {
    for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
      args[_key - 1] = arguments[_key]
    }
    if (this.isObject(obj) && args.length > 0) {
      if (Object.assign) {
        return Object.assign.apply(Object, [obj].concat(args))
      }
      args.forEach(function (arg) {
        if (this.isObject(arg)) {
          Object.keys(arg).forEach(function (key) {
            obj[key] = arg[key]
          })
        }
      })
    }
    return obj
  }
  sendRrquest(url, method, data, header, options) {
    return axiosInstance({
      method: method,
      url: url,
      data: data,
      headers: header,
      options
    })
  }
}
let myRequest = new MyRequest()
// 出请求
let MyHttp = function (defaultParams, ALL_API) {
  let resource = {}
  for (let actionName in ALL_API) {
    let _config = ALL_API[actionName]
    // options为对象包含(pathParam:url参数,dealException:是否单独处理错误信息, isLoading:是否显示Loading,contentType请求类型)
    resource[actionName] = (pdata, options = {}) => {
      let paramsData = myRequest.extend({}, defaultParams, pdata)
      let url = _config.url
      if (options.pathParam) {
        url = url + options.pathParam
      }
      // 头信息
      let head = { 'Content-Type': options.contentType || 'application/json;charset=UTF-8' }
      switch (url) {
        case '/generateTokenByCustom':
          delete head.token
          return myRequest.sendRrquest(url, _config.method, paramsData, head, options)
        default:
          if (global.token) {
            head.token = "Bearer " + global.token
            // console.log(head.token)
          }
          else{
            delete head.token
          }
          return myRequest.sendRrquest(url, _config.method, paramsData, head, options)
      }
    }
  }
  return resource
}
export default MyHttp

看看MyHttp这个方法,我们所有请求都是要走这里,这里对所请求还做了前置处理,具体需要看各位的需求。

最后绑定就简单了,直接在main.js里面处理就好,随你喜欢绑哪里

global.api = api
Vue.prototype.api = api

最最后,我传播的是偷懒的思想,也欢迎更舒服的方案分享探讨。