DevToolBox免费
博客

Next.js 高级指南:App Router、服务端组件、数据获取、中间件与性能优化

22 分钟阅读作者 DevToolBox Team

Next.js 高级指南:App Router、Server Components、缓存、ISR 与生产部署

全面掌握 Next.js 高级用法:App Router 架构与布局模板、Server 与 Client Components 对比、fetch 缓存与重新验证的数据获取、Server Actions 数据变更、中间件模式、路由处理器与流式传输、缓存策略、图片与字体优化、国际化、NextAuth.js 认证、ISR 与按需重新验证、平行与拦截路由、Docker 与 OpenTelemetry 生产部署。

TL;DRNext.js App Router 引入了以 React Server Components 为默认的新范式、嵌套布局和数据获取共置。Server Actions 无需 API 路由即可简化数据变更。四层缓存系统(请求记忆化、数据缓存、完整路由缓存、路由缓存)显著提升性能。使用 ISR 配合 revalidatePath/revalidateTag 进行增量更新。中间件在边缘处理认证、重定向和国际化。使用 Vercel 零配置部署或 Docker 完全掌控,并通过 OpenTelemetry 实现可观测性。
核心要点
  • App Router 默认使用 React Server Components,显著减少客户端 JavaScript 包体积
  • Server Actions 取代了大多数 API 路由,支持渐进增强和乐观更新
  • 四层缓存系统(请求记忆化、数据缓存、完整路由缓存、路由器缓存)需要逐层理解
  • 中间件在边缘运行,适合认证、重定向、国际化和 A/B 测试
  • ISR 配合 revalidateTag 实现 CMS webhook 驱动的即时内容更新
  • 平行路由和拦截路由实现模态框覆盖层等复杂 UI 模式
  • standalone 输出模式配合 Docker 多阶段构建是自托管的最佳实践

Next.js App Router 代表了 React 全栈开发的根本性转变。它引入了 React Server Components 作为默认渲染方式、嵌套布局系统、Server Actions 用于数据变更、以及多层缓存架构。本指南覆盖 13 个高级主题,每个部分都包含可直接使用的代码示例和生产环境最佳实践。

无论你是从 Pages Router 迁移还是从零构建新项目,理解这些概念将帮助你充分发挥 App Router 的能力。从架构设计到部署监控,让我们逐一深入。

本指南中的所有代码示例都基于 Next.js 14+ 和 React 18+ 版本。建议使用 TypeScript 以获得最佳的类型安全和开发体验。如果你还在使用 Pages Router,本指南末尾包含了迁移建议部分。

每个主题都包含一个实用代码示例、关键概念解释和最佳实践提示。建议按顺序阅读以建立完整的知识体系,也可以直接跳到你最关心的章节。

1. App Router 架构

App Router 基于文件系统约定构建路由。每个文件夹代表一个路由段,特殊文件名定义该段的 UI 行为。理解这些约定是使用 App Router 的基础。

  • layout.tsx持久布局,子路由导航时不重新渲染,保持滚动位置和状态
  • template.tsx每次导航时重新挂载,适合进入/退出动画或每页 analytics 追踪
  • loading.tsx自动包裹在 React Suspense 中的加载回退组件
  • error.tsx错误边界,捕获该段及子级的运行时错误,提供重试按钮
  • not-found.tsx当调用 notFound() 函数或路径不匹配时显示的 404 页面
// app/dashboard/layout.tsx — persistent layout
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <section>
      <nav>Dashboard Sidebar</nav>
      <main>{children}</main>
    </section>
  );
}

// app/dashboard/loading.tsx — Suspense fallback
export default function Loading() {
  return <div>Loading dashboard data...</div>;
}

布局在子路由间保持状态且不重新渲染。例如,仪表盘侧边栏在页面切换时不会闪烁。模板则在每次导航时创建新实例——在需要每页触发动画或重置表单状态的场景中使用模板而非布局。

提示: 路由组 (folder-name) 可以在不影响 URL 的情况下组织路由。例如 (marketing) 和 (shop) 可以共享不同的根布局。

文件层级与渲染顺序

