Axios的拦截器原理以及请求等待重发的实现

Vue项目越做越多,Axios一直作为请求发送的基础工程,这里就深究一下Axios的拦截器相关的一些逻辑和对应一个比较恶心的场景。
Axios GitHub

回顾下Promise

  1. Promise的基础知识不做多介绍可以参考两个文章

    1. 《ECMAScript 6 入门》:Promise 对象
    2. ES6 Promise的resolved深入理解 这个是我看到的对于Promise状态解释比较清晰的一个文章
  2. Promise的状态

    1. Promise状态一旦改变就不能再变,一直保持此状态
    2. Promise可以被其他Promise锁定----这个很重要,跟后面的要说到的Axios的请求阻塞等待有关系
    3. 一个重要的Demo
    Promise.resolve(
        new Promise((resolve,reject) => {
            console.log('inner Promise');
            resolve('123');
        }).then(data=>{
            console.log(1,typeof(data), data);
            return data+'4';
        })
    ).then(data=>{
        return Promise.resolve('Randy'+data);
    }).then(data=>{
        console.log(2,typeof(data), data)
    });
    
  • 输出如下结果

    inner Promise
    "string" "123"
    "string" "Randy1234"
    
  1. 简单解释下上面的结果
    1. Promise.resolve创建一个Promise对象,依赖于inner的Promise的resolve结果
    2. 内部的new Promise().then()创建了一个Promisenew Promise()resolve的结果是123then()将结果改为1234,打印"string" "123",然后返回'1234'这个作为外层的resolve结果
    3. 外层中第一个then()返回了一个Promise返回"Randy1234"作为resolve结果
    4. 外层中第二个then()接收到前一个的返回值,然后打印"string" "Randy1234"
  2. 人话描述下这里用到的几个知识点
    1. Promise.resolve(data)等于new Promise(resolve=>{resolve(data)})
    2. Promise A可以使用另一个Promise B的resolve值作为自己的resolve值进入A的调用链
    3. then()可以对处理结果进行修改

Axios

接下来开始整体,说说Axios。Axios是基于Promise机制实现的异步的链式请求框架。体积小,源码易懂。非常适合做基础的请求库。

Axios结构

  1. 代码结构

    1. axios.js:入口文件,将Axios实例的request函数绑定为入口函数,axios.create其实返回的是一个function,就是Axios实例的Axios.prototype.request
    2. lib/Axios.js:真正的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;
      
    3. lib/InterceptorManager.js:拦截器管理,是一个对[]的封装
    4. lib/dispatchRequest.js:发送请求的Promise,完成发送请求的逻辑。注意看Axios.js中的var chain = [dispatchRequest, undefined];
    5. adapter/*:适配器,这里的代码保证了Axios在ssr模式下和浏览器环境中区分环境实现请求返送的逻辑。里面存放了两个定义好的适配器,可以参照README.md中的描述自定义适配器
  2. 拦截器模型


    Axios拦截器示意图.png
    • request和response的拦截器都可以有多对,其中每一个点都会挂在一个then()的调用上,promise.then(chain.shift(), chain.shift());

使用场景:应对OAuth中refresh_tokenaccess_token时其他请求需等待的问题

  • 根据场景来看,我们需要有一下几个能力

    1. Request拦截器中任意的请求(比如请求A)进入之后,如果主动检测到了access_token的超时,那么停止当前请求A,开启refresh_token的请求,当成功之后再执行A请求
    2. 当请求已发送,服务端识别到了token失效,Response拦截器中的处理跟Request拦截器要做的事一样
    3. 当有进行中的refresh_token请求时,此请求需要等待这个进行中的refresh_token的请求成功之后再进行发送
  • 那我们一个一个来处理

    1. 当请求进入拦截器,主动发现需要refresh_token时(比如access_token有效期临近)需要将请求放置在refresh_token成功之后

      • 处理方式可以采用在then()调用拦截器的方法时返回一个Promise,然后在Promise中等待refresh_token的请求成功之后再进行当前进入的请求的发送
      // axios 的 request拦截器
      axios.interceptors.request.use(config => {
          return new Promise(resolve => {
              // 模拟等待refresh_token
              setTimeout(function (config_param) {
                  resolve(config_param);
              }, 2000, config)
          });
      });
      
      • 上面的代码只是一个简单的示意,实际处理中要注意以下几点,
        • 刷新token之后config_param要处理新Token的拼装;
        • 请求拦截器中要能识别出是否是refresh_token的请求;
        • 能识别出是否正在进行refresh_token,并能正确处理其他进入的请求,这个后面会讲到
      • 处理之后调用链会变成这样


        请求拦截器中加入Promise
    2. 当请求已发送,服务端识别到了Token失效时(这个情况比较多,服务器时间与本地有间隙;Token不支持多点登陆等等),需要先refresh_token,然后重发请求

      • 可以采用与Request拦截器相似的处理,在拦截器中同样开启refresh_token,成功之后重新创建已经失败的请求,执行完请求之后将重新创建的请求获取到的返回值resolve给response的返回值
        let res = response.data;
        switch (res.code) {
            case RespStatus.UNAUTHORIZED.code: {
                let respConfig = response.config;
                if (isRefreshTokenReq(respConfig.url)) {
                    //刷新Token的请求如果出现401直接退出登录
                    showLoginOut();
                } else {
                    logDebug('请求的返回值出现401,由请求' + config.url + '的返回值触发,开始进行refresh_token!');
                    let auth = storage.state.user.auth;
                    try {
                        res = doRefreshToken(auth.refresh_token, auth.wmq_d_current_username, respConfig)
                            .then(config => {
                                return wmqhttp(attachAuthInfoToConfig(storage.state.user.auth, config));
                            }).then(value => {
                                return Promise.resolve(value);
                            });
                    } catch (e) {
                        console.log('无法等待刷新Token!', e);
                        showLoginOut();
                    }
                }
                break;
            }
            default:
                logDebug('Axios response default data:', res);
                break;
        }
        return res;
  • 处理之后调用链会变成这样


    响应拦截器中加入Promise和二次请求
  1. 对于在refresh_token时其他请求的进入需要安排这个请求动作,让请求发生在refresh_token之后进行
    - 解决思路如下,在全局的状态中记录是否正在刷新请求,并且保存refresh_tokenPromise。当遇到请求之后新创建一个Promise交给拦截器,在新创建的Promise中用then()等待refresh_token。
       new Promise(resolve => {
           pendingPromise.then(() => {
               logDebug('刷新Token成功,开始处理之前等待的请求', config.url);
               resolve(attachAuthInfoToConfig(storage.state.user.auth, config));
           });
       });
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容