App Router 路由系统原理

App Router 是 Next.js 13+ 引入的全新路由范式,完全基于 React Server Components 构建。它用文件系统定义路由、用嵌套布局管理 UI 层次、用 Suspense 控制加载状态。本章深入剖析 App Router 的底层原理——从路由匹配算法到 RSC 渲染流程,帮你建立对路由系统的完整心智模型。

1. 路由匹配原理

1.1 文件系统到路由表

Next.js 在启动时扫描 app/ 目录,构建路由表。核心规则:

文件系统:                          路由表:
app/                               /
├── page.tsx                  →    / (页面)
├── layout.tsx                →    / (布局)
├── loading.tsx               →    / (加载态)
├── chat/
│   ├── page.tsx              →    /chat (页面)
│   ├── layout.tsx            →    /chat (布局)
│   └── [id]/
│       ├── page.tsx          →    /chat/:id (动态页面)
│       └── loading.tsx       →    /chat/:id (加载态)
├── (marketing)/
│   └── pricing/
│       └── page.tsx          →    /pricing (Route Group 不影响 URL)
└── api/
    └── chat/
        └── route.ts          →    /api/chat (API 路由)

1.2 路由匹配优先级

当多个路由可能匹配同一 URL 时,Next.js 按以下优先级选择:

优先级从高到低:
1. 静态路由         /chat/new         → app/chat/new/page.tsx
2. 动态路由         /chat/abc         → app/chat/[id]/page.tsx
3. Catch-all 路由   /chat/a/b/c       → app/chat/[...slug]/page.tsx
4. 可选 Catch-all   /chat 或 /chat/a  → app/chat/[[...slug]]/page.tsx

实际例子——当 URL 是 /chat/new 时:

app/chat/new/page.tsx      ✅ 精确匹配(优先)
app/chat/[id]/page.tsx     ❌ 被跳过
app/chat/[...slug]/page.tsx ❌ 被跳过

1.3 路由段(Segments)

URL 被 / 分割为多个路由段,每个段对应 app/ 下的一个目录:

URL: /chat/abc/messages

段分析:
├── /             → app/ (根段)
├── /chat         → app/chat/ (静态段)
├── /abc          → app/chat/[id]/ (动态段,id="abc")
└── /messages     → app/chat/[id]/messages/ (静态段)

每个路由段可以有自己的 layout.tsxloading.tsxerror.tsx,实现独立的 UI 控制

2. 布局系统深度解析

2.1 布局嵌套机制

App Router 的布局是自动嵌套的——子路由自动被父布局包裹,且父布局在导航时不会重新渲染

// app/layout.tsx — 根布局
export default function RootLayout({ children }: { children: React.ReactNode }) {
  console.log('RootLayout rendered') // 只在首次访问时执行一次
  return (
    <html lang="zh-CN">
      <body>{children}</body>
    </html>
  )
}
 
// app/(dashboard)/layout.tsx — 仪表盘布局
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  console.log('DashboardLayout rendered') // /chat → /settings 导航时不重新执行
  return (
    <div className="flex">
      <Sidebar />
      <main>{children}</main>
    </div>
  )
}
 
// app/(dashboard)/chat/page.tsx
export default async function ChatPage() {
  console.log('ChatPage rendered') // 每次访问 /chat 都执行
  return <div>Chat</div>
}

/chat 导航到 /settings 时:

  • RootLayout不重新渲染(状态保持)
  • DashboardLayout不重新渲染(Sidebar 状态保持)
  • ChatPageSettingsPage替换

2.2 布局状态保持的价值

这个设计在 AI SaaS 中极其重要:

// app/(dashboard)/layout.tsx
'use client' // 需要状态,所以标记为客户端组件
 
import { useState } from 'react'
 
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  const [sidebarOpen, setSidebarOpen] = useState(true)
 
  // 从 /chat 导航到 /settings 时,sidebarOpen 状态保持!
  // 用户不需要重新打开/关闭侧边栏
  return (
    <div className="flex">
      <Sidebar open={sidebarOpen} onToggle={() => setSidebarOpen(!sidebarOpen)} />
      <main>{children}</main>
    </div>
  )
}

2.3 根布局的特殊性

