亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 編程 > JavaScript > 正文

node.js實現BigPipe詳解

2019-11-20 13:48:43
字體:
來源:轉載
供稿:網友

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 文件如下:

復制代碼 代碼如下:

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

運行 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('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 html

head
  title Hello, World!
  style
    section {
      margin: 20px auto;
      border: 1px dotted gray;
      width: 80%;
      height: 150px;
    }

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

效果如下:

接下來我們把兩個 section 模版放到兩個不同的模版文件里:

views/s1.jade:

復制代碼 代碼如下:

h1 Partial 1
.content!=content

views/s2.jade:

復制代碼 代碼如下:

h1 Partial 2
.content!=content

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

復制代碼 代碼如下:

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

將 app.js 的 app.use() 部分更改為:

復制代碼 代碼如下:

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." })
  })
})

之前我們說“以子模版渲染完成以后的 HTML 作為父模版的數據”,指的就是這樣,temp.s1 和 temp.s2 兩個方法會生成 s1.jade 和 s2.jade 兩個文件的 HTML 代碼,然后把這兩段代碼作為 layout.jade 里面 s1、s2 兩個變量的值。

現在頁面看起來是這樣子:

一般來說,兩個 section 的數據是分別獲取的――不管是通過查詢數據庫還是 RESTful 請求,我們用兩個函數來模擬這樣的異步操作。

復制代碼 代碼如下:

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." })
    }
}

這樣一來,app.use() 里的邏輯就會比較復雜了,最簡單的處理方式是:

復制代碼 代碼如下:

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)
      })
    })
  })
})

這樣也可以得到我們想要的結果,但是這樣的話,要足足 8 秒才會返回。

其實實現邏輯可以看出 getData.d2 是在 getData.d1 的結果返回后才開始調用,而它們兩者并沒有這樣的依賴關系。我們可以用如 async 之類的處理 JavaScript 異步調用的庫來解決這樣的問題,不過我們這里就簡單手寫吧:

復制代碼 代碼如下:

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)
    })
  }
})

這樣就只需 5 秒。

在接下來的優化之前,我們加入 jquery 庫并把 css 樣式放到外部文件,順便,把之后我們會用到的瀏覽器端使用 jade 模板所需要的 runtime.js 文件也加入進來,在包含 app.js 的目錄下運行:

復制代碼 代碼如下:

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

并且把 layout.jade 中的 style 標簽里的代碼拿出來放到 static/style.css 里,然后把 head 標簽改為:

復制代碼 代碼如下:

head
  title Hello, World!
  link(href="/static/style.css", rel="stylesheet")
  script(src="/static/jquery.js")
  script(src="/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 請求就把 head 部分返回,然后兩個 section 等到異步操作結束后再返回,這是利用了 HTTP 的分塊傳輸編碼機制。在 node.js 里面只要使用 res.write() 方法就會自動加上 Transfer-Encoding: chunked 這個 header 了。這樣就能在瀏覽器加載靜態文件的同時,node 服務器這邊等待異步調用的結果了,我們先刪除 layout.jade 中的這 section 這兩行:

復制代碼 代碼如下:

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

因此我們在 res.render() 里也不用給 { s1: …, s2: … } 這個對象,并且因為 res.render() 默認會調用 res.end(),我們需要手動設置 render 完成后的回調函數,在里面用 res.write() 方法。layout.jade 的內容也不必在 writeResult() 這個回調函數里面,我們可以在收到這個請求時就返回,注意我們手動添加了 content-type 這個 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()
  })
})

現在最終加載速度又回到大概 5 秒左右了。實際運行中瀏覽器先收到 head 部分代碼,就去加載三個靜態文件,這需要兩秒時間,然后到第三秒,出現 Partial 1 部分,第 5 秒出現 Partial 2 部分,網頁加載結束。就不給截圖了,截圖效果和前面 5 秒的截圖一樣。

但是要注意能實現這個效果是因為 getData.d1 比 getData.d2 快,也就是說,先返回網頁中的哪個區塊取決于背后的接口異步調用結果誰先返回,如果我們把 getData.d1 改成 8 秒返回,那就會先返回 Partial 2 部分,s1 和 s2 的順序對調,最終網頁的結果就和我們的預期不符了。

這個問題最終將我們引導到 BigPipe 上來,BigPipe 就是能讓網頁各部分的顯示順序與數據的傳輸順序解耦的技術。

其基本思路就是,首先傳輸整個網頁大體的框架,需要稍后傳輸的部分用空 div(或其他標簽)表示:

復制代碼 代碼如下:

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>')
})

然后將返回的數據用 JavaScript 寫入

復制代碼 代碼如下:

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

s2 的處理與此類似。這時你會看到,請求網頁的第二秒,出現兩個空白虛線框,第五秒,出現 Partial 2 部分,第八秒,出現 Partial 1 部分,網頁請求完成。

至此,我們就完成了一個最簡單的 BigPipe 技術實現的網頁。

需要注意的是,要寫入的網頁片段有 script 標簽的情況,如將 s1.jade 改為:

復制代碼 代碼如下:

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

然后刷新網頁,會發現這句 alert 沒有執行,而且網頁會有錯誤。查看源代碼,知道是因為 <script> 里面的字符串出現 </script> 而導致的錯誤,只要將其替換為 <//script> 即可

復制代碼 代碼如下:

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

以上我們便說明了 BigPipe 的原理和用 node.js 實現 BigPipe 的基本方法。而在實際中應該怎樣運用呢?下面提供一個簡單的方法,僅供拋磚引玉,代碼如下:

復制代碼 代碼如下:

