兔兔的奶爸

如何使用 React, Redux, and Immutable.js构建一个待办事项APP

兔兔的奶爸 · 2017-03-31翻译 · 839阅读 原文链接

这篇文章于2016年5月3日重写。原文中的评论已被删除

React 使用组件和单向数据流的方式,使其成为描述用户界面结构的理想选择。然而组件与状态工作的接口却一直故意设计的很简单,以提醒我们 React 只是传统Model-View-Controller 模型中的视图

作者的更多文章

没有什么可以阻止我们只使用 React 来构建大型应用,但是很快的,你就会发现,为了保持代码足够简洁,可能需要在其他地方管理状态。

虽然没有处理应用程序状态的官方解决方案,但是有些库与React的范例特别吻合。今天我们将React与两个这样的库配对,并使用它们构建一个简单的应用程序。

Redux

Redux 是一个极小的库,通过结合 FluxElm 的理论并加以完善,它可以充当我们应用程序状态的容器, 我们可以使用 Redux 管理任何种类应用的状态,下面提供我们需要遵守的两条准则:

  1. 单一数据源

  2. State 是只读的,惟一改变 state 的方法就是触发 action (原文:Changes come from actions not mutations

Redux 存储的核心函数是使用当前应用程序状态,操作并将其组合起来以创建新的应用程序状态的功能。 我们称这个函数为reducer

我们的React组件将负责将操作发送到store 中,反过来store 会告诉组件何时需要重新渲染。

ImmutableJS

由于Readux 不允许我们直接修改应用的 state,所以通过使用不可变数据结构对应用程序状态进行建模来实施这一点是有帮助的。

ImmutableJS 通过从Clojure和Scala中得到的启发为我们提供了许多不可变的数据结构,并且提供了以高效的方式实现的改变数据的接口。

Demo

我们将使用React结合redux和 ImmutableJS 构建一个简单的待办事项清单应用。它允许我们添加待办事项,并可以在完成和未完成间进行切换。

CodePen中查看 SitePoint (@SitePoint)的React, Redux & Immutable Todo 源码。

这份代码也可以通过github)查看。

Setup

我们将从创建项目目录和使用npm init初始化一个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

我们将使用 JSXES2015,所以我们将使用 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对源码进行编译。

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

我们需要在我们想编译代码的时候执行 npm run build 命令。

React 和组建

在我们编写任何组件之前,创建一些虚拟数据可能会有所帮助。这样可以让我们清楚我们的组件需要渲染什么。

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 组件TodoTodoList

// src/components.js

import React from 'react';

export function Todo(props) {
  const { todo } = props;
  if(todo.isDone) {
    return <strike>{todo.text}</strike>;
  } else {
    return <span>{todo.text}</span>;
  }
}

export function TodoList(props) {
  const { todos } = props;
  return (
    <div className='todo'>
      <input type='text' placeholder='Add todo' />
      <ul className='todo__list'>
        {todos.map(t => (
          <li key={t.id} className='todo__item'>
            <Todo todo={t} />
          </li>
        ))}
      </ul>
    </div>
  );
}

此刻,我们可以在项目目录中创建一个index.html用下面的代码来测试这些组件。(你可以在GitHub找到一个简单的样式表。)

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="style.css">
    <title>Immutable Todo</title>
  </head>
  <body>
    <div id="app"></div>
    ``<script src="bundle.js">``</script>
  </body>
</html>

我们还需要在src/app.js有一个入口点(entry point);

// 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(
  <TodoList todos={dummyTodos} />,
  document.getElementById('app')
);

使用 npm run build命令编译代码之后,在你的浏览器打开 index.html文件,并确保它可以工作。

Redux & ImmutableJS

现在我们感觉用户界面还不错,我们可以开始考虑它背后的状态。假数据是一个很好的切入点,我们可以很容易把它转换成ImmutableJS Map

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 MapJavaScript对象的工作方式不同,所以我们需要对组件进行一些微调。在每个属性访问的(例如todo.id)地方都需要改为方法调用(todo.get('id'))。

