# Next.js 项目初始化最佳实践
## 规则类型
代码生成 (GENERATOR)
## 适用技术
Next.js, React, TypeScript
## 问题描述
初始化 Next.js 项目时,开发者经常面临以下挑战:
- 不清楚如何组织项目文件结构
- 缺乏对必要依赖包的了解
- 配置文件设置不当导致性能问题
- 没有遵循最佳实践导致后期维护困难
- 缺少必要的开发工具和质量保障措施
- 未考虑项目的可扩展性和长期维护
## 规则内容
创建一个新的 Next.js 项目应遵循以下最佳实践:
1. 使用官方推荐的初始化方法
2. 采用清晰、模块化的文件结构
3. 选择合适的依赖包并锁定版本
4. 正确配置 TypeScript、ESLint 和其他工具
5. 设置合理的环境变量管理
6. 实现基础组件和布局结构
7. 配置适当的构建和部署流程
## 初始化命令示例
```bash
# 使用 create-next-app 创建项目
npx create-next-app@latest my-nextjs-app --typescript --eslint --tailwind --app --src-dir --import-alias "@/*"
# 进入项目目录
cd my-nextjs-app
# 安装核心依赖
npm install next-auth@latest zod@latest react-hook-form@latest @hookform/resolvers@latest axios@latest date-fns@latest
# 安装UI组件库
npm install class-variance-authority@latest clsx@latest tailwind-merge@latest
# 安装开发依赖
npm install -D prettier@latest prettier-plugin-tailwindcss@latest @types/node@latest
```
## 推荐的项目文件结构
```
my-nextjs-app/
├── .env.local # 本地环境变量
├── .env.example # 环境变量示例
├── .eslintrc.json # ESLint 配置
├── .gitignore # Git 忽略文件
├── next.config.js # Next.js 配置
├── package.json # 项目依赖
├── postcss.config.js # PostCSS 配置
├── tailwind.config.js # Tailwind 配置
├── tsconfig.json # TypeScript 配置
├── public/ # 静态资源
│ ├── favicon.ico
│ └── images/
├── src/ # 源代码
│ ├── app/ # App Router 路由
│ │ ├── (auth)/ # 认证相关路由组
│ │ │ ├── login/
│ │ │ └── register/
│ │ ├── (dashboard)/ # 仪表盘路由组
│ │ ├── api/ # API 路由
│ │ ├── globals.css # 全局样式
│ │ ├── layout.tsx # 根布局
│ │ └── page.tsx # 首页
│ ├── components/ # 组件
│ │ ├── ui/ # UI 组件
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ └── ...
│ │ ├── forms/ # 表单组件
│ │ ├── layout/ # 布局组件
│ │ │ ├── header.tsx
│ │ │ ├── footer.tsx
│ │ │ └── ...
│ │ └── shared/ # 共享组件
│ ├── config/ # 配置文件
│ │ ├── site.ts # 站点配置
│ │ └── dashboard.ts # 仪表盘配置
│ ├── hooks/ # 自定义 Hooks
│ │ ├── use-toast.ts
│ │ └── ...
│ ├── lib/ # 工具库
│ │ ├── auth.ts # 认证工具
│ │ ├── db.ts # 数据库连接
│ │ └── utils.ts # 通用工具函数
│ ├── styles/ # 样式
│ │ └── ...
│ └── types/ # TypeScript 类型
│ ├── index.d.ts
│ └── ...
└── README.md # 项目说明
```
## 关键配置文件设置
### next.config.js
```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
domains: ['images.unsplash.com', 'avatars.githubusercontent.com'],
formats: ['image/avif', 'image/webp'],
},
experimental: {
serverActions: true,
},
// 国际化配置(如需要)
i18n: {
locales: ['zh-CN', 'en-US'],
defaultLocale: 'zh-CN',
},
}
module.exports = nextConfig
```
### tsconfig.json
```json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
```
### tailwind.config.js
```javascript
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}
```
### .env.local 示例
```
# 应用
NEXT_PUBLIC_APP_URL=http://localhost:3000
# 认证
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-key-here
# 数据库
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
# 第三方服务
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
```
## 核心工具和库的设置
### 1. 工具函数 (src/lib/utils.ts)
```typescript
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function formatDate(input: string | number | Date): string {
const date = new Date(input)
return date.toLocaleDateString("zh-CN", {
month: "long",
day: "numeric",
year: "numeric",
})
}
export function absoluteUrl(path: string) {
return `${process.env.NEXT_PUBLIC_APP_URL}${path}`
}
```
### 2. 认证配置 (src/lib/auth.ts)
```typescript
import { NextAuthOptions } from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import GitHubProvider from "next-auth/providers/github"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { db } from "@/lib/db"
import { compare } from "bcrypt"
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(db),
session: {
strategy: "jwt",
},
pages: {
signIn: "/login",
},
providers: [
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
CredentialsProvider({
name: "Credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null
}
const user = await db.user.findUnique({
where: { email: credentials.email }
})
if (!user) {
return null
}
const isPasswordValid = await compare(
credentials.password,
user.password!
)
if (!isPasswordValid) {
return null
}
return {
id: user.id,
email: user.email,
name: user.name,
}
}
})
],
callbacks: {
async session({ token, session }) {
if (token) {
session.user.id = token.id
session.user.name = token.name
session.user.email = token.email
session.user.image = token.picture
}
return session
},
async jwt({ token, user }) {
const dbUser = await db.user.findFirst({
where: {
email: token.email,
},
})
if (!dbUser) {
if (user) {
token.id = user.id
}
return token
}
return {
id: dbUser.id,
name: dbUser.name,
email: dbUser.email,
picture: dbUser.image,
}
},
},
}
```
### 3. 数据库连接 (src/lib/db.ts)
```typescript
import { PrismaClient } from "@prisma/client"
declare global {
var prisma: PrismaClient | undefined
}
export const db = globalThis.prisma || new PrismaClient()
if (process.env.NODE_ENV !== "production") globalThis.prisma = db
```
## 基础组件示例
### 1. 按钮组件 (src/components/ui/button.tsx)
```tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
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 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
```
### 2. 根布局 (src/app/layout.tsx)
```tsx
import { Inter } from 'next/font/google'
import { Metadata } from 'next'
import { cn } from '@/lib/utils'
import { Toaster } from '@/components/ui/toaster'
import { ThemeProvider } from '@/components/theme-provider'
import { SiteHeader } from '@/components/layout/site-header'
import { SiteFooter } from '@/components/layout/site-footer'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: {
default: '我的 Next.js 应用',
template: '%s | 我的 Next.js 应用',
},
description: '使用 Next.js 构建的现代化 Web 应用',
keywords: ['Next.js', 'React', 'TypeScript', 'Web 应用'],
authors: [
{
name: '您的名字',
url: 'https://yourwebsite.com',
},
],
creator: '您的名字',
metadataBase: new URL(process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'),
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN" suppressHydrationWarning>
<body className={cn(
'min-h-screen bg-background font-sans antialiased',
inter.className
)}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<div className="relative flex min-h-screen flex-col">
<SiteHeader />
<main className="flex-1">{children}</main>
<SiteFooter />
</div>
<Toaster />
</ThemeProvider>
</body>
</html>
)
}
```
## 常见陷阱和避免方法
### 1. 图片优化
**问题**: 未使用 Next.js 的 Image 组件导致性能问题。
**解决方案**:
```tsx
// ❌ 错误方式
<img src="/images/hero.jpg" alt="Hero" />
// ✅ 正确方式
import Image from 'next/image'
<Image
src="/images/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority
/>
```
### 2. 客户端状态管理
**问题**: 过度使用全局状态管理导致性能下降。
**解决方案**:
- 优先使用 React 的内置状态管理
- 使用 Context API 进行局部状态共享
- 只在必要时引入 Redux 或其他状态管理库
### 3. 数据获取
**问题**: 在客户端组件中进行不必要的数据获取。
**解决方案**:
```tsx
// ❌ 错误方式 (在客户端组件中)
useEffect(() => {
fetch('/api/data').then(...)
}, [])
// ✅ 正确方式 (在服务器组件中)
async function Page() {
const data = await fetch('https://api.example.com/data')
const result = await data.json()
return <Component data={result} />
}
```
### 4. 环境变量
**问题**: 错误地在客户端暴露服务器端环境变量。
**解决方案**:
- 服务器端环境变量: 直接使用 `process.env.SECRET_KEY`
- 客户端环境变量: 必须以 `NEXT_PUBLIC_` 开头,如 `process.env.NEXT_PUBLIC_API_URL`
### 5. 路由和导航
**问题**: 使用传统的 `window.location` 进行导航,导致整页刷新。
**解决方案**:
```tsx
// ❌ 错误方式
<a href="/about">关于我们</a>
// 或
window.location.href = '/about'
// ✅ 正确方式
import Link from 'next/link'
import { useRouter } from 'next/navigation'
// 声明式导航
<Link href="/about">关于我们</Link>
// 命令式导航
const router = useRouter()
router.push('/about')
```
## 部署准备
### 1. 构建脚本
在 package.json 中添加以下脚本:
```json
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"format": "prettier --write "**/*.{js,ts,tsx,md}"",
"analyze": "ANALYZE=true next build"
}
```
### 2. CI/CD 配置 (.github/workflows/ci.yml)
```yaml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Type check
run: npm run type-check
- name: Build
run: npm run build
```
## 例外情况
在以下情况下可以适当调整这些规则:
- 小型演示项目可以简化文件结构
- 特定领域的应用可能需要不同的文件组织方式
- 与现有项目集成时可能需要遵循已有的项目结构
- 特定的部署环境可能需要特殊的配置
## 相关规则
- Next.js 性能优化最佳实践
- React 组件设计模式
- TypeScript 类型定义最佳实践
- CSS-in-JS vs CSS 模块化方案对比
## 参考资料
- [Next.js 官方文档](https://nextjs.org/docs)
- [Next.js GitHub 仓库](https://github.com/vercel/next.js)
- [React 官方文档](https://react.dev/)
- [TypeScript 官方文档](https://www.typescriptlang.org/docs/)
- [Tailwind CSS 文档](https://tailwindcss.com/docs)
- [Vercel 部署文档](https://vercel.com/docs)