本系列(架构师篇)将带你脱离"增删改查"的舒适区,构建可维护、可扩展的企业级 React 应用。这是第八篇,我们将彻底走进 Next.js 的世界------这已经不只是"React 框架",而是一套完整的全栈开发范式。
引言:2026 年,Next.js 就是 React 的"默认答案"
如果你还在用 Vite + React Router + 手写 API 的方式搭建新项目,我需要告诉你一个残酷的事实:这套组合在 2026 年已经落后了。
不是说它们不好,而是 Next.js 已经将前端开发的"默认配置"彻底改写。从 Next.js 13 引入 App Router 开始,到 14/15 版本的持续打磨,Next.js 已经从一个"服务端渲染框架"进化为一个完整的全栈应用平台。
Pages Router 的问题在于:它诞生于 React Server Components 之前。 所有组件都要在浏览器端 hydration,即使只是渲染一段静态文本,也要把完整的组件代码发送到客户端。而 App Router 默认使用 React Server Components,服务端组件只在服务器上运行,永远不会出现在你的 JavaScript bundle 中 。真实项目的基准测试显示,从 Pages Router 迁移到 App Router 后,数据密集型页面的 bundle 体积减少了 40% 到 60% 。
今天这篇文章,我们将从零开始,用 Next.js 15 构建一个完整的全栈应用,涵盖 App Router、Server Components、Server Actions、数据缓存、部署上线等全链路。
第一部分:App Router vs Pages Router------根本性的范式转移
在开始写代码之前,必须先理解这个根本性的转变。
| 维度 | Pages Router | App Router |
|---|---|---|
| 数据获取 | getServerSideProps、getStaticProps |
组件内直接 async/await |
| 数据变更 | 独立 API 路由 + 客户端 fetch | Server Actions("use server") |
| 布局 | 单个 _app.tsx 全局包裹 |
嵌套 layout.tsx,逐层复用 |
| 流式渲染 | 不支持 | 内置,配合 <Suspense> |
| 默认渲染 | 客户端(hydration) | 服务端组件 |
Pages Router 的核心问题是"一刀切" :所有页面组件都要在客户端 hydration,无论是否需要交互。这意味着大量只负责展示静态内容的组件也被迫打包进客户端 bundle。
App Router 的核心思想是"按需交互" :默认所有组件跑在服务端,只有明确标记 "use client" 的组件才会进入客户端 bundle。
创建新项目(2026 标准姿势)
perl
npx create-next-app@latest my-app --typescript --app --eslint --tailwind --src-dir
cd my-app
--app 标志确保生成的是 app/ 目录而非 pages/。生成的结构如下:
lua
my-app/
src/
app/
layout.tsx ← 根布局
page.tsx ← 首页路由 "/"
prisma/ ← 稍后添加
next.config.ts
第二部分:App Router 的文件系统路由
在 App Router 中,文件即路由:
app/page.tsx→/app/dashboard/page.tsx→/dashboardapp/blog/[slug]/page.tsx→/blog/:slug
这种"约定优于配置"的设计,让你无需手动维护路由表,目录结构本身就是路由映射。
嵌套布局:Layout 的威力
Pages Router 时代,我们只有一个全局 _app.tsx。而在 App Router 中,每个目录都可以有自己的 layout.tsx,实现细粒度的布局复用。
javascript
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div>
<Sidebar />
<main>{children}</main>
<Footer />
</div>
)
}
这个布局会自动包裹 /dashboard 下的所有页面,而不影响 / 或其他路径。
第三部分:Server Components------数据获取的革命
这是 Next.js 最核心的变化。在 App Router 中,每个组件默认都是 Server Component。
Server Component 可以:
- 直接
async/await数据库查询或 API 调用 - 访问环境变量、API 密钥等敏感信息(不会暴露给客户端)
- 产生 零客户端 JavaScript
一个纯粹的 Server Component
javascript
// app/posts/page.tsx ------ 这是一个 Server Component
export default async function PostsPage() {
// 直接 fetch 数据------没有 useEffect,没有 loading 状态,没有客户端 bundle
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10", {
next: { revalidate: 60 }, // ISR:每 60 秒重新验证
})
const posts = await res.json()
return (
<ul>
{posts.map((post: { id: number; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
没有 getServerSideProps,没有 useEffect,没有状态管理库。数据在服务器上获取,随 HTML 一起到达浏览器。
何时使用 Client Component?
当需要交互时------事件处理、useState、useEffect、浏览器 API------在文件顶部添加 "use client" 指令:
javascript
'use client'
import { useState } from 'react'
export default function LikeButton({ likes }: { likes: number }) {
const [count, setCount] = useState(likes)
return (
<button onClick={() => setCount(c => c + 1)}>
❤️ {count}
</button>
)
}
Server Component 与 Client Component 的协作模式
最佳实践是:父组件在服务端获取数据,将数据通过 props 传递给需要交互的子组件:
javascript
// app/[id]/page.tsx ------ Server Component
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const post = await getPost(id)
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* 只有这个按钮是 Client Component */}
<LikeButton likes={post.likes} />
</div>
)
}
这种模式让你既享受服务端渲染的性能优势,又保有客户端交互的灵活性。
第四部分:Server Actions------消灭 API 路由
这是 Next.js 最令人兴奋的特性之一。Server Actions 让你直接在组件中写后端逻辑,无需创建独立的 API 路由。
什么是 Server Action?
Server Action 是一个在服务器上执行的异步函数 。通过 "use server" 指令标记:
javascript
// app/lib/actions.ts
'use server'
import { auth } from '@/lib/auth'
export async function createPost(formData: FormData) {
const session = await auth()
if (!session?.user) {
throw new Error('Unauthorized')
}
const title = formData.get('title')
const content = formData.get('content')
// 直接操作数据库
await db.post.create({ data: { title, content, authorId: session.user.id } })
// 重新验证缓存
revalidatePath('/posts')
redirect('/posts')
}
在表单中使用 Server Action
javascript
// app/posts/new/page.tsx
import { createPost } from '@/lib/actions'
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="标题" required />
<textarea name="content" placeholder="内容" />
<button type="submit">发布</button>
</form>
)
}
就是这么简单。不需要写 API 路由,不需要手动 fetch,不需要处理 loading 状态。
渐进增强(Progressive Enhancement)
Server Actions 支持渐进增强:即使 JavaScript 尚未加载或被禁用,表单依然可以正常提交。Next.js 会在一次服务器往返中同时返回更新后的 UI 和新数据。
⚠️ 安全警告
Server Actions 可以通过直接的 POST 请求调用,不仅仅是通过你的应用 UI。务必在每个 Server Action 内部验证身份认证和授权:
javascript
export async function deletePost(formData: FormData) {
'use server'
const session = await auth()
if (!session?.user) throw new Error('Unauthorized')
const id = formData.get('id')
// 验证用户是否有权删除此资源
const post = await db.post.findUnique({ where: { id } })
if (post.authorId !== session.user.id) {
throw new Error('Forbidden')
}
await db.post.delete({ where: { id } })
revalidatePath('/posts')
}
第五部分:缓存机制------性能的"双刃剑"
Next.js 的缓存机制非常强大,但也非常容易踩坑。理解它是写出高性能应用的关键。
四层缓存体系
Next.js 有四个层次的缓存机制:
| 缓存机制 | 缓存内容 | 存储位置 | 生命周期 |
|---|---|---|---|
| Request Memoization | 函数返回值 | 服务器 | 单次请求 |
| Data Cache | 数据 | 服务器 | 持久化(可重新验证) |
| Full Route Cache | HTML + RSC Payload | 服务器 | 持久化(可重新验证) |
| Router Cache | RSC Payload | 客户端(浏览器) | 用户会话期间 |
默认行为:能缓存就缓存。Next.js 默认会静态渲染路由,并缓存数据请求,除非你主动退出。
控制缓存的关键 API
php
// 1. 按时间重新验证(ISR)
fetch('https://api.example.com/posts', {
next: { revalidate: 60 } // 每 60 秒重新验证
})
// 2. 按需重新验证
import { revalidatePath, revalidateTag } from 'next/cache'
// 路径级别重新验证
revalidatePath('/posts')
// 标签级别重新验证(更精细)
revalidateTag('posts')
// 3. 完全跳过缓存
fetch('https://api.example.com/live-data', {
cache: 'no-store'
})
Next.js 16+ 的"缓存组件"
Next.js 16 引入了显式缓存模型,不再由框架自动判断缓存规则,而是让开发者自主定义"哪些内容需要缓存"以及"何时重新验证"。
javascript
// 使用 'use cache' 指令显式标记可缓存的部分
'use cache'
export async function getCachedPosts() {
// 只有这个函数的结果会被缓存
return await db.post.findMany()
}
这解决了一个长期痛点:部署后出问题,分不清是代码的锅还是框架缓存的锅。
第六部分:部署与最佳实践
Vercel:零配置部署
Vercel 是 Next.js 的官方部署平台,支持零配置部署。当你推送代码时,Vercel 自动识别 Next.js 项目,应用最优构建设置。
对于个人开发者,Vercel 提供每月 100 小时的免费服务器资源,支持静态网站、Serverless 函数及全栈应用部署。
r
# 本地构建(可选)
vercel build
# 直接部署
vercel --prod
生产环境检查清单
Next.js 官方提供了详细的生产环境最佳实践: 自动优化(无需配置) :
- Server Components 默认启用,减少客户端 bundle
- 自动代码分割(按路由 segments)
- 链接预取(Link 进入视口时自动预加载)
- 静态渲染与缓存
开发时建议 :
项目架构最佳实践
一个生产级 Next.js 项目应该将业务逻辑、共享规则和外部集成与 Next.js 路由层分离:
bash
src/
app/ # 路由层(页面、布局、路由处理)
components/ # 可复用 UI 组件
lib/ # 业务逻辑、工具函数
lib/actions/ # Server Actions
lib/db/ # 数据库操作
lib/auth/ # 认证逻辑
types/ # TypeScript 类型定义
第七部分:完整示例------博客应用
把以上所有概念整合成一个完整的博客应用:
bash
src/
app/
layout.tsx # 根布局
page.tsx # 首页(Server Component)
posts/
page.tsx # 文章列表(Server Component,直接 fetch)
[id]/
page.tsx # 文章详情(Server Component,动态路由)
posts/new/
page.tsx # 创建文章(含 Server Action 表单)
lib/
actions/
posts.ts # 所有 Server Actions('use server')
db/
index.ts # 数据库连接
components/
LikeButton.tsx # Client Component
CommentForm.tsx # Client Component + Server Action
核心代码量 :整个应用的数据获取、数据变更、路由、布局全部在一个代码库中完成,不需要单独的后端服务。
总结:Next.js 改变了什么?
| 传统方式 | Next.js 15 方式 |
|---|---|
| 前端项目 + 后端 API 分离 | 单一全栈项目 |
| 手动维护路由表 | 文件系统即路由 |
useEffect + fetch 获取数据 |
Server Component 中直接 await |
| 独立 API 路由处理变更 | Server Actions 内联在组件中 |
| 手动配置缓存 | 默认缓存,按需退出 |
| 多步部署(前端+后端) | 一键部署到 Vercel |
Next.js 的全栈革命,本质上是将前后端的边界模糊化,让开发者可以用 React 的思维模型同时编写前端 UI 和后端逻辑。