Chapter 02

路由系统深度解析

嵌套路由、动态路由、路由组、平行路由与拦截路由的完整指南

2.1 文件系统路由基础

App Router 使用文件系统作为路由的唯一来源(Single Source of Truth)。app/ 目录下的每个文件夹都代表一个 URL 路由段。文件夹名称映射到 URL 路径,页面文件(page.tsx)的存在使该路由"可访问"。

路由段(Route Segment)是 URL 路径中以 / 分隔的每个部分。例如 /dashboard/settings 有两个路由段:dashboardsettings。每个路由段对应 app/ 目录下的一个文件夹。

app/
├── page.tsx            → URL: /
├── about/
│   └── page.tsx        → URL: /about
├── blog/
│   ├── page.tsx        → URL: /blog
│   └── first-post/
│       └── page.tsx    → URL: /blog/first-post
└── dashboard/
    ├── layout.tsx        嵌套布局(不生成 URL)
    ├── page.tsx        → URL: /dashboard
    └── settings/
        └── page.tsx    → URL: /dashboard/settings
STRUCTURE
ℹ️

仅 page.tsx 才可公开访问在 app/ 目录中,只有 page.tsx 文件才会生成对应的 URL 路由。其他文件(如组件、工具函数、样式文件)放在同一文件夹下,不会被路由系统识别为页面,这被称为"共置(Colocation)"。

2.2 嵌套路由与嵌套布局

嵌套路由(Nested Routes)是 App Router 最强大的特性之一。当一个文件夹包含 layout.tsx 时,该布局会包裹该路由段及其所有子路由的 page.tsx,形成嵌套层级。

与 Pages Router 的 _app.tsx 不同,嵌套布局在路由切换时不会重新挂载。只有当前导航路径中变化的段才会重新渲染,父布局的状态、滚动位置都会保留。这是 App Router 性能更优的关键原因之一。

// app/dashboard/layout.tsx — 仪表板的共享布局
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="dashboard-wrapper">
      {/* 侧边栏在所有 dashboard/* 路由中保持挂载状态 */}
      <Sidebar />
      <main className="dashboard-content">
        {children} {/* 子页面渲染在这里 */}
      </main>
    </div>
  )
}
TSX
// app/dashboard/page.tsx — /dashboard 页面
export default function DashboardPage() {
  return <h1>仪表板首页</h1>
}

// app/dashboard/settings/page.tsx — /dashboard/settings 页面
// 自动继承 DashboardLayout,无需手动引入
export default function SettingsPage() {
  return <h1>系统设置</h1>
}
TSX

2.3 动态路由 [slug]

动态路由允许使用方括号语法 [param] 创建可匹配任意值的路由段。常见场景包括文章详情页(/blog/[slug])、用户主页(/user/[id])等。

// app/blog/[slug]/page.tsx
interface Params {
  params: Promise<{ slug: string }>
}

export default async function BlogPost({ params }: Params) {
  // Next.js 15: params 是 Promise,需要 await
  const { slug } = await params

  const post = await getPostBySlug(slug)

  if (!post) {
    notFound() // 触发 not-found.tsx
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

// 静态生成:告知 Next.js 预渲染哪些 slug
export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map(post => ({ slug: post.slug }))
}
TSX
⚠️

Next.js 15 Breaking Change在 Next.js 15 中,paramssearchParams 都变成了 Promise 类型,需要使用 await 解包。这是为了支持 React 19 的异步组件特性。从 Next.js 14 迁移时需要更新所有动态页面。

2.4 路由组 (group)

路由组使用圆括号语法 (groupName) 命名文件夹,该文件夹不会出现在 URL 路径中,仅用于组织文件结构。路由组的主要用途有两个:

app/
├── (marketing)/            路由组,不占用 URL
│   ├── layout.tsx          营销页布局(无侧边栏)
│   ├── page.tsx          → URL: /
│   └── about/
│       └── page.tsx      → URL: /about
│
├── (dashboard)/            另一个路由组
│   ├── layout.tsx          后台布局(有侧边栏)
│   ├── dashboard/
│   │   └── page.tsx      → URL: /dashboard
│   └── settings/
│       └── page.tsx      → URL: /settings
STRUCTURE

上述结构中,//about 使用营销布局(无侧边栏),而 /dashboard/settings 使用后台布局(有侧边栏)。URL 中均不包含 (marketing)(dashboard)

2.5 平行路由与拦截路由

平行路由(Parallel Routes)和拦截路由(Intercepting Routes)是 App Router 两个高级特性,用于实现复杂的 UI 模式,如模态框路由(Modal Route)。

平行路由 @slot

平行路由使用 @slotName 命名的文件夹定义,允许在同一布局中同时渲染多个独立的页面。每个 slot 作为 props 传入 layout,可以独立加载、报错、刷新。

app/dashboard/
├── layout.tsx            接收 @analytics 和 @team 两个 slot
├── page.tsx
├── @analytics/
│   ├── page.tsx          analytics 内容
│   └── revenue/
│       └── page.tsx
└── @team/
    └── page.tsx          team 内容
STRUCTURE
// app/dashboard/layout.tsx
export default function Layout({
  children,
  analytics,  // 对应 @analytics slot
  team,        // 对应 @team slot
}: {
  children: React.ReactNode
  analytics: React.ReactNode
  team: React.ReactNode
}) {
  return (
    <div>
      {children}
      <div className="dashboard-grid">
        {analytics}
        {team}
      </div>
    </div>
  )
}
TSX

拦截路由 (..) — 模态框模式

拦截路由使用 (..) 语法,允许在当前布局中"拦截"并渲染另一个路由,而不完全导航到那个路由。经典用例是 Instagram 风格的图片模态框:直接访问 /photos/123 显示完整页面,但从相册页面点击图片时,URL 变为 /photos/123 但界面显示为在当前页面叠加的模态框。

app/
├── feed/
│   └── page.tsx                图片列表页
├── photos/
│   └── [id]/
│       └── page.tsx          → 直接访问显示完整图片页
└── @modal/
    ├── default.tsx             无模态时渲染 null
    └── (.)photos/              拦截同级 photos 路由
        └── [id]/
            └── page.tsx      → 从列表进入时显示为模态框
STRUCTURE
💡

URL 分享友好拦截路由的最大价值在于:软导航(点击链接)触发模态框,但刷新页面或分享 URL 后,用户会看到完整的独立页面,两者共用同一个 URL,兼顾了交互体验和 URL 语义。

2.6 Link 组件与编程式导航

Next.js 提供 next/linkLink 组件作为客户端导航的标准方式。它在视口中预取目标页面,实现即时跳转体验,同时保留浏览器历史记录。

import Link from 'next/link'
import { useRouter } from 'next/navigation'

// 声明式导航 — 推荐方式
function Nav() {
  return (
    <nav>
      <Link href="/">首页</Link>
      <Link href="/blog" prefetch={false}>博客</Link>
      <Link href={{ pathname: '/blog/[slug]', query: { slug: 'hello' } }}>
        文章
      </Link>
    </nav>
  )
}

// 编程式导航 — 需要 'use client'
'use client'
function SearchForm() {
  const router = useRouter()

  function handleSearch(term: string) {
    router.push(`/search?q=${term}`)
    // router.replace() — 替换历史记录
    // router.back()    — 返回上一页
    // router.refresh() — 刷新 Server Component
  }

  return /* ... */
}
TSX
ℹ️

next/navigation vs next/routerApp Router 使用 next/navigation 中的 hooks(useRouterusePathnameuseSearchParams)。Pages Router 使用 next/router。两者不可混用,迁移时需注意。