Next.js 完全指南

Next.js 14+ 是基于 React 的全栈框架,App Router 是其核心架构,内置 React Server Components、流式渲染、Server Actions 等能力。 相关文档:React完全指南(/react-wan-quan-zhi-nan/) | TypeScript完全指南(/typescript-wan-quan-zhi-nan/) 默认情况下 app/ 下的所有组件都是 Server Components,在服务端渲染后发送 HTML 给客户端,不包含客户端 JavaScript bundle。 在文件顶部添加 '

分享

官方文档:https://nextjs.org/docs
适用版本:Next.js 15(2026-05-07 核实)

Next.js 14+ 是基于 React 的全栈框架,App Router 是其核心架构,内置 React Server Components、流式渲染、Server Actions 等能力。

相关文档:React完全指南 | TypeScript完全指南


核心概念

App Router vs Pages Router

维度 App Router(推荐) Pages Router(旧)
目录 app/ pages/
默认组件类型 Server Component Client Component
数据获取方式 async/await 直接在组件中使用 getServerSideProps / getStaticProps
布局 嵌套 layout.tsx 文件 _app.tsx 单一全局布局
流式渲染 原生支持(loading.tsx / Suspense 不支持
Server Actions 支持 不支持
Metadata API export const metadata / generateMetadata() <Head> 组件

Server Components vs Client Components

默认情况下 app/ 下的所有组件都是 Server Components,在服务端渲染后发送 HTML 给客户端,不包含客户端 JavaScript bundle。

在文件顶部添加 'use client' 声明变为 Client Component,拥有完整的 React 能力(状态、事件、Effect)。

特性 Server Component Client Component
声明方式 默认(无需声明) 文件顶部 'use client'
运行环境 服务端 客户端(也会服务端预渲染)
直接访问数据库/文件系统 可以 不可以
使用 useState / useEffect 不可以 可以
使用浏览器 API 不可以 可以
接收事件处理器(onClick 等) 不可以 可以
减少客户端 bundle
// app/page.tsx - 默认是 Server Component
async function HomePage() {
  // 可以直接访问数据库,无需 API 层
  const posts = await db.posts.findMany()
  return <PostList posts={posts} />
}

// components/Counter.tsx - Client Component
'use client'
import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

渲染策略

策略 触发条件 说明
Static 无动态数据获取 构建时生成,CDN 缓存,性能最优
Dynamic 使用 cookies()headers()searchParams,或 cache: 'no-store' 每次请求时在服务端渲染
Streaming 使用 Suspense 包裹组件 逐步发送 HTML,提升首屏感知速度

文件约定

app/ 目录文件约定

文件 说明
page.tsx 路由的 UI 页面(必须导出默认组件才能访问该路由)
layout.tsx 该路由及其子路由共享的布局,不因路由变化重新渲染
loading.tsx 基于 Suspense 的加载 UI,数据获取期间自动展示
error.tsx 错误边界,必须是 Client Component('use client'
not-found.tsx 404 页面,由 notFound() 函数触发或路由不存在时展示
route.ts API 端点(Route Handler),不能与同路径的 page.tsx 共存
template.tsx 类似 layout.tsx,但每次路由变化都重新挂载
default.tsx Parallel Routes 的默认内容
middleware.ts 只能放在项目根目录(与 app/ 同级)

动态路由

文件路径 匹配路由 params 类型
app/blog/[id]/page.tsx /blog/123 { id: string }
app/blog/[...slug]/page.tsx /blog/a/b/c { slug: string[] }
app/blog/...slug/page.tsx /blog/blog/a/b { slug?: string[] }
// app/blog/[id]/page.tsx
interface Props {
  params: { id: string }
  searchParams: { [key: string]: string | string[] | undefined }
}

export default async function BlogPost({ params, searchParams }: Props) {
  const post = await getPost(params.id)
  return <article>{post.content}</article>
}

Route Groups

用括号包裹目录名(如 (marketing)),该目录不会出现在 URL 中,仅用于组织文件结构。

app/
  (marketing)/
    about/page.tsx      -> /about
    layout.tsx          -> 仅用于 marketing 组
  (shop)/
    products/page.tsx   -> /products
    layout.tsx          -> 仅用于 shop 组

Parallel Routes 和 Intercepting Routes

Parallel Routes(并行路由):用 @slot 目录名定义,在同一个 layout.tsx 中同时渲染多个页面。

app/
  layout.tsx            -> 引用 @dashboard 和 @analytics
  @dashboard/
    page.tsx
  @analytics/
    page.tsx
  page.tsx
// app/layout.tsx
export default function Layout({
  children,
  dashboard,
  analytics,
}: {
  children: React.ReactNode
  dashboard: React.ReactNode
  analytics: React.ReactNode
}) {
  return (
    <div>
      {children}
      {dashboard}
      {analytics}
    </div>
  )
}

Intercepting Routes(拦截路由):在不离开当前页面的情况下展示另一个路由的内容(如在列表页弹出详情 Modal)。

约定 说明
(.)photo/[id] 拦截同级路由
(..)photo/[id] 拦截上一层路由
(...)photo/[id] 拦截根路由

数据获取

Server Component 直接 async/await

在 Server Component 中可以直接用 async/await 获取数据,无需任何 Hook:

// app/posts/page.tsx
async function PostsPage() {
  // 这段代码只在服务端运行,可以直接访问数据库
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())

  return (
    <ul>
      {posts.map((post: Post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

fetch 缓存选项

Next.js 扩展了原生 fetch,增加了缓存控制选项。

选项 行为
cache: 'force-cache' 默认值,强制缓存,等同于静态数据
cache: 'no-store' 不缓存,每次请求都重新获取(动态数据)
next: { revalidate: 60 } ISR 策略,60 秒后缓存失效,下次请求时重新生成
next: { tags: ['posts'] } 按标签缓存,配合 revalidateTag('posts') 按需清除
// 静态数据:构建时获取,长期缓存
const staticData = await fetch('/api/config', { cache: 'force-cache' })

// 动态数据:每次请求都获取最新
const dynamicData = await fetch('/api/user', { cache: 'no-store' })

// ISR:定时重新验证
const revalidatedData = await fetch('/api/posts', { next: { revalidate: 3600 } })

generateStaticParams

在动态路由中使用 generateStaticParams,在构建时预生成静态页面:

// app/blog/[id]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch('/api/posts').then(r => r.json())

  return posts.map((post: Post) => ({
    id: post.id.toString(),
  }))
}

// 访问未预生成的路径时的行为
export const dynamicParams = true  // 动态生成(默认)
// export const dynamicParams = false  // 返回 404

Server Actions

Server Actions 允许在客户端组件中直接调用服务端函数,主要用于表单提交和数据变更。

// app/actions.ts
'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  await db.posts.create({ data: { title } })
  revalidatePath('/posts')  // 清除缓存,触发页面重新生成
}

// app/posts/new/page.tsx - 可以是 Server Component
import { createPost } from '../actions'

export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button type="submit">发布</button>
    </form>
  )
}

在 Client Component 中使用 Server Actions:

'use client'
import { createPost } from '../actions'
import { useTransition } from 'react'

export function PostForm() {
  const [isPending, startTransition] = useTransition()

  function handleSubmit(formData: FormData) {
    startTransition(async () => {
      await createPost(formData)
    })
  }

  return (
    <form action={handleSubmit}>
      <input name="title" />
      <button disabled={isPending}>
        {isPending ? '发布中...' : '发布'}
      </button>
    </form>
  )
}

API Routes

Route Handler(app/api/xxx/route.ts)

// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const page = searchParams.get('page') ?? '1'

  const posts = await getPosts({ page: parseInt(page) })
  return NextResponse.json(posts)
}

export async function POST(request: NextRequest) {
  const body = await request.json()
  const post = await createPost(body)
  return NextResponse.json(post, { status: 201 })
}

动态路由 Route Handler:

// app/api/posts/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const post = await getPost(params.id)
  if (!post) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 })
  }
  return NextResponse.json(post)
}

