32.同构渲染
# 1.什么是同构渲染,为什么使用它?
# 1.1 什么是渲染?
以现在前端流行的react和vue框架为例。react中的jsx和vue里面的模板,都是是无法直接在浏览器运行的。将它们转换成可在浏览器中运行的html,这个过程被称为渲染。
# 1.2 什么是客户端渲染(client-side-render,简称csr)
CSR是现在前端开发者最熟悉的渲染方式。利用vue-cli或create-react-app创建一个应用,不作任何额外配置直接打包的出来代码就是CSR。
你可以用如下的方法辨别一个web页面是否是CSR:打开chrome控制台 - 网络面板,查看第一条请求,就能看到当前页面向服务器请求的html资源;如果是CSR,这个html的body中是没有实际内容的。
那么页面内容是如何渲染出来的呢?仔细看html,会发现存在一个script标签,打包器正是把整个应用都打包进了这个js文件里面。
当浏览器请求页面的时候,服务器先会返回一个空的html和打包好的js代码;等到js代码下载完毕,浏览器再执行js代码,页面就被渲染出来了。因为页面的渲染是在浏览器中而非服务器端进行的,所以被称为客户端渲染。
# CSR的优劣
CSR会把整个网站打包进js里,当js下载完毕后,相当于网站的页面资源都被下载好了。这样在跳转新页面的时候,不需要向服务器再次请求资源(js会直接操作dom进行页面渲染),从而让整个网站的使用体验上更加流畅。
但是这种做法也带来了一些问题:在请求第一个页面的时候需要下载js,而下载js直至页面渲染出来这段时间,页面会因为没有任何内容而出现白屏。在js体积较大或者渲染过程较为复杂的情况下,白屏问题会非常明显。
另外,由于使用了CSR的网站,会先下载一个空的html,然后才通过js进行渲染;这个空的html会导致某些搜索引擎无法通过爬虫正确获取网站信息,从而影响网站的搜索引擎排名(一般称之为搜索引擎优化Search Engine Optimization,简称SEO)。
总而言之,客户端渲染就是通过牺牲首屏加载速度和SEO,来获取用户体验的一种技术
# 1.3 什么是服务器端渲染(server-side-render, 以下简称SSR)
理解了CSR,SSR也很好理解了,其实就是把渲染过程放在了在服务器端。以早年比较流行的java服务器端渲染技术jsp为例,会先写一个html模板,并用特殊的语法<%...%>标记动态内容,里面可以写一些java程序。
渲染的时候,jsp会通过字符串替换的方式,把<%...%>替换为程序执行的结果。最后服务器将替换完毕的html以字符串的形式发送给用户即可。
同时我们还可以写很多个JSP,根据用户的http请求路径返回相应的文件,这样就完成了一个网站的开发。
像jsp这类SSR技术,优劣势和客户端渲染正好相反:因为html在服务器端就已经渲染好了,所以不存在客户端的白屏和seo问题;相对应地,每次跳转页面都要向服务器重新请求,意味着用户每次切换页面都要等待一小段时间,所以用户体验方面则不如客户端。
还有一点显而易见的问题,就是SSR相比CSR会占用较多的服务器端资源。
总而言之,服务器端渲染拥有良好的首屏性能和SEO,但用户体验方面较差。且会占用较多的服务器端资源
# 2. 什么是同构(Isomorphic)
CSR和SSR的优劣势是互补的,所以只要把它们二者结合起来,就能实现理想的渲染方法,也就是同构渲染。next.js(react)或nuxt.js(vue)
同构的理念十分简单,最开始的步骤和SSR相同,将生成的html字符串返回给用户即可;但同时我们可以将CSR生成的JS也一并发送给用户;这样用户在接收到SSR生成的html后,页面还会再执行一次CSR的流程。
这导致用户只有请求的第一个页面是在服务器端渲染的,其他页面则都是在客户端进行的。这样我们就拥有了一个同时兼顾首屏、SEO和用户体验的网站。
# 3. 实现脱水(Dehydrate)和注水(Hydrate)
同构应用还有一个比较重要的点,就是如何实现服务器端的数据的预取,并让其随着html一起传递到浏览器端。
例如我们有一个列表页,列表数据是从其他服务器获取的;为了让用户第一时间就看到页面内容,最好的方法当然是在服务器就拿到数据,然后随着html一起传递给浏览器。浏览器拿到html和传过来的数据,直接对页面进行初始化,而不需要再在客户端请求这个接口(除非浏览器因为某些原因获取数据失败)。
为了实现这个功能,整个过程分为两部分:
- 服务端获取数据之后,把数据伴随着html一起传给客户端的过程,一般叫做脱水(Dehydrate)
- 客户端拿到html和数据,利用这个数据来初始化组件的过程叫做注水(Hydrate)
注水其实就是前面提到过的客户端激活,区别只是前面的没有数据,而这次我们会试着加上数据。国内也有翻译成"水合"的,现在你应该知道了,注水、客户端激活、水合还有Hydrate其实都是一码事。
注意: 服务端是node环境,客户端是浏览器环境,如果在node端直接使用了像window或document,这种仅浏览器可用的全局变量,则会在Node.js中执行时抛出错误;反之,在浏览器使用了node端的api也是如此。
需要注意的是,在vue组件中,服务器端渲染时只会执行beforeCreate和created生命周期,在这两个生命周期之外执行浏览器api是安全的。所以推荐将操作dom或访问window之类的浏览器行为,一并写在onMounted生命周期中,这样就能避免在node端访问到浏览器api。
如果要在这两个生命周期中使用浏览器端api,可以利用相关打包工具提供的变量(如vite提供了import.meta.env.SSR),来避免服务器端调用相关代码。
尤其需要注意的是,一些组件库可能也会因为编写的时候没有考虑到服务器端渲染的情况,导致渲染出错。这时候可以借助一些第三方组件,如nuxt中的ClientOnly[7],可以避免这些出错的组件在服务器端进行渲染。
# CSR、SSR区别
- 执行位置不同
- 客户端渲染是在浏览器中执行,将静态资源HTML、CSS、JS下载到浏览器后渲染
- 服务端渲染是在服务器中执行,将渲染好的HTML发送到客户端,浏览器直接显示
- 首屏加载速度
- 客户端渲染首屏加载速度相对较慢,需要等待所有资源加载完成后在渲染
- 服务端渲染首屏加载较快,服务器端已经渲染好HTML,浏览器直接显示
- SEO优化
- 客户端渲染不利于SEO,爬虫只能抓取到空的HTML
- 服务端渲染有利于SEO,爬虫可以直接抓取到完全渲染的HTML
- 开发难度
- 客户端渲染开发相对简单
- 服务端渲染需要考虑服务器端实现,开发更复杂
- 交互处理
- 客户端渲染交互直接在浏览器中完成
- 服务端渲染需要考虑同构,在服务器端预加载数据