Parse Axios source line by line

Keywords: Javascript axios JSON Attribute network

Source directory structure


# 📁 lib
# |-Request method used primarily by adapters // axios
# Request functions used by the node side in |-|-http.js // Axios
# Request functions used on browser side in |-|-xhr.js // Axios
# |-  📁 cancel
# |-|-|-Cancel.js //Defined, cancel the information structure returned by the request
# |-|-|-CancelToken.js//Defines the main method used to cancel the request
# |-|-|-isCancel.js //Information to determine whether the request was cancelled
# |-  📁 core
# |-|-|-Axios.js // Axios class
# |-|-|-dispatchRequest.js//Where the request originated 
# |--|-InterceptorManager.js // InterceptorManager class, interceptor class
# |-|-|-mergeConfig.js //Merge Configuration Items
# |-|-|-settle.js //Processing Promise based on request status
# |-|-|-createError.js//Generate the specified error
# |-|-|-enhanceError.js//toJSON method specifying error object
# |--|-transformData.js //Format responses and requests using transformRequest and transformResponse in default.js
# |-  📁 helpers
# |-|-|-bind.js//tool function
# |--|-parseHeaders.js //Convert header information returned by getAllResponseHeaders into objects
# |-|-|-buildURL.js //params parameter
# |-|-|-cookies.js//Encapsulates methods to read, write, and delete cookies
# |-|-|-isURLSameOrigin.js //Check if the current url is the same as the requested url
# |--|-normalizeHeaderName.js //Format object property names, delete, create new case-sensitive properties
# |-|-|-combineURLs.js //combined baseurl
# |-|-|-isAbsoluteURL.js //Determine if it is an absolute path (refers to: //or//begins with an absolute path)
# |-  📃 axios.js
# |-Default configuration in defaults.js // axios
# |-utils.js //Some tool methods
# |-|-|-isFormData //Determine whether it is a formData object
# | -| -isStandard BrowserEnv //Determine if the current environment is a standard browser environment
# | -| -isUndefined //Determine whether it is undefined
# |-  |-  ⏹ merge
# |-|-|-isURLSearchParams //Determine whether it is a URLSearchParams object

Preface

This article focuses mainly on the source code of the main processes in axios, and skips the implementation of some tool functions.Please forgive me.If there are errors in the article, please point them out in time.

Overview of Request Process

Here is a rough code flow from the axios source when a request is made

Source Progressive Analysis

/lib/cancel/CancelToken.js

The code in CancelToken.js defines the behavior associated with canceling axios requests.However, the cancel method returned by CancelToken.source for canceling requests is based on the premise that CancelToken.source needs to be returned to token and combined into the config of the specific request to work properly.

How can I use the cancel request function in axios?

Before I looked at the Axios source code, I didn't even know the requests Axios could make, so let's first learn how to cancel a request in axios.Here is an example_

// axios class for cancelling requests
const CancelToken = axios.CancelToken
// The source method returns an object that contains
// {
  // token, added to the config of the request to identify the request
  // Cancel, call cancel method to cancel request
// }
const source = CancelToken.source()

axios.get('/info', {
  cancelToken: source.token
}).catch(function(error) {
  if (axios.isCancel(error)) {
    console.log('Error Canceling Request')
  } else {
    // Other Errors
  }
})

// Calling source.cancel cancels the request for axios.get('/info')
source.cancel('Cancel Request')

Source code

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

function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;

  // Create a Promise
  // The promise will remain pending until the cancel function is called
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;

  executor(function cancel(message) {
    // Determine if the request has been cancelled
    if (token.reason) {
      return;
    }

    // Create cancel request information and add it to the reason property of the instance
    token.reason = new Cancel(message);
  
    // End pending state of this.promise
    // Set this.promise status to resolve
    resolvePromise(token.reason);
  });
}

// Method to determine if the request has been cancelled
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

module.exports = CancelToken;

_Seeing this, we still can't understand how axios canceled a request.Because cancel returned by CancelToken.source alone cannot cancel a request, we need to understand it in conjunction with the code in xhr.js.


// /lib/adapters/xhr.js

request.open()

// ...omitted

// If cancelToken option is configured
if (config.cancelToken) {
  
  // Add a successful callback to Proise created in CancellToken
  // The callback is triggered when the CancelToken.source exposed cancel function is called
  config.cancelToken.promise.then(function onCanceled(cancel) {

    if (!request) {
      return;
    }

    // Cancel xhr request
    request.abort();
    
    // Set promise returned by axios to reject state
    reject(cancel);

    request = null;
  });
}

// ...omitted

request.send()

