2.1 文件系统路由基础
App Router 使用文件系统作为路由的唯一来源(Single Source of Truth)。app/ 目录下的每个文件夹都代表一个 URL 路由段。文件夹名称映射到 URL 路径,页面文件(page.tsx)的存在使该路由"可访问"。
路由段(Route Segment)是 URL 路径中以 / 分隔的每个部分。例如 /dashboard/settings 有两个路由段:dashboard 和 settings。每个路由段对应 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])等。
-
[slug]
单一动态段,匹配单个路径段。例如
/blog/hello-world中 slug 值为 "hello-world"。 -
[...slug]
Catch-all 段,匹配一个或多个路径段,值为字符串数组。例如
/docs/a/b/c中 slug 为 ["a","b","c"]。 -
[[...slug]]
可选 Catch-all 段,也能匹配父路由本身(无参数情况)。例如
/docs和/docs/a/b都能匹配。
// 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 中,params 和 searchParams 都变成了 Promise 类型,需要使用 await 解包。这是为了支持 React 19 的异步组件特性。从 Next.js 14 迁移时需要更新所有动态页面。
2.4 路由组 (group)
路由组使用圆括号语法 (groupName) 命名文件夹,该文件夹不会出现在 URL 路径中,仅用于组织文件结构。路由组的主要用途有两个:
- 组织代码:将功能相关的路由放在一起,而不影响 URL
- 多个根布局:不同路由组可以有不同的 layout.tsx,实现同一应用内多种页面框架
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
- (.) 拦截 拦截同级路由段(在同一目录层级)
- (..) 拦截 拦截上一级路由段
- (...) 拦截 从根 app/ 目录开始拦截
URL 分享友好拦截路由的最大价值在于:软导航(点击链接)触发模态框,但刷新页面或分享 URL 后,用户会看到完整的独立页面,两者共用同一个 URL,兼顾了交互体验和 URL 语义。
2.6 Link 组件与编程式导航
Next.js 提供 next/link 的 Link 组件作为客户端导航的标准方式。它在视口中预取目标页面,实现即时跳转体验,同时保留浏览器历史记录。
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(useRouter、usePathname、useSearchParams)。Pages Router 使用 next/router。两者不可混用,迁移时需注意。