概述
在 Next.js(尤其是 App Router 体系)里,客户端组件(Client Component)和服务端组件(Server Component) 是基于 React 的 RSC(React Server Components)模型实现的。
- 服务端组件(Server Component)默认执行在服务端(Node.js),用于数据获取、减少 bundle
- 客户端组件(Client Component)运行在浏览器,用于交互、状态、事件
- 二者通过 序列化协议(Flight) 进行通信
| 能力 | Server | Client |
|---|---|---|
| 访问数据库 | ✅ | ❌ |
| 使用 useState | ❌ | ✅ |
| 使用 useEffect | ❌ | ✅ |
| 绑定事件 onClick | ❌ | ✅ |
| 减少 bundle 体积 | ✅ | ❌ |
tsx
// 默认就是 Server Component
export default function Page() {
return <div>Hello</div>;
}
tsx
// 显式声明 Client Component
"use client";
export default function Button() {
return <button onClick={() => alert(1)}>Click</button>;
}
"use client" 是一个 编译时指令(不是运行时)
Server Component 深入
为什么默认是 Server?
核心目标:减少前端 bundle + 提升首屏性能
tsx
// 直接在服务端查数据
async function Page() {
const data = await fetch("https://api.com/user");
return <div>{data.name}</div>;
}
- ❌ 不需要把 fetch 逻辑打包到浏览器
- ❌ 不需要 useEffect + loading
- ✅ SSR 天然支持
可以直接访问后端资源
ts
import db from "@/lib/db";
export default async function Page() {
const users = await db.user.findMany();
return <div>{users.length}</div>;
}
- Prisma
- 内部 RPC(BFF)
- GraphQL
不支持交互
tsx
// ❌ 错误
export default function Page() {
return <button onClick={() => {}}>Click</button>;
}
- Server Component 不会在浏览器执行
- 没有事件系统
Client Component 深入
只要涉及以下任意一点,就需要使用客户端组件:
- useState
- useEffect
- DOM 操作
- 事件(onClick)
- 浏览器 API(localStorage)
tsx
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
- 会被打进 JS bundle
- 会增加 hydration 成本
- 会影响 TTI
所以 能不用 client 就不用
核心机制
Server → Client 的传输原理(Flight)
Server Component 渲染后不会直接输出 HTML,而是:
输出一种特殊 JSON(React Flight)
示意:
json
[
["div", null, "Hello"],
["$L1", "Button"]
]
客户端再:
- 解析结构
- 加载对应 Client Component
- Hydration
限制:只能传"可序列化数据"
tsx
// ❌ 错误
<ClientComp fn={() => {}} />
tsx
// ✅ 正确
<ClientComp count={1} />
因为需要跨网络传输
组合模式
推荐结构:Server + Client 嵌套
tsx
// Server Component
import Button from "./Button";
export default async function Page() {
const data = await fetchData();
return (
<div>
<h1>{data.title}</h1>
<Button />
</div>
);
}
tsx
// Client Component
"use client";
export default function Button() {
return <button>Click</button>;
}
注意: Client 不能 import Server
tsx
// ❌ 错误
"use client";
import ServerComp from "./ServerComp";
tsx
// ✅ Server 可以 import Client