Skip to content
服务端渲染(Next.js App Router)

服务端渲染(Next.js App Router)

Next.js 是 React 官方的全栈框架,提供路由、渲染策略(SSR/SSG/ISR)、数据获取、Server Actions 等开箱即用方案。

Next.js 14+ 使用 App Router(基于文件系统的路由 + Server Components)。本章以 App Router 为准,不涉及旧的 Pages Router。


核心概念

Server Components vs Client Components

这是 Next.js App Router 最核心的范式:

Server Component(默认) Client Component
运行环境 服务端 浏览器
标记 无需标记(默认) 文件顶部加 'use client'
能做什么 async 函数、直接读 DB、文件系统 useState、useEffect、事件处理、浏览器 API
JS 大小 0(不发送到浏览器) 正常大小
// Server Component(默认)
// 可以直接 async + 直接读写数据库
async function ProductList() {
  const products = await db.product.findMany();  // 无 API 层!
  return <ul>{products.map(p => <ProductCard key={p.id} {...p} />)}</ul>;
}

// Client Component
'use client';
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}

💡 最佳实践:尽可能用 Server Component(SEO 友好、减小 JS 体积)。只在需要交互时(state/effect/event handler/浏览器 API)才添加 'use client'

组件树中的边界

Server Component (无需 JS)
├── Server Component
├── Client Boundary ─────────────
│   └── Client Component (需要 JS)
│       ├── Server Component (作为 children)
│       └── Client Component
└── Server Component

🔬 深入原理:Server Components 在服务端被执行,其 JSX 输出被序列化为一种特殊的 JSON 流(RSC Payload),通过 HTTP 发送到浏览器。React 在浏览器端将其水合(hydrate)为交互式 UI。Client Components 仍走传统的 SSR → hydrate 流程。


路由系统

App Router 的路由基于 app/ 目录的文件结构:

app/
├── layout.jsx          # 根布局(必须存在)
├── page.jsx            # 首页 /
├── about/
│   └── page.jsx        # /about
├── blog/
│   ├── page.jsx        # /blog
│   └── [slug]/
│       └── page.jsx    # /blog/:slug
├── dashboard/
│   ├── layout.jsx      # /dashboard 的局部布局
│   ├── page.jsx        # /dashboard
│   └── settings/
│       └── page.jsx    # /dashboard/settings
└── api/
    └── users/
        └── route.js    # API 端点 /api/users

特殊文件约定

文件 作用
page.js 路由对应的页面内容
layout.js 该路由及其子路由的共享布局(状态在切换时保留)
loading.js Suspense fallback — 路由加载中自动显示
error.js Error Boundary — 路由错误时自动显示
not-found.js 404 页面
route.js API 路由(替代 pages router 的 api 目录)
template.js 类似 layout,但每次导航重新挂载
// app/layout.jsx — 根布局
export default function RootLayout({ children }) {
  return (
    <html lang="zh-CN">
      <body>
        <Navbar />
        <main>{children}</main>
      </body>
    </html>
  );
}

// app/blog/[slug]/page.jsx
export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);
  return <article><h1>{post.title}</h1><p>{post.content}</p></article>;
}

渲染策略

Next.js 在同一应用中混合使用三种渲染策略:

策略 何时渲染 用于
SSG (Static) 构建时 不常变的页面(博客、文档)
SSR (Dynamic) 每个请求 个性化内容(dashboard、用户主页)
ISR (Revalidation) 构建时 + 定时更新 SSG 但需要定期刷新
// SSG(默认 — 不调用动态函数)
export default async function Page() {
  const data = await fetch('https://api.example.com/data'); // 构建时缓存
}

// SSR(动态 — 使用 cookies/headers/searchParams)
import { cookies } from 'next/headers';

export default async function Page() {
  const cookieStore = cookies();
  const theme = cookieStore.get('theme');
  // 每个请求都重新渲染
}

// ISR(定时重新验证)
export const revalidate = 3600; // 每小时重新生成一次

// 按需重新验证(配合 API Route)
import { revalidatePath } from 'next/cache';
revalidatePath('/blog/[slug]');  // 在 mutation 后调用

数据获取模式

在 Server Component 中直接获取

async function UserList() {
  const users = await fetch('https://api.example.com/users').then(r => r.json());
  // 或直接查数据库
  // const users = await prisma.user.findMany();

  return (
    <ul>
      {users.map(u => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

并行请求(避免瀑布)

async function Page() {
  // ❌ 瀑布:profile 等 posts
  const profile = await getProfile();
  const posts = await getPosts();

  // ✅ 并行
  const [profile, posts] = await Promise.all([
    getProfile(),
    getPosts(),
  ]);
}

使用 loading.js 做流式渲染

// app/dashboard/loading.js
export default function Loading() {
  return <Skeleton />;  // 在页面加载时自动显示
}

// 或手动用 Suspense:
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div>
      <h1>仪表盘</h1>
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />           {/* 这个组件异步加载 */}
      </Suspense>
    </div>
  );
}

Server Actions(服务端变更)

Server Actions 让你在 React 组件中直接调用服务端函数,无需手写 API 路由:

// app/actions.js
'use server';
import { revalidatePath } from 'next/cache';
import { prisma } from '@/lib/prisma';

export async function createTodo(formData) {
  const title = formData.get('title');
  await prisma.todo.create({ data: { title } });
  revalidatePath('/todos');  // 刷新缓存
}

// 在组件中使用
import { createTodo } from '@/app/actions';

function NewTodo() {
  return (
    <form action={createTodo}>
      <input name="title" />
      <button type="submit">添加</button>
    </form>
  );
}

useFormStatus(表单提交状态)

'use client';
import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? '提交中...' : '提交'}</button>;
}

Next.js vs 纯 React SPA

维度 Next.js (SSR/SSG) 纯 React (SPA)
首屏速度 快(HTML 已渲染好) 慢(等 JS 加载、执行)
SEO ✅ 好(服务端 HTML) ❌ 差(需搜索引擎执行 JS)
服务器 需要 Node.js 服务器 静态文件即可
部署 Vercel / Node / Docker CDN / Nginx / 任何静态托管
复杂度 高(SSR 知识、hydration) 低(只关心浏览器)
适用 内容型网站、电商、SEO 需求 后台管理、工具型 SPA

💡 最佳实践:不确定就选 Next.js。它的 SSG 模式可以当纯静态站点用,未来需要 SSR 时无需迁移。只有明确不需要 SEO 的后台系统才直接选纯 SPA。


快速检查清单

  • 理解了 Server Component vs Client Component 的边界
  • 尽可能使用 Server Component,'use client' 仅加在叶子交互节点
  • API 路由不用于 Server Component 间的数据传递(直接调用函数/查数据库)
  • 使用 loading.js / Suspense 做流式渲染
  • 并行请求避免瀑布