Bootstrap

Next.js 学习 - 路由系统(Routing)

Next.js 的路由系统基于文件系统,这意味着文件和文件夹的结构决定了 URL 路径。相较于传统的 React 应用中的路由配置,Next.js 的文件路由系统非常简洁和自动化。下面是对 Next.js 路由的详细介绍。

1. 目录结构

在 Next.js 13 中,app 目录是路由系统的核心,每个子目录对应于一个 URL 路径,page.tsx 文件用来定义该路径的页面内容,而 layout.tsx 文件则用于共享布局。

示例目录结构:

app/
  ├── layout.tsx              // 全局布局,应用于所有页面
  ├── page.tsx                // 对应 URL: /
  ├── about/
  │   └── page.tsx            // 对应 URL: /about
  ├── posts/
  │   ├── layout.tsx          // 单独为 /posts 下的页面定义的布局
  │   ├── page.tsx            // 对应 URL: /posts
  │   └── [id]/
  │       └── page.tsx        // 对应 URL: /posts/[id] (动态路由)
  └── dashboard/
      ├── layout.tsx          // 专属于 dashboard 页面和子页面的布局
      ├── page.tsx            // 对应 URL: /dashboard
      └── settings/
          └── page.tsx        // 对应 URL: /dashboard/settings

2. 基础路由

app 目录中的每个文件夹都对应一个 URL 路径,而 page.tsx 文件定义了该路径上的页面内容。

app/page.tsx

const HomePage = () => {
  return <h1>Home Page</h1>;
};

export default HomePage;

page.tsx 文件对应根路径 /,渲染主页。

app/about/page.tsx

const AboutPage = () => {
  return <h1>About Us</h1>;
};

export default AboutPage;

此页面对应 /about,渲染关于我们页面。

3. 动态路由

在 Next.js 的新架构中,动态路由通过文件夹命名方式实现。
在动态路由中,Next.js 支持以下几种模式:

  1. [folder]:单一动态路由。
  2. [...folder]:捕获所有路由的动态路由(Catch-all route)。
  3. [[...folder]]:可选的捕获所有路由(Optional Catch-all route)。
3.1 [folder]:单一动态路由。

动态路由文件或文件夹用方括号 [] 包裹动态部分名称,它会匹配 URL 中该部分的任何值。

示例目录结构:

app/
  ├── posts/
  │   └── [id]/
  │       └── page.tsx     // 对应 /posts/[id]

app/posts/[id]/page.tsx 文件:

const PostPage = ({ params }: { params: { id: string } }) => {
  return <h1>Post ID: {params.id}</h1>;
};

export default PostPage;
  • 路径 /posts/123 会匹配这个路由,params.id 将是 '123'
  • 路径 /posts/abc 会匹配这个路由,params.id 将是 'abc'
  • [id] 可以是 URL 中任何值,但它是必须存在的。如果访问 /posts,不会匹配这个路由。

动态参数获取方式:
在 Next.js 13 的 app 目录中,动态路由的参数通过 params 对象传递给组件。例如上面的 params.id。

3.2 [...folder]:捕获所有路由(Catch-all route)

[...folder] 是一种捕获所有路由的动态路由模式,可以匹配该层级后面的所有路径部分。它常用于构建处理多个路径层级的页面。

示例目录结构:

app/
  └── blog/
      └── [...slug]/
          └── page.tsx      // 对应 /blog/[...slug]

app/blog/[...slug]/page.tsx 文件:

const BlogPage = ({ params }: { params: { slug: string[] } }) => {
  return (
    <div>
      <h1>Blog Slug: {params.slug.join('/')}</h1>
    </div>
  );
};

export default BlogPage;
  • 路径 /blog/2024/10 会匹配,params.slug['2024', '10']
  • 路径 /blog/nextjs/release 会匹配,params.slug['nextjs', 'release']
  • [...slug] 捕获路径 /blog 之后的所有层级,可以包含多个部分,params.slug 是一个数组,包含这些部分的值。

