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
文件:
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>
</>
);
}
关键注意事项
- 导出方式:必须使用
export default
,不支持 Named Export - 目录结构:避免在同一项目中混用
pages/
和app/
目录 - 数据获取:可以直接在组件中使用
fetch
API 和async/await
- 组件类型:默认为 Server Component,需要客户端交互时添加
'use client'
2. layout.tsx - Layout 文件
什么是 layout.tsx
layout.tsx
创建共享的 Layout,包装页面内容,定义多个页面共同的 UI 结构。
关键点
Layout 文件让你创建可复用的 UI 结构,减少重复代码,确保应用一致性。Layout 在页面导航时不会重新渲染,性能更好。
如何使用
根布局示例(NEXTDEVKIT 的根布局):
import type { PropsWithChildren } from "react";
import "./globals.css";
export default function RootLayout({ children }: PropsWithChildren) {
return children;
}
国际化布局示例:
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>
);
}
关键注意事项
- Root Layout:根布局(
app/layout.tsx
)必须定义<html>
和<body>
标签 - 嵌套 Layout:注意嵌套布局间的交互关系
- 路由信息:Layout 无法访问当前 Route Segment,需要时可用
useSelectedLayoutSegment
Hook - 渲染行为:Layout 在页面导航时不重新渲染,避免使用路由特定数据
3. template.tsx - Template 文件
什么是 template.tsx
template.tsx
创建可复用的 Template,类似 layout.tsx
,但在每次导航时都会重新渲染。
关键点
当需要在每次导航时重置状态或触发动画时,Template 文件很有用,这是 Layout 无法做到的。
如何使用
"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>
);
}
关键注意事项
- 使用场景:仅在确实需要每次导航重新渲染时使用 Template
- 性能影响:过度使用可能影响性能,因为缺少 Layout 的优化
- 与 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(感知加载时间)。
如何使用
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>
);
}
关键注意事项
- Suspense Boundary:Loading UI 自动包装在 React Suspense Boundary 中
- Layout Shift:平衡显示加载状态与避免内容加载时的 Layout Shift
- 作用域:Loading 状态在同一 Segment 的所有页面间共享
- Dynamic Routes:即使在不同 Dynamic Route 间导航也会显示加载状态
- 层级关系:
loading.tsx
只处理其 Route Segment 和子级的加载状态
5. error.tsx - Error Handling 文件
什么是 error.tsx
error.tsx
创建在 Route Segment 发生错误时显示的自定义 Error UI。
关键点
优雅处理 Runtime Error,提供更好的用户体验,而不是让整个应用崩溃。
如何使用
'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>
);
}
关键注意事项
- Client Component:Error 组件必须是 Client Component,需要
'use client'
- Error Boundary:自动包装在 React Error Boundary 中
- 错误作用域:不会捕获同一 Segment 中
layout.tsx
或template.tsx
的错误 - 错误冒泡:错误会冒泡到最近的父级 Error Boundary
6. global-error.tsx - Global Error Handling 文件
什么是 global-error.tsx
global-error.tsx
创建在应用根级别捕获和处理错误的 Global Error UI。
关键点
确保用户在最高级别发生错误时也能得到有意义的错误信息,而不是白屏或浏览器默认的错误页面。
如何使用
'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>
)
}
关键注意事项
- 环境行为:仅在 Production 环境中工作,Development 环境会显示默认 Error Overlay
- Layout 替换:发生错误时完全替换 Root Layout
- HTML 结构:需要在组件中包含
<html>
和<body>
标签 - 复杂度控制:保持组件简单,避免自身引起错误
- Error Handling 层级:作为最后的错误处理手段,尽可能在更细粒度层级处理错误
7. not-found.tsx - 404 页面文件
什么是 not-found.tsx
not-found.tsx
为 404 Not Found 错误创建自定义 UI。
关键点
创建品牌化、有用的页面,引导用户回到有效内容,而不是显示浏览器默认的 404 页面。
如何使用
NEXTDEVKIT 的 404 页面示例:
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're looking for doesn'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>
);
}
关键注意事项
- 作用域:
not-found.tsx
只处理其文件夹和子文件夹的 Not Found 场景 - notFound() 函数:使用
notFound()
函数触发时,确保同一 Segment 或父 Segment 中存在not-found.tsx
- API Routes:
not-found.tsx
不处理 API Routes 的 404 错误,需要单独处理
8. default.tsx - Parallel Routes 默认文件
什么是 default.tsx
default.tsx
为 Parallel Routes 提供 Fallback UI,当没有找到特定匹配时显示。
关键点
在复杂路由场景中创建平滑用户体验,特别是在使用 Parallel Routes 时。
如何使用
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>
);
}
关键注意事项
- 使用上下文:仅在 Parallel Routes 上下文中相关
- 兼容性:确保默认组件与 Parallel Routes 的所有可能状态兼容
- 渲染行为:初始渲染时显示,匹配到更具体路由时被替换
- 设计考虑:设计时要考虑在没有特定路由内容时提供有意义的信息或功能
9. route.ts - API Routes 文件
什么是 route.ts
route.ts
在 app 文件夹中直接创建 API Routes。
关键点
让你能够在前端代码旁边创建 Serverless API 端点,对 HTTP Request 进行细粒度控制。
如何使用
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" });
}
关键注意事项
- 文件冲突:不要在同一文件夹中同时放置带有 GET Handler 的
route.ts
和page.tsx
- 执行上下文:
route.ts
始终在 Server 上执行,不要使用 Browser API - Static Generation:用
route.ts
定义的 API Routes 不会在 Build Time 静态生成
NextDevKit 中的文件结构示例
让我们看看 NextDevKit 是如何组织这些特殊文件的:
最佳实践和使用建议
1. 选择合适的文件类型
- 基础页面:从
page.tsx
和layout.tsx
开始 - 用户体验:添加
loading.tsx
和error.tsx
提升体验 - 特殊需求:根据需要添加其他特殊文件
2. 性能优化
- Layout 优化:利用 Layout 不重新渲染的特性
- Loading 状态:合理使用 Loading 状态,避免 Layout Shift
- Error Boundary:在适当层级实现错误处理
3. 代码组织
- 文件结构:保持清晰的文件结构
- 组件复用:充分利用 Layout 和 Template 的复用性
- API Routes 设计:合理设计 API Routes 结构
4. 开发流程
- 渐进增强:从简单开始,逐步添加复杂功能
- 测试覆盖:确保每个特殊文件都有适当测试
- 文档维护:保持代码文档及时更新
总结
Next.js App Router 的 9 个特殊文件为构建现代 Web 应用提供了强大而灵活的基础:
- page.tsx - 定义页面内容
- layout.tsx - 创建共享 Layout
- template.tsx - 需要重新渲染的 Template
- loading.tsx - Loading UI
- error.tsx - Error Handling UI
- global-error.tsx - Global Error Handling
- not-found.tsx - 404 页面
- default.tsx - Parallel Routes 默认内容
- route.ts - API Routes
通过理解和正确使用这些文件,你可以构建出性能优异、用户体验良好的现代 Web 应用。NextDevKit 为你提供了这些最佳实践的完整示例,帮助你快速上手并构建生产级应用。
记住,不需要在每个项目中都使用所有这些文件。从基础的 page.tsx
和 layout.tsx
开始,根据需要逐步添加其他特殊文件。随着对这些概念的深入理解,你将能构建出更复杂、功能丰富的 Next.js 应用。