You must have seen here that you have a general understanding of how requests are made in axios.To summarize, we created an additional PromiseA from CancelToken, mounted PromiseA into config, and exposed the resolve method of that PromiseA.We add monitoring of PromiseA's status before calling the send method (before sending the request), and when the PromiseA's status is modified, we cancel the request in the callback of PromiseA and set the PromiseB status returned by Axios to reject.In order to cancel the request

/lib/adapters/xhr.js

The xhrAdapter method exported by xhr.js is the default request method used by axios in a browser environment.We can use the adapter configuration item in the configuration to replace the default request method.

Source code


module.exports = function xhrAdapter(config) {

  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var requestHeaders = config.headers;

    // Determine if it is a FormData object, and if so, delete the Content-Type field of the header and let the browser set the Content-Type field automatically
    if (utils.isFormData(requestData)) {
      delete requestHeaders['Content-Type'];
    }

    // Create xtr object
    var request = new XMLHttpRequest();

    // Set Authorization field in http request header
    // About Authorization fields
    // For more information, refer to https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Authorization
    if (config.auth) {
      var username = config.auth.username || '';
      var password = config.auth.password || '';
      // Encoding username and password using btoa method base64
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }

    // Initialization Request Method
    // open(method: requested http method, url: requested URL address, whether asynchronous is supported)
    request.open(
      config.method.toUpperCase(),
      buildURL(config.url, config.params, config.paramsSerializer),
      true
    );

    // Set timeout
    request.timeout = config.timeout;

    // Listens for changes in the readyState state, and when the readyState state is 4, the ajax request succeeds
    request.onreadystatechange = function handleLoad() {
      if (!request || request.readyState !== 4) {
        return;
      }

      // The digital status code of the request.status response, equal to 0 before the request is completed
      // A request.status error returns 0, but status equal to 0 is also a successful request except for the file Protocol
      // See https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/status for more information
      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        return;
      }

      // The getAllResponseHeaders method returns all response headers
      // For more information, see https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/getAllResponseHeaders
      var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;

      // Get the request.response Text value if the data response type is not set (default is "json") or if the responseType is set to text, otherwise get the request response.
      // ResponsseType is an enumerated type, manually setting the type of returned data can be found at https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/responseType for more information
      // ResponsseText is a value that returns data from all backends as plain text. For more information, see https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/responseText
      // Response is text, and the type of response depends on the type of response. See more at https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/response
      var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;

      var response = {
        data: responseData, // Response Content
        status: request.status, // Response Status
        statusText: request.statusText, // Text information for response status
        headers: responseHeaders, // Response Header
        config: config,
        request: request
      };

      // status >= 200 && status < 300 resolve
      // Otherwise reject
      settle(resolve, reject, response);

      request = null;
    };

    // Triggered when ajax is interrupted
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }

      // Throw Request aborted error
      reject(createError('Request aborted', config, 'ECONNABORTED', request));

      request = null;
    };

    // Triggered when ajax fails
    request.onerror = function handleError() {
      // Throw Network Error Error
      reject(createError('Network Error', config, null, request));

      request = null;
    };

    // ajax request timeout call
    request.ontimeout = function handleTimeout() {
      // Throw timeout error
      reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED',
        request));

      request = null;
    };

    // Determine that the current browser environment is standard and, if so, add an xsrf header
    // What is xsrf header?Xsrf header is used to defend against CSRF attacks
    // The principle is that the service side generates an XSRF-TOKEN and saves it in the browser's cookie, and ajax sets the XSRF-TOKEN in the request header for each request
    // The server compares the XSRF-TOKEN in the cookie to the XSRF-TOKEN in the header
    // According to the homology policy, non-homologous sites cannot read and modify the site cookies of this source, avoiding forgery of cookies
    if (utils.isStandardBrowserEnv()) {
      var cookies = require('./../helpers/cookies');

      // With Credentials Sets whether cookie s should be used in cross-domain requests See more at https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/withCredentials
      // (set withCredentials to true or homologous request) and set xsrfCookieName
      var xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ?
      // Read XSRF-TOKEN from cookie s
        cookies.read(config.xsrfCookieName) :
        undefined;

      if (xsrfValue) {
        // Setting XSRF-TOKEN in request header
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }

    // setRequestHeader is the method used to set the request header
    if ('setRequestHeader' in request) {
      // Set request headers configured in config, loop to request headers
      utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
          delete requestHeaders[key];
        } else {
          request.setRequestHeader(key, val);
        }
      });
    }

    // Set the withCredentials property of the xhr object to allow cookie s to request across domains
    if (config.withCredentials) {
      request.withCredentials = true;
    }

    // Set the responseType property of the xhr object
    if (config.responseType) {
      try {
        request.responseType = config.responseType;
      } catch (e) {
        if (config.responseType !== 'json') {
          throw e;
        }
      }
    }

    // Download Progress
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
    }

    // Upload Progress
    // The request.upload XMLHttpRequest.upload attribute returns an XMLHttpRequestUpload object that represents the progress of the upload
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
    }

    if (config.cancelToken) {
      // Cancel requests, described in Introduction/lib/cancel/CancelToken.js and not covered here
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }
        request.abort();
        reject(cancel);
        request = null;
      });
    }

    if (requestData === undefined) {
      requestData = null;
    }

    // Send http request
    request.send(requestData);
  });
};