使用场景:

  • 捕获路径中不确定层级数的 URL,例如博客、文档系统。
  • 例如 /docs/getting-started/installation 可以被 [...slug] 捕获。
3.3 [[...folder]]:可选的捕获所有路由(Optional Catch-all route)

[[...folder]] 是一种可选的捕获所有路由,它类似于 [...folder],但参数是可选的,这意味着没有额外路径时也可以匹配。

示例目录结构:

app/
  └── docs/
      └── [[...slug]]/
          └── page.tsx      // 对应 /docs/[[...slug]]

app/docs/[[...slug]]/page.tsx 文件:

const DocsPage = ({ params }: { params: { slug?: string[] } }) => {
  if (!params.slug) {
    return <h1>Documentation Home</h1>;
  }
  return <h1>Doc: {params.slug.join('/')}</h1>;
};

export default DocsPage;
  • 路径 /docs 会匹配,params.slugundefined,显示文档主页。
  • 路径 /docs/getting-started 会匹配,params.slug['getting-started']
  • 路径 /docs/getting-started/installation 会匹配,params.slug['getting-started', 'installation']
  • 这种方式常用于当需要捕获路径中的多个可选部分时,但不确定它们是否存在。也就是说,[[...slug]] 可以匹配 /docs(无参数)以及 /docs/anything/here(带参数)。

4. 布局 (Layout)

Next.js 13 引入了布局文件 (layout.tsx),用于共享页面之间的 UI 组件(如导航栏、页脚等)。布局可以嵌套,允许每个路由都有自己的布局。

app/layout.tsx
const RootLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <html lang="en">
      <body>
        <header>
          <nav>Global Navigation</nav>
        </header>
        {children}  {/* 每个页面的内容 */}
        <footer>Global Footer</footer>
      </body>
    </html>
  );
};

export default RootLayout;

此布局会应用到所有页面,并包含一个全局的导航栏和页脚。

子布局示例

假设 posts 目录需要一个不同的布局:

app/posts/layout.tsx
const PostsLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div>
      <aside>Posts Sidebar</aside>
      <main>{children}</main> {/* 每个页面的内容 */}
    </div>
  );
};

export default PostsLayout;

此布局只应用于 /posts 路径及其子路由。

5. 嵌套路由

使用新 App Router 时,路由可以嵌套,并且通过 layout.tsx 文件来组织页面结构。例如,/dashboard/settings 页面将继承 dashboard 目录中的布局。

app/dashboard/layout.tsx
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div>
      <nav>Dashboard Navigation</nav>
      <main>{children}</main>
    </div>
  );
};

export default DashboardLayout;
app/dashboard/page.tsx
const DashboardPage = () => {
  return <h1>Dashboard Home</h1>;
};

export default DashboardPage;
app/dashboard/settings/page.tsx
const SettingsPage = () => {
  return <h1>Dashboard Settings</h1>;
};

export default SettingsPage;

/dashboard/dashboard/settings 都将使用 DashboardLayout 作为布局。

6. 嵌套的动态路由

你也可以将动态路由与嵌套结构结合起来。例如,/posts/[id]/comments/[commentId] 这样的嵌套路由。

目录结构
app/
  └── posts/
       └── [id]/
            ├── page.tsx           // 对应 URL: /posts/[id]
            └── comments/
                └── [commentId]/
                     └── page.tsx  // 对应 URL: /posts/[id]/comments/[commentId]
app/posts/[id]/comments/[commentId]/page.tsx
const CommentPage = ({ params }: { params: { id: string; commentId: string } }) => {
  return (
    <div>
      <h1>Post ID: {params.id}</h1>
      <h2>Comment ID: {params.commentId}</h2>
    </div>
  );
};

export default CommentPage;

这个页面会匹配 /posts/123/comments/456,并且显示帖子 ID 和评论 ID。

