mpvue项目中基于flyio实现的可更新cookie的拦截器

2,207 阅读5分钟

    昨天刚刚完成了小程序接口的合并优化,顺便还对拦截器改造了一番,由之前的在响应拦截器对cookie过期进行处理,改为了在请求拦截器里就先处理一番,这种修改一定程度上可以减少接口调用所需的时间,虽然可能减少的时间很少,大约200,100甚至几十毫秒?但是总归是有减少的,对于老板提出的让用户尽快地看到我们的商品列表的优化需求,多少还是有点作用的!下面我们就一起来看看怎么实现吧~

一.前期准备

    关于mpvue项目的目录,可以参考这篇文章;至于请求库flyio(即Fly.js),大伙可以去该库的文档去一探究竟,文档里其实有关于拦截器的基础的讲解,我的这个拦截器就是在该文档的基础上实现的,我觉得flyio还是挺好用的,我自己并没有在项目里使用微信小程序原生的wx.request()
    该实现的文件主要有两个,一个是config.js,即配置文件,里面放一些通用的配置,另一个就是今天的主角ajax.js,即我们要实现的拦截器。

二.中心思想

    首先来看下怎么实现在请求发起前在本地判断cookie是否过期,这点需要用到本地存储wx.setStorageSync,实现核心逻辑如下:
if(本地无cookie){
    将登录接口responseHeader里的date和set-Cookie字段的值存储在本地
    return
}
(后续请求发起前)根据storage里的date与现在的时间进行对比
if(cookie已过期){
    锁定fly实例
    使用新的fly实例调用登录接口更新cookie (异步)
    解锁fly实例,重新发起过期时调用的请求
    return
}
// 若无过期则正常处理
...
...

三.具体代码

没什么好隐藏的,不啰嗦直接上干货,各位看官仔细看🧐 ⬇️
src/api/config.js:

const config = {
Host: {
  production: 'https://xxx.xxx.cn',
  development: 'http://test-xxx.xxx.cn',
  test: 'http://test-xxx.xxx.cn'
},
// 不需要检查cookie的名单
urlNotNeedSession: [
  '/xxx/xxx.xx.xxxx/xx1', // 我是该接口注释
  '/xxx/xxx.xx.xxxx/xx2', // 我是该接口注释
  '/xxx/xxx.xx.xxxx/xx3', // 我是该接口注释
  '/xxx/xxx.xx.xxxx/xx4', // 我是该接口注释
  '/xxx/xxx.xx.xxxx/xx5' // 我是该接口注释
],
// cookie信息
COOKIE_VALID_DURATION: 1000 * 60 * 60 * 24 * 30 // 有效期30天
}

module.exports = config

src/api/ajax.js:

/**
* http请求拦截器
*/
const Fly = require('flyio/dist/npm/wx')
const config = require('./config')

const log = function (msg) {
console.log(msg)
}
// 以下为生成简易随机的x-request-id的函数
const CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split
/**
* 随机生成x-request-id
*/
let getXRequestId = function () {
let chars = CHARS
let uuid = new Array(36)
let rnd = 0
let r
for (let i = 0; i < 36; i++) {
  if (i === 8 || i === 13 || i === 18 || i === 23) {
    uuid[i] = '-'
  } else if (i === 14) {
    uuid[i] = '4'
  } else {
    if (rnd <= 0x02) rnd = 0x2222222 + (Math.random() * 0x1222222) | 0
    r = rnd & 0xf
    rnd = rnd >> 4
    uuid[i] = chars[(i === 19) ? (r & 0x3) | 0x8 : r]
  }
}
return uuid.join('')
}
// 不同环境下的请求baseurl
const ajaxUrl =
process.env.NODE_ENV === 'development'
  ? config.Host.development
  : process.env.NODE_ENV === 'production'
    ? config.Host.production
    : config.Host.test
/**
* 定义两个flyio实例
*/
let fly = new Fly()
let loginFly = new Fly()

// 实例级配置
// 定义公共headers
const headers = {
  ...
  'x-request-id': getXRequestId()
}
Object.assign(fly.config, {
  headers: headers,
  baseURL: ajaxUrl,
  timeout: 10000,
  withCredentials: true
})
loginFly.config = fly.config

/**
* 本地重新登录
*/
const login = () => {
return new Promise((resolve) => {
  wx.login({
    success: res => {
      wx.getUserInfo({
        success: infoRes => {
          let loginParams = {
            saleChannel: 5,
            userInfo: infoRes.userInfo,
            code: res.code,
            encryptedData: infoRes.encryptedData,
            iv: infoRes.iv
          }
          wx.showLoading({
            title: '正在重新登录'
          })
          // 记住要使用新的fly实例loginFly,因为旧的fly 实例已被锁定
          loginFly.post('/xxxx/xxxxx.xxxx.xxx.xxxxxxx/xxxx', loginParams)
            .then(d => {
              if (!d.data.hasOwnProperty('success')) {
                wx.showLoading({
                  title: '重新登录已完成'
                })
                if (d.headers && typeof d.headers['set-cookie'][0] !== 'undefined') {
                  // 当前时间戳
                  let timeStampExpired = new Date().getTime()
                  if (typeof d.headers['date'][0] !== 'undefined') {
                    //将respondHeader里的date转化为时间戳
                    timeStampExpired = new Date(d.headers['date'][0]).getTime() + config.COOKIE_VALID_DURATION
                  } else {
                    timeStampExpired += config.COOKIE_VALID_DURATION
                  }
                  // 将cookie信息存储在本地
                  wx.setStorageSync('COOKIE_EXPIRES', {
                    cookieValue: d.headers['set-cookie'][0],
                    expiredTimeStamp: timeStampExpired
                  })
                }
                // 返回新的cookie值
                resolve({
                  newCookie: d.headers['set-cookie'][0]
                })
              } else {
                wx.showToast({
                  title: '本地登录失败',
                  icon: 'none',
                  duration: 2000
                })
              }
            })
        }
      })
    },
    fail: res => {
      console.error(res.errMsg)
    },
    complete: res => {}
  })
})
}

