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();
});
注意事项:
forbidden和unauthorized不能在根布局中调用- 可在 Server Components、Server Actions、Route Handlers 中使用
- 需要创建对应的
forbidden.tsx和unauthorized.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