第3章:react应用(基于react脚手架)

213 阅读18分钟

3.1 使用create-react-app创建react应用

3.1.1 初始化react脚手架

概要总结

1、全局安装脚手架:npm i create-react-app -g

2、创建项目命令:create-react-app 项目名

3、启动项目命令:yarn start/npm start

一、全局安装脚手架

npm i create-react-app -g

image.png

二、创建项目命令

create-react-app 项目名

image.png

image.png npm run eject命令:React脚手架也是借助webpack搭建的,webpack最主要的就是它的配置文件,如果开放出来给我们去改,webpack环境很容易受到破坏。React脚手架默认把所有webpack相关的文件都隐藏了,如果执行了eject命令,就会把所有webpack相关的配置都暴露出来了。而且它最后还给了一个提示:如果执行了这个命令,也就是把webpack所有的配置都暴露出来了之后,你不能再把它隐藏起来了。

三、启动项目命令

yarn start

npm start

推荐使用yarn包管理,因为yarn和react都是Facebook出的。

image.png

3.1.2 脚手架文件介绍_public

概要总结

1、public—静态资源文件夹

(1)favicon.icon —— 网站页签图标

(2)index.html —— 主页面

(3)logo192.png —— logo图

(4)logo512.png —— logo图

(5)manifest.json —— 应用加壳的配置文件

(6)robots.txt —— 爬虫协议文件

2、index.html—应用的主页面

3、robots.txt—爬虫规则文件

一、public—静态资源文件夹

image.png

1、favicon.icon —— 网站页签图标

2、index.html —— 主页面

3、logo192.png —— logo图

4、logo512.png —— logo图

5、manifest.json —— 应用加壳的配置文件

6、robots.txt —— 爬虫协议文件

二、index.html

应用的主页面。在React里只有一个html文件,多功能可以拆成多个组件。所以它也叫SPA应用,S是Single单一的意思,P是页面,A是应用,合起来就是单页应用。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <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>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

1、%PUBLIC_URL%

%PUBLIC_URL%是React脚手架的一个关键词的写法,它代表的是public文件夹的路径

2、<meta name="viewport" content="width=device-width, initial-scale=1" />

开启理想视口,用于做移动端网页的适配。

3、<meta name="theme-color" content="#000000" />

用于配置浏览器页签+地址栏的颜色。 image.png <meta name="theme-color" content="red" /> image.png

注意:它仅支持安卓手机浏览器,而且兼容性并不太好。

4、<meta name="description" content="Web site created using create-react-app" />

描述网站信息,在搜索引擎搜索的时候可以查询。

5、<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />

用于指定网页添加到手机主屏幕后的图标。

我们在浏览器打开一个网站,然后把它添加到手机的主屏上。这看起来好像在手机安装了一个应用似的,实际上不是应用而是链接。

image.png

点完"添加到主屏幕"之后,它会弹出要添加到主屏幕的名称和链接,其中左边的图标就受到的控制。

image.png

image.png

注意:它仅支持苹果手机,而且兼容性并不太好。

6、<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

应用加壳时的配置文件。

image.png

我们按照手机的布局来写页面,然后在网页的外面套一个安卓的壳,这个网页摇身一变就变成了一个安卓手机的应用,最终生成一个.apk。本来要开发Android必须学java,开发IOS就必须学OC或者Swift。但如果你学会使用加壳技术,就可以生成一个.apk文件安装在安卓手机上,它打开的是一个壳,壳里面内嵌一个网页。加上安卓的壳就变成安卓应用,加上IOS的壳就变成IOS应用。

manifest.json文件里就是应用的配置信息,例如:应用名、应用图标、访问设备权限等等。

三、robots.txt

这个是爬虫规则文件。当爬虫在爬取页面的时候,你可以规定可以爬取的内容。

3.1.3 脚手架文件介绍_src

概要总结

1、src—源码文件夹

(1)App.css —— App组件的样式

(2)App.js —— App组件

(3)App.test.js —— 用于给App做测试

(4)index.css —— 样式

(5)index.js —— 入口文件

(6)logo.svg —— logo图

(7)reportWebVitals.js —— 页面性能分析文件(需要web-vitals库的支持)

(8)setupTests.js —— 组件单元测试的文件(需要jest-dom库的支持)

一、src—源码文件夹

