分享

Facebook 的 Flux 应用架构介绍


问题导读

1.Flux的作用是什么?
2.在比较Flux和MVC时,需理解哪三件事?
3.Flux使事情可预测,为什么这么说?





Flux是由Facebook提出的,用于组织应用的一种架构,它基于一个简单的原则:数据在应用中单向流动。这就是所谓的“单向数据流”,简单的记法是把数据比作鲨鱼:鲨鱼只能向前游
Facebook公布了一些Flux的范例,至少有六种第三方库实现如雨后春笋般涌现。在本文中,当我们提及“Flux”时,我们讲的是Facebook的实现
一个Flux例子
为理解 Flux,咱们来完整做一个 Todo 基本应用。在 Facebook 的 Flux 代码库,可以得到该项目的完整代码。

加载ToDo条目
1.jpg
当应用启动的时候,ToDoApp的响应模块获得存储在ToDoStore中的数据并展示,ToDoStore完全不知道ToDoApp的模块。如果把模块看做是View部分、ToDoStore看做Model部分,那么目前为止,这和MVC没什么不同。

  1. //TodoApp1.react.js
  2. // Loading the initial data into the application:
  3. // ...
  4. /**
  5. * Retrieve the current TODO data from the TodoStore
  6. */
  7. function getTodoState() {
  8.   return {
  9.     allTodos: TodoStore.getAll(),
  10.     areAllComplete: TodoStore.areAllComplete()
  11.   };
  12. }
  13. var TodoApp = React.createClass({
  14.   getInitialState: function() {
  15.     return getTodoState();
  16.   },
  17. // ...
复制代码


在这个简单的例子中,我们不关心 ToDoStore 如何加载初始化数据。

创建一个新的ToDo条目
2.jpg
ToDoApp组件有一个用于创建新条目的表格,当用户提交了表格后,它就会如上图演示的那样,从Flux系统中踢出一条数据流。
1. 组件通过调用自己的回调方法来处理表格提交。

  1. // Header1.react.js
  2. // Saving a new ToDo calls the '_onSave' callback
  3. // ...
  4. var Header = React.createClass({
  5.   /**
  6.    * @return {object}
  7.    */
  8.   render: function() {
  9.     return (
  10.       <header id="header">
  11.         <h1>todos</h1>
  12.         <TodoTextInput
  13.           id="new-todo"
  14.           placeholder="What needs to be done?"
  15.           onSave={this._onSave}
  16.         />
  17.       </header>
  18.     );
  19.   },
  20. // ...
复制代码


2.  组件回调方法调用ToDoAction的Create方法。
  1. // Header2.react.js
  2. // The '_onSave' callback calls the 'TodoActions' method to create an action
  3. // ...
  4.   /**
  5.    * Event handler called within TodoTextInput.
  6.    * Defining this here allows TodoTextInput to be used in multiple places
  7.    * in different ways.
  8.    * @param {string} text
  9.    */
  10.   _onSave: function(text) {
  11.     if (text.trim()){
  12.       TodoActions.create(text);
  13.     }
  14.   }
复制代码
3. ToDoAction创建一个TODO_CREATE类型的动作。
  1. // TodoActions.js
  2. // The 'create' method creates an action of type 'TODO_CREATE'
  3. // ...
  4. var TodoActions = {
  5.   /**
  6.    * @param  {string} text
  7.    */
  8.   create: function(text) {
  9.     AppDispatcher.handleViewAction({
  10.       actionType: TodoConstants.TODO_CREATE,
  11.       text: text
  12.     });
  13.   },
  14. // ...
复制代码


4. 该动作被发送到调度器。

5. 调度器把该动作传递到Store中所有注册了该动作的回调方法中。


  1. // AppDispatcher.js
  2. // The 'handleViewAction' dispatches the action to all stores.
  3. // ...
  4. var Dispatcher = require('flux').Dispatcher;
  5. var assign = require('object-assign');
  6. var AppDispatcher = assign(new Dispatcher(), {
  7.   /**
  8.    * A bridge function between the views and the dispatcher, marking the action
  9.    * as a view action.  Another variant here could be handleServerAction.
  10.    * @param  {object} action The data coming from the view.
  11.    */
  12.   handleViewAction: function(action) {
  13.     this.dispatch({
  14.       source: 'VIEW_ACTION',
  15.       action: action
  16.     });
  17.   }
  18. });
  19. // ...
复制代码
6. ToDoStore有一个注册了的监听TODO_CREATE动作的回调方法,因此更新了自己的数据。

  1. // TodoStore1.js
  2. // The TodoStore has registered a callback for the 'TODO_CREATE' action.
  3. // ...
  4. /**
  5. * Create a TODO item.
  6. * @param  {string} text The content of the TODO
  7. */
  8. function create(text) {
  9.   // Hand waving here -- not showing how this interacts with XHR or persistent
  10.   // server-side storage.
  11.   // Using the current timestamp + random number in place of a real id.
  12.   var id = (+new Date() + Math.floor(Math.random() * 999999)).toString(36);
  13.   _todos[id] = {
  14.     id: id,
  15.     complete: false,
  16.     text: text
  17.   };
  18. }
  19. // Register to handle all updates
  20. AppDispatcher.register(function(payload) {
  21.   var action = payload.action;
  22.   var text;
  23.   switch(action.actionType) {
  24.     case TodoConstants.TODO_CREATE:
  25.       text = action.text.trim();
  26.       if (text !== '') {
  27.         create(text);
  28.       }
  29.       break;
  30. // ...
复制代码


7. 在更新了自己的数据后,ToDoStore发出了一个变更事件。

  1. // TodoStore2.js
  2. // TodoStore emits a 'change' event after handling the action.
  3. // ...
  4. // Register to handle all updates
  5. AppDispatcher.register(function(payload) {
  6.   var action = payload.action;
  7.   var text;
  8.   switch(action.actionType) {
  9.     case TodoConstants.TODO_CREATE:
  10.       text = action.text.trim();
  11.       if (text !== '') {
  12.         create(text);
  13.       }
  14.       break;
  15. // ...
  16.     default:
  17.       return true;
  18.   }
  19.   // This often goes in each case that should trigger a UI change. This store
  20.   // needs to trigger a UI change after every view action, so we can make the
  21.   // code less repetitive by putting it here.  We need the default case,
  22.   // however, to make sure this only gets called after one of the cases above.
  23.   TodoStore.emitChange();
  24.   return true; // No errors.  Needed by promise in Dispatcher.
  25. });
  26. // ...
复制代码
8.ToDoApp组件监听到了ToDoStore的变更事件,并基于ToDoStore中最新的数据重新渲染了UI。
  1. // TodoApp2.react.js
  2. // The component listens for changes and calls the '_onChange' callback
  3. // ...
  4. var TodoApp = React.createClass({
  5.   getInitialState: function() {
  6.     return getTodoState();
  7.   },
  8.   componentDidMount: function() {
  9.     TodoStore.addChangeListener(this._onChange);
  10.   },
  11.   componentWillUnmount: function() {
  12.     TodoStore.removeChangeListener(this._onChange);
  13.   },
  14. // ...
  15.   /**
  16.    * Event handler for 'change' events coming from the TodoStore
  17.    */
  18.   _onChange: function() {
  19.     this.setState(getTodoState());
  20.   }
  21. // ...
复制代码
Flux 与 MVC 对比
Flux是作为MVC的一种替代而问世的,其文档解释说它“通过支持单向数据流回避了MVC”。在比较Flux和MVC时,需理解三件事:
  • 在JavaScript中,“MVC”实际上指的是“MV*”。
  • Flux并不比MV*简单。
  • 相比于MV*,Flux使事情更有可能预测。
在JavaScript中,“MVC”实际上指的是“MV*”
为了比较Flux和MVC,必须首先搞清楚,我们所说的MVC的含义。
ToDoMVC(一个测评站点)上给出的15个 JS 框架中,没有一个是严格实现了“Model、View、Control”的设计模式。以Backbone.js为例:它拥有model和view部分,但可以说是不含有controller(控制器)的。许多JavaScript框架中控制器的角色都被view或者model吸收了,并且可能存在其他的功能类别,如路由器。
当我们用“MVC”或者“MV*”来描述JavaScript架构时,我们一般是指在处理业务逻辑和用户交互时,其关注点是分离的;在展示和用户交互时,其数据存储是分割为不同的“model”的。
这个过程可能是如下方式:view从model中得到信息并展示给用户,然后用户与view交互,这些交互触发view从model中获得更新数据,这可能会出发view中的用户交互的更新。
题图:基本的MVC数据流
3.jpg


Flux并不比MV*简单
图题:复杂的MVC数据流
4.jpg
你可能已经观看过Facebook对Flux的介绍(注:YouTube上的一个视频),以及关于为什么“MVC不扩展”的分析,包括下图中7对不同的model和view之间的数据流动:
这让MVC看起来特别令人困惑——看那一堆箭头!谁能理清图中究竟发生了什么?所以看起来Flux会比较简单一点,是不?
但是在视频中,我们没能看到在Flux的实现中复杂的层面,一切都“简化”到一个简单的数据流。
图题:基本的Flux数据流
5.jpg
现在有必要看看一个复杂系统的Flux实现的样貌了。如下图所示,你会发现相比于MVC,这里有更多的剪头和图标,而不是更少。
图题:复杂的Flux数据流
6.jpg
Flux实际上和MV*拥有的组件数目是相同的,这也是为什么上图中它看起来和MV*一样复杂的原因——但是有个关键性的差别是:所有的剪头都指向一个方向,在整个系统中形成一个闭环。

Flux使事情可预测
在Flux和MV*的图中,都有很多事情在进行,但在可预测层面上,Flux有更好的表现。
Flux中的调度器保证系统中同时只有一个事务流,如果调度器在处理完一个已存在的事务之前收到另一个事务,则会抛出一个错误:
“未捕获错误:违反不变性:Dispatch.dispatch(…):不能在已调度中途再调度。”
这是另一种让事情变得可预测的方式,它迫使开发者构建数据资源无复杂交互的应用。
调度器还允许开发者通过使用waitFor方法,使得store在执行回调方法前等待其他的store,从而指定各个store执行回调方法的顺序,如果代码中出现了两个store互相等待的情况,调度器会抛出一个详实的错误
在Flux的Facebook实现中,可以清楚地看到数据改变的原因,每个store都包含了其监听的任务列表。

  1. // ThreadStore.js
  2. // The case statement documents which actions this store listens to
  3. // ...
  4. ThreadStore.dispatchToken = ChatAppDispatcher.register(function(payload) {
  5.   var action = payload.action;
  6.   switch(action.type) {
  7.     case ActionTypes.CLICK_THREAD:
  8.       _currentID = action.threadID;
  9.       _threads[_currentID].lastMessage.isRead = true;
  10.       ThreadStore.emitChange();
  11.       break;
  12.     case ActionTypes.RECEIVE_RAW_MESSAGES:
  13.       ThreadStore.init(action.rawMessages);
  14.       ThreadStore.emitChange();
  15.       break;
  16.     default:
  17.       // do nothing
  18.   }
  19. });
  20. // ...
复制代码


在这个例子中,ThreadStore 监听 CLICK_THREAD 和 RECEIVE_RAW_MESSAGES 动作,如果store没有像预期一样更新,注册器回调方法会为我们提供启动调试的机会,基于此可以对它接收的所有动作做日志记录并坚持其数据的有效负载。

相似的,所有的组件也都维护了其监听的所有store的列表。

  1. // ThreadSection.react.js
  2. // Looking at the 'componentDidMount' will usually show
  3. // whic stores this component listens to.
  4. // ...
  5. function getStateFromStores() {
  6.   return {
  7.     threads: ThreadStore.getAllChrono(),
  8.     currentThreadID: ThreadStore.getCurrentID(),
  9.     unreadCount: UnreadThreadStore.getCount()
  10.   };
  11. }
  12. var ThreadSection = React.createClass({
  13.   getInitialState: function() {
  14.     return getStateFromStores();
  15.   },
  16.   componentDidMount: function() {
  17.     ThreadStore.addChangeListener(this._onChange);
  18.     UnreadThreadStore.addChangeListener(this._onChange);
  19.   },
  20.   componentWillUnmount: function() {
  21.     ThreadStore.removeChangeListener(this._onChange);
  22.     UnreadThreadStore.removeChangeListener(this._onChange);
  23.   },
  24. // ...
复制代码



上文中,我们看到了ThreadSection组件监听ThreadStore和UnreadThreadStore变更,如果我们始终使用该方法为组件建立对store变更的监听,那么我们就可以确认没有其他的store会影响到该组件的行为。
Flux分离了数据的接收和发送环节,所以在调试时,可以方便地跟踪数据流以便发现错误之处。
Flux的困难之处
在软件工程领域,每一个选择都是权衡之举,Flux也不例外,下面是已经认识到的不利之处:
  • 它牵扯到写更多的样板代码
  • 迁移现有资源是一项大任务
  • 在没有良好的组织结构的情况下,单元测试非常困难
相比于普遍认为足够处理数据流的数量的文件和代码行数,Flux确实在应用中加入了更多,这种情况下,为新的数据资源书写新代码要比在已存的Flux代码中添加新的内容要痛苦的多。未来我们可能通过引入代码生成器来快速建立Flux工程,使用Vim的snippet功能也能加快这个过程。
写一个新的项目是体验Flux最容易的方法。说服他人接受新的事物总是有挑战的,本文、以及其他文档,还有来自Facebook的范例,可以为你提供教授他们的材料,你可以自信地认为,既然Facebook和其他公司都把Flux用于或大或小的生产环境,那么它也自然能用在你的项目中。
在把已有应用迁移到Flux时,可以一次一个地尝试将数据资源变为Flux架构。当考虑使用Flux管理应用中的一组数据时,要考虑到有多少组件使用了该数据。如果绝大部分的组件都使用了该数据,那么把数据迁移到Flux管理之下可能会是个大工程,首次尝试迁移到Flux时,从更孤立一些的数据开始。
使用Flux,你的组件开始依赖ActionCreator和Store,而且通常它们会互相依赖,这导致单元测试变得困难。把应用中与Store的交互重定向到顶层的“控制器”组件,那么在执行子组件的单元测试时,就无需担心Store的问题了。而为了测试那些确实需要发送动作并监听Store的组件,有一些成功的方法是伪造Store的方法,或伪造获响应动作以及Store获得数据的API。



来源:http://www.uml.org.cn/qiyezjjs/201504231.asp


已有(1)人评论

跳转到指定楼层
howtodown 发表于 2015-4-25 00:25:09
进一步补充


【Github热门项目】Flux:利用单向数据流实现的应用架构



Flux 是一个Facebook开发的、利用单向数据流实现的应用架构,用于 React。Flux应用有三个主要的部分组成:调度程序、存储和视图(React 组件)。
Facebook工程经理Tom Occhino说,由于他们“非常巨大”的代码库和庞大的组织,因而需要“以某种方式使代码结构化,使其更加可预测”。这已经通过 FluxReact 完成。Flux是一个系统架构,用于推进应用中的数据单向流动。React是一个JavaScript框架,用于构建“可预期的”和“声明式的”Web用户界面,它已经使Facebook更快地开发Web应用。
Flux 应用示例:
Flux 一个数据流周期:


  1. Views ---> (actions) ----> Dispatcher ---> (registered callback) ---> Stores -------+
  2. &#581;                                                                                   |
  3. |                                                                                   V
  4. +-- (Controller-Views "change" event handlers) ---- (Stores emit "change" events) --+
复制代码
Flux 相关文档:





回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

推荐上一条 /2 下一条