【转】关于 React 服务器组件的 5 个误解


随着 React 19 Next.js 15 稳定版本的临近,React 团队引入了React架构中最重要的变化之一:React 服务器组件(RSC)范式,该范式区分了服务器组件和客户端组件。

服务器组件仅在服务器上运行,对包大小没有任何影响。它们的代码从不会被下载到客户端,有助于减少包的大小并提高启动时间。

相比之下,客户端组件是你已经习惯的典型组件。它们可以访问 React 的全部功能:状态、effectsDOM 访问等。

这种新方法旨在利用服务器和客户端环境的优势,优化效率、加载时间和交互性。

虽然它给 React 开发带来了显著的改进和灵活性,但也引入了容易被误解的概念。在本文中,我们将解决关于 React 服务器组件(RSC)的五个常见误解,以帮助澄清这种新架构的一些细微之处。

误解 1:服务器组件应该优先使用,而客户端组件应该尽量少用

你可能认为,为了最大化利用新的 RSC 架构,你应该将所有组件转换为服务器组件,并尽量少用客户端组件。然而,最佳方法实际上更加平衡:

虽然服务器组件提供了许多好处,但它们并不适用于所有场景,尤其是那些需要交互的场景。React 服务器组件是一种新的 React 组件类型,仅在服务器上运行,获取数据并完全在服务器上渲染以提高性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// UserProfile.js (Server Component)

// This component can directly access the database or API
// without exposing sensitive information to the client
async function UserProfile({ userId }) {
const response = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
const user = response.user;

return (
<div>
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
<p>Location: {user.location}</p>
<LikeButton initialLikes={user.likes} />
</div>
);
}

客户端组件对于需要客户端交互、状态管理或浏览器 API 的功能至关重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// LikeButton.js (Client Component)

// This component can handle interactivity
'use client'

import { useState } from 'react';

function LikeButton({ initialLikes }) {
const [likes, setLikes] = useState(initialLikes);

const handleLike = () => {
setLikes(likes + 1);
};

return (
<button onClick={handleLike}>
Like ({likes})
</button>
);
}

推荐的做法是将客户端组件尽可能低地放置在组件树中,理想情况下使它们成为叶组件。

服务器组件的引入并不意味着它们本质上优于客户端组件或应始终优先使用。两者在精心架构的 React 应用中都有其位置。客户端组件对于创建交互式、动态用户界面仍然至关重要,在适当的地方使用它们不仅可以接受,而且通常是必要的。

记住,这种新架构的目标是提供更多工具来构建高效的 React 应用,而不是完全取代客户端组件。在合适的地方使用服务器组件,不要犹豫在需要时使用客户端组件。

误解 2:use client 标记组件为客户端组件,use server 标记组件为服务器组件

你可能认为我们声明服务器组件和客户端组件的方式是对称的。新的架构旨在支持 React 服务器组件,使它们仅在服务器上运行并利用服务器端渲染的优势。实际情况有些不同:

  • use client 确实标记了客户端组件的边界。
1
2
3
4
5
6
// LikeButton.js - 'use client' lets you mark what code runs on the client
'use client'

function LikeButton({ initialLikes }) {
// Rest of the component code
}

然而,没有对应的 use server 指令来标记服务器组件。在 Next.js App Router React 服务器组件范式中,服务器组件是默认的。

1
2
3
4
5
// UserProfile.js - Server Component by default

async function UserProfile({ userId }) {
// Rest of the component code
}

这种区别根本上影响了你构建和组织 React 应用的方式。通过将服务器组件设为默认,React 鼓励“服务器优先”的方法,使你无需额外努力即可利用服务器端渲染的优势。你只需显式标记客户端组件,这有助于快速识别需要客户端交互的部分。这种方法可以改善初始加载时间并减少客户端 JavaScript,优化应用性能。

值得注意的是,确实存在一个 use server 指令,但它的用途不同。它用于标记服务器操作,而不是声明服务器组件。

误解 3:服务器组件总是在服务器上渲染,客户端组件总是在浏览器中渲染

