BigPipe 是 Facebook 开发的优化网页加载速度的技术。网上几乎没有用 node.js 实现的文章,实际上,不止于 node.js,BigPipe 用其他语言的实现在网上都很少见。以至于这技术出现很久以后,我还以为就是整个网页的框架先发送完毕后,用另一个或几个 ajax 请求再请求页面内的模块。直到不久前,我才了解到原来 BigPipe 的核心概念就是只用一个 HTTP 请求,只是页面元素不按顺序发送而已。

了解了这个核心概念就好办了,得益于 node.js 的异步特性,很容易就可以用 node.js 实现 BigPipe。本文会一步一步详尽地用例子来说明 BigPipe 技术的起因和一个基于 node.js 的简单实现。

我会用 express 来演示,简单起见,我们选用 jade 作为模版引擎,并且我们不使用引擎的子模版(partial)特性,而是以子模版渲染完成以后的 HTML 作为父模版的数据。

先建一个 nodejs-bigpipe 的文件夹,写一个 package.json 文件如下:

  1. {
  2. "name": "bigpipe-experiment"
  3. , "version": "0.1.0"
  4. , "private": true
  5. , "dependencies": {
  6. "express": "3.x.x"
  7. , "consolidate": "latest"
  8. , "jade": "latest"
  9. }
  10. }

运行 npm install 安装这三个库,consolidate 是用来方便调用 jade 的。

先做个最简单的尝试,两个文件:

app.js:

  1. var express = require('express')
  2. , cons = require('consolidate')
  3. , jade = require('jade')
  4. , path = require('path')
  5. var app = express()
  6. app.engine('jade', cons.jade)
  7. app.set('views', path.join(__dirname, 'views'))
  8. app.set('view engine', 'jade')
  9. app.use(function (req, res) {
  10. res.render('layout', {
  11. s1: "Hello, I'm the first section."
  12. , s2: "Hello, I'm the second section."
  13. })
  14. })
  15. app.listen(3000)

views/layout.jade

  1. doctype html
  2. head
  3. title Hello, World!
  4. style
  5. section {
  6. margin: 20px auto;
  7. border: 1px dotted gray;
  8. width: 80%;
  9. height: 150px;
  10. }
  11. section#s1!=s1
  12. section#s2!=s2

效果如下:

接下来我们把两个 section 模版放到两个不同的模版文件里:

views/s1.jade:

  1. h1 Partial 1
  2. .content!=content

views/s2.jade:

  1. h1 Partial 2
  2. .content!=content

在 layout.jade 的 style 里增加一些样式

  1. section h1 {
  2. font-size: 1.5;
  3. padding: 10px 20px;
  4. margin: 0;
  5. border-bottom: 1px dotted gray;
  6. }
  7. section div {
  8. margin: 10px;
  9. }

将 app.js 的 app.use() 部分更改为:

  1. var temp = {
  2. s1: jade.compile(fs.readFileSync(path.join(__dirname, 'views', 's1.jade')))
  3. , s2: jade.compile(fs.readFileSync(path.join(__dirname, 'views', 's2.jade')))
  4. }
  5. app.use(function (req, res) {
  6. res.render('layout', {
  7. s1: temp.s1({ content: "Hello, I'm the first section." })
  8. , s2: temp.s2({ content: "Hello, I'm the second section." })
  9. })
  10. })

之前我们说“以子模版渲染完成以后的 HTML 作为父模版的数据”,指的就是这样,temp.s1 和temp.s2 两个方法会生成 s1.jade 和 s2.jade 两个文件的 HTML 代码,然后把这两段代码作为 layout.jade 里面 s1、s2 两个变量的值。

现在页面看起来是这样子:

一般来说,两个 section 的数据是分别获取的——不管是通过查询数据库还是 RESTful 请求,我们用两个函数来模拟这样的异步操作。

  1. var getData = {
  2. d1: function (fn) {
  3. setTimeout(fn, 3000, null, { content: "Hello, I'm the first section." })
  4. }
  5. , d2: function (fn) {
  6. setTimeout(fn, 5000, null, { content: "Hello, I'm the second section." })
  7. }
  8. }

这样一来,app.use() 里的逻辑就会比较复杂了,最简单的处理方式是:

  1. app.use(function (req, res) {
  2. getData.d1(function (err, s1data) {
  3. getData.d2(function (err, s2data) {
  4. res.render('layout', {
  5. s1: temp.s1(s1data)
  6. , s2: temp.s2(s2data)
  7. })
  8. })
  9. })
  10. })

这样也可以得到我们想要的结果,但是这样的话,要足足 8 秒才会返回。