image.png

1、App.css —— App组件的样式

2、App.js —— App组件

3、App.test.js —— 用于给App做测试

4、index.css —— 样式

5、index.js —— 入口文件

6、logo.svg —— logo图

7、reportWebVitals.js —— 页面性能分析文件(需要web-vitals库的支持)

8、setupTests.js —— 组件单元测试的文件(需要jest-dom库的支持)

二、App.js

React只把一个组件放到容器里,因为ReactDOM.render()并不是一个追加的动作,它会替换掉上一个render操作。而这个组件就是App组件。其它所有的组件都属于App的子组件来处理。

三、App.test.js

这个文件是用于做测试的,而且是专门测试App组件的。这个文件几乎不用,我们直接运行看效果就可以了。

四、index.css

它属于通用的样式。

五、index.js

它是一个入口文件。

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

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

reportWebVitals();

它首先引入了React和ReactDOM两个核心库,然后就把App组件引入进来,挂载到id为root的节点上。

<React.StrictMode>的作用是检查App以及App下的子组件是否存在不合理的代码,例如组件里写了ref="demo",它会发出警告字符串类型的ref准备弃用。

六、reportWebVitals.js

它是用于记录页面上的性能,做一个分析。它里面用了一个库叫做web-vitals来实现页面性能的监测。

const reportWebVitals = onPerfEntry => {
  if (onPerfEntry && onPerfEntry instanceof Function) {
    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
      getCLS(onPerfEntry);
      getFID(onPerfEntry);
      getFCP(onPerfEntry);
      getLCP(onPerfEntry);
      getTTFB(onPerfEntry);
    });
  }
};

export default reportWebVitals;

七、setupTests.js

它是用于做页面的整体测试,也可以做组件测试。它里面也用了一个jest-dom库来支持。

// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

3.1.4 一个简单的hello组件

概要总结

1、重新搭建React目录

2、优化组件代码

一、重新搭建React目录

1、public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>react脚手架</title>
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
  </head>
  <body>
  <div id="root"></div>
  </body>
</html>

2、src/index.js

// 引入react核心库
import React from 'react'
// 引入ReactDOM
import ReactDOM from 'react-dom'
// 引入App组件
import App from './App'

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

3、src/App.js

// 创建"外壳"组件App
import React from 'react' class App extends React.Component {
  render() {
    return (
      <div> hello, react </div>
    )
  }
}
// 暴露App组件
export default App

image.png

二、优化组件代码

1、优化App.js

import React from 'react' class App extends React.Component { ...... }

这里可以优化成以下代码:

import React, {Component} from 'react' class App extends Component { ...... }

注意:import里的{Component}并不是解构赋值,而是react库里既导出了默认的React模块,还导出了多个独立的模块,Component是其中之一。

2、优化export default语句

import React, {Component} from 'react'

export default class App extends Component {
  ......
}

3、把内容封装成组件

Hello.js:

import React, {Component} from "react";
export default class Hello extends Component {
  render() {
    return <h2>Hello,React!</h2>
  }
}

App.js:

import React, {Component} from 'react' import Hello from "./components/Hello"; // 创建并暴露App组件 export default class App extends Component { render() { return (

) } }

4、调整自定义组件目录

由于App.js和index.js属于最顶级的js,因此新建一个目录来管理自定义的组件。

image.png

5、新建并引入组件样式

在组件里通过import方式引入css文件。

image.png

image.png

image.png

6、使用jsx文件格式

因为现在组件是用js文件创建的,这样跟普通的js文件很容易产生混淆,除了组件的js文件名首字母是大写以外就没其它区别。此时我们可以把组件的js改用jsx文件格式:

image.png

7、组件统一命名为index.jsx

在React脚手架当中,在引入一个组件的时候,如果文件名就是index.js或者index.jsx,那么可以省略:

image.png

3.2 组件的组合使用-TodoList

3.2.1 TodoList案例_静态组件

概要总结

1、TodoList案例需求

2、编写TodoList静态组件

需求:

1、输入任务名称按回车,在列表最上方添加一项,并更新底部全部的总数

2、悬浮每一项会显示高亮并显示删除按钮

3、点击删除弹出确认弹窗,确定后删除对应项,更新底部全部的总数

image.png

4、勾选每一项之后,底部导航出现"清除已完成任务"按钮,底部会更新已完成数量

