新版卖家中心 Bigpipe 实践(二)_html/css_WEB-ITnose
程序员文章站
2022-04-20 08:21:38
...
自从上次通过 新版卖家中心 Bigpipe 实践(一) 阐述了 Bigpipe 实现思路和原理之后,一转眼春天就来了。而整个实践过程,从开始冬天迎着冷风前行,到现在逐渐回暖。其中感受和收获良多,和大家分享下。代码偏多,请自带编译器。
核心问题
一切技术的产生或者使用都是为了解决问题,所以开始前,看下要解决的问题:
- 同步加载首屏模块,服务端各个模块并行生成内容,客户端渲染内容依赖于最后一个内容的生成时间。这里的痛点是 同步 。因为要多模块同步,所以难免浏览器要等待,浏览器等待也就是用户等待。
- 于是我们采用了滚动异步加载模块,页面框架优先直出,几朵菊花旋转点缀,然后首屏模块通过异步请求逐个展现出来。虽然拿到什么就能在客户端渲染显示,但还是有延迟感。这里的痛点是 请求 ,每个模块都需要多一个请求,也需要时间。
- Facebook 的工程师们会不会是这样想的:一次请求,各个首屏模块服务端并行处理生成内容,生成的内容能直接传输给客户端渲染,用户能马上看到内容,这样好猴赛雷~
- 其实 Bigpipe 的思路是从 微处理器的流水线 中受到启发
技术突破口
卖家中心主体也是功能模块化,和 Facebook 遇到的问题是一致的。核心的问题换个说法: 通过一个请求链接,服务端能否将动态内容分块传输到客户端实时渲染展示,直到内容传输结束,请求结束。
概念
- 技术点:HTTP 协议的分块传输(在 HTTP 1.1 提供) 概念入口
- 如果一个 HTTP 消息(请求消息或应答消息)的 Transfer-Encoding 消息头的值为 chunked ,那么,消息体由数量未定的块组成,并以最后一个大小为 0 的块为结束。
- 这种机制使得网页内容分成多个内容块,服务器和浏览器建立管道并管理他们在不同阶段的运行。
实现
如何实现数据分块传输,各个语言的方式并不一样。
PHP 的方式
php chunked
- PHP 利用 ob_flush 和 flush 把页面分块刷新缓存到浏览器,查看 network ,页面的 Transfer-Encoding=chunked ,实现内容的分块渲染。
- PHP 不支持线程,所以服务器无法利用多线程去并行处理多个模块的内容。
- PHP 也有并发执行的方案,这里不做扩展,有兴趣地可以去深入研究下。
Java 的方式
- Java 也有类似于 flush 的函数 实现简单页面的分块传输。
- Java 是多线程的,方便并行地处理各个模块的内容。
flush 的思考
- Yahoo 34 条性能优化 Rules 里面提到 flush 时机是 head 之后,可以让浏览器先行下载 head 中引入的 CSS/js。
- 我们会把内容分成一块一块 flush 到浏览器端,flush 的内容优先级应该是用户关心的。比如 Yahoo 之前优先 flush 的就是搜索框,因为这个是核心功能。
- flush 的内容大小需要进行有效地拆分,大内容可以拆成小内容。
Node.js 实现
通过对比 PHP 和 Java 在实现 Bigpipe 上的优势和劣势,很容易在 Node.js 上找到幸福感。
- Node.js 的异步特性可以很容易地处理并行的问题。
- View 层全面控制,对于需要服务端处理数据和客户端渲染有天然的优势。
- Node.js 中的 HTTP 接口的设计支持许多 HTTP 协议中原本用起来很困难的特性。
回到 HelloWorld
var http = require('http');http.createServer(function (request, response){ response.writeHead(200, {'Content-Type': 'text/html'}); response.write('hello'); response.write(' world '); response.write('~ '); response.end();}).listen(8080, "127.0.0.1");
- HTTP 头 Transfer-Encoding=chunked ,我的天啊,太神奇了!
- 如果只是 response.write 数据,没有指示 response.end ,那么这个响应就没有结束,浏览器会保持这个请求。在没有调用 response.end 之前,我们完全可以通过 response.write 来 flush 内容。
- 把 Bigpipe Node.js 实现是从 HelloWorld 开始,心情有点小激动。
完整点
layout.html
- head 里面放我们要加载的 assets
- 输出页面框架,A/B/C 模块的占位
var http = require('http');var fs = require('fs');http.createServer(function(request, response) { response.writeHead(200, { 'Content-Type': 'text/html' }); // flush layout and assets var layoutHtml = fs.readFileSync(__dirname + "/layout.html").toString(); response.write(layoutHtml); // fetch data and render response.write(''); response.write(''); response.write(''); // close body and html tags response.write(''); // finish the response response.end();}).listen(8080, "127.0.0.1");
页面输出:
moduleAmoduleBmoduleC
- flush layout 的内容 包含浏览器渲染的函数
- 然后进入核心的取数据、模板拼装,将可执行的内容 flush 到浏览器
- 浏览器进行渲染(此处还未引入并行处理)
- 关闭 body 和 HTML 标签
- 结束响应 完成一个请求
express 实现
var express = require('express');var app = express();var fs = require('fs');app.get('/', function (req, res) { // flush layout and assets var layoutHtml = fs.readFileSync(__dirname + "/layout.html").toString(); res.write(layoutHtml); // fetch data and render res.write(''); res.write(''); res.write(''); // close body and html tags res.write('