跳到主要内容

数据获取

本章将介绍 Next.js 中的数据获取方式,包括服务端数据获取、客户端数据获取和缓存策略。

服务端数据获取

在 Server Component 中可以直接使用 async/await 获取数据。这是 Next.js App Router 的核心优势之一:组件天然支持异步操作。

基本用法

// app/posts/page.tsx
async function getPosts() {
const res = await fetch("https://api.example.com/posts");
if (!res.ok) {
throw new Error("获取数据失败");
}
return res.json();
}

export default async function PostsPage() {
const posts = await getPosts();

return (
<ul>
{posts.map((post: Post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

服务端数据获取的优势:

  • 直接访问后端资源:可以安全地使用数据库连接、内部 API 等
  • 保护敏感信息:API 密钥、数据库凭证等不会暴露给客户端
  • 减少客户端 JavaScript:数据获取逻辑留在服务端
  • 更好的 SEO:搜索引擎可以直接抓取渲染后的内容

获取单个资源

在动态路由中获取特定资源。注意 Next.js 15 中 params 是 Promise 类型:

// app/posts/[id]/page.tsx
async function getPost(id: string) {
const res = await fetch(`https://api.example.com/posts/${id}`);
if (!res.ok) return null;
return res.json();
}

export default async function PostPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const post = await getPost(id);

if (!post) {
return <div>文章不存在</div>;
}

return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}

并行数据获取

当需要同时获取多个数据时,使用 Promise.all 并行请求:

async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`);
return res.json();
}

async function getUserPosts(id: string) {
const res = await fetch(`https://api.example.com/users/${id}/posts`);
return res.json();
}

export default async function UserPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;

// 并行获取,两个请求同时开始
const [user, posts] = await Promise.all([
getUser(id),
getUserPosts(id),
]);

return (
<div>
<h1>{user.name}</h1>
<ul>
{posts.map((post: Post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
为什么需要 Promise.all?

如果在组件中依次 await 两个请求,第二个请求必须等待第一个完成才能开始。使用 Promise.all 可以让所有请求同时发起,显著减少总等待时间。

顺序数据获取

当第二个请求依赖第一个请求的结果时,需要顺序获取:

export default async function ArtistPage({
params,
}: {
params: Promise<{ username: string }>;
}) {
const { username } = await params;

// 先获取艺术家信息
const artist = await getArtist(username);

// 再根据艺术家 ID 获取专辑
const albums = await getAlbums(artist.id);

return (
<div>
<h1>{artist.name}</h1>
<AlbumList albums={albums} />
</div>
);
}

缓存策略

Next.js 提供了多层缓存机制,理解缓存行为对于构建高性能应用至关重要。

默认行为

在 Next.js 15 中,fetch 请求默认不缓存

// 默认行为:每次请求都会重新获取数据
const res = await fetch("https://api.example.com/posts");

但是,Next.js 会预渲染路由并缓存输出以提升性能。如果你想选择动态渲染,使用 { cache: 'no-store' } 选项。

启用缓存

使用 cache: 'force-cache' 显式启用缓存:

// 缓存请求结果
const res = await fetch("https://api.example.com/posts", {
cache: "force-cache",
});

禁用缓存

显式禁用缓存,每次请求都获取新数据:

// 每次请求都重新获取
const res = await fetch("https://api.example.com/posts", {
cache: "no-store",
});

定时重新验证(ISR)

设置重新验证时间间隔,实现增量静态再生:

// 每 60 秒重新验证一次
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 },
});

重新验证的工作原理:

  1. 首次请求时,数据被缓存
  2. 60 秒内的后续请求使用缓存数据
  3. 60 秒后的首次请求仍返回缓存数据,同时在后台重新获取
  4. 新数据获取成功后,缓存被更新

按需重新验证

为请求添加标签,支持按需失效:

// 获取数据时设置标签
export async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] },
});
return res.json();
}

// 获取单个文章
export async function getPost(id: string) {
const res = await fetch(`https://api.example.com/posts/${id}`, {
next: { tags: ["post", `post-${id}`] },
});
return res.json();
}

