前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >用Flux实现TodoMVC

用Flux实现TodoMVC

作者头像
IMWeb前端团队
发布2019-12-03 17:30:42
8280
发布2019-12-03 17:30:42
举报
文章被收录于专栏:IMWeb前端团队IMWeb前端团队

本文作者:IMWeb frankfang 原文出处:IMWeb社区 未经同意,禁止转载

本文通过实现一个 TodoMVC 应用来说明一个 Flux 应用的结构是怎样的。本文会告诉你如何一步一步地实现这个应用,完整的源代码可以从 Github 下载。

首先,我们需要

  1. 基本的项目模板,方便把 jsx 文件编译为 js 文件
  2. 一个基于 CommonJS 的模块系统,因为JS本身并没有模块系统

我们可以从 react-boilerplate 这个模板开始。你应该已经安装了 Node.js 了吧,那么就直接从 github 将 react-boilerplate clone 下来,进入目录,依次运行 npm install npm run buildnpm start,然后我们的 jsx 文件就会被持续地编译为 js 文件了。

我们的 TodoMVC 应用也是基于这个模板。现在你可以按照 TodoMVC 的 package.json 来编辑你自己的 package.json 文件,确保你的文件结构和 dependencies 与 TodoMVC 的一致(译注:最好直接拷贝 package.json 到你自己的目录),否则下文中的代码可能无法运行。

目录结构

index.html 文件是应用的入口,它会加载 bundle.js,js 目录下的文件会被自动合并为 bundle.js,合成工作由 Browserify 负责。我们的项目目录的结构如下所示:

代码语言:javascript
复制
myapp
  |
  + ...
  + js
    |
    + actions
    + components // React 组件都放在这样,包括 views 和 controller-views
    + constants
    + dispatcher
    + stores
    + app.js
    + bundle.js // 由 Browserify 自动生成
  + index.html
  + ...

创建派发器(Dispatcher)

现在我们就可以创建派发器了。下面实现了一个简单的派发器,用到了 Promise,对于不支持 ES6 Promise 的浏览器,使用 es6-promise 来兼容。

代码语言:javascript
复制
// js/dispatcher/Dispatcher.js
var Promise = require('es6-promise').Promise;
var assign = require('object-assign');

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

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

  /**
   * 注册一个数据仓库的回调,可以被一个 action 触发。
   * Register a Store's callback so that it may be invoked by an action.
   * [@param](/user/param) {function} callback 需要注册的回调函数.
   * [@return](/user/return) {number} 返回 callback 在 _callback 数组中的索引.
   */
  register: function(callback) {
    _callbacks.push(callback);
    return _callbacks.length - 1; // index
  },

  /**
   * 派发
   * dispatch
   * [@param](/user/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;

这个派发器只有两个方法:注册(register)和派发(dispatch)。register() 用于注册一个回调函数。dispatch() 用于在动作(actions)发生后触发这些回调。

接下来我们创建 AppDispatcher,它基于 Dispatcher,只不过在 Dispatcher 的基础上添加了 handleViewAction 方法:

代码语言:javascript
复制
js/dispatcher/AppDispatcher.js
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](/user/param)  {object} action The data coming from the view.
   */
  handleViewAction: function(action) {
    this.dispatch({
      source: 'VIEW_ACTION',
      action: action
    });
  }

});

module.exports = AppDispatcher;

现在我们拥有了一个简单好用的派发器,它有一个 handleViewAction 方法用来处理视图(View)中发出的动作(Action)。如果之后我们需要向服务器发出请求,可以再添加一个方法,不过现在这样已经可以了。

创建数据仓库(Store)

你可以借助 Node 的事件发射器(EventEmitter)来创建数据仓库。事件发射器用来向控制视图(Controller-View)广播 change 事件。代码如下,为了简洁我删除了部分代码,完整的代码见 TodoStore.js

代码语言:javascript
复制
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

/**
 * 创建一个 Todo 项
 * [@param](/user/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
  };
}

/**
 * 删除一个 Todo 项
 * [@param](/user/param) {string} id
 */
function destroy(id) {
  delete _todos[id];
}

