React从前的SPA(CSR)到现在的SSR和SSG原理解析

在Vue、React、Anglar三分天下时,SPA应用风靡前端技术圈。随前端技术发展,SSR、SSG概念出现并催生了一系列框架和库,说来这两个概念也不是新东西。就像某位故人所说,这世间的创新都是新瓶装旧酒,这些概念在php时代之前就有了,只不过那时还不叫SSR,所谓大师都是起名大师。那我就从SPA出现之后开始说起,假设你有 React 客户端单页应用 (SPA) 的开发经验,我会从你的角度出发,用通俗的方式解释 React 18 引入的服务器端渲染 (SSR) 和静态站点生成 (SSG) 特性,特别是 hydrateRoot 的作用和背后的工作原理。我会尽量避免过于复杂的术语,逐步拆解,让你理解这些特性的"幕后故事"。


  1. 先回顾 SPA,理解 SSR 和 SSG 的出发点

在传统的 React SPA 中:

  • 浏览器请求页面时,服务器返回一个几乎空的 HTML(通常只有一个<div id="root"></div>)。

  • React 在客户端加载 JavaScript 后,运行代码,生成 DOM 结构,渲染出完整的 UI。

  • 问题:

    • 首屏加载慢:用户需要等待 JavaScript 下载、解析、执行,才能看到内容。
    • SEO 不友好:爬虫看到的是空 HTML,搜索引擎难以索引内容。

SSR 和 SSG 的目标:

  • SSR (Server-Side Rendering):让服务器预先生成完整的 HTML,浏览器收到后立即显示内容,之后 React 在客户端"接管"页面,使其可交互。
  • SSG (Static Site Generation):在构建时生成静态 HTML 文件,适合内容不经常变化的页面(如博客、文档),兼顾速度和 SEO。
  • 两者都让用户更快看到内容,同时对 SEO 更友好。

hydrateRoot 的角色:

  • 在 SSR 或 SSG 中,服务器已经生成了 HTML,hydrateRoot 是 React 在客户端"激活"这些 HTML 的步骤,让静态的 HTML 变成动态的 React 应用。

  1. 从 SPA 到 SSR:发生了什么?

假设你有一个简单的 React 组件:

jsx 复制代码
function App() {
  return <h1>Hello, World!</h1>;
}

SPA 模式:

  1. 服务器返回:

    html 复制代码
    <div id="root"></div>
    <script src="app.js"></script>
  2. 浏览器加载 app.js,React 调用 ReactDOM.createRoot 和 root.render(),生成:

    html 复制代码
    <div id="root">
      <h1>Hello, World!</h1>
    </div>
  3. 用户等待 JavaScript 执行完成才看到内容。

SSR 模式:

  1. 服务器端:

    • 服务器运行 React 代码(通过 Node.js),调用 renderToString(),生成 HTML:

      html 复制代码
      <div id="root">
        <h1>Hello, World!</h1>
      </div>
    • 服务器返回完整的 HTML 给浏览器:

      html 复制代码
      <!DOCTYPE html>
      <html>
        <body>
          <div id="root">
            <h1>Hello, World!</h1>
          </div>
          <script src="app.js"></script>
        </body>
      </html>
    • 用户立即看到 <h1>Hello, World!</h1>,无需等待 JavaScript。

  2. 客户端:

    • 浏览器收到 HTML,显示静态内容。
    • JavaScript 加载后,React 需要让这个静态 HTML 变成"活的"(可交互的 React 应用)。
    • 这就是 hydrateRoot 干的事!它不会重新生成 DOM,而是"复用"服务器的 HTML,绑定 React 的事件和状态。

SSG 模式:

  • 与 SSR 类似,但 HTML 是在 构建时生成的,保存在静态文件中(如 index.html)。
  • 部署后,服务器直接返回这些文件,客户端依然用 hydrateRoot 激活。

  1. hydrateRoot 到底在做什么?

想象一下,服务器送来了一盘"做好的菜"(HTML),但这盘菜是冷的,不能直接吃。hydrateRoot 就像微波炉,把"冷的 HTML"加热成"热的 React 应用",让它可以响应用户操作。

