Flux mimarisinde Mağaza yaşam döngüsünü nasıl yönetirsiniz?


132

Flux hakkında bir şeyler okuyorum ama örnek Todo uygulaması bazı önemli noktaları anlayamayacak kadar basit.

Facebook gibi kullanıcı profili sayfalarına sahip tek sayfalık bir uygulama hayal edin . Her kullanıcı profili sayfasında, sonsuz kaydırma ile bazı kullanıcı bilgilerini ve son gönderilerini göstermek istiyoruz. Bir kullanıcı profilinden diğerine gidebiliriz.

Flux mimarisinde bu, Mağazalar ve Dağıtıcılara nasıl karşılık gelir?

PostStoreKullanıcı başına bir tane mi kullanırdık , yoksa bir çeşit global mağazamız mı olurdu? Sevk görevlileri ne olacak, her "kullanıcı sayfası" için yeni bir Dağıtıcı oluşturacak mıydık yoksa tek bir tane mi kullanacağız? Son olarak, rota değişikliğine yanıt olarak "sayfaya özgü" Mağazaların yaşam döngüsünü yönetmekten mimarinin hangi bölümü sorumludur?

Ayrıca, tek bir sözde sayfa aynı türden birkaç veri listesine sahip olabilir. Örneğin, bir profil sayfasında, her iki göstermek istiyorum Takip ve takip . UserStoreBu durumda bir singleton nasıl çalışır? Misiniz UserPageStoreyönetmek followedBy: UserStoreve follows: UserStore?

Yanıtlar:


124

Bir Flux uygulamasında yalnızca bir Dağıtıcı olmalıdır. Tüm veriler bu merkezi merkezden akar. Tek bir Dağıtıcıya sahip olmak, tüm Mağazaları yönetmesine olanak tanır. Bu, Mağaza 1'in kendisini güncellemesine ihtiyaç duyduğunuzda ve ardından 2 Numaralı Mağaza'nın hem Eylem hem de Mağaza 1'in durumuna göre kendisini güncellemesini sağladığınızda önemli hale gelir. Flux, bu durumun büyük bir uygulamada bir olasılık olduğunu varsayar. İdeal olarak, bu durumun olması gerekmez ve geliştiriciler mümkünse bu karmaşıklıktan kaçınmak için çaba göstermelidir. Ancak singleton Dispatcher, zamanı geldiğinde bununla başa çıkmaya hazırdır.

Mağazalar da tekildir. Mümkün olduğunca bağımsız ve ayrıştırılmış kalmalıdırlar - Kontrolcü Görünümünden sorgulayabileceğiniz bağımsız bir evren. Mağazaya giden tek yol, Sevk Görevlisine kaydettirdiği geri aramadır. Tek çıkış yolu alıcı işlevleridir. Mağazalar ayrıca durumları değiştiğinde bir olay yayınlar, böylece Controller-Views alıcıları kullanarak yeni durumu ne zaman sorgulayacağını bilir.

Örnek uygulamanızda bir single olacaktır PostStore. Aynı mağaza, farklı kullanıcılardan gelen gönderilerin göründüğü FB'nin Haber Beslemesine benzeyen bir "sayfadaki" (sözde sayfa) gönderileri yönetebilir. Mantıksal etki alanı, gönderilerin listesidir ve herhangi bir gönderi listesini işleyebilir. Sözde sayfadan sözde sayfaya geçtiğimizde, deponun durumunu yeni durumu yansıtacak şekilde yeniden başlatmak istiyoruz. Ayrıca, sahte sayfalar arasında ileri geri hareket etmek için bir optimizasyon olarak localStorage'daki önceki durumu önbelleğe almak isteyebiliriz, ancak benim eğilimim, PageStorediğer tüm mağazaları bekleyen, tüm mağazalar için localStorage ile ilişkiyi yöneten bir sözde sayfa ve ardından kendi durumunu günceller. Bunun PageStoregönderiler hakkında hiçbir şey saklamayacağını unutmayın - bu,PostStore. Sadece belirli bir sözde sayfanın önbelleğe alınıp alınmadığını bilir, çünkü sözde sayfalar onun etki alanıdır.

