Next.js项目结构 #
一、基础目录结构 #
1.1 默认项目结构 #
text
my-nextjs-app/
├── src/
│ └── app/
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── public/
│ └── images/
├── .eslintrc.json
├── .gitignore
├── next.config.ts
├── package.json
├── postcss.config.mjs
├── README.md
├── tailwind.config.ts
└── tsconfig.json
1.2 推荐的完整结构 #
text
my-nextjs-app/
├── src/
│ ├── app/ # App Router 目录
│ │ ├── (auth)/ # 路由组:认证相关
│ │ │ ├── login/
│ │ │ │ └── page.tsx
│ │ │ └── register/
│ │ │ └── page.tsx
│ │ ├── (dashboard)/ # 路由组:仪表盘
│ │ │ ├── layout.tsx
│ │ │ ├── dashboard/
│ │ │ │ └── page.tsx
│ │ │ └── settings/
│ │ │ └── page.tsx
│ │ ├── api/ # API 路由
│ │ │ ├── auth/
│ │ │ │ └── route.ts
│ │ │ └── users/
│ │ │ └── route.ts
│ │ ├── blog/ # 博客页面
│ │ │ ├── [slug]/
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── components/ # 共享组件
│ │ │ ├── ui/ # UI基础组件
│ │ │ └── features/ # 功能组件
│ │ ├── error.tsx # 全局错误页面
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── layout.tsx # 根布局
│ │ ├── loading.tsx # 全局加载页面
│ │ ├── not-found.tsx # 404页面
│ │ └── page.tsx # 首页
│ ├── components/ # 全局共享组件
│ │ ├── ui/
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ └── input.tsx
│ │ ├── layout/
│ │ │ ├── footer.tsx
│ │ │ ├── header.tsx
│ │ │ └── sidebar.tsx
│ │ └── features/
│ │ ├── auth-form.tsx
│ │ └── user-profile.tsx
│ ├── hooks/ # 自定义Hooks
│ │ ├── use-auth.ts
│ │ └── use-local-storage.ts
│ ├── lib/ # 工具库
│ │ ├── api.ts
│ │ ├── auth.ts
│ │ └── utils.ts
│ ├── types/ # TypeScript类型
│ │ ├── api.ts
│ │ └── index.ts
│ └── styles/ # 全局样式
│ └── variables.css
├── public/ # 静态资源
│ ├── fonts/
│ ├── images/
│ │ ├── logo.png
│ │ └── og-image.png
│ └── robots.txt
├── .env.local # 本地环境变量
├── .env.example # 环境变量示例
├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── components.json # shadcn/ui配置
├── middleware.ts # 中间件
├── next.config.ts
├── package.json
├── postcss.config.mjs
├── tailwind.config.ts
└── tsconfig.json
二、App Router目录详解 #
2.1 特殊文件 #
App Router 定义了一系列特殊文件,用于处理不同的功能:
| 文件名 | 用途 | 说明 |
|---|---|---|
page.tsx |
页面组件 | 定义路由页面 |
layout.tsx |
布局组件 | 共享布局 |
template.tsx |
模板组件 | 类似布局但会重新创建 |
loading.tsx |
加载状态 | 显示加载UI |
error.tsx |
错误处理 | 处理运行时错误 |
not-found.tsx |
404页面 | 处理未找到路由 |
global-error.tsx |
全局错误 | 处理根布局错误 |
default.tsx |
默认页面 | 并行路由默认页 |
route.ts |
API路由 | 处理API请求 |
middleware.ts |
中间件 | 请求拦截处理 |
2.2 页面文件 page.tsx #
tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: '页面标题',
description: '页面描述',
}
export default function Page() {
return <div>页面内容</div>
}
2.3 布局文件 layout.tsx #
tsx
export default function Layout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="layout">
<header>头部</header>
<main>{children}</main>
<footer>底部</footer>
</div>
)
}
2.4 加载状态 loading.tsx #
tsx
export default function Loading() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
</div>
)
}
2.5 错误处理 error.tsx #
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 className="flex flex-col items-center justify-center min-h-screen">
<h2>出错了!</h2>
<button onClick={() => reset()}>重试</button>
</div>
)
}
2.6 404页面 not-found.tsx #
tsx
import Link from 'next/link'
export default function NotFound() {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h1 className="text-6xl font-bold">404</h1>
<p className="text-xl mt-4">页面未找到</p>
<Link href="/" className="mt-8 text-blue-500 hover:underline">
返回首页
</Link>
</div>
)
}
三、路由组织 #
3.1 路由段 #
text
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/:slug
└── shop/
├── [category]/
│ └── [id]/
│ └── page.tsx → /shop/:category/:id
└── page.tsx → /shop
3.2 路由组 #
使用括号创建路由组,不影响URL路径:
text
app/
├── (marketing)/
│ ├── about/
│ │ └── page.tsx → /about
│ ├── contact/
│ │ └── page.tsx → /contact
│ └── layout.tsx # 营销页面共享布局
├── (shop)/
│ ├── cart/
│ │ └── page.tsx → /cart
│ ├── products/
│ │ └── page.tsx → /products
│ └── layout.tsx # 商店页面共享布局
└── layout.tsx # 根布局
3.3 并行路由 #
使用 @ 前缀创建并行路由:
text
app/
├── dashboard/
│ ├── @team/
│ │ └── page.tsx
│ ├── @analytics/
│ │ └── page.tsx
│ ├── layout.tsx
│ └── page.tsx
布局中使用:
tsx
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode
team: React.ReactNode
analytics: React.ReactNode
}) {
return (
<div>
{children}
<div className="grid grid-cols-2 gap-4">
{team}
{analytics}
</div>
</div>
)
}
3.4 拦截路由 #
使用 (.) 前缀拦截同级路由:
text
app/
├── feed/
│ └── page.tsx → /feed
├── photo/
│ └── [id]/
│ └── page.tsx → /photo/:id
└── (.)photo/
└── [id]/
└── page.tsx # 拦截 /feed 中的 /photo/:id
四、组件组织 #
4.1 组件分类 #
text
src/components/
├── ui/ # 基础UI组件
│ ├── button.tsx
│ ├── card.tsx
│ ├── input.tsx
│ ├── select.tsx
│ └── index.ts # 统一导出
├── layout/ # 布局组件
│ ├── header.tsx
│ ├── footer.tsx
│ ├── sidebar.tsx
│ └── navigation.tsx
├── features/ # 功能组件
│ ├── auth/
│ │ ├── login-form.tsx
│ │ └── register-form.tsx
│ ├── blog/
│ │ ├── post-card.tsx
│ │ └── post-list.tsx
│ └── user/
│ ├── avatar.tsx
│ └── profile.tsx
└── shared/ # 共享组件
├── loading-spinner.tsx
└── error-boundary.tsx
4.2 UI组件示例 #
tsx
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors',
{
variants: {
variant: {
default: 'bg-primary text-white hover:bg-primary/90',
outline: 'border border-input bg-background hover:bg-accent',
ghost: 'hover:bg-accent hover:text-accent-foreground',
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
4.3 组件导出规范 #
tsx
export { Button } from './button'
export { Card } from './card'
export { Input } from './input'
export { Select } from './select'
五、工具库组织 #
5.1 lib目录结构 #
text
src/lib/
├── api.ts # API请求封装
├── auth.ts # 认证相关
├── constants.ts # 常量定义
├── db.ts # 数据库连接
├── utils.ts # 工具函数
└── validations.ts # 表单验证
5.2 工具函数示例 #
tsx
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function formatDate(date: Date | string): string {
return new Date(date).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
export function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '')
}
5.3 API封装示例 #
tsx
const BASE_URL = process.env.NEXT_PUBLIC_API_URL
interface FetchOptions extends RequestInit {
params?: Record<string, string>
}
export async function fetchApi<T>(
endpoint: string,
options: FetchOptions = {}
): Promise<T> {
const { params, ...fetchOptions } = options
const url = new URL(`${BASE_URL}${endpoint}`)
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value)
})
}
const response = await fetch(url.toString(), {
...fetchOptions,
headers: {
'Content-Type': 'application/json',
...fetchOptions.headers,
},
})
if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`)
}
return response.json()
}
六、类型定义组织 #
6.1 types目录结构 #
text
src/types/
├── api.ts # API响应类型
├── models.ts # 数据模型类型
├── components.ts # 组件Props类型
└── index.ts # 统一导出
6.2 类型定义示例 #
tsx
export interface User {
id: string
name: string
email: string
avatar?: string
createdAt: Date
updatedAt: Date
}
export interface Post {
id: string
title: string
content: string
slug: string
author: User
tags: string[]
publishedAt: Date
}
export interface ApiResponse<T> {
data: T
message: string
success: boolean
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number
limit: number
total: number
totalPages: number
}
}
七、命名规范 #
7.1 文件命名 #
| 类型 | 命名规范 | 示例 |
|---|---|---|
| 页面 | 小写 | page.tsx |
| 布局 | 小写 | layout.tsx |
| 组件 | 小写kebab-case | user-card.tsx |
| Hook | camelCase | useAuth.ts |
| 工具函数 | camelCase | formatDate.ts |
| 类型 | camelCase | user.ts |
| 常量 | UPPER_SNAKE_CASE | API_ENDPOINTS.ts |
7.2 组件命名 #
tsx
export function UserCard({ user }: UserCardProps) {}
export function BlogPostList({ posts }: BlogPostListProps) {}
export function NavigationHeader() {}
7.3 目录命名 #
text
src/app/
├── blog/ # 小写
├── user-profile/ # kebab-case
├── (dashboard)/ # 路由组用括号
└── @team/ # 并行路由用@前缀
八、最佳实践 #
8.1 目录组织原则 #
- 按功能分组:相关文件放在一起
- 就近原则:组件和样式就近放置
- 共享提取:公共组件提取到共享目录
- 扁平结构:避免过深的目录嵌套
8.2 导入路径 #
使用别名简化导入:
tsx
import { Button } from '@/components/ui/button'
import { formatDate } from '@/lib/utils'
import { User } from '@/types'
8.3 代码分割 #
大型组件按需加载:
tsx
import dynamic from 'next/dynamic'
const HeavyComponent = dynamic(() => import('./heavy-component'), {
loading: () => <p>加载中...</p>,
})
九、总结 #
项目结构要点:
| 要点 | 说明 |
|---|---|
| App Router | 使用app目录组织路由 |
| 特殊文件 | page/layout/error等 |
| 组件分类 | ui/layout/features |
| 工具库 | lib目录统一管理 |
| 类型定义 | types目录集中管理 |
| 命名规范 | 统一命名风格 |
下一步,让我们深入学习 Next.js 的路由系统!
最后更新:2026-03-28