第 7 章:Next.js 数据获取体系

深入理解 Next.js 的数据获取机制:fetch 扩展、缓存策略、revalidate 机制、SSR vs RSC 数据获取,掌握现代全栈应用的数据流设计。

本章是卷 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实时更新、用户体验好
需要 SEOServer 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

特性revalidatePathrevalidateTag
失效范围指定路径指定标签
使用场景单个页面多个页面共享数据
灵活性
推荐简单场景复杂场景

选择指南

  • 如果数据只在一个页面使用 → 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. 博客列表页(展示所有文章)
  2. 博客详情页(展示单篇文章)
  3. 创建文章(表单提交)
  4. 更新文章(表单提交)

数据流

博客列表页 → 获取文章列表 → 缓存 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

  1. fetch 扩展机制

    • cache:缓存策略
    • next.revalidate:重新验证时间
    • next.tags:缓存标签
  2. SSR fetch vs RSC fetch

    • Server Component:直接 await fetch
    • Client Component:useEffect 或 SWR
  3. 缓存分级

    • Request Memoization(请求去重)
    • Data Cache(数据缓存)
    • Full Route Cache(整页缓存)
    • Router Cache(路由缓存)
  4. revalidate 机制

    • revalidatePath:按路径失效
    • revalidateTag:按标签失效
  5. use() 与 await

    • await:Server Component 中使用
    • use():Client Component 中读取 Promise
  6. 最佳实践

    • 默认使用 Server Component
    • 合理使用缓存策略
    • 使用 Tags 管理共享数据
    • 使用 Suspense 优化加载体验

下一步

恭喜!你已经完成了第 7 章的学习。接下来,我们将进入第 8 章:Route Handlers(API 路由),学习如何在 Next.js 中构建 RESTful API。


参考资料

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页