Next.js 入门
2 / 14
页面和布局
自在学
首页课程创意工坊价格
首页课程创意工坊价格
编程Next.js指南项目结构和文件系统路由

项目结构和文件系统路由

在上一节课中,我们创建了第一个 Next.js 项目,并初步了解了项目的基本结构。在这一个部分,我们将深入探索 Next.js 的项目组织方式和文件系统路由机制。

项目结构和文件系统路由

Next.js 13 引入了全新的 App Router,这是基于 React Server Components 构建的路由系统。App Router 使用文件系统来定义路由,这意味着你的文件夹结构直接决定了你的应用的路由结构。这种设计哲学被称为"约定优于配置",它减少了我们需要编写的样板代码,让开发变得更加直观。

App Router 是 Next.js 13+ 推荐的路由方式。虽然 Next.js 仍然支持旧的 Pages Router,但新项目应该使用 App Router,因为它提供了更好的性能、更强大的功能和更简洁的 API。

在 App Router 中,所有的路由都定义在 app 目录下。这个目录是特殊的,Next.js 会扫描这个目录下的文件,并根据文件结构自动生成路由。

目录结构

让我们先看看一个典型的 Next.js 项目的完整目录结构。在项目根目录下,你会看到以下主要目录和文件:

|
my-nextjs-app/ ├── app/ # App Router 的核心目录 │ ├── layout.tsx # 根布局 │ ├── page.tsx # 首页 │ ├── globals.css # 全局样式 │ ├── about/ # 关于页面路由 │ │ └── page.tsx │ └── blog/ # 博客路由组 │ ├── layout.tsx # 博客布局 │ └── [slug]/ # 动态路由 │ └── page.tsx ├── components/ # 可重用组件 │ ├── Button.tsx │ └── Header.tsx ├── lib/ # 工具函数和配置 │ └── utils.ts ├── public/ # 静态资源 │ ├── images/ │ └── favicon.ico ├── next.config.mjs # Next.js 配置 ├── package.json # 项目依赖 └── tsconfig.json # TypeScript 配置

让我们逐个了解这些目录和文件的作用。

  • app 目录是路由的核心。在这个目录下,每个文件夹代表一个路由段,每个 page.tsx 文件代表一个可访问的页面。这种设计让路由结构一目了然,你只需要查看文件夹结构就能知道应用有哪些页面。
  • components 目录用于存放可重用的 React 组件。这些组件不直接参与路由,但可以被页面和布局使用。良好的组件组织可以让代码更加模块化和可维护。
  • lib 目录通常用于存放工具函数、配置文件和第三方库的封装。比如,你可能在这里放置数据库连接、API 客户端、工具函数等。
  • public 目录用于存放静态资源。放在这个目录下的文件可以通过 URL 直接访问,无需通过路由。比如,public/logo.png 可以通过 /logo.png 访问。

文件系统路由

在 App Router 中,路由是通过文件系统自动生成的。理解这个机制是掌握 Next.js 的关键。让我们从最简单的例子开始。 假设我们在 app 目录下创建了以下文件:

|
app/ ├── page.tsx # 对应路由: / └── about/ └── page.tsx # 对应路由: /about

当我们访问 / 时,Next.js 会渲染 app/page.tsx。当我们访问 /about 时,Next.js 会渲染 app/about/page.tsx。 这个机制非常简单直观:文件夹名成为 URL 路径的一部分,page.tsx 文件定义了该路径下要渲染的内容。

让我们创建一个实际的例子。首先,我们在 app 目录下创建 about 文件夹:

|
// app/about/page.tsx export default function AboutPage() { return ( <div> <h1>关于我们</h1> <p>这是关于页面</p> </div> ); }

保存文件后,访问 http://localhost:3000/about,你就会看到这个页面。

嵌套路由

Next.js 支持嵌套路由,这意味着你可以在文件夹中创建子文件夹,形成多层级的路由结构。比如:

|
app/ ├── page.tsx # / ├── about/ │ └── page.tsx # /about └── products/ ├── page.tsx # /products └── [id]/ └── page.tsx # /products/[id]

在这个结构中,/products 是一个路由,/products/[id] 是它的子路由。方括号 [id] 表示这是一个动态路由段,我们稍后会详细讲解。 让我们创建一个嵌套路由的例子。首先创建 products 文件夹和它的 page.tsx:

|
// app/products/page.tsx export default function ProductsPage() { return ( <div> <h1>产品列表</h1> <p>这里显示所有产品</p> </div> ); }

然后,在 products 文件夹下创建 [id] 文件夹(注意方括号),并创建 page.tsx:

