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

10次阅读

共计 11410 个字符,预计需要花费 29 分钟才能阅读完成。

前言

这应该是一个大多数都常用的请求库, 因为它可以支持多种配置, 跨平台实现, 返回 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 库的源码已经讲完了.

正文完
 0