这是我使用 facebook Flux 和 Immutable.js 的实现,我认为它响应了您的许多担忧,基于一些经验法则:
商店
- Stores 负责通过Immutable.Record维护数据状态,并通过全局Immutable.OrderedMap引用
Record
实例维护缓存ids
。
- 商店直接调用
WebAPIUtils
的读取操作和触发actions
的写操作。
RecordA
和之间的关系通过参数FooRecordB
从RecordA
实例解析foo_id
并通过调用检索,例如FooStore.get(this.foo_id)
- 商店只公开
getters
的方法,如get(id)
,getAll()
等。
应用程序
- 我使用SuperAgent进行 ajax 调用。每个请求都包含在
Promise
- 我使用由 url + params 的哈希索引的读取请求映射
Promise
- 当
Promise
解决或拒绝时,我通过 ActionCreators 触发操作,例如 fooReceived 或 fooError 。
fooError
动作当然应该包含带有服务器返回的验证错误的有效负载。
组件
- 控制器视图组件侦听存储中的更改。
- 我的所有组件,除了控制器视图组件,都是“纯”的,所以我使用ImmutableRenderMixin只重新渲染它真正需要的东西(这意味着如果你打印
Perf.printWasted
时间,它应该非常低,几毫秒。
- 由于Relay 和 GraphQL尚未开源,我强制
props
通过propsType
.
- 父组件应该只传递必要的props。如果我的父母组件中包含诸如对象
var fooRecord = { foo:1, bar: 2, baz: 3};
(我使用的不是Immutable.Record
这里的这个例子的简单起见)和我的子组件需要显示fooRecord.foo
和fooRecord.bar
,我不传递整个foo
对象,但只fooRecordFoo
和fooRecordBar
props到我的子组件因为其他组件可以编辑该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>
)
},
});
让我们通过一个完整的循环 for FooList
:
状态 1:
- 用户通过
FooList
控制器视图组件点击页面 /foos/ 列出 Foos
FooList
控制器视图组件调用 FooStore.getAll()
_foos
地图为空,FooStore
因此FooStore
执行请求通过FooAPI.getAll()
- 所述
FooList
控制器视图组件呈现自身作为自其装载状态state.fooList.size == 0
。
这是我们列表的实际外观:
++++++++++++++++++++++++
+ +
+ "loading..." +
+ +
++++++++++++++++++++++++
FooAPI.getAll()
请求解析并触发FooActionCreators.receiveAllSuccess
操作
FooStore
接收此操作,更新其内部状态,并发出更改。
状态 2:
FooList
控制器视图组件接收更改事件并更新其状态以从 FooStore
this.state.fooList.size
不再== 0
因此列表可以实际呈现自身(请注意,我们使用toJS()
显式获取原始 javascript 对象,因为React
尚未正确处理非原始对象上的映射)。
- 我们将需要的props传递给
FooListItem
组件。
- 通过打电话,
foo.getBar()
我们告诉FooStore
我们想要Bar
恢复记录。
getBar()
Foo
记录方法Bar
通过检索记录BarStore
BarStore
Bar
在它的_bars
缓存中没有这个记录,所以它触发一个请求BarAPI
来检索它。
- 同样的情况对于所有
Foo
在this.sate.fooList
的FooList
控制器视图分量
- 该页面现在看起来像这样:
++++++++++++++++++++++++++
+ +
+ Foo1 "name1" +
+ Foo1 "baz1" +
+ Foo1 条:+
+ "加载中..." +
+ +
+ Foo2 "name2" +
+ Foo2 "baz2" +
+ Foo2 栏:+
+ "加载中..." +
+ +
+ Foo3 "name3" +
+ Foo3 "baz3" +
+ Foo3 栏:+
+ "加载中..." +
+ +
++++++++++++++++++++++++++
- 现在假设BarAPI.get(2)
(由 Foo2 请求)在BarAPI.get(1)
(由 Foo1 请求)之前解决。由于它是异步的,因此完全合理。-BarAPI
触发BAR_RECEIVED_SUCCESS' action via the
BarActionCreators .
- The
BarStore` 通过更新其内部存储并发出更改来响应此操作。这就是现在有趣的部分......
状态 3:
- 所述
FooList
控制器查看组件来响应BarStore
通过更新其状态改变。
- 该
render
方法被称为
- 该
foo.getBar()
调用现在Bar
从 中检索真实记录BarStore
。由于此Bar
记录已被有效检索,因此ImmutablePureRenderMixin
将比较旧props与当前props并确定Bar
对象已更改!Bingo,我们可以重新渲染FooListItem
组件(这里更好的方法是创建一个单独的 FooListBarDetail 组件,只让这个组件重新渲染,这里我们也重新渲染了 Foo 没有改变的细节,但为了简单,让我们这样做)。
- 该页面现在看起来像这样:
++++++++++++++++++++++++++
+ +
+ Foo1 "name1" +
+ Foo1 "baz1" +
+ Foo1 条:+
+ "加载中..." +
+ +
+ Foo2 "name2" +
+ Foo2 "baz2" +
+ Foo2 栏:+
+ "酒吧名称" +
+ "条形描述" +
+ +
+ Foo3 "name3" +
+ Foo3 "baz3" +
+ Foo3 栏:+
+ "加载中..." +
+ +
++++++++++++++++++++++++++
如果您希望我从非详细部分添加更多详细信息(例如动作创建者、常量、路由等,使用BarListDetail
带有表单的组件、POST 等),请在评论中告诉我:)。