当请求到达一个路由段时,Next.js 按以下顺序渲染特殊文件:layout.tsx 包裹 template.tsx,template.tsx 包裹 error.tsx 错误边界,error.tsx 包裹 loading.tsx 的 Suspense 回退,最后是 page.tsx 的实际内容。理解这个嵌套关系有助于你正确放置错误处理和加载逻辑。

注意 error.tsx 无法捕获同级 layout.tsx 中的错误,因为错误边界是嵌套在布局内部的。要捕获根布局的错误,需要使用 global-error.tsx 文件。这个文件会替换整个根布局,因此必须包含自己的 html 和 body 标签。

使用路由组 (folder-name) 可以在不影响 URL 的情况下为不同部分的应用创建独立的布局。例如 (marketing)/layout.tsx 和 (app)/layout.tsx 可以拥有完全不同的导航栏和页脚,而 URL 路径中不会出现 marketing 或 app 前缀。

2. Server Components 与 Client Components

在 App Router 中,所有组件默认是 Server Components。它们在服务器上渲染,可以直接访问数据库和后端资源,且不向客户端发送任何 JavaScript。这是减少包体积的关键架构决策。

只有需要交互性(useState、useEffect、事件处理器)或浏览器 API(window、localStorage)的组件才需要标记 "use client"。关键原则是将 Client Components 推到组件树的叶子节点。

// Server Component (default) — direct database access
async function ProductPage({ id }: { id: string }) {
  const product = await db.product.findUnique({ where: { id } });
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <AddToCartButton productId={id} />
    </div>
  );
}

// Client Component — needs 'use client'
'use client';
function AddToCartButton({ productId }: { productId: string }) {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>Add ({count})</button>;
}

Server Components 可以渲染 Client Components,但反过来不行。如果需要在 Client Component 内部使用服务端内容,通过 children 或 props 传递。这种组合模式被称为"串联模式",是 App Router 性能优化的核心。

提示: "use client" 标记的是模块边界,不是单个组件。该文件中的所有导出和它导入的模块都成为客户端包的一部分。

何时使用哪种组件

  • 使用 Server Components: 数据获取、访问后端资源、渲染静态内容、SEO 关键内容、敏感信息(API 密钥)
  • 使用 Client Components: 用户交互(点击、输入)、useState/useEffect、浏览器 API(geolocation、localStorage)、第三方交互式库

常见错误是将整个页面标记为 "use client"。这会导致该页面及其所有子组件都被打包到客户端 JavaScript 中,失去 Server Components 的零 JS 优势。始终先问自己:这个组件真的需要在客户端运行吗?

第三方库的选择也很重要:许多 React 库(如状态管理库、动画库)依赖客户端 API,因此需要在 Client Component 边界内使用。你可以创建一个薄的 Client Component 包装器来封装这些库,保持页面的其余部分作为 Server Components。

3. 数据获取模式

App Router 使用扩展的 fetch API 进行数据获取。在 Server Components 中直接使用 async/await,配合缓存和重新验证选项控制数据新鲜度。React 自动对同一 render pass 中相同 URL 的请求进行记忆化去重。

// Cached fetch with time-based revalidation
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 3600 }, // revalidate every hour
  });
  return res.json();
}

// Parallel data fetching — avoid waterfalls
async function Dashboard() {
  const [users, revenue, orders] = await Promise.all([
    getUsers(),
    getRevenue(),
    getRecentOrders(),
  ]);
  return <DashboardView users={users} revenue={revenue} orders={orders} />;
}

关键选项:cache: "no-store" 获取始终最新的动态数据。next: { tags: ["products"] } 为数据打标签以支持按需失效。next: { revalidate: 60 } 设置基于时间的自动刷新。避免在客户端组件中使用 useEffect 获取数据——尽可能在服务端获取。

使用 Promise.all 并行获取独立数据可以显著减少总加载时间。如果某些数据不关键,可以配合 Suspense 实现流式加载——让页面的关键部分先显示,非关键部分异步加载。

注意: 在 Server Components 中使用 fetch 时,不能传递 AbortController 或自定义 agent。如果你使用 ORM(如 Prisma)直接查询数据库而非 fetch API,请用 React 的 cache() 函数包装查询函数以获得请求去重能力。

