2803 字
14 分钟
SSR篇1-从认识渲染方式到简单的SSR问题
2025-11-09

唠叨#

上一篇博客 (9月末) 的时候, 本人在写Nuxt 操作localstorage的时候遇到了一个问题, 起初以为是代码设计的问题, 后来发现是需要避免的 水合(Hydration)问题。 当时其实就挺想写一篇文章的, 不过无奈现实生活的繁忙 加上才疏学浅, ssr的水又太深 得花大精力研究我才能写出一篇文章。就拖到现在才写一小篇。悲

一开始我研究 水合问题 我想着就解决水合问题是什么就行了, 然后随便总结几个可能触发水合问题的情况。 后来再看, 水合问题是 SSR(Server Side Rendering) 和 SSG(Static Side Generation) 两种 渲染方式(Rendering Patterns) 共有的一种问题, 那我也得搞懂SSR和SSG是什么, 不然无法理解为什么要水合, 以及水合到底是在干什么。 那要理解SSR和SSG, 我又得知道渲染方式是什么 现在大概有哪些渲染方式…

所以那也只能原谅我了吧, 原谅我拖了这么久才写出这一小篇文章

不过虽然我好像关于SSR和水合问题没多少经验, 但回头一算我竟发现自己已经接触了三个带SSR的项目 (算上我的博客,它是Astro的孤岛渲染方式, 我对博客也改造过)。 那看来我应该还是有底气写好文章的吧。

概念引入#

从知识体系的层层递进来说吧, 我应当先说什么是渲染方式再说下什么是SSR再说什么是水合问题…

不过这可真是条长路, 那就先非常笼统的讲下几个核心名次概念(渲染方式, SSR), 再容我用简单的例子来让大家基本的理解理解SSR和水合问题,能明白几个简单的SSR报错情况, 之后再谈对SSR什么的深刻理解吧。

从渲染说起吧#

渲染指的是从三件套代码变为具体的页面的过程 这里我对于其具体含义掌握也不深, 直接放一段gemini的解释。总的来说你需要理解渲染是比较耗时的, 是有一定成本的, 所以我们才会有不同的渲染方式

alt text

SSR —— Server Side Rendering —— 服务端渲染, 是一种渲染方式, 渲染方式描述的是: 浏览器是如何把你写的前端代码渲染成网页; 以下我举例两个读者大概率接触过的渲染方式

静态渲染#

比如你直接写个Hello.html文件 然后跑起来, 那么采用的就是Static WebSites——静态渲染, 就啥也不操作, 纯纯把你的Hello.html搬到浏览器上让浏览器把他渲染出来就完成了。 那么如果你想要分页面, 你就应该在url里写 /page1.html ,/page2.html 通过切换访问的文件来切换页面

客户端渲染#

相对应的比如说我们直接用npm create vue@latest 创建一个新的Vue项目, 那么它的渲染方式默认就是 CSR - Client Side Rendering - 客户端渲染。

在一个CSR的前端项目里, 服务器返回的html一开始只是一个空壳, 后续再由js动态的去在这个html里生成页面内容;

如vue里就是给你一个<div id="app"></div>, 你所写的所有.vue文件, 都是通过js操作来嵌到前面那个id为app的div里, 进而展示内容的; 在这里呢, js和html始终都是一体的

服务端渲染#

通过上面两种大家熟悉但是不知道名字的渲染方式, 相信大家已经对于浏览器渲染这一操作有了一定的理解了吧。 接下来来到重头戏SSR(服务端渲染)了

那么经常改造自己博客的朋友们都知道, 一般来说博客都是采用 ssr来渲染的 (Hexo是纯静态); 它的具体渲染逻辑, 简单来说就是 在服务器上预先把页面渲染成完整的 HTML 字符串,然后返回给浏览器, 之后再想办法把js代码嵌到HTML上(实际表现为接管已经渲染好的DOM节点)

相比于CSR呢, 比如说如果你的博客采用CSR, 那么在用户进入博客的时候, 服务器传给用户的会是一个的空壳, 然后浏览器再运行服务器传来的js代码, 在空壳里渲染;

而如果用SSR, 那么服务器会直接把完整的预先渲染好的博客首页内容以带内容的HTML文件的形式传过来, 用户不用再去渲染; 也就是说

首屏加载速度 相比起来会非常的快, 类比餐饮就相当于给了你预制菜, 而CSR是搞了半天还得自己煮;

前置步骤-服务端渲染#

这一步对于用户来说并不能算一个第一步, 因为往往服务器都是提前把html, js代码渲染好的, 用户(客户端)只要等着获取就行了;

需要注意, html和js代码这里是分开的, 正如我前面所说。 js代码会在后面通过客户端水合 嵌到html上

另一方面但是其他如CSR的渲染方式, 都是在客户端渲染的, 那服务端和客户端的环境是有区别的, 这会导致书写SSR时容易造成一些报错; 关于这点, 本文后面有一些相关的例子。

第一步-获取静态文件#

