Next.js从零搭建完整教程:3个必踩坑与最佳实践

刚开始我也以为Next.js项目搭建很简单,不就是npx create-next-app嘛?结果第一个线上项目就踩了三个坑:SSR缓存策略不对导致页面空白、API路由安全没处理好、还有那个该死的“水合错误”搞了我整整一天。今天就把这些血泪经验整理出来,希望能帮你少走弯路。

开篇:从零开始的项目骨架,展示初始化和核心文件结构

先看:脚手架初始化与目录结构

官方脚手架确实方便,但默认配置对新手不太友好。我推荐用这个命令:

bash
npx create-next-app@latest my-project --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
`

为什么要这么写?因为:

  • –app:启用App Router,这是Next.js 13+的推荐方式
  • –src-dir:源码放src目录,方便后期维护
  • –import-alias “@/*”:用@符号代替相对路径,避免../../../这种地狱

初始化完成后,我的习惯是立刻删掉src/app/page

<

p>.tsx里的默认内容,还有public/下的默认图标。为什么?因为这些模板代码会干扰你理解真正的路由逻辑。

目录结构我会这样调整:

`
src/
├── app/ # 路由页面
│ ├── layout.tsx # 根布局
│ ├── page.tsx # 首页
│ └── api/ # API路由
├── components/ # 组件
├── lib/ # 工具函数
└── styles/ # 全局样式
`

核心坑1:App Router的“隐式路由”陷阱

刚开始用App Router时,我以为所有路由都要手动创建文件。结果发现Next.js会自动将page.tsx映射为路由,而loading.tsxerror.tsx这些文件会自动成为该路由的加载和错误边界。

这个设计真的反人类——我第一天就犯了个错误:在/products/[id]目录下同时放了page.tsxloading.tsx,结果loading一直没生效。后来发现是因为loading.tsx必须和page.tsx同级,且文件名必须严格匹配。

正确的结构应该是:

`typescript
// src/app/products/[id]/page.tsx
// 这是产品详情页

import { notFound } from 'next/navigation'

inter

face ProductPageProps {
params: { id: string }
}

export default async function ProductPage({ params }: ProductPageProps) {
// 异步获取数据
const product = await getProduct(params.id)

if (!product) {
notFound() // 触发 404
}

return (

{product.name}

{product.description}

)
}

// src/app/products/[id]/loading.tsx
// 这个文件会自动成为该路由的加载状态
export default function Loading() {
return

加载中...

}
`

为什么要这么写?因为App Router的约定式路由要求:

  • page.tsx:页面内容
  • loading.tsx:异步组件加载时的占位
  • error.tsx:错误处理
  • not-found.tsx:404页面

核心坑2:数据获取的SSR缓存策略

这个坑让我线上项目白屏了整整两个小时。默认情况下,Next.js的fetch在服务端组件中会自动缓存,但在客户端组件中不会。如果你混合使用,就会出现“水合错误”——服务端渲染的内容和客户端不一致。

来看我踩过的代码:

`typescript
// ❌ 错误写法:混合使用会导致水合错误
// src/app/dashboard/page.tsx
export default async function DashboardPage() {
const data = await fetch('https://api.example.com/dashboard', {
cache: 'force-cache' // 服务端缓存
}).then(res => res.json())

return
}

// src/components/ClientComponent.tsx
'use client'
export default function ClientComponent({ data }: { data: any }) {
const [localData, setLocalData] = useState(data)

useEffect(() => {
// 客户端又发起请求,导致数据不一致
fetch('/api/dashboard').then(res => res.json()).then(setLocalData)
}, [])

return

{localData.name}

}
`

正确的做法是:要么全在服务端处理,要么全在客户端处理。我推荐用服务端组件获取数据,然后通过props传给客户端组件:

`typescript
// ✅ 正确写法:服务端一次性获取数据
// src/app/dashboard/page.tsx
export default async function DashboardPage() {
const data = await fetch('https://api.example.com/dashboard', {
next: { revalidate: 60 } // 每60秒重新验证
}).then(res => res.json())

return
}

