axios源码系列(四) --- Axios和dispatchRequest与拦截器

前言

这应该是一个大多数都常用的请求库,因为它可以支持多种配置,跨平台实现,返回promise进行链式调用.完全过一遍源码可以提升自己对请求库的理解知识

axios源码系列(一) — 目录结构和工具函数
axios源码系列(二) — 适配器内部
axios源码系列(三) — 默认配置和取消请求
axios源码系列(四) — Axios和dispatchRequest与拦截器

axios创建

这就是Axios库暴露出来的使用方法

axios/lib/axios.js

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);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to 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);

axios实例实际上就是由Axios构造函数创建出来,再将原型上的request的this指向axios实例,然后调用utils.extend扩展函数将axios.prototypecontext附加在axios实例上,

上面可能有疑问的点在于这两点,好像显得多此一举

// Copy axios.prototype to instance
utils.extend(instance, Axios.prototype, context);

// Copy context to instance
utils.extend(instance, context);

但是在这里的extend里不单单是做扩展,还额外增加了绑定this指向的功能,所以在两次操作实现目的不一样

  1. instance是被扩展对象,Axiox.prototype是用于扩展的对象,context是作为扩展对象相关functionthis指定对象
  2. instance依然是被扩展对象,但这次context是用于扩展的对象
// 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));
};

我们看看内部是怎么合并配置的

axios/lib/core/mergeConfig.js

这是负责合并配置的方法

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

/**
 * Config-specific merge-function which creates a new config-object
 * by merging two configuration objects together.
 *
 * @param {Object} config1
 * @param {Object} config2
 * @returns {Object} New object resulting from merging config2 to config1
 */
module.exports = function mergeConfig(config1, config2) {
  // eslint-disable-next-line no-param-reassign
  config2 = config2 || {};
  var config = {};

  var valueFromConfig2Keys = ['url', 'method', 'params', 'data'];
  var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy'];
  var defaultToConfig2Keys = [
    'baseURL', 'url', 'transformRequest', 'transformResponse', 'paramsSerializer',
    'timeout', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName',
    'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress',
    'maxContentLength', 'validateStatus', 'maxRedirects', 'httpAgent',
    'httpsAgent', 'cancelToken', 'socketPath'
  ];

  utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) {
    if (typeof config2[prop] !== 'undefined') {
      config[prop] = config2[prop];
    }
  });

  utils.forEach(mergeDeepPropertiesKeys, function mergeDeepProperties(prop) {
    if (utils.isObject(config2[prop])) {
      config[prop] = utils.deepMerge(config1[prop], config2[prop]);
    } else if (typeof config2[prop] !== 'undefined') {
      config[prop] = config2[prop];
    } else if (utils.isObject(config1[prop])) {
      config[prop] = utils.deepMerge(config1[prop]);
    } else if (typeof config1[prop] !== 'undefined') {
      config[prop] = config1[prop];
    }
  });

  utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) {
    if (typeof config2[prop] !== 'undefined') {
      config[prop] = config2[prop];
    } else if (typeof config1[prop] !== 'undefined') {
      config[prop] = config1[prop];
    }
  });

  var axiosKeys = valueFromConfig2Keys
    .concat(mergeDeepPropertiesKeys)
    .concat(defaultToConfig2Keys);

  var otherKeys = Object
    .keys(config2)
    .filter(function filterAxiosKeys(key) {
      return axiosKeys.indexOf(key) === -1;
    });

  utils.forEach(otherKeys, function otherKeysDefaultToConfig2(prop) {
    if (typeof config2[prop] !== 'undefined') {
      config[prop] = config2[prop];
    } else if (typeof config1[prop] !== 'undefined') {
      config[prop] = config1[prop];
    }
  });

  return config;
};

从调用的方式可得,config1是axios的默认配置,config2是用户自定义的配置

里面默认有三种不同优先级的配置

  • valueFromConfig2Keys: 指定只要config2内的相关属性不为undefined则取config2的值,这里根本不考虑config1配置
  • mergeDeepPropertiesKeys: 深层请求头结构,config2优先级高于config1,先处理对象结构再处理其他非undefined类型
  • defaultToConfig2Keys: 可直接赋值的请求头列表,config2优先级高于config1

遍历处理得出上面三种配置之后,我们可以得到

  • axiosKeys: config1和config2含有上面三种默认配置合并后得到的映射数组
  • otherKeys: 过滤出config2中axiosKeys里不包含的配置属性,基本是非常用或者自定义的字段,同样不考虑config1的情况

最后就是将otherKeys的相关配置也合并到最终结果并返回该结果

// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

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

module.exports = axios;

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

