1

发现最近看到的框架入门程序都变成Todos,我花了三个小时才自己实现了一个Todos...感叹前端入门越来越复杂了,怀念几年前还是hello world的时代。。。

吐槽不多说,这里主要说的只是ReactRedux这一块儿,css样式完全是从这里抄过来的。

代码的结构准备是这个样子的:
clipboard.png

转化成代码结构,就是这个样子:
clipboard.png

另外,按照官方示例,在Header左边的toggleAll按钮放到了Section中。

Redux

Types/todos.js

Redux中,type是用于actionreducer交流的时候的一个flag,让reducer知道这是一个什么请求。

我比较习惯把type单独分离出来,列到一个文件里面,让Redux的文件更干净,并便于管理。

/** ------------------------- TODO ---------------------*/
export const TODO_INSERT_ITEM = 'TODO_INSERT_ITEM';
export const TODO_DELETE_ITEM = 'TODO_DELETE_ITEM';
export const TODO_SWITCH_FILTER = 'TODO_SWITCH_FILTER';
export const TODO_TOGGLE_ACTIVE = 'TODO_TOGGLE_ACTIVE';
export const TODO_TOGGLE_ALL = 'TODO_TOGGLE_ALL';
export const TODO_CHANGE_VALUE = 'TODO_CHANGE_VALUE';
export const TODO_CLEAR_COMPLETED = 'TODO_CLEAR_COMPLETED';

Actions/todos.js

根据上面列出的type,列出对应的action creator

import { TODO_INSERT_ITEM, TODO_DELETE_ITEM, TODO_SWITCH_FILTER, TODO_TOGGLE_ACTIVE, TODO_TOGGLE_ALL, TODO_CHANGE_VALUE, TODO_CLEAR_COMPLETED } from '../types';

// 插入一个TODO 
export function insertItem(value){
  return {
    type: TODO_INSERT_ITEM,
    value
  };
}

// 删除一个TODO
export function deleteItem(id) {
  return {
    type: TODO_DELETE_ITEM,
    id
  }
}

// 转换一个TODO的状态
export function switchFilter(filter) {
  return {
    type: TODO_SWITCH_FILTER,
    filter
  }
}

// 清楚所有完成的TODO
export function clearCompleted(){
  return {
    type: TODO_CLEAR_COMPLETED
  }
}

export function toggleActive(id){
  return {
    type: TODO_TOGGLE_ACTIVE,
    id
  }
}

// 转换所有的状态到active
export function toggleAll(active){
  return {
    type: TODO_TOGGLE_ALL,
    active
  }  
}

// 改变对应TODO的值
export function changeValue(id, value) {
  return {
    type: TODO_CHANGE_VALUE,
    id,
    value
  }
}

Reducers/todos.js

reducer中需要注意几点:

  1. 初始化的state要从localStorage中获取

  2. 每次做出修改,都要重新更新localStorage

  3. 数据没有发生改变的时候,尽量使用原数据,减少re-render

  4. 为了便于查找,我在这里用了lodashuniqueId方法,给每一个item加一个id

  5. 为了便于储存和展示,我这里包含一个items用来保存所有的items,一个showedItems用来储存需要展示的items

先提供一个简单的简写localStorage方法

const local = (function(KEY){
  return {
    set: value=>{ localStorage.setItem(KEY, value) },
    get: ()=>localStorage.getItem(KEY),
    check: ()=>localStorage.getItem(KEY) != undefined
  };
})("todo");

然后几个辅助的方法:

// 制造一个新的item
function generateItem(value) {
  return {
    id: _.uniqueId(),
    active: true,
    value
  }
}

// 判断当前的item是否正在展示
function include(active, filter) {
  return filter === "ALL" || (active && filter === "ACTIVE")  || (!active && filter === "COMPLETED");
}

// 获取页面上需要展示的items
function getShowedItems(items, filter) {
  let showedItems = [], keys = Object.keys(items);
  for(let i = 0; i < keys.length; i++){
    let item = items[keys[i]];

    if(include(item.active, filter)) {
      showedItems.push(item);
    }
  }
  return showedItems;
}

初始化的时候,获取localStorage中的值,或者给一个默认值:

let defaultTodo;
(function(){
  if(local.check()) {
    defaultTodo = JSON.parse(local.get());
  } else {
    defaultTodo = {
      items: {},
      filter: "ALL", // ALL, COMPLETED, ACTIVE
      count: 0,
      showedItems: [],
      hasCompleted: false
    }
  }
})();

注:在这里提一句,由于我不喜欢文档中把所有的处理方法放在一个函数里面的方式,所以我写了一个方法,把reducers分开成多个函数

// 很简单,其实就是循环调用。。。
export function combine(reducers){
  return (state, action) => {
    for(let key in reducers) {
      if(reducers.hasOwnProperty(key)) {
          state = reducers[key](state, action) || state;
      }
    }
    return state;
  }
} 

下面上所有的reducers,具体逻辑就不多说了:

let exports = {};
exports.insertItem = function(state = defaultTodo, action) {
  const type = action.type;

  if(type === TODO_INSERT_ITEM) {
    let { count, items, filter, showedItems } = state;
    let item = generateItem(action.value);

    items = {
      ...items,
      [item.id] : item 
    }

    count = count + 1;
    
    state = {
      ...state,
      items,
      count,
      showedItems: filter !== "COMPLETED" ? getShowedItems(items, filter) : showedItems
    }

    local.set(JSON.stringify(state));
  }

  return state;
}

exports.deleteItem = function(state = defaultTodo, action) {
  const type = action.type;

  if(type === TODO_DELETE_ITEM && state.items[action.id]) {
    let { count, items, filter, hasCompleted } = state;
    let item = items[action.id];

    delete items[action.id];
    if(item.active) count--;
    
    state = {
      ...state,
      items,
      count,
      showedItems: include(item.active, filter) ? getShowedItems(items, filter) : state.showedItems,
      hasCompleted: Object.keys(items).length !== count
    }

    local.set(JSON.stringify(state));
  }

  return state;
}

exports.switchFilter = function(state = defaultTodo, action) {
  const type = action.type;
  if(type === TODO_SWITCH_FILTER && state.filter !== action.filter) {
    state = {
      ...state,
      filter: action.filter,
      showedItems: getShowedItems(state.items, action.filter)
    }

    local.set(JSON.stringify(state));
  }

  return state;
}

exports.clearCompleted = function(state = defaultTodo, action) {
  const type = action.type;
  if(type === TODO_CLEAR_COMPLETED) {
    let { items, filter, showedItems } = state;

    let keys = Object.keys(items);
    let tempItems = {};
    for(let i = 0; i < keys.length; i++) {
      let item = items[keys[i]];
      if(item.active) {
        tempItems[item.id] = item;
      }
    }

    state = {
      ...state,
      items: tempItems,
      showedItems: filter === "ACTIVE" ? showedItems : getShowedItems(tempItems, filter),
      hasCompleted: false
    }
    local.set(JSON.stringify(state));
  }

  return state;
}

exports.toggleActive = function(state = defaultTodo, action) {
  const { type, id } = action;

  if(type === TODO_TOGGLE_ACTIVE && state.items[id]) {
    let { items, filter, count, showedItems } = state;
    
    let item = items[id];
    item.active = !item.active;
    
    items = {
      ...items,
      [id]: item
    };

    if(item.active) count++; // 如果变为active
    else count--; // 如果变为completed

    state = {
      ...state,
      items,
      count,
      showedItems: getShowedItems(items, filter),
      hasCompleted: Object.keys(items).length !== count
    }

    local.set(JSON.stringify(state));
  }
  return state;
}

exports.toggleAll = function(state = defaultTodo, action) {
  const { type, active } = action;

  if(type === TODO_TOGGLE_ALL) {
    let { items, filter, showedItems } = state;
    let keys = Object.keys(items);
    
    for(let i = 0; i < keys.length; i++) {
      items[keys[i]].active = active;
    }

    let count = active ? keys.length : 0; 

    state = {
      ...state,
      items,
      count,
      showedItems: include(active, filter) ? getShowedItems(items, filter) : showedItems,
      hasCompleted: !active
    }

    local.set(JSON.stringify(state));
  }
  return state;
}

exports.changeValue = function(state = defaultTodo, action){
  const { type, id } = action;

  if(type === TODO_CHANGE_VALUE && state.items[id]) {
    let { items, filter, showedItems } = state;
    let item = items[id];

    item.value = action.value;

    items = {
      ...items,
      [id]: item    
    };

    state = {
      ...state,
      items,
      showedItems: include(item.active, filter) ? getShowedItems(items, filter) : showedItems
    }
    local.set(JSON.stringify(state));
  }
  return state;
}

export default combine(exports); // 用combine方法包裹

Reducers中,我在很多的showedItems都做了是否发生改变的检查,如果没有发生改变,那么就用原来的,便于在Section组件中,可以避免不必要的重新渲染。虽然,在我这里似乎没有什么用。不过对复杂的项目还是有必要的。

Views/Todos.js

import React from 'react';
import Header from 'containers/ToDo/Header';
import Footer from 'containers/ToDo/Footer';
import Section from 'containers/ToDo/Section';
import 'components/ToDo/index.scss';

export default class ToDo extends React.Component {
  constructor(props) {
    super(props);
  }

  render(){
    return (
      <div id="TODO">
        <Header />
        <Section />
        <Footer />
      </div>
    )
  }
}

Contianers

Header.js

Header.js主要负责logo的渲染,和那个input框的功能。

利用controlled componentinput的值进行控制,然后监听键盘来判断是输入还是提交。

import React from 'react';
import { CONTROLS } from 'utils/KEYCODE';
import { connect } from 'react-redux';
import { insertItem } from 'actions/todo';

