preface

Axios’s interceptors can be used in many scenarios. Take the project I am responsible for for example, the functions that need to be implemented through interceptors are as follows:

  1. When the request to return the login permission expires, the website switches to the login page
  2. Handle data race issues
  3. When the request fails, the failure prompt and failure reason are presented in a unified UI and UR and copywriting format
  4. The response data is processed in a unified data structure and then returned to the business layer

Each function requires many lines of code to implement. If we were to write all of these functions in a single interceptor function, we would end up with too much file code for maintenance. I recently solved this problem by reading the source code of Axios.

Source code analysis

The source code involved in interceptors is shown and analyzed below:

lib\axios.js

This is the main file of Axios, which we import from node_modules either by require or import.

'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 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);
  // Create an instance that is actually the axios.prototype.request method in which the context above is executed
  // Axios.prototype.request is the method to make the request
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  // the utils.extend method: copies the property of the second argument to the first argument. If the value of the copied property is of type function, the context of the value is set to the third argument
  // Copy the axios.prototype value and the method bound to context into 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);

// Expose Axios class to allow class inheritance
axios.Axios = Axios;

// Factory for creating new instances
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

module.exports = axios;

// Allow use of default import syntax in TypeScript
module.exports.default = axios;
Copy the code

As you can see above, we call the Axios.prototype.request method whether we make a request through the axios(config) or the axios(url[, config]) method. Next, look at the source code that defines the Axios class.

lib\core\Axios.js

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

/**
 * Create a new instance of Axios
 *
 * @param {Object} InstanceConfig The default config for The instance * The initialization process of an Axios class is to declare The defaults that hold The configuration and The objects that hold The requests and The corresponding interceptors */
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

Axios.prototype.request = function request(config) {
	//....
    // This code will be analyzed next
}

module.exports = Axios;
Copy the code

As you can see above, the axios.interceptors. Use method we use to register interceptors is an internal method of the InterceptorManager class. Next, look at the source code that defines the InterceptorManager class.

lib\core\InterceptorManager.js

'use strict';

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

function InterceptorManager() {
  // Define an array of type handlers to store interceptors
  this.handlers = [];
}

/**
 * Add a new interceptor to the stack
 *
 * @param {Function} fulfilled The function to handle `then` for a `Promise`
 * @param {Function} rejected The function to handle `reject` for a `Promise`
 *
 * @return {Number} An ID used to remove interceptor later
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

/**
 * Remove an interceptor from the stack
 *
 * @param {Number} id The ID that was returned by `use`
 */
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null; }};/**
 * Iterate over all the registered interceptors
 *
 * This method is particularly useful for skipping over any
 * interceptors that may have become `null` calling `eject`.
 *
 * @param {Function} fn The function to call for each interceptor
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
 ForEach ((arr[index],index,arr)=>fn(arr[index],index,arr))
 / / here InterceptorManager. Prototype. ForEach used to traverse this. Handlers. Equivalent to this. Handlers. ForEach (h = > fn (h))
  utils.forEach(this.handlers, function forEachHandler(h) {
    if(h ! = =null) { fn(h); }}); };module.exports = InterceptorManager;
Copy the code

From the above, we can register multiple interceptors with axios.interceptor.use. This method returns the ID of the interceptor you registered. This is used later when you want to override the interceptor, via axios.Interceptor.eject (id). It is not yet known what effect the registered interceptors have on each other’s requests and responses, so let’s move on to the Axios.prototype.request method.

lib\core\Axios.js

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

/**
 * Create a new instance of Axios
 *
 * @param {Object} InstanceConfig The default config for The instance * The initialization process of an Axios class is to declare The defaults that hold The configuration and The objects that hold The requests and The corresponding interceptors */
function Axios(instanceConfig) {
  / /...
}

/**
 * Dispatch a request
 *
 * @param {Object} config The config specific for this request (merged with this.defaults)
 */
Axios.prototype.request = function request(config) {
  // ...

  // Hook up interceptors middleware
  // dispatchRequest is the method of sending the request
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // Insert the request interceptor into the chain header
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  // Insert the corresponding interceptor at the end of the chain
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  // Loop through the chain, each time taking two elements and putting them in the RESOLVE and reject positions of the then
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

// Provide aliases for supported request methods
// We know that axios.delete and other methods are implemented by calling array.prototype. request and passing in the config parameter
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
    }));
  };
});

Get is implemented by calling array.prototype. request and passing in the config configuration parameter
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
    }));
  };
});

module.exports = Axios;
Copy the code

As you can see from the above, the registered request and response interceptors are placed on a PROMISE chain for execution.

