axios source code structure and parameter analysis

Learning purpose

  • Understand the implementation principle of axios and learn the basic knowledge of js
  • Understand the configuration and functions of axios that are not commonly used in daily life
  • Helps solve problems encountered in front-end and back-end interaction
  • Improve the ability to read source code and accumulate some useful methods
  • Expand the functions you want - if you want to write a package or open source library, it will help provide ideas

Source code directory analysis

Download address: git clone https://github.com/axios/axios.git

└─ examples   // demo
└─ lib
	└─ adapters
   		├─ http.js // node环境下利用 http 模块发起请求
   		├─ xhr.js // 浏览器环境下利用 xhr 发起请求
	└─ cancel
   		├─ Cancel.js      // Cancel构造函数
   		├─ CancelToken.js // 取消请求构造函数
   		├─ isCancel.js   // 是否取消boolean
	└─ core
    	├─ Axios.js // 生成 Axios 实例
    	├─ InterceptorManager.js // 拦截器
    	├─ dispatchRequest.js  // 调用适配器发起请求
    	├─ mergeConfig.js // 合并配置
    	├─ transformData  // 数据转化
    	...
	└─ helpers
    	├─ bind.js  // bind函数
    	├─ buildURL.js // 根据类型转换为最终的url
    	├─ spread.js // sprend扩展
    	├─ validator.js // 校验函数
    	├─ cookies.js // cookie的读写方法
    	├─ ...
	├─ axios.js  // 入口文件
	├─ defaults.js  // axios 默认配置项
	├─ utils.js     // 工具函数
└─ sandbox // 沙箱测试
	client.html // 前端页面
	server.js // 后端服务
└─ webpack.config.js // webpack配置

Basic Functional Analysis

Entry file

View webpack.config.js, the entry file is index.js

