前几节我们了解了布局、页面和导航,本节来深入探讨 Next.js 中最重要的概念之一:服务端组件和客户端组件。理解这两者的区别和使用场景,是掌握 Next.js 的关键。
核心概念
在 Next.js App Router 中,所有组件默认是服务端组件。很多从传统 React 过渡过来的开发者可能会不太习惯,但这个设计能带来很多好处。
服务端组件
服务端组件在服务器上渲染,有几个显著特点:
- 可以直接访问数据库、文件系统等后端资源
- 减少发送到客户端的 JavaScript 代码
- 保持敏感代码安全,不会暴露给客户端
- 不能使用 React Hooks(useState, useEffect 等)
客户端组件
使用 'use client' 指令标记的组件就是客户端组件,它们在浏览器中渲染,特点包括:
- 可以使用 React Hooks(useState, useEffect 等)
- 可以处理浏览器事件(onClick, onChange 等)
- 可以访问浏览器 API(window, localStorage 等)
- 会增加发送到客户端的 JavaScript 代码量
服务端组件详解
基本用法
最简单的服务端组件就是一个普通的函数组件,不需要任何特殊标记:
// app/users/page.tsx - 服务器组件(默认)
async function getUsers() {
const res = await fetch('https://api.example.com/users', {
next: { revalidate: 3600 },
})
return res.json()
}
export default async function UsersPage() {
const users = await getUsers()
return (
<div>
<h1>用户列表</h1>
<ul>
{users.map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
直接访问数据库
服务端组件最大的优势之一就是可以直接访问数据库,不需要创建 API 层:
// app/posts/page.tsx
import { db } from '@/lib/db'
export default async function PostsPage() {
const posts = await db.post.findMany({
orderBy: { createdAt: 'desc' },
take: 10,
})
return (
<div>
<h1>最新文章</h1>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
服务端组件的优势
使用服务端组件有几个明显的好处:
// 1. 减少客户端 JavaScript
// 这个组件不会发送任何 JS 到浏览器
export default function ServerComponent() {
return <div>纯 HTML,无 JS</div>
}
// 2. 保持数据访问安全
// API 密钥不会暴露给客户端
const API_KEY = process.env.SECRET_KEY // 仅服务器可用
export async function SecureComponent() {
const data = await fetch(`https://api.com?key=${API_KEY}`)
return <div>{/* 私密数据 */}</div>
}
// 3. 大依赖包留在服务器
import { BigLibrary } from 'big-library' // 不会发送到客户端
export default function HeavyComponent() {
const result = BigLibrary.process()
return <div>{result}</div>
}
服务端组件的限制
服务端组件有一些限制需要注意:
// 不能使用 React Hooks
import { useState } from 'react' // 错误
export default function ServerComponent() {
const [count, setCount] = useState(0) // 错误!
return <div>{count}</div>
}
// 不能使用浏览器事件
export default function ServerComponent() {
return (
<button onClick={() => console.log('click')}>
点击
</button>
)
}
// 不能使用浏览器 API
export default function ServerComponent() {
const width = window.innerWidth // 错误!
return <div>{width}</div>
}
客户端组件详解
使用 'use client' 指令
如果需要使用 React Hooks 或浏览器 API,需要在文件顶部添加 'use client' 指令:
// app/counter/page.tsx
'use client' // 必须在文件顶部
import { useState } from 'react'
export default function CounterPage() {
const [count, setCount] = useState(0)
return (
<div>
<h1>计数器: {count}</h1>
<button onClick={() => setCount(count + 1)}>增加</button>
<button onClick={() => setCount(count - 1)}>减少</button>
</div>
)
}
使用浏览器 API
客户端组件可以访问浏览器的各种 API:
'use client'
import { useState, useEffect } from 'react'
export default function Geolocation() {
const [location, setLocation] = useState(null)
useEffect(() => {
navigator.geolocation.getCurrentPosition((position) => {
setLocation({
lat: position.coords.latitude,
lng: position.coords.longitude,
})
})
}, [])
return (
<div>
{location ? (
<p>位置: {location.lat}, {location.lng}</p>
) : (
<p>获取位置中...</p>
)}
</div>
)
}
处理用户交互
表单处理这类需要交互的功能必须用客户端组件:
'use client'
import { useState } from 'react'
export default function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: '',
})
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setFormData({
...formData,
[e.target.name]: e.target.value,
})
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(formData),
})
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
/>
<button type="submit">提交</button>
</form>
)
}
混用组件
在实际开发中,我们经常需要混用服务端组件和客户端组件。推荐的做法是:大部分组件用服务端,只有需要交互的部分用客户端。
服务端组件导入客户端组件
这是最常见也是最推荐的模式:
// app/page.tsx - 服务器组件
import InteractiveButton from '@/components/InteractiveButton'
export default function Page() {
return (
<div>
<h1>欢迎</h1>
<InteractiveButton />
</div>
)
}
// components/InteractiveButton.tsx - 客户端组件
'use client'
import { useState } from 'react'
export default function InteractiveButton() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
点击了 {count} 次
</button>
)
}
传递数据给客户端组件
服务端组件获取数据后传递给客户端组件:
// app/posts/[id]/page.tsx - 服务器组件
import CommentSection from '@/components/CommentSection'
export default async function PostPage({ params }: { params: { id: string } }) {
const post = await fetchPost(params.id)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<CommentSection postId={post.id} initialCount={post.commentsCount} />
</article>
)
}
组件边界模式
在实际项目中,推荐采用"叶子客户端组件"的模式,就是把交互性限制在组件树的最底层:
// components/PostList.tsx - 服务器组件
// 整个列表在服务器渲染
import PostCard from './PostCard'
export default async function PostList() {
const posts = await getPosts()
return (
<div>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
)
}
// components/PostCard.tsx - 客户端组件
// 只有交互部分是客户端组件
'use client'
export default function PostCard({ post }: { post: Post }) {
const [liked, setLiked] = useState(false)
return (
<article>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<button onClick={() => setLiked(!liked)}>
{liked ? '已点赞' : '点赞'}
</button>
</article>
)
}
决策指南
很多初学者会困惑,什么时候用服务端组件,什么时候用客户端组件?这里有个简单的判断标准:
使用服务端组件的场景:
- 需要获取数据
- 需要直接访问后端资源(数据库、文件系统)
- 需要保持敏感信息安全
- 希望减少客户端 JavaScript
- 需要提升性能(大依赖包)
- 需要 SEO 优化
使用客户端组件的场景:
- 需要使用 React Hooks(useState, useEffect 等)
- 需要处理浏览器事件(onClick, onChange 等)
- 需要访问浏览器 API(window, localStorage 等)
- 需要状态管理
- 需要使用第三方库(需要浏览器环境)
实用建议
这里分享几个在实际项目中特别有用的技巧。
默认使用服务端组件
实际开发中,除非有明确的交互需求,否则建议默认使用服务端组件。这个简单的小习惯能让你的应用性能提升不少:
// 推荐这样做 - 默认使用服务端组件
export default function Component() {
return <div>内容</div>
}
// 除非真的需要交互,才添加 'use client'
'use client'
export default function Component() {
return <div>内容</div>
}
将交互性推向边缘
这里有个小建议:尽量把交互性限制在组件树的最底层。这个技巧在实际开发中特别有用:
// 这样写更好 - 只有按钮是客户端组件
export default function Page() {
return (
<div>
<h1>标题</h1>
<p>内容</p>
<LikeButton />
</div>
)
}
// 避免这种情况 - 整个页面都变成客户端组件
'use client'
export default function Page() {
return (
<div>
<h1>标题</h1>
<p>内容</p>
<LikeButton />
</div>
)
}
总结
本节我们深入了解了服务端组件和客户端组件的区别、使用场景以及最佳实践。掌握好这两种组件的使用,能让你的应用性能更好、代码更简洁。记住一个原则:能默认用服务端就用服务端,只在需要交互时才用客户端。
如果你对本节内容有任何疑问,欢迎在评论区提出来,我们一起学习讨论。