Am'm citit despre Flux, dar exemplu Todo app este prea simplistă pentru mine să înțeleg unele puncte-cheie.
Imaginați-vă o singură pagină aplicații ca Facebook că a profil utilizator pagini. Pe fiecare pagina de profil de utilizator, vrem să arătăm unele informații de utilizator și ultimele lor posturi, cu scroll infinit. Putem naviga de la un profil de utilizator la altul.
În arhitectura Flux, cum ar fi acest lucru corespunde Magazine și Dispecerii?
Am putea sa o folosim PostStore
per utilizator, sau ne-ar fi un fel de magazin la nivel mondial? Ce zici de dispeceri, va vom crea un nou Dispecer pentru fiecare "pagina utilizatorului", sau ne-ar folosi un singleton? În cele din urmă, ce face parte din arhitectura este responsabil pentru gestionarea ciclului de viață "pagină" Magazine ca răspuns la schimbare de traseu?
Mai mult decât atât, un singur pseudo-pagina poate avea mai multe liste de date de același tip. De exemplu, pe o pagină de profil, vreau să arăt atât Urmași și după cum Urmează. Cum poate un singleton UserStore în acest caz? Ar
UserPageStore "administrare" urmată de: UserStore " și " cum urmează: UserStore`?
Într-un Flux app ar trebui să fie doar un Dispecer. Toate fluxurile de date prin acest hub central. Având un singleton Dispecer permite să gestionați toate Magazinele. Acest lucru devine important atunci când aveți nevoie Store #1 actualizarea în sine, și apoi au Magazin #2 actualizarea în sine, bazată atât pe Acțiune și de pe statul de Magazin #1. Flux presupune această situație este o eventualitate într-o mare aplicație. În mod ideal, această situație nu ar trebui să se întâmple, și dezvoltatorii ar trebui să depună eforturi pentru a evita această complexitate, dacă este posibil. Dar singleton Dispecer este gata să se ocupe de ea, atunci când vine momentul.
Magazinele sunt singletons la fel de bine. Ei ar trebui să rămână independent și decuplate posibil-o auto-conținută univers care se poate interoga, de la un Controler de Vedere. Singurul drum în Magazin este prin apel invers se înregistrează cu Dispeceratul. Singura cale de ieșire este prin getter funcții. Magazine publica, de asemenea, un eveniment atunci când starea lor s-a schimbat, deci Controller-Vedere poate ști când să interogare pentru noul stat, folosind getters.
În aplicație exemplu, ar exista o singur PostStore
. Acest fel de magazin ar putea gestiona mesaje de pe un "pagina" (pseudo-page) care este mai mult ca FB's Newsfeed, în cazul în care apar mesajele de la utilizatori diferiți. Sa logică domeniu este lista de posturi, și se poate ocupa orice listă de posturi. Când vom trece de la pseudo-pagina pseudo-pagină, ne-o dorim pentru a reinițializa stat din magazin pentru a reflecta noul stat. Am putea, de asemenea, doresc să cache starea anterioară în localStorage ca o optimizare pentru deplasarea înainte și înapoi între pseudo-pagini, dar înclinația mea ar fi să înființeze o PageStore
care asteapta pentru toate celelalte magazine, gestionează relația cu localStorage pentru toate magazinele pe pseudo-pagină, și apoi actualizează propriile stat. Rețineți că această PageStore
ar stoca nimic despre posturi ... asta's domeniu de PostStore`. Ar fi pur și simplu știu dacă un anumit pseudo-pagina a fost memorate în cache sau nu, pentru că pseudo-paginile sunt de domeniul său.
Anii PostStore
ar fi o initialize () metodă. Această metodă ar întotdeauna clar vechea stare, chiar dacă aceasta este prima inițializare, și apoi a crea stat bazat pe datele primite prin Acțiune, prin Dispecer. Trecerea de la un pseudo-pagină la alta, probabil, ar implica o
PAGE_UPDATE acțiune, care ar declanșa invocarea initialize()
. Acolo sunt detalii pentru a lucra în jurul valorii de preluarea datelor din cache-ul local, preluarea datelor de pe server, optimist redare și XHR eroare membre, dar aceasta este ideea.
Dacă un anumit pseudo-pagina nu are nevoie de toate Magazinele din aplicație, am'm nu este în întregime sigur că nu există nici un motiv pentru a distruge cele neutilizate, altele decât constrângeri de memorie. Dar magazinele nu't de obicei, consumă o mare parte din memorie. Trebuie doar să asigurați-vă că pentru a elimina ascultătorii eveniment din Controller-Vedere te distrug. Acest lucru se face în Reacționa's `componentWillUnmount () metodă.
(Notă: am folosit ES6 sintaxă, folosind JSX Armonie opțiune.)
Ca un exercițiu, am scris un eșantion Flux app, care vă permite să răsfoiți `Github utilizatorilor și repos. Ea se bazează pe fisherwebdev's a răspunde, dar reflectă, de asemenea, o abordare folosesc pentru normalizarea API răspunsuri.
Am făcut-o pentru a documenta câteva abordări am încercat în timp ce de învățare Flux. Am încercat să-l țină aproape de lumea reală (paginare, nici fals localStorage APIs).
Există câteva biți de aici am fost interesati in special de:
Am încercat să evit unele dintre dublarea am'am văzut în alte Fluxului de exemplu, în special în Magazine. Am găsit că este util să logic Magazine împărți în trei categorii:
ConținutulMagazine** țineți toate app entități. Tot ce are un ID are nevoie de propriul său Magazin de Conținut. Componentele care face elemente individuale cere Conținut Magazine pentru date proaspete.
Conținutul Magazine de recolta lor obiecte din toate server de acțiuni. De exemplu, UserStore
arată înaction.response.entities.users
dacă există indiferent de acțiune care a tras. Nu este nevoie de un "comutator". Normalizr îl face ușor pentru a aplatiza orice API reponses pentru acest format.
// Content Stores keep their data like this
{
7: {
id: 7,
name: 'Dan'
},
...
}
Lista de Magazine țineți evidența de Id-uri de entitățile care apar în unele listă la nivel mondial (de exemplu, "furaje", "notificări"). În acest proiect, am don't au astfel de Magazine, dar am crezut că am'd mai vorbim oricum. Ei se ocupe de paginare.
Ei raspund in mod normal la doar câteva acțiuni (de exemplu, REQUEST_FEED
, REQUEST_FEED_SUCCESS
, REQUEST_FEED_ERROR
).
// Paginated Stores keep their data like this
[7, 10, 5, ...]
Listă indexată de Magazine sunt ca o Listă de Magazine, dar ele definesc unul-la-multe relații. De exemplu, "utilizator's abonați", "depozit's stargazers", "utilizator's magazii". Se ocupa, de asemenea, paginare.
Ei au, de asemenea, răspunde în mod normal la doar câteva acțiuni (de exemplu, REQUEST_USER_REPOS
, REQUEST_USER_REPOS_SUCCESS
, REQUEST_USER_REPOS_ERROR
).
În cele mai multe aplicații sociale, ai'll au o mulțime de aceste și doriți să fie capabil de a crea rapid unul dintre ele mai mult.
// Indexed Paginated Stores keep their data like this
{
2: [7, 10, 5, ...],
6: [7, 1, 2, ...],
...
}
Notă: acestea nu sunt reale clase sau ceva; it's tocmai cum îmi place să cred despre Magazine. Am făcut câteva ajutoare, deși.
StoreUtils
createStore
Această metodă vă oferă cele mai de bază de Magazin:
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;
}
Am folosi pentru a crea toate Magazinele.
isInBag
, mergeIntoBag
Mici ajutoare utile pentru Conținutul Magazine.
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]));
}
}
}
PaginatedList
Magazine de paginare de stat și impune anumite afirmații (poate't aduce pagină în timp ce preluarea, etc).
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++;
}
}
PaginatedStoreUtils
createListStore
, createIndexedListStore
, createListActionHandler
Face crearea de Listă Indexată de Magazine la fel de simplu posibil, prin furnizarea de șabloane metode și acțiuni de manipulare:
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
};
createStoreMixin
Un mixin care permite componentelor să tune în la Magazinele au're interesat în, de exemplu, 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;
}
Deci, în Reflux conceptul de Dispecer este eliminat și trebuie doar să se gândească în termeni de flux de date prin acțiuni și magazine. I. e.
Actions <-- Store { <-- Another Store } <-- Components
Fiecare săgeată aici modele de modul în care fluxul de date este ascultat, care, la rândul său, înseamnă că fluxurile de date în direcția opusă. Cifra reală pentru fluxul de date este:
Actions --> Stores --> Components
^ | |
+----------+------------+
În caz de utilizare, dacă am înțeles corect, avem nevoie de un `openUserProfile acțiune care inițiază profil de utilizator de încărcare și de comutare pagină și, de asemenea, unele posturi de încărcare acțiuni care vor posturi de încărcare atunci când pagina de profil de utilizator este deschis și în timpul de parcurgere infinit de evenimente. Așa că m-am'd imaginați-vă că avem următoarele date stochează în aplicația:
În Reflux te'd setați-l astfel:
// Set up the two actions we need for this use case.
var Actions = Reflux.createActions(['openUserProfile', 'loadUserProfile', 'loadInitialPosts', 'loadMorePosts']);
var currentPageStore = Reflux.createStore({
init: function() {
this.listenTo(openUserProfile, this.openUserProfileCallback);
},
// We are assuming that the action is invoked with a profileid
openUserProfileCallback: function(userProfileId) {
// Trigger to the page handling component to open the user profile
this.trigger('user profile');
// Invoke the following action with the loaded the user profile
Actions.loadUserProfile(userProfileId);
}
});
var currentUserProfileStore = Reflux.createStore({
init: function() {
this.listenTo(Actions.loadUserProfile, this.switchToUser);
},
switchToUser: function(userProfileId) {
// Do some ajaxy stuff then with the loaded user profile
// trigger the stores internal change event with it
this.trigger(userProfile);
}
});
var currentPostsStore = Reflux.createStore({
init: function() {
// for initial posts loading by listening to when the
// user profile store changes
this.listenTo(currentUserProfileStore, this.loadInitialPostsFor);
// for infinite posts loading
this.listenTo(Actions.loadMorePosts, this.loadMorePosts);
},
loadInitialPostsFor: function(userProfile) {
this.currentUserProfile = userProfile;
// Do some ajax stuff here to fetch the initial posts then send
// them through the change event
this.trigger(postData, 'initial');
},
loadMorePosts: function() {
// Do some ajaxy stuff to fetch more posts then send them through
// the change event
this.trigger(postData, 'more');
}
});
Am'm presupunând că au o componentă pentru întreaga pagină vedere, pagina de profil de utilizator și lista de posturi. Următoarele trebuie să fie conectat:
currentPostsStore
pentru a primi încărcat de posturiȘi că ar trebui să fie destul de mult.