我的网站百家饭OpenAPI平台是vuepress写的,前段时间我还写了个专栏讲了vuepress2.0教学。
最开始我们的网站是类似公司网站的情况,以介绍为主,后来又加了一个openapi编辑器,编辑器主要在一个页面里面,vuepress还勉强可以用,虽然其中出现了一些小的问题,但是还是被我们坚持下来了,但是最近遇到的一个问题,让我们不得不在部分页面改用别的框架来搭建一个完全独立的部分。
这就是vue ssr的hydration问题。
(以下vue都包含vue,vuepress,nuxt等vue体系下的框架)
什么是ssr
ssr是server side render的简称,就是服务器端渲染,我们知道最开始html就是服务器传输给浏览器的,只是近年因为js渲染的崛起,像vue这些技术都是通过vdom在客户端进行页面架构,服务器端实际传输的以js代码为主。
但是这种方式第一是需要更长的时间来渲染页面(先传代码,再渲染,耗时长),第二是对搜索引擎不友好(搜索引擎只能看到代码),所以像vuepress这种以内容为主攻的框架肯定是需要部分核心内容要能够直接在服务器上进行渲染,保证内容搜索引擎也能读取。这就用到了ssr。
ssr通过在服务器端进行部分组件的编译,还原成html,使得搜索引擎拿到的就是html的具体内容。这就是ssr的功能。
ssr看起来是开历史的倒车,但我觉得还是有很多有益的地方,因为js渲染之后,其实出来了很多以此为基础的vue组件,例如鼎鼎大名的elementui,你不用vue就没法用到这些组件,如果要走原始的html编程的老路,这才是最麻烦的事情,对效率的打压在50%以上。
vue ssr的难点:如何部分ssr,又能在客户端保留vdom的优势
上面我们讲了ssr,其实完全ssr做出静态页面是一点问题没有的,但是从现代网页的设计来看,大量客户端动态操作,又不得不依赖vdom等技术来实现(仅讨论vue的情况)
揭秘 Vue 中的 Virtual Dom - 掘金 (juejin.cn)https://juejin.cn/post/6844903874688450568于是,vue的做法是在ssr生成的html的基础上,再同时生成一个js文件,js文件里同时保存在客户端能再现渲染的js版本,js版本里是通过vdom进行渲染的操作函数,而html是ssr后的结果。
当浏览器拿到这两个东西之后,浏览器就开始重新执行一遍vdom的生成过程,并在此基础上去执行客户端动态操作。
那一旦出现不匹配的情况,那dom渲染就会出问题,部分界面组件会重复渲染,控制台会报错
hydration completed but contains mismatches.
一般的ssr需要由nodejs做后端服务器,这里有一个较官方的教程
Server-Side Rendering (SSR) | Vue.js (vuejs.org)https://vuejs.org/guide/scaling-up/ssr.html在这种情况下,ssr mismatch不经常出现,但是有可能由于例如内含精度太细的时间戳等信息,导致服务端和客户端两次生成的时间不匹配等情况造成。
vuepress的ssr
当ssr具体在vuepress里体现的时候,情况又发生了一些变化,由于vuepress编译md文件生成vue,再由vue生成静态文件的做法,vuepress的ssr实际是在编译过程中发生的一个前置步骤,已经失去了动态render的功能,退化成了ssg(server side generation),也就是说html是一个预先编译好的静态内容。
ssr和托管服务器的不兼容问题
ssr要在客户端重新match这个特性,除了官方提到的问题之外,我们在和golang http server配合的过程中还发现了其他一些问题,这些问题是因为和golang template一起使用造成的:
1)html不能再优化,这个问题对我们也造成了一些困扰,他的来源是vue这个hydration除了依靠html结构之外,对html的注释也有格式要求,可能要靠注释去匹配动态段
类似这里面绿色的注释部分,而golang在通过template处理之后,这些内容即便只是静态内容,也会被默认优化去掉。造成了最开始一段时间我们死活找不到问题所在。
这个问题可能和其他服务器也有关系,我猜想一些cdn缓存服务器的默认优化方案也会造成类似的问题。
2)无法再通过注入实现更多动态内容的展示
随着百家饭平台加入API评论等功能,我们需要的动态内容静态化展示功能越来越多,比如优质评论等内容,实际是类似blog的动态输入内容,但是呈现上要以静态页面的形态。
我们原本设计,在vuepress输出的页面中,将内容部分替换成golang template的替换符,然后把生成的文件作为golang http server的template文件,再对其中的替换内容进行动态替换,达到动态展示页面的目的。
这个替换过程中,由于实际的模板内容只在html被动态替换,而对应的js文件中的没有被正确替换,造成了客户端因mismatch导致渲染错误。
(下图可见,如果在vue中定义了golang template,则对应会在html和js中都存在一份)
ssr上述不兼容问题的解决尝试
知晓了问题之后,我们大概做了以下尝试,但均告失败:
1)使用dehydration插件vuepress/vuepress-plugin-dehydrate: Dehydrate HTML files in VuePress. (github.com)https://github.com/vuepress/vuepress-plugin-dehydrate
问题:
- 这是个vuepress插件,不是vuepress2.0插件
- 他会做页面级的处理,比如一个页面标记成noScript之后,所有该页面的js部分就不再生成了,这样页面的动态js功能也同时失去了,完全变成了静态页面
2)尝试把js里面的模板部分也进行解析
上面的mismatch问题是因为html里的模板被解析了,但是js没有解析,那是不是可以通过把js部分也解析掉,使得js也保持一致,这样就没有问题了呢
问题:
- 需要模板化解析的页面通常是动态页面,要使得js也动态解析,实际上,要把html里对js的script引用变成动态化,这个过程要求对vuepress的html打包部分进行插件化处理。修改难度较大
- js文件实际是一次二次请求,会造成服务器动态内容解析的性能损耗翻倍。
- 也可以将script引用,直接改成js内容内嵌到html中,但js部分实际又用于vuepress的本地化跳转等流程,需要在其他页码中也引用,所以内嵌也不是一个好方案。
3)使用一款叫vue-lazy-hydration的插件
maoberlehner/vue-lazy-hydration: Lazy Hydration of Server-Side Rendered Vue.js Components (github.com)https://github.com/maoberlehner/vue-lazy-hydration这款插件按介绍可以标注部分组件延后hydrate,里面有never选项,表示不进行hydrate,但是遗憾的是这款插件和vuepress不兼容。
4)换用nuxt作为整体框架
问题:
- 太麻烦
- nuxt作为vue体系,似乎也采用了hydration机制,如果不用nodejs应该是同样的问题
至此,我们觉得解决这个问题似乎已经跨越了我们可以解决的难度了,于是有了以下一些结论,供遇到类似问题的同学参考:
- vuepress还是只适合静态内容展示,不要做动态blog
- 我对vue ssr的功能首先产生了怀疑,这样是不是只适用于用nodejs的小型项目?
- 我对vue ssr的性能也产生了怀疑,同样的内容要渲染两次
最后,这个问题通过使用一款叫lit的轻量级框架解决掉了,关于lit的使用介绍,我们会随着开发的进展逐步也介绍给大家。
PS:
写的过程中,突然觉得可以有这么个方案:
在app.vue中预留一个display:none的元素,里面放置需要用ssr来渲染给搜索引擎的内容:
<body>
<div id="ssr" style="display:none">{
{ .Content }}</div>
<div id="app"></div>
</body>
因为vue挂载在app节点里,其他的都算静态内容,这样我们可以使用template去渲染一份给搜索引擎的,而用户侧内容还是由vue在客户端渲染,这也许是个方案。