这更像是“你的意见是什么/我在思考这个问题时是否正确?”题。
在理解Flux的同时尽量严格,我试图找出XHR调用的位置,处理websockets /外部刺激,路由发生等等。
从我阅读的文章,访谈和浏览facebook示例中,我可以通过几种方式处理这些问题。严格遵循通量,Action创建者可以执行所有XHR调用,并且可能在请求完成之前和之后触发PENDING/SUCCESS/FAILURE
操作。
另一个来自facebook的Ian Obermiller,所有READ(GET)请求都由Stores直接处理(没有Action创建者/调度员的参与),WRITE(POST)请求由Action Creators处理整个action>dispatcher>store
流程。
我们提出/想要坚持的一些理解/结论:
PENDING/PROGRESS(think file uploads)/SUCCESS/FAILURE
操作。Action>Dispatcher>Store
调用是严格同步的,以坚持调度无法在内部启动另一个调度以避免链接事件/操作。我们得出一些结论的一些问题,但我并不完全满意:
/api/teams/{id}
发出的API调用,返回类似于:
{
entities: {
teams: [{
name: ...,
description: ...,
members: [1, 2, 4],
version: ...
}],
users: [{
id: 1
name: ...,
role: ...,
version: ...
},
{
id: 2
name: ...,
role: ...,
version: ...
},
{
id: 3
name: ...,
role: ...,
version: ...
}]
}
}
理想情况下,我还想使用此API中返回的信息更新MemberStore。我们维护每个实体的版本号,这些版本在记录更新时更新,这是我们在内部使用的拒绝对陈旧数据的调用等。使用这个,我可以有一个内部逻辑,如果我作为副作用一些其他API调用,我知道我的数据是陈旧的,我触发了对该记录的刷新。
看起来,解决方案是你需要商店来触发一个动作(这将有效地更新其他相关商店)。这会使Store> View> Action to Store> Action短路,我不确定它是不是一个好主意。我们已经有一件事与商店进行他们自己的XHR调用不同步了。像这样的让步最终会开始蔓延到整个系统。
或知道其他商店并能够与他们沟通的商店。但这打破了商店没有Setters规则。
解决上述问题的一个简单方法就是坚持将动作作为外部传入/传出刺激的唯一场所。这简化了多个商店更新的逻辑。
但是现在,你在哪里以及如何处理缓存?我们得出结论,缓存将发生在API Utils / DAO级别。 (如果你看一下通量图)。
但这引入了其他问题。为了更好地理解/解释我的意思,例如:
/api/teams
返回所有球队的列表,我显示所有球队的列表。
在点击团队链接时,我会查看其详细信息视图,如果商店中尚未存在数据,则需要/api/teams/{id}
的数据。
如果Actions处理所有的XHR,那么View会像TeamActions.get([id])
那样执行TeamDAO.get([id])
。为了能够立即返回此调用(因为我们已将其缓存),DAO必须进行缓存,但也要保持集合/项之间的关系。按照设计,这种逻辑已经存在于商店中。
这里有问题:
你是否在DAO和商店中复制了这个逻辑?
你是否让DAO了解商店,他们可以向商店询问他们是否已经拥有一些数据并且只返回302说,你很高兴你有最新的数据。
您如何处理涉及XHR API的验证?简单的重复团队名称。
视图直接命中DAO并执行像TeamDAO.validateName([name])
这样的东西,它会返回一个承诺,或者你是否创建了一个Action?如果您创建一个Action,Store会通过哪个有效/无效流回View,考虑到它主要是瞬态数据?
你如何处理路由?我查看了react-router,我不确定是否喜欢它。我不一定认为迫切需要JSX提供路由映射/配置的方法。此外,显然,它使用了自己的RouteDispatcher,它依赖于单个调度程序规则。
我更喜欢的解决方案来自一些博客帖子/ SO答案,其中路由映射存储在RouteStore中。
RouteStore还维护CURRENT_VIEW。 reactConConiner组件已在RouteStore中注册,并在更改时将其子视图替换为CURRENT_VIEW。当前视图通知AppContainer它们完全加载并且AppContainer触发RouteActions.pending / success / failure,可能带有一些上下文,以通知其他组件达到稳定状态,显示/隐藏繁忙/加载指示。
我无法设计干净的东西是你设计类似于Gmail的路由,你会怎么做? Gmail的一些观察结果我很喜欢:
在页面准备好加载之前,URL不会更改。它在“加载”时保留在当前URL上,并在加载完成后移动到新URL。这样做......
如果失败,您根本不会丢失当前页面。因此,如果您正在撰写,并且“发送”失败,则不会丢失您的邮件(即您不会丢失当前的稳定视图/状态)。 (他们不会这样做,因为自动保存是可以的,但你明白了)你可以选择将邮件复制/粘贴到某个地方以便安全保存,直到你可以再次发送。
一些参考:
https://github.com/gaearon/flux-react-router-example http://ianobermiller.com/blog/2014/09/15/react-and-flux-interview/ https://github.com/facebook/flux这是我使用facebook Flux和Immutable.js的实现,我认为根据一些经验法则回答你的许多问题:
STORES
Record
实例的全局ids
维护缓存。WebAPIUtils
进行读操作,并触发actions
进行写操作。RecordA
和FooRecordB
之间的关系是通过RecordA
params从foo_id
实例中解决的,并通过FooStore.get(this.foo_id)
等调用获得getters
方法,如get(id)
,getAll()
等。APIUTILS
Promise
中Promise
的映射Promise
被解析或拒绝时,我通过ActionCreators触发操作,例如fooReceived或fooError。fooError
操作当然应包含服务器返回的验证错误的有效负载。组件
Perf.printWasted
时间,它应该是非常低,几毫秒。props
保持我的组件propsType
尽可能明确。var fooRecord = { foo:1, bar: 2, baz: 3};
(为了简化本例,我不在这里使用Immutable.Record
)并且我的子组件需要显示fooRecord.foo
和fooRecord.bar
,我不传递整个foo
对象但只传递fooRecordFoo
和fooRecordBar
作为我的子组件的道具,因为另一个组件可以编辑foo.baz
值,使子组件重新渲染,而这个组件根本不需要这个值!路由 - 我只是使用ReactRouter
这是一个基本的例子:
火
apiUtils / Request.js
var request = require('superagent');
//based on http://stackoverflow.com/a/7616484/1836434
var hashUrl = function(url, params) {
var string = url + JSON.stringify(params);
var hash = 0, i, chr, len;
if (string.length == 0) return hash;
for (i = 0, len = string.length; i < len; i++) {
chr = string.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
var _promises = {};
module.exports = {
get: function(url, params) {
var params = params || {};
var hash = hashUrl(url, params);
var promise = _promises[hash];
if (promise == undefined) {
promise = new Promise(function(resolve, reject) {
request.get(url).query(params).end( function(err, res) {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
_promises[hash] = promise;
}
return promise;
},
post: function(url, data) {
return new Promise(function(resolve, reject) {
var req = request
.post(url)
.send(data)
.end( function(err, res) {
if (err) {
reject(err);
} else {
resolve(res);
}
});
});
}
};
apiUtils / FooAPI.js
var Request = require('./Request');
var FooActionCreators = require('../actions/FooActionCreators');
var _endpoint = 'http://localhost:8888/api/foos/';
module.exports = {
getAll: function() {
FooActionCreators.receiveAllPending();
Request.get(_endpoint).then( function(res) {
FooActionCreators.receiveAllSuccess(res.body);
}).catch( function(err) {
FooActionCreators.receiveAllError(err);
});
},
get: function(id) {
FooActionCreators.receivePending();
Request.get(_endpoint + id+'/').then( function(res) {
FooActionCreators.receiveSuccess(res.body);
}).catch( function(err) {
FooActionCreators.receiveError(err);
});
},
post: function(fooData) {
FooActionCreators.savePending();
Request.post(_endpoint, fooData).then (function(res) {
if (res.badRequest) { //i.e response return code 400 due to validation errors for example
FooActionCreators.saveInvalidated(res.body);
}
FooActionCreators.saved(res.body);
}).catch( function(err) { //server errors
FooActionCreators.savedError(err);
});
}
//others foos relative endpoints helper methods...
};
商店
商店/ BarStore.js
var assign = require('object-assign');
var EventEmitter = require('events').EventEmitter;
var Immutable = require('immutable');
var AppDispatcher = require('../dispatcher/AppDispatcher');
var ActionTypes = require('../constants/BarConstants').ActionTypes;
var BarAPI = require('../APIUtils/BarAPI')
var CHANGE_EVENT = 'change';
var _bars = Immutable.OrderedMap();
class Bar extends Immutable.Record({
'id': undefined,
'name': undefined,
'description': undefined,
}) {
isReady() {
return this.id != undefined //usefull to know if we can display a spinner when the Bar is loading or the Bar's data if it is ready.
}
getBar() {
return BarStore.get(this.bar_id);
}
}
function _rehydrate(barId, field, value) {
//Since _bars is an Immutable, we need to return the new Immutable map. Immutable.js is smart, if we update with the save values, the same reference is returned.
_bars = _bars.updateIn([barId, field], function() {
return value;
});
}
var BarStore = assign({}, EventEmitter.prototype, {
get: function(id) {
if (!_bars.has(id)) {
BarAPI.get(id);
return new Bar(); //we return an empty Bar record for consistency
}
return _bars.get(id)
},
getAll: function() {
return _bars.toList() //we want to get rid of keys and just keep the values
},
Bar: Bar,
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
});
var _setBar = function(barData) {
_bars = _bars.set(barData.id, new Bar(barData));
};
var _setBars = function(barList) {
barList.forEach(function (barData) {
_setbar(barData);
});
};
BarStore.dispatchToken = AppDispatcher.register(function(action) {
switch (action.type)
{
case ActionTypes.BAR_LIST_RECEIVED_SUCESS:
_setBars(action.barList);
BarStore.emitChange();
break;
case ActionTypes.BAR_RECEIVED_SUCCESS:
_setBar(action.bar);
BarStore.emitChange();
break;
case ActionTypes.BAR_REHYDRATED:
_rehydrate(
action.barId,
action.field,
action.value
);
BarStore.emitChange();
break;
}
});
module.exports = BarStore;
商店/ FooStore.js
var assign = require('object-assign');
var EventEmitter = require('events').EventEmitter;
var Immutable = require('immutable');
var AppDispatcher = require('../dispatcher/AppDispatcher');
var ActionTypes = require('../constants/FooConstants').ActionTypes;
var BarStore = require('./BarStore');
var FooAPI = require('../APIUtils/FooAPI')
var CHANGE_EVENT = 'change';
var _foos = Immutable.OrderedMap();
class Foo extends Immutable.Record({
'id': undefined,
'bar_id': undefined, //relation to Bar record
'baz': undefined,
}) {
isReady() {
return this.id != undefined;
}
getBar() {
// The whole point to store an id reference to Bar
// is to delegate the Bar retrieval to the BarStore,
// if the BarStore does not have this Bar object in
// its cache, the BarStore will trigger a GET request
return BarStore.get(this.bar_id);
}
}
function _rehydrate(fooId, field, value) {
_foos = _foos.updateIn([voucherId, field], function() {
return value;
});
}
var _setFoo = function(fooData) {
_foos = _foos.set(fooData.id, new Foo(fooData));
};
var _setFoos = function(fooList) {
fooList.forEach(function (foo) {
_setFoo(foo);
});
};
var FooStore = assign({}, EventEmitter.prototype, {
get: function(id) {
if (!_foos.has(id)) {
FooAPI.get(id);
return new Foo();
}
return _foos.get(id)
},
getAll: function() {
if (_foos.size == 0) {
FooAPI.getAll();
}
return _foos.toList()
},
Foo: Foo,
emitChange: function() {
this.emit(CHANGE_EVENT);
},
addChangeListener: function(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener: function(callback) {
this.removeListener(CHANGE_EVENT, callback);
},
});
FooStore.dispatchToken = AppDispatcher.register(function(action) {
switch (action.type)
{
case ActionTypes.FOO_LIST_RECEIVED_SUCCESS:
_setFoos(action.fooList);
FooStore.emitChange();
break;
case ActionTypes.FOO_RECEIVED_SUCCESS:
_setFoo(action.foo);
FooStore.emitChange();
break;
case ActionTypes.FOO_REHYDRATED:
_rehydrate(
action.fooId,
action.field,
action.value
);
FooStore.emitChange();
break;
}
});
module.exports = FooStore;
组件
components / BarList.react.js(控制器视图组件)
var React = require('react/addons');
var Immutable = require('immutable');
var BarListItem = require('./BarListItem.react');
var BarStore = require('../stores/BarStore');
function getStateFromStore() {
return {
barList: BarStore.getAll(),
};
}
module.exports = React.createClass({
getInitialState: function() {
return getStateFromStore();
},
componentDidMount: function() {
BarStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
BarStore.removeChangeListener(this._onChange);
},
render: function() {
var barItems = this.state.barList.toJS().map(function (bar) {
// We could pass the entire Bar object here
// but I tend to keep the component not tightly coupled
// with store data, the BarItem can be seen as a standalone
// component that only need specific data
return <BarItem
key={bar.get('id')}
id={bar.get('id')}
name={bar.get('name')}
description={bar.get('description')}/>
});
if (barItems.length == 0) {
return (
<p>Loading...</p>
)
}
return (
<div>
{barItems}
</div>
)
},
_onChange: function() {
this.setState(getStateFromStore();
}
});
组件/ BarListItem.react.js
var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Immutable = require('immutable');
module.exports = React.createClass({
mixins: [ImmutableRenderMixin],
// I use propTypes to explicitly telling
// what data this component need. This
// component is a standalone component
// and we could have passed an entire
// object such as {id: ..., name, ..., description, ...}
// since we use all the datas (and when we use all the data it's
// a better approach since we don't want to write dozens of propTypes)
// but let's do that for the example's sake
propTypes: {
id: React.PropTypes.number.isRequired,
name: React.PropTypes.string.isRequired,
description: React.PropTypes.string.isRequired
}
render: function() {
return (
<li>
<p>{this.props.id}</p>
<p>{this.props.name}</p>
<p>{this.props.description}</p>
</li>
)
}
});
组件/ BarDetail.react.js
var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Immutable = require('immutable');
var BarActionCreators = require('../actions/BarActionCreators');
module.exports = React.createClass({
mixins: [ImmutableRenderMixin],
propTypes: {
id: React.PropTypes.number.isRequired,
name: React.PropTypes.string.isRequired,
description: React.PropTypes.string.isRequired
},
handleSubmit: function(event) {
//Since we keep the Bar data up to date with user input
//we can simply save the actual object in Store.
//If the user goes back without saving, we could display a
//"Warning : item not saved"
BarActionCreators.save(this.props.id);
},
handleChange: function(event) {
BarActionCreators.rehydrate(
this.props.id,
event.target.name, //the field we want to rehydrate
event.target.value //the updated value
);
},
render: function() {
return (
<form onSubmit={this.handleSumit}>
<input
type="text"
name="name"
value={this.props.name}
onChange={this.handleChange}/>
<textarea
name="description"
value={this.props.description}
onChange={this.handleChange}/>
<input
type="submit"
defaultValue="Submit"/>
</form>
)
},
});
components / FooList.react.js(控制器视图组件)
var React = require('react/addons');
var FooStore = require('../stores/FooStore');
var BarStore = require('../stores/BarStore');
function getStateFromStore() {
return {
fooList: FooStore.getAll(),
};
}
module.exports = React.createClass({
getInitialState: function() {
return getStateFromStore();
},
componentDidMount: function() {
FooStore.addChangeListener(this._onChange);
BarStore.addChangeListener(this._onChange);
},
componentWillUnmount: function() {
FooStore.removeChangeListener(this._onChange);
BarStore.removeChangeListener(this._onChange);
},
render: function() {
if (this.state.fooList.size == 0) {
return <p>Loading...</p>
}
return this.state.fooList.toJS().map(function (foo) {
<FooListItem
fooId={foo.get('id')}
fooBar={foo.getBar()}
fooBaz={foo.get('baz')}/>
});
},
_onChange: function() {
this.setState(getStateFromStore();
}
});
组件/ FooListItem.react.js
var React = require('react/addons');
var ImmutableRenderMixin = require('react-immutable-render-mixin')
var Bar = require('../stores/BarStore').Bar;
module.exports = React.createClass({
mixins: [ImmutableRenderMixin],
propTypes: {
fooId: React.PropTypes.number.isRequired,
fooBar: React.PropTypes.instanceOf(Bar).isRequired,
fooBaz: React.PropTypes.string.isRequired
}
render: function() {
//we could (should) use a component here but this answer is already too long...
var bar = <p>Loading...</p>;
if (bar.isReady()) {
bar = (
<div>
<p>{bar.get('name')}</p>
<p>{bar.get('description')}</p>
</div>
);
}
return (
<div>
<p>{this.props.fooId}</p>
<p>{this.props.fooBaz}</p>
{bar}
</div>
)
},
});
让我们来看看FooList
的整个循环:
FooList
controller-view组件点击页面/ foos /列出FoosFooList
controller-view组件调用FooStore.getAll()
_foos
地图在FooStore
是空的,所以FooStore
通过FooAPI.getAll()
执行请求FooList
控制器视图组件自其state.fooList.size == 0
以来将自身呈现为加载状态。这是我们列表的实际外观:
++++++++++++++++++++++++
+ +
+ "loading..." +
+ +
++++++++++++++++++++++++
FooAPI.getAll()
请求解决并触发FooActionCreators.receiveAllSuccess
行动FooStore
接收此操作,更新其内部状态,并发出更改。FooList
controller-view组件接收更改事件并更新其状态以从FooStore
获取列表this.state.fooList.size
不再是== 0
所以列表实际上可以渲染自己(请注意,我们使用toJS()
显式获取原始javascript对象,因为React
无法正确映射非原始对象)。FooListItem
组件。foo.getBar()
,我们告诉FooStore
我们想要Bar
记录。getBar()
记录的Foo
方法通过Bar
检索BarStore
记录BarStore
在Bar
缓存中没有这个_bars
记录,所以它通过BarAPI
触发请求来检索它。Foo
控制器 - 视图组件的this.sate.fooList
中的所有FooList
也是如此++++++++++++++++++++++++ + + + Foo1 "name1" + + Foo1 "baz1" + + Foo1 bar: + + "loading..." + + + + Foo2 "name2" + + Foo2 "baz2" + + Foo2 bar: + + "loading..." + + + + Foo3 "name3" + + Foo3 "baz3" + + Foo3 bar: + + "loading..." + + + ++++++++++++++++++++++++
- 现在让我们说BarAPI.get(2)
(由Foo2请求)在BarAPI.get(1)
之前解决(由Foo1请求)。因为它是异步的,所以它是完全合理的。 - BarAPI
触发BAR_RECEIVED_SUCCESS' action via the
BarActionCreators.
- The
BarStore`通过更新其内部存储并发出更改来响应此操作。这就是现在有趣的部分......
FooList
控制器视图组件通过更新其状态来响应BarStore
更改。render
方法foo.getBar()
电话现在从Bar
检索一个真正的BarStore
记录。由于这个Bar
记录已被有效检索,ImmutablePureRenderMixin
将旧道具与当前道具进行比较,并确定Bar
对象已经改变! Bingo,我们可以重新渲染FooListItem
组件(这里更好的方法是创建一个单独的FooListBarDetail组件,只让这个组件重新渲染,这里我们也重新渲染Foo的细节,但没有改变,但为了这个缘故简单,让我们这样做)。++++++++++++++++++++++++ + + + Foo1 "name1" + + Foo1 "baz1" + + Foo1 bar: + + "loading..." + + + + Foo2 "name2" + + Foo2 "baz2" + + Foo2 bar: + + "bar name" + + "bar description" + + + + Foo3 "name3" + + Foo3 "baz3" + + Foo3 bar: + + "loading..." + + + ++++++++++++++++++++++++
如果您希望我从非详细部分添加更多详细信息(例如动作创建者,常量,路由等,使用BarListDetail
组件与表单,POST等),请在评论中告诉我:)。
我的实施中有一些差异: