原文地址: https://facebook.github.io/flux/docs/todo-list.html
让我们用一个经典的TodoMVC的应用的简单代码来展示一下Flux的结构体系。虽然完整的项目案例你可以在Github中下载得到,但是还是让我们一步步来完成开发。
最开始,我们需要一些模板和模块。基于CommonJS的Node模块系统与react-boilerplate十分适合,有利于快速启动与构建。假设你已经安装了npm,将react-boilerplate从GitHub上clone下来,从终端进入文件根目录,运行npm命令npm install
, 然后npm run build
, 最后npm start
利用Browserify进行构建。
TodoMVC能够在此之中很好地构建,但是你需要确认react-boilerplate的package.json与TodoMVC示例的package.json具有相同的文件结构与依赖描述,否则你的代码将于下面的说明不相符合。
源代码结构
index.html
文件是我们应用的顶端入口,其加载了bundle.js
结果文件。我们会写入很多自己的代码到js文件夹中,这时Browserify就会帮助我们进行模块打包。当我们现在查看应用的目录,是下面这样的:
|
|
然后我们进入js文件目录,应用的主要目录结构如下:
|
|
创建Dispatcher(调度)
现在我们准备创建一个Dispatcher,这里有一个Dispatcher类的原生例子,使用了Jake Archibald的ES6-Promises模块。
|
|
基础Dispatcher的公共API仅有两个方法组成: register()与dispatch(),我们将使用register()去注册每一个store的回调函数,使用dispatch()让action触发callback的回调。
对于我们的应用而言需要创建一个更为完善的调度,称为AppDispatcher
|
|
对于我们的需求而言现在已经比较完善了,建立了一个处理view action的函数。之后我们也可以对此进行扩展,比如对服务器数据进行更新,虽然我们目前还不需要。
创建Stores(存储)
我们使用node的EventEmitter去开展一个Store,我们需要EventEmitter向controller-views广播’change’事件。为了更为清楚地表达目的,我省去了部分代码,详细的可见TodoStore.js
|
|
这里有许多重要的事情需要记录一下。我们开始维护_todos
的数据结构,其内涵了每一个单独的to-do。虽然_todos
存在于TodoStore
的外部,但依然位于模块的作用域内,它依然是私有的,无法被模块外部所直接修改。这样没有action就不可能直接更新store了。
另外一个重要的部分是在dispatcher中注册store的callback。我们将带有payload的回调函数传入dispatcher以及在dispatcher内部保护store的索引。callback目前只有两种action类型,但你可以按照你的需求来添加。
监听Controller-View的改变
我们需要一个在各个组件顶部的React组件去监听store的变化。在大型的应用中,我们会拥有更多这样的监听组件,或许是每一个页面的任何部分都会拥有。在Facebook的创建工具中,我们拥有许多类似于控制器的视图(controller-like views),每一个都管理着它所从属的UI。在视频回放编辑器中,我们仅仅拥有两个:一个是动画预览,一个是图片选择接口。这里是我们TodoMVC的例子,这里依然是删减过的,详细代码请见TodoApp.react.js
|
|
现在我们处于我们所熟悉的React范围内,利用React的生命周期方法。我们利用getInitialState()
初始化controller-view
,利用componentDidMount()
注册事件的监听,使用componentWillUnmount()
进行清除。我们渲染div容器,从TodoStore得到数据传入state。
Header组件中包含应用主要的input,但是不需要知道store的状态。而组件MainSection、Footer需要这些数据,因此我们向它俩传入。
更多的View
React组件顶层结构应该是这样的:
|
|
如果TodoItem处于编辑状态,渲染TodoTextInput作为子元素。让我们来看一看这些组件是如何将作为props的数据展示出来的,如何利用dispatcher来action通信的。MainSection需要去遍历接受自TodoApp所创建的to-do单元集合。在组件的render方法中,我们可以看到遍历器是这样的:
|
|
如今,每一个Todo单元都展示自己的内容,利用他们自己的ID去执行action。解释Todo单元上所有不同的action已经超出本文的范围,你可以查看代码仓库,但是让我来看一看to-do单元的删除操作,以下是一个简略的版本:
|
|
随着TodoAction中删除的action可以使用,store已经可以被操作了,与用户的行为进行交互来改变应用的state再简单不过。我们通过所提供的id,利用click事件触发删除的。现在用户可以点击删除的按钮,开始Flux流程去更新应用了。
另一方面, Text input就有一些复杂因为我们需要等待在React组件中text input本身的state。让我们来看看TodoTextInput是如何工作的。
让我们看看下面这段代码,React期望我们更新component的state随着input每一次的改变。所以当我们最终将文本保存进input时,我们将值储存于组件的state中。这是UI的state,而不是应用的,保持这个差异很好地指引了state应该在哪里被储存。所有应用的state应存在于store,虽然React组件偶尔会保持在UI state中,理想中应尽可能少得保持state。
因为TodoTextInput在我们的应用中被应用于多处,具有不同的行为,我们需要通过onSave方法,传自组件父层的prop。这样就允许通过传入不同的onSave去执行不同的操作。
|
|
Header中的onSave就是让TodoTextInput去创建一个新的to-do单元:
|
|
在不同的环境中,就像编辑一个已经存在的to-do单元,_onSave的回调函数就需要用TodoActions.update(text)
来代替。
创建语义化的actions
这是最基础的代码在我们的view中所使用的action:
|
|
你可以看到,我们并不需要AppDispatcher.handleViewAction()
或是TodoActions.create()
这样的帮助。在理论上,直接定义为AppDispatcher.dispatch()
,向其提供payload。但随着我们应用的增长,具有帮助能使我们的代码保持整洁与语义化。清楚地定义TodoActions.destroy(id)
好过于写了一堆无法明白的东西。
TodoActions.create()
生成payload像这样:
|
|
payload通过所注册的回调函数被提供给TodoStore,TodoStore广播’change’事件,MainSection会做出响应,从TodoStore中获取最新的to-do集合,然后改变他的state。这个改变来源于TodoApp组件所定义的render()方法,其它组件的render方法都是它的子代。
从顶层开始
在我们的应用中,引导文件是app.js,仅含有TodoApp组件,render作为应用的根元素。
|
|
为Dispatcher添加依赖管理
在我之前所说的,我们的Dispatcher是原生的,这是十分棒的,但不能满足大多数应用。与Store之间,我们需要一个方法去管理依赖,所以我们设计了waitFor()在Dispatcher内。
waitFor()方法返回一个Promise,这个Promise依次序返回Store的回调函数。
|
|
现在对于TodoStore的回调函数我们可以明确地等待任何依赖先更新后再向前推进。然而StoreA等待StoreB,StoreB等待StoreA,会出现循环依赖。面对这一情况,一个健壮的dispatcher可在控制台中发出警告。
Flux的未来
很多人问FaceBook是否打算将flux作为一个开源框架。事实上,Flux代表是一种结构,而不是框架。这个项目作为Flux的模板工程会十分有意义。请让我们知道你喜欢我们所做的。
感谢你花时间阅读如何构建客户端应用在Facebook中。我们希望我们能向你证明Flux是有用的。