Vue 2 服务端渲染初探

写这篇文章, 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 开销, 要是代码量不小在么办?
具体效果还是要等正式发布后, 等有权威的评测...

此外服务端抓取数据的策略需要挖一挖, 找找更漂亮的策略,
我个人希望能更好地解耦, 梳理出更加清晰的依赖,
那样也可以适应更多的场景, 灵活地使用, 而不是限定死了这样用.
当然也是因为服务端渲染, 这个本来存在的问题显得更明确了.

阅读 35.8k

推荐阅读
题叶
用户专栏

ClojureScript 爱好者.

630 人关注
252 篇文章
专栏主页