翻译|如何使用React,Redux和Immutable.js构建Todo App

本文是翻译版本,原文请见
By Dan Prince May 03, 2016

React使用组件和单向数据流方式描述用户界面,但是React对state的处理非常的简单.这一点让我们知道,React仅仅只当于传统的Model-View-Controller构架的View层.

仅仅使用React也可以构建大型的app,但是很快我们会发现,要保持代码的简洁,我们需要在其他地方管理state(把state的管理独立出来).

没有官方管理应用state的工具,但是有几个库工作的的不错.今天我们添加两个库和React一起来构建一个简单的app.

Redux

Redux是一个小型的js库,作为app的state容器.糅合了Fluc和Elm的概念.我们可以使用Redux管理任何app的state,只要我们紧扣下面的指导:

  1. 我们的state保持在一个单一的store中
  2. state的改变只会来自于actions

Redux的核心 store是一个函数,它接收当前的application的state和一个action,合并创建一个新的application state,这个函数叫做Reducer.

我们的React组件负责发送actions到我们的store,反过来,如果组件需要渲染的时候,store会通知他.

ImmutableJS

因为Redux不允许我们mutate程序的state,如果借助immutable数据结构模型化应用程序的state将会非常的有用.
Immutable.js使用突变界面(mutative interfaces)提供一些immutable数据结构,这些界面实施时非常的高效,灵感来自于Clojure和Scala.

Demo

我们将会使用React,Redux和ImmutableJS去构建一个简单的todo list,允许我们添加todos,在完成和未完成之间切换.

//html
 

//css
 html, body, input, button {
  font-family: Sawasdee;
  font-size: 20px;
}

.todo {
}

.todo__list {
  margin: 0;
  padding: 0;
  list-style-type: none;
}

.todo__item {
  padding: .5em .25em;
  border-bottom: solid 1px #eee;
}

.todo__item:hover {
  background: #f7f7f7;
  cursor: pointer;
}

.todo__entry {
  border: solid 1px #ccc;
  padding: .25em .5em;
  border-radius: .2em;
  background: #f3f3f3;
  width: 100%;
  box-sizing: border-box;
}

.todo__button {
  border: 0;
  border-radius: .2em;
  background: #71B7FF;  
  color: #fff;
  padding: .25em .5em;
  margin: .5em 0;
  margin-right: .25em;
  cursor: pointer;
}

.todo__button:hover {
  background: #B2D8FF;
}

//js
const { Map, List } = Immutable;
const { createStore } = Redux;
const { Provider, connect } = reactRedux;

const components = {
  Todo({ todo }) {
    if(todo.isDone) {
      return {todo.text};  
    } else {
      return {todo.text};
    }
  },
  TodoList({ todos, toggleTodo, addTodo }) {
    const onSubmit = (e) => {
      const text = e.target.value;
      if(e.which === 13 && text.length > 0) {
        addTodo(text);
        e.target.value = '';
      }
    };
    
    const toggleClick = (id) => () => toggleTodo(id);
    
    const { Todo } = components;
    
    return (
      
    {todos.map(t => (
  • ))}
); } }; const actions = { addTodo(text) { return { type: 'ADD_TODO', payload: { id: Math.random().toString(34).slice(2), isDone: false, text } }; }, toggleTodo(id) { return { type: 'TOGGLE_TODO', payload: id } } }; const init = List(); const reducer = function(state=init, action) { switch(action.type) { case 'ADD_TODO': return state.push( Map(action.payload) ); case 'TOGGLE_TODO': return state.map(t => { if(t.get('id') == action.payload) { return t.update('isDone', isDone => !isDone); } else { return t; } }); default: return state; } }; const containers = { TodoList: connect( function mapStateToProps(state) { return { todos: state }; }, function mapDispatchToProps(dispatch) { return { toggleTodo: (id) => dispatch(actions.toggleTodo(id)), addTodo: (text) => dispatch(actions.addTodo(text)) }; } )(components.TodoList) }; const { TodoList } = containers; const store = createStore(reducer); ReactDOM.render( , document.getElementById('app') );

代码在 Github

可能提示build失败,npm install babel-core试试

setup

