Lesson 01:搭建项目 + 展示 Todo 静态页面
🧩 本节信息卡(学习前先看)
- 阶段定位:Phase 1(基础篇)
- 推荐时长:45~75 分钟(首次学习)
- 先修要求:HTML / CSS / JavaScript 基础,Node.js 基本命令
- 学习产出:搭建 React 19 静态 Todo 页面,能解释 JSX 与启动入口
✅ 本节完成标准(自检清单)
- [ ] 我可以独立复现文中的核心代码片段
- [ ] 我能解释“为什么这样实现”,而不只是“照着写”
- [ ] 我记录了至少 1 个踩坑点和修复方法
🧭 本节统一学习流程
- 学习目标:先明确本节要解决的业务问题与核心 API。
- 主线实战:跟随课程实现可运行功能(先跑通,再优化)。
- 原理深挖:理解为什么这样设计,以及常见误区。
- 练习挑战:完成 L1/L2(阶段收官课建议加 L3)巩固迁移能力。
- 本节小结:回顾“做了什么 / 学到了什么 / 下节前检查项”。
建议节奏:阅读 20% + 编码 60% + 复盘 20%。
🎯 本节目标:用 Vite + React 19 + TypeScript + Tailwind CSS v4 搭建项目,渲染 Todo App 的静态界面。
📦 本节产出:一个可运行的静态 Todo 页面(数据硬编码,还不能交互)。
一、创建项目
1.1 初始化 Vite 项目
课程使用 React 19、Vite 7 和 Tailwind CSS 4。请安装 Node.js 22.12+(22.x)或 Node.js 24 LTS,再执行下面的命令。创建后保留 package-lock.json,后续用 npm ci 复现依赖;不要在课程中途混用其他大版本的模板。
npm create vite@8 phase1-todo -- --template react-ts
cd phase1-todo
npm installNOTE
为什么选 Vite 而不是 Create React App(CRA)?
- React 官方于 2025 年 2 月宣布弃用 CRA,不再推荐用它创建新项目。
- Vite 7 的开发服务器按需提供原生 ES 模块,并用 esbuild 预构建依赖;启动速度取决于项目规模与环境,不能保证固定耗时。
- Vite 提供热模块更新,适合在本课程中快速验证组件修改。
1.2 安装 Tailwind CSS v4
npm install tailwindcss@4 @tailwindcss/vite@4修改 vite.config.ts:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
react(),
tailwindcss(),
],
})替换 src/index.css 全部内容为:
@import "tailwindcss";TIP
Tailwind v4 vs v3 关键变化:
- 常见配置可以直接写在 CSS 的
@theme中;已有 JavaScript 配置需要通过@config显式加载。 - ✅ 用 CSS 原生
@import "tailwindcss"替代@tailwind指令 - ✅ 注册 Vite 插件并导入 CSS 后,Tailwind 会自动检测常见项目源文件中的类名。
1.3 清理模板
rm src/App.css src/assets/react.svg先完成第五节的 src/App.tsx 替换,再运行 npm run dev。原模板仍导入这两个文件,提前启动会报找不到模块;也可以在编辑器中删除文件。
二、项目结构解析
phase1-todo/
├── index.html ← 真正的入口文件(Vite 从这里开始)
├── package.json ← 依赖和脚本
├── tsconfig.json ← TypeScript 配置
├── vite.config.ts ← Vite 构建配置
├── public/ ← 静态资源(不经过构建)
└── src/
├── main.tsx ← React 启动入口
├── App.tsx ← 根组件(我们写代码的地方)
└── index.css ← 全局样式 + Tailwind 入口Vite 7 模板在 tsconfig.app.json 的 types 中引用 vite/client 类型;较早模板可能通过 src/vite-env.d.ts 引用,两种方式不必重复添加。
启动流程
关键文件:src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)三件事:
| 代码 | 作用 |
|---|---|
createRoot(...) | 创建 React 渲染根节点,接管 #root DOM |
<StrictMode> | 开发环境额外调用渲染逻辑,并检查 Effect 和 ref 回调的清理;不影响生产构建 |
<App /> | 渲染我们的根组件 |
三、JSX — 看起来像 HTML 的 JavaScript
3.1 JSX 的本质
JSX 会被编译为创建 React Element 的函数调用。本模板使用自动 JSX 转换,生成 react/jsx-runtime 中的 jsx / jsxs 调用;旧转换使用 React.createElement()。这些调用得到 UI 描述对象,React 再在提交阶段更新 DOM。
3.2 JSX 语法速查
function Demo() {
const name = 'React'
return (
<>
{/* 1. {} 嵌入 JS 表达式 */}
<h1>Hello, {name}! {2 + 3}</h1>
{/* 2. className 代替 class */}
<div className="text-blue-500">蓝色文字</div>
{/* 3. 内联样式用对象 + camelCase */}
<div style={{ fontSize: '20px', backgroundColor: '#f0f0f0' }}>样式</div>
{/* 4. 条件渲染 */}
{name === 'React' && <p>Yes!</p>}
{/* 5. 列表渲染(每项需要 key) */}
{['A', 'B', 'C'].map(item => <span key={item}>{item} </span>)}
{/* 6. 所有标签必须闭合 */}
<img src="/logo.png" alt="logo" />
</>
)
}JSX 与 HTML 的差异速查:
| HTML | JSX | 原因 |
|---|---|---|
class | className | React DOM 属性采用 className 命名 |
for | htmlFor | React DOM 用 htmlFor 关联 label 和控件 |
style="color: red" | style={{ color: 'red' }} | JSX 中 style 是对象 |
<br> | <br /> | JSX 要求所有标签闭合 |
onclick | onClick | JSX 事件用 camelCase |
四、TypeScript 快速入门
本项目中常用的类型:
// 基础类型
const name: string = 'React'
const version: number = 19
const isNew: boolean = true
// 对象 —— 用 interface 或 type
interface Todo {
id: number
text: string
completed: boolean
}
// 联合类型
type Filter = 'all' | 'active' | 'completed'
// 数组
const todos: Todo[] = []
// 函数
const greet = (name: string): string => `Hello, ${name}!`TIP
interface vs type?
interface:适合定义对象结构(如组件 Props),可extends继承type:更灵活,支持联合A | B、交叉A & B- 本课程约定:Props 用
interface,其他用type
五、实战:编写 Todo 静态页面
替换 src/App.tsx 全部内容:
// src/App.tsx
// 硬编码数据(后续用 state 管理)
const todos = [
{ id: 1, text: '学习 React 19 基础知识', completed: true },
{ id: 2, text: '用 Vite 搭建开发环境', completed: true },
{ id: 3, text: '实现 Todo 的添加功能', completed: false },
{ id: 4, text: '学习 useState 和 useEffect', completed: false },
{ id: 5, text: '掌握 React Router v7', completed: false },
]
function App() {
const total = todos.length
const completed = todos.filter(t => t.completed).length
return (
<div className="min-h-screen bg-linear-to-br from-indigo-50 via-white to-cyan-50">
<div className="max-w-xl mx-auto px-4 py-12">
{/* ── 头部 ── */}
<header className="text-center mb-10">
<h1 className="text-4xl font-bold bg-linear-to-r from-indigo-600 to-cyan-600 bg-clip-text text-transparent">
📝 Todo App
</h1>
<p className="mt-2 text-gray-500">React 19 · TypeScript · Tailwind v4</p>
<div className="mt-4 inline-flex items-center gap-2 bg-white px-4 py-2 rounded-full shadow-sm border border-gray-100">
<span className="text-sm text-gray-600">
已完成 <strong className="text-indigo-600">{completed}</strong> / {total}
</span>
<div className="w-20 h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-linear-to-r from-indigo-500 to-cyan-500 rounded-full transition-all"
style={{ width: `${total > 0 ? (completed / total) * 100 : 0}%` }}
/>
</div>
</div>
</header>
{/* ── 输入区域(静态) ── */}
<div className="flex gap-2 mb-6">
<input
type="text"
aria-label="新任务"
placeholder="添加新任务..."
className="flex-1 px-4 py-3 bg-white rounded-xl border border-gray-200 shadow-sm
focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-400
transition-all placeholder:text-gray-400"
/>
<button className="px-6 py-3 bg-indigo-600 text-white rounded-xl font-semibold shadow-sm
hover:bg-indigo-700 active:scale-95 transition-all">
添加
</button>
</div>
{/* ── 筛选按钮(静态) ── */}
<div className="flex gap-2 mb-4">
{['全部', '未完成', '已完成'].map((label, i) => (
<button
key={label}
className={`px-4 py-1.5 rounded-full text-sm font-medium transition-colors ${
i === 0
? 'bg-indigo-600 text-white shadow-sm'
: 'bg-white text-gray-600 hover:bg-gray-100 border border-gray-200'
}`}
>
{label}
</button>
))}
</div>
{/* ── 任务列表 ── */}
<div className="space-y-2">
{todos.map(todo => (
<div
key={todo.id}
className="flex items-center gap-3 p-4 bg-white rounded-xl border border-gray-100
shadow-sm hover:shadow-md transition-all group"
>
<input
type="checkbox"
checked={todo.completed}
aria-label={`完成任务:${todo.text}`}
readOnly
className="w-5 h-5 rounded-md border-gray-300 text-indigo-600
focus:ring-indigo-500/20 cursor-pointer"
/>
<span className={`flex-1 transition-colors ${
todo.completed ? 'line-through text-gray-400' : 'text-gray-800'
}`}>
{todo.text}
</span>
<button aria-label={`删除任务:${todo.text}`} className="opacity-0 group-hover:opacity-100 focus:opacity-100 text-gray-400
hover:text-red-500 transition-all text-lg">
✕
</button>
</div>
))}
</div>
<footer className="mt-8 text-center text-sm text-gray-400">
Phase 1 · Lesson 01 — 静态页面
</footer>
</div>
</div>
)
}
export default App运行 npm run dev,你会看到一个渐变背景、带进度条、鼠标悬停显示删除按钮的 Todo 页面。
目前列表数据是硬编码的,按钮还没有事件处理。Lesson 02 先拆分组件,Lesson 03 再用 useState 实现添加任务。
六、🧠 深度专题:Virtual DOM 与 Reconciliation
6.1 为什么需要 Virtual DOM?
DOM 修改可能触发样式计算、布局或绘制,具体取决于修改内容;浏览器也会合并部分工作。Virtual DOM 本身也有计算成本,并不保证比直接操作 DOM 更快。它让我们用声明式组件描述界面,由 React 协调更新。
React 的方案:用 JS 对象描述 UI 树,变化时对比新旧两棵树,只更新差异部分。
6.2 Reconciliation 流程
6.3 Diff 算法的两个核心假设
通用树编辑距离算法代价很高。React 采用基于类型和 key 的启发式协调规则,常用 O(n) 描述这一遍历思路;它不求全局最小编辑距离,也不代表整个应用的渲染成本总是线性的。
假设 1:不同类型 → 整棵子树替换
假设 2:通过 key 识别列表元素
在我们的代码中:
{todos.map(todo => (
<div key={todo.id}> {/* ← key 帮助 React 精确追踪每一项 */}
{todo.text}
</div>
))}WARNING
不要用数组 index 做 key(当列表会增删/重排时)!key={index} 按位置识别元素,头部插入后可能把旧组件的状态或输入框值留给另一条任务。用同一列表内 唯一且稳定的 id 表示身份;key 本身不会阻止组件重新渲染。
6.4 Fiber 架构简介
React 16+ 将 Reconciliation 重写为 Fiber 架构:
| 特性 | Stack Reconciler | Fiber Reconciler |
|---|---|---|
| 渲染 | 同步递归,不可中断 | 支持在并发渲染阶段暂停和恢复;同步更新与提交阶段仍不可中断 |
| 优先级 | 无 | 高优先级任务(如用户输入)可插队 |
| 并发 | ❌ | ✅ 支持并发渲染特性(Concurrent Rendering) |
Fiber 提供了调度工作单元的基础,React 18/19 的并发渲染使用这一架构。useTransition 和 useDeferredValue 可以标记非紧急更新;这不意味着每次更新都会分片执行。
七、练习
- 修改数据:在
todos数组中添加你自己的任务,确认页面更新 - 改样式:把渐变背景换成
from-purple-50 to-pink-50,调整圆角、颜色 - 加元素:在底部添加一个"清空已完成"按钮(纯静态,不需要交互)
- 思考:页面上的进度条是怎么算出来的?如果
todos为空会怎样?
📌 本节小结
| 你做了什么 | 你学到了什么 |
|---|---|
| 用 Vite 创建 React 19 + TS 项目 | Vite 的优势、项目结构、启动流程 |
| 安装配置 Tailwind CSS v4 | Vite 插件与 CSS 入口 |
| 渲染了静态 Todo 页面 | JSX 编译为创建 React Element 的调用 |
用 .map() + key 渲染列表 | key 如何帮助 Diff 算法高效运作 |
| — | Virtual DOM → Reconciliation → Fiber 架构全景 |