image.png

5、点击"清除已完成任务"按钮,删除掉勾选项,更新底部全部的总数

一、拆分组件

image.png

二、编写静态组件

在拆分组件之前,可以把所有的代码全部写在App.js和App.css,先看效果,然后再一步一步建立组件拆分。

1、Header组件

import React, {Component} from 'react';
import './index.css'

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

2、List组件

import React, {Component} from 'react';
import Item from '../Item'
import './index.css'

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

3、Item组件

import React, {Component} from 'react';
import './index.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>
    );
  }
}

4、Footer组件

import React, {Component} from 'react';
import './index.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>
    );
  }
}

5、App.js统一引入

// 创建"外壳"组件App
import React, {Component} from 'react'
import './App.css'
import Header from "./components/Header";
import List from "./components/List";
import Footer from "./components/Footer";

// 创建并暴露App组件
export default class App extends Component {
  render() {
    return (
      <div className="todo-container">
        <div className="todo-wrap">
          <Header/>
          <List/>
          <Footer/>
        </div>
      </div>
    )
  }
}

image.png

3.2.2 TodoList案例_动态初始化列表

概要总结

1、初始化数据状态

2、父子组件传参

一、初始化状态

按理说,任务列表todos的状态应该定义在List组件里,但由于Header组件也需要操作这个todos状态,而且Header组件与List组件属于兄弟组件,目前来说还不能直接通信,需要借助于它们的父组件帮它们通信,因此任务列表todos的状态先定义在父组件App.js里,后续可以通过消息订阅发布的方式进行优化。

App.js:

// 创建"外壳"组件App
import React, {Component} from 'react'
import './App.css'
import Header from "./components/Header";
import List from "./components/List";
import Footer from "./components/Footer";

// 创建并暴露App组件
export default class App extends Component {
  // 初始化状态
  state = {
    todos: [
      {id: '001', name: '吃饭', done: true},
      {id: '002', name: '睡觉', done: true},
      {id: '003', name: '打代码', done: false}
    ]
  }

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

二、父子组件传参

1、App父组件传参给List子组件

在App父组件定义好状态之后,传给List组件接收。

App.js:

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

List.jsx:

import React, {Component} from 'react';
import Item from '../Item'
import './index.css'

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

image.png

2、List父组件传参给Item子组件

目前List组件已经通过接参的方式渲染出任务列表,而列表的每一项信息则需要通过传参给Item的形式继续展示:

注意:多个参数可以使用...扩展运算符一次性传参,例如{...todo}

List.jsx:

import React, {Component} from 'react';
import Item from '../Item'
import './index.css'

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

Item.jsx:

import React, {Component} from 'react';
import './index.css'

export default class Item extends Component {
  render() {
    const {name, done} = this.props
    return (
      <li>
        <label>
          <input type="checkbox" checked={done}/>
          <span>{name}</span>
        </label>
        <button className="btn btn-danger" style={{display: 'none'}}>删除</button>
      </li>
    );
  }
}

image.png

三、解决checkbox警告信息

如果使用checked来控制默认是否选中,那么它会发出警告:你提供了一个checked属性却没有提供onChange处理函数,所以它变成了一个只读属性。

image.png

解决方案:把checked属性换成defaultChecked属性即可。

Item.jsx

import React, {Component} from 'react';
import './index.css'

export default class Item extends Component {
  render() {
    const {name, done} = this.props
    return (
      <li>
        <label>
          <input type="checkbox" defaultChecked={done}/>
          <span>{name}</span>
        </label>
        <button className="btn btn-danger" style={{display: 'none'}}>删除</button>
      </li>
    );
  }
}

3.2.3 TodoList案例_添加todo

概要总结

1、头部输入框绑定回车事件

2、子给父组件传参

3、使用nanoid库获取唯一id

一、头部输入框绑定回车事件

Header.jsx:

import React, {Component} from 'react';
import './index.css'

export default class Header extends Component {
  handleKeyUp = (event) => {
    if (event.keyCode === 13) {
      console.log(event.target.value)
    }
  }

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

image.png

二、子给父组件传参

目前Header组件属于App组件的子组件,现在Header需要把任务项传递给App组件,这里就涉及到子组件传参给父组件的功能。

父组件给子组件传参,直接通过props即可实现。而子给父传参,可以通过父给子传一个函数参数,在子组件里执行该函数的时候,父组件就可以接收它的值,这样就实现了子传父的功能。

父组件App.js:给子组件Header传递addTodo参数

export default class App extends Component {
  // 初始化状态
  state = {
    ......
  }
  
