Vue Server Side Render 的爱与恨

2,560 阅读4分钟

昨天整了一天的 SSR,卒,总结一些经验教训仅供参考。

为什么又双叒叕要用 Server Side Render

这就要说到天下合久必分,分久必合的道理了——最初的时候,静态页面就是静态页面,前后端 MVC,服务端渲染出页面;之后,前后端分离,后端提供 API,由客户端渲染页面;最后,我们又回到了最初的起点,不过是前后端分离后再由后端多渲染一次。

这样做的优点当然有很多啦,比如说我们要照顾爬虫(不),照顾蜘蛛,Vue 官方文档写的几点都已经很清楚了:

  • SEO
  • 客户端网络慢 SPA 亚历山大
  • 客户端版本太低

如果只是某些页面需要使用预渲染去照顾搜索引擎,可以考虑 使用预渲染(prerendering):prerender-spa-plugin,这个库需要指定待渲染的页面,即使没有使用 vue-router

那么面对的自然有两个思路:

  • phantomjs 渲染首屏(现在是 chrome headless 了)
  • 编译渲染内容

官方当然不会用第一种拉。

开始使用 SSR

官方最近提供了一个很完善的文档,比起之前来说已经好配很多了。

首先安装 vue-server-renderer

  1. npm install vue-server-renderer --save-dev

最简单的方法可以让我们最快的理解 SSR 中的原理:

  1. // Step 1: Create a Vue instance
  2. const Vue = require('vue')
  3. const app = new Vue({
  4. template: `<div>Hello World</div>`
  5. })
  6. // Step 2: Create a renderer
  7. const renderer = require('vue-server-renderer').createRenderer()
  8. // Step 3: Render the Vue instance to HTML
  9. renderer.renderToString(app, (err, html) => {
  10. if (err) throw err
  11. console.log(html)
  12. // =>
  13. <div data-server-rendered="true">hello world</div>
  14. })

这其实也就是一个核心的 render 函数,通过 createRenderercreateBundleRenderer 中的 template 参数,我们可以构建一个完整的渲染后的 HTML。

最终我们完成的 render 函数类似于,通过构建工具比如 gulp 在编译完成后调用即可:

  1. const { createBundleRenderer } = require('vue-server-renderer');
  2. const bundle = require('./dist/vue-ssr-server-bundle.json');
  3. const renderer = createBundleRenderer(bundle, {
  4. runInNewContext: false, // 2.3.x 中才有
  5. template: require('fs').readFileSync('./template.html', 'utf-8'),
  6. clientManifest: require('./dist/vue-ssr-client-manifest.json')
  7. });
  8. renderer.renderToString({ url: '/' }, (error, html) => {
  9. if (error) throw error.stack;
  10. require('fs').writeFileSync('./dist/index.html', html);
  11. });

一部分没有解释过的字段我们会在之后解释。

配置 Webpack

在 Vue 2.3.x 中的 vue-server-render 已经集成了 server-plugin 和 client-plugin,在旧版中需要安装单独的包引入:vue-ssr-webpack-plugin

在配置 webpack 中,官方建议将 server 和 client 分开配置(反正我也玩不溜 webpack,照着做^*&@)。

在 Server 中,需要注意避免使用 CommonsChunkPlugin,必须保证 bundle 是单一入口的。

剩下的照着官方文档配置,加入或修改没有被省略号的部分:

  1. module.exports = {
  2. target: 'node',
  3. entry: '...',
  4. output: {
  5. path: '...',
  6. filename: '...',
  7. libraryTarget: 'commonjs2'
  8. },
  9. // ...
  10. plugins: [
  11. new VueSSRServerPlugin()
  12. ]
  13. }

在客户端编译中,只需要引入 VueSSRClientPlugin 作插件并编译即可。

之后运行 webpack 编译就会有 render.js 中需要的两个 json 文件——client-manifest 负责渲染资源文件,server 渲染出首屏结构。

入口隔离

在最新的 SSR 文档中,官方在编译中分离了 client 和 server 的入口文件,代码可以见官方文档的结构中的代码:ssr.vuejs.org/en/structur…

template 处理

在 render.js 中我们提供了一个 template,与普通的 template 唯一不同的地方就是我们规定了 ssr 输出的位置:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <title>ssr test</title>
  5. <meta charset="utf-8">
  6. <meta name="mobile-web-app-capable" content="yes">
  7. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
  8. <meta name="theme-color" content="#f60">
  9. </head>
  10. <body>
  11. <!--vue-ssr-outlet-->
  12. </body>
  13. </html>

SSR 所需的处理

由于我们 SSR 本质上是使用了 Node 环境,因此对于一些 Browser 提供的变量并不能用,在此之前,基本上都是靠在库中就开始判断是否是在客户端中运行——

然而,库我们当然不可能完全掌控啦,除非自己一个个 clone 下来改。

在旧版中并没有 runInNewContext,目测了一下源代码,似乎是在 sandbox 中运行的,render 期间具有独立上下文,也不太好改,在新版中可以通过设置 runInNewContext: false ,这样就可以利用 node 中的 global 设置,通过一些 mock 库解决 undefined 的问题,不过可能 mock 中存在问题会让 render 后的页面不可用,比如下面我们 mock 了一波 window:

  1. const WindowMock = require('window-mock').default;
  2. let window = new WindowMock();
  3. global.window = window;
  4. global.localStorage = window.localStorage;
  5. global.document = window.document;

最后吐槽的遗言

完整的项目代码:github.com/csvwolf/vue…

坑略大……