05-React组件的组合使用
1.TodoList案例
需求:TodoList组件化实现此功能
- 显示所有todo列表
- 输入文本, 点击按钮显示到列表的首位, 并清除输入的文本
1).实现:
-
完成TodoList组件的静态页面以及拆解组件
-
动态初始化列表
//App.jsx export default class App extends Component { // 初始化状态 state = { todos: [ { id: '001', name: '吃饭', done: true }, { id: '002', name: '睡觉', done: true }, { id: '003', name: '敲代码', done: false }, { id: '004', name: '逛街', done: true }, ] } render() { const { todos } = this.state return ( <div className="todo-container"> <div className="todo-wrap"> <Header/> <List todos={todos}/> <Footer/> </div> </div> ) } }
//List.jsx export default class List extends Component { render() { // 接收来自App父组件传递的数据 const { todos } = this.props return ( <ul className="todo-main"> {todos.map(todo => { return <Item key={todo.id} {...todo}/> })} </ul> ) } }
//Item.jsx export default class Item extends Component { render() { // 接收来自List父组件传递的数据 const { id, name, done } = this.props return ( <li> <label> <input type="checkbox" checked={done}/> <span>{name}</span> </label> <button>删除</button> </li> ) } }
-
完成添加“任务”的功能(父子组件之间传值)
在父组件中创建一个添加数据的方法
addTodo
,然后将方法传递给子组件,当子组件向该方法传递了参数后,父组件便可以接收到子组件传递的参数//App.jsx export default class App extends Component { // 初始化状态 state = { todos: [ { id: '001', name: '吃饭', done: true }, { id: '002', name: '睡觉', done: true }, { id: '003', name: '敲代码', done: false }, { id: '004', name: '逛街', done: true }, ] } //addTodo用于添加一个todo,接收的参数是todo对象 addTodo = (todoObj) => { console.log('APP:' + data); // 获取原数组 const { todos } = this.state // 追加一个新数组 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
创建一个点击回车键回调的方法handlerKeyUp
,在该方法中向从父组件接收到的方法addTodo
中传递参数,将新添加的数据反馈给父组件//Header.jsx export default class Header extends Component { // 键盘事件的回调 handlerKeyUp = (event) => { // keyCode 触发事件的按键码 // 解构赋值keyCode, target const { keyCode, target } = event // 判断是否是回车按键 if (keyCode !== 13) return // 添加的todo名字不能为空 if (target.value.trim() == '') { alert('输入不能为空') return } // 准备一个新的todo对象 const todoObj = { // 使用nanoid库创建一个不重复的ID id: nanoid(), name: target.value, done: false } this.props.addTodo(todoObj); // 清空输入 target.value=''; } render() { return ( <div className="todo-header"> <input onKeyUp={this.handlerKeyUp} type="text" placeholder="请输入你的任务名称,按回车键确认" /> </div> ) } }
-
完成删除"任务"的功能(祖孙组件之间的传值)
在祖组件中创建一个删除数据的方法
deleteTodo
,然后将方法传递给子组件,再由子组件传递给孙组件,当孙组件向该方法传递了参数后,祖组件便可以接收到子组件传递的参数//App.jsx export default class App extends Component { // 初始化状态 state = { todos: [ { id: '001', name: '吃饭', done: true }, { id: '002', name: '睡觉', done: true }, { id: '003', name: '敲代码', done: false }, { id: '004', name: '逛街', done: true }, ] } // deleteTodo用于删除一个todo对象 deleteTodo = (id) => { const { todos } = this.state // 删除指定id的todo对象 const newTodos = todos.filter(todoObj => { return todoObj.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} deleteTodo={this.deleteTodo}/> <Footer/> </div> </div> ) } }
List
组件接收从父组件传递来的删除数据的方法deleteTodo
,再将deleteTodo
方法传递给自己的子组件Item
//List.jsx export default class List extends Component { render() { const { todos, updateTodo} = this.props return ( <ul className="todo-main"> {todos.map(todo => { return <Item key={todo.id} {...todo} deleteTodo={deleteTodo} /> })} </ul> ) } }
Item
组件创建一个删除“任务”的回调方法handleDelete
,然后在该方法中调用deleteTodof
方法,将需要删除的数据id
传递给祖组件App
//Item.jsx export default class Item extends Component { // 删除一个todo的回调 handleDelete = (id) => { // console.log('通知App删除'+id); if (window.confirm('确定删除吗?')) { this.props.deleteTodo(id) } } render() { const { id, name, done } = this.props const { mouse } = this.state return ( <li style={{ backgroundColor: mouse ? '#ddd' : 'white' }} onMouseLeave={this.handleMouse(false)} onMouseEnter={this.handleMouse(true)}> <label> <input type="checkbox" checked={done} onChange={this.handleChange(id)} /> <span>{name}</span> </label> <button onClick={() => this.handleDelete(id)} className="btn btn-danger" style={{ display: mouse ? 'block' : 'none' }}>删除</button> </li> ) } }
-
为每个
Item
组件添加鼠标移入高亮且删除按钮显现、鼠标移出无高亮显示且删除按钮隐藏的效果//Item.jsx export default class Item extends Component { state = { mouse: false //标识鼠标移入、移出 } //鼠标移入、移出的回调 handleMouse = (flag) => { return () => { this.setState({ mouse: flag }) } } // 删除一个todo的回调 handleDelete = (id) => { if (window.confirm('确定删除吗?')) { this.props.deleteTodo(id) } } render() { const { id, name, done } = this.props const { mouse } = this.state return ( <li style={{ backgroundColor: mouse ? '#ddd' : 'white' }} onMouseLeave={this.handleMouse(false)} onMouseEnter={this.handleMouse(true)}> <label> <input type="checkbox" checked={done}/> <span>{name}</span> </label> <button onClick={() => this.handleDelete(id)} className="btn btn-danger" style={{ display: mouse ? 'block' : 'none' }}>删除</button> </li> ) } }
-
修改
Item
组件勾选框的状态和修改数据(祖孙组件之间的传值)在祖组件中创建一个更新数据的方法
updateTodo
,然后将方法传递给子组件,再由子组件传递给孙组件,当孙组件向该方法传递了参数后,祖组件便可以接收到子组件传递的参数//App.jsx export default class App extends Component { // 初始化状态 state = { todos: [ { id: '001', name: '吃饭', done: true }, { id: '002', name: '睡觉', done: true }, { id: '003', name: '敲代码', done: false }, { id: '004', name: '逛街', done: true }, ] } // updateTodo用于更新一个todo对象 updateTodo = (id, done) => { // 获取状态中的todos const { todos } = this.state // 匹配处理数据 const newTodos = todos.map(todoObj => { if (todoObj.id === id) return { ...todoObj, done } else return todoObj }) 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
组件接收从父组件传递来的删除数据的方法updateTodo
,再将updateTodo
方法传递给自己的子组件Item
//List.jsx export default class List extends Component { 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
组件创建一个更新数据的回调方法handleDelete
,然后在该方法中调用updateTodo
方法,将需要更新的数据id
传递给祖组件App
export default class Item extends Component { state = { mouse: false //标识鼠标移入、移出 } //鼠标移入、移出的回调 handleMouse = (flag) => { return () => { this.setState({ mouse: flag }) // console.log(flag); } } //勾选、取消勾选某一个todo的回调 handleChange = (id) => { return (event) => { // console.log(id,event.target.checked); this.props.updateTodo(id, event.target.checked); } } // 删除一个todo的回调 handleDelete = (id) => { // console.log('通知App删除'+id); if (window.confirm('确定删除吗?')) { this.props.deleteTodo(id) } } render() { const { id, name, done } = this.props const { mouse } = this.state return ( <li style={{ backgroundColor: mouse ? '#ddd' : 'white' }} onMouseLeave={this.handleMouse(false)} onMouseEnter={this.handleMouse(true)}> <label> <input type="checkbox" checked={done} onChange={this.handleChange(id)} /> <span>{name}</span> </label> <button onClick={() => this.handleDelete(id)} className="btn btn-danger" style={{ display: mouse ? 'block' : 'none' }}>删除</button> </li> ) } }
-
完成底部组件全选修改数据的功能
//APP.jsx export default class App extends Component { // 初始化状态 state = { todos: [ { id: '001', name: '吃饭', done: true }, { id: '002', name: '睡觉', done: true }, { id: '003', name: '敲代码', done: false }, { id: '004', name: '逛街', done: true }, ] } // checkAllTodo用于全选 checkAllTodo=(done)=>{ // 获取原来的todos const {todos}=this.state // 处理数据 const newTodos=todos.map(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}/> </div> </div> ) } }
//Footer.jsx export default class Footer extends Component { handleChange=(event)=>{ this.props.checkAllTodo(event.target.checked) } 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.handleChange} checked={doneCount===total&&total!=0?true:false}/> </label> <span> <span>已完成{doneCount}</span> / 全部{total} </span> <button className="btn btn-danger">清除已完成任务</button> </div> ) } }
-
完成“清楚已完成任务”按钮的功能
export default class App extends Component { // 初始化状态 state = { todos: [ { id: '001', name: '吃饭', done: true }, { id: '002', name: '睡觉', done: true }, { id: '003', name: '敲代码', done: false }, { id: '004', name: '逛街', done: true }, ] } // 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> ) } }
export default class Footer extends Component { 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.handleChange} checked={doneCount===total&&total!=0?true:false}/> </label> <span> <span>已完成{doneCount}</span> / 全部{total} </span> <button onClick={this.handleClearAllDone} className="btn btn-danger">清除已完成任务</button> </div> ) } }
2).总结:
todoList案例相关知识点:
- 拆分组件、实现静态组件,注意:className、style的写法
- 动态初始化列表,如何确定将数据放在哪个组件的state中?
- 某个组件使用:放在其自身的state中
- 某些组件使用:放在他们共同的父组件state中(官方称此操作为:状态提升)
- 关于父子之间通信:
- 【父组件】给【子组件】传递数据:通过props传递
- 【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数
- 注意defaultChecked和checked的区别,类似的还有:defaultValue和value
- 状态在哪里,操作状态的方法就在哪里
2.GitHub搜索案例
效果:
1).实现:
a.使用axios发送请求
-
搭建组件静态效果
//List.jsx export default class List extends Component { render() { return ( <div className="row"> <div className="card"> <a href="" target="_blank"> <img src=""/> </a> <p className="card-text">{user.login}</p> </div> </div> ) } }
//Search.jsx export default class Search extends Component { render() { return ( <section className="jumbotron"> <h3 className="jumbotron-heading">搜索Github用户</h3> <div> <inputtype="text" placeholder="输入关键词点击搜索" /> <button>搜索</button> </div> </section> ) } }
-
配置脚手架代理(配置代理)
const { createProxyMiddleware } = require('http-proxy-middleware'); module.exports = function (app) { app.use( createProxyMiddleware('/api1', { target: 'http://localhost:5000', secure: false, // changeOrigin: true, pathRewrite: { '^/api1': '', }, }) ) }
-
完成搜索框的搜索和发送请求的功能
-
收集
Search
组件输入框的内容export default class Search extends Component { Search = () => { // 连续解构赋值+重命名 const { KeyWordElement: { value: keyWord } } = this; console.log(keyWord); } render() { return ( <section className="jumbotron"> <h3 className="jumbotron-heading">搜索Github用户</h3> <div> <input ref={c => this.KeyWordElement = c} type="text" placeholder="输入关键词点击搜索" /> <button onClick={this.Search}>搜索</button> </div> </section> ) } }
-
发送请求
export default class Search extends Component { Search = () => { const { KeyWordElement: { value: keyWord } } = this; // 发送请求 axios.get(`/api1/search/users2?q=${keyWord}`).then( response => { console.log(response.data.items); }, error => { console.log('失败了', error); } ) } render() { return ( <section className="jumbotron"> <h3 className="jumbotron-heading">搜索Github用户</h3> <div> <input ref={c => this.KeyWordElement = c} type="text" placeholder="输入关键词点击搜索" /> <button onClick={this.Search}>搜索</button> </div> </section> ) } }
-
-
动态初始化组件的状态
//App.jsx export default class App extends Component { // 初始化状态 state={ users:[],//users初始值为数组 isFirst:true,//是否为第一次打开页面 isLoading:false,//标识是否处于加载中 err:""// 存储请求相关的错误信息 } render() { const {users}=this.state return ( <div className="container"> <Search/> <List {...this.state}/> </div> ) } }
//List.jsx export default class List extends Component { render() { const { users,isFirst,isLoading,err } = this.props return ( <div className="row"> { //使用三元运算符动态显示组件的内容 isFirst?<h3>欢迎使用,输入关键字,随后点击搜索</h3>: isLoading?<h3>Loading......</h3>: err?<h3 style={{color:'red'}}>{err}</h3>: users.map(user => { return ( <div className="card" key={user.id}> <a href={user.html_url} target="_blank"> <img src={user.avatar_url} style={{ width: '100px' }} /> </a> <p className="card-text">{user.login}</p> </div> ) }) } </div> ) } }
-
发起请求并将数据显示到组件上
//App.jsx export default class App extends Component { // 初始化状态 state={ users:[],//users初始值为数组 isFirst:true,//是否为第一次打开页面 isLoading:false,//标识是否处于加载中 err:""// 存储请求相关的错误信息 } // 更新App的state updateAppState=(stateObj)=>{ this.setState(stateObj) } render() { const {users}=this.state return ( <div className="container"> <Search updateAppState={this.updateAppState}/> <List {...this.state}/> </div> ) } }
//Search.jsx export default class Search extends Component { Search = () => { // 连续解构赋值+重命名 const { KeyWordElement: { value: keyWord } } = this; // 发送请求前通知App更新状态 this.props.updateAppState({ isFirst:false,isLoading:true,}) // 发送请求 axios.get(`/api1/search/users2?q=${keyWord}`).then( response => { //请求成功后通知App更新状态 this.props.updateAppState({isLoading:false,users:response.data.items}) }, error => { console.log('失败了', error); // 请求成功后通知App更新状态 this.props.updateAppState({isLoading:false,err:error.message}) } ) } render() { return ( <section className="jumbotron"> <h3 className="jumbotron-heading">搜索Github用户</h3> <div> <input ref={c => this.KeyWordElement = c} type="text" placeholder="输入关键词点击搜索" /> <button onClick={this.Search}>搜索</button> </div> </section> ) } }
b.使用PubSub实现(兄弟组件之间传值)
消息订阅-发布机制
-
工具库:
PubSubJS
-
下载:
npm install pubsub-js --save
-
使用:
-
import PubSub from 'pubsub-js' //引入
-
PubSub.subscribe('delete', function(data){ }); //订阅
-
PubSub.publish('delete', data) //发布消息
-
PubSub.unsubscribe(this.xxx)
-
-
初始化各个组件
//App.jsx export default class App extends Component { render() { return ( <div className="container"> <Search/> <List/> </div> ) } } //Search.jsx export default class Search extends Component { render() { return ( <section className="jumbotron"> <h3 className="jumbotron-heading">搜索Github用户</h3> <div> <input type="text" placeholder="输入关键词点击搜索" /> <button>搜索</button> </div> </section> ) } } //List.jsx export default class List extends Component { render() { return ( <div className="row"> <div className="card"> <a href="" target="_blank"> <img src=""/> </a> <p className="card-text">{user.login}</p> </div> </div> ) } }
-
初始化组件的状态
//List.jsx export default class List extends Component { // 初始化状态 state = { users: [], isFirst: true, isLoading: false, err: "" } render() { const { users, isFirst, isLoading, err } = this.state return ( <div className="row"> { isFirst ? <h3>欢迎使用,输入关键字,随后点击搜索</h3> : isLoading ? <h3>Loading......</h3> : err ? <h3 style={{ color: 'red' }}>{err}</h3> : users.map(user => { return ( <div className="card" key={user.id}> <a href={user.html_url} target="_blank"> <img src={user.avatar_url} style={{ width: '100px' }} /> </a> <p className="card-text">{user.login}</p> </div> ) }) } </div> ) } }
-
发布消息与发送请求数据
//Search.jsx export default class Search extends Component { Search = () => { const { KeyWordElement: { value: keyWord } } = this; PubSub.publish('Spongebob',{isFirst:false,isLoading:true}) axios.get(`/api1/search/users2?q=${keyWord}`).then( response => { PubSub.publish('Spongebob',{isLoading:false,users:response.data.items}) }, error => { PubSub.publish('Spongebob',{isLoading:false,err:error.message}) } ) } render() { return ( <section className="jumbotron"> <h3 className="jumbotron-heading">搜索Github用户</h3> <div> <input ref={c => this.KeyWordElement = c} type="text" placeholder="输入关键词点击搜索" /> <button onClick={this.Search}>搜索</button> </div> </section> ) } }
-
订阅消息与动态更新数据
export default class List extends Component { // 初始化状态 state = { users: [], isFirst: true, isLoading: false, err: "" } componentDidMount() { this.token = PubSub.subscribe('Spongebob', (_, stateObj) => { this.setState(stateObj) }) } componentWillUnmount() { // 组件销毁后取消订阅消息 PubSub.unsubscribe(this.token) } render() { const { users, isFirst, isLoading, err } = this.state return ( <div className="row"> { isFirst ? <h3>欢迎使用,输入关键字,随后点击搜索</h3> : isLoading ? <h3>Loading......</h3> : err ? <h3 style={{ color: 'red' }}>{err}</h3> : users.map(user => { return ( <div className="card" key={user.id}> <a href={user.html_url} target="_blank"> <img src={user.avatar_url} style={{ width: '100px' }} /> </a> <p className="card-text">{user.login}</p> </div> ) }) } </div> ) } }
c.使用fetch发送请求
Fetch
特点:
- fetch: 原生函数,不再使用XmlHttpRequest对象提交ajax请求
- 老版本浏览器可能不支持
export default class Search extends Component {
Search = async() => {
const { KeyWordElement: { value: keyWord } } = this;
PubSub.publish('Spongebob',{isFirst:false,isLoading:true})
// 发送请求
try {
const response=await fetch(`/api1/search/users2?q=${keyWord}`);
PubSub.publish('Spongebob',{isFirst:false,isLoading:true})
const result=await response.json();
// PubSub.publish('Spongebob',{isLoading:false,users:result})
console.log(result);
} catch (error) {
console.log('请求出错',error);
}
}
render() {
return (
<section className="jumbotron">
<h3 className="jumbotron-heading">搜索Github用户</h3>
<div>
<input ref={c => this.KeyWordElement = c} type="text" placeholder="输入关键词点击搜索" />
<button onClick={this.Search}>搜索</button>
</div>
</section>
)
}
}
2).总结:
github搜索案例相关知识点:
-
设计状态时要考虑全面,例如带有网络请求的组件,要考虑请求失败怎么办。
-
ES6小知识点:解构赋值+重命名
let obj {a:{b:1}} //传统解构赋值 const{a}=obj; //连续解构赋值 const{a:(b}}=obj; //连续解构赋值+重命名 const{a:{b:value}}=obj;
-
消息订阅与发布机制
- 先订阅,再发布(理解:有一种隔空对话的感觉)
- 适用于任意组件间通信
- 要在组件的
componentwillUnmount
中取消订阅
-
fetch发送请求(关注分离的设计思想)
try{ const response=await fetch(/api1/search/users2?q=${keyWord)) const data await response.json() console.log(data); }catch (error){ console.1og('请求出错',error); }