Wie verwalten Sie in der Flux-Architektur den Store-Lebenszyklus?

Lesezeit: 14 Minuten

Benutzer-Avatar
Dan Abramow

Ich lese ungefähr Fluss aber die Beispiel Todo-App ist für mich zu einfach, um einige Schlüsselpunkte zu verstehen.

Stellen Sie sich eine Single-Page-App wie Facebook vor Benutzerprofilseiten. Auf jeder Benutzerprofilseite möchten wir einige Benutzerinformationen und ihre letzten Posts mit unendlichem Scrollen anzeigen. Wir können von einem Benutzerprofil zu einem anderen navigieren.

Wie würde dies in der Flux-Architektur Stores und Dispatchern entsprechen?

Würden wir einen verwenden PostStore pro Benutzer, oder hätten wir eine Art globalen Shop? Was ist mit Dispatchern? Würden wir für jede „Benutzerseite“ einen neuen Dispatcher erstellen oder würden wir einen Singleton verwenden? Welcher Teil der Architektur schließlich ist für die Verwaltung des Lebenszyklus „seitenspezifischer“ Stores als Reaktion auf Routenänderungen verantwortlich?

Darüber hinaus kann eine einzelne Pseudo-Seite mehrere Listen von Daten des gleichen Typs haben. Auf einer Profilseite möchte ich beispielsweise beides anzeigen Anhänger und Folgt. Wie kann ein Singleton UserStore Arbeit in diesem Fall? Möchten UserPageStore verwalten followedBy: UserStore und follows: UserStore?

Benutzer-Avatar
fisherwebdev

In einer Flux-App sollte es nur einen Dispatcher geben. Alle Daten fließen über diesen zentralen Knotenpunkt. Mit einem Singleton-Dispatcher können alle Stores verwaltet werden. Dies wird wichtig, wenn Sie Store Nr. 1 selbst aktualisieren müssen und Store Nr. 2 dann sowohl basierend auf der Aktion als auch auf dem Status von Store Nr. 1 aktualisieren lassen. Flux geht davon aus, dass diese Situation in einer großen Anwendung eine Eventualität ist. Im Idealfall müsste diese Situation nicht eintreten, und Entwickler sollten sich bemühen, diese Komplexität nach Möglichkeit zu vermeiden. Aber der Singleton-Dispatcher ist bereit, sich darum zu kümmern, wenn die Zeit gekommen ist.

Stores sind ebenfalls Singletons. Sie sollen möglichst unabhängig und entkoppelt bleiben – ein in sich geschlossenes Universum, das man aus einer Controller-Ansicht abfragen kann. Der einzige Weg in den Store führt über den Rückruf, den er beim Dispatcher registriert. Der einzige Ausweg führt über Getter-Funktionen. Stores veröffentlichen auch ein Ereignis, wenn sich ihr Status geändert hat, sodass Controller-Ansichten mithilfe der Getter wissen können, wann sie den neuen Status abfragen müssen.

In Ihrer Beispiel-App wäre es eine Single PostStore. Derselbe Shop könnte die Posts auf einer “Seite” (Pseudo-Seite) verwalten, die eher dem Newsfeed von FB ähnelt, wo Posts von verschiedenen Benutzern erscheinen. Seine logische Domäne ist die Liste der Posts, und es kann jede Liste von Posts verarbeiten. Wenn wir von Pseudo-Seite zu Pseudo-Seite wechseln, möchten wir den Zustand des Speichers neu initialisieren, um den neuen Zustand widerzuspiegeln. Wir möchten vielleicht auch den vorherigen Zustand in localStorage zwischenspeichern, um das Hin- und Herbewegen zwischen Pseudoseiten zu optimieren, aber ich würde gerne eine PageStore die auf alle anderen Geschäfte wartet, die Beziehung mit localStorage für alle Geschäfte auf der Pseudoseite verwaltet und dann ihren eigenen Zustand aktualisiert. Beachten Sie, dass dies PageStore würde nichts über die Posts speichern – das ist die Domäne der PostStore. Es würde einfach wissen, ob eine bestimmte Pseudo-Seite zwischengespeichert wurde oder nicht, weil Pseudo-Seiten seine Domäne sind.

