(Hinweis: Ich habe die ES6-Syntax mit der Option JSX Harmony verwendet.)
Als Übung habe ich eine Beispiel-Flux-App geschrieben , die das Durchsuchen Github users
und Repos ermöglicht.
Es basiert auf der Antwort von Fisherwebdev , spiegelt aber auch einen Ansatz wider, den ich zur Normalisierung von API-Antworten verwende.
Ich habe es geschafft, einige Ansätze zu dokumentieren, die ich beim Erlernen von Flux ausprobiert habe.
Ich habe versucht, es nahe an der realen Welt zu halten (Paginierung, keine gefälschten localStorage-APIs).
Hier sind einige Punkte, die mich besonders interessiert haben:
- Es verwendet die Flux-Architektur und den React-Router .
- Es kann eine Benutzerseite mit teilweise bekannten Informationen anzeigen und unterwegs Details laden.
- Es unterstützt die Paginierung sowohl für Benutzer als auch für Repos.
- Es analysiert Githubs verschachtelte JSON-Antworten mit normalizr .
- Content Stores müssen keinen Riesen
switch
mit Aktionen enthalten .
- "Zurück" ist sofort verfügbar (da sich alle Daten in Geschäften befinden).
Wie ich Geschäfte klassifiziere
Ich habe versucht, einige der Duplikate zu vermeiden, die ich in anderen Flux-Beispielen gesehen habe, insbesondere in Stores. Ich fand es nützlich, Stores logisch in drei Kategorien zu unterteilen:
Inhaltsspeicher enthalten alle App-Entitäten. Alles, was eine ID hat, benötigt einen eigenen Content Store. Komponenten, die einzelne Elemente rendern, fragen Content Stores nach den neuen Daten.
Inhaltsspeicher sammeln ihre Objekte aus allen Serveraktionen. Zum Beispiel UserStore
untersuchtaction.response.entities.users
, wenn es vorhanden ist, unabhängig davon Aktion gefeuert. Es besteht keine Notwendigkeit für eine switch
. Normalizr macht es einfach, API-Antworten auf dieses Format zu reduzieren.
// Content Stores keep their data like this
{
7: {
id: 7,
name: 'Dan'
},
...
}
Listenspeicher verfolgen die IDs von Entitäten, die in einer globalen Liste angezeigt werden (z. B. "Feed", "Ihre Benachrichtigungen"). In diesem Projekt habe ich keine solchen Geschäfte, aber ich dachte, ich würde sie trotzdem erwähnen. Sie behandeln die Paginierung.
Sie reagieren normalerweise auf wenige Aktionen (zB REQUEST_FEED
, REQUEST_FEED_SUCCESS
, REQUEST_FEED_ERROR
).
// Paginated Stores keep their data like this
[7, 10, 5, ...]
Indizierte Listenspeicher sind wie Listenspeicher, definieren jedoch eine Eins-zu-Viele-Beziehung. Zum Beispiel "Abonnenten des Benutzers", "Sterngucker des Repositorys", "Repositorys des Benutzers". Sie behandeln auch die Paginierung.
Sie reagieren auch normalerweise nur ein paar Aktionen (zB REQUEST_USER_REPOS
, REQUEST_USER_REPOS_SUCCESS
, REQUEST_USER_REPOS_ERROR
).
In den meisten sozialen Apps gibt es viele davon, und Sie möchten schnell eine weitere erstellen können.
// Indexed Paginated Stores keep their data like this
{
2: [7, 10, 5, ...],
6: [7, 1, 2, ...],
...
}
Hinweis: Dies sind keine tatsächlichen Klassen oder ähnliches. So denke ich gerne über Geschäfte nach. Ich habe allerdings ein paar Helfer gemacht.
createStore
Diese Methode bietet Ihnen den grundlegendsten Store:
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;
}
Ich benutze es, um alle Stores zu erstellen.
isInBag
, mergeIntoBag
Kleine Helfer für Content Stores.
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]));
}
}
}
Speichert den Paginierungsstatus und erzwingt bestimmte Zusicherungen (beim Abrufen können keine Seiten abgerufen werden usw.).
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
Vereinfacht die Erstellung indizierter Listenspeicher so einfach wie möglich, indem Boilerplate-Methoden und die Handhabung von Aktionen bereitgestellt werden:
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
};
Ein Mixin, mit dem Komponenten auf Stores zugreifen können, an denen sie interessiert sind, z 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
mit allen relevanten Benutzern. Und jeder Benutzer hätte ein paar boolesche Flags, die die Beziehung zum aktuellen Benutzerprofil beschreiben. So etwas{ follower: true, followed: false }
zum Beispiel. Die MethodengetFolloweds()
undgetFollowers()
würden die verschiedenen Benutzergruppen abrufen, die Sie für die Benutzeroberfläche benötigen.