美文网首页
Axios源码阅读(一):核心功能源码

Axios源码阅读(一):核心功能源码

作者: 前端艾希 | 来源:发表于2021-11-26 19:39 被阅读0次

    一、Axios核心功能梳理

    先看下Axios官方文档的介绍:

    Axios is a promise-based HTTP Client for node.js and the browser. It is isomorphic (= it can run in the browser and nodejs with the same codebase). On the server-side it uses the native node.js http module, while on the client (browser) it uses XMLHttpRequests.

    通过介绍我们了解到Axios是一个可以运行在浏览器和Node.js环境的基于PromiseHTTP库,那么既然是一个HTTP库,核心功能自然是构造并发起HTTP请求。那么我们就从Axios如何实现一个GET/POST请求为切入点进行阅读。

    二、源码阅读

    引用文档上第一个示例代码:

    const axios = require('axios');
    
    // Make a request for a user with a given ID
    axios.get('/user?ID=12345')
      .then(function (response) {
        // handle success
        console.log(response);
      })
      .catch(function (error) {
        // handle error
        console.log(error);
      })
      .then(function () {
        // always executed
      });
    

    通过以上代码我们了解到:

    1. Axios库暴露出的axios上有一个get方法;
    2. 使用get方法可以发起HTTP请求;
    3. get方法返回一个promise对象;

    下面我们围绕这三点展开阅读。

    2.1 axios 对象和 get 方法

    首先通过入口文件index.js找到暴露对象来自./lib/axios.js,打开该文件找到了module.exports.default = axios这行代码,所以这个文件的axios对象就是示例代码中使用的对象,我们看下这个对象是如何产生的。

    传送门:./lib/axios.js

    var Axios = require('./core/Axios');
    
    /**
     * Create an instance of Axios
     *
     * @param {Object} defaultConfig The default config for the instance
     * @return {Axios} A new instance of Axios
     */
    function createInstance(defaultConfig) {
      var context = new Axios(defaultConfig);
      var instance = bind(Axios.prototype.request, context);
    
      // Copy axios.prototype to instance
      utils.extend(instance, Axios.prototype, context);
    
      // Copy context to instance
      utils.extend(instance, context);
    
      return instance;
    }
    
    // Create the default instance to be exported
    var axios = createInstance(defaults);
    

    传送门:./lib/core/Axios.js

    /**
     * Create a new instance of Axios
     *
     * @param {Object} instanceConfig The default config for the instance
     */
    function Axios(instanceConfig) {
      this.defaults = instanceConfig;
      this.interceptors = {
        request: new InterceptorManager(),
        response: new InterceptorManager()
      };
    }
    

    我们整理下上面代码的逻辑:

    1. 使用库提供的默认配置defaults通过Axios构造函数生成一个上下文对象context
    2. 声明变量instanceAxios.prototype.request并且绑定thiscontext
    3. instance上扩展了Axios.prototype的属性以及defaultsinterceptors属性;
    4. 返回instance,也就是说Axios库暴露的对象就是这个的instantce对象。

    总结一下axios的本质是Axios.prototype.request,并且扩展了Axios.prototype的属性和方法。那么axios.get也就是Axios.prototype上的方法咯,所以我们再次打开Axios.js文件一探究竟。

    传送门:.lib/core/Axios.js

    // Provide aliases for supported request methods
    utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
      /*eslint func-names:0*/
      Axios.prototype[method] = function(url, config) {
        return this.request(mergeConfig(config || {}, {
          method: method,
          url: url,
          data: (config || {}).data
        }));
      };
    });
    
    utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
      /*eslint func-names:0*/
      Axios.prototype[method] = function(url, data, config) {
        return this.request(mergeConfig(config || {}, {
          method: method,
          url: url,
          data: data
        }));
      };
    });
    

    从上面代码可以看出,axios上的getpostput等方法都是Axios.prototype.request的别名,所以使用Axios发出的HTTP请求其实都是Axios.prototype.request发起的。这也就解释了为什么发起一个get请求可以有2种写法:

    • axios.get(url)
    • axios({method: 'get', url: url})

    2.2 axios.get 发起请求的过程

    通过前面的代码,我们已经知道axios所有的请求都是通过Axios.prototype.request发起的,所以我们找到这个方法:

    传送门:./lib/core/Axios.js

    /**
     * Dispatch a request
     *
     * @param {Object} config The config specific for this request (merged with this.defaults)
     */
    Axios.prototype.request = function request(config) {
      /*eslint no-param-reassign:0*/
      // Allow for axios('example/url'[, config]) a la fetch API
      if (typeof config === 'string') {
        config = arguments[1] || {};
        config.url = arguments[0];
      } else {
        config = config || {};
      }
    
      config = mergeConfig(this.defaults, config);
    
      // Set config.method
      if (config.method) {
        config.method = config.method.toLowerCase();
      } else if (this.defaults.method) {
        config.method = this.defaults.method.toLowerCase();
      } else {
        config.method = 'get';
      }
      
      // 注意:这里我删去了与主流程无关的代码
      // 删去的代码主要功能:
      // 1. 判断拦截器是异步调用还是同步调用
      // 2. 把拦截器的 fulfilled回调 和 rejected回调整理到数组中
    
      var promise;
    
      if (!synchronousRequestInterceptors) {
        // 如果拦截器异步调用
        var chain = [dispatchRequest, undefined];
        
        Array.prototype.unshift.apply(chain, requestInterceptorChain);
        chain.concat(responseInterceptorChain);
        
        promise = Promise.resolve(config);
        while (chain.length) {
          promise = promise.then(chain.shift(), chain.shift());
        }
        
        // 返回一个从请求拦截器开始的链式调用的 promise 对象
        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());
      }
      
      // 返回一个从 dispatchRequest 开始的链式调用的 promise 对象
      return promise;
    };
    

    从上面代码我们了解到:在reqeust方法中使用promise对整个处理过程进行了封装,在没有拦截器的情况下返回的是dispatchRequest(newConfig),也就是说在dispatchRequest中发起请求并返回一个promise,我们找到dispatchRequest的代码:

    传送门: ./lib/core/dispatchRequest.js

    module.exports = function dispatchRequest(config) {
      throwIfCancellationRequested(config);
    
      // 删除了对 headers 和对 data 的处理,查看源代码点击上方传送门
    
      var adapter = config.adapter || defaults.adapter;
    
      return adapter(config).then(function onAdapterResolution(response) {
        // 对响应的处理
        return response;
      }, function onAdapterRejection(reason) {
        // 请求失败后对响应的处理
        return Promise.reject(reason);
      });
    };
    

    dispatchRequest返回的是adapter的结果,adapter来自config,而axios是通过默认配置产生的,所以我们找到defaults.js的代码:

    传送门: ./lib/defaults.js

    var defaults = {
      adapter: getDefaultAdapter(),
      // 删除无关的属性
    }
    
    function getDefaultAdapter() {
      var adapter;
      if (typeof XMLHttpRequest !== 'undefined') {
        // For browsers use XHR adapter
        adapter = require('./adapters/xhr');
      } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
        // For node use HTTP adapter
        adapter = require('./adapters/http');
      }
      return adapter;
    }
    

    这里adapter是一个适配器,因为axios在浏览器时依赖XHR对象,在node环境运行时依赖底层的http库,这两个对象都不是基于promise的,但是Axios希望和他们用promise交互,所以这里需要适配器来做一个兼容,为交互双方提供桥梁。我们找到浏览器环境下的xhr.js代码:

    传送门: ./lib/adapters/xhr.js

    module.exports = function xhrAdapter(config) {
      // 在这里使用 axios 对 xhr 进行封装
      return new Promise(function dispatchXhrRequest(resolve, reject) {
        // 这里我去掉了与认证以及请求头相关的处理
        
        // 实例化 xhr 对象
        var request = new XMLHttpRequest();
        // 构造 url
        request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    
        // Set the request timeout in MS
        // 设置超时时间
        request.timeout = config.timeout;
        
        // loadend回调
        function onloadend() {
          if (!request) {
            return;
          }
          // Prepare the response
          var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
          var responseData = !responseType || responseType === 'text' ||  responseType === 'json' ?
            request.responseText : request.response;
          var response = {
            data: responseData,
            status: request.status,
            statusText: request.statusText,
            headers: responseHeaders,
            config: config,
            request: request
          };
    
          settle(resolve, reject, response);
    
          // Clean up request
          request = null;
        }
    
        if ('onloadend' in request) {
          // Use onloadend if available
          request.onloadend = onloadend;
        } else {
          // Listen for ready state to emulate onloadend
          request.onreadystatechange = function handleLoad() {
            if (!request || request.readyState !== 4) {
              return;
            }
    
            // The request errored out and we didn't get a response, this will be
            // handled by onerror instead
            // With one exception: request that using file: protocol, most browsers
            // will return status as 0 even though it's a successful request
            // 这里看注释是为了处理当使用文件传输协议的时候浏览器会返回状态码 0
            if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
              return;
            }
            // readystate handler is calling before onerror or ontimeout handlers,
            // so we should call onloadend on the next 'tick'
            setTimeout(onloadend);
          };
        }
    
        // 这里删除了防范xsrf,以及一些其他的 eventhandler
        request.onabort = function handleAbort() {
          // do sth ...
          request = null;
        };
    
        // Handle low level network errors
        request.onerror = function handleError() {
          // do sth ...
          request = null;
        };
    
        // Handle timeout
        request.ontimeout = function handleTimeout() {
          // do sth ...
          request = null;
        };
        
        // Send the request
        request.send(requestData);
      });
    };
    

    上面的代码直接印证了文档所说的Axios在浏览器是基于XHR对象的,并且在这里清晰的展示了如何用promisexhr进行封装。

    总结下axios发起请求的过程:

    1. Axios.prototype.request方法中针对请求拦截器的同步/异步返回不同的promise(分别是拦截器的promisedispatchRequestpromise);
    2. dispatchReques中对请求头和请求的实体做了处理,然后返回adapter执行结果的promise
    3. 通过判断环境选择不同的adapter,在浏览器中使用的是xhr.js
    4. xhr.js中展示了使用XHR对象请求的过程以及事件处理,还有promise的封装过程。

    2.3 axios 返回的是 promise 对象

    在研究axios如何发起http请求时我们已经得到了结果:

    • 使用了异步拦截器的请情况下,返回的是拦截器的promise
    • 未使用异步拦截器或者未使用拦截器,返回的是dispatchRequestpromise,底层是用promisexhr的封装。

    在查看xhr.js的代码时,我对作者为什么要在onreadystatechangesetTimeout(onloadend);异步调用onloadend有些不解,看了作者的注释说是因为onreadystatechange会在ontimeout/onerror之前调用,所以如果这里同步调用的话,就会使用settle改变promise的状态了,但是作者希望不在settle中处理错误,而是通过xhr的事件去处理,因为我从来直接使用过xhr,所以我这里验证下:

    var xhr = new XMLHttpRequest();
    
    xhr.timeout = 1;
    xhr.open('get', '/api', true);
    xhr.ontimeout = function () {
        console.log('timeout & redystate = ', xhr.readyState);
    }
    
    xhr.onerror = function () {
        console.log('onerror & redystate = ', xhr.readyState);
    }
    
    xhr.onreadystatechange = function () {
        console.log('onreadystatechange & redystate = ', xhr.readyState);
    }
    
    xhr.send(null)
    

    结果如下:


    执行结果

    总结下:xhr.js的过程中了解了一些使用XHR对象的坑,也借此机会去 MDN 上又重新学习了下XHR,获益匪浅~

    三、总结及计划

    总结

    通过阅读代码,我了解到:

    • Axios整个项目的代码结构以及项目的大体思路;
    • Axios是如何管理拦截器的,方便下一步阅读拦截器代码;
    • Axios构造函数以及axios对象的属性以及方法;
    • Axios是如何使用Promise封装的;

    计划

    后续会按照官网给出的feature分步阅读源码:

    • Intercept request and response
    • Transform request and response data
    • Cancel requests
    • Automatic transforms for JSON data
    • Client side support for protecting against XSRF

    相关文章

      网友评论

          本文标题:Axios源码阅读(一):核心功能源码

          本文链接:https://www.haomeiwen.com/subject/dquxxrtx.html