preface

Axios is an HTTP library based on Promise, which can be used in browsers and Node.js. Most people’s understanding of Axios is still at the API level. As an excellent open source project, its source code is not complicated. Analysis of some of the packaging skills, specific functions to achieve, as well as read some ideas of the source code.

The directory structure

Copy the source code axios Clone to the local directory as follows:

  • Dist/Compile output folder
  • Example/official example
  • lib/ Source directory
    • / Adapters/Defines the adapter that sends the request
    • /cancel/ Defines the cancellation function
    • / core/core Axios
    • Helper/helper methods
    • Axios.js entry file
    • Defaults.js is the default configuration of axios
    • Util utility functions
  • Test Unit test
  • Sandbox Sandbox mode

Tool function

Source code has a few high frequency of tool functions, first familiar

bind

/** Bind function execution context this refers to */
module.exports = function bind(fn, thisArg) {
  return function wrap() {
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    return fn.apply(thisArg, args);
  };
};
Copy the code

forEach

/ * * * / iterate through group | object
function forEach(obj, fn) {
  // Don't bother if no value provided
  if (obj === null || typeof obj === "undefined") {
    return;
  }

  // Force an array if not already something iterable
  if (typeofobj ! = ="object") {
    obj = [obj];
  }

  if (isArray(obj)) {
    // Iterate over array values
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj); }}else {
    // Iterate over object keys
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj); }}}}Copy the code

extend

/** * extend object A by adding object B's attributes to object A. ThisArg Optional argument, binding for the context in which the function is executed when the attribute is a function *@return {Object} The resulting value of object a
 */
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;
}
Copy the code

merge

/** * When multiple objects contain the same key, the latter object argument list takes precedence
function merge(/* obj1, obj2, obj3, ... * /) {
  var result = {};
  function assignValue(val, key) {
    if (isPlainObject(result[key]) && isPlainObject(val)) {
      result[key] = merge(result[key], val);
    } else if (isPlainObject(val)) {
      result[key] = merge({}, val);
    } else if (isArray(val)) {
      result[key] = val.slice();
    } else{ result[key] = val; }}for (var i = 0, l = arguments.length; i < l; i++) {
    forEach(arguments[i], assignValue);
  }
  return result;
}
Copy the code

The core code

axios.js

First go to the entry file axios.js, not much code, directly to 👍 :

'use strict';

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

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

  // Specify the context in which the function is executed
  var instance = bind(Axios.prototype.request, context);

  // Attribute extension
  utils.extend(instance, Axios.prototype, context);

  utils.extend(instance, context);

  return instance;
}

// Create an axios instance with the default configuration, which will eventually be exported as an object
var axios = createInstance(defaults);

// The constructor is exposed
axios.Axios = Axios;

// Create a new instance of the factory function, which can pass in configurations to override the default defaults configuration
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// Bind cancellation request related methods
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

// Binding an all method essentially calls promise.all
axios.all = function all(promises) {
  return Promise.all(promises);
};

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

// Determine if there is an axios internal error
axios.isAxiosError = require('./helpers/isAxiosError');

module.exports = axios;

// Allow the default import syntax in Ts
module.exports.default = axios;

Copy the code

To sum up, three things are done here:

  1. callcreateInstanceMethod to create instance
  2. Mount some methods and properties on the instance
  3. Expose this instance to the public

CreateInstance createInstance

  • Create context with default configuration
  • usingbindThe functionAxios.prototype.requestContext: create instance
  • extensionAxiosThis refers to the context context
  • Extend attributes in context context to instance instance
  • Returns the instance

Return value Instance is a request function bound to the execution context, with a number of properties and methods mounted on it (the execution context of the method is determined)

Let’s go back to the source code for Axios and axios.prototype. request

Axios.js

'use strict';

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

var validators = validator.validators;
/** * Create a new instance of Axios */
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

/** * send out a request * request core code *@param {Object} config The config specific for this request (merged with this.defaults)
 */
Axios.prototype.request = function request(config) {
  // Allow for axios('example/url'[, config]) a la fetch API
  // Allow another way to pass the parameter url first
  if (typeof config === 'string') {
    config = arguments[1) | | {}; config.url =arguments[0];
  } else {
    config = config || {};
  }
  
  // omit
  // ... 
};


