2016-11-03 18:31:15

Vue2服务端渲染: 踩坑合集

本文内容原计划发在上一博文的结尾, 然而坑太多, 因此专门为Vue2的SSR新建了本文

花费了一周的零散时间, 本博客前台终于成功部署了服务端渲染(SSR), 首屏从之前的1000毫秒以上降低至最高400, 最低150.

我搭这个博客的目的之一, 就是赶上QUQU大神的100毫秒首屏时间. 这个目的算是完成了一半. 仍然可以做很多优化, 毕竟SPA应用的优化点和普通的后端MVC博客的优化点完全不同, 例如, 本博客目前尚未开启LocalStorage.

SSR好处其实也就两个, 减少首屏渲染时间和SEO了. 虽然Vue2的SSR效果还算不错, 但是这周踩了太多的坑, 开发体验真心不好, 难怪React的SSR一直都没合并到主分支上.

当然,这里再注明一下, SSR的成品确实很不错, 这里吐槽的只是vue2 SSR正式发布后第一个月的开发环境的体验而已

是时候扔掉vue-resource了

vue-resource并不是vue官方出的

vue-resource号称支持vue2, 然并卵, window,location,host等纯前端的变量随处可见,感情您是根本不知道vue2还有SSR这块?

除此之外, 作者好多天都没有露面, 支持SSR的PR已经被晾了三周

除了不支持SSR外, vue-resource的URI TEMPLATE是我觉得最垃圾的东西, 我从不轻易给一个开源项目如此低俗的评价, 然而这破玩意纯粹是恶心我们开发者, URI TEMPLATE有什么作用呢?如下:

?conditions={"type":0}
  | |
  | |
  \/
?conditions["type"]=0

转换出来的URI会被我见过的所有body-parser解析成空对象,导致通过URI来查询mongoDB这一方便的办法完全行不通:是的,vue-resource会将所有输入全部用URI TEMPLATE转一遍,一个跳过的方法都没有

替换vue-resource建议使用superagent, 8000多星星,比vue-resource好用多了,至少它不会莫名其妙把我的请求链接都给改了

单向数据流

我一直奇怪为毛vue2只保留单向数据流,现在终于理解了一部分含义:为了可以在任何地方跑起来的虚拟dom。 当然这里只讨论SSR,不考虑Weex了

SSR流程是这样的: 客户端请求服务端, 服务端根据请求链接获得匹配到的组件, 再通过调用所有组件的某个返回Promise的埋点(官方是preFetch方法)来将需要的数据拿到, 最后再通过<script>window.__initial_state=blabla</script>将其写入网页,最后将服务端渲染好的网页返回回去。

接下来还没完,现在是客户端还要再跑: 它先用vuex将写入的__initial_state__替换为当前的全局状态树,再用这个状态树去检查服务端渲染好的数据有没有问题。遇到没被服务端渲染的组件,再去发异步请求拿数据。说白了就是一个类似React的shouldComponentUpdate的Diff操作。

我个人认为客户端那边的Diff操作是有跳过的可能的,毕竟我们开发者可以肯定首屏到底是全部渲染好还是只是部分,完全可以通过服务端写一个Flag在网页里,客户端检查到这个Flag就跳过Diff。

好了, 现在回到单向数据流,为什么一定要用它呢? 因为用了它,就可以通过SSR返回唯一一个全局状态, 并确认某个组件是否已经SSR过了。

这样看来单向数据流还不错? 是的, 我只是vue1升vue2升得头晕目眩吐槽一下,如果vue1当初就明确单向数据流就好了。 不过话又说回来, 当初号称数据驱动并与浏览器dom强耦合的vue1竟然能够解耦出来,确实是相当厉害

不尽人意的热重载环境

当年写Node的原因之一就是热重载,比tomcat重启一次30秒来说开发效率好多了,SSR的热重载现在多了整整一个dev-server的角色,至少我配了十几个小时webpack还是有一处没配好 。

官方的vue-cli仍然没有提供升级的选项, 这也导致了升级非常非常麻烦。另外,官方SSR的demo vue-hackernews 2.0的webpack是用的2.1.0beta版的, 而vue-cli之前生成的项目都是webpack 1.13的,API变化极大,因此Build配置参考意义真的很小。

除此之外,是否能够方便地打包关闭SSR功能的前台单页?

这部分不再多说,毕竟webpack配好的配置一发出来,大家依样画葫芦很容易弄好,只能说目前我写SSR还在刀耕火种,改个地方重启一次dev-server。

2017-02-02更新

本文完成的七天后,Evan就更新了vue-hackernews-2.0,添加好了客户端和服务端的热重载。 (也就是说之前当面给Evan提问时我没提错咯?当时Evan和一个同事说可以热重载客户端)

入口组件不要忘记给顶层div加id

上面讲了SSR的渲染流程, 现在再将其具体化到页面中, 服务端渲染出来的HTML代码, 在顶层组件上会带有server-rendered="true"属性, 客户端的vue再根据这个属性进行hydrate打补丁操作via,客户端完成后,才会将server-rendered属性去除

