详解React-Todos入门例子

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

开始之前

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

最后的实际效果:

我们需要做到的功能有:

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

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

(0)

相关推荐

  • 详解React Angular Vue三大前端技术

    一.[React] React(也被称为React.js或ReactJS)是一个用于构建用户界面的JavaScript库.它由Facebook和一个由个人开发者和公司组成的社区来维护. React可以作为开发单页或移动应用的基础.然而,React只关注向DOM渲染数据,因此创建React应用通常需要使用额外的库来进行状态管理和路由,Redux和React Router分别是这类库的例子. 基本用法 下面是一个简单的React在HTML中使用JSX和JavaScript的例子. Greeter函数

  • 详解React 元素渲染

    元素是构成 React 应用的最小单位,它用于描述屏幕上输出的内容. const element = <h1>Hello, world!</h1>; 与浏览器的 DOM 元素不同,React 当中的元素事实上是普通的对象,React DOM 可以确保 浏览器 DOM 的数据内容与 React 元素保持一致. 将元素渲染到 DOM 中 首先我们在一个 HTML 页面中添加一个 id="example" 的 <div>: <div id="

  • 详解React 条件渲染

    在 React 中,你可以创建不同的组件来封装各种你需要的行为.然后还可以根据应用的状态变化只渲染其中的一部分. React 中的条件渲染和 JavaScript 中的一致,使用 JavaScript 操作符 if 或条件运算符来创建表示当前状态的元素,然后让 React 根据它们来更新 UI. 先来看两个组件: function UserGreeting(props) { return <h1>欢迎回来!</h1>; } function GuestGreeting(props)

  • 详解React的回调渲染模式

    一.一个简单的小例子 1.父组件 <Twitter username='tylermcginnis33'> {(user) => user === null ? <Loading /> : <Badge info={user} />} </Twitter> 2.子组件框架 import React, { Component, PropTypes } from 'react' import fetchUser from 'twitter' // fetc

  • 详解React中key的作用

    要了解React中key的作用,可以从key的取值入手,key的取值可以分为三种,不定值.索引值.确定且唯一值 在下面的代码中,key的取值是不定值(Math.random()) 问题: 点击按钮的时候,span的颜色会变成红色吗? import React, { useState } from 'react'; function App() { const [initMap, setInitMap] = useState([1,2,3,4]); const handleClick = () =

  • 详解React中的不可变值

    什么是不可变值 函数式编程是指程序里面的函数和表达式都能像数学中的函数一样,给定了输入值,输出是确定的.比如 let a = 1; let b = a + 1; => a = 1 b = 2; 变量b出现,虽然使用了变量a的值,但是没有修改a的值. 再看我们熟悉的react中的代码,假如初始化了this.state = { count: 1 } componentDidMount() { const newState = { ...state, count: 2 }; // { count: 2

  • 详解react应用中的DOM DIFF算法

    前言 对我们搞前端的来说,目前最流行的两大前端框架毫无疑问当属React和Vue,对于这两大框架,想必大家也是再熟悉不过了.然而,这两大框架无一例外的全部放弃使用传统的DOM技术,却采用了以JS为基础的Virtual DOM技术,也可称作虚拟DOM.所以,到底什么是Virtual DOM?两大热门框架全部使用Virtual DOM的原因又是什么?接下来让我这个搞前端的人来好好地为您讲解一下DOM DIFF算法的牛逼之处. 什么是Virtual DOM? 如字面意思所说,Virtual DOM即

  • 详解React Native与IOS端之间的交互

    前置准备 首先最好了解一点关于 oc 的语法知识 1.创建声明文件nativeModule.h #import <Foundation/Foundation.h> #import <React/RCTBridgeModule.h> @interface nativeModule : NSObject <RCTBridgeModule> @end 2.创建文件nativeModule.m #import <Foundation/Foundation.h> #i

  • C++中的STL中map用法详解(零基础入门)

    目录 一.什么是 map ? 二.map的定义 2.1 头文件 2.2 定义 2.3 方法 三.实例讲解 3.1 增加数据 3.2 删除数据 3.3 修改数据 3.4 查找数据 3.5 遍历元素 3.6 其它方法 四.总结 map 在编程中是经常使用的一个容器,本文来讲解一下 STL 中的 map,赶紧来看下吧! 一.什么是 map ? map 是具有唯一键值对的容器,通常使用红黑树实现. map 中的键值对是 key value 的形式,比如:每个身份证号对应一个人名(反过来不成立哦!),其中

  • 详解React Native中如何使用自定义的引用路径

    目录 RN的相对路径地狱 RN的自定义路径需要的依赖 解决自定义引用路径导致的eslint报错问题 RN的相对路径地狱 我刚接触RN时,就发现所有的demo中给出来的路径都是相对路径,我自己的练习项目中想改成自定义的绝对路径,但是发现并没有我做前端时熟悉的webpack.config.js,所以也就不知道该怎么改了.伴随着RN的学习和开发练习,我的代码也变得越来越多,越来越复杂,我逐渐发现RN的相对路径越来越麻烦,比如我把某个文件移动到另一个不同深度的文件夹中,那么就需要把所有引用这个文件的地方

随机推荐