// Mount some methods on the prototype
// Different requests have different parameters, so there are two groups. The second group has the data parameter, see get post
utils.forEach(['delete'.'get'.'head'.'options'].function forEachMethodNoData(method) {
  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) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

module.exports = Axios;
Copy the code

To summarize what the code does:

  1. The request methods get, post and so on are registered in the Axios prototype
  2. Define request and response interceptor management objects
  3. Defines the request method that initiates the request

Here’s why Axios offers multiple uses:

  1. axios(config)
  2. axios(url, {... })
  3. axios.get(url, {... })
  4. axios.request(config)

A: Because axios extends axios’ prototype methods, these methods essentially call Request methods, while Request does a layer of parameter type judgment, supporting different ways of passing arguments.

The axios.prototype. request method is the entry to the start of the request. It handles the request config separately, and chases the request, responds to the interceptor, and returns the Proimse format for handling callbacks. Let’s move on:

Since this section covers a lot, we need to understand the concept of an interceptor first.

The interceptor

Interceptors act as promise-based middleware, intercepting requests or responses before they are processed by THEN or catch.

Back when initializing Axios, the Interceptors object has two properties: Request and Response, both of which are an instance of the InterceptorManager. The InterceptorManager constructor is used to manage the interceptor. Take a look at the definition:

function InterceptorManager() {
  // An array of interceptors
  this.handlers = [];
}

// Register interceptor, return an ID, useful when clearing interceptors
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
  });
  return this.handlers.length - 1;
};

// Clear the interceptor
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null; }};// The loop interceptor array is called in turn
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if(h ! = =null) { fn(h); }}); };Copy the code

Code looks really pretty simple, it is some way of operating an array, so that when we pass axios. Interceptors. Request. Use add interceptors, how let the interceptor can before the request, the request of data to get what we want?

To see how Request does this, the source code comes with a detailed parsing:

Axios.prototype.request = function request(config) {
  // Omit some code.// Request an array of intercepts
  var requestInterceptorChain = [];
  var synchronousRequestInterceptors = true;
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
      return;
    }

    synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
    / / in every time the unshift come in pairs: [fulfilled, rejected, fulfilled, rejected,...
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  
  // The same goes for array intercepts
  var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  var promise;
  / / synchronousRequestInterceptors request interceptor is synchronous to true said, usually are asynchronous
  if(! synchronousRequestInterceptors) {// Create an array of request sequences, the first is the method to send the request, the second is empty
    var chain = [dispatchRequest, undefined];
    // Push the request interceptor array to the front of the request sequence
    // This is a pity, my request rejected, dispatchRequest, undefined.
    Array.prototype.unshift.apply(chain, requestInterceptorChain);
    // Push the response interceptor array back to the request sequence
    // This is a big pity, which is fulfilled someday. // This is a big pity, which is fulfilled by dispatchRequest, undefined, response rejected]
    chain.concat(responseInterceptorChain);
    // Construct a promise, passing in config
    promise = Promise.resolve(config);
    // Chain "then" after promise
    / / equal to promise (). Then (). Then ()...
    while (chain.length) {
     // 1. Each loop fetches a pair from the chain array as the first and second arguments to the promise.then method.
     / / 2. Request for interceptors, read in sequence from the interceptor array is through the unshift method after added into the chain number of arrays, and by the method of shift from the chain array, so come to the conclusion: request for interceptors, first add the interceptor after the meeting
     // 3. For responder interceptors, the chain array is added to by shift, and the chain array is removed by shift. For responder interceptors, the chain array is added to by shift
     // The big pity function of the first request interceptor will receive the Config object passed in when the Promise object is initialized, and the request interceptor specifies that the user-written forgetting function must return a Config object, so when implementing the chain call through the Promise, The FULFILLED function of each request interceptor receives a Config object
     The fulfilled function will accept the data requested by dispatchRequest (also known as our request method), which is also the response object. The response interceptor also specifies that the fulfilled function written by the user must return a response object. So when chain calls are implemented through promises, each response interceptor's depressing function will receive a response object
     // 6. An error from a dispatchRequest is received by the response interceptor (Rejected) function, so an error from dispatchRequest is received by the response interceptor.
     // 7. Since AXIos is a chained call through a promise, we can do it asynchronously in the interceptor, and the interceptor will be executed in the same order as above, i.e. the dispatchRequest method must wait for all request interceptors to execute. The response interceptor must wait for dispatchRequest to finish before it starts executing.
      promise = promise.then(chain.shift(), chain.shift());
    }

    return promise;
  }

  // The synchronization case is simple
  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;
};
Copy the code

After analyzing the above code, I found that the promise feature was actually used.Value passedandError bubble.Callback delayed bindingThe final result is also a promise. The user gets the result of the request through the promise.then method.

Now that we know how the interceptor works, how do our requests actually get sent to the server? Look at the dispatchRequest method.

dispatchRequest

