前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >axios 如何设计拦截器

axios 如何设计拦截器

作者头像
copy_left
发布2022-03-23 14:11:34
6640
发布2022-03-23 14:11:34
举报
文章被收录于专栏:方球

最近在做axios的二次封装,在配置拦截器时。发现实际的调用流程与预想的不太一致。所以去看了看axios拦截器部分的源码,大概了解拦截器的实现。 一下是对拦截器实现的一些理解。

拦截器的使用方式

代码语言:javascript
复制
// 请求拦截
axios.interceptors.request.use(
  // 处理器
  function onFulfilled (){...},
  // 错误捕获
  function onRejected (){...},
)


// 响应拦截器
axios.interceptors.response.use(
   // 处理器
  function onFulfilled (){...},
  // 错误捕获
  function onRejected (){...},
)

一个简单例子

代码语言:javascript
复制
const c = axios.create({
  baseURL: '/proxy',
  timeout: 1000,
})


const CancelToken = axios.CancelToken;
const source = CancelToken.source();


function req1(conf: AxiosRequestConfig){
  console.log('r1')
  return conf
}


function req2(conf: AxiosRequestConfig){
  console.log('r2')
  throw new Error('from r2')
}


function req3(conf: AxiosRequestConfig){
  console.log('r3')
  return conf
}


function err1(){
  console.log('e1')
}


function err2(){
  console.log('e2')
}


function err3(){
  console.log('e3')
}


c.interceptors.request.use(req1, err1)
c.interceptors.request.use(req2, err2)
c.interceptors.request.use(req3, err3)


c.get('/', {cancelToken: source.token})
.then(() => console.log('req end'))
.catch((e) => console.log('err end', e))
// r3
// r2
// e1
// err end

因为平常一直使用promise.then(success).catch(fail) 的模式,潜意识认为axios拦截器的流程也类似, 而实际调用的结果与预期不一致, 预期调用流程: r1 → r2 → e2。 那拦截器真是的调用流程是什么样的呢?

拦截器实现

axios 拦截器相关的代码主要在,lib/core/Axios.js lib/core/InterceptorManager.js 两个文件中。

注册拦截器

请求和响应拦截器都是 InterceptorManager 的实例。所以两者的注册方式是一致的

InterceptorManager 拦截器对象

代码语言:javascript
复制
'use strict';


var utils = require('./../utils');


function InterceptorManager() {
  // 执行函数缓存队列
  this.handlers = [];
}


/**
 * fulfilled, rejected 将交由Promise.then函数
 * 返回注册id,以便移除对映的拦截器
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
   // 向执行队列中添加拦截器配置对象
   this.handlers.push({
    // 执行器
    fulfilled: fulfilled,
    // 错误捕获
    rejected: rejected,
    // 同步执行标识符
    // 该标识符将影响拦截器的调用模式
    synchronous: options ? options.synchronous : false,
    // 筛选函数
    runWhen: options ? options.runWhen : null
  });
  // 返回的id为队列的长度
  return this.handlers.length - 1;
};


/**
 * 通过id移除拦截器
 */
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
     // 应为拦截器的标识id,为队列的长度
     // 而 handlers.length 是动态的
     // 为了防止id重复,删除拦截器时,将对应的位置置空,而不是删除
     // 保证length的值一直处于递增的状态
    this.handlers[id] = null;
  }
};


