跳到主要内容

数据获取

本章将介绍 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>
);
}

小结

本章我们学习了:

  1. 服务端数据获取的基本用法
  2. 缓存策略的配置
  3. 客户端数据获取方式
  4. Server Actions 的使用
  5. 数据获取最佳实践

练习

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