|
// app/products/[id]/page.tsx export default function ProductDetailPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; return ( <div> <h1>产品详情</h1> <p>产品 ID: {id}</p> </div> ); }

注意这里我们使用了 await params。在 Next.js 15+ 中,params 是一个 Promise,我们需要使用 await 来获取参数值。这是为了支持异步路由参数,我们会在后面的课程中详细讲解。

现在,访问 /products 会显示产品列表,访问 /products/123 会显示 ID 为 123 的产品详情。


特殊文件:page.tsx

page.tsx 是 App Router 中的特殊文件。只有包含 page.tsx 的文件夹才会成为可访问的路由。这意味着你可以创建其他文件(比如组件文件、工具文件)在同一个文件夹中,它们不会影响路由。 比如,这样的结构是合法的:

|
app/ └── about/ ├── page.tsx # 路由: /about ├── Header.tsx # 组件,不影响路由 └── utils.ts # 工具函数,不影响路由

在这个例子中,只有 page.tsx 定义了路由,Header.tsx 和 utils.ts 只是普通的文件,可以被 page.tsx 导入使用,但不会创建新的路由。

page.tsx 文件必须导出一个默认的 React 组件。这个组件就是页面要渲染的内容。组件可以接收 params 和 searchParams 作为 props,我们会在后面的学习中详细讲解这些参数。


特殊文件:layout.tsx

layout.tsx 是另一个特殊文件,它用于定义共享的 UI 结构。与 page.tsx 不同,layout.tsx 不会创建新的路由,而是为当前路由及其所有子路由提供共享的布局。

让我们通过一个例子来理解布局的作用。假设我们有这样的结构:

|
app/ ├── layout.tsx # 根布局 ├── page.tsx # 首页 └── blog/ ├── layout.tsx # 博客布局 └── page.tsx # 博客首页

当我们访问 /blog 时,Next.js 会按照以下顺序渲染组件:

  1. 首先渲染 app/layout.tsx(根布局)
  2. 然后渲染 app/blog/layout.tsx(博客布局)
  3. 最后渲染 app/blog/page.tsx(博客页面)

这些布局会嵌套在一起,形成最终的页面结构。这就像俄罗斯套娃一样,外层的布局包裹着内层的布局和页面。

让我们创建一个实际的例子。首先,我们修改根布局,添加一个导航栏:

|
// app/layout.tsx export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="zh"> <body> <nav> <a href="/">首页</a> <a href="/about">关于</a> <a href="/blog">博客</a> </nav> {children} </body> </html> ); }

然后,我们为博客创建一个专门的布局:

|
// app/blog/layout.tsx export default function BlogLayout({ children, }: { children: React.ReactNode; }) { return ( <div className="blog-container"> <aside> <h2>博客分类</h2> <ul> <li>技术文章</li> <li>生活随笔</li> </ul> </aside> <main>{children}</main> </div> ); }

现在,当我们访问 /blog 时,页面会包含根布局的导航栏和博客布局的侧边栏,最后是博客页面的内容。这种嵌套布局让我们可以灵活地组织页面的结构。


特殊文件:loading.tsx

loading.tsx 是用于显示加载状态的特殊文件。当 Next.js 正在加载页面内容时,它会自动显示 loading.tsx 中定义的组件。

让我们创建一个加载组件:

|
// app/blog/loading.tsx export default function Loading() { return ( <div> <p>加载中...</p> </div> ); }

当用户访问 /blog 时,如果页面还在加载,Next.js 会显示这个加载组件。一旦页面加载完成,加载组件会被实际的页面内容替换。

我们可以让加载组件更加美观:

|
// app/blog/loading.tsx export default function Loading() { return ( <div className="flex items-center justify-center min-h-screen"> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div> </div> ); }

这个加载组件显示一个旋转的加载动画,给用户更好的视觉反馈。


特殊文件:error.tsx

error.tsx 用于处理错误情况。当页面或布局组件抛出错误时,Next.js 会显示 error.tsx 中定义的错误边界组件。

让我们创建一个错误处理组件:

|
// app/blog/error.tsx 'use client'; export default function Error({ error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { return ( <div> <h2>出错了!</h2> <p>{error.message}</p> <button onClick={reset}>重试</button> </div> ); }

注意这里我们使用了 'use client' 指令。这是因为错误边界需要使用客户端功能(比如 onClick)。我们会在后面的学习中详细讲解客户端组件和服务器组件的区别。

error prop 包含错误信息,reset 函数可以尝试重新渲染组件。这给用户提供了一个恢复错误的机会。


特殊文件:not-found.tsx

