Next.js从入门到实战保姆级教程:项目结构与文件系统约定
本系列文章将围绕Next.js技术栈旨在为AI Agent开发者提供一套完整的客户端侧工程实践指南。Next.js 与传统 React 项目的最大差异在于其基于文件系统的路由机制。在传统 React 项目中开发者可以自由组织文件结构路由需要单独配置而 Next.js 通过文件系统的目录结构直接定义 URL 路由。这种设计虽然初看起来具有约束性但一旦掌握将大幅减少繁琐的路由配置工作。本章将系统性地讲解这套约定体系。一、核心原则文件路径映射 URL 路径这是 App Router 的核心约定必须深刻理解src/app/page.tsx → / src/app/about/page.tsx → /about src/app/blog/page.tsx → /blog src/app/blog/[slug]/page.tsx → /blog/任意值 src/app/dashboard/settings/page.tsx → /dashboard/settings文件与路由映射的基本规则如下每个路由段URL 中两个斜杠之间的部分对应一个目录目录中的page.tsx文件即为该路由的页面组件仅page.tsx会成为可访问的页面其他文件不会暴露为路由这一设计允许开发者在app/目录中存放组件、工具函数甚至测试文件无需担心用户直接访问到它们。二、特殊文件Next.js 的约定系统在每个路由目录中Next.js 识别若干特殊文件名这些文件承担不同的职责layout.tsx共享布局持久存在目录下的所有路由共享page.tsx页面内容必需每个路由的页面组件loading.tsx页面处于加载状态时展示error.tsx错误处理界面当发生错误时就会替代page组件展示notfound404 页面当路由片段对应的路由不存在时展示template.tsx每次导航重置的布局当用户访问/dashboard/settings时Next.js 按以下顺序组合页面app/layout.tsx ← 最外层包裹所有页面 app/dashboard/layout.tsx ← dashboard 专属布局 app/dashboard/settings/loading.tsx ← 数据加载时的占位 app/dashboard/settings/page.tsx ← 实际页面内容若数据加载出错error.tsx将替代page.tsx显示若路由不存在not-found.tsx将接管。这套机制被称为分层错误边界提供了优雅的错误处理方案。三、特殊文件详解1. layout.tsx — 持久化布局布局文件是 App Router 中最重要的概念之一。1核心特性用户在同一个布局下的子路由间切换时布局组件不会重新渲染。这意味着布局内的状态、滚动位置、动画等都会被保留。2典型应用场景管理后台的左侧导航栏不应在点击不同菜单项时重新加载。layout.tsx正是为实现这种稳定的外壳而设计。// src/app/dashboard/layout.tsx// 此布局将在所有 /dashboard/* 页面中持续存在exportdefaultfunctionDashboardLayout({children,}:{children:React.ReactNode}){return(div classNameflex h-screen{/* 左侧导航切换页面时不会重新渲染 */}aside classNamew-64 bg-gray-900 text-white p-4nav classNamespace-y-2a href/dashboard概览/aa href/dashboard/analytics数据分析/aa href/dashboard/settings设置/a/nav/aside{/* 右侧内容区每次路由变化时更新 */}main classNameflex-1 overflow-auto p-8{children}/main/div)}3根布局的特殊性根布局src/app/layout.tsx必须包含html和body标签因为它是整个应用的 HTML 骨架。此处适合放置全局字体配置全局样式导入全局状态 Provider如 Redux、Context全局的Meta数据// src/app/layout.tsximporttype{Metadata}fromnextimport{Inter}fromnext/font/googleimport./globals.cssconstinterInter({subsets:[latin]})exportconstmetadata:Metadata{title:My Application,description:Built with Next.js,}exportdefaultfunctionRootLayout({children,}:{children:React.ReactNode}){return(html langzh-CNbody className{inter.className}{children}/body/html)}2. template.tsx — 重置布局template.tsx与layout.tsx非常相似都可以包裹子路由但两者的核心区别在于渲染行为。1核心特性模板文件在导航时会被重新挂载。当用户在不同路由间切换时即使它们共享同一个template.tsxNext.js 也会销毁旧的模板实例并创建一个全新的实例。这意味着状态不保留模板内的 React 状态会被重置。副作用重新执行useEffect等副作用钩子会重新运行。动画重置CSS 动画或过渡效果会从头开始播放。2典型应用场景template.tsx适用于那些需要“每次进入都重新开始”的场景比如进入动画、表单重置、埋点统计等。最典型的就是页面切换动画。如果你希望每次进入页面时都有一个“淡入”或“滑入”的动画使用layout.tsx是很难实现的因为它不会重新渲染而template.tsx则能完美解决。// src/app/dashboard/template.tsx// 每次导航到 /dashboard 下的页面时此组件都会重新挂载exportdefaultfunctionDashboardTemplate({children,}:{children:React.ReactNode}){return(// 利用 key 或组件重新挂载特性触发动画div classNameanimate-fade-in{children}/div)}3layout.tsx 与 template.tsx 对比为了更直观地理解我们可以通过下表对比两者的区别特性layout.tsxtemplate.tsx导航行为持久化不重新渲染重置重新挂载状态保持保持状态重置状态副作用不重新运行重新运行CSS 动画仅在初次加载时触发每次导航都会触发性能更高复用 DOM稍低重建 DOM适用场景导航栏、侧边栏、页脚页面过渡动画、重置表单状态4共存规则你可以在同一个路由层级同时拥有layout.tsx和template.tsx。在这种情况下template.tsx会包裹在layout.tsx内部。文件结构示例src/app/ ├── layout.tsx -- 根布局 (始终存在) └── dashboard/ ├── layout.tsx -- 持久化侧边栏 ├── template.tsx -- 页面切换动画容器 └── page.tsx -- 实际页面内容渲染层级关系RootLayout └── DashboardLayout (持久化) └── DashboardTemplate (每次导航重新创建) └── PageContent选择建议 默认使用 layout.tsx只有当需要每次进入页面都重新执行的逻辑时才考虑使用 template.tsx。3. loading.tsx — 优雅的加载状态当页面需要从服务器获取数据时loading.tsx提供等待期间的视觉反馈。// src/app/blog/loading.tsxexportdefaultfunctionBlogLoading(){return(div classNamespace-y-4{/* 骨架屏用灰色方块模拟内容形状 */}{Array.from({length:5}).map((_,i)(div key{i}classNameanimate-pulsediv classNameh-6 bg-gray-200 rounded w-3/4 mb-2/div classNameh-4 bg-gray-100 rounded w-full/div classNameh-4 bg-gray-100 rounded w-5/6 mt-1//div))}/div)}1 工作原理Next.js 自动将page.tsx包裹在 React 的Suspense组件中使用loading.tsx作为 fallback。2最佳实践骨架屏Skeleton Screen的体验显著优于旋转 Loading 图标。骨架屏让用户预知内容即将呈现及其大致布局有效降低等待焦虑。设计时应尽量模拟真实内容的布局比例。4. error.tsx — 错误边界处理任何页面都可能因网络请求失败、数据库异常或代码错误而出错。error.tsx提供安全网确保用户看到友好的错误提示而非白屏或浏览器默认错误页。// src/app/blog/error.tsx// 注意error.tsx 必须是客户端组件use clientimport{useEffect}fromreactexportdefaultfunctionBlogError({error,reset,}:{error:Error{digest?:string}reset:()void// 重试函数尝试重新渲染此路由段}){useEffect((){// 将错误上报至监控系统如 Sentryconsole.error(Blog section error:,error)},[error])return(div classNametext-center py-16h2 classNametext-2xl font-bold text-gray-800 mb-2内容加载失败/h2p classNametext-gray-500 mb-6可能是网络问题请尝试刷新/pbutton onClick{reset}classNamepx-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors重试/button/div)}关键要求error.tsx必须是客户端组件需添加use client原因需捕获客户端渲染错误且通常涉及事件处理如重试按钮5. not-found.tsx — 404 页面当调用notFound()函数或路由不存在时Next.js 将显示此组件。// src/app/not-found.tsximportLinkfromnext/linkexportdefaultfunctionNotFound(){return(div classNamemin-h-screen flex items-center justify-centerdiv classNametext-centerh1 classNametext-6xl font-bold text-gray-300404/h1p classNamemt-4 text-xl text-gray-600页面未找到/pLink href/classNamemt-6 inline-block px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600返回首页/Link/div/div)}四、路由组代码组织与 URL 解耦随着项目规模扩大app/目录会变得复杂通常我们能够对路由进行分组这就是路由组Route Groups。路由组通过括号命名目录实现代码组织与 URL 结构的解耦——括号目录不会出现在最终 URL 中。1. 目录结构示例src/app/ ├── (auth)/ ← 括号不出现在 URL 中 │ ├── login/ │ │ └── page.tsx → /login │ └── register/ │ └── page.tsx → /register ├── (marketing)/ │ ├── about/ │ │ └── page.tsx → /about │ └── pricing/ │ └── page.tsx → /pricing └── (app)/ ├── layout.tsx ← 此布局仅应用于 (app) 组的页面 ├── dashboard/ │ └── page.tsx → /dashboard └── settings/ └── page.tsx → /settings2. 核心价值差异化布局路由组最实用的能力是为不同页面组应用不同的布局。例如认证页面登录、注册采用居中卡片的极简布局应用页面包含侧边栏导航营销页面使用品牌化的导航栏通过路由组三套布局互不干扰// src/app/(auth)/layout.tsx// 仅 login、register 页面使用此布局exportdefaultfunctionAuthLayout({children}:{children:React.ReactNode}){return(div classNamemin-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-centerdiv classNamebg-white rounded-2xl shadow-xl p-8 w-full max-w-md{children}/div/div)}⚠️ 注意由于路由分组分组名不会体现在最终 URL 中。如果在不同分组里创建了相同路径的页面如/(groupA)/user/page.tsx和/(groupB)/user/page.tsx造成路由冲突。请确保跨分组的页面路径唯一。五、动态路由URL 参数化处理动态路由允许路由包含可变的部分比如博客文章、用户主页、商品详情等页面的id参数。动态路由使用方括号匹配这些可变段。1. 基础动态路由src/app/blog/[slug]/page.tsx → /blog/hello-world /blog/my-first-post /blog/anything-here在页面组件中通过params获取动态值// src/app/blog/[slug]/page.tsxexportdefaultasyncfunctionBlogPost({params,}:{params:Promise{slug:string}}){const{slug}awaitparams// 使用 slug 从数据库或 API 获取文章数据constpostawaitgetPostBySlug(slug)if(!post){// 找不到文章显示 404notFound()}return(articleh1{post.title}/h1div{post.content}/div/article)}Next.js 15 重要变更params现在是 Promise 类型需要使用await解包。旧版本教程中直接解构{ params }的写法已不适用。2. 捕获所有路由段对于不确定数量的路径段如文档系统使用[...slug]语法最终的params会被处理成一个数组/docs/getting-started /docs/api/components/button /docs/guides/authentication/jwt// src/app/docs/[...slug]/page.tsxexportdefaultasyncfunctionDocsPage({params,}:{params:Promise{slug:string[]}}){const{slug}awaitparams// slug 为数组[getting-started] 或 [api, components, button]returndiv文档内容/div}六、API 路由Route Handlers除了页面组件Next.js 支持在同一项目中编写 API 接口。在app/目录下route.ts文件而非page.tsx定义 HTTP 端点。1. 基本用法基本的使用实在route.ts中导出指定HTTP方法名的函数// src/app/api/posts/route.tsimport{NextResponse}fromnext/server// 处理 GET /api/postsexportasyncfunctionGET(request:Request){const{searchParams}newURL(request.url)constpagesearchParams.get(page)||1constpostsawaitgetPosts({page:parseInt(page)})returnNextResponse.json(posts)}// 处理 POST /api/postsexportasyncfunctionPOST(request:Request){constbodyawaitrequest.json()constpostawaitcreatePost(body)returnNextResponse.json(post,{status:201})}2. 支持的 HTTP 方法Route Handlers 支持所有标准 HTTP 方法GET、POST、PUT、PATCH、DELETEHEAD、OPTIONS同一文件中可导出多个函数分别处理不同的 HTTP 方法。3. 适用场景虽然 Route Handlers 功能强大但在 App Router 中服务端数据操作有更推荐的方案——Server Actions详见第 9 章。Route Handlers 更适合以下场景第三方服务集成移动 App、其他微服务需要 HTTP 接口Webhook 接收端第三方支付回调、GitHub 事件通知特定 HTTP 语义需求需要返回特定的 HTTP 状态码、Headers流式响应SSEServer-Sent Events、AI 流式输出反模式警示避免在服务端组件中fetch自己编写的 Route Handler。应直接在服务端组件中调用数据库或业务逻辑。七、推荐的项目代码组织结构上述内容聚焦于app/目录的约定。完整的项目还需考虑组件、工具函数、类型定义的组织方式。1. 通用项目结构以下是被广泛采用的目录结构src/ ├── app/ ← Next.js 路由仅存放路由相关文件 │ ├── (auth)/ ← 认证相关页面 │ ├── (main)/ ← 主应用页面 │ └── api/ ← API 路由 ├── components/ ← 可复用的 React 组件 │ ├── ui/ ← 纯 UI 组件Button、Input、Modal 等 │ └── features/ ← 业务功能组件PostCard、UserAvatar 等 ├── lib/ ← 工具函数和业务逻辑 │ ├── db.ts ← 数据库客户端配置 │ ├── auth.ts ← 认证相关逻辑 │ └── utils.ts ← 通用工具函数 ├── hooks/ ← 自定义 React Hooks ├── types/ ← TypeScript 类型定义 └── styles/ ← 全局样式文件可选2. 设计原则此结构遵循一个简单原则app/目录仅负责路由具体逻辑和 UI 组件置于外部。这样设计的优势便于未来迁移至其他框架方便提取功能为独立库最小化改动范围3. 文件命名规范React 社区有两种主流命名风格PascalCaseUserProfile.tsx组件文件kebab-caseuser-profile.tsx工具函数、配置文件选择哪种风格均可关键是全项目保持一致。个人建议组件文件使用 PascalCase其他文件工具函数、类型定义、配置使用 kebab-case八、本章小结通过本章学习你应该掌握了文件系统路由的核心约定文件路径即 URL 路径特殊文件的用途layout、loading、error、not-found、template路由组的价值代码组织与 URL 解耦支持差异化布局动态路由的实现[param]和[...param]语法Route Handlers 的基本用法及适用场景推荐的项目代码组织结构与命名规范下一章将深入探讨路由系统的高级特性——导航机制、URL 参数处理、并行路由及路由守卫的实现。