最近学完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去加载所需要的模块。通过命令行进行安装。
$ npm install
这里提一下,因为我们这里仅仅是前端静态的,并不涉及到数据库。所以我自己写了一个非常简单的用于操作localStorage的小模块localDb。所以涉及到数据存储的时候,都是用localStorage来代替数据库。它的原理就是,通过将数据格式化成JSON字符串进行存储,使用的时候就解析JSON字符串。这个模块在我的github的例子里有,需要从那里复制一份来,放在node_modules的文件夹内。
配置webpack
经过一轮漫长的等待,我们终于安装好所需要的各个模块了。我们在开始我们的react的编码前,需要对webpack进行配置。关于webpack的学习,我这里就不赘述了,在前一篇刚讲完。下面直接看一看webpack.config.js。
// webpack.config.js 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。
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}); } // 添加任务,是传递给Header组件的方法 addTodo(todoItem){ this.state.todos.push(todoItem); this.allChecked(); this.db.set('todos',this.state.todos); } // 改变任务状态,传递给TodoItem和Footer组件的方法 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); } // 清除已完成的任务,传递给Footer组件的方法 clearDone(){ let todos = this.state.todos.filter(todo => !todo.isDone); this.setState({ todos: todos, isAllChecked: false }); this.db.set('todos', todos); } // 删除当前的任务,传递给TodoItem的方法 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"));