7. Route Groups(路由组)

Route Groups 是 Next.js 13 中的一个新特性,允许你在不影响 URL 结构的情况下,对项目的路由进行分组和组织。这意味着你可以通过创建路由组来更好地管理和组织复杂的页面和组件,而不会在最终生成的 URL 中显示这些分组名称。

创建 Route Group

app 目录中,可以通过以 (group) 形式命名文件夹来创建 Route Group。括号中的名称不会出现在生成的 URL 中。

目录结构:

app/
  ├── (marketing)/
  │   ├── about/
  │   │   └── page.tsx       // 对应 /about
  │   └── contact/
  │       └── page.tsx       // 对应 /contact
  ├── (admin)/
  │   └── dashboard/
  │       └── page.tsx       // 对应 /dashboard
  └── page.tsx                // 对应 /
  • (marketing)(admin) 文件夹将被忽略,不会影响 URL。
  • /about 路径由 (marketing)/about/page.tsx 定义。
  • /dashboard 路径由 (admin)/dashboard/page.tsx 定义。

好处:

  • 更好的代码组织:可以将相关页面分组,而不影响最终的 URL。
  • 避免 URL 命名冲突:你可以在同一个项目中组织不同的路由组,而无需担心命名冲突。

示例代码:
app/(marketing)/about/page.tsx

const AboutPage = () => {
  return <h1>About Us</h1>;
};

export default AboutPage;

生成的 URL 是 /about,而不是 /marketing/about,因为 marketing 被视为一个 Route Group,不会影响 URL。

在 Route Group 中使用 Layouts

Route Groups 可以与 layouts 结合使用,为组内的所有页面定义共享的布局。

目录结构:

app/
  ├── (marketing)/
  │   ├── layout.tsx          // 为 marketing 路由组定义布局
  │   ├── about/
  │   │   └── page.tsx
  │   └── contact/
  │       └── page.tsx
  └── page.tsx

app/(marketing)/layout.tsx

const MarketingLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div>
      <nav>Marketing Navigation</nav>
      <main>{children}</main>
    </div>
  );
};

export default MarketingLayout;
  • 所有位于 (marketing) 路由组中的页面(如 aboutcontact)将共享 MarketingLayout 布局。
  • 生成的页面 /about/contact 会在同一个布局中渲染。
组合多个 Route Groups

你可以组合多个 Route Groups 来更好地组织项目,而不影响 URL。

目录结构:

app/
  ├── (marketing)/
  │   └── (features)/
  │       └── products/
  │           └── page.tsx   // 对应 /products
  └── page.tsx               // 对应 /

即使嵌套了多个 Route Groups,生成的 URL 仍然是 /products,不会包含 marketingfeatures

8. Private Folders(私有文件夹)

Private Folders 是指在 app 目录中以下划线 _ 开头的文件夹或文件。它们不会生成对应的路由,通常用于存放不需要暴露在 URL 中的文件,如帮助文件、组件库、上下文等。

目录结构:

app/
  ├── _components/            // 存放私有组件
  │   └── Button.tsx
  ├── _utils/                 // 存放工具函数
  │   └── formatDate.ts
  ├── about/
  │   └── page.tsx            // 对应 /about
  └── page.tsx                // 对应 /
  • _components_utils 文件夹中的内容不会生成任何路由。
  • 这些私有文件夹通常用于存放页面中使用的辅助函数、组件、hooks 或其他逻辑。

app/_components/Button.tsx

const Button = ({ label }: { label: string }) => {
  return <button>{label}</button>;
};

export default Button;

app/about/page.tsx

import Button from '../_components/Button';

const AboutPage = () => {
  return (
    <div>
      <h1>About Us</h1>
      <Button label="Click Me" />
    </div>
  );
};

export default AboutPage;

