uniapp 前端定时刷新token,接口排队等待,promise 接口封装

一、需求

       此项目为小程序。小程序完成第一版token刷新设计思路是:根据接口调用返回的errorCode来判断当前用户的token和refreshToken是否过期。根据不同的errorCode,前端去调用接口完成token的刷新或者跳转到登录页面重新登录。

       由于小程序的用户功能权限可以在后台管理系统中配置,但是小程序的用户权限变化后,没有办法通知到小程序去更新token或者提示用户重新登录。所以跟后端配合,需要前端增加一个定时调用刷新token接口的需求。比如:后端设置的token是10h过期,那前端就可以每隔5h去调用刷新接口去更新token。新的token接口会返回当前用户最新的权限。从而实现用户功能权限的更新。

二、实现思路

 1、第一版代码(未增加定时刷新功能)接口封装如下

// 封装请求
const request = function(url, params) {
  if (!url) {
    console.log('请传入url......')
    return
  }
  return new Promise((resolve, reject) => {
    // to do
    // 接口调用前 参数params封装

    // 调用uniapp 请求接口
    uni.request({
      url: basePath + url, // 仅为示例,并非真实接口地址。
      data: params.data,
      header: {
        'token': store.getters.getToken,
        'content-type': params.contentType || 'application/x-www-form-urlencoded',
        'refreshToken': store.getters.getRefreshToken,
      },
      method: params.method || 'POST',
    }).then((uniData) => {
      // uniData为一个数组
      // 数组第一项为错误信息 即为 fail 回调
      // 第二项为返回数据
      const [err, uniRes] = uniData;
      if (err) {
        console.log('uni err-------------------:', err)
        uni.showToast({
          icon: 'none',
          title: '服务异常,请稍后重试!'
        })
        return
      }
      const res = uniRes.data || {}
      
      if (res.success) {
        let resultData = res.data || {}
        resolve(resultData)
      } else {
        const code = res.code
        // to do
        // 根据code处理异常

        reject()
      }
    })
  })
}

2. 实现定时刷新思路

  2.1、记录用户信息刷新的时间(使用的时间戳),存vuex:timeOfUserInfo

在一切重新获取了用户权限信息(一定是token变更了)的接口处,记录当前的时间(记录的是用户本地的时间,也是用用户本地的时间去计算的时间差,当本地时间有误的时候,可能会出现接口没有调用的问题)。

  2.2、判断是否调用refresh接口

接口A调用时,根据当前时间与timeOfUserInfo时间差,【1】如果超过3h,则先去调用refresh接口,同时挂起接口A.【2】如果没有超过3h,则直接调用

这里面存在2个问题

(1)、如果页面刷新调用接口时,有多个接口同时调用,那么就会refresh接口就会调用多次

             这时,我们可以用一个变量isRefreshing(默认为false)来记录当前是否在调用刷新接口,调用refresh接口时,isRefreshing设置为true。refresh接口调用成功后,isRefreshing设置为false。

(2)、如何挂起promise

              由于uni.request本来就是一个promise,我们可以直接设置他为pendding状态,放入队列中。等待refresh接口调用成功后,再充队列中将promise取出来,执行

代码如下:

1.模仿axios做了个,请求拦截器,返回拦截器

(1)、请求拦截器

beforeRequest(options = {}) {
      const $this = this
      return new Promise((resolve, reject) => {
        options.url =
          options.url.indexOf('http') === 0
            ? options.url
            : this.baseURL + options.url
        options.method = options.method || 'POST'
        options.data = options.data || {}

        // 封装自己的请求头
        options.header = {
          Authorization: store.getters.getToken,
          refreshToken: store.getters.getRefreshToken
        }
        if (!options.isFile) {
          // 非文件上传
          options.header['content-type'] =
            options.contentType || 'application/x-www-form-urlencoded'
        }
        $this.options = options
        // 排除不需要走refresh token的接口
        if (refreshTokenByTime.excludeUrls.indexOf(options.url) !== -1) {
          resolve(options)
          return
        }

        // 刷新时间到了后
        if (refreshTokenByTime.needRefresh()) {
          // 如果需要刷新token 并且当前token没有在刷新中,则去调用刷新接口
          if (!refreshTokenByTime.isRefresh) {
            // 触发refresh接口的接口,需要暂存起来
            pendingRequests.push({
              resolved: resolve,
              options: options
            })
            refreshTokenByTime.isRefresh = true
            refreshTokenByTime.refreshToken().then(() => {
              // 接口调用成功后,将request中的接口都调用一遍
              // 重点: 用于存储数据
              pendingRequests.forEach(item => {
                // 由于已经更新了token,则需要传新的token了
                item.options.header.Authorization = store.getters.getToken
                item.options.header.refreshToken = store.getters.getRefreshToken
                item.resolved(item.options)
              })
              // 执行完成后,清空队列
              pendingRequests = []
            })
          } else {
            // 这里存储了resolve,变相的实现请求的挂起
            //(只要没有resolved或rejected,请求就会一直处于pedding状态)
            // 并将Promise状态的改变放到了外部一个对象来控制 ,
            // 待定池缓存这个对象即可,待需要执行后续被拦截请求,
            // 只需要利用这个对象引用的 resolved 来改变Promise状态即可实现请求挂起的放行
            pendingRequests.push({
              resolved: resolve,
              options: options
            })
            return pendingRequests
          }
        } else {
          if (options.isShowLoading) {
            uni.showLoading({
              title: options.loadingText || '加载中...',
              mask: true
            })
          }
          resolve(options)
        }
      })
    },

