Vue Server Side Render 的爱与恨

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

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

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

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

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

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

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

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

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

开始使用 SSR

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

首先安装 vue-server-renderer

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

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

// Step 1: Create a Vue instance
const Vue = require('vue')
const app = new Vue({
  template: `<div>Hello World</div>`
})

// Step 2: Create a renderer
const renderer = require('vue-server-renderer').createRenderer()

// Step 3: Render the Vue instance to HTML
renderer.renderToString(app, (err, html) => {
  if (err) throw err
  console.log(html)
  // => <div data-server-rendered="true">hello world</div>
})

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

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

const { createBundleRenderer } = require('vue-server-renderer');
const bundle = require('./dist/vue-ssr-server-bundle.json');

const renderer = createBundleRenderer(bundle, {
  runInNewContext: false,  // 2.3.x 中才有
  template: require('fs').readFileSync('./template.html', 'utf-8'),
  clientManifest: require('./dist/vue-ssr-client-manifest.json')
});

renderer.renderToString({ url: '/' }, (error, html) => {
  if (error) throw error.stack;
  require('fs').writeFileSync('./dist/index.html', html);
});

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

配置 Webpack

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

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

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

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

module.exports = {
  target: 'node',
  entry: '...',
  output: {
    path: '...',
    filename: '...',
    libraryTarget: 'commonjs2'
  },
  // ...
  plugins: [
    new VueSSRServerPlugin()
  ]
}

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

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

入口隔离

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

template 处理

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

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>ssr test</title>
    <meta charset="utf-8">
    <meta name="mobile-web-app-capable" content="yes">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
    <meta name="theme-color" content="#f60">
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>

SSR 所需的处理

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

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

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

const WindowMock = require('window-mock').default;
let window = new WindowMock();
global.window = window;
global.localStorage = window.localStorage;
global.document = window.document;

最后吐槽的遗言

完整的项目代码:https://github.com/csvwolf/vue-ssr-demo

坑略大……

植入部分

如果您觉得文章不错,可以通过赞助支持我。

如果您不希望打赏,也可以通过关闭广告屏蔽插件的形式帮助网站运作。

标签: 成品, 源码, 知识, 代码段, Vue

添加新评论