axios現在最新的版本的是v0.19.0,本節我們來分析一下它的實現源碼,首先通過 gitHub地址獲取到它的源代碼,地址:https://github.com/axios/axios/tree/v0.19.0 下載後就可以看到axios的目錄結構,主目錄下有一個index.js文件,該文件比較簡 ...
axios現在最新的版本的是v0.19.0,本節我們來分析一下它的實現源碼,首先通過 gitHub地址獲取到它的源代碼,地址:https://github.com/axios/axios/tree/v0.19.0
下載後就可以看到axios的目錄結構,主目錄下有一個index.js文件,該文件比較簡單,內容如下:
就是去引入./lib/axios模塊而已,lib目錄內容如下:
大致文件說明如下:
index.js ;入口文件
├lib ;代碼主目錄
├helpers ;定義了一些輔助函數
├adapters ;原生ajax和node環境下請求的封裝
├cancel ;取消請求的一些封裝
├core ;請求派發、攔截器管理、數據轉換等處理
axios.js ;也算是入口文件吧
default.js ;預設配置文件
utils.js ;工具函數
writer by:大沙漠 QQ:22969969
./lib/axios應該也可以說是一個入口文件,主要的分支如下:
var utils = require('./utils'); var bind = require('./helpers/bind'); var Axios = require('./core/Axios'); var mergeConfig = require('./core/mergeConfig'); var defaults = require('./defaults'); //預設配置對象 /*略*/ function createInstance(defaultConfig) { //創建一個Axios的實例 參數為:Axios的預設配置 var context = new Axios(defaultConfig); //創建一個./lib/core/Axios對象,作為上下文 var instance = bind(Axios.prototype.request, context); //創建一個instance屬性,值為bind()函數的返回值 // Copy axios.prototype to instance utils.extend(instance, Axios.prototype, context); //將Axios.prototype上的方法(delete、get、head、options、post、put、patch、request)extend到instans上,通過bind進行綁定 // Copy context to instance utils.extend(instance, context); //將context上的兩個defaults和interceptors屬性保存到utils上面,這兩個都是對象,這樣我們就可以通過axios.defaults修改配置信息,通過axios.interceptors去設置攔截器了 return instance; //返回instance方法 } // Create the default instance to be exported var axios = createInstance(defaults); //創建一個預設的實例作為輸出 /*略*/ module.exports = axios; //導出符號 // Allow use of default import syntax in TypeScript module.exports.default = axios; //預設導出符號
createInstance會創建一個./lib/core/Axios的一個對象實例,保存到局部變數context中,然後調用bind函數,將返回值保存到instance中(這就是我們調用axios()執行ajax請求時所調用的符號),bind()是一個輔助函數,如下:
module.exports = function bind(fn, thisArg) { //以thisArg為上下文,執行fn函數 return function wrap() { var args = new Array(arguments.length); //將arguments按照順序依次保存到args裡面 for (var i = 0; i < args.length; i++) { args[i] = arguments[i]; } return fn.apply(thisArg, args); //執行fn函數,參數為thisArg為上下文,args為參數 }; };
該函數是一個高階函數的實現,它會以參數2作為上下文,執行參數1,也就是以context為上下文,執行Axios.prototype.request函數,Axios.prototype.request就是所有非同步請求的入口了
我們看一下Axios.prototype.request的實現,如下:
Axios.prototype.request = function request(config) { //派發一個請求,也是ajax請求的入口 /*eslint no-param-reassign:0*/ // Allow for axios('example/url'[, config]) a la fetch API if (typeof config === 'string') { //如果config對象是個字元串, ;例如:axios('/api/1.php').then(function(){},function(){}) config = arguments[1] || {}; //則將其轉換為對象 config.url = arguments[0]; } else { config = config || {}; } config = mergeConfig(this.defaults, config); //合併預設值 config.method = config.method ? config.method.toLowerCase() : 'get'; //ajax方法,例如:get,這裡是轉換為小寫 // Hook up interceptors middleware var chain = [dispatchRequest, undefined]; //這個是發送ajax的非同步對列 var promise = Promise.resolve(config); //將config轉換為Promise對象 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) { //如果chain.length存在 promise = promise.then(chain.shift(), chain.shift()); //則執行promise.then(),這裡執行dispatchRequest函數,這樣就組成了非同步隊列 } return promise; //最後返回promise對象 };
這裡有一個while(chain.length){}遍歷迴圈比較難以理解,這個設計思想很新穎,這裡理解了整個axios的執行流程就能理解了,攔截器也是在這裡實現的。它就是遍歷chain數組,依次把前兩個元素分別作為promise().then的參數1和參數2來執行,這樣當promise之前的隊列執行完後就會接著執行後面的隊列,預設就是[dispatchRequest,undefined],也就是首先會執行dispatchRequest,如果有添加了請求攔截器則會在dispatchRequest之前執行攔截器里的邏輯,同樣的,如果有響應攔截器,則會在執行dispatchRequest之後執行響應攔截器里的邏輯。
dispatchRequest邏輯如下:
module.exports = function dispatchRequest(config) { //派發一個到伺服器的請求,用config里的配置 throwIfCancellationRequested(config); // Support baseURL config if (config.baseURL && !isAbsoluteURL(config.url)) { //如果config.baseURL存在,且config.url不是絕對URL(以http://開頭的) config.url = combineURLs(config.baseURL, config.url); //則調用combineURLs將config.baseURL拼湊在config.url的前面,我們在項目里設置的baseURL="api/"就是在這裡處理的 } // Ensure headers exist config.headers = config.headers || {}; //確保headers存在 // Transform request data config.data = transformData( //修改請求數據,會調用預設配置里的transformRequest進行處理 config.data, config.headers, config.transformRequest ); // Flatten headers config.headers = utils.merge( //將請求頭合併為一個數組 config.headers.common || {}, config.headers[config.method] || {}, config.headers || {} ); utils.forEach( //再刪除config.headers里的delete、get、head、post、put、patch、common請求頭 ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'], function cleanHeaderConfig(method) { delete config.headers[method]; } ); //執行到這裡請求頭已經設置好了 var adapter = config.adapter || defaults.adapter; //獲取預設配置里的adapter,也就是封裝好的ajax請求器 return adapter(config).then(function onAdapterResolution(response) { //執行adapter()就會發送ajax請求了,then()的第一個參數會修正返回的值 throwIfCancellationRequested(config); // Transform response data response.data = transformData( //調用預設配置里的transformResponse對返回的數據進行處理 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屬性對應的函數,我們來看一下,如下:
function getDefaultAdapter() { //獲取預設的適配器,就是Ajax的發送器吧 var adapter; // Only Node.JS has a process variable that is of [[Class]] process if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { //對於瀏覽器來說,用XHR adapter // For node use HTTP adapter adapter = require('./adapters/http'); } else if (typeof XMLHttpRequest !== 'undefined') { //對於node環境來說,則使用HTTP adapter // For browsers use XHR adapter adapter = require('./adapters/xhr'); } return adapter; } var defaults = { adapter: getDefaultAdapter(), //適配器 /*略*/ }
./adapters/http就是最終發送ajax請求的實現,主要的邏輯如下:
module.exports = function xhrAdapter(config) { //發送XMLHTtpRequest()請求等 return new Promise(function dispatchXhrRequest(resolve, reject) { var requestData = config.data; var requestHeaders = config.headers; if (utils.isFormData(requestData)) { delete requestHeaders['Content-Type']; // Let the browser set it } var request = new XMLHttpRequest(); // HTTP basic authentication if (config.auth) { var username = config.auth.username || ''; var password = config.auth.password || ''; requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); } request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true); //初始化HTTP請求,採用非同步請求 調用buildURL獲取URL地址 // Set the request timeout in MS request.timeout = config.timeout; //設置超時時間 // Listen for ready state request.onreadystatechange = function handleLoad() { //綁定onreadystatechange事件 if (!request || request.readyState !== 4) { //如果HTTP響應已經還沒有接收完成 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)) { //請求出錯,沒有得到響應的邏輯 如果request.responseURL不是以file:開頭且request.status=0,則直接返回 return; } // Prepare the response var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null; //解析響應頭,並調用parseHeaders將其轉換為對象,保存到responseHeaders裡面 var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response; //如果未設置config.responseType或者設置了responseType.responseType且等於text,則直接獲取request.responseText,否則獲取request.response var response = { //拼湊返回的數據,也就是上一篇說的axios請求後返回的promise對象 data: responseData, //接收到的數據 status: request.status, //狀態 ie瀏覽器是用1223埠代替204埠 ,見:https://github.com/axios/axios/issues/201 statusText: request.statusText, //響應頭的狀態文字 headers: responseHeaders, //頭部信息 config: config, //配置信息 request: request //對應的XmlHttpRequest對象 }; settle(resolve, reject, response); //調用settle函數進行判斷,是resolve或者reject // Clean up request request = null; }; /*略,主要是對於錯誤、超時、的一些處理*/ // Add headers to the request if ('setRequestHeader' in request) { //如果request裡面存在setRequestHeader utils.forEach(requestHeaders, function setRequestHeader(val, key) { //遍歷requestHeaders if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') { //如果key等於content-type 且沒有發送數據 // Remove Content-Type if data is undefined delete requestHeaders[key]; //則刪除content-type這個請求頭 ;只有發送數據時content-type才有用的吧 } else { // Otherwise add header to the request request.setRequestHeader(key, val); //否則設置請求頭 } }); } // Add withCredentials to request if needed if (config.withCredentials) { //如果設置了跨域請求時使用憑證 request.withCredentials = true; //設置request.withCredentials為true } // Add responseType to request if needed if (config.responseType) { //如果設置了伺服器響應的數據類型,預設為json try { request.responseType = config.responseType; } catch (e) { // Expected DOMException thrown by browsers not compatible XMLHttpRequest Level 2. // But, this can be suppressed for 'json' type as it can be parsed by default 'transformResponse' function. if (config.responseType !== 'json') { throw e; } } } // 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); } if (config.cancelToken) { // Handle cancellation config.cancelToken.promise.then(function onCanceled(cancel) { if (!request) { return; } request.abort(); reject(cancel); // Clean up request request = null; }); } if (requestData === undefined) { //修正requestData,如果為undefined,則修正為null requestData = null; } // Send the request request.send(requestData); //發送數據 }); };
也就是原生的ajax請求了,主要的邏輯都備註了一下,這樣整個流程就跑完了
對於便捷方法來說,例如axios.get()、axios.post()來說,就是對Axios.prototype.request的一次封裝,實現代碼如下:
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) { //定義delete、get、head、options方法 /*eslint func-names:0*/ Axios.prototype[method] = function(url, config) { return this.request(utils.merge(config || {}, { //調用utils.merge將參數合併為一個對象,然後調用request()方法 method: method, url: url })); }; }); utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) { //定義post、put、patch方法 /*eslint func-names:0*/ Axios.prototype[method] = function(url, data, config) { //調用utils.merge將參數合併為一個對象,然後調用request()方法 return this.request(utils.merge(config || {}, { method: method, url: url, data: data //post、put和patch比get等請求多了個data,其它一樣的 })); }; });
OK,搞定。