设计 Actions

现在已经搞定了shape和机构,我们可以开始考虑Action 如何更新它。在这个实例中我们需要两个 Action,一个用于添加待办事项,另一个用于切换现有事项的状态(完成/未完成)。

定义一些方法来创建这些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
  }
}

Each action is just a JavaScript object with a type and payload properties. The type property helps us decide what to do with the payload when we process the action later. 每个action只是一个具有 typepayload 属性的 JavaScript 对象。当我们稍后处理操作时,type属性可以帮助我们决定如何处理相应payloadtodo

设计一个 Reducer

现在我们知道状态的样子和更新它的动作,我们可以构建我们的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_TODO action是很简单的,因为我们可以使用 .push() 方法,这个方法将返回一个新的list将待办事项添加在todos的末尾。

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

注意,在我们pushlist之前,我们将todo object转换为了一个immutable map

我们需要处理的更复杂的actionTOGGLE_TODO

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

我们使用.map() 迭代list,找到与actionid匹配的todo。然后调用.update() 方法,它接受一个key和一个function,然后返回一个从map中复制的新的todo。这个todo的值会使用调用update方法并传递初始值返回的结果替换。

这是可能有助于查看的文字版本。

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

把所有东西关联起来

现在我们需要用准备好的actionsreducer创建一个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(
  <TodoList todos={store.getState()} />,
  document.getElementById('app')
);

我们需要让我组件知道这个store。使用 react-redux来帮助简化这个过程。它允许创建一个store-aware容器包裹我们的组件,因此我们没有改变原有的接口。

我们将在我们需要的“组件”周围包裹一个container。来看看这是什么样的。

// 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);

当我们传递两个functionmapStateToProps()mapDispatchToProps()调用 connect() 时,我们就使用connect()方法创建了容器。 connect() mapStateToProps 方法将store的当前状态作为一个参数(在这个例子里是代办事件的list),然后它期望结过是描述从该状态到wrapped component的属性对象。

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

它可能有助于理解如何在React组件的实例上操作。

`<TodoList todos={state} />`

我们也需要提供一个mapDispatchToProps方法,它传递storedispatch方法,所以我们可以使用它分发从 action creators中的actions

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

再一次,下面的代码可能有助于了解这些属性如何同时在React组件的实例上操作。

<TodoList todos={state}
          addTodo={text => dispatch(addTodo(text))}
          toggleTodo={id => dispatch(toggleTodo(id))} />

现在我们已经通过action creators映射了我们的组件,这时可以在event listeners中调用他们。

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 (
    <div className='todo'>
      <input type='text'
             className='todo__entry'
             placeholder='Add todo'
             onKeyDown={onSubmit} />
      <ul className='todo__list'>
        {todos.map(t => (
          <li key={t.get('id')}
              className='todo__item'
              onClick={toggleClick(t.get('id'))}>
            <Todo todo={t.toJS()} />
          </li>
        ))}
      </ul>
    </div>
  );
}

容器会自动订阅store的改变,并且当他们绑定的属性值改变时会重新渲染被包裹的组件。

最后,我们需要使用Provider组件来让容器知道这个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(
  <Provider store={store}>
    <TodoList />
  </Provider>,
  document.getElementById('app')
);

总结

不可否认,对初学者而言,整个React生态和Redux确实有点复杂,但好消息是,几乎所有这些概念都是可替换的。我们几乎没有碰到Redux的架构,但当我们开始了解The Elm Architecture,或者提到ClojureScript的库像 Om 或者 Re-frame的时候,我们已经得到了足够多的帮助。同样,我们只看到一些不可变数据的可能性,但是现在我们更好地开始学习一种语言,如 ClojureHaskell

无论您是探索Web应用程序开发的状态,还是花费整天编写JavaScript;基于action和不可变数据架构的经验,已经成为开发人员的重要技能,现在正是学习它的好时机。

相关文章