Axios is a small, promise-based HTTP request library, currently available at Stars 82K on Github, that runs in browsers and Nodejs. Perennial occupation of the list, its source is very worth analyzing.

Written in the beginning

This article is based on the source code version V0.21.1 analysis, detailed Axios instance generation process, constructors, interceptors, cancels, and other functions, each section will be from how to use the source code step by step.

Before analyzing the source code, let’s think about a few questions: if you were to write the source code, what would happen?

  1. Why can Axios be used in both browser side and Nodejs?
  2. How is the Axios interceptor implemented?
  3. Every open source project has its own library. Is there a way to slap your thighs in the Axios toolkit?
  4. How does Axios implement cancellation?
  5. What process does Axios go through from request to completion, and what are some clever ways to implement it that are worth learning?
  6. From the user’s perspective, Axios can be used in Axios ({config}), axios.post(), axios.get(), and so on. Axios is like an Object or a Function, but what type is it? How does the source code support these requests?
├─ /dist/ # Build ├─ └ │ ├─ http.js # ├── ├.js # ├─ ├.js # ├─ ├.js #.js # ├─.js #.js #.js #.js #.js #.js #.js #.js #.js #.js #.js #.js #.js #.js #.js #.js #.js #.js #.js #.js #.js #.js #.js #.js │ ├─ │ ├─ Axios. Js # │ ├─ Axios. Js # │ ├─ Axios │ │ ├─ interceptorMan. js # ├─ sole.js # │ ├─ ├─ helpers # change the state │ ├─ Helpers # change the state │ ├─ axios.js # ├─ ├─ ├─ ├─ ├─ download. json # ├─ ├─ TypeScript declaration Index.js # entry fileCopy the code

Let me give you an example

Call the specified method before the asynchronous request, and call the specified method after the request to “filter”

function myAxios(config){
    this.config = config;
    this.before = [];
    this.after = [];
}
myAxios.prototype.request = function(config){
    return new Promise(resolve= > {
        setTimeout(() = > {
            resolve(config)
        },3000)})}Copy the code

This is a simple constructor myAxios with a request method on the prototype chain. If a method is present in the before array, execute it first. If a method is present in the after array, execute after the request method. The answer is the Promise chain call. Students who are not familiar with Promise suggest reading Ruan’s INTRODUCTION to ES6 first. Next, let’s modify the request method

function myAxios(config){
    this.config = config;
    this.before = [];
    this.after = [];
}
myAxios.prototype.addBeforeFn = function(fn){
    this.before.push(fn)
}
myAxios.prototype.addAfterFn = function(fn){
    this.after.push(fn)
}
function dispatchRequest(config){
    // Send the real request
    return new Promise(resolve= > {
        console.log('Send a truly asynchronous request')
        setTimeout(() = > {
            resolve(config)
        },3000)
    })
}
myAxios.prototype.request = function(){
    let promise = Promise.resolve(this.config);
    let chain = [dispatchRequest];
    if(this.before.length) chain.unshift(this.before[0])
    if(this.after.length) chain.push(this.after[0])
    for(let i=0; i<chain.length; i++){ promise = promise.then(chain[i]) }return promise
}
Copy the code

This is the modified method: added methods that add attributes to the instance attribute before or after (note: added methods must return). An array of chain properties is declared in the request method on the prototype chain, and dispatchRequest is a truly asynchronous request. Interception methods are put before and after the dispatchRequest attribute in the chain array, so that promises can be followed in sequence

var instance = new myAxios({a:1});
instance.addBeforeFn((config) = > {
    console.log('Before the actual request')
    return config
})
instance.addAfterFn((config) = > {
    console.log('After the actual request')
    return config
})
instance.request().then(res= > {
    console.log(res)
})
Copy the code

The output is as follows:

// Before the actual request
// Send a truly asynchronous request
// After the actual request
// {a:1}
Copy the code

