服务端渲染 SSR

CSR 、 SSR 、 以及同构渲染
CSR: 客户端渲染
CSR 是在浏览器中完成模版与数据的融合,并渲染出最终的 HTML 页面

客户端向服务器或 CDN 发送请求,获取静态的 HTML 页面。注意,此时获取的 HTML 页面通常是空页面。在 HTML 页面中,会包含 <style>
、<link>
和<script>
等标签。例如:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
<link rel="stylesheet" href="/dist/app.css" />
</head>
<body>
<div id="app"></div>
<script src="/dist/app.js"></script>
</body>
</html>
这是一个包含 <link rel="stylesheet">
与 <script>
标签的空 HTML 页面。浏览器在得到该页面后,不会渲染出任何内容,所以从用户的视角看,此时页面处于“白屏”阶段。虽然 HTML 页面是空的,但浏览器仍然会解析 HTML 内容。由于 HTML 页面中存在 <link rel="stylesheet">
和 <script>
等标签,所以浏览器会加载 HTML 中引用的资源,例如 app.css 和 app.js。接着,服务器或 CDN 会将相应的资源返回给浏览器,浏览器对 CSS 和 JavaScript 代码进行解释和执行。因为页面的渲染任务是由 JavaScript 来完成的,所以当 JavaScript 被解释和执行后,才会渲染出页面内容,即“白屏”结束。但初始渲染出来的内容通常是一个“骨架”,因为还没有请求 API 获取数据。
客户端再通过 AJAX 技术或 Fetch 请求 API 获取数据,一旦接口返回数据,客户端就会完成动态内容的渲染,并呈现完整的页面。当用户再次通过点击“跳转”到其他页面时,浏览器并不会真正的进行跳转动作,即不会进行刷新,而是通过前端路由的方式动态地渲染页面,这对用户的交互体验会非常友好。但很明显的是,与 SSR 相比,CSR 会产生所谓的“白屏”问题。实际上,CSR 不仅仅会产生白屏问题,它对 SEO(搜索引擎优化)也不友好。
为什么客户端渲染对于 SEO 不友好?
- 搜索引擎爬虫难以解析动态的内容,在 CSR 中,页面的内容是通过 js 在客户端动态生成的,而搜索引擎爬虫在抓取页面的时候不会无法执行 js,因此无法获取到动态生成的内容,导致无法正确解析和索引页面内容。
- CSR 加载网页需要执行大量的 js 代码,而网页的加载速度对于 SEO 也是一个权重问题,所以网页加载慢会影响 SEO
- 缺乏静态 HTML 版本,CSR 不会像 SSR 服务端渲染原因在服务器端生成静态的 HTML 版本,而是依赖于客户端 js 动态生成页面内容,这导致搜索引擎无法获取到静态的 HTML 内容进行索引
SSR 是服务端渲染

- 用户通过浏览器请求站点。
- 服务器请求 API 获取数据。
- 接口返回数据给服务器。
- 服务器根据模板和获取的数据拼接出最终的 HTML 字符串。
- 服务器将 HTML 字符串发送给浏览器,浏览器解析 HTML 内容并渲染。当用户再次通过超链接进行页面跳转,会重复上述 5 个步骤。
- 可以看到,传统的服务端渲染的用户体验非常差,任何一个微小的操作都可能导致页面刷新。
服务端渲染的特点

服务端渲染的是应用的当前快照,它不存在数据变更后重新渲染的情况。因此,所有数据在服务端都无须是响应式的。利用这一点,我们可以减少服务端渲染过程中创建响应式数据对象的开销。
服务端渲染只需要获取组件要渲染的 subTree 即可,只是拼接成 html 字符串,让浏览器去识别解析,无须调用渲染器完成真实 DOM 的创建。因此,在服务端渲染时,可以忽略“设置 render effect 完成渲染”这一步。
同构渲染
那既然客户端渲染会导致首页白屏,而服务端渲染不会,但服务端渲染每次一个微小的操作都会导致服务器重新生成 html 字符串给浏览器重新渲染,即页面刷新,那有没有一种权衡的方法让首次加载不会出现白屏,非首次加载不用重复刷新页面呢?
有的,同构渲染可以做到权衡
什么是同构渲染?
同构一词指的是同一套代码既可以在服务端运行,也可以在客户端运行
vue 既可以在服务端运行,也可以在客户端运行
vue 在服务端运行,然后拼接成 html 字符串返回给前端,这个过程就是 ssr 服务端渲染
同构渲染即 csr 和 ssr 的结合,首次访问采用 ssr(服务器拼接好 html 返回给前端,不用因为加载 js 来生成 dom 节点导致页面加载出现的白屏),此时渲染的是服务端拼接好的 html,这时候还需要一步激活操作,即使当前服务端生成的真实 dom 和 vnode 虚拟 dom 建立关联,并且绑定相应的事件,这样就可以交互了,后续的操作就都交给 csr 客户端渲染,除非手动刷新网页,那么就重新走一下首页服务端渲染流程
非首次访问采用 csr(使用 js 来实现路由跳转,不用每次都刷新页面)
那同构渲染可以提升用户的交互时间吗?
不可以,因为首屏加载是服务端拼接字符串,解决的是加载白屏,但是 vue 在服务端拼接 html 字符串只是生成 dom 而已,数据并不是响应式的、而且事件也没有绑定,所以服务端返回的 html 还没有事件交互,所以没有提升交互时间,还需要 vue 在客户端完成虚拟 dom 和真实 dom 的关联和事件的绑定,这样才可以交互。
同构渲染总结:

同构渲染方案在首次渲染时和浏览器刷新时仍然需要服务端完成渲染工作,所以也需要部分服务端资源,但相比所有页面跳转都需要服务端完成渲染来说,同构渲染所占用的服务端资源相对少一些。
同构组件注意点
- 注意组件的生命周期 vue 在服务端只执行 create 生命周期函数,并不执行 mount 挂载 dom,只生成 html 字符串,所以使用定时器的时候需要注意不要在 mount 生命周期函数回调进行清除,因为不会执行,会导致内存泄漏 可以使用环境变量来判断哪些代码需要在服务端执行,哪些是在客户端进行
- 使用跨平台的 API,兼容浏览器和 nodeJS 宿主环境
- ClinetOnly 组件只在客户端环境才执行
clinetOnly 组件源码
import { ref, onMounted, defineComponent } from 'vue';
export const ClientOnly = defineComponent({
setup(_, { slots }) {
// 标记变量,仅在客户端渲染时为 true
const show = ref(false);
// onMounted 钩子只会在客户端执行
onMounted(() => {
show.value = true;
});
// 在服务端什么都不渲染,在客户端才会渲染 <ClientOnly> 组件的插槽内容
return () => (show.value && slots.default ? slots.default() : null);
},
});
可以看到,整体实现非常简单。其原理是利用了 onMounted 钩子只会在客户端执行的特性。我们创建了一个标记变量 show,初始值为 false,并且仅在客户端渲染时将其设置为 true。这意味着,在服务端渲染的时候,<ClientOnly>
组件的插槽内容不会被渲染。而在客户端渲染时,只有等到 mounted 钩子触发后才会渲染 <ClientOnly>
组件的插槽内容。这样就实现了被 <ClientOnly>
组件包裹的内容仅会在客户端被渲染。