Next.js 16 从入门到实战:一篇文章掌握全栈开发核心技术

本文基于一个真实的 Next.js 学习项目,涵盖路由系统、数据获取策略、API 路由、中间件、图片优化、国际化、性能监控、Server Actions、错误处理、SWC 编译器、Dynamic Import、SSE 流式通信、第三方 SDK 集成、高级路由(Interception/Parallel/Catch-all)、Preview Mode、Automatic Static Optimization、MDX 等核心知识点,适合新手循序渐进地学习。


目录

基础篇

进阶篇

高级篇


一、环境搭建

1.1 技术栈

技术版本用途
Next.js16.3.0全栈框架(默认 SWC 编译器)
React19.2.8UI 框架
TypeScript5.x类型安全
Tailwind CSS4.x原子化样式
next-intl4.x国际化
@next/mdx16.3.1MDX 支持(Markdown + JSX)

1.2 创建项目

# 使用 create-next-app 创建项目
npx create-next-app@latest my-next-app \
  --typescript \
  --tailwind \
  --eslint \
  --app \
  --src-dir \
  --import-alias "@/*"

# 安装 i18n 依赖
npm install next-intl

# 安装 MDX 依赖
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx

# 启动开发服务器
npm run dev

1.3 项目目录结构

src/
├── app/                              # App Router 目录
│   ├── layout.tsx                    # 根布局
│   ├── page.tsx                      # 首页
│   ├── loading.tsx                   # 全局 Loading
│   ├── error.tsx                     # 全局错误边界
│   ├── not-found.tsx                 # 404 页面
│   ├── globals.css                   # 全局样式
│   ├── mdx-components.tsx            # 全局 MDX 组件注册(必需)
│   │
│   ├── about/                        # /about
│   ├── login/                        # /login
│   ├── dashboard/                    # /dashboard(受认证保护)
│   │   ├── page.tsx
│   │   └── loading.tsx
│   │
│   ├── users/                        # /users, /users/[id]
│   │   ├── page.tsx
│   │   ├── loading.tsx
│   │   └── [id]/
│   │       ├── page.tsx
│   │       └── loading.tsx
│   │
│   ├── blog/                         # /blog, /blog/[slug]
│   │   ├── layout.tsx                # ⭐ 嵌套布局(侧边栏常驻)
│   │   ├── loading.tsx
│   │   ├── page.tsx
│   │   └── [slug]/
│   │       ├── page.tsx              # ISR: revalidate=60
│   │       └── loading.tsx
│   │
│   ├── performance/                  # 性能监控演示
│   ├── api-demo/                     # API 演示入口
│   │
│   ├── preview/                      # ⭐ Preview Mode 演示
│   │   ├── page.tsx
│   │   └── [slug]/
│   │       └── page.tsx
│   │
│   ├── compiler/                     # ⭐ SWC 编译器详解
│   │   ├── page.tsx
│   │   ├── CompilerOverview.tsx
│   │   ├── CompilerPipeline.tsx
│   │   ├── CompilerBuildDemo.tsx
│   │   ├── CompilerConfig.tsx
│   │   └── CompilerTransform.tsx
│   │
│   ├── dynamic-import/               # ⭐ Dynamic Import 演示
│   │   └── page.tsx
│   │
│   ├── static-optimization/          # ⭐ 静态优化对比
│   │   ├── page.tsx
│   │   ├── static/page.tsx           # 自动静态
│   │   ├── dynamic/page.tsx          # cookies() 强制动态
│   │   └── isr/page.tsx              # revalidate=10
│   │
│   ├── mdx/                          # ⭐ MDX 支持
│   │   ├── page.tsx                  # 入口
│   │   ├── demo/
│   │   │   ├── page.tsx              # 渲染 .mdx
│   │   │   └── content.mdx
│   │   └── guide/
│   │       ├── page.tsx
│   │       └── guide-content.mdx
│   │
│   ├── playground/                   # ⭐ 实验场
│   │   ├── page.tsx
│   │   ├── auth/                     # 认证演示
│   │   ├── router/                   # 路由演示
│   │   ├── middleware/               # 中间件演示
│   │   ├── breadcrumb/               # 面包屑演示
│   │   ├── sse-streaming/            # SSE 流式聊天
│   │   ├── map-demo/                 # 高德地图集成
│   │   ├── script-optimization/      # 脚本优化
│   │   └── interception/             # ⭐ 高级路由
│   │       ├── layout.tsx            # Parallel Routes 布局
│   │       ├── page.tsx
│   │       ├── photos/
│   │       │   ├── page.tsx          # 完整列表页
│   │       │   └── [id]/page.tsx     # 完整详情页
│   │       └── @modal/               # ⭐ Parallel Route 插槽
│   │           ├── default.tsx
│   │           └── (..)photos/[id]/page.tsx  # ⭐ 拦截路由
│   │
│   ├── actions/                      # Server Actions
│   │   └── auth.ts
│   │
│   └── api/                          # API Routes(详见第四章)
│       ├── auth/{login,logout,me}/route.ts
│       ├── chat/stream/route.ts      # SSE 流式 API
│       ├── users/[{id}]/route.ts     # 动态 API 路由
│       ├── hello/route.ts            # Hello World
│       ├── cors-demo/route.ts        # CORS 跨域演示
│       ├── response-helpers/route.ts # NextResponse 助手
│       ├── segment-config-demo/route.ts  # Segment Config
│       ├── files/[...slug]/route.ts  # Catch-all 路由
│       ├── middleware-demo/route.ts  # 中间件演示
│       ├── preview/{enter,exit}/route.ts  # 预览模式 API
│       └── segment-config-demo/route.ts
│
├── components/                       # 组件目录
│   ├── Header.tsx / Footer.tsx       # 布局组件
│   ├── Sidebar.tsx                   # ⭐ 中后台风格侧边栏
│   ├── LanguageSwitcher.tsx          # 语言切换
│   ├── WebVitalsMonitor.tsx          # 性能监控
│   ├── OptimizedAvatar.tsx           # 图片优化
│   ├── Breadcrumb.tsx                # ⭐ 面包屑导航
│   ├── Skeleton.tsx                  # ⭐ 加载骨架屏
│   ├── SSEChatDemo.tsx               # ⭐ SSE 流式聊天组件
│   ├── AMapLoader.tsx                # ⭐ 高德地图加载器
│   ├── MapDemo.tsx                   # 地图演示
│   ├── ChatWidget.tsx                # 客服聊天
│   ├── PreviewToggle.tsx             # ⭐ 预览模式切换
│   ├── AuthStatus.tsx                # 认证状态
│   ├── PhotoModalContent.tsx         # ⭐ 拦截路由弹窗
│   ├── Counter.tsx / Card.tsx        # 通用组件
│   ├── AdSense.tsx / AdUnit.tsx      # 广告组件
│   ├── Analytics.tsx / PageTracker.tsx  # GA 分析
│   ├── PerformanceTestClient.tsx     # 性能测试
│   ├── UserListClient.tsx            # 用户列表
│   ├── mdx-components.tsx            # MDX 自定义组件
│   └── demos/                        # Demo 组件
│       ├── HeavyChart.tsx            # Dynamic Import 重型组件
│       ├── ClientOnlyComponent.tsx   # ssr:false 演示
│       └── MarkdownViewer.tsx
│
├── i18n/                             # 国际化配置
│   ├── config.ts
│   ├── request.ts
│   └── messages/{zh,en}.json
│
├── lib/                              # 工具库
│   ├── auth.ts                       # 客户端认证
│   ├── auth-server.ts                # 服务端认证
│   ├── data.ts                       # 数据源
│   ├── cms-data.ts                   # ⭐ CMS 草稿数据(Preview Mode)
│   └── photos.ts                     # ⭐ 拦截路由共享数据
│
└── proxy.ts                          # 中间件(认证 + i18n)

二、路由系统

2.1 文件路由约定

Next.js 采用文件系统路由,每个文件夹对应一个 URL 路径:

app/
├── page.tsx              →  /
├── about/
│   └── page.tsx          →  /about
├── users/
│   ├── page.tsx          →  /users
│   └── [id]/
│       └── page.tsx      →  /users/1, /users/2 ...
├── blog/
│   ├── page.tsx          →  /blog
│   └── [slug]/
│       └── page.tsx      →  /blog/nextjs-tutorial
└── dashboard/
    └── page.tsx          →  /dashboard

2.2 动态路由

用方括号 [param] 表示动态参数,在组件中通过 useParams() 获取:

// app/users/[id]/page.tsx
export default async function UserPage({ params }: { params: Promise<{ id: string }> }) {
  // Next.js 16 中 params 是 Promise,需要 await
  const { id } = await params;
  const user = await getUser(parseInt(id));

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
}

2.3 嵌套路由与 Layout

每一级目录可以放一个 layout.tsx,它会包裹该目录下所有页面:

app/
├── layout.tsx          ← 根布局(Header + Footer)
├── users/
│   ├── layout.tsx      ← 用户列表专属布局(可选)
│   ├── page.tsx        ← 被 users/layout.tsx 包裹
│   └── [id]/
│       └── page.tsx    ← 也被 users/layout.tsx 包裹

2.4 路由文件种类

文件作用渲染时机
page.tsx页面内容URL 匹配时渲染
layout.tsx布局包裹该目录及子目录所有页面
loading.tsx加载骨架屏页面加载时自动显示
error.tsx错误边界子树抛出运行时错误
not-found.tsx404 页面路由不存在或调用 notFound()
default.tsxParallel Route 默认插槽没有匹配内容时
mdx-components.tsxMDX 全局组件注册启用 MDX 时必需

2.5 客户端导航 Hook(useRouter / usePathname / useParams / useSearchParams)

"use client";
import { useRouter, usePathname, useParams, useSearchParams } from "next/navigation";

export default function RouterDemo() {
  const router = useRouter();         // 命令式导航
  const pathname = usePathname();      // 当前路径
  const params = useParams();          // 动态参数
  const searchParams = useSearchParams();  // 查询参数

  return (
    <div>
      <p>当前路径: {pathname}</p>
      <button onClick={() => router.push("/users")}>跳转</button>
      <button onClick={() => router.back()}>返回</button>
    </div>
  );
}
Hook返回值典型场景
useRouter()router 对象命令式导航(push/replace/back)
usePathname()当前路径字符串导航高亮、面包屑
useParams()动态路由参数获取 [id]、[slug]
useSearchParams()URLSearchParams读取查询字符串

2.6 学习答疑:路由参数与跳转常见误区 ✍️

本节整理学习过程中容易混淆的几个点,避免以后踩坑。

🔍 疑惑 1:Link href="/blog/${post.slug}" 真的能匹配到 blog/[slug]/page.tsx 吗?

完全正确。 这就是 Next.js 文件系统路由的核心约定:

Link href="/blog/hello-world"
        ↓
匹配目录 src/app/blog/[slug]/
        ↓
动态段 [slug] 被赋值为 "hello-world"
        ↓
页面组件通过 params.slug 拿到 "hello-world"

只要你的目录名是 [slug](方括号),Link 里填什么 URL,就会把对应位置的内容塞进去。


🔍 疑惑 2:两种参数——什么情况下用 params,什么情况下用 searchParams

这是最容易混淆的点,两者完全独立,互不干扰

类型来源 URL 位置声明方式典型用途
动态路由 params问号 的路径段目录名写 [slug]主键、slug 等唯一标识资源的参数
查询参数 searchParams问号 的键值对不用声明,自由传递排序、筛选、页码、追踪来源等附加信息

例子:

/blog/hello-world?utm=wechat&sort=new
      ↑ params.slug              ↑ searchParams

记忆口诀:路径里的用 params,问号后的用 searchParams


🔍 疑惑 3:一个页面能不能同时拿到 params 和 searchParams?两种接收方式分别是什么?

可以!完全能同时拿。 但 Server Component 和 Client Component 的写法不同:

✅ Server Component(默认,推荐,不写 use client)

通过函数的两个入参直接拿到:

// src/app/blog/[slug]/page.tsx — 默认就是 Server Component
export default async function BlogDetailPage({
  params,        // 第 1 个入参:动态路由参数
  searchParams,  // 第 2 个入参:查询参数
}: {
  params: Promise<{ slug: string }>;
  searchParams: Promise<{ utm?: string; sort?: string }>;
}) {
  // ⚠️ Next.js 16 / React 19 起:两者都是 Promise,必须 await
  const { slug } = await params;
  const { utm, sort } = await searchParams;

  // 同时用两个参数
  const post = await fetchPost(slug);
  if (utm === "hotlist") trackFromHotList();
  if (sort === "new") { /* 重新排序评论 */ }
}

也适用于 generateMetadata

