扣丁書屋

一文讀懂Axios核心源碼思想

閱讀完本文,下面的問題會迎刃而解,

  • Axios 的適配器原理是什么?
  • Axios 是如何實現請求和響應攔截的?
  • Axios 取消請求的實現原理?
  • CSRF 的原理是什么?Axios 是如何防范客戶端 CSRF 攻擊?
  • 請求和響應數據轉換是怎么實現的?

我們以特性作為入口,解答上述問題的同時一起感受下 Axios 源碼極簡封裝的藝術。

Features

  • 從瀏覽器創建 XMLHttpRequest
  • 從 Node.js 創建 HTTP 請求
  • 支持 Promise API
  • 攔截請求與響應
  • 取消請求
  • 自動裝換 JSON 數據
  • 支持客戶端 XSRF 攻擊

前兩個特性解釋了為什么 Axios 可以同時用于瀏覽器和 Node.js 的原因,簡單來說就是通過判斷是服務器還是瀏覽器環境,來決定使用 XMLHttpRequest 還是 Node.js 的 HTTP 來創建請求,這個兼容的邏輯被叫做適配器,對應的源碼在 lib/defaults.js 中,

// defaults.js
function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

以上是適配器的判斷邏輯,通過偵測當前環境的一些全局變量,決定使用哪個 adapter。其中對于 Node 環境的判斷邏輯在我們做 ssr 服務端渲染的時候,也可以復用。接下來我們來看一下 Axios 對于適配器的封裝。

Adapter xhr

定位到源碼文件 lib/adapters/xhr.js,先來看下整體結構,

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // ...
  })
}

導出了一個函數,接受一個配置參數,返回一個 Promise。我們把關鍵的部分提取出來,

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // ...
  })
}

是不是感覺很熟悉?沒錯,這就是 XMLHttpRequest 的使用姿勢呀,先創建了一個 xhr 然后 open 啟動請求,監聽 xhr 狀態,然后 send 發送請求。我們來展開看一下 Axios 對于 onreadystatechange 的處理,

request.onreadystatechange = function handleLoad() {
  if (!request || request.readyState !== 4) {
    return;
  }

  // The request errored out and we didn't get a response, this will be
  // handled by onerror instead
  // With one exception: request that using file: protocol, most browsers
  // will return status as 0 even though it's a successful request
  if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
    return;
  }

  // Prepare the response
  var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
  var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
  var response = {
    data: responseData,
    status: request.status,
    statusText: request.statusText,
    headers: responseHeaders,
    config: config,
    request: request
  };

  settle(resolve, reject, response);

  // Clean up request
  request = null;
};

首先對狀態進行過濾,只有當請求完成時(readyState === 4)才往下處理。需要注意的是,如果 XMLHttpRequest 請求出錯,大部分的情況下我們可以通過監聽 onerror 進行處理,但是也有一個例外:當請求使用文件協議(file://)時,盡管請求成功了但是大部分瀏覽器也會返回 0 的狀態碼。

Axios 針對這個例外情況也做了處理。

請求完成后,就要處理響應了。這里將響應包裝成一個標準格式的對象,作為第三個參數傳遞給了 settle 方法,settle 在 lib/core/settle.js 中定義,

function settle(resolve, reject, response) {
  var validateStatus = response.config.validateStatus;
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    reject(createError(
      'Request failed with status code ' + response.status,
      response.config,
      null,
      response.request,
      response
    ));
  }
};

settle 對 Promise 的回調進行了簡單的封裝,確保調用按一定的格式返回。

以上就是 xhrAdapter 的主要邏輯,剩下的是對請求頭,支持的一些配置項以及超時,出錯,取消請求等回調的簡單處理,其中對于 XSRF 攻擊的防范是通過請求頭實現的。

我們先來簡單回顧下什么是 XSRF (也叫 CSRF,跨站請求偽造)。

CSRF

背景:用戶登錄后,需要存儲登錄憑證保持登錄態,而不用每次請求都發送賬號密碼。

怎么樣保持登錄態呢?

目前比較常見的方式是,服務器在收到 HTTP請求后,在響應頭里添加 Set-Cookie 選項,將憑證存儲在 Cookie 中,瀏覽器接受到響應后會存儲 Cookie,根據瀏覽器的同源策略,下次向服務器發起請求時,會自動攜帶 Cookie 配合服務端驗證從而保持用戶的登錄態。

所以如果我們沒有判斷請求來源的合法性,在登錄后通過其他網站向服務器發送了偽造的請求,這時攜帶登錄憑證的 Cookie 就會隨著偽造請求發送給服務器,導致安全漏洞,這就是我們說的 CSRF,跨站請求偽造。

