美文网首页STEAM编程实践你不知道的JavaScript
Vue项目总结(4)-API+token处理流程

Vue项目总结(4)-API+token处理流程

作者: 全栈顾问 | 来源:发表于2020-01-05 21:02 被阅读0次

    本文介绍了一些Vue和axios的实用技巧,解决前端API调用中access_token的处理问题,包括:Promise的链式调用,axios的拦截器,vue-router记录导航历史等。

    参考项目:https://github.com/jasony62/tms-vue.git

    问题分析

    前后端完全分离的项目中,一个前端应用会访问多个后端的API,API调用都要通过传递token进行用户身份认证。用户登录就是用用户名和口令换取token,获得token后前端自行保留(例如:放在sessionStorage里),然后每次发起API调用时添加上这个参数。为了安全,token会设置有效期,过期了就需要重新登录获取新的token。我们可以看到用户登录流程设计的核心,其实就是一个管理和使用token的问题。

    基于token的使用,需要考虑如下情况:

    • 进入页面,页面内同时发起多个API调用
    • 用户在页面中执行操作,操作中发起多个API调用,例如:提交表单
    • 调用API,本地没有token,打开登录页
    • 调用API,本地有token,API调用返回token已经过期,打开登录页
    • 调用API,其他请求正在获取token,停止或取消调用
    • 调用认证API,不添加token
    • 登录成功后,保存获得的token,关闭登录,重新发起API调用

    这里面临几个技术问题:

    • 前端本地存储token;
    • 区分API调用是否需要添加token;
    • 如果同时发出多个请求,当其中的一个已经开始获取token,如何取消或暂停其它请求;
    • 登录后完成后,如何自动回到之前的状态。

    理解axios拦截器

    为了满足上面提到的要求,需要能够控制API请求的执行过程,axios中是通过拦截器添加控制逻辑,因为我们先深入了解一下axios中拦截器的相关代码。

    // 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()

    链式调用

    prmise链式调用就将几个promise串起来执行,上一个promise执行的结果,作为下一个promise的输入。看个例子:

    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行输出
    
    

    通过上面的方式就可以把多个异步操作串联起来执行。

    then方法

    Promise的then方法传入两个参数,分别在调用then方法的promise对象完成或失败时调用。注意这个调用是异步调用(需要去排队执行),这就是为什么上面的例子中最后1句console.log()是第1个输出,因为then中的回调函数是排队执行的。

    掌握then方法的关键是理解返回值。首先,then方法返回的是Promise对象,这是可以进行链式调用的基础;第二,执行哪个回调函数由调用then的Promise对象的执行结果决定(两个回调函数之间没有关系);第三,返回的Promise对象的状态由执行的回调函数的返回值决定(和是哪个回调函数返回无关)。例如:回调函数内返回的是一个值(数字、字符串、对象等),那么生成的Promise对象的状态是完成(fulfilled)。具体规则请参考在线文档。

    需要注意的是,失败回调函数只是当前执行的promise对象的结果,并不是整个链的结果,完成和失败回调函数都可以通过返回值,告诉下一个环节要进入完成函数还是失败函数。因此,链式调用中每一个环节都可以修正上一个环节的“错误”,继续让链执行。

    这里有个有意思的问题:catch一定是除finally外最后执行的环节吗,它可以写在then的前面吗?答案是可以。因为,catchthen的缩写,等价于then(undefined, err=>{...})

    参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/then

    axios拦截器

    明白了链式调用和then方法,axios的拦截器机制就好理解了。

    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift());
    }
    

    每条拦截规则都由完成函数(fulfilled)和失败函数(rejected)构成,可以理解为:请求的上一步成功了做什么,失败了又该做什么。这个理解很关键,因为添加拦截规则时容易想成:在完成函数中添加拦截逻辑,如果这个逻辑失败了,在失败函数中进行处理。完成函数发生异常,失败函数不会被执行,因为是否调用它不是由完成函数决定,而是由上一个执行环节的执行结果决定。完成函数的异常要在后续环节的失败函数中处理。

    另外,需要注意的是,请求规则和响应规则的执行顺序不一样,请求规则是先定义的后执行(unshift),响应规则是先定义的先执行(push)

    再有,请求规则和响应规则是在同一个链上,因此,请求规则中的异常,可以由响应阶段失败函数处理。例如:无论执行请求发生了什么问题,都需要给用户一个消息框进行说明,那么即使是在请求阶段发生的异常,也都可以放在响应拦截规则中进行统一处理。

    本地存储token

    获取token后可以放在localStorage或者sessionStorage中,例如:

    sessionStorage.setItem('access_token', token)
    

    区分API是否添加token

    axios支持创建新实例,可以给不同的实例指定不同的拦截规则。

     axios.create(config)
    

    tms-vue项目中可以给axios实例进行命名,并且指定不同的拦截规则。

    Vue.TmsAxios({ name: 'file-api', rules })
    Vue.TmsAxios({ name: 'auth-api' })
    

    暂停API调用

    通过设置拦截规则,我们可以对API调用的前端过程进行控制。

    调用API时,围绕token,一个axios请求可能碰到两种情况:1、请求阶段发现token不存在,获得token后,继续发送;2、响应阶段返回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+登录”解决认证的关键技术问题,但是仍然需要进行细化,例如:登录组件的组件化设计,失败情况处理等。后续文章中将继续探讨这些问题。

    本系列其他文章:

    Vue项目总结(1)-基本概念+Nodejs+VUE+VSCode

    Vue项目总结(2)-前端独立测试+VUE

    Vue项目总结(3)-前后端分离导致的跨域问题分析

    相关文章

      网友评论

        本文标题:Vue项目总结(4)-API+token处理流程

        本文链接:https://www.haomeiwen.com/subject/detmoctx.html