Function dispatchRequest (config) {/ / request related alone speak throwIfCancellationRequested (config); // Omit a series of standardized processing of the requested data //.... var adapter = config.adapter || defaults.adapter; Return adapter(config). Then (function onAdapterResolution(response) { throwIfCancellationRequested(config); Response. data = transformdata. call(config, response.data, response.headers, config.transformResponse); return response; }, function onAdapterRejection(reason) { if (! isCancel(reason)) { throwIfCancellationRequested(config); if (reason && reason.response) { reason.response.data = transformData.call( config, reason.response.data, reason.response.headers, config.transformResponse ); } } return Promise.reject(reason); }); };Copy the code

The summary code does three things:

  1. Cancellation request processing and judgment, which will be covered later
  2. Handle parameters and default parameters
  3. The adapter is called to send the request, and the response results are processed in either successful or failed cases

When the user does not specify an adapter, defaults.adapter is used by default, so we can find the definition of adapter in defaults.js

function getDefaultAdapter() {
  var adapter;
  if (typeofXMLHttpRequest ! = ='undefined') {
    // In a browser environment with XHR support
    adapter = require('./adapters/xhr');
  } else if (typeofprocess ! = ='undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // In node environment
    adapter = require('./adapters/http');
  }
  return adapter;
}
Copy the code

Axios is essentially a wrapped XHR object in the browser environment, and an HTTP method in the NodeJS environment. Here we only analyze the processing in the browser environment:

function xhrAdapter(config) {
    return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var requestHeaders = config.headers;
    varresponseType = config.responseType; .var request = newXMLHttpRequest(); . request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer),true);

    // Set the request timeout in MS
    request.timeout = config.timeout;

    function onloadend() {...varresponseData = ! responseType || responseType ==='text' ||  responseType === 'json' ?
        request.responseText : request.response;
      // The requested data is in the data attribute
      // axios(...) .then(res=>{res.data})
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };
      
      // Determine the status of the promise based on the status returned by XHR
      // status >= 200 && status < 300
      settle(resolve, reject, response);

      // Empty the request
      request = null;
    }

    if ('onloadend' in request) {
      request.onloadend = onloadend;
    } else {
      // Listen for ready state to emulate onloadend
      request.onreadystatechange = function handleLoad() {... }; }// Handle browser request cancellation (as opposed to a manual cancellation)
    request.onabort = function handleAbort() {... };// Handle low level network errors
    request.onerror = function handleError() {... };// Handle timeout
    request.ontimeout = function handleTimeout() {... };// TODO:CRSF defense will be discussed separately
    / /...

    // Add headers to the request.// Add withCredentials to request if needed.// Add responseType to request if needed. .// TODO:Cancel the request and speak separately
    / /...

    // send Sends a request
    request.send(requestData);
  });
};
Copy the code

Conclusion: AXIOS is implemented on the browser side using a promise that encapsulates the XMLHttpRequest request process. To summarize what the code does:

  1. Send the request using the browser XHR object
  2. The request process is encapsulated as a promise
  3. Listen for events that XHR might throw to determine the state of the promise
  4. Cancel the request and speak separately
  5. CSRF prevention, separate

This time we’ve basically covered the process of initiating and responding to an AXIos request. Next we’ll look at some of the features axios has 🎉

Cancel request – CancelToken

It’s not uncommon to send an HTTP request and suddenly not need the result while waiting for an interface response. For example, if a TAB page is switched quickly, its content depends on the return of a request interface. Because the interface takes a long time, the user switches another TAB page during the waiting period. In this case, if the original request is not cancelled, it will be very chaotic.

How does Axios help cancel the original request? We know that the promise is implemented internally, so the question becomes: How do you cancel a promise?

In fact, once the promise is implemented, it cannot be cancelled. But we can realize the function like canceling the promise through the features of the promise. Once the state of the promise changes (from pending to pity or Rejected), it cannot be changed again.

We can expose a cancel function that is called when we need to cancel a Promise. When called, the Promise’s resovle or Reject methods are executed, so that resolve or Reject is ignored when the interface responds. This is a way to implement something like cancel promises.

let cancel;
let p = new Promise((resolve, reject) = > {
  cancel = resolve;
  setTimeout(() = > {
    console.log('xxx')
    resolve("done");
  }, 2000);
});
cancel('promise cancel')
p.then((r) = > console.log(r)); // output: promise cancel
Copy the code

In fact, the setTimeout code still executes, but the state of the promise is determined by Cancel. In fact, Axios makes use of this feature, which can be found in the source code.

function CancelToken(executor) {
  // Some code is omitted.var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

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

  return {
    token: token,
    cancel: cancel,
  };
};

module.exports = CancelToken;
Copy the code
  1. The CancelToken constructor instantiates a promise property, a promise, that exposes its resolve method
  2. The cancel function is responsible for changing the state of this promise

