Next.js 全栈开发完全指南:从零入门到生产实战

0 阅读14分钟

基于 Next.js v16.2.6 官方文档与源码,系统梳理核心概念、最佳实践与实战示例


目录

  1. 什么是 Next.js?
  2. 安装与初始化
  3. 项目结构详解
  4. 文件路由系统:Layouts 与 Pages
  5. 导航与链接
  6. Server Components vs Client Components
  7. 数据获取
  8. 数据变更:Server Functions
  9. 缓存与重新验证
  10. 错误处理
  11. 样式方案
  12. 图片与字体优化
  13. SEO 与元数据
  14. Route Handlers(API 接口)
  15. 认证与授权
  16. 环境变量
  17. 部署与自托管
  18. 测试
  19. 实战示例:从简单到高级
  20. 综合知识地图
  21. 常见问题与最佳实践
  22. 总结

一、什么是 Next.js?{#what-is-nextjs}

Next.js 是基于 React 的全栈 Web 应用框架,由 Vercel 开源维护,目前在 GitHub 上拥有 139k Star。它扩展了 React 的最新特性,并集成了基于 Rust 的高性能 JavaScript 工具链,实现最快的构建速度。

┌──────────────────────────────────────────────────────────┐
│                    Next.js 能力全景                       │
├────────────────┬─────────────────────────────────────────┤
│  前端能力       │  React 组件、CSR、SSR、SSG、ISR、PPR    │
│  路由系统       │  文件系统路由、动态路由、嵌套布局        │
│  API 能力       │  Route Handlers、Server Functions       │
│  数据层         │  数据获取、缓存、重验证、流式渲染         │
│  优化           │  图片、字体、脚本、包体积、SEO           │
│  部署           │  Node.js、Docker、静态导出、适配器       │
└────────────────┴─────────────────────────────────────────┘

App Router vs Pages Router

Next.js 提供两套路由系统:

┌─────────────────────────────────────────────────────────────┐
│                    两种路由器对比                             │
├─────────────────────┬───────────────────────────────────────┤
│   App Router(推荐) │   Pages Router(遗留)                │
├─────────────────────┼───────────────────────────────────────┤
│ 目录:/app          │ 目录:/pages                          │
│ 默认 Server Components│ 默认 Client Components              │
│ 支持流式渲染 Streaming│ 有限流式支持                         │
│ 嵌套布局            │ 单层布局                               │
│ React 19 / Canary   │ 使用 package.json 里的 React 版本     │
│ 更多新特性           │ 稳定、向后兼容                         │
└─────────────────────┴───────────────────────────────────────┘

本文全程基于 App Router,这也是官方推荐的方式。


二、安装与初始化{#installation}

系统要求

  • Node.js ≥ 20.9
  • 操作系统:macOS、Windows(含 WSL)、Linux

快速开始(推荐)

# 一键创建并启动(使用默认配置,跳过所有提示)
pnpm create next-app@latest my-app --yes
cd my-app
pnpm dev

访问 http://localhost:3000 即可看到欢迎页。

默认配置包含:TypeScript、Tailwind CSS、ESLint、App Router、Turbopack(新一代打包器)

自定义创建

pnpm create next-app

会交互式询问:

  • TypeScript / JavaScript
  • ESLint / Biome / 无
  • 是否使用 React Compiler
  • 是否使用 Tailwind CSS
  • 是否使用 src/ 目录
  • 是否使用 App Router
  • 自定义 import 别名(默认 @/*

手动安装

pnpm add next@latest react@latest react-dom@latest

package.json 添加脚本:

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  }
}

创建最基础的两个文件:

// app/layout.tsx — 根布局(必须)
export default function RootLayout({
  children,
}: { children: React.ReactNode }) {
  return (
    <html lang="zh">
      <body>{children}</body>
    </html>
  )
}

// app/page.tsx — 首页
export default function Page() {
  return <h1>Hello, Next.js!</h1>
}

三、项目结构详解{#project-structure}

my-app/
├── app/                  ← App Router 核心目录
│   ├── layout.tsx        ← 根布局(必须)
│   ├── page.tsx          ← 首页 /
│   ├── globals.css       ← 全局样式
│   ├── favicon.ico       ← 网站图标
│   ├── blog/
│   │   ├── layout.tsx    ← /blog 共享布局
│   │   ├── page.tsx      ← /blog 页面
│   │   └── [slug]/
│   │       └── page.tsx  ← /blog/:slug 动态页面
│   └── api/
│       └── route.ts      ← API 路由
├── public/               ← 静态资源(图片、字体等)
├── components/           ← 共享组件(惯例命名)
├── lib/                  ← 工具函数、数据库操作
├── next.config.ts        ← Next.js 配置
├── tsconfig.json
├── tailwind.config.ts
└── package.json

特殊文件约定

┌────────────────────────────────────────────────────────────┐
│                   路由特殊文件一览                           │
├─────────────────────┬──────────────────────────────────────┤
│  文件名              │  作用                                │
├─────────────────────┼──────────────────────────────────────┤
│  layout.tsx         │  共享布局(导航栏、侧边栏等)          │
│  page.tsx           │  页面内容(公开路由入口)              │
│  loading.tsx        │  加载中 UI(Suspense 骨架屏)          │
│  error.tsx          │  错误边界 UI                          │
│  not-found.tsx      │  404 页面                             │
│  route.ts           │  API 端点(GET/POST/PUT/DELETE)       │
│  template.tsx       │  每次导航都重新挂载的布局              │
│  default.tsx        │  并行路由降级页面                      │
└─────────────────────┴──────────────────────────────────────┘

组件渲染层次结构

layout.tsx
  └── template.tsx
        └── error.tsx (ErrorBoundary)
              └── loading.tsx (Suspense)
                    └── not-found.tsx (ErrorBoundary)
                          └── page.tsx

路由组织策略

1. 私有文件夹(不纳入路由):以 _ 开头

app/
├── _components/     ← 私有,不可路由
├── _utils/          ← 私有,不可路由
└── page.tsx

2. 路由组(不影响 URL):用 () 包裹

app/
├── (marketing)/
│   ├── about/page.tsx    ← URL: /about
│   └── layout.tsx
├── (shop)/
│   ├── products/page.tsx ← URL: /products
│   └── layout.tsx
└── page.tsx              ← URL: /

这样可以为不同区域设置不同的布局,而 URL 保持简洁。


四、文件路由系统:Layouts 与 Pages{#layouts-and-pages}

路由与文件夹的对应关系

app/
├── page.tsx            → /
├── about/
│   └── page.tsx        → /about
├── blog/
│   ├── page.tsx        → /blog
│   └── [slug]/
│       └── page.tsx    → /blog/:slug  (动态路由)
└── shop/
    └── [...slug]/
        └── page.tsx    → /shop/a/b/c  (捕获所有路由)

布局嵌套

布局是页面间共享的 UI,导航时保持状态、不重新渲染

app/layout.tsx(根布局)
  └── app/blog/layout.tsx(Blog 区域布局)
        └── app/blog/[slug]/page.tsx(具体页面)
// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: { children: React.ReactNode }) {
  return (
    <div>
      <nav>博客导航栏</nav>
      <main>{children}</main>
    </div>
  )
}

动态路由

// app/blog/[slug]/page.tsx
export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPost(slug)
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

搜索参数

// app/products/page.tsx
export default async function ProductsPage({
  searchParams,
}: {
  searchParams: Promise<{ page?: string; category?: string }>
}) {
  const { page = '1', category } = await searchParams
  const products = await getProducts({ page: Number(page), category })
  return <ProductList products={products} />
}

TypeScript 路由辅助类型

Next.js 提供全局可用的 PagePropsLayoutProps,无需手动导入:

// app/blog/[slug]/page.tsx
export default async function Page(props: PageProps<'/blog/[slug]'>) {
  const { slug } = await props.params
  return <h1>博客文章:{slug}</h1>
}

五、导航与链接{#linking-and-navigating}

Next.js 导航工作原理

用户点击链接
      │
      ▼
┌─────────────────────────────────────────────────┐
│              客户端导航流程                       │
│                                                  │
│  1. 检查预取缓存(viewport 进入时已预取)          │
│     ├── 命中缓存 → 立即展示(近似瞬间)            │
│     └── 未命中  → 发起服务器请求                  │
│                                                  │
│  2. 保留共享布局(不重新渲染)                    │
│                                                  │
│  3. 展示 loading.tsx 骨架屏(若有)               │
│                                                  │
│  4. 服务器返回新页面内容(RSC Payload)            │
│                                                  │
│  5. 替换页面内容,滚动到顶部                      │
└─────────────────────────────────────────────────┘

<Link> 组件

import Link from 'next/link'

// 基础用法
<Link href="/blog">博客</Link>

// 动态路由
<Link href={`/blog/${post.slug}`}>{post.title}</Link>

// 禁用预取(大列表场景节省资源)
<Link prefetch={false} href="/blog">博客</Link>

编程式导航

'use client'
import { useRouter } from 'next/navigation'

export default function LoginButton() {
  const router = useRouter()
  
  const handleLogin = async () => {
    await login()
    router.push('/dashboard')      // 跳转
    // router.replace('/dashboard') // 替换历史记录
    // router.back()                // 后退
    // router.refresh()             // 刷新当前页
  }
  
  return <button onClick={handleLogin}>登录</button>
}

慢网络下的加载指示器

// app/ui/loading-indicator.tsx
'use client'
import { useLinkStatus } from 'next/link'

export default function LoadingIndicator() {
  const { pending } = useLinkStatus()
  return (
    <span
      style={{
        display: pending ? 'inline-block' : 'none',
        width: 16,
        height: 16,
        border: '2px solid #ccc',
        borderTopColor: '#333',
        borderRadius: '50%',
        animation: 'spin 0.6s linear infinite',
      }}
    />
  )
}

六、Server Components vs Client Components{#server-and-client-components}

这是 Next.js App Router 最核心、最需要理解的概念。

概念图解

┌────────────────────────────────────────────────────────────────┐
│                    组件运行环境对比                              │
├──────────────────────────┬─────────────────────────────────────┤
│    Server Components      │    Client Components               │
│    (默认,无需标记)       │    (需加 'use client' 指令)       │
├──────────────────────────┼─────────────────────────────────────┤
│  ✅ 直接访问数据库         │  ✅ useState、useEffect 等 Hooks   │
│  ✅ 使用 API Keys(保密)  │  ✅ 事件处理(onClick 等)          │
│  ✅ 减少客户端 JS 体积     │  ✅ 浏览器 API(localStorage 等)  │
│  ✅ 在服务器运行,更快      │  ✅ 自定义 Hooks                   │
│  ❌ 无法使用 Hooks         │  ❌ 无法直接访问数据库              │
│  ❌ 无法处理交互事件        │  ❌ 无法使用服务器专属 API          │
└──────────────────────────┴─────────────────────────────────────┘

如何选择?

需要交互(点击、输入、状态)?
    是 → Client Component ('use client')
    否 →
        需要数据库/API/密钥?
            是 → Server Component(默认)
            否 → 均可,优先 Server Component

RSC 渲染流程

服务器
  │
  ├─ 1. 渲染 Server Components → RSC Payload(二进制格式)
  │
  ├─ 2. 预渲染 HTML(含 Client Component 占位符)
  │
  └─ 发送给客户端
           │
客户端      │
  ├─ 3. 展示 HTML(快速非交互预览)
  │
  ├─ 4. 下载 JS,水合(Hydration)Client Components
  │
  └─ 5. 应用可交互

基础示例

// app/page.tsx — Server Component(默认)
import LikeButton from '@/components/like-button'
import { getPost } from '@/lib/data'

export default async function Page({
  params,
}: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const post = await getPost(id)  // 服务器端直接读数据库

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      {/* Server Component 传 props 给 Client Component */}
      <LikeButton likes={post.likes} />
    </div>
  )
}

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

