05-服务端渲染与元框架——04. 数据获取 - fetch、缓存与重新验证

05-服务端渲染与元框架——04. 数据获取 - fetch、缓存与重新验证 04. 数据获取 - fetch、缓存与重新验证概述Next.js 扩展了原生 fetch API添加了强大的缓存和重新验证能力。在 Server Components 中你可以直接使用 async/await 获取数据并精细控制每个请求的缓存行为。维度内容WhatNext.js 扩展了原生 fetch支持缓存和重新验证Why简化数据获取提供细粒度的缓存控制When在 Server/Client Component 中获取数据Where组件内部、布局、路由处理器中Who需要数据获取的开发者Howfetch(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每次重新获取实时数据ISRnext: { 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 classNameposts-grid {posts.map(post ( article key{post.id} classNamepost-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 classNamesidebar 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 classNamelatest-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 classNameblog-container div classNamemain-content h1博客文章/h1 Suspense fallback{div加载文章.../div} PostList / /Suspense /div div classNamesidebar 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 classNamepost-content{post.body}/div /article ); } async function Comments({ postId }) { const comments await getComments(postId); return ( div classNamecomments h3评论 ({comments.length})/h3 {comments.map(comment ( div key{comment.id} classNamecomment strong{comment.name}/strong p{comment.body}/p /div ))} /div ); } export default async function BlogPostPage({ params }) { const { id } await params; return ( div classNamepost-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-storeISR 定时 revalidate按需重验证用 tag9. 相关资源Next.js 数据获取Next.js 缓存fetch API 文档