Do you understand the principle of Axios? Have you seen its source code?

Posted by shinichi_nguyen on Mon, 25 Oct 2021 06:46:38 +0200

1, Use of axios

The basic use of axios has been covered in the previous article. Here is a brief review:

Send request

import axios from 'axios';

axios(config) //  Direct incoming configuration
axios(url[, config]) //  Incoming url and configuration
axios[method](url[, option]) //  Directly call the request mode method, and pass in the url and configuration
axios[method](url[, data[, option]]) //  Directly call the request mode method and pass in data, url and configuration
axios.request(option) //  call   request   method

const axiosInstance = axios.create(config)
//  axiosInstance   Also have the above   axios   Ability of

axios.all([axiosInstance1, axiosInstance2]).then(axios.spread(response1, response2))
//  call   all   And incoming   spread   Callback

request interceptor

axios.interceptors.request.use(function (config) {
    //  Write the code for pre-processing of sending request here
    return config;
}, function (error) {
    //  Write the code related to sending request error here
    return Promise.reject(error);
});

Response interceptor

axios.interceptors.response.use(function (response) {
    //  Write the post-processing code of response data here
    return response;
}, function (error) {
    //  The code for error response processing is written here
    return Promise.reject(error);
});

Cancel request

//  Mode 1
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('xxxx', {
  cancelToken: source.token
})
//  Cancel request   (request reason is optional)
source.cancel('Active cancellation request');

//  Mode II
const CancelToken = axios.CancelToken;
let cancel;

axios.get('xxxx', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c;
  })
});
cancel('Active cancellation request');

2, Implement a simple version of axios

Build an Axios constructor, and the core code is request

class Axios {
    constructor() {

    }

    request(config) {
        return new Promise(resolve => {
            const {url = '', method = 'get', data = {}} = config;
            //  Send ajax request
            const xhr = new XMLHttpRequest();
            xhr.open(method, url, true);
            xhr.onload = function() {
                console.log(xhr.responseText)
                resolve(xhr.responseText);
            }
            xhr.send(data);
        })
    }
}

Export axios instance

//  Finally, the axios method is exported, that is, the request method of the instance
function CreateAxiosFn() {
    let axios = new Axios();
    let req = axios.request.bind(axios);
    return req;
}

//  Get the final global variable axios
let axios = CreateAxiosFn();

The above can already implement the request of axios({})

The following is to implement the axios.method() request

//  Define get,post... Methods and hang them on the Axios prototype
const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post'];
methodsArr.forEach(met => {
    Axios.prototype[met] = function() {
        console.log('implement'+met+'method');
        //  Processing a single method
        if (['get', 'delete', 'head', 'options'].includes(met)) { //  2 parameters (url[,   config])
            return this.request({
                method: met,
                url: arguments[0],
                ...arguments[1] || {}
            })
        } else { //  3 parameters (url[,data[,config]])
            return this.request({
                method: met,
                url: arguments[0],
                data: arguments[1] || {},
                ...arguments[2] || {}
            })
        }

    }
})

Move the methods on Axios.prototype to request

First, implement a tool class, mix the b method into a, and modify the point of this

const utils = {
  extend(a,b, context) {
    for(let key in b) {
      if (b.hasOwnProperty(key)) {
        if (typeof b[key] === 'function') {
          a[key] = b[key].bind(context);
        } else {
          a[key] = b[key]
        }
      }
      
    }
  }
}

Modify export method

function CreateAxiosFn() {
  let axios = new Axios();
  
  let req = axios.request.bind(axios);
  //  Add code
  utils.extend(req, Axios.prototype, axios)
  
  return req;
}

Constructor for building interceptors

class InterceptorsManage {
  constructor() {
    this.handlers = [];
  }

  use(fullfield, rejected) {
    this.handlers.push({
      fullfield,
      rejected
    })
  }
}

Implement axios.interceptors.response.use and axios.interceptors.request.use

class Axios {
    constructor() {
        //  New code
        this.interceptors = {
            request: new InterceptorsManage,
            response: new InterceptorsManage
        }
    }

    request(config) {
   ...
    }
}

When executing the statements axios.interceptors.response.use and axios.interceptors.request.use, the implementation obtains the interceptors object on the Axios instance, then obtains the response or request interceptor, and then executes the use method of the corresponding interceptor

Move the methods and properties on Axios to request

function CreateAxiosFn() {
  let axios = new Axios();
  
  let req = axios.request.bind(axios);
  //  Mixing method,   Handle the request method of axios and make it have get,post... Methods
  utils.extend(req, Axios.prototype, axios)
  //  New code
  utils.extend(req, axios)
  return req;
}

