前段時間寫了篇文章《axios如何利用promise無痛刷新token》,陸陸續續收到一些反饋。發現很多同窗會想要從在請求前攔截
的思路入手,甚至收到了幾個郵件來詢問博主遇到的問題,因此索性再寫一篇文章來講說另外一個思路的實現和注意的地方。過程會稍微囉嗦,不想看實現過程的同窗能夠直接拉到最後面看最終代碼。前端
PS:在本文就略過一些前提條件了,請新同窗閱讀本文前先看一下前一篇文章《axios如何利用promise無痛刷新token》。ios
前端登陸後,後端返回token
和token有效時間段tokenExprieIn
,當token過時時間到了,前端須要主動用舊token去獲取一個新的token,作到用戶無感知地去刷新token。json
PS:
tokenExprieIn
是一個單位爲秒的時間段,不建議使用絕對時間,絕對時間可能會因爲本地和服務器時區不同致使出現問題。axios
在請求發起前攔截每一個請求,判斷token的有效時間是否已通過期,若已過時,則將請求掛起,先刷新token後再繼續請求。後端
不在請求前攔截,而是攔截返回後的數據。先發起請求,接口返回過時後,先刷新token,再進行一次重試。api
前文已經實現了方法二,本文會從頭實現一下方法一。數組
在請求前進行攔截,咱們主要會使用axios.interceptors.request.use()
這個方法。照例先封裝個request.js
的基本骨架:promise
import axios from 'axios' // 從localStorage中獲取token,token存的是object信息,有tokenExpireTime和token兩個字段 function getToken () { let tokenObj = {} try { tokenObj = storage.get('token') tokenObj = tokenObj ? JSON.parse(tokenObj) : {} } catch { console.error('get token from localStorage error') } return tokenObj } // 給實例添加一個setToken方法,用於登陸後方便將最新token動態添加到header,同時將token保存在localStorage中 instance.setToken = (obj) => { instance.defaults.headers['X-Token'] = obj.token window.localStorage.setItem('token', JSON.stringify(obj)) // 注意這裏須要變成字符串後才能放到localStorage中 } // 建立一個axios實例 const instance = axios.create({ baseURL: '/api', timeout: 300000, headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }) // 請求發起前攔截 instance.interceptors.request.use((config) => { const tokenObj = getToken() // 爲每一個請求添加token請求頭 config.headers['X-Token'] = tokenObj.token // **接下來主要攔截的實現就在這裏** return config }, (error) => { // Do something with request error return Promise.reject(error) }) // 請求返回後攔截 instance.interceptors.response.use(response => { const { code } = response.data if (code === 1234) { // token過時了,直接跳轉到登陸頁 window.location.href = '/' } return response }, error => { console.log('catch', error) return Promise.reject(error) }) export default instance 複製代碼
與前文略微不一樣的是,因爲方法二不須要用到過時時間,因此前文localStorage中只存了token一個字符串,而方法一這裏須要用到過時時間了,因此得存多一個數據,所以localStorage中存的是Object
類型的數據,從localStorage中取值出來須要JSON.parse
一下,爲了防止發生錯誤因此儘可能使用try...catch
。bash
首先不須要想得太複雜,先不考慮多個請求同時進來的狀況,咱從最多見的場景入手:從localStorage拿到上一次存儲的過時時間,判斷是否已經到了過時時間,是就當即刷新token而後再發起請求。服務器
function refreshToken () { // instance是當前request.js中已建立的axios實例 return instance.post('/refreshtoken').then(res => res.data) } instance.interceptors.request.use((config) => { const tokenObj = getToken() // 爲每一個請求添加token請求頭 config.headers['X-Token'] = tokenObj.token if (tokenObj.token && tokenObj.tokenExpireTime) { const now = Date.now() if (now >= tokenObj.tokenExpireTime) { // 當前時間大於過時時間,說明已通過期了,返回一個Promise,執行refreshToken後再return當前的config return refreshToken().then(res => { const { token, tokenExprieIn } = res.data const tokenExpireTime = now + tokenExprieIn * 1000 instance.setToken({ token, tokenExpireTime }) // 存token到localStorage console.log('刷新成功, return config便是恢復當前請求') config.headers['X-Token'] = token // 將最新的token放到請求頭 return config }).catch(res => { console.error('refresh token error: ', res) }) } } return config }, (error) => { // Do something with request error return Promise.reject(error) }) 複製代碼
這裏有兩個須要注意的地方:
tokenExpireIn
,而咱們存到localStorage中的是已是一個基於當前時間
和有效時間段
算出的最終時間tokenExpireTime
,是一個絕對時間,好比當前時間是12點,有效時間是3600秒(1個小時),則存到localStorage的過時時間是13點的時間戳,這樣能夠少存一個當前時間的字段到localStorage中,使用時只須要判斷該絕對時間便可。instance.interceptors.request.use
中返回一個Promise,就可使得該請求是先執行refreshToken
後再return config
的,才能保證先刷新token後再真正發起請求。其實博主直接運行上面代碼後發現了一個嚴重錯誤,進入了一個死循環。這是由於博主沒有注意到一個問題:axios.interceptors.request.use()
會攔截全部使用該實例發起的請求,即執行refreshToken()
時又一次進入了axios.interceptors.request.use()
,致使一直在return refreshToken()
。
所以須要將刷新token和登陸這兩種狀況排除出去,登陸和刷新token都不須要判斷是否過時的攔截,咱們能夠經過config.url來判斷是哪一個接口:
instance.interceptors.request.use((config) => { const tokenObj = getToken() // 爲每一個請求添加token請求頭 config.headers['X-Token'] = tokenObj.token // 登陸接口和刷新token接口繞過 if (config.url.indexOf('/refreshToken') >= 0 || config.url.indexOf('/login') >= 0) { return config } if (tokenObj.token && tokenObj.tokenExpireTime) { const now = Date.now() if (now >= tokenObj.tokenExpireTime) { // 當前時間大於過時時間,說明已通過期了,返回一個Promise,執行refreshToken後再return當前的config return refreshToken().then(res => { const { token, tokenExprieIn } = res.data const tokenExpireTime = now + tokenExprieIn * 1000 instance.setToken({ token, tokenExpireTime }) // 存token到localStorage console.log('刷新成功, return config便是恢復當前請求') config.headers['X-Token'] = token // 將最新的token放到請求頭 return config }).catch(res => { console.error('refresh token error: ', res) }) } } return config }, (error) => { // Do something with request error return Promise.reject(error) }) 複製代碼
接下來就是要考慮複雜一點的問題了
當幾乎同時進來兩個請求,爲了不屢次執行refreshToken,須要引入一個isRefreshing
的進行標記:
let isRefreshing = false instance.interceptors.request.use((config) => { const tokenObj = getToken() // 爲每一個請求添加token請求頭 config.headers['X-Token'] = tokenObj.token // 登陸接口和刷新token接口繞過 if (config.url.indexOf('/refreshToken') >= 0 || config.url.indexOf('/login') >= 0) { return config } if (tokenObj.token && tokenObj.tokenExpireTime) { const now = Date.now() if (now >= tokenObj.tokenExpireTime) { if (!isRefreshing) { isRefreshing = true return refreshToken().then(res => { const { token, tokenExprieIn } = res.data const tokenExpireTime = now + tokenExprieIn * 1000 instance.setToken({ token, tokenExpireTime }) // 存token到localStorage isRefreshing = false //刷新成功,恢復標誌位 config.headers['X-Token'] = token // 將最新的token放到請求頭 return config }).catch(res => { console.error('refresh token error: ', res) }) } } } return config }, (error) => { // Do something with request error return Promise.reject(error) }) 複製代碼
咱們已經知道了當前已通過期或者正在刷新token,此時再有請求發起,就應該讓後面的這些請求等一等,等到refreshToken結束後再真正發起,因此須要用到一個Promise來讓它一直等。然後面的全部請求,咱們將它們存放到一個requests
的隊列中,等刷新token後再依次resolve
。
instance.interceptors.request.use((config) => { const tokenObj = getToken() // 添加請求頭 config.headers['X-Token'] = tokenObj.token // 登陸接口和刷新token接口繞過 if (config.url.indexOf('/refreshToken') >= 0 || config.url.indexOf('/login') >= 0) { return config } if (tokenObj.token && tokenObj.tokenExpireTime) { const now = Date.now() if (now >= tokenObj.tokenExpireTime) { // 當即刷新token if (!isRefreshing) { console.log('刷新token ing') isRefreshing = true refreshToken().then(res => { const { token, tokenExprieIn } = res.data const tokenExpireTime = now + tokenExprieIn * 1000 instance.setToken({ token, tokenExpireTime }) isRefreshing = false return token }).then((token) => { console.log('刷新token成功,執行隊列') requests.forEach(cb => cb(token)) // 執行完成後,清空隊列 requests = [] }).catch(res => { console.error('refresh token error: ', res) }) } const retryOriginalRequest = new Promise((resolve) => { requests.push((token) => { // 由於config中的token是舊的,因此刷新token後要將新token傳進來 config.headers['X-Token'] = token resolve(config) }) }) return retryOriginalRequest } } return config }, (error) => { // Do something with request error return Promise.reject(error) }) 複製代碼
這裏作了一點改動,注意到refreshToken()
這一句前面去掉了return
,而是改成了在後面return retryOriginalRequest
,即當發現有請求是過時的就存進requests
數組,等refreshToken結束後再執行requests
隊列,這是爲了避免影響原來的請求執行次序。 咱們假設同時有請求1
,請求2
,請求3
依次同時進來,咱們但願是請求1
發現過時,refreshToken後再依次執行請求1
,請求2
,請求3
。 按以前return refreshToken()
的寫法,會大概寫成這樣
if (tokenObj.token && tokenObj.tokenExpireTime) { const now = Date.now() if (now >= tokenObj.tokenExpireTime) { // 當即刷新token if (!isRefreshing) { console.log('刷新token ing') isRefreshing = true return refreshToken().then(res => { const { token, tokenExprieIn } = res.data const tokenExpireTime = now + tokenExprieIn * 1000 instance.setToken({ token, tokenExpireTime }) isRefreshing = false config.headers['X-Token'] = token return config // 請求1 }).catch(res => { console.error('refresh token error: ', res) }).finally(() => { console.log('執行隊列') requests.forEach(cb => cb(token)) // 執行完成後,清空隊列 requests = [] }) } else { // 只有請求2和請求3能進入隊列 const retryOriginalRequest = new Promise((resolve) => { requests.push((token) => { config.headers['X-Token'] = token resolve(config) }) }) return retryOriginalRequest } } } return config 複製代碼
隊列裏面只有請求2
和請求3
,代碼看起來應該是return了請求1後,再在finally執行隊列的,但實際的執行順序會變成請求2
,請求3
,請求1
,即請求1變成了最後一個執行的,會改變執行順序。
因此博主換了個思路,不管是哪一個請求進入了過時流程,咱們都將請求放到隊列中,都return一個未resolve的Promise,等刷新token結束後再一一清算,這樣就能夠保證請求1
,請求2
,請求3
這樣按原來順序執行了。
這裏多說一句,可能不少剛接觸前端的同窗沒法理解requests.forEach(cb => cb(token))
是如何執行的。
// 咱們先看一下,定義fn1 function fn1 () { console.log('執行fn1') } // 執行fn1,只需後面加個括號 fn1() // 迴歸到咱們request數組中,每一項其實存的就是一個相似fn1的一個函數 const fn2 = (token) => { config.headers['X-Token'] = token resolve(config) } // 咱們要執行fn2,也只需在後面加個括號就能夠了 fn2() // 因爲requests是一個數組,因此咱們想遍歷執行裏面的全部的項,因此用上了forEach requests.forEach(fn => { // 執行fn fn() }) 複製代碼
import axios from 'axios' // 從localStorage中獲取token,token存的是object信息,有tokenExpireTime和token兩個字段 function getToken () { let tokenObj = {} try { tokenObj = storage.get('token') tokenObj = tokenObj ? JSON.parse(tokenObj) : {} } catch { console.error('get token from localStorage error') } return tokenObj } function refreshToken () { // instance是當前request.js中已建立的axios實例 return instance.post('/refreshtoken').then(res => res.data) } // 給實例添加一個setToken方法,用於登陸後方便將最新token動態添加到header,同時將token保存在localStorage中 instance.setToken = (obj) => { instance.defaults.headers['X-Token'] = obj.token window.localStorage.setItem('token', JSON.stringify(obj)) // 注意這裏須要變成字符串後才能放到localStorage中 } instance.interceptors.request.use((config) => { const tokenObj = getToken() // 添加請求頭 config.headers['X-Token'] = tokenObj.token // 登陸接口和刷新token接口繞過 if (config.url.indexOf('/rereshToken') >= 0 || config.url.indexOf('/login') >= 0) { return config } if (tokenObj.token && tokenObj.tokenExpireTime) { const now = Date.now() if (now >= tokenObj.tokenExpireTime) { // 當即刷新token if (!isRefreshing) { console.log('刷新token ing') isRefreshing = true refreshToken().then(res => { const { token, tokenExprieIn } = res.data const tokenExpireTime = now + tokenExprieIn * 1000 instance.setToken({ token, tokenExpireTime }) isRefreshing = false return token }).then((token) => { console.log('刷新token成功,執行隊列') requests.forEach(cb => cb(token)) // 執行完成後,清空隊列 requests = [] }).catch(res => { console.error('refresh token error: ', res) }) } const retryOriginalRequest = new Promise((resolve) => { requests.push((token) => { // 由於config中的token是舊的,因此刷新token後要將新token傳進來 config.headers['X-Token'] = token resolve(config) }) }) return retryOriginalRequest } } return config }, (error) => { // Do something with request error return Promise.reject(error) }) // 請求返回後攔截 instance.interceptors.response.use(response => { const { code } = response.data if (code === 1234) { // token過時了,直接跳轉到登陸頁 window.location.href = '/' } return response }, error => { console.log('catch', error) return Promise.reject(error) }) export default instance 複製代碼
建議一步步調試的同窗,能夠先去掉window.location.href = '/'
這個跳轉,保留log方便調試。
感謝看到最後,感謝點贊^_^。