NextRequest 常用属性

属性/方法 说明
request.url 完整 URL 字符串
request.nextUrl 扩展的 URL 对象(含 pathnamesearchParams 等)
request.nextUrl.searchParams URL 查询参数(URLSearchParams 实例)
request.method HTTP 方法
request.headers 请求头(Headers 实例)
request.cookies 请求 Cookie
request.json() 解析 JSON 请求体(异步)
request.text() 读取文本请求体(异步)
request.formData() 解析 FormData(异步)

NextResponse 常用方法

方法 说明
NextResponse.json(data, init?) 返回 JSON 响应
NextResponse.redirect(url, init?) 重定向
NextResponse.rewrite(url) 内部重写(URL 不变)
NextResponse.next() 中间件中继续处理请求
response.cookies.set(name, value, opts?) 在响应上设置 Cookie
response.cookies.delete(name) 删除 Cookie

中间件(middleware.ts)

中间件运行在所有路由之前,必须放在项目根目录(与 app/ 同级)。

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')

  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

// 配置匹配规则(不配置则匹配所有路由)
export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
}

性能优化

next/image

<Image> 组件自动进行图片优化:懒加载、格式转换(WebP/AVIF)、尺寸调整、避免 CLS。

属性 类型 必填 说明
src string | StaticImageData 图片路径或导入的静态图片
alt string 无障碍描述
width number 是(非 fill) 显示宽度(px)
height number 是(非 fill) 显示高度(px)
fill boolean 填充父容器(父容器需 position: relative
sizes string 响应式图片尺寸提示(类似 <img sizes>
priority boolean 预加载,用于 LCP 图片,禁用懒加载
quality number 压缩质量(1-100,默认 75)
placeholder 'blur' | 'empty' | 'data:...' 加载占位符
blurDataURL string placeholder="blur" 时的模糊预览 base64
loading 'lazy' | 'eager' 加载策略(默认 lazy
unoptimized boolean 跳过 Next.js 图片优化
style object 内联样式
className string CSS 类名
onLoad function 图片加载完成回调
onError function 图片加载失败回调
import Image from 'next/image'
import heroImg from '@/public/hero.jpg'

// 固定尺寸
<Image src={heroImg} alt="Hero" width={800} height={400} priority />

// 响应式填充
<div style={{ position: 'relative', height: '400px' }}>
  <Image
    src="/banner.jpg"
    alt="Banner"
    fill
    sizes="(max-width: 768px) 100vw, 50vw"
    style={{ objectFit: 'cover' }}
  />
</div>

next/font

自动优化字体加载,消除布局偏移,字体文件托管在本地。

// app/layout.tsx
import { Inter, Noto_Sans_SC } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
})

const notoSansSC = Noto_Sans_SC({
  subsets: ['chinese-simplified'],
  weight: ['400', '700'],
  variable: '--font-noto',
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh" className={`${inter.variable} ${notoSansSC.variable}`}>
      <body>{children}</body>
    </html>
  )
}

<Link> 组件在视口内自动预取页面资源,实现无感知导航。

属性 类型 默认值 说明
href string | UrlObject 目标路由(必填)
replace boolean false 替换 history 记录而非 push
scroll boolean true 导航后滚动到页面顶部
prefetch boolean | null null null 视口预取,true 强制预取,false 禁用
shallow boolean Pages Router 选项,App Router 不适用
import Link from 'next/link'

<Link href="/about">关于</Link>
<Link href={{ pathname: '/blog/[id]', query: { id: '123' } }}>文章</Link>
<Link href="/external" prefetch={false}>不预取此链接</Link>

最佳实践

Server / Client 组件边界设计

将 Client Component 尽量下移到叶节点,让大部分组件保持 Server Component:

app/page.tsx (Server)
  └── PostList (Server) - 获取数据、渲染列表
        └── PostCard (Server) - 展示卡片内容
              └── LikeButton (Client) - 只有这个需要交互

数据获取放在尽可能靠近使用的位置

// 推荐:在用到数据的 Server Component 内部直接获取
async function PostCard({ postId }: { postId: string }) {
  const post = await getPost(postId)  // 靠近使用位置
  return <div>{post.title}</div>
}

// 不推荐:在顶层获取所有数据再逐层传递(prop drilling)

踩坑与注意事项

Server / Client 边界错误

Server Component 不能导入 Client Component 中的浏览器 API,但 Client Component 可以导入 Server Component(作为 children props 传入)。

// 错误:Server Component 中使用 useState
// app/page.tsx(Server Component)
import { useState } from 'react'  // 报错:不能在 Server Component 使用

// 正确:只在有 'use client' 的文件中使用

Context Provider 必须是 Client Component,但可以包裹 Server Component:

// providers.tsx
'use client'
import { ThemeProvider } from './theme-context'

export function Providers({ children }: { children: React.ReactNode }) {
  return <ThemeProvider>{children}</ThemeProvider>
}

// layout.tsx(Server Component)
import { Providers } from './providers'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>  {/* children 仍是 Server Component */}
      </body>
    </html>
  )
}

