Axios 原始碼解讀

叫我小明呀發表於2019-04-16

Axios 是什麼?

Axios 是一個基於 promise 的 HTTP 庫,可以用在瀏覽器和 node.js 中。

Axios 功能

  • 從瀏覽器中建立 XMLHttpRequests
  • 從 node.js 建立 http 請求
  • 支援 Promise API
  • 攔截請求和響應
  • 轉換請求資料和響應資料
  • 取消請求
  • 自動轉換 JSON 資料
  • 客戶端支援防禦 XSRF

希望通過原始碼來慢慢理清這些功能的實現原理

Axios 使用

執行 GET 請求

axios.get('/user?ID=12345')
  .then(function (response) {
    console.log(response);
  })
複製程式碼

執行 POST 請求

axios.post('/user', {
    name: 'zxm',
    age: 18,
  })
  .then(function (response) {
    console.log(response);
  })
複製程式碼

使用方式不是本次主題的重點,具體使用方式可以參照 Axios 中文說明

原始碼拉下來直接進入 lib 資料夾開始解讀原始碼

原始碼解讀

lib/ axios.js 開始

'use strict';

var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var mergeConfig = require('./core/mergeConfig');
var defaults = require('./defaults');

// 重點 createInstance 方法
// 先眼熟一個程式碼 下面講完工具函式會再具體來講解 createInstance
function createInstance(defaultConfig) {
    // 例項化 Axios
  var context = new Axios(defaultConfig);
    // 自定義 bind 方法 返回一個函式()=> {Axios.prototype.request.apply(context,args)}
  var instance = bind(Axios.prototype.request, context);
    // Axios 原始碼的工具類 
  utils.extend(instance, Axios.prototype, context);
    
  utils.extend(instance, context);
    
  return instance;
}
// 傳入一個預設配置   defaults 配置先不管,後面會有具體的細節
var axios = createInstance(defaults);


// 下面都是為 axios 例項化的物件增加不同的方法。
axios.Axios = Axios;
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
module.exports = axios;
module.exports.default = axios;
複製程式碼

lib/ util.js 工具方法

有如下方法:

module.exports = {
  isArray: isArray,
  isArrayBuffer: isArrayBuffer,
  isBuffer: isBuffer,
  isFormData: isFormData,
  isArrayBufferView: isArrayBufferView,
  isString: isString,
  isNumber: isNumber,
  isObject: isObject,
  isUndefined: isUndefined,
  isDate: isDate,
  isFile: isFile,
  isBlob: isBlob,
  isFunction: isFunction,
  isStream: isStream,
  isURLSearchParams: isURLSearchParams,
  isStandardBrowserEnv: isStandardBrowserEnv,
  forEach: forEach,
  merge: merge,
  deepMerge: deepMerge,
  extend: extend,
  trim: trim
};
複製程式碼

is開頭的isXxx方法名 都是判斷是否是 Xxx 型別 ,這裡就不做明說 主要是看下 後面幾個方法

extend 將 b 裡面的屬性和方法繼承給 a , 並且將 b 裡面的方法的執行上個下文都繫結到 thisArg

// a, b,thisArg 引數都為一個物件
function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
      // 如果指定了 thisArg 那麼繫結執行上下文到 thisArg
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  });
  return a;
}
複製程式碼

抽象的話看個例子

Axios 原始碼解讀

這樣是不是就一目瞭然。fn2 函式沒有拿自己物件內的 age = 20 而是被指定到了 thisArg 中的 age

自定義 forEach 方法遍歷基本資料,陣列,物件。

