Axios 是一個基於 Promise 的 HTTP 客戶端,同時支持瀏覽器和 Node.js 環境。它是一個優秀的 HTTP 客戶端,被普遍地應用在大量的 Web 項目中。javascript
由上圖可知,Axios 項目的 Star 數爲 77.9K,Fork 數也高達 7.3K,是一個很優秀的開源項目,因此接下來阿寶哥將帶你們一塊兒來分析 Axios 項目中一些值得借鑑的地方。閱讀完本文,你將瞭解如下內容:html
下面咱們從簡單的開始,先來了解一下 Axios。java
Axios 是一個基於 Promise 的 HTTP 客戶端,擁有如下特性:node
在瀏覽器端 Axios 支持大多數主流的瀏覽器,好比 Chrome、Firefox、Safari 和 IE 11。此外,Axios 還擁有本身的生態:ios
(數據來源 —— https://github.com/axios/axio...)git
簡單介紹完 Axios,咱們來分析一下它提供的一個核心功能 —— 攔截器。github
對於大多數 SPA 應用程序來講, 一般會使用 token 進行用戶的身份認證。這就要求在認證經過後,咱們須要在每一個請求上都攜帶認證信息。針對這個需求,爲了不爲每一個請求單獨處理,咱們能夠經過封裝統一的 request
函數來爲每一個請求統一添加 token 信息。ajax
但後期若是須要爲某些 GET 請求設置緩存時間或者控制某些請求的調用頻率的話,咱們就須要不斷修改 request
函數來擴展對應的功能。此時,若是在考慮對響應進行統一處理的話,咱們的 request
函數將變得愈來愈龐大,也愈來愈難維護。那麼對於這個問題,該如何解決呢?Axios 爲咱們提供瞭解決方案 —— 攔截器。axios
Axios 是一個基於 Promise 的 HTTP 客戶端,而 HTTP 協議是基於請求和響應:數組
因此 Axios 提供了請求攔截器和響應攔截器來分別處理請求和響應,它們的做用以下:
在 Axios 中設置攔截器很簡單,經過 axios.interceptors.request
和 axios.interceptors.response
對象提供的 use
方法,就能夠分別設置請求攔截器和響應攔截器:
// 添加請求攔截器 axios.interceptors.request.use(function (config) { config.headers.token = 'added by interceptor'; return config; }); // 添加響應攔截器 axios.interceptors.response.use(function (data) { data.data = data.data + ' - modified by interceptor'; return data; });
那麼攔截器是如何工做的呢?在看具體的代碼以前,咱們先來分析一下它的設計思路。Axios 的做用是用於發送 HTTP 請求,而請求攔截器和響應攔截器的本質都是一個實現特定功能的函數。
咱們能夠按照功能把發送 HTTP 請求拆解成不一樣類型的子任務,好比有用於處理請求配置對象的子任務,用於發送 HTTP 請求的子任務和用於處理響應對象的子任務。當咱們按照指定的順序來執行這些子任務時,就能夠完成一次完整的 HTTP 請求。
瞭解完這些,接下來咱們將從 任務註冊、任務編排和任務調度 三個方面來分析 Axios 攔截器的實現。
經過前面攔截器的使用示例,咱們已經知道如何註冊請求攔截器和響應攔截器,其中請求攔截器用於處理請求配置對象的子任務,而響應攔截器用於處理響應對象的子任務。要搞清楚任務是如何註冊的,就須要瞭解 axios
和 axios.interceptors
對象。
// lib/axios.js 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
對象的定義,很明顯默認的 axios
實例是經過 createInstance
方法建立的,該方法最終返回的是 Axios.prototype.request
函數對象。同時,咱們發現了 Axios
的構造函數:
// lib/core/Axios.js function Axios(instanceConfig) { this.defaults = instanceConfig; this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() }; }
在構造函數中,咱們找到了 axios.interceptors
對象的定義,也知道了 interceptors.request
和 interceptors.response
對象都是 InterceptorManager
類的實例。所以接下來,進一步分析 InterceptorManager
構造函數及相關的 use
方法就能夠知道任務是如何註冊的:
// lib/core/InterceptorManager.js function InterceptorManager() { this.handlers = []; } InterceptorManager.prototype.use = function use(fulfilled, rejected) { this.handlers.push({ fulfilled: fulfilled, rejected: rejected }); // 返回當前的索引,用於移除已註冊的攔截器 return this.handlers.length - 1; };
經過觀察 use
方法,咱們可知註冊的攔截器都會被保存到 InterceptorManager
對象的 handlers
屬性中。下面咱們用一張圖來總結一下 Axios
對象與 InterceptorManager
對象的內部結構與關係:
如今咱們已經知道如何註冊攔截器任務,但僅僅註冊任務是不夠,咱們還須要對已註冊的任務進行編排,這樣才能確保任務的執行順序。這裏咱們把完成一次完整的 HTTP 請求分爲處理請求配置對象、發起 HTTP 請求和處理響應對象 3 個階段。
接下來咱們來看一下 Axios 如何發請求的:
axios({ url: '/hello', method: 'get', }).then(res =>{ console.log('axios res: ', res) console.log('axios res.data: ', res.data) })
經過前面的分析,咱們已經知道 axios
對象對應的是 Axios.prototype.request
函數對象,該函數的具體實現以下:
// lib/core/Axios.js Axios.prototype.request = function request(config) { config = mergeConfig(this.defaults, config); // 省略部分代碼 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; };
任務編排的代碼比較簡單,咱們來看一下任務編排前和任務編排後的對比圖:
任務編排完成後,要發起 HTTP 請求,咱們還須要按編排後的順序執行任務調度。在 Axios 中具體的調度方式很簡單,具體以下所示:
// lib/core/Axios.js Axios.prototype.request = function request(config) { // 省略部分代碼 var promise = Promise.resolve(config); while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } }
由於 chain 是數組,因此經過 while 語句咱們就能夠不斷地取出設置的任務,而後組裝成 Promise 調用鏈從而實現任務調度,對應的處理流程以下圖所示:
下面咱們來回顧一下 Axios 攔截器完整的使用流程:
// 添加請求攔截器 —— 處理請求配置對象 axios.interceptors.request.use(function (config) { config.headers.token = 'added by interceptor'; return config; }); // 添加響應攔截器 —— 處理響應對象 axios.interceptors.response.use(function (data) { data.data = data.data + ' - modified by interceptor'; return data; }); axios({ url: '/hello', method: 'get', }).then(res =>{ console.log('axios res.data: ', res.data) })
介紹完 Axios 的攔截器,咱們來總結一下它的優勢。Axios 經過提供攔截器機制,讓開發者能夠很容易在請求的生命週期中自定義不一樣的處理邏輯。此外,也能夠經過攔截器機制來靈活地擴展 Axios 的功能,好比 Axios 生態中列舉的 axios-response-logger 和 axios-debug-log 這兩個庫。
參考 Axios 攔截器的設計模型,咱們就能夠抽出如下通用的任務處理模型:
Axios 同時支持瀏覽器和 Node.js 環境,對於瀏覽器環境來講,咱們能夠經過 XMLHttpRequest
或 fetch
API 來發送 HTTP 請求,而對於 Node.js 環境來講,咱們能夠經過 Node.js 內置的 http
或 https
模塊來發送 HTTP 請求。
爲了支持不一樣的環境,Axios 引入了適配器。在 HTTP 攔截器設計部分,咱們看到了一個 dispatchRequest
方法,該方法用於發送 HTTP 請求,它的具體實現以下所示:
// lib/core/dispatchRequest.js module.exports = function dispatchRequest(config) { // 省略部分代碼 var adapter = config.adapter || defaults.adapter; return adapter(config).then(function onAdapterResolution(response) { // 省略部分代碼 return response; }, function onAdapterRejection(reason) { // 省略部分代碼 return Promise.reject(reason); }); };
經過查看以上的 dispatchRequest
方法,咱們可知 Axios 支持自定義適配器,同時也提供了默認的適配器。對於大多數場景,咱們並不須要自定義適配器,而是直接使用默認的適配器。所以,默認的適配器就會包含瀏覽器和 Node.js 環境的適配代碼,其具體的適配邏輯以下所示:
// lib/defaults.js var defaults = { adapter: getDefaultAdapter(), xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', //... } 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; }
在 getDefaultAdapter
方法中,首先經過平臺中特定的對象來區分不一樣的平臺,而後再導入不一樣的適配器,具體的代碼比較簡單,這裏就不展開介紹。
其實除了默認的適配器外,咱們還能夠自定義適配器。那麼如何自定義適配器呢?這裏咱們能夠參考 Axios 提供的示例:
var settle = require('./../core/settle'); module.exports = function myAdapter(config) { // 當前時機點: // - config配置對象已經與默認的請求配置合併 // - 請求轉換器已經運行 // - 請求攔截器已經運行 // 使用提供的config配置對象發起請求 // 根據響應對象處理Promise的狀態 return new Promise(function(resolve, reject) { var response = { data: responseData, status: request.status, statusText: request.statusText, headers: responseHeaders, config: config, request: request }; settle(resolve, reject, response); // 此後: // - 響應轉換器將會運行 // - 響應攔截器將會運行 }); }
在以上示例中,咱們主要關注轉換器、攔截器的運行時機點和適配器的基本要求。好比當調用自定義適配器以後,須要返回 Promise 對象。這是由於 Axios 內部是經過 Promise 鏈式調用來完成請求調度,不清楚的小夥伴能夠從新閱讀 「攔截器的設計與實現」 部分的內容。
如今咱們已經知道如何自定義適配器了,那麼自定義適配器有什麼用呢?在 Axios 生態中,阿寶哥發現了 axios-mock-adapter 這個庫,該庫經過自定義適配器,讓開發者能夠輕鬆地模擬請求。對應的使用示例以下所示:
var axios = require("axios"); var MockAdapter = require("axios-mock-adapter"); // 在默認的Axios實例上設置mock適配器 var mock = new MockAdapter(axios); // 模擬 GET /users 請求 mock.onGet("/users").reply(200, { users: [{ id: 1, name: "John Smith" }], }); axios.get("/users").then(function (response) { console.log(response.data); });
對 MockAdapter 感興趣的小夥伴,能夠自行了解一下 axios-mock-adapter 這個庫。到這裏咱們已經介紹了 Axios 的攔截器與適配器,下面阿寶哥用一張圖來總結一下 Axios 使用請求攔截器和響應攔截器後,請求的處理流程:
跨站請求僞造(Cross-site request forgery),一般縮寫爲 CSRF 或者 XSRF, 是一種挾制用戶在當前已登陸的 Web 應用程序上執行非本意的操做的攻擊方法。
跨站請求攻擊,簡單地說,是攻擊者經過一些技術手段欺騙用戶的瀏覽器去訪問一個本身曾經認證過的網站並運行一些操做(如發郵件,發消息,甚至財產操做如轉帳和購買商品)。因爲瀏覽器曾經認證過,因此被訪問的網站會認爲是真正的用戶操做而去運行。
爲了讓小夥伴更好地理解上述的內容,阿寶哥畫了一張跨站請求攻擊示例圖:
在上圖中攻擊者利用了 Web 中用戶身份驗證的一個漏洞:簡單的身份驗證只能保證請求發自某個用戶的瀏覽器,卻不能保證請求自己是用戶自願發出的。既然存在以上的漏洞,那麼咱們應該怎麼進行防護呢?接下來咱們來介紹一些常見的 CSRF 防護措施。
HTTP 頭中有一個 Referer 字段,這個字段用以標明請求來源於哪一個地址。在處理敏感數據請求時,一般來講,Referer 字段應和請求的地址位於同一域名下。
以示例中商城操做爲例,Referer 字段地址一般應該是商城所在的網頁地址,應該也位於 www.semlinker.com 之下。而若是是 CSRF 攻擊傳來的請求,Referer 字段會是包含惡意網址的地址,不會位於 www.semlinker.com 之下,這時候服務器就能識別出惡意的訪問。
這種辦法簡單易行,僅須要在關鍵訪問處增長一步校驗。但這種辦法也有其侷限性,因其徹底依賴瀏覽器發送正確的 Referer 字段。雖然 HTTP 協議對此字段的內容有明確的規定,但並沒有法保證來訪的瀏覽器的具體實現,亦沒法保證瀏覽器沒有安全漏洞影響到此字段。而且也存在攻擊者攻擊某些瀏覽器,篡改其 Referer 字段的可能。
CSRF 攻擊之因此可以成功,是由於服務器沒法區分正常請求和攻擊請求。針對這個問題咱們能夠要求全部的用戶請求都攜帶一個 CSRF 攻擊者沒法獲取到的 token。對於 CSRF 示例圖中的表單攻擊,咱們可使用 同步表單 CSRF 校驗 的防護措施。
同步表單 CSRF 校驗 就是在返回頁面時將 token 渲染到頁面上,在 form 表單提交的時候經過隱藏域或者做爲查詢參數把 CSRF token 提交到服務器。好比,在同步渲染頁面時,在表單請求中增長一個 _csrf
的查詢參數,這樣當用戶在提交這個表單的時候就會將 CSRF token 提交上來:
<form method="POST" action="/upload?_csrf={{由服務端生成}}" enctype="multipart/form-data"> 用戶名: <input name="name" /> 選擇頭像: <input name="file" type="file" /> <button type="submit">提交</button> </form>
雙重 Cookie 防護 就是將 token 設置在 Cookie 中,在提交(POST、PUT、PATCH、DELETE)等請求時提交 Cookie,並經過請求頭或請求體帶上 Cookie 中已設置的 token,服務端接收到請求後,再進行對比校驗。
下面咱們以 jQuery 爲例,來看一下如何設置 CSRF token:
let csrfToken = Cookies.get('csrfToken'); function csrfSafeMethod(method) { // 如下HTTP方法不須要進行CSRF防禦 return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } $.ajaxSetup({ beforeSend: function(xhr, settings) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) { xhr.setRequestHeader('x-csrf-token', csrfToken); } }, });
介紹完 CSRF 攻擊的方式和防護手段,最後咱們來看一下 Axios 是如何防護 CSRF 攻擊的。
Axios 提供了 xsrfCookieName
和 xsrfHeaderName
兩個屬性來分別設置 CSRF 的 Cookie 名稱和 HTTP 請求頭的名稱,它們的默認值以下所示:
// lib/defaults.js var defaults = { adapter: getDefaultAdapter(), // 省略部分代碼 xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', };
前面咱們已經知道在不一樣的平臺中,Axios 使用不一樣的適配器來發送 HTTP 請求,這裏咱們以瀏覽器平臺爲例,來看一下 Axios 如何防護 CSRF 攻擊:
// lib/adapters/xhr.js module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { var requestHeaders = config.headers; var request = new XMLHttpRequest(); // 省略部分代碼 // 添加xsrf頭部 if (utils.isStandardBrowserEnv()) { var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ? cookies.read(config.xsrfCookieName) : undefined; if (xsrfValue) { requestHeaders[config.xsrfHeaderName] = xsrfValue; } } request.send(requestData); }); };
看完以上的代碼,相信小夥伴們就已經知道答案了,原來 Axios 內部是使用 雙重 Cookie 防護 的方案來防護 CSRF 攻擊。好的,到這裏本文的主要內容都已經介紹完了,其實 Axios 項目還有一些值得咱們借鑑的地方,好比 CancelToken 的設計、異常處理機制等,感興趣的小夥伴能夠自行學習一下。