Next.js 框架文件结构深度解析:每个文件都是干什么的?
Next.js 框架文件结构深度解析每个文件都是干什么的标签Next.js、React、前端工程化、App Router、文件结构前言很多开发者用create-next-app创建项目后看着满屏的文件夹和文件一头雾水app和pages有什么区别layout.tsx是干嘛的middleware.ts放在哪里next.config.js能配置什么本文从零开始逐一解析 Next.js 项目中每个文件和文件夹的作用彻底搞清楚 Next.js 的项目结构。一、项目初始化npx create-next-applatest my-app\--typescript\--tailwind\--eslint\--app\--src-dir生成的完整目录结构my-app/ ├── .next/ # 构建产物自动生成不要动 ├── node_modules/ # 依赖包 ├── public/ # 静态资源 ├── src/ │ ├── app/ # App Router核心 │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx # 根布局 │ │ └── page.tsx # 首页 │ ├── components/ # 公共组件 │ ├── lib/ # 工具函数 │ └── middleware.ts # 中间件 ├── .env.local # 本地环境变量 ├── .eslintrc.json # ESLint 配置 ├── .gitignore ├── next.config.js # Next.js 配置 ├── package.json ├── tailwind.config.ts # Tailwind 配置 └── tsconfig.json # TypeScript 配置二、App Router 核心文件重点Next.js 13 引入了 App Router彻底改变了文件组织方式。每个文件夹对应一个路由文件夹内有一套约定文件名各司其职。2.1 约定文件一览app/ └── dashboard/ ├── page.tsx # 页面内容必须 ├── layout.tsx # 布局可选 ├── loading.tsx # 加载状态可选 ├── error.tsx # 错误处理可选 ├── not-found.tsx # 404页面可选 ├── route.ts # API路由可选 └── template.tsx # 模板可选2.2 page.tsx — 页面组件最核心的文件每个路由必须有page.tsx才能被访问。// app/dashboard/page.tsx // 对应路由/dashboard // 默认是 Server Component服务端组件 // 可以直接 async/await无需 useEffect async function DashboardPage({ params, // 动态路由参数 searchParams, // URL查询参数 (?keyvalue) }: { params: { slug: string } searchParams: { [key: string]: string } }) { // 直接在组件内请求数据无需 getServerSideProps const data await fetch(https://api.example.com/dashboard, { cache: no-store, // SSR每次请求都重新获取 // cache: force-cache, // SSG构建时获取永久缓存 // next: { revalidate: 60} // ISR60秒重新验证 }) const json await data.json() return ( div h1Dashboard/h1 p{json.title}/p /div ) } export default DashboardPage // 生成静态路由SSG export async function generateStaticParams() { return [{ slug: home }, { slug: about }] } // 动态 SEO 元数据 export async function generateMetadata({ params }: { params: { slug: string } }) { return { title: Dashboard - ${params.slug}, description: 仪表盘页面, } }2.3 layout.tsx — 布局文件布局组件包裹子页面切换路由时不会重新渲染适合放导航栏、侧边栏等。// app/layout.tsx根布局必须存在 import type { Metadata } from next import { Inter } from next/font/google import ./globals.css const inter Inter({ subsets: [latin] }) // 全局 SEO 配置 export const metadata: Metadata { title: { default: My App, template: %s | My App, // 子页面标题模板 }, description: 我的Next.js应用, } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( html langzh body className{inter.className} {children} /body /html ) }// app/dashboard/layout.tsx嵌套布局 // 只作用于 /dashboard 及其子路由 export default function DashboardLayout({ children, }: { children: React.ReactNode }) { return ( div classNameflex aside classNamew-64 bg-gray-800 nav a href/dashboard概览/a a href/dashboard/settings设置/a /nav /aside main classNameflex-1 p-6 {children} /main /div ) }布局嵌套规则RootLayoutapp/layout.tsx └── DashboardLayoutapp/dashboard/layout.tsx └── page.tsxapp/dashboard/page.tsx2.4 loading.tsx — 加载状态基于 ReactSuspense实现页面数据加载时自动显示。// app/dashboard/loading.tsx export default function Loading() { return ( div classNameflex items-center justify-center h-screen div classNameanimate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 / span classNameml-3 text-gray-600加载中.../span /div ) }2.5 error.tsx — 错误处理页面报错时的降级展示必须是Client Component。// app/dashboard/error.tsx use client import { useEffect } from react export default function Error({ error, reset, }: { error: Error { digest?: string } reset: () void }) { useEffect(() { console.error(页面错误:, error) }, [error]) return ( div classNametext-center p-10 h2 classNametext-xl font-bold text-red-500页面出错了/h2 p classNametext-gray-500 mt-2{error.message}/p button onClick{reset} classNamemt-4 px-4 py-2 bg-blue-500 text-white rounded 重试 /button /div ) }2.6 not-found.tsx — 404 页面// app/not-found.tsx import Link from next/link export default function NotFound() { return ( div classNametext-center p-20 h1 classNametext-6xl font-bold text-gray-300404/h1 p classNametext-xl mt-4页面不存在/p Link href/ classNamemt-6 inline-block text-blue-500 返回首页 /Link /div ) }在代码中手动触发import { notFound } from next/navigation async function ProductPage({ params }: { params: { id: string } }) { const product await fetchProduct(params.id) if (!product) notFound() return div{product.name}/div }2.7 route.ts — API 路由// app/api/users/route.ts// 对应接口GET/POST /api/usersimport{NextRequest,NextResponse}fromnext/serverexportasyncfunctionGET(request:NextRequest){const{searchParams}newURL(request.url)constpagesearchParams.get(page)||1constusersawaitdb.query(SELECT * FROM users LIMIT 10 OFFSET ?,[(parseInt(page)-1)*10])returnNextResponse.json({data:users,page:parseInt(page)})}exportasyncfunctionPOST(request:NextRequest){constbodyawaitrequest.json()if(!body.name||!body.email){returnNextResponse.json({error:缺少必填参数},{status:400})}constuserawaitdb.create(body)returnNextResponse.json(user,{status:201})}// app/api/users/[id]/route.ts// 动态路由GET/DELETE /api/users/:idexportasyncfunctionGET(request:NextRequest,{params}:{params:{id:string}}){constuserawaitdb.findById(params.id)if(!user)returnNextResponse.json({error:不存在},{status:404})returnNextResponse.json(user)}exportasyncfunctionDELETE(request:NextRequest,{params}:{params:{id:string}}){awaitdb.delete(params.id)returnnewNextResponse(null,{status:204})}2.8 template.tsx — 模板和layout.tsx类似但每次路由切换都会重新渲染。// app/dashboard/template.tsx // 适合页面进入动画、每次切换都需要执行的埋点 use client import { useEffect } from react export default function Template({ children }: { children: React.ReactNode }) { useEffect(() { // 每次路由切换都执行 console.log(页面切换上报埋点) }, []) return div classNameanimate-fadeIn{children}/div }三、路由系统详解3.1 基础路由app/ ├── page.tsx → / ├── about/page.tsx → /about └── dashboard/ ├── page.tsx → /dashboard └── settings/page.tsx → /dashboard/settings3.2 动态路由app/ ├── blog/[slug]/page.tsx → /blog/hello-world ├── shop/[...categories]/page.tsx → /shop/a/b/c └── docs/[[...slug]]/page.tsx → /docs 或 /docs/a/b// app/blog/[slug]/page.tsx export default function BlogPost({ params }: { params: { slug: string } }) { return h1文章{params.slug}/h1 }3.3 路由分组不影响URLapp/ ├── (marketing)/ # 括号分组不出现在URL │ ├── layout.tsx # 营销页布局 │ └── about/page.tsx → /about └── (dashboard)/ ├── layout.tsx # 后台布局 └── admin/page.tsx → /admin3.4 并行路由// app/dashboard/layout.tsx export default function Layout({ children, analytics, // 对应 analytics 插槽 team, // 对应 team 插槽 }: { children: React.ReactNode analytics: React.ReactNode team: React.ReactNode }) { return ( div {children} div classNamegrid grid-cols-2 gap-4 {analytics} {team} /div /div ) }四、middleware.ts — 中间件// src/middleware.tsimport{NextResponse}fromnext/serverimporttype{NextRequest}fromnext/serverexportfunctionmiddleware(request:NextRequest){const{pathname}request.nextUrlconsttokenrequest.cookies.get(token)?.value// 1. 登录鉴权constprotectedRoutes[/dashboard,/admin,/profile]constisProtectedprotectedRoutes.some(rpathname.startsWith(r))if(isProtected!token){returnNextResponse.redirect(newURL(/login,request.url))}// 2. 权限控制if(pathname.startsWith(/admin)){constrolerequest.cookies.get(role)?.valueif(role!admin){returnNextResponse.redirect(newURL(/403,request.url))}}// 3. 添加自定义请求头constresponseNextResponse.next()response.headers.set(x-custom-header,my-value)returnresponse}exportconstconfig{matcher:[/dashboard/:path*,/admin/:path*,/((?!_next/static|_next/image|favicon.ico).*),],}五、Server Component vs Client Component// Server Component默认 // 服务端运行可直接访问数据库不能用 Hooks async function ServerComponent() { const data await db.query(SELECT * FROM users) return div{data.map(u p key{u.id}{u.name}/p)}/div }// Client Component加 use client // 浏览器运行可用 Hooks 和事件处理 use client import { useState } from react function ClientComponent() { const [count, setCount] useState(0) return button onClick{() setCount(c c 1)}点击{count}/button }最佳实践Server Component 负责数据获取Client Component 只处理交互// app/dashboard/page.tsxServer Component import LikeButton from ./LikeButton // Client Component async function DashboardPage() { const posts await fetchPosts() // 服务端请求 return ( div {posts.map(post ( div key{post.id} h2{post.title}/h2 LikeButton postId{post.id} / {/* 只有交互部分是客户端 */} /div ))} /div ) }六、next.config.js — 核心配置/** type {import(next).NextConfig} */constnextConfig{// 图片域名白名单images:{remotePatterns:[{protocol:https,hostname:images.example.com},],},// 301重定向asyncredirects(){return[{source:/old/:slug,destination:/new/:slug,permanent:true},]},// 请求代理解决跨域asyncrewrites(){return[{source:/api/:path*,destination:http://backend:8080/:path*},]},// 响应头配置asyncheaders(){return[{source:/(.*),headers:[{key:X-Frame-Options,value:DENY},],},]},}module.exportsnextConfig七、环境变量# .env.local不提交gitDATABASE_URLmysql://localhost:3306/mydbJWT_SECRETyour-secret-key# NEXT_PUBLIC_ 前缀才能在客户端访问NEXT_PUBLIC_API_URLhttps://api.example.com// 服务端可访问所有环境变量 const dbUrl process.env.DATABASE_URL // 客户端只能访问 NEXT_PUBLIC_ 变量 const apiUrl process.env.NEXT_PUBLIC_API_URL八、完整文件速查表文件作用是否必须app/layout.tsx根布局包裹所有页面✅ 必须app/page.tsx首页内容✅ 必须app/*/page.tsx对应路由的页面有路由就必须app/*/layout.tsx嵌套布局切换路由不重渲染可选app/*/loading.tsx数据加载时的骨架屏可选app/*/error.tsx页面报错降级展示可选app/*/not-found.tsx404页面可选app/*/route.tsAPI接口RESTful可选app/*/template.tsx每次路由切换都重渲染可选middleware.ts请求拦截鉴权/重定向可选next.config.jsNext.js全局配置✅ 必须public/静态资源直接通过URL访问可选.env.local本地环境变量可选总结Next.js App Router 的文件约定系统记住几个核心原则文件夹 路由page.tsx是路由的入口没有它路由不存在layout.tsx不重新渲染适合导航栏、侧边栏等持久化 UI默认都是 Server Component需要交互才加use clientroute.ts是 API 接口支持 GET、POST、PUT、DELETE 等所有方法middleware.ts最先执行是做鉴权和重定向的最佳位置掌握这套文件体系Next.js 的其他内容就迎刃而解了。如果本文对你有帮助欢迎点赞收藏有问题欢迎评论区交流。