一.场景
现有功能为token过期后跳转登录页重新登录,需要实现token过期后无感刷新
后端更新登录接口数据,以前只给了token,改为
新增数据 | 作用 |
---|---|
accessToken | 原token |
accessTokenExpiry | accesToken有效日期的时间戳,有效时间一天(例) |
refreshToken | 用来刷新accesToken |
refreshTokenExpiry | refreshToken有效日期的时间戳,有效时间三十天(例) |
二.思路
以前是判断接口返回token已过期的错误信息,未过期继续,过期则会跳转
所以就有了两种思路:
1.请求拦截中处理
2.相应拦截中处理
个人认为在相应拦截中处理会浪费一次请求,所以选择了在在请求拦截中处理
三.实现过程
一个很重要的点:Promise可以被另一个Promise锁定
下面是做的一些准备和初步实现:
//模拟数据
let mock = {
accessToken: '123',
accessTokenExpiry: true,
refreshToken: 'asd',
refreshTokenExpiry: false
}
//模拟刷新接口
function refreshApi() {
return request({
url: '/refresh',
method: 'get'
})
}
//白名单防止死循环
const whiteList = ['/refresh']
请求拦截
request.interceptors.request.use(config => {
// 判断白名单
if (whiteList.some(item => item === config.url)) {
return config
}
config.headers.token = mock.accessToken
// 判断accessToken是否过期,refreshToken是否过期
if (mock.accessTokenExpiry && !mock.refreshTokenExpiry) {
console.log('accessTokenExpiry已过期,refreshToken未过期')
return refreshApi().then(res => {
mock.accessToken = res.data + ''
mock.accessTokenExpiry = true
config.headers.token = mock.accessToken
return config
}
} else {
return config
}
})
这样确实是实现了在请求拦截中判断并刷新token,但是只能用在单个请求,如果用户进入的页面一次会发出多个请求,会出现一个请求刷新一次token的情况,所以做出以下变化:
1.新增变量refreshIng,用来判断当前是否已经开始刷新token
2.准备一个数组,将每个拦截器返回的promise的resolve封装在回调函数中依次存放在数组中,像这样:
let list = []
request.interceptors.request.use(config => {
...
return new Promise(reslove => {
// 因为return 的Promise一直在等resolve,所以这个拦截器暂时被锁定了,
let cb = token => {
config.headers.token = token
reslove(config)
}
list.push(cb)
})
...
})
3.当刷新token的接口返回时,调用并更改list里每一个config所携带的token,并且reslove
request.interceptors.request.use(config => {
...
// 判断是否已经在刷新token
if (!refreshIng) {
refreshIng = true
refreshApi()
.then(res => {
mock.accessToken = res.data + ''
mock.accessTokenExpiry = true
refreshIng = false
return mock.accessToken
})
.then(token=>{
// 更改token,给每个pending中的Promise 返回resolve
list.forEach(cb => cb(token))
// 清空list
list.length = 0
})
}
...
})
改完之后的拦截器完整代码如下:
let mock = {
accessToken: '123',
accessTokenExpiry: true,
refreshToken: 'asd',
refreshTokenExpiry: false
}
function refreshApi() {
return request({
url: '/refresh',
method: 'get'
})
}
const whiteList = ['/refresh']
let refreshIng = false
let list = []
request.interceptors.request.use(config => {
// 判断白名单
if (whiteList.some(item => item === config.url)) {
return config
}
config.headers.token = mock.accessToken
// 判断accessToken是否过期,refreshToken是否过期
if (mock.accessTokenExpiry && !mock.refreshTokenExpiry) {
console.log('accessTokenExpiry已过期,refreshToken未过期')
// 判断是否已经在刷新token
if (!refreshIng) {
refreshIng = true
refreshApi()
.then(res => {
mock.accessToken = res.data + ''
mock.accessTokenExpiry = true
refreshIng = false
return mock.accessToken
})
.then(res => {
// 更改token,给每个pending中的Promise 返回resolve
list.forEach(fn => fn(res))
// 清空list
list.length = 0
})
}
return new Promise(reslove => {
// 因为return 的Promise一直在等resolve,所以这个拦截器暂时被锁定了
let fn = token => {
config.headers.token = token
reslove(config)
}
list.push(fn)
})
} else {
return config
}
})
五.结果
实现了在refreshToken有效期内的无感刷新,提升了用户体验。
学习了Promise和如何在本地用express在本地开服务,真不错。