Here’s a summary in the form of a picture:

Suppose we register A and B as the success and failure interceptors in the request, and C and D as the corresponding success and failure interceptors. As shown below will be placed in the correspondinghandlersIn the.

The array.prototype. request method is run when a request is made by calling Axios or Axios.request. In this case, the chain defined in array.prototype. request will change as follows:

Eventually the elements in the chain are both taken out and placed in the promise chain, and the promise chain is returned.

Register multiple interceptors

There are two types of interceptors, either request.interceptors or Response. interceptors. At this point, the basic idea is to split the code in one interceptor into multiple interceptors based on different requirements. For example, the request success interceptor in section A of the previous chapter is split into A1 and A2, and the response success interceptor in section C is split into C1 and C2. Then we can do this:

const axios=require('axios')

/ / registered a1, B
axios.interceptors.request.use(
    function a1(config){
        console.log('a1')
        return config
    },
    function B(err){
        console.log('B')
        return Promise.reject(err)
    }
)
A2 / / registration
axios.interceptors.request.use(
    function a2(config){
        console.log('a2')
        return config
    },
    undefined
)
/ / registered c1, D
axios.interceptors.response.use(
    function c1(response){
        console.log('c1')
        return response
    },
    function D(err){
        console.log('D')
        return Promise.reject(err)
    }
)
C2 / / registration
axios.interceptors.response.use(
    function c2(response){
        console.log('c2')
        return response
    },
    undefined
)

axios.head('https://www.baidu.com')
Copy the code

The above code will eventually be output successively:

a2
a1
c1
c2
Copy the code

Note that because is inpromiseThe code executed on the chain for each request success interceptor and for each response to the Chen interceptor must be separatereturn configandreturn responseAt the end. Ensure that the next interceptor receives the passed parameter.

Similarly, the code for each request failure interceptor and response failure interceptor is in thereturn Promise.reject(err)At the end. To ensure thaterrCan be passed to the next interceptor while ensuring that the next interceptor is either a request failure interceptor or a response failure interceptor. If you are in a response failure interceptor code withreturn errIs passed to the next response success interceptor for execution.

Extension: How do I optimize in my own projects

My directory structure:

The js file stored in modules contains the interceptor for the corresponding business. Each interceptor has the same name:

  • Request successful interceptor:requestSuccessHandler
  • Request failure interceptor:requestFailHandler
  • Response successful interceptor:responseSuccessHandler
  • Response failure interceptor:responseFailHandler

All interceptors are exported if they are defined.

handlers\index.js

import axios from 'axios'
import { AXIOS_DEFAULT_CONFIG } from 'Config/index'
import baseHandler from './modules/base.js' // Extract the base.js file separately

// Scan to import files in modules except base.js
const files = require.context('./modules'.false./ (? 
      )
const handlers = files.keys().reduce((cur, key) = > {
  cur.push(files(key).default)
  return cur
}, [])

// AXIOS_DEFAULT_CONFIG Creates an AXIOS instance with default parameters
const axiosInstance = axios.create(AXIOS_DEFAULT_CONFIG)
// Put the exported interceptors in base.js at the end, and then iterate through the registered interceptors
const handlerList = [...handlers, baseHandler]
handlerList.forEach(handler= > {
  const { requestSuccessHandler, requestFailHandler, responseSuccessHandler, responseFailHandler } = handler
  if (requestSuccessHandler || requestFailHandler) {
    axiosInstance.interceptors.request.use(requestSuccessHandler, requestFailHandler)
  }
  if (responseSuccessHandler || responseFailHandler) {
    axiosInstance.interceptors.response.use(responseSuccessHandler, responseFailHandler)
  }
})
export default axiosInstance
Copy the code

Why put base.js last? Look at the code I wrote inside base.js:

const responseSuccessHandler = function (reponse) {
  return Promise.resolve({ err: null.res: reponse.data })
}

const responseFailHandler = function (err) {
  return Promise.resolve({ err, res: null})}export default {
  responseSuccessHandler,
  responseFailHandler
}
Copy the code

Because base.js is used to process the returned data back to the business layer in a uniform format. So make sure that the responseSuccessHandler and responseFailHandler in base.js are executed last. To ensure that the result is returned as an object in the {err,res} format. To ensure that the business layer code can accept the request result in the following code format:

const {err,res}=await axios(config)
Copy the code

Afterword.

The benefit of splitting interceptors into multiple files is easy maintenance. Improves readability while reducing merge conflicts caused by multi-player collaboration. I will write more articles about front-end project innovation later.