/**
* 检测本地cookie信息是否存在以及过期
*/
const _checkCookieExpires = () => {
return new Promise((resolve, reject) => {
  const nowTimeStamp = new Date().getTime()
  const cookieExpiresInfo = wx.getStorageSync('COOKIE_EXPIRES')
  // console.log(cookieExpiresInfo)
  // 本地不存在cookie
  if (!cookieExpiresInfo) {
    resolve({
      isCookieExpired: true,
      cookieValue: null
    })
  }
  // 本地存储的cookie有问题
  if (!cookieExpiresInfo.hasOwnProperty('expiredTimeStamp')) {
    reject(new Error('本地数据有问题'))
  }
  // 本地cookie未过期
  if (nowTimeStamp < cookieExpiresInfo.expiredTimeStamp) {
    resolve({
      isCookieExpired: false,
      cookieValue: cookieExpiresInfo.cookieValue
    })
  } else {
  // 本地cookie已过期
    resolve({
      isCookieExpired: true,
      cookieValue: null
    })
  }
})
}

/**
* 请求拦截器
*/
fly.interceptors.request.use(request => {
// log(request)
// 白名单内的url不需要检查cookie
if (config.urlNotNeedSession.includes(request.url)) {
  return request
} else {
  // 如果在本地存储中没有COOKIE_EXPIRES  或者  COOKIE_EXPIRES的EXPIRES_TIME已过期,则重新请求cookie
  return _checkCookieExpires()
    .then(res => {
      // log(res)
      if (res.isCookieExpired) {
        wx.showLoading({
          title: '本地登录已失效'
        })
        log('已有cookie失效,即将请求新的cookie')
        // 锁定当前fly实例
        fly.lock()
        return login()
          .then((loginRes) => {
            wx.hideLoading()
            log('cookie已更新')
            // log(`重新请求:path:${response.request.url},baseURL:${response.request.baseURL}`)
            // 将header中的cookie置为新获取的cookie
            request.headers.cookie = loginRes.newCookie
            return request
          })
          .finally(() => {
          // 解锁当前fly实例
            fly.unlock()
          })
      } else {
        request.headers.cookie = res.cookieValue
        return request
      }
    })
    .catch(errInfo => {
      log(errInfo)
    })
}
})

/**
* 响应拦截器
*/
fly.interceptors.response.use(
response => {
  // session已经失效,需要重新登录小程序
  if (response.data.errCode === 100009) {
    wx.showLoading({
      title: '本地登录已失效'
    })
    log('本地已有cookie失效,即将请求新的cookie')
    // 锁定当前fly实例
    fly.lock()
    return login()
      .then(() => {
        wx.hideLoading()
        log('cookie已更新')
      })
      .finally(() => {
        // 解锁当前fly实例
        fly.unlock()
      })
      .then(() => {
        log(`重新请求:path:${response.request.url},baseURL:${response.request.baseURL}`)
        return fly.request(response.request)
      })
  } else {
    return response.data
  }
},
err => {
  if (err.status) {
    wx.showModal({
      title: '温馨提示',
      content: 'blablablaaaaa',
      showCancel: true,
      cancelText: '取消',
      success: (res) => {
        if (res.confirm) {
          ...
          ...
        }
        if (res.cancel) {
          log('error-interceptor', err)
        }
      }
    })
  } else {
    wx.showModal({
      title: '温馨提示',
      content: '无法获取数据,请检查您的设备网络',
      showCancel: false,
      success: (res) => {
        if (res.confirm) {
          log(res)
        }
      }
    })
  }
}
)

export default fly

四.总结

    之前在响应拦截器中设置的根据后端返回cookie过期信息后更新cookie的代码并没有删除,一切为了防止意外情况的发生。
    总的来讲就我们目前的需求来讲,整套下来覆盖的已经比较全面了,如果你正在做mpvue项目的小程序并且准备采用同样的策略,那么这个拦截器据对适合你,代码稍作修改即可~如果你在做小程序之外的其他种类的项目,或者用其他框架开发的小程序,那也无所谓,因为不管框架/项目如何变,思想总是可以通用的,你同样可以在这边文章里找到你想要的!但是呢,如果有用的话,千万不能只ctrl+c/ctrl+v,一定要做到真正理解其实现原理,这样我们才能自己写出更好更优解的代码来!
    如果有发现问题的话,欢迎指正!如果有更优解的话,也欢迎讨论!非常感谢!