Next.js 基础知识参考字典

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 的路由
上一篇
下一篇