Now the request also has interceptors object. When sending the request, it will first obtain the handlers method of the request interceptor for execution

First, encapsulate the request to execute ajax into a method

request(config) {
    this.sendAjax(config)
}
sendAjax(config){
    return new Promise(resolve => {
        const {url = '', method = 'get', data = {}} = config;
        //  Send ajax request
        console.log(config);
        const xhr = new XMLHttpRequest();
        xhr.open(method, url, true);
        xhr.onload = function() {
            console.log(xhr.responseText)
            resolve(xhr.responseText);
        };
        xhr.send(data);
    })
}

Get callback in handlers

request(config) {
    //  Interceptor and request assembly queue
    let chain = [this.sendAjax.bind(this), undefined] //  Failed callbacks that appear in pairs will not be processed temporarily

    //  Request interception
    this.interceptors.request.handlers.forEach(interceptor => {
        chain.unshift(interceptor.fullfield, interceptor.rejected)
    })

    //  Response interception
    this.interceptors.response.handlers.forEach(interceptor => {
        chain.push(interceptor.fullfield, interceptor.rejected)
    })

    //  Execute the queue, one pair at a time, and assign the latest value to promise
    let promise = Promise.resolve(config);
    while(chain.length > 0) {
        promise = promise.then(chain.shift(), chain.shift())
    }
    return promise;
}

chains are in the form of ['fullilled1 ',' reject1 ',' fullilled2 ',' reject2 ',' this. Sendajax ',' undefined ',' fullilled2 ',' reject2 ',' fullilled1 ',' reject1 ']

In this way, a simple version of axios can be successfully implemented

3, Source code analysis

First look at the directory structure

There are many implementation methods for sending requests from Axios. The implementation entry file is axios.js

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);

  //  Instance points to the request method, and the context points to context, so you can directly   instance(option)   Mode call  
  //  Axios.prototype.request   The data type judgment of the first parameter enables us to   instance(url,   option)   Mode call
  var instance = bind(Axios.prototype.request, context);

  //  Extend the method on Axios.prototype to the instance object,
  //  And specify the context as context, so that this will point to the context when executing the methods on the Axios prototype chain
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  //  Extend its own attributes and methods on the context object to instance
  //  Note: when the forEach method used inside extend traverses the object for in, it only traverses the attributes of the object itself, not the attributes on the prototype chain
  //  In this way, instance has    defaults, interceptors properties.
  utils.extend(instance, context);
  return instance;
}

//  Create   the   default   instance   to   be   exported   Create an axios instance generated by the default configuration
var axios = createInstance(defaults);

//  Factory   for   creating   new   instances   Extend the axios.create factory function, as well as the internal function   createInstance
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// Expose all/spread
axios.all = function all(promises) {
  return Promise.all(promises);
};

axios.spread = function spread(callback) {
  return function wrap(arr) {
    return callback.apply(null, arr);
  };
};
module.exports = axios;

The main core is   Axios.prototype.request, the call implementation of various request modes is in   request   Internal implementation, a simple look   request   Logic of

Axios.prototype.request = function request(config) {
  // Allow for axios('example/url'[, config]) a la fetch API
  //  judge   config   Whether the parameter is   String, if yes, the first parameter is considered to be   URL, the second parameter is the real config
  if (typeof config === 'string') {
    config = arguments[1] || {};
    //  hold   url   Place to   config   Object for later   mergeConfig
    config.url = arguments[0];
  } else {
    //  If   config   Whether the parameter is   String, the whole is treated as config
    config = config || {};
  }
  //  Merge the default configuration with the incoming configuration
  config = mergeConfig(this.defaults, config);
  //  Set request method
  config.method = config.method ? config.method.toLowerCase() : 'get';
  /*
    something... This part will be described separately in the following interceptors
  */
};

//  stay   Axios   Mount on Prototype  ' delete',  ' get',  ' head',  ' options'   The request method without parameters is also implemented internally   request
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
    }));
  };
});

//  stay   Axios   Mount on Prototype  ' post',  ' put',  ' patch'   The request method of parameter transmission is also implemented internally   request
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
    }));
  };
});

The request entry parameter is config. It can be said that config implements the life of axios

axios   Medium   config is mainly distributed in these places:

  • Default configuration   defaults.js

  • config.method defaults to   get

  • call   createInstance   Method creation   axios instance, incoming config

  • Direct or indirect call   request   Method, passed in   config

// axios.js
//  Create an axios instance generated by the default configuration
var axios = createInstance(defaults);

