The introduction

Last time we looked at the basic examples and configuration of Axios, this time we’ll focus on its core request logic and cancel request logic

Axios source code analysis (a) — Axios instance and configuration

axios.request

Now let’s take a look at the core logic of making a request. This logic consists of three parts, and we’ll work our way down

request

// lib/core/Axios.js
var dispatchRequest = require('./dispatchRequest');

Axios.prototype.request = function request(config) {
  if (typeof config === 'string') {
    config = arguments[1) | | {}; config.url =arguments[0];
  } else {
    config = config || {};
  }

  config = mergeConfig(this.defaults, config);

  // Set config.method
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }

  // Hook up interceptors middleware
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

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

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};
Copy the code

This code isn’t that hard to understand, so let’s break it down

  • if (typeof config === 'string') {
      config = arguments[1) | | {}; config.url =arguments[0];
    } else {
      config = config || {};
    }
    
    config = mergeConfig(this.defaults, config);
    
    if (config.method) {
      config.method = config.method.toLowerCase();
    } else if (this.defaults.method) {
      config.method = this.defaults.method.toLowerCase();
    } else {
      config.method = 'get';
    }
    Copy the code

    If config is a string, it is considered as the URL attribute of the config object. Finally, the config object is used to ensure the consistency of the data structure. Then, the default configuration of this instance is merged, and the method attribute is guaranteed. At least, is the ‘get’

    Method is not specified in the default configuration, so you must assign a value to method so it cannot be null.

  • var chain = [dispatchRequest, undefined];
    var promise = Promise.resolve(config);
    
    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);
    });
    
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift());
    }
    
    return promise;
    Copy the code

    DispatchRequest is the abstract method that actually sends the request, which we’ll explain in detail below, while interceptors.request and interceptors.response are the request and response interceptors. For then and catch, each method returns a Promise, puts it in the chain array, and then loops through the array to generate a Promise call chain. As long as there is an exception thrown in the middle, it will go to the nearest catch. If the catch returns a Promise of a resolve state, then the call chain can continue down

Request is added to the InterceptorManager class in order. The first interceptor to be added will be called last. This is actually the case. If multiple request interceptor rules are added and the order is required, the first interceptor will be called last. Always write backwards, otherwise you won’t get the result you want

dispatchRequest

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

  // Ensure headers exist
  config.headers = config.headers || {};

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

  // Flatten headers
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers
  );

  utils.forEach(
    ['delete'.'get'.'head'.'post'.'put'.'patch'.'common'].function cleanHeaderConfig(method) {
      deleteconfig.headers[method]; });var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    // Transform response data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    if(! isCancel(reason)) { throwIfCancellationRequested(config);// Transform 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

There’s also the code for Cancel, so let’s skip over it and focus on the request part

  • config.data = transformData(
      config.data,
      config.headers,
      config.transformRequest
    );
    Copy the code

    The config.transformRequest method in configuration is applied to config.data using the transformData method

  • config.headers = utils.merge(
      config.headers.common || {},
      config.headers[config.method] || {},
      config.headers
    );
    
    utils.forEach(
      ['delete'.'get'.'head'.'post'.'put'.'patch'.'common'].function cleanHeaderConfig(method) {
        deleteconfig.headers[method]; });Copy the code

    This section merges headers from different sources, and you can clearly see the source in the code, This includes config.headers.com common configuration, config.headers[config.method] configuration (i.e., config.headers. Get/config.headers. Post, etc…..) The merge method is a deep copy, so delete all of the common, get, and POST configurations without affecting the original object

  • return adapter(config).then(function onAdapterResolution(response) {
      throwIfCancellationRequested(config);
    
      // Transform response data
      response.data = transformData(
        response.data,
        response.headers,
        config.transformResponse
      );
    
      return response;
    }, function onAdapterRejection(reason) {
      if(! isCancel(reason)) { throwIfCancellationRequested(config);// Transform 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

    The last part is also easy to understand, calling the adapter, passing config, and then processing the then, or catch, step, where the config.transformResponse method is applied to the result

adapter

The final process of sending the request will be implemented in different adapters. Personally, I don’t use Node very much. I’ll take a look at the browser adapter, which is lib/ Adapters /xhr.js

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

    // Delete content-type for formData
    if(utils.isFormData(requestData)) {... }var request = new XMLHttpRequest();

    // Set header Authorization
    if(config.auth) {... }var fullPath = buildFullPath(config.baseURL, config.url);
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

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

    // Listen for XHR events
    request.onreadystatechange = function handleLoad() {... }; request.onabort =function handleAbort() {... }; request.onerror =function handleError() {... }; request.ontimeout =function handleTimeout() {... };// Against XSRF/CSRF attacks, you can configure cookies to be placed in headers
    if(utils.isStandardBrowserEnv()) {... }// Set the other config.headers to the header
    if ('setRequestHeader' inrequest) {... }// Set the withCredentials attribute
    if(! utils.isUndefined(config.withCredentials)) {... }/ / set the responseType
    if(config.responseType) {... }// Set the upload progress
    if (typeof config.onDownloadProgress === 'function') {... }if (typeof config.onUploadProgress === 'function'&& request.upload) {... }// Cancel the requested action
    if(config.cancelToken) {... }// Request body
    if(! requestData) { requestData =null;
    }

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

Here I have omitted most of the code, so that the whole adaptation process is more clear, I have marked the basic process in the above code comments, let’s take a step by step

  • if (utils.isFormData(requestData)) {
      delete requestHeaders['Content-Type']; // Let the browser set it
    }
    Copy the code

    If it’s FormData, remove the content-Type and let the browser set it, which is usually multipart/form-data

  • if (config.auth) {
      var username = config.auth.username || ' ';
      var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : ' ';
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }
    Copy the code

    This section provides a quick way to set HTTP authentication. The Authorization field is added to the request header based on the user name and password. The value is the user name and password encoded in Basic and Base64

  • var fullPath = buildFullPath(config.baseURL, config.url);
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    Copy the code

    It initializes a request, including the request method, the request address, whether it is asynchronous, etc. The request method is all lowercase, and the request address is spelled with the parameters and address, as in: XXX? a=b&c=d

  • request.onreadystatechange = function handleLoad() {
      if(! request || request.readyState ! = =4) {
        return;
      }
    
      // The request errored out and we didn't get a response, this will be
      // handled by onerror instead
      // With one exception: request that using file: protocol, most browsers
      // will return status as 0 even though it's a successful request
      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') = = =0)) {
        return;
      }
    
      // Prepare the response
      var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
      varresponseData = ! config.responseType || config.responseType ==='text' ? request.responseText : request.response;
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };
    
      settle(resolve, reject, response);
    
      // Clean up request
      request = null;
    };
    Copy the code
    module.exports = function settle(resolve, reject, response) {
      var validateStatus = response.config.validateStatus;
      if(! response.status || ! validateStatus || validateStatus(response.status)) { resolve(response); }else {
        reject(createError(
          'Request failed with status code ' + response.status,
          response.config,
          null, response.request, response )); }};Copy the code

    We’ll focus on onreadyStatechange, which is the processing of the request response, and we’ll notice that there’s an important comment in the status statement, which says: If the request has an error, it is handled by the onError handler, with the exception of file: Most browsers will return status: 0, even if the request was successful. As we know, the HTTP status code 200 indicates success, so this code makes a special determination

    The rest is handled as normal, passing resolve and reject to settle methods, where the judgment logic can define its own validateStatus method

    Finally, there are a few additional processes that don’t affect the main flow, but are worth looking at

  • if (utils.isStandardBrowserEnv()) {
      // Add xsrf header
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;
    
      if(xsrfValue) { requestHeaders[config.xsrfHeaderName] = xsrfValue; }}Copy the code

    If you have xsrfHeaderName and xsrfCookieName configured, the cookie values are automatically read and carried to the header when the request is made

  • var requestHeaders = config.headers;
    if ('setRequestHeader' in request) {
      utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
          // Remove Content-Type if data is undefined
          delete requestHeaders[key];
        } else {
          // Otherwise add header to the requestrequest.setRequestHeader(key, val); }}); }Copy the code

    If the headers attribute is set, it goes to the request header. Note that the content-Type header is dropped if the request body has no data

Finally send ajax request, if there is no requestData, send(NULL)

Cancel a request

In the previous code, we skipped the cancellation request processing logic, because it has little to do with the logic of the topic

usage

So let’s review, how does Cancel work, which most of you probably haven’t used

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error}})// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
Copy the code

