数据获取
本章将介绍 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>
);
}
如果在组件中依次 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 },
});
重新验证的工作原理:
- 首次请求时,数据被缓存
- 60 秒内的后续请求使用缓存数据
- 60 秒后的首次请求仍返回缓存数据,同时在后台重新获取
- 新数据获取成功后,缓存被更新
按需重新验证
为请求添加标签,支持按需失效:
// 获取数据时设置标签
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
使用 useFormState 和 useFormStatus 处理表单状态:
"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>
);
}
小结
本章我们学习了:
- 服务端数据获取:async/await、并行获取、顺序获取
- 缓存策略:默认不缓存、force-cache、revalidate、按需重新验证
- 客户端数据获取:use API、useEffect、SWR、TanStack Query
- 请求去重和预加载数据
- Server Actions 处理数据变更
- 根据场景选择合适的数据获取方式
练习
- 创建一个博客列表页面,使用服务端数据获取
- 实现一个搜索功能,使用客户端数据获取(SWR 或 TanStack Query)
- 创建一个表单,使用 Server Actions 提交数据
- 配置缓存策略,实现定时重新验证(ISR)