其实实现逻辑可以看出 getData.d2 是在 getData.d1 的结果返回后才开始调用,而它们两者并没有这样的依赖关系。我们可以用如 async 之类的处理 JavaScript 异步调用的库来解决这样的问题,不过我们这里就简单手写吧:

  1. app.use(function (req, res) {
  2. var n = 2
  3. , result = {}
  4. getData.d1(function (err, s1data) {
  5. result.s1data = s1data
  6. --n || writeResult()
  7. })
  8. getData.d2(function (err, s2data) {
  9. result.s2data = s2data
  10. --n || writeResult()
  11. })
  12. function writeResult() {
  13. res.render('layout', {
  14. s1: temp.s1(result.s1data)
  15. , s2: temp.s2(result.s2data)
  16. })
  17. }
  18. })

这样就只需 5 秒。

在接下来的优化之前,我们加入 jquery 库并把 css 样式放到外部文件,顺便,把之后我们会用到的浏览器端使用 jade 模板所需要的 runtime.js 文件也加入进来,在包含 app.js 的目录下运行:

  1. mkdir static
  2. cd static
  3. curl http://code.jquery.com/jquery-1.8.3.min.js -o jquery.js
  4. ln -s ../node_modules/jade/runtime.min.js jade.js

并且把 layout.jade 中的 style 标签里的代码拿出来放到 static/style.css 里,然后把 head 标签改为:

  1. head
  2. title Hello, World!
  3. link(href="/static/style.css", rel="stylesheet")
  4. script(src="/static/jquery.js")
  5. script(src="/static/jade.js")

