BigPipe 簡介
2010年的 Facebook 提出 BigPipe 技術,通過將站點分解為多個 pagelet 小塊,每個pagelet 獲取數(shù)據與渲染均是獨立的。
BigPipe 模式可以實現(xiàn) pagelet 的數(shù)據一旦返回,就可以無阻塞的在瀏覽器端進行渲染,以此來實現(xiàn)大型復雜頁面的性能加速。
分塊傳輸編碼
分塊傳輸編碼是在 HTTP/1.1 版本中引入的一種數(shù)據傳輸機制,允許服務器為返回的內容維持持久連接,向客戶端發(fā)送多個分塊的數(shù)據。
當我們在請求一個圖片資源的時候,瀏覽器可以通過響應頭中 Content-Length 長度信息,判斷出響應體結束,但是,大多數(shù)情況下,服務端輸出的內容長度不能確定,無法通過長度信息,來判斷實體的邊界,這個時候就就需要使用分塊傳輸編碼,在響應頭中會出現(xiàn)了 Transfer-Encoding: chunked 這樣的標識。
每個經過 chunked 編碼后的分塊包含兩個部分:數(shù)據以及數(shù)據的長度信息,當遇到分塊長度為 0,表示該分塊沒有內容,實體結束,Content-Encoding 和 Transfer-Encoding 二者經常會結合來用,對傳輸?shù)膬热菥幋a壓縮,提高傳輸效率,BigPipe 就是基于分塊傳輸編碼,實現(xiàn)頁面的分塊加載。
使用 Node.js 實現(xiàn)最小化示例
BigPipe 的服務端可以用各種語言實現(xiàn),這里介紹的使用 Node.js 作為服務端語言,主要用到的是下面的兩個方法
- response.write(chunk[, encoding][, callback])
- response.end([data][, encoding][, callback])
當我們多次調用 response.write,數(shù)據自動被流式傳輸,向瀏覽器提供多了連續(xù)的響應體片段,chunk 可以是字符串或 buffer 類型, res.end 用來告訴服務器,已發(fā)送所有響應頭和主體,callback 是成功執(zhí)行后的回調方法。
const server = http.createServer(async(req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
res.write(`<DOCTYPE html>
<html lang="en">
<head ><title>BigPipe</title></head>
<body>`);
res.write(`<div>1</div>`);
await sleep(1000);
res.write(`<div>2</div>`);
res.end(` </body></html>`);
}).listen(3000);
通過 htttp.createServer 啟動一個簡單的 web 服務,通過 res.write 連續(xù)發(fā)送多個響應體片段,首先輸出的是 body 以上片段,然后發(fā)送 <div>1</div>,間隔1s,發(fā)送<div>2</div>,最后調用 res.end,閉合標簽,結束響應體傳輸。在頁面上,我們先看到 1,1s之后,會出現(xiàn) 2。
基于 express 框架實現(xiàn)頁面的分塊加載
前端頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>BigPipe Demo</title>
<style>
.box {
width: 100px;
height: 100px;
}
</style>
<script>
const BigPipe = {
view(selector, temp) {
document.querySelector(selector).innerHTML = temp;
}
};
</script>
</head>
<body>
<div id="a" class="box">loading...</div>
<div id="b" class="box">loading...</div>
<div id="c" class="box">loading...</div>
</body>
</html>
服務端代碼
const express = require('express');
const fs = require('fs');
const app = express();
const renderModule = (moduleId, res) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const template = `<span>${moduleId.toUpperCase()}</span>`;
res.write(
`<script>BigPipe.view('#${moduleId}', '${template}');</script>`
);
resolve();
}, 1000 * (Math.random() * 3 + 1));
});
};
app.get('/', (req, res, next) => {
const layoutHtml = fs.readFileSync(__dirname + '/layout.html').toString();
res.write(layoutHtml);
const moduleIds = ['a', 'b', 'c'];
const promises = moduleIds.map(moduleId => renderModule(moduleId, res));
Promise.all(promises).then(() => {
res.end();
});
});
app.listen(3000, () => console.log('server started at : 3000'));
客戶端處理分塊返回的數(shù)據
一般的 ajax 請求的處理,只有兩種情況:成功、失敗。,如何處理正在執(zhí)行中的數(shù)據呢? 我們可以借助xhr.readyState 屬性,該屬性表示請求的狀態(tài),一共有5個狀態(tài)
- 0: 請求未初始化
- 1: 服務器連接已建立
- 2: 請求已接收
- 3: 請求處理中
- 4: 請求已完成,且響應已就緒
我們經常用到是 xhr.readyState === 4 ,然后結合 xhr.status 來判斷請求的成功或者失敗,執(zhí)行對應的回調方法。
為了處理分塊返回的數(shù)據,我們需要監(jiān)聽 xhr.readyState === 3,每次有分塊數(shù)據返回的時候,都會觸發(fā) xhr.onreadystatechage 的方法,進入 this.readyState === 3 的判斷執(zhí)行語句,實現(xiàn)分塊數(shù)據的成功回調方法。
注意:當分塊數(shù)據返回的時間較短的時候,會出現(xiàn)多個分塊一起返回的情況,所以我們不能用字符串截取的方式,把已結束的 chunk 存放在數(shù)組中。
以下只是部分代碼,用來分析客戶端處理分塊返回數(shù)據的過程
let xhr = new XMLHttpRequest();
let chunked = [];
xhr.onreadystatechange = function() {
if ([3, 4].includes(this.readyState)) {
// 因為請求響應較快時,會出現(xiàn)一次返回多個塊,所以使用取出數(shù)組新增項的做法
if (this.response) {
let chunks = this.response.match(/<chunk>(.*?)<\/chunk>/g);
chunks = chunks.map((item: string): string => item.replace(/<\/?chunk>/g, ''));
const data = chunks.slice(chunked.length);
data.forEach(item => {
try {
callback(JSON.parse(item));
} catch (e) {
callback(options.onData(item));
}
});
chunked = chunks;
}
}
}
小結
當我們遇到批量處理,后端處理時間比較長的時候,我們可以引入 BigPipe 方案,將每條記錄的結果,一個一個的分塊返回的,實時渲染出來;當我們遇到一個頁面出現(xiàn)很多模塊,大量 api 請求的時候,就可以考慮 BigPipe 方案,分塊返回數(shù)據。
如果這篇文章對您有幫助,記得給作者點個贊,謝謝!