具体步骤:

  1. 读取现有 DOM:

    • hydrateRoot 找到容器(比如 <div id="root">),检查里面的 HTML 结构。
    • 它假设这些 HTML 是由服务器的 renderToString 生成的。
  2. 绑定 React 逻辑:

    • React 遍历 DOM 树,将每个节点与对应的 React 组件关联。
    • 为节点添加事件监听器(比如 onClick)、状态管理(useState)等。
    • 这个过程尽量复用现有 DOM,避免重新创建,节省性能。
  3. 验证一致性:

    • React 比较服务器生成的 HTML 和客户端预期的组件树。
    • 如果一致,hydration 成功,页面变成可交互的 React 应用。
    • 如果不一致(比如服务器和客户端渲染结果不同),React 会抛出错误("hydration mismatch")。

代码示例:

服务器端(生成 HTML):

jsx 复制代码
import { renderToString } from 'react-dom/server';
import App from './App';

const html = renderToString(<App />);
console.log(html); // <h1>Hello, World!</h1>

客户端(激活 HTML):

jsx 复制代码
import { hydrateRoot } from 'react-dom/client';
import App from './App';

const container = document.getElementById('root');
hydrateRoot(container, <App />);

最终效果:

  • 用户先看到服务器的 <h1>Hello, World!</h1>
  • hydrateRoot 在后台绑定 React 逻辑,页面变成动态的。

  1. 为什么用 hydrateRoot 而不是 createRoot?

在 SPA 中,你用 createRoot 和 root.render 从零开始渲染 DOM:

jsx 复制代码
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);

但在 SSR/SSG 中,DOM 已经存在(服务器生成的 HTML)。如果用 createRoot,React 会清空现有 DOM,重新渲染,这会:

  • 导致页面闪烁(用户先看到内容,又被清空重绘)。
  • 浪费性能(丢弃服务器的成果)。

hydrateRoot 更聪明,它:

  • 复用服务器的 DOM,减少重绘。
  • 只添加事件和状态,保持页面流畅。

  1. React 18 的新特性如何影响 SSR 和 SSG?

React 18 引入了一些与 SSR/SSG 相关的改进,让 hydrateRoot 更强大:

(1) Concurrent Rendering:

  • React 18 可以"分优先级"处理任务。比如,hydration 期间,用户点击按钮不会卡住页面。
  • hydrateRoot 支持这种并发特性,确保 hydration 不阻塞交互。

(2) Selective Hydration:

  • 老版本的 React 要求一次性 hydration 整个页面。

  • React 18 允许"部分 hydration"。配合 Suspense,你可以选择先激活关键组件,其他部分延迟加载:

    jsx 复制代码
    import { hydrateRoot } from 'react-dom/client';
    import { Suspense } from 'react';
    
    hydrateRoot(
      document.getElementById('root'),
      <Suspense fallback={<div>Loading...</div>}>
        <App />
      </Suspense>
    );
  • 比如,导航栏可以先交互,文章内容稍后加载。

(3) Streaming SSR:

  • 服务器可以"流式"发送 HTML(边生成边发送),用户更早看到内容。

  • hydrateRoot 支持流式 hydration,逐步激活收到的 HTML 片段。

  • 示例(服务器端):

    jsx 复制代码
    import { renderToPipeableStream } from 'react-dom/server';
    
    const { pipe } = renderToPipeableStream(<App />);
    pipe(response); // 流式发送到客户端

(4) 更好的错误恢复:

  • 如果 hydration 失败(比如服务器和客户端不匹配),React 18 不会直接崩溃,而是尝试跳过问题部分,继续 hydration。

  1. 为什么会遇到 Hydration 失败?

你提到的"hydration failed"问题,通常是因为服务器和客户端渲染的内容不一致。以下是常见的"陷阱"和解决办法,用 SPA 的视角解释:

陷阱 1:客户端专属逻辑

在 SPA 中,你可能直接写:

jsx 复制代码
function Component() {
  return <div>{window.innerWidth}</div>;
}
  • 在 SSR 中,服务器没有 window,会导致服务器渲染 <div></div>,客户端渲染 <div>1920</div>,不匹配。

  • 解决:用 useEffect 延迟客户端逻辑:

    jsx 复制代码
    function Component() {
      const [width, setWidth] = useState(0);
      useEffect(() => {
        setWidth(window.innerWidth);
      }, []);
      return <div>{width}</div>;
    }

陷阱 2:动态内容

在 SPA 中,你可能用随机数:

jsx 复制代码
function Component() {
  return <div>{Math.random()}</div>;
}
  • 在 SSR 中,服务器和客户端的随机数不同,导致不匹配。

  • 解决:动态内容放客户端:

    jsx 复制代码
    function Component() {
      const [random, setRandom] = useState(0);
      useEffect(() => {
        setRandom(Math.random());
      }, []);
      return <div>{random}</div>;
    }

