用 React 写的 SPA 项目搜索引擎爬不到内容,首屏加载白屏半天,还要花大量时间配置路由、TypeScript 和样式工具?那 Next.js 绝对是你的 “开发加速器”—— 它不仅解决了 React 的核心痛点,还自带企业级优化,让你从 “配置工程师” 专注于 “业务开发”。
今天这篇文章,我们将从 “环境搭建→项目实战→底层原理” 层层递进,手把手带你开发第一个 Next.js Todo 项目。
什么是 Next.js?
在开始写代码前,我们必须明确:Next.js 不是 “另一个框架”,而是基于 React 的 “增强方案”,专门解决 React 开发中的 3 大核心痛点:
1. React 单页应用(SPA)的 SEO 噩梦
纯 React 项目默认是客户端渲染(CSR) :浏览器请求服务器时,只能拿到一个空的 <div id="root"></div>,页面内容需要等 JS 下载、解析、挂载后才会显示。
但搜索引擎爬虫(如百度、谷歌)的逻辑很 “简单”—— 只读取服务器返回的原始 HTML,不会等待 JS 执行。这就导致:
- CSR 项目的内容在爬虫眼里是 “空的”,无法被索引;
- 企业官网、博客、电商等需要流量的项目,用纯 React 等于 “放弃搜索引擎”。
Next.js:支持 服务器端渲染(SSR) 和 静态站点生成(SSG) 。
- SSR:页面在服务器编译成完整 HTML 后返回,爬虫能直接读取内容;
- SSG:构建时预生成所有页面的 HTML,首屏加载速度快到毫秒级。
2. 首屏加载慢,用户没耐心等
CSR 项目的首屏加载流程是:
1.下载空 HTML → 2. 下载大体积 JS → 3. 解析 JS → 4. 发起 API 请求 → 5. 渲染页面
如果网络差或 JS 体积大,这个过程可能要 3-5 秒,用户看到的是 “白屏”,很容易关闭页面。
Next.js:
- SSR/SSG 让服务器直接返回带内容的 HTML,首屏加载时间缩短 50%+;
- 内置代码分割(Code Splitting):只加载当前页面需要的 JS,不用一次性下载所有代码。
3. 配置繁琐,搭环境半天跑不通
纯 React 开发需要手动配置:
- 路由:用
react-router-dom写一堆<Route><Switch>,嵌套路由还容易出问题; - TypeScript:安装
@types/react@types/node等依赖,配置tsconfig.json; - 样式:手动集成 Tailwind、Less,解决热更新和打包问题;
- 规范:配置 ESLint、Prettier,避免团队代码风格混乱。
Next.js:开箱即用,零配置支持所有核心功能:
- 文件系统路由:
pages目录下的文件自动对应路由,不用写一行路由配置; - 内置 TypeScript:初始化时可直接开启,自动生成类型配置;
- 自带 ESLint、热更新、静态资源处理,甚至能直接写后端 API(
pages/api目录)。
环境准备:3 分钟搞定前置依赖
Next.js 对环境有明确要求,我们先一步到位配置好,避免后续出现问题。
1. 安装 Node.js(关键:版本必须达标)
Next.js 2024 年最新版要求 Node.js ≥ 18.17(LTS 长期支持版),低于这个版本会报错。
- 下载地址:Node.js 官网(推荐下载 “LTS” 版本,如 20.x);
- 验证安装:打开终端,输入
node -v,显示v18.17.0或更高版本即可。
2. 选择包管理器(推荐 pnpm)
Next.js 支持 npm、yarn、pnpm,推荐用 pnpm—— 比 npm 快 2 倍,且能避免依赖冲突。如果没安装 pnpm,先执行:
# 全局安装 pnpm(仅第一次需要)
npm install -g pnpm
实战:从 0 到 1 搭建 Next.js Todo 项目
Next.js 官方提供 create-next-app 工具,能一键生成带 TS、Tailwind、ESLint 的项目,我们用它快速上手。
1. 初始化项目
打开终端,执行以下命令(my-next-todo 是项目名,可自定义):
# npx 不用提前安装,直接运行(适合快速测试和创建项目)
npx create-next-app@latest my-next-todo --typescript
npx优势:不用全局安装create-next-app,用完不占内存,尝试新技术很方便。--typescript:强制开启 TypeScript 支持,自动生成tsconfig.json和类型依赖;
2. 初始化时的关键选项(每一步都要注意!)
执行 npx create-next-app@latest 后,终端会弹出几个选项,我们先按以下推荐选择(避免后续踩坑):
# 1. Would you like to use ESLint? (Y/n)
Y → 代码规范必须开,提前发现语法错误(React项目必备)
# 2. Would you like to use Tailwind CSS? (Y/n)
Y → 内置Tailwind,不用手动配置postcss(写样式快10倍)
# 3. Would you like to use `src/` directory? (Y/n)
Y → 用src目录放源码,项目结构更清晰(React项目最佳实践)
# 4. Would you like to use App Router? (Y/n) [Recommended]
Y → 我们的核心目标,选App Router(Next.js 13+主推)
# 5. Would you like to use Turbopack? (Y/n) [Recommended]
Y → 解决传统打包工具(如 Webpack)在大型项目中构建速度慢的问题
# 6. Would you like to customize the default import alias? (Y/n)
N → 暂时用默认别名(@/对应src/),后续再自定义
3. App Router 项目结构解析(重点看这 5 个目录)
App Router 的项目结构和 Pages Router 完全不同,核心是src/app目录(路由和布局的入口),我们重点解析关键目录,别迷路啦~ 🗺️
my-next-app-router-todo/
├── src/
│ ├── app/ # ★ App Router核心目录(文件即路由,包含布局和页面)
│ │ ├── page.tsx # ★ 首页(对应/路由,必须叫page.tsx,改名字就找不到啦)
│ │ ├── layout.tsx # ★ 根布局(所有页面共享的布局,如导航栏、页脚,一次写全页面复用)
│ │ ├── globals.css # 全局样式(Tailwind默认在这里,全局样式放这里准没错)
│ │ ├── todo/ # 自定义路由目录(对应/todo路由,想加新路由就新建文件夹)
│ │ │ ├── page.tsx # /todo页面(必须叫page.tsx,是路由的“入口文件”)
│ │ │ └── [id]/ # 动态路由目录(对应/todo/1、/todo/2等,处理详情页超方便)
│ │ │ └── page.tsx # 动态路由页面(获取[id]参数就能显示对应内容)
│ ├── components/ # 公共组件目录(如TodoItem、Input组件,复用代码不重复写)
│ ├── lib/ # 工具函数目录(如API请求、类型定义,把通用逻辑放这里)
│ └── types/ # TypeScript类型定义目录(全局类型,避免到处写any)
├── public/ # 静态资源目录(图片、字体等,用的时候直接写路径,不用import)
├── tsconfig.json # TypeScript配置文件(Next.js自动生成,一般不用改)
└── next.config.js # Next.js核心配置文件(如改端口、加插件,按需配置)
App Router 核心规则(必须记住!不然写路由会懵) :
-
路由由
src/app目录下的文件夹和page.tsx文件决定,相当于 “文件夹 = 路由路径,page.tsx = 页面内容”:src/app/page.tsx→ 路由/(首页,访问域名直接看到的页面);src/app/todo/page.tsx→ 路由/todo(访问http://localhost:3000/todo就能看到);src/app/todo/[id]/page.tsx→ 动态路由/todo/1、/todo/2(比如点击某个 Todo 跳详情页,就用这个);
-
layout.tsx是 “布局组件”,作用于当前目录及子目录的所有页面(比如根布局src/app/layout.tsx里写个导航栏,所有页面都会显示这个导航栏,不用每个页面都写一遍~); -
组件默认是 “服务器组件”(Server Component),无需导入 React,也不能用
useState/useEffect;若要使用 React Hooks(比如管理输入框状态),需显式在组件顶部加'use client'指令(变成客户端组件),这点和纯 React 不一样,要注意哦~
| 组件类型 | 声明方式 | 运行环境 | 可使用的 API | 适用场景 |
|---|---|---|---|---|
| Server 组件 | 无需声明(默认) | 服务端 | 无 React Hooks(useState/useEffect 等),可直接获取数据 | 页面布局、静态内容展示、数据预获取(比如 Todo 列表数据) |
| Client 组件 | 顶部加'use client' | 客户端 | 所有 React Hooks,可操作 DOM | 按钮、输入框、表单、状态管理组件(比如添加 Todo 的输入框) |
数据获取:不用 useEffect,服务端直接获取 🚀
纯 React 中,你需要用useEffect+useState获取数据,还要处理 “加载中 / 错误” 状态,代码又多又容易出问题。
但 App Router 的 Server 组件支持顶部直接 await 获取数据,在服务端渲染前就把数据准备好,首屏不用等 JS 加载完再请求,速度快多了!举个例子(获取 Todo 列表):
// src/app/todo/page.tsx(Server 组件,不用加 'use client')
// 直接在顶部 await 请求数据,不用 useEffect!
const fetchTodos = async () => {
const res = await fetch('https://api.example.com/todos'); // 服务端发起请求,前端看不到这个请求
if (!res.ok) throw new Error('Failed to fetch todos');
return res.json();
};
// 直接 await 获取数据,渲染时数据已经准备好了
const todos = await fetchTodos();
export default function TodoPage() {
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">我的 Todo 列表</h1>
<ul>
{todos.map((todo: { id: number; title: string }) => (
<li key={todo.id} className="border-b pb-2 mb-2">
{todo.title}
</li>
))}
</ul>
</div>
);
}
这样写不仅代码少,还能在服务端提前获取数据,首屏加载时直接显示 Todo 列表,不用等 “加载中” 动画,用户体验直接拉满~ 😍
4. 开发第一个 Todo 功能(核心:添加 + 展示 Todo)
我们先实现最核心的 “添加 Todo” 和 “展示 Todo 列表” 功能,步骤清晰,新手也能跟上~
步骤 1:创建 Todo 类型定义(TypeScript 必备)
先在 src/types 目录下新建 todo.ts 文件,定义 Todo 的类型,避免到处写 any:
// src/types/todo.ts
export interface Todo {
id: string; // 用字符串ID,避免数字溢出
title: string; // Todo 内容
completed: boolean; // 是否完成
createdAt: string; // 创建时间
}
步骤 2:创建 Todo 输入组件(Client 组件,需加 'use client')
因为要用到 useState 管理输入框状态,所以这个组件是 Client 组件。在 src/components 目录下新建 TodoInput.tsx:
// src/components/TodoInput.tsx
'use client'; // 必须加!因为要用 useState
import { useState } from 'react';
// 接收一个添加Todo的回调函数
interface TodoInputProps {
onAddTodo: (title: string) => void;
}
export default function TodoInput({ onAddTodo }: TodoInputProps) {
const [title, setTitle] = useState(''); // 管理输入框内容
// 处理表单提交
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); // 阻止默认刷新
if (!title.trim()) return; // 空内容不添加
onAddTodo(title); // 调用父组件传的回调,添加Todo
setTitle(''); // 清空输入框
};
return (
<form onSubmit={handleSubmit} className="flex gap-2 mb-6">
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="输入你的Todo..."
className="flex-1 px-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
>
添加
</button>
</form>
);
}
步骤 3:在 Todo 页面集成组件(Server + Client 结合)
修改 src/app/todo/page.tsx,集成 TodoInput 组件,实现 “添加 Todo 并展示” 的功能(这里用临时数组存储 Todo,后续可替换成 API 请求):
// src/app/todo/page.tsx
'use client'; // 这里要加 'use client'!因为要管理 Todo 列表的状态(useState)
import { useState } from 'react';
import TodoInput from '@/components/TodoInput';
import { Todo } from '@/types/todo';
export default function TodoPage() {
// 初始化 Todo 列表(可后续替换成 await fetch 获取的真实数据)
const [todos, setTodos] = useState<Todo[]>([
{
id: '1',
title: '学习 Next.js App Router',
completed: false,
createdAt: new Date().toISOString(),
},
{
id: '2',
title: '开发 Todo 项目',
completed: false,
createdAt: new Date().toISOString(),
},
]);
// 添加 Todo 的逻辑
const addTodo = (title: string) => {
const newTodo: Todo = {
id: Date.now().toString(), // 用时间戳当唯一ID
title,
completed: false,
createdAt: new Date().toISOString(),
};
setTodos([...todos, newTodo]); // 新增 Todo 到列表
};
// 切换 Todo 完成状态
const toggleTodo = (id: string) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
return (
<div className="container mx-auto p-4 max-w-2xl">
<h1 className="text-3xl font-bold mb-6 text-center text-gray-800">
我的 Todo 清单 📝
</h1>
{/* 集成 TodoInput 组件,传入添加 Todo 的回调 */}
<TodoInput onAddTodo={addTodo} />
{/* 展示 Todo 列表 */}
{todos.length === 0 ? (
<div className="text-center text-gray-500">
还没有 Todo,快添加一个吧~
</div>
) : (
<ul className="space-y-3">
{todos.map((todo) => (
<li
key={todo.id}
className={`flex items-center justify-between p-4 border rounded-md ${
todo.completed ? 'bg-gray-50 line-through text-gray-500' : 'bg-white'
} hover:shadow-md transition-shadow`}
>
<div className="flex items-center gap-3">
{/* 复选框:切换完成状态 */}
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
className="w-5 h-5 accent-blue-500"
/>
{/* Todo 内容 */}
<span>{todo.title}</span>
</div>
{/* 创建时间(格式化显示) */}
<span className="text-xs text-gray-400">
{new Date(todo.createdAt).toLocaleString()}
</span>
</li>
))}
</ul>
)}
</div>
);
}
步骤 4:启动项目,查看效果
在终端进入项目目录,执行以下命令启动项目:
# 进入项目目录
cd my-next-todo
# 启动开发服务器(默认端口 3000)
pnpm run dev
启动成功后,打开浏览器访问 http://localhost:3000/todo,就能看到你的 Todo 项目啦!可以尝试添加 Todo、勾选完成,功能都能正常工作~ 🎉
底层原理:Next.js 是怎么解决 React 痛点的? 🤔
光会用还不够,我们简单理解下 Next.js 的核心原理,知其然更知其所以然~
1. 渲染逻辑:SSR/SSG/ISR 三选一,按需优化
Next.js 提供了 3 种渲染方式,可根据场景灵活选择,这是它解决 “SEO 差、首屏慢” 的核心:
| 渲染方式 | 核心逻辑 | 适用场景 | 优势 |
|---|---|---|---|
| 服务器端渲染(SSR) | 每次用户请求时,在服务器编译 HTML(带数据),返回给浏览器 | 内容实时更新的页面(如电商商品详情、用户中心) | 内容实时性高,SEO 好,首屏快 |
| 静态站点生成(SSG) | 项目构建时(pnpm build)预生成所有页面的 HTML,后续请求直接返回静态文件 | 内容不常变的页面(如博客、官网、文档) | 首屏加载最快(毫秒级),服务器压力小,可 CDN 加速 |
| 增量静态再生(ISR) | 先预生成静态页面,后续每隔一段时间或当内容更新时,在服务器重新生成页面(不用重新构建整个项目) | 内容偶尔更新的页面(如新闻列表、产品列表) | 兼顾 SSG 的速度和 SSR 的实时性,更新内容不用重新部署整个项目 |
比如我们的 Todo 项目,如果是个人使用,可先用 SSG 预生成页面;如果需要多人协作、实时同步 Todo,就用 SSR 渲染用户专属的 Todo 列表。
2. 路由原理:文件系统路由,告别手动配置
Next.js 的路由本质是 “基于文件系统的约定式路由”,不用写 react-router 的配置,而是通过 src/app 目录的结构自动生成路由,背后逻辑很简单:
- 文件夹对应路由路径(如
src/app/todo→/todo); page.tsx是路由的 “入口文件”,必须存在才能访问该路由;- 动态路由用
[参数名]命名文件夹(如src/app/todo/[id]→/todo/1),通过params获取参数:
// src/app/todo/[id]/page.tsx(动态路由页面)
interface TodoDetailProps {
params: { id: string }; // 动态路由参数,Next.js 自动注入
}
export default function TodoDetail({ params }: TodoDetailProps) {
// params.id 就是路由里的 id(如 /todo/1 → params.id = '1')
return <h1>Todo 详情:{params.id}</h1>;
}
3. 代码分割:自动拆分 JS,减少首屏加载体积
Next.js 会自动对代码进行分割,每个路由对应一个 JS chunk(块),用户访问 /todo 时,只加载 /todo 路由的 JS,不会加载 /about 或其他路由的代码,大大减少了首屏需要下载的 JS 体积。
比如我们的 Todo 项目,首页(/)的 JS 只包含首页的逻辑,Todo 页面(/todo)的 JS 只包含 Todo 相关的逻辑,不用一次性下载所有代码。
总结:Next.js 为什么值得学? 🚀
- 解决核心痛点:一键解决 React 的 SEO 差、首屏慢、配置繁琐问题,不用自己造轮子;
- 开箱即用:内置路由、TS、Tailwind、ESLint 等,搭环境快,专注业务开发;
- 企业级优化:支持 SSR/SSG/ISR、代码分割、图片优化等,满足生产环境需求;
- 生态成熟:React 官方推荐,文档完善,社区活跃,遇到问题容易找到解决方案。