这本教程,始于 2025 年末我对 Next.js 的一次摸索。
当时的记录是零散的:一段代码、一个疑问、一点理解的偏差,夹杂在学习的时间缝隙里。后来借助 Claude 的整理,这些片段被重新串联起来,逐渐显出轮廓——像把一张未完成的草图,慢慢描出结构与光影。
于是,它成了现在的样子:一份回望,也是一条路径。
学习 Next.js 时,人们很容易走进一个不易察觉的惯性:把它当作"React 的延伸",用熟悉的方式继续书写组件。页面可以运行,逻辑也似乎成立,但很多关键的问题却被轻轻绕开:服务端组件与客户端组件的边界究竟在哪里?Server Action 是在补足什么?缓存,是优化,还是隐患?又该在什么时候让它失效?
这些问题,不会在"能跑起来"的那一刻得到答案。
所以,这本教程选择从更底层的分野出发:客户端渲染,与服务端渲染。
你会先沿着一条熟悉的路径前行:用客户端组件完成一个博客的增删改查;然后转身,从另一侧重新走一遍,用服务端组件重写同样的功能。当两条路径在终点相遇,差异不再需要解释,它会自然显现:有的更直接,有的更克制;有的依赖浏览器,有的回归服务器;而取舍,也在此之间慢慢变得具体。
理解 Next.js,先要理解它做了什么:用文件系统替代路由配置,用约定好的文件名(page.tsx、layout.tsx、route.ts)赋予不同功能。这个阶段用两章把这套基础规则讲清楚,后面所有章节都建立在它之上。
第 1 章:路由与布局(01_getting_start)
用一个个人博客网站演示 Next.js 的文件系统路由,把"文件夹即路由"这个核心规则从简单到复杂走一遍。最后用 Route Groups 解决"同层级页面需要完全不同的顶层 layout"这个看似绕不过去的问题。
- 文件系统路由:
app/about/page.tsx→/about,不需要任何路由配置 layout.tsx:包裹page.tsx的共享 UI,跨页面跳转时不重新渲染- 动态路由
[id]:方括号文件夹,通过await params读取动态片段 - 嵌套 layout:影响范围由所在目录决定,外层不动,内层独立
next/link:客户端导航,避免整页刷新,自动预加载链接目标- Route Groups
(name):括号文件夹,不参与 URL,解决同层级多套 layout 并存的问题
第 2 章:API Route(02_api_route)
Next.js 不只是前端框架,它允许在同一个项目里写后端 API。这一章用 Route Handler 实现一套完整的博客 CRUD,数据库用 SQLite + Drizzle ORM,入参校验用 Zod。这套 API 会一直被后续章节复用。
- Route Handler:
route.ts文件,export 函数名即 HTTP 方法(GET、POST、PUT、DELETE) - Drizzle ORM:用
schema.ts描述表结构,TypeScript 类型自动推断,不需要手写接口 db/目录分层:schema.ts定义结构、client.ts管理连接、blogs.ts集中增删改查- Zod 运行时校验:TypeScript 类型只在编译期存在,API 入口必须对外来数据做运行时验证
- HTTP 语义:201 Created / 204 No Content / 400 Bad Request / 404 Not Found 各自的用法
Next.js 最核心的概念是"服务端组件"和"客户端组件"——但这两种模式各自的特点和适用场景,很难从文档里直接读懂。这个阶段的策略是:先用客户端组件完整做一遍,再用服务端组件重做一遍,对比看差异。
第 3 章:客户端渲染(03_client_rendering)
用 "use client" + useEffect + useState 实现博客的完整增删改查。这是最接近"普通 React"的写法——先渲染空壳,挂载后异步获取数据,数据回来再触发重渲染。这种模式你会熟悉,也会感受到它的繁琐,为下一章做铺垫。
"use client":客户端组件的声明方式,Next.js 里所有组件默认是服务端组件useEffect+useState数据获取:固定套路,先空壳后填充useParams():客户端组件读取动态路由参数的方式- 手动 loading 状态:
if (loading) return <Spinner /> useRouter().push():客户端组件里的编程式导航
第 4 章:服务端渲染(04_server_rendering)
用服务端组件重写第三章的博客增删改查。代码量明显更少:组件函数加 async,数据直接在函数体里 await,拿到数据才渲染,一步到位。loading.tsx 和 error.tsx 自动接管加载和错误状态,不再需要手动维护。
async组件:服务端组件可以直接await,数据准备好了才渲染await params:服务端组件读取路由参数的方式,Next.js 16 中params是 Promise- Server Action:带
"use server"指令的 async 函数,传给<form action={...}>,在服务器上处理表单提交 defaultValue:非受控表单的预填充,替代受控组件的value+onChangeloading.tsx:基于 React Suspense,服务端组件await期间自动显示error.tsx:错误边界,服务端组件throw时自动接管,必须是客户端组件notFound():语义化的 404 处理,比throw new Error更准确- 服务端 fetch 必须用绝对路径:Node.js 环境没有"当前域名"的概念
第 5 章:Server Action 进阶(05_server_rendering_2)
第四章的 Server Action 有两个缺陷:提交期间没有任何反馈,请求失败时也没有错误提示。这一章用 useActionState 解决这两个问题,同时通过三个递进的 Action 模式展示 useActionState 从简单到复杂的完整用法。
useActionState:React 19 的 hook,让 Server Action 的返回值能渲染到页面上ActionState类型设计:status: "error" | "success" | "confirm" | null+ 可选messageprevState参数:Server Action 新签名的第一个参数,大多数时候只是占位——但deleteAction用它实现了二次确认- 三种 Action 模式:① 只返回错误、成功由服务端
redirect;② 成功和失败都返回状态、前端useEffect延迟跳转;③ 读取prevState实现二次确认 isPending:useActionState第三个返回值,Action 执行期间为true,用于禁用按钮和切换文字actions.ts+types.ts:文件级"use server"只能导出 async 函数,常量和类型必须放到独立文件revalidatePath:数据变更后主动通知 Next.js 缓存失效,只在 Server Action 和 Route Handler 里有效
认证是真实应用里不可绕过的部分。这个阶段用三章依次呈现三种架构:手写 JWT、前后端分离的 Better Auth、以及全栈一体化的 Better Auth。每一章都是对前一章的反思:你会亲眼看到什么是"手写解决的",什么是"框架封装掉的",以及"分离部署"和"全栈整合"各自意味着什么取舍。
第 6 章:JWT 认证(06_jwt)
用 Fastify 搭建一个独立的认证后端,手写 JWT 签发、验证、刷新的完整流程。Next.js 前端通过 rewrites() 代理 API 请求,绕开 CORS,用 httpOnly cookie 存 token。这一章故意不用认证库——把所有细节暴露在外,让你清楚认证到底在处理哪些问题。
- Fastify 后端:独立进程(端口 4000),
@fastify/jwt签发 token,@fastify/cookie管理 httpOnly cookie - 双 token 机制:access token(短效,Bearer 验证)+ refresh token(长效,存 cookie,静默续期)
next/rewrites():Next.js 服务端代理,前端请求/api/*透明转发到后端,彻底绕开浏览器 CORS 限制- JWT 的局限:token 一旦签发无法撤销;用户注销只能清客户端 cookie,服务端 token 仍然有效直到过期
- REST Client 测试:
.http文件测试 API,手动管理 token(对比第七章 cookie 自动携带的差异)
第 7 章:Better Auth + Fastify(07_betterauth)
把第六章的手写 JWT 替换为 Better Auth,后端仍然是独立的 Fastify 服务,前端仍然是独立的 Next.js。通过对比,清楚看到认证库封装了哪些细节:session 存数据库、token 自动续期、密码哈希、schema 自动生成。
- Session-based vs JWT:session 存服务端数据库,可随时撤销;JWT 无状态,无法主动失效
- Drizzle + SQLite:Better Auth 用
@better-auth/cli generate自动生成 schema,drizzle-kit push同步建表 - Fastify 桥接:
fromNodeHeaders把 Node.js headers 转成 Web API Headers,让 Better Auth handler 能接收 Fastify 请求 - CSRF 保护:Better Auth 的 POST 请求需要
Origin头,REST Client 测试时需要手动添加 - 三步启动:
db:generate-schema→db:push→dev,每步在做什么、为什么这个顺序 - 两种 session 读取:
GET /api/auth/get-session(内置端点)vsGET /api/me(自定义受保护路由,用auth.api.getSession()验证)
第 8 章:Better Auth + Next.js 全栈(08_betterauth_2)
把第七章的 Fastify 后端去掉,将 Better Auth 直接集成进 Next.js。认证逻辑变成一个三行的 Route Handler,Server Component 可以零网络请求读取 session,proxy.ts 在请求到达页面前就完成鉴权拦截。
本章同时提供四个对比页面,横向展示客户端与服务端两种处理方式的差异:
| 路由 | 方式 | 核心 API |
|---|---|---|
/login/c |
客户端组件 | authClient.signIn.email() |
/login/s |
Server Component + Server Action | auth.api.signInEmail() + nextCookies plugin |
/dashboard/c |
客户端组件 | authClient.getSession() → HTTP fetch |
/dashboard/s |
Server Component | auth.api.getSession({ headers: await headers() }) → 直接查库 |
toNextJsHandler:三行代码把 Better Auth 挂进 Next.js Route Handler,替代第七章 40 行的 Fastify 桥接代码proxy.ts:Next.js 16 把middleware.ts改名为proxy.ts,函数名改为proxy;在页面渲染前拦截未登录请求,无闪屏- Server Component 读 session:
auth.api.getSession({ headers: await headers() }),cookie → DB 查询,同进程完成,零 HTTP - Server Action 登录:
auth.api.signInEmail()在服务端直接执行,nextCookiesplugin 负责把 Set-Cookie 写入响应 authClient:createAuthClient()提供类型安全的客户端方法,替代裸 fetch
- 缓存深入:
revalidateTag、按时间过期的 ISR、generateStaticParams静态预生成——第五章只触及了缓存的冰山一角 - Metadata API:
generateMetadata,SEO 标签的正确处理方式
本文档及相关代码以 MIT 协议发布。
Author: Linlin Wang
Contact: wanglinlin.cn@gmail.com