Skip to content
第 22 章 前端 ⏱ 14 分钟阅读

第 22 章:Next.js 数据获取 ​

学习目标 ​

  • 掌握 Server Component 中 fetch 的 4 种缓存策略
  • 学会 Route Handlers(API Routes)
  • 理解 Server Actions(React 19/Next 14+)
  • 避开数据获取的 4 个常见坑

一、为什么在 Server Component 取数? ​

tsx
// app/users/page.tsx(Server)
async function getUsers() {
  const r = await fetch('https://api.example.com/users', {
    next: { revalidate: 60 },       // ISR:60 秒缓存
  });
  return r.json();
}

export default async function UsersPage() {
  const users = await getUsers();
  return (
    <ul>
      {users.map((u: User) => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
}

优势:

  • 直接在服务器 await,无 loading 抖动
  • 自动数据去重
  • SEO 友好
  • 不暴露 API key

二、4 种缓存策略 ​

tsx
// 1. 静态(默认,构建时)
fetch(url, { cache: 'force-cache' });

// 2. 不缓存(每次请求重新拿)
fetch(url, { cache: 'no-store' });

// 3. ISR(指定秒数后过期)
fetch(url, { next: { revalidate: 60 } });

// 4. 标签失效(精确控制)
fetch(url, { next: { tags: ['users'] } });

revalidateTag 主动失效 ​

tsx
// app/api/users/route.ts
import { revalidateTag } from 'next/cache';

export async function POST() {
  // 写操作后失效
  revalidateTag('users');
}

三、Suspense 流式渲染 ​

tsx
// app/page.tsx
import { Suspense } from 'react';

async function SlowData() {
  await new Promise(r => setTimeout(r, 2000));
  const r = await fetch('https://slow-api/data');
  return <div>{await r.text()}</div>;
}

export default function Page() {
  return (
    <>
      <h1>立即显示</h1>
      <Suspense fallback={<Skeleton />}>
        <SlowData />     {/* 慢的部分流式加载 */}
      </Suspense>
    </>
  );
}

四、Route Handlers(API 路由) ​

tsx
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(req: NextRequest) {
  const users = await db.users.findAll();
  return NextResponse.json(users);
}

export async function POST(req: NextRequest) {
  const body = await req.json();
  const user = await db.users.create(body);
  return NextResponse.json(user, { status: 201 });
}

支持的 HTTP 方法:GET / POST / PUT / PATCH / DELETE / OPTIONS / HEAD。

⚠️ 坑 1:route.ts 文件名固定,不能叫 api.ts;同目录下有 page.tsx 会冲突。

五、动态路由 API ​

tsx
// app/api/users/[id]/route.ts
export async function GET(req: NextRequest, { params }: { params: { id: string } }) {
  const user = await db.users.findById(params.id);
  if (!user) return NextResponse.json({ error: 'Not found' }, { status: 404 });
  return NextResponse.json(user);
}

六、Server Actions(RSC 表单) ​

Next.js 14 引入,直接在服务端处理表单提交,不需要写 API:

tsx
// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function createUser(formData: FormData) {
  const name = formData.get('name') as string;
  await db.users.create({ name });
  revalidatePath('/users');     // ⚠️ 失效缓存
}

// app/users/page.tsx
import { createUser } from '../actions';

export default function Page() {
  return (
    <form action={createUser}>
      <input name="name" />
      <button type="submit">提交</button>
    </form>
  );
}

优势:

  • 无需写 API Route
  • 自动 CSRF 保护
  • 与 Server Component 无缝衔接

⚠️ 坑 2:'use server' 必须放在文件顶部或函数顶部。

七、Server Action + useFormState ​

tsx
'use server';

export async function login(prev: State, formData: FormData) {
  const email = formData.get('email');
  if (!email) return { error: '邮箱必填' };
  // ...
  return { error: null };
}

// 'use client';
import { useFormState } from 'react-dom';

const initial = { error: null };

export function Form() {
  const [state, action] = useFormState(login, initial);
  return (
    <form action={action}>
      <input name="email" />
      {state.error && <span>{state.error}</span>}
      <button>登录</button>
    </form>
  );
}

八、客户端数据获取 ​

服务端组件 + SWR / TanStack Query:

tsx
'use client';

import useSWR from 'swr';

function Users() {
  const { data, error } = useSWR('/api/users', fetcher);
  if (error) return <div>失败</div>;
  if (!data) return <div>加载</div>;
  return <ul>{data.map(...)}</ul>;
}

九、Streaming 与 Partial Prerendering ​

Next.js 14 引入 PPR(实验),静态壳 + 动态流:

tsx
// next.config.js
module.exports = {
  experimental: { ppr: true },
};

// 页面
export const experimental_ppr = true;
export default async function Page() {
  return (
    <>
      <StaticHeader />
      <Suspense fallback={<Loading />}>
        <DynamicContent />
      </Suspense>
    </>
  );
}

⚠️ 坑 3:PPR 还在实验,生产前查最新文档。

十、本章小结 ​

要点关键
Server fetch默认静态,可设 revalidate/no-store/tags
缓存失效revalidateTag / revalidatePath
流式渲染<Suspense> 包异步组件
Route Handlerapp/api/.../route.ts
Server Actions'use server',直接处理表单
客户端数据SWR / TanStack Query
PPR实验特性,静态壳 + 动态流

动手练习 ​

  1. 静态数据:/users 服务端组件 fetch 用户列表
  2. 动态 API:/api/users GET/POST,支持增删
  3. Server Action:做一个留言表单,提交后 revalidate
  4. 找坑:以下代码为什么 build 报错?
    tsx
    // app/page.tsx
    'use client';
    export default async function Page() { ... }   // async + 'use client' 冲突

下一章:第 23 章:Tailwind CSS →

本站基于 VitePress 构建 · 由 StackHub 团队维护