likes
comments
collection
share

Axios源码解析(四):核心工具方法(1)

作者站长头像
站长
· 阅读数 21

上篇 Axios 源码解析(三):适配器 解析了适配器部分的源码。下面来解析核心工具方法的代码,本篇先看 default.js/cancel 目录。

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

default.js

default.js 文件中,最终导出了 10 个变量和方法,顾名思义,这 10 个导出的内容全都是为默认功能服务的:

  • transitional: 默认过渡 option 提示标志(之后的版本会被废弃)
  • adapter: 默认的适配器
  • transformRequest: 默认的请求转换器
  • transformResponse: 默认的响应转换器
  • timeout: 默认超时时间(0 代表无限制)
  • xsrfCookieName: 默认 XSRFcookie 配置
  • xsrfHeaderName: 默认 XSRFheader 配置
  • maxContentLength: 默认 content 最大长度限制
  • maxBodyLength: 默认 body 最大长度限制
  • validateStatus: 默认状态校验器(200-300 才被看作为正常响应)
  • headers: 默认的 header 配置,如下所示
defaults.headers = {
  common: {
    Accept: 'application/json, text/plain, */*',
  },
  delete: {},
  get: {},
  head: {},
  post: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  put: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  patch: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
};

default.js 的源码如下:

'use strict';

var utils = require('./utils');
var normalizeHeaderName = require('./helpers/normalizeHeaderName');
var enhanceError = require('./core/enhanceError');

// 默认的content-type类型为form-urlencoded
var DEFAULT_CONTENT_TYPE = {
  'Content-Type': 'application/x-www-form-urlencoded',
};

// 如果content-type没有设置,就设置content-type为value
function setContentTypeIfUnset(headers, value) {
  if (
    !utils.isUndefined(headers) &&
    utils.isUndefined(headers['Content-Type'])
  ) {
    headers['Content-Type'] = value;
  }
}

// 获取默认的adapter
function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // 浏览器使用 XHR adapter
    adapter = require('./adapters/xhr');
  } else if (
    typeof process !== 'undefined' &&
    Object.prototype.toString.call(process) === '[object process]'
  ) {
    // nodejs使用 HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

var defaults = {
  // 以下三个options是过渡性的
  transitional: {
    silentJSONParsing: true,
    forcedJSONParsing: true,
    clarifyTimeoutError: false,
  },

  // 选择默认的适配器
  adapter: getDefaultAdapter(),

  // 请求时转化
  transformRequest: [
    function transformRequest(data, headers) {
      // 标准化accept和content-type请求头
      normalizeHeaderName(headers, 'Accept');
      normalizeHeaderName(headers, 'Content-Type');

      // FormData、ArrayBuffer、Buffer、Stream、File、Blob类型时,直接返回data
      if (
        utils.isFormData(data) ||
        utils.isArrayBuffer(data) ||
        utils.isBuffer(data) ||
        utils.isStream(data) ||
        utils.isFile(data) ||
        utils.isBlob(data)
      ) {
        return data;
      }
      // ArrayBuffer的View类型时,返回buffer
      if (utils.isArrayBufferView(data)) {
        return data.buffer;
      }
      // URLSearchParam类型时,返回toString方法的结果
      if (utils.isURLSearchParams(data)) {
        setContentTypeIfUnset(
          headers,
          'application/x-www-form-urlencoded;charset=utf-8'
        );
        return data.toString();
      }
      // 为其他对象时或者content-type为json时,返回stringify的结果
      if (
        utils.isObject(data) ||
        (headers && headers['Content-Type'] === 'application/json')
      ) {
        setContentTypeIfUnset(headers, 'application/json');
        return JSON.stringify(data);
      }
      return data;
    },
  ],

  // 响应时转化
  transformResponse: [
    function transformResponse(data) {
      var transitional = this.transitional;
      var silentJSONParsing = transitional && transitional.silentJSONParsing;
      var forcedJSONParsing = transitional && transitional.forcedJSONParsing;
      var strictJSONParsing =
        !silentJSONParsing && this.responseType === 'json';

      // 解析响应结果
      if (
        strictJSONParsing ||
        (forcedJSONParsing && utils.isString(data) && data.length)
      ) {
        try {
          return JSON.parse(data);
        } catch (e) {
          // 报错时返回错误对象
          if (strictJSONParsing) {
            if (e.name === 'SyntaxError') {
              throw enhanceError(e, this, 'E_JSON_PARSE');
            }
            throw e;
          }
        }
      }

      return data;
    },
  ],

  /**
   * 中止请求的超时时间(以毫秒为单位)。如果设置为 0(默认),则不会限制超时。
   */
  timeout: 0,

  // XSRF的配置
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',

  // content和body的最大长度限制
  maxContentLength: -1,
  maxBodyLength: -1,

  // 校验状态,只有2开头的请求才是正常响应
  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  },
};

