Axios is the most commonly used asynchronous request module in the front end. Knowing the source code of Axios helps us to configure Axios more reasonably in development and locate the exception when the request fails more quickly.

The directory structure

├ ─ ─ / dist /# project output directory├ ─ ─ / lib /# project source directory│ ├ ─ ─ / cancel /# Define the cancel function│ ├ ─ ─ / core /# Some core features│ │ ├ ─ ─ Axios. JsThe core main class of Axios│ │ ├ ─ ─ dispatchRequest. JsUse the HTTP request adapter method to send the request│ │ ├ ─ ─ InterceptorManager. JsInterceptor constructor│ │ └ ─ ─ settle. JsChange the state of the Promise based on the HTTP response state│ ├ ─ ─ / helpers# Some auxiliary methods│ ├ ─ ─ / adaptersDefine the requested adapter XHR, HTTP│ │ ├ ─ ─ HTTP. JsImplement the HTTP adapter│ │ └ ─ ─ XHR. JsImplement the XHR adapter│ ├ ─ ─ axios. Js# External exposure interface│ ├ ─ ─ defaults. Js# default configuration│ └ ─ ─ utils. Js# Utility├ ─ ─ package. Json# Project information├ ─ ─ the index, which sConfigure the TypeScript declaration file└ ─ ─ index. Js# import file
Copy the code

Syntactic sugar

Axios’ common API

import axios from 'axios';

const apiLogin = '/login';
const apiUser = '/user';
const headers = {'x-token': 'asdfasdf'};
const data = {username: ' '.pwd: ' '};

axios.defaults.timeout= 60 * 10000;
axios.interceptors.response.use(response= > {
  return response.data;
}, error= > {
  return Promise.reject(error);
})

// axios(options);
axios({ url: apiLogin, method: 'post', headers })

// axios(url[, options]);
axios(apiLogin, {mehtod: 'post', headers }

// axios[method](url[, options]) 
// Apply the methods get/delete/head/options
axios.get(apiUser, { headers })

// axios[method](url[, data[, options]]) 
// Put/POST /patch
axios.post(apiLogin, data, { headers })

axios.request({url: apiLogin, mehtod: 'post', headers })
Copy the code

Syntax sugar source code analysis

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  / / equivalent var instance = Axios. Prototype. Request. Bind (context);
  // Inttance is equivalent to context.request for invoking AXIos (options) or AXIos (URL, options)
  var instance = bind(Axios.prototype.request, context);

  // Extend all methods on the axios.prototype prototype to instance, with the method's context pointing to the context
  // Extend other methods for inttance to be called through axios.get(), axios.request(), axios.all(), etc
  utils.extend(instance, Axios.prototype, context);

  // Extend the properties defaults and Interceptors on context(instance of Axios) to instance
  / / in order to realize axios. Defaults. The timeout, axios. Interceptors. Request. Use () method call
  utils.extend(instance, context);

  return instance;
}
Copy the code

The class structure of Axios

Axios properties and stereotype methods

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

// Implement axios(config), axios(url, config) call method
Axios.prototype.request = function request(config) {
  // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1) | | {}; config.url =arguments[0];
  } else {
    config = config || {};
  }

  config = mergeConfig(this.defaults, config);

  var promise;
  / /... Omit the implementation
  return promise;
};

Axios.prototype.getUri = function getUri(config) {};

// implement the axios.get(url[, config]) call
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
    }));
  };
});

// implement the axios.post(url[, data[, config]]) call
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

The basic flow of the request

The interceptor

Classification of interceptors

Interceptors are request interceptors and response interceptors and are registered in the following way

  • Request interceptor: Used for modificationconfigConfigure data, useinterceptors.request.use()registered
  • Response interceptor: Used for modificationresponseResponse data, usinginterceptors.response.use()registered
// Request interceptor
axios.interceptors.request.use(
(config) = > { return config; }, 
(error) = > { return Promise.reject(error);}
);

// Response interceptor
axios.interceptors.response.use(
(response) = > { return response; }, 
(error) = > { return Promise.reject(error);}
);
Copy the code

How are interceptors handled in the source code?

Let’s look at the chain call for Promise

