本文只是对 Vue.js 官方 SSR 文档和对官方 hackernews demo 的个人学习总结,说得不够完整的请见谅
本文主要对以下几方面内容对 Vue.js SSR 的内容进行分析总结
- SSR 出现的原因
- Vue.js SSR 的总体原理
- SSR 当中的数据预取
- SSR 在编写代码时候的限制
- SSR 的 webpack 构建原理
SSR 出现的原因
- 单页应用有一个很大的缺点就是 SEO 问题,搜索引擎目前只能对同步的 javascript 进行索引,但对于需要异步获取数据的单页应用来说,搜索引擎并不会抓取到它们的内容
- 更快的首屏内容展示速度,单页应用需要等待 JS 文件加载完成,然后再进行页面渲染,而 SSR 是将渲染完毕的 html 传输给客户端
Vue.js SSR 的总体原理
如果用一句话概括 Vue.js SSR 的运作过程,那就是 在服务端将 Vue.js 实例转换成 html 字符串传输到客户端,然后进行客户端激活,使网页内容能在 Vue 实例的控制之下
这一句话包含两步内容
- Vue.js 实例转换成 html 字符串
- 客户端激活
先来看第一步
Vue.js 应用转换成 html 字符串
一个最简单的 Vue.js 单页应用是这样的:
new Vue({render: h => h('div', '123')
}).$mount('#app')
这里也包含两步
- 新建 Vue 实例
- 挂在到 DOM 上面
在服务端当中我们不进行上面第二步操作,取而代之的是将这个实例直接渲染成字符串,做这个工作的就是我们的vue-server-renderer
const renderer = require('vue-server-renderer').createRenderer()
const Vue = require('vue')
renderer.renderToString(new Vue({render: h => h('div', 123)
})).then(html => {console.log(html)
}).catch(err => {console.error(err)
})
// 输出 <div data-server-rendered="true">123</div>
到现在一个最简单的 vue ssr 应用在服务端的工作已经完成了,下面我们转向下一步 客户端激活
客户端激活
客户端激活跟我们单页应用所做的工作相比,最大的不同点就是它并不会构建 DOM 元素,只会对现有的 DOM 元素进行 激活 ,使它们能被 Vue 实例进行控制,而判断激活的关键就是上面的data-server-rendered
属性
至此,最简单的一个 SSR 应用已经构建完成了,下面是对这个应用的功能进行进一步的补充
SSR 当中的数据预取
数据预取包含着两个方面,客户端的数据预取和服务端的数据预取
服务端的数据预取
我们渲染一个内容完整页面的时候往往需要向服务器请求数据,所以现在服务端的逻辑变成 等待数据获取完毕,然后将页面转换成 html 字符串
其中数据获取有以下几个问题:
- 获取哪些数据?
- 如何得到获取数据的方法?
- 应在何时预取数据?
- 预取的数据应保存在哪里?
- 预取的数据应该怎么样跟客户端进行同步?
问题 1:
我们的数据用来渲染页面,那么我们就需要组成当前页面的所有组件各自所需要的数据
问题 2:
每个需要进行服务端数据预取的组件定义一个 asyncData
方法,此方法用于数据预取
问题 3:
我们需要先得到当前页面所有需要渲染的组件,然后再进行数据预取
问题 4:
由于还需要进行数据同步,所以很难将数据保存在组件的私有 data 上面,放在 vuex 上面是个普遍的选择
问题 5:
服务端在返回 html 字符串的时候,store 数据将被序列化以后以 window.__INITIAL_STATE__=/* store state */
的形式插入到脚本当中被客户端获取,客户端的 store 使用 store.replaceState
方法同步 state
简单复述一下上面的流程就是:
在渲染当前页面的所有组件加载完毕以后,执行这些组件的 asyncData
方法,这些方法将获取到的数据将由 vuex
托管,获取数据完毕以后即可将应用渲染成 html 字符串,vuex store 的 state 将会被序列化以后一并传输到客户端,被客户端进行同步
下面是实现的一些细节:
- 判断组件加载完毕的方法是 vue-router 的 onReady 方法
- 获取当前页面的所有组件为 vue-router 的 getMatchedComponents 方法
由于源码太长所以没贴出来,具体可以到官网浏览
服务端数据预取的关键点算是总结的差不多了,下面简单说一下客户端的数据预取
客户端数据预取
客户端的数据预取方法可分为两种:
- 等待数据获取完毕后再进行视图切换
- 先进行视图切换然后在进行数据获取
两种方法区别在于让用户在什么时候产生等待的感觉,第一种是在页面切换时,而第二种是在页面切换完毕等待内容的出现时
第一种方法的实现使用了 vue-router 实例的 beforeResolve
方法,这个方法执行在异步组件加载完毕后,导航被确认之前,当完成数据预取以后 router 才会进行 DOM 更新等步骤
第二种方法的实现跟我们一般进行数据获取一致,在 beforeMount 钩子当中执行
SSR 在编写代码时候的限制
- 由于浏览器特定的 API 将会在服务端报错,如 ’document’、’window’ 等,尽量避免使用此类 API 或者在非服务端运行的声明周期函数中调用如 ’mounted’ 等等
- 指令由于能直接操作 DOM 会受到很大的限制
SSR 的 webpack 构建原理
以官方的 hackernews demo 为例,webpack 有两个入口 entry-client
和entry-server
分别负责构建客户端和服务端的文件
服务端方面 webpack 会输出一个名叫 vue-ssr-server-bundle
的 json 文件,此文件由官方提供的 VueSSRServerPlugin
插件所构建而成,是服务端的构建清单,传入 createBundleRenderer
生成服务端渲染所需要的renderer
客户端方面 webpack 输出的是由代码分割而成的 chunk 和公用 bundle,与一般单页应用的构建相似,不同的是会生成一个vue-ssr-client-manifest
,此文件是客户端方面的构建清单,包含所有 chunk 的信息,将其传入上面的 renderer 当中能自动将 chunk 嵌入到 html 当中,当然用户也能够取消,自行选择手动嵌入的内容