Das PostStore hätte ein initialize() Methode. Diese Methode würde immer den alten Status löschen, selbst wenn dies die erste Initialisierung ist, und dann den Status basierend auf den Daten erstellen, die sie über die Aktion über den Dispatcher erhalten hat. Der Wechsel von einer Pseudo-Seite zu einer anderen würde wahrscheinlich Folgendes beinhalten: a PAGE_UPDATE Aktion, die den Aufruf von auslösen würde initialize(). Es gibt Details zum Abrufen von Daten aus dem lokalen Cache, zum Abrufen von Daten vom Server, zu optimistischem Rendering und zu XHR-Fehlerzuständen, aber dies ist die allgemeine Idee.

Wenn eine bestimmte Pseudoseite nicht alle Stores in der Anwendung benötigt, bin ich mir nicht ganz sicher, ob es außer Speicherbeschränkungen einen Grund gibt, die nicht verwendeten zu zerstören. Speicher verbrauchen jedoch normalerweise nicht viel Speicher. Sie müssen nur sicherstellen, dass Sie die Ereignis-Listener in den Controller-Ansichten entfernen, die Sie zerstören. Dies geschieht in React componentWillUnmount() Methode.

  • Es gibt sicherlich ein paar verschiedene Herangehensweisen an das, was Sie tun möchten, und ich denke, es hängt davon ab, was Sie bauen möchten. Ein Ansatz wäre a UserListStore, mit allen relevanten Benutzern darin. Und jeder Benutzer hätte ein paar boolesche Flags, die die Beziehung zum aktuellen Benutzerprofil beschreiben. Etwas wie { follower: true, followed: false }, zum Beispiel. Die Methoden getFolloweds() und getFollowers() würde die verschiedenen Gruppen von Benutzern abrufen, die Sie für die Benutzeroberfläche benötigen.

    – fisherwebdev

    11. Mai 2014 um 23:45 Uhr

  • Alternativ könnten Sie einen FollowedUserListStore und einen FollowerUserListStore haben, die beide von einem abstrakten UserListStore erben.

    – fisherwebdev

    11. Mai 2014 um 23:46 Uhr

  • Ich habe eine kleine Frage – warum nicht Pub Sub verwenden, um Daten direkt aus den Speichern zu senden, anstatt die Abonnenten zum Abrufen der Daten zu verpflichten?

    – sunwukung

    3. Juni 2014 um 10:17 Uhr

  • @sunwukung Dies würde erfordern, dass die Geschäfte nachverfolgen, welche Controller-Ansichten welche Daten benötigen. Es ist sauberer, wenn die Geschäfte die Tatsache veröffentlichen, dass sie sich in irgendeiner Weise geändert haben, und dann die interessierten Controller-Ansichten abrufen lassen, welche Teile der Daten sie benötigen.

    – fisherwebdev

    5. Juli 2014 um 17:50 Uhr

  • Was ist, wenn ich eine Profilseite habe, auf der ich Informationen über einen Benutzer, aber auch eine Liste seiner Freunde zeige? Sowohl Benutzer als auch Freunde wären die gleiche Art davon. Sollten sie in diesem Fall im selben Geschäft bleiben?

    – Nick Dima

    3. März 2015 um 18:31 Uhr

Benutzer-Avatar
Dan Abramow

(Hinweis: Ich habe die ES6-Syntax mit der Option JSX Harmony verwendet.)

Als Übung schrieb ich a Beispiel Flux-App das erlaubt zu stöbern Github users und Repos.
Es basiert auf der Antwort von fisherwebdev, spiegelt aber auch einen Ansatz wider, den ich zum Normalisieren von API-Antworten verwende.

Ich habe es geschafft, einige Ansätze zu dokumentieren, die ich beim Erlernen von Flux ausprobiert habe.
Ich habe versucht, es nahe an der realen Welt zu halten (Paginierung, keine gefälschten LocalStorage-APIs).

Hier sind ein paar Dinge, die mich besonders interessiert haben:

Wie ich Geschäfte klassifiziere

