细读Axios源码系列三 - 拦截器,.use(fn, fn) 方法实现、同步与异步拦截器、移除拦截器

「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战」。

写在开头

上一周,因为工作上比较忙,又有小伙伴催更 Element源码系列 ,所以先跑去更新了两篇。时隔一周,小编又回来更新 Axios源码系列 啦。

就是不知道还有没有人在关注,哈哈哈,不过,这没有关系,咱还是要坚持下去,把该干的事情干完。

那么,经过上一篇 细读Axios源码系列二 - axios对象创建、request核心方法、发起网络请求 文章的学习,我们自己从头写的 axios 已经是能正常发起请求并且获取到响应结果了。

接下来,我们继续来完善它,但在开始本章内容前,我们先来看看下面这个例子:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
    <script>
        // 添加拦截器
        axios.interceptors.request.use(config => {
           console.log('请求拦截1', config);
           config.url = 'http://localhost:3000/posts'; // 拦截,添加请求路径
           return config;
        }, function request11(error) {
           return Promise.reject(error);
        });
        axios.interceptors.request.use(config => {
           console.log('请求拦截2', config);
           return config;
        }, function request11(error) {
           return Promise.reject(error);
        });
        axios.interceptors.response.use(response => {
           console.log('响应拦截', response);
           response.my = '橙某人'; // 拦截, 添加自定义响应结果
           return response;
        }, function request22(error) {
           return Promise.reject(error);
        });
        // 发起请求
        axios({
           url: '', // 未添加路径
           method: 'get'
        }).then(res => {
           console.log(res)
        })
    </script>
</body>
</html>
复制代码

记得启动自己的 测试服务 哦:json-server --watch server.json

image.png

查看浏览器控制台结果,我们能观察到上面这个例子的一些情况:

  • axios 对象身上会有 axios.interceptors.[request/response].use(fn, fn) 方法,并且它接收两个函数作为参数。
  • 添加拦截器可以有多个,并且在相应时机都会被执行。
  • 请求拦截器不是按添加顺序执行,是倒序执行,响应拦截器是按添加顺序进行顺序执行。

拦截器

了解完上面的例子后,下面我们就开始来逐步实现本章的核心拦截器

image.jpg

.use(fn, fn) 方法实现

我们先来到 Axios.js 文件,进行拦截器的一些初始化操作:

// lib/core/Axios.js
var dispatchRequest = require('./dispatchRequest');
var InterceptorManager = require('./InterceptorManager'); // 引入拦截器

function Axios() {
  // 初始化拦截器容器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

Axios.prototype.request = function request(config) {
    var promise;
    var newConfig = config;
    try {
      promise = dispatchRequest(newConfig); 
    } catch (error) {
      return Promise.reject(error);
    }
    return promise;
}

module.exports = Axios;
复制代码

新建 lib/core/InterceptorManager.js 文件:

function InterceptorManager() {
  this.handlers = []; // 拦截器可以有多个
}
// 注册 use() 方法
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  console.log('use')
}
复制代码

上面代码我们初始化了请求拦截器响应拦截器,还注册了 .use() 方法并且把它挂载在拦截器身上。

不过,要注意我们的拦截器现在是放在 Axios 对象身上,而 上一篇文章 我们说过使用 axios 对象本质是在执行 Axios.prototype.request() 方法,那当我们添加拦截器时 axios.interceptors.response.use(),实际也就变成 request.interceptors.response.use() 这样子,但我们 request() 方法身上可没有拦截器,它在 Axios 对象身上,这可咋办?(。•́︿•̀。)

不要着急,我们来看看 axios 源码中是如何解决这个问题的,回到 axios.js 文件:

// lib/axios.js
var Axios = require('./core/Axios');
var bind = require('./helpers/bind');
var utils = require('./utils'); // 引入工具函数

function createInstance() {
  var context = new Axios();
  var instance = bind(Axios.prototype.request, context);

  // 把 Axios 实例对象上原型的方法拷贝到 request() 身上, 如get/post
  utils.extend(instance, Axios.prototype, context);

  // 把 Axios 实例对象上本身的东西拷贝到 request() 身上, 如拦截器
  utils.extend(instance, context);

  return instance;
}

var axios = createInstance();
module.exports = axios;
复制代码

上面我们添加了两行代码,看代码就能知道 axios 源码中是把 Axios 对象身上拥有的东西直接拷贝到 request() 方法身上,从而来实现使用 axios 对象也能访问到 Axios 对象身上的过程。