数据获取最佳实践

  • 在 Server Components 中获取数据,避免在 Client Components 中使用 useEffect + fetch 模式
  • 使用 Promise.all 并行获取互不依赖的数据,减少总延迟
  • 利用 Suspense 边界实现流式渲染——关键内容先显示,非关键内容异步加载
  • 为 fetch 请求设置合理的 revalidate 时间或 tags,而不是一律使用 no-store
  • 使用 React cache() 函数包装数据库查询,确保同一 render pass 中的去重

对于非 fetch API 的数据源(如直接数据库查询),使用 React 的 cache() 函数实现请求记忆化。对于需要客户端实时更新的数据(如在线状态、协作编辑),仍然需要在 Client Components 中使用 WebSocket 或轮询。

4. Server Actions

Server Actions 是在服务器上运行的异步函数,通过 "use server" 标记。它们可以从 Client Components 直接调用,取代传统 API 路由用于表单提交和数据变更。最大的优势是支持渐进增强——即使 JavaScript 未加载,HTML 表单也能提交。

// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';

export async function createTodo(formData: FormData) {
  const title = formData.get('title') as string;
  await db.todo.create({ data: { title } });
  revalidatePath('/todos');
}

// app/todos/page.tsx — works without JS!
import { createTodo } from '../actions';

export default function TodoPage() {
  return (
    <form action={createTodo}>
      <input name="title" required />
      <button type="submit">Add Todo</button>
    </form>
  );
}

useOptimistic hook 可以在等待服务器响应时立即更新 UI,提供即时反馈。useActionState 提供 pending 状态用于显示加载指示器并接收服务端返回的验证错误。Server Actions 不限于表单——它们可以在任何客户端事件中调用。

最佳实践: 始终在 Server Actions 中验证输入数据。使用 zod 等库进行服务端验证,因为客户端验证可以被绕过。调用 revalidatePath 或 revalidateTag 确保缓存更新。

Server Actions 与 API 路由的选择

Server Actions 适用于表单提交、数据变更、与 UI 紧密耦合的操作。API Routes (Route Handlers) 适用于第三方 webhook、公开 API 端点、文件下载/上传和 SSE 流。一般原则:如果操作由用户在前端触发,用 Server Actions;如果需要被外部系统调用,用 Route Handlers。

Server Actions 的另一大优势是类型安全——你可以直接在客户端代码中导入并调用服务端函数,TypeScript 会自动推导参数和返回值类型。这消除了手动定义 API 请求和响应类型的样板代码。

5. 中间件

Next.js 中间件在边缘运行,在请求到达任何路由之前执行。它适合认证检查、基于地理位置的重定向、请求头操作、A/B 测试和国际化语言检测。中间件文件必须放在项目根目录(与 app 目录同级)。

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session-token')?.value;
  const isProtected = request.nextUrl.pathname.startsWith('/dashboard');

  if (isProtected && !token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  const response = NextResponse.next();
  response.headers.set('x-request-id', crypto.randomUUID());
  return response;
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
};

matcher 配置精确指定中间件应用的路由。避免使用 matcher: "/(.*)" 匹配所有路由——这会让静态资源请求也经过中间件。保持中间件逻辑简单快速,不要执行数据库查询或外部 API 调用。

常见中间件用例:检查 JWT token 有效性、根据 Accept-Language 头重定向到对应语言版本、为 A/B 测试设置 cookie 和重写 URL、添加安全相关的响应头(CSP、CORS)。

注意: 中间件只能使用 Edge Runtime 支持的 API。这意味着不能使用 Node.js 特定的模块如 fs、path 或大多数 npm 包。如果需要访问数据库,考虑使用支持边缘运行时的客户端(如 Planetscale 或 Neon)。

6. Route Handlers (API 路由)

Route Handlers 在 App Router 中创建 RESTful API 端点。使用 route.ts 文件定义 GET、POST、PUT、DELETE 等 HTTP 方法。它们使用 Web 标准 Request/Response API,支持流式响应和 Server-Sent Events。

// app/api/stream/route.ts — Server-Sent Events
export async function GET() {
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      const events = ['Processing...', 'Analyzing...', 'Done!'];
      for (const event of events) {
        await new Promise(r => setTimeout(r, 1000));
        controller.enqueue(encoder.encode('data: ' + event + '\n\n'));
      }
      controller.close();
    },
  });
  return new Response(stream, {
    headers: { 'Content-Type': 'text/event-stream' },
  });
}

