Ответ 1
Это моя реализация с использованием facebook Flux и Immutable.js, которые, как мне кажется, отвечают на многие ваши проблемы, основанные на нескольких эмпирических правилах:
МАГАЗИНЫ
- Магазины отвечают за сохранение состояния данных через Immutable.Record и сохраняют кеш через глобальный Immutable.OrderedMap ссылка
Record
на экземплярids
. - Сохраняет непосредственно вызов
WebAPIUtils
для операций читать и запускаетactions
для операций write. - Отношения между
RecordA
иFooRecordB
разрешаются из экземпляраRecordA
с помощью параметровfoo_id
и извлекаются с помощью вызова, такого какFooStore.get(this.foo_id)
- В магазинах хранятся только методы
getters
, такие какget(id)
,getAll()
и т.д.
APIUTILS
- Я использую SuperAgent для вызовов ajax. Каждый запрос завернут в
Promise
- Я использую карту read запроса
Promise
, индексированного хешем URL + params - Я запускаю действие через ActionCreators, например fooReceived или fooError, когда
Promise
разрешен или отклонен. -
fooError
действие обязательно должно содержать полезную нагрузку с ошибками проверки, возвращаемыми сервером.
компоненты
- Компонент просмотра контроллера прослушивает изменения в хранилищах.
- Все мои компоненты, кроме компонента контроллера, являются "чистыми", поэтому я использую ImmutableRenderMixin только для повторного отображения того, что действительно нужно (это означает, что if вы печатаете
Perf.printWasted
время, оно должно быть очень низким, несколько мс. - Поскольку Relay и GraphQL еще не открыты, я обязуюсь как можно более четко указать мой компонент
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 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 do that for the example 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
:
Состояние 1:
- Пользователь попадает на страницу/foos/перечисляет Foos через компонент
FooList
для просмотра контроллера -
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
не обрабатывает корректное отображение на необработанном объекте). - Мы передаем необходимые реквизиты для компонента
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` отвечает на это действие, обновляя его внутреннее хранилище и испуская изменения. Что теперь самое интересное...
Состояние 3:
- Компонент контроллера
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 и т.д.), просто скажите мне в комментариях:).