Home  >  Article  >  Web Front-end  >  Node.js implements BigPipe in detail_node.js

Node.js implements BigPipe in detail_node.js

WBOY
WBOYOriginal
2016-05-16 16:29:031557browse

BigPipe is a technology developed by Facebook to optimize web page loading speed. There are almost no articles implemented using node.js on the Internet. In fact, not only node.js, BigPipe implementations in other languages ​​are rare on the Internet. So long after this technology appeared, I thought that after the entire web page frame was sent first, another or several ajax requests were used to request the modules in the page. Until not long ago, I learned that the core concept of BigPipe is to use only one HTTP request, but the page elements are sent out of order.

It will be easier once you understand this core concept. Thanks to the asynchronous features of node.js, it is easy to implement BigPipe with node.js. This article will use examples step by step to explain the origin of BigPipe technology and a simple implementation based on node.js.

I will use express to demonstrate. For simplicity, we choose jade as the template engine, and we do not use the sub-template (partial) feature of the engine. Instead, we use the HTML after the sub-template is rendered as the data of the parent template.

First create a nodejs-bigpipe folder and write a package.json file as follows:

Copy code The code is as follows:

{
"name": "bigpipe-experiment"
, "version": "0.1.0"
, "private": true
, "dependencies": {
"express": "3.x.x"
, "consolidate": "latest"
, "jade": "latest"
}
}

Run npm install to install these three libraries. consolidate is used to facilitate calling jade.

Let’s try the simplest first, two files:

app.js:

Copy code The code is as follows:

var express = require('express')
, cons = require('consolidate')
, jade = require('jade')
, path = require('path')

var app = express()

app.engine('jade', cons.jade)
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'jade')

app.use(function (req, res) {
res.render('layout', {
s1: "Hello, I'm the first section."
, s2: "Hello, I'm the second section."
})
})

app.listen(3000)

views/layout.jade

Copy code The code is as follows:

doctype html

head
title Hello, World!
style
Section {
        margin: 20px auto;
Border: 1px dotted gray;
       width: 80%;
Height: 150px;
}

section#s1!=s1
section#s2!=s2

The effect is as follows:

Next we put the two section templates into two different template files:

views/s1.jade:

Copy code The code is as follows:

h1 Partial 1
.content!=content

views/s2.jade:

Copy code The code is as follows:

h1 Partial 2
.content!=content

Add some styles to the style of layout.jade

Copy code The code is as follows:

section h1 {
font-size: 1.5;
padding: 10px 20px;
margin: 0;
border-bottom: 1px dotted gray;
}
section div {
margin: 10px;
}

Change the app.use() part of app.js to:

Copy code The code is as follows:

var temp = {
s1: jade.compile(fs.readFileSync(path.join(__dirname, 'views', 's1.jade')))
, s2: jade.compile(fs.readFileSync(path.join(__dirname, 'views', 's2.jade')))
}
app.use(function (req, res) {
res.render('layout', {
s1: temp.s1({ content: "Hello, I'm the first section." })
, s2: temp.s2({ content: "Hello, I'm the second section." })
})
})

Before we said "use the HTML after the sub-template is rendered as the data of the parent template", that's what it means. The two methods temp.s1 and temp.s2 will generate two files, s1.jade and s2.jade. HTML code, and then use these two pieces of code as the values ​​of the two variables s1 and s2 in layout.jade.

The page now looks like this:

Generally speaking, the data of the two sections are obtained separately - whether by querying the database or RESTful request, we use two functions to simulate such asynchronous operations.

Copy code The code is as follows:

var getData = {
d1: function (fn) {
​ ​ ​ setTimeout(fn, 3000, null, { content: "Hello, I'm the first section." })
}
, d2: function (fn) {
​ ​ ​ setTimeout(fn, 5000, null, { content: "Hello, I'm the second section." })
}
}

In this way, the logic in app.use() will be more complicated. The simplest way to deal with it is:

Copy code The code is as follows:

app.use(function (req, res) {
getData.d1(function (err, s1data) {
GetData.d2(function (err, s2data) {
res.render('layout', {
          s1: temp.s1(s1data)
, , s2: temp.s2(s2data)
})
})
})
})

This can also get the result we want, but in this case, it will take a full 8 seconds to return.

In fact, the implementation logic shows that getData.d2 is called only after the result of getData.d1 is returned, and there is no such dependency between the two. We can use libraries such as async that handle JavaScript asynchronous calls to solve this problem, but let’s simply write it by hand here:

Copy code The code is as follows:

app.use(function (req, res) {
var n = 2
, result = {}
getData.d1(function (err, s1data) {
​ result.s1data = s1data
--n || writeResult()
})
getData.d2(function (err, s2data) {
​ result.s2data = s2data
--n || writeResult()
})
function writeResult() {
res.render('layout', {
        s1: temp.s1(result.s1data)
, s2: temp.s2(result.s2data)
})
}
})

This will only take 5 seconds.

Before the next optimization, we added the jquery library and put the css style into an external file. By the way, we also added the runtime.js file needed to use the jade template on the browser side that we will use later. Run in the directory containing app.js:

Copy code The code is as follows:

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

And take out the code in the style tag in layout.jade and put it into static/style.css, and then change the head tag to:

Copy code The code is as follows:

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

