React: Next.js 入门
最后更新:2026-08-26
Tom 用 create-react-app 搭建了一个博客网站,上线后发现首屏加载要 3 秒,SEO 抓取不到内容,文章更新还得手动刷新 CDN 缓存。他意识到纯客户端渲染已经不够用了——需要一个能服务端渲染、静态生成、自动优化的框架。这就是 Next.js 登场的时刻。
Next.js 由 Vercel 开发维护,是目前最流行的 React 全栈框架。它解决了 React 作为纯客户端 UI 库无法处理的核心问题:路由系统、服务端渲染、静态站点生成、API 路由、图片优化、字体优化、中间件等。无论你是构建博客、电商、SaaS 还是企业级应用,Next.js 都提供了开箱即用的最佳实践。
本课先厘清 Next.js 的核心概念与架构设计。你将理解四种渲染模式的区别、App Router 的工作原理,以及 Server/Client 组件的分工策略。这些基础概念是后续深入学习数据获取、部署、综合项目实战的根基。通过 Tom 的真实迁移案例,你可以直观感受从 CRA 到 Next.js 的完整技术升级路径。
1. 你将学到
- CSR vs SSR vs SSG vs ISR 四种渲染模式的区别与选型
- Next.js App Router 基于文件系统的路由机制
- Layout 嵌套系统的设计与复用
- Server Component 与 Client Component 的分工协作
- Pages Router 与 App Router 的迁移路径
- 动态路由、catch-all 路由、API 路由的配置方法
- Loading、Error、Not Found 等路由状态组件的使用
- 从 CRA 项目迁移到 Next.js 的实操路线
- 渲染模式选择决策树:根据页面类型和数据特点选择最佳方案
- 性能指标:首屏加载时间、SEO 评分、TTFB(首字节时间)在不同渲染模式下的表现对比
2. 概念图解
Tom 在学习 Next.js 时画了一张流程图来理解请求的处理路径。用户请求到达后,Next.js 会根据页面的配置(静态/动态/增量)选择不同的渲染路径,最终返回 HTML 并在浏览器端完成交互激活。
这张图展示了四个关键决策节点:首先判断页面类型(静态/动态/增量/客户端),然后选择对应的渲染引擎,生成 HTML 返回给浏览器,最后通过 Hydration 过程让页面中的 Client Component 获得交互能力。
flowchart LR
A[用户请求] --> B{Next.js 路由匹配}
B -->|静态页面| C[SSG<br/>构建时生成]
B -->|动态页面| D[SSR<br/>请求时渲染]
B -->|增量更新| E[ISR<br/>按需重新验证]
B -->|客户端交互| F[CSR<br/>浏览器执行]
C --> G[返回 HTML]
D --> G
E --> G
F --> G
G --> H[Hydration<br/>激活交互]
H --> I[Client Component<br/>运行 JS]
3. 一个真实场景
Tom 的博客最初用 create-react-app 开发,所有页面都是客户端渲染。用户打开文章时,先下载一个空的 HTML 壳子,然后等待 JS 加载完再去 API 取数据渲染内容。这对内容型网站来说是灾难——搜索引擎爬虫只看到了空白页,首屏加载时间长达 3 秒。
他调研后发现需要一种"混合渲染"方案:博客文章用静态生成(构建时生成 HTML,CDN 分发),用户档案页用服务端渲染(每次请求生成最新数据),评论区用客户端渲染(浏览器端交互)。Next.js 恰好提供了这一切。
他花了一周时间将博客从 CRA 迁移到 Next.js,具体做了三件事:第一,将文章页面改为 SSG,通过 generateStaticParams 在构建时生成所有文章页面的 HTML,部署到 Vercel 后 CDN 直接返回,首屏加载降至 0.3 秒;第二,用户档案页改为 SSR,每次请求从数据库获取最新数据;第三,评论区用 Client Component 实现,只在浏览器端加载交互 JS。迁移完成后,网站在 Google Lighthouse 的 SEO 评分从 45 分提升到 98 分,首屏加载时间缩短了 90%。
迁移过程中 Tom 也遇到了不少坑。比如他最初在 pages/ 目录下使用 Pages Router,后来发现 App Router 更好用,花了额外时间做迁移。他建议新项目直接使用 App Router,避免二次迁移。他还发现 Server Component 中不能直接使用 useRouter 和 usePathname,需要将这些路由逻辑提取到 Client Component 中。这些经验教训让他深刻理解了 Next.js 的设计哲学——"服务端优先,客户端按需"。
(1) 渲染模式对比
Next.js 提供了四种渲染模式,每一种对应不同的场景需求。理解它们的区别是选择正确架构的关键。
CSR(客户端渲染):浏览器下载空的 HTML 壳子,然后执行 JavaScript 渲染内容。Tom 的博客最初就是这种模式。优点是交互流畅、后端压力小;缺点是首屏慢、SEO 差。适合仪表盘、管理后台等需要用户登录的交互型应用。
SSR(服务端渲染):每次用户请求时,服务端动态生成完整的 HTML 返回。Tom 用这种方式解决了博客的 SEO 问题,但每次请求都要等待服务端渲染完毕才能返回,服务器压力较大。适合新闻网站、电商商品页等需要实时数据的页面。
SSG(静态生成):在构建阶段就生成所有页面的 HTML 文件,部署到 CDN 后用户直接获取静态文件。速度最快,SEO 最好,但内容更新需要重新构建。Tom 的博客文章页面就适合这种模式——文章写完后内容就固定了。适合博客、文档站、营销页面。
ISR(增量静态生成):是 SSG 的增强版,允许在构建后按需重新验证并更新特定页面,无需重建整个站点。例如 Tom 的产品目录页设置每 60 秒重新验证一次,有新商品时最多延迟 60 秒就能展示。适合电商产品页、内容频繁更新的站点。
选择策略可以概括为:内容优先用 SSG,数据实时用 SSR,交互密集用 CSR,更新频繁但不想重建全部用 ISR。
渲染模式性能对比表
| 指标 | SSG | ISR | SSR | CSR |
|---|---|---|---|---|
| 首屏速度 | 最快 | 快 | 中等 | 慢 |
| SEO 友好度 | 高 | 高 | 高 | 低 |
| 数据实时性 | 构建时固定 | 按需更新 | 每次请求最新 | 浏览器加载后 |
| 服务器负载 | 无 | 低 | 高 | 低 |
| 适用场景 | 博客/文档 | 电商/新闻 | 个性化页面 | 后台管理 |
| 典型 TTFB | <50ms | <100ms | 200-500ms | 200-500ms |
▶ 示例 1:四种渲染模式在 Next.js 中的实现
下面的代码展示了 Tom 博客中三种渲染模式的实现。注意 generateStaticParams 在构建时被调用,返回所有可能的参数组合,Next.js 会据此生成所有页面的静态 HTML。dynamic = 'force-dynamic' 则强制页面在每次请求时重新渲染。
// === 静态生成 SSG(默认行为) ===
// app/blog/[slug]/page.tsx
// 构建时自动扫描所有文章 slug,生成对应的 HTML 页面
// 生成后 CDN 直接返回静态文件,不需要服务端渲染
export async function generateStaticParams() {
// 构建时调用:获取所有文章列表
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
// 返回 slug 数组,Next.js 会为每个 slug 生成一个 page
return posts.map((post: any) => ({ slug: post.slug }))
}
async function BlogPost({ params }: { params: { slug: string } }) {
// 每个页面在构建时独立获取数据
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json())
return (
<article>
<h1>{post.title}</h1>
<time>{post.publishedAt}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
export default BlogPost
// === 服务端渲染 SSR(动态渲染) ===
// app/dashboard/page.tsx
// 每次用户请求都从 API 获取最新数据,确保数据的实时性
export const dynamic = 'force-dynamic' // 强制关闭静态缓存,每次都走 SSR
async function DashboardPage() {
// 每次请求都会执行这个 fetch
const stats = await fetch('https://api.example.com/dashboard/stats').then(r => r.json())
return (
<div>
<h1>实时仪表盘</h1>
<p>当前在线用户:{stats.onlineUsers}</p>
<p>今日订单:{stats.todayOrders}</p>
<p>本月收入:${stats.monthlyRevenue}</p>
</div>
)
}
export default DashboardPage
// === 增量静态生成 ISR ===
// app/products/[id]/page.tsx
// 构建时生成静态页面,但每 60 秒后首次访问会触发重新生成
async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 60 } // ISR:60 秒后重新验证
}).then(r => r.json())
return (
<div>
<h1>{product.name}</h1>
<p>价格:${product.price}</p>
<p>库存:{product.stock}</p>
<p>描述:{product.description}</p>
</div>
)
}
export default ProductPage
Tom 在博客中选择了 SSG + ISR 的混合方案:文章页面用 SSG 确保首页速度,产品列表页用 ISR 每 60 秒更新一次价格和库存。这样既保证了 SEO 评分,又兼顾了数据时效性。
(2) App Router 文件路由
Next.js 13+ 引入了 App Router,基于文件系统自动生成路由。你只需要在 app/ 目录下创建文件夹和 page.tsx 文件,Next.js 就会自动映射对应的 URL 路径。不再需要手动维护路由配置文件,目录结构就是路由结构。
核心文件类型
page.tsx— 定义路由对应的 UI 页面,一个文件夹下只能有一个layout.tsx— 定义该路由段及所有子路由的共享布局,支持嵌套loading.tsx— 路由段加载时显示的 UI,基于 React Suspenseerror.tsx— 路由段发生错误时显示的 UI,配合 error.tsx 的 reset 函数可重试not-found.tsx— 404 页面,可精确到每个路由段route.ts— 定义 API 端点,支持 GET/POST/PUT/DELETE 等方法
动态路由与高级模式
使用 [param] 语法创建动态路由,例如 app/blog/[slug]/page.tsx 匹配 /blog/hello-world。使用 [...param] 的 catch-all 语法匹配多级路径,例如 app/docs/[...slug]/page.tsx 匹配 /docs/guide/getting-started/installation。
Layout 文件可以实现深度嵌套布局——父 Layout 包裹子页面的内容。导航栏、侧边栏、页脚等公共部分只需写一次,Next.js 会自动维护 Layout 的持久化状态。切换页面时 Layout 不会被重新渲染,只有子 page 会更新。
▶ 示例 2:完整的博客路由结构
这里展示了 Tom 博客的完整 App Router 目录结构。注意每个文件夹下的特殊文件名(page、layout、loading、error、not-found)都是 Next.js 的约定——框架会自动识别这些文件并赋予它们特定的路由行为。dashboard/layout.tsx 会嵌套在根 layout.tsx 内部,形成两层布局:最外层是全局导航+页脚,内层是仪表盘专属侧边栏。
app/
├── page.tsx # → / 首页
├── layout.tsx # → 全局布局(导航 + 页脚)
├── loading.tsx # → 全局加载态(页面过渡时的骨架屏)
├── error.tsx # → 全局错误页(500 错误的兜底 UI)
├── not-found.tsx # → 404 页面
├── about/
│ └── page.tsx # → /about 关于页
├── blog/
│ ├── page.tsx # → /blog 文章列表页
│ └── [slug]/ # → 动态路由:匹配 /blog/任意值
│ ├── page.tsx # → /blog/hello-world 文章详情
│ └── loading.tsx # → 文章详情页的专属加载态
├── dashboard/
│ ├── page.tsx # → /dashboard 仪表盘总览
│ ├── layout.tsx # → 仪表盘专属布局(带侧边栏导航)
│ └── settings/
│ └── page.tsx # → /dashboard/settings 设置页面
└── api/
└── hello/
└── route.ts # → /api/hello API 端点
路由映射对照表
| 文件路径 | 对应 URL | 说明 |
|---|---|---|
app/page.tsx |
/ |
首页 |
app/about/page.tsx |
/about |
静态路由 |
app/blog/[slug]/page.tsx |
/blog/:slug |
动态路由 |
app/blog/[slug]/loading.tsx |
/blog/:slug |
同一路由的加载态 |
app/dashboard/layout.tsx |
/dashboard/* |
嵌套布局 |
app/api/hello/route.ts |
/api/hello |
API 端点 |
// app/layout.tsx - 全局布局
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>
<header>
<nav>
<a href="/">首页</a>
<a href="/blog">博客</a>
<a href="/about">关于</a>
</nav>
</header>
<main>{children}</main>
<footer>© 2026 Tom 的博客. All rights reserved.</footer>
</body>
</html>
)
}
// app/dashboard/layout.tsx - 仪表盘专属布局(嵌套在全局布局内)
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: 240, background: '#f5f5f5', padding: 16 }}>
<nav>
<a href="/dashboard">总览</a>
<a href="/dashboard/settings">设置</a>
</nav>
</aside>
<section style={{ flex: 1, padding: 16 }}>{children}</section>
</div>
)
}
// app/blog/[slug]/loading.tsx - 文章详情加载态
export default function Loading() {
return (
<div style={{ padding: 24 }}>
<div style={{ height: 32, width: '60%', background: '#eee', borderRadius: 4, marginBottom: 16 }} />
<div style={{ height: 16, width: '30%', background: '#eee', borderRadius: 4, marginBottom: 24 }} />
<div style={{ height: 200, background: '#eee', borderRadius: 4 }} />
</div>
)
}
(3) Server Component 与 Client Component
Next.js 的一个革命性设计是 Server Component。这是在 React 生态中第一次将组件分为服务端和客户端两个执行环境。默认情况下,App Router 中的所有组件都是 Server Component——它们在服务端执行,可以直接访问数据库、文件系统、敏感环境变量,最重要的是不会发送任何 JavaScript 到浏览器。
当你需要一个交互功能时(点击按钮、输入文本、使用 useState/useEffect、监听窗口事件等),单纯的服务端组件就不够了。这时需要在文件顶部加上 'use client' 声明,Next.js 会将该组件及其所有子组件标记为 Client Component,打包发送到浏览器端执行。
Server Component 的特点
- 可以
async/await直接获取数据,无需 useEffect 或 SWR - 可以直接访问数据库、文件系统、后端服务
- 不发送 JS 到浏览器,减少包体积
- 支持环境变量(非 NEXT_PUBLIC_ 前缀)的安全访问
- 不能使用 useState、useEffect、onClick 等浏览器 API
Client Component 的特点
- 完整的浏览器交互能力(状态、事件、DOM API)
- 可以和 Server Component 自由组合
- 仍然支持 SSR(服务端生成初始 HTML)
- 文件越大,发送到浏览器的 JS 越多
最佳实践模式
采用"Server Component 做容器、Client Component 做叶子"的分层架构。外层 Server Component 负责获取数据和布局,内层 Client Component 只负责有交互需求的小部件。这样大部分逻辑和数据获取都在服务端完成,浏览器只运行最小必要的 JavaScript。
▶ 示例 3:Server/Client 组件的最佳分工
下面代码展示了 Tom 博客中"数据获取在服务端、交互在浏览器"的分层模式。PostsPage 是 Server Component——它在服务端 fetch 数据,渲染成 HTML 后直接发送给浏览器,用户看到的是完整页面,不需要等待 JS 加载。PostList 是 Client Component——它接收服务端准备好的数据,只负责浏览器端的搜索过滤和删除交互。注意 'use client' 只在需要交互的"叶子组件"上标注,而不是整个页面。
// app/posts/page.tsx - Server Component(默认)
// 这个组件在服务端执行,不发送任何 JS 到浏览器
import PostList from './PostList'
async function PostsPage() {
// 服务端直接 fetch,数据在构建 HTML 时就已准备好
// 用户看到的是完整页面,没有 loading 状态
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return (
<div>
<h1>全部文章</h1>
<PostList posts={posts} />
</div>
)
}
export default PostsPage
// app/posts/PostList.tsx - 标注 'use client' 成为 Client Component
// 只有这个文件会发送 JS 到浏览器,包含它的父组件(Server Component)不会
'use client'
import { useState } from 'react'
interface Post {
id: number
title: string
body: string
}
function PostList({ posts: initialPosts }: { posts: Post[] }) {
// useState 只在 'use client' 组件中可用
const [posts, setPosts] = useState(initialPosts)
const [search, setSearch] = useState('')
const filtered = posts.filter(p =>
p.title.toLowerCase().includes(search.toLowerCase())
)
function handleDelete(id: number) {
setPosts(prev => prev.filter(p => p.id !== id))
}
return (
<div>
<input
type="text"
placeholder="搜索文章..."
value={search}
onChange={e => setSearch(e.target.value)}
style={{ width: '100%', padding: 8, marginBottom: 16, border: '1px solid #ddd', borderRadius: 4 }}
/>
{filtered.map(post => (
<div key={post.id} style={{
border: '1px solid #ddd', padding: 16, marginBottom: 8, borderRadius: 8,
display: 'flex', justifyContent: 'space-between', alignItems: 'center'
}}>
<div>
<h2 style={{ margin: 0 }}>{post.title}</h2>
<p style={{ margin: '8px 0', color: '#666' }}>{post.body}</p>
</div>
<button
onClick={() => handleDelete(post.id)}
style={{ padding: '4px 12px', background: '#ff4444', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}
>
删除
</button>
</div>
))}
{filtered.length === 0 && <p>没有找到匹配的文章</p>}
</div>
)
}
export default PostList
// pages/old-posts.js - Pages Router 模式(老项目兼容)
// 如果你维护的是 Next.js 12 或更早的项目,Pages Router 使用 getStaticProps/getServerSideProps
export async function getStaticProps() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return { props: { posts }, revalidate: 60 }
}
export default function OldPostsPage({ posts }: { posts: Post[] }) {
return (
<div>
<h1>Pages Router 模式</h1>
{posts.map(p => <p key={p.id}>{p.title}</p>)}
</div>
)
}
Server Component 与 Client Component 的限制
| 能力 | Server Component | Client Component |
|---|---|---|
| 使用 useState/useEffect | ❌ | ✅ |
| 使用 onClick/onChange | ❌ | ✅ |
| 直接访问数据库 | ✅ | ❌ |
| 访问环境变量(无前缀) | ✅ | ❌ |
| 发送 JS 到浏览器 | ❌ | ✅ |
| 使用 async/await | ✅ | ❌ |
| 使用 useRouter/usePathname | ❌ | ✅ |
| 使用 requestAnimationFrame | ❌ | ✅ |
选择流程图:Server Component 还是 Client Component?
组件需要交互(点击、输入、滚动等)?
├── 是 → 需要 useState/useEffect?
│ ├── 是 → 加 'use client' → Client Component
│ └── 否 → 是否只接收 props 渲染?
│ ├── 是 → 保留 Server Component
│ └── 否 → 加 'use client'
└── 否 → 保留 Server Component
4. Tom 的迁移路线图
前面学完了 Next.js 的全部核心概念。接下来把这些知识整合起来,看看 Tom 如何将他的 CRA 博客一步步迁移到 Next.js。这个过程也是你在实际项目中采用 Next.js 的标准路线。
Tom 将博客从 CRA 迁移到 Next.js 的完整过程分为五个步骤,每一步都对应本课的核心知识点:
- 第一步:项目初始化 — 用
create-next-app创建新项目,选择 App Router,将旧的src/代码迁移到app/目录。这一步的重点是理解 App Router 和 Pages Router 的目录差异 - 第二步:路由重构 — 将 CRA 的
react-router-dom路由配置替换为 App Router 文件路由。之前的手动路由配置(<Routes><Route path="..." /></Routes>)变成了直观的文件夹结构。动态路由用[param]语法代替了:param语法 - 第三步:按页面选择渲染模式 — 对不同类型的页面选择合适的渲染策略:博客文章用 SSG(加
generateStaticParams预生成),用户档案用 SSR(加dynamic = 'force-dynamic'),评论区用 CSR + Client Component(加'use client') - 第四步:Layout 整合 — 将原来每个页面重复的导航栏和页脚代码提取到全局 Layout 中,减少重复代码 60%。仪表盘区域使用嵌套 Layout 添加侧边栏导航,做到布局的职责分离
- 第五步:优化与测试 — 添加
loading.tsx实现骨架屏加载态、error.tsx实现错误兜底 UI。用 Chrome Lighthouse 验证 SEO 和性能指标,确保迁移后的 SEO 评分达到 90+
▶ 示例 4:App Router loading.tsx 和 error.tsx
// app/dashboard/loading.tsx - 自动显示的加载状态
export default function DashboardLoading() {
return (
<div style={{ padding: 24 }}>
<div style={{ height: 32, width: 200, background: '#f0f0f0', borderRadius: 4, marginBottom: 16 }} />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
{[1, 2, 3].map(i => (
<div key={i} style={{
height: 120, background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%', borderRadius: 8, animation: 'shimmer 1.5s infinite',
}} />
))}
</div>
</div>
)
}
// app/dashboard/error.tsx - 自动显示的错误状态
'use client'
export default function DashboardError({ error, reset }) {
return (
<div style={{ padding: 40, textAlign: 'center' }}>
<h2>Something went wrong!</h2>
<p style={{ color: '#999' }}>{error.message}</p>
<button onClick={reset} style={{ padding: '8px 24px', cursor: 'pointer' }}>Try again</button>
</div>
)
}
// app/dashboard/page.tsx - 会自动使用上面的 loading 和 error
async function DashboardPage() {
const data = await fetch('https://api.example.com/dashboard').then(r => r.json())
return (
<div>
<h1>Dashboard</h1>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
{data.stats.map(stat => (
<div key={stat.label} style={{ padding: 16, border: '1px solid #eee', borderRadius: 8 }}>
<p style={{ color: '#999', margin: 0 }}>{stat.label}</p>
<p style={{ fontSize: 24, fontWeight: 'bold', margin: '4px 0 0' }}>{stat.value}</p>
</div>
))}
</div>
</div>
)
}
export default DashboardPage
▶ 示例 5:Layout 嵌套与模板
// app/layout.tsx - 根布局(必须有 html 和 body)
export default function RootLayout({ children }) {
return (
<html lang="zh">
<body style={{ margin: 0, fontFamily: 'sans-serif' }}>
{children}
</body>
</html>
)
}
// app/(dashboard)/layout.tsx - 仪表盘布局(路由组不影响 URL)
import Sidebar from '@/components/Sidebar'
export default function DashboardLayout({ children }) {
return (
<div style={{ display: 'flex', minHeight: '100vh' }}>
<Sidebar />
<main style={{ flex: 1, padding: 24 }}>
{children}
</main>
</div>
)
}
// app/(auth)/layout.tsx - 认证页面布局(居中卡片)
export default function AuthLayout({ children }) {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#f5f5f5' }}>
<div style={{ background: 'white', padding: 32, borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.1)', width: 400 }}>
{children}
</div>
</div>
)
}
// app/(auth)/login/page.tsx - 登录页
function LoginPage() {
return (
<>
<h2 style={{ textAlign: 'center' }}>Sign In</h2>
<form style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<input type="email" placeholder="Email" style={{ padding: 8, borderRadius: 4 }} />
<input type="password" placeholder="Password" style={{ padding: 8, borderRadius: 4 }} />
<button type="submit" style={{ padding: 10, background: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>
Sign In
</button>
</form>
</>
)
}
export default LoginPage
❓ 常见问题
'use client'。一个常见模式是:Server Component 负责获取数据,把数据传给内部的 Client Component 处理交互。这样数据获取在服务端完成,交互逻辑在浏览器端运行,两全其美。初学者的常见错误是在整个页面加 'use client'——应该只给最小的交互叶子节点加。如果你发现一个组件既不需要交互也不需要状态,就不要加 'use client'。pages/ 目录,通过文件名映射路由。App Router 是 Next.js 13 引入的新路由系统,使用 app/ 目录,支持 Layout 嵌套、Server Component、流式渲染等新特性。新项目建议直接用 App Router,老项目可以逐步迁移。Pages Router 的 getStaticProps/getServerSideProps 在 App Router 中已经被 async component + fetch 替代。两个路由系统可以共存,迁移时可以逐步将页面从 Pages Router 移到 App Router。next build && next start 部署到任何 Node.js 服务器,或使用 Docker 容器化部署。Netlify、AWS Amplify、Cloudflare Pages 也都支持 Next.js 部署。但如果你使用 ISR 和 Middleware 等高级特性,Vercel 的兼容性最好。pages/ 中的页面逐步搬到 app/,每搬一个验证一个,不必一次性全部迁移。迁移时注意:getStaticProps 改为 Server Component 中直接 fetch;Layout 改为在 app/ 目录下创建 layout.tsx;pages/_app.tsx 的逻辑迁移到根 layout.tsx。建议从最简单的页面开始迁移,积累经验后再处理复杂页面。params prop 获取动态路由参数,通过 searchParams prop 获取查询字符串参数。例如 function Page({ params, searchParams }: { params: { slug: string }, searchParams: { q: string } })。但注意 Server Component 中不能使用 useRouter 或 usePathname——这些 hooks 只在 Client Component 中可用。📖 小节
- Next.js 是一个全栈 React 框架,提供路由、渲染优化、API 路由、图片优化、中间件等开箱即用的功能
- 四种渲染模式:SSG 最快适合内容型、SSR 实时适合数据型、ISR 折中方案(按需重新验证)、CSR 适合交互密集型应用
- 性能对比:SSG 首屏最快无服务端负载,SSR 数据实时但服务器压力大,ISR 兼顾速度与实时性,CSR 适合后台交互场景
- App Router 基于文件系统路由,
app/目录下的page.tsx/layout.tsx/loading.tsx/error.tsx/not-found.tsx各自对应一个路由能力 - 动态路由使用
[param]语法,catch-all 使用[...param]语法;API 路由使用route.ts文件 - Layout 支持深度嵌套且状态持久化——切换子页面不会销毁正在加载的 Layout
- Server Component 默认在服务端执行,不发送 JS 到浏览器;需要交互的组件加
'use client'声明为 Client Component - 分层架构:Server Component 做数据容器,Client Component 做交互叶子节点,最大限度减少浏览器端 JavaScript 体积
- Pages Router 的
getStaticProps/getServerSideProps在 App Router 中被async component+fetch替代 - 选择渲染模式的决策路径:页面内容是否频繁变化?用户是否需要实时数据?页面是否需要 SEO?综合这些因素选择 SSG/ISR/SSR/CSR
- 迁移路线:CRA ->
create-next-app创建 -> 迁移组件到app/目录 -> 按页面选择渲染模式 -> 提取 Layout -> 添加 loading/error 状态 - 本课是后续 Next.js 数据获取、部署 CI/CD、组件库集成、综合项目实战等课程的基础
- 牢记 Tom 的教训:新项目直接选 App Router,避免二次迁移的成本
📝 作业
-
使用
npx create-next-app@latest my-blog创建一个新项目,确认使用了 App Router 目录结构。在app/下创建/about、/blog、/blog/[slug]三个页面路由,每个页面至少包含一个标题和一段说明文字。运行npm run dev并访问每个路由(/about、/blog、/blog/test-article),验证映射是否正确。 -
实现一个全局 Layout(
app/layout.tsx),包含顶部导航栏(首页、博客、关于三个链接)和底部页脚。再为/blog/*路由创建嵌套 Layout,在文章详情页左侧显示文章目录列表。打开浏览器的 React DevTools,确认 Layout 的嵌套层级是否正确,切换页面时导航栏和侧边栏是否保持状态而不会重新渲染。 -
将博客首页改为 Server Component,直接从 JSONPlaceholder API(
https://jsonplaceholder.typicode.com/posts)获取文章列表并渲染。然后创建一个 Client Component 实现关键词搜索过滤功能,让用户在浏览器端可以实时搜索过滤文章。确保 Server Component 负责数据获取,Client Component 只负责交互过滤逻辑。完成后再添加一个loading.tsx(展示骨架屏)和error.tsx(显示错误提示和重试按钮),体验完整的路由段配置。 -
选做:将博客部署到 Vercel(免费),在 Vercel 控制台观察 ISR 的重新验证效果——修改 API 返回的数据后,观察页面在
revalidate时间到期后是否自动更新。