在上面的例子中,_components/Button.tsx 文件中的按钮组件可以在 about 页面中使用,但它不会单独生成一个路由。私有文件夹常用于这种场景。

Private Folders 的用途:

  • 存放组件库、上下文、工具函数等代码,这些代码不需要直接通过路由访问。
  • 避免对外暴露内部实现细节。
  • 有助于将文件结构整理得更干净,提升项目可维护性。

9. Parallel Routes(并行路由)

Parallel Routes 允许在页面的不同区域(或 Layout 中的不同插槽)中渲染多个不同的路由。它的设计目标是处理更复杂的 UI 需求,例如侧边栏导航、工具栏和内容区需要并行渲染不同的路由。

并行路由是通过 @ 符号定义的。你可以为页面布局中的不同区域创建多个并行路由。每个并行路由会有一个特定的路由名称。

目录结构:

app/
  ├── layout.tsx                // 全局布局
  ├── dashboard/
  │   ├── @main/                // 主区域路由
  │   │   └── page.tsx          // 对应 /dashboard
  │   ├── @sidebar/             // 侧边栏路由
  │   │   └── page.tsx          // 对应 /dashboard/sidebar
  └── page.tsx

app/layout.tsx

const RootLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div>
      <main>{children.main}</main>
      <aside>{children.sidebar}</aside>
    </div>
  );
};

export default RootLayout;

app/dashboard/@main/page.tsx

const DashboardMain = () => {
  return <h1>Main Content</h1>;
};

export default DashboardMain;

app/dashboard/@sidebar/page.tsx

const DashboardSidebar = () => {
  return <nav>Sidebar Content</nav>;
};

export default DashboardSidebar;
  • app/layout.tsx 中的 children.mainchildren.sidebar 表示两个并行的区域,分别渲染来自 @main@sidebar 文件夹的内容。
  • /dashboard 路径下,DashboardMain 会显示在 <main> 元素中,而 DashboardSidebar 会显示在 <aside> 元素中。
  • 页面 /dashboard 会同时渲染主内容区域(Main Content)和侧边栏(Sidebar Content)。
  • 你可以为不同的区域指定不同的路由,从而实现复杂的页面布局。

10. Intercepted Routes(拦截路由)

Intercepted Routes 允许你在某个页面或组件内部拦截路由的导航,并在已有页面的上下文中渲染新的内容。这个特性对于处理模式对话框(Modal)、侧边栏导航等场景非常有用。

使用场景
假设你有一个详情页面,当用户点击一个项目时,你希望在当前页面内显示一个模式对话框,而不是完全离开当前页面。这种交互可以通过拦截路由来实现。

如何使用
拦截路由通过特殊的文件夹 [folder] 和命名来实现,拦截的页面会在当前布局上下文中显示,而不改变原来的布局。

目录结构:

app/
  ├── dashboard/
  │   └── @main/
  │       ├── page.tsx           // 对应 /dashboard
  │       └── @modal/            // 对应 /dashboard/modal
  │           └── page.tsx       // 拦截路由,作为模式窗口渲染
  └── layout.tsx

app/layout.tsx

const RootLayout = ({ children }: { children: React.ReactNode }) => {
  return (
    <div>
      <main>{children.main}</main>
      {children.modal && <div className="modal">{children.modal}</div>}
    </div>
  );
};

export default RootLayout;

app/dashboard/@main/page.tsx

import Link from 'next/link';

const DashboardMain = () => {
  return (
    <div>
      <h1>Dashboard</h1>
      <Link href="/dashboard/modal">Open Modal</Link>
    </div>
  );
};

export default DashboardMain;

app/dashboard/@modal/page.tsx

const Modal = () => {
  return (
    <div>
      <h2>This is a modal</h2>
    </div>
  );
};

export default Modal;
  • 当用户访问 /dashboard 页面时,DashboardMain 将渲染在 <main> 中。
  • 当用户点击链接 /dashboard/modalModal 会拦截导航并显示在布局中的 <div className="modal"> 内,而不是直接导航到一个新页面。
  • 这种方式让你可以在页面内显示模式对话框(或其他交互),而不需要离开当前上下文。