export default function LikeButton({ likes }: { likes: number }) {
  const [count, setCount] = useState(likes)
  return (
    <button onClick={() => setCount(c => c + 1)}>
      ❤️ {count}
    </button>
  )
}

将 Server Components 嵌入 Client Components(高级)

// ✅ 正确:通过 children prop 传入
// components/modal.tsx(Client Component)
'use client'
import { useState } from 'react'

export default function Modal({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(false)
  return (
    <>
      <button onClick={() => setOpen(true)}>打开</button>
      {open && <div className="modal">{children}</div>}
    </>
  )
}

// app/page.tsx(Server Component)
import Modal from '@/components/modal'
import Cart from '@/components/cart'  // 也是 Server Component

export default function Page() {
  return (
    <Modal>
      <Cart />  {/* Server Component 作为 children 传入 */}
    </Modal>
  )
}

Context Provider 的处理

// components/theme-provider.tsx
'use client'
import { createContext, useContext } from 'react'

const ThemeContext = createContext<string>('light')

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  return (
    <ThemeContext.Provider value="dark">
      {children}
    </ThemeContext.Provider>
  )
}

export const useTheme = () => useContext(ThemeContext)

// app/layout.tsx(Server Component 可以渲染 Client Provider)
import { ThemeProvider } from '@/components/theme-provider'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}

防止服务器代码泄漏到客户端

pnpm add server-only
// lib/db.ts
import 'server-only'  // 如果被客户端导入,构建时报错

export async function getSecretData() {
  return db.query('SELECT * FROM secrets')
}

七、数据获取{#fetching-data}

Server Components 中获取数据

方式 1:fetch API

// app/blog/page.tsx
export default async function BlogPage() {
  const res = await fetch('https://api.example.com/posts')
  const posts = await res.json()

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

方式 2:ORM/数据库

// app/users/page.tsx
import { db } from '@/lib/db'

export default async function UsersPage() {
  const users = await db.query('SELECT * FROM users')
  return <UserList users={users} />
}

流式渲染(Streaming)

不使用 Streaming:
页面 ───────────────────────────────────────► 展示(全部加载完)
      ←─── 数据A ─── 数据B ─── 数据C ───►

使用 Streaming:
HTML 骨架 ─►   Suspense 占位符立即展示
数据A 就绪 ─► 替换占位符A
数据B 就绪 ─► 替换占位符B
数据C 就绪 ─► 替换占位符C

方式 1:loading.tsx(整页流式)

// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="space-y-4">
      {[1,2,3].map(i => (
        <div key={i} className="h-20 bg-gray-200 animate-pulse rounded" />
      ))}
    </div>
  )
}

