Axios源码解析(五):核心工具方法(2)

1,079 阅读10分钟

上篇 Axios 源码解析(四):核心工具方法(1) 解析了 default.js/cancel 目录下的源码,下面继续解析 /cancel 目录下核心工具方法的代码。

github.com/MageeLin/ax… 中的 analysis 分支可以看到当前已解析完的文件。

总览

《Axios 源码解析(一):模块分解》中已经分析过,/core 目录中包含如下这些文件:

├─core
│      Axios.js
│      buildFullPath.js
│      createError.js
│      dispatchRequest.js
│      enhanceError.js
│      InterceptorManager.js
│      mergeConfig.js
│      README.md
│      settle.js
│      transformData.js

同样在 README.md 中,介绍了该目录的作用:

core/ 中的模块是特定于 axios 内部使用的模块。因为它们的逻辑特定于专门情况,所以在 axios 模块之外使用这些模块很可能没有用。core 模块的一些例子如下:

  • 调度请求
    • 通过 adapters/ 发送的请求(参见 lib/adapters/README.md
  • 管理拦截器
  • 处理配置

整个 core/ 目录中,Axios.js 是最复杂也是最核心的,放在最后,剩余的文件咱们挨个来看:

buildFullPath.js

这个 buildFullPath 方法组合了上篇分析过的 isAbsoluteURLcombineURLs 方法,最终目的让返回的 url 必须为一个绝对地址。

var isAbsoluteURL = require('../helpers/isAbsoluteURL');
var combineURLs = require('../helpers/combineURLs');

/**
 * 仅当请求的 URL 不是绝对 URL 时,才通过将 baseURL 与请求的 URL 组合来创建新 URL。
 * 如果请求的 URL 是绝对URL,此函数会原封不动地返回所请求的 URL。
 *
 * @param {string} baseURL base URL
 * @param {string} requestedURL 要组合的url(绝对或相对)
 * @returns {string} 组合后的url
 */
module.exports = function buildFullPath(baseURL, requestedURL) {
  // 仅当请求的 URL 不是绝对 URL 时,才组合
  if (baseURL && !isAbsoluteURL(requestedURL)) {
    return combineURLs(baseURL, requestedURL);
  }
  // 否则直接返回
  return requestedURL;
};

enhanceError.js

enhanceError 方法就是单纯为了升级 Error 对象,给 Axios 生成的错误对象添加 configcoderequestresponse 属性

/**
 * 使用指定的配置、错误代码和响应来升级Error。
 *
 * @param {Error} error 要升级的error对象
 * @param {Object} config 配置.
 * @param {string} [code] 错误代码 (比如, 'ECONNABORTED').
 * @param {Object} [request] 请求对象.
 * @param {Object} [response] 响应对象.
 * @returns {Error} 返回升级后的error对象.
 */
module.exports = function enhanceError(error, config, code, request, response) {
  // 把各种属性赋给error对象
  error.config = config;
  if (code) {
    error.code = code;
  }

  error.request = request;
  error.response = response;
  error.isAxiosError = true;

  // 自定义一个toJSON方法,进行了序列化
  error.toJSON = function toJSON() {
    return {
      // Standard
      message: this.message,
      name: this.name,
      // Microsoft
      description: this.description,
      number: this.number,
      // Mozilla
      fileName: this.fileName,
      lineNumber: this.lineNumber,
      columnNumber: this.columnNumber,
      stack: this.stack,
      // Axios
      config: this.config,
      code: this.code,
    };
  };
  return error;
};

createError.js

enhanceError 方法增强的就是 createError 方法生成的对象,Error 对象刚生成的时候只有一个 message 属性。

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

/**
 * 使用指定的message、配置、错误代码、请求和响应来创建Error。
 *
 * @param {string} message 错误 message.
 * @param {Object} config 配置.
 * @param {string} [code] 错误代码 (比如 'ECONNABORTED').
 * @param {Object} [request] 请求对象.
 * @param {Object} [response] 响应对象.
 * @returns {Error} 创建后的错误对象.
 */
module.exports = function createError(
  message,
  config,
  code,
  request,
  response
) {
  // 创建一个错误后给它添加各种属性
  var error = new Error(message);
  return enhanceError(error, config, code, request, response);
};

settle.js

settle 方法是用来 settle 请求过程这个 promise 的,结果是 resolve 还是 reject 取决于 response

如果响应成功,则 resolve(response);如果响应失败,则用 createError 生成一个错误对象,然后 reject 掉。

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

/**
 * 依据响应状态来 resolve 或 reject 一个 Promise.
 *
 * @param {Function} resolve resolve函数.
 * @param {Function} reject reject函数.
 * @param {object} response 响应对象.
 */
module.exports = function settle(resolve, reject, response) {
  var validateStatus = response.config.validateStatus;
  // 只有validateStatus方法校验通过时,才会resolve
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
    // 否则reject升级后的错误对象
  } else {
    reject(
      createError(
        'Request failed with status code ' + response.status,
        response.config,
        null,
        response.request,
        response
      )
    );
  }
};