var config = {
    
    
  entry: './index.js',
  output: {
    
    
    path: __dirname + '/dist/',
    filename: name + '.js',
    sourceMapFilename: name + '.map',
    library: 'axios',
    libraryTarget: 'umd',
    globalObject: 'this'
  }

View index.js in the root directory

module.exports = require('./lib/axios');

Follow the clues and check the ./lib/axios.js file

Let’s analyze the real entry file ./lib/axios.js

Import dependent modules

'use strict';

var utils = require('./utils'); // 工具函数
var bind = require('./helpers/bind'); // bind方法
var Axios = require('./core/Axios'); // 核心Axio方法
var mergeConfig = require('./core/mergeConfig'); // 配置参数的处理
var defaults = require('./defaults'); // 默认配置参数

create instance

/**
 * Create an instance of Axios
 *
 * @param {Object} defaultConfig The default config for the instance
 * @return {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
    
    
  // context实例自身拥有2个属性:defaults、interceptors
  // 原型对象上有一些方法:request、getUri、delete、get、post、head、options、put、patch
  var context = new Axios(defaultConfig);
  // bind返回一个新函数,这个新函数内部会调用这个request函数,改变Axios.prototype.request中的this指向
  var instance = bind(Axios.prototype.request, context);
	
  // Axios原型上的方法拷贝到instance上:request()/get()/post()/put()/delete()
  utils.extend(instance, Axios.prototype, context);

  // 将Axios实例对象(context)上的属性拷贝到instance上:defaults和interceptors属性
  utils.extend(instance, context);

  // 工厂模式,用于创建新的实例
  instance.create = function create(instanceConfig) {
    
    
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}

// 创建实例
var axios = createInstance(defaults);

// 往外暴漏
axios.Axios = Axios;

Hang other methods and properties on the instance

// 暴漏取消请求相关实例
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
axios.VERSION = require('./env/data').version;

// 暴漏all方法
axios.all = function all(promises) {
    
    
  return Promise.all(promises);
};
// 暴漏spread方法
axios.spread = require('./helpers/spread');

// 暴漏isAxiosError
axios.isAxiosError = require('./helpers/isAxiosError');
// 导出
module.exports = axios;

// TS方式下导出
module.exports.default = axios;

What are the differences between axios, axios.Axios and axios.instance to create instances?

  • Configuration may vary
  • axios creates instances using all default parameters.
  • axios.Axios all uses the form passed in by the developer to create instances
  • axios.instance creates an instance using the combination passed in by the developer and the default combination.
  • The mounted attributes will be different
    • More properties (Cancel, CancelToken, isCancel, all, spread) are mounted on axios

axios.spread and axios.all are used together with axios.spread as a callback function.

// 源码
/*
 * @param {Function} callback
 * @returns {Function}
 */
module.exports = function spread(callback) {
    
    
  return function wrap(arr) {
    
    
    return callback.apply(null, arr);
  };
};
// 使用
axios.all([
  axios.get('https://api.github.com/users/mzabriskie'),
  axios.get('https://api.github.com/users/mzabriskie/orgs')
]).then(axios.spread(function (user, orgs) {
    
    })

Parameter configuration processing

default parameters

The default parameters are in /lib/defaults.js

var defaults = {
    
    
  // 对transitional 属性进行版本校验,会提示自某个版本之后移除该属性
  // transitional包含了一些过度属性配置项,这些项对开发者不可见,主要是为了兼容之前的版本,经过配置可在较新版本中删除向后兼容的某些性质
  transitional: {
    
    },
  adapter: getDefaultAdapter(),
  transformRequest: [function transformRequest(data, headers) {
    
    }],
  transformResponse: [function transformResponse(data) {
    
    }],
  // `params` 是即将与请求一起发送的 URL 参数
  // 必须是一个无格式对象(plain object)或 URLSearchParams 对象
  params: {
    
    
    ID: 12345
  },
  // `paramsSerializer` 是一个负责 `params` 序列化的函数
  paramsSerializer: function(params) {
    
    
    return Qs.stringify(params, {
    
    arrayFormat: 'brackets'})
  },
  timeout: 0,
  // xsrfCookieName 是用作 xsrf token 值的 cookie 名称
  xsrfCookieName: 'XSRF-TOKEN',
  // xsrfHeaderName 是 xsrf token 值的 http 头名称
  xsrfHeaderName: 'X-XSRF-TOKEN',
  // maxContentLength 限制http响应的最大字节数
  maxContentLength: -1,
  // maxContentLength 限制请求体的最大字节数
  maxBodyLength: -1,
  // 提前拿到服务器相应过来的状态信息。如果该函数返会True或者null或者undefined, 则axios返回的promise状态则会被更改为resolved。 如果该函数返会false, 则该promise状态变为rejected
  validateStatus: function validateStatus(status) {
    
    
    return status >= 200 && status < 300;
  },
  // headers 是即将被发送的自定义请求头
  headers: {
    
    
    common: {
    
    
      'Accept': 'application/json, text/plain, */*'
    }
  }
}

adapter adapter, mainly distinguishes between browser environment and node environment

function getDefaultAdapter() {
    
    
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    
    
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    
    
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

transformRequest , allows to modify the request data before sending it to the server, it can only be used in the request methods of 'PUT', 'POST' and 'PATCH' , the functions in the latter array must return a String, or ArrayBuffer, or Stream

transformResponse , allows the response data to be modified before passing to then/catch

xsrfCookieName and xsrfHeaderName are used to prevent xsrf (cross-site request forgery) attacks

Interlude: login authentication generally has session authentication, JWT (token) authentication, session authentication has some shortcomings, the best authentication method is token, and token generally has three methods in front-end and back-end communication

   	1. get放在查询字符串中,URL中拼接(post可以放body中)
    2. 放到Authorization/或者自定义headers中
    3. 放到cookie中

xsrfCookieName && xsrfHeaderName is mainly the key value for processing the token placed in the cookie

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

No default processing configuration

  • withCredentials
  • auth
  • responseEncoding
  • onUploadProgress
  • onDownloadProgress
  • maxRedirects
  • socketPath // socketPathDefines a UNIX Socket used in node.js. For example '/var/run/docker.sock' sends a request to the docker daemon. socketPathOnly or can be specified proxy. If both are specified, they are used socketPath.
  • httpAgent // Used in node.js to customize the agent
  • httpsAgent
  • proxy
  • cancelToken
  • signal: new AbortController().signal, // an alternative way to cancel Axios requests using AbortController

In addition, there is a configuration item auth (password authorization)

// HTTP basic authentication
    if (config.auth) {
    
    
      var username = config.auth.username || '';
      var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }

onUplodProgress , to facilitate obtaining progress when uploading files

var config = {
    
    
  onUploadProgress: function(progressEvent) {
    
    
    var percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total );
    console.log('percentCompleted', percentCompleted)
  }
};

axios.put('/upload/server', data, config)
  .then(function (res) {
    
    
  output.className = 'container';
  output.innerHTML = res.data;
})
  .catch(function (err) {
    
    
  output.className = 'container text-danger';
  output.innerHTML = err.message;
});

Call the adapter to initiate a request

function dispatchRequest(config) {
    
    
  // ....config参数的处理
  var adapter = config.adapter || defaults.adapter;
  return adapter(config).then(function onAdapterResolution(response) {
    
    
    // 成功
  },function onAdapterRejection(reason) {
    
    
    // 失败
  })
}

XHR request processing in browser environment

module.exports = function xhrAdapter(config) {
    
    
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    
    
    // ...定义一些变量
    function done() {
    
    
      // 取消一些监听
    }
    
    var request = new XMLHttpRequest();
    // HTTP basic authentication
    // url的处理
    // Set the request timeout
    // 定义loadend函数
    function onloadend() {
    
    
      // 解析响应
      // 根据校验状态去处理promise
      settle(function _resolve(value) {
    
    
        resolve(value);
        done();
      }, function _reject(err) {
    
    
        reject(err);
        done();
      }, response);
    }
    if ('onloadend' in request) {
    
    
      // Use onloadend if available
      request.onloadend = onloadend;
    } else {
    
    
      // 使用 onreadystatechange 模拟 onload 事件
    }
    
    request.onabort = function () {
    
    }
    request.onerror = function () {
    
    }
    request.ontimeout = function () {
    
    }
    
    // Add xsrf header
    // Add headers to the request
    // Add withCredentials to request if needed
    // Add responseType to request if needed
    // Handle progress if needed 下载
    // Not all browsers support upload events
    
    // 。。。
    // 取消请求对处理
    if (config.cancelToken || config.signal) {
    
    
      // Handle cancellation
      // eslint-disable-next-line func-names
      onCanceled = function(cancel) {
    
    
        ...
      }
        
    }
    if (!requestData) {
    
    
      requestData = null;
    }

    // Send the request
    request.send(requestData);
  })

​ You can learn and understand adapter processing in node environment (lib/adapters/http.js) by yourself

Section 2: Implementation of important functions

cancelToken implementation

execute CancelTokenfunction,

1. Create a promise object, assign the promise object to the promise attribute, and expose the resolve execution right

2. Execute the executor method, use cancel as the executor parameter, and call resolve internally to cancel the request.

3. Define the promise's .then method and then callback function. The .then method implements the subscription and unsubscription monitoring methods, and the callback function executes the specific action of the cancellation method

Mount the source method on the instance for easy calling. It returns two methods. token is to create the instance, and cancel is the method to trigger the cancellation request.

function CancelToken(executor) {
    
    
  // 取promise的resolve方法的控制权给resolvePromise
  var resolvePromise;

  this.promise = new Promise(function promiseExecutor(resolve) {
    
    
    resolvePromise = resolve;
  });
  this.promise.then(cancel)  // token.reason 传递过来的cancel实例
  this.promise.then = function (onfulfilled) {
    
    }
  ......
  executor(function cancel(message) {
    
    
    if (token.reason) {
    
    
      // Cancellation has already been requested
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}
......
CancelToken.source = function source() {
    
    
  var cancel;
  var token = new CancelToken(function executor(c) {
    
    
    cancel = c;
  });
  return {
    
    
    token: token,
    cancel: cancel
  };
};

module.exports = CancelToken;

Interceptor and request processing

Axios prototype object, a default configuration defaults and two interceptors are defined on the instance

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

Prototype method request

Axios.prototype.request = function request(configOrUrl, config) {
    
    
  /*eslint no-param-reassign:0*/
  // Allow for axios('example/url'[, config]) a la fetch API
  console.log('request', configOrUrl, config)
  // 1. 配置项处理
  if (typeof configOrUrl === 'string') {
    
    
    config = config || {
    
    };
    config.url = configOrUrl;
  } else {
    
    
    config = configOrUrl || {
    
    };
  }

  config = mergeConfig(this.defaults, config);
  // 2. 请求方式处理
  // 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';
  }
 
  // 拦截器处理
  // filter out skipped interceptors
  // 请求拦截器储存数组
  var requestInterceptorChain = [];
  // 默认所有请求拦截器的变量
  var synchronousRequestInterceptors = true;

  // 遍历注册好的请求拦截器数组
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    
    
    // 这里interceptor是注册的每一个拦截器对象,axios请求拦截器向外暴露了runWhen配置
    // 来针对一些需要运行时检测来执行的拦截器
    // 如果配置了该函数,并且返回结果为true,则记录到拦截器链中,反之则直接结束该层循环
    if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
    
    
      return;
    }
    // interceptor.synchronous 是对外提供的配置,可标识该拦截器是异步还是同步 默认为false(异步)
    // 这里是来同步整个执行链的执行方式的,如果有一个请求拦截器为异步,
    // 那么下面的promise执行链则会有不同的执行方式
    synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
    // 塞到请求拦截器数组中
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  // 响应拦截器存储数组
  var responseInterceptorChain = [];
  // 遍历按序push到拦截器存储数组中
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    
    
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  var promise;
  // ***** 如果为异步 其实也是默认情况 ******//
  if (!synchronousRequestInterceptors) {
    
    
    // 创建存储链式调用的数组 首位是核心调用方法dispatchRequest,第二位是空
    var chain = [dispatchRequest, undefined];
    // 请求拦截器塞到前面
    Array.prototype.unshift.apply(chain, requestInterceptorChain);
    // 响应拦截器塞到后面
    chain = chain.concat(responseInterceptorChain);
    // 传参数config给dispatchRequest方法
    promise = Promise.resolve(config);
     // 循环 每次取两个出来组成promise链.then执行[]
    while (chain.length) {
    
    
      promise = promise.then(chain.shift(), chain.shift());
    }

    return promise;
  }

  // ***** 这里则是同步的逻辑  ******//
  // 请求拦截器一个一个的走 返回 请求前最新的config
  var newConfig = config;
  // 循环 每次取两个出来组成promise链.then执行
  while (requestInterceptorChain.length) {
    
    
    var onFulfilled = requestInterceptorChain.shift();
    var onRejected = requestInterceptorChain.shift();
    // 做异常捕获 有错直接抛出
    try {
    
    
      newConfig = onFulfilled(newConfig);
    } catch (error) {
    
    
      onRejected(error);
      break;
    }
  }
  // 请求
  try {
    
    
    promise = dispatchRequest(newConfig)
  } catch (error) {
    
    
    return Promise.reject(error);
  }
  // 响应拦截器执行
  while (responseInterceptorChain.length) {
    
    
    promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
  }

  return promise;
};

It can be seen from the source code that multiple request interceptors can be set, and the request interceptors are stored in the array in unshift mode, and the request interceptors defined later will be executed first, so the interceptors defined earlier for the same process will overwrite the ones defined later Interceptor

Implement other methods based on request

// Provide aliases for supported request methods
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
    }));
  };
});

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

