React全家桶笔记(四):React脚手架与TodoList实战
本篇进入 React 工程化开发阶段,学习 Create React App 脚手架的使用,并通过 TodoList 案例掌握组件化编码的完整流程。 📺 对应张天禹react全家桶视频:P49 - P64
一、初始化 React 脚手架(P49)
1.1 什么是脚手架?
- 脚手架(Scaffold)是用来帮助程序员快速创建一个基于某个库的模板项目的工具
- React 提供了一个用于创建 React 项目的脚手架库:create-react-app
- 项目的整体技术架构为:React + Webpack + ES6 + ESLint
1.2 创建项目
# 全局安装 create-react-app(只需一次)
npm i -g create-react-app
# 创建项目
create-react-app my-app
# 进入项目目录
cd my-app
# 启动项目
npm start
💡 现在更推荐用 npx 方式,不需要全局安装:
npx create-react-app my-app
二、脚手架文件结构(P50-P51)
2.1 public 目录(P50)
public/
├── favicon.ico → 网站页签图标
├── index.html → 主页面 ⭐(整个应用只有这一个 HTML 文件)
├── logo192.png → logo 图
├── logo512.png → logo 图
├── manifest.json → 应用加壳的配置文件(PWA 相关)
└── robots.txt → 爬虫协议文件
index.html 关键内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- %PUBLIC_URL% 代表 public 文件夹的路径 -->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- 开启理想视口,用于做移动端网页适配 -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- 用于配置浏览器页签+地址栏的颜色(仅安卓手机浏览器) -->
<meta name="theme-color" content="#000000" />
<!-- 网站描述 -->
<meta name="description" content="Web site created using create-react-app" />
<!-- 用于指定网页添加到手机主屏幕后的图标(仅苹果手机) -->
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- 应用加壳时的配置文件(PWA) -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<!-- 根容器 -->
<div id="root"></div>
</body>
</html>
2.2 src 目录(P51)
src/
├── App.css → App 组件的样式
├── App.js → App 组件 ⭐
├── App.test.js → App 组件的测试文件
├── index.css → 全局样式
├── index.js → 入口文件 ⭐⭐
├── logo.svg → logo 图
├── reportWebVitals.js → 页面性能分析文件(需要 web-vitals 库)
└── setupTests.js → 组件单元测试文件(需要 jest-dom 库)
index.js 入口文件:
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
🔗 概念扩展:
React.StrictMode是什么? 它是 React 提供的一个用于检测潜在问题的工具组件,只在开发模式下有效,不会影响生产构建。它会帮你检查:过时的 API 使用、意外的副作用等。
三、一个简单的 Hello 组件(P52)
3.1 项目结构规范
src/
├── components/ → 存放所有组件
│ ├── Hello/ → 每个组件一个文件夹
│ │ ├── index.jsx → 组件代码
│ │ └── index.css → 组件样式
│ └── Welcome/
│ ├── index.jsx
│ └── index.css
├── App.jsx → 根组件
└── index.js → 入口文件
3.2 Hello 组件示例
// src/components/Hello/index.jsx
import React, { Component } from 'react'
import './index.css'
export default class Hello extends Component {
render() {
return <h2 className="title">Hello, React!</h2>
}
}
/* src/components/Hello/index.css */
.title {
background-color: orange;
padding: 10px;
}
// src/App.jsx
import React, { Component } from 'react'
import Hello from './components/Hello'
import Welcome from './components/Welcome'
export default class App extends Component {
render() {
return (
<div>
<Hello />
<Welcome />
</div>
)
}
}
文件命名约定:
- 组件文件用
.jsx后缀(非强制,但推荐,便于区分) - 组件文件夹内用
index.jsx命名,这样导入时可以省略文件名:import Hello from './components/Hello'
四、样式的模块化(P53)
当多个组件的 CSS 中有相同的类名时,会产生样式冲突。解决方案:
4.1 CSS Modules
/* 文件名改为 index.module.css */
.title {
background-color: orange;
}
import React, { Component } from 'react'
import hello from './index.module.css'
export default class Hello extends Component {
render() {
// 通过 hello.title 引用样式,生成的类名是唯一的
return <h2 className={hello.title}>Hello, React!</h2>
}
}
4.2 其他方案(了解)
- Less/Sass 嵌套:通过组件外层类名嵌套避免冲突
- CSS-in-JS:styled-components 等库
- Tailwind CSS:原子化 CSS,不存在类名冲突
五、组件化编码流程(P55)
拿到一个功能需求后的标准开发流程:
1. 拆分组件:拆分界面,抽取组件
2. 实现静态组件:使用组件实现静态页面效果(只有结构和样式,没有交互)
3. 实现动态组件:
a. 动态显示初始化数据
- 数据类型?
- 数据名称?
- 保存在哪个组件的 state 中?
b. 交互(从绑定事件开始)
🔗 核心原则:数据放在哪?
- 如果数据只有某个组件用 → 放在该组件自身的 state 中
- 如果数据是多个组件共用的 → 放在它们共同的父组件的 state 中(状态提升)
六、TodoList 案例完整实战(P56-P64)
6.1 组件拆分
App(根组件,管理 todos 数据)
├── Header(输入框,添加 todo)
├── List(列表容器)
│ └── Item(单个 todo 项)× N
└── Footer(底部统计和操作)
6.2 静态组件(P56)
先把 HTML + CSS 写好,不考虑任何交互逻辑。
6.3 动态初始化列表(P57)
// App.jsx — 数据放在 App 中,因为 Header 和 List 都需要用
export default class App extends Component {
state = {
todos: [
{ id: '001', name: '吃饭', done: true },
{ id: '002', name: '睡觉', done: true },
{ id: '003', name: '写代码', done: false },
]
}
render() {
const { todos } = this.state
return (
<div className="todo-container">
<div className="todo-wrap">
<Header />
<List todos={todos} />
<Footer />
</div>
</div>
)
}
}
// List/index.jsx — 接收 todos,遍历渲染 Item
export default class List extends Component {
render() {
const { todos } = this.props
return (
<ul className="todo-main">
{todos.map(todo => (
<Item key={todo.id} {...todo} />
))}
</ul>
)
}
}
6.4 添加 Todo(P58)
关键问题:Header 组件需要把数据传给 App 组件(子 → 父通信)
解决方案:App 通过 props 传一个函数给 Header,Header 调用这个函数把数据传回来
// App.jsx
addTodo = (todoObj) => {
const { todos } = this.state
const newTodos = [todoObj, ...todos]
this.setState({ todos: newTodos })
}
render() {
return (
<Header addTodo={this.addTodo} />
// ...
)
}
// Header/index.jsx
import { nanoid } from 'nanoid' // 生成唯一 id
export default class Header extends Component {
handleKeyUp = (event) => {
// 判断是否按下回车
if (event.keyCode !== 13) return
// 输入不能为空
if (event.target.value.trim() === '') {
alert('输入不能为空')
return
}
// 准备好一个 todo 对象
const todoObj = {
id: nanoid(),
name: event.target.value,
done: false
}
// 调用父组件传来的函数,把数据传回去
this.props.addTodo(todoObj)
// 清空输入
event.target.value = ''
}
render() {
return (
<div className="todo-header">
<input onKeyUp={this.handleKeyUp} type="text" placeholder="请输入你的任务名称,按回车键确认" />
</div>
)
}
}
🎯 核心模式:子组件 → 父组件通信 父组件通过 props 传递一个回调函数给子组件,子组件在需要时调用该函数并传入数据。这是 React 中最基本的子传父通信方式。
6.5 鼠标移入效果(P59)
// Item/index.jsx
export default class Item extends Component {
state = { mouse: false }
handleMouse = (flag) => {
return () => {
this.setState({ mouse: flag })
}
}
render() {
const { name, done } = this.props
const { mouse } = this.state
return (
<li
style={{ backgroundColor: mouse ? '#ddd' : 'white' }}
onMouseEnter={this.handleMouse(true)}
onMouseLeave={this.handleMouse(false)}
>
<label>
<input type="checkbox" checked={done} onChange={this.handleCheck(this.props.id)} />
<span>{name}</span>
</label>
<button
className="btn btn-danger"
style={{ display: mouse ? 'block' : 'none' }}
>
删除
</button>
</li>
)
}
}
6.6 勾选与取消勾选(P60)
// App.jsx — 更新 todo 的 done 状态
updateTodo = (id, done) => {
const { todos } = this.state
const newTodos = todos.map(todoObj => {
if (todoObj.id === id) return { ...todoObj, done }
else return todoObj
})
this.setState({ todos: newTodos })
}
⚠️ 注意:
checked和onChange必须配合使用。如果只写checked不写onChange,复选框会变成只读的。或者用defaultChecked(只在初始化时生效)。
6.7 对 props 进行限制(P61)
import PropTypes from 'prop-types'
// 在 List 组件中
static propTypes = {
todos: PropTypes.array.isRequired,
}
6.8 删除 Todo(P62)
// App.jsx
deleteTodo = (id) => {
const { todos } = this.state
const newTodos = todos.filter(todoObj => todoObj.id !== id)
this.setState({ todos: newTodos })
}
// Item/index.jsx
handleDelete = (id) => {
if (window.confirm('确定删除吗?')) {
this.props.deleteTodo(id)
}
}
6.9 底部功能(P63)
// Footer/index.jsx
export default class Footer extends Component {
handleCheckAll = (event) => {
this.props.checkAllTodo(event.target.checked)
}
handleClearAllDone = () => {
this.props.clearAllDone()
}
render() {
const { todos } = this.props
// 已完成的数量
const doneCount = todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
// 总数
const total = todos.length
return (
<div className="todo-footer">
<label>
<input
type="checkbox"
onChange={this.handleCheckAll}
checked={doneCount === total && total !== 0}
/>
</label>
<span>
已完成{doneCount} / 全部{total}
</span>
<button onClick={this.handleClearAllDone} className="btn btn-danger">
清除已完成任务
</button>
</div>
)
}
}
🔗 概念扩展:
reduce条件计数todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)是一个经典的用 reduce 做条件计数的写法。初始值为 0,每遇到一个 done 为 true 的就 +1。
6.10 TodoList 案例总结(P64)
TodoList 案例核心收获:
├── 组件拆分思路:按功能区域拆分(Header/List/Item/Footer)
├── 数据放置原则:
│ ├── 某个组件独用 → 放自身 state
│ └── 多个组件共用 → 放共同父组件 state(状态提升)
├── 父子通信:
│ ├── 父 → 子:通过 props 传递数据
│ └── 子 → 父:父组件传递回调函数,子组件调用
├── 状态在哪里,操作状态的方法就在哪里
├── checked 和 onChange 必须配合使用
├── defaultChecked 只在初始化时生效(不受控)
└── 数组方法的灵活运用:map、filter、reduce
⚠️ 注意事项:
- 不要直接修改 state 中的数据,要产生新数据再 setState
setState是合并更新,不是替换- 状态在哪里,操作状态的方法就定义在哪里(这是一个重要的设计原则)
本章知识图谱
React 脚手架与实战
├── Create React App
│ ├── 创建:npx create-react-app my-app
│ ├── public/index.html → 唯一的 HTML 页面(SPA)
│ ├── src/index.js → 入口文件
│ └── src/App.jsx → 根组件
├── 项目规范
│ ├── 组件文件夹化:components/Hello/index.jsx
│ ├── 样式模块化:index.module.css
│ └── .jsx 后缀区分组件文件
├── 组件化编码流程
│ ├── 1. 拆分组件
│ ├── 2. 实现静态组件
│ └── 3. 实现动态组件(数据 + 交互)
└── TodoList 实战
├── 状态提升:共享数据放父组件
├── 子传父:回调函数模式
├── 状态在哪,方法在哪
└── 不可变数据:产生新数据再 setState
📌 下一篇:[React全家桶笔记(五):React网络请求 — 代理、Axios与Fetch] 将学习如何在 React 中发送网络请求,配置代理解决跨域问题。