// 默认headers
defaults.headers = {
  common: {
    Accept: 'application/json, text/plain, */*',
  },
};

// 设置六种方法的Content-Type
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
  defaults.headers[method] = {};
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});

module.exports = defaults;

/cancel 目录

/cancel 目录的内容与主动取消 Axios 请求有关,包含三个文件。

├─cancel // 取消请求
│      Cancel.js
│      CancelToken.js
│      isCancel.js

Axioscancel token API 是基于 TC39cancelable promises proposal 提议封装的。使用时需要在配置项中手动添加 cancelToken 属性,通过执行回调函数的参数形式来实现“取消”功能。

源码分析

这块的源代码看着非常不直观,先从依赖最少的两个文件入手:

Cancel.js

首先 Cancel.js 中,实现了一个 Cancel 类,这个类有一个实例属性 message,一个原型方法 toString 来组装并返回 message,一个原型属性CANCEL来判别是否为 Cancel 类的实例。

// Cancel.js
'use strict';

/**
 * 当操作被取消时会抛出一个Cancel类的对象
 *
 * @class
 * @param {String} message 取消的消息
 */
function Cancel(message) {
  this.message = message;
}

// 给Cancel添加一个toString原型方法
Cancel.prototype.toString = function toString() {
  return 'Cancel' + (this.message ? ': ' + this.message : '');
};

// 是否为Cancel类的标志
Cancel.prototype.__CANCEL__ = true;

module.exports = Cancel;

isCancel.js

在 isCancel.js 中,实现就更加简单,返回一个 isCancel 方法,通过CANCEL这个标志来判断是否为 Cancel 实例。

'use strict';

/**
 * @description: 判断是否为Cancel类的对象
 * @param {Any} value 要判断的值
 * @return {Boolean}
 */
module.exports = function isCancel(value) {
  return !!(value && value.__CANCEL__);
};

CancelToken.js

下一步就是最让人一头雾水的 CancelToken.js,看源码之前先看官网给出的用法:

// 官方示例1
const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    cancel = c;
  }),
});

// 取消请求
cancel();

通过实例可以发现,这里用的就是一个 CancelToken 类,所以咱们先看最核心的 CancelToken 类:

// CancelToken.js
/**
 *  `CancelToken` 是被用于取消请求操作的类
 *
 * @class
 * @param {Function} executor 执行器
 */
function CancelToken(executor) {
  // 执行器的类型判断
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  // 自定义一个Promise,利用作用域拿到resolve
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  // 给执行器传参为“cancel函数”,通过闭包给到调用方
  // 最终目的是将resolvePromise通过两次闭包拿到外部,给用户来自己取消
  executor(function cancel(message) {
    if (token.reason) {
      // 如果reason属性已经有值,说明已经取消过了
      return;
    }

    // 将取消原因组成的Cancel对象 resolve出去
    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

分解为代码流:

Axios源码解析(四):核心工具方法(1)

大家可以反复理解下上面这段源码,这个地方最难的一点就是通过两层闭包将“resolve”最终拿给了外部。

剩下的两个原型方法就很容易理解了,一个是 throwIfRequested,用于抛出报错对象:

// CancelToken.js
/**
 * 把Cancel对象报错抛出去
 */
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

另一个是 source,这个其实是个工厂方法,实现的原理和上面给出的官方示例1一致:

// CancelToken.js
/**
 * 返回一个对象,对象包含新的"CancelToken"对象和一个函数,该函数在调用时取消"CancelToken"。
 */
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel,
  };
};

当然,Axios 的官方也给出了 source 的使用方法,如下:

// 官方示例2
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 {
      // 处理错误
    }
  });

axios.post(
  '/user/12345',
  {
    name: 'new name',
  },
  {
    cancelToken: source.token,
  }
);

// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');

总结

本篇先介绍了 default.js 中的 10 个默认属性,又将较难理解的 CancelToken 进行了详细分析。

下一篇 Axios 源码解析(四):核心工具方法(2)来解析另外一部分 Axios 中耦合度较高的工具方法。