1

前面有篇文章已经写了一遍, 就是在 Respo 站点上有的优化方案,
https://segmentfault.com/a/11...
这种方案当中, 页面的静态内容是提前编译好的, 但同时前端也做渲染,
大致的流程是

  • 编译阶段, 调用 SSR API 对页面进行一次渲染

  • 页面加载到前端, HTML 先于 js 在页面上展示出来

  • js 启动后, 生成和 HTML 几乎一样的 DOM, 做一次初始化

  • 之后按照正常的更新步骤做界面的更新

这个做法其实近似于单页面技术流行之前, jQuery 加模板引擎的做法,
服务端首先会用模板引擎渲染好带数据的页面, 前端再动态更新,
但单纯用模板引擎做渲染存在的一些局限性:

  • 对后端依赖严重, 必需有一个独立的渲染服务器

  • 调试不方便, 比如没有热替换技术, 而且与模板引擎相关

  • 开发方式, 单页面技术有完整的前端路由等等方案, 用模板引擎增加复杂度

所以更好的办法是既是前端渲染单页面, 又能原样拿到后端运行,
也就是最近出现频率更高的服务端渲染, 简称 SSR,
然而 SSR 实现起来需要处理一些配套的技术细节, 否则麻烦挺多:

  • 数据请求用户验证相关的问题, 需要往两边转发 Cookies

  • 首屏可能需要不止一个甚至不同时发出的请求, 需要额外的抽象

  • 服务端的性能不太好控制, 除非能很好利用缓存, 或者对性能要求不高

因此在 SSR 周边的方案成熟之前, 我认为提供过渡的方案比较可行,
也就是, 在编译阶段先渲染静态内容, 而动态内容仍然采用纯客户端渲染.
这也是 Addy Osmani 所以的 App Shell 渲染的问题,
这个方案能带来一些基础的好处:

  • 用户能更快地看 App Shell, 页面也能缓存, 因而用户以为页面更快了

  • 编译阶段渲染, 因而不存在服务器开销, 暂时也不会涉及数据请求的麻烦

  • 前端开发的方案更加一致, 也就是目前熟悉的单页面应用的手法

同时相比前面最为理想化的 SSR 的效果存在一些差距:

  • 毕竟首屏是没有数据的, 用户看到的是框框, 甚至只是色块

  • 首屏也对应加载动画的页面, 这相比之前需要更额外的一些代码来完成

  • 对于 SEO 来说, 还是差一些, 只有头部和导航, 没有具体数据

如果后面需要再填充数据以应对 SEO, 迁移方案仍然不乐观,
从单纯前端渲染, 到完全 SSR, 这之间有不少的台阶需要迈过,
而且即便现在基于已有的方案来讲, 还是有一些不成熟的地方:

  • 前端路由和编译阶段路由要保持一致, 甚至对于 Nginx 也要做到一致.
    路由能不能完全做到支持, 其实是不确定的

  • 代码拆分, 整个站的路由分割, 实际场景当中会更复杂

当然, 我还是觉得基于目前大量纯前端渲染的单页面, 这是一个加分项,
只要框架层面对 SSR API 做了实现, 用不大的成本就能完成这个优化,
然后对首屏加载的效果做一点点提升.

React 和 Respo 我前面已经做了演示, 感兴趣往前面翻一翻,
这边加上前面几天用 Vue 2 仿制的一个 Demo, 几乎同样的功能:
Demo http://vue-coffee-workflow.co...
Repo https://github.com/coffee-js/...
大致有一些要点, 类似的项目可以参考:

  • 代码在到 Node 运行, 可以用 Webpack 预处理, 或者想办法直接载入

  • 用户可能从多个路径访问, 所以需要编译出多个 HTML 文件作为入口

  • 路由建议使用 .html 作为后缀, 方便 Nginx 直接命中文件

  • 组件当中不建议直接引入资源文件, 不方便处理, 建议在 CSS 当中进行打包

最主要的因素还是框架本身对 SSR 的支持尽量做到简单,
现在 React, Respo, Vue 2 的 Virtual DOM 都较好地支持 SSR 渲染了,
至于 Angular, 前面收集到的资料不够, 我等等看别人的进展.

我用 Vue 2 试验的例子, 直接放在了 GitHub 上, 打包方式有注明.
中间为了加载代码的方便, 我直接用 js 来写 vue render 方法,
实际开发当中几乎没人这么做, 但是对于 Node 环境这样更友好,
不过大概还是 Vue 早起那种 template 的写法更加实在一点...
核心的渲染代码是 gulpfile 当中的这一段, 编译路由, 生成 HTML 文件:
https://github.com/coffee-js/...

# this is the initial address
entries = [
  'index.html'
  'page/a.html'
  'page/b.html'
]
entries.forEach (address) ->
  app = new Vue
    router: router
    store: store
    components:
      container: Container
    render: (h) ->
      h 'container'
  router.push address
  renderer.renderToString app, (err, appHtml) ->
    if err?
      throw err
    else
      html = template.render appHtml, store.state, settings
      htmlPath = path.join 'build', address
      console.log 'render entry:', htmlPath
      mkpath.sync path.dirname(htmlPath)
      fs.writeFileSync htmlPath, html

细节不展开了, 毕竟是试验的代码, 实际项目并不是我这样写的.
如果有 SSR 相关的想法, 可以一起交流下, 我这边方案还比较粗浅.


题叶
17.3k 声望2.6k 粉丝

Calcit 语言作者