本文介绍了一些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+登陆”解决认证的关键技术问题,可是仍然须要进行细化,例如:登陆组件的组件化设计,失败状况处理等。后续文章中将继续探讨这些问题。