GET Route Handlers 在不使用动态函数(cookies()、headers()、searchParams)时默认静态缓存。使用 export const dynamic = "force-dynamic" 禁用缓存。注意 route.ts 不能与同目录的 page.tsx 共存——它们会冲突。

Route Handlers 适合第三方 webhook 接收端点、文件上传处理、SSE 流式推送、以及需要自定义响应头或状态码的场景。对于纯数据变更操作,优先使用 Server Actions 而非 Route Handlers。

Route Handlers 高级模式

Route Handlers 支持所有 Web 标准 Response 类型。你可以返回 JSON、纯文本、二进制流、重定向和自定义状态码。对于文件下载场景,设置 Content-Disposition 头和正确的 Content-Type。对于 CORS 请求,在 OPTIONS 方法中返回适当的 Access-Control 头。

使用动态路由段(如 app/api/users/[id]/route.ts)创建 RESTful 端点。通过解构 params 参数获取动态值。Route Handlers 可以导出 GET、POST、PUT、PATCH、DELETE、HEAD 和 OPTIONS 方法。

注意: Route Handlers 默认运行在 Node.js 运行时。如果需要使用 Edge Runtime(更快的冷启动、更低的延迟),添加 export const runtime = "edge"。但 Edge Runtime 不支持所有 Node.js API,如 fs 模块和某些 npm 包。

7. 缓存策略

Next.js 的缓存是性能优化的核心,也是最容易产生困惑的部分。App Router 引入了一个四层缓存体系,每层解决不同层级的缓存需求。理解每层的行为、持续时间和失效机制至关重要。

  • 请求记忆化同一 render pass 中相同 fetch 自动去重,无需手动管理,组件树中多处获取相同数据只触发一次请求
  • 数据缓存跨请求和部署持久化 fetch 结果,通过 revalidate 时间或 tags 标签控制失效
  • 完整路由缓存构建时缓存渲染的 HTML 和 RSC Payload,用于静态路由的即时响应
  • 路由器缓存客户端缓存预获取的路由段,实现前进/后退的即时导航体验
// Opting out of caching at each level

// No data cache — always fetch fresh data
const data = await fetch(url, { cache: 'no-store' });

// Time-based revalidation — refresh every 60s
const data = await fetch(url, { next: { revalidate: 60 } });

// Tag-based invalidation
const data = await fetch(url, { next: { tags: ['products'] } });
// Invalidate: revalidateTag('products');

// Route segment config — disable full route cache
export const dynamic = 'force-dynamic';
export const revalidate = 0;

在开发环境中,所有缓存层默认禁用以便调试。生产环境中,使用 router.refresh() 手动刷新客户端路由器缓存。如果你的页面数据总是过期,首先检查是否正确配置了 revalidate 选项。

缓存调试技巧

当缓存行为不符合预期时,可以通过以下方式排查:在 fetch 请求中添加自定义 header 检查请求是否真正发出、查看 Next.js 构建输出中的路由类型标记(静态 / 动态)、使用 next build 的输出日志确认哪些页面被静态生成。

一个常见的混淆点:即使你在 fetch 中设置了 revalidate: 60,如果该路由的 generateStaticParams 没有返回对应的参数,页面仍然会在每次请求时动态渲染。确保你的静态生成参数配置正确。

注意 Next.js 构建输出中的路由标记:圆圈 (○) 表示静态路由,lambda (λ) 表示动态路由。如果一个你期望是静态的路由被标记为动态,检查是否在该路由中使用了 cookies()、headers()、searchParams 或 cache: "no-store" 等动态 API。

8. 图片与字体优化

next/image 自动优化图片:转换 WebP/AVIF 格式、生成响应式 srcset、默认懒加载、通过强制 width/height 防止累积布局偏移 (CLS)。next/font 自托管字体,消除外部网络请求并通过 CSS size-adjust 实现零布局偏移。

import Image from 'next/image';
import { Inter, Fira_Code } from 'next/font/google';

const inter = Inter({ subsets: ['latin'], display: 'swap' });
const firaCode = Fira_Code({
  subsets: ['latin'],
  weight: ['400', '700'],
});