/lib/core/dispatchRequest.js

The dispatchRequest.js file is where requests are actually called from the axios source.

Source code

var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
var isAbsoluteURL = require('./../helpers/isAbsoluteURL');
var combineURLs = require('./../helpers/combineURLs');

// Determines if the request has been cancelled and if canceled, the throw has been canceled
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // Combine baseUrl with config.url if it contains baseUrl and is not an absolute path to config.url
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    // Combining baseURL s with URLs to form a complete request path
    config.url = combineURLs(config.baseURL, config.url);
  }

  config.headers = config.headers || {};

  // Format config.headers and config.data using the transformRequest method in/lib/defaults.js
  // For example, capitalize Accept, Content-Type in headers
  // For example, if the request body is an Object, it is formatted as a JSON string and application/json is added; charset=utf-8 Content-Type
  // And so on
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // Configuration priority of config.headers is higher when combining headers of different configurations
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

  // Remove method attribute from headers
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

  // If config configures adapter, use the alternative default request method in config that configures adapter
  var adapter = config.adapter || defaults.adapter;

  // Initiate requests using the adapter method (adapters vary depending on the browser or Node environment)
  return adapter(config).then(
    // Callback requesting correct return
    function onAdapterResolution(response) {
      // Determine if and cancel the request, and if canceled throws to cancel
      throwIfCancellationRequested(config);

      // Format the data returned by the server using the transformResponse method in/lib/defaults.js
      // For example, use JSON.parse to parse the response body
      response.data = transformData(
        response.data,
        response.headers,
        config.transformResponse
      );

      return response;
    },
    // Callbacks with failed requests
    function onAdapterRejection(reason) {
      if (!isCancel(reason)) {
        throwIfCancellationRequested(config);

        if (reason && reason.response) {
          reason.response.data = transformData(
            reason.response.data,
            reason.response.headers,
            config.transformResponse
          );
        }
      }
      return Promise.reject(reason);
    }
  );
};

/lib/core/InterceptorManager.js

The axios interceptor class is defined in the InterceptorManager.js file.Includes add, remove, loop interceptors.Both response and request interceptors are stored using arrays.

var utils = require('./../utils');
 
// Interceptor Class
function InterceptorManager() {
  // handlers arrays are used to store interceptors
  this.handlers = [];
}

// Add interceptor, use method receives two parameters, successful callback and failed callback
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    // Successful callbacks
    fulfilled: fulfilled,
    // Failed callbacks
    rejected: rejected
  });
  return this.handlers.length - 1;
};

// Delete interceptors in instance handlers property based on ID (index)
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

// Cycle Interceptor
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

module.exports = InterceptorManager;

/lib/core/Axios.js

The Axios.js file defines the request, get, post, delete methods on the Axios instance.Get, post, delete and other methods are based on Axios.prototype.request encapsulation.In Axios.prototype.request, request interceptors, dispatchRequest (actually initiated), are executed in turn to respond to interceptors.The overall process is shown in the diagram above.

Source code

var utils = require('./../utils');
var buildURL = require('../helpers/buildURL');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
var mergeConfig = require('./mergeConfig');

function Axios(instanceConfig) {
  // Configuration of Axios
  this.defaults = instanceConfig;
  // Interceptor
  this.interceptors = {
    request: new InterceptorManager(), // request interceptor
    response: new InterceptorManager() // Response Interceptor
  };
}