乍一看,你可能认为服务器组件和客户端组件的渲染位置有明确的分界线。传统的 React 组件通常在客户端渲染,但随着服务器组件和客户端组件的引入,渲染过程变得更加复杂:

  • React 服务器组件架构中,服务器组件确实在服务器上渲染。
  • 然而,客户端组件比许多人想象的更具灵活性。它们可以在服务器(用于初始页面加载)和客户端渲染。
  • 关键区别在于它们的功能:客户端组件可以使用客户端功能,例如状态、effects 和事件处理程序,而服务器组件不能。

依赖仅客户端 JavaScript 渲染网页可能导致显著的缺点。用户最初会看到一个空白页面,直到浏览器下载、处理并运行JavaScript代码,导致内容可见性延迟。这种延迟可能会使网站感觉缓慢和无响应,特别是在较慢的网络或性能较差的设备上。此外,这可能会阻碍搜索引擎的索引,可能影响网站在搜索结果中的可见性。

为了优化这一点,你可以在服务器上渲染客户端组件以进行初始页面加载。这种预渲染的 HTML 允许用户立即看到内容,而React在后台工作,使组件完全交互,而不会对用户产生任何可见的变化。这种方法显著改善了感知加载速度和应用程序的实际性能。

误解 4:每个具有交互性的组件都需要自己的 use client 指令

你可能认为每个具有任何客户端行为的组件都需要自己的 use client 指令。然而,事实并非如此:

  • 你只需在需要客户端功能的最上层组件文件顶部添加 use client
  • 该文件中定义的所有组件自动成为客户端组件。
  • 这包括文件中的嵌套组件、实用程序和函数。

在下面的组件树中,FormatText.js 原本是一个服务器组件,当它被导入到 Dashboard.js(由 use client 指令标记的客户端组件)时,它在客户端执行。(白色表示服务器组件,蓝色表示客户端组件。)

理解这一点为什么重要?

由于 JavaScript 模块可以在服务器组件和客户端组件之间共享,因此用于服务器的代码可能会在客户端运行。

考虑这个数据获取函数:

1
2
3
4
5
6
7
8
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}

虽然它看起来可以在服务器和客户端上工作,但它包含了一个仅用于服务器端的 API_KEY。如果这个函数被导入到客户端组件中,可能会暴露敏感信息。通过将敏感数据和逻辑(如令牌和 API 密钥)保留在服务器上,服务器组件增强了应用程序的安全性。

Next.js 在客户端用空字符串替换私有环境变量(那些没有以 NEXT_PUBLIC 前缀开头的变量)以防止泄漏。这意味着如果在客户端执行,getData() 将无法按预期工作。

理解这些细微差别有助于你更安全高效地构建应用程序。通过注意代码运行的位置,你可以防止敏感信息泄露,并确保组件无论在服务器还是客户端运行,都能按预期行为工作。

误解 5:服务器组件不能嵌套在客户端组件内

你可能认为,一旦进入客户端组件的领域,就不能回头,因此服务器组件不能嵌套在客户端组件内。但实际上有比你预期更多的灵活性:

虽然你不能直接将服务器组件嵌套在客户端组件内,但你可以将服务器组件作为 props 传递给客户端组件。这种方法允许客户端和服务器组件的无缝集成,优化性能和交互性。

一种常见的模式被称为“将子组件作为 props 模式”。这里是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ServerComponent.js
function ServerComponent() {
return <h1>Hello from the server</h1>;
}

// ClientComponent.js
'use client'
function ClientComponent({ children }) {
return (
<div>
<p>This is a client component</p>
{children}
</div>
);
}

// Usage
function App() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
);
}

这种灵活性允许你通过在服务器上保持数据获取和繁重的计算,同时在需要的地方提供交互性,从而创建更高效的应用程序。这就像拥有两全其美的优势——服务器端操作的强大能力与客户端交互的响应性。

React 服务器组件的引入并不意味着你需要一夜之间彻底改变构建 React 应用的方式。相反,将其视为开发者工具包中的一个强大新工具,你可以逐步采用和实验,以优化你的 React 应用程序。