Next.js 面试题详细答案 - Q5
Q5: 什么是 React Server Components (RSC)?它和 Client Components 的根本区别是什么?如何在 Next.js 中定义它们?('use client' 指令)
React Server Components (RSC) 概念
React Server Components 是一种新的 React 组件类型,它们在服务器上渲染,而不是在客户端。这是 React 18+ 和 Next.js 13+ 的重要特性。
RSC vs Client Components 根本区别
1. 渲染位置
// Server Component (默认)
async function BlogPost({ slug }) {
const post = await fetch(`/api/posts/${slug}`)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
)
}
// Client Component (需要 'use client')
;('use client')
function LikeButton({ postId }) {
const [liked, setLiked] = useState(false)
return <button onClick={() => setLiked(!liked)}>{liked ? '❤️' : '🤍'}</button>
}
2. 功能限制对比
| 特性 | Server Components | Client Components |
|---|---|---|
| 渲染位置 | 服务器 | 浏览器 |
| 状态管理 | ❌ 无 useState | ✅ 支持 useState |
| 生命周期 | ❌ 无 useEffect | ✅ 支持 useEffect |
| 事件处理 | ❌ 无 onClick | ✅ 支持 onClick |
| 浏览器 API | ❌ 无 window | ✅ 支持 window |
| 数据获取 | ✅ 直接 fetch | ❌ 需要 useEffect |
| 文件系统 | ✅ 直接访问 | ❌ 无法访问 |
| 数据库 | ✅ 直接连接 | ❌ 需要 API |
详细功能对比
Server Components 特性
// ✅ 可以直接获取数据
async function ProductList() {
const products = await db.products.findMany()
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}
// ✅ 可以直接访问文件系统
import fs from 'fs'
import path from 'path'
async function BlogPost({ slug }) {
const filePath = path.join(process.cwd(), 'posts', `${slug}.md`)
const content = fs.readFileSync(filePath, 'utf8')
return <div>{content}</div>
}
// ✅ 可以直接连接数据库
import { db } from './lib/db'
async function UserProfile({ userId }) {
const user = await db.user.findUnique({
where: { id: userId },
})
return <div>{user.name}</div>
}
// ❌ 不能使用状态
function ServerComponent() {
// const [count, setCount] = useState(0) // 错误!
return <div>静态内容</div>
}
// ❌ 不能使用事件处理
function ServerComponent() {
return <button onClick={() => alert('click')}> // 错误! 点击我</button>
}
Client Components 特性
'use client'
import { useState, useEffect } from 'react'
// ✅ 可以使用状态
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>计数: {count}</button>
}
// ✅ 可以使用生命周期
function DataFetcher() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/data')
.then((res) => res.json())
.then(setData)
}, [])
return <div>{data ? data.message : '加载中...'}</div>
}
// ✅ 可以使用浏览器 API
function WindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 })
useEffect(() => {
const updateSize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
window.addEventListener('resize', updateSize)
updateSize()
return () => window.removeEventListener('resize', updateSize)
}, [])
return (
<div>
窗口大小: {size.width} x {size.height}
</div>
)
}
// ❌ 不能直接访问数据库
function ClientComponent() {
// const user = await db.user.findMany() // 错误!
return <div>需要通过 API 获取数据</div>
}
在 Next.js 中定义组件
1. Server Components (默认)
// app/page.js - 默认是 Server Component
export default function HomePage() {
return <h1>首页</h1>
}
// app/blog/page.js - 异步 Server Component
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts')
return (
<div>
<h1>博客</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
2. Client Components (需要 'use client')
// components/InteractiveButton.js
'use client'
import { useState } from 'react'
export default function InteractiveButton() {
const [clicked, setClicked] = useState(false)
return (
<button onClick={() => setClicked(!clicked)}>
{clicked ? '已点击' : '点击我'}
</button>
)
}
// components/Form.js
'use client'
import { useState } from 'react'
export default function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
})
const handleSubmit = (e) => {
e.preventDefault()
// 处理表单提交
console.log(formData)
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="姓名"
value={formData.name}
onChange={(e) => setFormData({
...formData,
name: e.target.value
})}
/>
<button type="submit">提交</button>
</form>
)
}
混合使用示例
// app/blog/[slug]/page.js - Server Component
import InteractiveButton from '@/components/InteractiveButton'
import LikeButton from '@/components/LikeButton'
export default async function BlogPost({ params }) {
// 在服务器获取数据
const post = await fetch(`/api/posts/${params.slug}`)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* 嵌入 Client Components */}
<div className="actions">
<LikeButton postId={post.id} />
<InteractiveButton />
</div>
</article>
)
}
// components/LikeButton.js - Client Component
'use client'
import { useState } from 'react'
export default function LikeButton({ postId }) {
const [liked, setLiked] = useState(false)
const [count, setCount] = useState(0)
const handleLike = async () => {
const newLiked = !liked
setLiked(newLiked)
setCount(count + (newLiked ? 1 : -1))
// 调用 API 更新点赞状态
await fetch(`/api/posts/${postId}/like`, {
method: 'POST',
body: JSON.stringify({ liked: newLiked })
})
}
return (
<button onClick={handleLike}>
{liked ? '❤️' : '🤍'} {count}
</button>
)
}
组件边界和传递
1. 组件边界
// Server Component 不能直接包含 Client Component 的状态逻辑
// ❌ 错误示例
async function ServerComponent() {
const [state, setState] = useState(0) // 错误!
return <div>{state}</div>
}
// ✅ 正确示例
async function ServerComponent() {
return <ClientComponent />
}
;('use client')
function ClientComponent() {
const [state, setState] = useState(0)
return <div>{state}</div>
}
2. 数据传递
// Server Component 向 Client Component 传递数据
async function BlogPost({ params }) {
const post = await fetch(`/api/posts/${params.slug}`)
return (
<div>
<h1>{post.title}</h1>
{/* 传递服务器数据给客户端组件 */}
<CommentSection postId={post.id} comments={post.comments} />
</div>
)
}
;('use client')
function CommentSection({ postId, comments }) {
const [newComment, setNewComment] = useState('')
return (
<div>
<h3>评论</h3>
{comments.map((comment) => (
<div key={comment.id}>{comment.text}</div>
))}
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
/>
</div>
)
}
最佳实践
1. 组件选择原则
// 优先使用 Server Components
// ✅ 静态内容、数据获取
async function ProductList() {
const products = await db.products.findMany()
return <ProductGrid products={products} />
}
// 必要时使用 Client Components
// ✅ 交互、状态管理
;('use client')
function ProductGrid({ products }) {
const [selectedProduct, setSelectedProduct] = useState(null)
return (
<div>
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
onClick={() => setSelectedProduct(product)}
/>
))}
</div>
)
}
2. 性能优化
// 将 Client Components 放在组件树的叶子节点
async function BlogPost({ params }) {
const post = await fetch(`/api/posts/${params.slug}`)
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* 只在需要交互的地方使用 Client Component */}
<LikeButton postId={post.id} />
</article>
)
}
总结
React Server Components 和 Client Components 的根本区别:
- 渲染位置:服务器 vs 浏览器
- 功能限制:Server Components 无状态和事件,Client Components 无直接数据访问
- 性能影响:Server Components 减少客户端包大小,Client Components 提供交互性
- 使用场景:Server Components 适合数据展示,Client Components 适合用户交互
通过合理使用两种组件类型,可以构建既高性能又交互丰富的应用。