  // addTodo用于添加一个todo,接收的参数是todo对象
  addTodo = todoObj => {
    // 获取原todos
    const {todos} = this.state
    // 追加一个todo
    const newTodos = [todoObj, ...todos]
    // 更新状态
    this.setState({todos: newTodos})
  }

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

子组件Header.jsx:执行父组件App传递的addTodo函数,例如this.props.addTodo()

import React, {Component} from 'react';
import './index.css'

export default class Header extends Component {
  handleKeyUp = (event) => {
    // 解构赋值获取keyCode,target
    const {keyCode, target} = event;
    // 判断是否是回车按键
    if (keyCode === 13) {
      const todoObj = {id: 1, name: target.value, done: false}
      this.props.addTodo(todoObj)
    }
  }

  render() {
    ......
  }
}

三、使用nanoid库获取唯一id

1、安装nanoid库

npm i nanoid

image.png

2、引入并使用nanoid库

它这个库会暴露一个nanoid出来,这个nanoid是一个函数,每次调用它都会生成一个字符串,而且这个字符串它能保证是全球唯一的。

import {nanoid} from 'nanoid'

console.log(nanoid())
console.log(nanoid())
console.log(nanoid())

image.png

在Header.jsx生成一个任务项的时候,id可以用nanoid来生成唯一值:

import React, {Component} from 'react';
import {nanoid} from 'nanoid'
import './index.css'

export default class Header extends Component {
  handleKeyUp = (event) => {
    // 解构赋值获取keyCode,target
    const {keyCode, target} = event;
    // 判断是否是回车按键
    if (keyCode === 13) {
      const todoObj = {id: nanoid(), name: target.value, done: false}
      this.props.addTodo(todoObj)
    }
  }

  render() {
    ......
  }
}

image.png

四、优化功能

1、如果输入框为空则不能添加

if (target.value.trim() === '') return

Header.jsx:

handleKeyUp = (event) => {
  // 解构赋值获取keyCode,target
  const {keyCode, target} = event
  // 判断是否是回车按键
  if (keyCode === 13) {
    // 添加的todo名字不能为空
    if (target.value.trim() === '') {
      alert('输入不能为空')
      return
    }

    // 准备好一个todo对象
    const todoObj = {id: nanoid(), name: target.value, done: false}
    // 将todoObj传递给App
    this.props.addTodo(todoObj)
  }
}

2、添加项之后清空输入框内容 target.value = ''

handleKeyUp = (event) => {
  // 解构赋值获取keyCode,target
  const {keyCode, target} = event
  // 判断是否是回车按键
  if (keyCode === 13) {
    // 添加的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 = ''
  }
}

3.2.4 TodoList案例_鼠标移入效果

概要总结

1、绑定鼠标移入移出事件

2、添加移入移出背景色

3、动态控制删除按钮显示隐藏

一、绑定鼠标移入移出事件

在state定义一个移入移出的标识符mouse表示是否移入,通过绑定mouseEnter和mouseLeave事件控制标识符变量。

item.jsx

import React, {Component} from 'react';
import './index.css'

export default class Item extends Component {
  state = {
    mouse: false
  }
  handleMouse = (flag) => {
    return () => {
      this.setState({mouse: flag})
    }
  }
  render() {
    const {name, done} = this.props
    return (
      <li 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: 'none'}}>删除</button>
      </li>
    );
  }
}

二、添加移入移出背景色

在每一项添加style,动态根据mouse状态设置对应的背景色。

item.jsx:

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

三、动态控制删除按钮显示隐藏

item.jsx:

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

image.png

3.2.5 TodoList案例_更新todo状态

概要总结

1、绑定复选框事件

2、App组件添加更新todo方法

3、父子组件事件传递

一、绑定复选框事件

在勾选/取消勾选每一项任务的时候,我们需要拿到它的id以及勾选状态,那么可以给checkbox绑定一个onChange事件。

item.jsx:

import React, {Component} from 'react';
import './index.css'

export default class Item extends Component {
  state = {
    mouse: false // 标识鼠标移入、移出
  }

