LogoNEXTDEVKIT 教程

Next.js 文件路由详解

深入了解 Next.js App Router 的 9 个特殊文件,学习如何使用文件系统路由 (File-system Routing) 构建现代 Web 应用。

提示:本篇为 Next.js 理论内容,与 NextDevKit 没有强关联。如果你已经熟悉 Next.js App Router,可以选择跳过。

学习本节可以帮你更好地理解 NextDevKit 的代码结构。

在学习了 Next.js 项目结构后,现在来深入了解 Next.js App Router 的核心概念——特殊文件系统。

Next.js App Router 引入了 9 个特殊文件,每个都有独特的作用和使用场景。本教程将逐一详解这些文件,并通过 NextDevKit 的实际代码示例帮助你理解和使用它们。

代码示例说明:下面的代码示例部分来自 NextDevKit 模板,部分来自 Next.js 官方示例。

什么是 Next.js App Router

Next.js App Router 是 Next.js 13 引入的新路由系统,基于 File-system Routing,使用 app 目录来定义路由。相比传统的 pages 目录,App Router 提供了:

  • File-system Routing:通过文件夹和文件结构定义路由
  • Server Components & Client Components:更好的性能和用户体验
  • Layouts 系统:共享 UI 组件和状态管理
  • Loading & Error Handling:内置加载状态和错误处理
  • Parallel Routes:同时显示多个页面内容

App Router 的 9 个特殊文件

让我们逐一学习这 9 个特殊文件:

1. page.tsx - 页面文件

什么是 page.tsx

page.tsx 定义了 Route Segment 的独特 UI 内容,是用户访问特定路径时看到的页面内容。

关键点

这是 App Router 的基础文件,每个可访问的路由都需要一个 page.tsx。它与文件夹结构直接对应,形成应用的路由系统。

如何使用

app 目录中的任何文件夹内创建 page.tsx 文件:

app/dashboard/page.tsx
import { DashboardHeader } from "@/components/dashboard/dashboard-header";
import GettingStarted from "@/components/examples/dashboard";
import { useTranslations } from "next-intl";

export default function DashboardPage() {
  const t = useTranslations();

  const breadcrumbs = [
    {
      label: t("menu.application.dashboard.title"),
      isCurrentPage: true,
    },
  ];

  return (
    <>
      <DashboardHeader breadcrumbs={breadcrumbs} />
      <div className="flex flex-1 flex-col">
        <div className="@container/main flex flex-1 flex-col gap-2">
          <div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
            <GettingStarted />
          </div>
        </div>
      </div>
    </>
  );
}

关键注意事项

  1. 导出方式:必须使用 export default,不支持 Named Export
  2. 目录结构:避免在同一项目中混用 pages/app/ 目录
  3. 数据获取:可以直接在组件中使用 fetch API 和 async/await
  4. 组件类型:默认为 Server Component,需要客户端交互时添加 'use client'

2. layout.tsx - Layout 文件

什么是 layout.tsx

layout.tsx 创建共享的 Layout,包装页面内容,定义多个页面共同的 UI 结构。

关键点

Layout 文件让你创建可复用的 UI 结构,减少重复代码,确保应用一致性。Layout 在页面导航时不会重新渲染,性能更好。

如何使用

根布局示例(NEXTDEVKIT 的根布局):

src/app/layout.tsx
import type { PropsWithChildren } from "react";
import "./globals.css";

export default function RootLayout({ children }: PropsWithChildren) {
  return children;
}

国际化布局示例

src/app/[locale]/layout.tsx
export default async function LocaleLayout({
  children,
  params,
}: Readonly<{
  children: React.ReactNode;
  params: Promise<{ locale: Locale }>;
}>) {
  const { locale } = await params;

  if (!hasLocale(routing.locales, locale)) {
    notFound();
  }

  return (
    <AppProviders locale={locale}>
      <NextIntlClientProvider>{children}</NextIntlClientProvider>
    </AppProviders>
  );
}

关键注意事项

  1. Root Layout:根布局(app/layout.tsx)必须定义 <html><body> 标签
  2. 嵌套 Layout:注意嵌套布局间的交互关系
  3. 路由信息:Layout 无法访问当前 Route Segment,需要时可用 useSelectedLayoutSegment Hook
  4. 渲染行为:Layout 在页面导航时不重新渲染,避免使用路由特定数据

3. template.tsx - Template 文件

什么是 template.tsx

template.tsx 创建可复用的 Template,类似 layout.tsx,但在每次导航时都会重新渲染。

关键点

当需要在每次导航时重置状态或触发动画时,Template 文件很有用,这是 Layout 无法做到的。

如何使用

app/blog/template.tsx
"use client";
import Link from "next/link";
import { motion } from "framer-motion";

export default function BlogTemplate({
  children,
}: {
  children: React.ReactNode,
}) {
  return (
    <div>
      <nav>
        <ul>
          <li>
            <Link href="/blog/1">文章 1</Link>
          </li>
          <li>
            <Link href="/blog/2">文章 2</Link>
          </li>
        </ul>
      </nav>
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        transition={{ duration: 0.5 }}
      >
        {children}
      </motion.div>
    </div>
  );
}