Axios.prototype.request = function request(config) {

  // If config is a string, treat the string as the url address of the request
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  // Consolidated Configuration
  config = mergeConfig(this.defaults, config);
  // If no request method is specified, use the get method
  config.method = config.method ? config.method.toLowerCase() : 'get';

  var promise = Promise.resolve(config);

  // Combine request interceptors, response interceptors, and actual request (dispatchRequest) methods into an array similar to the following structure
  // [Request Interceptor 1 success, Request Interceptor 1 error, Request Interceptor 2 success, Request Interceptor 2 error, dispatchRequest, undefined, Response Interceptor 1 success, Response Interceptor 1 error]
  var chain = [dispatchRequest, undefined];
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  
  // Start the entire request process (request interceptor - > dispatchRequest - > response interceptor)
  // The process can be interpreted as the picture above
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

Axios.prototype.getUri = function getUri(config) {
  config = mergeConfig(this.defaults, config);
  return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
};

// Encapsulate other methods based on Axios.prototype.request
// Add delete, get, head, options, post, put, patch to the prototype chain of Axios.prototype
// Axios.prototype.delete =
// Axios.prototype.get =
// Axios.prototype.head =
// Axios.prototype.options =
// ...
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

module.exports = Axios;

/lib/defaults.js

The defaults.js file configures the default request header for axios, the default request method for Axios in different environments, the method for formatting the request body, the method for formatting the response body, and so on.

Source code

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

// Default Content-Type
var DEFAULT_CONTENT_TYPE = {
  'Content-Type': 'application/x-www-form-urlencoded'
};

// Set ContentType, without setting
function setContentTypeIfUnset(headers, value) {
  if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
    headers['Content-Type'] = value;
  }
}

// Get the default request method based on the current environment
function getDefaultAdapter() {
  var adapter;
  // Determine if a process object exists in the current environment
  if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // node environment
    adapter = require('./adapters/http');
  } else if (typeof XMLHttpRequest !== 'undefined') {
    // Browser Environment
    adapter = require('./adapters/xhr');
  }
  return adapter;
}

var defaults = {
  // Default Request Method
  adapter: getDefaultAdapter(),

  // Format request data, which will be used before the request is sent
  transformRequest: [
    function transformRequest(data, headers) {
      // Format the header property name to format the nonstandard property name in the header to the Accept property name
      normalizeHeaderName(headers, 'Accept');
      // Format the header attribute name, format the nonstandard attribute name in the header as the Content-Type attribute name
      normalizeHeaderName(headers, 'Content-Type');

      if (utils.isFormData(data) ||
        utils.isArrayBuffer(data) ||
        utils.isBuffer(data) ||
        utils.isStream(data) ||
        utils.isFile(data) ||
        utils.isBlob(data)
      ) {
        return data;
      }
      if (utils.isArrayBufferView(data)) {
        return data.buffer;
      }

      // URLSearchParams provides some string interfaces for handling URL queries
      // If it is a URLSearchParams object
      if (utils.isURLSearchParams(data)) {
        // Content-Type is set to application/x-www-form-urlencoded
        // application/x-www-form-urlencoded, data is encoded as key-value pairs separated by
        setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
        return data.toString();
      }

      // If it is an object
      if (utils.isObject(data)) {
        // Content-Type set to application/json
        setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
        // Format the request body as a JSON string and return
        return JSON.stringify(data);
      }

      return data;
    }
  ],

  // Format the response resposeData, which will be used when the response is accepted
  transformResponse: [
    function transformResponse(data) {
      if (typeof data === 'string') {
        try {
          data = JSON.parse(data);
        } catch (e) { /* Ignore */ }
      }
      return data;
    }
  ],

  // Default timeout
  timeout: 0,

  // The key of the cookie set by xsrf
  xsrfCookieName: 'XSRF-TOKEN',
  // xsrf sets the header's key
  xsrfHeaderName: 'X-XSRF-TOKEN',

  maxContentLength: -1,

  // Verify the status of the request
  // Romise is used when processing requests
  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  }
};

defaults.headers = {
  // Common HTTP Fields
  // Accept tells clients what types they can handle
  common: {
    'Accept': 'application/json, text/plain, */*'
  }
};

utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
  defaults.headers[method] = {};
});

// Set default Content-Type for post, put, patch request
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});

module.exports = defaults;

/lib/axios.js

The axios.js file is the entry method to the Axios tool library, at axios.js

var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var mergeConfig = require('./core/mergeConfig');
var defaults = require('./defaults');

// Create axios instance
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);

  // Change this of Axios.prototype.request to execute the context instance
  // instance equals the Axios.prototype.request method
  var instance = bind(Axios.prototype.request, context);

  // Merge attributes on Axios.prototype, context into instance
  // instance.get = Axios.prototype.get
  // instance.defaults = context.defaults
  // ...
  utils.extend(instance, Axios.prototype, context); 
  utils.extend(instance, context);

  return instance;
}

// Axios exposes a axios.request method directly to the user, so we can use it when using axios.You do not need an instance of new Axios
// import axios from 'axios'
// axios.get('/info')
var axios = createInstance(defaults);

axios.Axios = Axios;

// axios.create can generate a new axios instance based on a user-defined config
axios.create = function create(instanceConfig) {
  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);
};

axios.spread = require('./helpers/spread');

module.exports = axios;

module.exports.default = axios;

Posted by acroporas on Thu, 08 Aug 2019 09:06:08 -0700