// src/components/DashboardClient.tsx
'use client'
export default function DashboardClient({ initialData }: { initialData: any }) {
// 使用initialData作为初始值,不再发起额外请求
return

{initialData.name}

}
`

这样优化后,页面加载时间从原来的3.2秒降到了0.8秒。

核心部分:展示SSR缓存策略优化前后的性能对比图

核心坑3:API路由的安全隐患

Next.js的API路由默认就是公开的,刚开始我直接写:

`typescript
// ❌ 危险写法
export async function POST(request: Request) {
const { userId, data } = await request.json()
// 直接操作数据库
await db.updateUser(userId, data)
return Response.json({ success: true })
}
`

这种写法等于把数据库操作暴露给所有人。正确的做法是加中间件和验证:

`typescript
// ✅ 安全写法:API路由 + 中间件验证
// src/middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
// 检查API路由的认证
if (request.nextUrl.pathname.startsWith('/api/')) {
const token = request.cookies.get('auth-token')
if (!token) {
return NextResponse.json({ error: '未授权' }, { status: 401 })
}
}
return NextResponse.next()
}

// src/app/api/user/route.ts
export async function POST(request: Request) {
try {
const { userId, data } = await request.json()

// 验证输入
if (!userId || !data) {
return Response.json({ error: '参数缺失' }, { status: 400 })
}

// 使用服务端环境变量
const apiKey = process.env.INTERNAL_API_KEY
const response = await fetch('http://internal-api/user/update', {
method: 'POST',
headers: {
'Authorization':
Bearer ${apiKey},
'Content-Type': 'application/json'
},
body: JSON.stringify({ userId, data })
})

return Response.json(await response.json())
} catch (error) {
return Response.json({ error: '服务器错误' }, { status: 500 })
}
}
`

官方文档这段文档不够清晰,实际上核心就是:永远不要在API路由里直接暴露数据库或第三方服务的密钥

还有个技巧:环境变量管理

项目大了后,环境变量很容易混乱。我推荐用这样的结构:

`env

.env.local (本地开发)

NEXT_PUBLIC_API_URL=http://localhost:3000
DATABASE_URL=mysql://user:pass@localhost:3306/mydb

.env.production (生产环境)

NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=mysql://user:pass@production:3306/mydb
`

注意:只有NEXT_PUBLIC_开头的变量会暴露给客户端,其他的只在服务端可用。

部署实战:Vercel + 自定义域名

部署到Vercel是最简单的,但有几个坑要注意:

  • 构建命令:默认的next build就够了,但如果用了@/引用,需要在next.config.js里配置路径别名
  • 环境变量:在Vercel的Project Settings > Environment Variables里添加,不要写在代码里
  • 自定义域名:在Vercel的Domains里添加,然后去DNS服务商加CNAME记录
  • `typescript
    // next.config.js
    /** @type {import('next').NextConfig} */
    const nextConfig = {
    images: {
    domains: ['images.example.com'], // 允许加载外部图片的域名
    },
    // 自定义构建输出
    output: 'standalone', // 用于Docker部署
    }

    module.exports = nextConfig
    `

    总结前:Vercel部署配置截图,展示环境变量和域名设置

    总结一下,你可以立刻用的三个点:

  • 路由结构:用App Router的约定式路由,page.tsxloading.tsxerror.tsx自动匹配,别手动写路由文件
  • 数据获取:服务端组件用fetch+revalidate缓存策略,客户端组件用initialData`初始化,避免水合错误
  • API安全:用中间件验证,永远不要在API路由里直接暴露密钥,用环境变量管理
  • 最后提醒:Next.js发展太快,记得经常看官方文档的Changelog,特别是App Router的相关更新。我的项目就因为没及时升级到13.4的稳定版,踩了好几个坑。

    滚动至顶部