跳到主要内容

Next.js 知识速查表

本页面汇总了 Next.js 编程中最常用的语法和知识点,方便快速查阅。

Next.js 15 重要变更

Turbopack 默认启用

Turbopack 是 Next.js 16+ 的默认打包器,使用 Rust 编写,提供更快的开发体验:

# 默认使用 Turbopack
npm run dev

# 切换到 Webpack
npm run dev -- --webpack
npm run build -- --webpack

配置选项:

// next.config.ts
const nextConfig = {
turbopack: {
resolveAlias: {
"@components": "./src/components",
},
resolveExtensions: [".mdx", ".tsx", ".ts", ".jsx", ".js", ".json"],
},
};

支持特性:

  • JavaScript、TypeScript、JSX/TSX
  • CSS Modules、全局 CSS、Sass/SCSS、PostCSS
  • React Server Components、Fast Refresh
  • 路径别名、JSON 导入

不支持:

  • Webpack 插件
  • sassOptions.functions(自定义 Sass 函数)
  • Yarn PnP

部分预渲染(PPR)

PPR 结合静态预渲染和动态流式渲染,在单个请求中提供静态外壳和动态内容:

// next.config.ts
const nextConfig = {
experimental: {
ppr: true,
},
};
// 页面级别启用
export const experimental_ppr = true;

export default function Page() {
return (
<div>
{/* 静态内容:立即返回 */}
<Navigation />
<ArticleContent />

{/* 动态内容:流式传输 */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>

{/* 静态内容:立即返回 */}
<Footer />
</div>
);
}

异步请求 API

Next.js 15 将以下动态 API 改为异步:

// cookies
const cookieStore = await cookies();

// headers
const headersList = await headers();

// draftMode
const { isEnabled } = await draftMode();

// params(在 page.tsx, layout.tsx, route.ts 中)
type Params = Promise<{ slug: string }>;
const { slug } = await params;

// searchParams(在 page.tsx 中)
type SearchParams = Promise<{ q?: string }>;
const { q } = await searchParams;

缓存行为变更

// Next.js 15: fetch 默认不缓存
const data = await fetch("https://api.example.com/data");

// 需要缓存时显式启用
const data = await fetch("https://api.example.com/data", {
cache: "force-cache",
});

// 或使用页面级别配置
export const fetchCache = "default-cache";

use cache 指令

use cache 指令允许你标记路由、组件或函数为可缓存的。这是 Next.js 16 的核心特性。

// 启用配置:next.config.ts
import type { NextConfig } from "next";

// Next.js 16
const nextConfig: NextConfig = {
cacheComponents: true,
};

// Next.js 15(实验性)
const nextConfig: NextConfig = {
experimental: {
useCache: true,
},
};

// 文件级别 - 所有导出函数被缓存
"use cache";

export async function getData() {
const res = await fetch("/api/data");
return res.json();
}

// 函数级别
export async function getProducts(category: string) {
"use cache";
const res = await fetch(`/api/products?category=${category}`);
return res.json();
}

// 组件级别
export async function ProductList({ category }: { category: string }) {
"use cache";
const products = await getProducts(category);
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

// cacheLife 配置生命周期
import { cacheLife } from "next/cache";

export async function getProducts() {
"use cache";
cacheLife("hours"); // seconds | minutes | hours | days | weeks | max

// 或自定义配置
cacheLife({
stale: 3600, // 客户端缓存 1 小时
revalidate: 900, // 服务器 15 分钟验证一次
expire: 86400, // 1 天后过期
});
}

// cacheTag 标签化缓存
import { cacheTag } from "next/cache";

export async function getPosts() {
"use cache";
cacheTag("posts");
// ...
}

// 按需重新验证
import { revalidateTag, updateTag } from "next/cache";

revalidateTag("posts"); // 重新验证
updateTag("posts"); // 更新并重新验证

注意事项

  • 缓存函数不能直接使用 cookies()headers() 等请求时 API
  • 参数和返回值必须是可序列化的
  • 可以使用 children 或其他组合模式传递非序列化值(透传模式)

新增函数

import { forbidden, unauthorized } from "next/navigation";
import { after } from "next/server";

// 启用配置(forbidden 和 unauthorized 需要此配置)
// next.config.ts
const nextConfig: NextConfig = {
experimental: {
authInterrupts: true,
},
};

// forbidden - 显示 403 页面(权限不足)
// 用于已认证但权限不足的情况
if (session.role !== "admin") {
forbidden(); // 渲染 forbidden.tsx
}

// unauthorized - 显示 401 页面(未认证)
// 用于未登录的情况
if (!session) {
unauthorized(); // 渲染 unauthorized.tsx
}

// after - 在响应完成后执行
// 用于日志、分析等不应阻塞响应的任务
after(async () => {
await logAnalytics();
});

注意事项

  • forbiddenunauthorized 不能在根布局中调用
  • 可在 Server Components、Server Actions、Route Handlers 中使用
  • 需要创建对应的 forbidden.tsxunauthorized.tsx 来自定义 UI

项目结构

app/
├── layout.tsx # 根布局
├── page.tsx # 首页 (/)
├── loading.tsx # 加载状态
├── error.tsx # 错误处理
├── not-found.tsx # 404 页面
├── globals.css # 全局样式
├── favicon.ico # 网站图标
├── api/ # API 路由
│ └── hello/
│ └── route.ts # /api/hello
├── blog/
│ ├── layout.tsx # 博客布局
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/:slug
├── @modal/ # 并行路由插槽
│ ├── default.tsx
│ └── (.)photo/
│ └── [id]/
│ └── page.tsx
└── (group)/ # 路由组(不影响 URL)
└── page.tsx

路由

页面定义

// app/page.tsx
export default function Page() {
return <div>首页</div>;
}

// app/about/page.tsx
export default function AboutPage() {
return <div>关于我们</div>;
}

动态路由

// app/blog/[slug]/page.tsx
export default function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
// Next.js 15+ 中 params 是 Promise
const { slug } = await params; // 在 async 组件中
return <div>文章: {slug}</div>;
}

// 捕获所有路由
// app/docs/[...slug]/page.tsx
export default function DocsPage({
params,
}: {
params: Promise<{ slug: string[] }>;
}) {
const { slug } = await params;
return <div>路径: {slug.join("/")}</div>;
}

查询参数

// Server Component
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
const { q } = await searchParams; // Next.js 15+ 中是 Promise
return <div>搜索: {q}</div>;
}

