本文是《Next.js 完全指南》系列的速查参考页,适合在开发时快速定位 API 用法和配置选项。如需深入理解,请参考对应章节。
一、文件路由约定
app/
├── page.tsx # / 路由页面
├── layout.tsx # 根布局(包裹所有子路由)
├── loading.tsx # 加载状态(Suspense fallback)
├── error.tsx # 错误边界
├── not-found.tsx # 404 页面
├── template.tsx # 类似 layout 但每次导航重新挂载
├── route.ts # API Route(GET/POST/...)
├── default.tsx # 并行路由默认 fallback
├── global-error.tsx # 全局错误处理(根 layout error)
│
├── blog/
│ ├── page.tsx # /blog
│ ├── layout.tsx # /blog/* 的布局
│ └── [slug]/
│ ├── page.tsx # /blog/:slug
│ ├── loading.tsx
│ └── error.tsx
│
├── (marketing)/ # Route Group(无 URL 前缀)
│ ├── about/
│ │ └── page.tsx # /about
│ └── pricing/
│ └── page.tsx # /pricing
│
├── [locale]/ # 动态段:/en, /zh
│ └── [...catchAll]/ # 捕获所有:/anything/here
│ └── page.tsx
| 文件 | 作用域 | 备注 |
|---|---|---|
page.tsx | 当前路由 | 唯一必需文件 |
layout.tsx | 当前路由及子路由 | 导航时保持状态 |
template.tsx | 当前路由及子路由 | 导航时重新挂载 |
loading.tsx | 当前路由及子路由 | Suspense fallback |
error.tsx | 当前路由及子路由 | 错误边界 |
not-found.tsx | 当前路由 | 404 或 notFound() 触发 |
route.ts | 当前路由 | API endpoint |
二、渲染模式决策树
页面需要动态数据?
├── 否 → 数据是否可能变化?
│ ├── 否 → export const dynamicParams = false;
│ │ # SSG(构建时生成,永不更新)
│ └── 是 → export const revalidate = 3600;
│ # ISR(增量静态再生)
└── 是 → 数据是否用户相关/实时?
├── 是 → export const dynamic = 'force-dynamic';
│ # SSR(每次请求实时渲染)
└── 否 → export const revalidate = 60;
# ISR(定期更新 + 按需失效)
需要流式加载?
└── 部分组件用 Suspense + loading.tsx
# Streaming(渐进式渲染,先骨架后内容)
只有前端交互?
└── page.tsx 中不需要 async
# CSR(纯客户端渲染,"use client" 组件)
| 模式 | 首次访问 | 缓存 | 数据新鲜度 | 适用 | 配置 |
|---|---|---|---|---|---|
| SSG | 构建时 | CDN 永久 | 永不更新 | 法律页/关于 | 默认(无 async fetch) |
| ISR | 首次访问生成 | CDN + TTL | TTL 后后台更新 | 博客/商品页 | revalidate = N |
| SSR | 每次请求 | 无 | 最新 | 用户中心/搜索 | dynamic = 'force-dynamic' |
| Streaming | 渐进式 | 部分缓存 | 流内实时 | 大数据量页 | Suspense + loading.tsx |
| CSR | 客户端渲染 | 浏览器 | 客户端实时 | 后台管理 | "use client" |
三、fetch 与缓存
// 默认行为(Next.js 15):请求记忆 + 数据缓存
fetch('https://api.example.com/data');
// 完全禁用缓存(类似 SSR)
fetch('https://api.example.com/data', { cache: 'no-store' });
// ISR:缓存 1 小时后后台刷新
fetch('https://api.example.com/data', { next: { revalidate: 3600 } });
// 带标签缓存(支持按需失效)
fetch('https://api.example.com/posts', { next: { tags: ['posts'] } });
// 永久缓存(类似 SSG)
fetch('https://api.example.com/static', { cache: 'force-cache' });
// 页面级别缓存控制
export const revalidate = 60; // ISR 60 秒
export const dynamic = 'force-dynamic'; // SSR(无缓存)
export const dynamicParams = false; // 只预生成已知路径
四、Server Actions
// 定义 Server Action(可直接绑定到 <form>)
async function createPost(formData: FormData) {
'use server';
const title = formData.get('title') as string;
await prisma.post.create({ data: { title } });
revalidatePath('/posts');
}
// 带错误处理和返回值的 Server Action
async function submitContact(formData: FormData) {
'use server';
const email = formData.get('email') as string;
if (!email.includes('@')) {
return { error: 'Invalid email' };
}
try {
await sendEmail(email);
return { success: true };
} catch {
return { error: 'Failed to send' };
}
}
// 前端:用 useFormState 处理返回
'use client';
import { useFormState } from 'react-dom';
export default function ContactForm() {
const [state, formAction] = useFormState(submitContact, { error: null });
return (
<form action={formAction}>
<input name="email" type="email" required />
<button type="submit">Send</button>
{state?.error && <p>{state.error}</p>}
{state?.success && <p>Sent!</p>}
</form>
);
}
// useOptimistic:乐观更新
const [optimisticState, addOptimistic] = useOptimistic(
initialState,
(current, newItem) => [...current, newItem]
);
async function handleSubmit(formData: FormData) {
addOptimistic({ id: 'temp', title: formData.get('title') });
await serverAction(formData);
}
五、Metadata API
// page.tsx / layout.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My Page',
description: 'Page description for SEO',
openGraph: {
title: 'My Page',
description: 'OG description',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
title: 'My Page',
images: ['/twitter-image.jpg'],
},
robots: {
index: true,
follow: true,
},
alternates: {
canonical: 'https://example.com/page',
languages: {
'en-US': '/en/page',
'zh-CN': '/zh/page',
},
},
};
// 动态 Metadata(基于路由参数)
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
};
}
六、Error 处理
// app/error.tsx(局部错误边界)
'use client';
export default function Error({ error, reset }: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/global-error.tsx(全局错误)
'use client';
export default function GlobalError({ error, reset }) {
return (
<html>
<body>
<h2>Global Error</h2>
<button onClick={reset}>Reload</button>
</body>
</html>
);
}
// 主动抛出 404
import { notFound } from 'next/navigation';
export default async function Page({ params }) {
const post = await getPost(params.slug);
if (!post) notFound(); // 渲染 not-found.tsx
return <Post post={post} />;
}
七、Middleware
// middleware.ts(项目根目录)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 认证检查
const token = request.cookies.get('token')?.value;
if (!token && pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// i18n 重定向
if (!pathname.startsWith('/en') && !pathname.startsWith('/zh')) {
return NextResponse.redirect(new URL('/en' + pathname, request.url));
}
// URL 重写(URL 不变,内部路由改变)
if (pathname === '/docs') {
return NextResponse.rewrite(new URL('/documentation', request.url));
}
// 注入请求头
const headers = new Headers(request.headers);
headers.set('x-user-country', request.geo?.country || 'US');
return NextResponse.next({ request: { headers } });
}
// 匹配规则
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
八、next.config.js / next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// 独立运行模式(Docker 必需)
output: 'standalone',
// 图片配置
images: {
domains: ['cdn.example.com'],
remotePatterns: [
{ protocol: 'https', hostname: '**.example.com' },
],
formats: ['image/avif', 'image/webp'],
},
// 重写规则
async rewrites() {
return [
{ source: '/old-path', destination: '/new-path' },
{ source: '/api/:path*', destination: 'https://api.example.com/:path*' },
];
},
// 重定向
async redirects() {
return [
{ source: '/about-us', destination: '/about', permanent: true },
];
},
// 请求头
async headers() {
return [
{
source: '/:path*',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
],
},
];
},
// TypeScript/ESLint 构建时检查(临时禁用)
typescript: { ignoreBuildErrors: false },
eslint: { ignoreDuringBuilds: false },
// 实验性功能
experimental: {
ppr: true, // 部分预渲染
dynamicIO: true, // 动态 IO(Next.js 15+)
after: true, // after() API
},
};
export default nextConfig;
九、Server/Client 组件边界速查
| 能力 | Server Component | Client Component |
|---|---|---|
| async/await | ✅ | ❌(需用 useEffect + useState) |
| 访问数据库/ORM | ✅ | ❌(通过 API 或 props) |
访问 fs / path | ✅ | ❌ |
headers() / cookies() | ✅ | ❌(通过 API) |
useState / useEffect | ❌ | ✅ |
onClick / onChange | ❌ | ✅ |
window / document | ❌ | ✅ |
localStorage | ❌ | ✅ |
| 第三方库(Framer Motion) | 部分✅ | ✅ |
十、CLI 命令速查
# 创建项目
npx create-next-app@latest my-app --typescript --tailwind --app
# 开发
npm run dev # 启动开发服务器(默认 Turbopack)
npm run dev --turbo # 显式启用 Turbopack
# 构建
npm run build # 生产构建
npm start # 启动生产服务器(需先 build)
# 分析 Bundle
npm install @next/bundle-analyzer
# next.config.js: ANALYZE=true npm run build
# 类型检查
npx tsc --noEmit
十一、并行路由与拦截路由(Parallel & Intercepting Routes)
并行路由
通过 @folder 约定,将多个页面渲染到同一布局的不同插槽(slot)中。
dashboard/
├── @analytics/
│ ├── page.tsx # /dashboard 中展示分析面板
│ └── loading.tsx
├── @revenue/
│ ├── page.tsx # 同时展示收入面板
│ └── error.tsx
├── layout.tsx # 接收 { children, analytics, revenue }
└── page.tsx # children 插槽(默认页面)
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
revenue,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
revenue: React.ReactNode;
}) {
return (
<div className="grid grid-cols-2 gap-4">
<div>{analytics}</div>
<div>{revenue}</div>
<div className="col-span-2">{children}</div>
</div>
);
}
每个 @slot 独立加载和报错,互不影响,default.tsx 为未匹配到子路径时的回退画面。
拦截路由
通过 (.)、(..)、(..)(..)、(...) 前缀实现局部路由拦截,常用于模态框和抽屉面板。
feed/
├── page.tsx # /feed 列表页(常规渲染)
└── @modal/
└── (.)photo/[id]/ # 拦截 /feed → /feed/photo/123(模态框渲染)
└── page.tsx # 展开图片时以模态框呈现
直接访问 /feed/photo/123(如通过 URL 或刷新)则仍以全页面渲染(不拦截)。拦截路由与并行路由配合,是构建沉浸式导航体验的核心模式。
十二、Route Handler 进阶
route.ts 支持完整的 Web API:中间件、Request 流、Response 流、WebSocket 升级。
// app/api/stream/route.ts
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
let count = 0;
const interval = setInterval(() => {
controller.enqueue(encoder.encode(`data: ${++count}\n\n`));
if (count >= 5) {
clearInterval(interval);
controller.close();
}
}, 1000);
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
});
}
// 支持多种 HTTP 方法的统一文件
export async function POST(request: Request) {
const body = await request.json();
// ...处理逻辑
return Response.json({ id: 1, ...body }, { status: 201 });
}
export async function PUT(request: Request) { /* ... */ }
export async function DELETE(request: Request) { /* ... */ }
| 特性 | Server Component | Route Handler |
|---|---|---|
| 访问 request body | ❌ | ✅ await request.json() |
| 流式响应 (SSE) | ❌ | ✅ ReadableStream |
| 文件上传 | 间接 | ✅ FormData / multipart |
| 第三方 API 代理 | 间接 | ✅ 带 CORS 头的透明代理 |
Route Handler 是 Server Component 无法直接处理的 HTTP 交互场景的互补方案。
十三、四层缓存详解与失效策略
Next.js 15 的缓存体系远比 fetch 的 cache 选项复杂。理解四层缓存有助于排查数据不更新、重复渲染等问题:
┌─────────────────────────────────────────┐
│ Layer 4: Full Route Cache │ → CDN / Edge 缓存 HTML
│ (Next.js 渲染结果,ISR/SSG output) │
├─────────────────────────────────────────┤
│ Layer 3: Router Cache │ → 客户端导航缓存
│ (浏览器内存,保留最近访问的 RSC payload) │
├─────────────────────────────────────────┤
│ Layer 2: Data Cache │ → fetch 数据缓存
│ (Next.js 服务端,按 URL+options 键值) │
├─────────────────────────────────────────┤
│ Layer 1: Request Memoization │ → React 请求去重
│ (组件树内,同渲染周期相同 fetch 只执行一次)│
└─────────────────────────────────────────┘
| 层级 | 控制方式 | 默认行为 | 手动失效 |
|---|---|---|---|
| Request Memoization | React.cache() | 渲染周期自动去重 | 无法手动清除 |
| Data Cache | fetch() 的 cache/revalidate/tags | 自动缓存外部 fetch | revalidateTag() / revalidatePath() |
| Router Cache | 客户端导航行为 | 30s 软过期 | router.refresh() / router.push() |
| Full Route Cache | revalidate / dynamic | 静态路由启用 | revalidatePath() / 重新部署 |
手动按需失效(Cache Invalidation)是 ISR 场景的关键能力:
// app/api/revalidate/route.ts
import { revalidateTag, revalidatePath } from 'next/cache';
export async function POST(request: Request) {
const { slug } = await request.json();
// 方式 1:按标签失效(fine-grained)
revalidateTag(`post-${slug}`);
// 方式 2:按路径失效(coarse-grained)
revalidatePath(`/blog/${slug}`);
return Response.json({ revalidated: true, now: Date.now() });
}
生产环境中,推荐 CMS/Webhook 触发 "POST /api/revalidate" 实现内容更新秒级推送。
十四、Image 组件优化与 Core Web Vitals
Next.js 的 <Image> 组件自动处理图片优化、懒加载、尺寸调整:
import Image from 'next/image';
// 使用外部图片需先在 next.config.js 中配置 domains/remotePatterns
<Image
src="https://cdn.example.com/hero.jpg"
alt="Hero"
width={1200}
height={630}
priority // 首屏图片:preload,跳过懒加载
quality={85} // 默认 75,可调整
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ..."
sizes="(max-width: 768px) 100vw, 50vw"
/>
| CWV 指标 | Image 优化手段 | 目标值 |
|---|---|---|
| LCP (最大内容渲染) | 首屏 priority + placeholder + WebP 自动转换 | < 2.5s |
| CLS (累积布局偏移) | 必须提供 width/height 或使用 fill + sizes | < 0.1 |
| INP (交互延迟) | 懒加载非首屏图片,减少主线程阻塞 | < 200ms |
十五、i18n 路由与国际化
App Router 使用中间件实现多语言路由:
// middleware.ts —— 语言路由 + 内容协商
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const locales = ['en', 'zh', 'ja'];
const defaultLocale = 'en';
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
const pathnameLocale = locales.find((l) => pathname.startsWith(`/${l}`));
if (!pathnameLocale) {
// 根据 Accept-Language 或 Cookie 推断语言
const acceptLang = request.headers.get('accept-language')?.split(',')[0]?.split('-')[0];
const locale = locales.includes(acceptLang) ? acceptLang : defaultLocale;
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!api|_next|\\..*).*)'],
};
配合 next-intl 或 react-i18next 管理翻译文件,推荐目录结构:
app/
├── [locale]/
│ ├── layout.tsx # 根据 locale 注入方向(dir)和元数据
│ ├── page.tsx
│ └── about/
│ └── page.tsx
└── i18n/
├── en.ts
└── zh.ts
十六、Edge Runtime vs Node.js Runtime
Next.js 15 支持三种运行时,根据业务需求选择:
| 运行时 | 位置 | 特性 | 限制 | 适用场景 |
|---|---|---|---|---|
| Node.js | 自建服务器 / Docker | 完整 Node 生态(fs、db driver) | 冷启动慢 | 复杂业务逻辑、数据库直连 |
| Edge | Vercel Edge Network | 全球低延迟 (<50ms)、冷启动快 | 无 Node API,30s 超时 | 边缘认证、A/B 测试、轻量代理 |
| Serverless | Vercel Functions | 自动扩缩容 | 10s (Hobby) / 60s (Pro) 超时 | 常规 API 路由 |
// 在 Route Handler 中指定运行时
export const runtime = 'edge'; // 'nodejs' | 'edge'
export const preferredRegion = 'iad1'; // 强制部署到华盛顿地区
Auth.js (NextAuth v5) 的 Edge 兼容配置:
import NextAuth from 'next-auth';
import { authConfig } from './auth.config'; // 不含 Prisma 等 Node-only 依赖
export const { auth, signIn, signOut, handlers } = NextAuth(authConfig);
将数据库查询逻辑放在 auth() 之外的 Server Component 中使用,保留 Edge 运行时的低延迟优势。
相关阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。