根布局(app/layout.tsx)是唯一必须存在的布局,且有特殊要求:

// app/layout.tsx — 根布局必须包含 <html> 和 <body>
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN">
      <body>
        {/* 全局 Providers 放在这里 */}
        <ThemeProvider>
          <AuthProvider>
            <QueryProvider>
              {children}
            </QueryProvider>
          </AuthProvider>
        </ThemeProvider>
      </body>
    </html>
  )
}
根布局限制
  • 必须包含 &lt;html&gt;&lt;body&gt; 标签
  • 不能使用 'use client'(但可以导入客户端组件)
  • &lt;head&gt; 标签的管理通过 metadata 导出,不要手动写 &lt;head&gt;

3. Route Groups 实战模式

3.1 基于用户角色的分组

AI SaaS 项目最常用的模式——按用户状态分组:

app/
├── (public)/                 # 游客可见
│   ├── layout.tsx            # 公开布局:导航栏 + 页脚
│   ├── page.tsx              # 首页 /
│   ├── pricing/page.tsx      # /pricing
│   ├── blog/
│   │   ├── page.tsx          # /blog
│   │   └── [slug]/page.tsx   # /blog/:slug
│   └── changelog/page.tsx    # /changelog
├── (auth)/                   # 认证流程
│   ├── layout.tsx            # 居中卡片布局
│   ├── login/page.tsx        # /login
│   ├── register/page.tsx     # /register
│   └── forgot-password/page.tsx
├── (app)/                    # 已登录用户
│   ├── layout.tsx            # 侧边栏 + 顶栏布局
│   ├── chat/
│   │   ├── page.tsx          # /chat
│   │   └── [id]/page.tsx     # /chat/:id
│   ├── knowledge/page.tsx    # /knowledge
│   └── settings/page.tsx     # /settings
└── (admin)/                  # 管理员
    ├── layout.tsx            # 管理后台布局
    └── admin/
        ├── page.tsx          # /admin
        └── users/page.tsx    # /admin/users

3.2 多个根布局(高级)

Route Groups 可以有各自独立的根布局,适用于完全不同的 UI 体系:

// app/(marketing)/layout.tsx — 营销站点的根布局
export default function MarketingRootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN">
      <body className="bg-white">
        <MarketingHeader />
        {children}
        <MarketingFooter />
      </body>
    </html>
  )
}
 
// app/(app)/layout.tsx — 应用的根布局(完全不同的 UI)
export default function AppRootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN" className="dark">
      <body className="bg-gray-950">
        <AppSidebar />
        {children}
      </body>
    </html>
  )
}
注意

使用多根布局时,app/layout.tsx 不再需要。但从一个根布局导航到另一个会触发完整页面加载(不是客户端导航)。

4. 路由段配置(Segment Config)

每个路由段可以通过导出特定变量来控制行为:

4.1 动态行为控制

// app/chat/page.tsx
 
// 强制动态渲染(每次请求都重新渲染)
export const dynamic = 'force-dynamic'
 
// 或者强制静态(构建时生成)
export const dynamic = 'force-static'
 
// 自动判断(默认,推荐)
export const dynamic = 'auto'

4.2 重新验证配置

// 页面每 60 秒重新验证(ISR)
export const revalidate = 60
 
// 永不重新验证(纯静态)
export const revalidate = false
 
// 每次请求都重新验证
export const revalidate = 0

4.3 运行时选择

// 使用 Node.js 运行时(默认,支持所有 Node.js API)
export const runtime = 'nodejs'
 
// 使用 Edge 运行时(更快冷启动,但 API 受限)
export const runtime = 'edge'

4.4 配置项汇总

配置可选值默认值作用
dynamic'auto' / 'force-dynamic' / 'force-static' / 'error''auto'控制动态/静态渲染
revalidatenumber / falsefalseISR 重新验证间隔
runtime'nodejs' / 'edge''nodejs'运行时环境
preferredRegionstring / string[]自动部署区域偏好
maxDurationnumber平台限制最大执行时间(秒)

5. RSC 渲染流程与路由

5.1 首次请求的完整流程

用户访问 /chat
    │
    ▼
[1] Next.js Server 接收请求
    │
    ▼
