一、需求
此项目为小程序。小程序完成第一版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 = thisreturn new Promise((resolve, reject) => {options.url =options.url.indexOf('http') === 0? options.url: this.baseURL + options.urloptions.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 = truerefreshTokenByTime.refreshToken().then(() => {// 接口调用成功后,将request中的接口都调用一遍// 重点: 用于存储数据pendingRequests.forEach(item => {// 由于已经更新了token,则需要传新的token了item.options.header.Authorization = store.getters.getTokenitem.options.header.refreshToken = store.getters.getRefreshTokenitem.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 = thisuni.hideLoading()return new Promise((resolve, reject) => {const [err, uniRes] = dataif (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方法就是处理接口返回的一 // 些异常信息。同样是返回的promisereturn 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.gatewaylet pendingRequests = [] // 接口排队队列function requestObj(options) {this.config = {baseURL: basePath,options: options,// 请求拦截器beforeRequest(options = {}) {const $this = thisreturn new Promise((resolve, reject) => {options.url =options.url.indexOf('http') === 0? options.url: this.baseURL + options.urloptions.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 = truerefreshTokenByTime.refreshToken().then(() => {// 接口调用成功后,将request中的接口都调用一遍// 重点: 用于存储数据pendingRequests.forEach(item => {// 由于已经更新了token,则需要传新的token了item.options.header.Authorization = store.getters.getTokenitem.options.header.refreshToken = store.getters.getRefreshTokenitem.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 = thisuni.hideLoading()return new Promise((resolve, reject) => {const [err, uniRes] = dataif (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.datareturn uni.uploadFile(opt)}).then(res => this.config.handleResponse(res))}
}module.exports = requestObj
本文链接:https://my.lmcjl.com/post/12746.html
4 评论