axios源碼系列(四) --- Axios和dispatchRequest與攔截器

前言

這應該是一個大多數都經常使用的請求庫,由於它能夠支持多種配置,跨平臺實現,返回promise進行鏈式調用.徹底過一遍源碼能夠提高本身對請求庫的理解知識ios

axios源碼系列(一) --- 目錄結構和工具函數
axios源碼系列(二) --- 適配器內部
axios源碼系列(三) --- 默認配置和取消請求
axios源碼系列(四) --- Axios和dispatchRequest與攔截器axios

axios建立

這就是Axios庫暴露出來的使用方法segmentfault

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實例上,數組

上面可能有疑問的點在於這兩點,好像顯得畫蛇添足promise

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

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

可是在這裏的extend裏不僅僅是作擴展,還額外增長了綁定this指向的功能,因此在兩次操做實現目的不同app

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

咱們看看內部是怎麼合併配置的socket

axios/lib/core/mergeConfig.js

這是負責合併配置的方法ide

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庫的源碼已經講完了.

相關文章
相關標籤/搜索