服务端渲染(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做流式渲染 - 并行请求避免瀑布