所以防范偽造請求的關鍵就是檢查請求來源,refferer 字段雖然可以標識當前站點,但是不夠可靠,現在業界比較通用的解決方案還是在每個請求上附帶一個 anti-CSRF token,這個的原理是攻擊者無法拿到 Cookie,所以我們可以通過對 Cookie 進行加密(比如對 sid 進行加密),然后配合服務端做一些簡單的驗證,就可以判斷當前請求是不是偽造的。

Axios 簡單地實現了對特殊 csrf token 的支持,

// Add xsrf header
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
if (utils.isStandardBrowserEnv()) {
  // Add xsrf header
  var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
    cookies.read(config.xsrfCookieName) :
    undefined;

  if (xsrfValue) {
    requestHeaders[config.xsrfHeaderName] = xsrfValue;
  }
}

Interceptor

攔截器是 Axios 的一個特色 Feature,我們先簡單回顧下使用方式,

// Add xsrf header
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
if (utils.isStandardBrowserEnv()) {
  // Add xsrf header
  var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
    cookies.read(config.xsrfCookieName) :
    undefined;

  if (xsrfValue) {
    requestHeaders[config.xsrfHeaderName] = xsrfValue;
  }
}

那么攔截器是怎么實現的呢?

定位到源碼 lib/core/Axios.js 第 14 行,

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

通過 Axios 的構造函數可以看到,攔截器 interceptors 中的 request 和 response 兩者都是一個叫做 InterceptorManager 的實例,這個 InterceptorManager 是什么?

定位到源碼 lib/core/InterceptorManager.js,

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

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

InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

InterceptorManager 是一個簡單的事件管理器,實現了對攔截器的管理,

通過 handlers 存儲攔截器,然后提供了添加,移除,遍歷執行攔截器的實例方法,存儲的每一個攔截器對象都包含了作為 Promise 中 resolve 和 reject 的回調以及兩個配置項。

值得一提的是,移除方法是通過直接將攔截器對象設置為 null 實現的,而不是 splice 剪切數組,遍歷方法中也增加了相應的 null 值處理。這樣做一方面使得每一項ID保持為項的數組索引不變,另一方面也避免了重新剪切拼接數組的性能損失。

攔截器的回調會在請求或響應的 then 或 catch 回調前被調用,這是怎么實現的呢?

回到源碼 lib/core/Axios.js 中第 27 行,Axios 實例對象的 request 方法,

我們提取其中的關鍵邏輯如下,

Axios.prototype.request = function request(config) {
  // Get merged config
  // Set config.method
  // ...
  var requestInterceptorChain = [];
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

 var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  var promise;

  var chain = [dispatchRequest, undefined];

  Array.prototype.unshift.apply(chain, requestInterceptorChain);

  chain.concat(responseInterceptorChain);

  promise = Promise.resolve(config);

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

可以看到,當執行 request 時,實際的請求(dispatchRequest)和攔截器是通過一個叫 chain 的隊列來管理的。整個請求的邏輯如下,

  1. 首先初始化請求和響應的攔截器隊列,將 resolve,reject 回調依次放入隊頭
  2. 然后初始化一個 Promise 用來執行回調,chain 用來存儲和管理實際請求和攔截器
  3. 將請求攔截器放入 chain 隊頭,響應攔截器放入 chain 隊尾
  4. 隊列不為空時,通過 Promise.then 的鏈式調用,依次將請求攔截器,實際請求,響應攔截器出隊
  5. 最后返回鏈式調用后的 Promise

這里的實際請求是對適配器的封裝,請求和響應數據的轉換都在這里完成。

那么數據轉換是如何實現的呢?

Transform data

定位到源碼 lib/core/dispatchRequest.js,

Axios.prototype.request = function request(config) {
  // Get merged config
  // Set config.method
  // ...
  var requestInterceptorChain = [];
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

 var responseInterceptorChain = [];
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
  });

  var promise;

  var chain = [dispatchRequest, undefined];

  Array.prototype.unshift.apply(chain, requestInterceptorChain);

  chain.concat(responseInterceptorChain);

  promise = Promise.resolve(config);

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

這里的 throwIfCancellationRequested 方法用于取消請求,關于取消請求稍后我們再討論,可以看到發送請求是通過調用適配器實現的,在調用前和調用后會對請求和響應數據進行轉換。

轉換通過 transformData 函數實現,它會遍歷調用設置的轉換函數,轉換函數將 headers 作為第二個參數,所以我們可以根據 headers 中的信息來執行一些不同的轉換操作,

// 源碼 core/transformData.js
function transformData(data, headers, fns) {
  utils.forEach(fns, function transform(fn) {
    data = fn(data, headers);
  });

  return data;
};

Axios 也提供了兩個默認的轉換函數,用于對請求和響應數據進行轉換。默認情況下,

Axios 會對請求傳入的 data 做一些處理,比如請求數據如果是對象,會序列化為 JSON 字符串,響應數據如果是 JSON 字符串,會嘗試轉換為 JavaScript 對象,這些都是非常實用的功能,