// Client Component
"use client";
import { useSearchParams } from "next/navigation";

export default function SearchFilters() {
const searchParams = useSearchParams();
const q = searchParams.get("q");
return <div>搜索: {q}</div>;
}

导航

import Link from "next/link";

// Link 组件
<Link href="/">首页</Link>
<Link href="/blog/hello">文章</Link>
<Link href={{ pathname: "/search", query: { q: "test" } }}>搜索</Link>

// 编程式导航
"use client";
import { useRouter } from "next/navigation";

export default function Component() {
const router = useRouter();

router.push("/path"); // 导航
router.replace("/path"); // 替换
router.back(); // 返回
router.refresh(); // 刷新
}

并行路由

定义插槽

// 目录结构
// app/
// ├── layout.tsx
// ├── @team/page.tsx
// └── @analytics/page.tsx

// app/layout.tsx
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode;
team: React.ReactNode;
analytics: React.ReactNode;
}) {
return (
<>
{children}
{team}
{analytics}
</>
);
}

默认回退

// app/@analytics/default.tsx
export default function Default() {
return null; // 或占位内容
}

拦截路由

// 目录结构约定
// (.) 同级
// (..) 上一级
// (..)(..) 上两级
// (...) 根目录

// app/@modal/(.)photo/[id]/page.tsx
// 拦截同级的 photo 路由
export default async function PhotoModal({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return <Modal><Photo id={id} /></Modal>;
}

布局

根布局

// app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "我的网站",
description: "网站描述",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body>{children}</body>
</html>
);
}