Ich habe versucht, einige der Duplizierungen zu vermeiden, die ich in anderen Flux-Beispielen gesehen habe, insbesondere in Stores. Ich fand es nützlich, Stores logisch in drei Kategorien zu unterteilen:

Inhaltsspeicher Halten Sie alle App-Entitäten. Alles, was eine ID hat, braucht einen eigenen Content Store. Komponenten, die einzelne Elemente rendern, fragen Content Stores nach den aktuellen Daten.

Content Stores sammeln ihre Objekte aus alle Serveraktionen. Zum Beispiel, UserStore schaut hinein action.response.entities.users wenn es existiert trotzdem davon Aktion ausgelöst. Es besteht keine Notwendigkeit für eine switch. Normalizr macht es einfach, alle API-Antworten auf dieses Format zu reduzieren.

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

Geschäfte auflisten Verfolgen Sie die IDs von Entitäten, die in einer globalen Liste erscheinen (z. B. „Feed“, „Ihre Benachrichtigungen“). In diesem Projekt habe ich keine solchen Stores, aber ich dachte, ich erwähne sie trotzdem. Sie übernehmen die Paginierung.

Sie reagieren normalerweise auf nur wenige Aktionen (z REQUEST_FEED, REQUEST_FEED_SUCCESS, REQUEST_FEED_ERROR).

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

Indizierte Listenspeicher sind wie Listenspeicher, aber sie definieren eine Eins-zu-Viele-Beziehung. Zum Beispiel „Abonnenten des Benutzers“, „Sterngucker des Repositorys“, „Repositorys des Benutzers“. Sie übernehmen auch die Paginierung.

Sie reagieren normalerweise auch auf nur wenige Aktionen (z REQUEST_USER_REPOS, REQUEST_USER_REPOS_SUCCESS, REQUEST_USER_REPOS_ERROR).

In den meisten sozialen Apps haben Sie viele davon und Sie möchten in der Lage sein, schnell eine weitere davon zu erstellen.

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

Hinweis: Dies sind keine tatsächlichen Klassen oder so etwas; So stelle ich mir Stores gerne vor. Ich habe allerdings ein paar Helfer gemacht.

StoreUtils

createStore

Diese Methode gibt Ihnen den einfachsten Store:

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

Ich benutze es, um alle Stores zu erstellen.

isInBag, mergeIntoBag

Kleine Helfer, die für Content Stores nützlich sind.

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

Speichert den Paginierungsstatus und erzwingt bestimmte Behauptungen (Seite kann während des Abrufens nicht abgerufen werden usw.).

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

Macht die Erstellung von indizierten Listenspeichern so einfach wie möglich, indem Boilerplate-Methoden und Aktionshandhabung bereitgestellt werden:

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

Ein Mixin, das es Komponenten ermöglicht, sich auf Stores einzustellen, an denen sie interessiert sind, z 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;
}

  • Angesichts der Tatsache, dass Sie Stampsy geschrieben haben, würden Sie, wenn Sie die gesamte clientseitige Anwendung neu schreiben würden, FLUX und denselben Ansatz verwenden, den Sie zum Erstellen dieser Beispiel-App verwendet haben?

    – eAbi

    7. September 2014 um 16:35 Uhr

  • eAbi: Dies ist der Ansatz, den wir derzeit verwenden, wenn wir Stampsy in Flux umschreiben (in der Hoffnung, es nächsten Monat herauszubringen). Es ist nicht ideal, aber es funktioniert gut für uns. Wenn/falls wir bessere Möglichkeiten finden, diese Dinge zu tun, werden wir sie teilen.

    – Dan Abramow

    7. September 2014 um 17:57 Uhr

  • eAbi: Allerdings verwenden wir normalizr nicht mehr, weil ein Typ von unserem Team umgeschrieben hat alle unsere APIs, um normalisierte Antworten zurückzugeben. Es war jedoch nützlich, bevor das getan wurde.

    – Dan Abramow

    7. September 2014 um 17:59 Uhr

  • Vielen Dank für Ihre Informationen. Ich habe Ihr Github-Repo überprüft und versuche, ein Projekt (in YUI3 erstellt) mit Ihrem Ansatz zu beginnen, aber ich habe einige Probleme beim Kompilieren des Codes (wenn Sie das sagen können). Ich betreibe den Server nicht unter Knoten, also wollte ich die Quelle in mein statisches Verzeichnis kopieren, aber ich muss noch etwas arbeiten … Es ist ein bisschen umständlich, und außerdem habe ich einige Dateien mit unterschiedlicher JS-Syntax gefunden. Vor allem in jsx-Dateien.

    – eAbi

    7. September 2014 um 18:32 Uhr

  • @ Sean: Ich sehe das überhaupt nicht als Problem. Das Datenfluss Es geht darum, Daten zu schreiben, nicht sie zu lesen. Sicher ist es am besten, wenn Aktionen unabhängig von Geschäften sind, aber um Anfragen zu optimieren, denke ich, dass es vollkommen in Ordnung ist, von Geschäften zu lesen. Schließlich, Komponenten aus Geschäften lesen und diese Aktionen auslösen. Sie könnten diese Logik in jeder Komponente wiederholen, aber dafür ist der Aktionsersteller da.

    – Dan Abramow

    7. Mai 2015 um 14:46 Uhr

Also rein Rückfluss Das Konzept des Dispatchers wird entfernt und Sie müssen nur noch an den Datenfluss durch Aktionen und Speicher denken. Dh

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

Jeder Pfeil hier modelliert, wie der Datenfluss abgehört wird, was wiederum bedeutet, dass die Daten in die entgegengesetzte Richtung fließen. Die tatsächliche Zahl für den Datenfluss ist diese:

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

In Ihrem Anwendungsfall benötigen wir, wenn ich das richtig verstanden habe, a openUserProfile Aktion, die das Laden des Benutzerprofils und das Umschalten der Seite initiiert, sowie einige Aktionen zum Laden von Posts, die Posts laden, wenn die Benutzerprofilseite geöffnet wird, und während des Ereignisses des unendlichen Scrollens. Ich würde mir also vorstellen, dass wir die folgenden Datenspeicher in der Anwendung haben:

  • Ein Seitendatenspeicher, der das Wechseln von Seiten handhabt
  • Ein Benutzerprofil-Datenspeicher, der das Benutzerprofil lädt, wenn die Seite geöffnet wird
  • Ein Posts-Listen-Datenspeicher, der die sichtbaren Posts lädt und verarbeitet

In Reflux würdest du es so einrichten:

Die Taten

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

Der Seitenspeicher

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

Der Benutzerprofilspeicher

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

Der Postspeicher

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

Die Komponenten

Ich gehe davon aus, dass Sie eine Komponente für die Ansicht der gesamten Seite, die Benutzerprofilseite und die Beitragsliste haben. Folgendes muss verkabelt werden:

  • Die Schaltflächen, die das Benutzerprofil öffnen, müssen die aufrufen Action.openUserProfile mit der richtigen ID während des Klickereignisses.
  • Die Seitenkomponente sollte auf die lauschen currentPageStore damit es weiß, zu welcher Seite es wechseln soll.
  • Die Komponente der Benutzerprofilseite muss auf die lauschen currentUserProfileStore damit es weiß, welche Benutzerprofildaten angezeigt werden sollen
  • Die Beitragsliste muss angehört werden currentPostsStore um die geladenen Posts zu erhalten
  • Das unendliche Bildlaufereignis muss die aufrufen Action.loadMorePosts.

Und das sollte es auch schon sein.

  • Danke für die Zuschreibung!

    – Dan Abramow

    31. Juli 2014 um 16:24 Uhr

  • Ein bisschen spät zur Party vielleicht, aber hier ist eine nette Artikel erklären warum Vermeiden Sie es, Ihre API direkt von den Geschäften aufzurufen. Ich finde immer noch heraus, was die besten Praktiken sind, aber ich dachte, es könnte anderen helfen, darüber zu stolpern. Es gibt viele verschiedene Ansätze in Bezug auf Geschäfte.

    – Thijs Koerselman

    2. April 2015 um 13:48 Uhr

1257300cookie-checkWie verwalten Sie in der Flux-Architektur den Store-Lebenszyklus?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy