axios 是一个通用的宝藏申请库,此次探索了 axios 中三个根底能力的实现,并将过程记录于此.

零. 前置

  • axios我的项目地址:https://github.com/axios/axios
  • 浏览代码commit hash:fe52a611efe756328a93709bbf5265756275d70d
  • 最近 Release 版本:v0.21.1

一. 指标

浏览源码必定是带着问题来学习的,所以以下是本次源码阅读准备探索的问题:

  • Q1. 如何实现同时反对 axios(config)axios.get(config) 语法
  • Q2. 浏览器和 NodeJS 申请能力的兼容实现
  • Q3. 申请 & 响应拦截器实现

二. 我的项目构造

简略提炼下我的项目中比拟重要的局部:

三. 从 axios 对象开始剖析

当在我的项目中应用 axios 库时,总是要通过 axios 对象调用相干能力,在我的项目的 test/typescipt/axios.ts 中有比拟清晰的测试用例(以下为简化掉 then 和 catch 后的代码):

axios(config)axios.get('/user?id=12345')axios.get('/user', { params: { id: 12345 } })axios.head('/user')axios.options('/user')axios.delete('/user')axios.post('/user', { foo: 'bar' })axios.post('/user', { foo: 'bar' }, { headers: { 'X-FOO': 'bar' } })axios.put('/user', { foo: 'bar' })axios.patch('/user', { foo: 'bar' }) 

从下面代码能够看出 axios 库 同时反对 axios(config)axios.get(config) 语法, 并且反对多种申请办法;

接下来一一剖析每个能力的实现过程~

3.1. 弄清 axios 对象

同时反对 axios(config)axios.get(config) 语法,阐明 axios 是个函数,同时也是一个具备办法属性的对象. 所以接下来咱们来剖析一下 axios 库裸露的 axios对象.

从我的项目根目录能够找到入口文件 index.js,其指向 lib 目录下的 axios.js, 这里做了三件事:

  • 1)应用工厂办法创立实例axios

    function createInstance(defaultConfig) {  // 对 Axios 类传入配置对象失去实例,并作为 Axios.prototype.request 办法的上下文  // 作用:反对通过 axios('https://xxx/xx') 语法实现申请  var context = new Axios(defaultConfig);  var instance = bind(Axios.prototype.request, context); // 手撸版本的 `Function.prototype.bind`    // 为了实例具备 Axios 的能力,故将 Axios的原型 & Axios实例的属性 复制给 instance  utils.extend(instance, Axios.prototype, context);  utils.extend(instance, context);  return instance; // instance 的真面目是 request 办法}  var axios = createInstance(defaults); 
  • 2)给实例axios 挂载操作方法

    axios.Axios = Axios; // 能够通过实例拜访 Axios 类axios.create = function create(instanceConfig) {  // 应用新配置对象,通过工厂类取得一个新实例  // 作用:如果我的项目中有固定的配置能够间接 cosnt newAxios = axios.create({xx: xx})  //            而后应用 newAxios 依照 axios API 实现性能  return createInstance(mergeConfig(axios.defaults, instanceConfig));};  // 勾销申请三件套axios.Cancel = require('./cancel/Cancel');axios.CancelToken = require('./cancel/CancelToken');axios.isCancel = require('./cancel/isCancel');  axios.all = function all(promises) {  return Promise.all(promises); // 相当间接,其实就是 Promise.all};axios.spread = require('./helpers/spread'); // `Function.prototype.apply` 语法糖  // 断定是否为 createError 创立的 Error 对象axios.isAxiosError = require('./helpers/isAxiosError'); 
  • 3)裸露实例axios

    module.exports = axios;module.exports.default = axios; 

从以上剖析能够失去论断,之所以可能 "同时反对 axios(config)axios.get(config) 语法" ,是因为:

  • 裸露的 axios 实例原本就是 request 函数,所以反对 axios(config) 语法
  • 工厂办法 createInstance 最终返回的对象,具备复制得的 Axios的原型 & Axios实例的属性,所以也能像 Axios 实例一样间接应用 axios.get(config) 语法

3.2. 反对多种申请办法

axios 反对 get | head | options | delete | post | put | patch 当然并不是笨笨地一一实现的,浏览文件 lib/core/Axios.js 能够看到如下构造:

function function Axios(instanceConfig) {} // Axios 构造函数// 原型上只有两个办法Axios.prototype.request = function request(config) {};Axios.prototype.getUri = function getUri(config) {};// 为 request 办法提供别名utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {  Axios.prototype[method] = function(url, config) {    return this.request(mergeConfig(config...)); // 这里省略了 mergeConfig 对入参的整合  };});utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {   // 根本同上});module.exports = Axios; 

其中外围实现天然是通用申请办法 Axios.prototype.request,其余函数通过调用 request 办法并传入不同的配置参数来实现差异化.

四、封装申请实现--"适配器"

总所周知,浏览器端申请个别是通过 XMLHttpRequest 或者 fetch 来实现申请,而 NodeJS 端则是通过内置模块 http 等实现. 那么 axios 是如何实现封装申请实现使得一套 API 能够在浏览器端和 NodeJS 端通用的呢?让咱们持续看看 Axios.prototype.request 的实现,简化代码构造如下:

Axios.prototype.request = function request(config) {  // 此处省略申请配置合并代码,最终失去蕴含申请信息的 config 对象  // chain 能够视为申请流程数组,当不增加拦截器时 chain 如下  var chain = [dispatchRequest, undefined];  var promise = Promise.resolve(config);      // 下方是拦截器局部,临时疏忽  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {});  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {});  // 执行申请  while (chain.length) {    // 申请流程数组前两个出栈,以后别离为 dispatchRequest 和 undefined    promise = promise.then(chain.shift(), chain.shift());  }  return promise;}; 

当没有增加任何拦截器的时候,申请流程数组 chain 中就只有 [dispatchRequest, undefined] ,此时在下方的 while 只运行一轮,dispatchRequest 作为 then 逻辑接管 config 参数并运行,持续找到 dispatchRequest 的简化实现(/lib/core/dispatchRequest.js):

module.exports = function dispatchRequest(config) {  // 勾销申请逻辑,稍后剖析  throwIfCancellationRequested(config);    // 此处略去了对 config 的预处理    // 尝试获取 config 中的适配器,如果没有则应用默认的适配器  var adapter = config.adapter || defaults.adapter;  // 将 config 传入 adapte 执行,失去一个 Promise 后果     // 如果胜利则将数据后放入返回对象的 data 属性,失败则放入返回后果的 response.data 属性  return adapter(config).then(function onAdapterResolution(response) {    throwIfCancellationRequested(config);    response.data = transformData(...); // 此处省略入参    return response; // 等同于 Promise.resolve(response)  }, function onAdapterRejection(reason) {    if (!isCancel(reason)) {      throwIfCancellationRequested(config);      if (reason && reason.response) {        reason.response.data = transformData(...); // 此处省略入参      }    }    return Promise.reject(reason);  });}; 

简略过完一遍 dispatchRequest 看到重点在于 adapter(config) 之后产生了什么,于是找到默认配置的实现(/lib/default.js):

function getDefaultAdapter() {  var adapter;  if (typeof XMLHttpRequest !== 'undefined') {    // 提供给浏览器用的 XHR 适配器    adapter = require('./adapters/xhr');  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {    // 提供给 NodeJS 用的内置 HTTP 模块适配器    adapter = require('./adapters/http');  }  return adapter;}var defaults = {  adapter: getDefaultAdapter(),} 

不言而喻,适配器通过判断 XMLHttpRequest 和 process 对象来判断以后平台并取得对应的实现. 接下来持续进入 /lib/adapters 目录,外面的 xhr.js 和 http.js 别离对应适配器的浏览器实现和NodeJS 实现,而 README.md 介绍了实现 adapter 的标准:

// /lib/adapters/README.mdmodule.exports = function myAdapter(config) {  // 应用 config 参数构建申请并实现派发,取得返回后则交给 settle 解决失去 Promise  return new Promise(function(resolve, reject) {    var response = {      data: responseData,      status: request.status,      statusText: request.statusText,      headers: responseHeaders,      config: config,      request: request    };    // 依据 response 的状态,胜利则执行 resolve,否则执行 reject并传入一个 AxiosError    settle(resolve, reject, response);  });}// /lib/core/settle.jsmodule.exports = function settle(resolve, reject, response) {  var validateStatus = response.config.validateStatus;  if (!response.status || !validateStatus || validateStatus(response.status)) {    resolve(response);  } else {    // 省略参数,createError 创立一个 Error 对象并为其增加 response 相干属性不便读取    reject(createError(...));  }}; 

实现自定义适配器要先接管 config , 并基于 config 参数构建申请并实现派发,取得后果后返回 Promise,接下来的逻辑控制权就交回给 /lib/core/dispatchRequest.js 持续解决了.

五. 拦截器实现

5.1. 探索"申请|响应拦截器"的实现

axios 的一个罕用个性就是拦截器,只须要简略的一行 axios.interceptors.request.use(config => return config)就能实现申请/响应的拦挡,在我的项目有鉴权需要或者返回值须要预处理时相当罕用. 当初就来看看这个个性是如何实现的,回到刚刚看过的 Axios.js:

// /lib/core/Axios.jsAxios.prototype.request = function request(config) {  // 此处省略申请配置合并代码,最终失去蕴含申请信息的 config 对象  // chain 能够视为申请流程数组,当不增加拦截器时 chain 如下  var chain = [dispatchRequest, undefined];  var promise = Promise.resolve(config);      // 拦截器实现  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {    // 往 chain 数组头部插入 then & catch逻辑    chain.unshift(interceptor.fulfilled, interceptor.rejected);  });  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {    chain.push(interceptor.fulfilled, interceptor.rejected); // 往 chain 尾部插入解决逻辑  });  // 执行申请  while (chain.length) {    // 申请流程数组前两个出栈,以后别离为 dispatchRequest 和 undefined    promise = promise.then(chain.shift(), chain.shift());  }  return promise;}; 

之前说过了我的项目中应用的 axios 其实就是 Axios.prototype.request,所以当 Axios.prototype.request 触发时,会遍历 axios.interceptors.requestaxios.interceptors.response 并将其中的拦挡逻辑增加到"申请流程数组 chain" 中.

在 Axios.prototype.request 中并没有 interceptors 属性的实现,于是回到 Axios 构造函数中寻找对应逻辑(之前说过,工厂函数 createInstance 会将 Axios的原型 & Axios实例的属性 复制给生成的 axios 对象):

// /lib/core/Axios.jsfunction Axios(instanceConfig) {  this.defaults = instanceConfig;  // 实例化 Axios 时也为其增加了 interceptors 对象,其携带了 request & response 两个实例  this.interceptors = {    request: new InterceptorManager(),    response: new InterceptorManager()  };}// /lib/core/InterceptorManager.jsfunction InterceptorManager() {  this.handlers = []; // 数组,用于治理拦挡逻辑}InterceptorManager.prototype.use = function use(fulfilled, rejected) {} // 增加拦截器InterceptorManager.prototype.eject = function eject(id) {} // 删除拦截器InterceptorManager.prototype.forEach = function forEach(fn) {} // 遍历拦截器 

Axios 构造函数在创立实例时会实现 interceptors 属性的创立,实现 axios.interceptors.requestaxios.interceptors.response 对于拦挡逻辑的治理.

5.2. 试验:同 axios 内多个拦截器的执行程序

因为 axios.interceptors.request 遍历增加到 "申请流程数组 chain" 向数组头插入 request 拦截器,所以越后 use 的 request 拦截器会越早执行. 相同,越后 use 的 response 拦截器会越晚执行.

当初假如为 axios 增加两个申请拦截器和两个响应拦截器,那么 "申请流程数组 chain" 就会变成这样(申请拦截器越后 use 的会越先执行):

[    request2_fulfilled, request2_rejected,    request1_fulfilled, request1_rejected,    dispatchRequest, undefined    response3_fulfilled, response3_rejected,  response4_fulfilled, response4_rejected,] 

为此编写个单测用于打印验证:

it('should add multiple request & response interceptors', function (done) {  var response;  axios.interceptors.request.use(function (data) {    console.log('request1_fulfilled, request1_rejected')    return data;  });  axios.interceptors.request.use(function (data) {    console.log('request2_fulfilled, request2_rejected')    return data;  });  axios.interceptors.response.use(function (data) {    console.log('response3_fulfilled, response3_rejected')    return data;  });  axios.interceptors.response.use(function (data) {    console.log('response4_fulfilled, response4_rejected')    return data;  });  axios('/foo').then(function (data) {    response = data;  });  getAjaxRequest().then(function (request) {    request.respondWith({      status: 200,      responseText: 'OK'    });    setTimeout(function () {      expect(response.data).toBe('OK');      done();    }, 100);  });}); 

后果如下,拦截器的执行打印与冀望的后果统一:

5.3. 探索"勾销拦截器"实现

应用以下与 setTimeout 和 clearTimeout 类似的语法,即可将一个定义好的拦截器给勾销掉:

var intercept = axios.interceptors.response.use(function (data) { return data; }); // 定义拦截器axios.interceptors.response.eject(intercept); // 勾销拦截器 

这里用到了 use 和 eject 两个办法,所以到 InterceptorManager.js 中找找相干实现:

// /lib/core/InterceptorManager.jsfunction InterceptorManager() {  this.handlers = []; // 数组,用于治理拦挡逻辑}// 增加拦截器InterceptorManager.prototype.use = function use(fulfilled, rejected) {  // 将拦挡逻辑包装为对象,推入治理数组 handles 中  this.handlers.push({ fulfilled, rejected });  return this.handlers.length - 1; // 返回以后下标};// 通过勾销拦截器InterceptorManager.prototype.eject = function eject(id) {  if (this.handlers[id]) {    this.handlers[id] = null; // 依据下标判断,存在则置空掉  }}; 

这里逻辑就比较简单了,由 InterceptorManager 创立一个内置数组的实例来治理所有拦截器,use 推入一个数组并返回其数组下标,eject 时也用数组下标来置空,这样就起到了拦截器治理的成果了~

小结

至此文章就告一段落了,还记得开始提出的三个问题吗?

  • Q1. 如何实现同时反对 axios(config)axios.get(config) 语法
  • A1. axios 库裸露的 axios 对象原本就是一个具备 Axios 实例属性的 Axios.prototype.request 函数. 详见"第三节.从 axios 对象开始剖析".
  • Q2. 浏览器和 NodeJS 申请能力的兼容实现
  • A2. 通过判断平台后抉择对应平台的适配器实现. 详见"第四节. 封装申请实现--'适配器'"
  • Q3. 申请 & 响应拦截器实现
  • A3. 通过数组的模式治理, 将申请拦截器、申请、响应拦截器都放在"申请流程数组 chain" 中,申请时顺次执行直到 "申请流程数组 chain" 为空. 详见"第五节. 拦截器实现".

    欢送拍砖,感觉还行也欢送点赞珍藏~ 新开公号:「无梦的冒险谭」欢送关注(搜寻 Nodreame 也能够~)旅程正在持续 ✿✿ヽ(°▽°)ノ✿