如果你已经会写一点 React,第一次打开 Next.js 文档时,反而可能更迷糊:React 组件还是那些组件,项目里却突然多了 app、layout.tsx、Server Components、构建与部署等一整套概念。它们不是为了把简单事情复杂化,而是在回答一个 React 组件本身没有回答的问题:怎样把组件组织成一个可以访问、取数、渲染和上线的完整 Web 应用?
这一章不要求你一开始就记住所有术语。我们先建立一幅能用的地图,再亲手创建一个项目。读完后,你应该能独立完成这些动作:检查环境、运行 create-next-app、读懂核心目录、改出首页、添加第二个页面、放入一个小型客户端交互,并分清开发服务器与生产构建。

Next.js 没有取代 React,它把路由、渲染、数据与部署这些应用级问题放进同一套约定。
这门课以 App Router 为主。Next.js 仍然支持较早的 Pages Router,所以你搜索问题时可能看到 pages/、getServerSideProps 或 _app.tsx。这些内容不一定错误,但不能直接和 App Router 教程混用。先看清教程针对哪一种路由器,能省掉很多无谓排查。
React 解决的是用户界面问题。我们用组件描述页面,用 props 传数据,用 state 表达会变化的界面。可是一个真实网站还需要知道:访问 /about 时应显示哪个组件,数据应在服务器还是浏览器读取,首屏 HTML 从哪里来,图片和字体怎样加载,以及最终产物怎样运行。
Next.js 是建立在 React 之上的全栈 Web 框架。这里的“全栈”不意味着每个项目都必须自建数据库,也不意味着前后端从此不能拆开。它表示同一套框架既能组织浏览器里的界面,也能承载服务器侧的数据读取、请求处理与渲染。你的后端完全可以仍是 Java、Go 或独立 API;Next.js 负责把 Web 应用这一侧的工作连接起来。
可以先把框架提供的能力归成六类:
服务端渲染只是其中一部分。App Router 还会根据数据、缓存与路由需要决定何时预渲染、何时按请求渲染,并通过流式传输逐步交付内容。初学阶段不需要马上掌握这些策略,但要保留一个准确判断:Next.js 管理的是一条完整的页面生产链,不只是把 React 组件搬到服务器执行一次。
我们接下来观察项目时,可以反复问三个问题:
只要这三个问题逐渐清楚,后面的路由、数据获取和缓存就不会变成彼此孤立的 API。
当前官方安装要求的最低 Node.js 版本是 20.9,支持 macOS、Windows(包括 WSL)和 Linux。实际学习时,我更建议安装 Node.js 官网标记的当前 LTS 版本:它通常比最低版本更适合长期使用,也更容易和新工具保持兼容。

