(Not: JSX Harmony seçeneğini kullanarak ES6 sözdizimini kullandım.)
Bir alıştırma olarak, göz atmaya ve depoya koymaya izin veren örnek bir Flux uygulaması yazdım Github users
. Fisherwebdev'in cevabına
dayanıyor ama aynı zamanda API yanıtlarını normalleştirmek için kullandığım bir yaklaşımı yansıtıyor.
Flux'u öğrenirken denediğim birkaç yaklaşımı belgelemek için yaptım.
Onu gerçek dünyaya yakın tutmaya çalıştım (sayfalandırma, sahte localStorage API'leri yok).
Burada özellikle ilgilendiğim birkaç nokta var:
- Flux mimarisini ve react-router'ı kullanır ;
- Kısmi bilinen bilgileri içeren kullanıcı sayfasını gösterebilir ve hareket halindeyken ayrıntıları yükleyebilir;
- Hem kullanıcılar hem de depolar için sayfalandırmayı destekler;
- Github'ın iç içe geçmiş JSON yanıtlarını normalizr ile ayrıştırır ;
- İçerik Mağazalarının eylemleri olan bir dev içermesi gerekmez
switch
;
- "Geri" hemen verilir (çünkü tüm veriler Mağazalarda bulunur).
Mağazaları Nasıl Sınıflandırırım
Diğer Flux örneğinde, özellikle Mağazalarda gördüğüm bazı kopyalardan kaçınmaya çalıştım. Mağazaları mantıksal olarak üç kategoriye ayırmayı yararlı buldum:
İçerik Mağazaları tüm uygulama varlıklarını barındırır. Kimliği olan her şeyin kendi İçerik Mağazası olması gerekir. Tek tek öğeleri işleyen bileşenler, İçerik Mağazalarından yeni verileri ister.
İçerik Mağazaları, nesnelerini tüm sunucu eylemlerinden toplar. Örneğin, UserStore
içine bakaraction.response.entities.users
varsa bakılmaksızın eylem ateş hangi. A'ya gerek yoktur switch
. Normalizr , herhangi bir API yanıtını bu biçime göre düzleştirmeyi kolaylaştırır.
// Content Stores keep their data like this
{
7: {
id: 7,
name: 'Dan'
},
...
}
Liste Depoları , bazı küresel listede görünen varlıkların kimliklerini takip eder (örneğin, "besleme", "bildirimleriniz"). Bu projede böyle Mağazalarım yok ama yine de onlardan bahsedeyim diye düşündüm. Sayfalandırmayı onlar halleder.
Normalde (örneğin sadece birkaç eylemlerine yanıt REQUEST_FEED
, REQUEST_FEED_SUCCESS
, REQUEST_FEED_ERROR
).
// Paginated Stores keep their data like this
[7, 10, 5, ...]
Dizine Alınmış Liste Depoları , Liste Depoları gibidir, ancak bire çok ilişkisini tanımlarlar. Örneğin, "kullanıcının aboneleri", "bilgi havuzunun yıldız gözlemcileri", "kullanıcı havuzları". Ayrıca sayfalandırmayı da ele alırlar.
Onlar da normalde (örneğin sadece birkaç eylemlerine yanıt REQUEST_USER_REPOS
, REQUEST_USER_REPOS_SUCCESS
, REQUEST_USER_REPOS_ERROR
).
Çoğu sosyal uygulamada, bunlardan pek çoğuna sahip olacaksınız ve bunlardan bir tanesini hızlı bir şekilde oluşturmak isteyeceksiniz.
// Indexed Paginated Stores keep their data like this
{
2: [7, 10, 5, ...],
6: [7, 1, 2, ...],
...
}
Not: Bunlar gerçek sınıflar falan değildir; Mağazalar hakkında düşünmeyi sevdiğim gibi. Yine de birkaç yardımcı yaptım.
createStore
Bu yöntem size en temel Mağazayı verir:
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;
}
Tüm Mağazaları oluşturmak için kullanıyorum.
isInBag
, mergeIntoBag
İçerik Mağazaları için yararlı olan küçük yardımcılar.
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]));
}
}
}
Sayfalandırma durumunu depolar ve belirli iddiaları uygular (getirilirken sayfa getirilemez, vb.).
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
Ortak metin yöntemleri ve eylem yönetimi sağlayarak, Dizine Alınmış Liste Depolarının oluşturulmasını olabildiğince basit hale getirir:
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
};
Bileşenlerin ilgilendikleri Mağazaları ayarlamasına izin veren bir mixin, ör 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
, tüm ilgili kullanıcılarla birlikte a olacaktır. Ve her kullanıcı, mevcut kullanıcı profiliyle olan ilişkiyi açıklayan birkaç boole bayrağına sahip olacaktır.{ follower: true, followed: false }
Örneğin bir şey . YöntemlergetFolloweds()
vegetFollowers()
kullanıcı arabirimi için ihtiyacınız olan farklı kullanıcı gruplarını alır.