Implement Bigpipe with NodeJS

Source: Internet
Author: User
Tags jquery library node server

Bigpipe is a technology that Facebook has developed to optimize page loading speed. There are few articles on the web that are implemented with node. js, and in fact, not only are the implementations of Node.js,bigpipe in other languages rare on the web. So long after this technology appeared, I thought that is the entire Web page framework after the first send, with another or a few Ajax requests to request the module within the page. Not until recently did I realize that the core concept of the original bigpipe was to use only one HTTP request, except that the page elements were not sent sequentially.

Understanding this core concept is good, thanks to the asynchronous nature of node. js, it is easy to implement bigpipe with node. js. This article will take a step-by-step example to illustrate the causes of bigpipe technology and a simple implementation based on node. js.

I will use express to illustrate, for simplicity, we choose Jade as the template engine, and we do not use the sub-template (partial) feature of the engine, but rather the sub-template renders the finished HTML as the parent template data.

First build a Nodejs-bigpipe folder, write a Package.json file as follows:

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

Running NPM Install installs these three libraries, consolidate is used to make it easy to call Jade.

Let's make the simplest attempt, two files:

App.js:

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

doctype htmlhead  title Hello, World!  style    section {      margin: 20px auto;      border: 1px dotted gray;      width: 80%;      height: 150px;    }section#s1!=s1section#s2!=s2

The effect is as follows:

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

Views/s1.jade:

h1 Partial 1.content!=content

Views/s2.jade:

h1 Partial 2.content!=content

Add some styles to the Layout.jade style

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

Change the App.use () section of App.js to:

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 "the child template rendering finished after the HTML as the parent template data", referring to this, temp.s1 and temp.s2 two methods will generate S1.jade and s2.jade two files of HTML code, and then the two code as Layout.jade inside S1, S2 A value of two variables.

Now the page looks like this:

In general, two sections of data are obtained separately-whether by querying a database or a RESTful request, we use two functions to simulate such an asynchronous operation.

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 () is more complex, and the simplest processing is:

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 8 seconds to return.

In fact, the implementation of the logic can be seen getdata.d2 is the result of getdata.d1 after the return of the call, and they do not have such a dependency. We can solve this problem with a library that handles JavaScript asynchronous calls such as Async, but here's a simple handwriting:

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 is only 5 seconds.

Before the next optimizations, we added the jquery library and put the CSS style to the external file, and by the way, the Runtime.js file that we used to use the jade template on the browser side was added and ran in the directory containing the app.js:

mkdir staticcd staticcurl http://code.jquery.com/jquery-1.8.3.min.js -o jquery.jsln -s ../node_modules/jade/runtime.min.js jade.js

And put the code in the Layout.jade style tag in the static/style.css, and then change the head tag to:

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, app.use(function (req, res) { before adding:

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

Affected by external static files, our page now has a load time of about 7 seconds.

If we return the head section as soon as we receive the HTTP request, then the two sections wait until the end of the asynchronous operation to return, which is the block transfer encoding mechanism using HTTP. This header is automatically added to node. js using the Res.write () method Transfer-Encoding: chunked . This allows the browser to load the static file while the node server waits for the result of the asynchronous call, we first delete this section of the Layout.jade in the two lines:

section#s1!=s1section#s2!=s2

So we don't have to give {s1: ...} to this object in Res.render (), and because Res.render () calls Res.end () by default, we need to manually set the callback function after render is complete, with Res.write () in it. Method. The contents of the Layout.jade do not have to be in the Writeresult () callback function, we can return this request, note that we manually added the Content-type this header:

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(‘<section id="s1">‘ + temp.s1(s1data) + ‘</section>‘)    --n || res.end()  })  getData.d2(function (err, s2data) {    res.write(‘<section id="s2">‘ + temp.s2(s2data) + ‘</section>‘)    --n || res.end()  })})

The final loading speed is now back about 5 seconds. The actual running browser first received the head part of the code, to load three static files, which takes two seconds, and then to the third second, the partial 1 parts, 5 seconds appear partial 2 part, the end of page loading. Will not give, the effect and the previous 5 seconds of the same.

But be aware that this effect is achieved because GETDATA.D1 is faster than GETDATA.D2, that is, which chunk of the page is returned first depends on the interface behind the asynchronous call results who first returns, if we change the getdata.d1 to 8 seconds back, then we will first return to Partial 2 Points, S1 and S2, the results of the final page are not what we expected.

This problem eventually leads us to Bigpipe,Bigpipe is the technology that allows the display sequence of the pages to be decoupled from the data's order of transmission.

The basic idea is to first transfer the entire Web page frame, the part that needs to be transferred later is represented by an empty div (or other tag):

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(‘<section id="s1"></section><section id="s2"></section>‘)})

The returned data is then written in JavaScript

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

The processing of S2 is similar to this. At this point, you will see that the second second of the request page, there are two blank dotted box, the first five seconds, the partial 2 part, eight seconds, a partial 1 part, Web request completed.

So far, we have completed a Web page with the simplest bigpipe technology implementation.

It is important to note that the page fragment to be written has a script tag, such as changing the S1.jade to

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

Then refresh the page, you will find that this alert is not executed, and the page will be wrong. Look at the source code and know that it is an <script> error caused by a string inside, </script> just replace it <\/script> with

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

Above, we explain the principle of bigpipe and the basic method of implementing Bigpipe with node. js. And how should it be used in practice? Here's an easy way to do this, just for the code:

var Resproto = require (' express/lib/response ') resproto.pipe = function (selector, HTML, replace) {this.write (' <scrip T> ' + ' $ ("' + selector + '). ' + (replace = = = true? ') ReplaceWith ': ' html ') + ' (' + ' + html.replace (/'/g, ' \ \ '). Replace (/<\/script>/g, ' <\\/script> ') + ' ") &lt ;/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 (). substr ING (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 (Fu Nction (Key) {if (Options[key] instanceof pipename) options[key] = ' <span id= ' ' + options[key].id + ' "></span > ') 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 ()})}resPro to.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 (fun Ction (Err, S2data) {res.pipepartial (' s2name ', ' s2 ', S2data)})})

and add the two sections back to the Layout.jade:

section#s1!=s1section#s2!=s2

The idea here is that the content of the pipe needs to be preceded by a span tag placeholder, asynchronously fetching the data and rendering the corresponding HTML code and then outputting it to the browser, using JQuery's ReplaceWith method to replace the placeholder span element.

The code of this article in Https://github.com/undozen/bigpipe-on-node, I put each step into a commit, I hope you clone to the local actual run and hack look. Because the next few steps involve loading order, it is really necessary to open the browser itself to experience and can not see from the above (in fact, it should be possible to use GIF animation, but I am too lazy to do).

About the practice of bigpipe there is a lot of optimization space, for example, to pipe content is best set a trigger time value, if the asynchronous call to return the data quickly, you do not need to use Bigpipe, directly generated Web page send can, wait until the data request more than a certain time to use Bigpipe. Using Bigpipe, Ajax saves the number of requests from the browser to the node. JS Server and saves the number of requests from the node. JS server to the data source.

Original posts: Https://github.com/undoZen/bigpipe-on-node

Implement Bigpipe with NodeJS

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.