详解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 相关文章推荐
一段实时更新的时间代码
Jul 07 Javascript
js类后台管理菜单类-MenuSwitch
Sep 12 Javascript
使用jQuery的ajax功能实现的RSS Reader 代码
Sep 03 Javascript
Extjs 继承Ext.data.Store不起作用原因分析及解决
Apr 15 Javascript
浅析Javascript使用include/require
Nov 13 Javascript
Node.js模拟浏览器文件上传示例
Mar 26 Javascript
jQuery中contents()方法用法实例
Jan 08 Javascript
百度地图api如何使用
Aug 03 Javascript
浅谈js构造函数的方法与原型prototype
Jul 04 Javascript
vue动态设置img的src路径实例
Sep 18 Javascript
js实现图片局部放大效果详解
Mar 18 Javascript
jQuery实现简单三级联动效果
Sep 05 jQuery
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中突破基于HTTP_REFERER的防盗链措施(stream_context_create)
2011/03/29 PHP
Zend的Registry机制的使用说明
2013/05/02 PHP
php 常用算法和时间复杂度
2013/07/01 PHP
php内存缓存实现方法
2015/01/24 PHP
利用PHP生成CSV文件简单示例
2016/12/21 PHP
php下载远程大文件(获取远程文件大小)的实例
2017/06/17 PHP
彻底搞懂PHP 变量结构体
2017/10/11 PHP
ExtJS 设置级联菜单的默认值
2010/06/13 Javascript
优化Jquery,提升网页加载速度
2013/11/14 Javascript
JavaScript中的函数重载深入理解
2014/08/04 Javascript
js实现文字在按钮上滚动的方法
2015/08/20 Javascript
理解jquery事件冒泡
2016/01/03 Javascript
JavaScript的ExtJS框架中数面板TreePanel的使用实例解析
2016/05/21 Javascript
Javascript中字符串相关常用的使用方法总结
2017/03/13 Javascript
jquery ajaxfileuplod 上传文件 essyui laoding 效果【防止重复上传文件】
2018/05/26 jQuery
微信小程序实现form表单本地储存数据
2019/06/27 Javascript
小程序点击图片实现png转jpg
2019/10/22 Javascript
Vue自定义多选组件使用详解
2020/09/08 Javascript
玩转python爬虫之URLError异常处理
2016/02/17 Python
Python随机生成均匀分布在三角形内或者任意多边形内的点
2017/12/14 Python
解决seaborn在pycharm中绘图不出图的问题
2018/05/24 Python
Python适配器模式代码实现解析
2019/08/02 Python
python_array[0][0]与array[0,0]的区别详解
2020/02/18 Python
VSCode基础使用与VSCode调试python程序入门的图文教程
2020/03/30 Python
Ubuntu20下的Django安装的方法步骤
2021/01/24 Python
Html5游戏开发之乒乓Ping Pong游戏示例(三)
2013/01/21 HTML / CSS
英国工具中心:UK Tool Centre
2017/07/10 全球购物
JustFab加拿大:女鞋、靴子、手袋和服装在线
2018/05/18 全球购物
迅雷Cued工作心得体会
2014/01/27 职场文书
高二化学教学反思
2014/01/30 职场文书
校园环保建议书
2014/05/14 职场文书
老龄工作先进事迹
2014/08/15 职场文书
2015年考研复习计划
2015/01/19 职场文书
食品质检员岗位职责
2015/04/08 职场文书
关于车尾的标语大全
2015/08/11 职场文书
Windows Server 2019 配置远程控制以及管理方法
2022/04/28 Servers