最近学完React的最基本概念,闲下来的时候就自己写了一个Todo-List的小应用。这里做个简略的说明,给想好好学React的新手看。
开始之前
学习前提
这里我用了webpackb做了babel和JSX预处理和模块打包。所以对React和一些ES2015(ES6)的语法要有一定的了解。我相信学习ES2015绝对是划算的,因为它是Js的规范。这里给出学习的地方,阮一峰老师的ECMAScript 6 入门或者babel的相关文档Learn ES2015。
功能需求
最后的实际效果:
我们需要做到的功能有:
- 可以在最上面的input里,使用回车来添加任务。
- 在中间的任务列表里,由checkbox来控制任务的状态。
- 已完成的任务有一个
line-through
的样式。
- 当鼠标移到每一个任务时,都会出现删除按钮提供删除。
- 在底部有一个全选按钮,用于控制所有的任务状态。
- 还有已完成与总数的显示。
- 可以清空已完成的任务。
项目下载
上面就是一个Todo-List最基本的功能,而我们这次就是用React实现上述功能。例子在我的github上可以download下来,可以用作参考:React-Todos
正式开始
加载npm模块
重要要开始我们的React-Todo的项目了,首先我们就要新建项目,通过npm我们可以很轻松的创建项目,并加载我们所需要的各个组件。大家可以在自己的项目里,用我的package.json
去加载所需要的模块。通过命令行进行安装。
这里提一下,因为我们这里仅仅是前端静态的,并不涉及到数据库。所以我自己写了一个非常简单的用于操作localStorage的小模块localDb
。所以涉及到数据存储的时候,都是用localStorage来代替数据库。它的原理就是,通过将数据格式化成JSON字符串进行存储,使用的时候就解析JSON字符串。这个模块在我的github的例子里有,需要从那里复制一份来,放在node_modules的文件夹内。
配置webpack
经过一轮漫长的等待,我们终于安装好所需要的各个模块了。我们最开始我们的react的编码前,需要对webpack进行配置。关于webpack的学习,我这里就不赘述了,在前一篇刚讲完。下面直接看一看webpack.config.js
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| var path = require('path');
module.exports = { entry: "./src/entry.js", output: { path: path.join(__dirname, 'out'), publicPath: './out/', filename: "bundle.js" }, externals: { 'react': 'React' }, module: { loaders: [ { test: /\.js$/, loader: "jsx!babel", include: /src/}, { test: /\.css$/, loader: "style!css"}, { test: /\.scss$/, loader: "style!css!sass"}, { test: /\.(jpg|png)$/, loader: "url?limit=8192"} ] } };
|
这里一切从简,可以看到入口文件是在src文件夹里的entry.js
,然后输出文件放在out文件夹的bundle.js
里。
配置一下模块的loaders,先用babel-loader再用jsx-loader。这样子我们就可以让ES6配合JSX编写我们的React组件了。其它的加载器也没什么好说的了,如果不清楚可以翻我上一篇关于webpack的文章。
这里提一下externals属性,这个属性是告诉webpack当遇到require('react')
的时候,不去处理并且默认为全局的React变量。这样子,我们就需要在index.html
单独用src去加载js。
分析各个组件
App组件
我这里并不会教大家手把手将这个React-Todo做出来,但是可以结合例子进行分析理解。先来看看总的组件,也就是App。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| import React from "react"; import LocalDb from "localDb";
import TodoHeader from "./TodoHeader.js"; import TodoMain from "./TodoMain.js"; import TodoFooter from "./TodoFooter.js";
class App extends React.Component { constructor(){ super(); this.db = new LocalDb('React-Todos'); this.state = { todos: this.db.get("todos") || [], isAllChecked: false }; }
allChecked(){ let isAllChecked = false; if(this.state.todos.every((todo)=> todo.isDone)){ isAllChecked = true; } this.setState({todos: this.state.todos, isAllChecked}); }
addTodo(todoItem){ this.state.todos.push(todoItem); this.allChecked(); this.db.set('todos',this.state.todos); }
changeTodoState(index, isDone, isChangeAll=false){ if(isChangeAll){ this.setState({ todos: this.state.todos.map((todo) => { todo.isDone = isDone; return todo; }), isAllChecked: isDone }) }else{ this.state.todos[index].isDone = isDone; this.allChecked(); } this.db.set('todos', this.state.todos); }
clearDone(){ let todos = this.state.todos.filter(todo => !todo.isDone); this.setState({ todos: todos, isAllChecked: false }); this.db.set('todos', todos); }
deleteTodo(index){ this.state.todos.splice(index, 1); this.setState({todos: this.state.todos}); this.db.set('todos', this.state.todos); }
render(){ var props = { todoCount: this.state.todos.length || 0, todoDoneCount: (this.state.todos && this.state.todos.filter((todo)=>todo.isDone)).length || 0 }; return ( <div className="panel"> <TodoHeader addTodo={this.addTodo.bind(this)}/> <TodoMain deleteTodo={this.deleteTodo.bind(this)} todos={this.state.todos} changeTodoState={this.changeTodoState.bind(this)}/> <TodoFooter isAllChecked={this.state.isAllChecked} clearDone={this.clearDone.bind(this)} {...props} changeTodoState={this.changeTodoState.bind(this)}/> </div> ) } } React.render(<App/>, document.getElementById("app"));
|
用ES6写React最大的不同就是,组件可以通过继承React.Components来得到,并且初始化state也不需要冗长的getInitalialState,直接在构造函数里操作this.state即可。更优秀的便是...
spread扩展操作符,可以让我们省下一堆不必要的代码,这个接下来再说。
App状态state
我们知道React的主流思想就是,所有的state状态和方法都是由父组件控制,然后通过props传递给子组件,形成一个单方向的数据链路,保持各组件的状态一致。所以我们在这个父组件App上,看的东西稍微有点多。一点点来看:
1 2 3 4 5 6 7 8
| constructor(){ super(); this.db = new LocalDb('React-Todos'); this.state = { todos: this.db.get("todos") || [], isAllChecked: false }; }
|
在App组件的constructor内,我们先是初始化了我们的localStorage的数据库,放在了this.db上。然后便是初始化了state,分别有两个,一个是todos的列表,一个是所有的todos是否全选的状态。
App方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| allChecked()
addTodo(todoItem)
changeTodoState(index, isDone, isChangeAll=false)
clearDone()
deleteTodo(index)
render(){ <div className="panel"> <TodoHeader /> <TodoMain /> <TodoFooter /> </div> }
|
我们可以从render函数看到整个组件的结构,可以看到其实结构非常简单,就是上中下。上面的TodoHeader自然就是用来输入任务的地方,中间就是展示并操作todo-list的,而底部就是显示数据并提供特殊操作。这里还是要提醒一句,所有标签都必须闭合,即使是非结对的,也要用斜杠闭合上。
1 2 3 4 5 6 7 8 9 10 11 12 13
| render(){ var props = { todoCount: this.state.todos.length || 0, todoDoneCount: (this.state.todos && this.state.todos.filter((todo)=>todo.isDone)).length || 0 }; return ( <div className="panel"> <TodoHeader addTodo={this.addTodo.bind(this)}/> <TodoMain deleteTodo={this.deleteTodo.bind(this)} todos={this.state.todos} changeTodoState={this.changeTodoState.bind(this)}/> <TodoFooter isAllChecked={this.state.isAllChecked} clearDone={this.clearDone.bind(this)} {...props} changeTodoState={this.changeTodoState.bind(this)}/> </div> ) }
|
我们可以看到,其他的方法都是传到子组件上,就不一一详细说如何实现的了。总体的思想就是,方法在父组件定义,通过props传给需要的子组件进行调用传参,最后返回到父组件上执行函数,存储数据、改变state和重新render。方法需要bind(this)
,不然方法内部的this指向会不正确。
计算需要的数据后,通过props传递到子组件。如果细心的同学应该可以看到像这样的{...props}
,这就是我之前说过的spread操作符。如果我们没有用这个操作符,就要这样写:
1 2
| <TodoFooter {...props} /> // spread操作符 <TodoFooter todoCount={props.todoCount} todoDoneCount={props.todoDoneCount} />
|
最佳的实践就是,当父组件传props给子组件,然后子组件要将props转发给孙子组件的时候,spread操作符简直让人愉悦!可以对一堆麻烦又丑又长的代码可以say goodbye了!
最后我们将整个App渲染到DOM上即可。
1
| React.render(<App/>, document.getElementById("app"));
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import React from "react";
class TodoHeader extends React.Component {
handlerKeyUp(event){ if(event.keyCode === 13){ let value = event.target.value;
if(!value) return false;
let newTodoItem = { text: value, isDone: false }; event.target.value = ""; this.props.addTodo(newTodoItem); } }
render(){ return ( <div className="panel-header"> <input onKeyUp={this.handlerKeyUp.bind(this)} type="text" placeholder="what's your task ?"/> </div> ) } }
export default TodoHeader;
|
到了子组件,方法就没那么多了,一般子组件就是绑定事件。可以看到在子组件绑定了keyUp事件,用来确定回车键并调用父组件传来的addTodo()
,将新生成的todo任务作为参数传入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import React from "react"; export default class TodoFooter extends React.Component{
handlerAllState(event){ this.props.changeTodoState(null, event.target.checked, true); }
handlerClick(){ this.props.clearDone(); }
render(){ return ( <div className="clearfix todo-footer"> <input checked={this.props.isAllChecked} onChange={this.handlerAllState.bind(this)} type="checkbox" className="fl"/> <span className="fl">{this.props.todoDoneCount}已完成 / {this.props.todoCount}总数</span> <button onClick={this.handlerClick.bind(this)} className="fr">清除已完成</button> </div> ) } }
|
我们先来看看这个footer上有哪些方法。第一个就是处理todo状态的,它通过底部的checkbox的change事件触发。然后就是清空已完成的按钮的点击事件的方法handlerClick()
。然后下面的数据显示,就通过props的值进行显示。
TodoMain
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import React from "react"; import TodoItem from "./TodoItem.js"
export default class TodoMain extends React.Component{ // 遍历显示任务,转发props render(){ return ( <ul className="todo-list"> {this.props.todos.map((todo, index) => { return <TodoItem key={index} {...todo} index={index} {...this.props}/> })} </ul> ) } }
|
Main组件的作用就是,将props传过来的todos遍历显示出来。所以对每一个todo的细致操作都是放在TodoItem上。
TodoItem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import React from "react"; export default class TodoItem extends React.Component{
handlerChange(){ let isDone = !this.props.isDone; this.props.changeTodoState(this.props.index, isDone); }
handlerMouseOver(){ React.findDOMNode(this.refs.deleteBtn).style.display = "inline"; }
handlerMouseOut(){ React.findDOMNode(this.refs.deleteBtn).style.display = "none"; }
handlerDelete(){ this.props.deleteTodo(this.props.index); }
render(){ let doneStyle = this.props.isDone ? {textDecoration: 'line-through'} : {textDecoration: 'none'};
return ( <li onMouseOver={this.handlerMouseOver.bind(this)} onMouseOut={this.handlerMouseOut.bind(this)} > <input type="checkbox" checked={this.props.isDone} onChange={this.handlerChange.bind(this)}/> <span style={doneStyle}>{this.props.text}</span> <button style={{'display': 'none'}} ref="deleteBtn" onClick={this.handlerDelete.bind(this)} className="fr">删除</button> </li> ) } }
|
在TodoItem主要处理多个交互,包括修改任务状态,删除任务。还有就是鼠标移到相应的任务上才显示删除按钮。
我们可以看到render()
函数,是控制了任务的样式。标签内的style是需要接受一个对象的,所以所有的CSS属性名,都要变成驼峰形的。
总结
其实真正的回过头看React-Todos,会觉得React带给我们的组件化的思想用起来太舒服了。我们通过父组件来控制状态,并通过props传递,来保证组件内的状态一致。我们可以非常有效的维护我们的交互代码,因为我们一眼就知道,这个事件属于哪个组件管理。它的模型其实非常轻,只有View层,但是它带给我们全新的书写前端组件的方法是非常好的,我个人认为如果未来的站点交互性愈来愈多,React是很有可能代替JQuery成为必备的技能。