唠叨
上一篇博客 (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什么的深刻理解吧。
从渲染说起吧
SSR —— Server Side Rendering —— 服务端渲染, 是一种渲染方式, 渲染方式描述的是: 浏览器是如何把你写的前端代码渲染成网页; 以下我举例两个读者大概率接触过的渲染方式
静态渲染
比如你直接写个Hello.html文件 然后跑起来, 那么采用的就是Static WebSites——静态渲染, 就啥也不操作, 纯纯把你的Hello.html搬到浏览器上让浏览器把他渲染出来就完成了。
那么如果你想要分页面, 你就应该在payload里写 /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来渲染的; 它的具体渲染逻辑, 简单来说就是 在服务器上预先把页面渲染成完整的 HTML 字符串,然后返回给浏览器, 之后再想办法把js代码嵌到HTML上(实际表现为接管已经渲染好的DOM节点)
相比于CSR呢, 比如说如果你的博客采用CSR, 那么在用户进入博客的时候, 服务器传给用户的会是一个的空壳, 然后浏览器再运行服务器传来的js代码, 在空壳里渲染;
而如果用SSR, 那么服务器会直接把完整的预先渲染好的博客首页内容以带内容的HTML文件的形式传过来, 用户不用再去渲染; 也就是说
首屏加载速度 相比起来会非常的快, 类比餐饮就相当于给了你预制菜, 而SPA是
搞了半天还得自己煮;
前置步骤-服务端渲染
这一步对于用户来说并不能算一个第一步, 因为往往服务器都是提前把html, js文件渲染好的, 用户(客户端)只要等着获取就行了;
需要注意, html和js文件这里是分开的, 正如我前面所说。 js文件会在后面通过客户端水合 嵌到html上
另一方面但是其他如SPA的渲染方式, 都是在客户端渲染的, 那服务端和客户端的环境是有区别的, 这会导致书写SSR时容易造成一些报错; 关于这点, 本文后面有一些相关的例子。
第一步-获取静态文件
客户端获取服务端提前渲染的html和js文件, 并直接使用html来填充浏览器页面; 于是用户就很快的得到了一个静态的页面——SSR的核心优势之一
至于js文件呢, 将在下一步起作用。
第二步- 客户端水合
这一步是SSR最特殊于其他渲染方式的, 也是最难理解的一步; 在这一步, 我们需要把在上一步还没用到的js文件和已经用于呈现静态页面的html文件拼装起来, 拼装完成后的整体才是一个前端页面完整体
比如说, 我写了一个登录页, 通过一开始服务端传来的HTML我当然可以把登录按钮什么的渲染出来, 但是显然没有js的话, 这个按钮就是一个空壳, 没法实际登录的;
我们得 给它绑定上 @click=login() 以及 login函数的具体实现, 两段js代码, 这样页面才能正常运行; 这个把js绑定到对应html元素的操作 就叫做 水合(hydrate)
也就是说, 水合是
将在前置步骤(此处是服务端渲染)拆开的, 原就应当是一体的一份html代码和其对应的js代码, 重新关联起来的过程
第三步-客户端正常运行
最后就是在客户端正常运行已经水合完毕的完整前端了 同CSR
实例讲解及常见问题
上面我讲归讲了, 但是实际上肯定还是难以理解的, 以及有很多细节只是看概念也根本懂不了, 下面我来实例讲解下。
虽然普遍来说大家通过博客接触到ssr会多一点, 不过博客的ssr用的框架各不相同, 也不方便我进行实例的书写; 所以这里我用Nuxt框架来讲解
除了Nuxt外, 还有React的Next.js, hexo(搭博客的那个), astro框架(群岛渲染) 比较常见且知名
这里你也不用具体了解Nuxt是什么, 你只要知道他是基于Vue的, 同时又支持SSR渲染就ok了; 不过处于尊重还是贴个中文官网链接
npm create nuxt@latest ssrNuxtProject这样我们构建一个Nuxt新项目, 你不手动改配置的话那就是ssr渲染
我们来模拟一个key验证的场景
首先对于第一步-获取静态文件, 在自行书写一定的html模拟一个简单的页面后, 你应该也无法直观的感受到性能的提升(不过你大概率能感受到——SSR项目构建是真的慢)
想要体验性能提升的话还是去找找一些常规SSR网站, 或者听听一些框架吹的牛吧
以下来讲下SSR使用过程中的常见报错问题
服务端渲染时报错
不知道为什么 GPT 觉得这也能算水合问题,但我个人觉得这肯定不太算
localStorage无法调用
首先我们假设已经在localStorage(即浏览器缓存) 中存好了一个 key为API_KEY的键值对
<script setup lang="ts">const key = localStorage.getItem("API_KEY")</script>那么请问, 这么写在ssr渲染的情况下能正常运行吗? 并不能 我们会收获如下报错 
这里我们会报错; 原因是在 前置步骤-服务端渲染 因为在服务端上并没有localStorage这个api , localStorage是浏览器相关的操作,是用来调用浏览器缓存的;
那么这样会报错吗
import { useKey } from "~/composables/use-key";const key = useKey()//下为use-key.tsexport const useKey = () => { const key = localStorage.getItem("API_KEY") return key;};也是会的, 也就是即使是import操作, 也会在服务端渲染的时候执行
正确的不报错操作应该是如下
function getKey() { return ...}onMounted(()=> { getKey();})如上报错的原因均为服务端渲染时没有localStorage这个api导致的
而正确写法中, onMounted是会在第三步-客户端正常运行 中触发的(这里是onMounted的性质, 会在页面挂载后运行)
无法打请求
这个就同理了, 在服前置步骤-服务端渲染的时候是不能打请求的,
我们写前端的时候很经常直接把请求变量裸丢着, 比如
const res = fetch('https://api.example.com/login', { method: 'post' });//下略对res一通操作那这在ssr里就不能直接写了
那咋办呢? 哎, 像Nuxt框架里就有一个东西叫<client-only> 如下
<client-only> <!-- 内容物 --></client-only>你可以把要打请求的部分封装成一个组件, 然后填到<client-only>里, 这个元素的意思是只会在 第三步-客户端正常运行 时渲染相关的玩意, 那自然就不会有问题了
客户端水合时报错 (水合问题)
这里我举出的例子就是我上一篇文章;因为这个问题本身非常的复杂, 我觉得值得单独放在一篇文章里讲述;
虽然那篇文章标记着发布时间为9月份, 不过核心的根本解决方案部分是我在写完这篇SSR1后才再次撰写的, 其他部分也经过修改过, 可以放心观看
尾声
其实我还有好多想写的, 像是在我的博客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
我的项目代码
折乙先生的大脑