基于React实现ToDoList功能
背景
学习React,并实现ToDoList功能(分为2个部分,依靠父组件传值实现,dva实现):
目标分析
- 功能确定
- 组件划分
- 代码实现
我们可以确定大概的功能有发布事件,删除事件,显示事件内容和截止日期,统计事件等。
依照以上功能可以做出大概的组件划分图
其中ToDoListInput为发布事件功能,List完成对事件的显示,listItem是事件,其中包括对事件的删除,勾选,内容显示等功能,Statistics完成对事件的统计等。
代码部分采用React+antd完成
代码实现
容易从组件划分图可以得出,ToDoList,List,Statistics之间的联系应该通过ToDoListApp,即它们的父组件来完成,因此我们可以在ToDoListApp的state中设置list数组,用于储存事件,以及创建修改list的方法等,并且通过父组件传值的方式将list传入Statistics等子组件中。
为了简化代码,我并没有完全按照概念图来写组件内容,而因为功能比较简单所以我将ToDOListInPut,Statistics承担的功能均放在ToDoListAPP中,而List中就之间完成了渲染每个子组件。
分析代码需要完成的功能。
父组件ToDoList:
使用的库
import React, { Component } from 'react';
import TodoListItem from './TodoListItem';
import {nanoid} from 'nanoid'
import {DatePicker, Input,Button}from 'antd'
import moment from 'moment'
import './index.css';
其中nanoid是生成唯一标识的库,通过nanoid()调用
State设置
class TodoList extends Component {
constructor(props) {
super(props);
this.state = {
list: [],
finished: 0,
inputValue: '',
date:null
};
}
...
}
list用于存储发布事件,finished用于记录完成的事件数,inputValue和date则是为了获取发布事件的内容和截止日期。
添加事件。
handleAddTask = () => {
const { inputValue,date} = this.state;
if (!inputValue) {
alert("输入为空,请重新输入待办事项");
return
}
if(!date)
{
alert("截止时间为空,请重新选择截止日期")
return
}
var obj = {
content: inputValue,
status: false,
deadline: moment(date).format('YYYY-MM-DD HH:mm:ss'),
nanoid:nanoid()
};
const { list } = this.state;
list.push(obj);
this.setState({
list,
inputValue: '',
date:null
});
}
先判断发布的事件中的内容或者日期是否为空。再从state中获取inputValue,date数据,再设置事件的默认勾选状态status为false不勾选。设置好添加的事件以后,push进state中的list中,最后使用setState修改list中的值。
修改inputValue中的值
handleChangeValue = (event) => {
const { value } = event.target;
this.setState({
inputValue: value
});
}
修改date的值
handleDateChange=(e)=>{
this.setState({date:e})
}
传递给子组件用于删除事件
handleClickDelete = (indexID) => {
const { list } = this.state;
const List = list.filter(item =>
item.nanoid !== indexID);
this.setState({
list: List
});
}
传入事件中的nanoid,以确定事件在list中的下标,使用filter函数过滤传入的id值,最后使用setState修改list的值。
传递给子组件用于统计完成的事件
updateFinished = (indexID) => {
const { list } = this.state;
list.forEach((item) => {
if (item.nanoid === indexID) {
item.status=!item.status
}
})
this.setState({list:list})
let finishedTask = 0;
list.forEach((item) => {
if (item.status === true) {
++finishedTask;
}
});
this.setState({
finished: finishedTask
});
}
从state中获取list,遍历数组,并且统计其中status为true的个数,即被勾选的个数,最后使用setState修改finished的值,用于显示完成的任务数。
render函数
render() {
const { list,finished,inputValue,date} = this.state;
return (
<div className="container">
<h1>TO DO LIST</h1>
<hr></hr>
<div>
<TodoListItem
list={list}
handleClickDelete={this.handleClickDelete}
updateFinished={this.updateFinished}
/>
<div className="item-count">
{finished}
已完成任务/
{list.length}
任务总数
</div>
<div className="addItem">
<Input
placeholder="Add your item……"
onKeyPress={this.enterPress}
value={inputValue}
onChange={this.handleChangeValue}
/>
<DatePicker onChange={this.handleDateChange} showTime value={date ? moment(date, 'YYYY-MM-DD HH:mm:ss') : null}/>
<Button className="addButton"
onClick={this.handleAddTask}
shape='round'
>
添加
</Button>
</div>
</div>
</div>
);
}
通过父组件传值的方式将handleClickDelete,updateFinished方法传递给子组件,并且将list对象数组传递给子组件。
子组件ToDoListItem
使用的库
import { Radio, Button, Input } from 'antd';
import React, { Component } from 'react';
父组件传入的函数
handleClickDelete(indexID) {
this.props.handleClickDelete(indexID);
}
handleClickFinished = (indexID) => {
this.props.updateFinished(indexID);
}
渲染每个子组件
listMap = () => {
const { list } = this.props;
return (
list.map(item => {
return (this.listMapItem(item))
})
)
}
将list中的每个组件都使用listMapItem函数分别渲染,最后用render函数渲染。
渲染父组件传递进list中的每个对象
listMapItem = (item) => {
return (
<div className='wrapper-item'
key={item.nanoid}
>
<div className="item">
<div className='item-select'>
<Radio
checked={item.status}
onClick={this.handleClickFinished.bind(this, item.nanoid)}
/>
<Input
style={{ textDecoration: item.status ===false ? 'none' : 'line-through',flexBasis:500} }
value={item.content}
>
</Input>
</div>
<span>{item.deadline}</span>
<Button
style={{margin:'0px 0px 0px 20px'}}
onClick={this.handleClickDelete.bind(this, item.nanoid)}
>
删除
</Button>
</div>
</div>
);
}
最后render渲染
render() {
return (
<div>
{this.listMap()}
</div>
);
}
代码遗留问题以及值得注意的地方
遗留问题:发布事件,即handleAddTask方法中,既然已经使用了react中的组件,这里可以用message组件进行替代。
值得注意的地方:在开始的代码编写,使用map函数对list中每个对象进行渲染时的key值采用的默认id值,这是不可取的,因为如果其他地方也使用了map函数,就会出现重复的id值。这里可以使用nanoid生成唯一标识符来设置key值。
因为此次的代码较为简单,组件划分的层数也不多,依靠父组件传值就没有什么问题,但是在实际的工程项目中,往往一个项目往往有几十层,这时候一层一层的传递数据是低效的,且每增添一个函数,程序员就需要在每一层的代码中进行改动,难以维护、我们意识到仅仅靠父组件传值的方式,我们很难在大型的工程项目中应用。因此,我经过实验室老师指导,通过dva.js重新构建了此次的代码,并且修复了遗留的问题。
dva部分
背景
同上,安装dva的具体方法参考dva官网
目标分析
同上
与父组件传值方式差异性分析
有差别的是,此次通过dva来构建代码。dva中通过model层来管理需要共用的state部分,省去了一层一层传值的时间。并且dva是基于redux、redux-saga 和 react-router的基础上建立的,可以方便的进行页面跳转,异步处理等。而与父组件传值方式相同的是,当model中的state值修改后,关联了该model的组件也会重新渲染
dva程序运行图解
model层分析
dva 通过 model 的概念把一个领域的模型管理起来,包含同步更新 state 的 reducers,处理异步逻辑的 effects,订阅数据源的 subscriptions。(由于本次实现的功能较为简单不用subscriptions)
我们可以将原来ToDoListApp中State中的数据全部放入ToDoList.js这个model中
export default {
namespace: 'list',
state: {list:[],finished:0,inputValue:'',date:null},
reducers:{}
effect:{}
}
namespace 表示在全局 state 上的 key
state 是初始值,在这里是一个对象。
reducers 等同于 redux 里的 reducer,接收 action,同步更新 state
index.js
import dva from 'dva';
const app=dva();
app.model(require('./models/todoList').default);
app.router(require('./router').default);
app.start('#root');
router.js
import React from 'react';
import Products from './routes/Products';
import { Router, Route, Switch } from 'dva/router';
import IndexPage from './routes/IndexPage';
import TodoList from './routes/TodoList';
function RouterConfig({ history }) {
return (
<Router history={history}>
<Switch>
<Route path="/products" exact component={Products} />
<Route path="/index" exact component={IndexPage}/>
<Route path="/todoList" exact component={TodoList}/>
</Switch>
</Router>
);
}
export default RouterConfig;
此处每个Route都是不同的页面,path中为浏览器访问的路径,exact,component={}是要显示的组件。
Routes/ToDoList.js
使用的库
import React, { Component } from 'react';
import TodoListItem from './TodoListItem';
import {nanoid} from 'nanoid'
import {DatePicker, Input,Button,message}from 'antd'
import moment from 'moment'
import {connect} from 'dva'
import styles from './index.css';
connect可以理解为是组件与model层的桥梁
connect修改和获取model中的信息
//用于获取model中的list信息
const mapStateToProps=({list})=>{
return {list:list}
}
//用于修改model中的list信息
const mapDispatchToProps=(dispatch)=>{
return{
handleClickAdd:({status,deadline,content,nanoid})=>{
dispatch({type:'list/addListItem',obj:{status,deadline,content,nanoid}})
},
handleChangeInputValue:(value)=>{
dispatch({type:'list/handleChangeValue',value:value})
},
handleChangeDate:(date)=>{
dispatch({type:'list/handleChangeDate',date})
},
handleChangeInputValueAndDate:(inputValue,date)=>{
dispatch({type:'list/handleChangeDateAndInputValue',value:inputValue,date})
},
}
}
export default connect(mapStateToProps,mapDispatchToProps)(TodoList)
与redux的使用基本无差异,mapStateToProps是获取model中的state,其参数为model中的namespace,mapDisPatchToProps则是用于修改model的state。两者都通过高阶组件将对象传递给自身,所以可以用this.props来使用。dispatch中type匹配model中effect的关键字来实现修改model中的state参数,除了type外的其他元素均可以当做传递给了model相匹配的一个函数一个action对象。
添加任务
handleAddTask = () => {
const { inputValue,date} = this.props.list;
if (!inputValue) {
message.info("输入为空,请重新输入待办事项");
return
}
if(!date)
{
message.info("截止时间为空,请重新选择截止日期");
return
}
var obj = {
content: inputValue,
status: false,
deadline: moment(date).format('YYYY-MM-DD HH:mm:ss'),
nanoid:nanoid()
};
this.props.handleClickAdd(obj)
const nullInputValue=''
const nullDate=null
this.props.handleChangeInputValueAndDate(nullInputValue,nullDate)
}
其主要逻辑与父组件传值的逻辑基本一致,不同的是,此处是修改model中的state数据。
修改日期
handleDateChange=(e)=>{
this.props.handleChangeDate(e)
}
修改inputValue
handleChangeValue = (event) => {
const { value } = event.target;
this.props.handleChangeInputValue(value)
}
render函数
render() {
const { finished,inputValue,date,list} = this.props.list;
return (
<div className={styles.container}>
<h1>TO DO LIST</h1>
<hr></hr>
<div>
<TodoListItem/>
<div className={styles.itemCount}>
{finished}
已完成任务/
{list.length}
任务总数
</div>
<div className={styles.addItem}>
<Input
placeholder="Add your item……"
onKeyPress={this.enterPress}
value={inputValue}
onChange={this.handleChangeValue}
/>
<DatePicker onChange={this.handleDateChange} showTime value={date ? moment(date, 'YYYY-MM-DD HH:mm:ss') : null}/>
<Button className={styles.addButton}
onClick={this.handleAddTask}
shape='round'
>
添加
</Button>
</div>
</div>
</div>
);
}
与父组件传值不同的是,由于state已经在model层中,其是共用的,所以不再需要通过父组件来传递修改方法,而是在子组件中写即可。
Route/ToDoListItem.js
connect
const mapStateToProps=({list})=>{
return {list:list}
}
const mapDispatchToProps=(dispatch)=>{
return{
handleClickDelete:(id)=>{
dispatch({type:'list/deleteListItem',id:id})
},
handleUpdate:(list)=>{
dispatch({type:'list/updateList',list:list})
},
handleChangeFinished:(finishedTask)=>{
dispatch({type:'list/handleChangeFinished',finishedTask})
}
}
}
其余的方法格式与ToDoList一致,不再赘述
Model/ToDoList.js
reducers
update(state,action){
return action.newState
}
reduce是纯函数,所以只负责刷新状态即可
effect
effects:{
*deleteListItem(action,{put,select}){
const {list}=yield select(_=>_.list)
const newList=[...list.filter(item=>item.nanoid!==action.id)]
const newState={...yield select(_=>_.list),list:newList}
console.log(newState)
yield put({type:'update',newState})
},
*updateList(action,{put,select}){
const newList=[...action.list]
const newState={...yield select(_=>_.list),list:newList}
yield put({type:'unpdate',newState})
},
*addListItem(action,{put,select}){
const {list}=yield select(_=>_.list)
const newList=[...list,action.obj]
const newState={...yield select(_=>_.list),list:newList}
yield put({type:'update',newState})
},
*handleChangeValue(action,{put,select}){
let newState={...yield select(_=>_.list)}
newState['inputValue']=action.value
yield put({type:'update',newState})
},
*handleChangeDate(action,{put,select}){
let newState={...yield select(_=>_.list)}
newState['date']=action.date
yield put({type:'update',newState})
},
*handleChangeDateAndInputValue(action,{put,select}){
let newState={...yield select(_=>_.list)}
newState['inputValue']=action.value
newState['date']=action.date
yield put({type:'update',newState})
},
*handleChangeFinished(action,{put,select}){
let newState={...yield select(_=>_.list)}
newState['finished']=action.finishedTask
yield put({type:'update',newState})
},
}
effect负责业务逻辑的处理,因此在组件中dispatch应该匹配effct中相应的名称进行操作,其中action为dispatch中除了type以外的对象。而后面的对象为yield需要的操作,通常有call,select,put。此处用到select选取state中的数据,最后操作完后,通过type匹配reducer中相应的名称,完成对state的更新。
值得注意的地方
- model中的reducers是纯函数,不能在其内部处理业务逻辑,相关的操作应该在effects中处理
- 组件刷新与否取决于model层的state是否刷新,而state刷新则需要判断state是否改变,而仅仅改变state中的值是不会刷新的,因为state对象的地址并没有改变,所以在effect中我们采用{...对象名}来深拷贝state,使得组件可以正常刷新
- dva官方文档中state只是一个数组,其实其可以是一个对象,存储多个需要的对象,使得程序员从多次的父组件传值解脱出来。
从零搭建 Node.js 企业级 Web 服务器(零):静态服务
乌柏木赞 150阅读 12.3k评论 10
正则表达式实例
寒青赞 56阅读 7.9k评论 11
JavaScript有用的代码片段和trick
jenemy赞 46阅读 6k评论 12
从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望
乌柏木赞 66阅读 6.2k评论 16
再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
libinfs赞 39阅读 6.3k评论 12
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
乌柏木赞 44阅读 7.4k评论 6
CSS 绘制一只思否猫
XboxYan赞 43阅读 3k评论 14
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。