嵌套布局

// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="flex">
<nav>侧边栏</nav>
<main>{children}</main>
</div>
);
}

数据获取

Server Component

// Next.js 15: 默认不缓存
const res = await fetch("https://api.example.com/data");

// 显式启用缓存
const res = await fetch("https://api.example.com/data", {
cache: "force-cache",
});

// 禁用缓存(显式声明)
const res = await fetch("https://api.example.com/data", {
cache: "no-store",
});

// 定时重新验证
const res = await fetch("https://api.example.com/data", {
next: { revalidate: 60 },
});

// 标签重新验证
const res = await fetch("https://api.example.com/data", {
next: { tags: ["data"] },
});

// 页面级别缓存配置
export const fetchCache = "default-cache"; // 所有 fetch 默认缓存
export const revalidate = 3600; // 每小时重新验证

重新验证

import { revalidateTag, revalidatePath } from "next/cache";

// 按标签重新验证
revalidateTag("data");

// 按路径重新验证
revalidatePath("/posts");

Client Component

"use client";
import useSWR from "swr";

const fetcher = (url: string) => fetch(url).then((r) => r.json());

export default function Component() {
const { data, error, isLoading } = useSWR("/api/data", fetcher);

if (isLoading) return <div>加载中...</div>;
if (error) return <div>出错了</div>;
return <div>{data}</div>;
}

流式渲染

loading.tsx

// app/posts/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse space-y-4">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
</div>
);
}

手动 Suspense

import { Suspense } from "react";

export default function Page() {
return (
<div>
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
<Suspense fallback={<Skeleton />}>
<AnotherComponent />
</Suspense>
</div>
);
}

Form 组件

Next.js 15 引入的 <Form> 组件扩展了 HTML 表单,提供预取和客户端导航:

import Form from "next/form";

// GET 导航(action 为字符串)
<Form action="/search">
<input name="query" />
<button type="submit">搜索</button>
</Form>
// 提交后导航到 /search?query=xxx

// 数据变更(action 为 Server Action)
<Form action={createPost}>
<input name="title" />
<button type="submit">发布</button>
</Form>

属性

// action 为字符串时的属性
<Form
action="/search" // 目标路径
replace={false} // 使用 replace 替代 push
scroll={true} // 导航后滚动到顶部
prefetch={true} // 进入视口时预取目标页面
>
<input name="q" />
<button type="submit">搜索</button>
</Form>

// action 为函数时(Server Action)
<Form action={submitForm}>
{/* replace, scroll, prefetch 会被忽略 */}
</Form>

// 空字符串清空当前搜索参数
<Form action="">
<button type="submit">清除筛选</button>
</Form>

加载状态

"use client";

import { useFormStatus } from "react-dom";

function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "搜索中..." : "搜索"}
</button>
);
}

// 使用
<Form action="/search">
<input name="query" />
<SubmitButton />
</Form>

formAction 覆盖

<Form action={saveDraft}>
<input name="title" />
<button type="submit">保存草稿</button>
<button formAction={publishPost}>发布</button>
</Form>

Server Actions

// app/actions.ts
"use server";

import { revalidatePath } from "next/cache";

export async function createPost(formData: FormData) {
const title = formData.get("title") as string;

await db.post.create({ data: { title } });

revalidatePath("/posts");
}

// 使用
<form action={createPost}>
<input name="title" />
<button type="submit">提交</button>
</form>

带状态的表单(React 19)

"use client";

import { useActionState } from "react";
import { createPost } from "./actions";

function SubmitButton({ pending }: { pending: boolean }) {
return <button disabled={pending}>{pending ? "提交中..." : "提交"}</button>;
}

