详解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 相关文章推荐
onpropertypchange
Jul 01 Javascript
jQueryUI如何自定义组件实现代码
Nov 14 Javascript
基于jQuery的为attr添加id title等效果的实现代码
Apr 20 Javascript
使用JavaScript判断手机浏览器是横屏还是竖屏问题
Aug 02 Javascript
jQuery特殊符号转义的实现
Nov 30 Javascript
JavaScript使用ZeroClipboard操作剪切板
May 10 Javascript
Three.js基础学习教程
Nov 16 Javascript
AngularJS2 与 D3.js集成实现自定义可视化的方法
Dec 01 Javascript
JavaScript事件冒泡与事件捕获实例分析
Aug 01 Javascript
Vuejs+vue-router打包+Nginx配置的实例
Sep 20 Javascript
JavaScript判断浏览器运行环境的详细方法
Jun 30 Javascript
简述Vue中容易被忽视的知识点
Dec 09 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
PHP实现对文本数据库的常用操作方法实例演示
2014/07/04 PHP
PHP内核探索:哈希表碰撞攻击原理
2015/07/31 PHP
Zend Framework教程之Application和Bootstrap用法详解
2016/03/10 PHP
PHP实现文件上传功能实例代码
2017/05/18 PHP
Javascript技巧之不要用for in语句对数组进行遍历
2010/10/20 Javascript
js/jquery去掉空格,回车,换行示例代码
2013/11/05 Javascript
javascript的正则匹配方法学习
2016/02/24 Javascript
JavaScript:Date类型全面解析
2016/05/19 Javascript
Angular2 (RC5) 路由与导航详解
2016/09/21 Javascript
微信小程序(应用号)开发新闻客户端实例
2016/10/24 Javascript
JS日期对象简单操作(获取当前年份、星期、时间)
2016/10/26 Javascript
Centos7 中安装 Node.js v4.4.4
2016/11/03 Javascript
js实现文本上下来回滚动
2017/02/03 Javascript
React中使用collections时key的重要性详解
2017/08/07 Javascript
jQuery使用bind函数实现绑定多个事件的方法
2017/10/11 jQuery
ng-repeat指令在迭代对象时的去重方法
2018/10/02 Javascript
开发用到的js封装方法(20种)
2018/10/12 Javascript
antd组件Upload实现自己上传的实现示例
2018/12/18 Javascript
微信小程序生成分享海报方法(附带二维码生成)
2019/03/29 Javascript
javascript中undefined的本质解析
2019/07/31 Javascript
vue用elementui写form表单时,在label里添加空格操作
2020/08/13 Javascript
[01:38]完美世界高校联赛决赛花絮
2018/12/02 DOTA
[01:11:28]DOTA2-DPC中国联赛定级赛 RNG vs Phoenix BO3第一场 1月8日
2021/03/11 DOTA
解决python升级引起的pip执行错误的问题
2018/06/12 Python
使用Template格式化Python字符串的方法
2019/01/22 Python
python爬虫之遍历单个域名
2019/11/20 Python
利用Python计算KS的实例详解
2020/03/03 Python
婚礼主持词
2014/03/13 职场文书
幼儿园毕业寄语
2014/04/03 职场文书
物资采购方案
2014/06/12 职场文书
食品质量与安全专业毕业生求职信
2014/08/11 职场文书
党员评议表自我评价范文
2014/10/20 职场文书
2014年残联工作总结
2014/11/21 职场文书
义诊活动总结
2015/02/04 职场文书
分享Python异步爬取知乎热榜
2022/04/12 Python