(Uwaga: użyłem składni ES6 przy użyciu opcji JSX Harmony.)
W ramach ćwiczenia napisałem przykładową aplikację Flux, która umożliwia przeglądanie Github users
i repozytorium.
Opiera się na odpowiedzi fisherwebdev, ale odzwierciedla również podejście, które stosuję do normalizowania odpowiedzi API.
Zrobiłem to, aby udokumentować kilka podejść, które wypróbowałem podczas nauki Flux.
Starałem się, aby był jak najbardziej zbliżony do prawdziwego świata (paginacja, brak fałszywych API localStorage).
Jest tu kilka bitów, które mnie szczególnie zainteresowały:
- Używa architektury Flux i routera React ;
- Może wyświetlać stronę użytkownika z częściowymi znanymi informacjami i ładować szczegóły w podróży;
- Obsługuje paginację zarówno dla użytkowników, jak i repozytoriów;
- Analizuje zagnieżdżone odpowiedzi JSON Githuba za pomocą normalizr ;
- Sklepy z zawartością nie muszą zawierać giganta
switch
z akcjami ;
- „Wstecz” jest natychmiastowy (ponieważ wszystkie dane są w Sklepach).
Jak klasyfikuję sklepy
Próbowałem uniknąć niektórych powielania, które widziałem w innych przykładach Flux, szczególnie w sklepach. Uznałem, że warto logicznie podzielić Sklepy na trzy kategorie:
Magazyny treści przechowują wszystkie jednostki aplikacji. Wszystko, co ma identyfikator, potrzebuje własnego magazynu treści. Komponenty, które renderują poszczególne elementy, proszą magazyny zawartości o świeże dane.
Magazyny zawartości zbierają swoje obiekty ze wszystkich działań na serwerze. Na przykład UserStore
sprawdza,action.response.entities.users
czy istnieje, niezależnie od uruchomionej akcji. Nie ma potrzeby posiadania pliku switch
. Normalizr ułatwia spłaszczanie wszelkich odpowiedzi API do tego formatu.
{
7: {
id: 7,
name: 'Dan'
},
...
}
Magazyny z listami śledzą identyfikatory podmiotów, które pojawiają się na jakiejś globalnej liście (np. „Kanał”, „Twoje powiadomienia”). W tym projekcie nie mam takich Sklepów, ale pomyślałem, że i tak o nich wspomnę. Obsługują paginację.
Normalnie reagują oni do zaledwie kilku czynności (np REQUEST_FEED
, REQUEST_FEED_SUCCESS
, REQUEST_FEED_ERROR
).
[7, 10, 5, ...]
Magazyny z listami indeksowanymi są podobne do magazynów z listami, ale definiują relację jeden do wielu. Na przykład „subskrybenci użytkownika”, „obserwatorzy repozytorium”, „repozytoria użytkownika”. Obsługują również paginację.
Także zwykle reagują oni do zaledwie kilku czynności (np REQUEST_USER_REPOS
, REQUEST_USER_REPOS_SUCCESS
, REQUEST_USER_REPOS_ERROR
).
W większości aplikacji społecznościowych będziesz mieć ich dużo i chcesz mieć możliwość szybkiego utworzenia jeszcze jednej.
{
2: [7, 10, 5, ...],
6: [7, 1, 2, ...],
...
}
Uwaga: to nie są rzeczywiste klasy czy coś takiego; właśnie tak lubię myśleć o Sklepach. Zrobiłem jednak kilku pomocników.
createStore
Ta metoda zapewnia najbardziej podstawowy sklep:
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;
}
Używam go do tworzenia wszystkich Sklepów.
isInBag
, mergeIntoBag
Mali pomocnicy przydatni w magazynach zawartości.
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]));
}
}
}
Przechowuje stan stronicowania i wymusza pewne potwierdzenia (nie można pobrać strony podczas pobierania itp.).
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
Sprawia, że tworzenie magazynów z listami indeksowanymi jest tak proste, jak to tylko możliwe, zapewniając standardowe metody i obsługę akcji:
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;
}
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
}))
);
}
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
}))
);
}
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
};
Mixin, który umożliwia komponentom dostrojenie się do Sklepów, którymi są zainteresowane, np 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
, ze wszystkimi istotnymi użytkownikami w nim. Każdy użytkownik miałby kilka flag logicznych opisujących związek z bieżącym profilem użytkownika. Na przykład coś takiego{ follower: true, followed: false }
. MetodygetFolloweds()
igetFollowers()
będą pobierać różne zestawy użytkowników, których potrzebujesz dla interfejsu użytkownika.