Next.js 从第一行代码到上线
它到底是什么、什么时候该用它、怎么在今晚跑出你的第一个页面——顺便把 16 版里最容易踩空的三处改动讲清楚
📅 更新于 2026-09-13
本文校对于 Next.js @ 16.3.5 · 官方源:nextjs.org/docs
0030 秒速览:先拿走这三句话
你能在今晚跑出一个能被人访问的网站(首页 + 关于页 + 一个能点的链接),并且在自己写的代码里说清楚「这一行是在服务器上跑的,还是在你访客的浏览器里跑的」——后者是 Next.js 最容易搞错、也最值钱的一点。
如果只记三句话
- 文件夹就是网址:在
app/about/里放一个page.tsx,你就自动多了一个/about页面,不用填任何路由表。 - 默认在服务器上跑,要交互才切成浏览器:组件默认在服务端拼好 HTML 再发出去,只有点击、输入、动画这些必须在浏览器里发生的事,才在文件顶部加一行
"use client"。 - 先 build 再上线:
npm run dev能跑不等于能上线,npm run build过了才是真的过了。
一张表:你现在的处境 → 该看哪几节
| 你现在的状态 | 先看这几节 | 为什么是这几节 |
|---|---|---|
| 完全没碰过 React,只想先看看 Next.js 长什么样 | 00 → 01 → 02 → 03 → 04 | 先建立「它是什么、为什么值得学」的判断,再用最小例子尝到甜头,不碰组件分界也能跑 |
| 会写一点 React,想做一个能发出去的完整网站 | 03 → 04 → 06 → 07 → 09 → 10 | 你的卡点在「数据从哪来、怎么跳转、怎么上线」,这些正是 06–10 的主题 |
已经在用 Next.js,但报错 params 相关的东西看不懂 | 07-2、07-3、13 | 16 版把同步取参数彻底删掉了,这是旧教程最集中的过时点 |
在维护一个老项目,代码里写的是 middleware.ts | 11-4、13 | 官方已把它改名成 proxy,搞混会出现「改了文件没生效」 |
关键数字先给量级
上手路径:五步,今晚能走完
跑 node -v,要 ≥ 20.9.0。低于这个数,先去 nodejs.org 装 LTS 版。
npx create-next-app@latest my-app,第一个问题选「用推荐配置」,其余问题只回答一个「项目叫什么名字」。
cd my-app && npm run dev,浏览器开 http://localhost:3000。
建 app/about/page.tsx,访问 /about 看它出现——这一步做完你就理解「文件即路由」了。
npm run build 无报错,把代码推到 GitHub,在 Vercel 里导入仓库,两分钟后你有一个真实网址。
① 参数是异步的。16 版里 params 是一个需要 await 的东西(一个「承诺之后会给你值」的对象),写 params.slug 会直接报错。旧教程几乎全是你现在会撞车的那种写法。
② middleware.ts 已经改名。16 版起这个文件叫 proxy.ts,导出函数也叫 proxy,而且默认跑在完整的 Node.js 环境里而不是轻量边缘环境。老代码可以继续跑但会警告。
③ 取数据默认不缓存了。早年「fetch 默认帮你缓存」的说法已经失效,现在是「取数据要缓存,得你自己说一声」。
新手最容易提前踩的三个坑
app/ 和 pages/ 两套写法。新项目只用 app/;pages/ 只在接手老项目时查。混抄是新手第一大乱源。app/ 路由最核心的新概念。分不清就会遇到「useState 不允许用」这类报错——第 06 节专门拆它。01Next.js 到底是什么(先破四个错想)
它不是一门新语言,也不是 React 的替代品
这是最容易搞错的第一处。React 是一个画界面的库:你告诉它「这块内容长什么样」,它负责把画面渲染出来。它只管画面,不管你网站的地址怎么分、数据从哪来、接口写在哪、上线之后跑在哪。
Next.js 是建在 React 之上的一整套约定加工具。所谓「约定」,意思是它不给你一堆开关去配,而是规定「你把文件放在这个名字下、用这个名字命名,我就知道你想干什么」。你少配很多东西,代价是要先记住这套命名规则。
Next.js 的官方仓库描述只有一句:“The React Framework”——「给 React 用的框架」。这个自称很准确:它是来给 React 补齐短板的,不是来取代 React 的。
四个最常见的错想,逐个破掉
| 你可能会以为 | 实际上是 | 为什么重要 / 什么时候会撞上 |
|---|---|---|
| 「它是一个新语言/新语法」 | 写的是标准 JavaScript 或 TypeScript,加上 React 的写法。没有新语法要学 | 如果你在犹豫「要不要先学一门新语言再回来」,不需要。会 JS 就能开始抄 |
| 「它是 React 的竞品,选它就不用 React 了」 | 它内部就是 React,你写的每个页面仍然是 React 组件 | 决定「要不要学 React」时别绕开——React 的基础(组件、props、JSX、useState)仍然要用到 |
| 「它是一个部署平台/云服务」 | 它是一个可以下载、在自己电脑上跑的软件包。官方那个部署平台叫 Vercel,是另一件事 | 看到账单或「要付费」的说法时别慌:框架本身免费开源,你可以部署到任何能跑 Node 的服务器 |
| 「它只能做网站前台」 | 前台、后台接口、表单处理、定时更新,一个项目里都能写 | 判断「要不要再单独起一个后端项目」时用得上:小项目通常不需要 |
它和你已经见过的「网站」差在哪
用你每天都在经历的体验来说明。传统的纯前端 React 项目,你打开网址时会先看到一片空白,然后页面的字才一个一个冒出来——因为浏览器得先下载一大包 JavaScript,再自己把页面拼出来。如果你的网慢,这个空白会持续一两秒。
Next.js 默认换了个做法:它先在服务器上把页面拼成完整的 HTML 文字,再把这份拼好的结果发给你。浏览器拿到就能直接画出来,所以你看到的是内容,不是空白。拼好之后,它再悄悄把 JavaScript 送下来,让按钮、输入框这些能点能动——这一步的行业叫法是「注水」(hydration),意思是给已经画好的静态骨架注入生命力。
「先拼好再发」带来三个直接好处:打开快、搜索引擎能直接读到你的内容、密钥不用发到浏览器。反过来,它也是 Next.js 大部分「新手坑」的根源——因为代码现在可能在两个地方跑:服务器上,或者访客的浏览器里。第 06 节整节都在讲这条分界线。
它内部有哪几块拼图
| 拼图 | 它管什么 | 为什么重要 / 何时用 · 别当什么用 |
|---|---|---|
| App Router | 新的路由体系:app/ 目录下的文件夹结构决定网址 | 新项目默认用它;别和老的 pages/ 写法混着抄 |
| Turbopack | 打包与开发服务器(把源码整理成浏览器能跑的形式,并做热更新) | 16 版起是默认引擎,不再需要传额外参数;别把它当成另一个框架,它只是引擎 |
| React 服务端组件 | 组件默认在服务端渲染、可以不发 JavaScript 到浏览器 | 理解「为什么首屏快」的关键;别在它里面写点击事件 |
| Server Actions | 写在服务端的函数,表单可以直接调用它 | 留言、点赞这类写操作省掉一个接口文件;别拿它做大文件上传 |
| Image / Font / Metadata | 图片优化、字体优化、页面标题与描述 | 开箱就有的三项优化,每个页面顺手用;别等上线才补 |
版本这件事,你要知道的方法
本文写定时的最新稳定版是 16.3.5(2026-09-11 发布)。Next.js 大约每几个月出一个大版本,而你手里的 AI 助手、教程、搜索结果里混着好几个版本的写法——这是新手最容易被带偏的地方。
所以给你一条比「记住版本号」更有用的做法:在项目里问版本,而不是在脑子里记版本。
# 看你的项目装的是哪个版本
npm ls next
# 看当前最新稳定版是几
npm view next version
官方在升级指南开头写了一句很直白的话(大意):「这不是你认识的那个 Next.js,这个版本有破坏性改动,API(你能调用的那些函数和写法)、约定和文件结构都可能和你训练数据里的不一样。」——这句话是写给 AI 助手看的,也是写给你的:如果你拿一份 2024 年的教程对着 16 版敲,撞车是必然的,不是你笨。
02它替你做了什么:六个问题一次答完
用纯 React 做网站,你会接连撞上六个问题。Next.js 的价值就是这六个问题它都有现成答案。
问题一:地址栏里的 /about,怎么对应到一个页面?
纯 React 项目里,地址和页面的对应关系得你自己搭(要装一个专门的库,写一份路由表,再手动维护它和文件的同步)。Next.js 的做法是:你把文件放对位置,网址就自动有了。
app/about/page.tsx → 网址 /about
app/blog/page.tsx → 网址 /blog
app/page.tsx → 网址 /(首页)
这就是「文件即路由」。行业的通俗说法也叫「约定优于配置」——用一套命名规矩,换掉一大堆要你手填的配置文件。
顺便先埋一个你会反复遇到的词:middleware(直译「中间件」)。它指的是「请求进入页面之前先替你看一眼的那道关卡」,用来做登录拦截、跳转这类事。本文第 10 节会专门讲它——因为 16 版刚把它改了名字,那是本文要提醒你的第二个「旧教程会把你带偏」的地方。
问题二:搜索引擎抓不到我的内容怎么办?
搜索引擎派来的自动程序(行业叫爬虫)读的是服务器返回的原始 HTML。纯前端渲染的页面,返回的原始 HTML 几乎是空的,内容是浏览器后来用 JavaScript 拼上去的——爬虫未必等得及。Next.js 默认就在服务器把内容放进 HTML 里,爬虫一读就读到。
问题三:接口写在哪?
你不需要另起一个后端项目。在 app/api/hello/route.ts 里导出一个函数,/api/hello 就是一个能返回 JSON 数据的接口(JSON 是一种人和机器都好读的文本数据格式)。
问题四:图片太大、加载慢?
用官方的 Image 组件替代原生的 <img> 标签,它自动压缩、按需加载、并预留位置防止页面跳动。
问题五:上线要折腾多久?
把代码推到 GitHub,在 Vercel 上导入这个仓库,点一次部署。之后每次推送代码,网站自动更新。
问题六:AI 助手总给我写旧版 API 怎么办?
16 版的脚手架会随项目生成一份 AGENTS.md(给 AI 助手看的说明书),里面写明「请以本版本官方文档为准」。你也可以手动把它补到旧项目里:
npx @next/codemod@canary agents-md
六个问题,一张表收口
| 你本来要自己解决的问题 | Next.js 给的答案 | 为什么重要 / 何时用 · 别当什么用 |
|---|---|---|
| 地址与页面的对应 | 文件夹即路由(app/about/page.tsx) | 省掉一个路由库和一份要同步的路由表;别指望它能替你做权限判断 |
| 首屏空白、SEO 差 | 默认服务端渲染,先出 HTML | 对外页面、内容页的首选;内部强交互后台可以另说 |
| 接口无处安放 | Route Handlers(app/api/*/route.ts) | 给外部或跨端用的 JSON 接口;站内表单优先用 Server Actions,少写一个文件 |
| 图片与字体的体积 | Image / Font 组件 | 开箱即用,几乎零成本;别为了「自己可控」而绕开它们 |
| 上线流程 | 推到 GitHub → Vercel 自动部署 | 新手上线最快的一条路;公司内网等场景仍可自己 build 后部署 |
| AI 写出过时代码 | 脚手架自带 AGENTS.md 指向版本匹配的文档 | 提问时把官方文档链接一起贴给它;别完全依赖记忆里的写法 |
Next.js 的便利全部建立在约定上:文件名、目录名都有含义。享受便利的前提是先记住这套约定。写错一个文件名的字母,页面就是 404——这不是 bug,是约定的代价。第 05 节把四个关键文件名一次讲清。
03什么场景该用它,什么场景别用
一句话判断法
问自己:我要做的是「一个能在网上打开的完整网站」,还是「一段练习代码」?前者用 Next.js,后者别用。它给你的每一样好处(路由、渲染、部署)都只在「要发布出去」时才兑现;你只在自己电脑上练语法时,这些全是白背的负担。
该用它的五种场景
别用它的三种场景
| 你的情况 | 更合适的选择 | 为什么不要硬上 Next.js |
|---|---|---|
| 只想练 JavaScript / React 语法,做单页小 demo | Vite + React | Next.js 的服务端那一半对你完全用不上,反而多一层「这个文件到底在哪跑」的困惑 |
| 纯展示页面,完全不碰 Node | Hugo / Hexo 这类静态生成器 | 你要的只是「把 Markdown 变成网页」,用一个需要 Node 环境的框架是杀鸡用牛刀 |
| 已有庞大后端,只需要套一个管理后台壳子 | 任何前端框架都行 | Next.js 最大的优势(前后端一体)对你的项目不成立,学它的成本收不回来 |
和别的方案比:新手选型表
下面三条不是「谁更好」,是「谁的代价你更愿意付」。
| 方案 | 适合什么 | 你要付的代价 |
|---|---|---|
| 纯 React(Vite) | 练手 demo、给现有页面内嵌一小块组件 | 路由、搜索引擎优化、接口全部自己搭 |
| Next.js | 完整网站、博客、后台、全栈小产品 | 要接受它的文件命名约定,写错名字就没页面 |
| Vue + Nuxt | 喜欢 Vue 语法的同类需求 | 中文 AI 生态与招聘岗位规模略小于 React 系 |
| 静态生成器 | 纯文档、纯展示、完全无交互 | 一旦需要交互就得换方案重做 |
选 Next.js。理由是它的试错成本最低:往上能长成全栈应用,往下只当静态站发布也不需要改架构;而反过来,从静态生成器迁移到有交互的站点,通常要推倒重来。
「适合谁 / 不适合谁」——放在这里而不是开头
| 画像 | 适不适合 | 原因 |
|---|---|---|
| 完全不会 JavaScript | 建议先补 | 先去把变量、函数、数组、对象四样弄熟,再来会顺很多。这不是能力问题,是顺序问题 |
| 会一点 JS,没碰过 React | 可以边做边补 | 本文的例子都完整可抄。同时并行补 React 的组件、props、JSX、useState 四样就够 |
| 会 React,没做过上线 | 非常适合 | 你的瓶颈恰好是 Next.js 最强的那部分:渲染、路由、部署 |
| 只想要一个静态博客,讨厌折腾 | 考虑更轻的方案 | 先诚实评估:你愿不愿意为「以后可能加的交互」付现在的学习成本 |
04从零装到跑起来
这一节的目标很具体:十五分钟内,你的浏览器里出现一个属于你的页面,改一个字它自动变。全程只有四条命令。
第一步:确认你的 Node 版本够不够
Node.js 是让 JavaScript 能脱离浏览器、在你电脑上直接运行的程序(行业叫「运行时」)。Next.js 依赖它。打开终端(Mac 上按 Command + 空格,输入「终端」回车),敲:
node -v
# 要看到 v20.9.0 或更高。推荐 v20 或 v22 的 LTS(长期支持)版
npm -v
# 能输出版本号就说明 npm(Node 自带的包管理器)可用
很多教程写「Node 18 以上就行」。16 版起 Node 18 已经不支持了,最低要求是 20.9.0(官方升级指南原文:“Minimum version now 20.9.0 (LTS); Node.js 18 no longer supported”)。用旧版本会在一堆看不懂的地方报错。
还没装 Node?三步装好
打开 nodejs.org,下载标着 LTS 的那个版本,一路「下一步」装完。
再跑一次 node -v,能看到版本号就是装好了。重开这一步不能省,环境变量要重新加载。
如果你以后会有多个项目要求不同的 Node 版本,用 fnm 或 nvm 管起来,避免互相打架。
第二步:一条命令建项目
npx create-next-app@latest my-app
# my-app 是文件夹名,换成你想要的名字,比如 my-blog
# npx 的意思是「临时下载这个工具跑一次,不用先装到全局」
它会问你几个问题。16 版的流程被简化过:第一个问题选「用推荐配置」,剩下的问题就只剩「项目叫什么名字」。
| 它会问什么 | 建议怎么选 | 为什么这么选 / 选错的后果 |
|---|---|---|
| Would you like to use the recommended Next.js defaults? (要不要用推荐默认配置) | Yes(直接回车) | 一步拿到 TypeScript + ESLint + Tailwind + App Router + AGENTS.md 全套。选了 No 会掉进七连问,对新手没必要 |
| Would you like to use TypeScript? | Yes | 类型检查能提前发现拼写和结构错误,AI 也写得更准。不想学类型也能先按 JS 写,它不会拦你 |
| Which linter would you like to use? | ESLint | 注意 16 版这里是一个问题三选一:linter 就是「自动帮你挑毛病的代码检查器」。ESLint 是用了很多年的老牌,遇到问题最好搜到答案;Biome 是后起之秀,把检查和格式化合成一个工具;None 就是不装。新手选 ESLint |
| Would you like to use React Compiler? | No(先不装) | 这是 16 版新增的提问,早期阶段可选。新手先少一个变量,以后随时能开 |
| Would you like to use Tailwind CSS? | Yes | 不用单独写 CSS 文件,直接在 className 里写样式类名。想要纯 CSS 就选 No,脚手架不会拦你 |
Would you like your code inside a src/ directory? | No(新手不用) | 选了以后所有代码都套一层 src/,本教程的路径要自己加前缀。先不选,少一层心智负担 |
| Would you like to use App Router? | Yes | 本文全部基于 App Router。选 No 会得到旧的 pages/ 结构,和本文对不上 |
| Would you like to customize the import alias? | Yes,保持 @/* | 让你能用 @/components/X 代替 ../../../components/X。别改成别的 |
| Include AGENTS.md to guide coding agents? | Yes | 16 版默认自带,它是给 AI 助手看的版本说明书。跳过它等于让 AI 凭记忆写代码 |
第三步:启动,并确认真的成功了
cd my-app
npm run dev
# 终端出现 "Ready in Xms" 并且给出 http://localhost:3000
浏览器打开 http://localhost:3000,看到 Next.js 的欢迎页就对了。然后改一个字试试:打开 app/page.tsx,随便改一句文案,保存,回到浏览器——页面自己变了。这个能力叫热更新:不用手动重启服务器,改完就生效。
① 终端显示 Ready;② 浏览器能打开欢迎页;③ 改字之后页面自动变。任何一步不过,把终端里完整的红色报错复制给你的 AI 助手——不要只截一句话,也不要自己猜着改。
装出来的项目里都有什么
my-app/
├── app/ # 页面和路由都住这里(你 90% 的时间在这)
│ ├── layout.tsx # 全站外壳:导航栏、页脚写这里
│ ├── page.tsx # 首页,对应网址 /
│ └── globals.css # 全局样式
├── public/ # 图片、图标等静态文件,引用时从 / 开头
├── next.config.ts # Next.js 的配置文件
├── tsconfig.json # TypeScript 配置(自动生成,先别动)
├── package.json # 依赖清单和三条脚本:dev / build / start
└── AGENTS.md # 给 AI 助手的版本说明书(16 版自带)
三个常用命令,先分清它们的场合
| 命令 | 它做什么 | 什么时候用 · 别拿它做什么 |
|---|---|---|
npm run dev | 启动开发服务器,带热更新 | 写代码的全程都开着;别用它来判断「能不能上线」,它的检查比真实构建松很多 |
npm run build | 打包 + 全量类型检查 + 生成上线用的文件 | 每次推送代码前跑一次;它是唯一能告诉你「能不能上线」的命令 |
npm start | 以生产模式运行已经打包好的结果 | 本地预览「上线后的真实效果」时用;必须先 build,否则会报错 |
装的过程中卡住了
| 现象 | 原因 | 怎么办 |
|---|---|---|
| 下载依赖特别慢或超时 | 默认的包仓库在境外 | 切国内镜像:npm config set registry https://registry.npmmirror.com,然后重跑创建命令 |
| 提示权限不足 | 用了系统自带的旧 Node | 不要加 sudo。先确认 Node 是官方安装包或 fnm 装的,不是系统自带的那份 |
| 想用 pnpm / yarn 而不是 npm | —— | 完全可以。加 --use-pnpm 之类的参数,或建完后用对应包管理器重装依赖;dev / build 命令不变 |
| 端口 3000 被占用(Port in use) | 上一次的 dev 没关掉 | 换个端口启动:npm run dev -- -p 3001,然后开 localhost:3001 |
05最小例子:两个页面 + 一个能点的链接
目标:亲手验证「文件即路由」。做完你会有首页、关于页,以及它们之间不刷新的跳转。全程不用改任何配置。
第 1 步:确认项目正在跑
终端里 npm run dev 保持开着,浏览器能打开 localhost:3000。
第 2 步:把首页换成你自己的话
打开 app/page.tsx,把 return ( ... ) 里的内容换掉,保存,看浏览器自己变。这一步是在验证热更新通不通。
第 3 步:新建一个关于页
新建文件夹 app/about/,在里面新建文件 page.tsx。访问 http://localhost:3000/about——页面出现了,而你一行路由配置都没写。
第 4 步:让两个页面能互相跳
// app/page.tsx —— 首页
import Link from "next/link";
export default function Home() {
return (
<main>
<h1>我的第一个 Next.js 网站</h1>
<p>改一行字、保存、浏览器自动变——这就是热更新。</p>
<Link href="/about">去关于页看看 →</Link>
</main>
);
}// app/about/page.tsx —— 关于页
import Link from "next/link";
export default function About() {
return (
<main>
<h1>关于我</h1>
<p>这个文件住在 app/about/page.tsx,所以网址是 /about。</p>
<p>这就是「文件即路由」:文件夹名 = 网址路径。</p>
<Link href="/">← 回首页</Link>
</main>
);
}
为什么用 Link 而不是 <a> 标签
Link 是 Next.js 提供的链接组件,它做的是客户端跳转:只换掉页面内容,不整页重新加载。你看到的效果是切换很快、页面不闪白,而且页面上已填了一半的表单不会被清空。
用它自己的原话说,它通过「预取」(prefetch)提前把目标页准备好——你把鼠标移到链接上时,浏览器已经在悄悄下载下一页了。
<a>
跳到别的网站(比如 https://nodejs.org)时用普通的 <a>。Link 是给站内跳转用的——它管不了别人家的站点。
做完请确认这三条
| 检查什么 | 应该看到什么 | 没看到说明什么 |
|---|---|---|
访问 / | 首页显示你写的中文标题 | 文件存的位置不对,或 dev 服务器没在跑 |
访问 /about | 关于页出现,且你没改过任何配置 | 文件名不是 page.tsx,或大小写写错(Page.tsx 无效) |
| 点链接跳转 | 没有整页白屏闪一下 | 如果你用的是 <a>,换成 Link |
顺手加一点样式(可选)
脚手架带了 Tailwind:不写 CSS 文件,直接在 className 里写类名。把首页的 <main> 换成下面这样,立刻像个正经网站:
// app/page.tsx —— 只换样式,逻辑一个字没动
<main className="max-w-2xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-4">我的第一个 Next.js 网站</h1>
<p className="text-gray-400 mb-6">改一行字、保存、浏览器自动变——这就是热更新。</p>
<Link href="/about" className="text-indigo-400 underline">
去关于页看看 →
</Link>
</main>
max-w-2xl mx-auto = 限制最宽、左右居中;p-8 = 四边留白;text-3xl font-bold = 大标题、加粗。不用背——用到哪个现查,查三次就记住了。查不到就打开 Tailwind 官网的搜索框,输入你想做的效果。
06四个文件名,决定一个页面能不能被访问
这一节是全文最该记住的一节。Next.js 的「魔法」全靠这四个名字,写错一个字母,页面就是 404。
四个特殊文件名分别是干什么的
| 文件名 | 用房子打个比方,它是什么 | 每个路由都必需吗 · 缺了会怎样 |
|---|---|---|
page.tsx | 房间本身——用户真正看到的内容 | 必需。没有它,这个网址就是 404,哪怕文件夹和别的文件都在 |
layout.tsx | 楼房的框架——走廊、电梯、门牌,包住里面所有房间 | 不必需,但根目录那个必需。它对同级及以下所有页面生效,且跳转时不重新渲染 |
loading.tsx | 等电梯时墙上的广告屏——数据还没到,先给点东西看 | 不必需。有了它,进这个分区时先看到占位内容,数据到了自动替换 |
error.tsx | 房间里的灭火器——出错了兜住,别让整站崩 | 不必需。有了它,这个分区出错时显示你写的提示,而不是一片报错 |
为什么 page.tsx 这个名字不能省
因为 Next.js 用「有没有 page.tsx」来判断「这个文件夹算不算一个可访问的网址」。你可以有 app/blog/ 文件夹,里面放着组件、工具函数、样式;只要没有 page.tsx,/blog 就打不开。这不是限制,是给「只想放文件、不想开页面」留的空间。
「我明明建了 app/blog.tsx,为什么 /blog 是 404?」——因为路由只认文件夹里的 page.tsx,不认 app/blog.tsx 这种平铺的文件。文件系统大小写敏感,Page.tsx、page.ts(换成 js)之外的名字一律不认。
layout.tsx:导航栏只写一次,全站生效
根 layout.tsx 是每个页面外面那层壳。你把导航栏写在这里,所有页面自动都有——不用每个页面复制一遍。
// app/layout.tsx —— 根外壳,所有页面都会被它包住
import Link from "next/link";
import "./globals.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body>
<nav>
<Link href="/">首页</Link> |{/* 导航只写这一次,全站生效 */}
<Link href="/about">关于</Link> |
<Link href="/blog">博客</Link>
</nav>
<hr />
{children}{/* 每个页面的内容会被塞到这里 */}
<footer>© 2026 我的小站</footer>
</body>
</html>
);
}
{children} 是什么
它是一个「占位洞」。Next.js 把当前网址对应的那个页面的内容,塞进这个洞里。这就是为什么导航栏不会随页面切换而重新出现——壳没变,只是洞里的东西换了。副作用是壳里的状态(比如搜索框里打的字)在跳转时会保留。
layout.tsx 可以嵌套:不同板块穿不同外壳
把 layout.tsx 放进子文件夹,它就只包住那个文件夹里的页面。营销页一套外壳、后台另一套外壳,互不干扰——第 08 节的路由组就是为这个场景准备的。
节点级兜底:loading.tsx 与 error.tsx
// app/blog/loading.tsx —— 进博客分区先看到它,数据到了自动替换
export default function Loading() {
return <p>正在加载文章列表…请稍候</p>;
}// app/blog/error.tsx —— 这个分区出错时的兜底界面
"use client"; // error.tsx 必须是客户端组件:它要能响应点击
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<main>
<h1>出了一点小问题</h1>
<p>{error.message}</p>
<button onClick={reset}>再试一次</button>
</main>
);
}
error.tsx 必须标 "use client"
因为它里面有个「再试一次」按钮,按钮要能响应点击——而点击事件只能在浏览器里发生。这条规则在第 07 节会讲透。记住这个反直觉的点:报错页面本身也是一个交互组件。
07服务端组件和客户端组件:一条分界线,两套后果
这是 App Router 最核心、也最容易把人卡住的新概念。本节按五步拆:它不是什么 → 它是什么 → 怎么运作 → 为什么重要 → 新手坑在哪。
第一步:它不是「前端项目 vs 后端项目」那种分工
你可能会想:「服务端」和「客户端」听起来像是要建两个项目、或者两个文件夹,一个写后端一个写前端。
不是。这两个词说的不是项目、不是文件夹、也不是两种文件类型。它指的是同一份组件代码,在哪个地方被执行——是在你租的那台服务器上执行完再发结果,还是在访客的浏览器里执行。
第二步:它到底是什么——一行声明决定运行位置
在 app/ 里写的每一个组件,默认都是服务端组件:它在服务器上执行、把结果拼成 HTML 发出去,自身不往浏览器发送 JavaScript。行业缩写叫 RSC(React Server Component 的首字母)。
如果你想让某个组件变成客户端组件(在浏览器里跑、能响应点击和输入),就在那个文件的最顶部写一行:
"use client"; // 这一行写在文件最顶端,声明「我在浏览器里跑」
import { useState } from "react";
export default function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>点了 {n} 次</button>;
}
第三步:怎么运作——一句话版本
服务端组件在服务器执行,可以直接读数据库、用密钥调外部接口,这些代码和密钥都不会下发到浏览器;渲染出来的结果(HTML)发给浏览器。
客户端组件会被打包进 JavaScript 一并下发,浏览器拿到后接手,恢复点击、输入、动画这些交互能力——这一步行业叫「注水」(hydration),意思是给已经画好的静态页面注入生命力。
第四步:为什么这个默认值重要
| 默认服务端带来什么 | 具体效果 | 什么时候你会明显感受到 |
|---|---|---|
| 首屏更快 | 浏览器先拿到完整 HTML,不用等 JavaScript 下载完才看到内容 | 手机弱网下打开你的站时,差别最直观 |
| 密钥不泄露 | 只有客户端组件的代码会下发;服务端代码留在服务器上 | 接数据库、接第三方付费接口时,这就是安全底线 |
| 下发体积更小 | 不需要交互的组件不产生 JavaScript | 页面越多越明显;纯展示型页面几乎只有 HTML |
| 能直接读数据库 | 组件函数可以是 async,直接 await 查询结果 | 不必再写「请求接口 → 等 JSON」那一套 |
第五步:新手坑在哪(对照看,一眼就懂)
// 没加 "use client",却用了 useState → 运行时报错
import { useState } from "react";
export default function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}
"use client"; // 顶行声明:我在浏览器里跑
import { useState } from "react";
export default function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}
在服务端组件里用 useState、onClick、useEffect 这类东西,会报 useState is not allowed in Server Components 之类的错。修法不是把它改成别的东西,而是在那个文件第一行加 "use client"。
该把 "use client" 加在哪一层
这是最容易加错的地方。惯例是加到最末端的叶子组件上,不要加到整页。
// ✅ 推荐:页面本身是服务端组件,只有按钮那一小块是客户端
// app/blog/page.tsx
import LikeButton from "./like-button";
export default async function Blog() {
const posts = await getPosts(); // 服务端取数据
return (
<ul>
{posts.map((p) => (
<li key={p.id}>
{p.title}
<LikeButton id={p.id} />{/* 只有它标了 "use client" */}
</li>
))}
</ul>
);
}
反过来的做法——在 app/blog/page.tsx 顶部直接写 "use client"——会让整页都变成客户端渲染,你前面赚到的首屏快、体积小全部还回去。这是新手最常见的一种「明明照抄了却更慢」。
四条边界,记成一张表
| 你想做的事 | 该放哪边 | 为什么 / 别搞错什么 |
|---|---|---|
| 读数据库、带密钥调接口 | 服务端组件 | 代码与密钥不下发浏览器。别为了省事把它挪进 "use client" 文件 |
| 页面的正文、标题、列表 | 服务端组件 | 搜索引擎要能直接读到。别把对外内容整页标成客户端 |
| 点击、输入框、开关、动画 | 客户端组件 | 这些必须在浏览器发生。尽量拆成小叶子,别整页标 |
| 读取浏览器专有信息(当前位置、窗口宽度、登录态里的本地存储) | 客户端组件 | 服务器上没有这些概念。可以用 useEffect 在挂载后再读 |
默认放服务端,只有报错说「这个不能在这用」时才拆 client。判断顺序是「先写服务端,跑起来报错再拆」,不要提前猜。
08路由:文件夹名就是网址
三类文件夹能覆盖 90% 的路由需求:普通嵌套、动态参数、分组。只要记住括号和方括号的区别。
嵌套:文件夹套文件夹
app/
├── page.tsx # → /
├── about/
│ └── page.tsx # → /about
└── blog/
├── page.tsx # → /blog(列表页)
└── first-post/
└── page.tsx # → /blog/first-post(详情页)
动态参数:一个文件接住无数个网址
博客有几百篇文章,你不可能手工建几百个文件夹。把文件夹名写成方括号包起来的 [slug],就得到一个「占位」文件夹——里面任何值都进同一个文件。slug 是行业里对「网址里那一段用来标识内容的短名字」的叫法。
app/blog/[slug]/page.tsx # → /blog/任意值 都进这个文件
在 15 版之前,你可以直接写 params.slug。从 16 版起,同步取参数被彻底移除,只能先 await 再取。旧教程里清一色是同步写法,照抄必报错。正确的两种写法:
// 写法一(推荐):用官方命令生成的类型 helper
// 先在终端跑一次 npx next typegen,它会生成 PageProps 这个全局类型
// app/blog/[slug]/page.tsx
export default async function Page(props: PageProps<"/blog/[slug]">) {
const { slug } = await props.params; // 必须先 await
return <h1>文章:{slug}</h1>;
}// 写法二:手写类型,效果一样,不依赖 typegen
// app/blog/[slug]/page.tsx
export default async function Post({
params,
}: {
params: Promise<{ slug: string }>; // 注意是 Promise,不是对象
}) {
const { slug } = await params; // /blog/hello 就拿到 slug = "hello"
return (
<main>
<h1>文章:{slug}</h1>
<p>真实项目里,这里用 slug 去数据库查文章正文。</p>
</main>
);
}
为什么会有这个改动?因为 Next.js 想让「取参数」和「取数据」的写法统一——两者都是「要等一下才有值」,那就用同一种语法(await)表达。代价是所有旧代码都要改一遍,这也是 16 版最集中的升级工作量。
分组:加一对括号,只归类、不进网址
app/
├── (marketing)/
│ ├── about/page.tsx # → /about(括号不出现在网址里)
│ └── pricing/page.tsx # → /pricing
└── (admin)/
└── dashboard/page.tsx # → /dashboard
用途是给不同板块套不同的外壳:营销页一个 layout、后台另一个 layout,而网址依然干净,不会多出 /marketing/about 这种前缀。
(admin) 圆括号 = 分组,只影响谁和谁共用一个外壳,网址里看不到它。
[slug] 方括号 = 动态参数,会匹配那一段位置上的任意值,并把它交给你。
写错一个符号,路由行为天差地别,而且报错往往不明显。这是新手排错时最该先检查的地方。
页面跳转的三件套
| 写法 | 什么时候用 | 可复制的形式 |
|---|---|---|
Link 组件 | 用户点一下就跳(导航、列表项) | <Link href="/blog">看博客</Link> |
useRouter().push() | 代码里主动跳(表单提交成功之后) | router.push("/done"),需要 "use client" |
redirect() | 服务端直接跳(没登录就踢去登录页) | redirect("/login"),来自 next/navigation |
详情页的两个搭档:找不到就 404,找得到就提前生成
动态参数能接住任意网址,但用户可能乱输一个不存在的。这时要友好地显示 404,而不是让页面崩掉。另一个优化是:如果文章列表在构建时就是已知的,可以提前把页面生成好,用户打开就是秒开的静态页。
// app/blog/[slug]/page.tsx —— 404 兜底 + 预生成,一次配齐
import { notFound } from "next/navigation";
const posts: Record<string, string> = {
"hello-nextjs": "欢迎来到 Next.js 的世界…",
"first-deploy": "记录第一次上线的心情…",
};
// 构建时把这两篇提前生成好:用户打开就是秒开的静态页
export function generateStaticParams() {
return Object.keys(posts).map((slug) => ({ slug }));
}
export default async function Post({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params; // 16 版:先 await
const body = posts[slug];
if (!body) notFound(); // 乱输的 slug 进 404,不炸站
return (
<main>
<h1>{slug}</h1>
<p>{body}</p>
</main>
);
}
查得到就渲染、查不到就 notFound()、热门页提前 generateStaticParams()。详情页三件套,背下来走遍天下。
全站 404 页:not-found.tsx
上一节的 error.tsx 管的是「代码报错」,而 notFound() 触发的 404 归 not-found.tsx 管。放一个在 app/ 根目录,全站乱输网址都有体面提示:
// app/not-found.tsx —— 全站 404,先有它再谈体验
import Link from "next/link";
export default function NotFound() {
return (
<main>
<h1>404 · 页面去火星了</h1>
<p>地址可能输错了,或者内容被搬走了。</p>
<Link href="/">← 回首页</Link>
</main>
);
}
三个「兜底文件」的分工,别混
| 文件 / 函数 | 触发时机 | 为什么分开 · 别当什么用 |
|---|---|---|
loading.tsx | 数据还没到,正在等 | 管「慢」;别用它显示错误,那不是它的职责 |
error.tsx | 代码抛出了异常 | 管「坏」;必须是客户端组件,因为要能点「重试」 |
not-found.tsx | 你主动调了 notFound(),或网址根本不存在 | 管「没有」;404 在业务上是正常结果,不该报成错误 |
09取数据:一个 async 函数就够
先破一个过时的印象:取数据不用 useEffect 了
如果你看过早几年的 React 教程,取数据的标准写法是一长串:先声明一个装数据的变量、再声明一个装「加载中」的变量,然后在 useEffect(React 里「页面加载后要做的事」的钩子)里发请求,最后还要处理依赖警告。二十行起步。
在 Next.js 的服务端组件里,这一整套都不需要:组件函数自己可以是异步的,直接 await 结果就用。
// app/blog/page.tsx —— 整个函数是 async 的,这就是服务端组件的特权
async function getPosts() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5");
if (!res.ok) throw new Error("文章加载失败");
return res.json();
}
export default async function Blog() {
const posts = await getPosts(); // 服务端拼好 HTML 才发给浏览器
return (
<main>
<h1>博客列表</h1>
<ul>
{posts.map((p: { id: number; title: string }) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
</main>
);
}// 对照:老的客户端写法(需要交互的搜索框才这么写)
"use client";
import { useState, useEffect } from "react";
export default function Posts() {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/posts")
.then((r) => r.json())
.then((d) => { setPosts(d); setLoading(false); });
}, []);
if (loading) return <p>加载中…</p>; // 加载态还得自己画
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}
缓存:默认不缓存了,要缓存得自己说
很多教程教「fetch 默认会缓存,要新鲜数据才加 no-store」。这个前提已经反了。现在的基本形态叫「auto no cache」:开发时每次请求都会去取最新,构建时取一次并把页面静态化。缓存是选择加入的,不是默认的。(官方文档原话:“Caching is opt-in.”)
// 实时数据:每次请求都去取最新的(多数动态数据的正确选择)
const res = await fetch(url, { cache: "no-store" });
// 明确要缓存:告诉 Next.js 去用它自己的服务端缓存
const res3 = await fetch(url, { cache: "force-cache" });
// 折中:最多缓存 60 秒,之后后台悄悄更新(把旧的先给你,同时去取新的)
const res2 = await fetch(url, { next: { revalidate: 60 } });
| 写法 | 含义 | 什么时候用 · 别用错在哪 |
|---|---|---|
| 不写任何选项(默认) | 开发时每次都取最新;构建时取一次并静态化 | 内容不常变、又想快——博客、文档。别以为它「每次请求都最新」,构建后它就是一份静态快照 |
cache: "no-store" | 每次请求都去远端取 | 库存、在线人数、用户私有数据。别给高流量公开页用,会把服务器压力放大 |
cache: "force-cache" | 优先用服务端缓存,命中就直接返回 | 几乎不变的数据。别对需要实时的数据用——你会看到过期内容 |
next: { revalidate: 60 } | 最多缓存 60 秒,之后后台悄悄重新生成(这套机制官方叫 ISR,即「增量静态再生」) | 新闻列表、排行榜这类「又快又不太旧」的场景。别拿它当实时方案的替代 |
需要交互的数据,还是放客户端
搜索框、筛选器这类「用户一动就要变」的地方,仍然用老办法(useEffect + fetch),或者用专门管这件事的库。下面那段代码里的「防抖」是这里唯一的新词:它指「用户停下来之后再发请求」——人连着敲字时只算最后一次,而不是每敲一下都打一次接口。原则一句话:首屏要展示的数据放服务端,需要即时交互的数据放客户端。
"use client"; // 客户端搜索框:输入即筛选,必须跑在浏览器
import { useState, useEffect } from "react";
export default function SearchBox() {
const [q, setQ] = useState("");
const [list, setList] = useState<string[]>([]);
useEffect(() => {
if (!q) { setList([]); return; }
const t = setTimeout(async () => {
const res = await fetch(`/api/search?q=${q}`);
const data = await res.json();
setList(data.items);
}, 300); // 防抖 300ms:别每敲一个字母就请求一次
return () => clearTimeout(t); // 清掉上一次的定时器
}, [q]);
return (
<div>
<input value={q} onChange={(e) => setQ(e.target.value)} placeholder="搜点什么…" />
<ul>
{list.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
① 防抖别省:加了 300 毫秒的等待,用户连着敲字时只在停下后请求一次,否则输入法每敲一下都打一次接口。
② 加载态和空状态都要画:别只画「有数据」的样子,否则用户不知道是卡住了还是没结果。
③ 密钥绝不能出现在这个文件里:标了 "use client" 的代码会下发到浏览器,谁都看得到。
放进哪一边:一张决定表
| 数据的样子 | 放哪 | 理由 |
|---|---|---|
| 文章正文、商品详情 | 服务端 | 要能被抓取到,也要快 |
| 需要密钥才能调的接口 | 服务端 | 密钥不下发浏览器,这是安全底线 |
| 搜索联想、实时筛选 | 客户端 | 用户一边敲一边要变,服务端接不住这种频率 |
| 用户的私有草稿、本地偏好 | 客户端 | 和服务端渲染无关,且不该被缓存成公共页面 |
10进阶三件套:写操作、接口、请求拦截
学会这三样,你就能独立做完一个带写操作的完整站点。
一、Server Actions:表单不用再写接口了
先讲它是什么:一个写在服务端的普通函数,函数顶部加一行 "use server",然后表单可以直接把它当成提交目标。「Action」在这里就是「提交时要做的那件事」。
对比一下省了什么:老写法要前端发请求 → 后端接口接住 → 校验 → 写库,前后两个文件、两次数据搬运。现在一个文件、一个函数。
// app/guestbook/actions.ts —— 服务端函数
"use server";
export async function addMessage(formData: FormData) {
const text = formData.get("text");
// 这里可以直接写数据库,密钥不会泄露到浏览器
console.log("收到留言:", text);
}// app/guestbook/page.tsx —— 表单直接绑定它
import { addMessage } from "./actions";
export default function Guestbook() {
return (
<form action={addMessage}>
<input name="text" placeholder="说点什么…" />
<button type="submit">留言</button>
</form>
);
}
注意表单里没有 onSubmit、没有 fetch、没有 e.preventDefault()。表单在 JavaScript 还没加载完时也能提交(浏览器原生就支持表单提交),这对网络慢的用户是实打实的体验差别。这也是它相比「自己写 fetch」最容易被忽略的优势。
留言、评论、点赞这类小写入非常适合。但传文件、处理大文件、长时间运行的任务还是走专门的接口,别塞在这里。
写完之后页面还是旧的?用 revalidatePath
Server Action 写完数据库后,页面可能还在显示缓存的旧内容。要告诉 Next.js「这一页脏了,重做一份」:
"use server";
import { revalidatePath } from "next/cache";
export async function addMessage(formData: FormData) {
const text = formData.get("text");
// …把 text 写入数据库…
revalidatePath("/guestbook"); // 留言板页立即更新,不用等缓存过期
}
16 版还新增了一个更「即时」的同类 API 叫 updateTag,语义是「改完立刻让用户看到自己的改动」。新手先用熟 revalidatePath 就够,遇到「缓存按标签管理」的项目再学标签那一套。
二、Route Handlers:正经写一个 JSON 接口
在 app/api/ 下面建文件夹,里面放一个叫 route.ts 的文件,导出 GET、POST 这样的函数——它就变成了标准接口,可以被小程序、手机 App 或第三方调用。
// app/api/hello/route.ts —— 访问 /api/hello 得到 JSON
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ message: "你好,我是 Next.js 接口" });
}
export async function POST(request: Request) {
const body = await request.json();
return NextResponse.json({ received: body });
}
三、请求拦截:middleware.ts 已经改名叫 proxy.ts
官方文档现在明确写着:middleware 这个文件约定已弃用,改名为 proxy;导出的函数也从 middleware 改名为 proxy;而且它默认运行在完整的 Node.js 运行时,不再是那个连数据库都连不上的轻量边缘环境。
如果你在旧项目里写 middleware.ts,它还能跑但会有弃用提醒;新项目请直接用 proxy.ts。这解释了为什么你搜到的教程和官方文档对不上——不是谁写错了,是版本变了。
// proxy.ts —— 放在项目根目录(和 app/ 同级)
// 保护 /dashboard:没登录就放行到登录页
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function proxy(request: NextRequest) {
const loggedIn = request.cookies.get("token");
if (!loggedIn && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next(); // 放行
}
// 只对 /dashboard 开头的路径生效;不写 matcher 会对每个请求都跑
export const config = { matcher: ["/dashboard/:path*"] };
matcher 不写会出事
官方文档专门提醒:不写 matcher 时,代理会对每一个请求都执行,包括静态文件、图片优化请求和 public/ 里的资源。结果就是你的登录拦截可能把 CSS、JS、图片一起拦掉,页面样式全崩。所以要么写 matcher,要么用否定匹配把静态资源排除掉。
三件套怎么选
| 你的需求 | 用谁 | 一句话理由 · 别用错在哪 |
|---|---|---|
| 表单提交、点赞、评论 | Server Actions | 零接口文件,写个函数就行。别用它传大文件 |
| 给外部或跨端用的 JSON 接口 | Route Handlers | 用的是 REST 这套通行约定:网址代表「东西」,请求方法(GET 取、POST 交)代表「对它做什么」——别人照着约定就能接进来。站内表单别多此一举走它 |
| 登录拦截、地区跳转、A/B 分流 | proxy(旧名 middleware) | 请求进门前先过安检。别在里面做重计算——它是每个请求都要跑的 |
流式渲染:先出壳、再填肉
页面里有的部分快、有的部分慢(比如评论区要查数据库)。别让慢的拖住整页——用 Suspense 包住慢的那块,快的先显示,慢的到了自动补上。
// app/blog/[slug]/page.tsx —— 正文秒开,评论慢慢到
import { Suspense } from "react";
export default async function Post(props: PageProps<"/blog/[slug]">) {
const { slug } = await props.params; // 16 版:先 await
return (
<main>
<h1>{slug}</h1>
<p>正文内容(快的,先出)</p>
<Suspense fallback={<p>评论加载中…</p>}>
<Comments slug={slug} />{/* 慢的组件,后到 */}
</Suspense>
</main>
);
}
async function Comments({ slug }: { slug: string }) {
await new Promise((r) => setTimeout(r, 2000)); // 模拟慢查询
return <p>{slug} 的评论区(2 秒后出现)</p>;
}
loading.tsx 的关系
loading.tsx 是整页级的占位;Suspense 是组件级的占位。整页慢用前者,局部慢(评论区、推荐位)用后者。两个配合,体验最好。
11把零件拼成一个站:三个练习和一条路线
前面每一节给的都是零件。这一节把它们拼起来。建议按顺序做三个练习,每个只比上一步多一个能力。
练习一:个人博客(只读——先跑通路由和数据)
// app/blog/page.tsx —— 列表页骨架(先用假数据,跑通再说)
import Link from "next/link";
const posts = [
{ slug: "hello-nextjs", title: "你好,Next.js" },
{ slug: "my-first-deploy", title: "第一次部署上线" },
{ slug: "server-components", title: "搞懂服务端组件" },
];
export const metadata = { title: "博客 | 我的小站" };
export default function Blog() {
return (
<main>
<h1>博客</h1>
<ul>
{posts.map((p) => (
<li key={p.slug}>
<Link href={`/blog/${p.slug}`}>{p.title}</Link>
</li>
))}
</ul>
</main>
);
}
配套的详情页用第 08 节那份 [slug]/page.tsx(含 notFound() 和 generateStaticParams())。做完你会有:列表、详情、404 兜底、预生成。注意 params 必须 await。
练习二:留言板(读写闭环——加一个 Server Action)
// app/guestbook/page.tsx —— 读 + 写同页
import { addMessage } from "./actions";
const messages = ["第一条留言:这个网站真快!", "第二条:学会了文件即路由"];
export default function Guestbook() {
return (
<main>
<h1>留言板</h1>
<ul>
{messages.map((m, i) => (
<li key={i}>{m}</li>
))}
</ul>
<form action={addMessage}>
<input name="text" placeholder="说点什么…" />
<button type="submit">留言</button>
</form>
</main>
);
}
配套的 app/guestbook/actions.ts 用第 10 节那份(含 revalidatePath)。这一步之后你就有了「读 + 写」的完整闭环。
练习三:待办清单(增删改——第一次亲手管状态)
前两个练习都是服务端为主,这个专练客户端。顺手理解一件事:状态就是「会变的记忆」。
"use client"; // 待办清单:全是点击和输入,必须是客户端组件
import { useState } from "react";
type Todo = { id: number; text: string; done: boolean };
export default function Todos() {
const [todos, setTodos] = useState<Todo[]>([]);
const [input, setInput] = useState("");
function add() {
if (!input.trim()) return; // 空输入直接忽略
setTodos([...todos, { id: Date.now(), text: input, done: false }]);
setInput("");
}
function toggle(id: number) {
setTodos(todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
}
function remove(id: number) {
setTodos(todos.filter((t) => t.id !== id));
}
return (
<main>
<h1>我的待办</h1>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="输入后点新增"
/>
<button onClick={add}>新增</button>
<ul>
{todos.map((t) => (
<li key={t.id}>
<span onClick={() => toggle(t.id)}>
{t.done ? "✅ " : "⬜ "}
{t.text}
</span>
<button onClick={() => remove(t.id)}>删</button>
</li>
))}
</ul>
<p>共 {todos.length} 项,已完成 {todos.filter((t) => t.done).length} 项</p>
</main>
);
}
todos 是那份「会变的记忆」,setTodos 是唯一的修改入口。点一次按钮 → 调 setTodos → 页面自动重画。所有 React 交互都是这个循环,没有例外。看懂这一点,你就跨过了 React 最大的一道坎。
合体:把三个练习拼成一个站
app/
├── (marketing)/ # 营销页组:网址里看不到括号
│ ├── page.tsx # → / 首页:自我介绍 + 三个入口
│ ├── about/page.tsx # → /about
│ └── layout.tsx # 营销页专用外壳
├── (app)/ # 应用页组:后台、设置
│ ├── dashboard/page.tsx # → /dashboard
│ └── layout.tsx # 带侧边栏的外壳
├── blog/
│ ├── page.tsx # → /blog 列表
│ └── [slug]/page.tsx # → /blog/任意值 详情
├── guestbook/ # 留言板:page.tsx + actions.ts
├── todos/page.tsx # 待办:客户端组件
├── api/hello/route.ts # JSON 接口
└── layout.tsx # 根外壳:导航、字体、全局样式
components/ # 跨页面复用的 UI(按钮、卡片)
lib/ # 工具函数、数据库连接
public/ # 静态资源
练习路线:每一步都是上一步的自然延伸
| 顺序 | 做什么 | 你会新学到 |
|---|---|---|
| 1 | 博客(只读) | 文件即路由、动态参数、服务端取数据 |
| 2 | 留言板(读写) | Server Actions、缓存刷新 |
| 3 | 待办清单(增删改) | 客户端组件、状态、事件 |
| 4 | 接真实数据库 | 把假数组换成真实查询 |
| 5 | 加登录 | proxy 拦截、会话管理 |
七条军规,越早养成越省事
| 规则 | 为什么 | 违反的典型症状 |
|---|---|---|
| 服务端优先,交互才下沉 | 每多标一个 "use client",首屏就重一点 | 页面打开变慢,但你找不到是哪一行造成的 |
| 取数据放服务端 | 首屏数据在服务端 await;密钥只活在服务端 | 密钥出现在浏览器网络请求里(等于公开) |
图片永远用 Image | 原生 <img> 又大又会造成页面抖动 | 图片加载完页面内容突然往下跳 |
每个页面写 metadata | 五行代码换搜索排名和分享卡片 | 分享到微信/社交平台是一片空白 |
上线前必跑 build | dev 能跑 ≠ build 能过 | 推上去之后线上部署失败 |
params 记得 await | 16 版删掉了同步写法 | params.slug 是 undefined,或类型报错 |
| 让 AI 读版本匹配的文档 | 脚手架自带的 AGENTS.md 就是干这个的 | AI 给你写 middleware.ts 和同步 params |
提交代码前的自查七问
npm run build 本地过了吗?metadata 了吗?key / secret / token,确认它们没出现在标了 "use client" 的文件里。Image 了吗?<img> 换掉,同时记得写 width 和 height。"use client" 应该只盖在叶子上,不该盖在整页上。12部署上线:推代码就是上线
代码只在自己电脑上跑不叫作品,别人能打开才叫上线。这一节给你两条路,先走快的那条。
上线前,本地先过三关
# 第一关:生产构建能不能过
npm run build
# 期望看到 Compiled successfully / Generating static pages
# 报错就从第一条红色错误开始修,多半是类型问题或 params 没 await
# 第二关:以生产模式跑起来,亲手点一遍
npm start
# 打开 http://localhost:3000 验收
# 第三关:确认没有把测试地址、私钥硬编码进代码(搜一遍再上线)
npm run dev 一切正常,npm run build 直接挂。原因是 build 会做全量类型检查和路由校验,而 dev 为了快会放过一些东西。所以「能 dev 不能 build」不是玄学,是必然。
方案 A:Vercel 一键上线(新手首选)
建一个仓库,把项目推上去。不会 Git 的话先补一下最基本的 add / commit / push 三步。
打开 vercel.com,用 GitHub 账号登录,点 Add New → Project,选中你的仓库,一路默认,点 Deploy。
两分钟左右你会得到 xxx.vercel.app。之后每次 git push,它自动重新部署——这就是「推代码即上线」。
个人博客、作品站这类小流量项目,免费额度完全够用。先上线,再优化;等流量真的涨了再考虑付费。
方案 B:自己服务器 / Docker 部署
公司内网、有特殊合规要求时用。核心只有三步:
npm run build # 在服务器上打包
npm start # 启动生产服务,默认 3000 端口
# 再配一个反向代理(Nginx / Caddy)把域名指过来,并加上 HTTPS
环境变量:密钥怎么放才不泄露
# .env.production —— 生产环境的密钥放这里,不进 git
DATABASE_URL="postgresql://..."
NEXT_PUBLIC_SITE_URL="https://mysite.com"
「打包」在这里指:把浏览器要跑的那部分代码和服务端要跑的那部分分开整理。只有以 NEXT_PUBLIC_ 开头的变量,才会被打进浏览器那一份里(也就是任何人按 F12 都能看到)。所以:数据库密码、支付密钥这类东西,绝对不能加这个前缀——加了就等于把它们公开在网页源码里。反过来,需要在前端代码里读到的站点地址,就必须加。
两条路怎么选
| 维度 | Vercel(方案 A) | 自建服务器(方案 B) |
|---|---|---|
| 上手难度 | 点几下鼠标 | 要懂 Node、反向代理(挡在你自己程序前面、负责转发请求的那层软件)、HTTPS(把浏览器和服务器之间的传输加密,地址栏出现小锁)三样 |
| 费用 | 个人小站免费额度够用 | 云服务器按月付费 |
| HTTPS 证书 | 自带,自动续期 | 自己配(Caddy 最省事) |
| 适合谁 | 新手、个人项目、快速验证想法 | 有运维要求的团队、内网项目 |
上线后第一件事:看日志
| 你的部署方式 | 日志在哪看 | 为什么必须看 |
|---|---|---|
| Vercel | 项目面板 → Deployments → 某次部署 → Runtime Logs | 页面 500 时,真正的报错只在这里,前端看不到 |
| 自建 | npm start 那个终端的输出;生产建议用 pm2 start npm -- start 托管 | 进程挂了会自动重启,日志不会随终端关闭而丢 |
规矩一句话:页面出错先看日志第一行,再回本地复现,别靠猜。
绑定自己的域名
任意域名商都行,搜一个喜欢的名字下单,一年几十块。
项目 → Settings → Domains,输入你的域名,按提示去域名商后台加一条 DNS 记录(通常是一条 CNAME)。
DNS 生效要几分钟到几小时。之后访问自己的域名就是你的网站,证书自动续期,不用管。
① 手机关掉 Wi-Fi 用流量也能打开(排除只是本机缓存);② 所有页面点一遍没有 500;③ 在浏览器里「查看网页源代码」,能看到文章正文的文字(这是服务端渲染生效的直接证据)。
13常见坑与报错速修
新手九成的卡点集中在这一张表里。按报错查,不要从第一条开始读全文。
按现象查:十个高频坑
| 你看到的现象 | 真实原因 | 怎么修 |
|---|---|---|
useState is not allowed in Server Components 之类 | 在默认的服务端组件里写了交互 | 把用到交互的那一小块拆成独立文件,顶部加 "use client"。加在叶子上,别整页标 |
params.slug 是 undefined,或类型报错说它不是对象 | 16 版参数是异步的,同步取法已被删除 | 改成 const { slug } = await params;,类型写 Promise<{ slug: string }> |
| 访问路径 404,但文件明明存在 | 路由靠的是 page.tsx,不是文件夹本身 | 确认那个文件夹里有 page.tsx;再检查大小写,Page.tsx 无效 |
改了 middleware.ts 却没生效 | 16 版已把它改名成 proxy.ts,函数名也改成 proxy | 文件改名为 proxy.ts、导出函数改名为 proxy;或跑官方 codemod |
| 改了代码页面没变化 | 三连查:dev 还在跑吗 / 改的是不是当前访问的那个路由 / 是不是被缓存了 | 先看终端有没有报错;再确认改的文件属于当前网址;必要时重跑 npm run dev |
npm run build 挂了,dev 却好好的 | build 做全量类型检查:未用变量、隐式 any、类型不匹配都会拦 | 滚到最顶看第一条红色 Error,修完再跑。后面的报错常常会一起消失 |
| 图片不显示或页面抖动 | 路径或宽高没给 | 图片放 public/,src 用 / 开头;Image 必须给 width/height;远程图片要在 next.config.ts 里配域名白名单 |
| 终端里一堆英文报错,不知从哪看起 | —— | 只看第一条。滚到最顶找第一个红色的 Error:,它后面的文件名和行号就是案发现场 |
Port 3000 is in use | 上一次的 dev 没关 | 换端口:npm run dev -- -p 3001,然后开 localhost:3001 |
| Tailwind 的类名写了没效果 | 三连查:全局样式有没有被引入 / 类名有没有拼错 / dev 要不要重启 | 确认根 layout.tsx 里 import "./globals.css";类名大小写敏感(text-3xl 不是 text-3XL) |
从旧版 pages/ 迁移过来时的水土不服
如果你在别人的老项目里看到下面这些名字,要知道它们已经被取代了。别逐行翻译,重写更顺:
旧写法(pages/ 路由) | 现在用什么 | 为什么 |
|---|---|---|
getServerSideProps | 服务端组件里直接 await | 同一个文件里取数据,少一层数据传递 |
getStaticProps | 默认静态化,或 generateStaticParams() | 不再需要手动声明「我要静态生成」 |
_app.tsx / _document.tsx | app/layout.tsx | 用一层可嵌套的 layout 替代两个特殊文件 |
middleware.ts | proxy.ts | 16 版改名,且默认跑完整 Node.js 运行时 |
大版本升级:用官方工具,别手改版本号
# 1. 先提交干净代码(留一份能回滚的档)
git add -A && git commit -m "before upgrade"
# 2. 跑官方升级工具,它会自动改掉大部分机械性变动
# (注意:它不会跑全部迁移,同步 params 那类还要单独跑一次 async 迁移)
npx @next/codemod@canary upgrade latest
# 3. 过一遍官方升级指南里的破坏性改动,再 build 验证
npm run build
英文报错高频五句
| 报错里的关键词 | 多半是 | 先查哪里 |
|---|---|---|
Module not found | 路径写错了 | 检查 import 的路径和文件名大小写 |
Hydration failed | 服务端和浏览器渲染出的结果不一致 | 检查是不是用了随机数、当前时间这类两边不同的值 |
params / Promise 相关 | 参数没 await | 看第 08 节那两种正确写法 |
404 | 缺 page.tsx,或路径拼错 | 回到第 06 节对一遍四个文件名 |
Port in use | 端口被占 | 换端口,或关掉旧的 dev 进程 |
把数据库密码、支付密钥、第三方 API 密钥写进标了 "use client" 的文件,或任何浏览器能看到的地方。它们只能出现在服务端组件、Server Actions、Route Handlers 里,或者以不带 NEXT_PUBLIC_ 前缀的环境变量形式存在。
14命令小抄
贴在显示器边上,忘了就来翻。
命令
| 命令 | 作用 | 什么时候用 |
|---|---|---|
node -v | 看 Node 版本 | 装任何东西之前;要 ≥ 20.9.0 |
npx create-next-app@latest my-app | 建新项目 | 每次开新项目 |
npm run dev | 开发模式,带热更新 | 写代码全程 |
npm run build | 打包 + 全量检查 | 推送代码前;判断能不能上线 |
npm start | 跑生产构建的结果 | 本地预览上线效果(先 build) |
npm run dev -- -p 3001 | 换端口启动 | 3000 被占用时 |
npx next typegen | 生成 PageProps 这类类型 helper | 想让 await params 有完整类型提示时 |
npx @next/codemod@canary upgrade latest | 官方自动升级 | 升大版本时先跑它 |
npx @next/codemod@canary agents-md | 给旧项目补 AI 说明书 | 想让 AI 按当前版本文档写代码时 |
文件约定 → 网址
| 文件 | 网址效果 | 别忘了什么 |
|---|---|---|
app/page.tsx | / | 没有它首页就是 404 |
app/about/page.tsx | /about | 文件夹里必须有 page.tsx |
app/blog/[slug]/page.tsx | /blog/任意值 | params 要 await |
app/(admin)/dashboard/page.tsx | /dashboard | 圆括号不进网址 |
app/api/hello/route.ts | /api/hello | 导出的是 GET/POST 函数 |
app/blog/loading.tsx | 博客分区的加载占位 | 它是整页级的占位 |
app/blog/error.tsx | 博客分区的出错兜底 | 必须标 "use client" |
proxy.ts(旧名 middleware.ts) | 不产生网址,拦截所有匹配的请求 | 16 版改名;导出函数叫 proxy;记得写 matcher |
数据、跳转与写操作
| 你要做的事 | 写法 | 要点 |
|---|---|---|
| 在服务端取数据 | const data = await fetch(url) | 写在 async 的服务端组件里 |
| 每次都取最新 | fetch(url, { cache: "no-store" }) | 缓存现在是选择加入的 |
| 定时更新(ISR) | fetch(url, { next: { revalidate: 60 } }) | 60 秒后后台更新 |
| 明确要缓存 | fetch(url, { cache: "force-cache" }) | 只给几乎不变的数据用 |
| 点击跳转 | <Link href="/about"> | 站内用 Link,站外用 <a> |
| 代码里跳转 | router.push("/done") | 需要 "use client" |
| 服务端跳转 | redirect("/login") | 来自 next/navigation |
| 写操作(表单) | "use server" 函数 + 表单 action | 写完记得 revalidatePath |
| 查不到的动态页 | notFound() | 配合 not-found.tsx |
| 详情页提前生成 | generateStaticParams() | 已知有哪些页面时用 |
环境变量
| 需求 | 写法 | 为什么 |
|---|---|---|
| 只在服务端用的密钥 | DATABASE_URL="..."(不加前缀) | 不会下发到浏览器 |
| 浏览器也要读的值 | NEXT_PUBLIC_SITE_URL="..." | 必须有这个前缀才会被打包进前端 |
| 改完数据刷新页面缓存 | revalidatePath("/guestbook") | 不刷的话页面还是旧内容 |
15术语对照表
遇到看不懂的词回来查。第三列是为什么重要、什么时候用、别当什么用——比一句翻译有用得多。
| 术语 | 定义(说人话) | 为什么重要 / 何时用 · 别当什么用 |
|---|---|---|
| App Router | 由 app/ 目录驱动的新一代路由体系 | 新项目默认用它,本文全部基于它;别和旧 pages/ 的写法混着抄 |
| 服务端组件(RSC) | 在服务器上执行、默认不向浏览器发送 JavaScript 的组件 | 首屏内容、读数据库、带密钥的调用都放这里;别在里面写 onClick |
| 客户端组件 | 文件顶部标 "use client",在浏览器里恢复交互的组件 | 点击、输入、动画才用,且尽量拆到叶子;别整页标 client,那会把服务端优势全丢掉 |
| 注水(Hydration) | 服务端发来的静态 HTML 到浏览器后,绑定上交互能力的过程 | 理解「为什么首屏快、后面才可点」的关键;别把它当成「页面渲染了两次」的 bug |
| 文件即路由 | app/ 下的文件夹结构直接决定网址 | 建页面 = 建文件夹 + page.tsx;别再去找路由配置文件,那东西不存在 |
动态路由 [slug] | 方括号文件夹,一个文件匹配无数个网址 | 博客详情、商品页用它;别为每篇文章手建一个文件夹 |
params | 动态路由从网址里抓出来的那一段值 | 16 版起它是要 await 的;别写 params.slug,那是已删除的旧写法 |
路由组 (name) | 圆括号文件夹,只做归类、不出现在网址里 | 不同板块要套不同 layout 时用;别指望它出现在地址栏 |
| Server Actions | "use server" 标记的服务端函数,表单可以直接调用 | 留言、点赞这类写操作零接口成本;别拿它做重计算或大文件上传 |
| Route Handlers | app/api/*/route.ts 里导出 GET/POST 的标准接口 | 给外部或跨端用的 JSON 接口;站内表单优先用 Server Actions |
| proxy(旧名 middleware) | 请求进入页面之前先跑的拦截器 | 登录拦截、地区跳转、A/B 分流;16 版改名为 proxy,别在里面连数据库做重活 |
| ISR(增量静态再生) | 页面先按静态发出去,过期后在后台重新生成 | 新闻榜单这类「又快又不太旧」的场景;实时库存别用它 |
| 缓存(cache) | 把取过的结果存起来,下次直接用 | 现在默认不缓存,要缓存得自己声明;别以为不写选项就一定是「每次最新」 |
| SSR / SSG / CSR | 服务端每次渲染 / 构建时预先生成 / 浏览器里渲染 | 三种取舍:SSR 首屏好但耗服务器,SSG 最快最便宜但不适合实时数据,CSR 适合强交互内页但对外页吃 SEO 的亏 |
| Turbopack | Rust 写的打包与开发引擎,16 版起是默认 | 享受极速热更新;它只是引擎不是框架,构建出问题先看版本和配置,别乱改 |
| Metadata(元数据) | 每个页面导出的、描述这一页的标题与摘要对象 | 搜索引擎排名和分享卡片全靠它,每页顺手写;别堆关键词作弊 |
| 流式渲染 / Suspense | 快的部分先发出去,慢的部分随后补上 | 评论区、推荐位这类慢组件用它;整页都慢应该先优化查询,别拿它遮丑 |
| 预取(Prefetch) | 用户还没点,就提前把目标页下载好 | Link 默认自带,跳转如飞;别特意关掉它 |
next.config.ts | Next.js 的总配置文件 | 配远程图片域名、重定向、Turbopack 选项都在这;改完要重启 dev |
public/ 目录 | 放图片等静态文件的篮子 | 引用时用 / 开头的路径;别把源码和密钥放进来,它会被原样公开 |
| 环境变量(env) | 区分开发和生产配置的键值对 | 不带 NEXT_PUBLIC_ 前缀的只活在服务端;浏览器要读的才加前缀 |
| Vercel | Next.js 背后的公司做的部署平台 | 新手上线首选,推送即部署;别把它和 Next.js 本身搞混,框架免费且能部署到别处 |
| Codemod | 官方写的自动改代码脚本,用来做跨版本迁移 | 升大版本先跑它,省掉大量手工改动;跑完务必 build 验证,别直接上线 |
| runtime(运行时) | 代码实际执行的那个环境 | 服务端组件的运行时是 Node.js,客户端的运行时是浏览器;搞清楚「这行在哪跑」是排错的第一步 |
99一句话总结
Next.js 就是用 React 一次搞定网站的前台和后台:文件夹即路由、组件默认在服务器上跑、表单能直接调用服务端函数;照着「建文件 → 填数据 → 拆交互 → 跑 build」四步走,今晚就能上线你的第一个网站。
带走这三句
- 文件夹就是网址——建页面先建文件夹,里面必须有
page.tsx。 - 默认放服务端,交互才下沉——分不清时先全写服务端,报错再拆出
"use client"。 - 先 build 再上线——本地
build过了,上线就稳了一大半。
现在就动手
打开终端,跑这两行,把第 05 节的两个页面亲手敲出来:
npx create-next-app@latest my-app
cd my-app && npm run dev
遇到红色报错,把完整报错连同你用的 Next.js 版本一起发给 AI 助手。你已经学会了最难的那一步:开始。
练习:把这个教程变成你自己的
下面这份提问模板可以直接复制去问 AI,它保证 AI 按 16 版的写法回答你(尤其是 await params 和 proxy 这两处):
我想用 Next.js(App Router,v16.3)做一个【博客 / 留言板 / 待办(三选一)】。
请按以下要求生成代码:
1. 文件放在 app/xxx/page.tsx,TypeScript + Tailwind
2. 服务端组件优先,只有交互部分用 "use client"
3. 动态路由的 params 必须按 v16 写法 await,不要给同步写法
4. 如果要写请求拦截,用 proxy.ts 而不是 middleware.ts
5. 每页导出 metadata 标题
6. 告诉我每一步在终端敲什么命令验证
下面是我的报错(如有,粘贴完整的红色文字):【粘贴处】
留一个只有你自己能验的作业:用你现在手头真实的一个小需求,套用本文的四步走一遍。如果你能不看文章就说清楚「这个文件是在服务器上跑还是在浏览器里跑」,那这篇教程对你才算真的读完了。
97新手最常问的八个问题
useState。先会抄,再懂原理,学两遍最快。完全不会 JavaScript 的话,先补变量、函数、数组、对象,再回来。app/ 目录)。pages/ 那套只在接手老项目时查文档现学。官方的新能力——服务端组件、新的缓存模型、新的 proxy——全部先在 App Router 上落地。两套混着抄是新手最大的乱源。npm run build 之后,任何能跑 Node 的服务器、Docker、其他云平台都能部署。框架本身是开源免费的。AGENTS.md。直接说「按 Next.js 16 App Router 的最新写法生成,params 要 await,拦截用 proxy.ts」,出错的概率会低很多。第 15 节末尾有一份可以直接复制的提问模板。Error: 那一行到最后一行)整段贴给 AI 助手,不要只截一句——上下文丢了,它只能猜。98参考来源与证据边界
本文的关键事实均在 2026-09-13 逐条核对过以下来源:
| # | 来源 | 核对了什么 |
|---|---|---|
| [1] | Next.js 官方文档 nextjs.org/docs | 当前文档标注的 Latest Version 为 16.3.5;App Router 的章节结构(Server and Client Components、Fetching Data、Proxy、Cache Components 等) |
| [2] | 官方安装文档 /docs/app/getting-started/installation | 环境要求、create-next-app 流程、四个特殊文件名、目录结构导览 |
| [3] | create-next-app CLI 参考 /docs/app/api-reference/cli/create-next-app | 提问顺序与选项:推荐默认配置、linter 三选一(ESLint / Biome / None)、React Compiler、src/、别名、--agents-md |
| [4] | 16 版升级指南 /docs/app/guides/upgrading/version-16 | Node.js 最低 20.9.0、TypeScript 最低 5.1.0、Turbopack 成为默认、同步取参数被彻底移除、middleware → proxy、revalidateTag 现在要求第二个参数 |
| [5] | proxy.js 参考 /docs/app/api-reference/file-conventions/proxy | 原文声明「middleware 文件约定已弃用并改名为 proxy」;函数名 proxy;不写 matcher 会对所有请求生效 |
| [6] | 动态段参考 /docs/app/api-reference/file-conventions/dynamic-routes | 当前正确定义:params: Promise<{ slug: string }> 并 await params |
| [7] | fetch 参考 /docs/app/api-reference/functions/fetch | 默认行为为 auto no cache;官方原话「Caching is opt-in.」;force-cache / no-store / next.revalidate 的语义 |
| [8] | npm registry registry.npmjs.org/next/latest | 当前最新稳定版 16.3.5;engines.node 为 >=20.9.0 |
| [9] | GitHub 仓库 github.com/vercel/next.js | 仓库简介原文「The React Framework」;Stars 约 142k;v16.3.5 发布于 2026-09-11 |
本文的版本锚点是 Next.js 16.3.5(2026-09-13 校对)。需要留意的边界有三处:
① 缓存模型正在迁移。16 版里同时存在两套缓存心智:老的一套(fetch 选项、revalidatePath)和新的一套 Cache Components(在配置里打开 cacheComponents,用 use cache 指令)。本文讲的是老的那套,因为它仍然是当前没有额外开启时的实际行为,也是绝大多数项目在用的。等你的项目要开 Cache Components 时,请以官方 Caching 那一节为准。
② 「缓存细节」是这个版本里唯一真正在动的部分。你学到的三套核心心智——文件即路由、组件默认在服务端、数据在服务端取——从 13 版到 16 版一直稳定,可以放心投入。变的多数是缓存开关和性能选项。
③ 来源矛盾的处理。网络上大量教程(包括 AI 的记忆)仍在写同步 params 与 middleware.ts,与官方当前文档矛盾。本文一律以官方文档为准,并已在第 07、08、10、13 节点名指出这两处差异——这不是谁抄错了,是 16 版确实改了。
这 4 个链接建议收藏
| 链接 | 什么时候去 |
|---|---|
| nextjs.org/docs | 卡住了先来这搜;注意顶部要在 App Router / Pages Router 之间选对 |
| 安装页 | 环境要求、脚手架选项、目录结构都在这 |
| nextjs.org/learn | 官方免费互动教程,学完本文去那练手感 |
| 仓库 Discussions | 疑难杂症先搜这里,大概率有人踩过同一个坑 |