陷阱 3:数据不一致

在 SPA 中,你可能直接 fetch 数据:

jsx 复制代码
function Component() {
  const [data, setData] = useState(null);
  useEffect(() => {
    fetch('/api').then(res => setData(res));
  }, []);
  return <div>{data}</div>;
}
  • 在 SSR 中,服务器需要预先 fetch 数据,否则客户端会渲染不同内容。
  • 解决:服务器和客户端用一致的数据(比如 Next.js 的 getServerSideProps)。

  1. SSG 和 SSR 的对比(以 Next.js 为例)

Next.js 是 React SSR/SSG 的常见框架,帮你省去手动调用 hydrateRoot 的麻烦。它自动处理:

  • SSR:通过 getServerSideProps,在每次请求时生成 HTML。
  • SSG:通过 getStaticProps,在构建时生成 HTML。

SSG 示例:

jsx 复制代码
// pages/index.js
export async function getStaticProps() {
  return { props: { message: 'Hello from SSG!' } };
}

function Home({ message }) {
  return <h1>{message}</h1>;
}
export default Home;
  • 构建时生成静态 HTML,客户端用 hydrateRoot 激活。

SSR 示例:

jsx 复制代码
// pages/index.js
export async function getServerSideProps() {
  return { props: { time: new Date().toISOString() } };
}

function Home({ time }) {
  return <h1>Server time: {time}</h1>;
}
export default Home;
  • 每次请求生成新 HTML,客户端依然用 hydrateRoot。

  1. 背后的"魔法"总结
  • 服务器端:React 像在浏览器一样运行组件,生成 HTML(renderToString 或 renderToPipeableStream)。

  • 客户端:hydrateRoot 复用服务器的 HTML,绑定 React 的"灵魂"(状态、事件),让页面活起来。

  • React 18 的升级:

    • 更灵活(部分 hydration、流式渲染)。
    • 更高效(并发、优先级调度)。
    • 更健壮(错误恢复)。

  1. 动手试试

如果你想实践:

  1. 用 Next.js 创建项目(npx create-next-app),写一个页面试试 SSR 和 SSG。

  2. 或者手动搭一个简单的 SSR:

    • 服务器用 Express + renderToString。
    • 客户端用 hydrateRoot。
    • 观察服务器 HTML 和客户端交互的变化。

示例(简易 SSR): server.js:

jsx 复制代码
const express = require('express');
const { renderToString } = require('react-dom/server');
const App = require('./App').default;

const app = express();
app.get('/', (req, res) => {
  const html = renderToString(<App />);
  res.send(`
    <html>
      <body>
        <div id="root">${html}</div>
        <script src="/client.js"></script>
      </body>
    </html>
  `);
});
app.listen(3000);

client.js:

jsx 复制代码
import { hydrateRoot } from 'react-dom/client';
import App from './App';

hydrateRoot(document.getElementById('root'), <App />);

App.js:

jsx 复制代码
export default function App() {
  return <h1>Hello, SSR!</h1>;
}

  1. 你的问题解答

你提到看不懂 hydrateRoot,可能是因为 SSR/SSG 的流程和 SPA 差异大。现在你知道:

  • 它是为了让服务器的 HTML"活过来"。
  • 它需要服务器和客户端保持一致,否则会报错(hydration failed)。
  • React 18 的新特性让它更灵活,但核心逻辑还是"复用 DOM,绑定逻辑"。

如果还有具体疑惑(比如某个代码片段、Next.js 配置,或想深入某个点),告诉我,我可以再细化!想不想试着写个小例子加深理解?

相关推荐
we19a0sen2 小时前
npm 常用命令及示例和解析
前端·npm·node.js
倒霉男孩4 小时前
HTML视频和音频
前端·html·音视频
喜欢便码4 小时前
JS小练习0.1——弹出姓名
java·前端·javascript
chase。4 小时前
【学习笔记】MeshCat: 基于three.js的远程可控3D可视化工具
javascript·笔记·学习
暗暗那4 小时前
【面试】什么是回流和重绘
前端·css·html
小宁爱Python4 小时前
用HTML和CSS绘制佩奇:我不是佩奇
前端·css·html
weifexie5 小时前
ruby可变参数
开发语言·前端·ruby
千野竹之卫5 小时前
3D珠宝渲染用什么软件比较好?渲染100邀请码1a12
开发语言·前端·javascript·3d·3dsmax
sunbyte5 小时前
初识 Three.js:开启你的 Web 3D 世界 ✨
前端·javascript·3d