本文介紹了一些Vue和axios的實用技巧,解決前端API調用中access_token的處理問題,包括:Promise的鏈式調用,axios的攔截器,vue-router記錄導航歷史等。javascript
參考項目:github.com/jasony62/tm…前端
先後端徹底分離的項目中,一個前端應用會訪問多個後端的API,API調用都要經過傳遞token進行用戶身份認證。用戶登陸就是用用戶名和口令換取token,得到token後前端自行保留(例如:放在sessionStorage裏),而後每次發起API調用時添加上這個參數。爲了安全,token會設置有效期,過時了就須要從新登陸獲取新的token。咱們能夠看到用戶登陸流程設計的核心,其實就是一個管理和使用token的問題。vue
基於token的使用,須要考慮以下狀況:java
這裏面臨幾個技術問題:ios
爲了知足上面提到的要求,須要可以控制API請求的執行過程,axios中是經過攔截器添加控制邏輯,由於咱們先深刻了解一下axios中攔截器的相關代碼。git
// Hook up interceptors middleware
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
複製代碼
要理解上面的代碼,首先要理解promise鏈式調用
和promise.then()
。github
prmise鏈式調用就將幾個promise串起來執行,上一個promise執行的結果,做爲下一個promise的輸入。看個例子:vue-router
let p1 = Promise.resolve('a')
let p2 = Promise.resolve('b')
let p3 = Promise.resolve('c')
let p4
p4 = p1.then(v1 => {
console.log('then-1', v1) // 這個是第2行輸出,輸出a
return p2.then(v2 => v1 + v2)
}).then(v1 => {
console.log('then-2', v1) // 這個是第3行輸出,輸出ab
return p3.then(v2 => v1 +v2)
})
p4.then(v => {
console.log('then-3', v) // 這個是第4行輸出,輸出abc
})
console.log('begin...') // 這個是第1行輸出
複製代碼
經過上面的方式就能夠把多個異步操做串聯起來執行。axios
Promise的then方法傳入兩個參數,分別在調用then方法的promise對象完成或失敗時調用。注意這個調用是異步調用(須要去排隊執行),這就是爲何上面的例子中最後1句console.log()
是第1個輸出,由於then中的回調函數是排隊執行的。後端
掌握then
方法的關鍵是理解返回值。首先,then
方法返回的是Promise
對象,這是能夠進行鏈式調用的基礎;第二,執行哪一個回調函數由調用then的Promise對象的執行結果決定(兩個回調函數之間沒有關係);第三,返回的Promise對象的狀態由執行的回調函數的返回值決定(和是哪一個回調函數返回無關)。例如:回調函數內返回的是一個值(數字、字符串、對象等),那麼生成的Promise對象的狀態是完成(fulfilled)。具體規則請參考在線文檔。
須要注意的是,失敗回調函數只是當前執行的promise對象的結果,並非整個鏈的結果,完成和失敗回調函數均可以經過返回值,告訴下一個環節要進入完成函數仍是失敗函數。所以,鏈式調用中每個環節均可以修正上一個環節的「錯誤」,繼續讓鏈執行。
這裏有個有意思的問題:catch
必定是除finally
外最後執行的環節嗎,它能夠寫在then
的前面嗎?答案是能夠。由於,catch
是then
的縮寫,等價於then(undefined, err=>{...})
。
參考:developer.mozilla.org/zh-CN/docs/…
明白了鏈式調用和then方法,axios的攔截器機制就好理解了。
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
複製代碼
每條攔截規則都由完成函數(fulfilled)和失敗函數(rejected)構成,能夠理解爲:請求的上一步成功了作什麼,失敗了又該作什麼。這個理解很關鍵,由於添加攔截規則時容易想成:在完成函數中添加攔截邏輯,若是這個邏輯失敗了,在失敗函數中進行處理。完成函數發生異常,失敗函數不會被執行,由於是否調用它不是由完成函數決定,而是由上一個執行環節的執行結果決定。完成函數的異常要在後續環節的失敗函數中處理。
另外,須要注意的是,請求規則和響應規則的執行順序不同,請求規則是先定義的後執行(unshift),響應規則是先定義的先執行(push)。
再有,請求規則和響應規則是在同一個鏈上,所以,請求規則中的異常,能夠由響應階段失敗函數處理。例如:不管執行請求發生了什麼問題,都須要給用戶一個消息框進行說明,那麼即便是在請求階段發生的異常,也均可以放在響應攔截規則中進行統一處理。
獲取token後能夠放在localStorage
或者sessionStorage
中,例如:
sessionStorage.setItem('access_token', token)
複製代碼
axios支持建立新實例,能夠給不一樣的實例指定不一樣的攔截規則。
axios.create(config)
複製代碼
tms-vue項目中能夠給axios實例進行命名,而且指定不一樣的攔截規則。
Vue.TmsAxios({ name: 'file-api', rules })
Vue.TmsAxios({ name: 'auth-api' })
複製代碼
經過設置攔截規則,咱們能夠對API調用的前端過程進行控制。
調用API時,圍繞token,一個axios請求可能碰到兩種狀況:一、請求階段發現token不存在,得到token後,繼續發送;二、響應階段返回token不可用,得到token後,重發請求。若是「同時」調用多個API,當前面的請求已經開始獲取token,那麼請求都應該掛起,等待新的token,不該該重複獲取token。
咱們能夠把獲取token理解爲一種須要「鎖」控制的操做,就是說只有第一個請求能夠得到鎖,進行獲取token的操做(登陸),後序的請求都被鎖住了,等待第一個請求執行的結果。一旦第1個請求執行結束,後面的請求就都得到告終果,這樣就能夠避免每一個請求都重複執行獲取token的操做。
Promise的機制能夠很好的知足上面的需求。基本思路是,咱們將登陸作成一個Promise,全部請求都等待這個Promise的執行結果。請求攔截器中添加規則(示意):
function onFulfilled(config) {
......
if (requireLogin) return loginPromise
......
}
複製代碼
經過loginPromise
就能夠將axios的請求掛起,等待登陸完成後再繼續執行。
這裏存在一個關鍵問題,loginPromise
必須是共享的,全部正在發生的請求都要等待同一個Promise。可是,由於token有有效期,用戶在整個使用過程當中有可能須要屢次登陸,loginPromise一旦執行過一次就已經處於完成(fulfilled)狀態,後序的調用並不會發起新的登陸。爲了解決這個問題,須要在全部被掛起的請求被通知登陸完成後,將loginPromise刪除,再有新請求時,生成新的Promise。
爲了解決這個問題,tms-vue
中實現了lock-promise
組件。
onst RUNNING_LOCK_PROMISE = Symbol('running_lock_promise')
class TmsLockPromise {
constructor(fnLockGetter) {
this.lockGetter = fnLockGetter
this.waitingPromises = []
}
isRunning() {
return !!this[RUNNING_LOCK_PROMISE]
}
wait() {
if (!this.isRunning()) {
this[RUNNING_LOCK_PROMISE] = this.lockGetter()
}
let prom = new Promise(resolve => {
this[RUNNING_LOCK_PROMISE].then(token => {
// 刪除處理完的請求
this.waitingPromises.splice(this.waitingPromises.indexOf(prom), 1)
// 全部的請求都處理完,關閉登陸結果
if (this.waitingPromises.length === 0) {
setTimeout(() => {
this[RUNNING_LOCK_PROMISE] = null
})
}
resolve(token)
})
})
this.waitingPromises.push(prom)
return prom
}
}
export { TmsLockPromise }
複製代碼
調用代碼以下:
let lockPromise = new TmsLockPromise(function() {
// 返回一個須要等待執行結果的promise,例如登陸
})
...
let pendingPromise = lockPromise.wait()
複製代碼
lock-promise
組件的核心是wait
方法。每次調用該方法都會建立一個新的Promise對象,讓這個「代理」等待登陸的結果,這樣得到結果後就能夠執行一些管理狀態的操做了。
經過lock-promise
就能夠實現將「同時」(在登陸過程當中)發起的請求掛起,等待「鎖」操做完成後,繼續執行全部請求。全部請求都執行後,自動清除鎖的狀態。
前一部分介紹的是已經發起API調用時再處理token的狀況。咱們還能夠在進入頁面前檢查token是否已經具有,若是不具有,就跳轉到登陸頁,登陸完成後再返回要進入的頁面。這種方式適合首次進入應用的狀況。
實現這種功能要用到Vue-Router
,首先,經過導航守衛機制進行檢查;第二,登陸成功後,應該可以自動返回用戶原本要訪問的頁面。
爲了解決這個問題,tms-vue
中實現了router-history
插件。
router.beforeEach((to, from, next) => {
if (to.name !== 'login') { // 不是訪問登陸頁,檢查token
let token = sessionStorage.getItem('access_token')
if (!token) {
Vue.TmsRouterHistory.push(to.path) // 保存原始跳轉頁
return next('/login') // 沒有token,跳轉到登陸頁
}
}
next()
})
複製代碼
登陸成功後,檢查是否要返回原來要進入的頁面:
if (this.$tmsRouterHistory.canBack()) {
this.$router.back()
} else {
this.$router.push('/')
}
複製代碼
Promise是最重要的概念,它是實現不少複雜方案的底層機制,必需要熟練掌握!!!
解決以上問題就初步實現了「API+登陸」解決認證的關鍵技術問題,可是仍然須要進行細化,例如:登陸組件的組件化設計,失敗狀況處理等。後續文章中將繼續探討這些問題。