(Примітка. Я використовував синтаксис ES6 за допомогою параметра JSX Harmony.)
Як вправу я написав зразок програми Flux, який дозволяє переглядати Github users
та повторно репонувати.
Він заснований на відповіді Fisherwebdev, але також відображає підхід, який я використовую для нормалізації відповідей API.
Я зробив це, щоб задокументувати кілька підходів, які я спробував під час навчання Flux.
Я намагався тримати його поблизу від реального світу (сторінки розпізнавання, відсутні фальшиві API LocalStorage).
Тут є кілька бітів, які мене особливо зацікавили:
Як класифікувати магазини
Я намагався уникати деяких дублювань, які я бачив в інших прикладах Flux, зокрема в магазинах. Мені було корисно логічно розділити магазини на три категорії:
У Магазинах вмісту містяться всі об’єкти програм. Все, що має ідентифікатор, потребує власного магазину вмісту. Компоненти, які відображають окремі елементи, запитують у магазинах вмісту свіжі дані.
Магазини вмісту збирають свої об’єкти з усіх дій сервера. Наприклад, UserStore
вивчає,action.response.entities.users
чи існує вона незалежно від того, яку дію запустили. Немає потреби в switch
. Normalizr дозволяє легко вирівняти будь-які репозіції API до цього формату.
// Content Stores keep their data like this
{
7: {
id: 7,
name: 'Dan'
},
...
}
Магазини списку відстежують ідентифікатори суб’єктів, які відображаються в деякому глобальному списку (наприклад, "канал", "ваші сповіщення"). У цьому проекті у мене немає таких магазинів, але я думав, що все одно про них згадую. Вони обробляють пагінацію.
Як правило , вони реагують на всього кілька дій (наприклад REQUEST_FEED
, REQUEST_FEED_SUCCESS
, REQUEST_FEED_ERROR
).
// Paginated Stores keep their data like this
[7, 10, 5, ...]
Магазини індексованих списків схожі на магазини списків, але вони визначають відносини один до багатьох. Наприклад, "абоненти користувачів", "зірки репозиторію", "сховища користувачів". Вони також обробляють пагінацію.
Крім того, вони зазвичай реагують на всього лише кілька дій (наприклад REQUEST_USER_REPOS
, REQUEST_USER_REPOS_SUCCESS
, REQUEST_USER_REPOS_ERROR
).
У більшості соціальних додатків їх буде багато, і ви хочете швидко створити ще один із них.
// Indexed Paginated Stores keep their data like this
{
2: [7, 10, 5, ...],
6: [7, 1, 2, ...],
...
}
Примітка: це не фактичні класи чи щось таке; так я люблю думати про магазини. Але я зробив кілька помічників.
createStore
Цей метод дає вам найпростіший магазин:
createStore(spec) {
var store = merge(EventEmitter.prototype, merge(spec, {
emitChange() {
this.emit(CHANGE_EVENT);
},
addChangeListener(callback) {
this.on(CHANGE_EVENT, callback);
},
removeChangeListener(callback) {
this.removeListener(CHANGE_EVENT, callback);
}
}));
_.each(store, function (val, key) {
if (_.isFunction(val)) {
store[key] = store[key].bind(store);
}
});
store.setMaxListeners(0);
return store;
}
Я використовую його для створення всіх Магазинів.
isInBag
, mergeIntoBag
Невеликі помічники, корисні для контент-магазинів.
isInBag(bag, id, fields) {
var item = bag[id];
if (!bag[id]) {
return false;
}
if (fields) {
return fields.every(field => item.hasOwnProperty(field));
} else {
return true;
}
},
mergeIntoBag(bag, entities, transform) {
if (!transform) {
transform = (x) => x;
}
for (var key in entities) {
if (!entities.hasOwnProperty(key)) {
continue;
}
if (!bag.hasOwnProperty(key)) {
bag[key] = transform(entities[key]);
} else if (!shallowEqual(bag[key], entities[key])) {
bag[key] = transform(merge(bag[key], entities[key]));
}
}
}
Зберігає стан сторінок і застосовує певні твердження (неможливо отримати сторінку під час отримання тощо).
class PaginatedList {
constructor(ids) {
this._ids = ids || [];
this._pageCount = 0;
this._nextPageUrl = null;
this._isExpectingPage = false;
}
getIds() {
return this._ids;
}
getPageCount() {
return this._pageCount;
}
isExpectingPage() {
return this._isExpectingPage;
}
getNextPageUrl() {
return this._nextPageUrl;
}
isLastPage() {
return this.getNextPageUrl() === null && this.getPageCount() > 0;
}
prepend(id) {
this._ids = _.union([id], this._ids);
}
remove(id) {
this._ids = _.without(this._ids, id);
}
expectPage() {
invariant(!this._isExpectingPage, 'Cannot call expectPage twice without prior cancelPage or receivePage call.');
this._isExpectingPage = true;
}
cancelPage() {
invariant(this._isExpectingPage, 'Cannot call cancelPage without prior expectPage call.');
this._isExpectingPage = false;
}
receivePage(newIds, nextPageUrl) {
invariant(this._isExpectingPage, 'Cannot call receivePage without prior expectPage call.');
if (newIds.length) {
this._ids = _.union(this._ids, newIds);
}
this._isExpectingPage = false;
this._nextPageUrl = nextPageUrl || null;
this._pageCount++;
}
}
createListStore
, createIndexedListStore
,createListActionHandler
Створює магазини індексованих списків максимально просто, надаючи методи котлована та керування діями:
var PROXIED_PAGINATED_LIST_METHODS = [
'getIds', 'getPageCount', 'getNextPageUrl',
'isExpectingPage', 'isLastPage'
];
function createListStoreSpec({ getList, callListMethod }) {
var spec = {
getList: getList
};
PROXIED_PAGINATED_LIST_METHODS.forEach(method => {
spec[method] = function (...args) {
return callListMethod(method, args);
};
});
return spec;
}
/**
* Creates a simple paginated store that represents a global list (e.g. feed).
*/
function createListStore(spec) {
var list = new PaginatedList();
function getList() {
return list;
}
function callListMethod(method, args) {
return list[method].call(list, args);
}
return createStore(
merge(spec, createListStoreSpec({
getList: getList,
callListMethod: callListMethod
}))
);
}
/**
* Creates an indexed paginated store that represents a one-many relationship
* (e.g. user's posts). Expects foreign key ID to be passed as first parameter
* to store methods.
*/
function createIndexedListStore(spec) {
var lists = {};
function getList(id) {
if (!lists[id]) {
lists[id] = new PaginatedList();
}
return lists[id];
}
function callListMethod(method, args) {
var id = args.shift();
if (typeof id === 'undefined') {
throw new Error('Indexed pagination store methods expect ID as first parameter.');
}
var list = getList(id);
return list[method].call(list, args);
}
return createStore(
merge(spec, createListStoreSpec({
getList: getList,
callListMethod: callListMethod
}))
);
}
/**
* Creates a handler that responds to list store pagination actions.
*/
function createListActionHandler(actions) {
var {
request: requestAction,
error: errorAction,
success: successAction,
preload: preloadAction
} = actions;
invariant(requestAction, 'Pass a valid request action.');
invariant(errorAction, 'Pass a valid error action.');
invariant(successAction, 'Pass a valid success action.');
return function (action, list, emitChange) {
switch (action.type) {
case requestAction:
list.expectPage();
emitChange();
break;
case errorAction:
list.cancelPage();
emitChange();
break;
case successAction:
list.receivePage(
action.response.result,
action.response.nextPageUrl
);
emitChange();
break;
}
};
}
var PaginatedStoreUtils = {
createListStore: createListStore,
createIndexedListStore: createIndexedListStore,
createListActionHandler: createListActionHandler
};
Міксин, який дозволяє компонентам налаштовуватися на магазини, які їх цікавлять, наприклад mixins: [createStoreMixin(UserStore)]
.
function createStoreMixin(...stores) {
var StoreMixin = {
getInitialState() {
return this.getStateFromStores(this.props);
},
componentDidMount() {
stores.forEach(store =>
store.addChangeListener(this.handleStoresChanged)
);
this.setState(this.getStateFromStores(this.props));
},
componentWillUnmount() {
stores.forEach(store =>
store.removeChangeListener(this.handleStoresChanged)
);
},
handleStoresChanged() {
if (this.isMounted()) {
this.setState(this.getStateFromStores(this.props));
}
}
};
return StoreMixin;
}
UserListStore
із усіма відповідними користувачами. І кожен користувач матиме декілька булевих прапорів, що описують відношення до поточного профілю користувача. Щось подібне{ follower: true, followed: false }
, наприклад. МетодиgetFolloweds()
таgetFollowers()
отримають різні набори користувачів, які вам потрібні для користувальницького інтерфейсу.