export default function Page() {
  return (
    <div style={{ fontFamily: inter.style.fontFamily }}>
      <Image
        src="/hero.jpg" alt="Hero"
        width={1200} height={630}
        placeholder="blur"
        blurDataURL="data:image/jpeg;base64,/9j/4AAQ..."
        priority
      />
    </div>
  );
}

对首屏可见图片使用 priority 预加载以提升 LCP 分数。使用 sizes 属性帮助浏览器选择正确的响应式尺寸。远程图片需在 next.config.js 的 images.remotePatterns 中配置允许的域名、协议和路径。生成 blurDataURL 可使用 plaiceholder 库或 Next.js 内置的静态导入 blur 功能。

图片优化检查清单

  • 首屏可见图片添加 priority={true},触发预加载
  • 提供准确的 sizes 属性(如 "(max-width: 768px) 100vw, 50vw")避免下载过大图片
  • 使用 placeholder="blur" 提供模糊占位符,减少视觉跳动
  • 为装饰性图片设置 aria-hidden 或空 alt 文本
  • 考虑使用 quality 属性(默认75)在质量和文件大小间取得平衡

next/font 通过在构建时下载字体文件并作为静态资源提供,消除了运行时的外部字体请求。display: "swap" 确保文本在字体加载前使用后备字体显示。结合 variable 选项可以创建 CSS 变量用于在不同组件中引用字体。

9. 国际化 (i18n)

App Router 通过 [lang] 动态路由段和中间件语言检测实现国际化。这种方式不依赖外部 i18n 库——使用字典文件管理翻译,通过 getDictionary(lang) 在 Server Components 中加载。

// middleware.ts — locale detection and redirect
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';

const locales = ['en', 'zh', 'ja', 'de'];
const defaultLocale = 'en';

function getLocale(request: Request): string {
  const headers = {
    'accept-language': request.headers.get('accept-language') || ''
  };
  const languages = new Negotiator({ headers }).languages();
  return match(languages, locales, defaultLocale);
}

// app/[lang]/layout.tsx — generate all locale params
export async function generateStaticParams() {
  return [{ lang: 'en' }, { lang: 'zh' }, { lang: 'ja' }];
}

为每个语言使用 generateStaticParams 预渲染页面。如果语言版本过多导致构建时间过长,考虑只预渲染主要语言,其余使用 ISR 按需生成。将翻译字典文件放在 Server Components 中加载以避免增大客户端包体积。

为不同语言版本配置 hreflang 标签以帮助搜索引擎理解页面间的语言关系。在 metadata 中使用 alternates.languages 对象自动生成这些标签。

i18n 性能优化

当支持的语言版本较多时(例如 15 种以上),在构建时预渲染所有语言的所有页面可能导致构建时间过长和输出体积过大。推荐策略是:只预渲染默认语言和流量最大的 2-3 种语言,其余语言使用 ISR 按需生成。这种方式可以将构建时间从数十分钟缩短到几分钟。

将翻译字典文件保持在合理大小。如果翻译内容超过几百 KB,考虑按页面或功能模块拆分字典,只在需要时加载对应部分。在 Server Components 中加载字典不会增加客户端包体积。

提示: 使用 generateMetadata 函数为每个语言版本生成本地化的 SEO 元数据(title、description、Open Graph 标签)。这对多语言站点的搜索引擎排名至关重要。同时在 alternates 中设置 canonical URL 和 hreflang 链接。

10. 认证

NextAuth.js (Auth.js v5) 与 App Router 深度集成,提供 OAuth、Credentials 和 Magic Link 认证策略。Session 支持 JWT(无状态,适合边缘运行时)和数据库模式(适合需要服务端控制的会话管理)。

// auth.ts — NextAuth.js v5 configuration
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [GitHub],
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isProtected = nextUrl.pathname.startsWith('/dashboard');
      if (isProtected && !isLoggedIn) return false;
      return true;
    },
  },
});

// app/dashboard/page.tsx — protected page
import { auth } from '@/auth';
export default async function Dashboard() {
  const session = await auth();
  return <div>Welcome, {session?.user?.name}</div>;
}

auth() 可在 Server Components、Route Handlers 和 Server Actions 中使用。authorized 回调配合中间件保护整个路由组。对于需要更细粒度控制的场景,在页面中调用 auth() 并根据角色条件渲染不同内容。