function forEach(obj, fn) {
  if (obj === null || typeof obj === 'undefined') {
    return;
  }
  if (typeof obj !== 'object') {
    obj = [obj];
  }
  if (isArray(obj)) {
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}
複製程式碼

merge 合併物件的屬性,相同屬性後面的替換前的

function merge(/* obj1, obj2, obj3, ... */) {
  var result = {};
  function assignValue(val, key) {
    if (typeof result[key] === 'object' && typeof val === 'object') {
      result[key] = merge(result[key], val);
    } else {
      result[key] = val;
    }
  }

  for (var i = 0, l = arguments.length; i < l; i++) {
    forEach(arguments[i], assignValue);
  }
  return result;
}
複製程式碼

如下圖所示:

Axios 原始碼解讀

bind -> lib/ helpers/ bind.js 這個很清楚,返回一個函式,並且傳入的方法執行上下文繫結到 thisArg上。

module.exports = function bind(fn, thisArg) {
  return function wrap() {
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    return fn.apply(thisArg, args);
  };
};
複製程式碼

好勒那麼 axios/util 方法我們就基本沒有問題拉

看完這些工具類方法後我們在回過頭看之前的 createInstance 方法

function createInstance(defaultConfig) {
    // 例項化 Axios, Axios下面會講到
  var context = new Axios(defaultConfig);
    
    // 將 Axios.prototype.request 的執行上下文繫結到 context
    // bind 方法返回的是一個函式
  var instance = bind(Axios.prototype.request, context);
    
    // 將 Axios.prototype 上的所有方法的執行上下文繫結到 context , 並且繼承給 instance
  utils.extend(instance, Axios.prototype, context);
    
    // 將 context 繼承給 instance
  utils.extend(instance, context);
    
  return instance;
}
// 傳入一個預設配置  
var axios = createInstance(defaults);
複製程式碼

總結:createInstance 函式返回了一個函式 instance.

  1. instance 是一個函式 Axios.prototype.request 且執行上下文繫結到 context。
  2. instance 裡面還有 Axios.prototype 上面的所有方法,並且這些方法的執行上下文也繫結到 context。
  3. instance 裡面還有 context 上的方法。

Axios 例項原始碼

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

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

// 核心方法 request 
Axios.prototype.request = function request(config) {
  // ... 單獨講
};

// 合併配置將使用者的配置 和預設的配置合併
Axios.prototype.getUri = function getUri(config) {
  config = mergeConfig(this.defaults, config);
  return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
};
// 這個就是給 Axios.prototype 上面增加 delete,get,head,options 方法
// 這樣我們就可以使用 axios.get(), axios.post() 等等方法
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
     // 都是呼叫了 this.request 方法
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

module.exports = Axios;

複製程式碼

上面的所有的方法都是通過呼叫了 this.request 方法

那麼我們就來看這個 request 方法,個人認為是原始碼內的精華也是比較難理解的部分,使用到了 Promise 的鏈式呼叫,也使用到了中介軟體的思想。

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
Axios.prototype.request = function request(config) {
  // 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);
    // 請求方式,沒有預設為 get
  config.method = config.method ? config.method.toLowerCase() : 'get';
    
    // 重點 這個就是攔截器的中介軟體
  var chain = [dispatchRequest, undefined];
    // 生成一個 promise 物件
  var promise = Promise.resolve(config);

    // 將請求前方法置入 chain 陣列的前面 一次置入兩個 成功的,失敗的
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
	// 將請求後的方法置入 chain 陣列的後面 一次置入兩個 成功的,失敗的
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

   // 通過 shift 方法把第一個元素從其中刪除,並返回第一個元素。
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};
複製程式碼

看到這裡有點抽象,沒關係。我們先講下攔截器。在請求或響應被 then 或 catch 處理前攔截它們。使用方法參考 Axios 中文說明 ,大致使用如下。

// 新增請求攔截器
axios.interceptors.request.use(function (config) {
    // 在傳送請求之前做些什麼
    return config;
  }, function (error) {
    // 對請求錯誤做些什麼
    return Promise.reject(error);
  });

// 新增響應攔截器
axios.interceptors.response.use(function (response) {
    // 對響應資料做點什麼
    return response;
  }, function (error) {
    // 對響應錯誤做點什麼
    return Promise.reject(error);
  });
複製程式碼

通過 promise 鏈式呼叫一個一個函式,這個函式就是 chain 陣列裡面的方法

// 初始的 chain 陣列 dispatchRequest 是傳送請求的方法
var chain = [dispatchRequest, undefined];

// 然後 遍歷 interceptors 
// 注意 這裡的 forEach 不是 Array.forEach, 也不是上面講到的 util.forEach. 具體 攔截器原始碼 會講到
// 現在我們只要理解他是遍歷給 chain 裡面追加兩個方法就可以
  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);
  });

複製程式碼

然後新增了請求攔截器和相應攔截器 chain 會是什麼樣子呢 (重點)

chain = [ 請求攔截器的成功方法,請求攔截器的失敗方法,dispatchRequest, undefined, 響應攔截器的成功方法,響應攔截器的失敗方法 ]。
複製程式碼

好了,知道具體使用使用之後是什麼樣子呢?回過頭去看 request 方法