PostStoreBir olurdu initialize()yöntem. Bu yöntem, bu ilk başlatma olsa bile her zaman eski durumu temizler ve ardından Dispatcher aracılığıyla Eylem aracılığıyla aldığı verilere dayalı olarak durumu oluşturur. Bir sözde sayfadan diğerine PAGE_UPDATEgeçmek büyük olasılıkla bir eylemi içerecektir ve bu da çağrılmayı tetikleyecektir initialize(). Yerel önbellekten veri alma, sunucudan veri alma, iyimser oluşturma ve XHR hata durumları etrafında üzerinde çalışılması gereken ayrıntılar vardır, ancak genel fikir budur.

Belirli bir sözde sayfa uygulamadaki tüm Mağazalara ihtiyaç duymuyorsa, kullanılmayanları yok etmek için bellek kısıtlamaları dışında herhangi bir neden olup olmadığından tam olarak emin değilim. Ancak mağazalar genellikle çok fazla bellek tüketmez. İmha ettiğiniz Denetleyici Görünümlerindeki olay dinleyicilerini kaldırdığınızdan emin olmanız yeterlidir. Bu, React'in componentWillUnmount()yönteminde yapılır .


5
Yapmak istediklerinize dair kesinlikle birkaç farklı yaklaşım var ve bence bu, ne inşa etmeye çalıştığınıza bağlı. Bir yaklaşım 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öntemler getFolloweds()ve getFollowers()kullanıcı arabirimi için ihtiyacınız olan farklı kullanıcı gruplarını alır.
fisherwebdev

4
Alternatif olarak, her ikisi de soyut bir UserListStore'dan devralan bir FollowedUserListStore ve bir FollowerUserListStore'a sahip olabilirsiniz.
fisherwebdev

Küçük bir sorum var - abonelerin verileri almasını zorunlu kılmak yerine neden doğrudan mağazalardan veri göndermek için pub sub'u kullanmayalım?
sunwukung

2
@sunwukung Bu, mağazaların hangi denetleyici görünümlerinin hangi verilere ihtiyacı olduğunu takip etmesini gerektirir. Mağazaların bir şekilde değiştikleri gerçeğini yayınlamaları ve ardından ilgilenen kontrolcü görünümlerinin ihtiyaç duydukları verilerin hangi kısımlarını almalarına izin vermeleri daha temizdir.
fisherwebdev

Ya bir kullanıcı hakkında bilgi ve aynı zamanda arkadaşlarının listesini gösterdiğim bir profil sayfam varsa? Hem kullanıcı hem de arkadaşlar aynı türden olacaktır. Öyleyse aynı mağazada mı kalmalılar?
Nick Dima

79

(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 gerekmezswitch ;
  • "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.

StoreUtils

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]));
    }
  }
}

PaginatedList

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++;
  }
}

PaginatedStoreUtils

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
};

createStoreMixin

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;
}

1
Stampsy'yi yazdığınıza göre, tüm istemci tarafı uygulamasını yeniden yazarsanız, FLUX'u ve bu örnek uygulamayı oluşturmak için kullandığınız yaklaşımı kullanır mıydınız?
eAbi

2
eAbi: Bu, Stampsy'yi Flux'ta yeniden yazarken şu anda kullandığımız yaklaşımdır (önümüzdeki ay yayınlamayı umuyoruz). İdeal değil ama bizim için iyi çalışıyor. Böyle şeyleri yapmanın daha iyi yollarını bulduğumuzda / bulursak, onları paylaşacağız.
Dan Abramov

1
eAbi: Ancak artık normalizr kullanmıyoruz çünkü ekibimizden biri, normalleştirilmiş yanıtları döndürmek için tüm API'lerimizi yeniden yazdı . Yine de yapılmadan önce faydalıydı.
Dan Abramov

Bilgi için teşekkürler. Github deponuzu kontrol ettim ve yaklaşımınızla bir projeye (YUI3'te oluşturulmuş) başlamaya çalışıyorum, ancak kodu derlerken bazı sorunlar yaşıyorum (eğer söyleyebiliyorsanız). Sunucuyu düğüm altında çalıştırmıyorum, bu yüzden kaynağı statik dizinime kopyalamak istedim ama yine de biraz iş yapmam gerekiyor ... Bu biraz hantal ve ayrıca farklı JS sözdizimine sahip bazı dosyalar buldum. Özellikle jsx dosyalarında.
eAbi

2
@Sean: Ben bunu bir sorun olarak görmüyorum. Veri akışı okumuyor, veri yazma ile ilgili. Eylemlerin mağazalardan bağımsız olması elbette en iyisidir, ancak istekleri optimize etmek için mağazalardan okumanın son derece iyi olduğunu düşünüyorum. Sonuçta, bileşenler mağazalardan okur ve bu eylemleri ateşler. Bu mantığı her bileşende tekrar edebilirsiniz, ancak eylem yaratıcısı bunun için var ..
Dan Abramov

27

Böylece Reflü'de Dağıtıcı kavramı kaldırılır ve yalnızca eylemler ve depolar aracılığıyla veri akışı açısından düşünmeniz gerekir. yani

Actions <-- Store { <-- Another Store } <-- Components

Buradaki her ok, veri akışının nasıl dinlendiğini modellemektedir, bu da verinin ters yönde aktığı anlamına gelir. Veri akışı için gerçek rakam şudur:

Actions --> Stores --> Components
   ^          |            |
   +----------+------------+

Sizin kullanım durumunuzda, doğru anladıysam openUserProfile, kullanıcı profilinin sayfayı yüklemesini ve değiştirmesini başlatan bir işleme ve ayrıca kullanıcı profili sayfası açıldığında ve sonsuz kaydırma olayı sırasında gönderileri yükleyecek bazı gönderi yükleme eylemlerine ihtiyacımız var. Bu nedenle, uygulamada aşağıdaki veri depolarına sahip olduğumuzu hayal ediyorum:

  • Sayfaları değiştiren bir sayfa veri deposu
  • Sayfa açıldığında kullanıcı profilini yükleyen bir kullanıcı profili veri deposu
  • Görünen gönderileri yükleyen ve işleyen bir gönderi listesi veri deposu

Reflü'de bunu şu şekilde kurarsınız:

Eylemler

// Set up the two actions we need for this use case.
var Actions = Reflux.createActions(['openUserProfile', 'loadUserProfile', 'loadInitialPosts', 'loadMorePosts']);

Sayfa deposu

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);
    }
});

Kullanıcı profili deposu

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);
    }
});

Gönderi deposu

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');
    }
});

Bileşenler

Tüm sayfa görünümü, kullanıcı profili sayfası ve gönderi listesi için bir bileşene sahip olduğunuzu varsayıyorum. Aşağıdakilerin kablolanması gerekir:

  • Kullanıcı profilini açan düğmeler, Action.openUserProfiletıklama etkinliği sırasında doğru kimliğe sahip olanı çağırmalıdır .
  • Sayfa bileşeni, currentPageStorehangi sayfaya geçeceğini bilmesi için onu dinlemelidir .
  • Kullanıcı profili sayfası bileşeninin, currentUserProfileStorehangi kullanıcı profili verilerinin gösterileceğini bilmesi için dinlemesi gerekir
  • currentPostsStoreYüklenen gönderileri almak için gönderi listesinin dinlemesi gerekir
  • Sonsuz kaydırma olayının Action.loadMorePosts.

Ve bu hemen hemen olmalı.


Yazdığın için teşekkürler!
Dan Abramov

2
Partiye biraz geç kalmış olabilir, ancak işte size doğrudan mağazalardan API çağırmaktan neden kaçınmanız gerektiğini açıklayan güzel bir makale . Hala en iyi uygulamaların ne olduğunu bulmaya çalışıyorum, ancak bu konuda tökezleyen diğerlerine yardımcı olabileceğini düşündüm. Mağazalarla ilgili olarak dolaşan birçok farklı yaklaşım var.
Thijs Koerselman
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.