本节摘要:App Router 的核心约定是"文件系统路由"——文件放对位置,URL 就有了。本节讲清页面(page)、布局(layout)、动态路由([slug])、链接导航(Link)四大基础,并用一张文件系统路由示意建立直观理解。
阅读完本节,你应当能够:
传统 React 用 react-router 手动配置路由表:<Route path="/about" element={<About/>}>——路由和页面分开维护,改一处漏一处。Next.js 把路由做进文件系统:文件放对位置,URL 自动生成,不存在"路由表与页面不同步"的问题。
直觉类比:文件系统路由像"按目录归档的仓库"——文件在哪个格子(目录),地址就是什么。找东西(加页面)只需"放到对应格子",不用再记一份"仓库地图"(路由表)。
💡 关键直觉:App Router 里,一个"路由段(segment)" = 一个目录。
app/blog/[slug]/page.tsx对应 URL/blog/:slug,中间的目录层级就是 URL 的层级。
文件即页面:app/page.tsx → /,app/about/page.tsx → /about。
// app/about/page.tsx export default function AboutPage() { return <h1>关于我们</h1>; }
layout 包裹其下所有页面,切换路由时布局不重新渲染(保留状态):
// app/layout.tsx(根布局,必须有 html/body) export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="zh-CN"> <body> <header>全站导航</header> <main>{children}</main> <footer>全站页脚</footer> </body> </html> ); }
嵌套布局:每个目录都可以有自己的 layout.tsx,自动嵌套:
app/ ├── layout.tsx # 根布局(全站) ├── blog/ │ ├── layout.tsx # 博客布局(博客页共享,如侧边栏) │ └── page.tsx # /blog
方括号目录 = 动态参数:
// app/blog/[slug]/page.tsx // 文件路径 → /blog/xxx,slug 就是 xxx export default async function BlogPost({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; // Next.js 15 中 params 是 Promise return <h1>文章:{slug}</h1>; }
多段动态路由:app/docs/[version]/[page]/page.tsx → /docs/v1/intro。
可选参数:app/blog/[[slug]]/page.tsx(双层方括号)→ /blog 和 /blog/xxx 都匹配。
import Link from "next/link"; export default function Nav() { return ( <nav> <Link href="/">首页</Link> <Link href="/about">关于</Link> <Link href="/blog/hello-world">文章</Link> </nav> ); }
Link 与 <a> 的区别:Link 做客户端导航——不整页刷新,只加载变化的组件,保留页面状态(滚动位置、输入内容)。Next.js 还会预取视口内的 Link 目标页面,点击时几乎秒开。

需要"点击按钮后跳转"(非链接场景):
"use client"; import { useRouter } from "next/navigation"; export default function SubmitButton() { const router = useRouter(); return ( <button onClick={() => router.push("/blog")}>去博客</button> ); }
注意 useRouter 只能在客户端组件(带 "use client")中使用。
app/not-found.tsx(全局)或目录内 not-found.tsx;app/(marketing)/about/page.tsx 的括号目录不产生 URL 段,仅用于组织代码(如区分营销页/应用页)。| 误区 | 现象 | 正解 |
|---|---|---|
| 新建文件不生效 | URL 还是 404 | 确认文件名是 page.tsx 且路径正确 |
| 动态路由读不到参数 | params 是 Promise | const { slug } = await params |
| Link 整页刷新 | 用了 <a> 而非 Link |
用 next/link 的 Link |
| 布局不生效 | layout 文件位置错 | 布局放在对应目录内 |
| 文件同名冲突 | 启动报错 | 一个目录只允许一个 page.tsx |
// 1. app/page.tsx —— 首页,列出文章链接 import Link from "next/link"; const posts = [ { slug: "hello", title: "你好,Next.js" }, { slug: "router", title: "文件系统路由" }, ]; export default function Home() { return ( <ul> {posts.map((p) => ( <li key={p.slug}> <Link href={`/blog/${p.slug}`}>{p.title}</Link> </li> ))} </ul> ); }
// 2. app/blog/layout.tsx —— 博客共享布局 export default function BlogLayout({ children }) { return ( <div> <aside>博客导航</aside> <main>{children}</main> </div> ); }
// 3. app/blog/[slug]/page.tsx —— 动态文章页 export default async function Post({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; return <h1>文章:{slug}</h1>; }
访问 /blog/hello、/blog/router——一个动态路由服务所有文章,这就是文件系统路由的生产力。
除了 page/layout/dynamic 三个基础,App Router 还有几个实用机制。
并行路由(Parallel Routes):一个路径同时渲染多个独立区域,用命名槽(slot)实现:
app/dashboard/ ├── page.tsx ├── @analytics/page.tsx # 分析面板 └── @settings/page.tsx # 设置面板
适合"仪表盘"式页面——不同区域可以独立加载、独立刷新。
拦截路由(Intercepting Routes):在不改变 URL 的情况下拦截导航,常用于"列表页内点开详情,用模态框展示但 URL 是详情页地址"——用 (.) 前缀约定。社交网站的图片预览、电商的商品快速预览都是典型场景。
路由组与权限:路由组 (auth) 目录不产生 URL 段,但可以有自己的 layout——把登录/注册页放在 (auth)/ 下,给它们一个"无导航"的独立布局,是最常见的用法。
Link 的 prefetch 机制:Link 在视口内出现时会预取目标页面的数据与 JS(默认),点击时几乎秒开。prefetch={false} 可以关闭(流量敏感场景)。理解这个机制,你就明白"为什么 Next.js 的站内跳转特别快"——点击前就已经在准备了。
实战决策:
| 场景 | 用 |
|---|---|
| 普通页面跳转 | Link |
| 按钮触发的跳转 | useRouter().push |
| 详情模态框 | 拦截路由 |
| 仪表盘多面板 | 并行路由 |
| 登录/注册独立布局 | 路由组 |
排查技巧:路由不生效先看"文件是否叫 page.tsx"、"目录层级是否对应 URL"、"是否有同名文件冲突"——三个检查覆盖九成问题。
目录层级即 URL 层级,layout 包裹子路由,动态段匹配任意参数。
问:为什么 Next.js 15 的 params 是 Promise?
Next.js 15 为了支持异步组件与流式渲染,把 params/searchParams 改为 Promise,需要用 await params 解包。老代码 params.slug 会失效,迁移时注意。
问:Link 和浏览器刷新有什么区别?
Link 是客户端导航:只加载变化的组件、保留页面状态、自动预取;刷新是整页重新加载。站内导航一律用 Link。
问:布局里能取数据吗?
能。布局是服务器组件,可以 async 取数。但注意:布局不随子路由切换重新渲染,取数只在首次进入布局时执行。
问:动态路由和可选参数什么时候用?
必选参数 [slug] 用于"必须有值"的路径(文章详情);可选参数 [[slug]] 用于"有无皆可"的路径(如分类页,无 slug 显示全部)。