cookies() 和 headers() 的使用限制

cookies()headers() 来自 next/headers,只能在以下位置使用:

可以使用 不能使用
Server Component Client Component
Route Handler(route.ts generateStaticParams
Server Action 静态渲染的布局/页面(使用后会强制转为动态渲染)
Middleware(使用 request.cookies / request.headers
// app/profile/page.tsx
import { cookies, headers } from 'next/headers'

export default async function ProfilePage() {
  // 使用 cookies() 会使这个页面变为动态渲染
  const cookieStore = cookies()
  const token = cookieStore.get('auth-token')

  const headersList = headers()
  const userAgent = headersList.get('user-agent')

  // ...
}

notFound() 必须在 Server Component 或 Route Handler 中调用

import { notFound } from 'next/navigation'

export default async function PostPage({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)

  if (!post) {
    notFound()  // 触发同级或最近父级的 not-found.tsx
  }

  return <article>{post.content}</article>
}

Streaming 与 error.tsx

error.tsx 必须是 Client Component(需要使用 useEffect 访问错误对象),且只能捕获其子树内的错误,不能捕获同级 layout.tsx 的错误。

// app/blog/error.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>
      <p>出错了</p>
      <button onClick={reset}>重试</button>
    </div>
  )
}

最佳实践

默认使用 Server Components,仅在需要时降级为 Client Components:Server Components 在服务端渲染,零 JS bundle,可直接访问数据库和文件系统。只有需要浏览器 API、事件处理、useState/useEffect 时才加 'use client'

