axios 如何设计拦截器

image.png

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

拦截器的使用方式

// 请求拦截
axios.interceptors.request.use(
  // 处理器
  function onFulfilled (){...},
  // 错误捕获
  function onRejected (){...},
)


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

一个简单例子

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 拦截器对象

'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方法中

  • 收集请求拦截
// 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);
  });
  
  • 收集响应拦截
  // 响应拦截收集队列 
 var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    // 与请求拦截不同,这里没有对 runWhen,synchronous 的判断
    // 所以两个配置只作用于请求拦截
    // 这里添加的模式 push
    // 所以最终的队列顺序与注册顺序一致
    // 例如:注册顺序:[1, 2, 3] 收集器顺序:[1, 2, 3]
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });
  • 异步调用模式
  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;
  }
 // 当 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. 请求拦截器需要将最终的处理结果交给发送器执行, 所以必须保证最有执行的请求拦截有正确返回

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

两例子说明二者的差异

  • 异步
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
  • 同步
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)

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节点的状态来判断的,第二中方式比第一种方式,中间会多出一个节点。

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

你可能感兴趣的:(axios 如何设计拦截器)