初始化脚手架

使用 npx 创建

1
npx create-react-app 项目名

使用 npm install 创建

全局安装create-react-app包:

1
npm install -g create-react-app

创建脚手架:

1
create-react-app 项目名

npm 镜像

执行 create-react-app时,还会自动安装一些包,这个时候,默认使用的是npm,速度较慢,可以替换为国内源。

1
2
3
4
# 换源
npm config set registry https://registry.npm.taobao.org
# 查看修改的结果
npm config get registry

若已经安装cpm,则全局安装 create-react-app 时可以使用 cnpm ,但需要注意的是,在执行 create-react-app my-app时,还会自动安装一些包,这个时候,默认使用的还是npm

脚手架文件结构

image-20220403201401971

public

  • favicon.ico
    偏爱图标,设置默认图标

  • index.html
    详见注释(英文注释为react自动生成)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    <!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" />
    <!-- 用于配置浏览器页签和地址栏颜色(仅支持Android手机浏览器) -->
    <meta name="theme-color" content="#000000" />
    <meta
    name="description"
    content="Web site created using create-react-app"
    />
    <!-- 用于指定网页添加到手机桌面后的图标(iPhone) -->
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <!--
    manifest.json provides metadata used when your web app is installed on a
    user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    应用加壳时的配置文件
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
    Notice the use of %PUBLIC_URL% in the tags above.
    It will be replaced with the URL of the `public` folder during the build.
    Only files inside the `public` folder can be referenced from the HTML.

    Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
    work correctly both with client-side routing and a non-root public URL.
    Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React App</title>
    </head>
    <body>
    <!-- 若浏览器不支持js则展示标签中的内容 -->
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
    This HTML file is a template.
    If you open it directly in the browser, you will see an empty page.

    You can add webfonts, meta tags, or analytics to this file.
    The build step will place the bundled scripts into the <body> tag.

    To begin the development, run `npm start` or `yarn start`.
    To create a production bundle, use `npm run build` or `yarn build`.
    -->
    </body>
    </html>
  • robots.txt
    爬虫协议规则文件,限制网站爬虫抓取

src

  • App.js
    定义名为App的组件

  • App.css
    App组件的样式表

  • index.js
    入口文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import React from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    import App from './App';
    import reportWebVitals from './reportWebVitals';

    ReactDOM.render(
    <React.StrictMode>
    <App />
    </React.StrictMode>,
    document.getElementById('root')
    );

    // If you want to start measuring performance in your app, pass a function
    // to log results (for example: reportWebVitals(console.log))
    // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
    reportWebVitals();

    App组件外包裹React.StrictMode标签可以对App组件代码进行检查,如:警告字符串类型的ref

  • index.css
    全局通用样式表,也可放在public文件夹中,在index.html中用link标签引入(不建议,破坏结构)

  • reportWebVitals.js
    记录页面性能

  • setupTests.js
    做组件测试(整体测试或单元测试)

一个简单的Hello案例

image-20220403223840528

index.js

1
2
3
4
5
6
7
8
9
// 引入React核心库
import React from 'react'
// 引入React-DOM
import ReactDOM from 'react-dom'
// 引入App组件
import App from './App' // .js后缀可省略

// 渲染App到页面
ReactDOM.render(<App/>, document.getElementById('root'))

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建“外壳”组件App
import React, {Component} from "react" // 这里的{Component}能够引用是因为React.Component被局部暴露
import Hello from "./Hello"
import Welcome from "./components/Welcome"

// 创建并暴露App组件
export default class App extends Component {
render() {
return(
<div>
<Hello/>
<Welcome/>
</div>
)
}
}

// 暴露App组件
// export default App

Hello.js

1
2
3
4
5
6
7
8
9
import React, {Component} from "react"

export default class Hello extends Component {
render() {
return(
<h1>Hello React</h1>
)
}
}

Welcome.jsx

1
2
3
4
5
6
7
8
9
10
import React, {Component} from "react"
import './Welcome.css'

export default class Welcome extends Component {
render() {
return(
<h1 className="wel">Welcome to line2!</h1>
)
}
}