/**
 * 遍历拦截器队列
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
     // 跳过空值,既已删除位
    if (h !== null) {
      fn(h);
    }
  });
};


module.exports = InterceptorManager;

  1. 通过源码可以看到,拦截器实例实现很简单。主要是维护一个对应的队列。

2. synchronous runWhen 配置项只在项目README中有说明,当部分中文文档中没有提及,后面Axios源码中能了解实际的用途。

  1. 需要的注意的是,use 函数返回的绑定id,为队列的长度。所以不要直接通过InterceptorManager 实例修改拦截器队列

拦截器调用流程

拦截器调用流程的代码都在 Axios.prototype.request方法中

  • 收集请求拦截
代码语言:javascript
复制
// Axios.js


  // 请求拦截收集队列 
  var requestInterceptorChain = [];
  // 是否存在同步配置 
  var synchronousRequestInterceptors = true;
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
     // 判断是否存在runWhen函数,通过runWhen执行返回,判断是否跳过当前拦截器
    if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
      return;
    }
    // 收集同步状态设置
    // 必须要求所有可执行拦截器,都配置 synchronous 时。 
    // synchronousRequestInterceptors 最终值才能为true,执行同步调用模式
    // 否则为false, 将执行异步调用模式
    synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
    // 收集拦截器
    // 这里添加模式为unshift
    // 所以最终队列顺序与注册顺序相反
     // 例如:注册顺序:[1, 2, 3] 收集器顺序:[3, 2, 1]
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  • 收集响应拦截
代码语言:javascript
复制
  // 响应拦截收集队列 
 var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    // 与请求拦截不同,这里没有对 runWhen,synchronous 的判断
    // 所以两个配置只作用于请求拦截
    // 这里添加的模式 push
    // 所以最终的队列顺序与注册顺序一致
    // 例如:注册顺序:[1, 2, 3] 收集器顺序:[1, 2, 3]
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });
  • 异步调用模式
代码语言:javascript
复制
  var promise;
  // 拦截器的两种调用模式
  // 当所用拦截器都为配置,synchronous 属性时,使用异步队列(默认模式)
  if (!synchronousRequestInterceptors) {
    // chain为请求任务执行队列
    // dispatchRequest 请求发送器
    // 因为队列任务将通过Promise.then(task, error) 模式调用
    // 所以默认队列包含 一个 undefined 值,作为发送器的错误捕获器占位符
    // Promise.then(dispatchRequest, undefined)
    var chain = [dispatchRequest, undefined];
    // 将请求拦截追加到队列头部
    Array.prototype.unshift.apply(chain, requestInterceptorChain);
    // 将响应拦截追加到请求发送器之后
    chain = chain.concat(responseInterceptorChain);
    // 最终的任务队列顺序
    // 反序的请求拦截 -> 请求发送 -> 正序的响应拦截
    promise = Promise.resolve(config);
    // 执行任务队列
    while (chain.length) {
      // 每个任务都是由 执行器,错误捕获成对执行的
      // 所以初始队列包含一个undefined占位符
      promise = promise.then(chain.shift(), chain.shift());
    }
    // 返回promise
    return promise;
  }
代码语言:javascript
复制
 // 当 synchronousRequestInterceptors 为true时,启动同步模式
 var newConfig = config;
  // 直接遍历执行请求拦截队列
  while (requestInterceptorChain.length) {
    var onFulfilled = requestInterceptorChain.shift();
    var onRejected = requestInterceptorChain.shift();
    try {
      newConfig = onFulfilled(newConfig);
    } catch (error) {
      onRejected(error);
      break;
    }
  }
  
  // 发送请求
  try {
    promise = dispatchRequest(newConfig);
  } catch (error) {
    return Promise.reject(error);
  }
  
  // 执行响应队列
  // 响应队列的执行与异步模式一致,所以在收集任务时,没有做 synchronous 判断
  while (responseInterceptorChain.length) {
    promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
  }
  return promise;

小结

通过阅读源码,我们能大概梳理出拦截器的大致执行流程和特点

  1. 请求拦截存在异步 同步 两种模式
  2. 请求拦截(反序)和响应拦截(正序)的执行顺序与注册顺序不同
  3. 只有当所有请求拦截都开启同步模式时,才执行同步模式, 否者依然使用异步模式
  4. 请求拦截可根据情况跳过,而响应拦截不具备该功能
  5. 不要直接通过拦截对象修改拦截器队列
  6. 请求拦截器需要将最终的处理结果交给发送器执行, 所以必须保证最有执行的请求拦截有正确返回

异步,同步模式的执行差异

两例子说明二者的差异

  • 异步
代码语言:javascript
复制
function req1(){
  console.log('r1')
}


function req2(){
  console.log('r2')
}


function req3(){
  console.log('r3')
  throw new Error('from r3')
}


function err1(){
  console.log('e1')
}


function err2(){
  console.log('e2')
}


function err3(){
  console.log('e3')
}
const p = Promise.resolve()
p
.then(req3, err3)
.then(req2, err2)
.then(req1, err1)
// r3 -> e2 -> r1
  • 同步
代码语言:javascript
复制
const task = [
  [req3, err3],
  [req2, err2],
  [req1, err1]
]


for(let[req, err] of task){
  try {
    req()
  } catch (error) {
    err()
    break
  }
}
// r3 -> e3

比较连个例子可以发现,两种模式主要的区别在于错误的处理上

  1. 异步模式的错误处理类似分支,错误捕获的是之前节点最近的一次错误
  2. 同步模式的错误处理针对与当前执行函数

then(success, fail) 与 then(success).catch(fail)

代码语言:javascript
复制
p
.then(req3)
.catch(err3)


.then(req2)
.catch(err2)


.then(req1)
.catch(err1)
// r3 -> e3 -> r2 -> r1

异步任务是以 then(success, fail) 的方式调用的,错误捕获的节点与then(success).catch(fail) 是不同的,promise错误捕获的方式是根据当前promise节点的状态来判断的,第二中方式比第一种方式,中间会多出一个节点。

所以在配置错误处理回调时,需要注意处理的节点位置。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022.03.11 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 拦截器的使用方式
  • 一个简单例子
  • 拦截器实现
    • 注册拦截器
      • InterceptorManager 拦截器对象
        • 拦截器调用流程
          • 小结
          • 异步,同步模式的执行差异
            • 两例子说明二者的差异
              • then(success, fail) 与 then(success).catch(fail)
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档