实际效果:

  • /dashboard:显示主页面内容。
  • /dashboard/modal:在主页面内容不变的情况下,额外渲染一个对话框。

拦截路由的优势:

  • 保持页面上下文:你可以在不离开当前页面的情况下进行导航,这在需要保持页面状态时非常有用。
  • 处理模式对话框:你可以轻松地实现点击按钮弹出对话框的效果,同时保持当前页面不变。
特殊的路由前缀

在 Next.js 的拦截路由和文件结构中,诸如 (.)folder(..)folder(..)(..)folder(...)folder 这些符号是特殊的路由前缀,表示在不同层级的文件夹中如何加载页面或组件。它们帮助开发者以更加灵活的方式组织页面和路由,特别是在需要嵌套布局和模态对话框的场景下。

示例说明

app/
├── about
│   └── page.tsx  
├── f1/    
│   ├── (.)f2/      
│   │   └── page.tsx   
│   ├── f2/      
│   │   └── page.tsx   
│   ├── f3/    
│   │   └── page.tsx  
│   ├── f4/        
│   │	├── (..)f3/
│   │	│   └── page.tsx 
│   │	├── (...)about/    
│   │	│   └── page.tsx   
│   │   └── page.tsx   // link to 'f1/f3' and '/about'
│   └── page.tsx   // link to 'f1/f2'
  1. (.)folder

    • . 代表在当前目录拦截。
    • 例如,如果在 app/f1 目录下跳转到 /f1/f2,则渲染 app/f1/(.)f2 的内容;如果刷新页面或直接访问路由 /f1/f2,则渲染 app/f1/f2 的内容。
  2. (..)folder

    • .. 代表往上一级目录拦截。
    • 例如,如果在 app/f4 目录下跳转到 /f1/f3,则渲染 app/f1/f4/(..)f3 的内容;如果刷新页面或直接访问路由 /f1/f3,则渲染 app/f1/f3 的内容。
    • 如果在 app/f4 目录下跳转到 /about,则渲染 app/f1/f4/(...)about 的内容;如果刷新页面或直接访问路由 /about,则渲染 app/about 的内容。
  3. (..)(..)folder

    • .. .. 两个点表示往上两级目录拦截。
  4. (...)folder

    • ... 是通配符,表示从任意更高层级的路由进行拦截。在这个情况下,... 会使得这个拦截可以应用于所有更高层级的目录,而不必限定具体的某一级。

11. 错误页面与未捕获的路由

Next.js 提供了一个通用的 404 页面来处理未匹配的路由。你可以通过 not-found.tsx 自定义 404 页面。

app/not-found.tsx
const NotFound = () => {
  return <h1>404 - Page Not Found</h1>;
};

export default NotFound;

访问任何不存在的路由时,Next.js 将会自动渲染这个页面。

12. API 路由 (App Router)

app 目录中,Next.js 的 API 路由已经整合进了 pages/api 中,默认通过 pages 目录定义。如果需要使用 API 路由,仍然可以在 pages/api 目录中编写。

pages/api/hello.ts
import { NextApiRequest, NextApiResponse } from 'next';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  res.status(200).json({ message: 'Hello from Next.js API' });
}

请求 /api/hello 将返回 {"message": "Hello from Next.js API"}

13. 中间件(Middleware)

Next.js 13 仍然支持中间件功能,它允许你在请求到达页面之前执行操作,比如身份验证或重定向。

middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const url = request.nextUrl;

  // 如果用户访问的是 /about,重定向到 /
  if (url.pathname === '/about') {
    return NextResponse.redirect(new URL('/', request.url));
  }

  return NextResponse.next(); // 继续请求处理
}

中间件可以应用于 apppages 目录中的路由。

;