Back

Next.js 系列 02:客户端渲染与服务端渲染

Published on 29th August 2024

你现在看到这篇文章的内容,是我重写后的。在此之前我花了一些功夫试图解释什么是客户端渲染,什么又是服务端渲染,以及服务端渲染有哪些优势。

但我发现这不是我想要的。老实说,把这两种渲染模式将明白并且达到通俗易懂的程度,我没有足够的信心。我不希望读到这篇文章的人被我误导,所以假定读者对这两者渲染模式均有所知。

Hydration

当我们的应用使用服务端渲染时,浏览器访问我们的网站,在理想情况下,会先得到一个完整的页面,它的 HTML 与 CSS 均由服务器生成(参考文档:Server React DOM APIs), 它是纯静态的,没有任何交互。

但我们使用 React,目的就是创建动态的具有交互的页面。将初始静态 HTML 转换为交互性 Web 的过程,被称为 hydration(参考文档:hydrateRoot)。

React 核心团队成员 Dan Abramov 的解释相当形象:

Hydration is like watering the “dry” HTML with the “water” of interactivity and event handlers.

陷阱

大部分人都知道,服务器没有 window 对象,在 Next.js 有如下代码:

"use client"; // 表示客户端组件,目前无需关心
 
import React from "react";
 
export function Counter() {
  const [count, setCount] = React.useState(() => {
    return Number(window.localStorage.getItem("saved-count") || 0);
  });
 
  const increment = () => {
    setCount(count + 1);
    window.localStorage.setItem("saved-count", `${count + 1}`);
  };
 
  return (
    <div>
      <p>count is: {count}</p>
      <br />
      <button onClick={increment}>increment</button>
    </div>
  );
}

运行开发服务器,你应该会在终端看到类似输出: window-is-not-defined

这是最常见的陷进之一。

尝试修复

const [count, setCount] = React.useState(() => {
  if (typeof window === "undefined") {
    return 0;
  }
 
  return Number(window.localStorage.getItem("saved-count") || 0);
});

typeof window === ‘undefined’ 是用来判断服务端和客户端的管用手法之一。

此刻,控制台已不再报错,在刷新页面后,也能正确显示我们存在 localStorage 的值。

不幸的是,打开 Developer Tools,会看到新的错误: hydration-error

Text content did not match. Server: “0” Client: “1”
Text content does not match server-rendered HTML.

这是服务端渲染的另一个陷阱:Hydration 不匹配。

代码在服务器上生成的标记是:

<p>count is: 0</p>

Hydration 时候是:

<p>count is: 1</p>

最终

我们使用 useEffect 来修复它:

const [count, setCount] = React.useState(0);
 
React.useEffect(() => {
  const savedCount = window.localStorage.getItem("saved-count");
  if (savedCount !== null) {
    setCount(Number(savedCount));
  }
}, []);

useEffect 只会在客户端执行,巧妙的是,它执行的时候,Hydration 已经完成。

本篇,我与你讲述了服务端渲染的两个陷阱,愿你在写代码的过程中能够想起它们。下一篇,我会介绍 Next.js 的渲染策略。