React Server Component
npx create-next-app@latest my-rsc-app cd my-rsc-app npm run dev
进入项目后,你会看到 `app/page.tsx` 就是一个服务器组件。你可以直接从数据库获取数据,而无需使用 `useEffect` 或 API 路由:
```tsx
// app/page.tsx (默认就是服务器组件,不要 "use client")
async function getPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
cache: 'no-store', // 模拟动态数据
});
return res.json();
}
export default async function Page() {
const posts = await getPosts();
return (
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
在这个例子中,Page 组件运行在服务器上,fetch 直接在服务器发起,返回的 HTML 流式传输到客户端,浏览器不会下载该组件的任何 JS 代码。
服务器组件 vs 客户端组件:关键区别
在 App Router 架构下,组件树的渲染流程是自上而下的。父组件如果是服务器组件,它可以直接渲染客户端子组件;反过来,客户端组件内不能直接导入服务器组件作为子组件,但可以将服务器组件作为 children 属性传进来。
| 特性 | 服务器组件 | 客户端组件 |
|---|---|---|
| 渲染位置 | 服务器 | 浏览器(或 SSR 阶段也在服务器执行,但保留客户端 JS) |
| 交互能力 | 不支持(无状态、无事件处理) | 支持(useState、useEffect、事件绑定) |
| 打包结果 | 0 KB 客户端 JS | 包含在 bundle 中 |
| 访问后端资源 | 直接访问 | 需要通过 API 或 Server Actions |
| 声明方式 | 默认(或特殊标记?)——无需任何指令 | 在文件顶部声明 "use client" |
| 可以使用的 Hooks | 大部分不可用(除特殊如 use) |
所有标准 React Hooks |
“use client” 指令的作用
在文件最顶部(必须在 import 之前)加入 "use client",就声明了该文件及其所有导入的组件都是客户端组件。这个指令是一个“边界标记”,告诉打包工具和 Next.js:从这个文件往下,需要被浏览器执行。
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
注意:不要在服务器组件中声明客户端指令,否则会导致该组件被降级为客户端组件,失去 RSC 优势。最佳实践是把交互部分单独抽到一个小文件里,用 "use client" 隔离影响范围。
在服务器组件中直接获取数据
RSC 最自然的用法就是把数据获取放在组件内部,服务器组件可以是 async 函数,等待异步数据后直接渲染:
// app/products/page.tsx
import { sql } from '@vercel/postgres';
export default async function ProductsPage() {
const { rows } = await sql`SELECT * FROM products WHERE active = true`;
return (
<div>
{rows.map((product) => (
<article key={product.id}>
<h2>{product.name}</h2>
<p>{product.description}</p>
</article>
))}
</div>
);
}
优势:
- 不需要创建额外的 API 路由,减少样板代码。
- 查询直接在后端执行,避免暴露数据库结构到前端。
- 数据与组件共同驻留在同一代码块中,更易维护。
对于需要重新获取数据的场景(如搜索后刷新),可以结合 Next.js 提供的 revalidatePath 或 revalidateTag 搭配 Server Actions 实现无刷更新。
混合使用:服务器组件包裹客户端组件
最常见的模式:在服务器组件中获取数据,然后把数据作为 props 传递给客户端组件用于展示和交互。
// app/dashboard/page.tsx (服务器组件)
import { UserProfile } from './UserProfile'; // 客户端组件
import { getUser } from '@/lib/data';
export default async function DashboardPage() {
const user = await getUser();
// user 对象包含用户偏好、头像等
return (
<main>
<h1>Welcome, {user.name}</h1>
<UserProfile user={user} />
</main>
);
}
// app/dashboard/UserProfile.tsx
"use client";
export function UserProfile({ user }: { user: any }) {
// 客户端交互:表单、状态、动画等
const [editMode, setEditMode] = useState(false);
return (
<div>
<img src={user.avatar} alt={user.name} />
{editMode ? <EditForm user={user} /> : <button onClick={() => setEditMode(true)}>Edit</button>}
</div>
);
}
注意:user 对象在服务器组件获取,然后传递给客户端组件。这个序列化过程是自动的,但传递的数据必须是可序列化的(不能是函数、class 实例等)。对于复杂的只读数据结构,这完全没问题。
客户端组件中使用服务器组件作为 children
虽然客户端组件不能直接 import 服务器组件,但是可以通过组合(composition)的方式,把服务器组件当作 children 或通过 props 传入:
// app/layout.tsx (服务器组件)
import { ThemeProvider } from './ThemeProvider'; // 客户端组件
export default function RootLayout({ children }: { children: React.ReactNode }) {
// children 可以是服务器组件!
return (
<html>
<body>
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
}
这样 ThemeProvider 内部的交互逻辑在客户端运行,而它包裹的内容(如 page.tsx)保留在服务器渲染,两者互不干扰。
添加交互:Server Actions 与事件处理
服务器组件本身没有事件处理器,但 Next.js 引入了 Server Actions 作为客户端触发服务器逻辑的机制。你可以在服务器组件里定义一个异步函数,然后作为 action 传递给客户端组件或 <form> 元素。
定义 Server Action 必须使用 "use server" 指令:
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function addTodo(formData: FormData) {
const title = formData.get("title") as string;
// 直接写数据库
await db.todo.create({ data: { title } });
revalidatePath("/todos"); // 刷新 UI
}
在服务器组件中引入并使用:
// app/todos/page.tsx
import { addTodo } from '../actions';
import { TodoList } from './TodoList'; // 客户端组件负责展示和交互
export default async function TodosPage() {
const todos = await db.todo.findMany();
return (
<>
<form action={addTodo}>
<input name="title" required />
<button type="submit">Add</button>
</form>
<TodoList todos={todos} />
</>
);
}
这里的 <form> 被发送到客户端时,action 已经指向了服务器函数,用户提交表单时会直接触发服务器端的 addTodo,并通过 revalidatePath 重新渲染当前页面,整个过程无需手动 fetch,保持了高度的内聚性。
错误处理和加载状态
在 RSC + App Router 环境中,你可以使用 error.tsx 和 loading.tsx 文件约定来优雅地处理错误和加载状态,它们会自动包裹同级的路由段。
loading.tsx:在页面(服务器组件)数据获取完成之前显示一个回退 UI。error.tsx:必须是一个客户端组件(因为要使用useEffect记录错误等),用于捕获渲染期间的服务端错误。
更细粒度的控制可以使用 React 的 <Suspense> 组件,将服务器组件中较慢的部分包裹起来,并指定自己的 fallback:
import { Suspense } from "react";
import { SlowDataComponent } from "./SlowDataComponent";
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading data...</div>}>
<SlowDataComponent />
</Suspense>
</div>
);
}