Welcome.css

1
2
3
.wel {
color: blueviolet;
}

效果

image-20220403224228464

需要注意的点

  1. js文件可以更改为jsx文件(index.js也是)

  2. import React, {Component} from "react" 里的{Component}能够引用是因为React.Component被局部暴露,即:未暴露不能这么写,可以写成:const {Component} = React,继承时不再写extends React.Component,直接写extends Component即可

  3. 组件文件夹中为每个子组件创建文件夹,可以有两种形式:

    • 文件位置:components/Welcome/Welcome.js,引用方式:import Welcome from "./components/Welcome/Welcome"
    • 文件位置:components/Welcome/index.js,引用方式:import Welcome from "./components/Welcome"
  4. 样式的模块化:将Welcome.css更名为Welcome.module.css,随后在对应组件中引入import welcome from './index.module.css'组件完整代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import React, {Component} from "react"
    import welcome from './index.module.css'

    export default class Welcome extends Component {
    render() {
    return(
    <h1 className={welcome.wel}>Welcome to line2!</h1>
    )
    }
    }

功能界面的组件化编码流程

  1. 拆分组件:拆分界面,抽取组件
  2. 实现静态组件:使用组件实现静态页面效果
  3. 实现动态组件
    • 动态显示初始化数据(状态)
      • 数据类型
      • 数据名称
      • 保存在哪个组件
    • 交互(从绑定事件监听开始)

TodoList案例

文件结构

image-20220404145925526

静态页面准备

index.js

1
2
3
4
5
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(<App/>, document.getElementById('root'))

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from 'react'

import Header from './components/Header/Header'
import List from './components/List/List'
import Footer from './components/Footer/Footer'

import './App.css'

export default class App extends Component {
render() {
return (
<div className="todo-container">
<div className="todo-wrap">
<Header />
<List />
<Footer />
</div>
</div>
)
}
}

App.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*base*/
body {
background: #fff;
}

.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}

.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}

.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}

.btn:focus {
outline: none;
}

.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}

Header.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Component } from 'react'

import './Header.css'

export default class Header extends Component {
render() {
return (
<div className="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认"/>
</div>
)
}
}

Header.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}

.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}

List.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component } from 'react'

import Item from '../Item/Item'

import './List.css'

export default class List extends Component {
render() {
return (
<ul className="todo-main">
<Item />
<Item />
</ul>
)
}
}

List.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}

.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}

Item.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { Component } from 'react'

import './Item.css'

export default class Item extends Component {
render() {
return (
<li>
<label>
<input type="checkbox"/>
<span>xxxxx</span>
</label>
<button className="btn btn-danger" style={{display: 'none'}}>删除</button>
</li>
)
}
}

Item.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}

li label {
float: left;
cursor: pointer;
}

li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}

li button {
float: right;
display: none;
margin-top: 3px;
}

li:before {
content: initial;
}

li:last-child {
border-bottom: none;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { Component } from 'react'

import './Footer.css'

export default class Footer extends Component {
render() {
return (
<div className="todo-footer">
<label>
<input type="checkbox"/>
</label>
<span>
<span>已完成0</span> / 全部2
</span>
<button className="btn btn-danger">清除已完成任务</button>
</div>
)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}

.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}

.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}

.todo-footer button {
float: right;
margin-top: 5px;
}

动态初始化列表

目前没有学消息订阅与发布,兄弟组件间无法通信,所以将状态放在App组件中。

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 初始化状态
state = {todoList:[
{id: 1, name: '吃饭', done: true},
{id: 2, name: '睡觉', done: true},
{id: 3, name: '写代码', done: false}
]}

