前端通信:ajax設計方案(八)- 設計請求池,讓前端通信快
來自專欄極樂科技18 人贊了文章
直接進入主題,本篇文章有點長,包括從設計階段,到摸索階段,再到實現階段,最後全面覆蓋測試階段(包括數據搜集清洗),還有與主流前端通信框架進行對比PK階段。
首先介紹一下一些概念:
- 瀏覽器的並發能力:瀏覽器設計當初就定義了瀏覽器打開頁面,同時發送http請求的瞬時數量。這樣設計有很多原因,同時保護瀏覽器和伺服器。具體可以谷歌或者百度關鍵字:瀏覽器並發。
2. 瀏覽器針對伺服器域名請求的並發限制數量:
3. 請求池:類似於資料庫連接池一樣,對資料庫請求連接進行分配管理等等
4. 復用請求:對於生命周期已經結束的請求不進行銷毀,重複利用,減少重新向瀏覽器申請資源,減少通用庫設計中的通用檢查。
新功能:連接池
當初為啥要做這個功能:
上次迭代最後留了一個彩蛋,我在一次測試中,發送完成一個請求之後,將這個請求存到一個變數中,然後又嘗試重新打開,發現竟然可以發送出去。好奇心害死貓,我在瀏覽器中用console.time去計算執行這段js代碼的時間,發現重複使用的請求使用時間大概是前者的一半,甚至更少。既然,他可以這麼優秀,那我為啥不能欲罷不能呢。所以,這一步下去就是一波三折...
摸索思路:
按著當時的設計思路一步一步的來確認:
1. 首先確認了一個請求發送完了之後,可以再次打開然後發送。這樣就保證了方向是對的,可以繼續優化下去
2. 其次,對於一個域名,瀏覽器的同時發送請求的數目就如上面圖片上描述的,主流基本都是6個,所以請求池將基於瀏覽器最大並發做連接的配置,不過因為瀏覽器差異,會將連接池的連接數量做配置進行配置
3. 將這個請求池塞滿配置的請求將是第一任務,對於XMLHttpRequest這個對象,主流的想法都是做一個拷貝,當前淺拷貝是不行的,那就做了一次深度拷貝測試,塞滿請求池。不過,該方案夭折了。
4. 因為請求對象的原型是XMLHttpRequest,這個對象實現了瀏覽器的介面才能通過瀏覽器API發送http請求,但是深度拷貝的對象是Object,object不實現瀏覽器的介面,所以夭折了,無法直接拷貝,難道我要類似於發送6次空請求之後去搜集請求池連接嗎?這個是絕對不可能饒恕的行為,所以做了下一步的摸索
5. 如果XMLHttpRequest對象實現了瀏覽器的介面,那麼我們將深度拷貝的對象的原型的指向重新指向到XMLHttpRequest。這樣是不是可以發送請求了。然後測試了,阿西吧,非法請求,而且對於XMLHttpRequest對象中的response、responseText等熟悉還是只讀了,重新拷貝賦值瀏覽器報錯,非法,全都是非法。
6. 氣餒了很久,把大腦放空換個思路去解決這個問題。參考了其他類似請求池的設計思路,要麼一開始就做初始化,將池子中的連接一次性生產出來,或者每次建立的連接消費完畢之後再歸入池子中,等待連接滿了之後才啟用連接池。
7. 考慮到前端每個頁面,或者對於現在的每個組件中能發送請求的數量並不是很大,可能一個路由下就發送了4-5個請求就解決一個小業務需求,根本達不到啟動請求池的基本配置。其次,通過瀏覽器的performance監控頁面性能,發現初始化的時間就在幾毫秒之間,相對於後期使用加速,這個時間的付出,對於後期的使用是很值得的預算。所以,選擇了第一種方式,一次性初始化請求池,等待調用。下面縷清思維,做了更完整和全面的設計。
請求池設計方案:
左邊 -- 生成請求池部分
1. 載入ajax-js庫的時候,通過判斷請求池開關是否打開,打開的話就走初始化這一套流程。
2. 創建基於全局配置的空請求-->copy所需參數-->創建空XMLHttpRequest對象-->合併copy參數到空對象上-->讀取配置數量生成請求池
3. 因為請求池是基於全局配置生成的請求,所以如果全局配置變動,將觸發請求池的reload,重新生成。
中間 -- 通過請求池發送請求的生命周期
1. 請求進來,首先判斷開關是否打開,打開的話就先判斷請求池中請求的數量是否還有。
2. 對比參數,是否有額外預設參數的變更(除url、data、successEvent、eroorEvent外),如果有將在請求發送前進行設置。(例如:request的Header將在每次開啟的時候全部重新置空)
3. 在請求的生命周期結束之後,然後歸還請求池。注意:請求的生命周期結束有很多狀況,發送錯誤url、timeout超時等在瀏覽器層面就失敗的,以及請求發送成功時候,非200、304等錯誤,比如4XX或者5XX系列錯誤的生命周期結束
4. 最後判斷是否有排隊的請求,有的話取出再來,沒有直接返回池子
右邊 -- 請求排隊系統
1. 對於請求如果超出請求池數量,請求池肯定無法承載,就算能承載,其實瀏覽器也是將這些超出並發的請求進行排隊,等待一波結束之後再發送。所以有個排隊的系統很重要的啦,來管理請求。
2. 對於超出容量請求,首先排隊。然後,在請求生命結束之後,將會去檢查排隊系統是否還有排隊請求。其次,如果有就進行取出,再走請求的生命周期,如果每頁,那就沒請求啦
代碼片段展示:
- 載入類庫時判斷開關,初始化代碼:
var outputObj = function () { //雖然在IE6、7上可以支持,但是最好升級你的瀏覽器,畢竟xp已經淘汰,面向未來吧,騷年,和我一起努力吧!! if (tool.getIEVersion() < 7) { //實在不想說:升級你的瀏覽器吧 throw new Error("Sorry,please update your browser.(IE8+)"); } // 是否開啟連接池 if (initParam.pool.isOpen) { tool.createPool() } return tempObj; };
2. 創建方法
// 創建請求池中鏈接 createPool: function () { // IE 系列不支持發送請求傳,所以默認/ tempObj.common({url: /}, true) tool.deepCloneXhr(selfData.xhr, initParam.pool.requestNumber) },
3. 拷貝通用參數存入請求池
// 拷貝xhr參數 deepCloneXhr: function (data, requestNum) { var mapping = { currentUrl: true, onerror: true, onload: true, onreadystatechange: true, ontimeout: true, timeout: true, // IE系列只有open連接之後才支持覆蓋 withCredentials: true, xhr_ie8: true } var temp = {} for (var key in data) { if (mapping[key]) { if (!isNaN(tool.getIEVersion()) && key !== timeout) { temp[key] = data[key] } else { var newKey = _ + key temp[newKey] = data[key] } } } for (var i = 0; i < requestNum; i++) { var nullRequest = tool.createXhrObject() tool.MergeObject(nullRequest, temp) selfData.requestPool.push(nullRequest) } },
4. 使用加速,判斷是否開啟(只舉例post)
//非同步post請求 post: function (url, data, successEvent, errorEvent, timeoutEvent) { var ajaxParam = { type: "post", url: url, data: data, contentType: , successEvent: successEvent, errorEvent: errorEvent, timeoutEvent: timeoutEvent }; if (initParam.pool.isOpen) { tool.useRequestPool(ajaxParam) } else { tempObj.common(ajaxParam); } },
5. 使用請求池鏈接,設置基本配置(url,data,successEvent)
// 請求池申請請求使用 useRequestPool: function (param) { // 判斷請求池中是否有可用請求 if (selfData.requestPool.length !== 0) { var temp = selfData.requestPool.shift(), sendData = , tempHeader = {} // 賦值操作,將數據捆綁到原型上 temp.callback_success = param.successEvent temp.callback_error = param.errorEvent temp.callback_timeout = param.timeoutEvent temp.data = param.data // 處理參數 switch (param.contentType) { case : tool.each(tool.MergeObject(param.data, initParam.publicData), function (item, index) { sendData += (index + "=" + item + "&") }); sendData = sendData.slice(0, -1); break case json: sendData = JSON.stringify(tool.MergeObject(param.data, initParam.publicData)) break case form: if (!tool.isEmptyObject(initParam.publicData)) { tool.each(initParam.publicData, function (item, index) { param.data.append(index, item) }) } sendData = param.data break } //判斷請求類型 if (param.type === get) { temp.open(param.type, tool.checkRealUrl(param.url, temp) + (sendData === ? : (? + sendData))) } else { temp.open(param.type, tool.checkRealUrl(param.url, temp)) } param.responseType ? (temp.responseType = param.responseType) : null if (!isNaN(tool.getIEVersion())) { temp.timeout = temp._timeout } switch (param.contentType) { case : tempHeader[Content-Type] = application/x-www-form-urlencoded break case json: tempHeader[Content-Type] = application/json break } //設置http協議的頭部 tool.each(tool.MergeObject(tempHeader, initParam.requestHeader), function (item, index) { temp.setRequestHeader(index, item) }); //發送請求 temp.send(param.type === get ? : sendData); } else { // 沒有請求,載入到待發送隊列中 selfData.queuePool.push(param) } },
6. 在默認周期中回收鏈接(onreadystatechange和onload)
//xmlhttprequest每次變化一個狀態所監控的事件(可拓展) xhr.onreadystatechange = function () { switch (this.readyState) { case 1://打開 //do something break; case 2://獲取header //do something break; case 3://請求 //do something break; case 4://完成 //在ie8下面,無xhr的onload事件,只能放在此處處理回調結果 if (this.xhr_ie8) { if (this.status === 200 || this.status === 304) { if (this.responseType == "json") { this.callback_success ? this.callback_success(ajaxSetting.transformResponse(JSON.parse(this.responseText))) : ajaxSetting.successEvent(ajaxSetting.transformResponse(JSON.parse(this.responseText))) } else { this.callback_success ? this.callback_success(ajaxSetting.transformResponse(this.responseText)) : ajaxSetting.successEvent(ajaxSetting.transformResponse(this.responseText)) } } else { // 請求錯誤搜集 tool.uploadAjaxError({ type: request, errInfo: JSON.stringify(this.data ? this.data : ajaxSetting.data), errUrl: this.currentUrl, errLine: this.status, Browser: navigator.userAgent }) } // 針對IE8 請求池處理 if (ajaxSetting.pool.isOpen) { tool.responseOver(this) } } else { if (this.status === 0) { // 發送不存在請求,將不會走onload,直接這裡就掛了,請求歸還請求池 if (ajaxSetting.pool.isOpen) { tool.responseOver(this) } } } break; } ; };
//onload事件(IE8下沒有該事件) xhr.onload = function (e) { if (this.readyState === 4 && (this.status == 200 || this.status == 304)) { /* * ie瀏覽器全系列不支持responseType=json和response取值,所以在ie下使用JSON.parse進行轉換 * */ if (!isNaN(tool.getIEVersion())) { if (this.responseType === json) { this.callback_success ? this.callback_success(ajaxSetting.transformResponse(JSON.parse(this.responseText))) : ajaxSetting.successEvent(ajaxSetting.transformResponse(JSON.parse(this.responseText))); } else { this.callback_success ? this.callback_success(ajaxSetting.transformResponse(this.responseText)) : ajaxSetting.successEvent(ajaxSetting.transformResponse(this.responseText)); } } else { this.callback_success ? this.callback_success(ajaxSetting.transformResponse(this.response)) : ajaxSetting.successEvent(ajaxSetting.transformResponse(this.response)); } } else { /* * 這邊為了兼容IE8、9的問題,以及請求完成而造成的其他錯誤,比如404等 * 如果跨域請求在IE8、9下跨域失敗不走onerror方法 * 其他支持了Level 2 的版本 直接走onerror * */ this.callback_error ? this.callback_error(e.currentTarget.status, e.currentTarget.statusText) : ajaxSetting.errorEvent(e.currentTarget.status, e.currentTarget.statusText); // 請求錯誤搜集 tool.uploadAjaxError({ type: request, errInfo: JSON.stringify(this.data ? this.data : ajaxSetting.data), errUrl: this.currentUrl, errLine: this.status, Browser: navigator.userAgent }) } // 生命周期結束之後返回數據池,不綁定狀態(是否為成功或失敗狀態) if (ajaxSetting.pool.isOpen) { tool.responseOver(this) } };
7. 回收方法
// 請求周期結束操作 responseOver: function (xhr) { selfData.requestPool.push(xhr) if (selfData.queuePool.length > 0) { var tempData = selfData.queuePool.shift() tool.useRequestPool(tempData) } }
以下為測試結果展示(測試覆蓋面:針對主流瀏覽器測試自身不開請求池、開啟請求池、與主流框架axios、jquery的ajax對比,對比精度連續發送10、100、1000、5000次請求)
chrome(測試單位:微秒):
firefox(測試單位:毫秒) PS:在5000的請求測試下,除了開啟請求池可以正常進行,其他方式全都瀏覽器崩潰
safari(測試單位:微秒):
opera(測試單位:微秒)
edge(測試單位:毫秒)
IE11(測試單位:毫秒)
IE9(測試單位:毫秒)
IE8(測試單位:毫秒) PS:在IE8下面沒有找到axios ie8下運行方案,jquery就不做測試,就做自身對比測試
首先陳述幾個問題:
1. 測試單位取值
使用瀏覽器的console.time方法去取值,在不同瀏覽器獲得的參數不一樣
2. 為什麼不統一取值
因為ajax-js在開啟請求池的用時中達到了0.00X等級,也就是微秒的級別,在折線圖中無法展示,所以增大單位,好看出趨勢
從所有清洗數據獲得的統計圖中總結以下:
1. jquery.ajax速度和性能最慢
2. ajax-js 開啟請求池狀況下,性能最好,最快
3. ajax-js類庫不開啟請求池,相對來說,比jquery中的ajax性能高,但是,不得不承認,axios比ajax-js優秀。
4. ajax-js類庫開啟請求池的狀況下,針對於自身,性能提高至少一倍以上,比axios還快,快很多
數據搜集在:https://github.com/GerryIsWarrior/ajax/tree/master/ajax-dataCenter/collect
清洗完數據形成的報表:https://github.com/GerryIsWarrior/ajax/tree/master/ajax-dataCenter/collect
數據清洗的工具:https://github.com/GerryIsWarrior/ajax/tree/master/ajax-dataCenter/tool
清洗完成形成的數據報表可以直接打開,可以更清晰的看數據
如需測試,可以去github下載該項目,在ajax-interface文件下,寫了一個express的node伺服器,可以 npm i 初始化之後,然後 npm run start 啟動。然後在ajax-testing目錄下有個html文件,提供了測試案例和一些簡單demo。
此次對外暴露的方法都開啟了請求池加速(開啟全局加速配置),除了common方法,預留一個未加速通用方法,防止有特殊需求。而且,本次請求池只是針對一個域名的加速,二期,將會增加針對不同域名加速的請求池。
github地址:https://github.com/GerryIsWarrior/ajax 對你有幫助或啟發,點個小星星,支持繼續研究下去
這次的迭代一波三折,中間有很多次走到一半就持續不下去了,但是因為剛開始測試的基礎方向是對的,所以不想放棄,然後都是理順思路一步一步的去走,看自己到底錯在什麼地方,這個方向為什麼不能走。還有為了這次測試,做了大量的數據搜集,從mac系統和window不停切換,為主流瀏覽器做兼容測試和主流框架對比測試數據搜集。搜集完成之後,耐心的做數據清洗,將混雜的數據,進行分類歸納,然後找對應報表,最後將數據載入到報表上,做更直觀的展示。
過程是艱難的,結果是值得興奮的。正視自己的不足,axios是一個優秀的框架,速度和性能絕對是很好的。在沒有做請求池優化之前,自己寫的庫達不到那個程度,不過在開啟了請求池之後,在性能和速度方面已經超過了axios,jquery就不談了。在研究前端通信的過程中收穫了很多很多,從基礎到設計,從底層到優化,沒有什麼是解決不了的問題。認清自己,做更好的自己,沒有什麼是不可能的。
從0-1是艱難的,從99-100更是難上加難。但是,只要去做,終是離那個方向更近一點,前端共勉!!
sorry,打個call:餓了么中後台招人啦(P6左右的哦),歡迎大佬騷擾我!!
推薦閱讀:
※《從零構建前後分離web項目》:開篇 - 縱觀WEB歷史演變
※zzz周刊-第1061期-春宵苦短,少女前進吧!
※zzz周刊 - 第1062期 - 比宇宙更遠的地方
※【js全棧】-koa2-靜態資源管理 koa-static
※Web直播,你需要先知道這些