【第三章-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. 状态在哪里,操作状态的方法就在哪里!
相关推荐
空中海2 小时前
第一章:Vue 基础与模板语法
前端·javascript·vue.js
每天吃饭的羊2 小时前
水平,垂直居中
前端·javascript·html
倾颜2 小时前
React 19 源码怎么读:createRoot 和 root.render 到底做了什么?
react.js
鼎道开发者联盟2 小时前
鼎享会 | OpenClaw Control UI 前端架构全解析:自研 UI 对接 Server 实操指南
前端·ui·架构·openclaw·control ui
尘世中一位迷途小书童2 小时前
一套完整的给予ceium封装的组件库,可满足企业级开发
前端
Z_Wonderful2 小时前
微前端:Webpack 配置 vs Vite 配置 超清晰对比
前端·webpack·node.js
码云数智-园园2 小时前
HTTPS是如何工作的?从HTTP到HTTPS的加密演进
前端
隔窗听雨眠3 小时前
HTML头部元信息避坑指南
前端·html
Gauss松鼠会3 小时前
【openGauss】openGauss 磁盘引擎之 ustore
java·服务器·开发语言·前端·数据库·经验分享·gaussdb