flux demo todomvc


look

index.html文件只有用到js/bundle.js 文件, 然后bundle.js哪里来, 看看package.json,

  "scripts": {
    "start": "watchify -o js/bundle.js -v -d js/app.js",
    "build": "browserify . -t [envify --NODE_ENV production] | uglifyjs -cm > js/bundle.min.js",
    "test": "jest"
  }

app.js 作为入口, 然后监视, 一旦发生改变就回编译到 bundle.js.

然后接着看, app.js 内容:

var React = require('react');

var TodoApp = require('./components/TodoApp.react'); // here

React.render(
  <TodoApp />,
  document.getElementById('todoapp')
);

一个TodoApp 搞定一切. 至于它何来头这就要开篇讲了.

创建一个 dispatcher

创建一个 dispatcher, 下面是一个简单例子,使用了 javascript 的 promises


var Promise = require('es6-promise').Promise;
var assign = require('object-assign');

var _callbacks = [];
var _promises = [];

var Dispatcher = function() {};
Dispatcher.prototype = assign({}, Dispatcher.prototype, {

  /**
   * Register a Store's callback so that it may be invoked by an action.
   * @param {function} callback The callback to be registered.
   * @return {number} The index of the callback within the _callbacks array.
   */
  register: function(callback) {
    _callbacks.push(callback);
    return _callbacks.length - 1; // index
  },

  /**
   * dispatch
   * @param  {object} payload The data from the action.
   */
  dispatch: function(payload) {
    // First create array of promises for callbacks to reference.
    var resolves = [];
    var rejects = [];
    _promises = _callbacks.map(function(_, i) {
      return new Promise(function(resolve, reject) {
        resolves[i] = resolve;
        rejects[i] = reject;
      });
    });
    // Dispatch to callbacks and resolve/reject promises.
    _callbacks.forEach(function(callback, i) {
      // Callback can return an obj, to resolve, or a promise, to chain.
      // See waitFor() for why this might be useful.
      Promise.resolve(callback(payload)).then(function() {
        resolves[i](payload);
      }, function() {
        rejects[i](new Error('Dispatcher callback unsuccessful'));
      });
    });
    _promises = [];
  }
});

module.exports = Dispatcher;

这个dispatch 只公开了2个方法register()dispatch(),我们将会使用register() 来给store 注册回调函数, 通过dispatch() 来触发这些 action 完成回调.

那好我们来看看下面代码:

var Dispatcher = require('./Dispatcher');
var assign = require('object-assign');

var AppDispatcher = assign({}, Dispatcher.prototype, {

  /**
   * A bridge function between the views and the dispatcher, marking the action
   * as a view action.  Another variant here could be handleServerAction.
   * @param  {object} action The data coming from the view.
   */
  handleViewAction: function(action) {
    this.dispatch({
      source: 'VIEW_ACTION',
      action: action
    });
  }

});

module.exports = AppDispatcher;

创建 stores

我们可以通过nodeEventEmitter 来创建store, 我们需要通过EventEmitter 来广播change 事件给相关的controller-view 来看看到底长啥样,