class Header extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      value: ""
    };
  }

  onChange = (e)=>{
    let value = e.target.value;
    this.setState({
      value
    });
  }

  onKeyDown = (e)=>{
    let keyCode = e.keyCode;

    if(keyCode === CONTROLS.ENTER && this.state.value !== "") {
      this.props.insertItem(this.state.value);
      this.setState({
        value: ""
      });

      e.preventDefault();
      e.stopPropagation();
    }
  }

  render(){
    return (
      <header className="todo-header">
        <h1>todos</h1>
        <input type="text" className="insert" value={this.state.value} onChange={this.onChange} onKeyDown={this.onKeyDown} placeholder="What needs to be done?" />
      </header>
    )
  }
}

export default connect(null, { insertItem })(Header);

Footer.js

Footer主要是用于展示数量filter, Clear Completed按钮

import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { switchFilter, clearCompleted } from 'actions/todo';

class Footer extends React.Component {
  constructor(props) {
    super(props);
  }

  switchFilter = (filter)=>{
    this.props.switchFilter(filter.toUpperCase());
  }

  render(){
    const { count, hasCompleted, filter, clearCompleted } = this.props;

    if(count === 0 && !hasCompleted) return null;

    return (
      <footer className="todo-footer">
        <span className="counter">{count} items left</span>
        <ul className="filter">
          {
            ["All", "Active", "Completed"].map((status, index)=>{
              return (
                <li key={status} className={status.toUpperCase() === filter ? "active" : ""}>
                  <a href="javascript:;" onClick={()=>{ this.switchFilter(status) }}>{status}</a>
                </li>
              );
            })
          }
        </ul>
        {
          hasCompleted && <button className="clear-completed" onClick={clearCompleted}>Clear completed</button>
        }
      </footer>
    )
  }
}

Footer.propTypes = {
  count: PropTypes.number.isRequired,
  hasCompleted: PropTypes.bool.isRequired,
  filter: PropTypes.oneOf(['ALL', 'ACTIVE', 'COMPLETED']).isRequired
}

function mapStateToProps(state){
  let {
    todo: {
      count,
      hasCompleted,
      filter
    }
  } = state;

  return {
    count,
    hasCompleted,
    filter
  }
}

export default connect(mapStateToProps, { switchFilter, clearCompleted })(Footer);

Section.js

Section包含Todos的列表,还有删除, 改变状态修改value, toggle all等功能。

import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { deleteItem, toggleActive, toggleAll, changeValue } from 'actions/todo';
import Item from 'components/ToDo/Item';

class Section extends React.Component {
  constructor(props) {
    super(props);
  }

  render(){
    const { showedItems=[], count, toggleAll, changeValue, deleteItem, toggleActive, hasCompleted } = this.props;

    return (
      <section className="todo-section">
        <input type="checkbox" className="toggle-all" onChange={()=>{ toggleAll(count === 0) }} checked={count === 0 && hasCompleted} />
        <ul className="todo-items">
          {
            showedItems.map(item=><Item key={item.id} {...item} onValueChange={changeValue} onItemDelete={deleteItem} toggleActive={toggleActive} />)
          }
        </ul>
      </section>
    )
  }
}

Section.propTypes = {
  showedItems: PropTypes.arrayOf(PropTypes.object).isRequired,
  count: PropTypes.number.isRequired
}

function mapStateToProps(state) {
  let {
    todo: {
      showedItems,
      count,
      hasCompleted
    }
  } = state;

  return {
    showedItems,
    count,
    hasCompleted
  };
}

export default connect(mapStateToProps, { deleteItem, toggleActive, toggleAll, changeValue })(Section);

Components

Item.js

import React from 'react';
import ReactDOM from 'react-dom';

export default class Item extends React.Component {
  constructor(props) {
    super(props);

    this.state = { value: props.value, editing: false };
  }

  componentDidUpdate() {
    if(this.state.editing) {
      var node = ReactDOM.findDOMNode(this.edit);
      node.focus();
    }
  }

  inputInstance = (input) => {
    this.edit = input;
  }

  onToggle = (e)=>{
    this.props.toggleActive(this.props.id, !e.target.checked);
  }

  onValueChange = (e)=>{
    this.props.onValueChange(this.props.id, e.target.value);
    this.setState({
      editing: false
    });
  }

  onEditChange = (e)=>{
    this.setState({
      value: e.target.value
    });
  }

  onDoubleClick = (e)=>{
    this.setState({
      editing: true
    });
  }

  render(){
    let { id, active, onItemDelete, onValueChange } = this.props;
    let { value, editing } = this.state;

    return (
      <li className={editing ? "editing" : ""}>
        { editing || (<div className="view">
          <input type="checkbox" className="toggle" onChange={this.onToggle} checked={!active} />
          <label className={ `item-value${active ? "" : " completed"}`} onDoubleClick={this.onDoubleClick} >{value}</label>
          <button className="delete" onClick={()=>{ onItemDelete(id) }}></button>
        </div>)
        }
        { editing && <input type="text" ref={this.inputInstance} className="edit" onBlur={this.onValueChange} onChange={this.onEditChange} value={value} /> }
      </li>
    )
  }
}

写组件的时候,感觉代码贴出来看看就好了。需要讲解的不多。。。


Clark
3.4k 声望82 粉丝

[链接]