方式 2:<Suspense>(细粒度流式)

// app/blog/page.tsx
import { Suspense } from 'react'
import BlogList from '@/components/blog-list'

export default function BlogPage() {
  return (
    <div>
      {/* 立即展示(静态内容) */}
      <header>
        <h1>我的博客</h1>
        <p>分享技术与生活</p>
      </header>

      {/* 流式加载(动态内容) */}
      <Suspense fallback={<BlogSkeleton />}>
        <BlogList />
      </Suspense>
    </div>
  )
}

并行数据获取(提升性能)

// ❌ 串行(慢):后一个等前一个
const artist = await getArtist(username)  // 1秒
const albums = await getAlbums(username)  // 再等 1秒
// 总计 2秒

// ✅ 并行(快):同时发起
const [artist, albums] = await Promise.all([
  getArtist(username),   // 同时发起
  getAlbums(username),   // 同时发起
])
// 总计 ~1秒(取最长的那个)

Client Components 中获取数据

使用 SWR(推荐用于客户端数据)

'use client'
import useSWR from 'swr'

const fetcher = (url: string) => fetch(url).then(r => r.json())

export default function UserProfile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher)
  
  if (isLoading) return <div>加载中...</div>
  if (error) return <div>加载失败</div>
  
  return <div>欢迎,{data.name}</div>
}

八、数据变更:Server Functions{#mutating-data}

什么是 Server Function?

Server Function 是运行在服务器上的异步函数,可从客户端发起调用。当用于表单提交时,又称 Server Action

客户端(浏览器)                    服务器
       │                              │
       │ ── POST 请求 ──────────────► │  Server Function 执行
       │                              │  (访问数据库、发送邮件等)
       │ ◄── 返回新 UI 或数据 ──────── │
       │                              │

创建 Server Function

// app/actions.ts
'use server'

import { auth } from '@/lib/auth'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
  // 1. 验证身份(必须!)
  const session = await auth()
  if (!session?.user) {
    throw new Error('未授权')
  }

  // 2. 获取表单数据
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  // 3. 写入数据库
  await db.post.create({ data: { title, content, userId: session.user.id } })

  // 4. 刷新缓存 + 跳转
  revalidatePath('/posts')
  redirect('/posts')
}

在表单中使用

// app/new-post/page.tsx
import { createPost } from '@/app/actions'

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="标题" required />
      <textarea name="content" placeholder="内容" required />
      <button type="submit">发布</button>
    </form>
  )
}

显示提交状态(useActionState)

'use client'
import { useActionState } from 'react'
import { createPost } from '@/app/actions'

const initialState = { message: '' }

export default function PostForm() {
  const [state, formAction, pending] = useActionState(createPost, initialState)

  return (
    <form action={formAction}>
      <input name="title" placeholder="标题" required />
      <textarea name="content" placeholder="内容" required />
      {state?.message && (
        <p className="text-red-500">{state.message}</p>
      )}
      <button disabled={pending}>
        {pending ? '发布中...' : '发布'}
      </button>
    </form>
  )
}

在事件处理器中调用

'use client'
import { incrementLike } from './actions'
import { useState } from 'react'

export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes)

  return (
    <button onClick={async () => {
      const updated = await incrementLike()
      setLikes(updated)
    }}>
      ❤️ {likes}
    </button>
  )
}

九、缓存与重新验证{#caching-and-revalidating}

Next.js 的缓存策略是提升性能的关键。开启 Cache Components(推荐)后,使用 use cache 指令管理缓存。

启用 Cache Components

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig

缓存层级图

┌─────────────────────────────────────────────────────────────┐
│                    缓存决策流程                               │
│                                                              │
│  组件/函数                                                   │
│      │                                                       │
│      ├── 有 'use cache' → 缓存结果,纳入静态 Shell           │
│      │                                                       │
│      ├── 包在 <Suspense> 中 → 请求时流式渲染                 │
│      │                                                       │
│      └── 纯确定性操作 → 自动纳入静态 Shell                   │
└─────────────────────────────────────────────────────────────┘

use cache 指令

// 函数级缓存(数据层)
import { cacheLife, cacheTag } from 'next/cache'

export async function getProducts() {
  'use cache'
  cacheLife('hours')   // 1小时刷新
  cacheTag('products') // 标签,便于手动失效

  return db.query('SELECT * FROM products')
}

// 组件级缓存(UI层)
export default async function BlogPosts() {
  'use cache'
  cacheLife('hours')

  const posts = await fetch('https://api.example.com/posts').then(r => r.json())
  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  )
}

缓存生命周期配置

┌────────────┬──────────┬────────────┬──────────────┐
│  配置名    │  stale   │ revalidate │   expire     │
├────────────┼──────────┼────────────┼──────────────┤
│ seconds    │  01s       │  60s         │
│ minutes    │  5m      │   1m       │  1h          │
│ hours      │  5m      │   1h       │  1d          │
│ days       │  5m      │   1d       │  1w          │
│ weeks      │  5m      │   1w       │  30d         │
│ max        │  5m      │   30d      │  ~永久       │
└────────────┴──────────┴────────────┴──────────────┘

手动失效(On-Demand Revalidation)

// app/actions.ts
'use server'
import { revalidateTag, updateTag } from 'next/cache'

// 后台刷新(用户看到旧数据,后台重新生成)
export async function updateProduct(id: string, data: any) {
  await db.product.update({ where: { id }, data })
  revalidateTag('products')  // stale-while-revalidate
}

// 立即失效(用于"读己写"场景,用户立刻看到最新数据)
export async function createPost(formData: FormData) {
  const post = await db.post.create({ data: { /* ... */ } })
  updateTag('posts')  // 立即使缓存失效(仅 Server Action 可用)
  redirect(`/posts/${post.id}`)
}

静态内容 + 动态内容 + 流式内容同页

PPR(Partial Prerendering)架构图:

┌──────────────────────────────────────────────────────┐
│              页面(静态 Shell)                        │
│  ┌───────────────────────────────┐                   │
│  │  导航栏(静态)               │                   │
│  └───────────────────────────────┘                   │
│  ┌───────────────────────────────┐                   │
│  │  博客列表(cached,use cache) │← 缓存1小时        │
│  └───────────────────────────────┘                   │
│  ┌───────────────────────────────┐                   │
│  │  用户偏好(动态,流式渲染)    │← 请求时流入        │
│  │  ┌──────────────────────────┐ │                   │
│  │  │  loading 骨架屏 → 实际内容│ │                   │
│  │  └──────────────────────────┘ │                   │
│  └───────────────────────────────┘                   │
└──────────────────────────────────────────────────────┘

