BigPipe 简介
2010年的 Facebook 提出 BigPipe 技术,通过将站点分解为多个 pagelet 小块,每个pagelet 获取数据与渲染均是独立的。
BigPipe 模式可以实现 pagelet 的数据一旦返回,就可以无阻塞的在浏览器端进行渲染,以此来实现大型复杂页面的性能加速。
分块传输编码
分块传输编码是在 HTTP/1.1 版本中引入的一种数据传输机制,允许服务器为返回的内容维持持久连接,向客户端发送多个分块的数据。
当我们在请求一个图片资源的时候,浏览器可以通过响应头中 Content-Length
长度信息,判断出响应体结束,但是,大多数情况下,服务端输出的内容长度不能确定,无法通过长度信息,来判断实体的边界,这个时候就就需要使用分块传输编码,在响应头中会出现了 Transfer-Encoding: chunked
这样的标识。
每个经过 chunked 编码后的分块包含两个部分:数据以及数据的长度信息,当遇到分块长度为 0,表示该分块没有内容,实体结束,Content-Encoding
和 Transfer-Encoding
二者经常会结合来用,对传输的内容编码压缩,提高传输效率,BigPipe 就是基于分块传输编码,实现页面的分块加载。
使用 Node.js 实现最小化示例
BigPipe 的服务端可以用各种语言实现,这里介绍的使用 Node.js 作为服务端语言,主要用到的是下面的两个方法
- response.write(chunk[, encoding][, callback])
- response.end([data][, encoding][, callback])
当我们多次调用 response.write
,数据自动被流式传输,向浏览器提供多了连续的响应体片段,chunk 可以是字符串或 buffer 类型, res.end
用来告诉服务器,已发送所有响应头和主体,callback 是成功执行后的回调方法。
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 连续发送多个响应体片段,首先输出的是 body 以上片段,然后发送 <div>1</div>
,间隔1s,发送<div>2</div>
,最后调用 res.end
,闭合标签,结束响应体传输。在页面上,我们先看到 1,1s之后,会出现 2。
基于 express 框架实现页面的分块加载
前端页面
<!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'));
客户端处理分块返回的数据
一般的 ajax 请求的处理,只有两种情况:成功、失败。,如何处理正在执行中的数据呢? 我们可以借助xhr.readyState
属性,该属性表示请求的状态,一共有5个状态
- 0: 请求未初始化
- 1: 服务器连接已建立
- 2: 请求已接收
- 3: 请求处理中
- 4: 请求已完成,且响应已就绪
我们经常用到是 xhr.readyState === 4
,然后结合 xhr.status
来判断请求的成功或者失败,执行对应的回调方法。
为了处理分块返回的数据,我们需要监听 xhr.readyState === 3
,每次有分块数据返回的时候,都会触发 xhr.onreadystatechage
的方法,进入 this.readyState === 3
的判断执行语句,实现分块数据的成功回调方法。
注意:当分块数据返回的时间较短的时候,会出现多个分块一起返回的情况,所以我们不能用字符串截取的方式,把已结束的 chunk 存放在数组中。
以下只是部分代码,用来分析客户端处理分块返回数据的过程
let xhr = new XMLHttpRequest();
let chunked = [];
xhr.onreadystatechange = function() {
if ([3, 4].includes(this.readyState)) {
// 因为请求响应较快时,会出现一次返回多个块,所以使用取出数组新增项的做法
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 方案,将每条记录的结果,一个一个的分块返回的,实时渲染出来;当我们遇到一个页面出现很多模块,大量 api 请求的时候,就可以考虑 BigPipe 方案,分块返回数据。
如果这篇文章对您有帮助,记得给作者点个赞,谢谢!
网友评论