axios取消重复请求与更新token并续订上次请求
一、问题引入
当用户发起一个请求时,判断token是否已过期,若已过期则先调refreshToken接口,拿到新的token后再继续执行之前的请求。
难点:当同时发起多个请求,token 过期会调用多次更新 token 接口;此时刷新token的接口还没返回,此时其他请求该如何处理,在刷新token接口返回后才能续订请求
二、取消重复请求
针对同时发起多个请求,token 过期会调用多次更新 token 接口,这里只做了取消重复请求操作,未能续订之前请求
维护一个 请求列表 reqList
在 axios 请求拦截器中判断请求是否已经在 reqList 中存在,存在则调用 axios.CancelToken 取消请求
// 正在进行中的请求列表 let reqList = [] /** * 阻止重复请求 * @param {array} reqList - 请求缓存列表 * @param {string} url - 当前请求地址 * @param {function} cancel - 请求中断函数 * @param {string} errorMessage - 请求中断时需要显示的错误信息 */ const stopRepeatRequest = function (reqList, url, cancel, errorMessage) { const errorMsg = errorMessage || '' for (let i = 0; i < reqList.length; i++) { if (reqList[i] === url) { cancel(errorMsg) return } } reqList.push(url) } /** * 允许某个请求可以继续进行 * @param {array} reqList 全部请求列表 * @param {string} url 请求地址 */ const allowRequest = function (reqList, url) { for (let i = 0; i < reqList.length; i++) { if (reqList[i] === url) { reqList.splice(i, 1) break } } } const service = axios.create() // 请求拦截器 service.interceptors.request.use( config => { let cancel // 设置cancelToken对象 config.cancelToken = new axios.CancelToken(function(c) { cancel = c }) // 阻止重复请求。当上个请求未完成时,相同的请求不会进行 stopRepeatRequest(reqList, config.url, cancel, `${config.url} 请求被中断`) return config }, err => Promise.reject(err) ) // 响应拦截器 service.interceptors.response.use( response => { // 增加延迟,相同请求不得在短时间内重复发送 setTimeout(() => { allowRequest(reqList, response.config.url) }, 1000) // ...请求成功后的后续操作 // successHandler(response) }, error => { if (axios.isCancel(thrown)) { console.log(thrown.message); } else { // 增加延迟,相同请求不得在短时间内重复发送 setTimeout(() => { allowRequest(reqList, error.config.url) }, 1000) } // ...请求失败后的后续操作 // errorHandler(error) } )
三、更新token并续订上次请求
此方法可用于多个请求token失效,进行多次更新 token 请求操作时,取消其他更新 token 请求,并在完成更新后续订上次多个请求
// 是否正在刷新的标记 let isRefreshing = false // 重试队列,每一项将是一个待执行的函数形式 let retryRequests = [] // 请求拦截 service.interceptors.request.use( config => { const token = localStorage.getItem('access_token') if (token && !config.url.includes('/oauth/token')) { config.headers.Authorization = 'Bearer ' + token; } if (config.method === 'post' || config.method === 'put') { config.data = qs.stringify(config.data); } return config; }, err => { return Promise.reject(err); } ); // 响应拦截 service.interceptors.response.use( async response => { if (response.status === 200) { if (response.data && response.data.code == 4001) { const config = response.config if (!isRefreshing) { isRefreshing = true // const refresh_token = localStorage.getItem('refresh_token') // const token = localStorage.getItem('access_token') try { const result = await getToken({ grant_type: 'password', client_id: 'client-app', client_secret: '123', username: 'sunhj', password: '123' }) localStorage.setItem('access_token', result.token || '') // localStorage.setItem('refresh_token', res.refreshToken || '') const token = localStorage.getItem('access_token') config.headers['Authorization'] = 'Bearer ' + token // 已经刷新了token,将所有队列中的请求进行重试 retryRequests.forEach(cb => cb('Bearer ' + token)) // 重试完清空这个队列 retryRequests = [] // 这边不需要baseURL是因为会重新请求url,url中已经包含baseURL的部分了 config.baseURL = proxyHost if (config.method === 'post' || config.method === 'put') { config.data = qs.parse(config.data) } isRefreshing = false return service(config) } catch (error) { isRefreshing = false } } else { // 正在刷新token,返回一个未执行resolve的promise return new Promise((resolve) => { // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行 retryRequests.push((token) => { config.baseURL = proxyHost config.headers['Authorization'] = token if (config.method === 'post' || config.method === 'put') { config.data = qs.parse(config.data) } resolve(service(config)) }) }) } } else { // 二进制流文件下载 if (response.headers['content-type'] === "application/octet-stream;charset=UTF-8") { return response; } else { return response.data; } } } else { Promise.reject(); } }, err => { return Promise.reject(err); } );
参考:https://blog.csdn.net/weixin_45178716/article/details/103286459