在axios实例上添加方法,我们先看spread是有什么作用

axios/lib/helpers/spread.js

/**
 * Syntactic sugar for invoking a function and expanding an array for arguments.
 *
 * Common use case would be to use `Function.prototype.apply`.
 *
 *  ```js
 *  function f(x, y, z) {}
 *  var args = [1, 2, 3];
 *  f.apply(null, args);
 *  ```
 *
 * With `spread` this example can be re-written.
 *
 *  ```js
 *  spread(function(x, y, z) {})([1, 2, 3]);
 *  ```
 *
 * @param {Function} callback
 * @returns {Function}
 */
module.exports = function spread(callback) {
  return function wrap(arr) {
    return callback.apply(null, arr);
  };
};

只是一句语法糖,没有黑魔法

Axios核心构造函数

axios/lib/core/Axios.js

这是Axios的构造函数

var utils = require('./../utils');
var buildURL = require('../helpers/buildURL');
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
 */
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

创建Axios实例默认会保存配置,并且内置拦截器分别给请求和响应

/**
 * Dispatch a request
 *
 * @param {Object} config The config specific for this request (merged with this.defaults)
 */
Axios.prototype.request = function request(config) {
  /*eslint no-param-reassign:0*/
  // 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);

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

request分两部分逻辑

  1. 支持首个参数传入请求地址的调用方式,合并默认配置和传入配置
  2. chain作为钩子中间件进行链式调用,最终变成

    [
        this.interceptors.request.fulfilled, this.interceptors.request.rejected,// 请求拦截
        dispatchRequest, undefined,// 发起请求
        this.interceptors.response.fulfilled, this.interceptors.response.rejected,// 响应拦截
    ]
  3. 使用while链式调用chain的函数,完成从发起到接收请求的完整流程
Axios.prototype.getUri = function getUri(config) {
  // 合并配置
  config = mergeConfig(this.defaults, config);
  // 返回拼装URL
  return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
};

// 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(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

module.exports = Axios;

遍历所有请求方式调用内置的request方法

拦截器

axios/lib/core/InterceptorManager.js

定义拦截器的构造函数

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

function InterceptorManager() {
  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) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

module.exports = InterceptorManager;

整个函数其实只做了几件事:

  1. 生成实例,内置一个handlers队列
  2. 原型绑定use注册方法,队列传入包含满足和失败函数方法的拦截对象,返回该对象在队列中的id(实际上就是索引值)
  3. 原型绑定eject解绑方法,队列移除指定id的拦截对象
  4. 原型绑定forEach遍历方法,迭代所有已注册的拦截器

拦截器示例

// 添加请求拦截器
const reqInterceptor = axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
const resInterceptor = axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });
  
// 移除
axios.interceptors.request.eject(reqInterceptor);
axios.interceptors.request.eject(resInterceptor);

调度请求

axios/lib/core/dispatchRequest.js

这是负责调度请求的函数,返回Promise.

var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');

/**
 * Throws a `Cancel` if cancellation has been requested.
 */
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

/**
 * Dispatch a request to the server using the configured adapter.
 *
 * @param {object} config The config that is to be used for the request
 * @returns {Promise} The Promise to be fulfilled
 */
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) {
      delete config.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);
  });
};

throwIfCancellationRequested是如果已经取消请求则抛出错误

dispatchRequest使用配置的adapter向服务端发起请求,里面分几个步骤:

  1. 检测到如果请求已经取消则抛出错误
  2. 确保请求头存在
  3. 传入数据和请求头,根据数据类型转换请求数据
  4. 抽出请求头的common和当前请求的methods合并到首层结构
  5. 删除header属性里无用的属性
  6. 确认adapter
  7. 执行adapter(config)返回Promise然后执行对应操作

    • 成功

      • 检测到如果请求已经取消则抛出错误
      • 否则处理返回数据格式
    • 失败

      • 如果!isCancel(reason)成立,检测到如果请求已经取消则抛出错误
      • 否则直接返回数据

axios/lib/core/transformData.js

负责解析请求/响应数据,只是单纯的使用utils.forEach而已

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

/**
 * Transform the data for a request or a response
 *
 * @param {Object|String} data The data to be transformed
 * @param {Array} headers The headers for the request or response
 * @param {Array|Function} fns A single function or Array of functions
 * @returns {*} The resulting transformed data
 */
module.exports = function transformData(data, headers, fns) {
  /*eslint no-param-reassign:0*/
  utils.forEach(fns, function transform(fn) {
    data = fn(data, headers);
  });

  return data;
};

至此,整个axios库的源码已经讲完了.

    原文作者:炸鸡超人
    原文地址: https://segmentfault.com/a/1190000020547734
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