作者: 阮一峰
日期: 2016 年 1 月 15 日
过去一年中,前端技术大发展,最耀眼的明星就是 React。
React 本身只涉及 UI 层,如果搭建大型应用,必须搭配一个前端框架。也就是说,你至少要学两样东西,才能基本满足需要:React 前端框架。
Facebook 官方使用的是 Flux 框架。本文就介绍如何在 React 的基础上,使用 Flux 组织代码和安排内部逻辑,使得你的应用更易于开发和维护。
阅读本文之前,我假设你已经掌握了 React 。如果还没有,可以先看我写的《React 入门教程》。与以前一样,本文的目标是使用最简单的语言、最好懂的例子,让你一看就会。
一、Flux 是什么?
简单说,Flux 是一种架构思想,专门解决软件的结构问题。它跟 MVC 架构是同一类东西,但是更加简单和清晰。
Flux 存在多种实现(至少 15 种),本文采用的是 Facebook 官方实现。
二、安装 Demo
为了便于讲解,我写了一个 Demo。
请先安装一下。
$ git clone https://github.com/ruanyf/extremely-simple-flux-demo.git $ cd extremely-simple-flux-demo && npm install $ npm start
然后,访问 http://127.0.0.1:8080 。
你会看到一个按钮。这就是我们的 Demo。
三、基本概念
讲解代码之前,你需要知道一些 Flux 的基本概念。
首先,Flux 将一个应用分成四个部分。
- View: 视图层
- Action(动作):视图层发出的消息(比如 mouseClick)
- Dispatcher(派发器):用来接收 Actions、执行回调函数
- Store(数据层):用来存放应用的状态,一旦发生变动,就提醒 Views 要更新页面
Flux 的最大特点,就是数据的"单向流动"。
- 用户访问 View
- View 发出用户的 Action
- Dispatcher 收到 Action,要求 Store 进行相应的更新
- Store 更新后,发出一个"change"事件
- View 收到"change"事件后,更新页面
上面过程中,数据总是"单向流动",任何相邻的部分都不会发生数据的"双向流动"。这保证了流程的清晰。
读到这里,你可能感到一头雾水,OK,这是正常的。接下来,我会详细讲解每一步。
四、View(第一部分)
请打开 Demo 的首页index.jsx
,你会看到只加载了一个组件。
// index.jsx var React = require ('react'); var ReactDOM = require ('react-dom'); var MyButtonController = require ('./components/MyButtonController'); ReactDOM.render ( <MyButtonController/>, document.querySelector ('#example') );
上面代码中,你可能注意到了,组件的名字不是 MyButton
,而是 MyButtonController
。这是为什么?
这里,我采用的是 React 的 controller view 模式。"controller view"组件只用来保存状态,然后将其转发给子组件。MyButtonController
的源码很简单。
// components/MyButtonController.jsx var React = require ('react'); var ButtonActions = require ('../actions/ButtonActions'); var MyButton = require ('./MyButton'); var MyButtonController = React.createClass ({ createNewItem: function (event) { ButtonActions.addNewItem ('new item'); }, render: function () { return <MyButton onClick={this.createNewItem} />; } }); module.exports = MyButtonController;
上面代码中,MyButtonController
将参数传给子组件MyButton
。后者的源码甚至更简单。
// components/MyButton.jsx var React = require ('react'); var MyButton = function (props) { return <div> <button onClick={props.onClick}>New Item</button> </div>; }; module.exports = MyButton;
上面代码中,你可以看到MyButton
是一个纯组件(即不含有任何状态),从而方便了测试和复用。这就是"controll view"模式的最大优点。
MyButton
只有一个逻辑,就是一旦用户点击,就调用this.createNewItem
方法,向 Dispatcher 发出一个 Action。
// components/MyButtonController.jsx // ... createNewItem: function (event) { ButtonActions.addNewItem ('new item'); }
上面代码中,调用createNewItem
方法,会触发名为addNewItem
的 Action。
五、Action
每个 Action 都是一个对象,包含一个actionType
属性(说明动作的类型)和一些其他属性(用来传递数据)。
在这个 Demo 里面,ButtonActions
对象用于存放所有的 Action。
// actions/ButtonActions.js var AppDispatcher = require ('../dispatcher/AppDispatcher'); var ButtonActions = { addNewItem: function (text) { AppDispatcher.dispatch ({ actionType: 'ADD_NEW_ITEM', text: text }); }, };
上面代码中,ButtonActions.addNewItem
方法使用AppDispatcher
,把动作ADD_NEW_ITEM
派发到 Store。
六、Dispatcher
Dispatcher 的作用是将 Action 派发到 Store、。你可以把它看作一个路由器,负责在 View 和 Store 之间,建立 Action 的正确传递路线。注意,Dispatcher 只能有一个,而且是全局的。
Facebook 官方的 Dispatcher 实现输出一个类,你要写一个AppDispatcher.js
,生成 Dispatcher 实例。
// dispatcher/AppDispatcher.js var Dispatcher = require ('flux') .Dispatcher; module.exports = new Dispatcher ();
AppDispatcher.register ()
方法用来登记各种 Action 的回调函数。
// dispatcher/AppDispatcher.js var ListStore = require ('../stores/ListStore'); AppDispatcher.register (function (action) { switch(action.actionType) { case 'ADD_NEW_ITEM': ListStore.addNewItemHandler (action.text); ListStore.emitChange (); break; default: // no op } })
上面代码中,Dispatcher 收到ADD_NEW_ITEM
动作,就会执行回调函数,对ListStore
进行操作。
记住,Dispatcher 只用来派发 Action,不应该有其他逻辑。
七、Store
Store 保存整个应用的状态。它的角色有点像 MVC 架构之中的 Model 。
在我们的 Demo 中,有一个ListStore
,所有数据都存放在那里。
// stores/ListStore.js var ListStore = { items: [], getAll: function () { return this.items; }, addNewItemHandler: function (text) { this.items.push (text); }, emitChange: function () { this.emit ('change'); } }; module.exports = ListStore;
上面代码中,ListStore.items
用来保存条目,ListStore.getAll ()
用来读取所有条目,ListStore.emitChange ()
用来发出一个"change"事件。
由于 Store 需要在变动后向 View 发送"change"事件,因此它必须实现事件接口。
// stores/ListStore.js var EventEmitter = require ('events') .EventEmitter; var assign = require ('object-assign'); var ListStore = assign ({}, EventEmitter.prototype, { items: [], getAll: function () { return this.items; }, addNewItemHandler: function (text) { this.items.push (text); }, emitChange: function () { this.emit ('change'); }, addChangeListener: function (callback) { this.on ('change', callback); }, removeChangeListener: function (callback) { this.removeListener ('change', callback); } });
上面代码中,ListStore
继承了EventEmitter.prototype
,因此就能使用ListStore.on ()
和ListStore.emit ()
,来监听和触发事件了。
Store 更新后(this.addNewItemHandler ()
)发出事件(this.emitChange ()
),表明状态已经改变。 View 监听到这个事件,就可以查询新的状态,更新页面了。
八、View (第二部分)
现在,我们再回过头来修改 View ,让它监听 Store 的 change
事件。
// components/MyButtonController.jsx var React = require ('react'); var ListStore = require ('../stores/ListStore'); var ButtonActions = require ('../actions/ButtonActions'); var MyButton = require ('./MyButton'); var MyButtonController = React.createClass ({ getInitialState: function () { return { items: ListStore.getAll () }; }, componentDidMount: function () { ListStore.addChangeListener (this._onChange); }, componentWillUnmount: function () { ListStore.removeChangeListener (this._onChange); }, _onChange: function () { this.setState ({ items: ListStore.getAll () }); }, createNewItem: function (event) { ButtonActions.addNewItem ('new item'); }, render: function () { return <MyButton items={this.state.items} onClick={this.createNewItem} />; } });
上面代码中,你可以看到当MyButtonController
发现 Store 发出 change
事件,就会调用 this._onChange
更新组件状态,从而触发重新渲染。
// components/MyButton.jsx var React = require ('react'); var MyButton = function (props) { var items = props.items; var itemHtml = items.map (function (listItem, i) { return <li key={i}>{listItem}</li>; }); return <div> <ul>{itemHtml}</ul> <button onClick={props.onClick}>New Item</button> </div>; }; module.exports = MyButton;
九、致谢
本文受到了 Andrew Ray 的文章《Flux For Stupid People》的启发。
(完)