export default function Form() {
// useActionState 返回 [state, dispatchAction, isPending]
const [state, formAction, isPending] = useActionState(createPost, { error: null });

return (
<form action={formAction}>
<input name="title" />
<SubmitButton pending={isPending} />
{state.error && <p>{state.error}</p>}
</form>
);
}

API 路由

// app/api/posts/route.ts
import { NextResponse } from "next/server";

export async function GET() {
const posts = await getPosts();
return NextResponse.json(posts);
}

export async function POST(request: Request) {
const body = await request.json();
const post = await createPost(body);
return NextResponse.json(post, { status: 201 });
}

// 动态路由
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const post = await getPost(id);
return NextResponse.json(post);
}

元数据

// 静态元数据
export const metadata: Metadata = {
title: "页面标题",
description: "页面描述",
keywords: ["关键词1", "关键词2"],
};

// 动态元数据
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
};
}

图片

import Image from "next/image";

// 固定尺寸
<Image src="/photo.jpg" alt="照片" width={400} height={300} />

// 填充模式
<div className="relative w-full h-64">
<Image src="/photo.jpg" alt="照片" fill className="object-cover" />
</div>

// 远程图片(需配置域名)
<Image src="https://example.com/photo.jpg" alt="照片" width={400} height={300} />

// 优先加载
<Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority />

// 模糊占位符
<Image src="/photo.jpg" alt="照片" width={400} height={300} placeholder="blur" />

字体

import { Inter } from "next/font/google";
import localFont from "next/font/local";

// Google 字体
const inter = Inter({ subsets: ["latin"] });

// 本地字体
const myFont = localFont({
src: "./fonts/MyFont.woff2",
});

// 使用
<body className={inter.className}>{children}</body>

// CSS 变量
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
});

样式

// CSS Modules
import styles from "./Button.module.css";
<button className={styles.button}>点击</button>

// Tailwind CSS
<button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
点击
</button>

// 条件样式
import clsx from "clsx";
<button className={clsx(
"px-4 py-2 rounded",
variant === "primary" && "bg-blue-500",
variant === "secondary" && "bg-gray-500"
)}>
点击
</button>

错误处理

// error.tsx
"use client";

export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>出错了</h2>
<button onClick={reset}>重试</button>
</div>
);
}

// not-found.tsx
import Link from "next/link";

export default function NotFound() {
return (
<div>
<h2>页面未找到</h2>
<Link href="/">返回首页</Link>
</div>
);
}

// 触发 404
import { notFound } from "next/navigation";

if (!post) {
notFound();
}

中间件

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
const token = request.cookies.get("token");

if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}

return NextResponse.next();
}

export const config = {
matcher: ["/dashboard/:path*"],
};

国际化

// middleware.ts
const locales = ["zh-CN", "en-US", "ja"];
const defaultLocale = "zh-CN";

export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;

const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`)
);

if (!pathnameHasLocale) {
const locale = defaultLocale;
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
}

// app/[lang]/page.tsx
export default function Page({
params,
}: {
params: Promise<{ lang: string }>;
}) {
const { lang } = await params;
const dict = await getDictionary(lang);
return <h1>{dict.title}</h1>;
}

常用命令

# 开发
npm run dev

# 构建
npm run build

# 生产运行
npm run start

# 代码检查
npm run lint

# 类型检查
npx tsc --noEmit

配置文件

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "example.com",
},
],
},
env: {
CUSTOM_KEY: "value",
},
};

export default nextConfig;

环境变量

# .env.local
DATABASE_URL="postgresql://localhost:5432/mydb"

# 客户端可访问
NEXT_PUBLIC_API_URL="https://api.example.com"
// 服务端使用
console.log(process.env.DATABASE_URL);

// 客户端使用
console.log(process.env.NEXT_PUBLIC_API_URL);

路由段配置

// 页面级别配置
export const dynamic = "force-dynamic"; // 强制动态渲染
export const revalidate = 3600; // 每小时重新验证
export const fetchCache = "force-no-store"; // 不缓存 fetch