介绍
# 深入理解Vue SSR服务端渲染的“爱恨情仇”
# 前言
其实总结这篇关于Vue SSR
整体流程的文章已经算是一年前的任务了,还记得当时在准备面试的时候,似乎“八股文”多有出现关于服务端渲染整体流程的问题,为了能在面试中不被疯狂嘲讽😭,笔者当时还是花了点时间好好研究了下如果实现一个Vue SSR
的过程。
当然这个过程中并不顺利,可能是笔者当时水平有限,在查阅官方文档时,对于官方文档中描述的种种概念不太能理解,于是乎我打开了百度,扑面而来的文章数不胜数,然而在我打开了多个博客然后又退出来之后,发现很多都是水文,几乎没有提供有价值的、整体性的分析。即便是有大神也进行了相关内容的撰写,但理解起来也不是那么容易,所以在笔者“边哭边学”终于弄懂一二之后,决心一定要自己好好总结一篇完整的文章,巩固自身对这方面知识的理解,并也希望能够给存在相似经历的读者带来一点启发和帮助。
# 概念
在进入本文进行详细分析之前,我们需要理解以下几个概念:
CSR
- 客户端渲染Prerender
- 预渲染SSR
- 服务端渲染
渲染:将数据和模版组装成
html
# CSR-客户端渲染
顾名思义,客户端渲染即是由浏览器来负责全部的渲染工作,采用ajax
进行异步数据的获取。对于我们传统的SPA
(单页应用)来说,我们如果不去进行一些额外的工作,那么它默认就是采用客户端渲染。
也就是说服务端仅提供接口和静态资源,对于客户端渲染的应用来说,在用户初次访问网页的时候,会经历以下过程:
刚开始渲染的页面内容是空的,它需要执行JS
文件来进行页面的元素的创建和插入,并进行重新渲染,如果说该JS
文件过大,在请求该文件的过程中,我们看的页面就是空白的,所以对于SPA
应用来说,我们经常需要面临的问题就是,如何减少首页白屏时间,这也就牵扯到我们各种前端性能优化相关的内容了。
正所谓有利必有弊,福祸相依,那么对于客户端渲染来说,它又有哪些优缺点呢。这里将有笔者为你娓娓道来。
# 优点
- 首次加载完之后,页面响应速度快。
- 前后端分离。
- 可以进行各种组件服用以及懒加载等能力。
- 结构清晰,无需与服务端各项逻辑进行耦合,开发体验友好。
- 前端技术栈可以更加丰富,无需被各种模板引擎所束缚。
# 缺点
- 不利于
SEO
。 - 首页性能差,容易白屏。
针对于客户端渲染的这些问题来说,我们可以预见性的发现它更适合公司内部的管理后台或者其他不需要考虑SEO
和首屏加载速度的场景下。
当然为了解决以上两大让人头疼的问题,我们就有了以下的方案:预渲染和服务端渲染。
# Prerender-预渲染
即利用打包工具对应用进行预先渲染,让用户在首次获取到HTML
文件的时候就已经能看到我们的内容,接着等待Bundle
下载解析完成之后再进行接管。
那么我们在打包构建预渲染的核心原理又是什么呢?其实这里就要用到我们十分强大的无头浏览器来帮助实现这项功能了,他会在本地启动一个无头浏览器,并访问我们配置好的路由,接着将渲染好的页面HTML
内容输出到我们的HTML
文件中,并建立相关的目录,也就是我们上述所看到的结构。
我们一般常用的无头例如有:phantomjs
,puppeteer
,对于prerender-spa-plugin
插件来说,他内部就是采用了phantomjs
作为无头浏览器进行预渲染。
# 优点
SEO
- 对于搜索引擎爬虫来说(先排除高级爬虫),它不会等待你的JS
执行完成之后才进行抓取,如果不进行预渲染,对于客户端渲染应用来说,HTML
文件中几乎没有什么内容,故会影响你的搜索排名。采用预渲染就能保证在首次加载就能获取到相关的HTML
内容,利于SEO
。- 弱网环境:对于网络条件较差的用户来说,你的
Bundle
文件过大,会导致页面长时间白屏,这将使你白白流失很多用户,所以首次内容的快速呈现也是十分重要的,解决首页白屏问题。 - 兼容浏览器差异:对于部分浏览器(点谁心里有数啊QAQ)来说,有些高级特性是不支持的,这个时候如果在执行
JS
的过程中异常将可能存在浏览器页面显示异常的情况,这个时候预渲染的能力也是能兼容这种情况的。
那么我们又该如何进行预渲染呢?
这里就直接以Webpack
为例,我们可以直接使用它的预渲染插件:prerender-spa-plugin
。
我们直接使用该插件的时候可以配置需要预渲染的路由:
默认情况下
HTML
会在脚本执行完被捕获并输出。你也可以指定一些钩子,HTML
将会在特定时机被捕获。
var path = require('path')
var PrerenderSpaPlugin = require('prerender-spa-plugin')
{
//...
plugins: [
new PrerenderSpaPlugin({
path.resolve(__dirname, './dist'),
['/home', '/foo'],
{
// 监听到自定事件时捕获
// document.dispatchEvent(new Event('custom-post-render-event'))
captureAfterDocumentEvent: 'custom-post-render-event',
// 查询到指定元素时捕获
captureAfterElementExists: '#content',
// 定时捕获
captureAfterTime: 5000
}
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这样配置完之后我们就能在我们的dist
目录中找到相关路由的预渲染HTML
文件啦。
dist
│ index.html
│
├─home
│ index.html
│
├─foo
│ index.html
2
3
4
5
6
7
8
9
从宏观角度上看,是不是也是十分便捷呢,这样我们一些需要进行预先渲染的页面就能具备预渲染能力了。
# 缺点
也正是因为预渲染的构建是由打包工具在打包的时候就渲染出来了,所以如果不重新构建,那么用户所看到的预渲染页面永远都是一成不变的,即便你的页面数据早已更新,但是初次渲染的时候,浏览器展示的依旧是这套老旧的数据,如果想要看到最新的数据就需要等待JS
下载完毕重新渲染的时候才能出现,从而是用户感觉很突兀的感觉。
由于需要借助打包工具的力量,所以我们需要增加一些配置成本,不仅如此,在进行预渲染时,也同样会拉长打包的总时间,使我们每次构建的速度大大降低,这是十分糟糕的开发体验。
# SSR-服务端渲染
服务端渲染的方式其实就好比我们以前使用jsp
等技术直接在服务端借助模板引擎直接渲染出HTML
文档返回给客户端,对于一些小型项目而言,这种方式无疑是比较节约人力成本的,但不得不说这种开发方式十分不友好。
不同于预渲染方式,服务端渲染的好处在于,客户端在初次获取到页面时看到的就已经是最新的数据渲染出来的页面了,服务端会预先获取到需要渲染的数据,并组装成完整的页面返回给客户端,这种方式无疑就比预渲染数据延迟的模式友好得多。
对于我们目前主流的前端框架来说:Vue
、React
,都已支持了服务端渲染,只不过相对于纯SPA
页面开发来说,研发成本也相应的有所提高,我们需要考虑许多兼容情况。如果使用过这两大框架的童鞋可能就会接触到虚拟DOM
这个概念,在实现上,他们其实也就是一个个JS
对象,我们在前端一般操作DOM
的方式都是在操作虚拟DOM
,也正是因为有虚拟DOM
,我们才能方便的实现SSR
。
我们在浏览器端操作虚拟DOM
对应的是操作真实的DOM
元素,而在进行服务端渲染时,Node
端操作虚拟DOM
实际上是在操作字符串。
# 优点
- 用户看到完整页面的速度快,因为不需要客户端重新进行渲染,在服务端已经把当前页面渲染完毕了。
- 利于
SEO
,爬虫在抓取我们页面内容的时候就已经能获取到一个渲染好的页面了,所以能轻松获取到网站的关键信息。 - 节省客户端资源,不需要客户端进行渲染操作,对于移动端用户来说,耗电量减少。
- 可以利用缓存机制,将一些页面进行缓存,进一步提高响应速度。
- 数据实时性。
- 无需关注浏览器兼容问题。
# 缺点
- 服务器资源占用,使用服务端渲染,其实也就是把本该在客户端渲染的工作转交给了服务端,这在大流量场景下必然会给服务器带来一定压力。
- 开发成本提高,对于开发者而言我们需要兼顾两端的兼容性,比如
DOM
的操作,在服务端是不存在DOM
的。
# 同构的概念
在React
、Vue
中,我们或许经常能听到同构这样的词汇,那么何为同构呢?
其实就是客户端与服务端进行配合,将代码在客户端与服务端各跑一次,服务端仅负责初次页面的渲染工作,而其他页面的渲染就转交给客户端SPA
进行控制,这样不仅能减轻服务端的压力,也能在一定程度上有利于前后端分离,提高我们的开发体验。
# Hydrate
在这个过程当中,一般服务端渲染完初始页的内容之后,会有一个Hydrate
的过程,在客户端会创建出对应的虚拟DOM
并与服务端渲染的DOM
进行比对,如果匹配,那么客户端将直接接管服务端渲染好的页面,如果不匹配,那么客户端就会重新生成新的真实DOM
,然后抛弃服务端渲染出来的DOM
,这也将造成性能损耗,所以在代码编写中应该避免一些浏览器机制导致的坑。
浏览器会在
<table>
内部自动注入<tbody>
,然而,由于 Vue 生成的虚拟 DOM (virtual DOM) 不包含<tbody>
,所以会导致无法匹配。为能够正确匹配,请确保在模板中写入有效的 HTML。
# Vue SSR原理概览
这里我们从官网给出来的一张整体流程图来分析,我们可以发现我们的业务代码将由app.js
作为入口,并且需要配合webpack
进行打包,同时我们的项目需要提供两套webpack
配置,分别为服务端构建配置和客户端构建的配置。
通过这个过程打包完毕之后我们就能拿到两个Bundle
,服务端Bundle
将由服务器(Node.js
)进行服务端渲染,并将渲染好的结果返回给客户端,同时会有一个Hydrate
(注水)的过程,将我们服务端渲染好的HTML
与客户端代码进行混合,接着由客户端接管页面的渲染,正如前面所说,这个注水的过程我们也需要注意规避一些不必要的坑。
不仅如此,我们从图中可以看到,服务端构建配置的entry
与客户端构建配置的entry
都引入了同一个app.js
,所以这就是我们前面为什么提到我们的代码需要考虑两端的兼容问题,同一份代码将会执行在两个不同的环境中。
这里也不要被这个图吓到,本质上其实概念不多,如果说想要配置一个简易的
SSR
应该还是不难的。