【第三章-react 应用(基于 react 脚手架)】

【第三章-react 应用(基于 react 脚手架)】

  • [一、使用 create-react-app 创建 react 应用](#一、使用 create-react-app 创建 react 应用)
    • [1. react 脚手架](#1. react 脚手架)
    • [2. 创建项目并启动](#2. 创建项目并启动)
    • [3. react 脚手架项目结构](#3. react 脚手架项目结构)
    • [4. 一个简单的 Hello 组件](#4. 一个简单的 Hello 组件)
    • [5. vscode中 react 插件的安装](#5. vscode中 react 插件的安装)
  • 二、组件的组合使用
    • [1. 功能界面的组件化编码流程(通用)](#1. 功能界面的组件化编码流程(通用))
    • [2. 组件的组合使用------TodoList案例](#2. 组件的组合使用——TodoList案例)
      • [2.1 拆分组件](#2.1 拆分组件)
      • [2.2 完成静态页面](#2.2 完成静态页面)
      • [2.3 动态初始化列表](#2.3 动态初始化列表)
      • [2.4 添加一个todo](#2.4 添加一个todo)
      • [2.5 鼠标移入效果(背景色高亮并显示删除按钮)](#2.5 鼠标移入效果(背景色高亮并显示删除按钮))
      • [2.6 勾选或取消勾选某一个todo](#2.6 勾选或取消勾选某一个todo)
      • [2.7 对 props 进行限制](#2.7 对 props 进行限制)
      • [2.8 删除一个 todo](#2.8 删除一个 todo)
      • [2.9 实现底部功能](#2.9 实现底部功能)
      • [2.10 总结 TodoList 案例](#2.10 总结 TodoList 案例)

一、使用 create-react-app 创建 react 应用

1. react 脚手架

1.脚手架: 用来帮助程序员快速创建一个基于 xxx 库的模板项目;

  • 包含了所有需要的配置(语法检查、jsx编译、devServer...);
  • 下载好了所有相关的依赖;
  • 可以直接运行一个简单效果;

2.react 提供了一个用于创建 react 项目的脚手架库: create-react-app

3.项目的整体技术架构为:react + webpack + es6 + eslint

4.使用脚手架开发的项目的特点:模块化,组件化,工程化。

2. 创建项目并启动

  • 第一步,全局安装:npm install -g create-react-app
  • 第二步,创建项目hello-react:create-react-app react-staging
  • 第三步,进入项目文件夹:cd hello-react
  • 第四步,启动项目:npm start

创建项目成功:

页面效果:

3. react 脚手架项目结构

  • public ---- 静态资源文件夹

favicon.icon ------ 网站页签图标(偏爱图标)
index.html --------主页面

logo192.png ------- logo 图

logo512.png ------- logo 图

manifest.json ----- 应用加壳的配置文件

robots.txt -------- 爬虫协议文件

其中最重要的是主页面index.html

javascript 复制代码
<!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" />
    <!-- 应用加壳时的配置文件 -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <title>React App</title>
  </head>
  <body>
    <!-- 若浏览器不支持js,则展示标签中的内容 -->
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <!-- 相当于之前的test容器 -->
    <div id="root"></div>
  </body>
</html>
  • 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.html:主页面
  • index.js:入口文件
  • app.js:App 组件

4. 一个简单的 Hello 组件

效果:在页面输出一句话"Hello,React"

  1. 清空publicsrc文件夹,并在public文件夹新建index.html,新建root节点;
javascript 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>hello,react</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
  1. src文件夹创建入口文件index.js和组件App.js(组件名字要大写);

入口文件index.js需要引入ReactReactDom并渲染App组件挂载在root上:

javascript 复制代码
//引入react核心库
import React from 'react'
//引入ReactDOM
import ReactDOM from 'react-dom/client'
//引入App组件
import App from './App'

// 渲柒App到页面
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);

组件App.js里面只需要创建一个类式组件并暴露即可:

javascript 复制代码
//创建"外壳"组件App
import React from 'react'

//定义并暴露App组件
class App extends React.Component {
    render() {
        return (
            <h1>
                Hello,React
            </h1>
        )
    }
}

这样在页面就可以展示"Hello,React"了!

但这样的写法并不规范,我们需要创建Hello组件,并在App.js中引入使用。

  1. React.Component的两种优化方式:

第一种:解构赋值

javascript 复制代码
import React from 'react'
// 解构赋值
const {Component} = React;

class App extends Component {
    ...
}

第二种:利用模块化语法引入Component

javascript 复制代码
import React, {Component} from 'react'
class App extends Component {
    ...
}

我们来模拟下react中的写法,你就会明白这个地方的使用:

javascript 复制代码
let React = {
    a:1,
    b:2
}

// 这个地方暴露了Component,所以我们才能通过 import {Component} from 'react' 的方式引入
export class Component {

}

React.Component = Component;

export default React;
  1. App是外壳,要利用组件化的方式编写里面的内容;

同级目录下,创建组件Hello.js

javascript 复制代码
import React, {Component} from 'react'

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

App组件中引入Hello.js并使用:

javascript 复制代码
//创建"外壳"组件App
import React, {Component} from 'react'
import Hello from './Hello'

//创建并暴露App组件
export default class App extends Component {
    render() {
        return (
            <div>
                <Hello/>
            </div>
        )
    }
}
  1. 如果组件很多,放在App.js和入口文件index.js的同级目录里会混乱,所以我们创建一个components文件夹,专门用来存放组件文件;

通常我们的每个组件会对应一个文件夹,文件夹的名字一般以组件的名字命名(首字母大写),该文件夹里面有两个文件:index.jsxindex.css

  • index.jsx:组件代码(也就是我们的Hello.js);
  • index.css:组件样式;

注意:组件以.jsx后缀结尾,是为了方便我们区分该文件是组件 还是业务逻辑的js文件

我们创建一个Hello文件夹,放在components文件夹下面:


Hello/index.jsx代码如下:

javascript 复制代码
import React, {Component} from 'react'
// 引入样式文件
import './index.css'

export default class Index extends Component {
    render() {
        return (
            <h1 className="title">Hello,React</h1>
        )
    }
}

Hello/index.css代码如下:

javascript 复制代码
.title {
    background-color: orange;
}
  1. 这样写的优势就在于,如果你还有一个组件叫Welcome,那么你还可以继续写下去;

创建Welcome文件夹:

Welcome/index.jsx

javascript 复制代码
import React, {Component} from 'react'
import './index.css'

export default class Welcome extends Component {
    render() {
        return (
            <h2 className="desc">Welcome</h2>
        )


    }
}

Welcome/index.css

javascript 复制代码
 .desc {
  background-color: pink;
 }

App.js

javascript 复制代码
//创建"外壳"组件App
import React, {Component} from 'react'
// const {Component} = React;
import Hello from './components/Hello'
import Welcome from './components/Welcome'

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

此时页面效果:

  1. 样式的模块化;

如果Hello组件和Welcome组件都有title这个类名的时候,它们的样式最后汇总到App.js中就会发生冲突,一般有两种解决方案:

方案一:使用scss或者less文件,在容器最外层定义父类名;

Hello/index.jsx

javascript 复制代码
import React, {Component} from 'react'
import './index.css'

export default class Index extends Component {
    render() {
        return (
            <div className="hello">
                <h1 className="title">Hello,React</h1>
            </div>

        )
    }
}

Hello/index.scss

javascript 复制代码
.hello {
  .title {
    background-color: orange;
  }
}

方案二:样式的模块化;

先将index.css命名为index.module.css

Hello/index.jsx中以模块化的方式引入:

javascript 复制代码
import React, {Component} from 'react'
// 以模块化的方式引入样式,此时 hello 是个对象
import hello from './index.module.css'

export default class Index extends Component {
    render() {
        return (
            <h1 className={hello.title}>Hello,React</h1>
        )
    }
}

但是一般在我们的日常开发中,采用更多的还是使用scss或者less的方法写,然后形成嵌套关系。

5. vscode中 react 插件的安装

  1. 插件市场搜索"react",找到ES7+ React/Redux/React-Native snippets
  2. 一些常用的代码片段(代码模板);

rcc:创建一个类组件;

rfc:创建一个函数组件;

imp:引入模块;

更多的代码片段的使用请参考:vscode-react-javascript-snippets


二、组件的组合使用

1. 功能界面的组件化编码流程(通用)

  1. 拆分组件:拆分界面,抽取组件;

  2. 实现静态组件:使用组件实现静态页面效果;

  3. 实现动态组件:

  • 3.1 动态显示初始化数据;
    -- 数据类型;
    -- 数据名称;
    -- 保存在哪个组件?
  • 3.2 交互(从绑定事件监听开始);

2. 组件的组合使用------TodoList案例

功能:组件化实现此列表。

1显示所有todo列表;

2.输入文本,点击回车按钮,文本添加到列表中的首位,同时清除输入框中的文本;

3.鼠标悬浮每一列todo上,高亮并展示删除按钮,点击删除按钮可以对每一列todo进行删除;

4.底部对已完成todo和总数进行计数,并可以全部勾选、全部取消勾选,清除已完成todo;

2.1 拆分组件

建立组件文件夹:

2.2 完成静态页面

  1. 我们使用Gemini生成了一份htmlcss的文件;

index.html:

javascript 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>任务列表</title>
    <link rel="stylesheet" href="./style.css">
</head>
<body>

<div class="todo-container">
    <div class="input-section">
        <input type="text" placeholder="请输入你的任务名称,按回车键确认">
    </div>

    <ul class="todo-list">
        <li>
            <input type="checkbox">
            <span>xxxxx</span>
        </li>
        <li>
            <input type="checkbox">
            <span>yyyyy</span>
        </li>
    </ul>

    <div class="footer-section">
        <div class="stats">
            <input type="checkbox">
            <span>已完成0 / 全部2</span>
        </div>
        <button class="clear-btn">清除已完成任务</button>
    </div>
</div>

</body>
</html>

style.css:

javascript 复制代码
/* 基础样式重置 */
body {
    font-family: "Microsoft YaHei", Arial, sans-serif;
    background-color: #f5f5f5;
    display: flex;
    justify-content: center;
    padding-top: 50px;
}

/* 外层容器 */
.todo-container {
    width: 500px;
    background-color: #fff;
    border: 1px solid #ddd;
    border-radius: 5px;
    padding: 15px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

/* 输入框样式 */
.input-section input {
    width: 100%;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    box-sizing: border-box; /* 确保padding不撑开宽度 */
    outline: none;
    font-size: 14px;
}

.input-section input:focus {
    border-color: #4a90e2;
    box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6);
}

/* 列表样式 */
.todo-list {
    list-style: none;
    padding: 0;
    margin: 15px 0 0 0;
    border: 1px solid #ddd;
    border-radius: 4px;
}

.todo-list li {
    padding: 10px;
    border-bottom: 1px solid #ddd;
    display: flex;
    align-items: center;
}

.todo-list li:last-child {
    border-bottom: none;
}

.todo-list li input[type="checkbox"] {
    margin-right: 10px;
}

/* 底部区域 */
.footer-section {
    margin-top: 20px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.stats {
    display: flex;
    align-items: center;
    font-size: 14px;
}

.stats input {
    margin-right: 15px;
}

/* 红色清除按钮 */
.clear-btn {
    background-color: #da5a47;
    color: white;
    border: none;
    padding: 8px 15px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
}

.clear-btn:hover {
    background-color: #c04d3c;
}
  1. 将生成的html结构复制到App.jsx中,并将class替换成classNamestyle改写成双花括号的格式style={``{}}

App.jsx:

javascript 复制代码
import React, {Component} from 'react'

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

                <ul className="todo-list">
                    <li>
                        <input type="checkbox"/>
                        <span>xxxxx</span>
                    </li>
                    <li>
                        <input type="checkbox"/>
                        <span>yyyyy</span>
                    </li>
                </ul>

                <div className="footer-section">
                    <div className="stats">
                        <input type="checkbox"/>
                        <span>已完成0 / 全部2</span>
                    </div>
                    <button className="clear-btn">清除已完成任务</button>
                </div>
            </div>
        )
    }
}
  1. 将生成的css样式复制到App.css中,并在App.jsx中引入;

App.css:

javascript 复制代码
/* 基础样式重置 */
body {
    font-family: "Microsoft YaHei", Arial, sans-serif;
    background-color: #f5f5f5;
    display: flex;
    justify-content: center;
    padding-top: 50px;
}

/* 外层容器 */
.todo-container {
    width: 500px;
    background-color: #fff;
    border: 1px solid #ddd;
    border-radius: 5px;
    padding: 15px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* 输入框样式 */
.input-section input {
    width: 100%;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    box-sizing: border-box; /* 确保padding不撑开宽度 */
    outline: none;
    font-size: 14px;
}

.input-section input:focus {
    border-color: #4a90e2;
    box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6);
}

/* 列表样式 */
.todo-list {
    list-style: none;
    padding: 0;
    margin: 15px 0 0 0;
    border: 1px solid #ddd;
    border-radius: 4px;
}

.todo-list li {
    padding: 10px;
    border-bottom: 1px solid #ddd;
    display: flex;
    align-items: center;
}

.todo-list li:last-child {
    border-bottom: none;
}

.todo-list li input[type="checkbox"] {
    margin-right: 10px;
}

/* 底部区域 */
.footer-section {
    margin-top: 20px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.stats {
    display: flex;
    align-items: center;
    font-size: 14px;
}

.stats input {
    margin-right: 15px;
}

/* 红色清除按钮 */
.clear-btn {
    background-color: #da5a47;
    color: white;
    border: none;
    padding: 8px 15px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
}

.clear-btn:hover {
    background-color: #c04d3c;
}

App.jsx:

javascript 复制代码
import './App.css'
  1. 抽取Header组件;

Header/index.jsx:

javascript 复制代码
import React, {Component} from 'react';
import './index.css'

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

export default Header;

Header/index.css:

javascript 复制代码
/* 输入框样式 */
.header input {
    width: 100%;
    padding: 10px;
    border: 1px solid #ccc;
    border-radius: 4px;
    box-sizing: border-box; /* 确保padding不撑开宽度 */
    outline: none;
    font-size: 14px;
}

.header input:focus {
    border-color: #4a90e2;
    box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6);
}
  1. 抽取 List 和 Item 组件;

List/index.jsx:

javascript 复制代码
import React, {Component} from 'react';
import Item from './components/Item';
import './index.css'

class List extends Component {

    render() {
        return (
            <ul className="todo-list">
                <Item/>
            </ul>
        );
    }
}

export default List;

List/index.css:

javascript 复制代码
/* 列表样式 */
.todo-list {
    list-style: none;
    padding: 0;
    margin: 15px 0 0 0;
    border: 1px solid #ddd;
    border-radius: 4px;
}

.todo-list li {
    padding: 10px;
    border-bottom: 1px solid #ddd;
    display: flex;
    align-items: center;
}

.todo-list li:last-child {
    border-bottom: none;
}

.todo-list li input[type="checkbox"] {
    margin-right: 10px;
}

Item/index.jsx:

javascript 复制代码
import React, {Component} from 'react';
import './index.css'

class Item extends Component {
    render() {
        return (
            <li>
                <input type="checkbox"/>
                <span>xxxxx</span>
            </li>
        );
    }
}

export default Item;

Item/index.css:

javascript 复制代码
.item {
    position: relative;
}

.clear-btn.right {
    position: absolute;
    right: 10px;
}

.item .clear-btn {
    padding: 6px 10px;
    font-size: 12px;
}
  1. 抽取Footer组件;

Footer/index.jsx:

javascript 复制代码
import React, {Component} from 'react';
import './index.css'

class Footer extends Component {
    render() {
        return (
            <div className="footer-section">
                <div className="stats">
                    <input type="checkbox"/>
                    <span>已完成0 / 全部2</span>
                </div>
                <button className="clear-btn">清除已完成任务</button>
            </div>
        );
    }
}

export default Footer;

Footer/index.css:

javascript 复制代码
/* 底部区域 */
.footer {
    margin-top: 20px;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.stats {
    display: flex;
    align-items: center;
    font-size: 14px;
}

.stats input {
    margin-right: 15px;
}

/* 红色清除按钮 */
.clear-btn {
    background-color: #da5a47;
    color: white;
    border: none;
    padding: 8px 15px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
}

.clear-btn:hover {
    background-color: #c04d3c;
}

App.jsx:

javascript 复制代码
import React, {Component} from 'react'
import Header from './components/Header'
import List from './components/List'
import Footer from './components/Footer'
import './App.css'

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

页面效果:

2.3 动态初始化列表

  1. 由于HeaderListFooter组件都需要用到数据,所以我们把数据放到App.jsxstate中,然后在通过props的方式传递给各个子组件。

App.jsx:

javascript 复制代码
import React, {Component} from 'react'
import Header from './components/Header'
import List from './components/List'
import Footer from './components/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
        }]
    }

    render() {
        return (
            <div className="todo-container">
                <Header/>
                <List toDoList="toDoList"/>
                <Footer/>
            </div>
        )
    }
}
  1. 接收props,遍历Item组件;

List/index.jsx:

javascript 复制代码
import React, {Component} from 'react';
import Item from '../Item/index';
import './index.css'

class List extends Component {
    render() {
        const {toDoList} = this.props;
        return (
            <ul className="todo-list">
                {
                    toDoList.map((todo) => {
                        return <Item {...todo} key={todo.id}/>
                    })
                }

            </ul>
        );
    }
}

export default List;
  1. 使用defaultChecked暂时替代checked+onChange

Item/index.jsx:

javascript 复制代码
import React, {Component} from 'react';
import './index.css'

class Item extends Component {
    render() {
        const {name, done} = this.props;
        return (
            <li>
                <input type="checkbox" checked={done}/>
                <span>{name}</span>
            </li>
        );
    }
}

export default Item;

我们可以看到这里使用checked,控制台报错了,提示我们checked要和onChange事件一起使用

这里我们换一种或写法,使用defaultChecked属性来实现这里的勾选效果(现在没有问题,之后的交互会导致一些bug,后面会说)

javascript 复制代码
<li>
	<input type="checkbox" defaultChecked={done}/>
	<span>{name}</span>
</li>
  • defaultChecked:默认是否勾选,以后可以修改;
  • checked:是否勾选,但是不可改,想修改必须结合onChange事件一起使用;

2.4 添加一个todo

  1. 输入框绑定键盘事件,监听回车键;

Header/index.jsx:

javascript 复制代码
import React, {Component} from 'react';
import './index.css'

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

    // 绑定键盘事件
    handleKeyUp = (event) => {
        // 解构赋值获取keyCode,target
        const {target, keyCode} = event;
        // 判断是否是回车按键
        if (keyCode !== 13) {
            return;
        }
        console.log(target.value);
    }
}

export default Header;
  1. 添加addTodo函数,实现子组件向父组件传递件数据;

App.jsx

javascript 复制代码
render() {
	const {toDoList} = this.state;
	return (
		<div className="todo-container">
			{/* 将addTodo传递给header */}
			<Header addTodo={this.addTodo}/>
			<List toDoList={toDoList} />
			<Footer/>
		</div>
	)
}

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

此时我们的handleKeyUp函数需要返回新添加的todo对象。

Header/index.jsx:

javascript 复制代码
// 绑定键盘事件
handleKeyUp = (event) => {
    // 解构赋值获取keyCode,target
    const {target, keyCode} = event;
    // 判断是否是回车按键
    if (keyCode !== 13) {
        return;
    }
    // 准备好一个todo对象
    const {addTodo} = this.props;
    // 将todo对象传递给App
    addTodo({
        id: '4',
        name: target.value,
        done: false
    });
}

但是新todo对象的id这么直接定义有点问题。

  1. 使用nanoid生成唯一id;

我们可以使用npmuuidnanoid来生成唯一id,因为nanoid体积更小,所以我们更推荐。

NanoID 是一种轻量级、安全且 URL 友好的唯一字符串 ID 生成器,被视为 UUID 的现代替代品。它通过 Node.js 安装,默认生成 21 个字符的唯一 ID,广泛用于数据库主键、订单号生成 以及前端(如 Vue/React)项目中生成唯一标识。

  • 安装:
javascript 复制代码
npm i nanoid
  • 基本使用:
javascript 复制代码
import { nanoid } from 'nanoid';
const id = nanoid(); // 生成默认长21位的唯一ID
  • 指定长度:
javascript 复制代码
const shortId = nanoid(10); // 生成指定长度的唯一ID

我们使用nanoid来生成新todo对象的id

javascript 复制代码
import React, {Component} from 'react';
import {nanoid} from 'nanoid';
import './index.css'

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

    // 绑定键盘事件
    handleKeyUp = (event) => {
        // 解构赋值获取keyCode,target
        const {target, keyCode} = event;
        // 判断是否是回车按键
        if (keyCode !== 13) {
            return;
        }
        // 准备好一个todo对象
        const {addTodo} = this.props;
        // 将todo对象传递给App
        addTodo({
            id: nanoid(),
            name: target.value,
            done: false
        });
    }
}

export default Header;
  1. 细节优化(添加的 todo 名字不能为空 以及 清空输入);
javascript 复制代码
// 绑定键盘事件
handleKeyUp = (event) => {
    // 解构赋值获取keyCode,target
    const {target, keyCode} = event;
    // 判断是否是回车按键
    if (keyCode !== 13) {
        return;
    }
    // 添加的 todo名字不能为空
    if (target.value.trim() === '') {
        alert('输入不能为空!')
        return;
    }
    // 准备好一个todo对象
    const {addTodo} = this.props;
    // 将todo对象传递给App
    addTodo({
        id: nanoid(),
        name: target.value,
        done: false
    });
    // 清空输入
    target.value = ''
}

代码写完了,我们添加一个"逛街"的todo,看下页面效果:

2.5 鼠标移入效果(背景色高亮并显示删除按钮)

  1. 添加鼠标移入和移出事件;

Item/index.jsx:

javascript 复制代码
import React, {Component} from 'react';
import './index.css'

class Item extends Component {
    render() {
        const {name, done} = this.props;
        return (
            <li className="item" onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}>
                <input type="checkbox" defaultChecked={done}/>
                <span>{name}</span>
                <button className="clear-btn right">删除</button>
            </li>
        );
    }

    // 鼠标移入移出的回调
    handleMouse = (flag) => {
        return () => {
            console.log(flag);
        }
    }
}

export default Item;
  1. 在状态中定义变量isMouse,作为鼠标移入移出的标识;
javascript 复制代码
import React, {Component} from 'react';
import './index.css'

class Item extends Component {
	// 鼠标移入移出的标识
    state = {
        isMouse: false
    }

    render() {
        const {name, done} = this.props;
        return (
            <li className="item" onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}>
                <input type="checkbox" defaultChecked={done}/>
                <span>{name}</span>
                <button className="clear-btn right">删除</button>
            </li>
        );
    }

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

export default Item;
  1. 根据isMouse标识判断鼠标是否处于移入或移出的状态,并设置样式;
javascript 复制代码
render() {
    const {name, done} = this.props;
    const {isMouse} = this.state;
    return (
        <li className="item" onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}
            style={{background: isMouse ? '#eee' : '#fff'}}>
            <input type="checkbox" defaultChecked={done}/>
            <span>{name}</span>
            <button className="clear-btn right" style={{display: isMouse ? 'block' : 'none'}}>删除</button>
        </li>
    );
}

2.6 勾选或取消勾选某一个todo

  1. 对勾选框添加onChange事件;

Item/index.jsx:

javascript 复制代码
render() {
    const {id, name, done} = this.props;
    const {isMouse} = this.state;
    return (
        <li className="item" onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}
            style={{background: isMouse ? '#eee' : '#fff'}}>
            <input type="checkbox" defaultChecked={done} onChange={this.handleCheck(id)}/>
            <span>{name}</span>
            <button className="clear-btn right" style={{display: isMouse ? 'block' : 'none'}}>删除</button>
        </li>
    );
}

// 绑定勾选事件
handleCheck = (id) => {
    return (event) => {
        console.log(id, event.target.checked)
    }
}
  1. 通知最外层App更新当前todo对象的勾选框情况;

App.jsx:

javascript 复制代码
// updateTodo用于更新一个todo对象
updateTodo = (id, done) => {
    // 获取状态中的todos
    const {toDoList} = this.state;
    // 匹配处理数据
    const newToDoList = toDoList.map(todoObj => {
        if (todoObj.id === id) {
            return {...todoObj, done}
        } else {
            return todoObj;
        }
    })
    this.setState({toDoList: newToDoList});
}
  1. 由于AppItem是祖孙关系,所以我们需要一层层传递updateTodoApp先传递给ListList再传递给Item

App.jsx:

javascript 复制代码
render() {
	const {toDoList} = this.state;
	return (
		<div className="todo-container">
			{/* 将addTodo传递给header */}
            <Header addTodo={this.addTodo}/>
            <List toDoList={toDoList} updateTodo={this.updateTodo}/>
            <Footer/>
		</div>
	)
}

List/index.jsx:

javascript 复制代码
import React, {Component} from 'react';
import Item from '../Item/index';
import './index.css'

class List extends Component {
    render() {
        const {toDoList, updateTodo} = this.props;
        return (
            <ul className="todo-list">
                {
                    toDoList.map((todo) => {
                        return <Item {...todo} key={todo.id} updateTodo={updateTodo}/>
                    })
                }

            </ul>
        );
    }
}

export default List;

Item/index.jsx:

javascript 复制代码
// 绑定勾选事件
handleCheck = (id) => {
    return (event) => {
        const {updateTodo} = this.props;
        updateTodo(id, event.target.checked)
    }
}

总结:状态在哪里,操作状态的方法就在哪里。

2.7 对 props 进行限制

  1. 下载prop-types库;
javascript 复制代码
npm i prop-types
  1. 引入prop-types
javascript 复制代码
import PropTypes from 'prop-types
  1. 对传递的props进行类型和必要性的限制;

App.jsx:

javascript 复制代码
render() {
    const {toDoList} = this.state;
    return (
        <div className="todo-container">
            {/* 将addTodo传递给header */}
            <Header addTodo={this.addTodo}/>
            <List toDoList={toDoList} updateTodo={this.updateTodo}/>
            <Footer/>
        </div>
    )
}

Header/index.jsx:

javascript 复制代码
import PropTypes from 'prop-types'
...
class Header extends Component {
	// 对接收的props进行:类型、必要性的限制
    static propTypes = {
        addTodo: PropTypes.func.isRequired
    }
}

List/index.jsx:

javascript 复制代码
import PropTypes from 'prop-types'
...
class List extends Component {
	// 对接收的props进行:类型、必要性的限制
    static propTypes = {
        toDoList: PropTypes.array.isRequired,
        updateTodo: PropTypes.func.isRequired
    }
}

Item/index.jsx:

javascript 复制代码
import PropTypes from 'prop-types'
...
class List extends Component {
	// 对接收的props进行:类型、必要性的限制
    static propTypes = {
        updateTodo: PropTypes.func.isRequired
    }
}

2.8 删除一个 todo

  1. 添加点击删除按钮的回调函数;

Item/index.jsx:

javascript 复制代码
render() {
    const {id, name, done} = this.props;
    const {isMouse} = this.state;
    return (
        <li className="item" onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}
            style={{background: isMouse ? '#eee' : '#fff'}}>
            <input type="checkbox" defaultChecked={done} onChange={this.handleCheck(id)}/>
            <span>{name}</span>
            <button className="clear-btn right" style={{display: isMouse ? 'block' : 'none'}}
                    onClick={() => this.handleDelete(id)}>删除
            </button>
        </li>
    );
}

...

handleDelete = (id) => {
    console.log(id);
}
  1. 父组件App要有一个删除todo对象的方法,并传递给Item

App.jsx:

javascript 复制代码
render() {
    const {toDoList} = this.state;
    return (
        <div className="todo-container">
            {/* 将addTodo传递给header */}
            <Header addTodo={this.addTodo}/>
            <List toDoList={toDoList} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
            <Footer/>
        </div>
    )
}

...

// deleteTodo用于删除一个todo对象
deleteTodo = (id) => {
    // 获取原来的todos
    const {toDoList} = this.state;
    // 删除指定id的 todo 对象
    const newToDoList = toDoList.filter(todoObj => {
        return todoObj.id !== id;
    })
    // 更新状态
    this.setState({toDoList: newToDoList});
}
  1. 先将deleteTodo传递给List,在通过List再传递给Item,并对props进行限制;

List/index.jsx:

javascript 复制代码
import React, {Component} from 'react';
import Item from '../Item/index';
import PropTypes from 'prop-types'
import './index.css'

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-list">
                {
                    toDoList.map((todo) => {
                        return <Item {...todo} key={todo.id} updateTodo={updateTodo} deleteTodo={deleteTodo}/>
                    })
                }

            </ul>
        );
    }
}

export default List;

注意:delete是一个关键字,用于删除某个对象里的指定属性,比如:

javascript 复制代码
let obj = {a: 1};
delete obj.a;

所以删除todo对象的函数千万不要命名为delete

Item/index.jsx:

javascript 复制代码
// 删除一个todo的回调
handleDelete = (id) => {
    const {deleteTodo} = this.props;
    deleteTodo(id);
}
  1. 添加删除提示;

为了防止用户误删,我们需要在删除操作前添加一个确认删除的提示。

javascript 复制代码
// 删除一个todo的回调
handleDelete = (id) => {
    const {deleteTodo} = this.props;
    deleteTodo(id);
}

2.9 实现底部功能

  1. 计算已完成的个数和总数;

先将App状态中的toDoList作为props传给Footer

App.jsx:

javascript 复制代码
render() {
    const {toDoList} = this.state;
    return (
        <div className="todo-container">
            {/* 将addTodo传递给header */}
            <Header addTodo={this.addTodo}/>
            <List toDoList={toDoList} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
            <Footer toDoList={toDoList}/>
        </div>
    )
}

对数组进行条件统计,如果done值为true,则进行+1累计;

Footer/index.jsx:

javascript 复制代码
import React, {Component} from 'react';
import './index.css'

class Footer extends Component {
    render() {
        const {toDoList} = this.props;
        // 总数
        const total = toDoList.length;
        // 已完成的个数
        const doneCount = toDoList.reduce((prev, cur) => prev + (cur.done ? 1 : 0), 0)
        return (
            <div className="footer-section">
                <div className="stats">
                    <input type="checkbox"/>
                    <span>已完成{doneCount} / 全部{total}</span>
                </div>
                <button className="clear-btn">清除已完成任务</button>
            </div>
        );
    }
}

export default Footer;
  1. 底部全选框☑️

当已完成的个数和总数相等,则勾选全选框;

javascript 复制代码
import React, {Component} from 'react';
import './index.css'

class Footer extends Component {
    render() {
        const {toDoList} = this.props;
        // 总数
        const total = toDoList.length;
        // 已完成的个数
        const doneCount = toDoList.reduce((prev, cur) => prev + (cur.done ? 1 : 0), 0)
        return (
            <div className="footer-section">
                <div className="stats">
                    <input type="checkbox" checked={doneCount === total ? true : false}/>
                    <span>已完成{doneCount} / 全部{total}</span>
                </div>
                <button className="clear-btn">清除已完成任务</button>
            </div>
        );
    }
}

export default Footer;

效果是没有问题的,但是控制台报错了,提示checked属性要和onChange事件一起使用;

添加全选框的onChange事件;

javascript 复制代码
render() {
    const {toDoList} = this.props;
    // 总数
    const total = toDoList.length;
    // 已完成的个数
    const doneCount = toDoList.reduce((prev, cur) => prev + (cur.done ? 1 : 0), 0)
    return (
        <div className="footer-section">
            <div className="stats">
                <input type="checkbox" checked={doneCount === total ? true : false} onChange={this.handleCheckAll}/>
                <span>已完成{doneCount} / 全部{total}</span>
            </div>
            <button className="clear-btn">清除已完成任务</button>
        </div>
    );
}

// 全选chekcbox的回调
handleCheckAll = (event) => {
    console.log(event.target.checked);
}

App声明一个全选/全不选的方法,用于Footer通知App改变全部勾选状态;

App.jsx:

javascript 复制代码
render() {
    const {toDoList} = this.state;
    return (
        <div className="todo-container">
            {/* 将addTodo传递给header */}
            <Header addTodo={this.addTodo}/>
            <List toDoList={toDoList} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
            <Footer toDoList={toDoList} checkAllTodos={this.checkAllTodos}/>
        </div>
    )
}

...

// checkAllTodo用于全选
checkAllTodos = (done) => {
    // 获取原来的todos
    const {toDoList} = this.state;
    // 加工数据
    const newToDoList = toDoList.map(todoObj => {
        return {...todoObj, done};
    })
    // 更新状态
    this.setState({toDoList: newToDoList});
}

Footer/index.jsx:

javascript 复制代码
// 全选chekcbox的回调
handleCheckAll = (event) => {
    const {checkAllTodos} = this.props;
    checkAllTodos(event.target.checked)
}

此时,我们将Item中的checkboxdefaultChecked替换成checked

javascript 复制代码
<input type="checkbox" checked={done} onChange={this.handleCheck(id)}/>

我们还要注意一个特殊情况,当保持全部勾选状态,删除todo对象直至为0时,全选框应该取消勾选:

Footer/index.jsx:

javascript 复制代码
<input type="checkbox" checked={doneCount === total && total !== 0 ? true : false} onChange={this.handleCheckAll}/>
  1. 清除已完成任务

先在清除按钮上绑定点击事件:

javascript 复制代码
<button className="clear-btn" onClick={this.handleClearAllDone}>清除已完成任务</button>
...
// 清除已完成任务的回调
handleClearAllDone = () => {
	// 通知App.jsx更改状态
}

App声明一个过滤掉已完成todo对象的方法,用于Footer通知App改变数据状态;

App.jsx:

javascript 复制代码
<Footer toDoList={toDoList} checkAllTodos={this.checkAllTodos} clearAllDone={this.clearAllDone}/>
...
// clearAllDone用于清除所有已完成的
clearAllDone = () => {
    // 获取原来的todos
    const {toDoList} = this.state;
    // 过滤数据
    const newToDoList = toDoList.filter(todoObj => {
        return !todoObj.done;
    })
    // 更新状态
    this.setState({toDoList: newToDoList});
}

Footer/index.jsx:

javascript 复制代码
// 清除已完成任务的回调
handleClearAllDone = () => {
    this.props.clearAllDone();
}

2.10 总结 TodoList 案例

  1. 拆分组件、实现静态组件,注意:classNamestyle的写法;

  2. 动态初始化列表,如何确定将数据放在哪个组件的state中?

  • 某个组件使用 :放在其自身的state中;
  • 某些组件使用 :放在他们共同的父组件state 中(官方称此操作为:状态提升
  1. 关于父子之间通信:
  • 【父组件】给【子组件】传递数据:通过props传递
  • 【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数;
  1. 注意defaultcheckedchecked的区别,类似的还有:defaultValuevalue
  2. 状态在哪里,操作状态的方法就在哪里!
相关推荐
DFT计算杂谈1 分钟前
AMSET 设置多核并行计算
java·前端·css·html·css3
花椒技术5 分钟前
AI 协同开发落地复盘:1 小时生成首版后,为什么 Review 和修正又花了 2-3 天
前端·人工智能·架构
万少39 分钟前
万少用9个AI工具,帮朋友完成了一个"不可能"的项目
前端
小小小小宇41 分钟前
Vue `import` 为什么可以异步加载
前端
WMYeah1 小时前
【无标题】
前端·rust·抽奖程序·跨平台抽奖程序
Unbelievabletobe1 小时前
免费外汇api的响应时间在不同时段下的波动分析
大数据·开发语言·前端·python
大哥,带带弟弟1 小时前
Grafana 前端嵌入与 JWT 鉴权实战
前端·grafana
小小小小宇1 小时前
前端 V8 引擎垃圾回收机制与内存问题排查
前端
前端老石人1 小时前
CSS 值定义语法
前端·css
sheeta19981 小时前
Vue 前端基础笔记
前端·vue.js·笔记