// Server Component(默认,不需要声明)
async function UserList() {
  const users = await db.query('SELECT * FROM users')  // 直接访问数据库
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}

// Client Component(仅在需要交互时)
'use client'
function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

静态内容用 SSG,动态内容用 ISR,实时内容用 SSR:按数据更新频率选择渲染策略,避免把所有页面都做成 SSR。

// ISR:每 60 秒重新生成(适合博客、产品页)
export const revalidate = 60

// SSG:构建时生成,永不过期(适合文档、静态页)
export const revalidate = false

// SSR:每次请求重新渲染(适合个性化、实时数据)
export const dynamic = 'force-dynamic'

数据变更用 Server Actions,避免手动维护 API Route:Server Actions 可直接从 Client Component 调用服务端逻辑,无需创建 /api 路由,并自动处理 CSRF 保护。

// app/actions.ts
'use server'
export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  await db.insert({ title })
  revalidatePath('/posts')
}

// 在 Client Component 中直接调用
<form action={createPost}>
  <input name="title" />
  <button type="submit">发布</button>
</form>

使用 next/image 替代原生 <img>:自动优化图片格式(WebP/AVIF)、按需懒加载、防止 CLS(布局偏移)。

import Image from 'next/image'
// 正确:自动优化
<Image src="/hero.jpg" width={800} height={400} alt="hero" priority />

// 错误:跳过优化
<img src="/hero.jpg" />

路由分组用 (folder) 避免 URL 污染:括号命名的目录不影响 URL 路径,用于组织布局而不暴露目录结构。

app/
  (marketing)/         ← URL 中不出现 "marketing"
    layout.tsx         ← 只对 marketing 页面生效的布局
    page.tsx           ← 对应 /
  (dashboard)/
    layout.tsx
    analytics/page.tsx ← 对应 /analytics

常见陷阱

陷阱:在 Server Component 中使用浏览器 API

现象: 构建报错 "ReferenceError: window is not defined" 或 "localStorage is not defined"。

原因: Server Components 在 Node.js 中执行,没有浏览器环境,无法访问 windowdocumentlocalStorage 等。

解决: 将使用浏览器 API 的代码移到 Client Component(添加 'use client'),或使用动态导入加 ssr: false

// 动态导入,禁用 SSR
import dynamic from 'next/dynamic'
const BrowserOnlyChart = dynamic(() => import('./Chart'), { ssr: false })

陷阱:Hydration 错误(水合不匹配)

现象: 控制台报 "Hydration failed because the initial UI does not match what was rendered on the server",页面出现闪烁或内容重置。

原因: 服务端渲染的 HTML 与客户端 React 首次渲染的结果不一致,常见原因是在渲染时读取了只在客户端可用的值(如 Date.now()Math.random()localStorage)。

解决: 将依赖客户端状态的渲染延迟到 useEffect 后,或使用 suppressHydrationWarning(仅对已知安全的不一致使用)。

'use client'
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
if (!mounted) return null  // 服务端渲染返回 null,避免不一致
return <div>{localStorage.getItem('theme')}</div>

陷阱:fetch 缓存导致数据过期

现象: 更新了数据库数据,但页面显示的仍是旧内容,重启服务才恢复正常。

原因: Next.js 扩展了原生 fetch,默认对所有请求使用强缓存(cache: 'force-cache'),即构建后永久缓存。

解决: 显式设置缓存策略,或使用 revalidatePath / revalidateTag 手动清除缓存。

// 不缓存
fetch(url, { cache: 'no-store' })

// ISR:60 秒后重新验证
fetch(url, { next: { revalidate: 60 } })

// 带 tag 的缓存,可按需清除
fetch(url, { next: { tags: ['posts'] } })
// 在 Server Action 中清除
revalidateTag('posts')

参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog