這是學習源碼總體架構系列第六篇。總體架構這詞語好像有點大,姑且就算是源碼總體結構吧,主要就是學習是代碼總體結構,不深究其餘不是主線的具體函數的實現。本篇文章學習的是實際倉庫的代碼。javascript
感興趣的讀者能夠點擊閱讀。下一篇多是vue-router源碼。php
本文比較長,手機上閱讀,能夠滑到有圖的地方直接看文中的幾張圖便可。建議點贊或收藏後在電腦上閱讀,按照文中調試方式本身調試或許更容易吸取消化。html
導讀
文章詳細介紹了 axios 調試方法。詳細介紹了 axios 構造函數,攔截器,取消等功能的實現。最後還對比了其餘請求庫。前端
本文學習的版本是v0.19.0。克隆的官方倉庫的master分支。截至目前(2019 年 12 月 14 日),最新一次commit是2019-12-09 15:52 ZhaoXC dc4bc49673943e352,fix: fix ignore set withCredentials false (#2582)。vue
本文倉庫在這裏若川的 axios-analysis github 倉庫。求個star呀。java
若是你是求職者,項目寫了運用了axios,面試官可能會問你:node
1.爲何 axios 既能夠當函數調用,也能夠當對象使用,好比axios({})、axios.get。
2.簡述 axios 調用流程。
3.有用過攔截器嗎?原理是怎樣的?
4.有使用axios的取消功能嗎?是怎麼實現的?
5.爲何支持瀏覽器中發送請求也支持node發送請求?
諸如這類問題。webpack
前不久,筆者在知乎回答了一個問題,推薦了一些資料,閱讀量還不錯,你們有興趣能夠看看。主要有四點:ios
1.藉助調試
2.搜索查閱相關高贊文章
3.把不懂的地方記錄下來,查閱相關文檔
4.總結git
看源碼,調試很重要,因此筆者詳細寫下 axios 源碼調試方法,幫助一些可能不知道如何調試的讀者。
調試方法
axios打包後有sourcemap文件。
# 能夠克隆筆者的這個倉庫代碼 git clone https://github.com/lxchuan12/axios-analysis.git cd axios-analaysis/axios npm install npm start # open [http://localhost:3000](http://localhost:3000) # chrome F12 source 控制面板 webpack// . lib 目錄下,根據狀況自行斷點調試
本文就是經過上述的例子axios/sandbox/client.html來調試的。
順便簡單提下調試example的例子,雖然文章最開始時寫了這部分,後來又刪了,最後想一想仍是寫下。
找到文件axios/examples/server.js,修改代碼以下:
server = http.createServer(function (req, res) { var url = req.url; // 調試 examples console.log(url); // Process axios itself if (/axios\.min\.js$/.test(url)) { // 原來的代碼 是 axios.min.js // pipeFileToResponse(res, '../dist/axios.min.js', 'text/javascript'); pipeFileToResponse(res, '../dist/axios.js', 'text/javascript'); return; } // 原來的代碼 是 axios.min.map // if (/axios\.min.map$/.test(url)) { if (/axios\.map$/.test(url)) { // 原來的代碼 是 axios.min.map // pipeFileToResponse(res, '../dist/axios.min.map', 'text/javascript'); pipeFileToResponse(res, '../dist/axios.map', 'text/javascript'); return; } }
# 上述安裝好依賴後 # npm run examples 不能同時開啓,默認都是3000端口 # 能夠指定端口 5000 # npm run examples === node ./examples/server.js node ./examples/server.js -p 5000
打開http://localhost:5000,而後就能夠開心的在Chrome瀏覽器中調試examples裏的例子了。
axios 是支持 node 環境發送請求的。接下來看如何用 vscode 調試 node 環境下的axios。
在根目錄下 axios-analysis/建立.vscode/launch.json文件以下:
{ // 使用 IntelliSense 瞭解相關屬性。 // 懸停以查看現有屬性的描述。 // 欲瞭解更多信息,請訪問: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "program": "${workspaceFolder}/axios/sandbox/client.js", "skipFiles": [ "<node_internals>/**" ] }, ] }
按F5開始調試便可,按照本身的狀況,單步跳過(F10)、單步調試(F11)斷點調試。
其實開源項目通常都有貢獻指南axios/CONTRIBUTING.md,筆者只是把這個指南的基礎上修改成引用sourcemap的文件可調試。
git clone https://github.com/lxchuan12/axios-analysis.git cd axios-analaysis/axios npm install npm start
按照上文說的調試方法, npm start 後,直接在 chrome 瀏覽器中調試。打開 http://localhost:3000,在控制檯打印出axios,估計不少人都沒打印出來看過。
console.log({axios: axios});
層層點開來看,axios 的結構是怎樣的,先有一個大概印象。
筆者畫了一張比較詳細的圖表示。
看完結構圖,若是看過jQuery、underscore和lodash源碼,會發現其實跟axios源碼設計相似。
jQuery 別名 $,underscore loadsh 別名 _ 也既是函數,也是對象。好比jQuery使用方式。$('#id'), $.ajax。
接下來看具體源碼的實現。能夠跟着斷點調試一下。
斷點調試要領:
賦值語句能夠一步跳過,看返回值便可,後續詳細再看。
函數執行須要斷點跟着看,也能夠結合註釋和上下文倒推這個函數作了什麼。
看源碼第一步,先看package.json。通常都會申明 main 主入口文件。
// package.json { "name": "axios", "version": "0.19.0", "description": "Promise based HTTP client for the browser and node.js", "main": "index.js", // ... }
主入口文件
// index.js module.exports = require('./lib/axios');
axios.js文件 代碼相對比較多。分爲三部分展開敘述。
- 第一部分:引入一些工具函數utils、Axios構造函數、默認配置defaults等。
- 第二部分:是生成實例對象 axios、axios.Axios、axios.create等。
- 第三部分取消相關 API 實現,還有all、spread、導出等實現。
引入一些工具函數utils、Axios構造函數、默認配置defaults等。
// 第一部分: // lib/axios // 嚴格模式 'use strict'; // 引入 utils 對象,有不少工具方法。 var utils = require('./utils'); // 引入 bind 方法 var bind = require('./helpers/bind'); // 核心構造函數 Axios var Axios = require('./core/Axios'); // 合併配置方法 var mergeConfig = require('./core/mergeConfig'); // 引入默認配置 var defaults = require('./defaults');
是生成實例對象 axios、axios.Axios、axios.create等。
/** * Create an instance of Axios * * @param {Object} defaultConfig The default config for the instance * @return {Axios} A new instance of Axios */ function createInstance(defaultConfig) { // new 一個 Axios 生成實例對象 var context = new Axios(defaultConfig); // bind 返回一個新的 wrap 函數, // 也就是爲何調用 axios 是調用 Axios.prototype.request 函數的緣由 var instance = bind(Axios.prototype.request, context); // Copy axios.prototype to instance // 複製 Axios.prototype 到實例上。 // 也就是爲何 有 axios.get 等別名方法, // 且調用的是 Axios.prototype.get 等別名方法。 utils.extend(instance, Axios.prototype, context); // Copy context to instance // 複製 context 到 intance 實例 // 也就是爲何默認配置 axios.defaults 和攔截器 axios.interceptors 可使用的緣由 // 實際上是new Axios().defaults 和 new Axios().interceptors utils.extend(instance, context); // 最後返回實例對象,以上代碼,在上文的圖中都有體現。這時能夠仔細看下上圖。 return instance; } // Create the default instance to be exported // 導出 建立默認實例 var axios = createInstance(defaults); // Expose Axios class to allow class inheritance // 暴露 Axios class 容許 class 繼承 也就是能夠 new axios.Axios() // 但 axios 文檔中 並無提到這個,咱們平時也用得少。 axios.Axios = Axios; // Factory for creating new instances // 工廠模式 建立新的實例 用戶能夠自定義一些參數 axios.create = function create(instanceConfig) { return createInstance(mergeConfig(axios.defaults, instanceConfig)); };
這裏簡述下工廠模式。axios.create,也就是用戶不須要知道內部是怎麼實現的。
舉個生活的例子,咱們買手機,不須要知道手機是怎麼作的,就是工廠模式。
看完第二部分,裏面涉及幾個工具函數,如bind、extend。接下來說述這幾個工具方法。
axios/lib/helpers/bind.js
'use strict'; // 返回一個新的函數 wrap 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]; } // 把 argument 對象放在數組 args 裏 return fn.apply(thisArg, args); }; };
傳遞兩個參數函數和thisArg指向。
把參數arguments生成數組,最後調用返回參數結構。
其實如今 apply 支持 arguments這樣的類數組對象了,不須要手動轉數組。
那麼爲啥做者要轉數組,爲了性能?當時不支持?抑或是做者不知道?這就不得而知了。有讀者知道歡迎評論區告訴筆者呀。
關於apply、call和bind等不是很熟悉的讀者,能夠看筆者的另外一個面試官問系列。
面試官問:可否模擬實現 JS 的 bind 方法
舉個例子
function fn(){ console.log.apply(console, arguments); } fn(1,2,3,4,5,6, '若川'); // 1 2 3 4 5 6 '若川'
axios/lib/utils.js
function extend(a, b, thisArg) { forEach(b, function assignValue(val, key) { if (thisArg && typeof val === 'function') { a[key] = bind(val, thisArg); } else { a[key] = val; } }); return a; }
其實就是遍歷參數 b 對象,複製到 a 對象上,若是是函數就是則用 bind 調用。
axios/lib/utils.js
遍歷數組和對象。設計模式稱之爲迭代器模式。不少源碼都有相似這樣的遍歷函數。好比你們熟知的jQuery $.each。
/** * @param {Object|Array} obj The object to iterate * @param {Function} fn The callback to invoke for each item */ function forEach(obj, fn) { // Don't bother if no value provided // 判斷 null 和 undefined 直接返回 if (obj === null || typeof obj === 'undefined') { return; } // Force an array if not already something iterable // 若是不是對象,放在數組裏。 if (typeof obj !== 'object') { /*eslint no-param-reassign:0*/ obj = [obj]; } // 是數組 則用for 循環,調用 fn 函數。參數相似 Array.prototype.forEach 的前三個參數。 if (isArray(obj)) { // Iterate over array values for (var i = 0, l = obj.length; i < l; i++) { fn.call(null, obj[i], i, obj); } } else { // Iterate over object keys // 用 for in 遍歷對象,但 for in 會遍歷原型鏈上可遍歷的屬性。 // 因此用 hasOwnProperty 來過濾自身屬性了。 // 其實也能夠用Object.keys來遍歷,它不遍歷原型鏈上可遍歷的屬性。 for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { fn.call(null, obj[key], key, obj); } } } }
若是對Object相關的API不熟悉,能夠查看筆者以前寫過的一篇文章。JavaScript 對象全部 API 解析
取消相關 API 實現,還有all、spread、導出等實現。
// Expose Cancel & CancelToken // 導出 Cancel 和 CancelToken axios.Cancel = require('./cancel/Cancel'); axios.CancelToken = require('./cancel/CancelToken'); axios.isCancel = require('./cancel/isCancel'); // Expose all/spread // 導出 all 和 spread API 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 // 也就是能夠如下方式引入 // import axios from 'axios'; module.exports.default = axios;
這裏介紹下 spread,取消的API暫時不作分析,後文再詳細分析。
假設你有這樣的需求。
function f(x, y, z) {} var args = [1, 2, 3]; f.apply(null, args);
那麼能夠用spread方法。用法:
axios.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); }; };
上文var context = new Axios(defaultConfig);,接下來介紹核心構造函數Axios。
axios/lib/core/Axios.js
構造函數Axios。
function Axios(instanceConfig) { // 默認參數 this.defaults = instanceConfig; // 攔截器 請求和響應攔截器 this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() }; }
Axios.prototype.request = function(config){ // 省略,這個是核心方法,後文結合例子詳細描述 // code ... var promise = Promise.resolve(config); // code ... return promise; } // 這是獲取 Uri 的函數,這裏省略 Axios.prototype.getUri = function(){} // 提供一些請求方法的別名 // Provide aliases for supported request methods // 遍歷執行 // 也就是爲啥咱們能夠 axios.get 等別名的方式調用,並且調用的是 Axios.prototype.request 方法 // 這個也在上面的 axios 結構圖上有所體現。 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;
接下來看攔截器部分。
請求前攔截,和請求後攔截。
在Axios.prototype.request函數裏使用,具體怎麼實現的攔截的,後文配合例子詳細講述。
axios github 倉庫 攔截器文檔
如何使用:
// Add a request interceptor // 添加請求前攔截器 axios.interceptors.request.use(function (config) { // Do something before request is sent return config; }, function (error) { // Do something with request error return Promise.reject(error); }); // Add a response interceptor // 添加請求後攔截器 axios.interceptors.response.use(function (response) { // Any status code that lie within the range of 2xx cause this function to trigger // Do something with response data return response; }, function (error) { // Any status codes that falls outside the range of 2xx cause this function to trigger // Do something with response error return Promise.reject(error); });
若是想把攔截器,能夠用eject方法。
const myInterceptor = axios.interceptors.request.use(function () {/*...*/}); axios.interceptors.request.eject(myInterceptor);
攔截器也能夠添加自定義的實例上。
const instance = axios.create(); instance.interceptors.request.use(function () {/*...*/});
源碼實現:
構造函數,handles 用於存儲攔截器函數。
function InterceptorManager() { this.handlers = []; }
接下來聲明瞭三個方法:使用、移除、遍歷。
傳遞兩個函數做爲參數,數組中的一項存儲的是{fulfilled: function(){}, rejected: function(){}}。返回數字 ID,用於移除攔截器。
/** * @param {Function} fulfilled The function to handle `then` for a `Promise` * @param {Function} rejected The function to handle `reject` for a `Promise` * * @return {Number} 返回ID 是爲了用 eject 移除 */ InterceptorManager.prototype.use = function use(fulfilled, rejected) { this.handlers.push({ fulfilled: fulfilled, rejected: rejected }); return this.handlers.length - 1; };
根據 use 返回的 ID 移除 攔截器。
/** * @param {Number} id The ID that was returned by `use` */ InterceptorManager.prototype.eject = function eject(id) { if (this.handlers[id]) { this.handlers[id] = null; } };
有點相似定時器setTimeout 和 setInterval,返回值是id。用clearTimeout 和clearInterval來清除定時器。
// 提一下 定時器回調函數是能夠傳參的,返回值 timer 是數字 var timer = setInterval((name) => { console.log(name); }, 1000, '若川'); console.log(timer); // 數字 ID // 在控制檯等會再輸入執行這句,定時器就被清除了 clearInterval(timer);
遍歷執行全部攔截器,傳遞一個回調函數(每個攔截器函數做爲參數)調用,被移除的一項是null,因此不會執行,也就達到了移除的效果。
/** * @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); } }); };
上文敘述的調試時運行npm start 是用axios/sandbox/client.html路徑的文件做爲示例的,讀者能夠自行調試。
如下是一段這個文件中的代碼。
axios(options) .then(function (res) { response.innerHTML = JSON.stringify(res.data, null, 2); }) .catch(function (res) { response.innerHTML = JSON.stringify(res.data, null, 2); });
若是不想一步步調試,有個偷巧的方法。
知道 axios 使用了XMLHttpRequest。
能夠在項目中搜索:new XMLHttpRequest。
定位到文件 axios/lib/adapters/xhr.js
在這條語句 var request = new XMLHttpRequest();
chrome 瀏覽器中 打個斷點調試下,再根據調用棧來細看具體函數等實現。
Call Stack
dispatchXhrRequest (xhr.js:19) xhrAdapter (xhr.js:12) dispatchRequest (dispatchRequest.js:60) Promise.then (async) request (Axios.js:54) wrap (bind.js:10) submit.onclick ((index):138)
簡述下流程:
若是仔細看了文章開始的axios 結構關係圖,其實對這個流程也有大概的瞭解。
接下來看 Axios.prototype.request 具體實現。
這個函數是核心函數。主要作了這幾件事:
1.判斷第一個參數是字符串,則設置 url,也就是支持axios('example/url', [, config]),也支持axios({})。
2.合併默認參數和用戶傳遞的參數
3.設置請求的方法,默認是是get方法
4.將用戶設置的請求和響應攔截器、發送請求的dispatchRequest組成Promise鏈,最後返回仍是Promise實例。
也就是保證了請求前攔截器先執行,而後發送請求,再響應攔截器執行這樣的順序。<br> 也就是爲啥最後仍是能夠`then`,`catch`方法的緣故。<br>
Axios.prototype.request = function request(config) { /*eslint no-param-reassign:0*/ // Allow for axios('example/url'[, config]) a la fetch API // 這一段代碼 其實就是 使 axios('example/url', [, config]) // config 參數能夠省略 if (typeof config === 'string') { config = arguments[1] || {}; config.url = arguments[0]; } else { config = config || {}; } // 合併默認參數和用戶傳遞的參數 config = mergeConfig(this.defaults, config); // Set config.method // 設置 請求方法,默認 get 。 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 // 組成`Promise`鏈 這段拆開到後文再講述 };
這部分:用戶設置的請求和響應攔截器、發送請求的dispatchRequest組成Promise鏈。也就是保證了請求前攔截器先執行,而後發送請求,再響應攔截器執行這樣的順序
也就是保證了請求前攔截器先執行,而後發送請求,再響應攔截器執行這樣的順序<br> 也就是爲啥最後仍是能夠`then`,`catch`方法的緣故。<br>
若是讀者對Promise不熟悉,建議讀阮老師的書籍《ES6 標準入門》。阮一峯老師 的 ES6 Promise-resolve 和 JavaScript Promise 迷你書(中文版)
// 組成`Promise`鏈 // Hook up interceptors middleware // 把 xhr 請求 的 dispatchRequest 和 undefined 放在一個數組裏 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); }); // 遍歷 chain 數組,直到遍歷 chain.length 爲 0 while (chain.length) { // 兩兩對應移出來 放到 then 的兩個參數裏。 promise = promise.then(chain.shift(), chain.shift()); } return promise;
var promise = Promise.resolve(config);
解釋下這句。做用是生成Promise實例。
var promise = Promise.resolve({name: '若川'}) // 等價於 // new Promise(resolve => resolve({name: '若川'})) promise.then(function (config){ console.log(config) }); // {name: "若川"}
一樣解釋下後文會出現的Promise.reject(error);:
Promise.reject(error);
var promise = Promise.reject({name: '若川'}) // 等價於 // new Promise(reject => reject({name: '若川'})) // promise.then(null, function (config){ // console.log(config) // }); // 等價於 promise.catch(function (config){ console.log(config) }); // {name: "若川"}
接下來結合例子,來理解這段代碼。
很遺憾,在example文件夾沒有攔截器的例子。筆者在example中在example/get的基礎上添加了一個攔截器的示例。axios/examples/interceptors,便於讀者調試。
node ./examples/server.js -p 5000
promise = promise.then(chain.shift(), chain.shift());這段代碼打個斷點。
會獲得這樣的這張圖。
特別關注下,右側,local中的chain數組。也就是這樣的結構。
var chain = [ '請求成功攔截2', '請求失敗攔截2', '請求成功攔截1', '請求失敗攔截1', dispatch, undefined, '響應成功攔截1', '響應失敗攔截1', '響應成功攔截2', '響應失敗攔截2', ]
這段代碼相對比較繞。也就是會生成以下相似的代碼,中間會調用dispatchRequest方法。
// config 是 用戶配置和默認配置合併的 var promise = Promise.resolve(config); promise.then('請求成功攔截2', '請求失敗攔截2') .then('請求成功攔截1', '請求失敗攔截1') .then(dispatchRequest, undefined) .then('響應成功攔截1', '響應失敗攔截1') .then('響應成功攔截2', '響應失敗攔截2') .then('用戶寫的業務處理函數') .catch('用戶寫的報錯業務處理函數');
這裏提下promise then和catch知識:
Promise.prototype.then方法的第一個參數是resolved狀態的回調函數,第二個參數(可選)是rejected狀態的回調函數。因此是成對出現的。
Promise.prototype.catch方法是.then(null, rejection)或.then(undefined, rejection)的別名,用於指定發生錯誤時的回調函數。
then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)。所以能夠採用鏈式寫法,即then方法後面再調用另外一個then方法。
結合上述的例子更詳細一點,代碼則是這樣的。
var promise = Promise.resolve(config); // promise.then('請求成功攔截2', '請求失敗攔截2') promise.then(function requestSuccess2(config) { console.log('------request------success------2'); return config; }, function requestError2(error) { console.log('------response------error------2'); return Promise.reject(error); }) // .then('請求成功攔截1', '請求失敗攔截1') .then(function requestSuccess1(config) { console.log('------request------success------1'); return config; }, function requestError1(error) { console.log('------response------error------1'); return Promise.reject(error); }) // .then(dispatchRequest, undefined) .then( function dispatchRequest(config) { /** * 適配器返回的也是Promise 實例 adapter = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) {}) } **/ return adapter(config).then(function onAdapterResolution(response) { // 省略代碼 ... return response; }, function onAdapterRejection(reason) { // 省略代碼 ... return Promise.reject(reason); }); }, undefined) // .then('響應成功攔截1', '響應失敗攔截1') .then(function responseSuccess1(response) { console.log('------response------success------1'); return response; }, function responseError1(error) { console.log('------response------error------1'); return Promise.reject(error); }) // .then('響應成功攔截2', '響應失敗攔截2') .then(function responseSuccess2(response) { console.log('------response------success------2'); return response; }, function responseError2(error) { console.log('------response------error------2'); return Promise.reject(error); }) // .then('用戶寫的業務處理函數') // .catch('用戶寫的報錯業務處理函數'); .then(function (response) { console.log('哈哈哈,終於獲取到數據了', response); }) .catch(function (err) { console.log('哎呀,怎麼報錯了', err); });
仔細看這段Promise鏈式調用,代碼都相似。then方法最後返回的參數,就是下一個then方法第一個參數。
catch錯誤捕獲,都返回Promise.reject(error),這是爲了便於用戶catch時能捕獲到錯誤。
舉個例子:
var p1 = new Promise((resolve, reject) => { reject(new Error({name: '若川'})); }); p1.catch(err => { console.log(res, 'err'); return Promise.reject(err) }) .catch(err => { console.log(err, 'err1'); }) .catch(err => { console.log(err, 'err2'); });
err2不會捕獲到,也就是不會執行,但若是都返回了return Promise.reject(err),則能夠捕獲到。
最後畫個圖總結下 Promise 鏈式調用。
axios promise 鏈式調用小結:1. 請求和響應的攔截器能夠寫Promise。
- 若是設置了多個請求響應器,後設置的先執行。
- 若是設置了多個響應攔截器,先設置的先執行。
dispatchRequest(config) 這裏的config是請求成功攔截器返回的。接下來看dispatchRequest函數。
這個函數主要作了以下幾件事情:
1.若是已經取消,則 throw 緣由報錯,使Promise走向rejected。
2.確保 config.header 存在。
3.利用用戶設置的和默認的請求轉換器轉換數據。
4.拍平 config.header。
5.刪除一些 config.header。
6.返回適配器adapter(Promise實例)執行後 then執行後的 Promise實例。返回結果傳遞給響應攔截器處理。
'use strict'; // utils 工具函數 var utils = require('./../utils'); // 轉換數據 var transformData = require('./transformData'); // 取消狀態 var isCancel = require('../cancel/isCancel'); // 默認參數 var defaults = require('../defaults'); /** * 拋出 錯誤緣由,使`Promise`走向`rejected` */ 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 // 確保 headers 存在 config.headers = config.headers || {}; // Transform request data // 轉換請求的數據 config.data = transformData( config.data, config.headers, config.transformRequest ); // Flatten headers // 拍平 headers config.headers = utils.merge( config.headers.common || {}, config.headers[config.method] || {}, config.headers || {} ); // 如下這些方法 刪除 headers utils.forEach( ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'], function cleanHeaderConfig(method) { delete config.headers[method]; } ); // adapter 適配器部分 拆開 放在下文講 };
上文的代碼裏有個函數 transformData ,這裏解釋下。其實就是遍歷傳遞的函數數組 對數據操做,最後返回數據。
axios.defaults.transformResponse 數組中默認就有一個函數,因此使用concat連接自定義的函數。
使用:
文件路徑axios/examples/transform-response/index.html
這段代碼其實就是對時間格式的字符串轉換成時間對象,能夠直接調用getMonth等方法。
var ISO_8601 = /(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})Z/; function formatDate(d) { return (d.getMonth() + 1) + '/' + d.getDate() + '/' + d.getFullYear(); } axios.get('https://api.github.com/users/mzabriskie', { transformResponse: axios.defaults.transformResponse.concat(function (data, headers) { Object.keys(data).forEach(function (k) { if (ISO_8601.test(data[k])) { data[k] = new Date(Date.parse(data[k])); } }); return data; }) }) .then(function (res) { document.getElementById('created').innerHTML = formatDate(res.data.created_at); });
源碼:
就是遍歷數組,調用數組裏的傳遞 data 和 headers 參數調用函數。
module.exports = function transformData(data, headers, fns) { /*eslint no-param-reassign:0*/ utils.forEach(fns, function transform(fn) { data = fn(data, headers); }); return data; };
適配器,在設計模式中稱之爲適配器模式。講個生活中簡單的例子,你們就容易理解。
咱們經常使用之前手機耳機孔都是圓孔,而如今基本是耳機孔和充電接口合二爲一。統一爲typec。
這時咱們須要須要一個typec轉圓孔的轉接口,這就是適配器。
// adapter 適配器部分 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); });
接下來看具體的 adapter。
var adapter = config.adapter || defaults.adapter;
看了上文的 adapter,能夠知道支持用戶自定義。好比能夠經過微信小程序 wx.request 按照要求也寫一個 adapter。
接着來看下 defaults.ddapter。
文件路徑:axios/lib/defaults.js
根據當前環境引入,若是是瀏覽器環境引入xhr,是node環境則引入http。
相似判斷node環境,也在sentry-javascript源碼中有看到。
function getDefaultAdapter() { var adapter; // 根據 XMLHttpRequest 判斷 if (typeof XMLHttpRequest !== 'undefined') { // For browsers use XHR adapter adapter = require('./adapters/xhr'); // 根據 process 判斷 } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { // For node use HTTP adapter adapter = require('./adapters/http'); } return adapter; } var defaults = { adapter: getDefaultAdapter(), // ... };
xhr
接下來就是咱們熟悉的 XMLHttpRequest 對象。
可能讀者不瞭解能夠參考XMLHttpRequest MDN 文檔。
主要提醒下:onabort是請求取消事件,withCredentials是一個布爾值,用來指定跨域 Access-Control 請求是否應帶有受權信息,如 cookie 或受權 header 頭。
這塊代碼有刪減,具體能夠看若川的axios-analysis倉庫,也能夠克隆筆者的axios-analysis倉庫調試時再具體分析。
module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { // 這塊代碼有刪減 var request = new XMLHttpRequest(); request.open() request.timeout = config.timeout; // 監聽 state 改變 request.onreadystatechange = function handleLoad() { if (!request || request.readyState !== 4) { return; } // ... } // 取消 request.onabort = function(){}; // 錯誤 request.onerror = function(){}; // 超時 request.ontimeout = function(){}; // cookies 跨域攜帶 cookies 面試官常喜歡考這個 // 一個布爾值,用來指定跨域 Access-Control 請求是否應帶有受權信息,如 cookie 或受權 header 頭。 // Add withCredentials to request if needed if (!utils.isUndefined(config.withCredentials)) { request.withCredentials = !!config.withCredentials; } // 上傳下載進度相關 // Handle progress if needed if (typeof config.onDownloadProgress === 'function') { request.addEventListener('progress', config.onDownloadProgress); } // Not all browsers support upload events if (typeof config.onUploadProgress === 'function' && request.upload) { request.upload.addEventListener('progress', config.onUploadProgress); } // Send the request // 發送請求 request.send(requestData); }); }
而實際上如今 fetch 支持的很好了,阿里開源的 umi-request 請求庫,就是用fetch封裝的,而不是用XMLHttpRequest。文章末尾,大概講述下 umi-request 和 axios 的區別。
http
http這裏就不詳細敘述了,感興趣的讀者能夠自行查看,若川的axios-analysis倉庫。
module.exports = function httpAdapter(config) { return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) { }); };
上文 dispatchRequest 有取消模塊,我以爲是重點,因此放在最後來細講:
可使用cancel token取消請求。
axios cancel token API 是基於撤銷的 promise 取消提議。
The axios cancel token API is based on the withdrawn cancelable promises proposal.
axios 文檔 cancellation
文檔上詳細描述了兩種使用方式。
很遺憾,在example文件夾也沒有取消的例子。筆者在example中在example/get的基礎上添加了一個取消的示例。axios/examples/cancel,便於讀者調試。
node ./examples/server.js -p 5000
request中的攔截器和dispatch中的取消這兩個模塊相對複雜,能夠多調試調試,吸取消化。
const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios.get('/get/server', { cancelToken: source.token }).catch(function (err) { if (axios.isCancel(err)) { console.log('Request canceled', err.message); } else { // handle error } }); // cancel the request (the message parameter is optional) // 取消函數。 source.cancel('哎呀,我被若川取消了');
結合源碼取消流程大概是這樣的。這段放在代碼在axios/examples/cancel-token/index.html。
參數的 config.cancelToken 是觸發了source.cancel('哎呀,我被若川取消了');才生成的。
// source.cancel('哎呀,我被若川取消了'); // 點擊取消時纔會 生成 cancelToken 實例對象。 // 點擊取消後,會生成緣由,看懂了這段在看以後的源碼,可能就好理解了。 var config = { name: '若川', // 這裏簡化了 cancelToken: { promise: new Promise(function(resolve){ resolve({ message: '哎呀,我被若川取消了'}) }), reason: { message: '哎呀,我被若川取消了' } }, }; // 取消 拋出異常方法 function throwIfCancellationRequested(config){ // 取消的狀況下執行這句 if(config.cancelToken){ // 這裏源代碼 便於執行,我改爲具體代碼 // config.cancelToken.throwIfRequested(); // if (this.reason) { // throw this.reason; // } if(config.cancelToken.reason){ throw config.cancelToken.reason; } } } function dispatchRequest(config){ // 有多是執行到這裏就取消了,因此拋出錯誤會被err2 捕獲到 throwIfCancellationRequested(config); // adapter xhr適配器 return new Promise((resovle, reject) => { var request = new XMLHttpRequest(); console.log('request', request); if (config.cancelToken) { // Handle cancellation config.cancelToken.promise.then(function onCanceled(cancel) { if (!request) { return; } request.abort(); reject(cancel); // Clean up request request = null; }); } }) .then(function(res){ // 有多是執行到這裏就才取消 取消的狀況下執行這句 throwIfCancellationRequested(config); console.log('res', res); return res; }) .catch(function(reason){ // 有多是執行到這裏就才取消 取消的狀況下執行這句 throwIfCancellationRequested(config); console.log('reason', reason); return Promise.reject(reason); }); } var promise = Promise.resolve(config); // 沒設置攔截器的狀況下是這樣的 promise .then(dispatchRequest, undefined) // 用戶定義的then 和 catch .then(function(res){ console.log('res1', res); return res; }) .catch(function(err){ console.log('err2', err); return Promise.reject(err); }); // err2 {message: "哎呀,我被若川取消了"}
看如何經過生成config.cancelToken。
文件路徑:
axios/lib/cancel/CancelToken.js
const CancelToken = axios.CancelToken; const source = CancelToken.source(); source.cancel('哎呀,我被若川取消了');
由示例看 CancelToken.source的實現,
CancelToken.source = function source() { var cancel; var token = new CancelToken(function executor(c) { cancel = c; }); // token return { token: token, cancel: cancel }; };
執行後source的大概結構是這樣的。
{ token: { promise: new Promise(function(resolve){ resolve({ message: '哎呀,我被若川取消了'}) }), reason: { message: '哎呀,我被若川取消了' } }, cancel: function cancel(message) { if (token.reason) { // Cancellation has already been requested // 已經取消 return; } token.reason = {message: '哎呀,我被若川取消了'}; } }
接着看 new CancelToken
// CancelToken // 經過 CancelToken 來取消請求操做 function CancelToken(executor) { if (typeof executor !== 'function') { throw new TypeError('executor must be a function.'); } var resolvePromise; this.promise = new Promise(function promiseExecutor(resolve) { resolvePromise = resolve; }); var token = this; executor(function cancel(message) { if (token.reason) { // Cancellation has already been requested // 已經取消 return; } token.reason = new Cancel(message); resolvePromise(token.reason); }); } module.exports = CancelToken;
發送請求的適配器裏是這樣使用的。
// xhr if (config.cancelToken) { // Handle cancellation config.cancelToken.promise.then(function onCanceled(cancel) { if (!request) { return; } request.abort(); reject(cancel); // Clean up request request = null; }); }
dispatchRequest 中的throwIfCancellationRequested具體實現:throw 拋出異常。
// 拋出異常函數 function throwIfCancellationRequested(config) { if (config.cancelToken) { config.cancelToken.throwIfRequested(); } } // 拋出異常 用戶 { message: '哎呀,我被若川取消了' } CancelToken.prototype.throwIfRequested = function throwIfRequested() { if (this.reason) { throw this.reason; } };
取消流程調用棧
1.source.cancel()
2.resolvePromise(token.reason);
3.config.cancelToken.promise.then(function onCanceled(cancel) {})
最後進入request.abort();``reject(cancel);
到這裏取消的流程就介紹完畢了。主要就是經過傳遞配置參數cancelToken,取消時纔會生成cancelToken,判斷有,則拋出錯誤,使Promise 走向rejected,讓用戶捕獲到消息{message: '用戶設置的取消信息'}。
文章寫到這裏就基本到接近尾聲了。
能讀到最後,說明你已經超過不少人啦^_^
axios是很是優秀的請求庫,但確定也不能知足全部開發者的需求,接下來對比下其餘庫,看看其餘開發者有什麼具體需求。
FCC 成都社區負責人水歌開源的KoAJAX。
如何用開源軟件辦一場技術大會?如下這篇文章中摘抄的一段。
前端請求庫 —— KoAJAX 國內前端同窗最經常使用的 HTTP 請求庫應該是 axios 了吧?雖然它的 Interceptor(攔截器)API 是 .use(),但和 Node.js 的 Express、Koa 等框架的中間件模式徹底不一樣,相比 jQuery .ajaxPrefilter()、dataFilter() 並沒什麼實質改進;上傳、下載進度比 jQuery.Deferred() 還簡陋,只是兩個專門的回調選項。因此,它仍是要對特定的需求記憶特定的 API,不夠簡潔。
幸運的是,水歌在研究如何用 ES 2018 異步迭代器實現一個類 Koa 中間件引擎的過程當中,作出了一個更有實際價值的上層應用 —— KoAJAX。它的整個執行過程基於 Koa 式的中間件,並且它本身就是一箇中間件調用棧。除了 RESTful API 經常使用的 .get()、.post()、.put()、.delete() 等快捷方法外,開發者就只需記住 .use() 和 next(),其它都是 ES 標準語法和 TS 類型推導。
umi-request github 倉庫
umi-request 與 fetch, axios 異同。
umi-request 與 fetch, axios 異同不得不說,umi-request 確實強大,有興趣的讀者能夠閱讀下其源碼。
看懂axios的基礎上,看懂umi-request源碼應該不難。
好比 umi-request 取消模塊代碼幾乎與axios如出一轍。
文章詳細介紹了 axios 調試方法。詳細介紹了 axios 構造函數,攔截器,取消等功能的實現。最後還對比了其餘請求庫。
最後畫個圖總結一下 axios 的整體大體流程。
axios的整體大體流程解答下文章開頭提的問題:
若是你是求職者,項目寫了運用了axios,面試官可能會問你:
1.爲何 axios 既能夠當函數調用,也能夠當對象使用,好比axios({})、axios.get。
答:axios本質是函數,賦值了一些別名方法,好比get、post方法,可被調用,最終調用的仍是Axios.prototype.request函數。
2.簡述 axios 調用流程。
答:實際是調用的Axios.prototype.request方法,最終返回的是promise鏈式調用,實際請求是在dispatchRequest中派發的。
3.有用過攔截器嗎?原理是怎樣的?
答:用過,用axios.interceptors.request.use添加請求成功和失敗攔截器函數,用axios.interceptors.response.use添加響應成功和失敗攔截器函數。在Axios.prototype.request函數組成promise鏈式調用時,Interceptors.protype.forEach遍歷請求和響應攔截器添加到真正發送請求dispatchRequest的兩端,從而作到請求前攔截和響應後攔截。攔截器也支持用Interceptors.protype.eject方法移除。
4.有使用axios的取消功能嗎?是怎麼實現的?
答:用過,經過傳遞config配置cancelToken的形式,來取消的。判斷有傳cancelToken,在promise鏈式調用的dispatchRequest拋出錯誤,在adapter中request.abort()取消請求,使promise走向rejected,被用戶捕獲取消信息。
5.爲何支持瀏覽器中發送請求也支持node發送請求?
答:axios.defaults.adapter默認配置中根據環境判斷是瀏覽器仍是node環境,使用對應的適配器。適配器支持自定義。
回答面試官的問題,讀者也能夠根據本身的理解,組織語言,筆者的回答只是作一個參考。
axios 源碼相對很少,打包後一千多行,比較容易看完,很是值得學習。
建議 clone 若川的 axios-analysis github 倉庫,按照文中方法本身調試,印象更深入。
基於Promise,構成Promise鏈,巧妙的設置請求攔截,發送請求,再試試響應攔截器。
request中的攔截器和dispatch中的取消這兩個模塊相對複雜,能夠多調試調試,吸取消化。
axios 既是函數,是函數時調用的是Axios.prototype.request函數,又是對象,其上面有get、post等請求方法,最終也是調用Axios.prototype.request函數。
axios 源碼中使用了挺多設計模式。好比工廠模式、迭代器模式、適配器模式等。若是想系統學習設計模式,通常比較推薦豆瓣評分 9.1 的JavaScript 設計模式與開發實踐
若是讀者發現有不妥或可改善之處,再或者哪裏沒寫明白的地方,歡迎加我微信 lxchuan12 交流。另外以爲寫得不錯,對您有些許幫助,能夠點贊、評論、轉發分享,也是對筆者的一種支持,很是感謝呀。
在博客平臺裏,將來的路還很長,也但願本身之後的文章你們能多多支持,多多批評指正,咱們一塊兒進步,一塊兒走花路。
意見反饋
若本號內容有作得不到位的地方(好比:涉及版權或其餘問題),請及時聯繫咱們進行整改便可,會在第一時間進行處理。