关键注意事项

  1. 使用场景:仅在确实需要每次导航重新渲染时使用 Template
  2. 性能影响:过度使用可能影响性能,因为缺少 Layout 的优化
  3. 与 Layout 配合:Template 与 Layout 配合使用,不是替代关系

NextDevKit 使用情况:NextDevKit 没有使用 template.tsx,大多数情况用 layout.tsx 实现模板功能。

4. loading.tsx - Loading UI 文件

什么是 loading.tsx

loading.tsx 创建在 Route Segment 内容加载时显示的 Loading UI。

关键点

提供即时用户反馈,改善用户体验,让应用感觉更响应,减少 Perceived Loading Time(感知加载时间)。

如何使用

app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <div className="flex flex-col items-center space-y-4">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
        <p className="text-gray-600">正在加载博客文章...</p>
      </div>
    </div>
  );
}

关键注意事项

  1. Suspense Boundary:Loading UI 自动包装在 React Suspense Boundary 中
  2. Layout Shift:平衡显示加载状态与避免内容加载时的 Layout Shift
  3. 作用域:Loading 状态在同一 Segment 的所有页面间共享
  4. Dynamic Routes:即使在不同 Dynamic Route 间导航也会显示加载状态
  5. 层级关系loading.tsx 只处理其 Route Segment 和子级的加载状态

5. error.tsx - Error Handling 文件

什么是 error.tsx

error.tsx 创建在 Route Segment 发生错误时显示的自定义 Error UI。

关键点

优雅处理 Runtime Error,提供更好的用户体验,而不是让整个应用崩溃。

如何使用

app/blog/error.tsx
'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div className="flex flex-col items-center justify-center min-h-screen">
      <div className="text-center">
        <h2 className="text-2xl font-bold mb-4">出错了!</h2>
        <p className="text-gray-600 mb-4">
          {error.message || "加载博客时发生了错误"}
        </p>
        <button
          onClick={() => reset()}
          className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
        >
          重试
        </button>
      </div>
    </div>
  );
}

关键注意事项

  1. Client Component:Error 组件必须是 Client Component,需要 'use client'
  2. Error Boundary:自动包装在 React Error Boundary 中
  3. 错误作用域:不会捕获同一 Segment 中 layout.tsxtemplate.tsx 的错误
  4. 错误冒泡:错误会冒泡到最近的父级 Error Boundary

6. global-error.tsx - Global Error Handling 文件

什么是 global-error.tsx

global-error.tsx 创建在应用根级别捕获和处理错误的 Global Error UI。

关键点

确保用户在最高级别发生错误时也能得到有意义的错误信息,而不是白屏或浏览器默认的错误页面。

如何使用

app/global-error.tsx
'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <html>
      <body>
        <div className="flex flex-col items-center justify-center min-h-screen">
          <h2 className="text-2xl font-bold mb-4">系统错误!</h2>
          <p className="text-gray-600 mb-4">应用发生了意外错误</p>
          <button 
            onClick={() => reset()}
            className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
          >
            重新加载应用
          </button>
        </div>
      </body>
    </html>
  )
}

关键注意事项

  1. 环境行为:仅在 Production 环境中工作,Development 环境会显示默认 Error Overlay
  2. Layout 替换:发生错误时完全替换 Root Layout
  3. HTML 结构:需要在组件中包含 <html><body> 标签
  4. 复杂度控制:保持组件简单,避免自身引起错误
  5. Error Handling 层级:作为最后的错误处理手段,尽可能在更细粒度层级处理错误

7. not-found.tsx - 404 页面文件

什么是 not-found.tsx

not-found.tsx 为 404 Not Found 错误创建自定义 UI。

关键点

创建品牌化、有用的页面,引导用户回到有效内容,而不是显示浏览器默认的 404 页面。

如何使用

NEXTDEVKIT 的 404 页面示例:

src/app/not-found.tsx
import NotFoundIcon from "@/components/icons/not-found";
import { Button } from "@/components/ui/button";
import Link from "next/link";

export default function NotFound() {
  return (
    <html lang="en">
      <body>
        <main className="flex flex-col items-center justify-center min-h-screen px-4 py-12">
          <div className="text-center mb-16">
            <NotFoundIcon className="mx-auto" />
            <h1 className="text-4xl font-bold mb-4">404 Not Found</h1>
            <p className="text-lg mb-16 text-gray-600 dark:text-gray-400">
              The page you&apos;re looking for doesn&apos;t exist or has been
              moved.
            </p>
            <Link href="/" className="inline-block">
              <Button className="px-6 py-4text-lg rounded-md font-semibold cursor-pointer">
                Return Home
              </Button>
            </Link>
          </div>
        </main>
      </body>
    </html>
  );
}

关键注意事项

  1. 作用域not-found.tsx 只处理其文件夹和子文件夹的 Not Found 场景
  2. notFound() 函数:使用 notFound() 函数触发时,确保同一 Segment 或父 Segment 中存在 not-found.tsx
  3. API Routesnot-found.tsx 不处理 API Routes 的 404 错误,需要单独处理

