🙋 Next.js 的路由不需要配置文件,文件夹结构就是路由结构。这是如何工作的?
核心概念#
App Router 使用文件系统来定义路由:
- 文件夹 = 路由路径
- page.tsx = 页面内容
- layout.tsx = 共享布局
app/├── page.tsx # /├── about/│ └── page.tsx # /about├── blog/│ ├── page.tsx # /blog│ └── [slug]/│ └── page.tsx # /blog/:slug└── (marketing)/ ├── pricing/ │ └── page.tsx # /pricing └── contact/ └── page.tsx # /contact🎯 记住:只有包含 page.tsx 的文件夹才是可访问的路由。
路由段#
什么是路由段#
URL 路径由多个路由段组成:
https://example.com/blog/2024/hello-world ├──┘ ├──┘ └─────────┘ 段1 段2 段3对应的文件结构:
app/└── blog/ # 段1: /blog └── 2024/ # 段2: /blog/2024 └── hello-world/ # 段3: /blog/2024/hello-world └── page.tsx段类型#
| 类型 | 语法 | 示例 | 用途 |
|---|---|---|---|
| 静态段 | folder/ | about/ | 固定路径 |
| 动态段 | [param]/ | [id]/ | 单个参数 |
| 捕获所有 | [...slug]/ | 匹配多段 | 多级路径 |
| 可选捕获 | [[...slug]]/ | 可选多段 | 包含根路径 |
特殊文件#
文件约定一览#
app/└── dashboard/ ├── layout.tsx # 布局(包裹子路由) ├── template.tsx # 模板(每次导航重新挂载) ├── page.tsx # 页面内容 ├── loading.tsx # 加载状态 ├── error.tsx # 错误边界 ├── not-found.tsx # 404 页面 ├── route.ts # API 端点 └── default.tsx # 并行路由默认内容渲染层级#
特殊文件按固定顺序嵌套渲染:
<Layout> <Template> <ErrorBoundary fallback={<Error />}> <Suspense fallback={<Loading />}> <Page /> </Suspense> </ErrorBoundary> </Template></Layout>page.tsx#
定义路由的 UI 内容:
// Next.js 15.x
export default function AboutPage() { return ( <main> <h1>关于我们</h1> <p>这是关于页面。</p> </main> )}layout.tsx#
定义共享 UI,不随路由切换而重新渲染:
// Next.js 15.x
export default function DashboardLayout({ children,}: { children: React.ReactNode}) { return ( <div className="flex"> <aside className="w-64">侧边栏</aside> <main className="flex-1">{children}</main> </div> )}loading.tsx#
使用 React Suspense 显示加载状态:
// Next.js 15.x
export default function Loading() { return ( <div className="flex items-center justify-center h-64"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" /> </div> )}error.tsx#
捕获子树中的错误:
// Next.js 15.x'use client' // 必须是客户端组件
interface ErrorProps { error: Error & { digest?: string } reset: () => void}
export default function Error({ error, reset }: ErrorProps) { return ( <div className="p-4 bg-red-50 rounded"> <h2 className="text-red-600">出错了!</h2> <p className="text-gray-600">{error.message}</p> <button onClick={reset} className="mt-4 px-4 py-2 bg-red-600 text-white rounded" > 重试 </button> </div> )}not-found.tsx#
自定义 404 页面:
// Next.js 15.ximport Link from 'next/link'
export default function NotFound() { return ( <div className="flex flex-col items-center justify-center min-h-screen"> <h1 className="text-6xl font-bold text-gray-300">404</h1> <p className="mt-4 text-xl text-gray-600">页面不存在</p> <Link href="/" className="mt-8 px-6 py-3 bg-blue-600 text-white rounded"> 返回首页 </Link> </div> )}触发 404:
import { notFound } from 'next/navigation'
export default async function UserPage({ params }) { const { id } = await params const user = await getUser(id)
if (!user) { notFound() // 触发 not-found.tsx }
return <div>{user.name}</div>}Colocation(文件共置)#
安全的文件组织#
在 app 目录中,你可以安全地放置非路由文件:
app/└── dashboard/ ├── page.tsx # 路由页面 ├── layout.tsx # 布局 ├── components/ # ✅ 不会成为路由 │ ├── Chart.tsx │ └── Stats.tsx ├── hooks/ # ✅ 不会成为路由 │ └── useData.ts ├── utils.ts # ✅ 不会成为路由 └── types.ts # ✅ 不会成为路由🎯 规则:只有 page.tsx 和 route.ts 会使文件夹成为可访问路由。
私有文件夹#
使用下划线前缀标记私有文件夹:
app/├── _components/ # 私有,永远不会成为路由│ ├── Header.tsx│ └── Footer.tsx├── _lib/ # 私有│ └── utils.ts└── dashboard/ └── page.tsx组织策略对比#
策略一:按路由组织
app/└── dashboard/ ├── page.tsx ├── components/ # Dashboard 专用组件 └── hooks/ # Dashboard 专用 hooks策略二:按功能组织
src/├── app/│ └── dashboard/│ └── page.tsx├── components/ # 全局共享组件│ ├── ui/│ └── dashboard/└── lib/ # 全局工具函数✅ 推荐:小项目用策略一,大项目用策略二。
路由映射规则#
完整映射表#
| 文件路径 | URL 路径 |
|---|---|
app/page.tsx | / |
app/about/page.tsx | /about |
app/blog/page.tsx | /blog |
app/blog/[slug]/page.tsx | /blog/:slug |
app/shop/[...categories]/page.tsx | /shop/* |
app/(marketing)/pricing/page.tsx | /pricing |
app/@modal/login/page.tsx | 并行路由插槽 |
特殊路径处理#
app/├── (auth)/ # 路由组,不影响 URL│ ├── login/│ │ └── page.tsx # /login│ └── register/│ └── page.tsx # /register├── api/ # API 路由│ └── users/│ └── route.ts # /api/users└── [[...slug]]/ # 可选捕获所有 └── page.tsx # / 和 /* 都匹配实战示例#
博客网站结构#
app/├── layout.tsx # 根布局(导航、页脚)├── page.tsx # 首页├── blog/│ ├── layout.tsx # 博客布局(侧边栏)│ ├── page.tsx # 文章列表 /blog│ └── [slug]/│ ├── page.tsx # 文章详情 /blog/:slug│ └── loading.tsx # 加载状态├── about/│ └── page.tsx # 关于页面 /about└── not-found.tsx # 全局 404电商网站结构#
app/├── layout.tsx├── page.tsx # 首页├── products/│ ├── page.tsx # 商品列表│ ├── [category]/│ │ ├── page.tsx # 分类页 /products/electronics│ │ └── [id]/│ │ └── page.tsx # 商品详情 /products/electronics/123│ └── loading.tsx├── cart/│ └── page.tsx # 购物车├── checkout/│ ├── page.tsx # 结账│ └── success/│ └── page.tsx # 成功页└── (auth)/ ├── login/ │ └── page.tsx └── register/ └── page.tsx常见问题#
🤔 Q: 文件夹没有 page.tsx 会怎样?
该路径无法访问,返回 404。但可以放置其他文件(组件、工具函数等)。
🤔 Q: 多个 layout.tsx 如何嵌套?
自动嵌套,从根目录到当前路由的所有 layout 都会按顺序包裹 page。
🤔 Q: 如何共享多个路由的布局?
使用路由组 (folder):
app/├── (shop)/│ ├── layout.tsx # 商店布局│ ├── products/│ └── cart/└── (marketing)/ ├── layout.tsx # 营销布局 ├── about/ └── pricing/下一篇将深入动态路由,学习 [slug]、[...slug] 和 [[...slug]] 的使用场景。
-EOF-