宝哥软件园

详细说明通过node.js实现BigPipe

编辑:宝哥软件园 来源:互联网 时间:2021-09-09

BigPipe是脸书为优化网页加载速度而开发的技术。网上几乎没有node.js实现的文章。事实上,BigPipe在其他语言中的实现在互联网上并不多见。在这项技术出现这么久之后,我以为是先发送整个网页框架,然后用另一个ajax请求来请求页面中的模块。不久前,我意识到BigPipe的核心概念是只使用一个HTTP请求,但是页面元素发送顺序不对。

这个核心概念很容易理解。得益于node.js的异步特性,BigPipe可以很容易地用Node.js实现,本文将结合实例一步步详细讲解BigPipe技术的由来以及一个基于node.js的简单实现。

我会用快递来演示。为了简单起见,我们选择jade作为模板引擎,我们不使用引擎的部分特性,而是使用渲染子模板后的HTML作为父模板的数据。

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

复制代码如下: { ' name ' : ' big pipe-experience ',' version' :' 0.1.0 ',' private' : true,' dependencies ' 3360 { ' express ' : ' 3 . x . x '

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

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

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(' view ',path . join(_ dirname,' view ')app . set(' view engine ',' jade ')

app.use(function (req,res) { res.render('layout ',{ s1: '你好,我是第一节。s2:你好,我是第二段。' })})

app.listen(3000)

视图/布局

复制代码如下:doctype html

头像标题你好,世界!样式部分{ margin: 20px autoborder: 1px点灰色;宽度:80%;高度: 150像素;}

第s1节!=S1部分#s2!=s2

效果如下:

接下来,我们将两个节模板放入两个不同的模板文件中:

view/s 1 . jade :

复制代码如下:h1 Partial 1.content!=内容

浏览量/s2.jade:副本代码如下:h1 Partial 2.content!=内容

在布局风格上增加一些风格

副本代码如下: H1段{ font-size : 1.5;padding: 10px 20pxmargin : 0;边框-bottom: 1px点灰色;} section div { margin: 10px}

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

复制代码如下: vartemp={ s 13360 jade.compile(fs . readfilesync(path . join(_ dirname,' views ',' s1.jade '))。s : jade.compile(fs . readfilesync(path . join(_ dirname,' views ',' S2 . jade '))))app . use(function(req,res) { res.render('layout ',{ s : temp . S1({ content : ' Hello,我是第一节。}),s : temp . S2({ content : '你好,我是二段。' }) })})

之前我们说“渲染子模板后使用HTML作为父模板的数据”,就是说temp.s1和temp.s2两种方法会生成s1.jade和s2.jade的HTML代码,然后将这两个代码作为layout.jade中s1和s2变量的值

现在页面如下所示:

一般来说,两个断面的数据是分别得到的(——)。我们使用两个函数来模拟这种异步操作。

复制代码如下: var getdata={ d 1: function(fn){ settimeout(fn,3000,null,{content3360 '你好,我是第一节'})}.d2:函数(fn) { setTimeout(fn,5000,null,{ content: '你好,我是第二段。' }) }}

这样,app.use()中的逻辑会更加复杂,最简单的处理方式就是:

复制代码如下:app.use (function (req,RES) {getdata.d1 (function (err,S1 data){ getdata . D2(function(err,s2data) {res.render ('layout '),{ s 13360 temp . S1(S1 data),s233366)

我们可以得到我们想要的结果,但在这种情况下,需要8秒钟才能返回。

实际上,实现逻辑可以看到,getData.d2是在getData.d1的结果返回后才调用的,它们之间并没有这种依赖关系。我们可以用处理JavaScript异步调用的async之类的库来解决这个问题,但是让我们简单地写在这里:

复制代码如下:app.use (function (req,RES) {var n=2,result={ } getdata . D1(function(err,S1 data){ result . S1 data=S1 data-n | | write result()})getdata . D2(function(err,S2 data){ result . S2 data=S2 data-n | | write result()})function write result(){ RES . render(' layout ',{ s 13360 temp . S1(result . S1 data),s 3366

这只需要5秒钟。

在下一次优化之前,我们加入jquery库,并将css样式放在外部文件中。顺便说一下,我们还加入了runtime.js文件,我们将在浏览器中使用它来使用jade模板,并在包含app.js的目录中运行它:

复制的代码如下: mkdir静态CD静态curl http://code.jquery.com/jquery-1.8.3.min.js-o jquery . jsln-s/node _ modules/jade/runtime . min . jsjade . js

并将layout.jade中的style标签中的代码放入static/style.css中,然后将head标签改为:

复制代码如下:头像标题你好,世界!link(href='/static/style.css ',rel='样式表')脚本(src=' http :/static/jquery . js ')脚本(src='http:/static/jade.js ')

在app.js中,我们将两者的下载速度模拟为两秒。在app.use(function (req,res) {,我们添加了以下复制代码: var static=express . static(path . join(_ dirname,' static ')app . use('/static ',function (req,res,next) { setTimeout(static,2000,req,res,next)})

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

如果我们一收到HTTP请求就返回头部,然后这两个部分等到异步操作完成再返回,这就利用了HTTP的块传输编码机制。在node.js中,只要使用res.write()方法,就会自动添加Transfer-Encoding: chunked的头。这样,当浏览器加载静态文件时,节点服务器等待异步调用的结果。让我们删除布局中的这两行部分。jade第一:复制代码如下:section#s1!=S1部分#s2!=s2

因此,我们不需要在res.render()中给这个对象{ s 13360…,s 23360…},而且由于res.render()默认会调用res.end(),所以渲染后需要手动设置回调函数,并在其中使用res.write()方法。layout.jade的内容不必在回调函数writeResult()中。当我们收到这个请求时,我们可以归还它。请注意,我们手动添加了内容类型头:

复制代码如下: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(函数(err,S1 data){ RES . write(' section id=' S1 ' ' temp . S1(S1 data)'/section ')-n | | RES . end()})getdata . D2(函数(err,S2 data){ RES . write(' section id=' S2 ' ' temp . S2(S2 data)'/section ')-n | | RES . end()})

现在最终加载速度已经恢复到5秒左右。在实际操作中,浏览器首先接收头码,然后加载三个静态文件,耗时两秒。然后在第三秒出现Partial 1部分,在第五秒出现Partial 2部分,网页加载结束。我就不给截图了,截图效果和前面5秒一样。

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

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

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

复制代码代码如下:res.render('layout ',函数(err,str){ if(err)返回RES . req。next(err)RES . setheader(' content-type ',' text/html;charset=utf-8 ')RES . write(str)RES . write('节id=' S1 '/节id=' S2 '/节')})

然后将返回的数据用Java脚本语言写入

复制代码代码如下:getData.d1(函数(呃,S1数据){ RES . write(' script $(' # S1 ')).html('' temp.s1(s1data)' .替换(/'/g ', ' ')' '))/script ')-n | | RES . end()})

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

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

需要注意的是,要写入的网页片段有脚本标签的情况,如将s1。翡翠改为:

复制代码代码如下:h1部分1 .内容!=内容脚本警报("来自s1 .翡翠的警报")

然后刷新网页,会发现这句警报没有执行,而且网页会有错误。查看源代码,知道是因为脚本里面的字符串出现/script而导致的错误,只要将其替换为/脚本即可复制代码代码如下:res.write('script$('#s1 ').html('' temp.s1(s1data)' .替换(/'/g ',\ ').替换(//script/g,' \/script') '')/script ')

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

复制代码代码如下: var resp roto=require(快速/lib/响应)resp roto。pipe=function(选择器,html,replace){ this。编写('脚本' ' $('选择器' ')')(替换===真?替换为' : ' html ')'(' html。替换(/'/g ',\ ').替换(//script/g,' /script ')/script ')}函数管道名称(res,name){ RES .管道计数=RES .管道计数| | 0 RES .管道映射=RES .管道映射| | { } if(RES .管道映射[name])返回管道计数管道图[名称]=这个。id=[' pipe ',Math.random().toString().子字符串(2),(新的日期()).valueOf()].联接(' _ ')这个。RES=RES这个。name=name } resp roto。管道名称=功能(名称){ 0返回新的管道名称(这个,名字)和管道。管道布局=功能(视图,选项){ var RES=此对象。按键(选项).forEach(function(key){ if(options[key]管道名称实例)options[key]=' span id=' options[key]' .id ''/span' }) res.render(视图,选项,函数{ if(err)返回RES . req。next(err)RES . setheader(' content-type ',' text/html;charset=utf-8 ')RES . write(str)if(!管道数)管道末端()})管道。管道部分=函数(名称,视图,选项){ var RES=这个RES . render(视图,选项,函数(err,str){ if(err)返回RES . req。下一个(错误)RES . pipe(' # ' RES . pipe map[name],str,true)-RES . pipe count | | RES . end()})app。get(/'),function (req,res) { res.pipeLayout('layout ',{ s 13360 RES . pipe name(' s1n ')

还要在布局。翡翠把两个部分添加回来:复制代码代码如下第:节#s1!=S1部分#s2!=s2

这里的思想是管道的内容需要被span标记占据,数据异步获取,相应的HTML代码被渲染然后输出到浏览器,被占据的span元素被jQuery的replaceWith方法替代。

这篇文章的代码在https://github.com/undozen/bigpipe-on-node,我已经承诺了每一步。我希望你能克隆它在本地运行并黑掉它。因为接下来的几个步骤都涉及到加载顺序,确实要打开浏览器才能体验,但是从截图上看不出来(其实应该是用gif动画实现的,但是我懒得去做)。

BigPipe的实践还有很大的优化空间。例如,最好为管道内容设置一个触发时间值。如果异步调用的数据返回很快,就不需要使用BigPipe,而是直接生成一个网页发送出去。您可以等到数据请求超过一定时间后再使用BigPipe。与ajax相比,使用BigPipe可以节省浏览器对node.js服务器的请求数量和node.js服务器对数据源的请求数量。但是具体的优化和实践方法要在雪球网使用BigPipe后分享。

更多资讯
游戏推荐
更多+