在 Server Action 或 API 路由中触发重新验证:

"use server";

import { revalidateTag, revalidatePath } from "next/cache";

// 重新验证所有带 "posts" 标签的请求
export async function refreshPosts() {
revalidateTag("posts");
}

// 重新验证特定路径
export async function refreshPostPage() {
revalidatePath("/posts");
}

// 重新验证特定文章
export async function refreshPost(id: string) {
revalidateTag(`post-${id}`);
}

路由段配置

在页面或布局级别配置缓存行为:

// 设置页面级别的重新验证时间
export const revalidate = 3600; // 每小时重新验证

// 或完全禁用缓存
export const revalidate = 0;

// 控制动态行为
export const dynamic = "force-dynamic"; // 每次请求重新渲染
export const dynamic = "force-static"; // 强制静态渲染

非 fetch 数据的缓存

对于数据库查询等非 fetch 操作,使用 unstable_cache

import { unstable_cache } from "next/cache";
import { db } from "@/lib/db";

export const getCachedUser = unstable_cache(
async (id: string) => {
return db.user.findUnique({
where: { id },
});
},
["user"], // 缓存键前缀
{
revalidate: 3600, // 重新验证时间(秒)
tags: ["user"], // 用于按需重新验证
}
);

客户端数据获取

在 Client Component 中需要使用不同的数据获取方式。

使用 React 的 use API

React 19 引入了 use API,可以读取 Promise。结合服务端数据获取:

// app/posts/page.tsx(服务端组件)
import Posts from "./posts";
import { Suspense } from "react";

// 不要 await,直接传递 Promise
export default function Page() {
const posts = getPosts(); // 返回 Promise

return (
<Suspense fallback={<div>加载中...</div>}>
<Posts posts={posts} />
</Suspense>
);
}

async function getPosts() {
const res = await fetch("https://api.example.com/posts");
return res.json();
}
// app/posts/posts.tsx(客户端组件)
"use client";

import { use } from "react";

export default function Posts({
posts,
}: {
posts: Promise<Post[]>;
}) {
const allPosts = use(posts);

return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

使用 useEffect

传统的客户端数据获取方式:

"use client";

import { useState, useEffect } from "react";

type Post = {
id: number;
title: string;
};

export default function PostList() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
async function fetchPosts() {
try {
const res = await fetch("/api/posts");
const data = await res.json();
setPosts(data);
} finally {
setLoading(false);
}
}
fetchPosts();
}, []);

if (loading) return <div>加载中...</div>;

return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

使用 SWR

SWR 是 Vercel 开发的数据获取 Hook,提供缓存、重新验证、聚焦重新获取等功能:

"use client";

import useSWR from "swr";

const fetcher = (url: string) => fetch(url).then((res) => res.json());