對應的轉換器源碼可以在 lib/default.js 的第 31 行找到,

var defaults = {
 // Line 31
  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) {
    var result = data;
    if (utils.isString(result) && result.length) {
      try {
        result = JSON.parse(result);
      } catch (e) { /* Ignore */ }
    }
    return result;
  }],
}

我們說 Axios 是支持取消請求的,怎么個取消法呢?

CancelToken

其實不管是瀏覽器端的 xhr 或 Node.js 里 http 模塊的 request 對象,都提供了 abort 方法用于取消請求,所以我們只需要在合適的時機調用 abort 就可以實現取消請求了。

那么,什么是合適的時機呢?控制權交給用戶就合適了。所以這個合適的時機應該由用戶決定,也就是說我們需要將取消請求的方法暴露出去,Axios 通過 CancelToken 實現取消請求,我們來一起看下它的姿勢。

首先 Axios 提供了兩種方式創建 cancel token,

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

// 方式一,使用 CancelToken 實例提供的靜態屬性 source
axios.post("/user/12345", { name: "monch" }, { cancelToken: source.token });
source.cancel();

// 方式二,使用 CancelToken 構造函數自己實例化
let cancel;

axios.post(
  "/user/12345",
  { name: "monch" },
  {
    cancelToken: new CancelToken(function executor(c) {
      cancel = c;
    }),
  }
);

cancel();

到底什么是 CancelToken?定位到源碼 lib/cancel/CancelToken.js 第 11 行,

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

// 方式一,使用 CancelToken 實例提供的靜態屬性 source
axios.post("/user/12345", { name: "monch" }, { cancelToken: source.token });
source.cancel();

// 方式二,使用 CancelToken 構造函數自己實例化
let cancel;

axios.post(
  "/user/12345",
  { name: "monch" },
  {
    cancelToken: new CancelToken(function executor(c) {
      cancel = c;
    }),
  }
);

cancel();

CancelToken 就是一個由 promise 控制的極簡的狀態機,實例化時會在實例上掛載一個 promise,這個 promise 的 resolve 回調暴露給了外部方法 executor,這樣一來,我們從外部調用這個 executor方法后就會得到一個狀態變為 fulfilled 的 promise,那有了這個 promise 后我們如何取消請求呢?

是不是只要在請求時拿到這個 promise 實例,然后在 then 回調里取消請求就可以了?

定位到適配器的源碼 lib/adapters/xhr.js 第 158 行,

if (config.cancelToken) {
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }

    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}

以及源碼 lib/adaptors/http.js 第 291 行,

if (config.cancelToken) {
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (req.aborted) return;

    req.abort();
    reject(cancel);
  });
}

果然如此,在適配器里 CancelToken 實例的 promise 的 then 回調里調用了 xhr 或 http.request 的 abort 方法。試想一下,如果我們沒有從外部調用取消 CancelToken 的方法,是不是意味著 resolve 回調不會執行,適配器里的 promise 的 then 回調也不會執行,就不會調用 abort 取消請求了。

小結

Axios 通過適配器的封裝,使得它可以在保持同一套接口規范的前提下,同時用在瀏覽器和 node.js 中。源碼中大量使用 Promise 和閉包等特性,實現了一系列的狀態控制,其中對于攔截器,取消請求的實現體現了其極簡的封裝藝術,值得學習和借鑒。

參考鏈接

  • Axios Docs - axios-http.com[1]
  • Axios Github Source Code[2]
  • 源碼拾遺 Axios —— 極簡封裝的藝術[3]
  • Cross Site Request Forgery - Part III. Web Application Security[4]
  • tc39/proposal-cancelable-promises[5]

https://mp.weixin.qq.com/s/mq6LzZNgs9KZ7OA8w8dbuw

最多閱讀

iOS 性能檢測新方式?——AnimationHitches 1年以前  |  22531次閱讀
快速配置 Sign In with Apple 3年以前  |  6305次閱讀
APP適配iOS11 4年以前  |  5059次閱讀
App Store 審核指南[2017年最新版本] 4年以前  |  4899次閱讀
所有iPhone設備尺寸匯總 4年以前  |  4796次閱讀
使用 GPUImage 實現一個簡單相機 3年以前  |  4561次閱讀
開篇 關于iOS越獄開發 4年以前  |  4186次閱讀
在越獄的iPhone設置上使用lldb調試 4年以前  |  4093次閱讀
給數組NSMutableArray排序 4年以前  |  4009次閱讀
使用ssh訪問越獄iPhone的兩種方式 4年以前  |  3708次閱讀
UITableViewCell高亮效果實現 4年以前  |  3705次閱讀
關于Xcode不能打印崩潰日志 4年以前  |  3632次閱讀
iOS虛擬定位原理與預防 1年以前  |  3629次閱讀
使用ssh 訪問越獄iPhone的兩種方式 4年以前  |  3447次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>