再来新建 lib/utils.js 文件:

var bind = require('./helpers/bind');
/**
 * 扩展对象 a 身上的属性, 会将 b 身上的属性拷贝到 a 身上
 * @param {Object} a
 * @param {Object} b
 * @param {Object} thisArg: this 指向
 */
function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  });
  return a;
}
// 判断是否是数组
function isArray(val) {
  return toString.call(val) === '[object Array]';
}
// 重写 forEach
function forEach(obj, fn) {
  if (obj === null || typeof obj === 'undefined') return;
  // 不是对象类型, 则变成一个数组, 如函数
  if (typeof obj !== 'object') obj = [obj];
  if (isArray(obj)) {
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}
module.exports = {
  extend: extend,
  isArray: isArray,
  forEach: forEach,
}
复制代码

utils.js 文件是 axios 放所有工具函数的文件,里面具体细节就不一一介绍了,感兴趣的小伙伴可以慢慢一个一个函数揪出来研究一下。(✪ω✪)

我们先测试一下 .use() 方法是否注册成功了,先把项目进行打包 grunt build,然后编写测试用例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <script src="../dist/axios.js"></script>
</head>
<body>
<script>
    axios.interceptors.request.use(config => {
      return config;
    }, function request11(error) {
      return Promise.reject(error);
    });
    axios({
      url: 'http://localhost:3000/posts',
      method: 'get'
    }).then(res => {
      console.log(res)
    })
</script>
</body>
</html>
复制代码

image.png

可以看到 .use() 方法能正常访问、执行,那我们就放心。(-^O^-)

同步与异步拦截器

上面我们算是成功初始化了拦截器,接下来,我们来完善拦截器的执行过程。我们知道添加拦截器的时候会传递两个函数(axios.interceptors.[request/response].use(fn, fn)),作为成功或失败的反馈回调。

因为拦截器可以添加多个,所以第一步我们需要先来把这些回调收集起来,来 InterceptorManager.js 文件:

// lib/core/InterceptorManager.js
function InterceptorManager() {
  this.handlers = [];
}
/**
 * 收集拦截器的回调函数
 * @param {Function} fulfilled
 * @param {Function} rejected
 * @param {Object} options: 拦截器配置, {synchronous, runWhen}
 * @return {Number}: .use() 方法执行后, 返回当前拦截器总个数, 实际也就是当前这个拦截器的索引
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
    synchronous: options ? options.synchronous : false, // 默认情况下, axios 的拦截器是异步执行, 当添加请求拦截器时,可以通过 synchronous 来控制是否要同步执行
    runWhen: options ? options.runWhen : null, // 可以自定义提供一个函数, 用于验证请求是否能执行该拦截器
  });
  return this.handlers.length - 1;
};
复制代码

上面代码可能让你感到疑惑的是 options 参数吧?
我们先暂且叫它 "拦截器配置",这个参数应该只有很少的小伙伴们知道吧。因为官方文档好像貌似也没有提及到,平时我们也很少会使用到它,不知道也正常。
它是一个对象,只有两个属性,synchronous 属性用来控制拦截器是否要同步执行,默认是异步执行的,这里需要注意拦截器可以有多个,如果是注册了多个请求拦截器,那么你想同步执行拦截器,必须把每个拦截器的 synchronous 属性都设置为 true才行,不能只设置一个,除非你只注册了一个请求拦截器,你如果不理解,没关系,下面会有例子;而 runWhen 属性用来控制当前拦截器是否被执行。

收集好注册的所有拦截器后,我们来看看拦截器会在什么时候被执行,继续回到 Axios.js 文件,主要来看 request() 方法:

// lib/core/Axios.js
var dispatchRequest = require('./dispatchRequest');
var InterceptorManager = require('./InterceptorManager');
function Axios() {
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

Axios.prototype.request = function request(config) {
  // 拦截器是否是同步执行的
  var synchronousRequestInterceptors = true; 
  
  // 请求拦截器
  var requestInterceptorChain = [];
  // 主要这里的 forEach() 方法会在 InterceptorManager.js 文件中注册
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    // 判断是否提供了使用拦截器的判断条件, 如果提供了拦截器的使用条件, 则需要通过才会执行拦截器, 否则该请求不执行拦截器
    if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) return;
    // 可以配置 synchronous 使 axios 变成同步执行, 默认是异步的, true && false => false
    synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
    // 往头部依次插入拦截器回调, 这也就是为什么开头例子提到的 请求拦截器 会是倒序执行的原因
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  
  // 响应拦截器, 不需要考虑其他配置、执行顺序问题,直接取出来等着被执行就行
  var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });
  
  var promise;
  
  // 异步执行
  if(!synchronousRequestInterceptors) {
    // 这里的 undefined 主要作用是保持完整性, 成功与失败
    var chain = [dispatchRequest, undefined];
    
    // 加入拦截器
    Array.prototype.unshift.apply(chain, requestInterceptorChain); // 往头部添加 请求拦截器
    chain = chain.concat(responseInterceptorChain); // 往尾部添加 响应拦截器
    
    // 构建一个 fulfilled 状态的 Promise, 为后续的 Promise链 做准备
    promise = Promise.resolve(config);
    
    // 依次执行所有的回调函数
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift()); // 以 Promise链 来执行并传递结果
    }
    
    return promise;
  }
}
复制代码

InterceptorManager.js 文件中,实现 forEach 方法:

// lib/core/InterceptorManager.js
var utils = require('./../utils');

function InterceptorManager() {
  this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  ...
};

/**
 * 实现一个 forEach方法, 用于迭代所有的拦截器
 * @param {Function} fn: 接收一个函数, 会执行函数, 把拦截器传递出去
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    fn(h);
  });
};

module.exports = InterceptorManager;
复制代码

上面代码每行小编都贴心的写上详细的注释,可不能还说看不懂哦。当然,可能比较难的是关于 Promise链 的过程。

下面再写了一个小例子,希望对你有所帮助:

let promise;
promise = new Promise((resolve, reject) => {
  resolve('橙某人');
});
promise = promise.then(r1 => {
  return r1;
});
promise = promise.then(r2 => {
  return r2;
});
promise = promise.then(res => {
  console.log(res); // 橙某人
});
复制代码

接下来,我们再对项目进行测试,先执行 grunt build 命令对项目进行打包。

页面测试用例:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <script src="../dist/axios.js"></script>
</head>
<body>
    <script>
        axios.interceptors.request.use(config => {
            console.log('请求拦截1', config);
            config.url = 'http://localhost:3000/posts';
            return config;
        }, function request11(error) {
            return Promise.reject(error);
        });
        axios.interceptors.request.use(config => {
            console.log('请求拦截2', config);
            return config;
        }, function request11(error) {
            return Promise.reject(error);
        });
        axios.interceptors.response.use(response => {
            console.log('响应拦截1', response)
            // response.my = '橙某人' // 响应拦截, 因为我们的响应结果还没做处理, 它现在是一个字符串而已, 后续我们再对响应结果处理
            return response;
        }, function request22(error) {
            return Promise.reject(error);
        });
        axios.interceptors.response.use(response => {
            console.log('响应拦截2', response)
            return response;
        }, function request22(error) {
            return Promise.reject(error);
        });

        axios({
            url: '',
            method: 'get'
        }).then(res => {
            console.log(res)
        })
    </script>
</body>
</html>
复制代码

image.png

可以看到,控制台结果和我们文章开头的测试结果差不多,虽然我们还有很多东西没有完善,但是整体过程是正确的。

上面我们通过 Promise 对象来实现了异步的情况,下面我们来看看同步的情况应该如何做,我们先来把测试用例调整为同步情况:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <script src="../dist/axios.js"></script>
</head>
<body>
    <script>
        axios.interceptors.request.use(config => {
            console.log('请求拦截1', config);
            config.url = 'http://localhost:3000/posts';
            return config;
        }, function request11(error) {
            return Promise.reject(error);
        }, {
            synchronous: true // 同步
        });
        axios.interceptors.request.use(config => {
            console.log('请求拦截2', config);
            return config;
        }, function request11(error) {
            return Promise.reject(error);
        }, {
            synchronous: true // 同步
        });
        axios.interceptors.response.use(response => {
            console.log('响应拦截1', response)
            // response.my = '橙某人' 
            return response;
        }, function request22(error) {
            return Promise.reject(error);
        });
        axios.interceptors.response.use(response => {
            console.log('响应拦截2', response)
            return response;
        }, function request22(error) {
            return Promise.reject(error);
        });

        axios({
            url: '',
            method: 'get'
        }).then(res => {
            console.log(res)
        })
    </script>
</body>
</html>
复制代码

此时会报错,不过没有关系,因为我们还没写同步的逻辑。

image.png

同步的逻辑不难,还是在 request() 方法中编写:

Axios.prototype.request = function request(config) {
  var synchronousRequestInterceptors = true; 
  var requestInterceptorChain = [];
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) return;
    synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });
  
  var promise;
  
  // 异步执行
  if(!synchronousRequestInterceptors) {
    var chain = [dispatchRequest, undefined];
    Array.prototype.unshift.apply(chain, requestInterceptorChain);
    chain = chain.concat(responseInterceptorChain);
    promise = Promise.resolve(config);
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift());
    }
    return promise;
  }
  
  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);
  }

  // 执行响应拦截器 - 同步
  while (responseInterceptorChain.length) {
    // 从尾部开始获取并删除
    promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
  }

  return promise;
}
复制代码

写完,再次执行打包命令 grunt build 进行测试,如果没有报错了,并且正确返回结果就说明你大功告成了。

image.gif

上面我们提到过拦截器的配置,还有一个 runWhen 属性,它是怎么被使用的呢?我写个用例大家应该就清楚了。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <script src="../dist/axios.js"></script>
</head>
<body>
    <script>
        axios.interceptors.request.use(config => {
            console.log('请求拦截1', config);
            config.url = 'http://localhost:3000/posts';
            return config;
        }, function request11(error) {
            return Promise.reject(error);
        }, {
            synchronous: true 
        });
        axios.interceptors.request.use(config => {
            console.log('请求拦截2', config);
            return config;
        }, function request11(error) {
            return Promise.reject(error);
        }, {
            synchronous: true,
            runWhen: (config) => { // runWhen
              return config.method === 'post';
            }
        });
        axios.interceptors.response.use(response => {
            console.log('响应拦截1', response)
            // response.my = '橙某人' 
            return response;
        }, function request22(error) {
            return Promise.reject(error);
        });
        axios.interceptors.response.use(response => {
            console.log('响应拦截2', response)
            return response;
        }, function request22(error) {
            return Promise.reject(error);
        });

        axios({
            url: '',
            method: 'get'
        }).then(res => {
            console.log(res)
        })
    </script>
</body>
</html>
复制代码

image.png

可以发现 "请求拦截器2" 就不会被执行了,是不是也很简单。

移除拦截器

上面我们能通过 runWhen 属性来控制拦截器是否执行,接下来,我们来学一下手动移除拦截器的功能。

同样,我们先修改一下测试用例:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <script src="../dist/axios.js"></script>
</head>
<body>
    <script>
        let requestIndex;
        let responseIndex;
        
        axios.interceptors.request.use(config => {
            console.log('请求拦截1', config);
            config.url = 'http://localhost:3000/posts'
            return config;
        }, function request11(error) {
            return Promise.reject(error);
        });
        requestIndex = axios.interceptors.request.use(config => {
            console.log('请求拦截2', config);
            return config;
        }, function request11(error) {
            return Promise.reject(error);
        });
        axios.interceptors.response.use(response => {
            console.log('响应拦截1', response)
            // response.my = '橙某人' 
            return response;
        }, function request22(error) {
            return Promise.reject(error);
        });
        responseIndex = axios.interceptors.response.use(response => {
            console.log('响应拦截2', response)
            return response;
        }, function request22(error) {
            return Promise.reject(error);
        });
        // 移除拦截器
        axios.interceptors.request.eject(requestIndex);
        axios.interceptors.response.eject(responseIndex);

        axios({
            url: '',
            method: 'get'
        }).then(res => {
            console.log(res)
        })
    </script>
</body>
</html>
复制代码

知道了移除拦截器的过程,我们来具体看看应该如何实现,先来 InterceptorManager.js 文件注册相关方法:

// lib/core/InterceptorManager.js
var utils = require('./../utils');

function InterceptorManager() {
  this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  ...
};

// 注册移除拦截器方法
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    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;
复制代码

最后,我们重新打包项目:grunt build

image.png

可以发现,"请求拦截器2" 和 "响应拦截器2" 都被移除不会执行了。(^ω^)

那么到此 axios 源码中与拦截器相关的内容就都讲完啦,希望你看完本篇文章能有所收获,我们下一章再见囖。

image.gif




至此,本篇文章就写完啦,撒花撒花。

image.png

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。

おすすめ

転載: juejin.im/post/7067101781708406791