export default function PostList() {
const { data, error, isLoading } = useSWR("/api/posts", fetcher);

if (isLoading) return <div>加载中...</div>;
if (error) return <div>加载失败</div>;

return (
<ul>
{data.map((post: Post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

SWR 的优势:

  • 自动重新验证:窗口聚焦时自动更新数据
  • 请求去重:相同请求自动合并
  • 离线支持:使用缓存数据
  • 轮询:支持定时刷新

使用 TanStack Query

TanStack Query(原 React Query)是功能强大的数据同步库:

"use client";

import { useQuery } from "@tanstack/react-query";

async function getPosts() {
const res = await fetch("/api/posts");
return res.json();
}

export default function PostList() {
const { data, isLoading, error } = useQuery({
queryKey: ["posts"],
queryFn: getPosts,
});

if (isLoading) return <div>加载中...</div>;
if (error) return <div>加载失败</div>;

return (
<ul>
{data.map((post: Post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}

请求去重

fetch 自动去重

在同一个渲染过程中,相同的 fetch 请求会自动去重:

// 这两个组件中的相同请求只会执行一次
async function UserAvatar() {
const user = await fetch("/api/user").then(r => r.json());
return <img src={user.avatar} />;
}

async function UserName() {
const user = await fetch("/api/user").then(r => r.json()); // 自动去重
return <span>{user.name}</span>;
}

使用 React cache 函数

对于数据库查询等非 fetch 操作,使用 React 的 cache 函数:

import { cache } from "react";
import { db } from "@/lib/db";

// 包装后,同一渲染过程中的相同调用会被去重
export const getUser = cache(async (id: string) => {
return db.user.findUnique({
where: { id },
});
});

预加载数据

预加载可以在阻塞操作之前提前开始数据获取:

import { getItem, preload, checkIsAvailable } from "@/lib/data";

export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;

// 尽早开始加载数据
preload(id);

// 执行其他异步任务
const isAvailable = await checkIsAvailable();

return isAvailable ? <Item id={id} /> : null;
}

// lib/data.ts
import { cache } from "react";
import "server-only";

export const getItem = cache(async (id: string) => {
const res = await fetch(`https://api.example.com/items/${id}`);
return res.json();
});

export const preload = (id: string) => {
// void 表示我们不等待 Promise 完成
void getItem(id);
};

Server Actions

Server Actions 是在服务端执行的函数,可以直接在组件中调用,非常适合处理表单提交和数据变更。

基本 Server Action

// app/actions.ts
"use server";

import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";

export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;

await db.posts.create({
data: { title, content },
});

// 重新验证博客列表页
revalidatePath("/posts");
}

在表单中使用

// app/posts/new/page.tsx
import { createPost } from "../actions";

export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="标题" required />
<textarea name="content" placeholder="内容" required />
<button type="submit">发布</button>
</form>
);
}

带状态的 Server Action

使用 useFormStateuseFormStatus 处理表单状态:

"use client";

import { useFormState, useFormStatus } from "react-dom";
import { createPost } from "../actions";

const initialState = {
message: "",
};

function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "发布中..." : "发布"}
</button>
);
}

export default function NewPostForm() {
const [state, formAction] = useFormState(createPost, initialState);

return (
<form action={formAction}>
<input name="title" required />
<textarea name="content" required />
<SubmitButton />
{state.message && <p>{state.message}</p>}
</form>
);
}

Server Action 的完整实现:

"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { db } from "@/lib/db";

export async function createPost(prevState: { message: string }, formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;

// 验证
if (!title || !content) {
return { message: "请填写所有字段" };
}

try {
await db.posts.create({
data: { title, content },
});
} catch (error) {
return { message: "创建失败" };
}

// 重新验证并重定向
revalidatePath("/posts");
redirect("/posts");
}

数据获取最佳实践

选择合适的获取方式

场景推荐方式原因
SEO 重要页面Server Component + fetch服务端渲染,内容可被抓取
实时数据仪表盘Client Component + SWR自动刷新,实时更新
表单提交Server Actions安全,自动重新验证
敏感数据Server Component数据不暴露给客户端
用户个性化内容Server Component + cookies服务端读取用户状态

错误处理

import { notFound } from "next/navigation";

export default async function PostPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;

let post;
try {
post = await getPost(id);
} catch (error) {
// 可以记录错误日志
console.error("获取文章失败:", error);
throw new Error("获取文章失败");
}

if (!post) {
notFound(); // 显示 not-found.tsx
}

return <article>{post.content}</article>;
}

加载状态

使用 loading.tsx 配合 Suspense 提供良好的加载体验:

// app/posts/loading.tsx
export default function Loading() {
return (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-4 bg-gray-200 rounded w-1/2 mt-2"></div>
</div>
))}
</div>
);
}

小结

本章我们学习了:

  1. 服务端数据获取:async/await、并行获取、顺序获取
  2. 缓存策略:默认不缓存、force-cache、revalidate、按需重新验证
  3. 客户端数据获取:use API、useEffect、SWR、TanStack Query
  4. 请求去重和预加载数据
  5. Server Actions 处理数据变更
  6. 根据场景选择合适的数据获取方式

练习

  1. 创建一个博客列表页面,使用服务端数据获取
  2. 实现一个搜索功能,使用客户端数据获取(SWR 或 TanStack Query)
  3. 创建一个表单,使用 Server Actions 提交数据
  4. 配置缓存策略,实现定时重新验证(ISR)