W architekturze Flux jak zarządzać cyklem życia sklepu?

Czytam o Fluxale przykład aplikacji Todo jest zbyt uproszczony, aby zrozumieć niektóre kluczowe punkty.

Wyobraź sobie jednostronicową aplikację, taką jak Facebook, która ma strony profilu użytkownika. Na każdej stronie profilu użytkownika chcemy pokazać informacje o użytkownikach i ich ostatnich postach, z nieskończonym przewijaniem. Możemy przejść z jednego profilu użytkownika do drugiego.

W architekturze Flux, jak by to odpowiadało sklepom i dyspozytorom?

Czy użyjemy jeden PostStore Na użytkownika, czy mielibyśmy jakiś globalny sklep? A co z dyspozytorami, czy utworzylibyśmy nowego dyspozytora dla każdej "strony użytkownika", czy użylibyśmy Singletona? Wreszcie, która część architektury odpowiada za zarządzanie cyklem życia sklepów "specyficznych dla stron" w odpowiedzi na zmianę trasy?

Ponadto pojedyncza pseudo-strona może mieć kilka list danych tego samego typu. Na przykład na stronie profilu chcę pokazać zarówno obserwujących, jak i następuje . W jaki sposób singleton UserStore może działać w tym przypadku? Czy UserPageStore zarządzał followedBy: UserStore i follows: UserStore?

Author: Cory House, 2014-05-11

3 answers

W aplikacji Flux powinien być tylko jeden Dyspozytor. Wszystkie dane przepływają przez ten centralny węzeł. Posiadanie dyspozytora singleton pozwala na zarządzanie wszystkimi sklepami. Staje się to ważne, gdy potrzebujesz aktualizacji Store # 1, a następnie zaktualizuj Store # 2 na podstawie zarówno akcji, jak i stanu Sklepu # 1. Flux zakłada, że taka sytuacja jest ewentualnością w dużym zastosowaniu. Idealnie taka sytuacja nie musiałaby się zdarzyć, a deweloperzy powinni starać się unikać tej złożoności, jeśli możliwe. Ale Dyspozytor singleton jest gotowy zająć się tym, kiedy nadejdzie czas.

Sklepy są również singletony. Powinny pozostać jak najbardziej niezależne i odsprzęgnięte-autonomiczny wszechświat, który można odpytywać z widoku kontrolera. Jedyną drogą do sklepu jest połączenie zwrotne, które rejestruje się u dyspozytora. Jedyną drogą do wyjścia jest funkcja getter. Sklepy publikują również zdarzenie, gdy ich stan się zmienił, więc widoki kontrolerów mogą wiedzieć, kiedy odpytywać dla nowego państwa, używając getterów.

W Twojej przykładowej aplikacji będzie pojedynczy PostStore. Ten sam sklep może zarządzać postami na" stronie " (pseudo-stronie), która jest bardziej podobna do FB Newsfeed, gdzie pojawiają się posty od różnych użytkowników. Jego domeną logiczną jest lista postów i może obsługiwać dowolną listę postów. Kiedy przechodzimy z pseudo-strony na pseudo-stronę, chcemy ponownie zainicjować stan sklepu, aby odzwierciedlić nowy stan. Możemy również chcieć buforować poprzedni stan w localStorage jako optymalizacja do poruszania się tam iz powrotem między pseudo-stronami, ale moja skłonność byłaby do skonfigurowania PageStore, który czeka na wszystkie inne sklepy, zarządza relacją z localStorage dla wszystkich sklepów na pseudo-stronie, a następnie aktualizuje swój własny stan. Zauważ, że to PageStore nie przechowuje nic o postach - to domena PostStore. Po prostu wiedziałby, czy dana pseudo-strona została buforowana, czy nie, ponieważ pseudo-strony są jej domeną.

The PostStore miałby metodę initialize(). Metoda ta zawsze wyczyści stary stan, nawet jeśli jest to pierwsza inicjalizacja, a następnie wytworzy stan na podstawie danych, które otrzymał w ramach akcji, za pośrednictwem dyspozytora. Przejście z jednej pseudo-strony na drugą prawdopodobnie wiązałoby się z działaniem PAGE_UPDATE, które wywołałoby wywołanie initialize(). Są szczegóły dotyczące pobierania danych z lokalnej pamięci podręcznej, pobierania danych z serwera, optymistycznego renderowania i stanów błędów XHR, ale taka jest ogólna idea.

Jeśli konkretna pseudo-strona nie potrzebuje wszystkich sklepów w aplikacji, nie jestem do końca pewien, czy istnieje jakiś powód, aby zniszczyć nieużywane, inne niż ograniczenia pamięci. Ale sklepy zazwyczaj nie zużywają dużo pamięci. Musisz tylko upewnić się, że usuniesz słuchacze zdarzeń z kontrolera-widoków, które niszczysz. Odbywa się to za pomocą metody componentWillUnmount() Reacta.

 122
Author: fisherwebdev,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-05-11 23:56:26

(Uwaga: używałem składni ES6 używając opcji JSX Harmony.)

Jako ćwiczenie napisałem przykładowa aplikacja Flux pozwala na przeglądanie Github users i repo.
Opiera się ona na odpowiedzi fisherwebdev , ale odzwierciedla również podejście, którego używam do normalizacji odpowiedzi API.

Zrobiłem to, aby udokumentować kilka podejść, które próbowałem podczas nauki Flux.
Starałem się trzymać go blisko realnego świata( paginacja, brak fałszywych localStorage APIs).

Jest tu kilka bitów, które szczególnie mnie interesowały:

  • używa architektury Flux i react-router ;
  • Może wyświetlać Stronę Użytkownika z częściowymi znanymi informacjami i ładować szczegóły w podróży; Dzięki temu, że jest on w pełni funkcjonalny, nie jest w stanie go obsłużyć.]} W przeciwieństwie do Github ' a, nie jest on w stanie uzyskać dostępu do plików JSON.]}
  • sklepy z treścią nie muszą zawierać giganta switch z działania ;
  • "powrót" jest natychmiastowy (ponieważ wszystkie dane są w sklepach).

Jak Klasyfikuję Sklepy

Starałem się uniknąć powielania, które widziałem w innym przykładzie Flux, szczególnie w sklepach. Okazało się, że warto logicznie podzielić sklepy na trzy kategorie:]}

Sklepy zawartości przechowują wszystkie podmioty aplikacji. Wszystko, co ma identyfikator, wymaga własnego sklepu z treścią. Komponenty, które renderują poszczególne elementy, pytają sklepy treści o świeże data.

Zawartość przechowuje zbiory obiektów z wszystkich działań serwera . Na przykład, UserStore zagląda do action.response.entities.users jeśli istnieje niezależnie z której akcji wywołano. Nie ma potrzeby stosowania switch. Normalizr ułatwia spłaszczenie dowolnych reponsów API do tego formatu.

// Content Stores keep their data like this
{
  7: {
    id: 7,
    name: 'Dan'
  },
  ...
}

Lista przechowuje śledzi identyfikatory podmiotów, które pojawiają się na jakiejś liście globalnej (np. "feed", "Twoje powiadomienia"). W tym projekcie nie mam takich sklepów, ale pomyślałem, że i tak o nich wspomnę. Zajmują się paginacją.

Zazwyczaj odpowiadają tylko na kilka działań (np. REQUEST_FEED, REQUEST_FEED_SUCCESS, REQUEST_FEED_ERROR).

// Paginated Stores keep their data like this
[7, 10, 5, ...]

Indeksowane sklepy List są jak sklepy List, ale definiują relację jeden do wielu. Na przykład "subskrybenci użytkownika", "repozytoria", "repozytoria użytkownika". Zajmują się również paginacją.

One również zwykle reagują tylko na kilka działań (np. REQUEST_USER_REPOS, REQUEST_USER_REPOS_SUCCESS, REQUEST_USER_REPOS_ERROR).

W większości aplikacje społecznościowe, będziesz miał ich wiele i chcesz być w stanie szybko utworzyć jeszcze jedną z nich.

// Indexed Paginated Stores keep their data like this
{
  2: [7, 10, 5, ...],
  6: [7, 1, 2, ...],
  ...
}

Uwaga: to nie są rzeczywiste klasy czy coś; tak właśnie lubię myśleć o sklepach. Ale zrobiłem kilku pomocników.

StoreUtils

createStore

Ta metoda daje najbardziej podstawowy Sklep:

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

Używam go do tworzenia wszystkich sklepów.

isInBag, mergeIntoBag

Drobni pomocnicy przydatni do treści Sklepy.

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

Przechowuje stan paginacji i wymusza pewne twierdzenia (nie można pobrać strony podczas pobierania itp.).

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

Sprawia, że tworzenie indeksowanych magazynów List jest tak proste, jak to możliwe, dostarczając metody boilerplate i obsługę akcji:

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

Mixin, który pozwala komponentom dostroić się do interesujących ich sklepów, np. 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;
}
 78
Author: Dan Abramov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-30 15:23:46

Więc w Reflux pojęcie dyspozytora jest usuwane i trzeba myśleć tylko w kategoriach przepływu danych przez akcje i sklepy. I. E.

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

Każda strzałka modeluje sposób słuchania przepływu danych, co z kolei oznacza, że dane przepływają w przeciwnym kierunku. Rzeczywista liczba dla przepływu danych jest następująca:

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

W Twoim przypadku użycia, jeśli dobrze zrozumiałem, potrzebujemy openUserProfile akcji, która inicjuje wczytywanie profilu użytkownika i przełączanie strony oraz również niektóre akcje ładowania postów, które będą ładować posty, gdy strona profilu użytkownika jest otwarta i podczas zdarzenia nieskończonego przewijania. Tak więc wyobrażam sobie, że mamy w aplikacji następujące magazyny danych:

  • magazyn danych strony, który obsługuje przełączanie stron
  • magazyn danych profilu użytkownika, który ładuje profil użytkownika po otwarciu strony
  • magazyn danych listy postów, który ładuje i obsługuje widoczne posty

W Reflux ustawiłbyś to jak to:

Działania

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

The page store

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

Sklep z profilem użytkownika

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

The posts store

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

Komponenty

Zakładam, że masz komponent dla całego widoku strony, strony profilu użytkownika i listy postów. Należy podłączyć:

  • przyciski otwierające profil użytkownika muszą wywoływać Action.openUserProfile z prawidłowym identyfikatorem podczas zdarzenia kliknięcia.
  • komponent strony powinien słuchać currentPageStore, aby wiedział, na którą stronę się przełączyć.
  • Strona profilu użytkownika musi nasłuchiwać currentUserProfileStore, aby wiedzieć, jakie dane profilu użytkownika pokazać Lista postów wymaga odsłuchania currentPostsStore, aby otrzymać załadowane posty
  • Zdarzenie infinite scroll musi wywołać Action.loadMorePosts.

I to chyba wszystko.

 27
Author: Spoike,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-07-31 12:31:24