not-found.tsx 用于显示 404 页面。当用户访问不存在的路由时,Next.js 会显示这个组件。

让我们创建一个 404 页面:

|
// app/not-found.tsx export default function NotFound() { return ( <div> <h1>404 - 页面未找到</h1> <p>抱歉,您访问的页面不存在。</p> <a href="/">返回首页</a> </div> ); }

我们也可以在特定的路由下创建 not-found.tsx,这样只有该路由及其子路由会使用这个 404 页面:

|
// app/blog/not-found.tsx export default function BlogNotFound() { return ( <div> <h1>博客文章未找到</h1> <p>抱歉,您要查找的博客文章不存在。</p> <a href="/blog">返回博客首页</a> </div> ); }

动态路由

动态路由允许我们创建可以匹配不同 URL 模式的路由。在 App Router 中,我们使用方括号 [] 来创建动态路由段。 最简单的动态路由是单段动态路由:

|
app/ └── products/ └── [id]/ └── page.tsx # 匹配 /products/123, /products/abc 等

在这个例子中,[id] 是一个动态段,它可以匹配任何值。访问 /products/123 时,id 的值是 "123"。访问 /products/abc 时,id 的值是 "abc"。

在页面组件中,我们可以通过 params 获取动态段的值:

|
// app/products/[id]/page.tsx export default async function ProductPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; return ( <div> <h1>产品 {id}</h1> </div> ); }

注意这里我们使用了 async 函数和 await params。在 Next.js 15+ 中,这是获取路由参数的标准方式。

多段动态路由

我们也可以创建多个动态段:

|
app/ └── shop/ └── [category]/ └── [product]/ └── page.tsx # 匹配 /shop/electronics/phone

在这个例子中,访问 /shop/electronics/phone 时,category 的值是 "electronics",product 的值是 "phone"。

在页面组件中获取多个参数:

|
// app/shop/[category]/[product]/page.tsx export default async function ProductPage({ params, }: { params: Promise<{ category: string; product: string }>; }) { const { category, product } = await params; return ( <div> <h1>{category} - {product}</h1> </div> ); }

可选动态路由

有时候,我们希望某个路由段是可选的。我们可以使用双括号 [[...]] 来创建可选的路由段:

|
app/ └── blog/ └── [[...slug]]/ └── page.tsx # 匹配 /blog, /blog/2024, /blog/2024/01 等

在这个例子中,slug 是一个可选的动态段。访问 /blog 时,slug 是 undefined。访问 /blog/2024 时,slug 是 ["2024"]。访问 /blog/2024/01 时,slug 是 ["2024", "01"]。 注意可选动态路由会返回一个数组,即使只有一个值也是如此。如果路由段不存在,则返回 undefined。

|
// app/blog/[[...slug]]/page.tsx export default async function BlogPage({ params, }: { params: Promise<{ slug?: string[] }>; }) { const { slug } = await params; if (!slug) { return <div>博客首页</div>; } return ( <div> <h1>博客路径: {slug.join('/')}</h1> </div> ); }

Catch-all 路由

Catch-all 路由使用 [...slug] 语法,它可以匹配多个路由段,但不是可选的:

|
app/ └── docs/ └── [...slug]/ └── page.tsx # 匹配 /docs/a, /docs/a/b, /docs/a/b/c 等

与可选动态路由不同,catch-all 路由必须至少匹配一个段。访问 /docs 不会匹配这个路由。

|
// app/docs/[...slug]/page.tsx export default async function DocsPage({ params, }: { params: Promise<{ slug: string[] }>; }) { const { slug } = await params; return ( <div> <h1>文档路径: {slug.join('/')}</h1> </div> ); }

路由组

路由组允许我们组织路由而不影响 URL 结构。路由组使用括号 () 来定义,括号内的文件夹名不会成为 URL 的一部分。

比如,我们可能有这样的需求:管理后台和用户前台使用不同的布局,但它们的 URL 不应该包含 admin 或 user 前缀。

|
app/ ├── (marketing)/ │ ├── layout.tsx # 营销页面布局 │ ├── page.tsx # / (首页) │ └── about/ │ └── page.tsx # /about └── (shop)/ ├── layout.tsx # 商店页面布局 └── products/ └── page.tsx # /products

在这个结构中,(marketing) 和 (shop) 是路由组。它们不影响 URL,但允许我们为不同的路由组使用不同的布局。

访问 / 时,会使用 (marketing)/layout.tsx。访问 /products 时,会使用 (shop)/layout.tsx。


并行路由

