🙋 Server Components 是 App Router 的核心特性。组件在服务端执行意味着什么?能带来哪些优势?
什么是 Server Components#
React Server Components (RSC) 是一种新的组件类型,在服务器上执行并渲染,而不是在浏览器中。
核心特点#
| 特性 | Server Components | Client Components |
|---|---|---|
| 执行环境 | 服务器 | 浏览器 |
| JavaScript | 不发送到客户端 | 打包发送到客户端 |
| 数据获取 | 直接访问后端资源 | 通过 API 获取 |
| 状态/交互 | ❌ 不支持 | ✅ 支持 |
| 生命周期 | ❌ 不支持 | ✅ 支持 |
默认行为#
在 App Router 中,组件默认是 Server Components:
// app/page.tsx - 这是 Server Component// Next.js 15.x
export default function HomePage() { console.log('这行日志在服务器终端输出')
return <h1>Hello from Server</h1>}Server Components 优势#
1. 减少客户端 JavaScript#
// Next.js 15.x
// 这些依赖不会发送到客户端import { marked } from 'marked'import hljs from 'highlight.js'
async function getContent() { const res = await fetch('https://api.example.com/content') return res.json()}
export default async function Page() { const content = await getContent()
// 在服务端处理 Markdown const html = marked(content.markdown, { highlight: (code, lang) => hljs.highlight(code, { language: lang }).value, })
return <article dangerouslySetInnerHTML={{ __html: html }} />}🎯 效果:marked 和 highlight.js 的代码不会打包到客户端,减少 bundle 体积。
2. 直接访问后端资源#
// Next.js 15.ximport { db } from '@/lib/db'
export default async function UsersPage() { // 直接查询数据库 const users = await db.user.findMany({ select: { id: true, name: true, email: true }, })
return ( <ul> {users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> )}// Next.js 15.ximport { readFile } from 'fs/promises'import { join } from 'path'
export default async function FilesPage() { // 直接读取文件系统 const content = await readFile( join(process.cwd(), 'data', 'config.json'), 'utf-8' ) const config = JSON.parse(content)
return <pre>{JSON.stringify(config, null, 2)}</pre>}3. 敏感信息安全#
// Next.js 15.x
export default async function AdminPage() { // API 密钥不会暴露给客户端 const response = await fetch('https://api.stripe.com/v1/charges', { headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`, }, })
const charges = await response.json()
return <div>{/* 显示数据 */}</div>}4. 更好的初始加载#
// Server Component 的 HTML 可以立即显示// 不需要等待 JavaScript 加载和执行
export default async function Page() { const data = await getData()
// 用户立即看到完整 HTML return <div>{data.content}</div>}异步数据获取#
Server Components 可以是异步函数:
基础用法#
// Next.js 15.x
interface Post { id: number title: string body: string}
async function getPosts(): Promise<Post[]> { const res = await fetch('https://jsonplaceholder.typicode.com/posts')
if (!res.ok) { throw new Error('Failed to fetch posts') }
return res.json()}
export default async function PostsPage() { const posts = await getPosts()
return ( <ul className="space-y-4"> {posts.slice(0, 10).map((post) => ( <li key={post.id} className="border-b pb-4"> <h2 className="font-bold">{post.title}</h2> <p className="text-gray-600">{post.body}</p> </li> ))} </ul> )}并行数据获取#
// Next.js 15.x
async function getUser() { const res = await fetch('https://api.example.com/user') return res.json()}
async function getStats() { const res = await fetch('https://api.example.com/stats') return res.json()}
async function getNotifications() { const res = await fetch('https://api.example.com/notifications') return res.json()}
export default async function DashboardPage() { // 并行获取数据 const [user, stats, notifications] = await Promise.all([ getUser(), getStats(), getNotifications(), ])
return ( <div> <h1>欢迎, {user.name}</h1> <div>访问量: {stats.visits}</div> <div>通知: {notifications.length} 条</div> </div> )}瀑布流避免#
// ❌ 瀑布流:顺序请求,效率低async function Page() { const user = await getUser() // 1s const posts = await getPosts() // 1s const comments = await getComments() // 1s // 总计 3s}
// ✅ 并行:同时请求async function Page() { const [user, posts, comments] = await Promise.all([ getUser(), getPosts(), getComments(), ]) // 总计 ~1s}组件组合#
Server 包含 Server#
// Next.js 15.x
import { Header } from '@/components/Header'import { Footer } from '@/components/Footer'
// Server Component 可以导入其他 Server Componentexport default function Layout({ children }: { children: React.ReactNode }) { return ( <> <Header /> {/* Server Component */} {children} <Footer /> {/* Server Component */} </> )}Server 包含 Client#
// app/page.tsx - Server Component// Next.js 15.x
import { InteractiveButton } from '@/components/InteractiveButton'
async function getData() { return { message: 'Hello' }}
export default async function Page() { const data = await getData()
return ( <div> <h1>{data.message}</h1> {/* Client Component 作为子组件 */} <InteractiveButton /> </div> )}// components/InteractiveButton.tsx - Client Component'use client'
import { useState } from 'react'
export function InteractiveButton() { const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>点击次数: {count}</button>}传递数据给 Client#
// app/page.tsx - Server Component// Next.js 15.x
import { ProductList } from '@/components/ProductList'
async function getProducts() { const res = await fetch('https://api.example.com/products') return res.json()}
export default async function Page() { // 在服务端获取数据 const products = await getProducts()
// 传递给 Client Component return <ProductList initialProducts={products} />}// components/ProductList.tsx - Client Component'use client'
import { useState } from 'react'
interface Product { id: string name: string price: number}
interface Props { initialProducts: Product[]}
export function ProductList({ initialProducts }: Props) { const [products, setProducts] = useState(initialProducts) const [filter, setFilter] = useState('')
const filtered = products.filter((p) => p.name.toLowerCase().includes(filter.toLowerCase()) )
return ( <div> <input value={filter} onChange={(e) => setFilter(e.target.value)} placeholder="搜索产品..." /> <ul> {filtered.map((product) => ( <li key={product.id}> {product.name} - ¥{product.price} </li> ))} </ul> </div> )}🔶 注意:传递给 Client Component 的 props 必须是可序列化的(不能传递函数、类实例等)。
使用限制#
不能使用的功能#
// ❌ Server Component 不支持
// 1. React Hooksimport { useState, useEffect } from 'react'export default function Page() { const [state, setState] = useState(0) // 错误!}
// 2. 浏览器 APIexport default function Page() { window.localStorage.getItem('key') // 错误!}
// 3. 事件处理export default function Page() { return <button onClick={() => {}}>点击</button> // 错误!}何时需要 Client Component#
- 使用
useState、useEffect等 hooks - 使用浏览器 API(localStorage、window 等)
- 需要事件监听(onClick、onChange 等)
- 使用依赖状态的第三方库
最佳实践#
1. 尽可能使用 Server Components#
// ✅ 好:只在需要交互的地方使用 Clientimport { StaticContent } from '@/components/StaticContent'import { InteractiveWidget } from '@/components/InteractiveWidget'
export default function Page() { return ( <div> <StaticContent /> {/* Server */} <InteractiveWidget /> {/* Client */} </div> )}2. 将 Client 边界推到叶子节点#
// ❌ 差:整个组件是 Client'use client'export default function Page() { const [open, setOpen] = useState(false) return ( <div> <h1>标题</h1> <p>大量静态内容...</p> <button onClick={() => setOpen(!open)}>切换</button> {open && <Modal />} </div> )}
// ✅ 好:只有按钮是 Client// page.tsx (Server)import { ToggleButton } from './ToggleButton'
export default function Page() { return ( <div> <h1>标题</h1> <p>大量静态内容...</p> <ToggleButton /> </div> )}
// ToggleButton.tsx (Client);('use client')export function ToggleButton() { const [open, setOpen] = useState(false) return ( <> <button onClick={() => setOpen(!open)}>切换</button> {open && <Modal />} </> )}3. 使用 children 模式#
'use client'
export function ClientWrapper({ children }: { children: React.ReactNode }) { const [mounted, setMounted] = useState(false)
useEffect(() => { setMounted(true) }, [])
if (!mounted) return null
return <div>{children}</div>}import { ClientWrapper } from '@/components/ClientWrapper'import { ServerContent } from '@/components/ServerContent'
export default function Page() { return ( <ClientWrapper> {/* Server Component 作为 children 传入 */} <ServerContent /> </ClientWrapper> )}常见问题#
🤔 Q: 如何判断组件是 Server 还是 Client?
- 没有
"use client"指令的是 Server Component - 有
"use client"或被 Client Component 导入的是 Client Component
🤔 Q: Server Component 可以调用 API 吗?
可以直接用 fetch,不需要额外的 API 路由。
🤔 Q: Server Component 的错误如何处理?
使用 error.tsx 文件创建错误边界。
下一篇将介绍 Client Components,深入理解 "use client" 指令和交互式组件的开发。
-EOF-