十、错误处理{#error-handling}

预期错误(用返回值处理)

// app/actions.ts(Server Function 中的预期错误)
'use server'

export async function createPost(prevState: any, formData: FormData) {
  const title = formData.get('title') as string
  
  if (!title || title.length < 3) {
    return { message: '标题至少需要3个字符' }  // 返回错误,不抛出
  }

  const res = await fetch('https://api.example.com/posts', {
    method: 'POST',
    body: JSON.stringify({ title }),
  })

  if (!res.ok) {
    return { message: '发布失败,请重试' }
  }
}
'use client'
import { useActionState } from 'react'
import { createPost } from '@/app/actions'

export function PostForm() {
  const [state, formAction, pending] = useActionState(createPost, { message: '' })

  return (
    <form action={formAction}>
      <input name="title" />
      {state?.message && <p className="text-red-500">{state.message}</p>}
      <button disabled={pending}>发布</button>
    </form>
  )
}

404 处理

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

export default async function BlogPost({
  params,
}: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await getPost(slug)

  if (!post) {
    notFound()  // 展示 not-found.tsx
  }

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

// app/blog/[slug]/not-found.tsx
export default function NotFound() {
  return (
    <div>
      <h2>文章不存在</h2>
      <p>您访问的文章已被删除或不存在</p>
    </div>
  )
}

错误边界(非预期错误)

// app/dashboard/error.tsx
'use client'

export default function ErrorPage({
  error,
  unstable_retry,
}: {
  error: Error & { digest?: string }
  unstable_retry: () => void
}) {
  return (
    <div>
      <h2>出错了!</h2>
      <p>{error.message}</p>
      <button onClick={() => unstable_retry()}>
        重试
      </button>
    </div>
  )
}
错误边界层级(从细到粗):

app/dashboard/error.tsx     ← 处理 dashboard 段错误
app/error.tsx               ← 处理 app 层错误
app/global-error.tsx        ← 处理根布局错误(需含 html/body)

十一、样式方案{#css}

Tailwind CSS(推荐)

pnpm add -D tailwindcss @tailwindcss/postcss
// postcss.config.mjs
export default {
  plugins: { '@tailwindcss/postcss': {} },
}
/* app/globals.css */
@import 'tailwindcss';
// 在组件中使用
export default function Hero() {
  return (
    <div className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-blue-50 to-white">
      <h1 className="text-5xl font-bold text-gray-900">
        欢迎使用 Next.js
      </h1>
      <p className="mt-4 text-xl text-gray-600">
        构建现代化全栈 Web 应用
      </p>
    </div>
  )
}

CSS Modules(作用域样式)

/* app/blog/blog.module.css */
.article {
  max-width: 720px;
  margin: 0 auto;
  padding: 2rem;
}

.title {
  font-size: 2rem;
  color: #1a1a1a;
}
import styles from './blog.module.css'

export default function BlogPost() {
  return (
    <article className={styles.article}>
      <h1 className={styles.title}>文章标题</h1>
    </article>
  )
}

十二、图片与字体优化{#images-and-fonts}

<Image> 组件

传统 <img> 问题                    next/image 解决方案
├── 布局抖动(CLS)         →  自动预留空间,消除 CLS
├── 不必要的大图            →  按设备自动缩放
├── 慢加载                  →  懒加载 + 现代格式(WebP/AVIF)
└── 无模糊占位符            →  blur placeholder 平滑过渡

本地图片(自动获取尺寸):

import Image from 'next/image'
import profilePic from './profile.png'  // 静态导入,自动宽高

export default function Avatar() {
  return (
    <Image
      src={profilePic}
      alt="用户头像"
      placeholder="blur"  // 加载时显示模糊占位
    />
  )
}

远程图片(需配置白名单):

// app/page.tsx
import Image from 'next/image'

export default function Page() {
  return (
    <Image
      src="https://s3.amazonaws.com/my-bucket/photo.jpg"
      alt="图片描述"
      width={800}
      height={600}
      priority  // 首屏图片加 priority关闭懒加载
    />
  )
}
// next.config.ts — 配置远程图片白名单
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 's3.amazonaws.com',
        pathname: '/my-bucket/**',
      },
    ],
  },
}

填充父容器(响应式图片):

<div className="relative h-64 w-full">
  <Image
    src="/banner.jpg"
    alt="横幅"
    fill
    className="object-cover"
  />
</div>

字体优化

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

// 英文字体
const inter = Inter({ subsets: ['latin'] })

// 中文字体(注意:中文字体较大,建议指定 subset)
const notoSansSC = Noto_Sans_SC({
  weight: ['400', '700'],
  subsets: ['chinese-simplified'],
})

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

本地字体

import localFont from 'next/font/local'

const myFont = localFont({
  src: [
    { path: './fonts/MyFont-Regular.woff2', weight: '400' },
    { path: './fonts/MyFont-Bold.woff2', weight: '700' },
  ],
  variable: '--font-my',
})

字体文件会在构建时自动下载并托管到同域,用户浏览器不会向 Google Fonts 发送任何请求,提升隐私与性能。


十三、SEO 与元数据{#metadata}

静态元数据

// app/blog/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: '技术博客 | MyApp',
  description: '分享前端开发、全栈技术与实战经验',
  keywords: ['Next.js', 'React', 'TypeScript'],
  authors: [{ name: '张三' }],
  openGraph: {
    title: '技术博客',
    description: '分享前端开发经验',
    images: ['/og-image.jpg'],
    type: 'website',
  },
  twitter: {
    card: 'summary_large_image',
    title: '技术博客',
  },
}

动态元数据(依赖数据)

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
import { cache } from 'react'

// 用 cache 包裹,同一请求内避免重复查询
const getPost = cache(async (slug: string) => {
  return db.post.findUnique({ where: { slug } })
})

export async function generateMetadata({
  params,
}: { params: Promise<{ slug: string }> }): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)

  return {
    title: `${post?.title} | 技术博客`,
    description: post?.summary,
    openGraph: {
      images: [post?.coverImage ?? '/default-og.jpg'],
    },
  }
}

export default async function Page({
  params,
}: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await getPost(slug)  // 命中缓存,不重复请求
  return <article>{post?.content}</article>
}

生成 OG 图片

// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
import { getPost } from '@/lib/data'

export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'

export default async function OGImage({
  params,
}: { params: { slug: string } }) {
  const post = await getPost(params.slug)

  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          justifyContent: 'center',
          padding: '80px',
          background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
          width: '100%',
          height: '100%',
        }}
      >
        <p style={{ color: 'rgba(255,255,255,0.8)', fontSize: 28 }}>
          技术博客
        </p>
        <h1 style={{ color: 'white', fontSize: 72, fontWeight: 'bold' }}>
          {post?.title}
        </h1>
      </div>
    )
  )
}

站点地图与 robots.txt

// app/sitemap.ts
import { MetadataRoute } from 'next'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getPosts()

  return [
    { url: 'https://example.com', changeFrequency: 'daily', priority: 1 },
    ...posts.map(post => ({
      url: `https://example.com/blog/${post.slug}`,
      lastModified: post.updatedAt,
      changeFrequency: 'weekly' as const,
      priority: 0.8,
    })),
  ]
}