  // 鼠标移入、移出的回调
  handleMouse = (flag) => {
    return () => {
      this.setState({mouse: flag})
    }
  }
  // 勾选、取消勾选某一个todo的回调
  handleCheck = (id) => {
    return (event) => {
      console.log(id, event.target.checked)
    }
  }
  render() {
    const {id, name, done} = this.props
    const {mouse} = this.state
    return (
      <li style={{backgroundColor: mouse ? '#ddd' : 'white'}} onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}>
        <label>
          <input type="checkbox" defaultChecked={done} onChange={this.handleCheck(id)}/>
          <span>{name}</span>
        </label>
        <button className="btn btn-danger" style={{display: mouse ? 'block' : 'none'}}>删除  </button>
      </li>
    );
  }
}

image.png

二、App组件添加更新todo方法

拿到具体的任务项对应的id和是否勾选的状态之后,把它们传递给最上层的App组件,让App根据id获取对应的任务项然后更新状态。

App.js:

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

三、父子组件事件传递

由于Item与App组件是祖孙关系,因此在App定义了更新todo对象的事件之后,要先传给它的子组件List,List组件接收之后原封不动的继续传给子组件Item,最后让Item组件调用这个更新todo对象的方法来完成数据状态更新:

App.js:

// updateTodo用于更新一个todo对象
updateTodo = (id, done) => {
  // 获取状态中的todos
  const {todos} = this.state
  // 匹配处理数据

  const newTodos = todos.map(todoObj => {
    if (todoObj.id === id) return {...todoObj, done}
  })
  this.setState({todos: newTodos})
}

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

List.jsx:

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

Item.jsx:

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

image.png

3.2.6 TodoList案例_对props进行限制

概要总结

1、安装并引入propTypes

2、给各个子组件添加props类型限制

一、安装并引入propTypes

1、安装prop-types依赖

npm i prop-types

2、引入prop-types

import PropTypes from 'prop-types'

二、给各个子组件添加props类型限制

拿到具体的任务项对应的id和是否勾选的状态之后,把它们传递给最上层的App组件,让App根据id获取对应的任务项然后更新状态。

Header.jsx:

import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {nanoid} from 'nanoid'
import './index.css'

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

List.jsx:

import React, {Component} from 'react';
import PropTypes from "prop-types";
import Item from '../Item'
import './index.css'

export default class List extends Component {

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

3.2.7 TodoList案例_删除一个todo

概要总结

1、绑定删除按钮点击事件

2、App组件添加删除todo方法

3、父子组件事件传递

4、添加删除确认框

一、绑定删除按钮点击事件

在点击每一项任务的删除按钮的时候,我们只需要拿到它的id即可,那么可以给button绑定一个onClick事件。

Item.jsx:

import React, {Component} from 'react';
import './index.css'

export default class Item extends Component {
  state = {
    mouse: false // 标识鼠标移入、移出
  }
  ......
  // 删除一个todo的回调
  handleDelete = (id) => {
    console.log('通知App删除' + id)
  }
  render() {
    const {id, name, done} = this.props
    const {mouse} = this.state
    return (
      <li style={{backgroundColor: mouse ? '#ddd' : 'white'}} onMouseEnter={this.handleMouse(true)} onMouseLeave={this.handleMouse(false)}>
        <label>
          <input type="checkbox" defaultChecked={done} onChange={this.handleCheck(id)}/>
          <span>{name}</span>
        </label>
        <button onClick={() => this.handleDelete(id)} className="btn btn-danger" style={{display: mouse ? 'block' : 'none'}}>删除</button>
      </li>
    );
  }
}

image.png

二、App组件添加更新todo方法

拿到具体的任务项对应的id之后,把它们传递给最上层的App组件,让App根据id删除对应的任务项,然后更新状态。

App.js:

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

三、父子组件事件传递

由于Item与App组件是祖孙关系,因此在App定义了删除todo的事件之后,要先传给它的子组件List,List组件接收之后原封不动的继续传给子组件Item,最后让Item组件调用这个删除todo对象的方法来完成数据状态更新:

App.js:

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

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

List.jsx:

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

Item.jsx:

handleDelete = (id) => {
  this.props.deleteTodo(id)
}

image.png

image.png

四、添加删除确认框

使用原生的window.confirm即可实现确认框。

Item.jsx:

// 删除一个todo的回调
handleDelete = (id) => {
  if (window.confirm('确认删除吗?')) {
    this.props.deleteTodo(id)
  }
}

image.png

3.2.8 TodoList案例_实现底部功能

概要总结

1、计算已完成任务和总任务数

2、实现全选功能

3、实现清除已完成任务功能

一、计算已完成任务和总任务数

对于总任务数那就是任务列表的长度,而已完成任务可以使用reduce根据done为true而进行统计。

Footer.jsx:

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

image.png

二、实现全选功能

1、全选框关联各个复选框

当每一项复选框都选中的时候,全选框也随之而选中。这个只需要在全选框添加一个checked属性,如果已完成任务的数量等于总数,那就为true,否则为false。

Footer.jsx:

render() {
  const {todos} = this.props
  // 已完成的个数
  const doneCount = todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0)
  // 总数
  const total = todos.length
  return (
    <div className="todo-footer">
      <label>
        <input type="checkbox" checked={doneCount === total}/>
      </label>
      <span>
        <span>已完成{doneCount}</span> / 全部{total}
      </span>
      <button className="btn btn-danger">清除已完成任务</button>
    </div>
  );
}