从创建项目开始,建立一个package.json文件.然后安装需要的依赖包.

 npm install --save react react-dom redux react-redux immutable
npm install --save-dev webpack babel-loader babel-preset-es2015 babel-preset-react

使用JSX和ES2015,用Babel编译代码,使用Webpack来完成这个模块绑定过程.

webpack.config.js文件中创建Webpack配置文件.

 module.exports = {
  entry: './src/app.js',
  output: {
    path: __dirname,
    filename: 'bundle.js'
  },
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel',
        query: { presets: [ 'es2015', 'react' ] }
      }
    ]
  }
};

最后扩展一下package.json,添加一个npm script使用source maps编译我们的代码.

 "scripts": {
  "build": "webpack --debug"
}

每次编译代码的时候,运行npm run build.

React&Components

在实施项目之前,先创建一些傻瓜数据有很大的用处,但我们构思需要渲染的组件的时候,有一点点初步的感觉.

 const dummyTodos = [
  { id: 0, isDone: true,  text: 'make components' },
  { id: 1, isDone: false, text: 'design actions' },
  { id: 2, isDone: false, text: 'implement reducer' },
  { id: 3, isDone: false, text: 'connect components' }
];

我们需要两个React组件

 // src/components.js

import React from 'react';

export function Todo(props) {
  const { todo } = props;
  if(todo.isDone) {
    return {todo.text};
  } else {
    return {todo.text};
  }
}

export function TodoList(props) {
  const { todos } = props;
  return (
    
    {todos.map(t => (
  • ))}
); }

到了这一步,可以创建index.html文件来测试这些组价,添加下面的标记

 

  
    
    Immutable Todo
  
  
    

还有一个项目的入口文件src/app.js.

// src/app.js

import React from 'react';
import { render } from 'react-dom';
import { TodoList } from './components';

const dummyTodos = [
  { id: 0, isDone: true,  text: 'make components' },
  { id: 1, isDone: false, text: 'design actions' },
  { id: 2, isDone: false, text: 'implement reducer' },
  { id: 3, isDone: false, text: 'connect components' }
];

render(
  ,
  document.getElementById('app')
);

使用npm run build编译文件,然后在浏览器中打开index.html文件,确保运行.

Redux&ImmutableJS

现在我们有了很好的UI,可以开始考虑组件最后的state.开始创建的傻瓜数据是一个很好的开端,我们可以很容易转化为ImmutableJS集合.

 import { List, Map } from 'immutable';

const dummyTodos = List([
  Map({ id: 0, isDone: true,  text: 'make components' }),
  Map({ id: 1, isDone: false, text: 'design actions' }),
  Map({ id: 2, isDone: false, text: 'implement reducer' }),
  Map({ id: 3, isDone: false, text: 'connect components' })
]);

ImmutableJS map和Javascript的对象工作方式不同,所以我们要对组件做一点轻微的改变.property接入的地方(例如:todo.id)需要使用一个方法调用来代替(例如:todo.get(‘id’)).

设计Actions

现在我们获得了数据的特征,可以考虑一下actions的更新.这个实例中,我们仅仅需要两个acions,一个是添加新的todo,另一个转换todo的状态.

让我们定义几个函数创建这些actions

 // src/actions.js

// succinct hack for generating passable unique ids
const uid = () => Math.random().toString(34).slice(2);

export function addTodo(text) {
 return {
   type: 'ADD_TODO',
   payload: {
     id: uid(),
     isDone: false,
     text: text
   }
 };
}

export function toggleTodo(id) {
 return {
   type: 'TOGGLE_TODO',
   payload: id
 }
}

每一个action仅仅是一个有type和payload的属性对象.在我们触发action后,type属性帮助我们用payload来作什么.

设计一个Reducer

现在我们知道了state的特性和更新state的action,我们可以创建reducer了.仅仅提醒一下,reducer是一个接收state和action的函数,然后用来计算更新state.

这里是我们reducer的初始结构.

 // src/reducer.js

import { List, Map } from 'immutable';

const init = List([]);

export default function(todos=init, action) {
  switch(action.type) {
    case 'ADD_TODO':
      // ...
    case 'TOGGLE_TODO':
      // ...
    default:
      return todos;
  }
}