// app/robots.ts
import { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: { userAgent: '*', allow: '/', disallow: '/admin/' },
    sitemap: 'https://example.com/sitemap.xml',
  }
}

十四、Route Handlers(API 接口){#route-handlers}

Route Handlers 是 App Router 中的 API 端点,等价于 Pages Router 的 API Routes。

基础 CRUD 示例

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

// GET /api/posts
export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const page = Number(searchParams.get('page') ?? 1)

  const posts = await db.post.findMany({
    skip: (page - 1) * 10,
    take: 10,
    orderBy: { createdAt: 'desc' },
  })

  return Response.json({ posts, page })
}

// POST /api/posts
export async function POST(request: NextRequest) {
  const body = await request.json()
  const { title, content } = body

  if (!title) {
    return Response.json({ error: '标题不能为空' }, { status: 400 })
  }

  const post = await db.post.create({
    data: { title, content },
  })

  return Response.json(post, { status: 201 })
}
// app/api/posts/[id]/route.ts
import type { NextRequest } from 'next/server'

// PUT /api/posts/:id
export async function PUT(
  request: NextRequest,
  ctx: RouteContext<'/api/posts/[id]'>
) {
  const { id } = await ctx.params
  const body = await request.json()

  const post = await db.post.update({
    where: { id },
    data: body,
  })

  return Response.json(post)
}

// DELETE /api/posts/:id
export async function DELETE(
  _req: NextRequest,
  ctx: RouteContext<'/api/posts/[id]'>
) {
  const { id } = await ctx.params
  await db.post.delete({ where: { id } })
  return new Response(null, { status: 204 })
}

静态缓存的 Route Handler

// app/api/config/route.ts
export const dynamic = 'force-static'  // 构建时预渲染

export async function GET() {
  return Response.json({
    version: '1.0.0',
    features: ['blog', 'comments', 'search'],
  })
}

设置响应 Headers

export async function GET() {
  const data = await fetchData()

  return new Response(JSON.stringify(data), {
    status: 200,
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'public, s-maxage=60',
    },
  })
}

