Think Twice Before SSR

1 年前

SPA 进化到如今,已经从「拼能力」逐渐过渡到「拼体验」,大家也把目光转向了 SPA 相对多页后端渲染应用的两个最大缺陷:

  • 首屏时间
  • SEO(Search Engine Optimization)

在这样的背景下,主流框架都开始推出 SSR (server side rendering, 服务端渲染)方案,这里主要谈一下 科赛网 在迁移到 vue 2.x 的同时,对其 SSR 方案的评估及结论。

vue 的 SSR 思路比较清晰,使用服务端、客户端两个入口分别打包出适合在两端运行的应用,服务端会在 SSR 阶段预先注入页面所需的数据及输出对应的 DOM 字符串,客户端应用的起始状态就是服务端渲染的结果,完成状态混合之后的部分和普通的客户端渲染一致。

看起来没什么问题,实际操作了一下,好处不谈了,说一下潜在的坑:

API 稳定性 & vue 版本要求

SSR 相关 API 还未完全稳定,如果要入坑,建议升级到最新的 vue, vue-router, vuex 的发布版本。

服务器端要求

服务端必须是 Node.js,或者专门跑一个 Node.js 来支持 SSR。

库依赖

Polyfill 之类的对应用逻辑无影响的库可以放在客户端入口文件中,为客户端独有。其他的依赖库则要和应用打包在一起,也会在 SSR 阶段跑在服务端的 Node 环境里。但是服务端的 JS 环境和浏览器的环境不一致,有些库会在初始化时访问 window 对象或者使用 DOM API,造成异常。

这里点名批评一下 iView (A high quality UI Toolkit based on Vue.js),在 SSR 时会直接挂掉,和其定位与愿景并不相符。

注:当时测试的 iView 版本是 v2.0.0-rc.17,最新的 v2.0.0-rc.19 已经完善了对于 SSR 的支持, 见 iview/iview

应用状态管理

SSR 的标准实践是使用 vuex 来管理应用状态,也只有 vuex 的状态可以在服务端渲染后直接传递给客户端。在一个比较复杂的应用中,核心数据使用 vuex 管理没有任何问题,但同时有大量的状态不适于放在 vuex 体系内,在 SSR 场景下要做额外考量。

对开发的额外约束

在 SSR 环境下,某些组件和路由的钩子不会被调用,而在 SSR 阶段调用的钩子(如 created)中使用 DOM API 会导致异常。

在官方 demo 里,每个组件都要实现一个类似的 getAsyncData 方法,来配合服务端进行数据预取。这无可厚非,但是对于习惯使用路由及组件钩子完成数据获取的开发者,会带来额外的约束。

数据请求&用户登录态

应用中 API 请求的逻辑也需要既运行在服务端,也运行在客户端。所以 vue-resource 不再可用,需要 axios。

在 SSR 阶段,如果数据依赖用户的登录状态,需要手动将用户 Cookie 传递到 SSR 的渲染器,才能在数据请求时从 context 中获取用户 Cookie,走普通的 API 身份验证逻辑。

例如

// 这是服务端 router 代码
app.get('/my-ssr-app', (req, res) => {
    const context = {
        url: req.url,
        cookies: req.cookies
    }
    const renderStream = renderer.renderToStream(context)
    // 其他略
})

PS: 之前我测试的时候,如果 router 有 base 的话, 在 SSR 阶段不能正确去掉 base 再解析前端路由,需要手动处理,不知道现在是否修复

服务端性能

SSR 是一个 CPU 密集型的应用,如果有扛高并发的需求,请慎用。

应用部署

对于纯 SPA, 如果在构建时将打包后的 bundle 上传到 CDN,则仅需要部署一个 index.html 到服务器。以至于我之前专门写了一个工具,可以实现 SPA 入口的热部署和版本切换。 但是在 SSR 场景下,应用代码和服务器端有了耦合,所以典型的部署需要重启 Node.js 服务

注: 使用 createBundleRenderer,则可以实现服务端的热部署,详见文档

结论

花了一天时间研究以后,我否决了将科赛新版迁移到 SSR 的想法,目前 vue SSR 方案仅适用于重内容展示,并且规模有限的应用,科赛前端约 2w 行代码,并且新版增强了工具属性,强行 SSR 会带来更多问题。

另外,我本人非常认可 vue 团队在 SSR 上的努力,然而 vue 在浏览器里面写起来太爽,增加很多方便的特性,但是在 SSR 中某些特性会带来实现的困难,这是一枚硬币的两面。

另辟蹊径

那么首屏时间怎么办呢,SEO 呢,我是不是需要去知乎发帖「如何有效地糊弄产品经理,急,在线等」呢?

首先,对于以桌面端为主的前端应用,首屏时间不是瓶颈,只要以正确的姿势实现功能,首屏基本在 1s 左右。另外,CDN 和网站服务器升级到 HTTP/2 对加载速度的提升非常大。

其次,对于 SPA 的 SEO,有一个无痛的方案,叫做 prerender,代表实现就是 http://prerender.io

广告插入: 无痛 SEO, 今天 Prerender,明天就上线。

其原理是在你的网站服务器上判断请求来源,如果是来自搜索引擎的爬虫,则交给 prerender去处理。prerender 利用一个 无头(headless,无界面)的浏览器,模拟打开一个 SPA 应用,然后将 JS 渲染出的 DOM 抓下来,喂给搜索引擎,从而实现一种伪 SSR 的效果。

目前 prerender 的实现都是基于 PhantomJS ,但是 Chome 59 也加入了 headless,还有一众和它相关的特性,有心人可以去看一下。

既然说到这了,我最近自己动手撸了一个 spa-renderer,并且支持开箱即用地部署到阿里云函数计算或者 AWS Lambda。

为什么不用和 http://prerender.io 一样的常驻服务器的方案呢,因为搜索引擎抓取是一个频次较低的场景,完美契合函数计算或者 Lambda 的特性,用完就走,便宜,轻量,解耦。

之后会写文章详细介绍 spa-renderer,请关注专栏 DeepFE

鸣谢: 我家狗狗提供头图

0
推荐阅读