Einfacher Datenfluss in React-Apps mit Flux und Backbone: Ein Tutorial mit Beispielen
Veröffentlicht: 2022-03-11React.js ist eine fantastische Bibliothek. Manchmal scheint es das Beste seit gesliced Python zu sein. React ist jedoch nur ein Teil eines Front-End-Anwendungsstapels. Es hat nicht viel zu bieten, wenn es um die Verwaltung von Daten und Status geht.
Facebook, die Macher von React, haben dort in Form von Flux einige Anleitungen angeboten. Flux ist eine „Anwendungsarchitektur“ (kein Framework), die auf einem unidirektionalen Datenfluss mit React Views, einem Action Dispatcher und Stores basiert. Das Flux-Muster löst einige wichtige Probleme, indem es wichtige Prinzipien der Ereignissteuerung verkörpert, die es viel einfacher machen, React-Anwendungen zu begründen, zu entwickeln und zu warten.
Hier stelle ich grundlegende Flux-Beispiele für den Kontrollfluss vor, bespreche, was Stores fehlt, und wie man Backbone-Modelle und -Sammlungen verwendet, um die Lücke „Flux-konform“ zu schließen.
(Hinweis: Ich verwende CoffeeScript in meinen Beispielen der Einfachheit halber und der Kürze halber. Nicht-CoffeeScript-Entwickler sollten in der Lage sein, mitzumachen und die Beispiele als Pseudocode zu behandeln.)
Einführung in Facebooks Flux
Backbone ist eine ausgezeichnete und gut geprüfte kleine Bibliothek, die Ansichten, Modelle, Sammlungen und Routen enthält. Es ist eine De-facto -Standardbibliothek für strukturierte Front-End-Anwendungen und wird seit der Einführung von React-Apps im Jahr 2013 mit React-Apps gekoppelt. Die meisten Beispiele von React außerhalb von Facebook.com haben bisher Erwähnungen der gleichzeitigen Verwendung von Backbone enthalten.
Unglücklicherweise bringt es unglückliche Komplikationen mit sich, sich allein auf Backbone zu verlassen, um den gesamten Anwendungsablauf außerhalb der Ansichten von React zu handhaben. Als ich anfing, am React-Backbone-Anwendungscode zu arbeiten, dauerte es nicht lange, bis die „komplexen Ereignisketten“, von denen ich gelesen hatte, ihre Hydra-ähnlichen Köpfe erhoben. Das Senden von Ereignissen von der Benutzeroberfläche an die Modelle und dann von einem Modell zum anderen und dann wieder zurück macht es schwierig, den Überblick darüber zu behalten, wer wen in welcher Reihenfolge und warum geändert hat.
Dieses Flux-Tutorial zeigt, wie das Flux-Muster diese Probleme mit beeindruckender Leichtigkeit und Einfachheit handhabt.
Ein Überblick
Der Slogan von Flux lautet „unidirektionaler Datenfluss“. Hier ist ein praktisches Diagramm aus den Flux-Dokumenten, das zeigt, wie dieser Fluss aussieht:
Das Wichtige ist, dass die Dinge von React --> Dispatcher --> Stores --> React
fließen.
Schauen wir uns an, was die einzelnen Hauptkomponenten sind und wie sie miteinander verbunden sind:
Die Dokumentation bietet auch diesen wichtigen Vorbehalt:
Flux ist eher ein Muster als ein Framework und hat keine harten Abhängigkeiten. Wir verwenden jedoch häufig EventEmitter als Grundlage für Stores und React für unsere Views. Das einzige Stück Flux, das anderswo nicht ohne weiteres erhältlich ist, ist der Dispatcher. Dieses Modul ist hier verfügbar, um Ihre Flux-Toolbox zu vervollständigen.
Flux hat also drei Komponenten:
- Aufrufe (
React = require('react')
) - Dispatcher (
Dispatcher = require('flux').Dispatcher
) - Speichert (
EventEmitter = require('events').EventEmitter
)- (oder, wie wir gleich sehen werden,
Backbone = require('backbone')
)
- (oder, wie wir gleich sehen werden,
Die Ansichten
Ich werde React hier nicht beschreiben, da so viel darüber geschrieben wurde, außer zu sagen, dass ich es Angular bei weitem vorziehe. Im Gegensatz zu Angular fühle ich mich fast nie verwirrt , wenn ich React-Code schreibe, aber natürlich gehen die Meinungen auseinander.
Der Dispatcher
Der Flux Dispatcher ist ein zentraler Ort, an dem alle Ereignisse behandelt werden, die Ihre Stores verändern. Um es zu verwenden, lassen Sie jeden Store einen einzelnen Rückruf register
, um alle Ereignisse zu verarbeiten. Wenn Sie dann einen Store ändern möchten, dispatch
Sie ein Ereignis aus.
Wie React scheint mir der Dispatcher eine gute Idee zu sein, gut umgesetzt. Beispielsweise könnte eine App, die es dem Benutzer ermöglicht, Elemente zu einer To-Do-Liste hinzuzufügen, Folgendes enthalten:
# in TodoDispatcher.coffee Dispatcher = require("flux").Dispatcher TodoDispatcher = new Dispatcher() # That's all it takes!. module.exports = TodoDispatcher
# in TodoStore.coffee TodoDispatcher = require("./TodoDispatcher") TodoStore = {items: []} TodoStore.dispatchCallback = (payload) -> switch payload.actionType when "add-item" TodoStore.items.push payload.item when "delete-last-item" TodoStore.items.pop() TodoStore.dispatchToken = TodoDispatcher.registerCallback(TodoStore.dispatchCallback) module.exports = TodoStore
# in ItemAddComponent.coffee TodoDispatcher = require("./TodoDispatcher") ItemAddComponent = React.createClass handleAddItem: -> # note: you're NOT just pushing directly to the store! # (the restriction of moving through the dispatcher # makes everything much more modular and maintainable) TodoDispatcher.dispatch actionType: "add-item" item: "hello world" render: -> React.DOM.button { onClick: @handleAddItem }, "Add an Item!"
Damit lassen sich zwei Fragen ganz einfach beantworten:
- F: Was sind all die Ereignisse, die
MyStore
verändern?- A: Überprüfen Sie einfach die Fälle in der
switch
Anweisung inMyStore.dispatchCallback
.
- A: Überprüfen Sie einfach die Fälle in der
- F: Was sind alle möglichen Quellen dieses Ereignisses?
- A: Suchen Sie einfach nach diesem
actionType
.
- A: Suchen Sie einfach nach diesem
Das ist viel einfacher als zum Beispiel nach MyModel.set
und MyModel.save
und MyCollection.add
usw. zu suchen, wo es sehr schnell sehr schwierig wird, die Antworten auf diese grundlegenden Fragen zu finden.
Der Dispatcher ermöglicht es Ihnen auch, Rückrufe sequenziell auf einfache, synchrone Weise auszuführen, indem waitFor
verwenden. Zum Beispiel:
# in MessageStore.coffee MyDispatcher = require("./MyDispatcher") TodoStore = require("./TodoStore") MessageStore = {items: []} MessageStore.dispatchCallback = (payload) -> switch payload.actionType when "add-item" # synchronous event flow! MyDispatcher.waitFor [TodoStore.dispatchToken] MessageStore.items.push "You added an item! It was: " + payload.item module.exports = MessageStore
In der Praxis war ich schockiert zu sehen, wie viel sauberer mein Code war, als ich den Dispatcher zum Ändern meiner Stores verwendete, auch ohne waitFor
.
Die Läden
Daten fließen also über den Dispatcher in die Stores. Habe es. Aber wie fließen Daten von den Stores zu den Views (dh React)? Wie in den Flux-Dokumenten angegeben:
[Die] Ansicht lauscht auf Ereignisse, die von den Stores übertragen werden, von denen sie abhängt.
OK großartig. So wie wir Callbacks bei unseren Stores registriert haben, registrieren wir Callbacks bei unseren Views (die React-Komponenten sind). Wir weisen React an, neu zu render
wenn eine Änderung im Store auftritt, die über seine props
weitergegeben wurde.
Zum Beispiel:
# in TodoListComponent.coffee React = require("react") TodoListComponent = React.createClass componentDidMount: -> @props.TodoStore.addEventListener "change", => @forceUpdate() , @ componentWillUnmount: -> # remove the callback render: -> # show the items in a list. React.DOM.ul {}, @props.TodoStore.items.map (item) -> React.DOM.li {}, item
Fantastisch!
Wie geben wir also dieses "change"
-Ereignis aus? Nun, Flux empfiehlt die Verwendung von EventEmitter
. Aus einem offiziellen Beispiel:
var MessageStore = merge(EventEmitter.prototype, { emitChange: function() { this.emit(CHANGE_EVENT); }, /** * @param {function} callback */ addChangeListener: function(callback) { this.on(CHANGE_EVENT, callback); }, get: function(id) { return _messages[id]; }, getAll: function() { return _messages; }, // etc...
Grob! Ich muss das alles selbst schreiben, jedes Mal, wenn ich einen einfachen Laden will? Was soll ich jedes Mal verwenden, wenn ich eine Information habe, die ich anzeigen möchte? Es muss einen besseren Weg geben!
Das fehlende Stück
Die Modelle und Kollektionen von Backbone haben bereits alles, was die EventEmitter-basierten Stores von Flux zu tun scheinen.

Indem Flux Ihnen sagt, dass Sie rohen EventEmitter verwenden sollten, empfiehlt Flux, dass Sie jedes Mal, wenn Sie einen Shop erstellen, vielleicht 50-75 % der Backbone-Modelle und -Kollektionen neu erstellen. Die Verwendung von EventEmitter für Ihre Geschäfte ist wie die Verwendung von bloßem Node.js für Ihren Server, wenn bereits gut gebaute Mikroframeworks wie Express.js oder ähnliches vorhanden sind, um sich um alle Grundlagen und Boilerplates zu kümmern.
So wie Express.js auf Node.js aufbaut, basieren die Modelle und Sammlungen von Backbone auf EventEmitter. Und es hat alles, was Sie so ziemlich immer brauchen: Backbone gibt change
aus und hat Abfragemethoden, Getter und Setter und alles. Außerdem haben Jeremy Ashkenas von Backbone und seine Armee von 230 Mitwirkenden bei all diesen Dingen einen viel besseren Job gemacht, als ich es wahrscheinlich tun könnte.
Als Beispiel für dieses Backbone-Tutorial habe ich das MessageStore-Beispiel von oben in eine Backbone-Version konvertiert.
Es ist objektiv weniger Code (keine doppelte Arbeit erforderlich) und subjektiv klarer und prägnanter (z. B. this.add(message)
anstelle von _messages[message.id] = message
).
Verwenden wir also Backbone for Stores!
Das FluxBone-Muster: Flux Stores von Backbone
Dieses Tutorial ist die Grundlage eines Ansatzes, den ich stolz FluxBone genannt habe, die Flux-Architektur, die Backbone for Stores verwendet. Hier ist das Grundmuster einer FluxBone-Architektur:
- Stores sind instanziierte Backbone-Modelle oder Sammlungen, die einen Callback beim Dispatcher registriert haben. Typischerweise bedeutet dies, dass es sich um Singletons handelt.
- Ansichtskomponenten ändern Stores niemals direkt (z. B. kein
.set()
). Stattdessen senden Komponenten Aktionen an den Dispatcher. - View-Komponenten fragen Stores ab und binden an ihre Ereignisse, um Updates auszulösen.
Lassen Sie uns Backbone- und Flux-Beispiele verwenden, um sich die einzelnen Teile davon der Reihe nach anzusehen:
1. Stores sind instanziierte Backbone-Modelle oder Sammlungen, die einen Callback beim Dispatcher registriert haben.
# in TodoDispatcher.coffee Dispatcher = require("flux").Dispatcher TodoDispatcher = new Dispatcher() # That's all it takes! module.exports = TodoDispatcher
# in stores/TodoStore.coffee Backbone = require("backbone") TodoDispatcher = require("../dispatcher") TodoItem = Backbone.Model.extend({}) TodoCollection = Backbone.Collection.extend model: TodoItem url: "/todo" # we register a callback with the Dispatcher on init. initialize: -> @dispatchToken = TodoDispatcher.register(@dispatchCallback) dispatchCallback: (payload) => switch payload.actionType # remove the Model instance from the Store. when "todo-delete" @remove payload.todo when "todo-add" @add payload.todo when "todo-update" # do stuff... @add payload.todo, merge: true # ... etc # the Store is an instantiated Collection; a singleton. TodoStore = new TodoCollection() module.exports = TodoStore
2. Komponenten ändern Stores niemals direkt (z. B. kein .set()
). Stattdessen senden Komponenten Aktionen an den Dispatcher.
# components/TodoComponent.coffee React = require("react") TodoListComponent = React.createClass handleTodoDelete: -> # instead of removing the todo from the TodoStore directly, # we use the Dispatcher TodoDispatcher.dispatch actionType: "todo-delete" todo: @props.todoItem # ... (see below) ... module.exports = TodoListComponent
3. Komponenten fragen Speicher ab und binden an ihre Ereignisse, um Aktualisierungen auszulösen.
# components/TodoComponent.coffee React = require("react") TodoListComponent = React.createClass handleTodoDelete: -> # instead of removing the todo from the TodoStore directly, # we use the dispatcher. #flux TodoDispatcher.dispatch actionType: "todo-delete" todo: @props.todoItem # ... componentDidMount: -> # the Component binds to the Store's events @props.TodoStore.on "add remove reset", => @forceUpdate() , @ componentWillUnmount: -> # turn off all events and callbacks that have this context @props.TodoStore.off null, null, this render: -> React.DOM.ul {}, @props.TodoStore.items.map (todoItem) -> # TODO: TodoItemComponent, which would bind to # `this.props.todoItem.on('change')` TodoItemComponent { todoItem: todoItem } module.exports = TodoListComponent
Ich habe diesen Flux- und Backbone-Ansatz auf meine eigenen Projekte angewendet, und nachdem ich meine React-Anwendung neu gestaltet hatte, um dieses Muster zu verwenden, verschwanden fast alle hässlichen Teile. Es war ein kleines Wunder: Stück für Stück wurden die Codeteile, bei denen ich mit den Zähnen knirschen musste, um nach einem besseren Weg zu suchen, durch einen vernünftigen Fluss ersetzt. Und die Geschmeidigkeit, mit der sich Backbone in dieses Muster zu integrieren scheint, ist bemerkenswert: Ich habe nicht das Gefühl, dass ich gegen Backbone, Flux oder React kämpfe, um sie in einer einzigen Anwendung zusammenzufügen.
Beispiel Mixin
Das Schreiben des this.on(...)
und this.off(...)
jedes Mal, wenn Sie einen FluxBone Store zu einer Komponente hinzufügen, kann ein bisschen alt werden.
Hier ist ein Beispiel für React Mixin, das, obwohl es extrem naiv ist, das schnelle Iterieren sicherlich noch einfacher machen würde:
# in FluxBoneMixin.coffee module.exports = (propName) -> componentDidMount: -> @props[propName].on "all", => @forceUpdate() , @ componentWillUnmount: -> @props[propName].off "all", => @forceUpdate() , @
# in HelloComponent.coffee React = require("react") UserStore = require("./stores/UserStore") TodoStore = require("./stores/TodoStore") FluxBoneMixin = require("./FluxBoneMixin") MyComponent = React.createClass mixins: [ FluxBoneMixin("UserStore"), FluxBoneMixin("TodoStore"), ] render: -> React.DOM.div {}, "Hello, #{ @props.UserStore.get('name') }, you have #{ @props.TodoStore.length } things to do." React.renderComponent( MyComponent { UserStore: UserStore TodoStore: TodoStore } , document.body.querySelector(".main") )
Synchronisierung mit einer Web-API
Im ursprünglichen Flussdiagramm interagieren Sie mit der Web-API nur über ActionCreators, die eine Antwort vom Server erfordern, bevor Aktionen an den Dispatcher gesendet werden. Das passte nie zu mir; Sollte der Store nicht der erste sein, der von Änderungen erfährt, bevor der Server?
Ich drehe diesen Teil des Diagramms um: Die Stores interagieren direkt mit einer RESTful-CRUD-API über sync()
von Backbone. Dies ist wunderbar praktisch, zumindest wenn Sie mit einer tatsächlichen RESTful-CRUD-API arbeiten.
Die Datenintegrität wird problemlos aufrechterhalten. Wenn Sie eine neue Eigenschaft .set()
, löst das change
ein erneutes Rendern von React aus, wobei die neuen Daten optimistisch angezeigt werden. Wenn Sie versuchen, es auf dem Server zu .save()
, teilt Ihnen das request
mit, dass ein Ladesymbol angezeigt werden soll. Wenn die Dinge durchgehen, teilt Ihnen das sync
mit, dass Sie das Ladesymbol entfernen müssen, oder das error
lässt Sie wissen, dass die Dinge rot werden sollen. Hier können Sie sich inspirieren lassen.
Es gibt auch eine Validierung (und ein entsprechendes invalid
Ereignis) für eine erste Verteidigungsebene und eine .fetch()
Methode, um neue Informationen vom Server abzurufen.
Für weniger Standardaufgaben kann die Interaktion über ActionCreators sinnvoller sein. Ich vermute, dass Facebook nicht viel „reines CRUD“ macht, in diesem Fall ist es nicht verwunderlich, dass sie Stores nicht an die erste Stelle setzen.
Fazit
Die Engineering-Teams bei Facebook haben bemerkenswerte Arbeit geleistet, um das Frontend-Web mit React voranzutreiben, und die Einführung von Flux gibt einen Einblick in eine breitere Architektur, die wirklich skalierbar ist: nicht nur in Bezug auf Technologie, sondern auch in technischer Hinsicht. Die clevere und sorgfältige Verwendung von Backbone (wie in diesem Tutorial gezeigt) kann die Lücken in Flux füllen und es für jeden, von Ein-Personen-Indie-Shops bis hin zu großen Unternehmen, erstaunlich einfach machen, beeindruckende Anwendungen zu erstellen und zu warten.