像素

React server component

React server component 允许服务器和客户端(浏览器)协作渲染 React 应用。想象一棵典型的 React 组件树,它通常由不同的 React 组件构成,并且可以产生更多的 React 组件。RSC 使该树中的某些组件可以由服务器渲染,某些组件可以由浏览器渲染 。

RSC 的优势

服务端组件可以更好地利用服务器基础设施。比如将数据获取移动到服务器,更接近数据库,并保留之前会影响服务器上客户端 JavaScript 捆绑包大小的大量依赖项,从而提高性能。

在 next 中的使用

在 next 中使用 App Router 时默认使用 RSC。 可以在文件上方使用 “use client” 声明客户端组件, 一旦在文件中定义了"use client"导入其中的所有其他模块,包括子组件,将被视为客户端捆绑包的一部分。

"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

什么时候使用 RSC、客户端组件

这是图片

编写客户端组件和服务器组件

服务器和客户端组件可以组合在同一组件树中。 在幕后,React 处理渲染如下:

  • 在服务器上,React 在将结果发送给客户端之前渲染所有服务器组件。这包括嵌套在客户端组件中的服务器组件。在此阶段遇到的客户端组件被跳过。
  • 在客户端上,React 在服务器组件的渲染结果中呈现客户端组件和插槽,合并在服务器和客户端上完成的工作。
  • 如果任何服务器组件嵌套在客户端组件中,其渲染的内容将被正确放置在客户端组件中。

客户端组件无法导入服务器组件

因为服务器组件不能在浏览器中运行,并且可能有无法在浏览器中运行的代码。

"use client";

// This pattern will **not** work!
// You cannot import a Server Component into a Client Component.
import ExampleServerComponent from "./example-server-component";

export default function ExampleClientComponent({
  children,
}: {
  children: React.ReactNode;
}) {
  const [count, setCount] = useState(0);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>

      <ExampleServerComponent />
    </>
  );
}

可以使用 props 来标记服务器组件的“孔”。 服务器组件将在服务器上呈现,当客户端组件在客户端上呈现时*,“孔” *将用服务器组件的渲染结果进行填充。 比如使用 children 创建 “_孔_”,达到嵌套的目的。

"use client";

import { useState } from "react";

export default function ExampleClientComponent({
  children,
}: {
  children: React.ReactNode;
}) {
  const [count, setCount] = useState(0);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>

      {children}
    </>
  );
}

Context 的使用

不能在 RSC 中直接创建或使用 Context。这是因为服务器组件没有 React 状态(因为它们不是交互式的),并且上下文主要用于在更新某些 React 状态后在树的深处重新渲染交互式组件。 这样会报错:

import { createContext } from "react";

//  createContext is not supported in Server Components
export const ThemeContext = createContext({});

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
      </body>
    </html>
  );
}

解决方案:在客户端组件中创建上下文

"use client";

import { createContext } from "react";

export const ThemeContext = createContext({});

export default function ThemeProvider({ children }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}

改造后的使用

import { Providers } from "./providers";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

RSC 的渲染过程

1. 服务器接受请求并渲染

使用 RSC 的页面的生命周期总是从服务器开始,以响应一些用于渲染 React 组件的 API 调用。“根”组件始终是一个服务器组件,它可以渲染其他服务器组件或客户端组件。服务器根据请求传递的信息确定要使用的服务器组件和 props 。

2. 服务器将根组件序列化为 JSON

这里的最终目标是将初始根服务器组件渲染为一棵包含基本 html 标签和客户端组件“占位符”的树。然后序列化这棵树,将其发送到浏览器,浏览器可以对其进行反序列化,用真正的客户端组件填充客户端占位符,并渲染出最终结果。 React 元素实际上是一个对象,其 type 字段要么是字符串,要么是基本 html 标签,或者是一个函数、一个 React 组件的实例。但是函数不可序列化为 JSON 的。 为了正确地将 JSON 字符串化,React 将一个特殊的替换函数传递给 JSON.stringify() ,它能正确地处理这些组件函数的引用。 具体来说,每当它看到要序列化的 React 元素时,

  • 如果是一个基本 html 标签(type 字段是一个类似 div 的字符串)什么也不做
  • 如果是一个服务器组件,则使用其 props 调用服务器组件函数(存储在 type 字段中),并序列化调用结果。这实际上“渲染”了服务器组件;此处的目标是将所有服务器组件转换为基本 html 标签。

同样的针对客户端组件,利用构建工具也进行了处理 当服务器组件引入客户端组件时,它只会得到一个**模块引用对象,包含文件名和导出名。在服务器上构建的 React 树中从来没有客户端组件函数 (React 团队已经在 react-server-dom-webpack 中发布了对 webpack 的官方 RSC 支持(webpack loadernode-register)** 因为要将整个 React 树序列化为 JSON,所以传递给客户端组件和基本 html 标签的所有 props 也必须是可序列化的。这意味着,从服务器组件中,不能将事件处理程序作为 prop 传递下去!

// NOT OK: server components cannot pass functions as a prop
// to its descendents, because functions are not serializable.
function SomeServerComponent() {
  return <button onClick={() => alert("OHHAI")}>Click me!</button>;
}

3. 浏览器重建 React 树

浏览器接收服务器输出的 JSON,并开始重建要在浏览器中渲染的 React 树。每当遇到 type 是模块引用的元素时,我们都希望用对实际客户端组件函数的引用来替换它。 这项工作再次需要构建工具的帮助;构建工具在服务器上用模块引用替换了客户端组件函数,它也知道如何在浏览器中用真正的客户端组件函数替换这些模块引用。 最后我们像往常一样渲染并提交这棵树到 DOM 即可。

RSC 的输出格式

这是一种简单的格式,每行上有一个 JSON blob,用 ID 标记。

M1:{"id":"./src/ClientComponent.client.js","chunks":\["client1"\],"name":""}
J0:\["$","@1",null,{"children":\["$","span",null,{"children":"Hello from server land"}\]}\]

上面的片段中,以 M 开头的行定义了一个客户端组件模块引用,以及在客户端包中查找组件函数所需的信息。以 J 开头的一行定义了一个 React 元素树,比如 @1 引用了 M 行定义的客户端组件。

为什么不直接输出 HTML?

客户端的目标是重建 React 元素树,通过这种格式比通过 html 更容易实现这一点,在 html 中,必须解析 HTML 来创建 React 元素。注意,React 元素树的重建非常重要,因为这允许对 React 树的后续更改合并成对 DOM 的最小提交。