Skip to content

文件系统路由

🙋 Next.js 的路由不需要配置文件,文件夹结构就是路由结构。这是如何工作的?

核心概念#

App Router 使用文件系统来定义路由:

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 内容:

app/about/page.tsx
// Next.js 15.x
export default function AboutPage() {
return (
<main>
<h1>关于我们</h1>
<p>这是关于页面。</p>
</main>
)
}

layout.tsx#

定义共享 UI,不随路由切换而重新渲染:

app/dashboard/layout.tsx
// 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 显示加载状态:

app/dashboard/loading.tsx
// 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#

捕获子树中的错误:

app/dashboard/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 页面:

app/not-found.tsx
// Next.js 15.x
import 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:

app/users/[id]/page.tsx
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.tsxroute.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-