This allows interception of input or output before and after the actual request. Of course, the above demo has a lot of flaws in practice, but I’ll use this example before going through the axios source code to help you understand how interceptors work

Tools and Methods

Before diving into the axios source code, it’s worth examining the main utility methods in util.js to make it easier to understand the source code. 1. Function.prototype.bind

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 (obj, fn) executes the given array or object with the given method

function forEach(obj, fn) {
  // omit some boundary code
  if (isArray(obj)) {
    // How to handle an array
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj); }}else {
    // How objects are handled
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj); }}}}Copy the code

Merge (/* obj1, obj2, obj3,… */) Deeply merges multiple objects into a new object

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

Consider: What if we have to merge multiple objects into one new object at work? 3, extend(a, b, thisArg) extends methods or properties from b objects to A, specifying the context

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

Comment: The above three methods, although seem very simple, but very interesting, worth our manual implementation experience, I believe will understand more deeply.

Entrance to the filelib/axios.js

Introduce utility function util, bind function, default configuration defaults, constructor Axios, mergeConfig, etc

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

Generate instance object method axios

function createInstance(defaultConfig) {
  // Generate an instance object context containing the properties Defaults and interceptors.
  var context = new Axios(defaultConfig);
  The //bind method is covered in the tools section
  // All requests go through the axios.prototype. request method
  // The binding returns a new Function specifying that this points to context, which is why axios(config) can be used
  var instance = bind(Axios.prototype.request, context);
  // Copy the axios. prototype method to the instance and bind this to the context
  // This is why you can use axios.post, axios.get, etc
  utils.extend(instance, Axios.prototype, context);
  // Copy the context properties to instance
  // This is why axios.defaults and axios.interceptors are used in practice
  utils.extend(instance, context);
  // Return the instance object (actually a method)
  return instance;
}
var axios = createInstance(defaults);
Copy the code

Expose other methods such as canceling the API

// Expose the core library
axios.Axios = Axios;
// Expose the factory pattern to create instances to meet personalized user needs
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
//* * * *//

Export Cancel and CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
// The Promise. All method
axios.all = function all(promises) {
  return Promise.all(promises);
};
/ / export spread
axios.spread = require('./helpers/spread');
// Export error catch isAxiosError
axios.isAxiosError = require('./helpers/isAxiosError');
Copy the code

axiosWhat type and exposed methods and attributes

Axios is a method you can call from axios(config)

Object.prototype.toString.call(axios)
// [object Function]
Copy the code

Test the methods and properties exposed by Axios itself

console.log(Object.keys(axios))
//['request', 'getUri', 'delete', 'get', 'head', 'options', 'post', 'put', 'patch', 'defaults', 'interceptors', 'Axios', 'create', 'Cancel', 'CancelToken', 'isCancel', 'all', 'spread', 'default']
Copy the code

The core librarylib/Axios.js

1. Introduce tool methods, interceptors, methods for actually sending requests, etc

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

2. The core constructor Axios(InterceptorManager, which we’ll look at later)

function Axios(instanceConfig) {
  The defaults argument saves the default configuration
  this.defaults = instanceConfig;
  // Create request interceptors and corresponding interceptors
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
Copy the code

3. Core request method. Whether the request is made using axios, axios.post, or axios.get, the actual request method ends up being the request method on the prototype chain.

Summary: The real request of the request method is actually the real request interface of dispatchRequest, some other operations are actually for the interception of the request before and after, the application principle is actually the chain call of promise.then (). This will be easier to understand if you’ve seen the previous examples.

Axios.prototype.request = function request(config) {
  // Ignore some bounds, exception handling code
    
  // Create an array that holds the promise callback methods in pairs (one for success and one for failure, which is why the second argument is set to undefined when the array is initialized)
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);
  // Add the request interception method to the front of the array chain
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  // Add the response interceptor method to the end of the array chain
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  // Iterate through the numbers to form the Promise chain format
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
};
Copy the code

The request method ends up returning a Promise chained call in something like the following format

