第16章 数据获取与预渲染
SSG、SSR、ISR 三种渲染模式详解
作者:zyyc-0336 | 日期:2026-06-12
📋 学习路径建议
- 先学 16.1(预渲染),理解 SSG 和 SSR 的区别
- 再学 16.2(服务端组件),掌握 async/await 获取数据
- 然后学 16.3(客户端数据),用 fetch + useEffect 处理动态数据
- 最后学 16.4(Server Actions),实现表单提交
⚠️ 常见错误提示
⚠️ 服务端组件不能用 useState、useEffect 等客户端 Hook
⚠️ fetch 在服务端组件中会自动缓存,需要动态数据要加
{ cache: 'no-store' }⚠️ 客户端组件用
'use client'声明,但不要把所有组件都标记为客户端⚠️ Server Actions 只能在服务端执行,不能在客户端直接调用数据库
16.1 预渲染:SSG 与 SSR
Next.js 的核心优势是预渲染——页面在服务器上提前生成 HTML,而不是在浏览器中用 JavaScript 动态渲染。这带来更快的首屏加载和更好的 SEO。
两种预渲染方式
| 方式 | 全称 | 生成时机 | 适用场景 |
|---|---|---|---|
| SSG | Static Site Generation | 构建时(build time) | 内容不常变化的页面(博客、文档) |
| SSR | Server-Side Rendering | 每次请求时(request time) | 需要实时数据的页面(用户仪表盘) |
SSG 示例(默认行为)
// app/blog/page.js —— 默认是 SSG(构建时生成)
// 这个函数在 build 时执行,结果被缓存
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
const data = await res.json();
return data;
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<div>
<h1>📝 博客文章</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
SSR 示例(动态渲染)
// app/dashboard/page.js —— SSR(每次请求时生成)
// 加上 cache: 'no-store' 禁用缓存,每次请求都重新获取
async function getUserData() {
const res = await fetch('https://api.example.com/user', {
cache: 'no-store' // 关键:禁用缓存,启用 SSR
});
const data = await res.json();
return data;
}
export default async function DashboardPage() {
const user = await getUserData();
return (
<div>
<h1>👋 欢迎,{user.name}</h1>
<p>余额:¥{user.balance}</p>
</div>
);
}
ISR(增量静态再生)
// ISR:构建时生成,但每隔一段时间重新生成
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 60 } // 每 60 秒重新验证一次
});
const data = await res.json();
return data;
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<div>
<h1>🛍️ 商品列表</h1>
{products.map(p => (
<div key={p.id}>
<h3>{p.name}</h3>
<p>¥{p.price}</p>
</div>
))}
</div>
);
}
三种方式对比
| 特性 | SSG | SSR | ISR |
|---|---|---|---|
| 生成时机 | 构建时 | 每次请求 | 构建时 + 定时更新 |
| 性能 | ⭐⭐⭐ 最快 | ⭐ 较慢 | ⭐⭐⭐ 快 |
| 数据新鲜度 | 构建时快照 | 实时 | 可配置间隔 |
| 适用场景 | 博客、文档 | 用户数据、搜索 | 商品列表、新闻 |
| 配置方式 | 默认 | cache: 'no-store' | revalidate: 秒数 |
💡 选择建议:大多数页面用默认 SSG 就够了。只有需要实时数据时才用 SSR,需要"准实时"时用 ISR。
16.2 服务端组件中获取数据
Next.js 13+ 的组件默认是服务端组件,可以直接写 async 函数获取数据,不需要 useEffect 或 useState。
基本用法
// app/products/page.js —— 服务端组件直接获取数据
// 直接写 async 函数
async function getProducts() {
const res = await fetch('https://api.example.com/products');
if (!res.ok) {
throw new Error('获取商品失败');
}
return res.json();
}
// 组件本身也是 async
export default async function ProductsPage() {
const products = await getProducts();
return (
<div>
<h1>商品列表</h1>
<ul>
{products.map(product => (
<li key={product.id}>
{product.name} - ¥{product.price}
</li>
))}
</ul>
</div>
);
}
并行获取多个数据源
// 同时获取多个数据源(不互相依赖时用 Promise.all)
async function getProducts() {
const res = await fetch('https://api.example.com/products');
return res.json();
}
async function getCategories() {
const res = await fetch('https://api.example.com/categories');
return res.json();
}
export default async function ShopPage() {
// 并行请求,总耗时 = 最慢的那个
const [products, categories] = await Promise.all([
getProducts(),
getCategories()
]);
return (
<div>
<h1>商城</h1>
<nav>
{categories.map(cat => (
<a key={cat.id} href={`/shop/${cat.slug}`}>
{cat.name}
</a>
))}
</nav>
<ul>
{products.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</div>
);
}
串行获取(有依赖关系时)
// 第二个请求依赖第一个的结果
async function getUser(userId) {
const res = await fetch(`https://api.example.com/users/${userId}`);
return res.json();
}
async function getUserPosts(userId) {
const res = await fetch(`https://api.example.com/users/${userId}/posts`);
return res.json();
}
export default async function UserProfilePage({ params }) {
const { id } = params;
// 先获取用户信息
const user = await getUser(id);
// 再获取用户的文章(依赖 userId)
const posts = await getUserPosts(id);
return (
<div>
<h1>{user.name} 的主页</h1>
<p>{user.bio}</p>
<h2>文章列表</h2>
{posts.map(post => (
<article key={post.id}>
<h3>{post.title}</h3>
</article>
))}
</div>
);
}
错误处理
// 方式1:try-catch
async function getData() {
try {
const res = await fetch('https://api.example.com/data');
if (!res.ok) throw new Error('请求失败');
return await res.json();
} catch (error) {
console.error('获取数据失败:', error);
return null;
}
}
// 方式2:error.js 文件(推荐)
// app/dashboard/error.js
'use client';
export default function Error({ error, reset }) {
return (
<div>
<h2>出错了!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>重试</button>
</div>
);
}
⚠️ 注意:服务端组件中不能使用
useState、useEffect、onClick 等客户端 API。如果需要交互,把组件拆分成客户端子组件。16.3 客户端数据获取
当数据需要在客户端获取时(比如用户交互后刷新、实时搜索),需要用
'use client' + useEffect 或 SWR。
useEffect + fetch
'use client';
import { useState, useEffect } from 'react';
export default function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUsers() {
try {
const res = await fetch('https://api.example.com/users');
if (!res.ok) throw new Error('请求失败');
const data = await res.json();
setUsers(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchUsers();
}, []); // 空依赖数组 = 组件挂载时执行一次
if (loading) return <p>加载中...</p>;
if (error) return <p>错误:{error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
SWR(推荐)
// SWR = Stale-While-Revalidate
// 自动缓存、自动重新验证、去重请求
'use client';
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(res => res.json());
export default function UserList() {
const { data: users, error, isLoading } = useSWR(
'https://api.example.com/users',
fetcher
);
if (isLoading) return <p>加载中...</p>;
if (error) return <p>出错了</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
实时搜索
'use client';
import { useState, useEffect } from 'react';
export default function SearchBox() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
if (!query.trim()) {
setResults([]);
return;
}
// 防抖:用户停止输入 300ms 后才请求
const timer = setTimeout(async () => {
const res = await fetch(
`/api/search?q=${encodeURIComponent(query)}`
);
const data = await res.json();
setResults(data);
}, 300);
// 清除上一次的定时器
return () => clearTimeout(timer);
}, [query]);
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
/>
<ul>
{results.map(item => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
服务端 vs 客户端数据获取
| 特性 | 服务端组件 | 客户端组件 |
|---|---|---|
| 声明方式 | 默认(无需声明) | 'use client' |
| 获取方法 | 直接 async/await | useEffect / SWR |
| 数据对用户可见 | ❌ 不可见(HTML 中) | ✅ 可见(浏览器请求) |
| 适用场景 | SEO、首屏速度 | 交互、实时更新 |
| 缓存控制 | fetch 的 cache 选项 | SWR / 自定义缓存 |
💡 最佳实践:优先使用服务端组件获取数据。只在需要客户端交互(点击、输入、实时更新)时才用客户端组件。
16.4 表单与 Server Actions
Server Actions 是 Next.js 14+ 的新特性,允许在服务端直接处理表单提交,不需要写 API 路由。
基本用法
// app/contact/page.js
// 在服务端执行的函数
async function submitContact(formData) {
'use server';
const name = formData.get('name');
const email = formData.get('email');
const message = formData.get('message');
// 处理数据(可以操作数据库)
console.log('收到表单:', { name, email, message });
// 发送邮件、保存数据库等...
// await db.contacts.create({ name, email, message });
// 返回结果
return { success: true };
}
export default function ContactPage() {
return (
<div>
<h1>联系我们</h1>
<form action={submitContact}>
<input name="name" placeholder="姓名" required />
<input name="email" type="email" placeholder="邮箱" required />
<textarea name="message" placeholder="留言" required />
<button type="submit">提交</button>
</form>
</div>
);
}
带状态反馈的表单
// 使用 useFormState 和 useFormStatus 处理表单状态
'use client';
import { useFormStatus } from 'react-dom';
import { useFormState } from 'react-dom';
import { submitOrder } from './actions';
// 提交按钮组件(需要在客户端使用 useFormStatus)
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '提交订单'}
</button>
);
}
export default function OrderForm() {
const [state, formAction] = useFormState(submitOrder, null);
return (
<form action={formAction}>
<input name="product" placeholder="商品名" required />
<input name="quantity" type="number" min="1" required />
<SubmitButton />
{state?.success && (
<p style={{ color: 'green' }}>✅ 订单提交成功!</p>
)}
{state?.error && (
<p style={{ color: 'red' }}>❌ {state.error}</p>
)}
</form>
);
}
Server Action 文件
// app/contact/actions.js —— 单独的 Server Action 文件
'use server';
export async function submitContact(prevState, formData) {
const name = formData.get('name');
const email = formData.get('email');
// 验证
if (!name || !email) {
return { success: false, error: '请填写所有字段' };
}
try {
// 保存到数据库
// await db.contacts.create({ name, email });
return { success: true };
} catch (error) {
return { success: false, error: '保存失败,请重试' };
}
}
export async function deleteContact(id) {
'use server';
// 删除操作
// await db.contacts.delete(id);
return { success: true };
}
渐进式表单增强
// 不用 JavaScript 也能工作(渐进式增强)
// 传统表单提交(无 JS)
<form action="/api/submit" method="POST">
<input name="name" />
<button type="submit">提交</button>
</form>
// Server Action(有 JS 时更好体验)
<form action={submitAction}>
<input name="name" />
<button type="submit">提交</button>
</form>
// 两者可以共存:没有 JS 时用传统方式,有 JS 时自动用 Server Action
💡 Server Actions 优势:类型安全、自动序列化、与 React 无缝集成、支持渐进式增强。
本章复盘——数据获取与预渲染核心要点
| 要点 | 说明 |
|---|---|
| SSG | 构建时生成静态 HTML,适合内容不常变化的页面 |
| SSR | 请求时生成 HTML,用 cache: 'no-store' 启用 |
| ISR | 按需重新验证,用 revalidate: 秒数 配置 |
| 服务端组件 | 默认行为,直接用 async/await 获取数据 |
| 客户端组件 | 用 'use client' + useEffect/SWR 获取数据 |
| Server Actions | 用 'use server' 标记,直接处理表单提交 |
| 并行获取 | 用 Promise.all() 同时获取多个数据源 |
下一章预告(第17章):我们将学习API Routes——后端 API 路由开发、请求处理和数据验证。
本文档由 zyyc-0336 编写,最后更新:2026-06-14