@nullcc
2016-06-16T11:18:38.000000Z
字数 8158
阅读 1873
react
flux
前端组件
这部分要分析一个react+flux的demo。
单向数据流是一种应用架构模式,它关心数据如何存放,如何更改数据,如何通知数据更改等问题。并不是React自带的东西,它是一种用React构建客户端web应用的最佳实践。正因为Data Flow是一种模式,或者说是一种思想,所以市面上现在有不止一个的实现。比较常用的有官方的Flux和Redux。
本篇就要使用react+flux来构建客户端web应用。
React承担了MVC中View的职责,那Flux则承担了M和C的职责。Flux是Facebook使用的一套前端应用架构模式。一个Flux应用主要包括下面4个部分:
Dispatcher
处理动作分发
Stores
数据和逻辑部分
Views
React组件,这一层可以看做是controller-views,作为视图同时响应用户交互。
Actions
一般由Views调用,在内部调用Dispatcher分发事件给Stores。
有兴趣的童鞋可以去阅读Flux的源码。
简单看下单向数据流是怎么一回事:
Action -> Dispatcher -> Store -> View
更多时候用户会通过View触发事件,因此更准确的图如下:
解释一下这个数据流程图:
一句话来概括就是:所有的状态都由Store来维护,Dispatcher通过Action传递数据,View从Store获取数据更新UI。
Flux的Github页面还给出了一张更详细的图:
Dispatcher是应用的消息分发中心,一般来说一个应用只需要一个Dispatcher,它管理所有数据流向,分发动作给Store。
Dispatcher分发动作给Store注册的回调函数,这里和一般意义上的发布/订阅模式有点不同:
在Flux实现中,Dispatcher提供的API很简单:
register(function callback): string
注册回调函数,返回一个token供在waitFor()中调用。
unregister(string id): void
通过token移除回调。
waitFor(array ids): void
在指定的回调函数执行之后才执行当前回调,这个方法只能在分发动作的回调函数中使用。
isDispatching(): boolean
返回Dispatcher当前是否处于在分发的状态。
我们需要创建一些动作,定义一些action creator来创建,这些方法暴露给外部调用。动作就是用来封装传递数据的,动作就是一个简单的JS对象,包含了动作类型(type)和payload(数据载荷),type是一个字符串常量,我们需要事先定义好。
Store中包含了应用的状态和逻辑,不同的Store管理着应用中的不同部分。
在Store中向Dispatcher注册的回调函数会接受到Dispatcher分发的action,因为每个action都会分发给所有注册的回调函数,因此在回调函数中必须判断action的type,只处理自己关心的action类型,并获取action中的数据后调用Store的内部方法进行更新,然后通知View数据有变更,随后View重新渲染。
注意,Store对外只会暴露getter方法,不会暴露任何setter方法,唯一更新数据的手段就是通过Dispatcher分发action去更新Store。
View就是React组件,从Store获取数据,绑定事件处理。一个View可能关联多个Store来管理不同部分的状态(毕竟一个View可能要展示多方面的数据),React在更新View时仅仅是去Store拿最新的数据,然后调用setState,这部分解耦了。
下面我们来分析一个官方的实例:todo-mvc
先来围观一下这个demo的文件结构,我们只看js部分:
actions文件夹
存放应用中的所有类型的Action。
components文件夹
存放React组件文件。
constants文件夹
存放每个Action文件对应的Action Type常量。
dispatcher文件夹
存放Dispatcher文件,一般来说Dispatcher一个就够了。
stores文件夹
存放应用中的所有Store文件。
app.js
客户端web应用的入口文件。
bundle.js
这个文件是由工作流脚本生成的,我们需要在package.json文件中配置生成的命令,然后启动npm start开启监控,每当我们的js文件有变化,都会自动帮我们打包这个文件。bundle.js文件需要在index.html中手动用\标签引入.
先看一下index.html:
在index.html中,只引入了一个bundle.js,这是一个打包文件。
TodoConstants.js文件:
var keyMirror = require('keymirror');
module.exports = keyMirror({
TODO_CREATE: null,
TODO_COMPLETE: null,
TODO_DESTROY: null,
TODO_DESTROY_COMPLETED: null,
TODO_TOGGLE_COMPLETE_ALL: null,
TODO_UNDO_COMPLETE: null,
TODO_UPDATE_TEXT: null
});
TodoConstants.js文件中,定义了7个action type,对应了7种不同的更新Store中数据的方式。
AppDispatcher.js文件:
var Dispatcher = require('flux').Dispatcher;
module.exports = new Dispatcher();
AppDispatcher.js文件没什么特别的,就是导出一个Dispatcher实例而已。
TodoActions.js文件:
var AppDispatcher = require('../dispatcher/AppDispatcher');
var TodoConstants = require('../constants/TodoConstants');
var TodoActions = {
create: function(text) {
AppDispatcher.dispatch({
actionType: TodoConstants.TODO_CREATE,
text: text
});
},
updateText: function(id, text) {
AppDispatcher.dispatch({
actionType: TodoConstants.TODO_UPDATE_TEXT,
id: id,
text: text
});
},
toggleComplete: function(todo) {
var id = todo.id;
var actionType = todo.complete ?
TodoConstants.TODO_UNDO_COMPLETE :
TodoConstants.TODO_COMPLETE;
AppDispatcher.dispatch({
actionType: actionType,
id: id
});
},
toggleCompleteAll: function() {
AppDispatcher.dispatch({
actionType: TodoConstants.TODO_TOGGLE_COMPLETE_ALL
});
},
destroy: function(id) {
AppDispatcher.dispatch({
actionType: TodoConstants.TODO_DESTROY,
id: id
});
},
destroyCompleted: function() {
AppDispatcher.dispatch({
actionType: TodoConstants.TODO_DESTROY_COMPLETED
});
}
};
module.exports = TodoActions;
TodoActions.js中基本上就是一些对应TodoConstants.js中定义的action type(其中toggleComplete对应了两种),导出的TodoActions中包含这些action creator。这就是Action暴露给外部调用的方法了,通过调用这些action creator,我们很容易就可以让Dispatcher分发我们期望的action给Store。
TodoStore.js文件:
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 = {};
function create(text) {
var id = (+new Date() + Math.floor(Math.random() * 999999)).toString(36);
_todos[id] = {
id: id,
complete: false,
text: text
};
}
function update(id, updates) {
_todos[id] = assign({}, _todos[id], updates);
}
function updateAll(updates) {
for (var id in _todos) {
update(id, updates);
}
}
function destroy(id) {
delete _todos[id];
}
function destroyCompleted() {
for (var id in _todos) {
if (_todos[id].complete) {
destroy(id);
}
}
}
var TodoStore = assign({}, EventEmitter.prototype, {
areAllComplete: function() {
for (var id in _todos) {
if (!_todos[id].complete) {
return false;
}
}
return true;
},
getAll: function() {
return _todos;
},
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
});
AppDispatcher.register(function(action) {
var text;
switch(action.actionType) {
case TodoConstants.TODO_CREATE:
text = action.text.trim();
if (text !== '') {
create(text);
TodoStore.emitChange();
}
break;
case TodoConstants.TODO_TOGGLE_COMPLETE_ALL:
if (TodoStore.areAllComplete()) {
updateAll({complete: false});
} else {
updateAll({complete: true});
}
TodoStore.emitChange();
break;
case TodoConstants.TODO_UNDO_COMPLETE:
update(action.id, {complete: false});
TodoStore.emitChange();
break;
case TodoConstants.TODO_COMPLETE:
update(action.id, {complete: true});
TodoStore.emitChange();
break;
case TodoConstants.TODO_UPDATE_TEXT:
text = action.text.trim();
if (text !== '') {
update(action.id, {text: text});
TodoStore.emitChange();
}
break;
case TodoConstants.TODO_DESTROY:
destroy(action.id);
TodoStore.emitChange();
break;
case TodoConstants.TODO_DESTROY_COMPLETED:
destroyCompleted();
TodoStore.emitChange();
break;
default:
// no op
}
});
module.exports = TodoStore;
TodoStore.js负责管理数据和逻辑。
首先是定义了一个_todos变量用来保存todo数据,这个变量外部无法直接访问到。
在导出对象TodoStore中,定义了一系列的方法供外部调用(大部分属于getter方法,还有一些和事件相关的方法),在View中可以调用Store中的getter方法获取相应的状态数据,然后调用setState方法渲染自身。
另外还定义了一些私有方法(只能从内部访问到的),比如create, update, updateAll, destroy, destroyCompleted,这些私有方法用来直接更新数据,由于私有方法只能从内部访问到,所以基本上只能在注册的回调函数中调用它们。
在最后面,向Dispatcher注册了一个回调函数,这个函数内部用switch判断action type来执行相应的代码。观察可以发现,在回调函数的代码中,针对每个不同的action type,调用了刚才说到的私有方法来更新数据,然后调用TodoStore.emitChange();手动触发一个事件,由于定义了addChangeListener和removeChangeListener,相应的View可以在生命周期函数(比如componentDidMount和componentWillUnmount)中添加和删除监听器,View在监听器回调中可以调用setState进行渲染。
TodoStore.js的代码看似很长,其实并不难理解。总结一下,它分成三大块:
components文件夹中都是React组件,下面是其中一个文件TodoApp.react.js的代码:
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(),
areAllComplete: TodoStore.areAllComplete()
};
}
var TodoApp = React.createClass({
getInitialState: function() {
return getTodoState();
},
componentDidMount: function() {
TodoStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
TodoStore.removeChangeListener(this._onChange);
},
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;
TodoApp.react.js是一个顶层组件,它包含了Header ,MainSection和Footer三个组件(代码就不列出了),并把它们放在一个div中封装起来作为一个整体供使用。
在这个文件中,导出TodoApp对象,TodoApp实际上就是一个React组件了,里面定义了getInitialState, componentDidMount, componentWillUnmount, render和_onChange这些方法。
在getInitialState方法中,调用了一个私有方法getTodoState,在getTodoState中,通过TodoStore对外提供的接口去获取数据。
在componentDidMount和componentWillUnmount中分别注册和销毁了change事件的监听器回调函数。在这里其实我们可以定义自己的事件,不一定是change,比如可以自定义一个open事件,当然在TodoStore中也要有相应的处理。回调函数_onChange负责最终去调用this.setState方法渲染UI。
app.js代码如下:
var React = require('react');
var TodoApp = require('./components/TodoApp.react');
React.render(
<TodoApp />,
document.getElementById('todoapp')
);
app.js是应用的入口,在这里对todoapp这个节点添加了一个\组件,就是这样。
bundle.js是一个打包文件,在配置工作流环境后运行npm start会自动生成,这个不用担心。