前端
2025年8月15日
18 分钟阅读
Next.js 数据获取与缓存策略实战指南
Next.js 数据获取与缓存策略实战指南
数据获取是 Web 应用的核心。Next.js 提供了强大的数据获取和缓存机制,基于 React Server Components 和 Web 标准 Fetch API。本文将深入探讨这些机制的原理和实战应用。
数据获取基础:Server Components
默认的服务器组件
在 App Router 中,所有组件默认都是 Server Components,可以直接进行数据获取:
// app/posts/page.tsx
async function getPosts() {
const res = await fetch("https://api.example.com/posts");
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts(); // 在服务器执行
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}优势:
- 数据获取在服务器完成,不暴露 API 密钥
- 减少客户端 JavaScript 包大小
- 首次加载更快,SEO 友好
客户端数据获取
需要交互性或使用 Hooks 时,使用客户端组件:
"use client";
import { useEffect, useState } from "react";
export function ClientPosts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchPosts() {
const res = await fetch("/api/posts");
const data = await res.json();
setPosts(data);
setLoading(false);
}
fetchPosts();
}, []);
if (loading) return <div>Loading...</div>;
return (
<div>
{posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}Fetch 缓存机制
默认缓存行为
Next.js 扩展了原生 fetch,默认自动缓存:
// 默认情况下,这个请求会被缓存
async function getData() {
const res = await fetch("https://api.example.com/data");
return res.json();
}缓存规则:
- GET 请求默认缓存
- POST、PUT、DELETE 等请求不缓存
- 相同的 URL 和选项会在同一渲染过程中去重
禁用缓存
// 方式 1:使用 cache: 'no-store'
async function getDynamicData() {
const res = await fetch("https://api.example.com/data", {
cache: "no-store", // 每次都重新获取
});
return res.json();
}
// 方式 2:使用 revalidate: 0
async function getUncachedData() {
const res = await fetch("https://api.example.com/data", {
next: { revalidate: 0 }, // 等价于 cache: 'no-store'
});
return res.json();
}时间基础重新验证
使用 revalidate 设置缓存时间:
// 每 60 秒重新验证一次
async function getRevalidatedData() {
const res = await fetch("https://api.example.com/data", {
next: { revalidate: 60 }, // 60 秒后重新验证
});
return res.json();
}工作原理:
- 首次请求:获取数据并缓存 60 秒
- 60 秒内:直接返回缓存的数据
- 60 秒后:在后台重新获取数据,更新缓存
按需重新验证
使用 revalidateTag 或 revalidatePath 手动触发更新:
// app/api/revalidate/route.ts
import { revalidateTag, revalidatePath } from "next/cache";
export async function POST(request: Request) {
const { tag, path } = await request.json();
if (tag) {
revalidateTag(tag); // 使特定标签的缓存失效
}
if (path) {
revalidatePath(path); // 使特定路径的缓存失效
}
return Response.json({ revalidated: true });
}在数据获取时使用标签:
// app/posts/page.tsx
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] }, // 标记缓存
});
return res.json();
}
// 当创建新文章时,触发重新验证
async function createPost(data: FormData) {
"use server";
await savePost(data);
revalidateTag("posts"); // 使 posts 标签的缓存失效
}静态生成与动态渲染
静态生成(Static Generation)
默认情况下,页面会在构建时静态生成:
// app/about/page.tsx
export default async function AboutPage() {
// 这个数据在构建时获取并缓存
const data = await fetch("https://api.example.com/about", {
cache: "force-cache", // 强制使用缓存(默认行为)
});
return <div>{data.content}</div>;
}动态渲染(Dynamic Rendering)
使用 dynamic = 'force-dynamic' 强制动态渲染:
// app/dashboard/page.tsx
export const dynamic = "force-dynamic";
export default async function DashboardPage() {
// 每次请求都重新渲染
const data = await fetch("https://api.example.com/dashboard", {
cache: "no-store",
});
return <div>{data.content}</div>;
}部分预渲染(Partial Prerendering)
Next.js 15+ 支持部分预渲染,结合静态和动态内容:
// app/products/page.tsx
export default async function ProductsPage() {
// 静态部分:在构建时渲染
const staticData = await getStaticProducts();
return (
<div>
<h1>Products</h1> {/* 静态 */}
<Suspense fallback={<ProductSkeleton />}>
<DynamicProductList /> {/* 动态 */}
</Suspense>
</div>
);
}
async function DynamicProductList() {
// 动态部分:每次请求时渲染
const dynamicData = await fetch("https://api.example.com/products", {
cache: "no-store",
});
return <div>{/* 产品列表 */}</div>;
}数据获取模式
并行数据获取
使用 Promise.all 并行获取多个数据源:
export default async function UserDashboard({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// 并行获取,而不是串行
const [user, posts, comments] = await Promise.all([
getUser(id),
getUserPosts(id),
getUserComments(id),
]);
return (
<div>
<UserProfile user={user} />
<PostsList posts={posts} />
<CommentsList comments={comments} />
</div>
);
}串行数据获取
当后续请求依赖前面的结果时:
export default async function UserDetail({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// 先获取用户
const user = await getUser(id);
// 然后根据用户获取相关数据
const posts = await getUserPosts(user.id);
const followers = await getFollowers(user.id);
return (
<div>
<UserHeader user={user} />
<PostsList posts={posts} />
<FollowersList followers={followers} />
</div>
);
}流式渲染与 Suspense
使用 Suspense 实现渐进式渲染:
export default function BlogPage() {
return (
<div>
<Suspense fallback={<PostSkeleton />}>
<PostList />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</div>
);
}
async function PostList() {
// 即使这个请求很慢,页面也会先渲染其他部分
await new Promise((resolve) => setTimeout(resolve, 2000));
const posts = await getPosts();
return (
<div>
{posts.map((post) => (
<Post key={post.id} post={post} />
))}
</div>
);
}错误处理
错误边界
使用 error.tsx 处理错误:
// app/posts/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}try-catch 错误处理
async function getPostWithErrorHandling(slug: string) {
try {
const res = await fetch(`https://api.example.com/posts/${slug}`);
if (!res.ok) {
throw new Error("Failed to fetch post");
}
return await res.json();
} catch (error) {
console.error("Error fetching post:", error);
// 返回默认值或抛出错误
throw error;
}
}数据库查询最佳实践
使用 Prisma
// lib/db.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
// 单例模式避免多个实例
export default prisma;// app/posts/page.tsx
import prisma from "@/lib/db";
export default async function PostsPage() {
// Prisma 查询会自动使用连接池
const posts = await prisma.post.findMany({
where: { published: true },
orderBy: { createdAt: "desc" },
take: 10,
});
return (
<div>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}使用 Server Actions
// app/actions.ts
"use server";
import { revalidateTag } from "next/cache";
import prisma from "@/lib/db";
export async function createPost(data: FormData) {
const title = data.get("title") as string;
const content = data.get("content") as string;
try {
const post = await prisma.post.create({
data: {
title,
content,
published: true,
},
});
// 重新验证缓存
revalidateTag("posts");
return { success: true, post };
} catch (error) {
return { success: false, error: "Failed to create post" };
}
}缓存策略实战
场景 1:博客文章(静态 + ISR)
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // 1 小时重新验证
export async function generateStaticParams() {
// 预生成最受欢迎的文章
const popularPosts = await getPopularPosts();
return popularPosts.map((post) => ({
slug: post.slug,
}));
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600, tags: ["posts", `post-${slug}`] },
}).then((res) => res.json());
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}场景 2:用户仪表板(动态 + 缓存)
// app/dashboard/page.tsx
export const dynamic = "force-dynamic";
export default async function Dashboard() {
// 用户数据需要实时更新,但可以短期缓存
const userData = await fetch("https://api.example.com/user/dashboard", {
next: { revalidate: 30 }, // 30 秒缓存
}).then((res) => res.json());
return (
<div>
<Stats data={userData.stats} />
<RecentActivity activities={userData.activities} />
</div>
);
}场景 3:产品列表(标签缓存)
// app/products/page.tsx
async function getProducts() {
return fetch("https://api.example.com/products", {
next: { tags: ["products"] },
}).then((res) => res.json());
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<div>
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// app/admin/products/actions.ts
("use server");
import { revalidateTag } from "next/cache";
export async function updateProduct(id: string, data: FormData) {
await saveProduct(id, data);
revalidateTag("products"); // 更新后立即刷新产品列表
}性能优化技巧
- 使用请求去重:Next.js 自动在单次渲染中去重相同请求
- 合理设置 revalidate:根据数据更新频率设置合适的重新验证时间
- 使用标签缓存:为相关数据设置标签,便于批量失效
- 并行获取数据:使用 Promise.all 并行获取独立数据
- 流式渲染:使用 Suspense 实现渐进式页面加载
- 避免过度缓存:对于需要实时性的数据,使用较短的 revalidate 时间
// 请求去重示例
export default async function Page() {
// 这两个请求会被自动去重,只执行一次
const [data1, data2] = await Promise.all([
fetch("https://api.example.com/data"),
fetch("https://api.example.com/data"), // 相同的 URL
]);
return <div>{/* ... */}</div>;
}调试缓存
查看缓存状态
async function getData() {
const res = await fetch("https://api.example.com/data", {
next: { revalidate: 60 },
});
// 查看缓存状态
console.log(res.headers.get("x-cache")); // HIT 或 MISS
return res.json();
}开发环境调试
在开发模式下,Next.js 默认不缓存,但可以通过配置启用:
// next.config.js
module.exports = {
experimental: {
isrMemoryCacheSize: 0, // 禁用内存缓存(仅开发)
},
};掌握 Next.js 的数据获取和缓存机制,可以显著提升应用性能和用户体验。根据实际场景选择合适的策略,平衡性能和数据新鲜度是关键。