Implement a throttling logic yourself in the source code

function Axios(instanceConfig) {
    
    
  this.defaults = instanceConfig;
  this.interceptors = {
    
    
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
  // 添加一个对象,来存储请求参数
  this.throttleParamsConfig = {
    
    }
}
// 节流的实现
function throttle(handle, wait) {
    
    
  let lastTime = new Date().getTime();
  return function (configOrUrl, config) {
    
    
    let nowTime = new Date().getTime();
    // 超过wait时间,清空缓存参数
    if (nowTime - lastTime > wait) {
    
    
      this.throttleParamsConfig = {
    
    }
    }
    // 处理当前请求的参数
    config = handleConfig(configOrUrl, config, this.defaults)
    // 不同的请求
    const nextFlag = this.saveRequestConfig(config)
    if(nextFlag) {
    
    
      lastTime = nowTime  
      const promise = handle.apply(this, arguments);
      return promise;
    }
  }
}
// 存储请求配置
Axios.prototype.saveRequestConfig = function saveRequestConfig (config) {
    
    
  // 节流处理
  const prevConfigArray = this.throttleParamsConfig[config.url]
  if (prevConfigArray) {
    
    
    console.log(2)
    if (prevConfigArray.includes(JSON.stringify(config))) {
    
    
      console.log(prevConfigArray)
      return false;
    } else {
    
    
      console.log(3, this.throttleParamsConfig[config.url])
      this.throttleParamsConfig[config.url] = prevConfigArray.concat([JSON.stringify(config)])
    }
  } else {
    
    
    console.log(1)
    this.throttleParamsConfig[config.url] = [JSON.stringify(config)]
  }
  return true;
}
// throttle包裹request,时间可以自定义,暴漏出去
Axios.prototype.request = throttle(function (configOrUrl, config) {
    
    
  // 1. 配置项处理
  config = handleConfig(configOrUrl, config, this.defaults)
  // 改变this指针
  bind(Axios.prototype.saveRequestConfig, this);
  // 以下为原有逻辑处理
  ....
}, 1000)

Storage method

Think->Other Extensions

  • jsonp
  • Hook function (processing data)
  • Queue processing (implementation of await)
  • Anti-shake

Summarize

Key concepts:

​ The internal core code of axios is mainly composed of adapters , cancellation request classes , request method implementations, and interceptor processing.

Some configurations:

​ URL processing, data conversion, upload and download progress, auth authorization, anti-XSRF attack, proxy and other configuration support

Some methods:

​ Determine the type and environment such as: isPlainObject isStream isStandardBrowserEnv…

​ Binding classes such as: bind, extend

Design method:

​ Publish Subscriber Pattern (Cancel Request Class)

Custom implementation:

​Throttling implementation, you can try other function implementations

Guess you like

Origin blog.csdn.net/kang_k/article/details/131393707