对于不使用 NextAuth 的场景,你可以在中间件中直接验证 JWT token,或使用 iron-session 等轻量级库管理加密 cookie 会话。核心思路保持不变:在中间件中做路由级保护,在组件中做细粒度权限检查。

认证安全最佳实践

  • 使用 HttpOnly + Secure + SameSite=Lax 的 cookie 存储 session token,防止 XSS 和 CSRF 攻击
  • 在中间件中只做快速的 token 存在性检查,将完整的 token 验证放在 Server Components 或 Route Handlers 中
  • 使用 CSRF token 保护 Server Actions,NextAuth 默认提供此功能
  • 定期轮换 session secret,使用环境变量管理密钥
  • 对敏感操作实施速率限制,防止暴力破解攻击

在多租户应用中,确保认证中间件不仅检查用户是否已登录,还要验证用户是否有权限访问请求的资源。将授权逻辑放在 Server Components 或 Server Actions 中,在数据层面进行细粒度的权限检查。

11. ISR 与按需重新验证

ISR 让你在不重建整个站点的情况下更新静态页面。App Router 提供基于时间和按需两种重新验证方式。revalidatePath 按路径失效,revalidateTag 按数据标签失效——后者更灵活,因为一个标签可以关联多个页面的多个 fetch 调用。

// Time-based ISR — auto-revalidate every 60s
export const revalidate = 60;

async function BlogPost({ slug }: { slug: string }) {
  const post = await fetch('https://cms.example.com/posts/' + slug, {
    next: { tags: ['post-' + slug] },
  }).then(r => r.json());
  return <article>{post.title}</article>;
}

// On-demand revalidation via CMS webhook
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';

export async function POST(request: Request) {
  const { slug } = await request.json();
  revalidateTag('post-' + slug);
  return Response.json({ revalidated: true });
}

典型流程:内容编辑在 CMS 中发布文章 -> CMS webhook 调用 /api/revalidate -> revalidateTag 标记相关数据为过期 -> 下一次请求触发重新生成。这比定时重新验证更高效,因为只在数据变更时才刷新。

提示: 在 revalidation API 路由中添加密钥验证,防止未授权的缓存失效请求。使用环境变量存储密钥并在请求头中校验。

ISR 与 SSG 的区别

静态站点生成 (SSG) 在构建时一次性生成所有页面,之后不再更新。ISR 在此基础上增加了后台重新生成的能力——当 revalidate 时间到期或按需触发时,Next.js 在后台生成新版本,旧版本继续服务请求直到新版本就绪。这种 stale-while-revalidate 策略确保用户永远不会看到加载状态。

对于拥有大量内容的站点(如电商网站的数千个产品页),在构建时生成所有页面不现实。使用 dynamicParams: true(默认值)允许未在 generateStaticParams 中列出的路径在首次请求时按需生成并缓存。结合 revalidate 配置实现完美的 ISR 策略。

如果你需要在内容删除时返回 404 而非显示过期内容,可以在 Server Component 中检查数据是否存在,不存在时调用 notFound() 函数。这会触发最近的 not-found.tsx 文件渲染。

12. 平行路由与拦截路由

平行路由使用 @folder 约定在同一布局中同时渲染多个页面。每个 slot 独立加载和出错,非常适合仪表盘的独立面板。拦截路由使用 (.) 约定在不同上下文中展示路由——最典型的场景是照片画廊中的模态框。

// Parallel routes — dashboard layout
// app/dashboard/layout.tsx
export default function Layout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <div>
      {children}
      {analytics}  {/* @analytics/page.tsx */}
      {team}       {/* @team/page.tsx */}
    </div>
  );
}

拦截层级约定:(.) 匹配同级、(..) 上一级、(..)(..) 上两级、(...) 匹配根级。结合两者可以创建模态框体验——软导航(Link 点击)显示模态框叠加层,硬导航(直接访问 URL 或刷新)显示完整页面,且 URL 保持可共享。

为平行路由的每个 slot 提供 default.tsx 文件,定义当该 slot 没有匹配的活动路由时应该渲染的内容。这避免了 Next.js 在导航时因找不到匹配内容而返回 404。

模态框路由实战模式