Canceltoken.source () : canceltoken.source () : canceltoken.source () : canceltoken.source () : canceltoken.source () : canceltoken.source () The request can be cancelled at any time by calling the Cancel method

So, this source is the core of cancellation

Cancel core code

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

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

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

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

Resolution:

The canceltoken. source method, which generates a token from CancelToken and, while generating, CancelToken can pass a callback function that binds certain methods to the Cancel attribute

Moving on to the CancelToken factory, when called through the new operator, the instance itself is returned, that is, the token is an instance object of the CancelToken

During initialization, the token object has two attributes: Promise and Reason

First bind the resolvePromise variable to the resolve method of a promise property, and then pass a cancel method (canceltoken.source ().cancel) into the callback. When cancel is called, the resolve Promise will be used. The reason attribute of the token object may also have a value, which means that the token object has been cancelled

Token objects also have a throwIfRequested method, which checks whether there is a reason value to throw an exception, so that the request directly goes to the catch phase

Lib /cancel/ cancel. js and lib/cancel/ iscancel. js files are used to determine whether an instance has been cancelled

Cancel is used in the request

In the process of analyzing the sending of the request before, the logic of Cancel appeared, and we completed this part together

// lib/axios.js
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
Copy the code

Here is mainly the CancelToken factory that produces the source

// lib/core/dispatchRequest.js
function throwIfCancellationRequested(config) {
  if(config.cancelToken) { config.cancelToken.throwIfRequested(); }}function dispatchRequest(config) { throwIfCancellationRequested(config); .return adapter(config).then(function onAdapterResolution(response) { throwIfCancellationRequested(config); . },function onAdapterRejection(reason) {
    if(! isCancel(reason)) { throwIfCancellationRequested(config); . }return Promise.reject(reason);
  });
}
Copy the code

Here, in the initialization, success and failure of the request, throwIfRequested is used to determine whether the user has called the cancel method. Once it is called, an exception will be thrown directly and the process of catch will not be processed regardless of the status of the request

The throwIfRequested method will throw exceptions only if the token has a reason attribute. When the cancel method is not called, the token will never have a reason attribute, so throwIfRequested can be called at any time. Without worrying about whether the request is cancelled

// lib/adapters/xhr.js 
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

In the adapter, when you initialize the Ajax object, you bind a promise. Remember that the Token object has a Promise property, which is used here. When the promise is resolved, the user calls the cancel method, This gets the Ajax object through the closure and calls Request.Abort (), which in the XHR process also interrupts the request and sets the entire adapter state to reject

At this point, cancel participates in all phases of the request, and at any time you cancel, you go straight into the Reject process

conclusion

That’s the core of Axios. The logic isn’t that hard to understand. It’s about packaging ideas and some design patterns that you can learn from. If you have new ideas, you can also go to GitHub directly to raise requirements. I hope you don’t be afraid to look at the source code. After all, the source code is written more beautiful than the project we took over, isn’t it?