export async function generateMetadata({ params, searchParams }) {
  const { slug } = await params;
  // 一样的用法
}

⚠️ Client Component(写 use client,需要交互时才用)

通过 hook 拿,而且需要 React.use() 解包(Next.js 16 / React 19 异步 API 新约定):

"use client";
import { useParams, useSearchParams } from "next/navigation";
import { use } from "react";

export default function InteractivePage() {
  // 动态路由参数
  const params = use(useParams<{ slug: string }>());
  // 查询参数
  const searchParams = use(useSearchParams());

  console.log(params.slug);                  // "hello-world"
  console.log(searchParams.get("utm"));      // "wechat"
}

🔍 疑惑 4:拿到参数后需要做进一步处理,就必须写 use client 吗?

错!判断标准不是"有没有处理数据",而是"用不用浏览器能力"。 看下面这个分类:

👇 这些事情 Server Component 都能干(不写 use client):

// ❌ 没有 use client,照样做各种"处理"
export default async function BlogDetailPage({ params, searchParams }) {
  const { slug } = await params;
  const { sort, theme } = await searchParams;

  const post = await fetchPost(slug);               // 查数据库
  const related = await fetchRelated(slug);         // 查关联
  const filtered = related.filter(p => sort === "new" ? p.new : true);  // 过滤
  const readingTime = Math.ceil(post.content.length / 500);             // 计算
  const bg = theme === "dark" ? "#000" : "#fff";    // 条件判断

  return <article style={{ background: bg }}>...</article>;
}

拿到参数后做数据查询、计算、条件渲染、拼 JSX——再复杂都不用写 use client

👇 只有这些情况才必须写 use client:

  • 使用 React HookuseStateuseEffectuseRefuseRouteruseSearchParamsuseParams
  • 使用 浏览器 APIwindowdocumentlocalStoragenavigator.geolocation
  • 用户交互事件onClickonChange、受控表单输入的 value 绑定…

一句话判断标准:

🎯 拿数据、算数据、画页面 → Server Component 直接干;点一下、改个值、读浏览器 → use client。


🔍 疑惑 5:最佳实践——既需要拿两种参数,又需要交互怎么办?

强烈推荐:Page 保持 Server Component,交互部分拆成独立 Client 子组件。

src/app/blog/[slug]/
├── page.tsx          ← Server Component:拿 params/searchParams、查数据、渲染主体
└── components/
    └── LikeButton.tsx  ← Client Component(写 use client):只做点赞按钮的交互

示例代码:

// page.tsx — 保持 Server,不写 use client
export default async function Page({ params, searchParams }) {
  const { slug } = await params;
  const { utm } = await searchParams;
  const post = await fetchPost(slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
      {/* 交互部分拆出去,需要的数据通过 props 传入 */}
      <LikeButton initialCount={post.likes} postSlug={slug} />
    </article>
  );
}

// LikeButton.tsx — 只有这一个文件写 use client
"use client";
import { useState } from "react";
export function LikeButton({ initialCount, postSlug }) {
  const [count, setCount] = useState(initialCount);
  return <button onClick={() => setCount(c => c + 1)}>👍 {count}</button>;
}

好处:

  • SEO 友好(服务端渲染文章主体)
  • 首屏速度快(Server 直出 HTML,不用等客户端 JS)
  • 体积小(只有点赞按钮那一小块客户端代码)

🔍 疑惑 6:generateStaticParams / generateMetadata / revalidate 这些名字能随便写吗?

绝对不能!必须严格按约定名字写。

它们是 Next.js 的约定式具名导出(Convention Exports)。Next.js 在构建和运行时会扫描 page.tsx / layout.tsx 文件的具名导出,只有匹配到特定名字才会自动调用对应逻辑。

打个比方:考试答题纸
Next.js 只会按题号(generateMetadata / generateStaticParams)位置找答案
你把答案随便写在空白处叫 myBuildParams(),老师(Next.js)直接略过不看

如果改错名字会怎样:

// ❌ 错误:自己起的名字 buildParamsList — Next.js 不认识,永远不会被调用
export async function buildParamsList() {
  return posts.map(p => ({ slug: p.slug }));
}

// ❌ 错误:自己起的名字 genMeta — 自定义 SEO 标签不会被注入
export async function genMeta({ params }) {
  return { title: "文章标题" };
}

// ✅ 正确:必须用约定名字
export async function generateStaticParams() { ... }
export async function generateMetadata() { ... }
export const revalidate = 60;

结果(踩坑实录):

  • generateStaticParams 改错名 → 动态路由不会预生成任何静态页面,每次访问走 SSR
  • generateMetadata 改错名 → 博客详情页的 <title> 回退到 layout.tsx 的默认标题,SEO 崩了
  • revalidate 改错名 → ISR 失效,页面永远不重新校验

同类的约定式导出还有哪些(一整套机制):

导出名写在哪作用
generateStaticParams()page.tsx动态路由 SSG:告诉 Next.js 预渲染哪些 slug/id
generateMetadata()page.tsx / layout.tsx生成 <title><meta>、OG 图等 SEO 元数据
generateViewport()page.tsx / layout.tsx生成 viewport / theme-color 元数据
generateSitemaps()app/sitemap.ts生成 sitemap.xml
generateRobots()app/robots.ts生成 robots.txt
generateImageMetadata()app/favicon.ts / opengraph-image.tsx生成 favicon / OG 图片清单
revalidatepage.tsx / layout.tsxISR 重生成秒数(博客详情页的 export const revalidate = 60
dynamicpage.tsx / layout.tsx强制渲染模式:'force-static' / 'force-dynamic'
runtimepage.tsx / layout.tsx / route.ts运行时:'nodejs' / 'edge'

一句话记忆:

🎯 约定式导出认名字不认功能。写错名字代码逻辑再对也不会被 Next.js 调用。


🔍 疑惑 7:layout.tsx / page.tsx 里的组件函数名(比如 InterceptionLayout)也必须叫这个名字吗?

不需要!可以随便起。

这个和疑惑 6 的 generateStaticParams / generateMetadata两套完全不同的机制,千万别混。

Next.js 里的「名字」分两类:

类型名字要求例子
约定式具名导出❌ 必须一字不差generateStaticParams / generateMetadata / revalidate / dynamic / runtime
默认导出组件✅ 随便起page.tsx / layout.tsx / loading.tsx / error.tsx / not-found.tsx / template.tsx 里的 export default function XXX()

关键判断标准:看有没有 default 关键字。

// layout.tsx —— 默认导出,名字随便起
export default function InterceptionLayout({ children, modal }) { ... }
export default function MyLayout({ children, modal }) { ... }       // ✅ 也行
export default function Layout({ children, modal }) { ... }          // ✅ 也行
export default function ({ children, modal }) { ... }                // ✅ 匿名也行

Next.js 只关心三件事:

  • ✅ 文件名是不是 layout.tsx(按文件名识别角色)
  • ✅ 是不是 export default(按导出方式取组件)
  • ✅ props 接收对不对(children / modal 等)

Next.js 不关心:

  • ❌ 函数叫什么名字

项目里的验证(都是合法的):

  • src/app/layout.tsx 的函数叫 RootLayout
  • src/app/playground/interception/layout.tsx 的函数叫 InterceptionLayout

这些名字只是给开发者看的,方便阅读代码时理解意图。换成 LayoutFooBar 也能跑,只是不利于阅读。

对比记忆:

机制比喻
约定式具名导出generateMetadata 等)考试答题纸:必须写在「第 3 题第 2 行」对应位置,老师才改
默认导出page / layout 等组件)寄快递:只要贴对「快递单」(export default + 正确文件名),里面东西叫什么名字快递员根本不管

一句话判断标准:

🎯 default 关键字 → 函数名随便起;没有 default(比如 export const revalidateexport function generateMetadata)→ 名字必须一字不差。


🔍 疑惑 8:访问 /playground/interception/photos/1 时,进入的是哪个文件?

关键认知:同一个 URL,根据「进入方式」不同,进入的文件完全不同!

URL: /playground/interception/photos/1
                ↓
       你是怎么进来的?
                ↓
    ┌───────────┴───────────┐
    ↓                       ↓
 软导航                  硬导航
(<Link> 点击)        (地址栏输入/刷新/新标签页)
    ↓                       ↓
@modal/(..)photos/      photos/
  [id]/page.tsx           [id]/page.tsx
  (弹窗版本)            (完整详情页)

两种情况详细对比:

进入方式触发文件渲染内容layout.tsx 此时收到
<Link> 点击(软导航)@modal/(..)photos/[id]/page.tsx弹窗 + 背景列表children=列表页 + modal=弹窗
地址栏输入/刷新(硬导航)photos/[id]/page.tsx完整详情页children=详情页 + modal=null

为什么 <Link> 能触发拦截?

  • <Link> 是客户端路由(软导航),Next.js 知道用户"从哪来"(有来源上下文)
  • <a> 或地址栏输入是浏览器原生跳转(硬导航),Next.js 拿不到来源信息