// The return format is approximately
  Promise.resolve(config).then(config= > {
    // Interception before request
  }).then(config= > {
    // True request
  }).then(config= > {
    // The request is intercepted accordingly
  })
Copy the code

The function that gets the URL request.

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

Add alias methods delete, GET, Head, options, POST, PUT, Patch through the Axios prototype chain. In the instance generation methods, the Axios prototype chain methods are copied as Axios property methods, so you can request them using axios.post, axios.get, and so on

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
    }));
  };
});
Copy the code

Interceptor constructorlib/cor/InterceptorManager.js

1, how to use? Interceptors are divided into pre-request interceptors and post-request interceptors

// Prerequest interceptor usage method
axios.interceptors.request.use(config= > {
  return config
},err= > {
  Promise.reject(err)
})

// Post-request interceptor
axios.interceptors.response.use(response= > {
  return response
},err= >{})Copy the code

2. Source code analysis

// Add an array of interceptor operations to save interceptor functions
function InterceptorManager() {
  this.handlers = [];
}
// Add success or failure functions (in pairs)
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};
// Remove the interceptor method
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null; }};// Iterate over all interceptors, passing a function call. If null, no traversal is performed
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if(h ! = =null) { fn(h); }}); };Copy the code

Generally speaking, add function use is often used, the latter two use less, understand the principle can be.

The default configurationlib/defaults.js

1. Adapters. In the design mode is called the adapter mode, with voltage converter as an example, often global business people know that the voltage of each country is different, and our electrical appliances need voltage is 220V, with the converter, just use it, as for the input how much V, do not need to care

// If there is no ContentType, set the default value
function setContentTypeIfUnset(headers, value) {
    if(! utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
        headers['Content-Type'] = value; }}// Request the adapter
// This explains why AXIos supports both browsers and nodeJS environments
// In a browser environment, use xhr.js; otherwise, in a nodejs environment, use http.js
function getDefaultAdapter() {
    var adapter;
    if (typeofXMLHttpRequest ! = ='undefined') {
        // For browsers use XHR adapter
        adapter = require('./adapters/xhr');
    } else if (typeofprocess ! = ='undefined' && Object.prototype.toString.call(process) === '[object process]') {
        // For node use HTTP adapter
        adapter = require('./adapters/http');
    }
    return adapter;
}
Copy the code

2. Other default configurations

var defaults = {
    / / adapter
    adapter: getDefaultAdapter(),
    // Request converter
    transformRequest: [function transformRequest(data, headers) {}].// Post-request data converter
    transformResponse: [function transformResponse(data) {}].// The default timeout is 0, which means no timeout
    timeout: 0.xsrfCookieName: 'XSRF-TOKEN'.xsrfHeaderName: 'X-XSRF-TOKEN'.maxContentLength: -1.maxBodyLength: -1.validateStatus: function validateStatus(status) {
        return status >= 200 && status < 300; }};Copy the code

Sending request methoddispatchRequest

module.exports = function dispatchRequest(config) {
  // If cancelToken exists in config, throw an error, which makes the Promise go wrong
  throwIfCancellationRequested(config);

  // Make sure that headers exist
  config.headers = config.headers || {};

  // Convert request parameters
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  / / flat
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers
  );
  // Delete some useless headers
  utils.forEach(
    ['delete'.'get'.'head'.'post'.'put'.'patch'.'common'].function cleanHeaderConfig(method) {
      deleteconfig.headers[method]; });// The configured adapter is preferred, if not, the default adapter is used
  // As a reminder, if you don't want to use the default request, you can configure it yourself
  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    // Cancel the request by returning promise.reject ()
    throwIfCancellationRequested(config);

    // Convert the request result
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    if(! isCancel(reason)) {// Cancel correlation
      throwIfCancellationRequested(config);
      // Convert the response data
      if(reason && reason.response) { reason.response.data = transformData( reason.response.data, reason.response.headers, config.transformResponse ); }}return Promise.reject(reason);
  });
};
Copy the code

