04. 数据获取 - fetch、缓存与重新验证
概述
Next.js 扩展了原生 fetch API,添加了强大的缓存和重新验证能力。在 Server Components 中,你可以直接使用 async/await 获取数据,并精细控制每个请求的缓存行为。
| 维度 | 内容 |
|---|
| What | Next.js 扩展了原生 fetch,支持缓存和重新验证 |
| Why | 简化数据获取,提供细粒度的缓存控制 |
| When | 在 Server/Client Component 中获取数据 |
| Where | 组件内部、布局、路由处理器中 |
| Who | 需要数据获取的开发者 |
| How | fetch(url, { cache: 'force-cache', next: { revalidate: 3600 } }) |
1. 数据获取基础
1.1 在 Server Component 中获取数据
// app/users/page.js // Server Component 可以直接使用 async/await async function UsersPage() { const users = await fetch('https://api.example.com/users').then(res => res.json()); return ( <div> <h1>用户列表</h1> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> ); } export default UsersPage;
1.2 在 Client Component 中获取数据
'use client'; import { useState, useEffect } from 'react'; export default function ClientUsersPage() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { fetch('/api/users') .then(res => res.json()) .then(data => { setUsers(data); setLoading(false); }); }, []); if (loading) return <div>加载中...</div>; return ( <div> <h1>用户列表</h1> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> ); }
2. 缓存策略
2.1 静态缓存 (force-cache)
// 默认行为:缓存数据,相同请求只执行一次 async function StaticPage() { // 这个请求会被缓存 const data = await fetch('https://api.example.com/static-data', { cache: 'force-cache', }).then(res => res.json()); return <div>{data.content}</div>; }
2.2 动态数据 (no-store)
// 每次都重新获取,不缓存 async function DynamicPage() { const data = await fetch('https://api.example.com/live-data', { cache: 'no-store', // 每次请求都重新获取 }).then(res => res.json()); return <div>{data.currentValue}</div>; }
2.3 重新验证 (ISR)
// 缓存数据,但每隔一段时间重新验证 async function ISRPage() { const data = await fetch('https://api.example.com/semi-static', { next: { revalidate: 3600 }, // 3600 秒后重新验证 }).then(res => res.json()); return <div>{data.content}</div>; }
2.4 缓存策略对比
| 策略 | 配置 | 行为 | 适用场景 |
|---|
| 静态 | cache: 'force-cache' | 永久缓存 | 不变的内容 |
| 动态 | cache: 'no-store' | 每次重新获取 | 实时数据 |
| ISR | next: { revalidate: n } | 缓存 + 定期更新 | 准实时数据 |
3. 数据缓存
3.1 全局缓存配置
// app/layout.js import { unstable_noStore as noStore } from 'next/cache'; export default function Layout({ children }) { // 标记整个布局为动态 noStore(); return <div>{children}</div>; }
3.2 路由段配置
// app/products/page.js // 导出配置来控制缓存行为 // 静态渲染(默认) export const dynamic = 'force-static'; // 动态渲染 export const dynamic = 'force-dynamic'; // 自动(根据请求决定) export const dynamic = 'auto'; // 重新验证间隔 export const revalidate = 3600; // 1 小时 export default async function ProductsPage() { const products = await fetch('https://api.example.com/products').then(res => res.json()); return <ProductList products={products} />; }
3.3 缓存数据去重
// 相同 URL 的请求会自动去重 async function Dashboard() { // 这两个请求相同,只会发起一次 const user = await fetch('https://api.example.com/user').then(res => res.json()); const userProfile = await fetch('https://api.example.com/user').then(res => res.json()); return ( <div> <h1>{user.name}</h1> <p>{userProfile.bio}</p> </div> ); }
4. 重新验证 (Revalidation)
4.1 时间重新验证
// app/blog/page.js async function BlogPage() { const posts = await fetch('https://api.example.com/posts', { next: { revalidate: 60 }, // 每分钟重新验证 }).then(res => res.json()); return <PostList posts={posts} />; }
4.2 按需重新验证
// app/api/revalidate/route.js import { revalidateTag, revalidatePath } from 'next/cache'; import { NextResponse } from 'next/server'; export async function POST(request) { const { tag, path } = await request.json(); // 按标签重新验证 if (tag) { revalidateTag(tag); } // 按路径重新验证 if (path) { revalidatePath(path); } return NextResponse.json({ revalidated: true }); }
// 使用标签缓存 async function PostsPage() { const posts = await fetch('https://api.example.com/posts', { next: { tags: ['posts', 'blog'] }, // 添加标签 }).then(res => res.json()); return <PostList posts={posts} />; } // 在其他地方触发重新验证 await fetch('/api/revalidate', { method: 'POST', body: JSON.stringify({ tag: 'posts' }), });
4.3 路由处理器中的重新验证
// app/api/posts/route.js import { revalidatePath } from 'next/cache'; import { NextResponse } from 'next/server'; export async function POST(request) { const post = await request.json(); // 保存到数据库 await db.post.create({ data: post }); // 重新验证博客页面 revalidatePath('/blog'); return NextResponse.json(post); }
5. 高级数据获取模式
5.1 并行数据获取
// app/dashboard/page.js async function DashboardPage() { // 并行获取多个数据 const [user, posts, notifications] = await Promise.all([ fetch('https://api.example.com/user').then(res => res.json()), fetch('https://api.example.com/posts').then(res => res.json()), fetch('https://api.example.com/notifications').then(res => res.json()), ]); return ( <div> <UserCard user={user} /> <PostList posts={posts} /> <NotificationList notifications={notifications} /> </div> ); }
5.2 串行数据获取
// app/user/[id]/page.js async function UserProfilePage({ params }) { const { id } = await params; // 先获取用户 const user = await fetch(`https://api.example.com/users/${id}`).then(res => res.json()); // 然后获取用户的文章 const posts = await fetch(`https://api.example.com/users/${user.id}/posts`).then(res => res.json()); return ( <div> <h1>{user.name}</h1> <PostList posts={posts} /> </div> ); }
5.3 条件数据获取
// app/search/page.js async function SearchPage({ searchParams }) { const { q, category } = await searchParams; // 只有有关键词时才获取数据 if (!q) { return <div>请输入搜索关键词</div>; } const results = await fetch( `https://api.example.com/search?q=${q}&category=${category || ''}` ).then(res => res.json()); return <SearchResults results={results} query={q} />; }
6. 错误处理
6.1 基础错误处理
// app/products/page.js async function ProductsPage() { try { const products = await fetch('https://api.example.com/products').then(res => { if (!res.ok) throw new Error('获取失败'); return res.json(); }); return <ProductList products={products} />; } catch (error) { return <div>加载失败: {error.message}</div>; } }
6.2 使用 error.js
// app/products/error.js 'use client'; export default function Error({ error, reset }) { return ( <div> <h2>出错了</h2> <p>{error.message}</p> <button onClick={reset}>重试</button> </div> ); }
6.3 使用 not-found
// app/products/[id]/page.js import { notFound } from 'next/navigation'; async function ProductPage({ params }) { const { id } = await params; const product = await fetch(`https://api.example.com/products/${id}`).then(res => { if (!res.ok) return null; return res.json(); }); if (!product) { notFound(); } return <ProductDetail product={product} />; }
7. 完整示例:博客系统
// app/blog/page.js import Link from 'next/link'; import { Suspense } from 'react'; async function getPosts() { const res = await fetch('https://jsonplaceholder.typicode.com/posts', { next: { revalidate: 3600, tags: ['posts'] }, }); if (!res.ok) throw new Error('获取文章失败'); return res.json(); } async function getCategories() { const res = await fetch('https://api.example.com/categories', { cache: 'force-cache', }); return res.json(); } async function getLatestPosts() { const res = await fetch('https://api.example.com/posts/latest', { cache: 'no-store', }); return res.json(); } // 文章列表组件 async function PostList() { const posts = await getPosts(); return ( <div className="posts-grid"> {posts.map(post => ( <article key={post.id} className="post-card"> <h2> <Link href={`/blog/${post.id}`}>{post.title}</Link> </h2> <p>{post.body.substring(0, 100)}...</p> </article> ))} </div> ); } // 侧边栏组件 async function Sidebar() { const categories = await getCategories(); return ( <aside className="sidebar"> <h3>分类</h3> <ul> {categories.map(cat => ( <li key={cat.id}> <Link href={`/category/${cat.slug}`}>{cat.name}</Link> </li> ))} </ul> </aside> ); } // 最新文章组件 async function LatestPosts() { const posts = await getLatestPosts(); return ( <div className="latest-posts"> <h3>最新文章</h3> <ul> {posts.map(post => ( <li key={post.id}> <Link href={`/blog/${post.id}`}>{post.title}</Link> </li> ))} </ul> </div> ); } // 主页 export default async function BlogHomePage() { return ( <div className="blog-container"> <div className="main-content"> <h1>博客文章</h1> <Suspense fallback={<div>加载文章...</div>}> <PostList /> </Suspense> </div> <div className="sidebar"> <Suspense fallback={<div>加载分类...</div>}> <Sidebar /> </Suspense> <Suspense fallback={<div>加载最新文章...</div>}> <LatestPosts /> </Suspense> </div> </div> ); } // app/blog/[id]/page.js import { notFound } from 'next/navigation'; import { Suspense } from 'react'; async function getPost(id) { const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`, { next: { revalidate: 60 }, }); if (!res.ok) return null; return res.json(); } async function getComments(postId) { const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}/comments`, { cache: 'no-store', // 评论实时获取 }); return res.json(); } async function PostContent({ id }) { const post = await getPost(id); if (!post) { notFound(); } return ( <article> <h1>{post.title}</h1> <div className="post-content">{post.body}</div> </article> ); } async function Comments({ postId }) { const comments = await getComments(postId); return ( <div className="comments"> <h3>评论 ({comments.length})</h3> {comments.map(comment => ( <div key={comment.id} className="comment"> <strong>{comment.name}</strong> <p>{comment.body}</p> </div> ))} </div> ); } export default async function BlogPostPage({ params }) { const { id } = await params; return ( <div className="post-container"> <Suspense fallback={<div>加载文章...</div>}> <PostContent id={id} /> </Suspense> <Suspense fallback={<div>加载评论...</div>}> <Comments postId={id} /> </Suspense> </div> ); } // app/api/revalidate/route.js import { revalidateTag, revalidatePath } from 'next/cache'; import { NextResponse } from 'next/server'; export async function POST(request) { const { tag, path } = await request.json(); if (tag) { revalidateTag(tag); } if (path) { revalidatePath(path); } return NextResponse.json({ revalidated: true }); }
8. 总结
核心要点
| 要点 | 说明 |
|---|
| Server Component | 直接 async/await 获取数据 |
| 缓存策略 | force-cache、no-store、revalidate |
| 重新验证 | 时间间隔、按需(tag/path) |
| 最佳实践 | 并行获取、Suspense 流式渲染 |
缓存策略选择
| 数据类型 | 推荐策略 | 示例 |
|---|
| 静态内容 | force-cache | 产品说明、帮助文档 |
| 动态内容 | no-store | 实时数据、用户状态 |
| 准实时内容 | revalidate | 新闻、博客、商品列表 |
记忆口诀
数据获取用 fetch,缓存策略三个选
静态缓存 force-cache,动态数据 no-store
ISR 定时 revalidate,按需重验证用 tag
9. 相关资源
- Next.js 数据获取
- Next.js 缓存
- fetch API 文档