亲自验证 3 种操作:

  1. 列表页点照片 → 弹窗(软导航 → @modal/...
  2. 弹窗状态按 F5 刷新 → 完整详情页(刷新变成硬导航 → photos/[id]/...
  3. 弹窗状态右键复制链接 → 新标签页打开 → 完整详情页(新标签页是硬导航)

🎯 URL 决定"去哪",进入方式决定"看哪个文件"。


🔍 疑惑 9:用 <a> 跳转是不是就不需要 @modal 了?

技术上对,但思路错位了。 这不是"<a>@modal 失效",而是"你主动放弃了弹窗功能"。

核心认知升级:从机制导向翻转到产品导向

❌ 错误的思考路径(机制导向):

“我用了 <Link> 所以需要 @modal,用 <a> 就不需要 @modal 了”

✅ 正确的思考路径(产品导向):

“我的产品要不要弹窗体验?”

  • 要弹窗<Link> + @modal + layout.tsx(一套组合拳,缺一不可)
  • 不要弹窗<Link> + photos/[id]/page.tsx(直接走完整页,删掉 @modal

为什么永远别用 <a> 跳转内部页面?

对比项<Link><a>
页面刷新❌ 不刷新(客户端路由)✅ 完整刷新
跳转速度⚡ 秒开(只加载变化的组件)🐢 慢(重新加载所有 JS/CSS)
预取(prefetch)✅ 鼠标悬停就预取❌ 无
状态保留✅ 全局状态/Context 不丢❌ 全部重置
用户体验✅ 类似 SPA,丝滑❌ 传统多页,白屏闪烁

🎯 <Link>@modal 是配套关系,不是"用谁决定要不要谁"。决定权在你想要什么产品体验,而不是用什么标签。


🔍 疑惑 10:InterceptionLayout 看起来没必要,能不能删掉?

绝对不能删!它是整个拦截路由机制的「承重墙」。

路由匹配的理解是对的:

/playground/interception            → page.tsx       ✅
/playground/interception/photos     → photos/page.tsx ✅
/playground/interception/photos/1   → photos/[id]/page.tsx(硬导航时)✅

但漏掉的关键:点击照片(软导航)时,实际进入的是另一个文件!

点击照片(软导航)  → @modal/(..)photos/[id]/page.tsx   ← 弹窗版本
直接输入 URL(硬导航) → photos/[id]/page.tsx            ← 完整详情页

问题来了:弹窗内容要怎么同时显示在屏幕上?列表页又怎么保持不消失?

答案就是 InterceptionLayout。看 [layout.tsx](file:///d:/11-练习/my-next-app/src/app/playground/interception/layout.tsx#L37-L53):

export default function InterceptionLayout({
  children,    // ← 主内容:列表页 / 介绍页 / 完整详情页
  modal,       // ← 弹窗内容:@modal 插槽里渲染的组件
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <div>
      {children}   {/* 👈 背景内容(列表页保持可见) */}
      {modal}       {/* 👈 弹窗内容(overlay 在上面) */}
    </div>
  );
}

modal 这个 prop 就是 Parallel Routes 的核心机制:

  • @modal 目录是 Parallel Route 的「插槽」
  • Next.js 渲染时,会把这个插槽的内容作为 modal prop 注入到 layout.tsx
  • 如果没有 layout.tsx 接收 modal prop,这个插槽根本无处可去

删掉 InterceptionLayout 的后果:

  1. @modal 插槽没有地方渲染 → Next.js 报错或忽略插槽
  2. 点击照片 → 弹窗组件永远不会显示在页面上
  3. 拦截路由机制完全失效 → 退化成普通路由跳转
  4. 整个 @modal/(..)photos/[id]/page.tsx 全废了

图解三个文件的协作:

URL: /playground/interception/photos/1(从列表点击进入)

        ┌─────────────────────────────────────────────┐
        │  InterceptionLayout (layout.tsx)            │
        │  ┌─────────────────────────────────────┐    │
        │  │  children ← photos/page.tsx         │    │ ← 背景列表页保持可见
        │  │  (列表页内容)                      │    │
        │  └─────────────────────────────────────┘    │
        │  ┌─────────────────────────────────────┐    │
        │  │  modal ← @modal/(..)photos/[id]     │    │ ← 弹窗浮在上面
        │  │  (PhotoModalContent 组件)          │    │
        │  └─────────────────────────────────────┘    │
        └─────────────────────────────────────────────┘

🎯 InterceptionLayout 不是可选装饰,是拦截路由机制的承重墙。它通过 modal prop 接收 @modal 插槽内容,把弹窗和背景页面同时渲染到屏幕上。


🔍 疑惑 11:什么是「弹窗体验」?跳转后页面不刷新就是弹窗吗?

不是!「不刷新页面」不等于「弹窗体验」。 完整详情页跳转也不刷新,但那不是弹窗。

弹窗体验 = 当前页面不消失 + 一个浮层弹出来盖在上面。

两个关键词:

  1. 当前页面不消失(背景还在)
  2. 浮层盖在上面(弹出来的东西)

区分三个容易混的概念:

概念是否刷新页面背景页面是否保留是不是弹窗
完整详情页跳转不刷新(用 <Link>❌ 列表页消失❌ 不是弹窗
弹窗体验不刷新✅ 列表页保留✅ 是弹窗
window.alert()不刷新✅ 保留但卡住❌ 浏览器原生弹窗

弹窗体验的视觉效果:

点击照片 #1 之前:

┌─────────────────────────────────┐
│  📸 照片列表                     │
│  ┌─────┐ ┌─────┐ ┌─────┐       │
│  │ #1  │ │ #2  │ │ #3  │       │
│  └─────┘ └─────┘ └─────┘       │
└─────────────────────────────────┘


点击照片 #1 之后(弹窗体验):

┌─────────────────────────────────┐
│  📸 照片列表              ┌──────┤  ← 弹窗浮在列表上面
│  ┌─────┐ ┌─────┐ ┌─────┐  │ 照片1 │     背景列表还在!
│  │ #1  │ │ #2  │ │ #3  │  │ 详情 │
│  └─────┘ └─────┘ └─────┘  └──────┤
│                          半透明遮罩 │
└─────────────────────────────────┘

URL: /photos/1  ← 变了!
但列表页没消失!弹窗盖在上面!

「秒开」的真正原因(两层):

第一层:不刷新

传统跳转:           Next.js 软导航 + 弹窗:
浏览器重新加载         只请求弹窗需要的数据
HTML/CSS/JS          列表页 DOM 完全保留
白屏 → 渲染           直接渲染弹窗
~500ms              ~50ms

第二层:列表页的 DOM 状态完全保留

场景:列表页你滚到了底部,然后点击照片

传统跳转:
├── 弹窗关闭后
└── 回到列表页顶部(滚动位置丢了)❌

弹窗体验:
├── 弹窗关闭后
└── 回到列表页你刚才滚动的位置 ✅(DOM 完全保留,滚动位置也在)

这才是弹窗体验的真正价值:用户上下文不丢失。

完整对比表:

指标完整详情页跳转弹窗体验
页面刷新❌ 不刷新❌ 不刷新
列表页 DOM❌ 销毁重建✅ 完全保留
滚动位置❌ 丢失✅ 保留
输入框内容❌ 丢失✅ 保留
数据请求详情页数据弹窗数据
渲染耗时重建整个页面只渲染弹窗
用户体验跳转感强平滑过渡

为什么有了弹窗还要保留完整详情页?

因为弹窗体验有失效场景,需要兜底:

  1. 用户在弹窗状态按 F5 刷新 → URL 还是 /photos/1,但弹窗没了
  2. 用户复制链接 → 新标签页打开 → 新标签是硬导航,不触发拦截
  3. 用户把链接发给朋友 → 朋友打开是硬导航
  4. 搜索引擎爬虫抓取 → 爬虫不点 Link,直接抓 URL

所以必须有「兜底方案」:当弹窗失效时,完整详情页接管,保证刷新不白屏、链接可分享、SEO 可抓取。

这就是为什么项目里有两个文件共享同一个 URL

photos/[id]/page.tsx              ← 完整详情页(兜底)
@modal/(..)photos/[id]/page.tsx   ← 弹窗版本(软导航时)

一句话总结(升级版):

🎯 弹窗体验 = 旧页面不消失 + 新内容浮层覆盖 + DOM 状态全保留。

  • 用户体验:秒开 + 无白屏 + 滚动位置/输入状态全保留
  • 技术实现<Link> 软导航 + @modal 插槽 + layout.tsx 同屏渲染 children + modal
  • 工程兜底:两个文件共享同一 URL,硬导航时自动降级为完整详情页,保证可分享/可刷新/可 SEO

这就是 Next.js Interception Routes 的精髓:兼顾用户体验和工程健壮性。


🔍 疑惑 12:Catch-all 路由 [...slug] 会拦截所有 GET 请求吗?

绝对不是! [...slug] 不是「全局拦截器」,它只拦截特定 URL 前缀下的所有子路径。

❌ 常见错误理解:

以为:任何 GET 请求都会被 [...slug]/route.ts 捕获
/、/users、/blog/xxx、/api/chat、/api/files/xxx
            ↓
       全部进入 [...slug]/route.ts   ❌ 错!

如果真这样,其他 page.tsx / route.ts 全都失效了,整个项目就乱套了。

✅ 正确理解:前缀由文件位置决定,只捕获该前缀下的子路径

文件路径:src/app/api/files/[...slug]/route.ts
                ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
                文件位置决定了能捕获的 URL 前缀

✅ 能捕获的 URL(必须以 /api/files/ 开头):
✅ /api/files/a
✅ /api/files/a/b
✅ /api/files/a/b/c/d/e/f/g/h    (无限层)
✅ /api/files/images/2024/jan/photo.jpg

❌ 不能捕获的 URL(前缀不对):
❌ /                    (不是 /api/files/ 开头)
❌ /users               (不是 /api/files/ 开头)
❌ /blog/hello          (不是 /api/files/ 开头)
❌ /api/chat            (是 /api/chat,不是 /api/files)
❌ /api/files           ([...slug] 要求至少一层,根路径不匹配)

前缀是固定的,只有后面部分才是 catch-all 的通配范围。

图解:固定前缀 + 通配部分

                  固定前缀              通配部分
                 ┌──────┐    ┌──────────────┐
URL:           /api/files/  images/2024/jan
                 └──────┘    └──────────────┘
                    ↑              ↑
              必须完全匹配      任意层级都行
              (由文件位置决定)  [...slug] 收集这部分

项目里的路由分工(各管各的,互不干扰):

app/
├── api/
│   ├── files/
│   │   └── [...slug]/route.ts    ← 只管 /api/files/*
│   ├── chat/
│   │   └── ai/route.ts          ← 只管 /api/chat/ai
│   └── users/
│       └── [id]/route.ts        ← 只管 /api/users/{id}
├── blog/
│   └── [slug]/page.tsx          ← 只管 /blog/{slug}
└── page.tsx                      ← 只管 /

类比记忆:

[...slug] 想象成小区门口的保安

比喻对应代码
保安站岗的位置文件在 app/ 目录下的位置
小区访客以该前缀开头的 URL 请求
登记所有楼层访客[...slug] 收集所有子路径段
不管其他小区访客不捕获其他前缀的 URL

不是"全市所有保安管所有访客",是"指定小区的保安管本小区所有楼层访客"。


🔍 疑惑 13:[...slug][[...slug]] 有什么区别?

两种 catch-all 写法,差别在于**「根路径能不能匹配」**:

写法名称/api/files 能匹配吗典型用法
[...slug]必填 catch-all❌ 不能(至少要有一层)文件浏览、多级分类
[[...slug]]可选 catch-all✅ 能(slug=[] 空数组)列表 + 详情合一页

具体对比:

[...slug]  (单括号,必填)
├── /api/files/a      ✅ slug=["a"]
├── /api/files/a/b    ✅ slug=["a","b"]
└── /api/files        ❌ 不匹配!(至少要有一层)

[[...slug]] (双括号,可选)
├── /api/files/a      ✅ slug=["a"]
├── /api/files/a/b    ✅ slug=["a","b"]
└── /api/files        ✅ slug=[] (空数组也匹配)

本项目用的是 [...slug](单括号),所以 /api/files(不带子路径)不会被捕获。

params.slug 的类型也不同:

// 普通 [slug]
{ params: { slug: string } }        // 字符串

// Catch-all [...slug] / [[...slug]]
{ params: { slug: string[] } }       // 字符串数组!注意是数组

🔍 疑惑 14:Catch-all 路由到底解决什么问题?什么时候该用?

核心价值:用一个文件处理「深度不确定」的 URL 路径。

❌ 不用 catch-all 的痛苦场景(实现文件系统浏览):

假设要支持这些 URL(文件夹深度不确定):
/files/images/2024/jan/photo.jpg   (4层)
/files/documents/work/report.pdf   (4层)
/files/videos/2023/summer/vacation  (4层)

用普通 [slug] 路由要这样写:
app/files/
├── [category]/page.tsx                          ← 匹配 /files/images
└── [category]/[year]/page.tsx                   ← 匹配 /files/images/2024
    └── [category]/[year]/[month]/page.tsx       ← 匹配 /files/images/2024/jan
        └── [category]/[year]/[month]/[name]/page.tsx ← 匹配 4 层
            └── ... 还要 5 层?6 层?无穷无尽

问题: 每多一层路径,就要多建一层文件夹,永远写不完。

✅ 用 catch-all 一个文件搞定:

app/api/files/[...slug]/route.ts   ← 一个文件匹配所有层级!

典型应用场景:

场景URL 例子为什么用 catch-all
文件管理器/files/docs/2024/q1/report.pdf文件夹深度不确定,可能 3 层可能 7 层
电商分类/shop/electronics/laptops/gaming分类层级是动态的,不能写死
WordPress 风格 URL/blog/tech/frontend/react/hooks文章路径可能是任意层级
代理转发/api/proxy/service-a/users/123/api/proxy/* 全部转发到另一个服务
文档系统/docs/api/authentication/oauth2/google文档目录树层级不定

项目里的实际代码([route.ts L78-L123](file:///d:/11-练习/my-next-app/src/app/api/files/[…slug]/route.ts#L78-L123)):

function generateMockData(slug: string[]) {
  const firstSegment = slug[0];  // 取第一段做分类判断

  switch (firstSegment) {
    case "images":    // /api/files/images/... → 返回图片数据
      return { type: "image-folder", files: [...] };
    case "documents": // /api/files/documents/... → 返回文档数据
      return { type: "document-folder", files: [...] };
    case "products":  // /api/files/products/... → 返回产品数据
      return { type: "product-category", ... };
  }
}

一个文件就能响应所有这些 URL:

GET /api/files/images              → slug = ["images"]
GET /api/files/images/2024         → slug = ["images", "2024"]
GET /api/files/images/2024/jan     → slug = ["images", "2024", "jan"]
GET /api/files/documents/report    → slug = ["documents", "report"]
GET /api/files/products/electronics/laptops → slug = ["products","electronics","laptops"]

一句话总结:

🎯 Catch-all 路由 = 「在固定前缀下,用一个文件通配任意层级」的路由模式。

  • 前缀:由文件在 app/ 目录下的位置决定
  • 通配[...slug] 收集前缀后面所有路径段,组成数组
  • 不是全局拦截器:只捕获该前缀下的请求,其他 URL 各走各的路由
  • 适用场景:路径深度不确定的功能(文件系统、多级分类、代理转发)

类比:指定小区的保安管本小区所有楼层访客,不是全市保安管所有访客。


🔍 疑惑 15:Preview Mode 草稿预览是通过 cookie 判断作者/用户身份吗?

完全正确! 本项目就是用自定义 cookie 实现的,用真实代码走一遍。

1️⃣ 约定字段:preview_mode cookie

看 [enter/route.ts L18-L24](file:///d:/11-练习/my-next-app/src/app/api/preview/enter/route.ts#L18-L24):

response.cookies.set("preview_mode", "true", {
  httpOnly: true,
  maxAge: 60 * 60,  // 1 小时有效
});
//                  ↑
//   约定的字段名    约定的值

约定的字段:字段名 preview_mode,值 true = 作者模式,没有这个 cookie(或 false)= 普通用户。

2️⃣ 判断身份:读取 cookie

看 [preview/[slug]/page.tsx L25-L26](file:///d:/11-练习/my-next-app/src/app/preview/[slug]/page.tsx#L25-L26):

const cookieStore = await cookies();
const isPreview = cookieStore.get("preview_mode")?.value === "true";
//                                                ↑
//                            cookie 是 "true" → 作者(预览模式)
//                            cookie 不存在   → 普通用户

3️⃣ 根据身份返回不同内容

看 [cms-data.ts L74-L81](file:///d:/11-练习/my-next-app/src/lib/cms-data.ts#L74-L81):

export async function getPostBySlug(slug: string, preview?: boolean) {
  const allPosts = preview
    ? [...PUBLISHED_POSTS, ...DRAFT_POSTS]   // ← 作者:已发布 + 草稿
    : PUBLISHED_POSTS;                        // ← 用户:只看已发布
  return allPosts.find((post) => post.slug === slug);
}

权限差异:作者能看到草稿,用户只能看已发布。

完整流程图:

场景一:作者点击「进入预览模式」
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
作者访问 /api/preview/enter
    ↓
设置 cookie:preview_mode=true(1小时)
    ↓
重定向到 /preview
    ↓
此时作者访问任意文章
    ↓
page.tsx 读取 cookie → isPreview=true
    ↓
getPostBySlug(slug, true)
    ↓
返回:已发布 + 草稿 ✅(作者能看到草稿)


场景二:普通用户访问
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
用户直接访问 /preview/getting-started
    ↓
page.tsx 读取 cookie → 没有 preview_mode
    ↓
isPreview=false
    ↓
getPostBySlug(slug, false)
    ↓
返回:只有已发布 ❌(草稿看不到)


场景三:作者退出预览模式
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
作者访问 /api/preview/exit
    ↓
删除 cookie:preview_mode
    ↓
恢复成普通用户身份

真实效果对比:

访问 URL普通用户(无 cookie)作者(有 cookie)
/preview/getting-started✅ 看到「已发布」标签✅ 看到「已发布」标签
/preview/upcoming-features❌ 404(草稿看不到)✅ 看到「🟣 草稿文章」+ 紫色提示框
/preview/preview-mode-deep-dive❌ 404✅ 看到「🟣 草稿文章」+ 紫色提示框

两种实现方案对比:

方案优点缺点
自定义 cookie(本项目用的)简单直观,容易理解原理需要自己管理 cookie 安全性
官方 draftMode()安全性高,官方维护,自动加密API 比较黑盒,不利于学习

本项目选择自定义 cookie 方案,看 [preview/[slug]/page.tsx L6](file:///d:/11-练习/my-next-app/src/app/preview/[slug]/page.tsx#L6) 的注释:

/**
 * 通过自定义 cookie 判断预览状态
 * 不依赖 next/headers 的 draftMode()
 */

一句话总结:

🎯 Preview Mode = 通过 cookie 区分「作者」和「用户」身份,同一 URL 返回不同内容。

  • 约定字段preview_mode=true(作者)/ 不存在(用户)
  • 判断身份cookies().get("preview_mode")
  • 权限差异:作者能看到草稿,用户只能看已发布
  • 典型场景:CMS 编辑预览、草稿审核、灰度发布

不是"作者/用户看同一个页面的不同 UI",而是"作者能拿到草稿数据,用户拿不到"——数据层面的权限控制。


🔍 疑惑 16:Route Segment Config 是干什么的?

给「单个路由」单独设置运行规则的开关。page.tsx / route.tsexport 几个固定名字的常量,单独控制这个路由的缓存、渲染模式、运行时等行为。

这是疑惑 6「约定式具名导出」机制的具体应用——名字必须一字不差,Next.js 按名字识别。

五个核心配置项:

配置项作用可选值项目里用在哪
dynamic渲染模式auto / force-dynamic / force-static / error[chat/ai/route.ts L18](file:///d:/11-练习/my-next-app/src/app/api/chat/ai/route.ts#L18)
revalidate缓存秒数0 / false / 数字[blog/[slug]/page.tsx L17](file:///d:/11-练习/my-next-app/src/app/blog/[slug]/page.tsx#L17)
runtime运行时环境nodejs / edge[chat/stream/route.ts L26](file:///d:/11-练习/my-next-app/src/app/api/chat/stream/route.ts#L26)
fetchCachefetch 缓存策略auto / force-no-store / force-cache
preferredRegion数据中心区域home / iad1 / sfo1

项目里的 3 个真实例子:

例子 1:博客详情页用 ISR(之前学过的)

看 [blog/[slug]/page.tsx L17](file:///d:/11-练习/my-next-app/src/app/blog/[slug]/page.tsx#L17):

export const revalidate = 60;
//     ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
//     构建时生成静态页,60 秒后过期重新生成

一个配置让博客详情页变成 ISR 模式:用户访问时返回缓存的静态 HTML,60 秒后下次访问触发后台重新生成。不用改全局配置,只影响这一篇文章。

例子 2:AI 聊天接口必须实时

看 [chat/ai/route.ts L17-L18](file:///d:/11-练习/my-next-app/src/app/api/chat/ai/route.ts#L17-L18):

export const runtime = "nodejs";        // 用 Node.js 运行时(Edge 不支持某些 API)
export const dynamic = "force-dynamic";  // 每次请求都执行,不缓存

AI 聊天必须每次实时响应,不能缓存。这两个配置保证了:

  • 用 Node.js 运行时(而不是 Edge,因为调用 AI API 需要 Node 完整能力)
  • 强制动态(每次请求都执行,绝不返回缓存)

例子 3:SSE 流式响应需要 Node.js

看 [chat/stream/route.ts L26](file:///d:/11-练习/my-next-app/src/app/api/chat/stream/route.ts#L26):

export const runtime = "nodejs";
//     ↑↑↑↑↑↑↑↑↑↑
// SSE 需要 Node.js 运行时(Edge 不完全支持 ReadableStream)

注释里写得很清楚:Edge Runtime 对 ReadableStream 支持不全,所以必须用 Node.js 运行时。

Route Segment Config 解决什么问题?

❌ 不用它的问题:

假设全局配置:所有路由都用 SSG(静态生成)。但你的 /api/chat 必须实时响应,怎么办?

  • 方案 A:改全局配置 → 但其他页面也被影响,SEO 和性能全崩
  • 方案 B:每个路由单独配置 → ✅ 这就是 Route Segment Config

✅ 用它的好处:

全局默认:SSG 静态生成
        ↓
        单独调整某个路由:
        ├── /blog/[slug]      → revalidate=60   (ISR)
        ├── /api/chat/ai      → force-dynamic    (实时)
        ├── /api/chat/stream   → runtime=nodejs  (Node 运行时)
        └── /                 → 默认 SSG

每个路由各走各的路,互不干扰,这就是 Route Segment Config 的价值。

和疑惑 6 的联系:

所有这些 export const 都是 Next.js 按名字找的,改名字 = 配置失效:

// 名字必须一字不差!这就是约定式具名导出
export const revalidate = 60;            // ← Next.js 按名字识别
export const dynamic = "force-dynamic";  // ← 改成 myDynamic 就失效
export const runtime = "nodejs";         // ← 改成 myRuntime 就失效

完整配置选项速查表:

配置项类型作用典型场景
dynamicstring渲染模式force-dynamic 用于实时 API
revalidatenumber | false缓存秒数60 用于博客 ISR
runtimestring运行时环境nodejs 用于需要完整 Node API
fetchCachestringfetch 缓存策略force-no-store 不缓存 fetch
preferredRegionstring | array部署区域iad1 部署到美东
dynamicParamsboolean是否允许动态参数true 允许任意 slug

一句话总结:

🎯 Route Segment Config = 给单个路由单独配置规则的开关,通过约定名字的 export const 实现。

  • 作用:不用改全局配置,就能让某个路由单独用 ISR / 动态渲染 / Node 运行时
  • 写法export const 配置项 = 值(必须用约定名字)
  • 本质:疑惑 6 学的「约定式具名导出」机制的具体应用
  • 项目里用了 3 处:博客 ISR(revalidate)、AI 接口实时(force-dynamic)、SSE 流(runtime=nodejs)

配套记忆:疑惑 6 讲的是「为什么名字不能改」,疑惑 16 讲的是「名字不改能干什么具体的事」。


三、数据获取策略

Next.js 提供了多种数据获取方式,核心区别在于何时何地获取数据

3.1 四种数据获取策略

策略时机适用场景代码位置
SSR每次请求数据频繁变化、需要实时性Server Component 中 fetch()
SSG构建时内容固定、不常变化Server Component 中 fetch()
ISR构建 + 定时重生成需要更新但不需要实时fetch({ next: { revalidate: 3600 } })
CSR客户端挂载后交互性强、个性化数据Client Component 中 useEffect + fetch

3.2 Server Component 中的 fetch

// app/users/page.tsx — Server Component(默认)
// SSR 模式:每次请求都获取最新数据
export const dynamic = "force-dynamic";

export default async function UsersPage() {
  const res = await fetch("https://api.example.com/users");
  const users = await res.json();

  return (
    <div>
      {users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

3.3 ISR:增量静态再生成

// 每 3600 秒(1 小时)重新生成一次
// 期间访问会返回缓存的静态页面
const res = await fetch("https://api.example.com/posts", {
  next: { revalidate: 3600 },
});

// 也可以用页面级的 revalidate
// app/blog/[slug]/page.tsx
export const revalidate = 60;  // 项目里博客详情页用的是这个

3.4 CSR:客户端获取

// Client Component 中用 useEffect 获取
"use client";

import { useState, useEffect } from "react";

export default function Dashboard() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch("/api/stats")
      .then((res) => res.json())
      .then(setData);
  }, []);

  return <div>{data?.total}</div>;
}

3.5 generateStaticParams

动态路由中,如果使用 SSG/ISR,需要告诉 Next.js 预生成哪些路径:

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
  // Next.js 构建时会为每个 slug 生成静态页面
}

export const revalidate = 60;  // ISR

export default async function BlogPost({ params }) {
  const { slug } = await params;
  const post = await getPost(slug);
  return <article>{post.content}</article>;
}

四、API 路由(Route Handlers)

4.1 基本概念

API Route 是 Next.js 提供的轻量级后端,文件放在 app/api/ 目录下,每个 route.ts 文件对应一个 API 端点。

4.2 项目中的完整 API 清单

路径方法用途
/api/helloGET/POSTHello World 示例
/api/usersGET/POST用户列表/创建用户
/api/users/[id]GET/PUT/PATCH/DELETE动态路由 CRUD
/api/auth/loginPOST登录(设置 cookie)
/api/auth/logoutPOST登出(清除 cookie)
/api/auth/meGET获取当前用户
/api/chat/streamPOSTSSE 流式聊天回复
/api/cors-demoGET/OPTIONSCORS 跨域演示
/api/response-helpersGETNextResponse 助手演示
/api/segment-config-demoGETRoute Segment Config
/api/files/[...slug]GETCatch-all 路由
/api/middleware-demoGET中间件演示
/api/preview/enterGET开启预览模式
/api/preview/exitGET退出预览模式

4.3 基本示例:GET/POST

// app/api/hello/route.ts
import { NextResponse } from "next/server";

// GET /api/hello?name=World
export async function GET(request: NextRequest) {
  const name = request.nextUrl.searchParams.get("name") || "World";
  return NextResponse.json({
    message: `Hello, ${name}!`,
    timestamp: new Date().toISOString(),
  });
}

// POST /api/hello
export async function POST(request: NextRequest) {
  const body = await request.json();
  return NextResponse.json({ receivedData: body }, { status: 200 });
}

4.4 实战:登录 API(设置 httpOnly Cookie)

// app/api/auth/login/route.ts
import { NextResponse } from "next/server";
import { authenticate, AUTH_COOKIE_NAME } from "@/lib/auth";

export async function POST(request: NextRequest) {
  const { username, password } = await request.json();

  const result = authenticate(username, password);
  if (!result.success) {
    return NextResponse.json({ error: result.error }, { status: 401 });
  }

  // 设置 httpOnly cookie(安全)
  const response = NextResponse.json({ success: true, user: result.user });
  response.cookies.set(AUTH_COOKIE_NAME, result.token!, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "lax",
    maxAge: 60 * 60 * 24 * 7,
    path: "/",
  });

  return response;
}

4.5 动态 API 路由:CRUD

// app/api/users/[id]/route.ts
import { NextResponse } from "next/server";

// GET /api/users/1
export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const user = await getUser(parseInt(id));
  if (!user) return NextResponse.json({ error: "用户不存在" }, { status: 404 });
  return NextResponse.json(user);
}

// PUT /api/users/1 — 全量更新
export async function PUT(request: Request, { params }) {
  const { id } = await params;
  const body = await request.json();
  return NextResponse.json({ id, ...body });
}

// PATCH /api/users/1 — 部分更新
export async function PATCH(request: Request, { params }) {
  const { id } = await params;
  const body = await request.json();
  return NextResponse.json({ id, updated: body });
}

// DELETE /api/users/1 — 删除
export async function DELETE(request: Request, { params }) {
  const { id } = await params;
  return NextResponse.json({ success: true, deletedId: id });
}

五、样式方案

5.1 Tailwind CSS v4 配置

/* src/app/globals.css */
@import "tailwindcss";

/* 可以添加自定义主题变量 */
@theme {
  --color-brand: #3b82f6;
}

5.2 在组件中使用

<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
  <h1 className="text-2xl font-bold text-gray-900 dark:text-white">
    Hello Next.js
  </h1>
  <button className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
    点击
  </button>
</div>

5.3 next/font 字体优化

// app/layout.tsx
import { Geist, Geist_Mono } from "next/font/google";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

export default function RootLayout({ children }) {
  return (
    <html className={geistSans.variable}>
      <body>{children}</body>
    </html>
  );
}

Next.js 会自动:

  • 将字体文件内联到 CSS(零网络请求)
  • 使用 font-display: swap 防止文字闪烁
  • 提供自托管能力(无需外部域名)

六、中间件实战

6.1 什么是中间件

Next.js 中间件在请求到达页面/API 之前执行,可以做路由拦截、重定向、设置 cookie 等操作。

6.2 项目中的中间件:i18n 路由 + 认证

// src/proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { locales, defaultLocale } from "@/i18n/config";

// 公开路径(不需要登录)
const PUBLIC_PATHS = ["/", "/about", "/users", "/blog", "/login", "/performance", "/api/chat"];

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const token = request.cookies.get("auth-token");

  // 1️⃣ 国际化路由处理(/en/xxx → 重写到 /xxx + 设置 locale cookie)
  const pathSegments = pathname.split("/").filter(Boolean);
  const hasLocale = pathSegments.length > 0 && locales.includes(pathSegments[0]);

  if (hasLocale) {
    const locale = pathSegments[0];
    const restOfPath = "/" + pathSegments.slice(1).join("/");
    const response = NextResponse.rewrite(new URL(restOfPath, request.url));
    response.cookies.set("locale", locale, { path: "/", maxAge: 31536000 });
    return response;
  }

  // 2️⃣ 认证拦截
  const isPublicPath = PUBLIC_PATHS.includes(pathname);
  const isAuthenticated = !!token;

  // 已登录用户访问登录页 → 首页
  if (pathname === "/login" && isAuthenticated) {
    return NextResponse.redirect(new URL("/", request.url));
  }

  // 未登录访问受保护页面 → 登录页
  if (!isPublicPath && !isAuthenticated) {
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("redirect", pathname);
    return NextResponse.redirect(loginUrl);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

6.3 中间件流程图

用户请求 /dashboard
        ↓
   proxy.ts 执行
        ↓
  ┌───────────────────┐
  │ 检查 locale 前缀? │── 是 → 设置 cookie,rewrite
  └───────────────────┘
        ↓ 否
  ┌───────────────────┐
  │ 检查 auth-token?  │── 无 → 重定向到 /login
  └───────────────────┘
        ↓ 有
   渲染 /dashboard 页面 ✅

七、图片优化(next/image)

7.1 配置允许的远程域名

// next.config.ts
const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "api.dicebear.com", pathname: "/**" },
    ],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    formats: ["image/avif", "image/webp"],
  },
};

7.2 使用原生 img 处理 SVG

⚠️ next/image 对 SVG 支持不佳(会返回 400),SVG 头像建议用原生 <img>

// components/OptimizedAvatar.tsx
export default function OptimizedAvatar({ src, alt, size = 40 }) {
  return (
    <img
      src={src}
      alt={alt}
      width={size}
      height={size}
      loading="lazy"
      decoding="async"
      className="rounded-full bg-gray-100"
    />
  );
}

7.3 next/image vs 原生 img

图片类型推荐方案
SVG(图标、头像生成器)原生 <img>
位图(JPG/PNG/WebP)next/image
大量小图标<Image> + 合理的 sizes

八、国际化(i18n)

8.1 安装与配置

// next.config.ts
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);
// src/i18n/config.ts
export const locales = ["zh", "en"] as const;
export const defaultLocale = "zh";

8.2 翻译消息

// src/i18n/messages/zh.json
{
  "nav": { "home": "首页", "about": "关于", "users": "用户列表" },
  "home": { "title": "欢迎来到 Next.js 学习项目" }
}

8.3 在 Server / Client Component 中使用

// Server Component
import { getTranslations } from "next-intl/server";

export default async function HomePage() {
  const t = await getTranslations("home");
  return <h1>{t("title")}</h1>;
}
// Client Component
"use client";
import { useTranslations } from "next-intl";

export default function LanguageSwitcher() {
  const t = useTranslations("common");
  return <button>{t("switchLang")}</button>;
}

8.4 语言切换流程

用户点击 "EN" 按钮
    ↓
document.cookie = "locale=en"
router.push("/en/about")
    ↓
proxy.ts 检测 /en 前缀 → rewrite + 设置 cookie
    ↓
页面显示英文 ✅

8.5 两种 Hook 的区别

Hook适用场景注意事项
getTranslations()Server Component需要 await
useTranslations()Client Component不需要 await

九、性能监控与分析

9.1 核心 Web Vitals 指标

指标全称含义目标值
LCPLargest Contentful Paint最大内容绘制时间< 2.5s
INPInteraction to Next Paint交互响应时间< 200ms
CLSCumulative Layout Shift累积布局偏移< 0.1
FCPFirst Contentful Paint首次内容绘制< 1.8s
TTFBTime To First Byte首字节时间< 0.8s

9.2 useReportWebVitals

// components/WebVitalsMonitor.tsx
"use client";
import { useReportWebVitals } from "next/web-vitals";
import { useState } from "react";

export default function WebVitalsMonitor() {
  const [visible, setVisible] = useState(false);  // 默认隐藏,点击显示
  const [metrics, setMetrics] = useState<Record<string, number>>({});

  useReportWebVitals((metric) => {
    setMetrics(prev => ({ ...prev, [metric.name]: metric.value }));
  });

  return (
    <>
      <button onClick={() => setVisible(!visible)}>📊 显示性能</button>
      {visible && (
        <div className="fixed bottom-4 right-4 ...">
          <p>LCP: {metrics.LCP?.toFixed(2)}ms</p>
          <p>INP: {metrics.INP?.toFixed(2)}ms</p>
          <p>CLS: {metrics.CLS?.toFixed(3)}</p>
        </div>
      )}
    </>
  );
}

9.3 性能优化检查清单

优化项状态说明
图片优化next/image 或原生 lazy loading
字体优化next/font 内联字体
代码分割App Router 自动按路由分割
懒加载loading.tsx 骨架屏
缓存策略revalidate 控制 ISR
Dynamic Import按需加载重组件

十、Server Actions(进阶)

10.1 什么是 Server Actions

Server Actions 是 Next.js 提供的直接在客户端调用服务端函数的能力,不需要手动处理 HTTP 请求。

10.2 对比:API Route vs Server Action

对比项API RouteServer Action
HTTP 请求手动 fetch()自动处理
数据序列化JSON.stringify自动处理 FormData
错误处理手动检查 res.ok返回值直接判断
安全需要 CORS/CSRF 配置Next.js 自动处理
Cookie 操作response.cookies.set()cookies().set()

10.3 创建 Server Action

// src/app/actions/auth.ts
"use server";  // ⭐ 顶部指令,标记此文件为 Server Action 文件

import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { authenticate } from "@/lib/auth";

export async function loginAction(prevState, formData: FormData) {
  const username = formData.get("username") as string;
  const password = formData.get("password") as string;

  const result = authenticate(username, password);
  if (!result.success) {
    return { error: result.error };
  }

  // 直接设 cookie(不需要 response)
  const cookieStore = await cookies();
  cookieStore.set("auth-token", result.token!, {
    httpOnly: true,
    maxAge: 60 * 60 * 24 * 7,
  });

  redirect("/dashboard");
}

export async function logoutAction() {
  const cookieStore = await cookies();
  cookieStore.delete("auth-token");
  redirect("/");
}

10.4 在客户端调用(useActionState)

"use client";
import { useActionState } from "react";
import { loginAction } from "@/app/actions/auth";

export default function LoginPage() {
  const [state, formAction, isPending] = useActionState(loginAction, null);

  return (
    <form action={formAction}>
      <input name="username" defaultValue="admin" />
      <input name="password" type="password" defaultValue="123456" />

      {state?.error && <p>{state.error}</p>}

      <button disabled={isPending}>
        {isPending ? "登录中..." : "登录"}
      </button>
    </form>
  );
}

10.5 useActionState 解析

const [state, formAction, isPending] = useActionState(loginAction, null);
//    ↑        ↑              ↑
//  状态     绑定 form 的     是否正在提交
//  返回值    action 属性      (用于禁用按钮)
//  初始值

十一、错误处理体系

11.1 三层错误处理

app/
├── not-found.tsx     ← 404 错误(预期行为)
├── error.tsx         ← 运行时错误(页面级)
└── layout.tsx        ← 根布局

11.2 not-found.tsx(404 页面)

// app/not-found.tsx
import Link from "next/link";

export default function NotFound() {
  return (
    <div className="text-center py-24">
      <h1 className="text-6xl font-bold mb-4">404</h1>
      <p className="text-gray-600 mb-8">页面不存在</p>
      <Link href="/" className="px-6 py-3 bg-blue-600 text-white rounded-lg">
        返回首页
      </Link>
    </div>
  );
}

触发方式

  • 用户直接访问不存在的路由
  • 代码中调用 notFound()

11.3 error.tsx(错误边界)

// app/error.tsx
"use client";  // ⭐ 必须是 Client Component

import { useEffect } from "react";

export default function Error({ error, reset }) {
  useEffect(() => {
    console.error("【错误边界】", error);
  }, [error]);

  return (
    <div className="text-center py-24">
      <h1 className="text-6xl font-bold mb-4">500</h1>
      <p className="text-gray-600 mb-8">{error.message}</p>
      <button
        onClick={() => reset()}  // ⭐ 重试函数
        className="px-6 py-3 bg-blue-600 text-white rounded-lg"
      >
        重试
      </button>
    </div>
  );
}

十二、动态元数据 generateMetadata

12.1 静态 vs 动态

// ❌ 静态 metadata(所有页面共用)
export const metadata: Metadata = {
  title: "Blog",
  description: "Blog posts",
};

// ✅ 动态 metadata(根据参数生成)
export async function generateMetadata({ params }): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(slug);

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      type: "article",
    },
  };
}

12.2 继承机制

layout.tsx metadata(全局默认)
  └── blog/layout.tsx metadata(覆盖父级)
       └── blog/[slug]/page.tsx generateMetadata(动态覆盖)

十三、Next.js Compiler(SWC 编译器)

📁 项目路径:[src/app/compiler/](file:///d:/11-练习/my-next-app/src/app/compiler/page.tsx)

13.1 什么是 Next.js Compiler

Next.js Compiler 是 Next.js 12+ 默认使用的编译器,基于 SWC(Speedy Web Compiler)实现,用 Rust 编写。

一句话总结:把"时髦代码"翻译成"浏览器能跑的代码",速度比 Babel 快 17 倍。

13.2 SWC vs Babel

对比项Babel(旧)SWC(新)
实现语言JavaScriptRust
速度快 17 倍 🚀
是否需配置需要手动配插件默认启用,开箱即用
Next.js 中状态12 以后废弃12+ 默认

13.3 编译流水线

你写的代码 (.tsx/.ts/.jsx)
   ↓
1️⃣ 解析 (Parsing) — 把源码解析成 AST
   ↓
2️⃣ 转换 (Transform) — JSX → React.createElement,TS → JS
   ↓
3️⃣ 压缩 (Minification) — 移除空格、注释、缩短变量名
   ↓
浏览器运行的代码 (.js)

13.4 配置

// next.config.ts(默认配置即可,无需手动启用)
const nextConfig: NextConfig = {
  // SWC 已默认启用
  // 可选:启用 React Compiler(Next.js 15+)
  // reactCompiler: true,
};

13.5 大白话类比

Next.js Compiler 就像一个高速翻译官,你写的 TypeScript、JSX 它都能秒翻译成浏览器看得懂的 JavaScript,速度比以前的 Babel 翻译官快得多。


十四、Dynamic Import 动态导入

📁 项目路径:[src/app/dynamic-import/page.tsx](file:///d:/11-练习/my-next-app/src/app/dynamic-import/page.tsx)

14.1 什么是 Dynamic Import

按需加载组件或模块,而不是一开始就把所有代码都加载。

14.2 大白话类比

方式类比
普通 import搬家时把所有家具一次性全搬进去(重)
Dynamic Import搬家时先搬必需的,其他需要时再搬(轻)

14.3 四种用法

import dynamic from "next/dynamic";

// 1️⃣ 基本懒加载(组件首次用到时才加载)
const HeavyChart = dynamic(() => import("@/components/demos/HeavyChart"));

// 2️⃣ 带 loading 状态
const HeavyChartWithLoading = dynamic(
  () => import("@/components/demos/HeavyChart"),
  { loading: () => <div>加载中...</div> }
);

// 3️⃣ 禁用 SSR(仅客户端渲染)
const ClientOnly = dynamic(
  () => import("@/components/demos/ClientOnlyComponent"),
  { ssr: false }
);

// 4️⃣ 动态 import 表达式(按变量加载不同模块)
const module = await import(`/locales/${lang}.json`);

14.4 应用场景

  • 重型组件(图表、编辑器)懒加载
  • 仅客户端运行的组件(如访问 window)
  • 多语言文件按需加载
  • 减小首屏 JS 体积

十五、SSE 流式通信

📁 项目路径:[src/app/api/chat/stream/route.ts](file:///d:/11-练习/my-next-app/src/app/api/chat/stream/route.ts) + [src/components/SSEChatDemo.tsx](file:///d:/11-练习/my-next-app/src/components/SSEChatDemo.tsx)

15.1 什么是 SSE

SSE(Server-Sent Events) 是一种 HTTP 单向推送技术,服务器持续向客户端推送数据,客户端通过 fetch + ReadableStream 接收。

典型应用:AI 流式回复、实时通知、进度推送。

15.2 SSE vs WebSocket vs 普通 API

方式方向复杂度适用场景
普通 API单次请求→单次响应CRUD
SSE服务端→客户端(单向)AI 流式回复、通知
WebSocket双向聊天室、游戏

15.3 后端实现(Route Handler 返回 ReadableStream)

// app/api/chat/stream/route.ts
export const runtime = "nodejs";  // SSE 需要 Node.js 运行时

export async function POST(request: Request) {
  const { message } = await request.json();
  const fullReply = generateReply(message);

  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      // 按字符逐个推送(模拟打字机效果)
      for (let i = 0; i < fullReply.length; i++) {
        const payload = JSON.stringify({
          type: "char",
          content: fullReply[i],
          index: i,
          total: fullReply.length,
        });
        // ⭐ SSE 协议格式:data: {JSON}\n\n
        controller.enqueue(encoder.encode(`data: ${payload}\n\n`));
        await new Promise(resolve => setTimeout(resolve, 30));
      }
      // 推送完成事件
      controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "done" })}\n\n`));
      controller.close();
    },
  });

  // ⭐ Content-Type 必须是 text/event-stream
  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream; charset=utf-8",
      "Cache-Control": "no-cache",
      "Connection": "keep-alive",
    },
  });
}

15.4 前端消费(fetch + getReader)

"use client";
import { useState, useRef } from "react";

export default function SSEChatDemo() {
  const [reply, setReply] = useState("");
  const initFiredRef = useRef(false);  // ⭐ 防 React 18 StrictMode 重复请求

  async function sendMessage(message: string) {
    const res = await fetch("/api/chat/stream", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message }),
    });

    const reader = res.body!.getReader();
    const decoder = new TextDecoder();
    let buffer = "";

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });
      // 按 \n\n 分割 SSE 事件
      const events = buffer.split("\n\n");
      buffer = events.pop() || "";

      for (const event of events) {
        const data = JSON.parse(event.replace("data: ", ""));
        if (data.type === "char") {
          setReply(prev => prev + data.content);  // 逐字拼接
        } else if (data.type === "done") {
          console.log("回复完成");
        }
      }
    }
  }
  // ...
}

15.5 踩坑经验

问题原因解决
StrictMode 下请求两次React 18 开发模式 useEffect 执行两次useRef 做初始化标记
PowerShell curl 看不到流PowerShell 的 curl 不是真 curlcurl.exe 或浏览器
API 被认证拦截中间件拦截了 /api/chat把 /api/chat 加入公开路径白名单

十六、第三方 SDK 集成

📁 项目路径:[src/components/AMapLoader.tsx](file:///d:/11-练习/my-next-app/src/components/AMapLoader.tsx)

16.1 next/script 三种加载策略

策略加载时机适用
beforeInteractive页面交互前(阻塞)字体加载器、必须的 SDK
afterInteractive交互后立即(默认)GA、AdSense
lazyOnload浏览器空闲时客服 widget、广告

16.2 ⚠️ 坑:next/script + 条件渲染导致 hydration 错误

错误:Encountered a script tag while rendering React component
原因:<Script> 在 SSR 阶段输出 <script>,但配合条件渲染时
     SSR 和客户端的 DOM 结构不一致

16.3 正确做法:useEffect + 手动注入 script

// components/AMapLoader.tsx
"use client";
import { useState, useEffect } from "react";

export default function AMapLoader() {
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    const script = document.createElement("script");
    script.src = `https://webapi.amap.com/maps?v=2.0&key=${process.env.NEXT_PUBLIC_AMAP_KEY}`;
    script.onload = () => setLoaded(true);
    document.body.appendChild(script);

    return () => {
      document.body.removeChild(script);
    };
  }, []);

  return loaded ? <div id="map-container" /> : <div>地图加载中...</div>;
}

16.4 环境变量配置

# .env.local
NEXT_PUBLIC_AMAP_KEY=你的高德地图Key      # 客户端可访问(NEXT_PUBLIC_ 前缀)
NEXT_PUBLIC_ADSENSE_ID=ca-pub-xxxx        # Google AdSense ID
NEXT_PUBLIC_GA_ID=G-XXXXXX                # Google Analytics ID

💡 NEXT_PUBLIC_ 前缀的变量会被打包进客户端代码,浏览器可见;不加前缀的只在服务端可见。


十七、嵌套 Layout 与加载状态

📁 项目路径:[src/app/blog/layout.tsx](file:///d:/11-练习/my-next-app/src/app/blog/layout.tsx)

17.1 嵌套 Layout 的价值

app/
├── layout.tsx          ← 根布局(全站)
└── blog/
    ├── layout.tsx      ← ⭐ 嵌套布局(被下面所有路由继承)
    ├── page.tsx        ← /blog         (继承 blog/layout.tsx)
    └── [slug]/
        └── page.tsx    ← /blog/:slug   (同样继承)

核心价值

  1. 状态持久化:在 /blog → /blog/getting-started 之间切换时,侧边栏不会重新渲染
  2. 代码复用:所有博客页面共享的导航、侧边栏只写一次
  3. 数据共享:layout 里 fetch 的数据可以传给子页面

17.2 嵌套 Layout 实战

// app/blog/layout.tsx
import { getPosts } from "@/lib/data";

export const metadata = {
  title: { default: "博客", template: "%s | 博客" },
};

export default async function BlogLayout({ children }: { children: React.ReactNode }) {
  // 在 layout 层获取数据,路由切换时不会重复请求
  const posts = await getPosts();
  const recentPosts = [...posts].slice(0, 3);

  return (
    <div className="max-w-6xl mx-auto px-4 py-12">
      <div className="flex gap-8 flex-col lg:flex-row">
        {/* 侧边栏:在路由切换时保持不销毁 */}
        <aside className="w-full lg:w-64">
          <div className="lg:sticky lg:top-24">
            <h3>🔥 热门文章</h3>
            {recentPosts.map(post => (
              <Link key={post.slug} href={`/blog/${post.slug}`}>
                {post.title}
              </Link>
            ))}
          </div>
        </aside>

        {/* 主内容区(由子路由 page.tsx 注入) */}
        <div className="flex-1 min-w-0">{children}</div>
      </div>
    </div>
  );
}

17.3 loading.tsx 层级继承

📁 项目路径:[src/components/Skeleton.tsx](file:///d:/11-练习/my-next-app/src/components/Skeleton.tsx)

核心规则:子路由没有 loading.tsx 时,会自动继承父目录的 loading.tsx。

app/
├── loading.tsx              ← 全局 loading
├── users/
│   ├── loading.tsx          ← users 专用 loading
│   └── [id]/
│       └── loading.tsx      ← 用户详情专用 loading(没有则继承父级)
└── blog/
    ├── loading.tsx          ← blog 专用 loading
    └── [slug]/
        └── loading.tsx      ← 文章详情专用 loading

17.4 可复用的 Skeleton 组件

// components/Skeleton.tsx
"use client";

type SkeletonVariant = "list" | "detail" | "card" | "dashboard";

export default function Skeleton({ variant = "list" }: { variant?: SkeletonVariant }) {
  switch (variant) {
    case "list":
      return (
        <div className="space-y-3">
          {[1, 2, 3].map(i => (
            <div key={i} className="bg-white rounded-xl p-4 animate-pulse">
              <div className="h-4 bg-gray-200 rounded w-1/3 mb-2" />
              <div className="h-3 bg-gray-200 rounded w-2/3" />
            </div>
          ))}
        </div>
      );
    case "detail":
      return /* ... */;
    // ...
  }
}
// app/users/loading.tsx
import Skeleton from "@/components/Skeleton";
export default function Loading() {
  return <Skeleton variant="list" />;
}

十八、面包屑导航

📁 项目路径:[src/components/Breadcrumb.tsx](file:///d:/11-练习/my-next-app/src/components/Breadcrumb.tsx)

18.1 usePathname 的典型应用

面包屑是 usePathname() 最经典的使用场景,把"URL 路径"自动渲染为"层级导航"。

18.2 实现思路

1. usePathname() 获取当前路径:/playground/middleware
2. 按 / 分割:["playground", "middleware"]
3. 逐级累加 URL:/playground → /playground/middleware
4. 渲染为:首页 / 实验场 / 中间件

18.3 关键代码

// components/Breadcrumb.tsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";

const DEFAULT_LABEL_MAP: Record<string, string> = {
  users: "用户",
  blog: "博客",
  playground: "实验场",
  middleware: "中间件",
  router: "路由",
  "sse-streaming": "SSE 流式",
  // ...
};

export default function Breadcrumb({ labels = {} }: { labels?: Record<string, string> }) {
  const pathname = usePathname();
  const labelMap = { ...DEFAULT_LABEL_MAP, ...labels };

  // 去掉 locale 前缀(/en/xxx → /xxx)
  const cleanPath = pathname.replace(/^\/(en|zh)(?=\/|$)/, "");
  const segments = cleanPath.split("/").filter(Boolean);

  if (segments.length === 0) return null;

  const items = segments.map((segment, index) => {
    const href = "/" + segments.slice(0, index + 1).join("/");
    let label: string;
    if (labelMap[segment]) {
      label = labelMap[segment];
    } else if (/^\d+$/.test(segment)) {
      label = `#${segment}`;  // 动态参数显示为 #123
    } else {
      label = segment.charAt(0).toUpperCase() + segment.slice(1);
    }
    return { label, href, isLast: index === segments.length - 1 };
  });

  return (
    <nav aria-label="面包屑">
      <Link href="/">首页</Link>
      {items.map(item => (
        <span key={item.href}>
          <span> / </span>
          {item.isLast ? (
            <span className="font-medium">{item.label}</span>
          ) : (
            <Link href={item.href}>{item.label}</Link>
          )}
        </span>
      ))}
    </nav>
  );
}

十九、高级路由(Interception / Parallel / Catch-all)

📁 项目路径:[src/app/playground/interception/](file:///d:/11-练习/my-next-app/src/app/playground/interception/layout.tsx)

这是 Next.js 独有的高级路由特性,同一个 URL 根据访问来源不同,显示不同的 UI

19.1 Interception Routes(拦截路由)

一句话:URL 变了,但页面不跳转(弹窗形式打开)。

大白话解释
场景:图片列表 → 点击图片 → 图片详情
普通做法:跳转新页面(列表消失)
拦截路由:URL 变成 /photos/1,但显示为弹窗(列表保留在背景)
目录结构(用 (..) 表示拦截)
app/playground/interception/
├── photos/
│   ├── page.tsx              ← 照片列表
│   └── [id]/
│       └── page.tsx          ← 完整详情页(直接访问时)
└── @modal/                   ← Parallel Route 插槽(@开头)
    ├── default.tsx           ← 默认(没有 modal 时返回 null)
    └── (..)photos/
        └── [id]/
            └── page.tsx      ← ⭐ 拦截路由(弹窗形式)
(..) 的含义

类似文件系统的相对路径:

写法含义拦截对象
(.)photos同级当前目录下的 photos
(..)photos上一级父目录的 photos
(..)(..)photos上两级祖父目录的 photos
(...)photos根级app 根目录的 photos

19.2 Parallel Routes(并行路由)

一句话:同一个布局里同时渲染多个页面,通过 @文件夹 命名约定实现。

layout.tsx 接收多个插槽
// app/playground/interception/layout.tsx
export default function InterceptionLayout({
  children,   // 主内容
  modal,      // ⭐ 弹窗插槽(@modal)
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <div>
      {children}
      {modal}  {/* 没有 modal 时由 default.tsx 返回 null */}
    </div>
  );
}

19.3 Catch-all 路由(捕获所有路径段)

📁 项目路径:[src/app/api/files/[…slug]/route.ts](file:///d:/11-练习/my-next-app/src/app/api/files/[…slug]/route.ts)

写法含义示例
[slug]单段动态/files/a
[...slug]捕获所有/files/a/b/c → slug = [“a”,“b”,“c”]
[[...slug]]可选捕获/files 也能匹配(slug = [])
// app/api/files/[...slug]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ slug: string[] }> }
) {
  const { slug } = await params;
  // /api/files/images/2024/jan → slug = ["images", "2024", "jan"]
  return NextResponse.json({ path: slug.join("/") });
}

应用场景:文件系统路由、多级分类、通配 API。

19.4 三种高级路由对比

路由类型语法作用
Interception(..)folder拦截路由,URL 变但不跳转
Parallel@folder同一布局渲染多个内容
Catch-all[...slug]捕获多级路径

二十、Preview Mode 草稿预览

📁 项目路径:[src/app/preview/](file:///d:/11-练习/my-next-app/src/app/preview/page.tsx)

20.1 什么是 Preview Mode

一句话:内容编辑者通过 cookie 在自己的浏览器里预览未发布的草稿,其他访客看不到。

20.2 大白话解释

正常访客访问 /preview:
  → 看到 3 篇已发布文章(绿色)

编辑者开启 Preview Mode 后访问 /preview:
  → 看到 3 篇已发布 + 2 篇草稿(紫色虚线边框 + "[草稿]" 前缀)
  → 页面不被 SSG/ISR 缓存

20.3 实现原理(通过自定义 cookie)

// app/preview/page.tsx
import { cookies } from "next/headers";
import { getPosts } from "@/lib/cms-data";

export default async function PreviewDemoPage() {
  // ⭐ 通过 cookie 判断预览状态
  const cookieStore = await cookies();
  const isPreview = cookieStore.get("preview_mode")?.value === "true";

  // 根据模式获取不同数据
  const posts = await getPosts(isPreview);

  return (
    <div>
      {posts.map(post => (
        <article className={post.status === "draft" ? "border-dashed border-purple-300" : ""}>
          {post.status === "draft" && <span>草稿</span>}
          <h3>{post.title}</h3>
        </article>
      ))}
    </div>
  );
}

20.4 开启/退出 Preview Mode

// app/api/preview/enter/route.ts
export async function GET(request: NextRequest) {
  const response = NextResponse.redirect(new URL("/preview", request.url));
  response.cookies.set("preview_mode", "true", { path: "/" });
  return response;
}

// app/api/preview/exit/route.ts
export async function GET(request: NextRequest) {
  const response = NextResponse.redirect(new URL("/preview", request.url));
  response.cookies.delete("preview_mode");
  return response;
}

20.5 实际应用:CMS Webhook 集成

编辑在 CMS 点击"预览"
    ↓
CMS 调用 GET /api/preview/enter?slug=upcoming-features
    ↓
API 设置 cookie 并重定向到 /preview/upcoming-features
    ↓
页面读取 cookie,返回草稿内容
    ↓
预览完成 → 点击"退出预览" → GET /api/preview/exit

二十一、Automatic Static Optimization

📁 项目路径:[src/app/static-optimization/](file:///d:/11-练习/my-next-app/src/app/static-optimization/page.tsx)

21.1 什么是 Automatic Static Optimization

Next.js 自动判断页面是否可以静态生成,无需手动配置。

21.2 大白话类比

渲染方式餐厅类比
SSG(静态)预制套餐(构建时做好,秒上)
SSR(动态)现点现做(每次请求都做)
ISR预制+定期更新(每小时刷新菜单)

21.3 自动判断规则

代码特征渲染方式
没用 cookies() / headers() / searchParams✅ 自动静态
用了 cookies() / headers()❌ 动态
export const dynamic = "force-dynamic"❌ 强制动态
export const dynamic = "force-static"✅ 强制静态
export const revalidate = 60🔄 ISR

21.4 项目中的三个对比页面

// 1. 静态页面(app/static-optimization/static/page.tsx)
// 没有动态函数 → 自动静态
export default function StaticPage() {
  return <div>构建时间:{new Date().toLocaleString()}</div>;
}

// 2. 动态页面(app/static-optimization/dynamic/page.tsx)
// 使用 cookies() → 强制动态
import { cookies } from "next/headers";
export default async function DynamicPage() {
  const cookieStore = await cookies();
  return <div>当前时间:{new Date().toLocaleString()}</div>;
}

// 3. ISR 页面(app/static-optimization/isr/page.tsx)
export const revalidate = 10;  // 每 10 秒重新生成
export default function ISRPage() {
  return <div>生成时间:{new Date().toLocaleString()}</div>;
}

二十二、MDX 支持

📁 项目路径:[src/app/mdx/](file:///d:/11-练习/my-next-app/src/app/mdx/page.tsx)

22.1 什么是 MDX

MDX = Markdown + JSX,让你在 Markdown 文章里嵌入 React 组件

22.2 大白话类比

类型类比
普通 Markdown纯文本邮件(只有文字)
MDX带交互按钮的邮件(文字+投票+计数器)

22.3 配置步骤

1. 安装依赖
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx
2. 修改 next.config.ts
// next.config.ts
import createMDX from "@next/mdx";
import createNextIntlPlugin from "next-intl/plugin";

const withMDX = createMDX({
  extension: /\.(md|mdx)$/,
});

const withNextIntl = createNextIntlPlugin();

export default withMDX(withNextIntl(nextConfig));
3. 创建 mdx-components.tsx(必需!)

⚠️ 这个文件必须放在 src/ 根目录(不是 src/app/),且函数不接受参数

// src/mdx-components.tsx
import type { MDXComponents } from "mdx/types";
import { Callout, Counter, CodeBlock, VoteButtons } from "@/components/mdx-components";

const components: MDXComponents = {
  Callout,
  Counter,
  CodeBlock,
  VoteButtons,
};

export function useMDXComponents(): MDXComponents {
  return components;
}
4. 创建 .mdx 文件并 import
// app/mdx/demo/page.tsx
import DemoMDX from "./content.mdx";

export default function DemoPage() {
  return (
    <div>
      <Link href="/mdx">← 返回</Link>
      <DemoMDX />
    </div>
  );
}
<!-- app/mdx/demo/content.mdx -->
# 我的文章

这是普通文字。

<Callout type="warning">
  ⚠️ 这是嵌入的 React 组件!
</Callout>

<Counter initial={10} step={5} />

22.4 ⚠️ 常见坑

问题原因解决
页面 404page.mdxpage.tsx 重名MDX 文件改名(如 content.mdx),用 page.tsx import
useMDXComponents 报错文件位置错误 / 签名错误放在 src/ 根目录,函数不接受参数
<p> 嵌套 <p> 警告MDX 自动包 <p>,组件又用了 <p>组件内部用 <div> 代替 <p>

二十三、Absolute Imports 模块路径别名

23.1 什么是 Absolute Imports

@/ 代替冗长的相对路径,让 import 语句更清晰。

23.2 对比

// ❌ 相对路径(痛苦)
import { auth } from "../../../lib/auth";
import { UserCard } from "../../../components/UserCard";

// ✅ 绝对路径(清爽)
import { auth } from "@/lib/auth";
import { UserCard } from "@/components/UserCard";

23.3 配置(tsconfig.json)

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

@ 映射到 src 目录,项目里所有组件、lib、i18n 都可以用 @/ 引用。


二十四、API 路由进阶(CORS / Response Helpers / Segment Config)

24.1 NextResponse 响应助手

📁 项目路径:[src/app/api/response-helpers/route.ts](file:///d:/11-练习/my-next-app/src/app/api/response-helpers/route.ts)

import { NextResponse } from "next/server";

// 1. 返回 JSON(最常用)
return NextResponse.json({ data: "hello" });

// 2. 重定向(地址栏会变)
return NextResponse.redirect(new URL("/login", request.url));

// 3. 重写(地址栏不变,内部转发)
return NextResponse.rewrite(new URL("/secret", request.url));

// 4. 透传请求 + 修改 headers
const response = NextResponse.next();
response.headers.set("X-Custom", "hello");
return response;

// 5. Cookie 操作
response.cookies.set("token", "xxx", { httpOnly: true });
response.cookies.delete("token");

24.2 CORS 跨域处理

📁 项目路径:[src/app/api/cors-demo/route.ts](file:///d:/11-练习/my-next-app/src/app/api/cors-demo/route.ts)

// 允许的源(生产环境用具体域名,不要用 *)
const ALLOWED_ORIGINS = [
  "http://localhost:3000",
  "https://myapp.com",
];

const CORS_HEADERS = {
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
  "Access-Control-Max-Age": "86400",
};

function handleCors(request: NextRequest): NextResponse | null {
  const origin = request.headers.get("origin");

  // 预检请求 OPTIONS
  if (request.method === "OPTIONS") {
    const response = new NextResponse(null, { status: 204 });
    if (origin && ALLOWED_ORIGINS.includes(origin)) {
      response.headers.set("Access-Control-Allow-Origin", origin);
      Object.entries(CORS_HEADERS).forEach(([k, v]) =>
        response.headers.set(k, v)
      );
    }
    return response;
  }
  return null;
}

export async function GET(request: NextRequest) {
  const corsResponse = handleCors(request);
  if (corsResponse) return corsResponse;

  const response = NextResponse.json({ data: "hello" });
  const origin = request.headers.get("origin");
  if (origin && ALLOWED_ORIGINS.includes(origin)) {
    response.headers.set("Access-Control-Allow-Origin", origin);
  }
  return response;
}

export { handleCors as OPTIONS };

24.3 Route Segment Config

📁 项目路径:[src/app/api/segment-config-demo/route.ts](file:///d:/11-练习/my-next-app/src/app/api/segment-config-demo/route.ts)

通过 export const 配置路由行为:

// app/api/segment-config-demo/route.ts

// 1. 渲染模式
export const dynamic = "force-dynamic";  // 'auto' | 'force-dynamic' | 'force-static' | 'error'

// 2. 缓存时间(秒)
export const revalidate = 0;  // 0=不缓存,false=永久缓存,数字=秒数

// 3. 运行时
export const runtime = "nodejs";  // 'nodejs' | 'edge'

// 4. 数据中心区域
export const preferredRegion = "home";  // 'home' | 'iad1' | 'sfo1'

export async function GET(request: NextRequest) {
  return NextResponse.json({
    dynamic,  // "force-dynamic"
    revalidate,  // 0
    runtime,  // "nodejs"
    time: new Date().toISOString(),
  });
}

24.4 Segment Config 配置一览

配置可选值作用
dynamicauto / force-dynamic / force-static / error控制动态/静态
revalidate0 / false / 数字缓存时间
runtimenodejs / edge运行时环境
preferredRegionhome / iad1 / sfo1部署区域
fetchCacheauto / force-no-store / force-cachefetch 缓存策略

总结

本文通过一个完整的 Next.js 学习项目,涵盖了 24 个核心知识点

基础篇(1-8)

序号知识点核心要点
1环境搭建create-next-app、TypeScript、Tailwind、MDX
2路由系统文件路由、动态路由、layout、loading、error、客户端 Hook
3数据获取SSR、SSG、ISR、CSR、generateStaticParams
4API 路由GET/POST/PUT/DELETE、NextResponse、Cookie
5样式方案Tailwind、next/font
6中间件路由拦截、认证、i18n 路由
7图片优化next/image、remotePatterns、SVG 处理
8国际化next-intl、Server/Client Component

进阶篇(9-18)

序号知识点核心要点
9性能监控Web Vitals、useReportWebVitals
10Server ActionsuseActionState、form action
11错误处理not-found.tsx、error.tsx、reset()
12动态元数据generateMetadata、Open Graph
13SWC 编译器Rust 编写,比 Babel 快 17 倍
14Dynamic Importnext/dynamic、ssr:false、按需加载
15SSE 流式通信ReadableStream、打字机效果
16第三方 SDK 集成next/script、useEffect 注入、避坑
17嵌套 Layout状态持久化、loading 继承、Skeleton
18面包屑导航usePathname、路径段映射

高级篇(19-24)

序号知识点核心要点
19高级路由Interception(拦截)、Parallel(并行)、Catch-all
20Preview Mode草稿预览、cookie 标记、CMS 集成
21Automatic Static Optimization自动判断静态/动态、ISR
22MDX 支持Markdown + JSX、mdx-components.tsx
23Absolute Imports@/ 路径别名、tsconfig paths
24API 路由进阶CORS、Response Helpers、Segment Config

推荐学习路径(递进关系图)

Next.js 的知识点之间有明确的前置依赖关系。下面的图展示了从零基础到精通的完整路径,每一层都建立在前一层之上。

完整递进路线图
                        ┌─────────────────────────────────────────────────────────┐
                        │              第一层:基础地基(不学写不了代码)              │
                        │              预计学习时间:1-2 周                          │
                        ├─────────────────────────────────────────────────────────┤
                        │                                                         │
                        │   ① 环境搭建                                             │
                        │      ↓                                                   │
                        │   ② 文件路由系统 ← 基础中的基础                            │
                        │      ↓                                                   │
                        │   ③ Server/Client Component ← 决定代码在哪运行             │
                        │      ↓               ↓                                    │
                        │   ④ 数据获取        ⑤ layout.tsx 嵌套                      │
                        │   (SSR/SSG/ISR)    (状态持久化)                           │
                        │      ↓               ↓                                    │
                        │   ⑥ 动态路由 [param] + generateStaticParams               │
                        │      ↓                                                   │
                        │   ⑦ API Routes (GET/POST/PUT/DELETE)                     │
                        │                                                         │
                        └──────────────────────────┬──────────────────────────────┘
                                                   │
                                                   │ 基础打牢后才能学进阶
                                                   ↓
                        ┌─────────────────────────────────────────────────────────┐
                        │              第二层:进阶提升(让功能更完整)               │
                        │              预计学习时间:2-3 周                          │
                        ├─────────────────────────────────────────────────────────┤
                        │                                                         │
                        │   ⑧ loading.tsx ← 依赖 ⑤ layout                          │
                        │      ↓                                                   │
                        │   ⑨ error.tsx + not-found.tsx ← 错误边界                  │
                        │      ↓                                                   │
                        │   ⑩ 中间件 (Middleware) ← 依赖 ② 路由                     │
                        │      ↓               ↓                                    │
                        │   ⑪ 认证鉴权       ⑫ 国际化 i18n                          │
                        │   (proxy.ts)       (next-intl)                           │
                        │      ↓               ↓                                    │
                        │   ⑬ Server Actions ← 替代 API Route 的更简洁方案           │
                        │      ↓                                                   │
                        │   ⑭ 动态元数据 generateMetadata ← 依赖 ⑥ 动态路由          │
                        │      ↓                                                   │
                        │   ⑮ 图片优化 next/image                                   │
                        │      ↓                                                   │
                        │   ⑯ 样式方案 (Tailwind + CSS Modules)                    │
                        │                                                         │
                        └──────────────────────────┬──────────────────────────────┘
                                                   │
                                                   │ 功能完整后优化性能
                                                   ↓
                        ┌─────────────────────────────────────────────────────────┐
                        │              第三层:高级特性(提升体验和性能)             │
                        │              预计学习时间:2-3 周                          │
                        ├─────────────────────────────────────────────────────────┤
                        │                                                         │
                        │   ⑰ SWC 编译器 ← 理解构建原理                             │
                        │      ↓                                                   │
                        │   ⑱ Dynamic Import ← 依赖 ③ Server/Client                │
                        │      ↓                                                   │
                        │   ⑲ SSE 流式通信 ← 依赖 ⑦ API Routes                     │
                        │      ↓                                                   │
                        │   ⑳ 第三方 SDK 集成 ← 依赖 ③ (useEffect + script)        │
                        │      ↓                                                   │
                        │   ㉑ 嵌套 Layout + Skeleton ← 依赖 ⑤ ⑧                   │
                        │      ↓                                                   │
                        │   ㉒ 面包屑导航 (usePathname)                             │
                        │      ↓                                                   │
                        │   ㉓ 性能监控 (Web Vitals)                                │
                        │                                                         │
                        └──────────────────────────┬──────────────────────────────┘
                                                   │
                                                   │ 掌握高级特性后挑战顶级难度
                                                   ↓
                        ┌─────────────────────────────────────────────────────────┐
                        │              第四层:顶级特性(App Router 精髓)           │
                        │              预计学习时间:1-2 周                          │
                        ├─────────────────────────────────────────────────────────┤
                        │                                                         │
                        │   ㉔ Interception Routes ← 依赖 ② ⑤ (拦截路由)         │
                        │      ↓                                                   │
                        │   ㉕ Parallel Routes ← 依赖 ㉔ (并行插槽)               │
                        │      ↓                                                   │
                        │   ㉖ Catch-all Routes ← 依赖 ⑥ (全匹配路由)             │
                        │      ↓                                                   │
                        │   ㉗ Preview Mode ← 依赖 ⑩ 中间件 (草稿预览)             │
                        │      ↓                                                   │
                        │   ㉘ Automatic Static Optimization ← 依赖 ④ (静态判断)   │
                        │      ↓                                                   │
                        │   ㉙ MDX 支持 ← 依赖 ⑯ 样式 + ⑭ 元数据                   │
                        │      ↓                                                   │
                        │   ㉚ Absolute Imports ← 配置层,随时可学                   │
                        │      ↓                                                   │
                        │   ㉛ API 路由进阶 (CORS/Segment Config)                  │
                        │                                                         │
                        └──────────────────────────┬──────────────────────────────┘
                                                   │
                                                   │ 所有知识学完后准备上线
                                                   ↓
                        ┌─────────────────────────────────────────────────────────┐
                        │              第五层:上线部署(Going to Production)       │
                        │              预计学习时间:3-5 天                          │
                        ├─────────────────────────────────────────────────────────┤
                        │                                                         │
                        │   ㉜ 环境变量配置 (.env.local / NEXT_PUBLIC_)            │
                        │      ↓                                                   │
                        │   ㉝ CORS 跨域处理                                       │
                        │      ↓                                                   │
                        │   ㉞ Vercel 部署 (Git 推送自动部署)                       │
                        │      ↓                                                   │
                        │   ㉟ Core Web Vitals 监控 (线上性能)                      │
                        │      ↓                                                   │
                        │   ㊱ 持续集成 CI/CD                                      │
                        │                                                         │
                        └─────────────────────────────────────────────────────────┘
知识点依赖关系详解
层级知识点前置依赖为什么需要先学前置项目文件
第一层① 环境搭建一切的起点package.json
② 文件路由系统路由是 Next.js 的骨架src/app/
③ Server/Client Component决定代码在哪运行,影响一切后续全项目
④ 数据获取 (SSR/SSG/ISR)必须理解组件类型才能正确获取数据blog/[slug]/page.tsx
⑤ layout.tsx 嵌套路由系统的一部分,状态持久化基础blog/layout.tsx
⑥ 动态路由 [param]② ④需要理解路由和数据获取users/[id]/page.tsx
⑦ API Routes后端接口,路由系统的延伸src/app/api/
第二层⑧ loading.tsx需要理解 layout 才能理解层级继承blog/loading.tsx
⑨ error.tsx⑤ ⑧和 loading.tsx 同属特殊文件error.tsx
⑩ 中间件 Middleware拦截路由请求,必须先懂路由proxy.ts
⑪ 认证鉴权中间件的具体应用proxy.ts
⑫ 国际化 i18n中间件 + Cookie 实现i18n/
⑬ Server ActionsAPI Route 的进化版login/page.tsx
⑭ 动态元数据需要动态路由参数page.tsx
⑮ 图片优化需要理解组件渲染时机next.config.ts
⑯ 样式方案Tailwind 在 Client/Server 中行为不同全项目
第三层⑰ SWC 编译器理解构建原理,独立知识点next.config.ts
⑱ Dynamic Import需要理解组件类型和加载时机dynamic-import/
⑲ SSE 流式通信基于 API Route 的 ReadableStreamapi/chat/stream/
⑳ 第三方 SDK需要理解 useEffect 和 hydrationAMapLoader.tsx
㉑ 嵌套 Layout 进阶⑤ ⑧layout + loading 的综合运用blog/layout.tsx
㉒ 面包屑导航基于 usePathname 路由 HookBreadcrumb.tsx
㉓ 性能监控useReportWebVitals 是 Client HookWebVitalsMonitor.tsx
第四层㉔ Interception Routes② ⑤需要深入理解路由和布局interception/layout.tsx
㉕ Parallel Routes基于 Interception 的并行插槽interception/layout.tsx
㉖ Catch-all Routes动态路由的进阶版api/[...slug]/
㉗ Preview Mode依赖中间件设置 Cookiepreview/page.tsx
㉘ Static Optimization需要理解 SSR/SSG/ISRstatic-optimization/
㉙ MDX 支持⑯ ⑭需要样式和元数据知识mdx/
㉚ Absolute Imports配置层,随时可学tsconfig.json
㉛ API 路由进阶API Route 的延伸api/cors-demo/
第五层㉜ 环境变量配置层,随时可学.env.local
㉝ CORS 跨域API Route 安全配置api/cors-demo/
㉞ Vercel 部署全部需要项目完整才能部署
㉟ Web Vitals 线上性能监控的线上版WebVitalsMonitor.tsx
㊱ CI/CD部署的自动化
学习节奏建议
第 1-2 周 ─── 第一层:基础地基
│
│  目标:能独立写出一个带路由、数据获取、API 的页面
│  检验标准:能说出 Server/Client Component 的区别
│  关键练习:创建一个博客列表页 + 详情页
│
第 3-5 周 ─── 第二层:进阶提升
│
│  目标:能让项目功能完整(认证、错误处理、SEO、多语言)
│  检验标准:能给页面加 loading 状态、error 边界、metadata
│  关键练习:给博客加登录鉴权 + 中英文切换
│
第 6-8 周 ─── 第三层:高级特性
│
│  目标:掌握性能优化和高级交互
│  检验标准:能实现 SSE 流式回复、Dynamic Import、地图集成
│  关键练习:给博客加 AI 聊天助手 + 懒加载图表
│
第 9-10 周 ── 第四层:顶级特性
│
│  目标:掌握 App Router 最复杂的路由机制
│  检验标准:能说出 Interception Routes 的原理和使用场景
│  关键练习:实现点击图片弹出模态框(不跳转页面)
│
第 11 周 ─── 第五层:上线部署
│
│  目标:把项目部署到生产环境
│  检验标准:能在 Vercel 上访问到自己的项目
│  关键练习:配置环境变量 + 部署 + 性能监控
难度星级参考
知识点难度出错频率学习建议
文件路由多写几个页面就懂了
Server/Client Component⭐⭐⭐极高这是最容易踩坑的,务必理解透彻
数据获取 (SSR/SSG/ISR)⭐⭐⭐三种方式对比着学
API Routes⭐⭐和写 Express 差不多
中间件⭐⭐⭐注意匹配规则和执行顺序
Server Actions⭐⭐⭐新概念,需要转变思维
Interception Routes⭐⭐⭐⭐⭐最难的路由特性,多看官方文档
SSE 流式通信⭐⭐⭐⭐ReadableStream 的 API 要熟悉
缓存机制⭐⭐⭐⭐⭐极高Next.js 最难理解的部分
MDX⭐⭐配置容易出错,按步骤来
部署上线⭐⭐Vercel 基本是傻瓜式

常用命令

npm run dev      # 启动开发服务器(HMR 热更新)
npm run build    # 生产构建(SWC 编译,比 Babel 快 17 倍)
npm run start    # 启动生产服务器
npm run lint     # ESLint 代码检查

性能优化清单

优化方向项目中的实现
编译速度SWC 编译器(默认启用)
代码分割App Router 自动 + Dynamic Import
图片优化next/image / 原生 lazy loading
字体优化next/font 内联
缓存策略ISR revalidate
加载体验loading.tsx + Skeleton 组件
流式响应Route Handler + ReadableStream
第三方脚本next/script 三种策略 / useEffect 注入

💡 项目源码:Gitee 搜索 git@gitee.com:huihui-999/my-next-app.git 即可找到完整项目代码,包含本文所有示例的完整实现。


关于作者

这是一个用于系统学习 Next.js 16 的实战项目,覆盖从入门到高级的 24 个核心知识点。如果你觉得本文有帮助,欢迎点赞收藏!如有问题欢迎评论交流。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