Axios’ promise chain execution mechanism without adding any interceptors

  1. Configure the data firstconfigPackaged as onepromiseObject to make it ownthenmethods
  2. Will performXMLHttpRequestThe method is packaged aspromiseSo that it can be called by link
  3. Executed through the then method, willconfigPassed to thedispatchRequest
  4. throughdispatchRequestOn,responsePassing out

XMLHttpRequest object document MDN

const config = {url: '/login'.data: {username: 'abc'}};
// Wrap config as a Promise object with promise.resolve
let promise = Promise.resolve(config);

Wrap the XHR request as a Promise
const dispatchRequest = config= > {
  return new Promise((resolve, reject) = > {
     // Execute the request...
     const response = {code: 0.data: {}};
     resolve(response)
  })
}

// Define a Promise queue
const chain = [dispatchRequest, undefined];

// Execute the Promise queue
promise = promise.then(chain.shift(), chain.shift());
// dispatchRequest = promise.then(dispatchRequest, undefined);
Then ((config) => dispatchRequest(config), undefined);
Copy the code

Interceptor processing

 Axios.prototype.request = function request(config) {
  / /... omit
  config = mergeConfig(this.defaults, config);
  / /... omit
  
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);
   
  // Add the request interceptor to the front of the chain
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
   
  // Add response interceptors to the end of the chain
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  
  /* chain = [ reqSuccessFn1, reqErrorFn1, reqSuccessFn2, reqErrorFn2, ... dispatchRequest, undefined, resSuccessFn1, resErrorFn1, resSuccessFn2, resErrorFn2, ... ] * / 
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};
Copy the code

promiseChain execution flow

  1. performRequest interceptorTo deal withconfig
  2. To deal with goodconfigPassed to thedispatchRequest
  3. performAn asynchronous request dispatchRequestAnd return apromisePass the request resultresponse
  4. performResponse interceptorTo deal withresponse
  5. returnresponseerror
configPromise
  // reqSuccessFn1, reqErrorFn1,
  .then(config= > {
    return reqSuccessFn1(config)
  }, () = > {
    return reqErrorFn1()
  })
  // reqSuccessFn2, reqErrorFn2,
  .then(config= > {
    return reqSuccessFn2(config)
  }, (err) = > {
    return reqErrorFn2(err)
  })
  // dispatchRequest, undefined,
    .then(config= > {
      return dispatchRequest(config)
    }, undefined)
  // resSuccessFn1, resErrorFn1,
  .then(response= > {
    return resSuccessFn1(response)
  }, (err) = > {
    return resErrorFn1(err)
  })
  // resSuccessFn2, resErrorFn2,
  .then(response= > {
    return resSuccessFn2(response)
  }, (err) = > {
    return resErrorFn1(err)
  })
  .then(response= > {
    console.log(response)
  })
  .catch(err= > {
    console.log(err)
  })
Copy the code

Timeout handling

XMLHttpRequest.timeout

  • axiosThere’s one in the configurationtimeoutThe default value is 0
  • timeoutIt’s going to be directly assigned toxhr.timeout
  • configYou can customize the message content of the timeoutconfig.timeoutErrorMessage
var xhr = new XMLHttpRequest();
xhr.open('GET'.'/server'.true);

xhr.timeout = 2000; // Timeout time, in milliseconds ms

xhr.onload = function () {
  // Request completed. Process it here.
};

// Listen for timeout events
xhr.ontimeout = function handleTimeout() {
  // Timeout message content
  var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
  // The timeout message can be customized in config
  if (config.timeoutErrorMessage) {
    timeoutErrorMessage = config.timeoutErrorMessage;
  }
  reject(createError(
    timeoutErrorMessage,
    config,
    config.transitional && config.transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED',
    request));

  // Clean up request
  request = null;
};

xhr.send(null);
Copy the code

Exception handling for timeout

axios.defaults.timeout=30*1000;

// Single request processing timed out
axios(url).catch(error= > {
  const { message } = error;
  if(error.message.includes('timeout')) {console.log('Data response timed out, please refresh and try again')}})// Global processing timed out
axios.interceptors.response.use(function (response) {
  return response;
}, function (error) {
 if(error.message.includes('timeout')) {console.log('Data response timeout')}return Promise.reject(error);
});
Copy the code

Cancel the request

The use of cancel requests in Axios

Cancel all requests

Request configuration By using the same cancelToken, you can cancel all requests at once

const CancelToken = axios.CancelToken;

const source = CancelToken.source();
axios
  .get('/cancel/server', {
    cancelToken: source.token
  })
  .then(function (response) {
    console.log(response.data)
  })
  .catch(function (err) {
    console.log(err)
  });

axios
  .get('/cancel/server', {cancelToken: source.token
  })
  .then(function (response) {
    console.log(response.data)
  })
  .catch(function (err) {
    console.log(err)
  });

  setTimeout(() = > {
    source.cancel('I cancelled all requests.');
  }, 6*1000)
Copy the code

Cancel a single request

In request configuration, different cancelTokens can be used to cancel different requests on demand

let cancel1;
let cancel2;

axios
.get('/cancel/server', {cancelToken: new CancelToken(function executor(c) {
    cancel1 = c;
  })
})
.then(function (response) {
  console.log(response.data)
})
.catch(function (err) {
  console.log(err)
});

axios
.get('/cancel/server', {cancelToken: new CancelToken(function executor(c) {
    cancel2 = c;
  })
})
.then(function (response) {
  console.log(response.data)
})
.catch(function (err) {
  console.log(err)
});

setTimeout(() = > {
  cancel1('I cancelled the request for one');
}, 3*1000)

setTimeout(() = > {
  cancel2('I cancelled the request for 2.');
}, 6*1000)
Copy the code

Cancels source code parsing of the request

CancelToken

Source path lib/cancel/CancelToken. Js

  • new CancelTokenOutput acancelTokenInstance, there’s one on the instancepromiseobject
  • new CancelTokenThe argument received is a function that executes, and willpromiseState intoresolved, thus triggeringpromiseRegistered in the chainthenmethods
  • CancelToken.source()Will return aCancelTokenThe instancetokenAnd a canceled functioncancel
function CancelToken(executor) {
  if (typeofexecutor ! = ='function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  // Generate a Promise property (a promise object) and assign the resolve method to the resolvePromise variable
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  // The argument to the received function is a cancelled exception message
  When this function executes, it changes the this.promise state to Resolved and passes the canceled message object to the registered THEN method
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }
    // Generate a cancelled message object and assign it to the Reason attribute
    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason; }};Canceltoken.source () static method returns {token instance, canceltoken.source}
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};
Copy the code

cancelTokenListen to and trigger

The file lib/adapters/XHR. Js

  • ifcancelTokenregisteredcancelWhen the function executes, thepromsieBecomes the state ofresolved
  • resolvedAfter that,thenThe callback registered with the method is triggered and calledxhr.abort()Execute to cancel the request
  • reject(cancel)willcancelThe message object passes out, and the external request passes throughcatchMethod receives a cancel message objectcancel
module.exports = function xhrAdapter(config) {
    return new Promise(function dispatchXhrRequest(resolve, reject) {
      // ...
      var request = new XMLHttpRequest();
      // ...
      / / 168 rows
      if (config.cancelToken) {
      If the request has not been returned, performing Cancel triggers the execution of the then callback here
        config.cancelToken.promise.then(function onCanceled(cancel) {
          if(! request) {return;
          }
          request.abort();
          // Cancel is the received cancellation message object
          reject(cancel);
          // Request instance is set to null
          request = null;
        });
      }
      // ...})}Copy the code

If the request has not been initiated or completed while cancel is executed, execution is interrupted and a cancellation message is thrown that is caught by the outer promise.catch

The files lib/core/dispatchRequest. Js

/ / 23
module.exports = function dispatchRequest(config) {
  // The request did not start
  throwIfCancellationRequested(config);
  // ...
  return adapter(config).then(function onAdapterResolution(response) {
    // Request completed, return success
    throwIfCancellationRequested(config);
   // ...
    return response;
  }, function onAdapterRejection(reason) {
    // Request completed, return exception
    if(! isCancel(reason)) { throwIfCancellationRequested(config);// ...
    }
    return Promise.reject(reason);
  });
};
Copy the code

ThrowIfCancellationRequested invokes cancelToken. ThrowIfRequested ()

// lib/core/dispatchRequest.js
function throwIfCancellationRequested(config) {
  if(config.cancelToken) { config.cancelToken.throwIfRequested(); }}// lib/cancel/CancelToken.js
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    // Throws a cancel message
    throw this.reason; }};Copy the code

Data converter and Content-Type

There are three data converters in Axios, two for request data converters and one for response data converters, which are divided into URL data converters and body data converters.

  • transformRequestRequest data converter, format the data in the request body, modify headers
  • paramsSerializerPair configuration dataparamsDo conversion,paramsCommonly used inget/delete/headRequest, other requests can also be setparamsUsed for linking arguments, mainly specifying rules for data serialization (stringing)
  • transformResponseResponse data converter, which parses or formats the response data

Note that the configuration of transformRequest and transformResponse is an array of functions, while the paramsSerializer value is a function

TransformRequest and transformResponse

The file: lib/defaults.js has the default configuration of transformRequest and transformResponse

transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type');
    // If the body type is FormData, Buffer, stream, or file, the FormData browser automatically adds contentType
    if (utils.isFormData(data) ||
      utils.isArrayBuffer(data) ||
      utils.isBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
      return data;
    }
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    // If it is URLSearchParams, set contentType to 'Application /x-www-form-urlencoded; // If it is URLSearchParams, set contentType to' Application /x-www-form-urlencoded; Charset = UTF-8 'Data is encoded as & separated key-value pairs
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded; charset=utf-8');
      return data.toString();
    }
    // If it is a JSON object, set contentType to Application/JSON and serialize data
    if (utils.isObject(data) || (headers && headers['Content-Type'= = ='application/json')) {
      setContentTypeIfUnset(headers, 'application/json');
      return JSON.stringify(data);
    }
    returndata; }].transformResponse: [function transformResponse(data) {
  var transitional = this.transitional;
  var silentJSONParsing = transitional && transitional.silentJSONParsing;
  var forcedJSONParsing = transitional && transitional.forcedJSONParsing;
  varstrictJSONParsing = ! silentJSONParsing &&this.responseType === 'json';

  if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) {
    try {
      return JSON.parse(data);
    } catch (e) {
      Parse will throw an exception if it does not return the original data
      if (strictJSONParsing) {
        if (e.name === 'SyntaxError') {
          throw enhanceError(e, this.'E_JSON_PARSE');
        }
        throwe; }}}returndata; }].Copy the code
  • contentTypefordelete.get.headThere is no need to set them because their arguments are passed in the URL, only the request bodybodyData in thecontentType
  • If not setcontentType.post.put.patchThe default isapplication/x-www-form-urlencoded
  • ifbodyIs of typeFormData.axiosWill removecontentType, the browser defaultscontentTypemultipart/form-data;

Overwrite and append the transformRequest and transformResponse

The import axios from 'axios / / additional request converter axios. Defaults. TransformRequest. Push ((data, headers) = > {/ /... Data return data; }); / / override request converter axios. Defaults. TransformRequest = [(data, headers) = > {/ /... processing data return data;}]. / / transformResponse similarlyCopy the code

paramsSerializer

ParamsSerializer call time

Source file lib/ Adapters /xhr.js 32 lines

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
     // ...
     // When establishing a connection, serialize params into a string, concatenated after the URL
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    // ...
  });
};
Copy the code

BuildURL in the source file lib/helpers/ buildurl.js

module.exports = function buildURL(url, params, paramsSerializer) {
  // If you have not configured params, return the URL, no processing
  if(! params) {return url;
  }

  var serializedParams;
  // If the paramsSerializer serialization function is configured, the function is called
  if (paramsSerializer) {
    serializedParams = paramsSerializer(params);
  // If the params configured is an URLSearchParams object, toString is used directly
  } else if (utils.isURLSearchParams(params)) {
    serializedParams = params.toString();
  // If you do not specify the paramsSerializer serialization method, use the default serialization rules
  } else {
    var parts = [];
    
    // Two forEach loops params
    utils.forEach(params, function serialize(val, key) {
       // Important If value is null or undefinded, the current key will be deleted. Sometimes the server also needs a null key
      if (val === null || typeof val === 'undefined') {
        return;
      }
     [Important] If value is an array, concatenate the [] character after key. Sometimes the server needs [] to be indexed
      if (utils.isArray(val)) {
        key = key + '[]';
      } else {
      // A value that is not an array is changed to an array first to facilitate the loop
        val = [val];
      }
       
      // Loop through the value array
      utils.forEach(val, function parseValue(v) {
        // Date object will be converted to IOSString
        if (utils.isDate(v)) {
          v = v.toISOString();
        // Other objects use json.stringify directly
        } else if (utils.isObject(v)) {
          v = JSON.stringify(v);
        }
        // encode both key and value, but preserve: $, + []
        parts.push(encode(key) + '=' + encode(v));
      });
    });
    // Concatenate with &
    serializedParams = parts.join('&');
  }
  // If serialized strings are generated
  if (serializedParams) {
    // If the url contains a #, the # and subsequent characters will be removed when requested
    var hashmarkIndex = url.indexOf(The '#');
    if(hashmarkIndex ! = = -1) {
      url = url.slice(0, hashmarkIndex);
    }
    // The url has? If no, use & splicing. If no, use? Joining together
    url += (url.indexOf('? ') = = = -1 ? '? ' : '&') + serializedParams;
  }
  // returns the concatenated URL
  return url;
};
Copy the code

important

The params serialization method will directly affect the reception of the server, and the server should agree the rules of serialization, especially the rules of serialization of arrays into strings

Different rules will produce different strings, which will directly affect the way the server receives data. We can use QS packages to serialize data

qs.stringify({ a: ['b'.'c'] {},arrayFormat: 'indices' })
// 'a[0]=b&a[1]=c'
qs.stringify({ a: ['b'.'c'] {},arrayFormat: 'brackets' })
// 'a[]=b&a[]=c'
qs.stringify({ a: ['b'.'c'] {},arrayFormat: 'repeat' })
// 'a=b&a=c'
qs.stringify({ a: ['b'.'c'] {},arrayFormat: 'comma' })
// 'a=b,c'
Copy the code

The sample

Specify the serialization rules for Params for a single request

import Qs from 'qs'

axios.get('/userList', {
 params: {
     page: 1.pageSize: 20.keywords: 'a'.ids: [1.2.3]},paramsSerializer: function(params) {
    return Qs.stringify(params, {arrayFormat: 'brackets'}})},Copy the code

Please specify a serialization rule for Params on all requests

import Qs from 'qs'
axios.defaults.paramsSerializer = params= > Qs.stringify(params, {arrayFormat: 'brackets'})
Copy the code

The header configuration

How to configure the header

import axios from 'axios'

/ / common header
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; / / XHR logo

// A header set for a request type
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8';

// Set the header of a request
axios.get(url, {
  headers: {
    'Authorization': 'whr1',}})Copy the code

Source file lib/core/dispatchRequest js 38 lines

  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers
  );
Copy the code

Header Indicates the configured priority

For specific request configuration > Method Type Configuration > Common Configuration

How to configure headers in JWT?

If the user authentication mode of your project is JWT, the user authentication token is generally passed to the server for verification through the header. The general configuration process is as follows:

  1. The user login
  2. The server responds with login data and tokens
  3. The front-end stores tokens to the localstorage, usually as localstorage or cookies
  4. Subsequent requests that require authentication carry the token to the header and pass it to the server
  5. The server verifies whether the token is valid. If the token is valid, the server responds normally. If the token is invalid, the server returns an exception message
// When configuring a request, the header contains a token
axios.interceptors.request.use(function (config) {
  const token = window.localstorage.getItem('token');
  if(token){
    config.headers.common['Authorization'] = token;
  }
  return config;
});

// Caches tokens after login
axios.post( '/login', { username: ' '.pwd: ' ' }).then(res= > {
    const token = res.data.token
    window.localstorage.setItem('token', token)
})
Copy the code