Next.js 是由 Vercel 公司开发并维护的开源 React 全栈框架。它在 React 的基础上提供了服务端渲染(SSR)、静态站点生成(SSG)、增量静态再生(ISR)、文件系统路由、API 路由等开箱即用的能力,是目前构建生产级 React 应用的首选框架。
Next.js 自 2016 年发布以来迅速成为 React 生态中最受欢迎的框架,被 TikTok、Twitch、Hulu、Nike、Notion 等知名公司采用。2023 年推出的 App Router 和 React Server Components 支持更是引领了全栈 React 开发的新范式。
| 项目信息 | 详情 |
|---|---|
| 开发团队 | Vercel(美国旧金山) |
| GitHub Stars | 130,000+ |
| 官方网站 | nextjs.org |
| 开源协议 | MIT |
| 技术栈 | React / Node.js / TypeScript |
| 当前版本 | Next.js 15(App Router 为默认) |
| 包管理 | npm / yarn / pnpm / bun |
| 对比维度 | Next.js | Nuxt.js | Remix | Gatsby |
|---|---|---|---|---|
| 底层框架 | React | Vue.js | React | React |
| GitHub Stars | 130k+ | 55k+ | 30k+ | 55k+ |
| 渲染模式 | SSR/SSG/ISR/CSR/Streaming | SSR/SSG/ISR/CSR | SSR/CSR/Streaming | SSG 为主 |
| Server Components | 原生支持 | 不支持 | 实验性 | 不支持 |
| 路由系统 | 文件系统路由(App Router) | 文件系统路由 | 文件系统路由 | 文件系统 + GraphQL |
| 数据获取 | fetch + 缓存 + revalidate | useFetch / useAsyncData | loader / action | GraphQL 数据层 |
| 部署平台 | Vercel / 任意 Node.js | 任意 Node.js | 任意 Node.js | Gatsby Cloud / CDN |
| TypeScript | 开箱即用 | 开箱即用 | 开箱即用 | 需配置 |
| 学习曲线 | 中等 | 较低(Vue 更简单) | 中等 | 中等(GraphQL) |
| 适用场景 | 全栈 Web 应用、SaaS | Vue 生态全栈应用 | 数据密集型 Web 应用 | 博客、文档、营销站 |
# 使用 npx(推荐) npx create-next-app@latest my-app # 使用 yarn yarn create next-app my-app # 使用 pnpm pnpm create next-app my-app # 使用 bun bun create next-app my-app创建时会提示选择 TypeScript、ESLint、Tailwind CSS、App Router 等选项,建议全部选择"Yes"。
my-app/ ├── app/ # App Router 路由目录 │ ├── layout.tsx # 根布局(必需) │ ├── page.tsx # 首页 / │ ├── globals.css # 全局样式 │ ├── about/ │ │ └── page.tsx # /about 页面 │ └── api/ │ └── hello/ │ └── route.ts # API 路由 /api/hello ├── public/ # 静态资源 ├── next.config.js # Next.js 配置 ├── package.json ├── tsconfig.json └── tailwind.config.ts
cd my-app npm run dev访问
http://localhost:3000 即可看到 Next.js 的欢迎页面。开发服务器支持热模块替换(HMR),修改代码后浏览器自动刷新。
# 开发模式 npm run dev # 生产构建 npm run build # 启动生产服务器 npm run start # 代码检查 npm run lint
基于文件系统的路由,支持布局嵌套、加载状态、错误边界、并行路由、拦截路由等高级模式,是 Next.js 15 的默认路由方案。
组件默认在服务端运行,零客户端 JavaScript 开销。需要交互时使用 'use client' 指令切换为客户端组件。
同一项目中可混合使用服务端渲染、静态生成和增量静态再生,按页面粒度选择最优渲染策略。
在 app/api/ 目录下创建 route.ts 文件即可定义 RESTful API,无需额外后端服务器。
在请求到达页面之前执行逻辑,适用于认证、重定向、国际化、A/B 测试等场景。运行在 Edge Runtime 上,响应极快。
自动图片优化(next/image)、字体优化(next/font)、脚本优化(next/script)、代码分割、预取链接等,开箱即用提升 Core Web Vitals。
'use client':
// app/page.tsx — Server Component(默认)
// 可以直接 async、直接访问数据库、无需 useEffect
export default async function Home() {
const data = await fetch('https://api.example.com/posts')
const posts = await data.json()
return (
<main>
<h1>博客文章</h1>
{posts.map(post => (
<article key={post.id}>{post.title}</article>
))}
<LikeButton /> {/* 客户端组件 */}
</main>
)
}
// components/LikeButton.tsx — Client Component
'use client'
import { useState } from 'react'
export function LikeButton() {
const [likes, setLikes] = useState(0)
return <button onClick={() => setLikes(likes + 1)}>点赞 {likes}</button>
}
'use client'。Next.js App Router 基于文件系统自动生成路由,app/ 目录中的文件夹结构即为 URL 路径结构。每个路由段对应一个文件夹,page.tsx 使其可公开访问。
app/
├── page.tsx # / 路由
├── layout.tsx # 根布局(包裹所有页面)
├── loading.tsx # 加载中 UI(Suspense 边界)
├── error.tsx # 错误 UI(Error Boundary)
├── not-found.tsx # 404 页面
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/:slug(动态路由)
└── (marketing)/ # 路由组(不影响 URL)
├── pricing/
│ └── page.tsx # /pricing
└── contact/
└── page.tsx # /contact
// app/blog/[slug]/page.tsx — 动态路由
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPost(slug)
return <article>{post.content}</article>
}
// 静态生成动态路由页面
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map((post) => ({ slug: post.slug }))
}
动态路由类型:
[slug] — 匹配单个段:/blog/hello-world
[...slug] — 匹配多个段:/docs/a/b/c
[[...slug]] — 可选多段匹配:/docs 或 /docs/a/b
layout.tsx 在导航时保持状态不重新渲染,适合导航栏、侧边栏等持久 UI:
// app/layout.tsx — 根布局
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body>
<nav>全站导航栏</nav>
{children}
<footer>全站页脚</footer>
</body>
</html>
)
}
// app/dashboard/layout.tsx — 嵌套布局
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="flex">
<aside>仪表盘侧边栏</aside>
<main>{children}</main>
</div>
)
}
template.tsx 替代 layout.tsx。loading.tsx 和 error.tsx 文件自动创建 Suspense 和 Error Boundary:
// app/dashboard/loading.tsx
export default function Loading() {
return <div className="skeleton">加载中...</div>
}
// app/dashboard/error.tsx
'use client' // Error 组件必须是 Client Component
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>出错了!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>重试</button>
</div>
)
}
Next.js App Router 中推荐直接在 Server Component 中使用 async/await 获取数据,并通过 fetch 选项控制缓存和重新验证策略。
async/await,无需 useEffect 或客户端状态管理:
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // ISR:每小时重新验证
})
if (!res.ok) throw new Error('获取文章失败')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<a href={`/posts/${post.id}`}>{post.title}</a>
</li>
))}
</ul>
)
}
// 1. 静态数据(构建时获取,永久缓存)— 默认行为
const data = await fetch('https://api.example.com/data')
// 2. 定时重新验证(ISR)
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 } // 每 60 秒重新验证
})
// 3. 动态数据(每次请求都获取)
const data = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
// 4. 基于标签的按需重新验证
import { revalidateTag } from 'next/cache'
const data = await fetch('https://api.example.com/data', {
next: { tags: ['posts'] }
})
// 在 Server Action 中按需触发
revalidateTag('posts')
fetch 默认不缓存(cache: 'no-store')。如需缓存需显式指定 cache: 'force-cache' 或使用 next: { revalidate }。// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
await db.post.create({ data: { title, content } })
revalidatePath('/posts')
}
// app/posts/new/page.tsx
import { createPost } from '@/app/actions'
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" placeholder="标题" required />
<textarea name="content" placeholder="内容" required />
<button type="submit">发布</button>
</form>
)
}
# 安装 Vercel CLI npm i -g vercel # 一键部署 vercel # 或直接在 Vercel Dashboard 连接 GitHub 仓库 # 每次 push 自动触发部署Vercel 自动支持 Edge Functions、ISR、Image Optimization、Analytics 等 Next.js 特性。免费版已足够个人项目使用。
# Dockerfile FROM node:20-alpine AS base FROM base AS deps WORKDIR /app COPY package*.json ./ RUN npm ci --only=production FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build FROM base AS runner WORKDIR /app ENV NODE_ENV=production COPY --from=builder /app/public ./public COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static EXPOSE 3000 CMD ["node", "server.js"]需要在
next.config.js 中启用 standalone 输出:
# 构建并导出 npm run build # 静态文件在 out/ 目录(或自定义的 dist/)
以下是 Next.js 开发中最常见的 50 个问题与解决方案,覆盖构建错误、运行时问题、部署故障等。
Date.now()、Math.random() 等非确定性值 → 改用 useEffect 在客户端设置
suppressHydrationWarning 属性
<p> 标签内嵌套了 <div> 等非法 HTML → 修复嵌套结构
typeof window !== 'undefined' 条件渲染 → 改为 useEffect + state
// 错误做法
function Component() {
return <p>{typeof window !== 'undefined' ? 'client' : 'server'}</p>
}
// 正确做法
'use client'
function Component() {
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
return <p>{mounted ? 'client' : 'server'}</p>
}
useState、useEffect、onClick 等客户端 API。
'use client' 指令。
'use client' // 必须是文件的第一行(注释除外)
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
'use server')
suppressHydrationWarning 或在客户端格式化日期:
<time suppressHydrationWarning>
{new Date().toLocaleDateString()}
</time>
@types/react 版本过低,不支持 async Server Component。
tsconfig.json 中调整配置:
npm install @types/react@latest @types/react-dom@latest typescript@latest或临时使用类型断言:
{/* @ts-expect-error Async Server Component */}
useSearchParams() 在没有 Suspense 边界的情况下会使整个页面降级为客户端渲染。
Suspense 包裹使用 useSearchParams 的组件:
import { Suspense } from 'react'
function SearchBar() {
return (
<Suspense fallback={<div>加载中...</div>}>
<SearchBarContent />
</Suspense>
)
}
server-only 的模块(例如直接使用数据库客户端)。
server-only 包保护服务端代码:
// lib/db.ts
import 'server-only'
import { prisma } from './prisma'
export async function getUser(id: string) {
return prisma.user.findUnique({ where: { id } })
}
window、document、localStorage)。
useEffect 内访问
import dynamic from 'next/dynamic'
const MapComponent = dynamic(() => import('./Map'), {
ssr: false,
loading: () => <p>地图加载中...</p>
})
error.tsx 文件作为错误边界
'use client'
export { Button } from 'antd' // 重新导出为客户端组件
dynamic(() => import('antd').then(mod => mod.Button), { ssr: false })
npm run build)调用了本地 API Route,但此时开发服务器未运行。
// 错误:Server Component 中 fetch 自己的 API
const res = await fetch('http://localhost:3000/api/posts')
// 正确:直接调用业务函数
import { getPosts } from '@/lib/posts'
const posts = await getPosts()
@/ 路径别名未正确配置。确保 tsconfig.json 中有正确的路径映射:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}
create-next-app 默认会配置好,手动项目需自行添加。
next.config.js 中添加 eslint: { ignoreDuringBuilds: true }
typescript: { ignoreBuildErrors: true }(不推荐)
@types/react 和 @types/node 版本是否匹配
@next/bundle-analyzer 分析包内容
dynamic() 懒加载大组件
import { Button } from 'antd' 而非 import antd from 'antd'
npm install @next/bundle-analyzer # 在 next.config.js 中配置后运行: ANALYZE=true npm run build
experimental.appDir 在 Next.js 14+ 中已不需要(App Router 为默认)。
// next.config.js(Next.js 15 常用配置)
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: '**.example.com' },
],
},
}
module.exports = nextConfig
next dev --turbo=false 回退到 Webpack
node_modules 导入 CSS Modules。
transpilePackages 配置转译该包:
// next.config.js
module.exports = {
transpilePackages: ['some-package'],
}
layout.tsx 或 globals.css 中)
generateStaticParams 生成的页面数量过多,或数据获取过慢导致超时。
staticPageGenerationTimeout: 120
generateStaticParams 返回的页面数量,其余使用动态生成
dynamicParams = true 允许按需生成
# 确保 React 版本匹配 npm ls react react-dom # 清理重装 rm -rf node_modules .next package-lock.json npm install # Next.js 15 需要 React 19 npm install react@latest react-dom@latest next@latest
page.tsx(不是 index.tsx)。
app/about/index.tsx → 应改为 app/about/page.tsx
app/about/About.tsx → 应改为 app/about/page.tsx
(group) 中定义了相同路径的 page.tsx。
page.tsx 文件。路由组用圆括号 () 包裹,不会体现在 URL 中,但同一路径不能重复定义。
useRouter:
// App Router — 使用 next/navigation
import { useRouter } from 'next/navigation'
// Pages Router(旧版)— 使用 next/router
import { useRouter } from 'next/router'
如果在 App Router 项目中导入了 next/router,就会出现此错误。
params 变为异步 Promise,需要 await:
// Next.js 15 正确写法
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
// ...
}
// Next.js 14 旧写法(不再适用于 15)
export default function Page({ params }: { params: { slug: string } }) {
const { slug } = params
}
middleware.ts 必须放在项目根目录(与 app/ 同级),不能放在 app/ 内部。
my-app/ ├── middleware.ts ✅ 正确位置 ├── app/ │ ├── middleware.ts ❌ 不会生效 │ └── page.tsx同时确保
matcher 配置正确匹配了目标路由。
@folder 语法定义插槽,必须在同级 layout.tsx 中接收:
// app/layout.tsx
export default function Layout({
children,
modal, // 对应 app/@modal/page.tsx
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<>
{children}
{modal}
</>
)
}
并且需要提供 default.tsx 作为未匹配时的回退内容。
Link 组件而非原生 <a> 标签:
// 正确 — 客户端导航 import Link from 'next/link' <Link href="/about">关于我们</Link> // 错误 — 完整页面刷新 <a href="/about">关于我们</a>另外检查是否有 Middleware 进行了重定向(重定向会触发完整导航)。
(.)、(..)、(...) 语法,文件夹命名必须精确:
(.)folder — 拦截同级路由
(..)folder — 拦截上一级路由
(..)(..)folder — 拦截上两级路由
(...)folder — 从根路径拦截
page.tsx,且硬刷新(F5)会显示原始页面(拦截只在客户端导航时生效)。
redirect() 函数产生了无限重定向循环。
matcher 是否排除了重定向目标路径
// middleware.ts — 正确的 matcher
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico|login).*)',
],
}
generateStaticParams 返回空数组意味着没有预生成任何页面。确保函数正确返回参数数组,且 dynamicParams 设置为 true(默认)以允许按需生成:
// 确保返回正确的参数格式
export async function generateStaticParams() {
const posts = await db.post.findMany()
return posts.map((post) => ({
slug: post.slug, // 键名必须与文件夹名 [slug] 匹配
}))
}
// 允许按需生成未预渲染的页面
export const dynamicParams = true
force-cache 则会缓存。
// 禁用缓存(每次获取最新数据)
const res = await fetch(url, { cache: 'no-store' })
// 定时重新验证
const res = await fetch(url, { next: { revalidate: 60 } })
// 使用标签按需重新验证
const res = await fetch(url, { next: { tags: ['posts'] } })
// 在 Server Action 中:revalidateTag('posts')
'use server'
export async function getUser(id: string) {
const user = await db.user.findUnique({ where: { id } })
return {
...user,
createdAt: user.createdAt.toISOString(), // Date → string
}
}
cookies()、headers()、searchParams 等动态函数会使页面无法静态生成。
Suspense 包裹动态部分,让其余内容静态渲染
revalidatePath 只能在 Server Action 或 Route Handler 中调用。确保:
revalidatePath('/blog', 'layout') 可重新验证整个布局下的页面
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
// app/api/data/route.ts
export const dynamic = 'force-dynamic'
export async function GET() {
const data = await getData()
return Response.json(data)
}
// 错误:串行(慢) const user = await getUser(id) const posts = await getPosts(id) const comments = await getComments(id) // 正确:并行(快) const [user, posts, comments] = await Promise.all([ getUser(id), getPosts(id), getComments(id), ])
unstable_cache(Next.js 14)/ use cache(Next.js 15)需要正确的缓存键和标签:
import { unstable_cache } from 'next/cache'
const getCachedPosts = unstable_cache(
async () => {
return db.post.findMany()
},
['posts'], // 缓存键
{ revalidate: 3600, tags: ['posts'] } // 选项
)
const posts = await getCachedPosts()
确保缓存键是唯一的且不包含动态值(除非作为参数传入)。
generateStaticParams 预生成页面数量,使用 dynamicParams 按需生成
NEXT_PUBLIC_ 前缀开头的环境变量才会暴露给客户端:
# .env.local DATABASE_URL=xxx # 仅服务端可用 NEXT_PUBLIC_API_URL=xxx # 客户端和服务端都可用
NEXT_PUBLIC_ 变量中!它们会被打包到客户端 JavaScript 中。next/image 优化需要安装 sharp:
npm install sharp # Docker Alpine 镜像中可能需要额外依赖 RUN apk add --no-cache libc6-compat或在
next.config.js 中禁用图片优化:
module.exports = {
images: { unoptimized: true }
}
output: 'standalone' 不会自动复制 public/ 和 .next/static/ 目录。需要手动复制:
# Dockerfile 中 COPY --from=builder /app/public ./public COPY --from=builder /app/.next/static ./.next/static或使用 CDN 托管静态资源并设置
assetPrefix。
cacheHandler 自定义缓存后端(如 Redis):
// next.config.js
module.exports = {
cacheHandler: require.resolve('./cache-handler.js'),
cacheMaxMemorySize: 0, // 禁用内存缓存
}
// app/api/data/route.ts
export async function GET() {
const data = await getData()
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
})
}
export async function OPTIONS() {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
})
}
export const runtime = 'nodejs' 切换为 Node.js 运行时(放弃 Edge 优势)
next.config.js 中配置允许的域名:
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
pathname: '/images/**',
},
{
protocol: 'https',
hostname: '**.amazonaws.com',
},
],
},
}
next dev --turbo(Next.js 15 默认启用)
tsconfig.json 中调整 skipLibCheck: true
app/layout.tsx 中导入的全局组件数量
index.ts 重新导出大量模块)
<Image priority /> 预加载
next/font 优化字体加载,避免 FOIT/FOUT
<Script strategy="lazyOnload" /> 延迟非关键脚本
NODE_OPTIONS='--max-old-space-size=4096'
--inspect 标志和 Chrome DevTools 进行内存分析
next.config.js 中的 i18n 配置(那是 Pages Router 的功能)。
[locale] 动态路由段:app/[locale]/page.tsx
next-intl 等社区库
// middleware.ts
import createMiddleware from 'next-intl/middleware'
export default createMiddleware({
locales: ['zh', 'en', 'ja'],
defaultLocale: 'zh',
})
export const config = {
matcher: ['/((?!api|_next|.*\\..*).*)'],
}