学习该章节前先学习 webpack、ES6 效果最佳
一、使用 create-react-app 创建 react 应用
1.1 react 脚手架
- xxx 脚手架:用来帮助程序员快速创建一个基于 xxx 库的模板项目
- 包含所有需要的配置(语法检查、jsx 编译、devServer…)
- 下载好了所有相关的依赖
- 可以直接运行一个简单效果
- react 提供了一个用于创建 react 项目的脚手架库:create-react-app
- 项目的整体技术架构为:react + webpack + es6 + eslint
- 使用脚手架开发的项目的特点:模块化,组件化,工程化
1.2 创建项目并启动
-
全局安装:
npm i -g create-react-app
-
切换到想创建项目的目录,使用命令:
create-react-app hello-react
注意:
1)如发现报错
create-react-app不是内部或外部命令,也不是可运行的程序
,去环境变量中确认是否指定了node_global的地址,如未指定,指定后重新打开命令窗口执行create-react-app hello-react
即可2)若命令程序未继续执行,可按回车键让命令继续执行,直至创建项目完成
-
进入项目文件夹:
cd hello-react
-
启动项目:
yarn start
或npm start
1.3 react 脚手架项目结构
public ---- 静态资源文件夹
favicon.icon ------ 网站页签图标
index.html -------- 主页面
logo192.png ------- logo图 大小192*192,用于手机
logo512.png ------- logo图 大小512*512,用于收集
manifest.json ----- 应用加壳的配置文件
robots.txt -------- 爬虫协议文件
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库的支持)
1.4 代码演示
index路径:public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- 开启理想视口,用于做移动端网页的适配 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 显示浏览器页签中的图表 -->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<title>react脚手架</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Welcome 组件,路径:src/components/Welcome/index.jsx
import React, {Component} from 'react';
import './index.css'
export default class Welcome extends Component {
render() {
return (
<h2 className='title'>Wellcome!!</h2>
)
}
}
Welcome 组件css,路径:src/components/Welcome/index.css
.title {
background-color: skyblue;
}
Hello 组件,路径:src/components/Hello/index.jsx
import React,{Component} from "react";
import hello from "./index.module.css"
export default class Hello extends Component {
render() {
return (
<h2 className={hello.title}>
Hello, React!!!
</h2>
)
}
}
Welcome 组件css,路径:src/components/Welcome/index.module.css
.title {
background-color: orange;
}
解释一:
上述代码将 Welcome 和 Hello 组件放到一个单独文件夹下,方便管理;并且组件文件夹第一个单词大写,组件的不再以 js 为扩展名,而以 jsx 为扩展名,证明该文件是组件的,且JSX 执行更快,它是类型安全的,在编译过程中就能发现错误,使用 JSX 编写模板更加简单快速。
解释二:
上述代码中的 Hello 组件的 css 文件使用了 .module.css 的后缀即样式的模板化,这是因为该组件和 Welcome 组件都通过类选择 .title 修改了对应的组件背景样式,如果不使用 .module.css 的后缀,且在使用时通过className={hello.title}
的方式设置,就会导致只显示一种背景色。
解释三:
import React,{Component} from “react”;
其中 {Component} 不是解构赋值,而是因为 React 通过分别暴露,暴露了 Component 即在 react 中有这样的代码 export class Component
App 组件,路径:src/App.jsx
import React,{Component} from "react";
import Hello from "./components/Hello";
import Wellcome from "./components/Wellcome";
export default class App extends Component {
render() {
return (
<div>
<Hello/>
<Wellcome/>
</div>
)
}
}
解释四:
上面引入 Hello 和 Welcome 组件的时候并没有加 index.jsx,而直接写成 ./components/Hello
,因为这种方式默认引入的就是 index名的 js 文件
index.js,路径:src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from './App';
ReactDOM.render(<App/>, document.getElementById('root'));
index.js 的名字不允许修改,修改后无法渲染页面
二、案例 ToDoList (组件的组合使用)
功能:组件化实现此功能
1、显示所有 todo 列表
2、输入文本,点击按钮显示到列表的首位,并清除输入的文本
2.1 功能界面的组件化编码流程(通用)
- 拆分组件:拆分界面,抽取组件
最外层是 APP组件,APP 组件里包含了 Header 组件、List 组件、Footer 组件,List 组件中包含了 Item 组件
-
实现静态组件: 使用组件实现静态页面效果
-
实现动态组件
3.1. 动态显示初始化数据
3.1.1 数据类型
3.1.2 数据名称
3.1.3 保存在哪个组件?
3.2 交互(从绑定事件监听开始)
2.2 代码演示
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App'
ReactDOM.render(<App/>, document.getElementById("root"));
APP.jsx
import React, { Component } from 'react';
import Header from './components/Header';
import Footer from './components/Footer';
import List from './components/List';
import "./App.css";
export default class App extends Component {
//状态在哪里,操作状态的方法就在哪里
//初始化状态
state = {todos: [
{id: '001', name:'吃饭', done: true},
{id: '002', name:'睡觉', done: true},
{id: '003', name:'写代码', done: false},
]}
//addTodo用于添加一个todo,接收的参数是todo对象
addTodo = (todoObj) => {
// 获取 todos
const {todos} = this.state;
// 生成新的todos
const newTodos = [todoObj, ...todos];
// 修改 state
this.setState({todos: newTodos});
}
//updateTodo用于更新一个todo对象
updateTodo = (id, done) => {
// 获取 todos
const {todos} = this.state;
const newTodos = todos.map((todoObj) => {
if(id === todoObj.id) {
return {...todoObj, done}
} else {
return todoObj
}
})
this.setState({todos: newTodos})
}
//deleteTodo用于删除一个todo对象
deleteTodo = (id) => {
const {todos} = this.state;
const newTodos = todos.filter((todoObj) => {
return todoObj.id !== id;
})
this.setState({todos: newTodos})
}
//checkedAll用于全选
checkedAll = (done) => {
const {todos} = this.state;
const newTodos = todos.map((todoObj) => {
return {...todoObj, done}
})
this.setState({todos : newTodos})
}
//clearDone用于清除所有已完成的
clearDone = () => {
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} checkedAll={this.checkedAll} clearDone={this.clearDone}/>
</div>
</div>
)
}
}
APP.css
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
components/Header/index.jsx
import React, { Component } from 'react'
import {nanoid} from 'nanoid'
import {PropTypes} from 'prop-types'
import './index.css'
export default class Header extends Component {
//对接收的props进行:类型、必要性的限制
static propTypes = {
addTodo: PropTypes.func.isRequired
}
//键盘事件的回调
handleKeyUp = (event) => {
const {target, keyCode} = event;
// 回车事件
if(keyCode !== 13) return;
// 输入内容不能为空
if(target.value.trim() === '') {
alert('输入的内容不能为空');
return;
}
// 将输入的内容变成 todoObj
const todoObj = {id: nanoid(), name: target.value, done: false};
// 将输入的内容增加到 todos
this.props.addTodo(todoObj);
// 清空输入框
target.value = '';
}
render() {
return (
<div className="todo-header">
<input onKeyUp={this.handleKeyUp} type="text" placeholder="请输入你的任务名称,按回车键确认"/>
</div>
)
}
}
components/Header/index.css
/*header*/
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}
.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}
components/List/index.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,
deleteTodo: PropTypes.func.isRequired
}
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>
)
}
}
components/List/index.css
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
components/Item/index.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)=>{
this.props.updateTodo(id,event.target.checked)
}
}
//删除一个todo的回调
handleDelete = (id) => {
this.props.deleteTodo(id)
}
// defaultChecked 只在第一次加载时有效
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" checked={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>
)
}
}
components/Item/index.css
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
components/Footer/index.jsx
import React, { Component } from 'react'
import './index.css'
export default class Footer extends Component {
//全选checkbox的回调
handleCheckedAll = (event) => {
this.props.checkedAll(event.target.checked);
}
//清除已完成任务的回调
handleClearDone = () => {
this.props.clearDone();
}
render() {
const {todos} = this.props;
const total = todos.length;
const doneCount = todos.reduce((pre, todoObj) => {
return pre + (todoObj.done ? 1 : 0);
}, 0);
return (
<div className="todo-footer">
<label>
<input type="checkbox" checked={doneCount === total && total !== 0} onChange={this.handleCheckedAll}/>
</label>
<span>
<span>已完成{doneCount}</span> / 全部{total}
</span>
<button className="btn btn-danger" onClick={this.handleClearDone}>清除已完成任务</button>
</div>
)
}
}
components/Footer/index.css
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
2.3 总结
-
拆分组件、实现静态组件,注意:className、style 的写法
-
动态初始化列表,如何确定将数据放在哪个组件的 state 中?
原则一:某个组件使用:放在其自身的 state 中
原则二:某些组件使用:放在他们共同的父组件 state 中(官方称此操作为:状态提升) -
关于父子之间通信:
1)【父组件】给【子组件】传递数据:通过props传递<List todos={todos} updateTodo={this.updateTodo} deleteTodo={this.deleteTodo}/>
这是 App 组件中的一段代码,这里父组件 App 将 todos 、updateTodo 函数和 deleteTodo 函数,传递给子组件 List
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> )
上面这段是 List 组件中的代码,通过 props 接收父组件传递的
todos、updateTodo、deleteTodo
,再将todo、updateTodo、 deleteTodo
传递给子组件 Item 2)【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数
<button onClick={() => this.handleDelete(id)} className="btn btn-danger">删除</button>
handleDelete = (id) => { this.props.deleteTodo(id) }
上面这段代码是 Item 组件中的删除按钮功能的部分代码,这里调用了从父组件传递过来的 deleteTodo 函数将id传递给父组件中的 deleteTodo 函数,执行删除操作
-
注意defaultChecked 和 checked的区别,类似的还有:defaultValue 和 value
defaultChecked 只在第一次指定才起作用,以后不起作用
- 状态在哪里,操作状态的方法就在哪里
这个案例中 todos 在 App 组件中,所以操作 todos 的方法都在 App 组件中