(2)、响应拦截器

响应拦截器

// 响应拦截器
    handleResponse(data) {
      const $this = this
      uni.hideLoading()
      return new Promise((resolve, reject) => {
        const [err, uniRes] = data
        if (err) {
          console.log('uni err-------------------:', err)
          uni.showToast({
            icon: 'none',
            title: '服务异常,请稍后重试!'
          })
          reject()
          return
        }
        let res = uniRes.data || {}
        if (typeof res === 'string') {
          res = JSON.parse(res)
        }
        const options = {
          url: $this.options.url,
          resolve,
          reject
        }
        // 由于传了options,里面包含了resolve和reject,requestCallback方法就是处理接口返回的一 
        // 些异常信息。同样是返回的promise
        return requestCallback(res, options)
      })
    }

主要工作量在:请求拦截器中,如何做多接口的挂起操作。

三、代码实现

/**
 * description: 普通请求接口封装
 */
import config from '@/common/base-config.js'
import store from '@/store/index'
import { requestCallback, refreshTokenByTime } from './requestCallback.js'

const basePath = config.url + config.gateway

let pendingRequests = [] // 接口排队队列

function requestObj(options) {
  this.config = {
    baseURL: basePath,
    options: options,
    // 请求拦截器
    beforeRequest(options = {}) {
      const $this = this
      return new Promise((resolve, reject) => {
        options.url =
          options.url.indexOf('http') === 0
            ? options.url
            : this.baseURL + options.url
        options.method = options.method || 'POST'
        options.data = options.data || {}

        // 封装自己的请求头
        options.header = {
          Authorization: store.getters.getToken,
          refreshToken: store.getters.getRefreshToken
        }
        if (!options.isFile) {
          // 非文件上传
          options.header['content-type'] =
            options.contentType || 'application/x-www-form-urlencoded'
        }
        $this.options = options
        // 排除不需要走refresh token的接口
        if (refreshTokenByTime.excludeUrls.indexOf(options.url) !== -1) {
          resolve(options)
          return
        }

        // 刷新时间到了后
        if (refreshTokenByTime.needRefresh()) {
          // 如果需要刷新token 并且当前token没有在刷新中,则去调用刷新接口
          if (!refreshTokenByTime.isRefresh) {
            // 触发refresh接口的接口,需要暂存起来
            pendingRequests.push({
              resolved: resolve,
              options: options
            })
            refreshTokenByTime.isRefresh = true
            refreshTokenByTime.refreshToken().then(() => {
              // 接口调用成功后,将request中的接口都调用一遍
              // 重点: 用于存储数据
              pendingRequests.forEach(item => {
                // 由于已经更新了token,则需要传新的token了
                item.options.header.Authorization = store.getters.getToken
                item.options.header.refreshToken = store.getters.getRefreshToken
                item.resolved(item.options)
              })
              // 执行完成后,清空队列
              pendingRequests = []
            })
          } else {
            // 这里存储了resolve,变相的实现请求的挂起
            //(只要没有resolved或rejected,请求就会一直处于pedding状态)
            // 并将Promise状态的改变放到了外部一个对象来控制 ,
            // 待定池缓存这个对象即可,待需要执行后续被拦截请求,
            // 只需要利用这个对象引用的 resolved 来改变Promise状态即可实现请求挂起的放行
            pendingRequests.push({
              resolved: resolve,
              options: options
            })
            return pendingRequests
          }
        } else {
          if (options.isShowLoading) {
            uni.showLoading({
              title: options.loadingText || '加载中...',
              mask: true
            })
          }
          resolve(options)
        }
      })
    },
    // 响应拦截器
    handleResponse(data) {
      const $this = this
      uni.hideLoading()
      return new Promise((resolve, reject) => {
        const [err, uniRes] = data
        if (err) {
          console.log('uni err-------------------:', err)
          uni.showToast({
            icon: 'none',
            title: '服务异常,请稍后重试!'
          })
          reject()
          return
        }
        let res = uniRes.data || {}
        if (typeof res === 'string') {
          res = JSON.parse(res)
        }
        const options = {
          url: $this.options.url,
          resolve,
          reject
        }
        return requestCallback(res, options)
      })
    }
  }

  // request 请求
  // 下面最主要的一段代码,利用了promise的特性,当调用request方法时,先经过
  // 请求拦截可以拿到请求参数,在请求之前做处理,请求函数会把处理之后的参数作为结果抛出
  // 给了uni.request进行请求,uni.request执行完再次return了promise,接着执行响应
  // 拦截.then中的 response函数,response函数接收到uni请求响应的结果作为res传递给
  // 响应拦截(这里为什么能拿到uni的响应结果因为uni返回的也是promise),在response
  // 就可以对响应的数据处理后再进行promise的返回
  this.request = function request(options = {}) {
    return this.config
      .beforeRequest(options)
      .then(opt => {
        return uni.request(opt)
      })
      .then(res => this.config.handleResponse(res))
  }

  this.requestFile = function requestFile(options = {}) {
    return this.config
      .beforeRequest(options)
      .then(opt => {
        opt.formData = opt.data
        return uni.uploadFile(opt)
      })
      .then(res => this.config.handleResponse(res))
  }
}

module.exports = requestObj

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值