服务端与客户端组件
App Router 里,布局和页面默认都是 服务端组件(Server Components)。它们在服务器上运行,可以直接访问数据库、使用密钥,并把极少量 JavaScript 发到浏览器。需要交互时,你再加入 客户端组件(Client Components)。
何时选哪一种?
选服务端组件:
- 靠近数据源获取数据(数据库、API)。
- 使用 API Key、Token 等不能暴露给客户端的机密。
- 减少发到浏览器的 JS,优化 FCP。
- 渐进式流式输出。
选客户端组件:
- 需要
useState、useEffect等 hooks。 - 需要浏览器 API(
localStorage、window、Navigator.geolocation)。 - 监听
onClick、onChange等事件。
在 Next.js 里,它们如何运作?
服务器侧:
- 服务端组件被渲染为一种特殊的数据格式:RSC Payload。
- 它与客户端组件的占位符一起,在服务端拼出首屏 HTML。
客户端首次加载:
- HTML 立刻呈现一个非交互预览。
- RSC Payload 协调服务端/客户端组件树。
- JavaScript 加载并 水合 客户端组件,让页面变得可交互。
后续导航:
- RSC Payload 被预取并缓存,切换瞬间完成。
- 客户端组件完全在浏览器里渲染。
写一个客户端组件
在文件顶部加 'use client' 指令:
'use client';
import { useState } from 'react';
export default function Counter() {
const [n, setN] = useState(0);
return (
<div>
<p>{n} 次点击</p>
<button onClick={() => setN(n + 1)}>点我</button>
</div>
);
}
'use client' 声明了一道分界线:从该文件开始,它所有的 import 都属于客户端 bundle,无需每个文件都重复写指令。
减小 JS 体积的诀窍
把 'use client' 放在尽可能小的叶子组件上,而不是整块页面。例如:
// 布局:服务端组件
import Search from './search'; // 客户端组件
import Logo from './logo'; // 服务端组件
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<Search />
</nav>
<main>{children}</main>
</>
);
}
整个布局依然是服务端组件,只有 Search 是客户端 JS。
服务端传递数据到客户端
直接用 props 即可(注意 props 必须可序列化):
// 服务端组件
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const post = await getPost(id);
return <LikeButton likes={post.likes} />;
}
// 客户端组件
'use client';
export default function LikeButton({ likes }: { likes: number }) {
// ...
}
在客户端组件里嵌套服务端组件
客户端组件不能直接 import 服务端组件,但可以通过 children 插槽 把它传进来:
// 客户端组件:Modal
'use client';
export default function Modal({ children }: { children: React.ReactNode }) {
return <div className="modal">{children}</div>;
}
// 服务端组件:Page
import Modal from './ui/modal';
import Cart from './ui/cart'; // 服务端组件
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
);
}
这种「客户端外壳 + 服务端内容」的组合非常常见。
Context Provider
React Context 不被服务端组件支持。把 Provider 封装成客户端组件,在根布局里使用:
'use client';
import { createContext } from 'react';
export const ThemeContext = createContext({});
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}
防止「环境污染」
服务器代码可能意外 import 到客户端。用 server-only 包在构建期抛出错误:
import 'server-only';
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: { authorization: process.env.API_KEY },
});
return res.json();
}
对应地也有 client-only 用于标记浏览器专属的模块。