title: "Next.js App Router 实战:服务端组件与数据获取的最佳实践"
date: "2026-07-10"
tags: ["Next.js", "React", "SSR", "前端"]
Next.js App Router 实战:服务端组件与数据获取的最佳实践
Next.js 13+ 的 App Router 引入了服务端组件(RSC)和新的数据获取模式。理解这些概念对于构建高性能 React 应用至关重要。
服务端组件 vs 客户端组件
服务端组件(默认)
TSX
// app/posts/page.tsx - 服务端组件
import { db } from '@/lib/db'
import { PostList } from './PostList'
export default async function PostsPage() {
// 直接在服务端获取数据
const posts = await db.query('SELECT * FROM posts ORDER BY created_at DESC')
return (
<div>
<h1>文章列表</h1>
<PostList posts={posts} />
</div>
)
}客户端组件
TSX
// components/LikeButton.tsx
'use client'
import { useState } from 'react'
interface LikeButtonProps {
postId: string
initialLikes: number
}
export function LikeButton({ postId, initialLikes }: LikeButtonProps) {
const [likes, setLikes] = useState(initialLikes)
const [isLiked, setIsLiked] = useState(false)
async function handleLike() {
const response = await fetch(`/api/posts/${postId}/like`, {
method: 'POST'
})
if (response.ok) {
setLikes(prev => isLiked ? prev - 1 : prev + 1)
setIsLiked(!isLiked)
}
}
return (
<button onClick={handleLike} className={isLiked ? 'text-red-500' : ''}>
❤️ {likes}
</button>
)
}数据获取模式
1. 服务端直接获取
TSX
// app/users/[id]/page.tsx
import { notFound } from 'next/navigation'
interface UserPageProps {
params: { id: string }
}
export default async function UserPage({ params }: UserPageProps) {
const user = await fetch(`https://api.example.com/users/${params.id}`, {
next: { revalidate: 3600 } // ISR: 1 小时重新验证
}).then(res => res.ok ? res.json() : null)
if (!user) {
notFound()
}
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}2. 并行数据获取
TSX
// app/dashboard/page.tsx
async function getStats() {
const res = await fetch('https://api.example.com/stats')
return res.json()
}
async function getRecentOrders() {
const res = await fetch('https://api.example.com/orders/recent')
return res.json()
}
async function getTopProducts() {
const res = await fetch('https://api.example.com/products/top')
return res.json()
}
export default async function DashboardPage() {
// 并行获取数据
const [stats, orders, products] = await Promise.all([
getStats(),
getRecentOrders(),
getTopProducts()
])
return (
<div className="grid grid-cols-3 gap-4">
<StatsCard data={stats} />
<OrdersList orders={orders} />
<ProductsList products={products} />
</div>
)
}3. 流式渲染
TSX
// app/products/page.tsx
import { Suspense } from 'react'
function ProductListSkeleton() {
return (
<div className="animate-pulse space-y-4">
{[1, 2, 3].map(i => (
<div key={i} className="h-24 bg-gray-200 rounded" />
))}
</div>
)
}
async function ProductList() {
const products = await fetch('https://api.example.com/products', {
cache: 'no-store'
}).then(res => res.json())
return (
<ul>
{products.map((product: Product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}
export default function ProductsPage() {
return (
<div>
<h1>产品列表</h1>
<Suspense fallback={<ProductListSkeleton />}>
<ProductList />
</Suspense>
</div>
)
}Server Actions
TSX
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { db } from '@/lib/db'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
await db.execute(
'INSERT INTO posts (title, content) VALUES ($1, $2)',
[title, content]
)
revalidatePath('/posts')
}
export async function deletePost(id: string) {
await db.execute('DELETE FROM posts WHERE id = $1', [id])
revalidatePath('/posts')
}TSX
// app/posts/new/page.tsx
import { createPost } from '@/app/actions'
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="标题" required />
<textarea name="content" placeholder="内容" required />
<button type="submit">发布</button>
</form>
)
}缓存策略
TSX
// 静态生成(构建时)
const data = await fetch('https://api.example.com/data')
// ISR(定时重新验证)
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }
})
// 动态渲染(每次请求)
const data = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
// 手动重新验证
import { revalidatePath, revalidateTag } from 'next/cache'
revalidatePath('/posts') // 重新验证路径
revalidateTag('posts') // 重新验证标签路由组织
CODE
app/
├── layout.tsx # 根布局
├── page.tsx # 首页
├── posts/
│ ├── page.tsx # /posts
│ ├── [id]/
│ │ └── page.tsx # /posts/123
│ └── new/
│ └── page.tsx # /posts/new
├── (auth)/ # 路由组(不影响 URL)
│ ├── login/
│ │ └── page.tsx
│ └── register/
│ └── page.tsx
└── api/
└── posts/
└── route.ts # API 路由App Router 的核心思想是"服务端优先"。默认在服务端渲染,只在需要交互时才引入客户端组件。这种模式能显著减少客户端 JavaScript 体积,提升首屏性能。
读者评论 2