Vue 2 服務端渲染初探

題葉發表於2016-08-24

寫這篇文章, Vue 2 還在 Beta 呢...

參考資料

官方文件寫得很清楚

似乎 Vue 1 有看到過通過 jsdom 做後端渲染的例子, 效能不佳.
Vue 2 開始將 Virtual DOM 作為底層實現, 於是模組分離開始支援 SSR.

渲染步驟

4 步走戰略~

安裝 hackernews 的例子, 完整的 app 渲染的例子包括:

  1. 用 Webpack 的 node 模式把整個應用單獨打一個包

  2. Node 環境通過 API 將這個包載入到 vm 環境當中

  3. 應用在 vm 內部啟動 HTTP 請求抓取當前路由依賴的資料

  4. 生成網頁模板, 將 HTML 和初始資料嵌在中間

如果網頁依賴的資料少或者不依賴, 可以簡化一點,
比如中間抓取 HTTP 的步驟去掉, 可以簡化不少,
也許還可以去掉 vm 那步, 直接通過引用檔案來生成 HTML.

渲染 API

兩套 API 哦... 好像只用帶 bundle 那套...

https://github.com/vuejs/vue/...

  • createRenderer([rendererOptions])

  • renderer.renderToString(vm, cb)

  • renderer.renderToStream(vm)

  • createBundleRenderer(code, [rendererOptions])

  • bundleRenderer.renderToString([context], cb)

  • bundleRenderer.renderToStream([context])

後面三個 API 都帶上了 bundle, 此外看上去和前面的一樣,
bundle 是通過 Node.js 的 vm 模組執行的, 每次的都重新啟動一遍程式碼,
作者解釋這樣能清空整個 app 的狀態,
我推測這是因為用了 Vuex 之後, 資料會被快取在內部無法清理,
如果是單純通過 props 傳遞資料, 應該是可以用前一套 API.

服務端渲染原理

有了 Virtual DOM 就好辦了

VNode 定義 https://github.com/vuejs/vue/...

HTML 渲染的程式碼, 通過 write 同時支援到了 Stream 輸出:
https://github.com/vuejs/vue/...
https://github.com/vuejs/vue/...

如果用 bundle 模式, 注意每次都會執行 vm.runInNewContext 新建環境.
https://github.com/vuejs/vue/...
https://github.com/vuejs/vue/...

最後返回使用者的 HTML 其實是拼接出來的,
注意首屏的動態資料, 也通過 window.__INITIAL_STATE__ 傳送到瀏覽器,
https://github.com/vuejs/vue-...

快取

速度快是因為快取呢吧...

文件 https://github.com/vuejs/vue/...

大致就是如果元件可以根據一個 key 來確定, 就可以進行快取,
靜態的元件當然是有固定的 key, 動態的元件根據 id 等資料生成 key,

serverCacheKey: props => props.item.id + '::' + props.item.last_updated

如果元件可以找到快取, 就直接返回快取內容:
https://github.com/vuejs/vue/...

這也就意味著頂層的元件總之就是不能快取的, 效能開銷免不了.
hackernews 的例子本地用 ab 壓了一下, Mac Pro 到 130+qps 了,

Concurrency Level:      100
Time taken for tests:   3.013 seconds
Complete requests:      400
Failed requests:        0
Total transferred:      11545200 bytes
HTML transferred:       11506000 bytes
Requests per second:    132.77 [#/sec] (mean)
Time per request:       753.205 [ms] (mean)
Time per request:       7.532 [ms] (mean, across all concurrent requests)
Transfer rate:          3742.21 [Kbytes/sec] received

但是這個 Demo 是用了快取的, 破壞掉快取效能落差很大,
我自己做的 Demo, 實際上加上快取效能還不到這個一半...
看來跟應用的型別是有關的, 特別是節點偏多的應用影響更大.

資料策略

想象一下後端有個瀏覽器...

對於依賴資料, 目前的方案是在元件定義上提供 preFetch 函式,
服務端渲染時會主動查詢掛載的部分, 呼叫進行資料抓取:
https://github.com/vuejs/vue-...
https://github.com/vuejs/vue-...

官方的例子當中 App 是帶了 Vuex 跟 vue-router 的,
所以 preFetch 方案整個整合在這些庫當中.
從實驗看, 內部巢狀的 preFetch 是不會被呼叫的, 只能從路由開始,
同時中間要用到 Promise.all 合併請求, 腦補一下.

好吧我覺得這是一個相當簡單粗暴的獲取資料的辦法,
但其實也很難解耦, 不然就要從路由直接推算資料才行,
主要覺得還是不夠清晰, 限制挺多, 實際操作能犯錯的地方不少.

效能影響

反正比不上模板引擎

編譯後大致還能看到 Virtual DOM 的影子, 會有一些效能開銷,
不過話說回來 Virtual DOM 本來就很慢, 能優化一點已經不容易了...

module.exports={render:function(){with(this) {
  return _h('li', {
    staticClass: "news-item"
  }, [_h('span', {
    staticClass: "score"
  }, [_s(item.score)]), " ", _h('span', {
    staticClass: "title"
  }, [(item.url) ? [_h('a', {
    attrs: {
      "href": item.url,
      "target": "_blank"
    }
  }, [_s(item.title)]), " ", _h('span', {
    staticClass: "host"
  }, ["(" + _s(_f("host")(item.url)) + ")"])] : [_h('router-link', {
    attrs: {
      "to": '/item/' + item.id
    }

另外 vm.runInNewContext 有潛在的效能問題,
http://stackoverflow.com/q/98...
不清楚用在生產環境是怎樣, 我個人對此沒有多少經驗..

小結

越來越像 React...

Vue 2 算是把這麼多內容整合在一起相當不容易,
不過服務端渲染 React 那麼久了, 還是沒普及開, 效能是大問題,
相比較而言, Vue 2 增加了 cache 機制, 這可以提高效能,
但是依賴資料時會帶來啟動 vm 開銷, 要是程式碼量不小在麼辦?
具體效果還是要等正式釋出後, 等有權威的評測...

此外服務端抓取資料的策略需要挖一挖, 找找更漂亮的策略,
我個人希望能更好地解耦, 梳理出更加清晰的依賴,
那樣也可以適應更多的場景, 靈活地使用, 而不是限定死了這樣用.
當然也是因為服務端渲染, 這個本來存在的問題顯得更明確了.

相關文章