前言
前段時間寫了篇文章《axios如何利用promise無痛重新整理token》,陸陸續續收到一些回報。發現不少同學會想要從
在請求前攔截
的思路入手,甚至收到了幾個郵件來詢問部落客遇到的問題,是以索性再寫一篇文章來說說另一個思路的實作和注意的地方。過程會稍微啰嗦,不想看實作過程的同學可以直接拉到最後面看最終代碼。
PS:在本文就略過一些前提條件了,請新同學閱讀本文前先看一下前一篇文章《axios如何利用promise無痛重新整理token》。
前提條件
前端登入後,後端傳回
token
和token有效時間段
tokenExprieIn
,當token過期時間到了,前端需要主動用舊token去擷取一個新的token,做到使用者無感覺地去重新整理token。
PS: tokenExprieIn
是一個機關為秒的時間段,不建議使用絕對時間,絕對時間可能會由于本地和伺服器時區不一樣導緻出現問題。
實作思路
方法一
在請求發起前攔截每個請求,判斷token的有效時間是否已經過期,若已過期,則将請求挂起,先重新整理token後再繼續請求。
方法二
不在請求前攔截,而是攔截傳回後的資料。先發起請求,接口傳回過期後,先重新整理token,再進行一次重試。
前文已經實作了方法二,本文會從頭實作一下方法一。
實作
基本骨架
在請求前進行攔截,我們主要會使用
axios.interceptors.request.use()
這個方法。照例先封裝個
request.js
的基本骨架:
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
。
axios.interceptors.request.use()實作
首先不需要想得太複雜,先不考慮多個請求同時進來的情況,咱從最常見的場景入手:從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)
})
這裡有兩個需要注意的地方:
- 之前說到登入或重新整理token的接口傳回的是一個機關為秒的時間段
,而我們存到localStorage中的是已經是一個基于tokenExpireIn
和目前時間
算出的最終時間有效時間段
,是一個絕對時間,比如目前時間是12點,有效時間是3600秒(1個小時),則存到localStorage的過期時間是13點的時間戳,這樣可以少存一個目前時間的字段到localStorage中,使用時隻需要判斷該絕對時間即可。tokenExpireTime
-
中傳回一個Promise,就可以使得該請求是先執行instance.interceptors.request.use
後再refreshToken
的,才能保證先重新整理token後再真正發起請求。return config
其實部落客直接運作上面代碼後發現了一個嚴重錯誤,進入了一個死循環。這是因為部落客沒有注意到一個問題:
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)
})
問題和優化
接下來就是要考慮複雜一點的問題了
防止多次重新整理token
當幾乎同時進來兩個請求,為了避免多次執行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後再發起
我們已經知道了目前已經過期或者正在重新整理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友善調試。
感謝看到最後,感謝點贊^_^。