在 app.js 里,我们把它们两者的下载速度都模拟为两秒,在app.use(function (req, res) {之前加入:

  1. var static = express.static(path.join(__dirname, 'static'))
  2. app.use('/static', function (req, res, next) {
  3. setTimeout(static, 2000, req, res, next)
  4. })

受外部静态文件的影响,我们的页面现在的加载时间为 7 秒左右。

如果我们一收到 HTTP 请求就把 head 部分返回,然后两个 section 等到异步操作结束后再返回,这是利用了 HTTP 的分块传输编码机制。在 node.js 里面只要使用 res.write() 方法就会自动加上 Transfer-Encoding: chunked 这个 header 了。这样就能在浏览器加载静态文件的同时,node 服务器这边等待异步调用的结果了,我们先删除 layout.jade 中的这 section 这两行:

  1. section#s1!=s1
  2. section#s2!=s2

因此我们在 res.render() 里也不用给 { s1: …, s2: … } 这个对象,并且因为 res.render() 默认会调用 res.end(),我们需要手动设置 render 完成后的回调函数,在里面用 res.write() 方法。layout.jade 的内容也不必在 writeResult() 这个回调函数里面,我们可以在收到这个请求时就返回,注意我们手动添加了 content-type 这个 header:

  1. app.use(function (req, res) {
  2. res.render('layout', function (err, str) {
  3. if (err) return res.req.next(err)
  4. res.setHeader('content-type', 'text/html; charset=utf-8')
  5. res.write(str)
  6. })
  7. var n = 2
  8. getData.d1(function (err, s1data) {
  9. res.write('<section id="s1">' + temp.s1(s1data) + '</section>')
  10. --n || res.end()
  11. })
  12. getData.d2(function (err, s2data) {
  13. res.write('<section id="s2">' + temp.s2(s2data) + '</section>')
  14. --n || res.end()
  15. })
  16. })

现在最终加载速度又回到大概 5 秒左右了。实际运行中浏览器先收到 head 部分代码,就去加载三个静态文件,这需要两秒时间,然后到第三秒,出现 Partial 1 部分,第 5 秒出现 Partial 2 部分,网页加载结束。就不给截图了,截图效果和前面 5 秒的截图一样。

但是要注意能实现这个效果是因为 getData.d1 比 getData.d2 快,也就是说,先返回网页中的哪个区块取决于背后的接口异步调用结果谁先返回,如果我们把 getData.d1 改成 8 秒返回,那就会先返回 Partial 2 部分,s1 和 s2 的顺序对调,最终网页的结果就和我们的预期不符了。

这个问题最终将我们引导到 BigPipe 上来,BigPipe 就是能让网页各部分的显示顺序与数据的传输顺序解耦的技术

其基本思路就是,首先传输整个网页大体的框架,需要稍后传输的部分用空 div(或其他标签)表示:

  1. res.render('layout', function (err, str) {
  2. if (err) return res.req.next(err)
  3. res.setHeader('content-type', 'text/html; charset=utf-8')
  4. res.write(str)
  5. res.write('<section id="s1"></section><section id="s2"></section>')
  6. })

然后将返回的数据用 JavaScript 写入

  1. getData.d1(function (err, s1data) {
  2. res.write('<script>$("#s1").html("' + temp.s1(s1data).replace(/"/g, '\\"') + '")</script>')
  3. --n || res.end()
  4. })

s2 的处理与此类似。这时你会看到,请求网页的第二秒,出现两个空白虚线框,第五秒,出现 Partial 2 部分,第八秒,出现 Partial 1 部分,网页请求完成。

至此,我们就完成了一个最简单的 BigPipe 技术实现的网页。

需要注意的是,要写入的网页片段有 script 标签的情况,如将 s1.jade 改为

  1. h1 Partial 1
  2. .content!=content
  3. script
  4. alert("alert from s1.jade")

然后刷新网页,会发现这句 alert 没有执行,而且网页会有错误。查看源代码,知道是因为 <script>里面的字符串出现 </script> 而导致的错误,只要将其替换为 <\/script> 即可

  1. res.write('<script>$("#s1").html("' + temp.s1(s1data).replace(/"/g, '\\"').replace(/<\/script>/g, '<\\/script>') + '")</script>')

以上我们便说明了 BigPipe 的原理和用 node.js 实现 BigPipe 的基本方法。而在实际中应该怎样运用呢?下面提供一个简单的方法,仅供抛砖引玉,代码如下:

  1. var resProto = require('express/lib/response')
  2. resProto.pipe = function (selector, html, replace) {
  3. this.write('<script>' + '$("' + selector + '").' +
  4. (replace === true ? 'replaceWith' : 'html') +
  5. '("' + html.replace(/"/g, '\\"').replace(/<\/script>/g, '<\\/script>') +
  6. '")</script>')
  7. }
  8. function PipeName (res, name) {
  9. res.pipeCount = res.pipeCount || 0
  10. res.pipeMap = res.pipeMap || {}
  11. if (res.pipeMap[name]) return
  12. res.pipeCount++
  13. res.pipeMap[name] = this.id = ['pipe', Math.random().toString().substring(2), (new Date()).valueOf()].join('_')
  14. this.res = res
  15. this.name = name
  16. }
  17. resProto.pipeName = function (name) {
  18. return new PipeName(this, name)
  19. }
  20. resProto.pipeLayout = function (view, options) {
  21. var res = this
  22. Object.keys(options).forEach(function (key) {
  23. if (options[key] instanceof PipeName) options[key] = '<span id="' + options[key].id + '"></span>'
  24. })
  25. res.render(view, options, function (err, str) {
  26. if (err) return res.req.next(err)
  27. res.setHeader('content-type', 'text/html; charset=utf-8')
  28. res.write(str)
  29. if (!res.pipeCount) res.end()
  30. })
  31. }
  32. resProto.pipePartial = function (name, view, options) {
  33. var res = this
  34. res.render(view, options, function (err, str) {
  35. if (err) return res.req.next(err)
  36. res.pipe('#'+res.pipeMap[name], str, true)
  37. --res.pipeCount || res.end()
  38. })
  39. }
  40. app.get('/', function (req, res) {
  41. res.pipeLayout('layout', {
  42. s1: res.pipeName('s1name')
  43. , s2: res.pipeName('s2name')
  44. })
  45. getData.d1(function (err, s1data) {
  46. res.pipePartial('s1name', 's1', s1data)
  47. })
  48. getData.d2(function (err, s2data) {
  49. res.pipePartial('s2name', 's2', s2data)
  50. })
  51. })

还要在 layout.jade 把两个 section 添加回来:

  1. section#s1!=s1
  2. section#s2!=s2

这里的思路是,需要 pipe 的内容先用一个 span 标签占位,异步获取数据并渲染完成相应的 HTML 代码后再输出给浏览器,用 jQuery 的 replaceWith 方法把占位的 span 元素替换掉。

本文的代码在 https://github.com/undozen/bigpipe-on-node ,我把每一步做成一个 commit 了,希望你 clone 到本地实际运行并 hack 一下看看。因为后面几步涉及到加载顺序了,确实要自己打开浏览器才能体验到而无法从截图上看到(其实应该可以用 gif 动画实现,但是我懒得做了)。

关于 BigPipe 的实践还有很大的优化空间,比如说,要 pipe 的内容最好设置一个触发的时间值,如果异步调用的数据很快返回,就不需要用 BigPipe,直接生成网页送出即可,可以等到数据请求超过一定时间才用 BigPipe。使用 BigPipe 相比 ajax 即节省了浏览器到 node.js 服务器的请求数,又节省了 node.js 服务器到数据源的请求数。

原帖:https://github.com/undoZen/bigpipe-on-node

用 NodeJS 实现 BigPipe的更多相关文章

  1. nodejs简易实现一下bigpipe

    今天刚好看到bigpipe的文章,写个demo试一下: nodejs的实现: var fs = require('fs'); module.exports = function(req , res){ ...

  2. 基于NodeJS的全栈式开发

    前言 为了解决传统Web开发模式带来的各种问题,我们进行了许多尝试,但由于前/后端的物理鸿沟,尝试的方案都大同小异.痛定思痛,今天我们重新思考了“前后端”的定义,引入前端同学都熟悉的 NodeJS,试 ...

  3. (转)也谈基于NodeJS的全栈式开发(基于NodeJS的前后端分离)

    原文链接:http://ued.taobao.org/blog/2014/04/full-stack-development-with-nodejs/ 随着不同终端(pad/mobile/pc)的兴起 ...

  4. 也谈基于NodeJS的全栈式开发(基于NodeJS的前后端分离)

    前言 为了解决传统Web开发模式带来的各种问题,我们进行了许多尝试,但由于前/后端的物理鸿沟,尝试的方案都大同小异.痛定思痛,今天我们重新思考了“前后端”的定义,引入前端同学都熟悉的NodeJS,试图 ...

  5. nodeJS实现简单网页爬虫功能

    前面的话 本文将使用nodeJS实现一个简单的网页爬虫功能 网页源码 使用http.get()方法获取网页源码,以hao123网站的头条页面为例 http://tuijian.hao123.com/h ...

  6. 基于NodeJS进行前后端分离

    1.什么是前后端分离 传统的SPA模式:所有用到的展现数据都是后端通过异步接口(AJAX/JSONP)的方式提供的,前端只管展现. 从某种意义上来说,SPA确实做到了前后端分离,但这种方式存在两个问题 ...

  7. 大型NodeJS项目架构与优化

    使用场景: proxy(API冗余,跨域) vue ssr(服务端渲染) socket(大并发,通讯) 区块链(创业公司,新兴行业) 讨论什么? NodeJS异步IO原理及优化方案 NodeJS内存管 ...

  8. NodeJS中间层搭建

    前言 最近碰了个壁,公司开发的一个新项目里我大胆地提出要前后端完全分离,用JavaScript模板引擎.ajax.路由等技术替代繁琐的前后端混合的业务逻辑,项目进行到一半前辈提出来仅仅靠前端的力量无法 ...

  9. 基于NodeJS的全栈式开发(基于NodeJS的前后端分离)

    也谈基于NodeJS的全栈式开发(基于NodeJS的前后端分离) 前言 为了解决传统Web开发模式带来的各种问题,我们进行了许多尝试,但由于前/后端的物理鸿沟,尝试的方案都大同小异.痛定思痛,今天我们 ...

随机推荐

  1. [zoj解题] 1203

    #include <stdio.h> #include <stdlib.h> #include <math.h> #define MAXN 100 #define ...

  2. k-means算法的Python实现

    #coding=utf-8 import codecs import numpy from numpy import * import pylab def loadDataSet(fileName): ...

  3. webapp之路--之query media

    query media是css3中的模块,对于移动端的开发是非常重要的,是响应式web设计的中不可或缺的一部分.简单点说就是根据不同移动设备的屏幕参数来制定不同的css方案以实现web的响应式开发.目 ...

  4. hibernate--多对多单向关联 (重点!!!)

    老师和学生的关系, 一个老师对多个学生, 一个学生也对应多个老师. 数据库会需要3个表, 一个老师表, 一个学生表, 一个老师对应学生表. 单向: 老师知道自己有多少学生, 但是学生不知道自己有多少个 ...

  5. xmlns=""

    用dom4j把两个手上的element撮合到一起,dom4j就会在被加的元素上写上xmlns="",当时我也没在意,后来用户要求去掉,查了一下还不是那么容易,原来xml本身对nam ...

  6. Unity NGUI UIPanel下对粒子的剪裁

    使用 unity 做游戏开发时,有时需要在scroll view下使用粒子,但粒子是不会被 UIPanel 所裁剪的,本人提供了脚本 ParticleSystemClipper,用来处理这种情况.思路 ...

  7. Ubuntu安装PHP时候出错--xml2-config not found

    在Ubuntu下接着安装php时候,在configure后,又出现错误提示:error: xml2-config not found. Please check your libxml2 instal ...

  8. IO的五种模型

    为了区分IO的五种模型,下面先来看看同步与异步.阻塞与非阻塞的概念差别. 同步:所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回.按照这个定义,其实绝大多数函数都是同步调用(例如 ...

  9. 歪国人DIY的MINI四轴

    歪国人DIY的MINI四轴 Crazyflie 2.0 自己仿Crazyflie.CrazyPony

  10. Java通过JNI调用dll详细过程(转)

    源:Java通过JNI调用dll详细过程 最近项目有这样一个需求,在已有的CS软件中添加一个链接,将当前登录用户的用户名加密后放在url地址中,在BS的login方法里通过解密判断,如果为合法用户则无 ...