十五、认证与授权{#authentication}

认证是大多数应用必备的功能,下图展示了完整的认证流程:

┌──────────────────────────────────────────────────────────────┐
│                    Next.js 认证流程全景                        │
│                                                              │
│  用户访问受保护路由                                           │
│       │                                                      │
│       ▼                                                      │
│  ┌─────────────┐   检查 session cookie                       │
│  │   Proxy      │──────────────────────────────────────────► │
│  │  (proxy.ts) │                                             │
│  └─────────────┘                                             │
│       │ 无效/过期                                             │
│       ▼                                                      │
│  重定向到 /login                                              │
│       │                                                      │
│       ▼                                                      │
│  ┌───────────────────────────────────────────────────────┐   │
│  │           登录页面                                     │   │
│  │  1. 用户填写账号密码                                   │   │
│  │  2. Server Action 验证                                 │   │
│  │  3. 创建 JWT Session,写入 httpOnly Cookie              │   │
│  │  4. 重定向到 Dashboard                                 │   │
│  └───────────────────────────────────────────────────────┘   │
│                                                              │
│  Dashboard 中的每个数据请求 → DAL.verifySession() → 验证权限  │
└──────────────────────────────────────────────────────────────┘

推荐认证库

对于生产环境,强烈推荐使用成熟的认证库而非自己实现,可选:Auth.js(NextAuth)、Clerk、Auth0、Better Auth

Proxy 路由保护

// proxy.ts(项目根目录)
import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/lib/session'
import { cookies } from 'next/headers'

const protectedRoutes = ['/dashboard', '/profile', '/settings']
const publicRoutes = ['/login', '/signup', '/']

export default async function proxy(req: NextRequest) {
  const path = req.nextUrl.pathname
  const isProtectedRoute = protectedRoutes.some(r => path.startsWith(r))
  const isPublicRoute = publicRoutes.includes(path)

  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)

  // 未登录访问受保护页面 → 跳转登录
  if (isProtectedRoute && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }

  // 已登录访问登录页 → 跳转 Dashboard
  if (isPublicRoute && session?.userId && path === '/login') {
    return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

数据访问层(DAL)

// lib/dal.ts — 集中管理授权逻辑
import 'server-only'
import { cache } from 'react'
import { cookies } from 'next/headers'
import { decrypt } from './session'
import { redirect } from 'next/navigation'

export const verifySession = cache(async () => {
  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)

  if (!session?.userId) {
    redirect('/login')
  }

  return { isAuth: true, userId: session.userId as string }
})

export const getUser = cache(async () => {
  const { userId } = await verifySession()

  const user = await db.user.findUnique({
    where: { id: userId },
    select: { id: true, name: true, email: true, role: true },
    // 注意:永远不要返回 password 字段
  })

  return user
})

Server Action 中的授权检查

// app/actions.ts
'use server'
import { verifySession } from '@/lib/dal'

export async function deletePost(postId: string) {
  const session = await verifySession()  // 未认证会自动 redirect

  // 额外权限检查:只能删除自己的文章
  const post = await db.post.findUnique({ where: { id: postId } })
  if (post?.userId !== session.userId) {
    throw new Error('无权删除此文章')
  }

  await db.post.delete({ where: { id: postId } })
}

十六、环境变量{#environment-variables}

文件优先级(从高到低)

process.env
  └── .env.$(NODE_ENV).local    (如 .env.development.local)
  └── .env.local                (test 环境不加载)
  └── .env.$(NODE_ENV)          (如 .env.production)
  └── .env

服务端 vs 客户端变量

# .env.local
# 服务端专用(不暴露给浏览器)
DATABASE_URL=postgres://localhost/mydb
SECRET_API_KEY=sk-xxxxx

# 客户端可用(NEXT_PUBLIC_ 前缀,构建时内联到 JS bundle)
NEXT_PUBLIC_APP_URL=https://myapp.com
NEXT_PUBLIC_ANALYTICS_ID=G-XXXXXXXX
// 服务端组件(安全)
export default async function Page() {
  const db = await connect(process.env.DATABASE_URL)  // ✅ 服务端可用
  // ...
}

// 客户端组件
'use client'
export default function Analytics() {
  // ✅ NEXT_PUBLIC_ 前缀的变量可用
  const id = process.env.NEXT_PUBLIC_ANALYTICS_ID
  // ❌ 非 NEXT_PUBLIC_ 在客户端为 undefined
  const secret = process.env.SECRET_API_KEY  // undefined!
}

运行时环境变量(动态读取)

// 使用 connection() 确保在请求时读取,不在构建时固化
import { connection } from 'next/server'

export default async function Component() {
  await connection()
  const value = process.env.MY_RUNTIME_CONFIG  // 每次请求读取
  return <div>{value}</div>
}

十七、部署与自托管{#deployment}

部署方式对比

┌───────────────────┬────────────────┬───────────────────────────┐
│   部署方式         │  支持特性       │  适用场景                 │
├───────────────────┼────────────────┼───────────────────────────┤
│ Vercel(官方)     │  全功能        │  最省心,推荐中小团队     │
│ Node.js Server    │  全功能        │  自有服务器/VPS           │
│ Docker 容器        │  全功能        │  K8s/云原生部署           │
│ 静态导出           │  有限(无 SSR) │  纯静态站点、CDN 托管     │
│ 适配器            │  因平台而异     │  Cloudflare、Netlify 等   │
└───────────────────┴────────────────┴───────────────────────────┘

Node.js 部署

npm run build
npm run start   # 监听 3000 端口

Docker 部署(推荐生产)

# Dockerfile
FROM node:20-alpine AS base

# 安装依赖
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# 构建
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/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

EXPOSE 3000
CMD ["node", "server.js"]
// next.config.ts — 启用 standalone 输出
const nextConfig = {
  output: 'standalone',
}

静态导出

// next.config.ts
const nextConfig = {
  output: 'export',  // 生成纯静态文件
}
npm run build  # 生成 /out 目录
# 部署到 Nginx、S3、GitHub Pages 等任意静态托管

多实例部署注意事项

# 多实例需要统一 Server Action 加密密钥
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=<base64-encoded-32-byte-key>

# 版本漂移保护
DEPLOYMENT_VERSION=v1.2.3
// next.config.ts
const nextConfig = {
  deploymentId: process.env.DEPLOYMENT_VERSION,
  // 共享缓存配置
  cacheHandler: require.resolve('./cache-handler.js'),
  cacheMaxMemorySize: 0,
}

十八、测试{#testing}

测试类型选择

┌──────────────────┬──────────────────────────────────────────────────┐
│  测试类型         │  工具推荐          │  适用范围                  │
├──────────────────┼────────────────────┼────────────────────────────┤
│ 单元测试          │  Jest / Vitest    │  函数、Hooks、工具函数      │
│ 组件测试          │  Jest + RTL       │  React 组件渲染与交互      │
│ 端到端测试        │  Playwright / Cypress │  完整用户流程           │
│ 快照测试          │  Jest            │  UI 回归检测               │
└──────────────────┴────────────────────┴────────────────────────────┘

注意:async Server Components 目前主流测试工具支持有限,推荐用 E2E 测试覆盖。

Vitest 单元测试示例

pnpm add -D vitest @vitejs/plugin-react
// lib/utils.test.ts
import { describe, it, expect } from 'vitest'
import { formatDate, slugify } from './utils'

describe('formatDate', () => {
  it('应该正确格式化日期', () => {
    expect(formatDate(new Date('2024-01-15'))).toBe('2024年01月15日')
  })
})

describe('slugify', () => {
  it('应该将标题转为 slug', () => {
    expect(slugify('Hello World')).toBe('hello-world')
  })
})

Playwright E2E 测试示例

pnpm add -D @playwright/test
// tests/auth.spec.ts
import { test, expect } from '@playwright/test'

test('用户可以正常登录', async ({ page }) => {
  await page.goto('/login')

  await page.fill('input[name="email"]', 'test@example.com')
  await page.fill('input[name="password"]', 'password123')
  await page.click('button[type="submit"]')

  await expect(page).toHaveURL('/dashboard')
  await expect(page.locator('h1')).toContainText('欢迎')
})

test('未登录用户被重定向到登录页', async ({ page }) => {
  await page.goto('/dashboard')
  await expect(page).toHaveURL('/login')
})

十九、实战示例:从简单到高级{#examples}

🟢 简单示例:静态博客首页

// app/page.tsx
import Link from 'next/link'

const posts = [
  { slug: 'nextjs-intro', title: 'Next.js 入门指南', date: '2024-01-01' },
  { slug: 'react-hooks', title: 'React Hooks 深入解析', date: '2024-01-15' },
]

export default function HomePage() {
  return (
    <main className="max-w-2xl mx-auto py-16 px-4">
      <h1 className="text-4xl font-bold mb-8">我的博客</h1>
      <ul className="space-y-4">
        {posts.map(post => (
          <li key={post.slug} className="border rounded-lg p-4 hover:bg-gray-50">
            <Link href={`/blog/${post.slug}`}>
              <h2 className="text-xl font-semibold text-blue-600">{post.title}</h2>
              <p className="text-gray-500 text-sm mt-1">{post.date}</p>
            </Link>
          </li>
        ))}
      </ul>
    </main>
  )
}

🟡 进阶示例:带缓存的动态博客列表

// app/blog/page.tsx
import { Suspense } from 'react'
import { cacheLife } from 'next/cache'
import Link from 'next/link'

async function BlogList() {
  'use cache'
  cacheLife('hours')

  const res = await fetch('https://api.vercel.app/blog')
  const posts: { id: number; title: string; slug: string }[] = await res.json()

  return (
    <ul className="space-y-4">
      {posts.map(post => (
        <li key={post.id} className="border rounded-lg p-4">
          <Link href={`/blog/${post.slug}`} className="text-blue-600 hover:underline">
            {post.title}
          </Link>
        </li>
      ))}
    </ul>
  )
}

function BlogSkeleton() {
  return (
    <ul className="space-y-4">
      {Array.from({ length: 5 }).map((_, i) => (
        <li key={i} className="border rounded-lg p-4 animate-pulse">
          <div className="h-5 bg-gray-200 rounded w-3/4" />
        </li>
      ))}
    </ul>
  )
}

export default function BlogPage() {
  return (
    <main className="max-w-2xl mx-auto py-16 px-4">
      <h1 className="text-4xl font-bold mb-8">博客文章</h1>
      <Suspense fallback={<BlogSkeleton />}>
        <BlogList />
      </Suspense>
    </main>
  )
}

🟡 进阶示例:全功能 Todo 应用(Server Actions + 乐观更新)

// app/todos/page.tsx
import { addTodo, deleteTodo, toggleTodo } from './actions'
import { getTodos } from '@/lib/data'

export default async function TodoPage() {
  const todos = await getTodos()

  return (
    <main className="max-w-md mx-auto py-16 px-4">
      <h1 className="text-3xl font-bold mb-6">待办事项</h1>

      {/* 添加表单 */}
      <form action={addTodo} className="flex gap-2 mb-6">
        <input
          name="title"
          placeholder="新建待办..."
          className="flex-1 border rounded px-3 py-2"
          required
        />
        <button
          type="submit"
          className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
        >
          添加
        </button>
      </form>

      {/* 列表 */}
      <ul className="space-y-2">
        {todos.map(todo => (
          <li key={todo.id} className="flex items-center gap-3 p-3 border rounded">
            <form action={toggleTodo}>
              <input type="hidden" name="id" value={todo.id} />
              <button type="submit">
                <span className={todo.done ? 'text-green-500' : 'text-gray-400'}>
                  {todo.done ? '✅' : '⬜'}
                </span>
              </button>
            </form>
            <span className={`flex-1 ${todo.done ? 'line-through text-gray-400' : ''}`}>
              {todo.title}
            </span>
            <form action={deleteTodo}>
              <input type="hidden" name="id" value={todo.id} />
              <button type="submit" className="text-red-400 hover:text-red-600">
                🗑️
              </button>
            </form>
          </li>
        ))}
      </ul>
    </main>
  )
}
// app/todos/actions.ts
'use server'
import { revalidatePath } from 'next/cache'

export async function addTodo(formData: FormData) {
  const title = formData.get('title') as string
  await db.todo.create({ data: { title } })
  revalidatePath('/todos')
}

export async function toggleTodo(formData: FormData) {
  const id = formData.get('id') as string
  const todo = await db.todo.findUnique({ where: { id } })
  await db.todo.update({ where: { id }, data: { done: !todo?.done } })
  revalidatePath('/todos')
}

export async function deleteTodo(formData: FormData) {
  const id = formData.get('id') as string
  await db.todo.delete({ where: { id } })
  revalidatePath('/todos')
}

🔴 高级示例:电商产品页(PPR + 个性化推荐)

这个示例展示了如何在同一页面中混合使用静态内容、缓存内容和动态内容:

// app/products/[id]/page.tsx
import { Suspense } from 'react'
import { cacheLife, cacheTag } from 'next/cache'
import { cookies } from 'next/headers'
import Image from 'next/image'
import AddToCartButton from '@/components/add-to-cart-button'

// 产品数据(缓存1天,按产品ID标记)
async function ProductInfo({ id }: { id: string }) {
  'use cache'
  cacheLife('days')
  cacheTag(`product-${id}`)

  const product = await db.product.findUnique({ where: { id } })
  if (!product) return null

  return (
    <div className="grid md:grid-cols-2 gap-8">
      <div className="relative h-96">
        <Image
          src={product.imageUrl}
          alt={product.name}
          fill
          className="object-cover rounded-lg"
          priority
        />
      </div>
      <div>
        <h1 className="text-3xl font-bold">{product.name}</h1>
        <p className="text-2xl text-green-600 mt-2">¥{product.price}</p>
        <p className="text-gray-600 mt-4">{product.description}</p>
        <AddToCartButton productId={id} />
      </div>
    </div>
  )
}

// 个性化推荐(基于用户 Cookie,请求时流式渲染)
async function PersonalizedRecommendations({ productId }: { productId: string }) {
  const userId = (await cookies()).get('userId')?.value

  const recommended = await db.product.findMany({
    where: {
      id: { not: productId },
      ...(userId ? { viewedBy: { some: { userId } } } : {}),
    },
    take: 4,
  })

  return (
    <section>
      <h2 className="text-xl font-semibold mb-4">
        {userId ? '为你推荐' : '热门商品'}
      </h2>
      <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
        {recommended.map(product => (
          <a key={product.id} href={`/products/${product.id}`} className="border rounded p-3">
            <p className="font-medium text-sm">{product.name}</p>
            <p className="text-green-600">¥{product.price}</p>
          </a>
        ))}
      </div>
    </section>
  )
}

// 用户购物车状态(高度个性化,请求时渲染)
async function CartStatus() {
  const userId = (await cookies()).get('userId')?.value
  if (!userId) return null

  const cartCount = await db.cartItem.count({ where: { userId } })
  return (
    <div className="fixed top-4 right-4 bg-blue-500 text-white rounded-full px-3 py-1">
      购物车 ({cartCount})
    </div>
  )
}

export default function ProductPage({
  params,
}: { params: Promise<{ id: string }> }) {
  // 注意:这是一个非 async 的 Server Component 包装器
  // 它本身被预渲染为静态 Shell
  return (
    <main className="max-w-6xl mx-auto py-16 px-4">
      {/* 产品信息:缓存,纳入静态 Shell */}
      <Suspense fallback={<ProductSkeleton />}>
        <ProductInfoLoader params={params} />
      </Suspense>

      <div className="mt-16">
        {/* 个性化推荐:动态,请求时流式渲染 */}
        <Suspense fallback={<RecommendationSkeleton />}>
          <RecommendationsLoader params={params} />
        </Suspense>
      </div>

      {/* 购物车状态:高度动态 */}
      <Suspense>
        <CartStatus />
      </Suspense>
    </main>
  )
}

async function ProductInfoLoader({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  return <ProductInfo id={id} />
}

async function RecommendationsLoader({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  return <PersonalizedRecommendations productId={id} />
}

// 骨架屏组件
function ProductSkeleton() {
  return (
    <div className="grid md:grid-cols-2 gap-8 animate-pulse">
      <div className="h-96 bg-gray-200 rounded-lg" />
      <div className="space-y-4">
        <div className="h-8 bg-gray-200 rounded w-3/4" />
        <div className="h-6 bg-gray-200 rounded w-1/4" />
        <div className="h-32 bg-gray-200 rounded" />
      </div>
    </div>
  )
}

function RecommendationSkeleton() {
  return (
    <div className="grid grid-cols-4 gap-4 animate-pulse">
      {[1,2,3,4].map(i => (
        <div key={i} className="h-32 bg-gray-200 rounded" />
      ))}
    </div>
  )
}
// components/add-to-cart-button.tsx
'use client'
import { useState, useTransition } from 'react'
import { addToCart } from '@/app/actions'

export default function AddToCartButton({ productId }: { productId: string }) {
  const [isPending, startTransition] = useTransition()
  const [added, setAdded] = useState(false)

  const handleClick = () => {
    startTransition(async () => {
      await addToCart(productId)
      setAdded(true)
      setTimeout(() => setAdded(false), 2000)
    })
  }

  return (
    <button
      onClick={handleClick}
      disabled={isPending}
      className={`mt-6 w-full py-3 rounded-lg font-semibold transition-all ${
        added
          ? 'bg-green-500 text-white'
          : 'bg-blue-500 text-white hover:bg-blue-600'
      } disabled:opacity-50`}
    >
      {isPending ? '添加中...' : added ? '✅ 已添加到购物车' : '加入购物车'}
    </button>
  )
}

🔴 高级示例:多租户 SaaS 应用架构

多租户路由架构(基于子域名):

用户访问 tenant-a.myapp.com
         │
         ▼
     proxy.ts
         │ 读取子域名
         ▼
   解析 tenantId
         │
         ▼
   注入到 headers
         │
         ▼
  app/layout.tsx
   读取 tenantId
   加载租户配置
         │
         ▼
  渲染对应主题/数据
// proxy.ts(多租户路由)
import { NextRequest, NextResponse } from 'next/server'

export default function proxy(req: NextRequest) {
  const hostname = req.headers.get('host') ?? ''
  const subdomain = hostname.split('.')[0]

  // 排除主域名和特殊子域
  if (['www', 'app', 'api'].includes(subdomain)) {
    return NextResponse.next()
  }

  // 将 tenantId 注入 header,供下游 Server Component 使用
  const requestHeaders = new Headers(req.headers)
  requestHeaders.set('x-tenant-id', subdomain)

  return NextResponse.next({ request: { headers: requestHeaders } })
}

export const config = {
  matcher: ['/((?!_next|api|favicon.ico).*)'],
}
// app/layout.tsx(读取租户配置)
import { headers } from 'next/headers'
import { getTenantConfig } from '@/lib/tenants'

export default async function RootLayout({
  children,
}: { children: React.ReactNode }) {
  const tenantId = (await headers()).get('x-tenant-id') ?? 'default'
  const tenant = await getTenantConfig(tenantId)

  return (
    <html lang="zh">
      <body
        style={{
          '--primary-color': tenant.primaryColor,
          '--logo-url': `url(${tenant.logoUrl})`,
        } as React.CSSProperties}
      >
        <header className="border-b px-6 py-4">
          <div
            className="h-8 w-32 bg-contain bg-no-repeat"
            style={{ backgroundImage: `var(--logo-url)` }}
          />
          <h1 className="text-xl font-bold" style={{ color: 'var(--primary-color)' }}>
            {tenant.name}
          </h1>
        </header>
        <main className="px-6 py-8">{children}</main>
      </body>
    </html>
  )
}

二十、综合知识地图

通读全文后,用一张全景图来串联所有核心概念:

┌─────────────────────────────────────────────────────────────────────┐
│                      Next.js App Router 全景架构                     │
│                                                                     │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────────────┐   │
│  │  Browser │  │   CDN    │  │  Proxy   │  │  Next.js Server  │   │
│  │          │  │          │  │(proxy.ts)│  │                  │   │
│  │  React   │  │ 静态资源  │  │ 路由保护 │  │ ┌──────────────┐ │   │
│  │  Client  │◄─┤ 缓存页面 │◄─┤ 重定向   │◄─┤ │ App Router   │ │   │
│  │ 水合交互 │  │ HTML/JS  │  │ rewrite  │  │ │              │ │   │
│  └──────────┘  └──────────┘  └──────────┘  │ │ layout.tsx   │ │   │
│       ▲                                     │ │ page.tsx     │ │   │
│       │ RSC Payload                         │ │ loading.tsx  │ │   │
│       │ + HTML                              │ │ error.tsx    │ │   │
│       │                                     │ └──────────────┘ │   │
│       │                                     │                  │   │
│  ┌────┴──────────────────────────────────┐  │ ┌──────────────┐ │   │
│  │         渲染策略                       │  │ │  数据层      │ │   │
│  │                                       │  │ │              │ │   │
│  │  静态(SSG) ─── 构建时预渲染            │  │ │ fetch()      │ │   │
│  │  动态(SSR) ─── 请求时渲染             │  │ │ ORM/DB       │ │   │
│  │  ISR      ─── 定期重新验证            │  │ │ Server Fn    │ │   │
│  │  PPR      ─── 静态Shell+动态流        │  │ │ Route Handler│ │   │
│  └───────────────────────────────────────┘  │ └──────────────┘ │   │
│                                             │                  │   │
│  ┌───────────────────────────────────────┐  │ ┌──────────────┐ │   │
│  │         缓存层次                       │  │ │  缓存系统    │ │   │
│  │                                       │  │ │              │ │   │
│  │  React Cache ─── 单次请求去重         │  │ │ 'use cache'  │ │   │
│  │  use cache  ─── 跨请求持久化          │  │ │ cacheLife()  │ │   │
│  │  CDN Cache  ─── 全球边缘缓存          │  │ │ cacheTag()   │ │   │
│  │  浏览器缓存 ─── 资源本地缓存          │  │ │ revalidateTag│ │   │
│  └───────────────────────────────────────┘  │ └──────────────┘ │   │
│                                             └──────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

二十一、常见问题与最佳实践

Q1:什么时候用 Server Component,什么时候用 Client Component?

判断流程:

该组件需要...
├── useState / useEffect / 事件处理?
│   └── ✅ Client Component ('use client')
│
├── 直接读取数据库/API Key/密钥?
│   └── ✅ Server Component(默认)
│
├── 只是展示静态内容?
│   └── ✅ Server Component(减少 JS bundle)
│
└── 既有状态又有数据?
    └── 拆分:Server Component 获取数据 → props → Client Component 处理交互

Q2:revalidatePath vs revalidateTag vs updateTag

┌─────────────────┬──────────────────────────────────────────────┐
│  API             │  使用场景                                   │
├─────────────────┼──────────────────────────────────────────────┤
│ revalidatePath  │ 重验证某个路径的所有缓存(粗粒度)            │
│ revalidateTag   │ 重验证带有特定 tag 的缓存(细粒度,后台刷新) │
│ updateTag       │ 立即使缓存失效(读己写场景,仅 Server Action) │
└─────────────────┴──────────────────────────────────────────────┘

推荐优先级:updateTag > revalidateTag > revalidatePath

Q3:如何避免客户端 JS 包体积过大?

  1. 将静态 UI 保持为 Server Component,只对有交互的最小单元加 'use client'
  2. 使用 @next/bundle-analyzer 分析包体积
  3. 动态导入(懒加载) 非首屏组件:
import dynamic from 'next/dynamic'

// 仅在客户端加载(适合大型图表、编辑器等)
const RichEditor = dynamic(() => import('@/components/rich-editor'), {
  ssr: false,
  loading: () => <div>编辑器加载中...</div>,
})

Q4:数据获取应该放在哪一层?

推荐原则:在"最近的数据使用者"处获取

✅ 推荐:
  BlogPost 组件(Server Component)
  └── 直接 fetch 自己需要的 post 数据

❌ 不推荐(prop drilling):
  Page
  └── fetch 所有数据
  └── 传给 BlogPost → 传给 Comment → 传给 Author

Q5:如何处理表单验证?

// 服务端验证(Zod,必须做)
import * as z from 'zod'

const schema = z.object({
  email: z.email('请输入有效邮箱'),
  password: z.string().min(8, '密码至少8位'),
})

export async function login(prevState: any, formData: FormData) {
  'use server'
  const result = schema.safeParse({
    email: formData.get('email'),
    password: formData.get('password'),
  })

  if (!result.success) {
    return {
      errors: result.error.flatten().fieldErrors,
    }
  }

  // 执行登录逻辑...
}

二十二、Next.js 版本演进速查

v13  → App Router 引入(实验性)
v14  → App Router 稳定,Server Actions 正式支持
v15  → React 19 支持,Turbopack 默认,async cookies/headers
v16  → Cache Components(use cache),PPR 默认启用,proxy.ts 取代 middleware.ts

总结

Next.js 从一个简单的 SSR 框架,已经演进成为现代全栈 Web 开发的最佳选择之一。理解其核心理念的关键在于:

1. 服务器优先:默认在服务器渲染,只把必要的交互逻辑下沉到客户端,从而减少 JS 体积、提升 FCP。

2. 组合优于层级:Server Component 可以持有 Client Component(通过 props/children),这让我们可以精细地控制每一块 UI 的运行环境。

3. 缓存即架构use cachecacheLifecacheTag 构成了声明式的缓存体系——你描述"这些数据应该缓存多久",框架来决定何时从缓存读、何时重新生成。

4. 渐进增强:从静态导出到完整 SSR,从简单 fetch 到 PPR,Next.js 的能力是一个连续的谱系,可以按需取用。

希望这篇指南能帮助大家系统地建立起 Next.js 的知识体系。建议的学习路径是:先跑通安装 → 理解文件路由 → 掌握 Server/Client Component 边界 → 学会数据获取与缓存 → 再深入认证、部署等进阶主题。配合官方文档 nextjs.org/docs 与 GitHub 仓库 vercel/next.js,会对这套体系越来越得心应手。