Next.js(App Router / v16)条目式字典。每条给语义、多场景示例、边界与坑。教程序列见《Next.js 环境搭建与工具链》。
1. async params / searchParams – 动态段参数
描述
15+ 起动态路由参数与查询参数都是 Promise,必须 await(16 全面推行)。这是 React 19 并发渲染的必然结果:渲染可被中断,参数需按需解析。
语法和示例
// 页面组件
export default async function Page({
params,
}: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
return <Post slug={slug} />;
}
// 查询参数
export default async function List({
searchParams,
}: { searchParams: Promise<{ page?: string }> }) {
const { page } = await searchParams;
const n = Number(page ?? 1);
}
// generateMetadata 同样要 await
export async function generateMetadata({ params }: Props) {
const { slug } = await params;
return { title: slug };
}注意事项
- 忘记 await 会拿到 Promise 对象,值为 undefined 而不报错
- searchParams 的每个值可能是 string | string[] | undefined,处理前要收窄
- generateStaticParams 返回的 params 是同步普通对象(别也 await)
- 动态段参数变化会重新渲染页面,但不会重置组件状态(客户端组件内状态保留)
- 同一页面里多处取 params 只需 await 一次并传递
2. fetch 缓存五档
描述
Next 扩展了 fetch 的缓存语义:默认缓存(force-cache)、no-store 每次取、revalidate 秒级 ISR、tags 定向失效、use cache 显式声明(16 Cache Components)。
语法和示例
// 1. 默认缓存(GET 静态数据)
fetch('https://api.x.com/data');
// 2. 永不缓存(实时数据)
fetch(url, { cache: 'no-store' });
// 3. ISR:60 秒后后台再生
fetch(url, { next: { revalidate: 60 } });
// 4. 打标签 + 定向失效
fetch(url, { next: { tags: ['posts'] } });
revalidateTag('posts'); // 在 Server Action / Route Handler 内
// 5. 页面级
export const revalidate = 60;
export const dynamic = 'force-dynamic';
// 6. 16 Cache Components
async function getPosts() { 'use cache'; cacheTag('posts'); }注意事项
- no-store 与 revalidate 同时出现会抛错(互斥)
- revalidateTag 只能在 Server Action / Route Handler / 非渲染期调用
- 缓存键包含 URL 与选项,不同 revalidate 值算不同缓存项
- 开发环境默认不缓存(便于调试),行为与生产不同——别在 dev 里判断缓存
- POST 请求默认不缓存,要缓存需显式声明
3. ‘use client’ 边界与序列化规则
描述
App Router 默认服务端组件。’use client’ 标记文件后,该文件及导入的本地组件进客户端 bundle。props 必须可序列化。
语法和示例
'use client';
import { useState } from 'react';
export default function Counter({ initial }: { initial: number }) {
const [n, setN] = useState(initial);
return <button onClick={() => setN(v => v + 1)}>{n}</button>;
}
// 服务端组件里使用(传可序列化 props)
import Counter from './counter';
export default async function Page() {
const data = await db.get();
return <Counter initial={data.count} />;
}注意事项
- 函数、类实例、Symbol、undefined 作为 props 传到客户端会失败(需序列化)
- 服务端组件 import 客户端组件没问题;反向会把服务端代码打进客户端包(含密钥泄露风险)
- 边界文件标一次即可,其导入的本地组件自动客户端——但被其他服务端组件直接 import 时仍是服务端
- ‘use client’ 不影响该文件里的 import 从哪执行——它只是声明「本组件在客户端渲染」
- 在客户端组件里用 cookies()/headers() 会报错(这些是服务端 API)
4. 目录约定:文件即路由
描述
App Router 用文件系统定义路由:page/layout/template/route/loading/error/not-found/default 各司其职。
语法和示例
<table>
<thead><tr><th>文件</th><th>作用</th><th>必需性</th></tr></thead>
<tbody><tr><td>page.tsx</td><td>路由页(渲染内容)</td><td>每条路由必须有</td></tr><tr><td>layout.tsx</td><td>布局,跨导航保留状态</td><td>根布局必需</td></tr><tr><td>template.tsx</td><td>类似 layout 但每次导航重挂载</td><td>可选(做进入动画)</td></tr><tr><td>route.ts</td><td>Route Handler(API 端点)</td><td>与 page 互斥</td></tr><tr><td>loading.tsx</td><td>Suspense 骨架,自动包裹</td><td>可选</td></tr><tr><td>error.tsx</td><td>错误边界(必须 'use client')</td><td>可选</td></tr><tr><td>not-found.tsx</td><td>本段 404 渲染</td><td>可选</td></tr><tr><td>default.tsx</td><td>并行路由刷新兜底</td><td>有 @slot 时必需</td></tr><tr><td>[id] / [...slug] / [[...opt]]</td><td>动态段 / 捕获全部 / 可选捕获</td><td>按需</td></tr><tr><td>(group)</td><td>路由分组,不影响 URL</td><td>按需</td></tr></tbody></table>注意事项
- page.tsx 与 route.ts 不能在同一目录(冲突)
- error.tsx 必须是客户端组件且接收 reset 回调
- 并行路由的插槽刷新时需要 default.tsx 兜底,否则 404
- layout 里不能访问 usePathname(客户端 hook)——要路由信息传下来或用 client 组件
- 根 layout 必须有 html/body 标签
5. Server Action – 表单与变更
描述
‘use server’ 声明服务端函数,可直接作为 form action 或客户端调用。免写 API 端点,且自动做序列化与 CSRF 防护。
语法和示例
// 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.insert({ title });
revalidatePath('/posts');
}
// 客户端组件里
'use client';
import { createPost } from './actions';
<form action={createPost}>
<input name="title" />
<button>发布</button>
</form>
// 带返回状态(useActionState)
const [state, action, pending] = useActionState(createPost, null);注意事项
- Server Action 只能 export async 函数(不能 export 常量/组件)
- 参数必须可序列化;不能传函数/类实例
- Server Action 是 POST 请求,不能当 GET 用(缓存与幂等性不匹配)
- 大文件上传要走 route handler(Action 有体积限制)
- revalidatePath 与 revalidateTag 在 Action 内可用,在其他渲染期不可用
6. Route Handler – API 端点
描述
app/api/*/route.ts 导出 HTTP 方法函数,返回 Response 或 NextResponse。用于 webhook、上传、非页面请求。
语法和示例
// app/api/posts/route.ts
import { NextResponse } from 'next/server';
export async function GET(req: Request) {
const q = new URL(req.url).searchParams.get('q');
const list = await db.search(q);
return NextResponse.json(list, {
headers: { 'cache-control': 'no-store' },
});
}
export async function POST(req: Request) {
const body = await req.json();
const created = await db.insert(body);
return NextResponse.json(created, { status: 201 });
}
// 动态段:app/api/posts/[id]/route.ts
export async function DELETE(_: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
await db.remove(id);
return new Response(null, { status: 204 });
}注意事项
- Route Handler 与同目录 page 冲突——两者不能并存
- params 同样是 Promise(15+),要 await
- 默认 GET 会被静态化(若不含动态 API)——实时数据要声明 dynamic 或 headers 缓存策略
- request.json() 只能调一次(流),需要多次读先 clone
- 跨域要显式设 CORS 头,Route Handler 不做自动 CORS
7. next/image – 图片优化
描述
自动压缩、响应式尺寸、懒加载、格式转换(WebP/AVIF)。用 fill 或显式宽高避免布局抖动。
语法和示例
import Image from 'next/image';
// 固定尺寸
<Image src="/hero.png" width={800} height={600} alt="" />
// 填充父容器
<div style={{ position: 'relative', height: 400 }}>
<Image src={url} fill sizes="(max-width:768px) 100vw, 50vw" alt="" />
</div>
// 远程图需配置 next.config
// images: { remotePatterns: [{ hostname: 'cdn.example.com' }] }
// 优先级
<Image src="/hero.png" priority width={800} height={600} alt="" />注意事项
- 远程图片必须在 next.config 里配 remotePatterns,否则报错
- fill 需要父容器有定位且确定尺寸,否则塌陷
- sizes 影响生成的 srcset 档位,不写会按 100vw 处理下载过大图
- onLoadingComplete 已废弃改 onLoad
- SVG 默认不走优化(需 dangerouslyAllowSVG,有 XSS 风险慎用)
8. Tailwind v4 CSS-first 配置
描述
v4 取消 tailwind.config.ts,全部在 CSS 里:@import 引入、@theme 定义设计令牌、@source 指定扫描路径、@plugin 加载插件。
语法和示例
/* app/globals.css */
@import "tailwindcss";
@theme {
--color-brand: oklch(0.7 0.15 250);
--font-serif: "Noto Serif SC", serif;
--spacing-gutter: 1.5rem;
}
@source "../components"; /* 显式扫描路径 */
@plugin "@tailwindcss/typography";注意事项
- 动态拼接类名不被扫描(bg-${c} 无效)——用完整类名映射对象
- bg-opacity-50 等旧语义已废弃,用色彩修饰符 bg-black/50
- @theme 的变量名决定生成的原子类名(–color-brand → bg-brand)
- v3 的 content/extend/plugins 需迁移:config 文件不再生效
- 自定义 CSS 与原子类混用注意层顺序(@layer 控制优先级)
9. proxy.ts(原 middleware)
描述
16 起 middleware 改名 proxy。在请求到达路由前拦截:鉴权、重定向、改请求头。跑在边缘/Node 运行时。
语法和示例
// proxy.ts(根目录)
import { NextRequest } from 'next/server';
export default function proxy(req: NextRequest) {
const token = req.cookies.get('session')?.value;
if (!token && req.nextUrl.pathname.startsWith('/dash')) {
return Response.redirect(new URL('/login', req.url));
}
// 改写请求头
const headers = new Headers(req.headers);
headers.set('x-path', req.nextUrl.pathname);
return NextResponse.next({ request: { headers } });
}
export const config = { matcher: ['/dash/:path*', '/api/:path*'] };注意事项
- matcher 不配会对所有请求生效(含静态资源),拖慢全站
- proxy 里不能用 Node API(如 fs)——受限运行时
- 不要在 proxy 里做重 DB 查询(每次请求都跑)——只做轻量判断
- cookie 只能读,要写需在 response 上加 Set-Cookie
- 16 起 middleware.ts 仍兼容但已废弃,新项目用 proxy.ts
10. 部署形态:standard / standalone / export
描述
三种产出:标准 Node 服务器、standalone 自包含(容器首选)、静态导出(无服务端能力)。
语法和示例
bash
1. 标准
next build && next start
1. standalone(next.config: output: 'standalone')
1. 只需拷 .next/standalone + .next/static + public
node .next/standalone/server.js
1. 静态导出(output: 'export')
next build # 产物在 out/
1. ISR / SSR / Server Action 全部不可用注意事项
- 静态导出后没有 ISR、没有 Route Handler、没有 Server Action——纯内容站才用
- standalone 需手动拷贝 static 与 public,否则资源 404
- 容器部署要给 Node 足够内存(构建期压缩图片很吃内存)
- ISR 需要持久化缓存目录(多副本共享时用外部缓存)
- 自托管时健康检查打 /,别打需要 DB 的路由