首頁  >  文章  >  web前端  >  node.js實作BigPipe詳解_node.js

node.js實作BigPipe詳解_node.js

WBOY
WBOY原創
2016-05-16 16:29:031557瀏覽

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('
' temp.s1(s1data) '
')
    --n || res.end()
  })
  getData.d2(function (err, s2data) {
    res.write('
' temp.s2(s2data) '
')
    --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('
')
})

然後將傳回的資料用 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> 而導致的錯誤,只要將其替換為 即可

複製程式碼 程式碼如下:

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

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

複製程式碼 程式碼如下:

var resProto = require('express/lib/response')
resProto.pipe = 函數(選擇器、html、替換){
  this.write('<script>' '$("' 選擇器 '").' <br />     (替換 === 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 = 名稱
}
resProto.pipeName = 函數(名稱){
  回傳新的 PipeName(this, name)
}
resProto.pipeLayout = 函數(視圖,選項){
  var res = 這個
  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 = 函數(名稱、視圖、選項){
  var res = 這個
  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('佈局', {
      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把兩個部分加回來:

複製程式碼以下程式碼:

節#s1!=s1
部分#s2!=s2

這裡的想法是,需要管道的內容先用一個span標籤佔位,異步獲取資料並渲染完成對應的HTML程式碼然後輸出給瀏覽器,用jQuery的replaceWith方法把佔位的span元素替換掉。

正文的程式碼在https://github.com/undozen/bigpipe-on-node ,我把每一步都提交了,希望你克隆到本地實際運行並hack 一下看看看。因為後面涉及到加載順序了,確實要自己打開瀏覽器才能體驗到而無法從截圖上看到(其實應該可以用gif動畫實現,但是我懶得做了)。

關於BigPipe的實踐還有很多的優化空間,也就是說,要返回pipe的內容最好設定一個觸發的時間值,如果非同步調用的資料很快,就不需要用BigPipe,直接生成網頁送出即可,可以等到資料請求超過時間才用BigPipe。使用BigPipe相比ajax既節省了瀏覽器到node.js伺服器的請求數,也節省了一定的node.js伺服器到資料來源的請求數。不過具體的優化和實作方法,等雪球網用上BigPipe以後再分享吧。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn