在 Next.js 13+ 中,最重要的变化之一就是全面拥抱了 React Server Components(RSC)。这个特性彻底改变了我们构建 React 应用的方式,让我们可以在服务器端运行 React 组件,直接访问服务器资源,而无需通过 API 路由。 理解服务器组件和客户端组件的区别,是掌握 Next.js 开发的关键。

传统的 React 应用是客户端应用。所有的组件都在浏览器中运行,通过 JavaScript 来渲染和更新 UI。这种模式有很多优点,比如交互性强、用户体验好,但也有一些缺点,比如首屏加载慢、SEO 不友好、需要额外的 API 层来获取数据。
React Server Components 引入了一种新的组件类型:可以在服务器端运行的组件。这些组件可以直接访问数据库、文件系统等服务器资源,无需通过 API。它们只在服务器端运行,不会将 JavaScript 发送到客户端,这大大减少了客户端包的大小。
React Server Components 是 React 18+ 的实验性特性,Next.js 13+ 将其作为默认模式。在 Next.js 中,所有组件默认都是服务器组件,除非你明确标记为客户端组件。
让我们通过一个简单的例子来理解这个概念。在传统的 React 应用中,如果我们想显示数据库中的数据,我们需要:
fetch 或 useEffect 来调用 API而在 Next.js 中,使用服务器组件,我们可以直接这样做:
|// app/products/page.tsx async function getProducts() { // 直接访问数据库,无需 API const db = await connectToDatabase(); return await db.query('SELECT * FROM products'); } export default async function ProductsPage() { const products = await getProducts(); return ( <div> <h1>产品列表</h1> <ul> {products.map((product) => ( <li key={product.id}>{product.name}</li> ))} </ul> </div> ); }
这个组件在服务器端运行,直接访问数据库,然后将渲染好的 HTML 发送给客户端。客户端不需要下载任何 JavaScript 来显示这个列表,这大大提升了性能。
window、document、localStorage 等。它们也不能使用 React hooks,比如 useState、useEffect、useContext 等,因为这些 hooks 需要在客户端运行。async/await 来等待异步操作,比如数据库查询、API 调用等。让我们看一个更复杂的例子:
|// app/blog/[slug]/page.tsx async function getPost(slug: string) { const response = await fetch(`https://api.example.com/posts/${slug}`, { cache: 'no-store', // 不缓存,每次都获取最新数据 }); if (!response.ok) { throw new Error('Failed to fetch post'); } return response.
在这个例子中,我们做了两件重要的事情。首先,我们使用 fetch 从外部 API 获取博客文章。注意我们使用了 cache: 'no-store' 选项,这告诉 Next.js 不要缓存这个请求,每次都获取最新数据。
然后,我们直接从数据库获取评论。这两个操作都在服务器端完成,客户端只会收到渲染好的 HTML。
虽然服务器组件很强大,但并不是所有场景都适合使用服务器组件。当我们需要交互性、浏览器 API 或 React hooks 时,我们需要使用客户端组件。
客户端组件通过在文件顶部添加 'use client' 指令来标记。这个指令告诉 Next.js 这个组件及其所有子组件都应该在客户端运行。
让我们看一个需要客户端组件的例子:
|// app/components/Counter.tsx 'use client'; import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>计数: {count}</p> <button onClick=
这个组件使用了 useState hook 来管理状态,并使用了 onClick 事件处理器,这些都需要在客户端运行。所以我们需要添加 'use client' 指令。
让我们逐步理解这个组件。首先,我们在文件顶部添加了 'use client' 指令。这个指令必须放在文件的最顶部,在任何导入语句之前。
然后,我们导入了 useState hook。这个 hook 只能在客户端组件中使用。
接下来,我们创建了一个 Counter 组件,它使用 useState 来维护一个计数器状态。useState 返回一个数组,第一个元素是当前值,第二个元素是更新函数。
最终,我们渲染了计数器的值和两个按钮。当用户点击按钮时,我们调用 setCount 来更新状态,这会触发组件重新渲染。
在实际项目中,我们通常需要组合使用服务器组件和客户端组件。服务器组件用于获取数据和渲染静态内容,客户端组件用于处理交互。 让我们看一个例子,展示如何组合使用它们:
|// app/products/page.tsx (服务器组件) async function getProducts() { const db = await connectToDatabase(); return await db.query('SELECT * FROM products'); } export default async function ProductsPage() { const products = await getProducts(); return ( <div> <
|// app/components/ProductList.tsx (客户端组件) 'use client'; import { useState } from 'react'; export default function ProductList({ products }: { products: Product[] }) { const [filter, setFilter] = useState(''); const filteredProducts = products.filter((product
在这个例子中,ProductsPage 是一个服务器组件,它从数据库获取产品数据。然后,它将数据传递给 ProductList,这是一个客户端组件,用于处理搜索过滤功能。
这种组合模式非常强大。服务器组件负责数据获取,客户端组件负责交互,两者各司其职,共同构建出既高效又交互性强的应用。
理解服务器组件和客户端组件之间的边界很重要。当我们从服务器组件传递 props 给客户端组件时,这些 props 必须是可序列化的。这意味着我们不能传递函数、类实例、Symbol 等不可序列化的值。
让我们看一个错误的例子:
|// app/page.tsx (服务器组件) export default function HomePage() { const handleClick = () => { console.log('clicked'); }; return <Button onClick={handleClick} />; // 错误!不能传递函数 }
这个例子是错误的,因为 handleClick 是一个函数,不能从服务器组件传递给客户端组件。
正确的做法是在客户端组件内部定义事件处理器:
|// app/components/Button.tsx (客户端组件) 'use client'; export default function Button({ label }: { label: string }) { const handleClick = () => { console.log('clicked'); }; return <button onClick={handleClick}>{label}</button>; }
|// app/page.tsx (服务器组件) import Button from './components/Button'; export default function HomePage() { return <Button label="点击我" />; }
在这个正确的例子中,事件处理器在客户端组件内部定义,服务器组件只传递可序列化的数据(字符串)。
有时候,我们需要在客户端组件中使用服务器获取的数据,但又需要保持交互性。有几种方法可以实现这个需求。
第一种方法是使用 Server Actions,我们会在后面的课程中详细讲解。Server Actions 允许我们在客户端组件中调用服务器函数。
第二种方法是使用客户端数据获取。我们可以在客户端组件中使用 useEffect 和 fetch 来获取数据:
|// app/components/ProductList.tsx 'use client'; import { useEffect, useState } from 'react'; export default function ProductList() { const [products, setProducts] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { fetch
这种方法适用于需要实时更新或需要客户端交互的数据。但要注意,这种方法会增加客户端 JavaScript 的大小,并且需要额外的 API 路由。
客户端组件可以访问所有浏览器 API。让我们看几个常见的例子。
第一个例子是使用 localStorage:
|// app/components/ThemeToggle.tsx 'use client'; import { useEffect, useState } from 'react'; export default function ThemeToggle() { const [theme, setTheme] = useState('light'); useEffect(() => { const savedTheme = localStorage.getItem('theme') || 'light'
这个组件使用 localStorage 来保存用户的主题偏好,并在组件挂载时恢复它。
第二个例子是使用 window 对象:
|// app/components/WindowSize.tsx 'use client'; import { useEffect, useState } from 'react'; export default function WindowSize() { const [size, setSize] = useState({ width: 0, height: 0 }); useEffect(() => { const updateSize = () => { setSize({
这个组件监听窗口大小变化,并实时显示当前窗口尺寸。
使用服务器组件和客户端组件的组合可以带来显著的性能提升。服务器组件减少了客户端 JavaScript 的大小,提升了首屏加载速度。客户端组件只在需要交互的地方使用,保持了应用的交互性。
但要注意,过度使用客户端组件会抵消服务器组件的优势。我们应该遵循一个原则:默认使用服务器组件,只在需要交互、浏览器 API 或 React hooks 时才使用客户端组件。
让我们看一个例子,展示如何优化组件结构:
|// 不好的做法:整个页面都是客户端组件 'use client'; import { useState, useEffect } from 'react'; export default function ProductsPage() { const [products, setProducts] = useState([]); useEffect(() => { fetch('/api/products') .then((res) => res.json
这个例子将整个页面都标记为客户端组件,这会导致所有代码都发送到客户端,增加了包大小。 更好的做法是:
|// 好的做法:服务器组件获取数据,客户端组件处理交互 // app/products/page.tsx async function getProducts() { const db = await connectToDatabase(); return await db.query('SELECT * FROM products'); } export default async function ProductsPage() { const products = await getProducts(); return ( <div>
|// app/components/ProductList.tsx 'use client'; export default function ProductList({ products }: { products: Product[] }) { // 只处理交互逻辑 return ( <ul> {products.map((product) => ( <li key={product.id}>{product.name}</li> ))} </ul
在这个优化的例子中,数据获取在服务器端完成,只有必要的交互逻辑在客户端运行。
在这一部分中,我们探索了服务器组件和客户端组件的区别和使用场景。我们学习了服务器组件的强大功能,了解了何时需要使用客户端组件,以及如何组合使用它们来构建既高效又交互性强的应用。 通过合理使用这两种组件类型,我们可以创建性能优异、用户体验良好的应用。
在下一节课中,我们将学习数据获取和渲染策略,了解 Next.js 提供的各种数据获取方法,以及如何选择合适的渲染策略来优化应用性能。