var resProto = require('express/lib/response')
resProto.pipe = function (selector, html, replace) {
  this.write('<script>' + '$("' + selector + '").' +
    (replace === true ? 'replaceWith' : 'html') +
    '("' + html.replace(/"/g, '//"').replace(/<//script>/g, '<///script>') +
    '")</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] = '<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()
  })
}
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 以后再分享吧。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
亚洲国产欧美自拍| 成人精品aaaa网站| 欧美日韩国产精品一区二区不卡中文| 亚洲天堂网在线观看| 国产精品极品美女粉嫩高清在线| 日韩免费中文字幕| 奇米四色中文综合久久| 视频一区视频二区国产精品| 国产精品成人在线| 精品国产一区二区三区久久| 中文字幕日韩综合av| 2019中文在线观看| 国产成人精品一区二区在线| 国产精品h片在线播放| 91国内在线视频| 欧美疯狂xxxx大交乱88av| 国产精品美女主播在线观看纯欲| 精品久久久久久久久久久久久| 中文国产成人精品| 91精品国产91久久| 最近日韩中文字幕中文| 麻豆国产精品va在线观看不卡| 日韩天堂在线视频| 欧美电影在线播放| 国产精品精品视频| 亚洲美女视频网站| 97成人精品视频在线观看| 91精品美女在线| 538国产精品一区二区免费视频| 国产精品一区二区性色av| 成人黄色免费网站在线观看| 午夜精品久久17c| 欧美日韩一区二区免费在线观看| 国产精品v日韩精品| 亚洲精品视频在线观看视频| 久久久久久久国产精品视频| 国产精品久久久久久av| 国产美女主播一区| 日本最新高清不卡中文字幕| 国产精品日韩专区| 日韩美女中文字幕| 国产欧美精品在线播放| 亚洲色图av在线| 欧美另类交人妖| 亚洲www永久成人夜色| 国内外成人免费激情在线视频网站| 国产精品久久久久久久久粉嫩av| 色婷婷**av毛片一区| 国产精品wwww| 亚洲人成网站999久久久综合| 成人黄色网免费| 日韩一区二区av| 两个人的视频www国产精品| 日韩av在线电影网| 欧美国产第二页| 午夜精品美女自拍福到在线| 亚洲日韩中文字幕在线播放| 国产精品91在线观看| 成人高h视频在线| 国产性色av一区二区| 欧美激情va永久在线播放| 日韩精品在线电影| 日韩av电影中文字幕| 日韩免费观看视频| 日韩中文在线中文网三级| 欧美激情综合色综合啪啪五月| www.欧美三级电影.com| 国产精品成人aaaaa网站| 欧美久久久精品| 国产精品视频一| 日本免费一区二区三区视频观看| 欧美丝袜美女中出在线| 欧美精品videos性欧美| 丰满岳妇乱一区二区三区| 永久免费精品影视网站| 欧美成人在线网站| 久久激情视频久久| 成人在线一区二区| 国产精品日韩欧美大师| 欧美乱妇40p| 亚洲人成免费电影| 日韩视频永久免费观看| 成人444kkkk在线观看| 在线观看视频99| 在线视频欧美日韩| 亚洲欧美在线一区| 久久人91精品久久久久久不卡| 欧美激情va永久在线播放| 国产999精品久久久影片官网| 亚洲国产精品悠悠久久琪琪| 成人在线国产精品| 国产午夜精品一区理论片飘花| 久久久久日韩精品久久久男男| 欧美激情手机在线视频| 两个人的视频www国产精品| 久久综合免费视频影院| 国产日韩欧美在线视频观看| 欧美国产在线视频| 中文字幕亚洲欧美一区二区三区| 国产精品电影网| 成人天堂噜噜噜| 日本精品一区二区三区在线播放视频| 久久天天躁狠狠躁夜夜av| 尤物yw午夜国产精品视频| 国产成人亚洲精品| 亚洲视频视频在线| 日本精品视频在线观看| 亚洲电影在线观看| 日韩暖暖在线视频| 欧美日韩激情网| 欧美老肥婆性猛交视频| 国产精品久久网| 国产精品欧美在线| 日韩视频精品在线| 97**国产露脸精品国产| 欧美在线观看日本一区| 欧美电影免费观看电视剧大全| 欧美在线视频导航| 亚洲一区二区三区乱码aⅴ蜜桃女| 日韩高清电影好看的电视剧电影| 亚洲精品欧美极品| 色噜噜狠狠狠综合曰曰曰88av| 国产原创欧美精品| 2019中文字幕在线免费观看| 国产精品永久免费视频| 中文字幕免费精品一区| 国产精品女人网站| 青青a在线精品免费观看| 精品日本高清在线播放| 精品呦交小u女在线| 国产主播喷水一区二区| 国产精品一久久香蕉国产线看观看| 欧美在线视频网站| 欧美久久久精品| 亚洲男人7777| 国产亚洲精品久久久久久牛牛| 欧美亚洲成人免费| 国产亚洲精品久久久久久牛牛| 日韩三级成人av网| 国产精品三级网站| 色阁综合伊人av| 最新69国产成人精品视频免费| 日韩精品中文字幕有码专区| 国产精品偷伦一区二区| 久久手机精品视频| 97在线免费视频| 国产成人精品在线视频| 日韩在线中文字| 国产成人激情视频| 欧美精品电影免费在线观看| 国产精品大片wwwwww| 成人午夜黄色影院| 亚洲成人在线视频播放| 日韩激情视频在线播放| 亚洲的天堂在线中文字幕| 国产精品久久久久久av下载红粉| 国产亚洲成精品久久| 91精品视频免费| 国产精自产拍久久久久久| 成人福利免费观看| 亚洲理论片在线观看| 欧美日韩亚洲视频| 国产精品美女www| 欧美亚洲另类激情另类|