第16章 数据获取与预渲染

SSG、SSR、ISR 三种渲染模式详解

作者:zyyc-0336  |  日期:2026-06-12

📋 学习路径建议
  1. 先学 16.1(预渲染),理解 SSG 和 SSR 的区别
  2. 再学 16.2(服务端组件),掌握 async/await 获取数据
  3. 然后学 16.3(客户端数据),用 fetch + useEffect 处理动态数据
  4. 最后学 16.4(Server Actions),实现表单提交
⚠️ 常见错误提示

⚠️ 服务端组件不能用 useState、useEffect 等客户端 Hook

⚠️ fetch 在服务端组件中会自动缓存,需要动态数据要加 { cache: 'no-store' }

⚠️ 客户端组件用 'use client' 声明,但不要把所有组件都标记为客户端

⚠️ Server Actions 只能在服务端执行,不能在客户端直接调用数据库

16.1 预渲染:SSG 与 SSR

Next.js 的核心优势是预渲染——页面在服务器上提前生成 HTML,而不是在浏览器中用 JavaScript 动态渲染。这带来更快的首屏加载和更好的 SEO。

两种预渲染方式

方式全称生成时机适用场景
SSGStatic Site Generation构建时(build time)内容不常变化的页面(博客、文档)
SSRServer-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>
    );
}

三种方式对比

特性SSGSSRISR
生成时机构建时每次请求构建时 + 定时更新
性能⭐⭐⭐ 最快⭐ 较慢⭐⭐⭐ 快
数据新鲜度构建时快照实时可配置间隔
适用场景博客、文档用户数据、搜索商品列表、新闻
配置方式默认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>
    );
}
⚠️ 注意:服务端组件中不能使用 useStateuseEffectonClick 等客户端 API。如果需要交互,把组件拆分成客户端子组件。

16.3 客户端数据获取

当数据需要在客户端获取时(比如用户交互后刷新、实时搜索),需要用 'use client' + useEffectSWR

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/awaituseEffect / 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