mergeConfig.js

由于 Axios 中有默认配置、用户默认配置和用户自定义配置多种 config,所以 mergeConfig 就是用专门的规则来合并这些配置的,会返回一个新的合并后配置对象。

merge 的规则比较复杂,针对不同的属性需要有不同的合并策略:

'use strict';

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

/**
 * 配置专用的的合并函数,通过将两个配置对象合并在一起,创建一个新的配置对象。
 *
 * @param {Object} config1
 * @param {Object} config2
 * @returns {Object} 从config1和config2合并出来的新对象
 */
module.exports = function mergeConfig(config1, config2) {
  // 给config2默认为空对象
  // eslint-disable-next-line no-param-reassign
  config2 = config2 || {};
  var config = {};

  // 全部来源于config2的键
  var valueFromConfig2Keys = ['url', 'method', 'data'];
  // 优先来源于config2的键,深度合并会判别undefined
  var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params'];
  // 优先来源于config2的键,不进行深度合并
  var defaultToConfig2Keys = [
    'baseURL',
    'transformRequest',
    'transformResponse',
    'paramsSerializer',
    'timeout',
    'timeoutMessage',
    'withCredentials',
    'adapter',
    'responseType',
    'xsrfCookieName',
    'xsrfHeaderName',
    'onUploadProgress',
    'onDownloadProgress',
    'decompress',
    'maxContentLength',
    'maxBodyLength',
    'maxRedirects',
    'transport',
    'httpAgent',
    'httpsAgent',
    'cancelToken',
    'socketPath',
    'responseEncoding',
  ];
  // 优先来源于config2的键,直接深度合并 使用in来判断
  var directMergeKeys = ['validateStatus'];

  /**
   * @description: 两个同名属性 merge 时的规则,source 比 target 优先级高
   * @param {Object} target 要合并的属性
   * @param {Object} source 源属性
   * @return {Object} 最后返回source
   */
  function getMergedValue(target, source) {
    // 普通对象直接执行merge方法
    if (utils.isPlainObject(target) && utils.isPlainObject(source)) {
      return utils.merge(target, source);
      // 如果target不是纯对象,就把target抛弃
    } else if (utils.isPlainObject(source)) {
      return utils.merge({}, source);
      // 如果source是个数组,就返回source的副本
    } else if (utils.isArray(source)) {
      return source.slice();
    }
    return source;
  }

  /**
   * @description: 深度合并方法
   * @param {string} prop 键
   */
  function mergeDeepProperties(prop) {
    // 如果config2中该键存在,就直接执行合并
    if (!utils.isUndefined(config2[prop])) {
      config[prop] = getMergedValue(config1[prop], config2[prop]);
      // 如果config2中该键不存在,但config1中该键存在,就直接取config1
    } else if (!utils.isUndefined(config1[prop])) {
      config[prop] = getMergedValue(undefined, config1[prop]);
    }
  }

  // 1.如果是需要config2的键,就直接从config2中把值拿过来,绝对不会取config1
  utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) {
    if (!utils.isUndefined(config2[prop])) {
      config[prop] = getMergedValue(undefined, config2[prop]);
    }
  });

  // 2.需要深度合并的键就执行深度合并方法
  utils.forEach(mergeDeepPropertiesKeys, mergeDeepProperties);

  // 3.config2优先的键,就优先取config2,如果没有才取config1
  utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) {
    if (!utils.isUndefined(config2[prop])) {
      config[prop] = getMergedValue(undefined, config2[prop]);
    } else if (!utils.isUndefined(config1[prop])) {
      config[prop] = getMergedValue(undefined, config1[prop]);
    }
  });

  // 4.直接合并,这个跟深度合并的区别是用in来做判别,也就是undefined的区别
  utils.forEach(directMergeKeys, function merge(prop) {
    if (prop in config2) {
      config[prop] = getMergedValue(config1[prop], config2[prop]);
    } else if (prop in config1) {
      config[prop] = getMergedValue(undefined, config1[prop]);
    }
  });

  // axios专用的配置的键
  var axiosKeys = valueFromConfig2Keys
    .concat(mergeDeepPropertiesKeys)
    .concat(defaultToConfig2Keys)
    .concat(directMergeKeys);

  // 用户加的自定义配置的键
  var otherKeys = Object.keys(config1)
    .concat(Object.keys(config2))
    .filter(function filterAxiosKeys(key) {
      // 把axios专用的键过滤掉
      return axiosKeys.indexOf(key) === -1;
    });

  // 5.用户自定义配置的键执行深度合并
  utils.forEach(otherKeys, mergeDeepProperties);

  return config;
};