Once we get the cancel function, we can decide when to change the promise. There are two ways to cancel the request, and the internal principle is the same:

  • CancelToken’s executor argument is executed as a function, and internally cancel is executed as an argument. We get this argument to use the cancel function externally
  • When created using the factory method source, cancel is returned as cancel, and the token is an instance of CancelToken.

In fact, the factory method encapsulates the instantiation and unassignment operations.

CancelToken is the object we instantiated. When the cancel function is called, the promise state on the instance changes, and the then callback is fired:

  1. Request.abort () is used to interrupt the request
  2. A call to reject changes the state of the AXIos promise to Rejected, with the argument Cancel being token.reason
  3. Clearing the Request object

That’s the whole process of canceling a request

function xhrAdapter(config) {
    return new Promise(function dispatchXhrRequest(resolve, reject) {
            // ...
            if (config.cancelToken) {
                // Handle cancellation
                config.cancelToken.promise.then(function onCanceled(cancel) {
                    if(! request) {return;
                    }

                    request.abort();
                    reject(cancel);
                    // Clean up request
                    request = null;
                });
            }
           // ...}}Copy the code

CSRF protection

Cross-site request forgery (” CSRF “or” XSRF “) is an attack that tricks a user into performing unintended actions on a currently logged Web application.

A cross-site request attack, simply put, is a technique by which an attacker tricks a user’s browser into visiting a previously authenticated web site and performing operations (such as emailing, sending messages, or even property operations such as transferring money or buying goods). Because the browser has been authenticated, the site being visited will act as if it were a genuine user action.

As a request library, how does AXIos help us protect against CSRF attacks?

There are two variables in the default configuration (later merged into config) :

xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
Copy the code

The fields can be used as the front and back end conventions to help us defend against CRSF attacks. The code can be found in xhrAdapter as follows

// Standard browser environment
if (utils.isStandardBrowserEnv()) {
  var cookies = require('. /.. /helpers/cookies');
  // withCredentials: Carry cookies across domains
  // isURLSameOrigin: same origin
  var xsrfValue =
    (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName
      ? cookies.read(config.xsrfCookieName)
      : undefined;

  if(xsrfValue) { requestHeaders[config.xsrfHeaderName] = Value; }}Copy the code

The logic of the code is very simple. In fact, if the cookie contains the field xSRF-token, the x-XSRF-token value in the header is set to the corresponding value of XSRF-token, which is a TOKEN value returned to us by the server. Generally, it is stored in the session on the server and has an expiration time. The request compares the header token with the session token. If the header token does not match the session token, the request is considered as a CSRF attack.

Because cookies do not cross domains, the attacker cannot obtain the token value. In this case, the browser carries cookies but does not contain the X-XSRF-Token field of the header agreed on by the front-end and back-end. Therefore, the request fails, preventing CSRF attacks.

Data converter

TransformRequest and transformResponse are used to convert request and response data. The default defaults configuration provides a custom request converter and a response converter. Converting JSON data automatically is one of axiOS’s highlights. By default, AXIos will automatically serialize the incoming data object into a JSON string and convert the JSON string in the response data into a JavaScript object. The functions in the converter array take config data and header as arguments and return the processed data.

// /lib/defaults.js
var defaults = {

  transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Content-Type');
    // ...
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded; charset=utf-8');
      return data.toString();
    }
    if (utils.isObject(data)) {
      // serialize data
      setContentTypeIfUnset(headers, 'application/json; charset=utf-8');
      return JSON.stringify(data);
    }
    returndata; }].transformResponse: [function transformResponse(data) {
    if (typeof data === 'string') {
      try {
        // Try converting to a JS object
        data = JSON.parse(data);
      } catch (e) { /* Ignore */}}returndata; }]};Copy the code

Converters are used both before and after the dispatchRequest request process

  • The request converter is used to process the request data before the HTTP request and then pass it to the HTTP request adapter for use.
  • The response converter is used to convert data based on the return value of the HTTP request adapter after the HTTP request completes
// /lib/core/dispatchRequest.js
function dispatchRequest(config) {
  // transformData: Walks through an array of converters, executes each converter separately, and returns a new data based on the data and headers arguments
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  return adapter(config).then(/ *... * /);
};
Copy the code

conclusion

So far, we have a complete understanding of AXIos. After reading axios, I feel a lot. The design ideas in axios are worthy of reference, such as various processing methods before and after request, parameter combination, chain call of promise and so on. And then you can blow yourself up and see how it applies to the AXIos implementation, right

If you have any questions or I understand the wrong place welcome to criticize and correct, common progress. Please like three times QAQ🔥🔥

Refer to the link

  • Axios source code in depth analysis
  • What can be learned from the 77.9K Axios project
  • Safety | attack on a common Web CSRF attacks