使用 BigPipe 实现长链接

BigPipe 简介

2010年的 Facebook 提出 BigPipe 技术,通过将站点分解为多个 pagelet 小块,每个pagelet 获取数据与渲染均是独立的。

BigPipe 模式可以实现 pagelet 的数据一旦返回,就可以无阻塞的在浏览器端进行渲染,以此来实现大型复杂页面的性能加速。

分块传输编码

分块传输编码是在 HTTP/1.1 版本中引入的一种数据传输机制,允许服务器为返回的内容维持持久连接,向客户端发送多个分块的数据。

当我们在请求一个图片资源的时候,浏览器可以通过响应头中 Content-Length 长度信息,判断出响应体结束,但是,大多数情况下,服务端输出的内容长度不能确定,无法通过长度信息,来判断实体的边界,这个时候就就需要使用分块传输编码,在响应头中会出现了 Transfer-Encoding: chunked 这样的标识。

每个经过 chunked 编码后的分块包含两个部分:数据以及数据的长度信息,当遇到分块长度为 0,表示该分块没有内容,实体结束,Content-EncodingTransfer-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(`
                
                BigPipe
                `);
    res.write(`
1
`); await sleep(1000); res.write(`
2
`); res.end(` `); }).listen(3000);

通过 htttp.createServer 启动一个简单的 web 服务,通过 res.write 连续发送多个响应体片段,首先输出的是 body 以上片段,然后发送

1
,间隔1s,发送
2
,最后调用 res.end,闭合标签,结束响应体传输。在页面上,我们先看到 1,1s之后,会出现 2。

基于 express 框架实现页面的分块加载

前端页面



    
        
        
        
        BigPipe Demo
        
        
    
    
        
loading...
loading...
loading...

服务端代码

const express = require('express');
const fs = require('fs');
const app = express();
const renderModule = (moduleId, res) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const template = `${moduleId.toUpperCase()}`;
            res.write(
                ``
            );
            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>/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 方案,分块返回数据。

如果这篇文章对您有帮助,记得给作者点个赞,谢谢!

你可能感兴趣的:(使用 BigPipe 实现长链接)