前言
前段時間寫了篇文章《axios如何利用promise無痛刷新token》血巍,陸陸續(xù)續(xù)收到一些反饋雏搂。發(fā)現(xiàn)不少同學(xué)會想要從在請求前攔截
的思路入手藕施,甚至收到了幾個郵件來詢問博主遇到的問題,所以索性再寫一篇文章來說說另一個思路的實(shí)現(xiàn)和注意的地方凸郑。過程會稍微啰嗦裳食,不想看實(shí)現(xiàn)過程的同學(xué)可以直接拉到最后面看最終代碼。
PS:在本文就略過一些前提條件了芙沥,請新同學(xué)閱讀本文前先看一下前一篇文章《axios如何利用promise無痛刷新token》诲祸。
前提條件
前端登錄后,后端返回token
和token有效時間段tokenExprieIn
而昨,當(dāng)token過期時間到了救氯,前端需要主動用舊token去獲取一個新的token,做到用戶無感知地去刷新token歌憨。
PS:
tokenExprieIn
是一個單位為秒的時間段着憨,不建議使用絕對時間,絕對時間可能會由于本地和服務(wù)器時區(qū)不一樣導(dǎo)致出現(xiàn)問題务嫡。
實(shí)現(xiàn)思路
方法一
在請求發(fā)起前攔截每個請求甲抖,判斷token的有效時間是否已經(jīng)過期,若已過期心铃,則將請求掛起准谚,先刷新token后再繼續(xù)請求。
方法二
不在請求前攔截去扣,而是攔截返回后的數(shù)據(jù)柱衔。先發(fā)起請求,接口返回過期后愉棱,先刷新token唆铐,再進(jìn)行一次重試。
前文已經(jīng)實(shí)現(xiàn)了方法二奔滑,本文會從頭實(shí)現(xiàn)一下方法一艾岂。
實(shí)現(xiàn)
基本骨架
在請求前進(jìn)行攔截,我們主要會使用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
}
// 給實(shí)例添加一個setToken方法,用于登錄后方便將最新token動態(tài)添加到header叼耙,同時將token保存在localStorage中
instance.setToken = (obj) => {
instance.defaults.headers['X-Token'] = obj.token
window.localStorage.setItem('token', JSON.stringify(obj)) // 注意這里需要變成字符串后才能放到localStorage中
}
// 創(chuàng)建一個axios實(shí)例
const instance = axios.create({
baseURL: '/api',
timeout: 300000,
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
// 請求發(fā)起前攔截
instance.interceptors.request.use((config) => {
const tokenObj = getToken()
// 為每個請求添加token請求頭
config.headers['X-Token'] = tokenObj.token
// **接下來主要攔截的實(shí)現(xiàn)就在這里**
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過期了腕窥,直接跳轉(zhuǎn)到登錄頁
window.location.href = '/'
}
return response
}, error => {
console.log('catch', error)
return Promise.reject(error)
})
export default instance
與前文略微不同的是,由于方法二不需要用到過期時間筛婉,所以前文localStorage中只存了token一個字符串簇爆,而方法一這里需要用到過期時間了,所以得存多一個數(shù)據(jù)爽撒,因此localStorage中存的是Object
類型的數(shù)據(jù)入蛆,從localStorage中取值出來需要JSON.parse
一下,為了防止發(fā)生錯誤所以盡量使用try...catch
硕勿。
axios.interceptors.request.use()實(shí)現(xiàn)
首先不需要想得太復(fù)雜哨毁,先不考慮多個請求同時進(jìn)來的情況,咱從最常見的場景入手:從localStorage拿到上一次存儲的過期時間源武,判斷是否已經(jīng)到了過期時間扼褪,是就立即刷新token然后再發(fā)起請求。
function refreshToken () {
// instance是當(dāng)前request.js中已創(chuàng)建的axios實(shí)例
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) {
// 當(dāng)前時間大于過期時間粱栖,說明已經(jīng)過期了话浇,返回一個Promise,執(zhí)行refreshToken后再return當(dāng)前的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即是恢復(fù)當(dāng)前請求')
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的接口返回的是一個單位為秒的時間段
tokenExpireIn
闹究,而我們存到localStorage中的是已經(jīng)是一個基于當(dāng)前時間
和有效時間段
算出的最終時間tokenExpireTime
幔崖,是一個絕對時間,比如當(dāng)前時間是12點(diǎn)渣淤,有效時間是3600秒(1個小時)赏寇,則存到localStorage的過期時間是13點(diǎn)的時間戳,這樣可以少存一個當(dāng)前時間的字段到localStorage中砂代,使用時只需要判斷該絕對時間即可蹋订。 -
instance.interceptors.request.use
中返回一個Promise率挣,就可以使得該請求是先執(zhí)行refreshToken
后再return config
的刻伊,才能保證先刷新token后再真正發(fā)起請求。
其實(shí)博主直接運(yùn)行上面代碼后發(fā)現(xiàn)了一個嚴(yán)重錯誤椒功,進(jìn)入了一個死循環(huán)捶箱。這是因為博主沒有注意到一個問題:axios.interceptors.request.use()
會攔截所有使用該實(shí)例發(fā)起的請求,即執(zhí)行refreshToken()
時又一次進(jìn)入了axios.interceptors.request.use()
动漾,導(dǎo)致一直在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) {
// 當(dāng)前時間大于過期時間晨川,說明已經(jīng)過期了证九,返回一個Promise,執(zhí)行refreshToken后再return當(dāng)前的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即是恢復(fù)當(dāng)前請求')
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)
})
問題和優(yōu)化
接下來就是要考慮復(fù)雜一點(diǎn)的問題了
防止多次刷新token
當(dāng)幾乎同時進(jìn)來兩個請求共虑,為了避免多次執(zhí)行refreshToken愧怜,需要引入一個isRefreshing
的進(jìn)行標(biāo)記:
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 //刷新成功,恢復(fù)標(biāo)志位
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后再發(fā)起
我們已經(jīng)知道了當(dāng)前已經(jīng)過期或者正在刷新token拥坛,此時再有請求發(fā)起,就應(yīng)該讓后面的這些請求等一等猜惋,等到refreshToken結(jié)束后再真正發(fā)起,所以需要用到一個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成功蕾羊,執(zhí)行隊列')
requests.forEach(cb => cb(token))
// 執(zhí)行完成后,清空隊列
requests = []
}).catch(res => {
console.error('refresh token error: ', res)
})
}
const retryOriginalRequest = new Promise((resolve) => {
requests.push((token) => {
// 因為config中的token是舊的,所以刷新token后要將新token傳進(jìn)來
config.headers['X-Token'] = token
resolve(config)
})
})
return retryOriginalRequest
}
}
return config
}, (error) => {
// Do something with request error
return Promise.reject(error)
})
這里做了一點(diǎn)改動,注意到refreshToken()
這一句前面去掉了return
扒寄,而是改為了在后面return retryOriginalRequest
隔披,即當(dāng)發(fā)現(xiàn)有請求是過期的就存進(jìn)requests
數(shù)組英上,等refreshToken結(jié)束后再執(zhí)行requests
隊列杀糯,這是為了不影響原來的請求執(zhí)行次序。
我們假設(shè)同時有請求1
弃衍,請求2
呀非,請求3
依次同時進(jìn)來,我們希望是請求1
發(fā)現(xiàn)過期,refreshToken后再依次執(zhí)行請求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('執(zhí)行隊列')
requests.forEach(cb => cb(token))
// 執(zhí)行完成后恩闻,清空隊列
requests = []
})
} else {
// 只有請求2和請求3能進(jìn)入隊列
const retryOriginalRequest = new Promise((resolve) => {
requests.push((token) => {
config.headers['X-Token'] = token
resolve(config)
})
})
return retryOriginalRequest
}
}
}
return config
隊列里面只有請求2
和請求3
,代碼看起來應(yīng)該是return了請求1后剧董,再在finally執(zhí)行隊列的幢尚,但實(shí)際的執(zhí)行順序會變成請求2
,請求3
翅楼,請求1
侠草,即請求1變成了最后一個執(zhí)行的,會改變執(zhí)行順序犁嗅。
所以博主換了個思路边涕,無論是哪個請求進(jìn)入了過期流程,我們都將請求放到隊列中褂微,都return一個未resolve的Promise功蜓,等刷新token結(jié)束后再一一清算,這樣就可以保證請求1
宠蚂,請求2
式撼,請求3
這樣按原來順序執(zhí)行了。
這里多說一句求厕,可能很多剛接觸前端的同學(xué)無法理解requests.forEach(cb => cb(token))
是如何執(zhí)行的著隆。
// 我們先看一下,定義fn1
function fn1 () {
console.log('執(zhí)行fn1')
}
// 執(zhí)行fn1,只需后面加個括號
fn1()
// 回歸到我們request數(shù)組中呀癣,每一項其實(shí)存的就是一個類似fn1的一個函數(shù)
const fn2 = (token) => {
config.headers['X-Token'] = token
resolve(config)
}
// 我們要執(zhí)行fn2美浦,也只需在后面加個括號就可以了
fn2()
// 由于requests是一個數(shù)組,所以我們想遍歷執(zhí)行里面的所有的項项栏,所以用上了forEach
requests.forEach(fn => {
// 執(zhí)行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是當(dāng)前request.js中已創(chuàng)建的axios實(shí)例
return instance.post('/refreshtoken').then(res => res.data)
}
// 給實(shí)例添加一個setToken方法沼沈,用于登錄后方便將最新token動態(tài)添加到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成功,執(zhí)行隊列')
requests.forEach(cb => cb(token))
// 執(zhí)行完成后列另,清空隊列
requests = []
}).catch(res => {
console.error('refresh token error: ', res)
})
}
const retryOriginalRequest = new Promise((resolve) => {
requests.push((token) => {
// 因為config中的token是舊的芽腾,所以刷新token后要將新token傳進(jìn)來
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過期了,直接跳轉(zhuǎn)到登錄頁
window.location.href = '/'
}
return response
}, error => {
console.log('catch', error)
return Promise.reject(error)
})
export default instance
建議一步步調(diào)試的同學(xué)页衙,可以先去掉window.location.href = '/'
這個跳轉(zhuǎn)摊滔,保留log方便調(diào)試。
感謝看到最后,感謝點(diǎn)贊_惭载。