详解React-Todos入门例子


Posted in Javascript onNovember 08, 2016

最近学完React的最基本概念,闲下来的时候就自己写了一个Todo-List的小应用。这里做个简略的说明,给想好好学React的新手看。

开始之前

这里我用了webpackb做了babel和JSX预处理和模块打包。所以对React和一些ES2015(ES6)的语法要有一定的了解。我相信学习ES2015绝对是划算的,因为它是Js的规范。这里给出学习的地方,阮一峰老师的ECMAScript 6 入门或者babel的相关文档Learn ES2015。

最后的实际效果:

详解React-Todos入门例子

我们需要做到的功能有:

  1. 可以在最上面的input里,使用回车来添加任务。
  2. 在中间的任务列表里,由checkbox来控制任务的状态。
  3. 已完成的任务有一个line-through的样式。
  4. 当鼠标移到每一个任务时,都会出现删除按钮提供删除。
  5. 在底部有一个全选按钮,用于控制所有的任务状态。
  6. 还有已完成与总数的显示。
  7. 可以清空已完成的任务。

上面就是一个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"));

用ES6写React最大的不同就是,组件可以通过继承React.Components来得到,并且初始化state也不需要冗长的getInitalialState,直接在构造函数里操作this.state即可。更优秀的便是...spread扩展操作符,可以让我们省下一堆不必要的代码,这个接下来再说。

App状态state

我们知道React的主流思想就是,所有的state状态和方法都是由父组件控制,然后通过props传递给子组件,形成一个单方向的数据链路,保持各组件的状态一致。所以我们在这个父组件App上,看的东西稍微有点多。一点点来看:

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方法

// 判断是否所有任务的状态都完成,同步底部的全选框
allChecked()

// 添加一个任务,参数是一个todoItem的object
addTodo(todoItem)

// 改变任务的状态,index是第几个,isDone是状态,isChangeAll是控制全部状态的
changeTodoState(index, isDone, isChangeAll=false) // 参数默认位false

// 清空已完成
clearDone()

// 删除面板上第几个任务
deleteTodo(index)

// react用于渲染的函数
render(){
  <div className="panel">
    <TodoHeader />
    <TodoMain />
    <TodoFooter />
  </div>
}

我们可以从render函数看到整个组件的结构,可以看到其实结构非常简单,就是上中下。上面的TodoHeader自然就是用来输入任务的地方,中间就是展示并操作todo-list的,而底部就是显示数据并提供特殊操作。这里还是要提醒一句,所有标签都必须闭合,即使是非结对的,也要用斜杠闭合上。

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操作符。如果我们没有用这个操作符,就要这样写:

<TodoFooter {...props} /> // spread操作符
<TodoFooter todoCount={props.todoCount} todoDoneCount={props.todoDoneCount} />

最佳的实践就是,当父组件传props给子组件,然后子组件要将props转发给孙子组件的时候,spread操作符简直让人愉悦!可以对一堆麻烦又丑又长的代码可以say goodbye了!

最后我们将整个App渲染到DOM上即可。

React.render(<App/>, document.getElementById("app"));

AppHeader组件

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任务作为参数传入。

AppFooter组件

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

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

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成为必备的技能。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
锋利的jQuery jQuery中的DOM操作
Mar 21 Javascript
js操作table示例(个人心得)
Nov 29 Javascript
jQuery使用post方法提交数据实例
Mar 25 Javascript
深入理解JavaScript的React框架的原理
Jul 02 Javascript
javascript解决IE6下hover问题的方法
Jul 28 Javascript
JavaScript中eval()函数用法详解
Dec 14 Javascript
在WordPress中加入Google搜索功能的简单步骤讲解
Jan 04 Javascript
jQuery Mobile中的button按钮组件基础使用教程
May 23 Javascript
jQuery使用经验小技巧(推荐)
May 31 Javascript
将angular-ui的分页组件封装成指令的方法详解
May 10 Javascript
axios进阶实践之利用最优雅的方式写ajax请求
Dec 20 Javascript
vue.js通过路由实现经典的三栏布局实例代码
Jul 08 Javascript
JS+CSS3制作炫酷的弹窗效果
Nov 08 #Javascript
值得学习的bootstrap fileinput文件上传工具
Nov 08 #Javascript
BootStrap table使用方法分析
Nov 08 #Javascript
bootstrap监听滚动实现头部跟随滚动
Nov 08 #Javascript
AngularJS学习笔记(三)数据双向绑定的简单实例
Nov 08 #Javascript
bootstrapfileinput实现文件自动上传
Nov 08 #Javascript
JS实现探测网站链接的方法【测试可用】
Nov 08 #Javascript
You might like
用PHP产生动态的影像图
2006/10/09 PHP
dedecms模版制作使用方法
2007/04/03 PHP
PHP操作Memcache实例介绍
2013/06/14 PHP
强制PHP命令行脚本单进程运行的方法
2014/04/15 PHP
ThinkPHP使用心得分享-上传类UploadFile的使用
2014/05/15 PHP
YII路径的用法总结
2014/07/09 PHP
Prototype源码浅析 Enumerable部分之each方法
2012/01/16 Javascript
jquery入门必备的基本认识及实例(整理)
2013/06/24 Javascript
css+js实现部分区域高亮可编辑遮罩层
2014/03/04 Javascript
Javascript 读取操作Sql中的Xml字段
2014/10/09 Javascript
JavaScript简单表格编辑功能实现方法
2015/04/16 Javascript
js实现键盘上下左右键选择文字并显示在文本框的方法
2015/05/07 Javascript
jquery显示loading图片直到网页加载完成的方法
2015/06/25 Javascript
AngularJS Module方法详解
2015/12/08 Javascript
js防阻塞加载的实现方法
2016/09/09 Javascript
利用C/C++编写node.js原生模块的方法教程
2017/07/07 Javascript
详解基于Node.js的微信JS-SDK后端接口实现代码
2017/07/15 Javascript
Node Puppeteer图像识别实现百度指数爬虫的示例
2018/02/22 Javascript
this在vue和小程序中的使用详解
2019/01/28 Javascript
微信小程序 拍照或从相册选取图片上传代码实例
2019/08/28 Javascript
node.js中fs文件系统模块的使用方法实例详解
2020/02/13 Javascript
Python实现动态图解析、合成与倒放
2018/01/18 Python
对pandas的层次索引与取值的新方法详解
2018/11/06 Python
使用Python实现微信提醒备忘录功能
2018/12/04 Python
python频繁写入文件时提速的方法
2019/06/26 Python
Python调用C语言的实现
2019/07/26 Python
关于ZeroMQ 三种模式python3实现方式
2019/12/23 Python
美国最大的香水连锁店官网:Perfumania
2016/08/15 全球购物
ABOUT YOU匈牙利:500个最受欢迎的时尚品牌
2019/07/19 全球购物
世界上最大的字体市场:MyFonts
2020/01/10 全球购物
企业为何需要商业计划书
2013/12/26 职场文书
消防应急演练方案
2014/02/12 职场文书
2014年教研工作总结
2014/12/06 职场文书
2014年技术部工作总结
2014/12/12 职场文书
联欢会开场白
2015/06/01 职场文书
《钓鱼的启示》教学反思
2016/02/18 职场文书