var TodoStore = assign({}, EventEmitter.prototype, {

  /**
   * 获取所有 Todo 项
   * [@return](/user/return) {object}
   */
  getAll: function() {
    return _todos;
  },

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

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

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

  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;

代码中有一下几点需要注意:

一,我们用一个私有数组 _todos 来存储所有 Todo 项。由于它是闭包内的一个局部变量,所以外部不能直接访问它,只能通过动作(action)来更新数据。借此我们就控制了数据的流动(the flow of data)。

二,注意数据仓库是如何注册回调的:把回调函数传给 AppDispather.register() ,然后保留派发器的索引(index)。目前回调函数只处理两种动作(TODO_CREATETODO_DESTROY),不过很快我们就会添加更多动作。

用控制视图(Controller-View)监听数据变化

我们需要在组件的顶层添加一个组件来监听数据的所有变化。在大型项目中,你可能需要不止一个这样的组件,比如为页面的每个区块创建一个控制视图。在 Facebook 的广告创建工具中,我们有很多这样的控制视图,每个视图负责页面上的一块 UI。在我们的视频编辑器项目中,我们只有两个这样的组件,一个负责动画预览界面,一个负责图片选取界面。

下面是我们的实现。同样它被简化过,完整的代码见 TodoApp.react.js

代码语言:javascript
复制
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](/user/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 的诸多与组件生命周期相关的方法:

  • getInitialSate() 中对视图进行初始化
  • componentDidMount() 中创建事件监听
  • componentWillUnmount() 中清理现场
  • 然后从 TodoStore 中拿到所有数据,填充到一个 div 容器中,最终渲染到页面上

Header 组件只包含文字输入框,不需要数据; MainSection 组件和 Footer 组件则需要数据。

更多视图

我们整个应用的 React 组件结构是这样的:

代码语言:javascript
复制
<TodoApp>
  <Header>
    <TodoTextInput />
  </Header>

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

</TodoApp>

当 TodoItem 组件处于编辑状态时,它还会渲染出一个 TodoTextInput 组件作为其子元素。

现在我们来看看

  1. 这些组件是如何将 props 中是数据展现出来的。
  2. 这些组件是如何通过动作来与派发器通信的。

MainSection 通过遍历 TodoApp 传来的数据,来创建一组 TodoItem。在 MainSection 的 render() 方法如下:

代码语言:javascript
复制
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>
);

每个 TodoItem 显示自己的内容,触发动作时会带上自己的 ID。本文不打算把 TodoItem 触发的所有动作都讲到,只以删除动作为例。下面是一个 TodoItem 的简单实现:

代码语言:javascript
复制
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;

TodoActions 已经实现了 destroy 方法,而且数据仓库会负责更新数据,这样我们就能很容易地将用户的操作与应用的状态(Application State)连接起来。我们只需在点击事件里调用 destroy 方法,并传入 Todo 项的 ID,就行了。

现在用户一点击删除按钮,Flux 的数据流就会启动,页面的状态就会相应地发生变化。

输入框稍微复杂一点,因为你要在 TodoTextInput 里单独维护组件自己的状态。那么我们就来看看应该如何来实现。

React 建议一旦输入框的值有变化,组件的状态就应该立即做出相应的变化。所以我们可以把值保存在组件的状态中。你要记住这是组件的状态(UI State),而不是应用的状态(Application State)。

应用的状态永远存在数据仓库中,而组件的状态由组件自行维护。理想的情况下,组件的状态应该尽量少。

由于 TodoTextInput 会在应用的多个地方用到,每个地方可能有不同的行为,所以我们需要父组件把 onSave 作为 prop 参数传给 TodoTextInput。

代码语言:javascript
复制
var React = require('react');
var ReactPropTypes = React.PropTypes;

var ENTER_KEY_CODE = 13;

var TodoTextInput = React.createClass({

  propTypes: {
    className: ReactPropTypes.string,
    id: ReactPropTypes.string,
    placeholder: ReactPropTypes.string,
    onSave: ReactPropTypes.func.isRequired,
    value: ReactPropTypes.string
  },

  getInitialState: function() {
    return {
      value: this.props.value || ''
    };
  },

  /**
   * [@return](/user/return) {object}
   */
  render: function() /*object*/ {
    return (
      <input
        className={this.props.className}
        id={this.props.id}
        placeholder={this.props.placeholder}
        onBlur={this._save}
        onChange={this._onChange}
        onKeyDown={this._onKeyDown}
        value={this.state.value}
        autoFocus={true}
      />
    );
  },

  /**
   * Invokes the callback passed in as onSave, allowing this component to be
   * used in different ways.
   */
  _save: function() {
    this.props.onSave(this.state.value);
    this.setState({
      value: ''
    });
  },

  /**
   * [@param](/user/param) {object} event
   */
  _onChange: function(/*object*/ event) {
    this.setState({
      value: event.target.value
    });
  },

  /**
   * [@param](/user/param) {object} event
   */

  _onKeyDown: function(event) {
    if (event.keyCode === ENTER_KEY_CODE) {
      this._save();
    }
  }

});

module.exports = TodoTextInput;

Header 组件传给 TodoTextInput 的 onSave 会新建一个 Todo 项。

代码语言:javascript
复制
var React = require('react');
var TodoActions = require('../actions/TodoActions');
var TodoTextInput = require('./TodoTextInput.react');

var Header = React.createClass({

  /**
   * [@return](/user/return) {object}
   */
  render: function() {
    return (
      <header id="header">
        <h1>todos</h1>
        <TodoTextInput
          id="new-todo"
          placeholder="What needs to be done?"
          onSave={this._onSave}
        />
      </header>
    );
  },

  /**
   * Event handler called within TodoTextInput.
   * Defining this here allows TodoTextInput to be used in multiple places
   * in different ways.
   * [@param](/user/param) {string} text
   */
  _onSave: function(text) {
    TodoActions.create(text);
  }

});

module.exports = Header;

以此类推,在 TodoItem 的编辑模式中,我们可以把 TodoActions.update(text) 作为 onSave 的值。

创建语意化的动作

下面我们用到的两个动作 create 和 destroy 的简单实现:

代码语言:javascript
复制
/**
 * TodoActions
 */

var AppDispatcher = require('../dispatcher/AppDispatcher');
var TodoConstants = require('../constants/TodoConstants');

var TodoActions = {

  /**
   * [@param](/user/param)  {string} text
   */
  create: function(text) {
    AppDispatcher.handleViewAction({
      actionType: TodoConstants.TODO_CREATE,
      text: text
    });
  },

  /**
   * [@param](/user/param)  {string} id
   */
  destroy: function(id) {
    AppDispatcher.handleViewAction({
      actionType: TodoConstants.TODO_DESTROY,
      id: id
    });
  },

};

module.exports = TodoActions;

你可能会说,AppDispatcher.handleViewActionTodoActions.create() 是多余的。确实,理论上我们可以直接调用 AppDispatcher.dispatch() 来达到目的。但是随着我们的项目越来越大,添加这些方法能保持代码干净易读。

传给 TodoAction.create() 的数据结构如下:

代码语言:javascript
复制
{
  source: 'VIEW_ACTION',
  action: {
    type: 'TODO_CREATE',
    text: 'Write blog post about Flux'
  }
}

数据是通过 TodoStore 注册的回调函数送达给 TodoStore 的。然后 TodoStore 广播 change 事件,MainSection 听到消息,拿到最新数据,更新自己的状态。状态的更新使得 TodoApp 组件的 render() 方法被触发,TodoApp 所有后代组件的 render() 方法也被触发。

启动 React

应用的启动文件是 app.js,其内容很简单,就是拿到 TodoApp,然后在网页中渲染它。

代码语言:javascript
复制
var React = require('react');

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

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

为 Dispatcher 添加依赖管理

上文说过,我们实现的 Dispatcher 太简单。虽然它可以用,但是对大多数应用来说,它还不够好。因为我们需要管理储存时的依赖关系,有些数据的存储要等其他数据存完了才能进行。那么我们给 Dispatcher 添加一个 waitFor() 方法吧。

waitFor() 是一个公共方法,返回一个 Promise 对象:

代码语言:javascript
复制
/**
   * [@param](/user/param)  {array} promisesIndexes
   * [@param](/user/param)  {function} callback
   */
  waitFor: function(promiseIndexes, callback) {
    var selectedPromises = promiseIndexes.map(function(index) {
      return _promises[index];
    });
    return Promise.all(selectedPromises).then(callback);
  }

现在我们就可以在 TodoStore 里,等数据更新好了之后,再继续其他操作。

不过,有可能会出现循环依赖。一个更加健壮的 Dispatcher 应该在遇到循环依赖时,在控制台里发出警告。

未来会做的事情

很多人问 Facebook 是否会将 Flux 开源。请不要搞笑,Flux 只是一种架构,不是框架,如何发布呢?不过发布一套 Flux 模板倒是有可能的,前提是有足够多的人需要。如果你有需要,请告诉我们。

原文(有删改)

(全文完)

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2015-06-02 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目录结构
  • 创建派发器(Dispatcher)
  • 创建数据仓库(Store)
  • 用控制视图(Controller-View)监听数据变化
  • 更多视图
  • 创建语意化的动作
  • 启动 React
  • 为 Dispatcher 添加依赖管理
  • 未来会做的事情
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档