render() {
const {todoList} = this.state
return (
<div className="todo-container">
<div className="todo-wrap">
<Header/>
<List todoList={todoList}/>
<Footer/>
</div>
</div>
)

List组件接收后使用展开运算符传递给Item组件

List.jsx

1
2
3
4
5
6
7
8
9
10
11
12
render() {
const {todoList} = this.props
return (
<ul className="todo-main">
{
todoList.map((todo) => {
return <Item key={todo.id} {...todo}/>
})
}
</ul>
)
}

Item组件接收props并展示

Item.jsx

1
2
3
4
5
6
7
8
9
10
11
12
render() {
const {id, name, done} = this.props
return (
<li>
<label>
<input type="checkbox" defaultChecked={done}/> // 不能使用checked,会将选定状态写死,defaultChecked可能产生bug
<span>{name}</span>
</label>
<button className="btn btn-danger" style={{display: 'none'}}>删除</button>
</li>
)
}

效果如下:

image-20220404161249053

实现添加一个Todo

Header组件中为input添加onKeyUp事件

Header.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default class Header extends Component {
handleKeyUp = (event) => {
const {target, key} = event // 不建议使用keyCode
if (key !== 'Enter') {
return
}
console.log(target.value)
}
render() {
return (
<div className="todo-header">
<input onKeyUp={this.handleKeyUp} type="text" placeholder="请输入你的任务名称,按回车键确认"/>
</div>
)
}
}

GIF 2022-4-4 16-44-42

App组件向Header组件传递addTodo函数

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// addTodo用于添加一个todo,接收的参数是todo对象
addTodo = (todoObj) => {
// 获取原todoList
const {todoList} = this.state
// 追加一个todo
const newTodos = [todoObj, ...todoList]
// 更新状态
this.setState({todoList: newTodos})
}

render() {
const {todoList} = this.state
return (
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo}/>
<List todoList={todoList}/>
<Footer/>
</div>
</div>
)

Header组件onKeyUp对输入数据进行处理并传回App组件

Header.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
handleKeyUp = (event) => {
// 解构赋值获取target, key
const {target, key} = event
// 判断是否是回车
if (key !== 'Enter') {
return
}
// 添加的todo名字不能为空
if (target.value.trim() === '') {
alert('输入不能为空')
return
}
// 准备一个todo对象
const todoObj = {id: nanoid(), name: target.value, done: false}
// 将todoObj传递给App
this.props.addTodo(todoObj)
// 清空输入
target.value = ''
}

nanoid

使用nanoid或uuid生成唯一id值

执行npm i nanoidyarn add nanoid安装依赖,使用时执行引入语句import { nanoid } from 'nanoid',调用nanoid()函数即可

效果

GIF 2022-4-4 17-41-13

鼠标悬浮效果

Item组件设置state,并添加鼠标移入移出监听

Item.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default class Item extends Component {

state = {mouse: false}

handleMouse = flag => {
return () => {
this.setState({mouse: flag})
}
}

render() {
const {id, name, done} = this.props
const {mouse} = this.state
return (
<li style={{backgroundColor: mouse ? '#ddd' : '#fff'}} onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}>
<label>
<input type="checkbox" defaultChecked={done}/>
<span>{name}</span>
</label>
<button className="btn btn-danger" style={{display: mouse ? 'block' : 'none'}}>删除</button>
</li>
)
}
}

GIF 2022-4-4 22-14-16

复选框更新状态

App组件通过List组件向Item组件传递updateTodo函数

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// updateTodo用于更新一个todo对象
updateTodo = (id, done) => {
// 获取状态中的todoList
const {todoList} = this.state
// 加工数据(匹配处理)
const newTodos = todoList.map((todoObj) => {
if (todoObj.id === id) {
return {...todoObj, done} // 简写形式,{...todoObj, done: done}
} else {
return todoObj
}
})
this.setState({todoList: newTodos})
}

render() {
const {todoList} = this.state
return (
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo}/>
<List todoList={todoList} updateTodo={this.updateTodo}/>
<Footer/>
</div>
</div>
)

List.jsx

1
return <Item key={todo.id} {...todo} updateTodo={updateTodo}/>

Item.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
handleChange = id => {
return (event) => {
this.props.updateTodo(id, event.target.checked)
}
}

render() {
const {id, name, done} = this.props
const {mouse} = this.state
return (
<li style={{backgroundColor: mouse ? '#ddd' : '#fff'}} onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}>
<label>
<input type="checkbox" defaultChecked={done} onChange={this.handleChange(id)}/>
<span>{name}</span>
</label>
<button className="btn btn-danger" style={{display: mouse ? 'block' : 'none'}}>删除</button>
</li>
)
}

