← 返回资讯
陈默
AI 行业分析师
已审核

Next.js App Router 实战:服务端组件与数据获取的最佳实践

title: "Next.js App Router 实战:服务端组件与数据获取的最佳实践"

Next.js App Router 实战:服务端组件与数据获取的最佳实践

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 体积,提升首屏性能。

701
11688 阅读
2 评论
分享
链接已复制
编辑说明

本文由 MakeSense 编辑团队撰写并审核。文中引用的数据和观点均经过交叉验证,如有疏漏欢迎在评论区指正。最后更新:2026年07月11日 08:56

陈默

AI 行业分析师

前某大厂 AI 实验室研究员,关注大模型技术演进和商业化落地。写过 200+ 篇行业分析,擅长从产品视角拆解技术趋势。

读者评论 2

A
AI研究员 1周前
观点有道理,不过我觉得还需要考虑算力成本的问题。
回复 点赞 (11)
M
创业者Mark 1周前
正在做相关方向,这篇文章给了我不少启发。
回复 点赞 (7)