axios网络请求框架源码解析
早期axios0.1.0版本做了對IE瀏覽器與包含XmlHttpRequest的瀏覽器的支持。并且做了對請求參數拼接、Json對象序列化等基本功能。
到0.19.0版本時,內部請求已經變為了在Node環境下與主流瀏覽器的支持,其中Node環境下支持http請求與https請求。并且支持取消、攔截。
Axios執行開始之初,首先執行createInstance方法,createInstance方法用來創建一個新的Axios實例。但這里奇怪的是,返回的示例并不是真正new出來的實例,而是一個幻影。實際在執行時,內部代碼的指向還是內部的Axios對象。Axios內部使用了wrap來表示各個方法,可能真的是為了將真實的Axios實例隱藏。這么做作用在于防止外部修改內部的方法,做好了封裝和防護。
function createInstance(defaultConfig) {var context = new Axios(defaultConfig);var instance = bind(Axios.prototype.request, context);// Copy axios.prototype to instanceutils.extend(instance, Axios.prototype, context);// Copy context to instanceutils.extend(instance, context);return instance; }不過,外部代碼還是可以訪問到內部的Axios實例的,在創建了幻影之后,繼續執行以下代碼:
// Expose Axios class to allow class inheritance axios.Axios = Axios;// Factory for creating new instances axios.create = function create(instanceConfig) {return createInstance(mergeConfig(axios.defaults, instanceConfig)); };// Expose Cancel & CancelToken axios.Cancel = require('./cancel/Cancel'); axios.CancelToken = require('./cancel/CancelToken'); axios.isCancel = require('./cancel/isCancel');// Expose all/spread 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 module.exports.default = axios;好,這里是外部的一些構造。其實我們拿到axios對象時就可以發起請求了。接下來我們通過一個使用示例來說明請求過程。以下是我們的request請求層示例:
// 使用示例,業務網絡層request.js const service = axios.create({baseURL: BASE_API, timeout: TIMEOUT });service.interceptors.request.use(config => {return config;},error => {Promise.reject(error);} );service.interceptors.response.use(response => {return Promise.reject(response);},error => {return Promise.reject(error);} );export default service;假設我們的業務網絡層是上面的用法,然后在具體的業務代碼處通過get方法發起了一次業務請求:
axios.get('/get/server').then(function (response) {}).catch(function (err) {});當業務請求代碼發起時,具體執行的是lib/core/Axios.js中的request方法:
// lib/core/Axios.js Axios.prototype.request = function request(config) {/*eslint no-param-reassign:0*/// Allow for axios('example/url'[, config]) a la fetch APIif (typeof config === 'string') {config = arguments[1] || {};config.url = arguments[0];} else {config = config || {};}config = mergeConfig(this.defaults, config);config.method = config.method ? config.method.toLowerCase() : 'get';// Hook up interceptors middlewarevar 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; };request方法的參數為一個config對象,而這個對象是由以下信息組成的:
{method: 'get',url: '/get/server' }緊接著會通過mergeConfig方法將自定義的config對象與默認的config對象進行合并:
// lib/core/Axios.jsconfig = mergeConfig(this.defaults, config);而這里的this.defaults實際內部如下:
// lib/defaults.js var defaults = {adapter: getDefaultAdapter(),// 默認的請求轉換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;}],// 默認的相應轉換,自定義轉換方法,Json解析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;} };總之是一些待會會用到的默認配置。合并的機制是優先取自定義的配置,再取默認配置,組成一個新的合并對象。
request的方法繼續向下,到了關鍵的地方:
// lib/core/Axios.js// Hook up interceptors middlewarevar 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);});由于咱們的業務網絡層使用了interceptors.request.use,interceptors.response.use這樣的字樣,所以我們應該看看對應的use方法做了什么:
// lib/core/InterceptorManager.js InterceptorManager.prototype.use = function use(fulfilled, rejected) {this.handlers.push({fulfilled: fulfilled,rejected: rejected});return this.handlers.length - 1; };而InterceptorManager的構造方法如下:
// lib/core/InterceptorManager.js function InterceptorManager() {this.handlers = []; }InterceptorManager在構造之初內部只有一個數組屬性handlers,所以當業務網絡層使用use方法時,會將對應的fulfilled, rejected組成一個新的對象Push到這個數組中。所以再回來forEach的地方:
// lib/core/InterceptorManager.js InterceptorManager.prototype.forEach = function forEach(fn) {utils.forEach(this.handlers, function forEachHandler(h) {if (h !== null) {fn(h);}}); };forEach內部僅僅將handlers遍歷,將數組中的對象通過forEach的回調方法傳出,所以在Axios的request方法內,就是將業務網絡層所定義的request interceptors與response interceptors壓入數組chain中。所以在兩個forEach執行完之后,數組chain的執行如下:
chain = [request.interceptors.success, request.interceptors.fail, dispatchRequest, undefined, response.interceptors.success, response.interceptors.fail]然后就是根本性的一環:
// lib/core/Axios.jswhile (chain.length) {promise = promise.then(chain.shift(), chain.shift());}當while方法執行時,開始觸發chain中所塞進去的各個方法。在這里首先執行的是request.interceptors.success,也就是業務網絡層所定義的請求攔截層。在我們的示例中什么處理都沒做,于是再執行dispatchRequest。這個dispatchRequest可大有來頭,是請求的核心:
// lib/core/dispatchRequest.js function dispatchRequest(config) {throwIfCancellationRequested(config);// Support baseURL configif (config.baseURL && !isAbsoluteURL(config.url)) {config.url = combineURLs(config.baseURL, config.url);}// Ensure headers existconfig.headers = config.headers || {};// Transform request dataconfig.data = transformData(config.data,config.headers,config.transformRequest);// Flatten headersconfig.headers = utils.merge(config.headers.common || {},config.headers[config.method] || {},config.headers || {});// 為什么要刪除Header中的請求方式?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);// Transform response data// 允許自定義轉換方法response.data = transformData(response.data,response.headers,config.transformResponse);return response;}, function onAdapterRejection(reason) {if (!isCancel(reason)) {throwIfCancellationRequested(config);// Transform response dataif (reason && reason.response) {reason.response.data = transformData(reason.response.data,reason.response.headers,config.transformResponse);}}return Promise.reject(reason);}); };dispatchRequest方法執行了以下事情:
- 1.校驗這次的請求是否已取消。
- 2.構造完整的請求Url。
- 3.確認header存在。
- 4.轉換請求數據與header。轉換方法在這里也可以通過config自定義。默認轉換為Json串。
- 5.合并config.headers。
- 6.刪除config.headers中請求方法所對應的值。
- 7.獲取對應的網絡請求適配器并發起請求。默認的adapter主要有兩種:1.瀏覽器環境下為XMLHttpRequest. 2.Node環境下為https庫或者http庫,這里還支持自定義網路請求框架。
在這里代碼分析我們假設運行環境為瀏覽器,那我們進入瀏覽器環境的dispatchRequest方法:
// lib/adapters/xhr.js function xhrAdapter(config) {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 authenticationif (config.auth) {var username = config.auth.username || '';var password = config.auth.password || '';requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);}// 這里的method從哪來的?request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true);// Set the request timeout in MSrequest.timeout = config.timeout;// Listen for ready staterequest.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 requestif (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {return;}// Prepare the responsevar 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 requestrequest = null;};// Handle browser request cancellation (as opposed to a manual cancellation)request.onabort = function handleAbort() {if (!request) {return;}reject(createError('Request aborted', config, 'ECONNABORTED', request));// Clean up requestrequest = null;};// Handle low level network errorsrequest.onerror = function handleError() {// Real errors are hidden from us by the browser// onerror should only fire if it's a network errorreject(createError('Network Error', config, null, request));// Clean up requestrequest = null;};// Handle timeoutrequest.ontimeout = function handleTimeout() {reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED',request));// Clean up requestrequest = null;};// 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()) {var cookies = require('./../helpers/cookies');// Add xsrf headervar xsrfValue = (config.withCredentials || isURLSameOrigin(config.url)) && config.xsrfCookieName ?cookies.read(config.xsrfCookieName) :undefined;if (xsrfValue) {requestHeaders[config.xsrfHeaderName] = xsrfValue;}}// Add headers to the requestif ('setRequestHeader' in request) {utils.forEach(requestHeaders, function setRequestHeader(val, key) {if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {// Remove Content-Type if data is undefineddelete requestHeaders[key];} else {// Otherwise add header to the requestrequest.setRequestHeader(key, val);}});}// Add withCredentials to request if neededif (config.withCredentials) {request.withCredentials = true;}// Add responseType to request if neededif (config.responseType) {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 neededif (typeof config.onDownloadProgress === 'function') {request.addEventListener('progress', config.onDownloadProgress);}// Not all browsers support upload eventsif (typeof config.onUploadProgress === 'function' && request.upload) {request.upload.addEventListener('progress', config.onUploadProgress);}if (config.cancelToken) {// Handle cancellationconfig.cancelToken.promise.then(function onCanceled(cancel) {if (!request) {return;}request.abort();reject(cancel);// Clean up requestrequest = null;});}if (requestData === undefined) {requestData = null;}// Send the requestrequest.send(requestData);}); };xhrAdapter方法為瀏覽器環境下的核心,主要做了以下事情:
- 1.構造XMLHttpRequest對象。
- 2.判斷是否配置用戶名密碼,如果有,則添加到Header中。
- 3.通過open方法打開請求。
- 4.設置超時時間。
- 5.配置onreadystatechange回調方法。
- 6.配置onabort回調方法。
- 7.配置onerror回調方法。
- 8.配置ontimeout回調方法。
- 9.如果是標準的瀏覽器環境,則添加xsrf值。
- 10.如果要求withCredentials,則將withCredentials設為true。
- 11.如果配置了responseType,則配置responseType。
- 12.如果配置了下載回調方法,則添加下載回調方法。
- 13.如果配置了上傳回調方法,則添加上傳回調方法。
- 14.判斷這時是否取消了請求,如果取消請求,則取消請求。
- 15.發起請求。
可以看到,xhrAdapter內部很貼心的為我們做了各種適配和校驗。發起請求后,如果網絡執行正常,那么我們的關注點應該在onreadystatechange回調方法內:
// lib/adapters/xhr.js 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 requestif (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {return;}// Prepare the responsevar 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 requestrequest = null;};onreadystatechange內部做了以下事情:
- 1.對file:請求協議做特殊處理。
- 2.構造響應對象。
- 3.進入settle方法。
settle內部允許自行判斷可接收的狀態,通過validateStatus方法校驗。執行完這一部之后,就算執行完成了。
到此為止應該是執行剛剛xhrAdapter中所返回的Promise,我們需要回到這個Promise所對應的then方法:
// lib/core/dispatchRequest.jsreturn 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 dataif (reason && reason.response) {reason.response.data = transformData(reason.response.data,reason.response.headers,config.transformResponse);}}return Promise.reject(reason);});一切順利,這里應該執行onAdapterResolution方法。onAdapterResolution方法內執行了以下事情:
- 1.校驗是否取消請求。
- 2.對返回的對象做反序列化處理,這里同樣支持自定義反序列化方法。
- 3.返回反序列化后的對象。
到這里,我們應該又去哪里找對應的then方法呢?我們需要一層層返回。剛剛dispatchRequest方法是在lib/core/Axios.js中的request中調用的,所以這里的then方法應該還在這里。還記得之前在request中執行過的這段代碼嗎?
while (chain.length) {promise = promise.then(chain.shift(), chain.shift());}沒錯,由于之前chain中保存了業務網絡層自定義的請求攔截器與響應攔截器,所以dispatchRequest方法會被Promise執行到業務網絡層的響應攔截器中,我們在這里再貼一下業務網絡層的響應攔截器:
service.interceptors.response.use(response => {return Promise.reject(response);},error => {return Promise.reject(error);} );嗯,我們還是什么都沒處理,這里執行完,就開始觸發我們的業務請求處的then方法了:
axios.get('/get/server').then(function (response) {}).catch(function (err) {});當時我們的業務代碼中什么都沒寫,如果通過控制臺輸出的話,就能看到最終的返回結果了。
到這里為止就是Axios的整個請求過程了,你是否清楚了呢?
通過閱讀Axios源碼,有以下總結:
- Promise是其中的粘合劑,又做了協作的約定。
- 面向對象的熟練使用。
- 良好的代碼規范,入參出參與分號、隔行、注釋。
- 良好的說明文檔:https://www.npmjs.com/package/axios 。
- 允許控制內部的更多細節,例如數據轉換方式。
缺點:
- 沒有對高并發做處理。
前端中的其它源碼分析文章:
Promise源碼解析 https://sahadev.blog.csdn.net/article/details/90722543
深入解析Node.js setTimeout方法的執行過程 https://sahadev.blog.csdn.net/article/details/90703250
Vue源碼探究筆記 https://sahadev.blog.csdn.net/article/details/87943168
總結
以上是生活随笔為你收集整理的axios网络请求框架源码解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android官方开发文档Trainin
- 下一篇: 十分钟搞定特征值和特征向量