并行路由是 Next.js 的一个高级特性,它允许我们在同一个布局中同时渲染多个页面。并行路由使用 @folder 命名约定。

|
app/ ├── layout.tsx ├── page.tsx ├── @analytics/ │ └── page.tsx └── @team/ └── page.tsx

在这个结构中,@analytics 和 @team 是并行路由槽。它们会在同一个布局中同时渲染,但不会影响主路由的 URL。

并行路由通常与路由组和条件渲染一起使用,用于创建复杂的布局结构,比如仪表板的不同视图。


路由优先级

当多个路由可能匹配同一个 URL 时,Next.js 会按照以下优先级选择:

  1. 静态路由(最具体)
  2. 动态路由
  3. Catch-all 路由
  4. 可选 Catch-all 路由(最不具体)

比如,对于 URL /products/new:

  • app/products/new/page.tsx(静态路由)会优先匹配
  • 如果没有,app/products/[id]/page.tsx(动态路由)会匹配
  • 如果还没有,app/products/[...slug]/page.tsx(catch-all 路由)会匹配

这种优先级规则让我们可以灵活地组织路由,同时保持 URL 的清晰性。


路由元数据

Next.js 允许我们为路由定义元数据,比如页面标题、描述等。我们可以通过导出 metadata 对象或 generateMetadata 函数来定义元数据。

静态元数据:

|
// app/about/page.tsx export const metadata = { title: '关于我们', description: '了解我们的团队和使命', }; export default function AboutPage() { return <div>关于页面</div>; }

动态元数据:

|
// app/products/[id]/page.tsx export async function generateMetadata({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; const product = await fetchProduct(id); return { title: product.name, description: product.description, }; } export default async function ProductPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; const product = await fetchProduct(id); return <div>{product.name}</div>; }

我们会在后面的学习中详细讲解元数据的使用,不必急于一时。


组织大型项目

当项目变得复杂时,良好的文件组织变得至关重要。以下是一些最佳实践:

首先,将可重用的组件放在 components 目录下,并按功能或类型组织:

|
components/ ├── ui/ # 基础 UI 组件 │ ├── Button.tsx │ └── Input.tsx ├── layout/ # 布局组件 │ ├── Header.tsx │ └── Footer.tsx └── features/ # 功能组件 ├── ProductCard.tsx └── UserProfile.tsx

其次,将工具函数和配置放在 lib 目录下:

|
lib/ ├── utils.ts # 通用工具函数 ├── api.ts # API 客户端 └── constants.ts # 常量定义

使用路由组来组织相关的路由,即使它们不需要不同的布局:

|
app/ ├── (auth)/ │ ├── login/ │ └── register/ └── (dashboard)/ ├── profile/ └── settings/

最后,保持 app 目录的简洁,将复杂的逻辑提取到组件和工具函数中。


小结回顾

在这一节课中,我们探索了 Next.js 的项目结构和文件系统路由。我们学习了如何通过创建文件和文件夹来定义路由,了解了各种特殊文件的作用,掌握了动态路由、路由组、并行路由等高级特性。 文件系统路由是 Next.js 的核心特性之一,它让路由定义变得直观和可维护。

在接下来的一节课中,我们将学习页面和布局的使用,了解如何创建嵌套布局,如何使用模板,以及如何在不同页面之间共享 UI 结构,话不多说,让我们立即开始!

  • 目录结构
  • 文件系统路由
    • 嵌套路由
  • 特殊文件:page.tsx
  • 特殊文件:layout.tsx
  • 特殊文件:loading.tsx
  • 特殊文件:error.tsx
  • 特殊文件:not-found.tsx
  • 动态路由
    • 多段动态路由
    • 可选动态路由
    • Catch-all 路由
  • 路由组
  • 并行路由
  • 路由优先级
  • 路由元数据
  • 组织大型项目
  • 小结回顾

目录

  • 目录结构
  • 文件系统路由
    • 嵌套路由
  • 特殊文件:page.tsx
  • 特殊文件:layout.tsx
  • 特殊文件:loading.tsx
  • 特殊文件:error.tsx
  • 特殊文件:not-found.tsx
  • 动态路由
    • 多段动态路由
    • 可选动态路由
    • Catch-all 路由
  • 路由组
  • 并行路由
  • 路由优先级
  • 路由元数据
  • 组织大型项目
  • 小结回顾
自在学

© 2025 自在学,保留所有权利。

公网安备湘公网安备43020302000292号 | 湘ICP备2025148919号-1

关于我们隐私政策使用条款

© 2025 自在学,保留所有权利。

公网安备湘公网安备43020302000292号湘ICP备2025148919号-1