transformData.js

transformData 方法是用来支持 transformRequesttransformResponse 的,transformRequest 允许在向服务器发送请求前修改请求数据,transformResponse 允许在传递结果给 then/catch 前修改响应数据。

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

/**
 * 转换request或者response对象
 *
 * @param {Object|String} data 要被转换的对象
 * @param {Array} headers 请求或响应的header
 * @param {Array|Function} fns 单个函数或者函数数组
 * @returns {*} 转换后的对象
 */
module.exports = function transformData(data, headers, fns) {
  var context = this || defaults;
  /*eslint no-param-reassign:0*/
  utils.forEach(fns, function transform(fn) {
    // 主要是调用了fn方法来转换
    data = fn.call(context, data, headers);
  });

  return data;
};

dispatchRequest.js

dispatchRequest 方法最直接的执行发送请求的方法,它分为如下几步:

  1. 根据配置转换请求对象
  2. 整理请求头
  3. 选择一个适配器并发送请求
  4. 根据配置转换响应对象
  5. 返回响应
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');

/**
 * 如果取消请求的行为已经执行,则抛出一个 Cancel 对象
 */
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

/**
 * 使用已经配置好的适配器,向服务器发送请求
 *
 * @param {object} config 被用于发送请求的配置
 * @returns {Promise} 处理完的promise
 */
module.exports = function dispatchRequest(config) {
  // 若请求已取消,则抛出Cancel对象
  throwIfCancellationRequested(config);

  // 确保header存在
  config.headers = config.headers || {};

  // 利用header和data,以及请求转换器来转换data
  config.data = transformData.call(
    config,
    config.data,
    config.headers,
    config.transformRequest
  );

  // 合并请求头
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers
  );

  // 清理掉headers中的请求method
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

  // 选择一个适配器
  var adapter = config.adapter || defaults.adapter;

  // 返回一个promise
  return adapter(config).then(
    function onAdapterResolution(response) {
      // 同样,若请求已取消,则抛出Cancel对象
      throwIfCancellationRequested(config);

      // 利用header和data,以及响应转换器来转换data
      response.data = transformData.call(
        config,
        response.data,
        response.headers,
        config.transformResponse
      );

      return response;
    },
    function onAdapterRejection(reason) {
      // 同样,若请求已取消,则抛出Cancel对象
      if (!isCancel(reason)) {
        throwIfCancellationRequested(config);

        // 利用header和data,以及响应转换器来转换data
        if (reason && reason.response) {
          // 添加到reason下的response属性中
          reason.response.data = transformData.call(
            config,
            reason.response.data,
            reason.response.headers,
            config.transformResponse
          );
        }
      }

      return Promise.reject(reason);
    }
  );
};

InterceptorManager.js

InterceptorManager 是拦截器的类,使用栈保存了自定义的拦截器,use 方法安装拦截器,eject 方法卸载拦截器,forEach 迭代每一个拦截器。

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

function InterceptorManager() {
  this.handlers = [];
}

/**
 * 给栈中添加一个新的拦截器
 *
 * @param {Function} fulfilled fulfilled后怎么处理then的函数
 * @param {Function} rejected reject后怎么处理reject的函数
 *
 * @return {Number} 便于之后删除拦截器用的ID
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
    synchronous: options ? options.synchronous : false,
    runWhen: options ? options.runWhen : null,
  });
  return this.handlers.length - 1;
};

/**
 * 从栈中删除一个拦截器
 *
 * @param {Number} id 使用use方法给的id来删除
 */
