快速开始
官方文档:nextjs.org/docs
路由
App Router(新版)
- 文件都在app路径下
- 获取数据的方式:服务器组件中直接从外部数据源获取数据,或者在客户端组件中使用 fetch 或 use 钩子
- 元数据:直接使用 export const metadata 导出
Page Router(旧版)
- 文件都在src/pages路径下
- 获取数据的方式:使用 getStaticProps 或 getStaticPaths 获取数据
- 元数据: 通过 next/head 导出的 Head 组件包裹
getStaticProps: // 在构建时生成静态页面
return {
props: {
data: await getData(),
},
}
}
getStaticPaths: // 在构建时生成静态页面
return {
paths: [{ params: { id: '1' } }],
}
}
预渲染(Pre Rendering)
客户端直接拿到html内容,直接渲染,不需要等待js加载完成
分类
服务器渲染(SSR)
在每个请求上生成html内容
// app/form-client/page.tsx(客户端组件)
'use client';
export default function ClientFormPage() {
return (
<div>
<h1>表单页面</h1>
</div>
);
}
渲染流程
用户请求
↓
Next.js 服务器接收请求
↓
服务器执行 React 组件(虽然是客户端组件,但第一次在服务器执行)
↓
生成 HTML
↓
返回 HTML 给浏览器
↓
浏览器显示 HTML
↓
React 客户端代码加载
↓
Hydration(接管 HTML)
静态生成(SSG)
在构建时生成静态页面
// app/form/page.tsx(服务端组件)
import FormComponent from './FormComponent';
export default function FormPage() {
return (
<div>
<h1>表单页面</h1>
<FormComponent /> {/* 客户端组件 */}
</div>
);
}
渲染流程
构建时(npm run build)
↓
服务器执行 React,生成 HTML
↓
HTML 保存到文件系统
↓
用户请求
↓
直接返回预生成的 HTML(极快!)
↓
浏览器加载 HTML
↓
React 客户端代码加载
↓
Hydration(激活客户端组件)
增量生成(ISR)
在构建时生成静态页面,在运行时生成静态页面
客户端渲染(CSR)
// 如果完全禁用 SSR(不推荐)
'use client';
export default function CSRPage() {
const [data, setData] = useState(null);
useEffect(() => {
// 完全在客户端获取数据
fetch('/api/data').then(r => r.json()).then(setData);
}, []);
if (!data) return <div>Loading...</div>;
return <div>{data}</div>;
}
| 特性 | SSG | SSR | CSR |
|---|---|---|---|
| 生成时机 | 构建时 | 每次请求 | 浏览器中 |
| 返回 HTML | ✅ 是(预生成) | ✅ 是(实时生成) | ❌ 否(空壳) |
| 可以缓存 | ✅ 是 | ❌ 否 | ✅ 是(JS) |
| 响应速度 | ⚡ 最快 | 🐢 较慢 | ⚡ 快(交互后) |
| SEO | ✅ 友好 | ✅ 友好 | ❌ 不友好 |
| 适用场景 | 静态内容、博客 | 需要实时数据 | 不需要 SEO 的页面 |
⚠️注意
- next.js 默认使用静态生成(SSG)
- next.js 允许选择每个页面使用哪种预渲染方式
什么是 Hydration?
Hydration(水合) 是 React 的一个过程:
- 服务器预渲染生成 HTML(静态的)
- 浏览器加载这个 HTML(用户立即看到内容)
- React 在浏览器中加载并"接管"这些 HTML 元素
- 这个过程就是 Hydration
Hydration Point 在哪里?
Hydration Point 是客户端组件的边界,在 HTML 中表现为:
<!-- 预渲染的 HTML -->
<div>
<h1>服务端组件内容</h1>
<!-- 这是 Hydration Point -->
<div data-reactroot="">
<form>
<input type="text" />
<button>提交</button>
</form>
</div>
<!-- 客户端组件结束 -->
</div>
<!-- 对应的 React 代码 -->
<script>
// React 会在这里"hydrate"上面的表单元素
// 让它变成可交互的组件
</script>
App Router 中的渲染模式
- SSG(静态生成)
- 没有使用动态函数(cookies(), headers(), searchParams)
- 没有设置 dynamic = 'force-dynamic'
- 没有设置 revalidate
- SSR(服务端渲染) - 强制方式
- 方式一:使用 dynamic = 'force-dynamic'
- 方式二:使用动态函数(自动触发 SSR)
- ISR(增量静态再生)
- 判断条件:设置了 revalidate
动态函数
cookies() - 读取和设置 Cookies
headers() - 读取 HTTP 请求头
searchParams - 读取 URL 中的查询字符串参数(仅在页面组件中)
dynamicParams - 读取动态路由段(如 [id], [slug])
验证方式
-
运行 npm run build,查看输出:
- ○ = 静态生成(SSG)
- λ = 动态渲染(SSR)
- ● = 部分预渲染(PPR)
-
在开发模式下,查看网络请求:
- SSG:响应头包含 x-nextjs-cache: HIT
- SSR:响应头包含 x-nextjs-cache: MISS 或没有缓存头
- ISR:响应头包含 x-nextjs-cache: UPDATE
| 渲染模式 | 数据获取时机 | 配置方式 | 适用场景 |
|---|---|---|---|
| SSG | 构建时 | 默认(无需配置) | 静态内容、博客文章 |
| SSR | 每次请求时 | dynamic = 'force-dynamic' 或使用动态函数 | 需要实时数据的页面 |
| ISR | 构建时 + 定期更新 | revalidate = 秒数 | 需要定期更新的内容 |
组件类型
服务组件
在服务器端渲染的组件,默认都是服务端组件
- 默认类型,无需标记
- 在服务器上运行
- 可以 async,直接使用 Node.js API
- 可以访问数据库、文件系统等
- 不包含客户端 JavaScript,减少包体积
客户端组件
在客户端渲染的组件
- 必须在文件顶部添加 'use client' 指令
- 在浏览器中运行
- 可以使用 React Hooks(useState, useEffect 等)
- 可以处理用户交互(onClick, onChange 等)
- 可以使用浏览器 API(window, localStorage 等)
混合使用
- 服务端组件可以导入客户端组件
- 客户端组件不能导入服务端组件
- 客户端组件的子组件也会成为客户端组件
客户端组件和服务端组件与 SSG/SSR/ISR 的关系
核心理解
SSG/SSR/ISR 是针对页面(Route)的渲染模式,不是针对组件本身。
服务端组件
- 可以 SSG、SSR、ISR
- 原因:在服务器上运行,可在构建时或请求时执行
客户端组件
手动在文件顶部指定 'use client'
'use client'; // 这是客户端页面
export default function ClientFormPage() {
return (
<div className="container mx-auto p-8">
<h1 className="text-2xl font-bold mb-4">客户端页面示例</h1>
<p className="mb-4">
这个页面整个是客户端组件('use client')。
虽然会返回 HTML,但这是 SSR(服务端渲染),不是 SSG(静态生成)。
</p>
<p className="mb-4 text-red-600">
注意:这个页面无法在构建时预渲染(不能 SSG),只能在请求时渲染(SSR)。
</p>
</div>
);
}
注意: 在 Next.js App Router 中,即使页面是 'use client',Next.js 仍然会在服务器上执行一次并生成 HTML(这是 SSR,不是 CSR)。
客户端组件能被预渲染
客户端组件/页面为什么还会被预渲染呢?
关键理解:SSR vs SSG
当你给页面加上 'use client' 时,表示 这不是预渲染(SSG),而是服务端渲染(SSR)!
| 渲染方式 | 时机 | 是否返回 HTML | 能否静态生成 |
|---|---|---|---|
| SSG(静态生成) | 构建时 | ✅ 是 | ✅ 可以 |
| SSR(服务端渲染) | 每次请求时 | ✅ 是 | ❌ 不能 |
| CSR(客户端渲染) | 浏览器中 | ❌ 否(只有空壳) | ❌ 不能 |
✅ 会被预渲染的
- HTML 结构
→ HTML 中的// 客户端组件 return ( <form> <input type="text" /> <button>提交</button> </form> );<form>,<input>,<button>都会被预渲染 - 初始值
→ HTML 中会显示const [count, setCount] = useState(0); return <div>{count}</div>;<div>0</div> - Props 传递的值
→ HTML 中会显示传递的值<ClientComponent title="Hello" /> - 条件渲染的结果(基于 props)
→ 如果function ClientComponent({ show }) { return show ? <div>显示</div> : null; }show是true,HTML 中会有<div>显示</div>
❌ 不会被预渲染的
- JavaScript 逻辑
const handleClick = () => { console.log('clicked'); // 不会执行 }; - 事件处理器
<button onClick={handleClick}> // onClick 不会工作(直到 hydration) - useEffect 的效果
useEffect(() => { console.log('mounted'); // 只在浏览器中执行 }, []); - 浏览器 API
const width = window.innerWidth; // 错误,无法在服务器执行 - 动态状态变化
const [count, setCount] = useState(0); // 只有初始值 0 会被预渲染 // 用户点击后的变化不会预渲染
客户端页面('use client')的渲染流程
用户请求
↓
Next.js 服务器接收请求
↓
服务器执行 React 组件(虽然是客户端组件,但第一次在服务器执行)
↓
生成 HTML
↓
返回 HTML 给浏览器(这就是为什么你看到 HTML)
↓
浏览器显示 HTML
↓
React 客户端代码加载
↓
Hydration(接管 HTML 元素)
↓
页面完全可交互
不能预渲染的场景
- 整个页面是客户端组件
- 客户端组件中使用服务端 API
- 客户端组件中使用 Node.js API
总结
- 客户端组件会被预渲染 HTML 结构,但 JavaScript 逻辑不会
- Hydration Point 是客户端组件的边界,在 HTML 中可以找到
- 客户端页面('use client') 仍然返回 HTML,但这是 SSR,不是 SSG
- SSR 和 SSG 都返回 HTML,区别在于生成的时机:
- SSG:构建时生成,可以缓存
- SSR:每次请求时生成,无法缓存
API
文件命名规则
在 App Router 中,API 路由必须使用 app/api/route.ts 或 app/api/route.js 文件名:
- ✅
route.ts/route.js- API 路由文件 - ❌
api.ts/api.js- 不是 API 路由(会被忽略)
文件和路径的映射关系
文件路径 → 请求路径
─────────────────────────────────────────────────────
app/api/route.ts → /api
app/api/users/route.ts → /api/users
app/api/users/[id]/route.ts → /api/users/:id
app/api/posts/[slug]/route.ts → /api/posts/:slug
app/api/users/[id]/posts/route.ts → /api/users/:id/posts
支持多种 HTTP 方法
请求路径:
GET /api/usersPOST /api/usersPUT /api/usersDELETE /api/users
// app/api/users/route.ts
export async function GET() {
return Response.json({ users: [] });
}
export async function POST(request: Request) {
const body = await request.json();
return Response.json({ created: true, data: body });
}
export async function PUT(request: Request) {
const body = await request.json();
return Response.json({ updated: true, data: body });
}
export async function DELETE(request: Request) {
return Response.json({ deleted: true });
}
动态路由
// app/api/users/[id]/route.ts
// GET /api/users/:id - 获取单个用户
export async function GET(
request: Request
) {
// 从 URL 路径中获取 id
const url = new URL(request.url);
const pathnameParts = url.pathname.split('/');
const id = pathnameParts[pathnameParts.length - 1];
// 这里应该从数据库获取用户
// const user = await getUserById(id);
console.log(id);
// 模拟数据
if (id === '1') {
return Response.json({
user: { id: 1, name: 'Alice', email: 'alice@example.com' }
});
}
return Response.json(
{ error: 'User not found' },
{ status: 404 }
);
}
请求和响应处理
读取请求数据
// app/api/example/route.ts
export async function POST(request: Request) {
// 1. 读取 JSON body
const body = await request.json();
// 2. 读取 URL 参数
const { searchParams } = new URL(request.url);
const query = searchParams.get('query');
// 3. 读取 headers
const contentType = request.headers.get('content-type');
const authorization = request.headers.get('authorization');
// 4. 读取 cookies(需要从 headers 手动解析或使用 next/headers)
const cookieHeader = request.headers.get('cookie');
return Response.json({
body,
query,
contentType,
authorization
});
}
返回响应
// app/api/example/route.ts
export async function GET() {
// 1. 返回 JSON
return Response.json({ message: 'Hello' });
// 2. 返回 JSON 并设置状态码
return Response.json({ error: 'Not found' }, { status: 404 });
// 3. 返回文本
return new Response('Hello, world!', {
headers: { 'Content-Type': 'text/plain' },
});
// 4. 返回 HTML
return new Response('<h1>Hello</h1>', {
headers: { 'Content-Type': 'text/html' },
});
// 5. 重定向
return Response.redirect('https://example.com');
// 6. 设置自定义 headers
return Response.json({ data: 'secret' }, {
headers: {
'Cache-Control': 'no-store',
'X-Custom-Header': 'value',
},
});
}