框架问题常常先从环境问题查起;确认 Node.js 版本与包管理器,再开始排查代码。
打开终端,先运行:
node --version
npm --version
npx --version第一条会显示 Node.js 版本。如果主版本和次版本低于 20.9,先升级 Node.js,再创建项目。后两条用来确认 npm 与 npx 可用。npm 负责安装依赖与运行脚本,npx 可以临时执行 create-next-app,无需先把脚手架全局安装到电脑上。
“终端能找到 node”不等于版本一定正确。一台电脑上可能同时存在系统 Node、版本管理器安装的 Node 和编辑器终端使用的 Node。遇到版本报错时,以当前终端实际输出的 node --version 为准。
Next.js 官方入门材料默认读者了解 HTML、CSS、JavaScript 与 React。你不必先成为 React 专家,但至少应认识组件、props、JSX、模块导入与基本 state。如果下面的函数组件完全陌生,先复习 React 基础会更顺:
type GreetingProps = {
name: string
}
export default function Greeting({ name }: GreetingProps) {
return <p>你好,{name}!</p>
}Next.js 支持 npm、pnpm、Yarn 和 Bun。课程示例统一使用 npm,不代表其他工具不能用。真正重要的是在同一个项目里保持一致:看见 package-lock.json 就继续用 npm,不要又生成 pnpm-lock.yaml 或 yarn.lock。多个锁文件会让团队成员和部署平台难以判断应采用哪一组依赖解析结果。
在你准备存放代码的目录中运行:
npx create-next-app@latest nextjs-notes --use-npm这条命令可以拆开读:
npx 临时执行一个 npm 包中的命令。create-next-app@latest 使用当前最新的项目脚手架。nextjs-notes 是项目名,也是即将创建的目录名。--use-npm 明确让脚手架用 npm 安装依赖并生成 package-lock.json。脚手架会先问你是否使用推荐默认配置。当前推荐组合包括 TypeScript、ESLint、Tailwind CSS、App Router 与 Turbopack,默认导入别名是 @/*。Turbopack 已是开发与构建命令的默认打包器,所以不要再照着旧教程寻找“是否启用 Turbopack”的提问。

第一次创建可以看一遍交互式提问;需要团队复现时,再用明确参数或记录好的选择固定项目骨架。
如果这是你的第一个项目,直接选择推荐默认配置就够了。想逐项理解时,可以选择自定义:
src/ 目录:只影响源码放在根目录还是 src/ 下。本课程为了路径简洁,使用根目录的 app/。@/*,稍后可以用 @/app/... 或 @/components/... 代替很长的相对路径。需要一条更明确、便于复制到团队文档的命令时,可以写成:
npx create-next-app@latest nextjs-notes --ts --eslint --tailwind --app --use-npm--yes 也能跳过提问,但它可能使用默认值或之前保存的偏好。想得到可复现的项目骨架,显式参数通常更容易审查。
下面的配置规划器会根据你的选择实时生成命令。先试试推荐组合,再关闭 Tailwind 或切换 src/,观察哪些只是组织选择,哪些会改变本课程使用的路由模型。
先确认当前终端中的 Node.js 不低于 20.9,并决定本项目统一使用哪一个包管理器。
在准备存放项目的父目录执行 create-next-app。不要先手动创建同名且已有文件的目录,否则脚手架可能因目录不为空而停止。
第一次学习采用推荐默认配置,或用明确参数固定 TypeScript、ESLint、Tailwind 与 App Router。
等待依赖安装完成,确认终端没有错误,再进入新生成的项目目录。网络慢和安装失败是两回事,先看最后几行输出再判断。
进入项目目录:
cd nextjs-notes不同脚手架版本与选择会让文件略有差异。使用上面的推荐组合时,你通常会看到类似结构:
nextjs-notes/
├── app/
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── public/
├── eslint.config.mjs
├── next.config.ts
├── package.json
├── package-lock.json
├── postcss.config.mjs
└── tsconfig.json我们先只抓住真正会立刻用到的部分。
app/page.tsx 对应根路径 /。page.tsx 必须默认导出一个 React 组件,Next.js 才能把这个路由公开成页面。
app/layout.tsx 是根布局。它包裹所有页面,并且必须包含 <html> 与 <body>。适合放全站共享的语言标记、导航、字体、全局样式入口和元数据。
app/globals.css 保存全局样式。推荐模板通常在根布局中导入它。全局样式应该处理真正全局的规则;具体组件的样式仍可用 Tailwind 或 CSS Modules 组织。
放在 public 下的文件从网站根路径访问。例如:
public/logo.png → /logo.png
public/docs/start.pdf → /docs/start.pdfURL 中不写 public。如果用 next/image 显示本地图片,也会从类似 /logo.png 的路径引用。
package.json 记录依赖与可运行脚本,是排查“这个项目到底支持什么命令”的第一站。tsconfig.json 控制 TypeScript,也保存 @/* 等路径别名。eslint.config.mjs 保存 ESLint 规则。postcss.config.mjs 连接 Tailwind 等 CSS 处理工具。next.config.ts 放 Next.js 项目级配置。刚入门时通常无需修改。package-lock.json 固定 npm 解析出的依赖版本,应随项目保存。运行开发或构建命令后还会出现 .next/。它是生成目录,不是手写源码,也不应该拿来修改页面。
app 目录允许放普通组件、工具函数和样式文件。普通文件不会仅仅因为位于 app 中就自动成为页面;路由是否公开由 page、route 等特殊文件决定。后续课程会专门展开完整的特殊文件规则。
确认终端当前就在含有 package.json 的项目根目录,然后启动开发服务器:
npm run dev终端会显示实际访问地址,通常是 http://localhost:3000。如果 3000 端口被占用,以终端给出的结果为准。开发服务器会保持运行;需要继续输入其他命令时,可以另开一个终端标签页。
打开 app/page.tsx,把模板内容改成一个足够简单的首页:
export default function HomePage() {
return (
<main className="mx-auto min-h-screen max-w-3xl px-6 py-16">
<p className="text-sm font-semibold text-blue-600">第一站</p>
<h1 className="mt-3 text-4xl font-bold">我的 Next.js 学习站</h1>
<p className="mt-5 text-lg text-slate-600">
如果你能看到这句话,首页已经由 app/page.tsx 成功渲染。
</p>
</
保存文件后,浏览器会通过 Fast Refresh 更新。Fast Refresh 的目标不是简单粗暴地整页刷新,而是在开发阶段尽量快速反映模块变化并保留可保留的状态。如果页面没有变化,先看终端和浏览器错误,不要连续重启服务器掩盖真正问题。
推荐模板已经生成 app/layout.tsx。可以把语言和基础元数据改成自己的内容:
import type { Metadata } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: '我的 Next.js 学习站',
description: '记录 Next.js 学习过程与练习',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return
这里的 children 就是当前匹配页面的内容。访问首页时,它会被 app/page.tsx 的结果替换;以后访问关于页时,它会换成关于页,但根布局仍然包在外面。
App Router 最直观的规则是:文件夹对应 URL 片段,page.tsx 提供这个 URL 的页面,layout.tsx 提供共享外壳。

文件夹定义路由片段,page.tsx 暴露页面,layout.tsx 提供共享外壳;普通文件夹本身不会自动成为可访问页面。
在 app 中创建 about/page.tsx:
export default function AboutPage() {
return (
<main className="mx-auto max-w-3xl px-6 py-16">
<h1 className="text-4xl font-bold">关于这个学习站</h1>
<p className="mt-5 text-slate-600">
这里记录我对路由、渲染与数据获取的理解。
</p>
</main>
)
}目录与 URL 的对应关系是:
app/about/page.tsx → /about然后在首页导入 Link,增加内部导航:
import Link from 'next/link'
export default function HomePage() {
return (
<main className="mx-auto min-h-screen max-w-3xl px-6 py-16">
<p className="text-sm font-semibold text-blue-600">第一站</p>
<h1 className="mt-3 text-4xl font-bold">我的 Next.js 学习站</h1>
<p className="mt-5 text-lg text-slate-600">
如果你能看到这句话,首页已经成功渲染。
next/link 提供客户端过渡与预取等路由优化,是站内页面导航的主要方式。普通 <a> 仍适合外部网址、下载等场景,但站内页面不要习惯性地用它触发完整页面加载。
下面的互动页把路由匹配、根布局、服务器渲染、HTML 与局部水合放到一条时间线上。切换“纯服务器页面”和“带客户端计数器”,观察最后一站是否需要执行。
App Router 中的页面和布局默认是 Server Components。这意味着组件可以在服务器环境运行、靠近数据源读取数据,并减少发送到浏览器的 JavaScript。它们不需要写一个特殊的“服务器”标记。
需要 state、事件处理器、useEffect、自定义 Hooks 或 window、localStorage 等浏览器 API 时,才创建 Client Component,并在文件最上方写 'use client'。

App Router 默认把页面与布局留在服务器;需要状态、事件或浏览器 API 的小块再进入客户端边界。
在 app/ui/counter.tsx 创建一个最小交互组件:
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<section className="mt-8 rounded-xl border border-slate-200 p-5">
<p>当前练习次数:{count}</p>
<button
className="mt-3 rounded-lg bg-slate-900 px-4 py-2 text-white"
再把它导入默认的 Server Component 页面:
import Counter from './ui/counter'
export default function HomePage() {
return (
<main className="mx-auto min-h-screen max-w-3xl px-6 py-16">
<h1 className="text-4xl font-bold">我的 Next.js 学习站</h1>
<Counter />
</main>
)
}这里没有必要给 app/page.tsx 也加 'use client'。页面可以继续留在服务器,只让计数器及它导入的客户端子树进入客户端模块图。边界越贴近真正交互的小组件,发送给浏览器的 JavaScript 通常越少。
首次访问时,Next.js 仍会为 Client Components 预渲染 HTML,让用户先看到非交互预览;浏览器加载相关 JavaScript 后再进行水合,把事件处理器接到现有 DOM 上。'use client' 定义的是模块边界和能力边界,不是关闭首屏预渲染的开关。
Server Component 可以读取数据库或服务器环境中的秘密,但不能把秘密当成 props 传给 Client Component。传入客户端的 props 需要能被 React 序列化,而且它们最终对浏览器可见。
Client Component 也不是使用浏览器 API 的“随意通行证”。初始预渲染与浏览器首次渲染必须一致。如果在渲染阶段直接依赖 Date.now()、随机数或 window 分支,容易造成水合不匹配。浏览器专属副作用通常放进事件处理器或 useEffect,并设计稳定的首次输出。
不要为了消除一个 Hooks 报错,就把根布局或整个页面都标成客户端组件。先找到真正需要 state、事件或浏览器 API 的最小模块,在那个文件顶部建立客户端边界。
项目里的 package.json 通常会包含这些脚本:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
}
}实际脚本以你的 package.json 为准,不要因为某篇教程出现了命令,就假设当前项目一定定义了同名脚本。
npm run dev 启动开发服务器,当前默认使用 Turbopack,重点是快速反馈、错误覆盖层与 Fast Refresh。npm run build 生成并验证优化后的生产构建。开发页面能打开,不代表生产构建一定通过。npm start 运行已经成功生成的生产构建;它不会替你先做构建。next build 不再自动运行 linter。
开发阶段追求快速反馈,交付前则要分别确认代码检查和生产构建;两条通过才算真正可交付。
准备交付时,可以按这个顺序理解,而不是机械背命令:
在开发服务器中完成页面修改,并清理终端与浏览器里真正的运行错误。
查看 package.json,按项目实际配置单独运行 ESLint、Biome 或其他代码检查工具。
运行生产构建。构建会暴露类型、导入、服务端与客户端边界等开发浏览时未必触发的问题。
构建成功后,才用 start 脚本本地运行生产服务,或交给目标平台的部署流程。
下面的检查站把开发反馈轨和生产交付轨拆开。故意选择“构建前直接 start”或“dev 正常就部署”,看看缺少哪张通行票。
Vercel 对 Next.js 提供低配置部署和框架集成,但 Next.js 也可以部署为 Node.js 服务或 Docker 容器,这两种方式支持完整框架能力。静态导出可以放到只提供静态文件的主机,但服务器相关能力会受限。选择部署方式时,应先看应用用了哪些能力,再看目标平台是否支持,而不是把框架与某一家托管服务画等号。
很多入门问题不是 API 太难,而是心智模型混在一起。下面这张排查表比“删掉重装”更值得先看。
如果创建项目时选择了 src/,教程中的 app/page.tsx 在你的项目里会是 src/app/page.tsx。@/* 别名通常也会对准源码根。不要看到路径不同就复制出第二个 app 目录;先读 tsconfig.json 与现有目录结构。
给越来越上层的文件加 'use client',短期可能让 Hooks 报错消失,却会扩大客户端 JavaScript 边界,还可能让服务器专属代码无法使用。正确的排查方式是先标出交互点,再把边界放到最小的可交互子树。
现在先离开模板首页,做一个“学习记录站”。要求如下:
/notes 页面,写下三条准备学习的主题。Link 跳转到 /notes。'use client'。package.json 检查代码并完成一次生产构建。先自己动手。如果页面路径或客户端边界拿不准,再展开参考结构。
你不需要背下所有文件约定,但应该能不看答案说清楚下面几件事:
app/page.tsx 对应 /,app/about/page.tsx 对应 /about,根布局包裹它们。dev、代码检查、build 和 start 各自回答不同问题,不能互相替代。如果其中某一条仍然模糊,回到对应交互页再走一遍流程。下一步再展开项目结构与完整路由约定,会比一开始死记所有特殊文件更稳。
app/ui/practice-counter.tsx 承担唯一需要 state 的交互:
'use client'
import { useState } from 'react'
export default function PracticeCounter() {
const [completed, setCompleted] = useState(0)
return (
<div>
<p>今天已完成 {completed} 项练习</p>
<button type="button" onClick={() => setCompleted((value) => value + 1)}>
记录一项
</button>
</div>
)
}关键不在于样式和目录名必须一模一样,而在于 /notes 由 page.tsx 公开,交互状态只进入计数器的客户端边界。