操作ADD_TODOaction非常简单,可是使用.push()方法,返回一个新的列表,添加todo到末尾.

 case 'ADD_TODO':
  return todos.push(Map(action.payload));

记住要push到列表之前,要把todo对象转变为immutable map.

我们需要处理的稍微复杂的action是TOOGLE_TODO.

 case 'TOGGLE_TODO':
  return todos.map(t => {
    if(t.get('id') === action.payload) {
      return t.update('isDone', isDone => !isDone);
    } else {
      return t;
    }
  });

我们使用.map()遍历列表,找到与acitonid匹配的todo项目.之后我们调用.update()方法,接收一个键和函数,然后返回一个map的新拷贝到updata函数,新拷贝中新值替换了初始值.

字面量版本

  const todo = Map({ id: 0, text: 'foo', isDone: false });
todo.update('isDone', isDone => !isDone);
// => { id: 0, text: 'foo', isDone: true }

把所有的东西都连系到一起

actions和reducer准备好了,可以创建一个store,连接到我们的React组件中.

 // src/app.js

import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { TodoList } from './components';
import reducer from './reducer';

const store = createStore(reducer);

render(
  ,
  document.getElementById('app')
);

为了保持组件和store的独立,我们使用react-redux帮助简化这个过程.它允许我们创建独立于store的容器,包装所有的组件,我们不需要改变先前的设计.

我们需要一个容器包装组件,看看下面的内容

 // src/containers.js

import { connect } from 'react-redux';
import * as components from './components';
import { addTodo, toggleTodo } from './actions';

export const TodoList = connect(
  function mapStateToProps(state) {
    // ...
  },
  function mapDispatchToProps(dispatch) {
    // ...
  }
)(components.TodoList);

我们使用connect函数创建容器.当我们调用connect()函数,传递两个函数,mapStateToProps()mapDispatchToProps().

mapStateToProps()函数接收当前store的state作为参数,期待返回一个我们包装组件需要的对象映射.

 function mapStateToProps(state) {
  return { todos: state };
}

下面代码是一个包装组件根据映射map可视化的结果.

 

我们也需要提供mapDispatchProps函数,传递store的dispatch方法,所以我们可以使用action creatros来dispatch actions.

 function mapDispatchToProps(dispatch) {
  return {
    addTodo: text => dispatch(addTodo(text)),
    toggleTodo: id => dispatch(toggleTodo(id))
  };
}

再一次实例化组件

  dispatch(addTodo(text))}
          toggleTodo={id => dispatch(toggleTodo(id))} />

现在我们已经把action creators映射到组件,可以从事件监听中调用.

 export function TodoList(props) {
  const { todos, toggleTodo, addTodo } = props;

  const onSubmit = (event) => {
    const input = event.target;
    const text = input.value;
    const isEnterKey = (event.which == 13);
    const isLongEnough = text.length > 0;

    if(isEnterKey && isLongEnough) {
      input.value = '';
      addTodo(text);
    }
  };

  const toggleClick = id => event => toggleTodo(id);

  return (
    
    {todos.map(t => (
  • ))}
); }

container容器自动订阅store的变化,只要的映射的props变化的时候,容器包装的组件就会重新渲染.

最后,需要使容器组件独立于store,使用组件.

 // src/app.js

import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import reducer from './reducer';
import { TodoList } from './containers';
//                          ^^^^^^^^^^

const store = createStore(reducer);

render(
  
    
  ,
  document.getElementById('app')
);

结论

不可否认,对于初学者来说,React和Redux的生态系统是相当复杂和令人迷惑的.
但是好消息是这些概念是可以可以转移的.我们仅仅粗略的接触了Redux的基础构架,但是已经足够我们学习Elm 构架,或者选取ClojureScript库例如:Om,Re-frame.类似的,我们仅仅看到immutable数据结构的只言片语,但是已经足够我们学习Clojure或者Haskell.

不管你是刚开始探索有关state的web编程开发者,还是使用javascript很长时间的开发者,基于action构架的办成和immutable数据结构变得至观重要的技能.所以现在是学习这些内容的时间了.

你可能感兴趣的:(翻译|如何使用React,Redux和Immutable.js构建Todo App)