GIF 2022-4-4 22-43-51

对props进行限制

引入prop-types库

1
import PropTypes from 'prop-types'

Header.jsx

1
2
3
4
// 对接收的props进行类型、必要性的限制
static propTypes = {
addTodo: PropTypes.func.isRequired
}

List.jsx

1
2
3
4
5
// 对接收的props进行类型、必要性的限制
static propTypes = {
todoList: PropTypes.array.isRequired,
updateTodo: PropTypes.func.isRequired
}

实现删除一个Todo

addTodoupdateTodo实现同理,App通过List向Item传递deleteTodo

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// deleteTodo用于删除一个todo对象
deleteTodo = (id) => {
// 获取原todoList
const {todoList} = this.state
// 删除指定id的todo对象
const newTodos = todoList.filter((todoObj) => {
return todoObj.id !== id
})
// 更新状态
this.setState({todoList: newTodos})
}

render() {
const {todoList} = this.state
return (
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo}/>
<List todoList={todoList} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
<Footer/>
</div>
</div>
)
}

List.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default class List extends Component {

// 对接收的props进行类型、必要性的限制
static propTypes = {
todoList: PropTypes.array.isRequired,
updateTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired
}

render() {
const {todoList, updateTodo, deleteTodo} = this.props
return (
<ul className="todo-main">
{
todoList.map((todo) => {
return <Item key={todo.id} {...todo} updateTodo={updateTodo} deleteTodo={deleteTodo}/>
})
}
</ul>
)
}
}

Item.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 删除一个todo的回调
handleDelete = (id) => {
if (window.confirm('确定删除吗?')) { // 此处要加window,否则会报错Unexpected use of 'confirm'.
this.props.deleteTodo(id)
}
}

render() {
const {id, name, done} = this.props
const {mouse} = this.state
return (
<li style={{backgroundColor: mouse ? '#ddd' : '#fff'}} onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}>
<label>
<input type="checkbox" defaultChecked={done} onChange={this.handleChange(id)}/>
<span>{name}</span>
</label>
<button onClick={() => this.handleDelete(id) } className="btn btn-danger" style={{display: mouse ? 'block' : 'none'}}>删除</button>
</li>
)
}

JavaScript中,关于消息提示框的方法有三个:

  • alert(message)方法用于显示带有一条指定消息和一个 OK 按钮的警告框。
  • confirm(message)方法用于显示一个带有指定消息和 OK 及取消按钮的对话框。如果用户点击确定按钮,则 confirm() 返回 true。如果点击取消按钮,则 confirm() 返回 false。
  • prompt(text,defaultText)方法用于显示可提示用户进行输入的对话框。如果用户单击提示框的取消按钮,则返回 null。如果用户单击确认按钮,则返回输入字段当前显示的文本。

GIF 2022-4-5 10-10-52

实现footer的功能

重要

defaultChecked修改为checked

defaultChecked指定复选框的初始状态,一旦指定后无法修改

已完成/全部 + 全选

  • 已完成
    • App组件向Footer组件传递todoList
    • Footer组件计算已完成数目和总数
  • 全选
    • App组件向Footer组件传递checkAllTodo
    • Footer组件完成全选checkbox的回调

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// checkAllTodo用于todo对象的全选
checkAllTodo = (done) => {
// 获取原todoList
const {todoList} = this.state
// 加工数据
const newTodos = todoList.map((todoObj) => { return {...todoObj, done} })
// 更新状态
this.setState({todoList: newTodos})
}

render() {
const {todoList} = this.state
return (
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo}/>
<List todoList={todoList} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
<Footer todoList={todoList} checkAllTodo={this.checkAllTodo}/>
</div>
</div>
)
}