实现模态框路由的典型结构:使用 @modal 平行 slot 配合拦截路由。例如照片画廊中,/photos/[id] 是完整页面,(.)photos/[id] 拦截该路由在模态框中展示。用户通过 Link 组件导航时看到模态框覆盖层;直接访问 URL 或刷新页面时看到完整的照片详情页。这提供了最佳的用户体验和 URL 可共享性。

在 @modal 的 default.tsx 中返回 null,这样当没有模态框活动时不渲染任何内容。在模态框组件内部处理关闭逻辑时使用 router.back() 返回上一页,保持浏览器历史记录的正确性。

13. 部署与监控

选择部署方式取决于你的需求:Vercel 提供零配置部署,自动处理边缘函数、ISR、图片优化和全球 CDN,是最省心的选择。自托管则提供完全的控制权,适合有特殊合规要求或已有基础设施的团队。

自托管时,使用 output: "standalone" 生成精简的 Node.js 服务器,包含运行时需要的所有依赖。配合 Docker 多阶段构建将镜像缩到最小——通常可以从 1GB+ 缩到 100-200MB。OpenTelemetry 集成提供请求级别的追踪和性能监控。

# Multi-stage Dockerfile for Next.js
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]

在 next.config.js 中设置 output: "standalone"。创建 /api/health 健康检查端点供 Kubernetes 或负载均衡器使用。Nginx 反向代理处理 SSL 终止、gzip 压缩和静态资源长期缓存。使用 PM2 的 cluster 模式充分利用多核 CPU。

监控要点: 集成 @vercel/otel 启用 OpenTelemetry 追踪。监控 Core Web Vitals(LCP、FID、CLS)。在 instrumentation.ts 文件中初始化 SDK,追踪服务端请求延迟和数据库查询耗时。

生产部署检查清单

  • 在 next.config.js 中设置 output: "standalone" 以生成精简的独立服务器
  • 创建 /api/health 端点返回 200 状态码,供负载均衡器和 Kubernetes 探针使用
  • 配置 Nginx 反向代理:SSL 终止、gzip/brotli 压缩、_next/static 路径的长期缓存头
  • 设置 NEXT_SHARP_PATH 环境变量确保 Docker 中 sharp 图片库正常工作
  • 使用 PM2 cluster 模式运行,充分利用服务器的多核 CPU
  • 在 instrumentation.ts 中初始化 OpenTelemetry,追踪每个请求的完整生命周期
  • 配置环境变量管理(不要在 Docker 镜像中硬编码密钥)
  • 设置日志聚合(如 Datadog、Grafana Loki)监控错误率和响应时间分布

对于 Kubernetes 部署,配置就绪探针(readiness probe)指向健康检查端点,设置合理的 CPU 和内存限制,并使用 HPA (Horizontal Pod Autoscaler) 根据负载自动扩缩容。Next.js standalone 输出的内存占用通常在 100-300MB 之间,具体取决于应用大小。

总结

Next.js App Router 代表了 React 全栈开发的未来。Server Components 减少客户端负担,Server Actions 简化数据变更,四层缓存提供细粒度性能控制。中间件处理边缘横切关注点,ISR 实现按需增量更新。平行和拦截路由支持复杂 UI 模式。掌握这 13 个主题,你可以构建真正高性能的现代 Web 应用。

从小处开始:在新项目中使用 App Router,逐步采用 Server Components 和 Server Actions,在需要时引入缓存和 ISR 策略。随着对每层抽象的理解加深,你会发现 App Router 的设计决策为构建可扩展应用提供了坚实的基础。

迁移建议

如果你正在从 Pages Router 迁移,不需要一次性重写整个应用。Next.js 支持 app 和 pages 目录共存。推荐的迁移策略是:从新页面开始使用 App Router,然后逐步将现有页面迁移。将 getServerSideProps 替换为 Server Components 中的 async/await fetch,将 getStaticProps 替换为带 revalidate 的 fetch,将 API routes 迁移为 Route Handlers。

在迁移过程中,注意以下关键变化:useRouter 从 next/router 变为 next/navigation,pathname 获取方式改为 usePathname(),query 参数使用 useSearchParams(),redirect 从命令式变为声明式(从 next/navigation 导入 redirect 函数)。这些 API 变化反映了从客户端路由到服务端优先路由的范式转变。