如果拿到的HTML代码包含server-rendered属性,就说明客户端打补丁没有完成, 假如此时没有任何报错的话, 很可能是入口组件没有顶层id, 导致客户端入口文件中挂载dom语句失效

服务端谨慎配置静态资源代理

部署服务端渲染时,在nginx配置文件这里犯了一个错误:nginx代理的静态资源不存在时, 代理规则出错直接将其转发给了后台服务器(这里是express), 然后express的静态资源代理也没有配置fallthrough属性, 导致不存在的静态资源跳过了nginx和express两套代理规则进入到了服务端渲染的部分,因此静态资源实际返回来的竟然是首屏HTML页面。

这搞得我以为nginx代理规则完全出错, 写了一晚上nginx配置,完全按照官方文档location的描述来,搞得我都懵了,结果还是睡完觉脑袋清醒后,一下就找到了问题所在

这里我暂时是关掉express.static的fallthrough,应该像vue官方建议的那样,在nginx处配置一个全局404规则。当然,保证静态资源一定存在也可以临时解决掉这个问题。不过,这里我有点困惑,vue官方建议的全局404是之前没有SSR时建议的, 现在使用了SSR, 静态资源的404是该nginx给出还是服务端给出呢?

回到错误重现,这个问题是初始测试数据中包含线上的图片, 而我之后又将线上图片转移到了七牛云中。

SSR与按需分块加载策冲突

SSR目前不支持组件懒加载, 因为它需要将源码通过服务端入口文件打包,再提供给服务端在SSR时使用。

不过vue核心开发者之一目前正在解决这个问题,以vue的issue和pr响应速度来说,解决这个问题我相信没有问题。

2017-02-02更新

这一块已经被解决,不过是被纯粹的webpack2.0的ES6 Module语句System.import搞定的。稍后我会详细地写一篇文章

潜在的性能问题

SSR是否存在性能问题? 我觉得应该是有的,在本博客前台开发中,经常遇到服务端请求API的耗时占不到总耗时一半的情况,这说明SSR的开销比我想象地要大。

关于这一点,我有如下几个问题:

  • 目前是否有大型SSR项目上线?
    • 暂时没有发现。
  • vue2的SSR使用了node的vm模块,runInNextContext是否有内存有内存泄露的风险?
    • vm内存泄露非常隐蔽via
  • 性能是否有优化的余地?
    • 这一点是肯定的,目前已经有提升20%的PR被提交了via

router.getMatchedComponents的匹配问题

官方范例中, 服务端入口采用vue-router的router.getMatchedComponents()来获得当前匹配组件,然而,这个方法只能获得router-view匹配到的顶级组件。

它的含义是, 与顶级router-view平级的组件,以及router-view匹配组件的内部使用的组件,全部都无法被匹配到,因此SSR也不会渲染这些未被匹配的组件,即使你实现了preFetch接口。

上周我有幸抢到尤大左边的位置,也询问了这个问题。他的回答大体为:

我们可以只根据路径,就获得当前活动的组件。

确实是这样, 然而,上面提到的那两种组件,依此看法也应该被拿到才对。

因此,我认为现在的router.getMatchedComponents()对SSR的支持仍然有缺陷,有巨大的改进余地。

不过,幸好组件级缓存可以缓存到上面那两种组件

2017-01-22更新

router.getMatchedComponents的匹配问题

现在可以采用更好的办法,只需要在入口文件main.js导出公共组件的preFetch接口,组合后供给vue-server-renderer调用即可

全局404的思考

上了服务端渲染后,服务器重定向实际上是非常尴尬的。

vue的SSR采用stream流式输出,可以在stream未吐出任何代码前,把模板中的HTML(比如head和body)提前写入response,以大幅度降低TTFB,再将SSR吐出的代码依次pipe进response

另外,如果要支持服务器的重定向,那么肯定要组件中触发一个条件,例如某个文档不存在,然而这些条件全部都是写在前端逻辑中的,SSR的服务端完全没办法提前知道那个条件。

这会带来两个问题:

  • 无法使用stream,因为必须跑完组件代码,以确定是否需要服务端重定向
    • 这会直接导致10%的SSR性能损失,以及大幅度增加TTFB
  • 服务端重定向代码必须侵入前端源码中
    • 耦合度会高得不忍直视

因此,这条路肯定不行,那就只能将跳转放到客户端来做了。

还有一点: 有了SSR后,客户端访问的路由一定是我们早就指定好的,因为假如用户填了一个假路由,实际上触发的是服务端首屏渲染这个流程,而不是客户端进入假路由后SPA发送ajax请求 。

因此,实际上也没有必要专门给客户端的404页面弄一个指定的路由再跳转过去了。

最后,博主选择直接造假数据,来显示404页面

如上图,这个页面的标题和内容,实际上是博主设定好的:发现文章对象不存在时,给一个默认的文章对象,而这个对象内的键值就是404相关信息了。

也就是说,即使是服务端渲染,路由这方面还是得让前端给出404页面,这也符合单页应用的路由思想。

本文链接:https://smallpath.me/post/vue2-ssr-hardcore

-- EOF --