返回博客列表
前端
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();
}

工作原理

  1. 首次请求:获取数据并缓存 60 秒
  2. 60 秒内:直接返回缓存的数据
  3. 60 秒后:在后台重新获取数据,更新缓存

按需重新验证

使用 revalidateTagrevalidatePath 手动触发更新:

// 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"); // 更新后立即刷新产品列表
}

性能优化技巧

  1. 使用请求去重:Next.js 自动在单次渲染中去重相同请求
  2. 合理设置 revalidate:根据数据更新频率设置合适的重新验证时间
  3. 使用标签缓存:为相关数据设置标签,便于批量失效
  4. 并行获取数据:使用 Promise.all 并行获取独立数据
  5. 流式渲染:使用 Suspense 实现渐进式页面加载
  6. 避免过度缓存:对于需要实时性的数据,使用较短的 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 的数据获取和缓存机制,可以显著提升应用性能和用户体验。根据实际场景选择合适的策略,平衡性能和数据新鲜度是关键。