(Nota: ho usato la sintassi ES6 usando l'opzione JSX Harmony.)
Come esercizio, ho scritto un'app Flux di esempio che consente di navigare Github users
e riposizionare.
Si basa sulla risposta di fisherwebdev ma riflette anche un approccio che utilizzo per normalizzare le risposte API.
L'ho fatto per documentare alcuni approcci che ho provato durante l'apprendimento di Flux.
Ho cercato di tenerlo vicino al mondo reale (impaginazione, nessuna API locale di archivio falsa).
Ci sono alcuni bit qui a cui ero particolarmente interessato:
Come classifico i negozi
Ho cercato di evitare alcune delle duplicazioni che ho visto in altri esempi di Flux, in particolare nei negozi. Ho trovato utile dividere logicamente i negozi in tre categorie:
I Content Store contengono tutte le entità app. Tutto ciò che ha un ID ha bisogno del proprio Content Store. I componenti che rendono singoli elementi richiedono Content Store per i nuovi dati.
I Content Store raccolgono i loro oggetti da tutte le azioni del server. Ad esempio, UserStore
esaminaaction.response.entities.users
se esiste indipendentemente dall'azione attivata. Non è necessario per a switch
. Normalizr semplifica l'appiattimento di qualsiasi risposta API in questo formato.
// Content Stores keep their data like this
{
7: {
id: 7,
name: 'Dan'
},
...
}
I List List tengono traccia degli ID delle entità che compaiono in alcuni elenchi globali (ad es. "Feed", "le tue notifiche"). In questo progetto, non ho tali negozi, ma ho pensato di menzionarli comunque. Gestiscono l'impaginazione.
Normalmente rispondono a poche azioni (ad es REQUEST_FEED
. REQUEST_FEED_SUCCESS
, REQUEST_FEED_ERROR
).
// Paginated Stores keep their data like this
[7, 10, 5, ...]
I List List List sono simili ai List List ma definiscono una relazione uno-a-molti. Ad esempio, "abbonati dell'utente", "stargazers del repository", "repository dell'utente". Gestiscono anche l'impaginazione.
Normalmente rispondono anche a poche azioni (ad es REQUEST_USER_REPOS
. REQUEST_USER_REPOS_SUCCESS
, REQUEST_USER_REPOS_ERROR
).
Nella maggior parte delle app social, ne avrai molte e desideri poterne creare rapidamente un'altra.
// Indexed Paginated Stores keep their data like this
{
2: [7, 10, 5, ...],
6: [7, 1, 2, ...],
...
}
Nota: queste non sono classi reali o qualcosa del genere; è proprio come mi piace pensare ai negozi. Ho fatto alcuni aiutanti però.
createStore
Questo metodo ti offre lo Store più semplice:
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;
}
Lo uso per creare tutti i negozi.
isInBag
, mergeIntoBag
Piccoli aiutanti utili per i Content Store.
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]));
}
}
}
Memorizza lo stato di impaginazione e applica alcune asserzioni (impossibile recuperare la pagina durante il recupero, ecc.).
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
Semplifica al massimo la creazione di elenchi di elenchi indicizzati fornendo metodi di gestione delle piastre e gestione delle azioni:
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
};
Un mixin che consente ai componenti di sintonizzarsi sui negozi a cui sono interessati, ad es 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
, con tutti gli utenti pertinenti in esso. E ogni utente avrebbe un paio di flag booleani che descrivono la relazione con il profilo utente corrente. Qualcosa come{ follower: true, followed: false }
, per esempio. I metodigetFolloweds()
egetFollowers()
recuperare i diversi set di utenti necessari per l'interfaccia utente.