客户端获取服务端提前渲染的html和js文件, 并直接使用html来填充浏览器页面; 于是用户就很快的得到了一个静态的页面——SSR的核心优势之一

至于js文件呢, 将在下一步起作用。

第二步- 客户端水合#

这一步是SSR最特殊于其他渲染方式的, 也是最难理解的一步; 在这一步, 我们需要把在上一步还没用到的js文件和已经用于呈现静态页面的html文件拼装起来, 拼装完成后的整体才是一个前端页面完整体

比如说, 我写了一个登录页, 通过一开始服务端传来的HTML我当然可以把登录按钮什么的渲染出来, 但是显然没有js的话, 这个按钮就是一个空壳, 没法实际登录的; 我们得 给它绑定上 @click=login() 以及 login函数的具体实现, 两段js代码, 这样页面才能正常运行; 这个把js绑定到对应html元素的操作 就叫做 水合(hydrate)

也就是说, 水合是 将在前置步骤(此处是服务端渲染)拆开的, 原就应当是一体的一份html代码和其对应的js代码, 重新关联起来的过程

第三步-客户端正常运行#

最后就是在客户端正常运行已经水合完毕的完整前端了 同CSR

常见报错问题#

以下来讲下SSR使用过程中的

localStorage无法调用#

首先我们假设已经在localStorage(即浏览器缓存) 中存好了一个 key为API_KEY的键值对

<script setup lang="ts">
const key = localStorage.getItem("API_KEY")
</script>

那么请问, 这么写在ssr渲染的情况下能正常运行吗? 并不能 我们会收获如下报错 alt text

这里我们会报错; 原因是在 前置步骤-服务端渲染 因为在服务端上并没有localStorage这个api , localStorage是浏览器相关的操作,是用来调用浏览器缓存的;

那么这样会报错吗

import { getKey } from "~/use-key.ts";
const key = getKey()
//下为use-key.ts
export const getKey = () => {
const key = localStorage.getItem("API_KEY")
return key;
};

也是会的, 也就是即使是import操作, 也会在服务端渲染的时候执行

正确的不报错操作应该是如下

function getKey() {
return ...
}
onMounted(()=> {
getKey();
})

如上报错的原因均为服务端渲染时没有localStorage这个api导致的 而正确写法中, onMounted是会在第三步-客户端正常运行 中触发的(这里是onMounted的性质, 会在页面挂载后运行)

部分请求不该在服务端发送#

我们很经常写 当进入xxx页面就打xxx请求 的逻辑

然而, 有些请求在 前置步骤-服务端渲染 不应该被打出, 如你打请求时需要附带一些需要用户提交的东西, 例如有登录功能的项目里, 很多请求都要求带有用户登录态相关的东西(比如需要token鉴权), 那么这种情况这些请求在服务端渲染时打出就是没有意义的

那咋办呢? 哎, 像Nuxt框架里就有一个东西叫<client-only> 如下

<client-only>
<!-- 内容物 -->
</client-only>

你可以把要打请求的部分封装成一个组件, 然后填到<client-only>里, 这个元素的意思是只会在 第三步-客户端正常运行 时渲染相关的玩意, 那自然就不会有问题了

客户端水合时报错 (水合问题)#

这里我举出的例子就是我上一篇文章;因为这个问题本身非常的复杂, 我觉得值得单独放在一篇文章里讲述;

虽然那篇文章标记着发布时间为9月份, 不过核心的根本解决方案部分是我在写完这篇SSR1后才再次撰写的, 其他部分也经过修改过, 可以放心观看

自动保存apiKey功能遇初始化异步, 最终确诊水合问题

尾声#

其实我还有好多想写的, 像是在我的博客astro项目里见到的如下神秘写法

---
//本文件是一个组件 叫ConfigCarrier.astro, 被直接挂载到layout.astro (即挂载到所有页面上)
import { siteConfig } from "../config";
---
<div id="config-carrier" data-hue={siteConfig.themeColor.hue} data-lightDarkMode={siteConfig.lightDarkMode.defaultMode}>
</div>

首先解释下, 这里config文件是博客使用者的设置, siteConfig是里面的存储设置的const变量; 这个组件在其他文件里会被通过dom操作获取到存储的config信息。

那, 为什么不直接在需要这些信息的地方import siteConfig呢?

我目前理解是: 因为config文件并不会被服务端渲染, 也就是说实际压根不会发给客户端

这个东西我接下来也会再研究, 上述猜想要是错了的话, 这篇文章里我也不再修改了, 会在下一篇SSR相关文章写一大段来讲述这相关的内容

碍于时间以及篇幅, 这里就先停笔了 希望你们很快就能看到我的SSR篇2文章捏

参考#

视频: 10 Rendering Patterns for Web Apps (这分类的有点泛, 不过讲的够广)

文章: Understanding Hydration in React applications(SSR)

Bing搜索

ChatGpt

我的项目代码

折乙先生的大脑

SSR篇1-从认识渲染方式到简单的SSR问题
https://zheyi.in/posts/frontend/ssr1/
作者
折乙
发布于
2025-11-09
许可协议
CC BY-NC-SA 4.0