[2] 路由匹配 → app/(app)/chat/page.tsx
    │
    ▼
[3] 构建组件树(从外到内)
    ├── RootLayout (Server Component)
    ├── AppLayout (Server Component)
    ├── ChatLoading (作为 Suspense fallback)
    └── ChatPage (Server Component, async)
    │
    ▼
[4] 渲染 Server Components
    ├── RootLayout → 生成 HTML 外壳
    ├── AppLayout → 生成侧边栏 HTML
    └── ChatPage → 等待数据库查询...
    │
    ▼
[5] 流式发送
    ├── 先发送 HTML 外壳 + 侧边栏 + Loading 骨架屏
    ├── 数据库查询完成 → 发送 ChatPage 的 HTML
    └── 发送 Client Component 的 JS Bundle 引用
    │
    ▼
[6] 浏览器
    ├── 立即显示 HTML(用户看到侧边栏 + 骨架屏)
    ├── ChatPage HTML 到达 → 替换骨架屏
    └── JS 加载完成 → Hydration → Client Components 可交互

5.2 客户端导航的流程

/chat 导航到 /settings(客户端导航,不刷新页面):

用户点击 Settings 链接
    │
    ▼
[1] Next.js Router 拦截导航
    │
    ▼
[2] 发送 RSC 请求到服务端
    GET /settings?_rsc=xxx
    │
    ▼
[3] 服务端只渲染**变化的部分**
    ├── RootLayout → 跳过(已在客户端)
    ├── AppLayout → 跳过(已在客户端)
    └── SettingsPage → 渲染并返回 RSC Payload
    │
    ▼
[4] 浏览器接收 RSC Payload
    ├── React 将 SettingsPage 渲染结果替换到布局中
    └── 布局和侧边栏状态保持不变

关键:客户端导航不会重新请求布局——只请求变化的页面部分,这就是布局状态能保持的原因。

5.3 Prefetch 预加载

Next.js 的 &lt;Link&gt; 组件默认会预加载链接目标的 RSC Payload:

import Link from 'next/link'
 
// 当这个 Link 出现在视口中时,Next.js 自动预加载 /settings 的数据
<Link href="/settings">Settings</Link>
 
// 禁用预加载
<Link href="/settings" prefetch={false}>Settings</Link>
 
// 预加载整个页面(包括动态数据)
<Link href="/settings" prefetch={true}>Settings</Link>

预加载策略:

prefetch行为
null(默认)预加载静态部分(布局 + loading.tsx),不预加载动态数据
true预加载整个页面(包括动态数据)
false不预加载

6. Pages Router vs App Router

6.1 核心差异

维度Pages RouterApp Router
组件模型全部客户端组件Server Components 优先
数据获取getServerSideProps / getStaticPropsasync Server Components
布局手动嵌套 + _app.tsx文件系统自动嵌套
加载状态手动实现loading.tsx 自动 Suspense
错误处理_error.tsx 全局error.tsx 按路由段
流式渲染不支持原生支持
JS Bundle发送所有组件的 JS只发送 Client Components 的 JS

6.2 迁移心智模型

// Pages Router 思维
// pages/chat.tsx — 一个文件搞定所有
export async function getServerSideProps() {
  const chats = await getChats()
  return { props: { chats } }
}
export default function ChatPage({ chats }) {
  return <ChatList chats={chats} />
}
 
// App Router 思维
// app/chat/page.tsx — Server Component 直接获取数据
export default async function ChatPage() {
  const chats = await getChats() // 直接调用,无需 getServerSideProps
  return <ChatList chats={chats} />
}

本章小结

  • 路由匹配:静态 > 动态 > Catch-all > 可选 Catch-all,精确匹配优先
  • 布局嵌套:自动嵌套、导航时不重新渲染、状态保持——AI SaaS 侧边栏的基础
  • Route Groups:按用户角色分组(public/auth/app/admin),支持多根布局
  • 路由段配置dynamicrevalidateruntime 精细控制每个路由的行为
  • RSC 渲染流程:首次请求流式发送,客户端导航只请求变化部分,Prefetch 提前加载
  • vs Pages Router:App Router 是全新范式,不是简单升级

下一章深入动态路由、并行路由与拦截路由。