Footer.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
export default class Footer extends Component {

// 全选checkbox的回调
handleChangeAll = (event) => {
this.props.checkAllTodo(event.target.checked)
}

render() {

const {todoList} = this.props

// 已完成的个数
const doneCount = todoList.reduce((pre, currentTodo) => pre + (currentTodo.done ? 1 : 0), 0) // pre上一次的返回值
// console.log('@', doneCount)

// 总数
const total = todoList.length

return (
<div className="todo-footer">
<label>
<input type="checkbox" onChange={this.handleChangeAll} checked={doneCount === total && total !== 0}/>
</label>
<span>
<span>已完成{doneCount}</span> / 全部{total}
</span>
<button className="btn btn-danger">清除已完成任务</button>
</div>
)
}
}

清除全部已完成

App组件向Footer组件传递clearAllDoneTodo

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// clearAllDoneTodo用于清除所有已完成任务
clearAllDoneTodo = () => {
// 获取原todoList
const {todoList} = this.state
// 过滤数据
const newTodos = todoList.filter((todoObj) => { return !todoObj.done })
// 更新状态
this.setState({todoList: newTodos})
}

render() {
const {todoList} = this.state
return (
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo}/>
<List todoList={todoList} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
<Footer todoList={todoList} checkAllTodo={this.checkAllTodo} clearAllDoneTodo={this.clearAllDoneTodo}/>
</div>
</div>
)
}

Footer.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
this.handleClearAll = () => {
this.props.clearAllDoneTodo()
}

return (
<div className="todo-footer">
<label>
<input type="checkbox" onChange={this.handleChangeAll} checked={doneCount === total && total !== 0}/>
</label>
<span>
<span>已完成{doneCount}</span> / 全部{total}
</span>
<button onClick={this.handleClearAll} className="btn btn-danger">清除已完成任务</button>
</div>
)

完整逻辑代码与最终效果

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import React, { Component } from 'react'

import Header from './components/Header/Header'
import List from './components/List/List'
import Footer from './components/Footer/Footer'

import './App.css'

export default class App extends Component {

// 初始化状态
state = {todoList:[
{id: '1', name: '吃饭', done: true},
{id: '2', name: '睡觉', done: true},
{id: '3', name: '写代码', done: false}
]}

// addTodo用于添加一个todo,接收的参数是todo对象
addTodo = (todoObj) => {
// 获取原todoList
const {todoList} = this.state
// 追加一个todo
const newTodos = [todoObj, ...todoList]
// 更新状态
this.setState({todoList: newTodos})
}

// updateTodo用于更新一个todo对象
updateTodo = (id, done) => {
// 获取状态中的todoList
const {todoList} = this.state
// 加工数据(匹配处理)
const newTodos = todoList.map((todoObj) => {
if (todoObj.id === id) {
return {...todoObj, done} // 简写形式,{...todoObj, done: done}
} else {
return todoObj
}
})
this.setState({todoList: newTodos})
}

// deleteTodo用于删除一个todo对象
deleteTodo = (id) => {
// 获取原todoList
const {todoList} = this.state
// 删除指定id的todo对象
const newTodos = todoList.filter((todoObj) => {
return todoObj.id !== id
})
// 更新状态
this.setState({todoList: newTodos})
}

// checkAllTodo用于todo对象的全选
checkAllTodo = (done) => {
// 获取原todoList
const {todoList} = this.state
// 加工数据
const newTodos = todoList.map((todoObj) => { return {...todoObj, done} })
// 更新状态
this.setState({todoList: newTodos})
}

// clearAllDoneTodo用于清除所有已完成任务
clearAllDoneTodo = () => {
// 获取原todoList
const {todoList} = this.state
// 过滤数据
const newTodos = todoList.filter((todoObj) => { return !todoObj.done })
// 更新状态
this.setState({todoList: newTodos})
}

render() {
const {todoList} = this.state
return (
<div className="todo-container">
<div className="todo-wrap">
<Header addTodo={this.addTodo}/>
<List todoList={todoList} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
<Footer todoList={todoList} checkAllTodo={this.checkAllTodo} clearAllDoneTodo={this.clearAllDoneTodo}/>
</div>
</div>
)
}
}

Header.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { nanoid } from 'nanoid'

import './Header.css'

