Next.js 速查表 - Next.js 框架常用方法大全
面向用 React 搭全栈、需要决定页面用 SSR 还是 SSG 的开发者。Next.js 的核心不是列 API,而是把"谁在服务端渲染、谁在客户端交互、数据何时失效"理顺——服务端组件不能直接加 onClick,ISR 会缓存旧数据影响时效。读完能搭出实用的 App Router 页面与布局,区分客户端/服务端组件的边界,理解静态生成与 ISR 的取舍,并处理中间件与数据获取。
典型使用场景
面向正在 App Router 上把一个站点从「本地跑得通」推到「线上行为可预期」的工程师。Next.js 最贵的成本不是写页面,而是渲染边界没划清:哪些数据在构建期取、哪些在请求期取、哪些在客户端取。本表覆盖 App Router 的约定式文件(page/layout/loading/error/not-found)、路由组与拦截式路由、数据获取的缓存语义(force-cache / no-store / revalidate)、渲染策略开关(dynamic / revalidate / runtime)、Route Handler 与中间件、以及 metadata 与 next/image、next/font 这套内建优化。适合在 code review 与「为什么这个页面每次请求都变慢」排查时对照检查一遍。
App Router 基础 5
app/page.tsxapp/layout.tsxapp/loading.tsxapp/error.tsxapp/not-found.tsx路由与导航 6
useRouter()<Link href="/about">usePathname()useSearchParams()app/blog/[slug]/page.tsxapp/(marketing)/about/page.tsx数据获取 6
fetch(url, { cache: "force-cache" })fetch(url, { cache: "no-store" })fetch(url, { next: { revalidate: 60 } })generateStaticParams()"use server"server-only渲染策略 5
export const dynamic = "force-static"export const dynamic = "force-dynamic"export const revalidate = 3600"use client"export const runtime = "edge"布局与模板 5
export const metadata = { title: "页面标题" }export async function generateMetadata()app/@modal/default.tsxapp/(...)/[...slug]/page.tsxtemplate.tsxAPI 与中间件 6
app/api/route.tsexport async function GET(request)middleware.tsNextResponse.redirect(new URL("/login", request.url))NextResponse.next()cookies().get("token")静态资源与优化 5
import Image from "next/image"import { Inter } from "next/font/google"import Script from "next/script"export const metadata = { robots: { index: true, follow: true } }<Link href="/about" prefetch={true}>部署与配置 5
output: "standalone"output: "export"process.env.NEXT_PUBLIC_API_URLnext buildnext start参数矩阵
| 参数 | 作用 | 示例 |
|---|---|---|
app/page.tsx | 路由 / 的唯一页面文件;文件名即 URL,一个路由只能有一个 page.tsx | app/page.tsx |
app/layout.tsx | 布局,父级布局在切换子路由时不重渲染;必须包含 <html> 与 <body> | app/layout.tsx |
app/loading.tsx | 同级 Suspense 回退界面,路由段切换与数据等待期间自动显示 | app/loading.tsx |
app/error.tsx | 捕获同级页面段的渲染错误并要求导出 client component,可拿到 reset() 重试 | app/error.tsx |
app/not-found.tsx | notFound() 抛出的 404 页面,全局 404 放根目录即可 | app/not-found.tsx |
app/(marketing)/about/page.tsx | 路由组,用括号包住且不出现在 URL 里,用来放共享布局而不污染路径 | app/(marketing)/about/page.tsx |
app/blog/[slug]/page.tsx | 动态段,[slug] 由 params 传入;配合 generateStaticParams 在构建期预渲染全部条目 | app/blog/[slug]/page.tsx |
app/(...)/[...slug]/page.tsx | 捕获所有段 (..)、可选段 (...) 省略号写法,用来匹配 app 之外的多层路径 | app/(...)/[...slug]/page.tsx |
useRouter() | 客户端组件里拿到 router,用 router.push / replace 做导航,不能在服务端组件调用 | useRouter() |
<Link href="/about"> | 客户端路由跳转,默认预取后续路由的 JS 与数据,替代裸 <a> 避免整页刷新 | <Link href="/about"> |
useSearchParams() | 读取查询串;它会把整棵子树标为动态渲染(CSR bailout),需用 Suspense 包住 | useSearchParams() |
fetch(url, { cache: "force-cache" }) | 强制缓存并长期复用,等价于静态渲染该处数据 | fetch(url, { cache: "force-cache" }) |
fetch(url, { cache: "no-store" }) | 每次请求都回源,一个请求只用一次缓存;适合个性化与实时数据 | fetch(url, { cache: "no-store" }) |
fetch(url, { next: { revalidate: 60 } }) | ISR 语义,缓存 60 秒后过期并在下次请求后台重取 | fetch(url, { next: { revalidate: 60 } }) |
generateStaticParams() | 构建期为动态路由枚举参数,返回的每个值都会被静态预渲染 | generateStaticParams() |
"use server" | 把函数标记为 Server Action,可被客户端表单直接调用;内含的第三方凭据不会进包 | "use server" |
server-only | 模块级导入守卫,一旦被客户端组件引用即构建报错,用于保护密钥与 DB 连接 | server-only |
"use client" | 声明客户端边界,此文件及其 import 的模块进入浏览器 bundle;useState/useEffect 就属于这一侧 | "use client" |
export const dynamic = "force-dynamic" | 整段强制动态渲染,每次请求都跑,适合实时页面;会关闭该段所有缓存 | export const dynamic = "force-dynamic" |
export const dynamic = "force-static" | 强制静态化,忽略动态 API 并把结果固化,适合内容不变的说明页 | export const dynamic = "force-static" |
export const revalidate = 3600 | 页面级 ISR 间隔(秒),比逐个 fetch 设置 revalidate 更省事 | export const revalidate = 3600 |
export const runtime = "edge" | 切到 Edge Runtime,冷启动更低但无 Node API;用 Node 专属库会构建报错 | export const runtime = "edge" |
export async function generateMetadata() | 按 params 生成动态 title/description,产出各路由的 <head> 与 OG 标签 | export async function generateMetadata() |
app/@modal/default.tsx | 平行路由(Parallel Routes)的插槽目录,default.tsx 是必填的软回退 | app/@modal/default.tsx |
template.tsx | 每次导航都重新挂载的模板,放进入动画;与 layout 的差别就在「会不会重挂载」 | template.tsx |
app/api/route.ts | Route Handler 入口,按方法导出 GET/POST 等函数,取代 pages/api 写法 | app/api/route.ts |
export async function GET(request) | GET 处理函数,request 是标准 Web Request,可直接用 new URL(request.url) 取查询串 | export async function GET(request) |
middleware.ts | 在根目录(或 src)定义,匹配器决定拦截范围,用于鉴权与重写 | middleware.ts |
NextResponse.redirect(new URL("/login", request.url)) | 服务端重定向,origin 必须由 request.url 派生,不能写绝对域名(多域名部署会错跳) | NextResponse.redirect(new URL("/login", request.url)) |
NextResponse.next() | 放行继续执行,不写这一行 middleware 默认也是继续但显式写出更清楚 | NextResponse.next() |
cookies().get("token") | 在服务端组件/路由处理器读 Cookie;一旦读取即让该路由变为动态渲染 | cookies().get("token") |
import Image from "next/image" | 内建图片组件,自动生成 srcset/尺寸并默认走优化管线;忘记给宽高会报尺寸缺失警告 | import Image from "next/image" |
import { Inter } from "next/font/google" | 构建期自托管字体,消除外部请求与 CLS;网络受限时构建会失败 | import { Inter } from "next/font/google" |
output: "standalone" | 构建产物自带精简 node_modules 的独立服务目录,适合打进 Docker 镜像 | output: "standalone" |
output: "export" | 纯静态导出,只能用于完全静态的站点;任何 Route Handler、ISR、动态路由都会失败 | output: "export" |
process.env.NEXT_PUBLIC_API_URL | NEXT_PUBLIC_ 前缀的变量被打进客户端 bundle;改动必须重新构建且会暴露给所有人 | process.env.NEXT_PUBLIC_API_URL |
next build / next start | 构建并以生产模式启动;开发模式与生产模式的缓存与路由行为并不一致,判断线上问题必须用 next start | next build |
易错点与避坑指南
现象服务端组件里调用 useRouter / useState,报 "useState only works in a Client Component"。
原因App Router 默认为服务端组件,只有加了 "use client" 的文件及其 import 链才有这些 Hook。
处置把需要交互的最小叶子组件单独抽成一个文件并顶部写 "use client",不要在整个 layout 上加,否则整棵子树都会进 bundle。
现象页面首屏 HTML 里数据是空的,页面加载后才闪现出真实内容(hydration mismatch 警告)。
原因服务端渲染时取了缓存值、客户端又取了实时值,两侧输出不一致;常见于直接读 localStorage 或随机数。
处置把不一致的部分放进 useEffect 里设置状态,或对该组件加 "use client" 并让服务端与客户端输出一致。
现象明明用了 fetch 缓存,接口请求量还是等于访问量。
原因fetch 缓存只作用于服务端渲染与静态生成;请求里出现 cookies()、headers()、searchParams 等动态 API 会让整段变成动态渲染,缓存全部失效。
处置把数据获取放进单独的静态子段(不带动态 API),在页面层用 Suspense + 子组件消费;或确认 dynamic 未被设为 force-dynamic。
现象在 page.tsx 里直接用 useSearchParams(),构建时整页都变动态并抛 Suspense 边界缺失错误。
原因searchParams 是动态 API,读取它会让该路由段 opt out 静态渲染;没有 Suspense 包裹时 Next 无法确定流式边界。
处置把读取 searchParams 的逻辑单独放进一个客户端子组件,外层用 <Suspense> 包住。
现象NextResponse.redirect(new URL("https://example.com/login", request.url)) 在预发环境把用户跳到生产域名。
原因重定向 URL 写死了绝对域名,忽略了两层:请求的 origin 与部署域名可能不同(如预发子域)。
处置一律用 new URL("/login", request.url) 相对构造,让 Next 从当前请求派生 origin。
现象把 API 密钥写进客户端组件能读到的变量里,密钥出现在浏览器 Network 里。
原因NEXT_PUBLIC_ 前缀的变量与未加前缀但在客户端组件中 import 的模块都会被内联进 bundle。
处置密钥只放服务端:用 server-only 保护模块,把调用放进 Route Handler 或 Server Action。
现象export const runtime = "edge" 后构建报某个 Node 库在 Edge Runtime 下不可用。
原因Edge Runtime 只有 Web API,没有 fs、net、部分 npm 原生模块。
处置改回默认 Node runtime;确需 Edge 时把重依赖留在服务端并通过 Route Handler 调用。
现象next build 通过、next dev 正常,但 next start 后页面 404。
原因文件写在错误的目录层级(例如放在 pages/ 而非 app/,或 app 目录未位于根或 src 下),dev 与 build 的路由解析差异让问题被掩盖。
处置核对目录位置与文件名;用 next build 的路由表确认路径是否被收录。
现象部署到 CDN 后页面一直拿到旧数据,revalidate 似乎没生效。
原因CDN 层的缓存头优先于 Next 的 ISR,或 output: "export" 下完全没有 ISR,revalidate 被忽略。
处置检查响应头 Cache-Control 与 CDN 规则;静态导出场景改用构建期取数或改回标准部署。
排障路径
1用生产模式复现线上问题
next build && next startnext dev 不做完整优化且路由行为不同,判断缓存与构建错误必须用 next start。
2查看构建产物的路由与渲染模式
next build输出里每个路由会标注 Static / Dynamic / ISR 以及大小与首次加载 JS。
3确认客户端变量的取值是否进了 bundle
grep -r "NEXT_PUBLIC" .next/static在 .next/static 里搜到明文即说明该变量被内联进客户端产物。
4排查静态导出是否可行
grep -r "export const dynamic" app任何 Route Handler、ISR 或 force-dynamic 段都会让 output: "export" 构建失败。
5检查中间件实际拦截了哪些路径
grep -n "matcher" middleware.tsmatcher 配置过宽会让每个请求都过一遍中间件,是性能问题的常见来源。
6核对约定式文件是否被路由识别
ls -R app确认 page.tsx / layout.tsx 的文件名与层级,拼写错误(如 page.ts)不会报错只是没有路由。
命令示例
Route Handler 处理 GET 请求
// app/api/items/route.ts
import { NextResponse } from "next/server";
export async function GET() {
const res = await fetch("https://api.example.com/items", { cache: "no-store" });
return NextResponse.json(await res.json());
}Route Handler 里导出的函数名就是 HTTP 方法名,没有前缀路径。no-store 让每次请求都真实回源,否则结果会被缓存成一份陈旧数据。返回 NextResponse.json(...) 而不是裸对象,响应头和状态码才可控。
ISR:60 秒后后台重新验证
// app/products/[id]/page.tsx
export const revalidate = 60;
export async function generateStaticParams() {
const ids = await getIds();
return ids.map((id) => ({ id: String(id) }));
}revalidate = 60 让页面 60 秒后过期,并在下次有人访问时后台重新生成——首个请求仍是旧内容但立刻返回,SEO 与首屏速度都保住。generateStaticParams 决定构建期预渲染哪些动态路由路径,少写它这些页面会退化成首次访问才生成。
中间件拦截未登录请求
// middleware.ts
import { NextResponse } from "next/server";
export function middleware(request) {
const token = request.cookies.get("token");
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}中间件运行在请求到达路由之前,是做鉴权重定向的最早切入点。注意 NextResponse.redirect(new URL(..., request.url)) 必须基于完整 request.url 才能解析出正确的绝对地址,只写 "/login" 会抛错。
客户端组件里用 Hook 做导航
"use client";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
export function Nav() {
const router = useRouter();
const pathname = usePathname();
const params = useSearchParams();
return <button onClick={() => router.push("/about")}>{pathname}</button>;
}next/navigation 的三个 Hook 只能在客户端组件里调用,服务端组件用它们会直接报错。useSearchParams 会让该组件进入客户端渲染边界,静态导出的页面里读取它需要额外的 Suspense 包裹。
metadata 与动态 generateMetadata
export const metadata = {
title: "页面标题",
description: "页面描述",
robots: { index: true, follow: true },
};
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return { title: post.title, description: post.excerpt };
}静态 metadata 适合固定页面,动态路由必须用 generateMetadata 并接收 params 取出内容填 title/description——否则所有详情页会共用一个标题,SEO 上是硬伤。两处不要同时写,同一文件里 generateMetadata 会覆盖静态项。
常见坑与注意事项
- 「use client」是文件级的,一旦加上,该文件里导入的所有模块都会被打进客户端 bundle。把只在服务端用的数据库/密钥相关工具 import 进来,等于把代码和依赖一起发给浏览器。
- 同一文件里混用 process.env 与 import.meta.env 这类构建期常量访问:构建工具会把源码里的这些访问替换成字面量,客户端与服务端拿到的值不同就会 hydration mismatch,页面结构在刷新瞬间跳变。环境变量请只走 Next 的约定方式并在服务端读取。
- 忘记 server-only 标记:只靠目录约定来保证「这段代码只在服务端跑」,一次 import 路径写错就可能把密钥拼接逻辑打进客户端产物。用 server-only 包在模块顶部,让打包器直接报错。
- 把 fetch 的缓存策略想当然:默认缓存可能让你看到过期数据,no-store 又会让每个请求都打上游、账单和延迟一起上升。数据变更频繁用 no-store 或 revalidate 明确声明,不要靠猜。
- middleware.ts 里做重逻辑(查库、调第三方 API)会拖慢每一条经过它的请求,而 Edge Runtime 还不支持所有 Node API,报错往往指向第三方库。middleware 只做轻量判断,重活交给路由或 Server Action。
- output: "export" 静态导出后,任何依赖服务端运行时的东西都会构建失败或运行时为空:Route Handler、middleware、服务端数据获取、动态路由的 on-demand 生成。选静态导出前先确认页面确实能纯静态产出。
提示
- App Router 是 Next.js 13+ 默认路由方案,取代了 Pages Router
- 页面组件默认是服务端组件(Server Component),需要客户端交互时加 "use client"
- 使用 next/link 的 Link 组件代替 a 标签,自动预取和客户端导航
- 数据获取推荐在服务端组件中直接 async/await fetch,无需 useEffect
- 使用 generateStaticParams 实现静态生成动态路由
常见问题
页面组件报 "use client" 相关的错误,该在哪个文件加?
App Router 下 app/page.tsx 默认是服务端组件,只有需要浏览器 API、事件处理或状态时才加 "use client",把它及其所有导入树都切换为客户端组件。判断标准很简单:组件里出现 onClick、useState、useEffect 这类只能在浏览器工作的东西,就自底向上地把最小的那一层(往往是一棵叶子组件)标记为客户端,而不是在顶层页面文件一把梭加上。服务端组件里的数据获取不需要 useEffect,直接在组件中 async/await fetch 即可。另一个方向的选择是 server-only:它确保某段代码只在服务端运行,避免被误打包进客户端导致密钥泄漏。
静态生成、ISR 和动态渲染该怎么按需求选?
三条路线对应三类数据。fetch(url, { cache: "force-cache" }) 是静态数据获取,默认构建时就渲染成 HTML,适合内容不常变的页面。fetch(url, { next: { revalidate: 60 } }) 是 ISR,60 秒后后台重新验证并刷新页面,兼顾速度与新鲜度。fetch(url, { cache: "no-store" }) 是动态,每次请求都重新拉取,适合个性化或实时数据。页面级可以导出 const dynamic = "force-static" 或 "force-dynamic" 统一锁定策略,也可以导出 const revalidate = 3600 设定整页的 ISR 间隔。动态路由的静态化还需要 generateStaticParams() 提供要生成的路径列表。
form 提交要改数据,为什么优先用 Server Action 而不是 Route Handler?
因为这是组件内的一等能力:在函数顶部写 "use server" 就把它标记为服务端 Action,可以直接在客户端表单的 action 属性里调用,省掉自己写 fetch 和状态管理。Route Handler(app/api/route.ts 中导出 async function GET(request))面向的是外部消费方——需要被其它程序按 HTTP 语义调用、需要自定义请求方法或响应结构时用它。权限与安全上,server-only 能确保密钥只留在服务端;读取会话用 cookies().get("token") 在服务端完成,不要把 token 传给客户端再回传。
什么场景该用 middleware.ts 而不是每个页面各写判断?
middleware.ts 在请求抵达路由之前执行,适合做全局、统一的横切逻辑:登录校验、重定向、请求头改写。用 NextResponse.redirect(new URL("/login", request.url)) 把未登录用户送走,需要放行时调用 NextResponse.next()。写在页面里重复实现的问题是容易漏——新增一个路由就可能忘了加判断,而且逻辑散落各处难以统一调整。middleware 里拿得到 request 对象,适合读路径和 cookie 做决策;但它跑在服务端到路由之间,不要在里面做重量级数据查询,否则每个请求都要付这份开销,重活应放在页面或 Route Handler 里。
静态导出和 standalone 部署,配置文件里怎么写?
看部署目标。output: "export" 把应用导出为纯 HTML/CSS/JS,可以放到任何静态托管上,但代价是服务端能力(Route Handler、Server Action、ISR 重新验证)全部失效,页面必须能在构建期确定。需要 Node 运行时且希望部署产物自包含时用 output: "standalone",它会连同所需的 node_modules 片段一起输出,非常适合打进 Docker 镜像。本地验证生产行为用 next build 生成 .next 目录、再 next start 启动服务器读取该目录,而不是用 next dev。跨环境地址用 ${PROCESS_ENV}.NEXT_PUBLIC_API_URL 暴露给客户端,注意 NEXT_PUBLIC_ 前缀的变量会被内联进产物。
由 巧匠 维护
公开更新于 2026年7月21日,内容持续校对官方文档。