新版卖家中心 Bigpipe 实践(一)
作者: 发布于:

新版卖家中心已经上线 3 个多月,乔福老师的 Midway 之旅不只是前后端分离,还是工程化的大作坊。我们很清楚前端不再只是地平线上的人类,现在我们可以深入到 View 层和 Controller 层做更多的有趣事。

View 层

关于 View 层,有张图可能还活在大家的本地缓存里, UI Layer  已不是单纯在服务端或者在客户端。 Midway  做的就是将 View 层彻底授权给前端。然后我们的岁月里就剩下选择,是选择服务端渲染,还是客户端渲染,还是双管齐下。

页面加载方式

掌控了 View 层,除了提高效率,还可以考虑针对不同的场景采用不同的页面加载方式。加载方式的演变可以追溯到上个世纪,而当下能做的就是因地制宜,选择合适的方式到合适场景中。

有哪些加载方式?

1 直接同步加载

服务端渲染,一次加载到客户端

一般场景:页面首屏内容基本就是页面所有内容

2 滚动同步加载(lazyload)

服务端渲染首屏内容,次屏内容放在 textarea 或者注释中,先将内容加载到客户端,滚动时再渲染次屏内容

一般场景:页面内容长度高出首屏内容较多

3 异步加载

服务端渲染主 layout ,加载到客户端,通过 AJAX 获取其他页面内容,然后在客户端渲染

一般场景:目前淘宝无线 H5 的方案类似,通过接口获取数据然后在客户端渲染

4 滚动异步加载(lazyload)

服务端渲染首屏内容,加载到客户端,滚动时再通过 AJAX 获取次屏内容

一般场景:页面内容长度高于首屏内容长度较多

5 分块加载(Bigpipe)

服务端支持 chunk 输出,分块将内容传输到客户端,客户端渲染

一般场景:适合首屏输出

选择的初衷

采用什么样的加载方式除了考虑业务逻辑、页面元素、交互形式,第三方内容等诸多因素,最重要的应该还是用户的感受。就像冯小刚说的“电影要观众说好才是好”,就像 XXX 说的“页面要用户说快才是快”。

卖家中心首页

卖家中心首页的特点

  • 页面主体内容完全模块化
  • 首屏有固定的模块
  • 各个模块有各自的数据
  • 非官方模块卖家可添加和拖拽
  • 有些模块是第三方 iframe 进来

总的来说,首屏模块相对固定,模块较多,页面确实长。

容我三思

  • 选择直接同步加载明天肯定会被叫去谈话
  • 选择滚动同步加载 虽然好点,但是要取所有模块的数据然后拼装好 再放到 textarea ,这个服务端消耗时间太长
  • 选择异步加载 已经统治了 View 层,还做这样的事,会不会太小家子气
  • 选择滚动异步加载 嗯,有朋友圈的人应该都会选择它,首屏先服务端渲染加载,然后滚动的时候再异步加载各个模块
  • 选择分块加载 首屏内容是不是可以这样做

第一步:首屏内容同步,滚动异步加载

我们先选择滚动异步加载的方案实施,我们假设页面中 A/B 为首屏模块 C 为滚动异步加载的模块

  • 客户端请求页面
  • 服务端获取主 layout 数据
  • 服务端 A 模块获取数据
  • 服务端 A 模块模板拼装
  • 服务端 B 模块获取数据
  • 服务端 B 模块模板拼装
  • 拼装成首屏的 HTML
  • 首屏内容加载到客户端
  • 客户端滚动到 C 模块位置
  • 客户端发送请求 C 模块的请求
  • 服务端获取 C 模块数据
  • 服务端 C 模块模板拼装

时间

  • 首屏显示时间依赖 B 模块的处理时间
  • 白屏时间就是 B 模块处理完的时间

问题一

  • 首屏必须等到最后一个首屏模块完成渲染之后才能输出
  • 完成首屏 HTML 加载的时间较长,用户从请求到展现之间,页面白屏较长,这个时候“用户感觉慢”

第二步:减轻首屏内容

白屏太久,用户感知的内容太晚,这个对于整个网页体验是致命的。那我们就减少首屏内容,让首屏轻一点,这样用户就可以更快地感知到内容的输出。

  • A/B 模块都采用异步加载的方式
  • 首屏只包含主 layout 内容
  • 由于 A/B 模块在首屏,所以加载完 layout 之后会直接加载这两个模块

业务场景效果

用这种方式,进入页面主框架内容很快加载到客户端,首屏模块也在 loading 之后 很快展现,作为用户确实“感觉快”了。

时间

同步加载的方式