2、绑定全选框的onChange事件

当选中全选框的时候,所有项也随之而选中,反之则全部取消选中。全选功能不需要获取具体每一项的id,只需要把所有数据的done根据全选框的值来设置即可。

App.js:

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

Footer.jsx:

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

image.png

image.png

3、优化全选功能

因为全选框是根据已完成列表的长度与任务列表数是否相等来做判断,当没有任务项的时候,也就是任务总数为0的时候,那么全选框应该是不勾选状态。

Footer.jsx:

render() {
  const {todos} = this.props // 已完成的个数
  const doneCount = todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0) // 总数
  const total = todos.length
  return (
    <div className="todo-footer">
      <label>
        <input type="checkbox" onChange={this.handleAllCheck} checked={doneCount === total && total !== 0}/>
      </label>
      <span>
        <span>已完成{doneCount}</span> / 全部{total}
      </span>
      <button className="btn btn-danger">清除已完成任务</button>
    </div>
  );
}

三、实现清除已完成任务功能

在App组件定义好清除每一项done为true的任务项方法,然后传给Footer组件调用。

App.js:

// clearAllDone用于清除所有已完成的
clearAllDone = () => {
  // 获取原来的todos
  const {todos} = this.state
  // 过滤数据
  const newTodos = todos.filter((todoObj) => { return !todoObj.done })
  // 更新状态
  this.setState({todos: newTodos})
}
render() {
  const {todos} = this.state
  return (
    <div className="todo-container">
      <div className="todo-wrap">
        <Header addTodo={this.addTodo}/>
        <List todos={todos} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
        <Footer todos={todos} checkAllTodo={this.checkAllTodo} clearAllDone={this.clearAllDone}/>
      </div>
    </div>
  )
}

Footer.jsx:

// 清除已完成任务的回调
handleClearAllDone = () => {
  this.props.clearAllDone()
}
render() {
  const {todos} = this.props // 已完成的个数
  const doneCount = todos.reduce((pre, todo) => pre + (todo.done ? 1 : 0), 0) // 总数
  const total = todos.length
  return (
    <div className="todo-footer">
      <label>
        <input type="checkbox" onChange={this.handleAllCheck} checked={doneCount === total && total !== 0}/>
      </label>
      <span>
        <span>已完成{doneCount}</span> / 全部{total}
      </span>
      <button onClick={this.handleClearAllDone} className="btn btn-danger">清除已完成任务</button>
    </div>
  );
}

image.png

image.png

3.2.9 总结TodoList案例

一、TodoList案例相关知识点

1、拆分组件、实现静态组件,注意:className、style的写法

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

——某个组件使用:放在自身的state中

——某些组件使用:放在他们公共的父组件state中(官方称此操作为:状态提升)

3、关于父子之间通信:

(1)【父组件】给【子组件】传递数据:通过props传递

(2)【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数

4、注意defaultChecked和checked的区别,类似的还有:defaultValue和value

5、状态在哪里,操作状态的方法就在哪里

3.3 代码地址

gitee.com/huang_jing_…