export default class Header extends Component {

// 对接收的props进行类型、必要性的限制
static propTypes = {
addTodo: PropTypes.func.isRequired
}

handleKeyUp = (event) => {
// 解构赋值获取target, key
const {target, key} = event
// 判断是否是回车
if (key !== 'Enter') {
return
}
// 添加的todo名字不能为空
if (target.value.trim() === '') {
alert('输入不能为空')
return
}
// 准备一个todo对象
const todoObj = {id: nanoid(), name: target.value, done: false}
// 将todoObj传递给App
this.props.addTodo(todoObj)
// 清空输入
target.value = ''
}

render() {
return (
<div className="todo-header">
<input onKeyUp={this.handleKeyUp} type="text" placeholder="请输入你的任务名称,按回车键确认"/>
</div>
)
}
}

List.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React, { Component } from 'react'
import PropTypes from 'prop-types'

import Item from '../Item/Item'

import './List.css'

export default class List extends Component {

// 对接收的props进行类型、必要性的限制
static propTypes = {
todoList: PropTypes.array.isRequired,
updateTodo: PropTypes.func.isRequired,
deleteTodo: PropTypes.func.isRequired
}

render() {
const {todoList, updateTodo, deleteTodo} = this.props
return (
<ul className="todo-main">
{
todoList.map((todo) => {
return <Item key={todo.id} {...todo} updateTodo={updateTodo} deleteTodo={deleteTodo}/>
})
}
</ul>
)
}
}

Item.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React, { Component } from 'react'

import './Item.css'

export default class Item extends Component {

state = {mouse: false}

// 鼠标移入移出的回调
handleMouse = flag => {
return () => {
this.setState({mouse: flag})
}
}

// 勾选、取消某一个todo的回调
handleChange = id => {
return (event) => {
this.props.updateTodo(id, event.target.checked)
}
}

// 删除一个todo的回调
handleDelete = (id) => {
if (window.confirm('确定删除吗?')) { // 此处要加window,否则会报错Unexpected use of 'confirm'.
this.props.deleteTodo(id)
}
}

render() {
const {id, name, done} = this.props
const {mouse} = this.state
return (
<li style={{backgroundColor: mouse ? '#ddd' : '#fff'}} onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}>
<label>
<input type="checkbox" checked={done} onChange={this.handleChange(id)}/>
<span>{name}</span>
</label>
<button onClick={() => this.handleDelete(id) } className="btn btn-danger" style={{display: mouse ? 'block' : 'none'}}>删除</button>
</li>
)
}
}

Footer.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import React, { Component } from 'react'

import './Footer.css'

export default class Footer extends Component {

// 全选checkbox的回调
handleChangeAll = (event) => {
this.props.checkAllTodo(event.target.checked)
}

// 清除已完成任务的回调
handleClearAll = () => {

}

render() {

const {todoList} = this.props

// 已完成的个数
const doneCount = todoList.reduce((pre, currentTodo) => pre + (currentTodo.done ? 1 : 0), 0) // pre上一次的返回值
// console.log('@', doneCount)

// 总数
const total = todoList.length

// 清除已完成任务的回调
this.handleClearAll = () => {
this.props.clearAllDoneTodo()
}

return (
<div className="todo-footer">
<label>
<input type="checkbox" onChange={this.handleChangeAll} checked={doneCount === total && total !== 0}/>
</label>
<span>
<span>已完成{doneCount}</span> / 全部{total}
</span>
<button onClick={this.handleClearAll} className="btn btn-danger">清除已完成任务</button>
</div>
)
}
}

GIF 2022-4-5 11-45-48

总结

  1. 拆分组件、实现静态组件,注意:classNamestyle的写法
  2. 动态初始化列表,如何确定将数据放在哪个组件的state中:
    • 某个组件使用:放在自身的state中
    • 某些组件使用:放在他们共同的父组件state中(状态提升)
  3. 关于父子之间通信:
    • 【父组件】给【子组件】传递数据:通过props传递
    • 【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数
  4. 注意defaultCheckedchecked的区别,类似的还有:defaultValue和value
  5. 状态在哪里,操作状态的方法就在哪里