In app.js, we simulate the download speed of both of them to two seconds, and add:
before app.use(function (req, res) {

Copy code The code is as follows:

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

Our page now loads in around 7 seconds due to external static files.

If we return the head part as soon as we receive the HTTP request, and then the two sections wait until the asynchronous operation is completed before returning, this is using the HTTP chunked transfer encoding mechanism. In node.js, as long as you use the res.write() method, the Transfer-Encoding: chunked header will be automatically added. In this way, while the browser loads the static file, the node server is waiting for the result of the asynchronous call. Let's first delete these two lines in layout.jade:

Copy code The code is as follows:

section#s1!=s1
section#s2!=s2

So we don’t need to give the object { s1: …, s2: … } in res.render(), and because res.render() will call res.end() by default, we need to manually set render after completion The callback function uses the res.write() method in it. The content of layout.jade does not need to be in the writeResult() callback function. We can return when receiving this request. Note that we manually added the content-type header:

Copy code The code is as follows:

app.use(function (req, res) {
res.render('layout', function (err, str) {
If (err) return res.req.next(err)
res.setHeader('content-type', 'text/html; charset=utf-8')
res.write(str)
})
var n = 2
getData.d1(function (err, s1data) {
res.write('
' temp.s1(s1data) '
')
--n || res.end()
})
getData.d2(function (err, s2data) {
res.write('
' temp.s2(s2data) '
')
--n || res.end()
})
})

Final loading speed is now back to around 5 seconds. In actual operation, the browser first receives the head part of the code, and then loads three static files, which takes two seconds. Then in the third second, the Partial 1 part appears, and the Partial 2 part appears in the 5th second, and the web page is loaded. I won’t take a screenshot, the screenshot effect is the same as the previous 5 seconds screenshot.

But please note that this effect can be achieved because getData.d1 is faster than getData.d2. In other words, which block in the web page is returned first depends on the asynchronous call result of the interface behind which returns first. If we put getData. If d1 is changed to 8 seconds to return, Partial 2 will be returned first, and the order of s1 and s2 will be reversed. The final result of the web page will not match our expectations.

This question eventually leads us to BigPipe. BigPipe is a technology that can decouple the display order of each part of the web page from the data transmission order.

The basic idea is to first transfer the general frame of the entire web page, and the parts that need to be transferred later are represented by empty divs (or other tags):

Copy code The code is as follows:

res.render('layout', function (err, str) {
if (err) return res.req.next(err)
res.setHeader('content-type', 'text/html; charset=utf-8')
res.write(str)
res.write('
')
})

Then write the returned data using JavaScript

Copy code The code is as follows:

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

s2 is handled similarly. At this time, you will see that in the second second of requesting the web page, two blank dotted boxes appear, in the fifth second, the Partial 2 part appears, in the eighth second, the Partial 1 part appears, and the web page request is completed.

At this point, we have completed a web page implemented with the simplest BigPipe technology.

It should be noted that the web page fragment to be written has a script tag. For example, change s1.jade to:

Copy code The code is as follows:

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

Then refresh the webpage and you will find that the alert is not executed and there will be errors on the webpage. Check the source code and know that the error is caused by appearing in the string inside <script>. Just replace it with </script>

Copy code The code is as follows:

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

Above we have explained the principle of BigPipe and the basic method of implementing BigPipe using node.js. And how should it be used in practice? A simple method is provided below, just for reference. The code is as follows:

Copy code The code is as follows:

var resProto = require('express/lib/response')
resProto.pipe = function (selector, html, replace) {
  this.write('<script>' '$("' selector '").' <br>     (replace === true ? 'replaceWith' : 'html') <br>     '("' html.replace(/"/g, '\"').replace(/</script>/g, '<\/script>')
    '")')
}
function PipeName (res, name) {
  res.pipeCount = res.pipeCount || 0
  res.pipeMap = res.pipeMap || {}
  if (res.pipeMap[name]) return
  res.pipeCount
  res.pipeMap[name] = this.id = ['pipe', Math.random().toString().substring(2), (new Date()).valueOf()].join('_')
  this.res = res
  this.name = name
}
resProto.pipeName = function (name) {
  return new PipeName(this, name)
}
resProto.pipeLayout = function (view, options) {
  var res = this
  Object.keys(options).forEach(function (key) {
    if (options[key] instanceof PipeName) options[key] = ''
  })
  res.render(view, options, function (err, str) {
    if (err) return res.req.next(err)
    res.setHeader('content-type', 'text/html; charset=utf-8')
    res.write(str)
    if (!res.pipeCount) res.end()
  })
}
resProto.pipePartial = function (name, view, options) {
  var res = this
  res.render(view, options, function (err, str) {
    if (err) return res.req.next(err)
    res.pipe('#' res.pipeMap[name], str, true)
    --res.pipeCount || res.end()
  })
}
app.get('/', function (req, res) {
  res.pipeLayout('layout', {
      s1: res.pipeName('s1name')
    , s2: res.pipeName('s2name')
  })
  getData.d1(function (err, s1data) {
    res.pipePartial('s1name', 's1', s1data)
  })
  getData.d2(function (err, s2data) {
    res.pipePartial('s2name', 's2', s2data)
  })
})

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

复制代码 代码如下:

section#s1!=s1
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 服务器到数据源的请求数。不过具体的优化和实践方法,等到雪球网用上 BigPipe 以后再分享吧。

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn