← 返回文章列表
Next.js 全指南:从入门到精通
基于费曼学习法、西蒙学习法、SQ3R 阅读法和康奈尔笔记法,系统掌握 Next.js 的核心概念与实战技巧。
一、概览与提问(SQ3R · Survey & Question)
SQ3R 第一步:快速浏览全貌,提出关键问题。
什么是 Next.js?
Next.js 是由 Vercel 开发和维护的 React 全栈框架。它不仅仅是一个前端库的封装,而是一套完整的应用开发解决方案——从路由、渲染、数据获取到部署,覆盖了 Web 应用开发的全部生命周期。
Next.js 诞生的背景很明确:React 本身只是一个 UI 库,要构建一个完整的 Web 应用,开发者还需要自己处理路由、服务端渲染、代码分割、数据获取等一系列问题。Next.js 的出现就是为了填补这些空白,让开发者能用 React 快速构建生产级别的应用。
当前最新版本为 Next.js 16(基于文档版本 16.2.6),引入了 Cache Components、Partial Prerendering(PPR) 等重大特性,架构模型发生了根本性变化。
核心问题
- Next.js 适合什么场景?——从静态博客到动态 SaaS 平台,从内容站到电商平台,Next.js 都能胜任
- Next.js 与同类技术相比有什么优势?——内置 SSR/SSG/ISR、文件系统路由、自动代码分割、React Server Components 原生支持
- 学习 Next.js 需要什么基础?——HTML、CSS、JavaScript 和 React 基本知识
技术全景图
Next.js 的架构可以看作一棵分层树:
Next.js 应用
├── App Router (/app) ← 推荐的新路由系统
│ ├── 布局系统 (layout.js) ← 共享布局,不会重新渲染
│ ├── 页面系统 (page.js) ← 路由入口,每个路由一个
│ ├── 加载状态 (loading.js) ← 自动 Suspense 边界
│ ├── 错误处理 (error.js) ← 错误边界
│ ├── API 路由 (route.js) ← 服务端 API 端点
│ └── 特殊文件 (not-found.js 等)
├── Server Components ← 默认,服务端渲染,零 JS 发送到客户端
├── Client Components ("use client") ← 客户端交互,需要时使用
├── Cache Components ("use cache") ← Next.js 16 新增,缓存策略
├── 数据获取 (fetch / ORM) ← Server Components 中直接 await
├── Server Actions ("use server") ← 服务端函数,表单提交
└── next.config.ts ← 全局配置
二、用最简单的话说清楚(费曼学习法)
费曼学习法核心理念:如果你不能用简单的语言解释一件事,说明你还没有真正理解它。
核心概念讲解
1. 文件系统路由
Next.js 的路由完全由文件夹结构决定。你在 app/ 目录下创建一个文件夹,它就是一个路由。文件夹里放一个 page.js,它就是这个路由的页面。
// app/page.tsx → 对应路由 /
export default function HomePage() {
return <h1>这是首页</h1>;
}
// app/about/page.tsx → 对应路由 /about
export default function AboutPage() {
return <h1>关于我们</h1>;
}
// app/blog/[id]/page.tsx → 对应路由 /blog/123(动态路由)
export default async function BlogPost({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
return <h1>文章 {id}</h1>;
}不需要写任何路由配置文件,文件夹结构就是路由表。
2. 布局(Layout)
布局是一个共享的外壳,它包裹着所有子页面。最关键的是:导航时布局不会重新渲染。
// app/layout.tsx → 根布局(必须有 html 和 body 标签)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>
<nav>导航栏(导航时不重新渲染)</nav>
<main>{children}</main>
</body>
</html>
);
}3. Server Components 和 Client Components
这是 Next.js 最核心的概念。默认情况下,所有组件都是 Server Components——它们在服务端运行,不会发送任何 JavaScript 到客户端。当你需要交互(点击事件、状态管理、浏览器 API)时,才使用 Client Components。
// Server Component(默认)- 在服务端运行,发送 HTML 到客户端
// app/page.tsx
import { db } from "@/lib/db";
import LikeButton from "./like-button";
export default async function Page() {
const posts = await db.query("SELECT * FROM posts"); // 直接访问数据库
return (
<div>
{posts.map((post) => (
<p key={post.id}>{post.title}</p>
))}
<LikeButton /> {/* 交互部分交给 Client Component */}
</div>
);
}// Client Component - 需要交互时添加 "use client"
// app/like-button.tsx
"use client";
import { useState } from "react";
export default function LikeButton() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>赞 {count}</button>;
}4. 数据获取
在 Server Components 中,数据获取变得极其简单——直接用 async/await。
// Server Component 中直接 fetch
export default async function BlogPage() {
const res = await fetch("https://api.example.com/posts");
const posts = await res.json();
return (
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}不需要 useEffect,不需要 useState,不需要客户端数据获取库。代码就像在写普通的异步函数。
5. 缓存(Next.js 16 新特性)
Next.js 16 引入了 Cache Components,通过 "use cache" 指令让缓存变得声明式。
// next.config.ts 中启用
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;// 数据级缓存
import { cacheLife } from "next/cache";
export async function getUsers() {
"use cache";
cacheLife("hours"); // 缓存一小时
return db.query("SELECT * FROM users");
}// UI 级缓存
import { cacheLife } from "next/cache";
export default async function BlogPosts() {
"use cache";
cacheLife("hours");
const posts = await fetch("https://api.example.com/posts");
return <div>{/* 渲染文章列表 */}</div>;
}6. Server Actions
Server Actions 让你直接在组件中定义服务端函数,无需手动写 API 路由。
// app/actions.ts
"use server";
import { revalidateTag } from "next/cache";
export async function createPost(formData: FormData) {
await db.post.create({
data: { title: formData.get("title") as string },
});
revalidateTag("posts"); // 刷新缓存
}// 在表单中使用
import { createPost } from "./actions";
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="文章标题" />
<button type="submit">发布</button>
</form>
);
}类比与比喻
把 Next.js 应用想象成一家餐厅:
- Server Components = 后厨厨师——在厨房里准备好菜品(HTML),客人看不到烹饪过程,只能看到端上桌的成品。好处是厨房里可以用大型设备(数据库、文件系统),客人不需要在自己的桌上放这些设备。
- Client Components = 桌上的调料瓶——需要客人自己动手(交互),比如加酱油(点击按钮)、搅拌(拖动)。
- Layout = 餐厅装修——不管换哪道菜(页面导航),装修都不会变,不需要重新装修。
- Cache Components("use cache") = 预制菜——提前做好,客人点了直接上,不用现做。
- Streaming(Suspense) = 先上凉菜再上热菜——不必等所有菜做好才上桌,做好一道上一道。
- Server Actions = 服务员点单系统——客人只需填表,服务员自动传到后厨处理。
常见误解澄清
- 误解:Next.js 只能做 SSR —— 事实上它支持 SSG、SSR、ISR、CSR 和 PPR(Partial Prerendering),可以在同一个应用中混合使用。
- 误解:所有组件都需要 "use client" —— 恰恰相反,默认都是 Server Components,只有需要交互时才加
"use client"。 - 误解:Server Components 不能传 props 给 Client Components —— 完全可以,只要 props 是可序列化的(数字、字符串、对象等)。
- 误解:Next.js 16 还在用 Pages Router 的
getServerSideProps—— App Router 已经完全不同,数据获取直接await,不需要这些函数。 - 误解:缓存很难控制 —— Next.js 16 的
"use cache"+cacheLife+cacheTag让缓存变得声明式和可控。
三、锥形深入(西蒙学习法)
西蒙学习法:集中精力、目标导向、锥形深入——从核心开始,逐步扩展到周边。
第一层:核心基础
1. 创建项目
npx create-next-app@latest安装时会提示选择 TypeScript、ESLint、Tailwind CSS、src/ 目录、App Router 等。
手动安装:
npm install next@latest react@latest react-dom@latest// package.json
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}2. 项目结构
my-app/
├── app/
│ ├── layout.tsx ← 根布局
│ ├── page.tsx ← 首页 (/)
│ ├── blog/
│ │ ├── page.tsx ← /blog
│ │ └── [id]/
│ │ └── page.tsx ← /blog/123
│ ├── about/
│ │ └── page.tsx ← /about
│ └── api/
│ └── route.ts ← API 端点
├── public/ ← 静态资源
├── next.config.ts ← 配置文件
├── package.json
└── tsconfig.json
3. 页面和布局
// app/layout.tsx - 根布局(必需)
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>{children}</body>
</html>
);
}// app/blog/layout.tsx - 嵌套布局
export default function BlogLayout({ children }: { children: React.ReactNode }) {
return (
<section>
<h2>博客</h2>
{children}
</section>
);
}4. 链接和导航
import Link from "next/link";
export default function NavBar() {
return (
<nav>
<Link href="/">首页</Link>
<Link href="/about">关于</Link>
<Link href="/blog/123">文章</Link>
</nav>
);
}<Link> 组件会自动预取页面,实现即时导航。
5. 动态路由
// app/blog/[id]/page.tsx
export default async function BlogPost({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
return <h1>文章 ID: {id}</h1>;
}Catch-all 路由:[...slug] 匹配多个路径段,[[...slug]] 是可选的 catch-all。
6. 加载状态
// app/blog/loading.tsx - 自动 Suspense 边界
export default function Loading() {
return <div>加载中...</div>;
}当 blog/page.tsx 在服务端获取数据时,用户会立即看到 "加载中...",数据就绪后自动替换。
7. 错误处理
// app/error.tsx - 必须是 Client Component
"use client";
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>出错了!</h2>
<button onClick={reset}>重试</button>
</div>
);
}8. API 路由(Route Handlers)
// app/api/posts/route.ts
import { NextResponse } from "next/server";
export async function GET() {
const posts = await db.query("SELECT * FROM posts");
return NextResponse.json(posts);
}
export async function POST(request: Request) {
const body = await request.json();
const post = await db.post.create({ data: body });
return NextResponse.json(post, { status: 201 });
}第二层:进阶用法
1. Server Components 和 Client Components 的组合模式
最常见的模式是:Server Component 获取数据,通过 props 传递给 Client Component 处理交互。
// app/page.tsx(Server Component)
import LikeButton from "./like-button";
import { getPost } from "@/lib/data";
export default async function Page() {
const post = await getPost("1");
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<LikeButton likes={post.likes} />
</article>
);
}还可以通过 children prop 将 Server Component 嵌套在 Client Component 中:
// app/ui/modal.tsx(Client Component)
"use client";
export default function Modal({ children }: { children: React.ReactNode }) {
return <div className="modal">{children}</div>;
}// app/page.tsx(Server Component)
import Modal from "./ui/modal";
import Cart from "./ui/cart"; // Cart 是 Server Component
export default function Page() {
return (
<Modal>
<Cart /> {/* Server Component 作为 children 传入 Client Component */}
</Modal>
);
}2. Context Providers
React Context 不支持 Server Components,需要在 Client Component 中创建 Provider:
// app/theme-provider.tsx
"use client";
import { createContext } from "react";
export const ThemeContext = createContext("light");
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>;
}// app/layout.tsx
import ThemeProvider from "./theme-provider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}3. Streaming 和 Suspense
Suspense 是 Next.js 中流式渲染的核心。它让你可以把页面拆分成多个独立加载的块:
import { Suspense } from "react";
export default function Page() {
return (
<>
<h1>我的博客</h1>
{/* 这部分立即显示 */}
<p>欢迎来到我的博客</p>
{/* 这部分在数据加载完成后流式显示 */}
<Suspense fallback={<div>加载文章中...</div>}>
<BlogPosts />
</Suspense>
<Suspense fallback={<div>加载评论中...</div>}>
<Comments />
</Suspense>
</>
);
}4. 数据获取模式
并行获取(推荐):
// 多个请求同时发起
export default async function Page() {
const artistPromise = getArtist();
const albumsPromise = getAlbums();
const [artist, albums] = await Promise.all([artistPromise, albumsPromise]);
return (
<div>
<h1>{artist.name}</h1>
<AlbumList albums={albums} />
</div>
);
}顺序获取(当请求有依赖关系时):
export default async function Page() {
const artist = await getArtist(); // 先获取 artist
const playlists = await getPlaylists(artist.id); // 再用 artist.id 获取
return <div>{/* ... */}</div>;
}5. 缓存与 Revalidation(Next.js 16)
Next.js 16 的缓存系统基于 "use cache" 指令:
import { cacheLife, cacheTag } from "next/cache";
// 数据级缓存 + 标签
export async function getProducts() {
"use cache";
cacheLife("hours"); // 缓存存活时间
cacheTag("products"); // 缓存标签,用于按需刷新
return db.query("SELECT * FROM products");
}时间基准刷新:
"use cache";
cacheLife("seconds"); // 每秒刷新
cacheLife("minutes"); // 每分钟刷新
cacheLife("hours"); // 每小时刷新
cacheLife("days"); // 每天刷新
cacheLife("weeks"); // 每周刷新
cacheLife("max"); // 最大缓存按需刷新:
// app/actions.ts
"use server";
import { revalidateTag, revalidatePath, updateTag } from "next/cache";
export async function refreshProducts() {
revalidateTag("products"); // 刷新带 'products' 标签的所有缓存
}
export async function refreshPage() {
revalidatePath("/products"); // 刷新 /products 路径的所有缓存
}
export async function updateProducts() {
updateTag("products"); // 立即过期并重新生成
}6. 运行时 API 与缓存
运行时 API(cookies()、headers()、searchParams)需要请求级别的信息。在 Next.js 16 中,使用它们的组件需要用 <Suspense> 包裹:
import { cookies } from "next/headers";
import { Suspense } from "react";
async function UserGreeting() {
const cookieStore = await cookies();
const theme = cookieStore.get("theme")?.value || "light";
return <p>你的主题: {theme}</p>;
}
export default function Page() {
return (
<>
<h1>仪表盘</h1>
<Suspense fallback={<p>加载中...</p>}>
<UserGreeting />
</Suspense>
</>
);
}7. Metadata 和 SEO
// app/layout.tsx - 静态 metadata
export const metadata = {
title: "我的网站",
description: "这是我的网站描述",
};
// app/blog/[id]/page.tsx - 动态 metadata
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await getPost(id);
return {
title: post.title,
description: post.excerpt,
};
}第三层:深度解析
1. 渲染架构:Partial Prerendering(PPR)
Next.js 16 的默认渲染模式是 Partial Prerendering。在构建时,Next.js 会预渲染整个组件树,根据每个组件使用的 API 来决定如何处理:
"use cache"的组件 → 结果被缓存,包含在静态 Shell 中<Suspense>包裹的组件 → fallback 包含在静态 Shell 中,内容在请求时流式传输- 纯确定性操作 → 自动包含在静态 Shell 中
这意味着用户访问页面时,会立即看到静态的页面框架(导航栏、布局、缓存的内容),然后个性化内容通过流式传输逐步加载。
2. RSC Payload
React Server Components 渲染后会生成一种特殊的二进制格式——RSC Payload。它包含:
- Server Components 的渲染结果
- Client Components 的占位符和 JS 文件引用
- 从 Server Component 传递给 Client Component 的 props
在首次加载时,HTML 提供即时预览,RSC Payload 用于协调组件树,JavaScript 用于水合 Client Components。后续导航则直接使用 RSC Payload,不需要重新获取 HTML。
3. 环境隔离与安全
Next.js 在服务端和客户端之间建立了严格的安全边界:
- 环境变量只有
NEXT_PUBLIC_前缀的才会暴露给客户端 - 即使不慎在客户端导入了服务端代码,敏感变量也会被替换为空字符串
- 可以使用
server-only包在构建时报错阻止客户端导入服务端代码 - 可以使用
client-only包阻止服务端导入客户端代码
import "server-only"; // 阻止客户端导入
export async function getData() {
const res = await fetch("https://api.example.com/data", {
headers: { authorization: process.env.API_KEY },
});
return res.json();
}4. 减小客户端 JS 体积的最佳实践
// 正确做法:只在需要交互的组件上加 "use client"
// app/layout.tsx - 这是 Server Component
import Search from "./search"; // Client Component
import Logo from "./logo"; // Server Component
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<Search /> {/* 只有搜索栏是 Client Component */}
</nav>
<main>{children}</main>
</>
);
}将 "use client" 尽可能下推到叶子组件,减少客户端 JavaScript 的体积。
5. 共享数据的模式:React.cache + Context
在多个组件间共享数据而不重复请求:
import { cache, createContext } from "react";
// 创建缓存函数,同一请求内多次调用只执行一次
const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});React.cache 的作用域是单个请求,不同请求之间不会共享缓存。
6. 第三方组件集成
对于没有 "use client" 指令的第三方组件,创建一个包装文件:
// app/carousel.tsx
"use client";
import { Carousel } from "acme-carousel";
export default Carousel;然后在 Server Component 中直接导入这个包装后的组件即可。
7. 路由段配置
// app/api/route.ts
export const dynamic = "force-dynamic"; // 强制动态渲染
export const revalidate = 0; // 禁用缓存
export const runtime = "edge"; // 使用 Edge Runtime
export const maxDuration = 60; // 最大执行时间(秒)四、要点笔记(康奈尔笔记法)
康奈尔笔记法:将笔记分为线索栏、笔记栏和总结栏,便于复习和检索。
关键概念速查表
| 线索/关键词 | 详细笔记 |
|---|---|
| App Router | 基于 /app 目录的路由系统,支持嵌套布局、Server Components |
| Server Components | 默认组件类型,服务端渲染,不发送 JS 到客户端,可直接访问数据库 |
| Client Components | 添加 "use client" 指令,用于交互、状态管理、浏览器 API |
| Cache Components | Next.js 16 新增,"use cache" 指令实现声明式缓存 |
| PPR (Partial Prerendering) | 默认渲染模式,静态 Shell + 流式动态内容 |
| Server Actions | "use server" 定义的服务端函数,可直接在表单中使用 |
| RSC Payload | Server Components 渲染后的二进制格式,用于客户端协调 |
| Streaming | 通过 <Suspense> 实现流式渲染,先显示 fallback 再加载内容 |
cacheLife | 配合 "use cache" 设置缓存存活时间(seconds/minutes/hours 等) |
cacheTag | 为缓存设置标签,配合 revalidateTag / updateTag 按需刷新 |
| 文件系统路由 | 文件夹结构即路由,page.js 定义页面,layout.js 定义布局 |
generateStaticParams | 为动态路由预生成静态参数,用于 SSG |
核心 API 速查
| API / 配置 | 用途 | 示例 |
|---|---|---|
"use client" | 声明 Client Component | 'use client' 加在文件顶部 |
"use server" | 声明 Server Action | 'use server' 加在函数顶部或文件顶部 |
"use cache" | 声明缓存(需启用 cacheComponents) | 'use cache' 加在函数/组件顶部 |
cacheLife(profile) | 设置缓存策略 | cacheLife('hours') |
cacheTag(tag) | 为缓存打标签 | cacheTag('products') |
revalidateTag(tag) | 按标签刷新缓存 | revalidateTag('products') |
revalidatePath(path) | 按路径刷新缓存 | revalidatePath('/blog') |
updateTag(tag) | 立即过期并重新生成缓存 | updateTag('posts') |
cookies() | 读取请求 cookies | const store = await cookies() |
headers() | 读取请求 headers | const h = await headers() |
redirect(path) | 重定向 | redirect('/login') |
notFound() | 触发 404 页面 | notFound() |
<Link href> | 客户端导航,自动预取 | <Link href="/about">关于</Link> |
<Image> | 图片优化 | <Image src="/photo.jpg" alt="..." width={500} height={300} /> |
<Suspense> | 流式渲染边界 | <Suspense fallback={<Loading />}><Component /></Suspense> |
generateMetadata | 动态生成页面 metadata | export async function generateMetadata() { ... } |
generateStaticParams | 预生成动态路由参数 | export async function generateStaticParams() { ... } |
connection() | 标记需要请求级信息的代码 | await connection() 后使用 Date.now() 等 |
本节总结
Next.js 16 是一个基于 React 的全栈框架,核心围绕 App Router 构建应用。默认使用 Server Components 实现零 JS 的服务端渲染,需要交互时通过 "use client" 引入 Client Components。Next.js 16 引入的 Cache Components("use cache")和 Partial Prerendering 极大地简化了缓存策略,将渲染模型统一为"静态 Shell + 流式动态内容"。数据获取直接在 Server Components 中 async/await,无需客户端数据获取库。Server Actions 通过 "use server" 让表单提交和 mutations 回归简单。
五、复习与实践(SQ3R · Recite & Review)
SQ3R 最后两步:复述核心要点,通过实践巩固理解。
核心要点回顾
- 文件系统路由:
app/目录下的文件夹结构就是路由表,page.js是路由入口,layout.js是共享布局 - Server Components 是默认的:不需要任何指令,组件在服务端渲染,不发送 JS 到客户端
"use client"是边界:标记了客户端和服务端的分界线,该文件及其所有导入都会打包到客户端- 布局不重新渲染:导航时
layout.js保持不变,只有page.js的内容会更新 - 数据获取用
async/await:Server Components 中直接 await fetch 或 ORM 查询,不需要useEffect "use cache"+cacheLife:Next.js 16 的声明式缓存方案,替代了之前的fetch缓存选项- Streaming 是核心模式:用
<Suspense>包裹异步内容,先显示 fallback,数据就绪后流式替换 - Server Actions 简化 mutations:
"use server"定义服务端函数,直接绑定到表单的action - PPR 是默认渲染模式:静态内容预渲染,动态内容流式传输,最佳性能组合
- 环境安全:非
NEXT_PUBLIC_前缀的环境变量不会暴露给客户端,server-only包提供额外保护
动手练习
练习 1:个人博客(入门级)
创建一个简单的博客应用:
- 首页展示文章列表
- 文章详情页使用动态路由
[slug] - 添加
loading.tsx和error.tsx - 使用
generateStaticParams预生成页面
练习 2:带缓存的商品列表(中级)
构建一个商品展示页:
- 启用
cacheComponents: true - 使用
"use cache"+cacheLife('hours')缓存商品数据 - 使用
cacheTag('products')为缓存打标签 - 创建 Server Action 实现
updateTag('products')刷新缓存
练习 3:全栈仪表盘(高级)
创建一个带认证的仪表盘:
- 使用
cookies()读取 session - 用
<Suspense>包裹个性化内容,实现流式加载 - Server Components 获取数据,Client Components 处理图表交互
- 使用 Context Provider 共享主题状态
常见陷阱
- 在 Server Component 中使用
useState或onClick—— 这些是客户端 API,必须添加"use client"。解法:拆分为 Server + Client Components。 - 在 Client Component 中直接访问数据库 —— Client Component 在浏览器运行,无法访问服务端资源。解法:在 Server Component 中获取数据,通过 props 传递。
- 在 layout 中使用
cookies()却没有 Suspense 边界 —— Next.js 16 中,运行时 API 访问会导致构建错误。解法:将运行时数据访问提取到子组件中并用<Suspense>包裹。 - 忘记
await params—— Next.js 16 中params是 Promise 类型,必须await后才能使用。 - 过度使用
"use client"—— 把整个页面标记为 Client Component 会导致大量 JS 发送到客户端。解法:只在最小粒度的交互组件上使用。