To summarize what dispatchRequest does: in addition to some boundary handling, it does not require API operations to actually convert data before and after the request

Browser nativeXMLHttpRequestmethods

Location: lib/adapters/XHR. Js. In the browser, any real request is going to rely on and call the native XMLHttpRequest method to execute it, and axiOS is essentially just wrapping XMLHttpRequest around the request, doing a little bit of boundary handling, and a little bit of a better development experience, so that you don’t have to worry about what the underlying request is, Just configure the request parameters and address and wait for the result.

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // Ignore some boundary handling
    var request = new XMLHttpRequest();
    // Get the request full connection
    var fullPath = buildFullPath(config.baseURL, config.url);
    // Configure the request
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    // Set the timeout period
    request.timeout = config.timeout;
    // Listen for request status
    request.onreadystatechange = function handleLoad() {};// Handle browser requests for cancellation (as opposed to manual cancellation)
    request.onabort = function handleAbort() {};// Catch an error generated during the request
    request.onerror = function handleError() {};// Timeout request
    request.ontimeout = function handleTimeout() {};// Only in standard browsers will possible XSRF headers be added. In standard browsers, you can go to util.js to see the isStandardBrowserEnv method, which is relatively simple and won't be described here
    if (utils.isStandardBrowserEnv()) {
      // Add the XSRF header
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;
      if(xsrfValue) { requestHeaders[config.xsrfHeaderName] = xsrfValue; }}// Whether cross-site cookies are allowed
    if(! utils.isUndefined(config.withCredentials)) { request.withCredentials = !! config.withCredentials; }// If config.responseType is set to true, then responseType is added to the request
    if (config.responseType) {
      try {
        request.responseType = config.responseType;
      } catch (e) {
        
      }
    }
    // If onDownloadProgress is configured in advance, the process progress is monitored
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
    }

    // Not all browsers support upload events
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
    }
    // Handle cancellation request related
    if (config.cancelToken) {
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if(! request) {return;
        }
        request.abort();
        reject(cancel);
        request = null;
      });
    }

    if(! requestData) { requestData =null;
    }
    // Send the request
    request.send(requestData);
  });
};
Copy the code

How to use the cancel API and source code analysis

1. The idea behind canceling the request is to make the Promise throw an error to block the chain call. Cancel example:

const source = axios.CancelToken.source();
axios.post('/yourServerAddress', {
  cancelToken: source.token
}).catch(err= > {
  if (axios.isCancel(err)) {
    console.log('Request cancelled because:', err.message);
  }else{}});// Cancel the function
source.cancel('For some reason, the request was cancelled');

/ / * or cancel (personally recommend the latter wording, it seems intuitive, and more as a whole, easy to read) * / /
axios.post('/yourServerAddress', {cancelToken:new axios.CancelToken(cancel= > {
    if(/* for some reason */) {// Cancel the request}})})Copy the code

2, cancel the request source code analysis, location: lib/cancel/CancelToken js, key code:

function CancelToken(executor) {
  / /...
  // Ignore some boundary handling
  
  // Cancel key points: When cancelToken is found, abort() native XMLHttpRequest, and the current promise.reject, interrupts the chain call
  
  // delete related code in xhr.js
  if (config.cancelToken) {
    config.cancelToken.promise.then(function onCanceled(cancel) {
      request.abort();
      reject(cancel);
      request = null;
    });
  }
    
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      return;
    }
    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

/ /... Ignore some of the less important code

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

conclusion

At this point, the basic analysis of axios source code is complete. In my opinion, there are several points worth learning and their ideas should be used for reference in our project. 1. Tool methods forEach, Merge, extend, just a few lines of code, very clever; 2. Copy merge exposes prototype chain method when generating AXIOS instance; 3, Axios.prototype.request before and after request interception using Promise chain call; 4, native method XMLHttpRequest to request processing and a series of operations; 5. Cancel the API implementation