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.tsx、loading.tsx、error.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 状态保持)ChatPage→SettingsPage— 替换
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>
)
}- 必须包含
<html>和<body>标签 - 不能使用
'use client'(但可以导入客户端组件) <head>标签的管理通过metadata导出,不要手动写<head>
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 = 04.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' | 控制动态/静态渲染 |
revalidate | number / false | false | ISR 重新验证间隔 |
runtime | 'nodejs' / 'edge' | 'nodejs' | 运行时环境 |
preferredRegion | string / string[] | 自动 | 部署区域偏好 |
maxDuration | number | 平台限制 | 最大执行时间(秒) |
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 的 <Link> 组件默认会预加载链接目标的 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 Router | App Router |
|---|---|---|
| 组件模型 | 全部客户端组件 | Server Components 优先 |
| 数据获取 | getServerSideProps / getStaticProps | async 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),支持多根布局
- 路由段配置:
dynamic、revalidate、runtime精细控制每个路由的行为 - RSC 渲染流程:首次请求流式发送,客户端导航只请求变化部分,Prefetch 提前加载
- vs Pages Router:App Router 是全新范式,不是简单升级
下一章深入动态路由、并行路由与拦截路由。