改善程序性能和代碼質(zhì)量:通過(guò)代理模式組合HTTP請(qǐng)求
原文:https://levelup.gitconnected.com/improve-both-app-performance-and-code-quality-combining-http-requests-by-proxy-pattern-2cce132d60
作者:bitfish
在前端項(xiàng)目中,我們的網(wǎng)頁(yè)通常需要向服務(wù)器發(fā)送多個(gè)HTTP請(qǐng)求。
假設(shè)我們的產(chǎn)品具有一項(xiàng)功能,即每當(dāng)用戶單擊 li 標(biāo)記時(shí),客戶端都會(huì)向服務(wù)器發(fā)送一個(gè)HTTP請(qǐng)求。
這是一個(gè)簡(jiǎn)單的Demo:
- <html>
- <body>
- <ul>
- <li>1</li>
- <li>2</li>
- <li>3</li>
- <li>4</li>
- <li>5</li>
- <li>6</li>
- <li>7</li>
- <li>8</li>
- <li>9</li>
- </ul>
- <script>
- // Suppose this function is used to make HTTP requests to the server
- var sendHTTPRequest = function(message) {
- console.log('Start sending HTTP message to the server: ', message)
- console.log('1000ms passed')
- console.log('HTTP Request is completed')
- }
- var ul = document.getElementsByTagName('ul')[0];
- ul.onclick = function(event) {
- if (event.target.nodeName === "LI") {
- // Executes this function every time the <li> tag is clicked.
- sendHTTPRequest(event.target.innerText)
- }
- }
- </script>
- </body>
- </html>
在上面的代碼中,我們直接使用簡(jiǎn)單的 sendHTTPRequest 函數(shù)來(lái)模擬發(fā)送HTTP請(qǐng)求。這樣做是為了更好地專注于核心目標(biāo),因此我簡(jiǎn)化了一些代碼。
在上面的代碼中,我們直接使用簡(jiǎn)單的 sendHTTPRequest 函數(shù)來(lái)模擬發(fā)送HTTP請(qǐng)求。這樣做是為了更好地專注于核心目標(biāo),因此我簡(jiǎn)化了一些代碼。
上面的程序是這樣的:
為了使你們更容易嘗試,我制作了一個(gè)Codepen演示:https://codepen.io/bitfishxyz/pen/PobOZMm
當(dāng)然,在真實(shí)的項(xiàng)目中,我們可能會(huì)向服務(wù)器發(fā)送一個(gè)文件,推送通知,或者發(fā)送一些日志。但為了演示的慣例,我們將跳過(guò)這些細(xì)節(jié)。
好了,這是一個(gè)很簡(jiǎn)單的演示,那么上面的代碼有沒(méi)有什么缺點(diǎn)呢?
如果您的項(xiàng)目非常簡(jiǎn)單,那么編寫這樣的代碼應(yīng)該沒(méi)有問(wèn)題。但是,如果您的項(xiàng)目很復(fù)雜,并且客戶端需要頻繁向服務(wù)器發(fā)送HTTP請(qǐng)求,則此代碼效率很低。
在上面的示例中,如果任何用戶反復(fù)快速單擊 li 元素會(huì)發(fā)生什么?這時(shí),我們的客戶端需要向服務(wù)器發(fā)出頻繁的HTTP請(qǐng)求,并且每個(gè)請(qǐng)求都會(huì)消耗大量時(shí)間和服務(wù)器資源。
客戶端每次與服務(wù)器建立新的HTTP連接時(shí),都會(huì)消耗一些時(shí)間和服務(wù)器資源。因此,在HTTP傳輸機(jī)制中,一次傳輸所有文件比多次傳輸少量文件更為有效。
例如,您可能需要發(fā)送五個(gè)HTTP請(qǐng)求,每個(gè)HTTP請(qǐng)求的HTTP數(shù)據(jù)包大小為1MB?,F(xiàn)在,您一次發(fā)送一個(gè)HTTP請(qǐng)求,數(shù)據(jù)包大小為5MB。通常預(yù)期后者的性能要比前一個(gè)更好。
網(wǎng)頁(yè)上的大量HTTP請(qǐng)求可能會(huì)減慢網(wǎng)頁(yè)的加載時(shí)間,最終損害用戶體驗(yàn)。如果加載速度不夠快,這可能會(huì)導(dǎo)致訪問(wèn)者更快地離開(kāi)該頁(yè)面。
因此,在這種情況下,我們可以考慮合并HTTP請(qǐng)求。
在我們目前的項(xiàng)目中,我的思路是這樣的:我們可以在本地設(shè)置一個(gè)緩存,然后在一定范圍內(nèi)收集所有需要發(fā)送給服務(wù)器的消息,然后一起發(fā)送。
你可以暫停一下,自己試著想辦法。
提示:您需要?jiǎng)?chuàng)建一個(gè)本地緩存對(duì)象來(lái)收集需要發(fā)送的消息。然后,您需要使用定時(shí)器定時(shí)發(fā)送收集到的消息。
這是一個(gè)實(shí)現(xiàn)。
- var messages = [];
- var timer;
- var sendHTTPRequest = function (message) {
- messages.push(message);
- if (timer) {
- return;
- }
- timer = setTimeout(function () {
- console.log("Start sending messages: ", messages.join(","));
- console.log("1000ms passed");
- console.log("HTTP Request is completed.");
- clearTimeout(timer);
- timer = null;
- messages = [];
- }, 2000);
- };
每當(dāng)客戶端需要發(fā)送消息,也就是觸發(fā)一個(gè) onclick 事件的時(shí)候,sendHTTPRequest 并不會(huì)立即向服務(wù)器發(fā)送消息,而是先將消息緩存在消息中。然后,我們有一個(gè)計(jì)時(shí)器,該計(jì)時(shí)器在2秒鐘后執(zhí)行,并且在2秒鐘后,該計(jì)時(shí)器會(huì)將所有先前緩存的消息發(fā)送到服務(wù)器。此更改達(dá)到了組合HTTP請(qǐng)求的目的。
測(cè)試結(jié)果如下:
如你所見(jiàn),盡管我們多次觸發(fā)點(diǎn)擊事件,但在兩秒鐘內(nèi),我們只發(fā)送了一個(gè)HTTP請(qǐng)求。
當(dāng)然,為了方便演示,我將等待時(shí)間設(shè)置為2秒。如果你覺(jué)得這個(gè)等待時(shí)間太長(zhǎng),你可以縮短這個(gè)等待時(shí)間。
對(duì)于不需要太多實(shí)時(shí)交互的項(xiàng)目,2秒的延遲并不是一個(gè)巨大的副作用,但它可以減輕服務(wù)器的很多壓力。在適當(dāng)?shù)那闆r下,這是非常值得的。
上面的代碼確實(shí)為項(xiàng)目提供了一些性能改進(jìn)。但是就代碼設(shè)計(jì)而言,上面的代碼并不好。
第一,違反了單一責(zé)任原則。sendHTTPRequest 函數(shù)不僅向服務(wù)器發(fā)送HTTP請(qǐng)求,而且還組合HTTP請(qǐng)求。該函數(shù)執(zhí)行過(guò)多操作,使代碼看起來(lái)非常復(fù)雜。
如果某個(gè)功能(或?qū)ο?承擔(dān)了過(guò)多的責(zé)任,那么當(dāng)我們的需求發(fā)生變化時(shí),該功能通常將不得不發(fā)生重大變化。這樣的設(shè)計(jì)不能有效地應(yīng)對(duì)可能的更改,這是一個(gè)糟糕的設(shè)計(jì)。
我們理想的代碼如下所示:
我們沒(méi)有對(duì) sendHTTPRequest 進(jìn)行任何更改,而是選擇為其提供代理。這個(gè)代理函數(shù)執(zhí)行合并HTTP請(qǐng)求的任務(wù),并將合并后的消息傳遞給 sendHTTPRequest 發(fā)送。然后我們以后就可以直接使用 proxySendHTTPRequest 方法了。
您可以暫停片刻,然后嘗試自己解決。
這是一個(gè)實(shí)現(xiàn):
- var proxySendHTTPRequest = (function() {
- var messages = [],
- timer;
- return function(message) {
- messages.push(message);
- if (timer) {
- return;
- }
- timer = setTimeout(function() {
- sendHTTPRequest(messages.join(","));
- clearTimeout(timer);
- timer = null;
- messages = [];
- }, 2000);
- };
- })();
其基本思想與前面的代碼類似,該代碼使用 messages 變量在一定時(shí)間內(nèi)緩存所有消息,然后通過(guò)計(jì)時(shí)器統(tǒng)一地發(fā)送它們。此外,這段代碼使用了閉包技巧,將 messages 和 timer 變量放在局部作用域中,以避免污染全局名稱空間。
這段代碼與前面的代碼最大的區(qū)別是它沒(méi)有更改 sendHTTPRequest 函數(shù),而是將其隱藏在 proxySendHTTPRequest 后面。我們不再需要直接訪問(wèn) sendHTTPRequest,而是使用代理 proxySendHTTPRequest 來(lái)訪問(wèn)它。proxySendHTTPRequest 與sendHTTPRequest 具有相同的參數(shù)列表和相同的返回值。
這樣的設(shè)計(jì)有什么好處?
- 發(fā)送HTTP請(qǐng)求和合并HTTP請(qǐng)求的任務(wù)交給了兩個(gè)不同的函數(shù),每個(gè)函數(shù)專注于一個(gè)職責(zé)。它遵從單一責(zé)任原則,并使代碼更容易理解。
- 由于兩個(gè)函數(shù)的參數(shù)是相同的,我們可以簡(jiǎn)單地用 proxySendHTTPRequest 替換 sendHTTPRequest 的位置,而不需要做任何重大更改。
想象一下,如果將來(lái)網(wǎng)絡(luò)性能有所提高,或者由于某些其他原因,我們不再需要合并HTTP請(qǐng)求。在這一點(diǎn)上,如果我們使用以前的設(shè)計(jì),我們將不得不再次大規(guī)模地更改代碼。在當(dāng)前的代碼設(shè)計(jì)中,我們可以簡(jiǎn)單地替換函數(shù)名。
事實(shí)上,這個(gè)編碼技巧通常被稱為設(shè)計(jì)模式中的代理模式。
所謂的代理模式,其實(shí)在現(xiàn)實(shí)生活中很好理解。
- 比方說(shuō),你想訪問(wèn)一個(gè)網(wǎng)站,但你不想泄露你的IP地址。那么你可以使用,先訪問(wèn)你的代理服務(wù)器,然后通過(guò)代理服務(wù)器訪問(wèn)目標(biāo)網(wǎng)站。這樣目標(biāo)網(wǎng)站就無(wú)法知道你的IP地址了。
- 有時(shí)候,你會(huì)把你的真實(shí)服務(wù)器隱藏在Nginx服務(wù)器后面,讓Nginx服務(wù)器為你的真實(shí)服務(wù)器處理一些瑣碎的操作。
這些都是現(xiàn)實(shí)生活中代理模式的例子。
我們不需要為代理模式(或任何其他設(shè)計(jì)模式)的正式定義而煩惱,我們只需要知道,當(dāng)客戶端沒(méi)有直接訪問(wèn)它的便利(或能力)時(shí),我們可以提供代理功能(或?qū)ο?來(lái)控制對(duì)目標(biāo)功能(或?qū)ο?的訪問(wèn)即可??蛻魴C(jī)實(shí)際上訪問(wèn)代理函數(shù)(或?qū)ο?,代理函數(shù)對(duì)請(qǐng)求進(jìn)行一些處理,然后將請(qǐng)求傳遞給目標(biāo)。
本文轉(zhuǎn)載自微信公眾號(hào)「前端全棧開(kāi)發(fā)者」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系前端全棧開(kāi)發(fā)者公眾號(hào)。