每次請求的時候我們有一個

 while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
   意思就是將 chainn 內的方法兩兩拿出來執行 成如下這樣
    promise.then(請求攔截器的成功方法, 請求攔截器的失敗方法)
           .then(dispatchRequest, undefined)
           .then(響應攔截器的成功方法, 響應攔截器的失敗方法)
複製程式碼

現在看是不是清楚了很多,攔截器的原理。現在我們再來看 InterceptorManager 的原始碼

InterceptorManager 攔截器原始碼

lib/ core/ InterceptorManager.js

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

function InterceptorManager() {
    // 存放方法的陣列
  this.handlers = [];
}
// 通過 use 方法來新增攔截方法
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};
// 通過 eject 方法來刪除攔截方法
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};
// 新增一個 forEach 方法,這就是上述說的 forEach
InterceptorManager.prototype.forEach = function forEach(fn) {
    // 裡面還是依舊使用了 utils 的 forEach, 不要糾結這些 forEach 的具體程式碼
    // 明白他們幹了什麼就可以
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

module.exports = InterceptorManager;

複製程式碼

dispatchRequest 原始碼

lib/ core/ dispatchRequest .js

'use strict';
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
var isAbsoluteURL = require('./../helpers/isAbsoluteURL');
var combineURLs = require('./../helpers/combineURLs');
// 請求取消時候的方法,暫不看
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);
    // 請求沒有取消 執行下面的請求
  if (config.baseURL && !isAbsoluteURL(config.url)) {
    config.url = combineURLs(config.baseURL, config.url);
  }
  config.headers = config.headers || {};
	// 轉換資料
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );
    // 合併配置
  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);
	// 難道了請求的資料, 轉換 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);
  });
};

複製程式碼

看了這麼多,我們還沒看到是通過什麼來傳送請求的,現在我們看看在最開始例項化 createInstance 方法中我們傳入的 defaults 是什麼

var axios = createInstance(defaults);

lib/ defaults.js

'use strict';

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

var DEFAULT_CONTENT_TYPE = {
  'Content-Type': 'application/x-www-form-urlencoded'
};

function setContentTypeIfUnset(headers, value) {
  if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
    headers['Content-Type'] = value;
  }
}
// getDefaultAdapter 方法是來獲取請求的方式
function getDefaultAdapter() {
  var adapter;
  // process 是 node 環境的全域性變數
  if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // 如果是 node 環境那麼久通過 node http 的請求方法
    adapter = require('./adapters/http');
  } else if (typeof XMLHttpRequest !== 'undefined') {
   // 如果是瀏覽器啥的 有 XMLHttpRequest 的就用 XMLHttpRequest
    adapter = require('./adapters/xhr');
  }
  return adapter;
}

var defaults = {
    // adapter 就是請求的方法
  adapter: getDefaultAdapter(),
	// 下面一些請求頭,轉換資料,請求,詳情的資料
    // 這也就是為什麼我們可以直接拿到請求的資料時一個物件,如果用 ajax 我們拿到的都是 jSON 格式的字串
    // 然後每次都通過 JSON.stringify(data)來處理結果。
  transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type');
    if (utils.isFormData(data) ||
      utils.isArrayBuffer(data) ||
      utils.isBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
      return data;
    }
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    if (utils.isObject(data)) {
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
  }],

  transformResponse: [function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }],

  /**
   * A timeout in milliseconds to abort a request. If set to 0 (default) a
   * timeout is not created.
   */
  timeout: 0,

  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',

  maxContentLength: -1,

  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  }
};

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

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;

複製程式碼

總結

  1. Axios 的原始碼走讀一遍確實可以看到和學習到很多的東西。
  2. Axios 還有一些功能:請求的取消,請求超時的處理。這裡我沒有全部說明。
  3. Axios 通過在請求中新增 toke 並驗證方法,讓客戶端支援防禦 XSRF Django CSRF 原理分析

最後

如果看的還不是很明白,不用擔心,這基本上是我表達,書寫的不夠好。因為在寫篇文章時我也曾反覆的刪除,重寫,總覺得表達的不夠清楚。為了加強理解和學習大家可以去 github 將程式碼拉下來對照著來看。

git clone https://github.com/axios/axios.git

全文章,如有錯誤或不嚴謹的地方,請務必給予指正,謝謝!

參考:

相關文章