Next.js入门:从环境搭建到底层渲染逻辑,吃透第一个 Todo 项目 🚀

234 阅读12分钟

用 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 或更高版本即可。

image.png

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/),后续再自定义

image.png

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 核心规则(必须记住!不然写路由会懵)  :

  1. 路由由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 跳详情页,就用这个);
  2. layout.tsx是 “布局组件”,作用于当前目录及子目录的所有页面(比如根布局src/app/layout.tsx里写个导航栏,所有页面都会显示这个导航栏,不用每个页面都写一遍~);

  3. 组件默认是 “服务器组件”(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、勾选完成,功能都能正常工作~ 🎉

20250824-0518-45.5080473.gif

底层原理: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 为什么值得学? 🚀

  1. 解决核心痛点:一键解决 React 的 SEO 差、首屏慢、配置繁琐问题,不用自己造轮子;
  2. 开箱即用:内置路由、TS、Tailwind、ESLint 等,搭环境快,专注业务开发;
  3. 企业级优化:支持 SSR/SSG/ISR、代码分割、图片优化等,满足生产环境需求;
  4. 生态成熟:React 官方推荐,文档完善,社区活跃,遇到问题容易找到解决方案。