本章是卷 III 的开篇章节。读完本章,你将深入理解 Next.js 的数据获取体系,掌握 fetch 扩展机制、缓存策略和 revalidate 机制,能够设计高性能的数据流架构。
7.1 fetch 的扩展机制
Next.js 对 fetch 的增强
Next.js 扩展了原生的 fetch API,添加了缓存、标签和 revalidate 功能。
原生 fetch:
const res = await fetch('https://api.example.com/data');
const data = await res.json();
Next.js 扩展 fetch:
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache', // 缓存策略
next: {
revalidate: 60, // 60 秒后重新验证
tags: ['data-cache'], // 缓存标签
},
});
const data = await res.json();
扩展选项详解
1. cache:缓存策略
选项:
'force-cache'(默认):强制使用缓存,如果没有缓存则请求并缓存'no-store':不使用缓存,每次都请求'reload':不使用缓存,请求后更新缓存'no-cache':检查缓存,如果有则验证,没有则请求
示例:
// 强制缓存(默认)
const data = await fetch('https://api.example.com/posts', {
cache: 'force-cache',
});
// 不缓存(动态数据)
const data = await fetch('https://api.example.com/user', {
cache: 'no-store',
});
// 重新加载
const data = await fetch('https://api.example.com/data', {
cache: 'reload',
});
2. next.revalidate:重新验证时间
作用:设置缓存的有效期(秒),过期后重新请求。
// 每 60 秒重新验证
const data = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 },
});
行为:
T+0s 请求数据,缓存结果
T+30s 请求数据,返回缓存(未过期)
T+60s 请求数据,返回缓存,后台重新验证
T+61s 请求数据,返回新数据
适用场景:
- 博客文章列表(每小时更新)
- 商品信息(每 10 分钟更新)
- 新闻列表(每 5 分钟更新)
3. next.tags:缓存标签
作用:为缓存添加标签,方便按标签失效。
// 添加标签
const posts = await fetch('https://api.example.com/posts', {
next: { tags: ['blog-posts'] },
});
const comments = await fetch('https://api.example.com/comments', {
next: { tags: ['blog-comments'] },
});
失效标签:
import { revalidateTag } from 'next/cache';
// 失效所有带 'blog-posts' 标签的缓存
revalidateTag('blog-posts');
适用场景:
- 多个页面共享的数据
- 需要批量失效的缓存
- 复杂的数据依赖关系
缓存策略对比
| 策略 | 缓存行为 | 适用场景 | 性能 |
|---|---|---|---|
force-cache | 永久缓存 | 静态内容 | ⭐⭐⭐⭐⭐ |
revalidate: 60 | 定时重新验证 | 半动态内容 | ⭐⭐⭐⭐ |
tags: ['xxx'] | 按标签失效 | 共享数据 | ⭐⭐⭐⭐ |
no-store | 不缓存 | 动态数据 | ⭐⭐ |
7.2 SSR fetch vs RSC fetch
两种数据获取方式
1. Server Component 中的数据获取
特点:直接在组件中 await fetch,服务端执行。
// app/posts/page.tsx(Server Component)
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 },
});
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
优势:
- ✅ 代码简洁,无需
useEffect - ✅ 直接访问数据库
- ✅ 自动缓存
- ✅ 零客户端 JS
劣势:
- ❌ 不可交互
- ❌ 不能使用 Hooks
2. Client Component 中的数据获取
特点:使用 useEffect 或数据获取库(SWR、React Query)。
// app/posts/PostsClient.tsx(Client Component)
"use client";
import { useEffect, useState } from 'react';
export default function PostsClient() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('https://api.example.com/posts')
.then(r => r.json())
.then(data => {
setPosts(data);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
优势:
- ✅ 可交互
- ✅ 可以使用 Hooks
- ✅ 实时更新
劣势:
- ❌ 代码复杂
- ❌ 需要管理 loading/error 状态
- ❌ 客户端 JS 体积大
使用 SWR 简化客户端数据获取
// app/posts/PostsClient.tsx
"use client";
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(r => r.json());
export default function PostsClient() {
const { data: posts, error, isLoading } = useSWR(
'https://api.example.com/posts',
fetcher
);
if (error) return <div>Error loading posts</div>;
if (isLoading) return <div>Loading...</div>;
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
SWR 的优势:
- ✅ 自动缓存和重新验证
- ✅ 自动处理 loading/error 状态
- ✅ 支持乐观更新
- ✅ 支持分页和无限滚动
选择指南
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 展示型页面 | Server Component | 代码简洁、性能好 |
| 交互式页面 | Client Component + SWR | 实时更新、用户体验好 |
| 需要 SEO | Server Component | 服务端渲染 |
| 实时数据 | Client Component + SWR | 自动重新验证 |
| 需要直接访问数据库 | Server Component | 无需 API 层 |
7.3 缓存分级
Next.js 的 4 层缓存系统
┌─────────────────────────────────────────┐
│ 第 1 层:Request Memoization │
│ - 单次请求内去重 │
│ - 自动启用 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 第 2 层:Data Cache │
│ - 跨请求持久化 │
│ - fetch 扩展控制 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 第 3 层:Full Route Cache │
│ - 整页缓存 │
│ - 静态页面自动启用 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 第 4 层:Router Cache │
│ - 客户端路由缓存 │
│ - 自动启用 │
└─────────────────────────────────────────┘
第 1 层:Request Memoization(请求去重)
作用:在单次请求中,自动去重相同的 fetch 调用。
示例:
// app/layout.tsx
async function getUser() {
const res = await fetch('https://api.example.com/user');
return res.json();
}
export default async function Layout() {
const user = await getUser(); // 请求 1
return (
<div>
<Header />
<Sidebar user={user} />
<Main />
</div>
);
}
// app/Header.tsx
async function getUser() {
const res = await fetch('https://api.example.com/user');
return res.json();
}
export default async function Header() {
const user = await getUser(); // 请求 2(自动去重)
return <header>Welcome, {user.name}!</header>;
}
行为:
请求 1:Layout 调用 fetch('/user')
↓
Next.js 缓存结果
↓
请求 2:Header 调用 fetch('/user')
↓
Next.js 返回缓存结果(不发起网络请求)
优势:
- ✅ 避免重复请求
- ✅ 提升性能
- ✅ 无需手动管理
第 2 层:Data Cache(数据缓存)
作用:跨请求持久化缓存 fetch 结果。
示例:
// 定时缓存(ISR)
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }, // 1 小时
});
// 标签缓存
const comments = await fetch('https://api.example.com/comments', {
next: { tags: ['blog-comments'] },
});
// 不缓存
const user = await fetch('https://api.example.com/user', {
cache: 'no-store',
});
缓存行为:
缓存命中:
fetch → 检查 Data Cache → 返回缓存数据
缓存未命中:
fetch → 检查 Data Cache → 未命中 → 发起网络请求 → 缓存结果
第 3 层:Full Route Cache(整页缓存)
作用:缓存整个页面的 RSC Payload 和 HTML。
静态页面(自动缓存):
// app/about/page.tsx
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>We are a company.</p>
</div>
);
}
动态页面(禁用缓存):
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic';
export default async function DashboardPage() {
const user = await getUser();
return (
<div>
<h1>Welcome, {user.name}!</h1>
</div>
);
}
第 4 层:Router Cache(路由缓存)
作用:在客户端浏览器中缓存已访问页面的 RSC Payload。
行为:
1. 用户访问 /home
→ 下载 /home 的 RSC Payload
→ 缓存到 Router Cache
2. 用户点击 "About"
→ 导航到 /about
→ 下载 /about 的 RSC Payload
→ 缓存到 Router Cache
3. 用户点击浏览器"后退"
→ 返回 /home
→ 检查 Router Cache
→ 缓存命中 → 立即显示(无网络请求)
优势:
- ✅ 即时导航
- ✅ 保留滚动位置
- ✅ 减少服务器负载
缓存策略最佳实践
1. 静态内容:使用 Full Route Cache
// 静态页面(构建时缓存)
export default function AboutPage() {
return <div>About Us</div>;
}
2. 半动态内容:使用 Data Cache + ISR
// 博客列表(每小时更新)
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 },
});
return res.json();
}
3. 动态内容:禁用缓存
// 用户 Dashboard(每次请求都不同)
export const dynamic = 'force-dynamic';
async function getDashboard() {
const res = await fetch('https://api.example.com/dashboard', {
cache: 'no-store',
});
return res.json();
}
4. 共享数据:使用 Tags
// 多个页面共享的数据
async function getUser() {
const res = await fetch('https://api.example.com/user', {
next: { tags: ['user-data'] },
});
return res.json();
}
// 更新时失效所有相关页面
async function updateUser(formData: FormData) {
"use server";
await db.query('UPDATE users ...');
revalidateTag('user-data');
}
7.4 revalidatePath 与 revalidateTag
revalidatePath:按路径失效
作用:失效指定路径的缓存。
import { revalidatePath } from 'next/cache';
// 失效单个页面
revalidatePath('/blog');
// 失效动态路由
revalidatePath('/blog/[slug]');
// 失效所有博客文章
revalidatePath('/blog/[slug]', 'page');
// 失效整个目录
revalidatePath('/blog', 'layout');
示例:创建新文章后失效博客列表
// 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.query(
'INSERT INTO posts (title, content) VALUES (?, ?)',
[title, content]
);
// 失效博客列表缓存
revalidatePath('/blog');
}
// app/blog/new/page.tsx
import { createPost } from '@/app/actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" />
<textarea name="content" placeholder="Content" />
<button type="submit">Create Post</button>
</form>
);
}
revalidateTag:按标签失效
作用:失效所有带指定标签的缓存。
import { revalidateTag } from 'next/cache';
// 失效所有带 'blog-posts' 标签的缓存
revalidateTag('blog-posts');
示例:更新文章后失效相关缓存
// app/actions.ts
"use server";
import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';
export async function updatePost(id: string, formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.query(
'UPDATE posts SET title = ?, content = ? WHERE id = ?',
[title, content, id]
);
// 失效所有带 'blog-posts' 标签的缓存
revalidateTag('blog-posts');
}
// app/blog/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { tags: ['blog-posts'] },
});
return res.json();
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
// app/blog/[slug]/page.tsx
async function getPost(slug: string) {
const res = await fetch(`https://api.example.com/posts/${slug}`, {
next: { tags: ['blog-posts'] },
});
return res.json();
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
效果:
- 更新文章后,
/blog和/blog/[slug]都会失效 - 下次访问时重新获取数据
revalidatePath vs revalidateTag
| 特性 | revalidatePath | revalidateTag |
|---|---|---|
| 失效范围 | 指定路径 | 指定标签 |
| 使用场景 | 单个页面 | 多个页面共享数据 |
| 灵活性 | 低 | 高 |
| 推荐 | 简单场景 | 复杂场景 |
选择指南:
- 如果数据只在一个页面使用 →
revalidatePath - 如果数据在多个页面使用 →
revalidateTag
7.5 use() 与 await 实战
await:Server Component 中的数据获取
适用场景:Server Component 中直接获取数据。
// app/posts/page.tsx(Server Component)
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 (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
优势:
- ✅ 代码简洁
- ✅ 自动缓存
- ✅ 零客户端 JS
use():Client Component 中读取 Promise
适用场景:Client Component 中读取 Server Component 传递的 Promise。
// app/posts/page.tsx(Server Component)
import PostsList from './PostsList';
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
export default function PostsPage() {
const postsPromise = getPosts();
return <PostsList postsPromise={postsPromise} />;
}
// app/posts/PostsList.tsx(Client Component)
"use client";
import { use } from 'react';
export default function PostsList({
postsPromise,
}: {
postsPromise: Promise<any[]>;
}) {
const posts = use(postsPromise);
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
优势:
- ✅ 在 Client Component 中使用 Server Component 的数据
- ✅ 自动 Suspense
- ✅ 支持流式渲染
use() 与 Suspense 结合
// app/page.tsx(Server Component)
import { Suspense } from 'react';
import PostsList from './PostsList';
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
export default function Page() {
const postsPromise = getPosts();
return (
<div>
<h1>Blog</h1>
<Suspense fallback={<div>Loading posts...</div>}>
<PostsList postsPromise={postsPromise} />
</Suspense>
</div>
);
}
// app/PostsList.tsx(Client Component)
"use client";
import { use } from 'react';
export default function PostsList({
postsPromise,
}: {
postsPromise: Promise<any[]>;
}) {
const posts = use(postsPromise);
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
行为:
1. 服务端开始获取 posts
2. 立即返回 Suspense fallback
3. posts 获取完成
4. 替换为 PostsList
use() 与 Context 结合
// app/theme-context.tsx
"use client";
import { createContext, useContext } from 'react';
type Theme = 'light' | 'dark';
const ThemeContext = createContext<Promise<Theme> | null>(null);
export function ThemeProvider({
children,
themePromise,
}: {
children: React.ReactNode;
themePromise: Promise<Theme>;
}) {
return (
<ThemeContext.Provider value={themePromise}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const themePromise = useContext(ThemeContext);
if (!themePromise) {
throw new Error('useTheme must be used within ThemeProvider');
}
return use(themePromise);
}
// app/layout.tsx(Server Component)
import { ThemeProvider } from './theme-context';
async function getTheme(): Promise<'light' | 'dark'> {
// 从数据库或 Cookie 获取主题
return 'light';
}
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const themePromise = getTheme();
return (
<html>
<body>
<ThemeProvider themePromise={themePromise}>
{children}
</ThemeProvider>
</body>
</html>
);
}
// app/ThemeToggle.tsx(Client Component)
"use client";
import { useTheme } from './theme-context';
export default function ThemeToggle() {
const theme = useTheme();
return <div>Current theme: {theme}</div>;
}
use() 的限制
只能在以下位置使用:
- ✅ Client Component
- ✅ Server Component(React 19+)
- ❌ 不能在 Hooks 中使用
- ❌ 不能在条件语句中使用
错误示例:
// ❌ 错误:在条件语句中使用
function Component({ promise }) {
if (someCondition) {
const data = use(promise); // ❌ 错误
return <div>{data}</div>;
}
return <div>No data</div>;
}
正确示例:
// ✅ 正确:在顶层使用
function Component({ promise }) {
const data = use(promise);
if (someCondition) {
return <div>{data}</div>;
}
return <div>No data</div>;
}
7.6 实战:博客系统数据获取
需求分析
功能:
- 博客列表页(展示所有文章)
- 博客详情页(展示单篇文章)
- 创建文章(表单提交)
- 更新文章(表单提交)
数据流:
博客列表页 → 获取文章列表 → 缓存 1 小时
博客详情页 → 获取单篇文章 → 缓存 1 小时
创建文章 → 写入数据库 → 失效列表缓存
更新文章 → 更新数据库 → 失效相关缓存
完整实现
1. 数据库层
// lib/db.ts
import { PrismaClient } from '@prisma/client';
export const db = new PrismaClient();
export type Post = {
id: string;
title: string;
content: string;
slug: string;
createdAt: Date;
updatedAt: Date;
};
2. 博客列表页
// app/blog/page.tsx(Server Component)
import Link from 'next/link';
import { db } from '@/lib/db';
async function getPosts() {
const posts = await db.post.findMany({
orderBy: { createdAt: 'desc' },
take: 10,
});
return posts;
}
export default async function BlogPage() {
const posts = await getPosts();
return (
<div className="max-w-4xl mx-auto p-8">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">Blog</h1>
<Link
href="/blog/new"
className="bg-blue-600 text-white px-4 py-2 rounded"
>
New Post
</Link>
</div>
<ul className="space-y-4">
{posts.map(post => (
<li key={post.id} className="border p-4 rounded">
<Link href={`/blog/${post.slug}`}>
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-gray-600 mt-2">
{post.content.substring(0, 100)}...
</p>
<p className="text-sm text-gray-400 mt-2">
{new Date(post.createdAt).toLocaleDateString()}
</p>
</Link>
</li>
))}
</ul>
</div>
);
}
3. 博客详情页
// app/blog/[slug]/page.tsx(Server Component)
import { notFound } from 'next/navigation';
import { db } from '@/lib/db';
import EditButton from './EditButton';
async function getPost(slug: string) {
const post = await db.post.findUnique({
where: { slug },
});
if (!post) {
return null;
}
return post;
}
export default async function PostPage({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
if (!post) {
notFound();
}
return (
<div className="max-w-4xl mx-auto p-8">
<article>
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<p className="text-gray-600 mb-8">
{new Date(post.createdAt).toLocaleDateString()}
</p>
<div className="prose">{post.content}</div>
</article>
<EditButton postId={post.id} />
</div>
);
}
// app/blog/[slug]/EditButton.tsx(Client Component)
"use client";
import Link from 'next/link';
export default function EditButton({ postId }: { postId: string }) {
return (
<Link
href={`/blog/edit/${postId}`}
className="mt-8 inline-block bg-gray-600 text-white px-4 py-2 rounded"
>
Edit Post
</Link>
);
}
4. 创建文章
// app/blog/new/page.tsx(Client Component)
"use client";
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { createPost } from '@/app/actions';
export default function NewPostPage() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsSubmitting(true);
const formData = new FormData(e.currentTarget);
await createPost(formData);
setIsSubmitting(false);
router.push('/blog');
router.refresh();
};
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">New Post</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block mb-2">Title</label>
<input
name="title"
type="text"
required
className="w-full border p-2 rounded"
/>
</div>
<div>
<label className="block mb-2">Slug</label>
<input
name="slug"
type="text"
required
className="w-full border p-2 rounded"
/>
</div>
<div>
<label className="block mb-2">Content</label>
<textarea
name="content"
required
rows={10}
className="w-full border p-2 rounded"
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
>
{isSubmitting ? 'Creating...' : 'Create Post'}
</button>
</form>
</div>
);
}
// app/actions.ts(Server Actions)
"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 slug = formData.get('slug') as string;
const content = formData.get('content') as string;
await db.post.create({
data: { title, slug, content },
});
// 失效博客列表缓存
revalidatePath('/blog');
}
export async function updatePost(id: string, formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.post.update({
where: { id },
data: { title, content },
});
// 失效博客列表和详情页缓存
revalidatePath('/blog');
revalidatePath(`/blog/[slug]`, 'page');
}
5. 更新文章
// app/blog/edit/[id]/page.tsx(Server Component + Client Component)
import { notFound } from 'next/navigation';
import { db } from '@/lib/db';
import EditForm from './EditForm';
async function getPost(id: string) {
const post = await db.post.findUnique({
where: { id },
});
if (!post) {
return null;
}
return post;
}
export default async function EditPostPage({
params,
}: {
params: { id: string };
}) {
const post = await getPost(params.id);
if (!post) {
notFound();
}
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">Edit Post</h1>
<EditForm post={post} />
</div>
);
}
// app/blog/edit/[id]/EditForm.tsx(Client Component)
"use client";
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { updatePost } from '@/app/actions';
type Post = {
id: string;
title: string;
content: string;
};
export default function EditForm({ post }: { post: Post }) {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsSubmitting(true);
const formData = new FormData(e.currentTarget);
await updatePost(post.id, formData);
setIsSubmitting(false);
router.push(`/blog/${post.slug}`);
router.refresh();
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block mb-2">Title</label>
<input
name="title"
type="text"
defaultValue={post.title}
required
className="w-full border p-2 rounded"
/>
</div>
<div>
<label className="block mb-2">Content</label>
<textarea
name="content"
required
rows={10}
defaultValue={post.content}
className="w-full border p-2 rounded"
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
>
{isSubmitting ? 'Updating...' : 'Update Post'}
</button>
</form>
);
}
性能优化
1. 添加缓存
// app/blog/page.tsx
async function getPosts() {
const posts = await db.post.findMany({
orderBy: { createdAt: 'desc' },
take: 10,
});
return posts;
}
// 添加 ISR 缓存
export const revalidate = 3600; // 1 小时
2. 使用 Suspense
// app/blog/page.tsx
import { Suspense } from 'react';
export default function BlogPage() {
return (
<div>
<h1>Blog</h1>
<Suspense fallback={<div>Loading posts...</div>}>
<PostsList />
</Suspense>
</div>
);
}
async function PostsList() {
const posts = await getPosts();
return <ul>{/* ... */}</ul>;
}
本章小结
Key Takeaways
fetch 扩展机制:
cache:缓存策略next.revalidate:重新验证时间next.tags:缓存标签
SSR fetch vs RSC fetch:
- Server Component:直接
await fetch - Client Component:
useEffect或 SWR
- Server Component:直接
缓存分级:
- Request Memoization(请求去重)
- Data Cache(数据缓存)
- Full Route Cache(整页缓存)
- Router Cache(路由缓存)
revalidate 机制:
revalidatePath:按路径失效revalidateTag:按标签失效
use() 与 await:
await:Server Component 中使用use():Client Component 中读取 Promise
最佳实践:
- 默认使用 Server Component
- 合理使用缓存策略
- 使用 Tags 管理共享数据
- 使用 Suspense 优化加载体验
下一步
恭喜!你已经完成了第 7 章的学习。接下来,我们将进入第 8 章:Route Handlers(API 路由),学习如何在 Next.js 中构建 RESTful API。
参考资料
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。