Flux是由Facebook提出的,用于组织应用的一种架构,它基于一个简单的原则:数据在应用中单向流动。这就是所谓的“单向数据流”,简单的记法是把数据比作鲨鱼:鲨鱼只能向前游。
Facebook公布了一些Flux的范例,至少有六种第三方库实现如雨后春笋般涌现。在本文中,当我们提及“Flux”时,我们讲的是Facebook的实现。
一个Flux例子
为理解 Flux,咱们来完整做一个 Todo 基本应用。在 Facebook 的 Flux 代码库,可以得到该项目的完整代码。
加载ToDo条目
当应用启动的时候,ToDoApp的响应模块获得存储在ToDoStore中的数据并展示,ToDoStore完全不知道ToDoApp的模块。如果把模块看做是View部分、ToDoStore看做Model部分,那么目前为止,这和MVC没什么不同。
//TodoApp1.react.js // Loading the initial data into the application: // ... /** * Retrieve the current TODO data from the TodoStore */ function getTodoState() { return { allTodos: TodoStore.getAll(), areAllComplete: TodoStore.areAllComplete() }; } var TodoApp = React.createClass({ getInitialState: function() { return getTodoState(); }, // ... |
在这个简单的例子中,我们不关心 ToDoStore 如何加载初始化数据。
创建一个新的ToDo条目
ToDoApp组件有一个用于创建新条目的表格,当用户提交了表格后,它就会如上图演示的那样,从Flux系统中踢出一条数据流。
1. 组件通过调用自己的回调方法来处理表格提交。
// Header1.react.js // Saving a new ToDo calls the '_onSave' callback // ... var Header = React.createClass({ /** * @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> ); }, // ... |
2. 组件回调方法调用ToDoAction的Create方法。
// Header2.react.js // The '_onSave' callback calls the 'TodoActions' method to create an action // ... /** * Event handler called within TodoTextInput. * Defining this here allows TodoTextInput to be used in multiple places * in different ways. * @param {string} text */ _onSave: function(text) { if (text.trim()){ TodoActions.create(text); } } |
3. ToDoAction创建一个TODO_CREATE类型的动作。
// TodoActions.js // The 'create' method creates an action of type 'TODO_CREATE' // ... var TodoActions = { /** * @param {string} text */ create: function(text) { AppDispatcher.handleViewAction({ actionType: TodoConstants.TODO_CREATE, text: text }); }, // ... |
4. 该动作被发送到调度器。
5. 调度器把该动作传递到Store中所有注册了该动作的回调方法中。
// AppDispatcher.js // The 'handleViewAction' dispatches the action to all stores. // ... var Dispatcher = require('flux').Dispatcher; var assign = require('object-assign'); var AppDispatcher = assign(new Dispatcher(), { /** * 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 }); } }); // ... |
6. ToDoStore有一个注册了的监听TODO_CREATE动作的回调方法,因此更新了自己的数据。
// TodoStore1.js // The TodoStore has registered a callback for the 'TODO_CREATE' action. // ... /** * Create a TODO item. * @param {string} text The content of the TODO */ function create(text) { // Hand waving here -- not showing how this interacts with XHR or persistent // server-side storage. // Using the current timestamp + random number in place of a real id. var id = (+new Date() + Math.floor(Math.random() * 999999)).toString(36); _todos[id] = { id: id, complete: false, text: text }; } // Register to handle all updates 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); } break; // ... |
7. 在更新了自己的数据后,ToDoStore发出了一个变更事件。
// TodoStore2.js // TodoStore emits a 'change' event after handling the action. // ... // Register to handle all updates 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); } break; // ... default: return true; } // This often goes in each case that should trigger a UI change. This store // needs to trigger a UI change after every view action, so we can make the // code less repetitive by putting it here. We need the default case, // however, to make sure this only gets called after one of the cases above. TodoStore.emitChange(); return true; // No errors. Needed by promise in Dispatcher. }); // ... |
8.ToDoApp组件监听到了ToDoStore的变更事件,并基于ToDoStore中最新的数据重新渲染了UI。
// TodoApp2.react.js // The component listens for changes and calls the '_onChange' callback // ... var TodoApp = React.createClass({ getInitialState: function() { return getTodoState(); }, componentDidMount: function() { TodoStore.addChangeListener(this._onChange); }, componentWillUnmount: function() { TodoStore.removeChangeListener(this._onChange); }, // ... /** * Event handler for 'change' events coming from the TodoStore */ _onChange: function() { this.setState(getTodoState()); } // ... |
Flux 与 MVC 对比
Flux是作为MVC的一种替代而问世的,其文档解释说它“通过支持单向数据流回避了MVC”。在比较Flux和MVC时,需理解三件事:
1.在JavaScript中,“MVC”实际上指的是“MV*”。
2.Flux并不比MV*简单。
3.相比于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数据流
Flux并不比MV*简单
图题:复杂的MVC数据流
你可能已经观看过Facebook对Flux的介绍(注:YouTube上的一个视频),以及关于为什么“MVC不扩展”的分析,包括下图中7对不同的model和view之间的数据流动:
这让MVC看起来特别令人困惑——看那一堆箭头!谁能理清图中究竟发生了什么?所以看起来Flux会比较简单一点,是不?
但是在视频中,我们没能看到在Flux的实现中复杂的层面,一切都“简化”到一个简单的数据流。
图题:基本的Flux数据流
现在有必要看看一个复杂系统的Flux实现的样貌了。如下图所示,你会发现相比于MVC,这里有更多的剪头和图标,而不是更少。
图题:复杂的Flux数据流
Flux实际上和MV*拥有的组件数目是相同的,这也是为什么上图中它看起来和MV*一样复杂的原因——但是有个关键性的差别是:所有的剪头都指向一个方向,在整个系统中形成一个闭环。
Flux使事情可预测
在Flux和MV*的图中,都有很多事情在进行,但在可预测层面上,Flux有更好的表现。
Flux中的调度器保证系统中同时只有一个事务流,如果调度器在处理完一个已存在的事务之前收到另一个事务,则会抛出一个错误:
“未捕获错误:违反不变性:Dispatch.dispatch(…):不能在已调度中途再调度。”
这是另一种让事情变得可预测的方式,它迫使开发者构建数据资源无复杂交互的应用。
调度器还允许开发者通过使用waitFor方法,使得store在执行回调方法前等待其他的store,从而指定各个store执行回调方法的顺序,如果代码中出现了两个store互相等待的情况,调度器会抛出一个详实的错误。
在Flux的Facebook实现中,可以清楚地看到数据改变的原因,每个store都包含了其监听的任务列表。
// ThreadStore.js // The case statement documents which actions this store listens to // ... ThreadStore.dispatchToken = ChatAppDispatcher.register(function(payload) { var action = payload.action; switch(action.type) { case ActionTypes.CLICK_THREAD: _currentID = action.threadID; _threads[_currentID].lastMessage.isRead = true; ThreadStore.emitChange(); break; case ActionTypes.RECEIVE_RAW_MESSAGES: ThreadStore.init(action.rawMessages); ThreadStore.emitChange(); break; default: // do nothing } }); // ... |
在这个例子中,ThreadStore 监听 CLICK_THREAD 和 RECEIVE_RAW_MESSAGES
动作,如果store没有像预期一样更新,注册器回调方法会为我们提供启动调试的机会,基于此可以对它接收的所有动作做日志记录并坚持其数据的有效负载。
相似的,所有的组件也都维护了其监听的所有store的列表。
// ThreadSection.react.js // Looking at the 'componentDidMount' will usually show // whic stores this component listens to. // ... function getStateFromStores() { return { threads: ThreadStore.getAllChrono(), currentThreadID: ThreadStore.getCurrentID(), unreadCount: UnreadThreadStore.getCount() }; } var ThreadSection = React.createClass({ getInitialState: function() { return getStateFromStores(); }, componentDidMount: function() { ThreadStore.addChangeListener(this._onChange); UnreadThreadStore.addChangeListener(this._onChange); }, componentWillUnmount: function() { ThreadStore.removeChangeListener(this._onChange); UnreadThreadStore.removeChangeListener(this._onChange); }, // ... |
上文中,我们看到了ThreadSection组件监听ThreadStore和UnreadThreadStore变更,如果我们始终使用该方法为组件建立对store变更的监听,那么我们就可以确认没有其他的store会影响到该组件的行为。
Flux分离了数据的接收和发送环节,所以在调试时,可以方便地跟踪数据流以便发现错误之处。
Flux的困难之处
在软件工程领域,每一个选择都是权衡之举,Flux也不例外,下面是已经认识到的不利之处:
1.它牵扯到写更多的样板代码
2.迁移现有资源是一项大任务
3.在没有良好的组织结构的情况下,单元测试非常困难
相比于普遍认为足够处理数据流的数量的文件和代码行数,Flux确实在应用中加入了更多,这种情况下,为新的数据资源书写新代码要比在已存的Flux代码中添加新的内容要痛苦的多。未来我们可能通过引入代码生成器来快速建立Flux工程,使用Vim的snippet功能也能加快这个过程。
写一个新的项目是体验Flux最容易的方法。说服他人接受新的事物总是有挑战的,本文、以及其他文档,还有来自Facebook的范例,可以为你提供教授他们的材料,你可以自信地认为,既然Facebook和其他公司都把Flux用于或大或小的生产环境,那么它也自然能用在你的项目中。
在把已有应用迁移到Flux时,可以一次一个地尝试将数据资源变为Flux架构。当考虑使用Flux管理应用中的一组数据时,要考虑到有多少组件使用了该数据。如果绝大部分的组件都使用了该数据,那么把数据迁移到Flux管理之下可能会是个大工程,首次尝试迁移到Flux时,从更孤立一些的数据开始。
使用Flux,你的组件开始依赖ActionCreator和Store,而且通常它们会互相依赖,这导致单元测试变得困难。把应用中与Store的交互重定向到顶层的“控制器”组件,那么在执行子组件的单元测试时,就无需担心Store的问题了。而为了测试那些确实需要发送动作并监听Store的组件,有一些成功的方法是伪造Store的方法,或伪造获响应动作以及Store获得数据的API。
你是否已经尝试了Flux?
在Brigade,我们已经学习了转向Flux的经验,你是否已经尝试了Flux?你是否在运行中遇到了任何困难?你是否解决了这些困难?我期待在未来能够看到更多关于Flux的范例和讨论。
如果你还没有用过Flux,那么我希望本文能够让你对构建Flux应用有深入的了解。
|