var AppDispatcher = require('../dispatcher/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var TodoConstants = require('../constants/TodoConstants');
var assign = require('object-assign');

var CHANGE_EVENT = 'change';

var _todos = {}; // collection of todo items

/**
 * Create a TODO item.
 * @param {string} text The content of the TODO
 */
function create(text) {
  // Using the current timestamp in place of a real id.
  var id = Date.now();
  _todos[id] = {
    id: id,
    complete: false,
    text: text
  };
}

/**
 * Delete a TODO item.
 * @param {string} id
 */
function destroy(id) {
  delete _todos[id];
}

// 继承了 EventEmitter
var TodoStore = assign({}, EventEmitter.prototype, {

  /**
   * Get the entire collection of TODOs.
   * @return {object}
   */
  getAll: function() {
    return _todos;
  },

  emitChange: function() {
    this.emit(CHANGE_EVENT);
  },

  /**
   * @param {function} callback
   */
  addChangeListener: function(callback) {
    this.on(CHANGE_EVENT, callback);
  },

  /**
   * @param {function} callback
   */
  removeChangeListener: function(callback) {
    this.removeListener(CHANGE_EVENT, callback);
  },

  // 注册 action
  dispatcherIndex: AppDispatcher.register(function(payload) {
    var action = payload.action;
    var text;

    switch(action.actionType) {
      case TodoConstants.TODO_CREATE:
        text = action.text.trim();
        if (text !== '') {
          create(text);
          TodoStore.emitChange();
        }
        break;

      case TodoConstants.TODO_DESTROY:
        destroy(action.id);
        TodoStore.emitChange();
        break;

      // add more cases for other actionTypes, like TODO_UPDATE, etc.
    }

    return true; // No errors. Needed by promise in Dispatcher.
  })

});

module.exports = TodoStore;

上面代码里有些地方需要说明下, 从最开始的时候我们创建了一个_ tods 的私有对象,这个对象包含了所有的单独todilist 的 item, 因为它在 class 外面存在. 但是却它却却在于 module 的这个闭包里, 这样它就不可以在 module 意外的地方呗修改, 这样就保证了一个直接进出的接口给我们的数据流,保证只有在通过 action 的情况下才可以修改数据.

另外一个地方就是注册 action 的回调函数到dispatcher 上,我们把气球的数据交给个回调函数来处理,并且把它注册在 dispatcher 里.当前我们只负责两个不同的actiontype. 之后我们可以添加更多.

通过controller-view 来监听修改事件

我们需要一个位于组建体系的最上层的React 组件来监听所有的change 事件.在大型应用中我们可能需要更多的组件来监听.可能一个页面里每一个 section 都需要一个监听组件. 下面来看看简单的代码示例, 完整的可以查看github repo todomvc.

var Footer = require('./Footer.react');
var Header = require('./Header.react');
var MainSection = require('./MainSection.react');
var React = require('react');
var TodoStore = require('../stores/TodoStore');

function getTodoState() {
  return {
    allTodos: TodoStore.getAll()
  };
}

var TodoApp = React.createClass({

  getInitialState: function() {
    return getTodoState();
  },

  componentDidMount: function() {
    TodoStore.addChangeListener(this._onChange);
  },

  componentWillUnmount: function() {
    TodoStore.removeChangeListener(this._onChange);
  },

  /**
   * @return {object}
   */
  render: function() {
    return (
      <div>
        <Header />
        <MainSection
          allTodos={this.state.allTodos}
          areAllComplete={this.state.areAllComplete}
        />
        <Footer allTodos={this.state.allTodos} />
      </div>
    );
  },

  _onChange: function() {
    this.setState(getTodoState());
  }

});

module.exports = TodoApp;

到这里,我们已经进入 React的世界,通过使用 react 的生命周期方法,通过getInitialState()初始化这个controller-vew, 然后通过componentDidMount()来完成事件监听, 然后通过componentWillUnmount()来完成清理. 我们渲染了一个div 然后把从 store 哪里拿到的数据填充进去.

关于视图

站在更高一些的角度,我们可以把当前的 React 组件系统理解成如下的结构:

<TodoApp>
  <Header>
    <TodoTextInput />
  </Header>

  <MainSection>
    <ul>
      <TodoItem />
    </ul>
  </MainSection>

</TodoApp>

如果当前进入一个编辑模式,它还会渲染一个TodoTextInput的子类, 让我们来看看这些组件是如何从props 哪里拿到组件然后展示他们的, 然后还有他们如何在 action 和 dispatcher 之间交互通信. 这个MainSection 需要遍历todo item 从 Todoapp 那拿来的 list. 在组件的render() 方法里我们可以这样做:

var allTodos = this.props.allTodos;

for (var key in allTodos) {
  todos.push(<TodoItem key={key} todo={allTodos[key]} />);
}

return (
  <section id="main">
    <ul id="todo-list">{todos}</ul>
  </section>
);

到这里, 我们可以展示每一个 item, 通过 id 可以编辑, 同样可以删除一个 item

var React = require('react');
var TodoActions = require('../actions/TodoActions');
var TodoTextInput = require('./TodoTextInput.react');

var TodoItem = React.createClass({

  propTypes: {
    todo: React.PropTypes.object.isRequired
  },

  render: function() {
    var todo = this.props.todo;

    return (
      <li
        key={todo.id}>
        <label>
          {todo.text}
        </label>
        <button className="destroy" onClick={this._onDestroyClick} />
      </li>
    );
  },

  _onDestroyClick: function() {
    TodoActions.destroy(this.props.todo.id);
  }

});

module.exports = TodoItem;

创建一些语义化的动作方法