Next.js项目从零搭建实战指南 – 解决你必踩的3个坑

刚开始我也以为Next.js项目搭建不就是 npx create-next-app 吗?结果连踩三个坑:路由配置混乱、SSR缓存搞不定、部署后静态资源404。今天我就把这个过程掰开了揉碎了讲,每一步都带代码和避坑点。

先看环境准备。我用的Node.js 18.17.0,npx version 9.8.1。直接跑这个命令:
bash
npx create-next-app@latest my-next-project --typescript --app --src-dir

为什么要加 --app ?因为从Next.js 13.4开始,App Router是默认推荐的路由方案,比Pages Router更灵活,支持嵌套布局、加载状态、错误边界,后面你会体会到它有多香。 --src-dir 是把所有源码放 src/ 目录下,强迫症福音。注意,这里没加 --tailwind,因为Next.js 14+里这个参数变了,我建议先创建项目,再手动装Tailwind CSS,官方文档有详细步骤。

*开篇:环境配置完成后的项目结构,重点标注src/app目录和配置文件*

创建完项目后,第一件事不是写代码,是配置环境变量。生产环境和开发环境要分开,别学我当初把API密钥直接写死在代码里,GitHub上一提交就被机器人扫到了。
bash
# .env.local - 本地开发用,不上传到Git
DATABASE_URL="postgresql://user:pass@localhost:5432/db"
NEXT_PUBLIC_API_URL="http://localhost:3000/api"

# .env.production - 生产环境,在CI/CD里注入
DATABASE_URL="postgresql://user:pass@production-db:5432/db"
NEXT_PUBLIC_API_URL="https://yourapp.com/api"

这里有个坑:NEXT_PUBLIC_ 前缀的变量会暴露到浏览器端,绝对不能放密钥。官方文档这段文档不够清晰,我当初把JWT_SECRET加了前缀,结果前端直接打印出来了,还好是测试环境。另外注意,.env.production 只在构建时加载,运行时环境变量得通过平台(比如Vercel)注入,或者用 .env.production.local(但这个别提交到Git)。

另一个坑是App Router的路由规则。在 src/app 下,page.tsx 就是页面,layout.tsx 是布局,loading.tsx 是加载状态,error.tsx 是错误边界。看代码:
tsx
// src/app/layout.tsx - 全局布局,所有页面共享
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
title: '我的Next应用',
description: '从零搭建教程',
}

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (

{children}

)
}

这个设计真的反人类?一开始我也觉得,但用顺手后发现,布局和页面分离,比如你做博客,文章页和首页共享头部导航,直接改 layout.tsx 就行,不用每个页面复制粘贴。

*核心:App Router路由结构图,展示嵌套布局和页面层次关系*

接下来是API路由。用App Router后,API路由放在 src/app/api/ 下,文件名就是路由路径。比如建一个用户列表接口:
tsx
// src/app/api/users/route.ts
import { NextResponse } from 'next/server'

// 模拟数据库
const users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
]

export async function GET() {
// 这里可以加缓存控制
return NextResponse.json(users, {
status: 200,
headers: {
'Cache-Control': 'public, max-age=0, must-revalidate',
}
})
}

export async function POST(request: Request) {
try {
const body = await request.json()
// 实际项目中要校验数据
const newUser = { id: users.length + 1, name: body.name }
users.push(newUser)
return NextResponse.json(newUser, { status: 201 })
} catch (error) {
return NextResponse.json(
{ error: '参数错误' },
{ status: 400 }
)
}
}

为什么要加 Cache-Control ?因为Next.js的API路由默认没缓存,每次请求都重新执行。我在博客上遇到过,首页请求数据库3.2秒,加上这个后降到0.8秒,差距巨大。注意,这里我没用 s-maxage=60,因为API路由在无服务器环境(比如Vercel)下,s-maxage 只对CDN生效,本地开发没意义。我改成 public, max-age=0, must-revalidate 来禁用缓存,或者你也可以用 stale-while-revalidate 配合页面级 revalidate 选项。

还有个技巧:用 force-dynamicrevalidate 控制页面缓存级别。在页面文件里加:
tsx
// src/app/page.tsx
export const dynamic = 'force-dynamic' // 每次请求都重新渲染
// 或者
export const revalidate = 60 // 按需重新生成,不是定时刷新

官方文档上说 revalidate 是”增量静态生成”,但实际用起来是”按需重新生成”——比如设了 revalidate: 10,意思是10秒后触发重新生成,但首次访问可能还是旧缓存,得配合 stale-while-revalidate 理解。我踩坑过:设了 revalidate: 10,结果10秒后页面没更新,后来发现是CDN缓存没清,得配 VercelNginx 的缓存策略。

部署到Vercel是最简单的,但有个隐藏坑:静态资源路径。如果你用了 public/ 目录下的图片,在Vercel上路径是 /image.png,但本地开发是 localhost:3000/image.png,没问题。可如果你用自定义域名,比如 https://yourdomain.com/image.png,Vercel会自动处理。但我试过用 next/config 配图片域名,发现 images.domains 在Next.js 14+里已经废弃了,得换成 images.remotePatterns。比如在 next.config.mjs 里这样写:
js
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'yourdomain.com' },
{ protocol: 'https', hostname: 'cdn.example.com' },
],
}

这样就不会报错了。

滚动至顶部