这是我使用 facebook Flux 和 Immutable.js 的实现,我认为根据一些经验法则,我认为它可以满足您的许多担忧:
STORES
- 存储负责通过以下方式维护数据状态不可变记录并通过全局维护缓存不可变的有序映射参考
Record
实例通过ids
.
- 门店直接致电
WebAPIUtils
for read操作和触发actions
for write运营。
- 之间的关系
RecordA
and FooRecordB
解决自RecordA
实例通过一个foo_id
params 并通过调用检索,例如FooStore.get(this.foo_id)
- 商店只暴露
getters
方法如get(id)
, getAll()
, etc.
APIUTILS
- I use 超级特工用于 ajax 调用。每个请求都包含在
Promise
- 我使用的地图read要求
Promise
通过 url + params 的哈希索引
- 我通过 ActionCreators 触发操作,例如 fooReceived 或 fooError
Promise
已解决或被拒绝。
-
fooError
操作当然应该包含服务器返回的带有验证错误的有效负载。
成分
- 控制器视图组件监听存储中的更改。
- 除了控制器视图组件之外,我的所有组件都是“纯”的,所以我使用不可变渲染混合仅重新渲染真正需要的内容(这意味着如果您打印
Perf.printWasted
时间,应该非常低,几毫秒。
- Since 中继和 GraphQL尚未开源,我强制保留我的组件
props
尽可能明确地通过propsType
.
- 父组件应该只传递必要的道具。如果我的父组件包含一个对象,例如
var fooRecord = { foo:1, bar: 2, baz: 3};
(我没有使用Immutable.Record
为了简单起见,这里)和我的子组件需要显示fooRecord.foo
and fooRecord.bar
, I do not通过整个foo
对象但仅fooRecordFoo
and fooRecordBar
作为我的子组件的道具,因为其他组件可以编辑foo.baz
值,使子组件重新渲染,而该组件根本不需要这个值!
ROUTING- 我只是用反应路由器
执行
这是一个基本示例:
api
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...
};
stores
商店/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;
成分
组件/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>
)
},
});
组件/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
:
State 1:
- 用户通过以下方式点击页面 /foos/ 列出 Foos
FooList
控制器视图组件
-
FooList
控制器视图组件调用FooStore.getAll()
-
_foos
地图为空FooStore
so FooStore
通过执行请求FooAPI.getAll()
- The
FooList
控制器视图组件将自身呈现为加载状态,因为它state.fooList.size == 0
.
这是我们列表的实际外观:
++++++++++++++++++++++++
+ +
+ "loading..." +
+ +
++++++++++++++++++++++++
-
FooAPI.getAll()
请求解析并触发FooActionCreators.receiveAllSuccess
action
-
FooStore
接收此操作,更新其内部状态,并发出更改。
State 2:
-
FooList
控制器视图组件接收更改事件并更新其状态以从FooStore
-
this.state.fooList.size
不再是== 0
所以列表实际上可以呈现自身(请注意,我们使用toJS()
显式获取原始 javascript 对象React
尚未正确处理非原始对象上的映射)。
- 我们正在将所需的道具传递给
FooListItem
成分。
- 通过致电
foo.getBar()
我们要告诉FooStore
我们想要的Bar
记录回来。
-
getBar()
的方法Foo
记录检索Bar
记录通过BarStore
-
BarStore
没有这个Bar
记录在其_bars
缓存,所以它通过触发请求BarAPI
来检索它。
- 所有人都会发生同样的情况
Foo
in this.sate.fooList
of 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
酒吧动作创作者.
- The
BarStore` 通过更新其内部存储并发出更改来响应此操作。这就是现在有趣的部分......
State 3:
- The
FooList
控制器视图组件响应BarStore
通过更新其状态来改变。
- The
render
方法被称为
- The
foo.getBar()
现在调用检索一个真实的Bar
记录自BarStore
。从此Bar
记录已被有效检索,ImmutablePureRenderMixin
将比较旧的 props 和当前的 props 并确定Bar
对象发生了变化!宾果,我们可以重新渲染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 等的组件)只需在评论中告诉我:)。