性能优化总结

  • 默认使用 Server Components,将 Client Components 推到叶子节点以最小化 JS 包体积
  • 使用 Promise.all 并行获取数据,配合 Suspense 实现流式渲染
  • 合理配置缓存层级:为不同数据源选择合适的 revalidate 时间或 tags
  • 首屏图片使用 priority 预加载,所有图片使用 next/image 自动优化
  • 使用 next/font 自托管字体消除外部请求
  • 仅预渲染高流量语言版本,其余使用 ISR 按需生成以缩短构建时间
  • 使用 standalone 输出配合 Docker 多阶段构建优化部署镜像
  • 集成 OpenTelemetry 追踪以识别生产环境中的性能瓶颈

常见问题

Server Components 和 Client Components 有什么区别?

Server Components 在服务器渲染,不发送 JavaScript 到客户端,可直接访问后端资源。Client Components 用 "use client" 标记,支持 useState 和事件处理器等交互。默认使用 Server Components,仅在需要交互或浏览器 API 时使用 Client Components。

App Router 的缓存如何工作?

四层缓存:请求记忆化去重同一 render pass 的 fetch,数据缓存跨请求持久化结果,完整路由缓存存储构建时的 HTML/RSC Payload,路由器缓存在客户端缓存预取的路由段。每层通过 cache、revalidate、no-store 独立配置。

Server Actions 如何工作?

Server Actions 是 "use server" 标记的异步函数,在服务器运行。它们可从表单 action 或客户端事件直接调用,支持渐进增强、useOptimistic 乐观更新、useActionState 加载状态,并通过 revalidatePath/revalidateTag 触发缓存失效。

如何在 App Router 中实现 ISR?

通过 fetch 的 next: { revalidate: 3600 } 选项或 export const revalidate = 3600 路由段配置实现基于时间的 ISR。按需重新验证在 Server Actions 或 Route Handlers 中调用 revalidatePath() 或 revalidateTag(),通常由 CMS webhook 触发。

如何设置认证中间件?

在项目根创建 middleware.ts,检查请求中的 session token 或 cookie。未认证用户用 NextResponse.redirect() 重定向到登录页。config.matcher 精确指定需要保护的路由模式。中间件在边缘运行,在任何路由渲染之前执行。

什么是平行路由和拦截路由?

平行路由 (@folder) 在同一布局同时渲染多个独立页面,每个有自己的加载和错误状态。拦截路由 ((.) 约定) 在不同上下文展示路由,如模态框。两者结合实现软导航显示模态、硬导航显示完整页面的可共享 URL 模态体验。

如何优化图片和字体?

next/image 自动处理 WebP/AVIF 转换、响应式 srcset、懒加载和布局偏移防护。首屏图片加 priority。next/font 自托管字体,零布局偏移且无外部请求。使用 sizes 属性优化响应式加载,remotePatterns 配置远程图片域名。

如何用 Docker 部署 Next.js?

多阶段 Dockerfile:deps 阶段装依赖,builder 阶段构建(output: "standalone"),runner 阶段只复制 standalone 输出、public 和 .next/static。添加健康检查端点,设置 NODE_ENV=production,Nginx 反向代理处理 SSL 和压缩,@vercel/otel 集成 OpenTelemetry 追踪。

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

{ }JSON FormatterJSTypeScript to JavaScriptTSJSON to TypeScript

相关文章

React 设计模式指南:复合组件、自定义 Hook、HOC、Render Props 与状态机

完整的 React 设计模式指南,涵盖复合组件、render props、自定义 hooks、高阶组件、Provider 模式、状态机、受控与非受控、组合模式、观察者模式、错误边界和模块模式。

高级TypeScript指南:泛型、条件类型、映射类型、装饰器和类型收窄

掌握高级TypeScript模式。涵盖泛型约束、带infer的条件类型、映射类型(Partial/Pick/Omit)、模板字面量类型、判别联合、工具类型深入、装饰器、模块增强、类型收窄、协变/逆变以及satisfies运算符。

Tailwind CSS 高级指南:v4 新特性、自定义插件、暗黑模式、CVA、动画与性能优化

完整的 Tailwind CSS 高级指南,涵盖 v4 新特性、设计系统、自定义插件、响应式设计、暗黑模式、动画、CVA 组件模式、React 集成、性能优化、任意值、Grid 布局和 v3 到 v4 迁移。