异步加载首屏模块的方式

  • 首屏显示时间缩短,依赖 layout 的处理时间
  • 白屏时间就是 layout 的处理时间
  • 加载完成的时间变长

问题二

  • 首屏 layout 出来后,首屏模块的 loading 等待比较明显,也会有“模块加载慢”的感觉
  • 有几个模块,首屏就必须多几个请求

分块加载(Bigpipe)

分块传输编码

了解分块加载之前,可以先了解下分块传输编码,分块传输编码允许 HTTP 由应用服务器发送给客户端应用( 通常是网页浏览器)的数据可以分成多个块传输。

对于 Node.js 应用来说,可以很轻松实现分块传输编码, Stream 可以很好地在服务端和客户端建立管道,通过对 Response 的 body 分块 push ,达到内容的逐个展现,而这个过程我们只需要一个请求,没错你没听错,现在只要一个请求。

于是一个构思形成

同步加载首屏的方式

分块加载首屏的方式

  • 所有内容加载完成的时间和同步方式一致
  • 白屏的时间是 layout 的处理时间

有个 demo

  • 按顺序一块一块传输到客户端,逐块显示
  • 因为模块在页面结构的顺序要求,服务端还是得按顺序逐个 push 模块
  • 这么简单的 demo,是拯救不了世界的

打破顺序

  • 第 1 块内容叫做尊重,尊重模块在页面中的顺序
  • 第 2 块到第 4 块内容叫做无视,无视模块在页面中的顺序
  • 第 5 块内容完成页面结构的闭合
  • 第 1 块需要预先定义 doProgress 方法
  • doProgress 帮我们将模块内容放到该放的模块位置上

并行执行

现在我们可以不按顺序来输出内容,第 1 块内容我们可以优先输出主体的 layout ,而第 2 块到第 4 块我们可以有 3! 种排列方式,也就是说模块内容什么时候 push 都可以的。

  • 实际应用中,模块在后端需要取数据拼模板
  • 模块可以并行执行,谁先完成就输出到客户端,无需等待最后一个模块再进行输出

思路图

我们还是假设页面中  A/B 为首屏模块  C 为滚动异步加载的模块

  • 客户端请求页面
  • 服务端获取主 layout 数据
  • 完成 layout 拼装模板后 Flush 到客户端 Display
  • 输出的 layout 必须含有 doProgess 方法
  • 并行执行服务端 A 模块获取数据 A 模块模板拼装
  • 并行执行服务端 B 模块获取数据 B 模块模板拼装
  • A 模块先完成模板拼装,将  doProgess('#A','A template') Script Flush 到客户端
  • 执行方法 doProgess 实现 A 模板的渲染
  • B 模块先完成模板拼装,将  doProgess('#B','B template')  Script Flush 到客户端
  • 执行方法 doProgess 实现 B 模板的渲染
  • 完成并发执行后,Flush 页面结构的闭合标签
  • 客户端滚动到 C 模块位置
  • 客户端发送请求 C 模块的请求
  • 服务端获取 C 模块数据
  • 服务端 C 模块模板拼装

业务场景效果

这次 loading 很快就没了,基本没看到,感觉确实“快了”。

对比下滚动异步加载的效果:

分析

  • 分块加载和滚动异步加载,首屏的 layout 输出时间应该是差不多的
  • 唯一的区别是,分块加载在一个请求内完成了首屏模块的同步输出,而滚动异步加载,首屏的模块还需要往返几次请求之后才能到客户端,所以分块加载确实“快了”

Bigpipe

其实分块加载的原理,其实就是 Bigpipe 的原理,大伙儿可以看看这篇文章了解下。因为逐步推导的需要用分块加载可能会通俗一点。

新版卖家中心是个很好的场景,@乔福 计划做 Bigpipe 优化的决定是个华丽的开始。

实现

熟悉了原理,现在通过 Node.js 实现 Bigpipe ,我们可以发现前人已经积累了很多经验。不管是 Koa 还是 express 都有代码可寻。但是如何在现有的 Midway 体系里面,做一个适用于业务快速采用 Bigpipe 加载方式的东西,还需要在业务实践中慢慢提炼出来。下次可以从代码实现上来详细介绍 Bigpipe 的 Node.js 实现以及那些不为人知的坑。

最终感受

  • 权利越大,责任越大; View 层在手上,除了前后端分离,也要求前端要像后端。
  • 换位思考; 以前在浏览器和无线积累的优化经验换到服务端去实施又会怎样,比如压缩和模块加载等等。
  • 大思路小细节; Bigpipe 是个思路,实践中真正让方案前进的都是小细节的突破。