//  Extend the axios.create factory function, as well as the internal function   createInstance
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// Axios.js
//  Merge the default configuration with the incoming configuration
config = mergeConfig(this.defaults, config);
//  Set request method
config.method = config.method ? config.method.toLowerCase() : 'get';

From the source code, you can see the priority: the default configuration object is default  <  method:get  <  Axios instance property this.default  <  request parameter

Let's focus on the request method

Axios.prototype.request = function request(config) {
  /*
    First   mergeConfig  ...  Wait, don't elaborate
  */
  //  Hook   up   interceptors   middleware   Create interceptor chain   dispatchRequest   It is the top priority and the focus of follow-up
  var chain = [dispatchRequest, undefined];

  // push each interceptor method   Note: interceptor.fully or interceptor.rejected may be undefined
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    //  Request interceptor reverse order   Note here   forEach   Is the foreach method of a custom interceptor
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    //  Response interceptor sequence   Note here   forEach   Is the foreach method of a custom interceptor
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  //  Initialize a promise object with the status of resolved, and the received parameters are the config object that has been processed and merged
  var promise = Promise.resolve(config);

  //  Chain of circular interceptors
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift()); //  Every time the interceptor is ejected outward
  }
  //  return   promise
  return promise;
};

interceptors are properties instantiated when building axios

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(), //  Request interception
    response: new InterceptorManager() //  Response interception
  };
}

InterceptorManager constructor

//  Interceptor initialization   It's actually a set of hook functions
function InterceptorManager() {
  this.handlers = [];
}

//  When you call the use of the interceptor instance, you push the method into the hook function
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

//  The interceptor can be canceled. Set an interceptor method to null according to the ID returned when using
//  out-of-service   splice   perhaps   slice   The reason is   After deletion   id   It will change, resulting in uncontrollable sequence or operation
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

//  This is where   In the request method of Axios   Method of interceptor in circulation   forEach   Loop execution hook function
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
}

The request interceptor method is   unshift to the interceptor, and the response interceptor is push ed to the interceptor. Finally, they will be spliced with a method called dispatchRequest, which will be followed up   promise   Sequential execution

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');

//  Judge whether the request has been canceled. If it has been canceled, throw the canceled
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

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

  //  If baseUrl is included,   And it is not the absolute path of config.url. Combine baseUrl and config.url
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    //  Combine baseURL and url to form a complete request path
    config.url = combineURLs(config.baseURL, config.url);
  }

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

  //  Use the transformRequest method in / lib/defaults.js to format config.headers and config.data
  //  For example, Accept and content type in headers are uniformly capitalized
  //  For example, if the request body is an Object, it will be formatted as a JSON string and add application / JSON; Content type of charset = UTF-8
  //  And a series of operations
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  //  Merge headers with different configurations. The configuration priority of config.headers is higher
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

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

  //  If adapter is configured in config, use the default request method of configuring adapter in config instead
  var adapter = config.adapter || defaults.adapter;

  //  Use the adapter method to initiate the request (the adapter varies according to the browser environment or Node environment)
  return adapter(config).then(
    //  Request a callback that returns correctly
    function onAdapterResolution(response) {
      //  Judge whether or not the request has been canceled. If the request has been canceled, throw it to cancel
      throwIfCancellationRequested(config);

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

      return response;
    },
    //  Request failed callback
    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);
    }
  );
};

Let's take a look at how axios implements the cancellation request. The implementation file is in CancelToken.js

function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }
  //  stay   CancelToken   Define a   pending   Stateful   promise  , take   resolve   The callback is assigned to an external variable   resolvePromise
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  //  Execute now   The passed in executor function passes the real cancel method through parameters.
  //  Once called, the resolvePromise is executed, that is, the resolve of the previous promise, and the state of the promise is changed to resolve.
  //  Then, as defined in xhr   The CancelToken.promise.then method will execute,   This xhr internally cancels the request
  executor(function cancel(message) {
    //  Judge whether the request has been cancelled to avoid multiple executions
    if (token.reason) {
      return;
    }
    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

CancelToken.source = function source() {
  //  source   Method returns a   CancelToken   Examples, and direct use   new   CancelToken   It's the same operation
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  //  Return created   CancelToken   Instance and cancellation method
  return {
    token: token,
    cancel: cancel
  };
};

In fact, the operation of canceling the request is   xhr.js   There is also a response in the cooperation

if (config.cancelToken) {
    config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
            return;
        }
        //  Cancel request
        request.abort();
        reject(cancel);
    });
}

The clever place is   In CancelToken   executor   Function to control the state of promise through the transfer and execution of resolve function

Summary

Topics: Vue