8. default.tsx - Parallel Routes 默认文件

什么是 default.tsx

default.tsx 为 Parallel Routes 提供 Fallback UI,当没有找到特定匹配时显示。

关键点

在复杂路由场景中创建平滑用户体验,特别是在使用 Parallel Routes 时。

如何使用

app/@sidebar/default.tsx
export default function DefaultSidebar() {
  return (
    <div className="w-64 bg-gray-100 p-4">
      <h2 className="text-xl font-bold mb-4">默认侧边栏</h2>
      <p className="text-gray-600">选择一个分类查看更多信息。</p>
    </div>
  );
}

关键注意事项

  1. 使用上下文:仅在 Parallel Routes 上下文中相关
  2. 兼容性:确保默认组件与 Parallel Routes 的所有可能状态兼容
  3. 渲染行为:初始渲染时显示,匹配到更具体路由时被替换
  4. 设计考虑:设计时要考虑在没有特定路由内容时提供有意义的信息或功能

9. route.ts - API Routes 文件

什么是 route.ts

route.ts 在 app 文件夹中直接创建 API Routes。

关键点

让你能够在前端代码旁边创建 Serverless API 端点,对 HTTP Request 进行细粒度控制。

如何使用

app/api/todos/route.ts
import { NextResponse } from "next/server";

// 虚拟数据存储(实际应用中应该使用数据库)
let todos = [
  { id: 1, title: "学习 Next.js", completed: false },
  { id: 2, title: "构建应用", completed: false },
];

export async function GET() {
  return NextResponse.json(todos);
}

export async function POST(request: Request) {
  const data = await request.json();
  const newTodo = {
    id: todos.length + 1,
    title: data.title,
    completed: false,
  };
  todos.push(newTodo);
  return NextResponse.json(newTodo, { status: 201 });
}

export async function PUT(request: Request) {
  const data = await request.json();
  const todoIndex = todos.findIndex(todo => todo.id === data.id);
  
  if (todoIndex === -1) {
    return NextResponse.json({ error: "Todo not found" }, { status: 404 });
  }
  
  todos[todoIndex] = { ...todos[todoIndex], ...data };
  return NextResponse.json(todos[todoIndex]);
}

export async function DELETE(request: Request) {
  const { searchParams } = new URL(request.url);
  const id = parseInt(searchParams.get('id') || '0');
  
  const todoIndex = todos.findIndex(todo => todo.id === id);
  
  if (todoIndex === -1) {
    return NextResponse.json({ error: "Todo not found" }, { status: 404 });
  }
  
  todos.splice(todoIndex, 1);
  return NextResponse.json({ message: "Todo deleted" });
}

关键注意事项

  1. 文件冲突:不要在同一文件夹中同时放置带有 GET Handler 的 route.tspage.tsx
  2. 执行上下文route.ts 始终在 Server 上执行,不要使用 Browser API
  3. Static Generation:用 route.ts 定义的 API Routes 不会在 Build Time 静态生成

NextDevKit 中的文件结构示例

让我们看看 NextDevKit 是如何组织这些特殊文件的:

layout.tsx
not-found.tsx
globals.css
layout.tsx
layout.tsx

最佳实践和使用建议

1. 选择合适的文件类型

  • 基础页面:从 page.tsxlayout.tsx 开始
  • 用户体验:添加 loading.tsxerror.tsx 提升体验
  • 特殊需求:根据需要添加其他特殊文件

2. 性能优化

  • Layout 优化:利用 Layout 不重新渲染的特性
  • Loading 状态:合理使用 Loading 状态,避免 Layout Shift
  • Error Boundary:在适当层级实现错误处理

3. 代码组织

  • 文件结构:保持清晰的文件结构
  • 组件复用:充分利用 Layout 和 Template 的复用性
  • API Routes 设计:合理设计 API Routes 结构

4. 开发流程

  • 渐进增强:从简单开始,逐步添加复杂功能
  • 测试覆盖:确保每个特殊文件都有适当测试
  • 文档维护:保持代码文档及时更新

总结

Next.js App Router 的 9 个特殊文件为构建现代 Web 应用提供了强大而灵活的基础:

  1. page.tsx - 定义页面内容
  2. layout.tsx - 创建共享 Layout
  3. template.tsx - 需要重新渲染的 Template
  4. loading.tsx - Loading UI
  5. error.tsx - Error Handling UI
  6. global-error.tsx - Global Error Handling
  7. not-found.tsx - 404 页面
  8. default.tsx - Parallel Routes 默认内容
  9. route.ts - API Routes

通过理解和正确使用这些文件,你可以构建出性能优异、用户体验良好的现代 Web 应用。NextDevKit 为你提供了这些最佳实践的完整示例,帮助你快速上手并构建生产级应用。

记住,不需要在每个项目中都使用所有这些文件。从基础的 page.tsxlayout.tsx 开始,根据需要逐步添加其他特殊文件。随着对这些概念的深入理解,你将能构建出更复杂、功能丰富的 Next.js 应用。

本篇内容参考博客:Key Considerations for Next.js App Router Files