数据获取
本章将介绍 Next.js 中的数据获取方式,包括服务端数据获取、客户端数据获取和缓存策略。
服务端数据获取
在 Server Component 中可以直接使用 async/await 获取数据。
基本用法
// src/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>
);
}
获取单个资源
// src/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: { id: string };
}) {
const post = await getPost(params.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: { id: string };
}) {
const [user, posts] = await Promise.all([
getUser(params.id),
getUserPosts(params.id),
]);
return (
<div>
<h1>{user.name}</h1>
<ul>
{posts.map((post: Post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
缓存策略
Next.js 提供了灵活的缓存控制选项。
默认缓存
默认情况下,fetch 请求会被缓存:
// 默认行为:缓存直到手动失效
const res = await fetch("https://api.example.com/posts");
禁用缓存
// 每次请求都重新获取
const res = await fetch("https://api.example.com/posts", {
cache: "no-store",
});
定时重新验证
// 每 60 秒重新验证一次
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 },
});
按需重新验证
使用标签进行按需重新验证:
// 获取数据时设置标签
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] },
});
在 Server Action 或 API 路由中触发重新验证:
import { revalidateTag } from "next/cache";
export async function refreshPosts() {
revalidateTag("posts");
}
路径重新验证
import { revalidatePath } from "next/cache";
export async function refreshPostPage() {
revalidatePath("/posts");
}
客户端数据获取
在 Client Component 中使用 React 的数据获取方式。
使用 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>
);
}
使用 TanStack 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>
);
}
Server Actions
Server Actions 是在服务端执行的函数,可以直接在组件中调用。
基本 Server Action
// src/app/actions.ts
"use server";
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 },
});
}
在表单中使用
// src/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
"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";
export async function createPost(prevState: any, 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 |
错误处理
export default async function PostPage({
params,
}: {
params: { id: string };
}) {
let post;
try {
post = await getPost(params.id);
} catch (error) {
throw new Error("获取文章失败");
}
if (!post) {
notFound();
}
return <article>{post.content}</article>;
}
加载状态
使用 loading.tsx 配合 Suspense:
// src/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>
);
}
小结
本章我们学习了:
- 服务端数据获取的基本用法
- 缓存策略的配置
- 客户端数据获取方式
- Server Actions 的使用
- 数据获取最佳实践
练习
- 创建一个博客列表页面,使用服务端数据获取
- 实现一个搜索功能,使用客户端数据获取
- 创建一个表单,使用 Server Actions 提交数据
- 配置缓存策略,实现定时重新验证