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
:
1npm install vue-server-renderer --save-dev
2
最简单的方法可以让我们最快的理解 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 函数,通过 createRenderer
和 createBundleRenderer
中的 template 参数,我们可以构建一个完整的渲染后的 HTML。
最终我们完成的 render
函数类似于,通过构建工具比如 gulp
在编译完成后调用即可:
1const { createBundleRenderer } = require('vue-server-renderer');
2const bundle = require('./dist/vue-ssr-server-bundle.json');
3
4const renderer = createBundleRenderer(bundle, {
5 runInNewContext: false, // 2.3.x 中才有
6 template: require('fs').readFileSync('./template.html', 'utf-8'),
7 clientManifest: require('./dist/vue-ssr-client-manifest.json')
8});
9
10renderer.renderToString({ url: '/' }, (error, html) => {
11 if (error) throw error.stack;
12 require('fs').writeFileSync('./dist/index.html', html);
13});
14
一部分没有解释过的字段我们会在之后解释。
配置 Webpack
在 Vue 2.3.x 中的 vue-server-render
已经集成了 server-plugin 和 client-plugin,在旧版中需要安装单独的包引入:vue-ssr-webpack-plugin
在配置 webpack 中,官方建议将 server 和 client 分开配置(反正我也玩不溜 webpack,照着做^*&@)。
在 Server 中,需要注意避免使用 CommonsChunkPlugin,必须保证 bundle 是单一入口的。
剩下的照着官方文档配置,加入或修改没有被省略号的部分:
1module.exports = {
2 target: 'node',
3 entry: '...',
4 output: {
5 path: '...',
6 filename: '...',
7 libraryTarget: 'commonjs2'
8 },
9 // ...
10 plugins: [
11 new VueSSRServerPlugin()
12 ]
13}
14
在客户端编译中,只需要引入 VueSSRClientPlugin
作插件并编译即可。
之后运行 webpack 编译就会有 render.js 中需要的两个 json 文件——client-manifest
负责渲染资源文件,server 渲染出首屏结构。
入口隔离
在最新的 SSR 文档中,官方在编译中分离了 client 和 server 的入口文件,代码可以见官方文档的结构中的代码:https://ssr.vuejs.org/en/structure.html
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>
14
SSR 所需的处理
由于我们 SSR 本质上是使用了 Node 环境,因此对于一些 Browser 提供的变量并不能用,在此之前,基本上都是靠在库中就开始判断是否是在客户端中运行——
然而,库我们当然不可能完全掌控啦,除非自己一个个 clone 下来改。
在旧版中并没有 runInNewContext
,目测了一下源代码,似乎是在 sandbox 中运行的,render 期间具有独立上下文,也不太好改,在新版中可以通过设置 runInNewContext: false
,这样就可以利用 node 中的 global 设置,通过一些 mock 库解决 undefined 的问题,不过可能 mock 中存在问题会让 render 后的页面不可用,比如下面我们 mock 了一波 window:
1const WindowMock = require('window-mock').default;
2let window = new WindowMock();
3global.window = window;
4global.localStorage = window.localStorage;
5global.document = window.document;
6
最后吐槽的遗言
完整的项目代码:https://github.com/csvwolf/vue-ssr-demo
坑略大……
评论 (0)