InterceptorManager.prototype.eject = function eject(id) {
  // 直接把对应的地方设为null
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

/**
 * 迭代已注册的拦截器
 *
 * 该方法对于跳过任何可能变成"null"的拦截器。
 *
 * @param {Function} fn 使用fn方法来调用每一个拦截器
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

module.exports = InterceptorManager;

Axios.js

Axios.js/core 目录下的最终出口,也是生成 Axios 类的位置。Axios 的构造函数结构如下:

function Axios(instanceConfig) {
  this.defaults;
  this.interceptors;
}
Axios.prototype.request;
Axios.prototype.getUri;
Axios.prototype.delete;
Axios.prototype.get;
Axios.prototype.head;
Axios.prototype.options;
Axios.prototype.post;
Axios.prototype.put;
Axios.prototype.patch;

其中最核心的方法也就是 Axios.prototype.request 方法,在该方法中主要执行三步:

  1. 处理请求拦截器
  2. 发送请求
  3. 处理响应拦截器
  4. 返回响应

源码如下:

var utils = require('./../utils');
var buildURL = require('../helpers/buildURL');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
var mergeConfig = require('./mergeConfig');
var validator = require('../helpers/validator');

var validators = validator.validators;
/**
 * 创建一个Axios的新实例
 *
 * @param {Object} instanceConfig 实例的配置
 */
function Axios(instanceConfig) {
  // 存储配置和请求响应拦截器
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager(),
  };
}

/**
 * 原型上的发送请求方法
 *
 * @param {Object} config 请求时的配置(已经与默认配置合并)
 */
Axios.prototype.request = function request(config) {
  /*eslint no-param-reassign:0*/
  // 当config为字符串时,允许axios('example/url'[, config])这种风格的请求
  if (typeof config === 'string') {
    // 从arguments中取参数
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  // 合并手动和默认配置
  config = mergeConfig(this.defaults, config);

  // 设置config上的method属性,优先手动,次之默认,如果都没配置就选get
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }

  // 断言版本问题
  var transitional = config.transitional;

  if (transitional !== undefined) {
    validator.assertOptions(
      transitional,
      {
        silentJSONParsing: validators.transitional(validators.boolean, '1.0.0'),
        forcedJSONParsing: validators.transitional(validators.boolean, '1.0.0'),
        clarifyTimeoutError: validators.transitional(
          validators.boolean,
          '1.0.0'
        ),
      },
      false
    );
  }

  // 过滤拦截器
  // 设一个空数组来保存全部的请求拦截器
  var requestInterceptorChain = [];
  // 拦截是否异步的标志
  var synchronousRequestInterceptors = true;
  this.interceptors.request.forEach(function unshiftRequestInterceptors(
    interceptor
  ) {
    // 时机不对,不能添加到拦截器链中
    if (
      typeof interceptor.runWhen === 'function' &&
      interceptor.runWhen(config) === false
    ) {
      return;
    }

    // 只要有一个不是同步,就设为false
    synchronousRequestInterceptors =
      synchronousRequestInterceptors && interceptor.synchronous;
    // 保存拦截器
    requestInterceptorChain.unshift(
      interceptor.fulfilled,
      interceptor.rejected
    );
  });

  // 同样设一个空数组来保存全部的响应拦截器
  var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(
    interceptor
  ) {
    // 保存拦截器
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  var promise;

  // 如果含有异步的拦截器,则进行异步的处理方式
  if (!synchronousRequestInterceptors) {
    // 把选择适配器发送请求的方法放在chain数组的最后
    var chain = [dispatchRequest, undefined];

    // 请求拦截器放到数组前面
    Array.prototype.unshift.apply(chain, requestInterceptorChain);
    // 响应拦截器放到数组后面
    chain.concat(responseInterceptorChain);

    // 将config作为promise链的第一个
    promise = Promise.resolve(config);
    // 循环执行promise
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift());
    }

    return promise;
  }

  // 如果没有异步拦截器,则进行同步处理方式
  var newConfig = config;
  // 循环请求拦截器
  while (requestInterceptorChain.length) {
    var onFulfilled = requestInterceptorChain.shift();
    var onRejected = requestInterceptorChain.shift();
    // 执行拦截器的fulfilled
    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;
};

/**
 * @description: 通过config的配置组装url
 * @param {Object} config 配置
 * @return {String} 返回拼装成的uri
 */
Axios.prototype.getUri = function getUri(config) {
  config = mergeConfig(this.defaults, config);
  return buildURL(config.url, config.params, config.paramsSerializer).replace(
    /^\?/,
    ''
  );
};

// 给每个method添加别名,这四个方法有可能是没有data的
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,
        })
      );
    };
  }
);

// 下面三个方法有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,
      })
    );
  };
});

module.exports = Axios;

总结

本篇分析了 /core 目录下的核心工具方法,逐步接近了最核心的 Axios 类。

下一篇 Axios 源码解析(六):入口文件 来解析入口文件 axios.js,直达最终目标。