Hinweis:
Dieser Blogartikel ist älter als 5 Jahre – die genannten Inhalte sind eventuell überholt.
Mit Polymer und Web Components kann durch hierarchische Unterteilung und Komposition eine aufgeräumte, gut erweiterbare Struktur einer Web App aufrechterhalten werden. Wächst die Anwendung jedoch konstant weiter, müssen zusätzliche Strukturen gefunden werden, um den Datenfluss durch die Anwendung vorhersagbar und wartbar zu machen. Wer mit seiner Anwendung an diese Stelle im Entwicklungsverlauf kommt, hat die Möglichkeit, eine maßgeschneiderte, für die Anwendung genau passende Architektur selbst zu entwerfen und umzusetzen. Diese Vorgehensweise führt jedoch oft nicht zum gewünschten Ergebnis, da in dieser Phase leicht subtile Fehler gemacht werden können, die sich im späteren Projektverlauf stärker und stärker auswirken und schwer zu beheben sind, da die Fehler schon im grundlegenden Aufbau liegen.
Mit der von Facebook veröffentlichten Flux-Architektur, die als über die Jahre entwickelte Best-Practice-Lösung von Facebooks eigener Web App zu verstehen ist, steht ein Entwurf bereit, der von vielen kleineren und größeren Anwendungen adaptiert und daher gut getestet und validiert wurde.
Was ist Flux?
Die grundlegende Idee hinter Flux ist es, den Datenfluss durch die Webanwendung vorhersagbar und einheitlich zu gestalten, indem dieser unidirektional dieselben Komponenten in derselben Reihenfolge durchwandert. Das ist ein grundlegender Unterschied zu MVC-Architekturen, in denen Nachrichten von View
zu Controller
und Model
unabhängig voneinander ausgetauscht werden können und Kaskaden von Zustandsänderungen schwer nachzuvollziehen sind.
In Flux ist der gesamte Zustand oder State
der Anwendung in Stores
gespeichert, die ähnlich dem Model in MVC-Anwendungen auch die Business-Logik enthalten, die im Gegensatz zu Models jedoch nicht nur ein einzelnes Objekt, sondern alle Instanzen eines Typs verwalten. Der Zustand aus den Stores wird an die Views
weitergegeben, die üblicherweise in hierarchisch geschachtelten Komponenten modelliert sind. Die View-Komponenten ganz oben in der Hierarchie sind die sogenannten Controller Views
, die auf State-Änderungen in den Stores horchen und diesen an ihre direkten Kind-Komponenten weitergeben. Auf diese Weise ergibt sich ein Baum aus View-Komponenten, der vom aktuellen Zustand der Stores abhängig ist.
Interagiert nun der/die Nutzer:in mit der Anwendung und löst Funktionen aus oder es werden Push-Benachrichtigungen über Schnittstellen wie Websockets übermittelt, wird eine Action
erzeugt, die aus einem Typ und einer individuellen Payload besteht (z.B. clickedButton
und als Payload id=’startVideo’
). Diese Action< wird nun über einen zentralen Dispatcher
, der als Singleton implementiert ist, an alle vorhandenen Stores weitergegeben. Die Stores empfangen die Action und können darauf mit State-Änderungen reagieren, die anschließend wieder an die Controller Views weitergegeben werden.
Für jede Aktion des/der Nutzers/Nutzerin, die eine Reaktion der Anwendung hervorruft, wird dieser Weg des Datenflusses ausgeführt, was die Abhängigkeiten von Zustandsänderungen auch in großen Anwendungen klar ersichtlich macht.
Eine Besonderheit des Dispatchers, die erst in größeren Anwendungen zum Tragen kommt, ist die Möglichkeit, die Reihenfolge, in der die Stores auf eine Action reagieren, zu beeinflussen. Das ist dann relevant, wenn Stores aufeinander aufbauen und ein Store zum Verändern des eigenen Zustands den geänderten Zustand eines anderen Stores benötigt.
Durch die Konsolidierung aller State-relevanten Aktionen des/der Nutzers/Nutzerin als Actions, die über den Dispatcher ausgeliefert werden, ist es außerdem möglich, die Stores nur als Aggregation der Actions und damit des echten Anwendungs-States zu sehen, der über die Sequenz der Actions definiert wird. Werden die Actions persistiert, kann auch nachträglich durch einen neuen Store eine neue Aggregation des Zustands generiert werden.
Implementierung
Flux wird von Facebook explizit als Architektur und nicht als Framework oder Technologie-Stack beschrieben, in Beispiel-Implementierungen werden allerdings meistens die selben Bibliotheken zur Umsetzung verwendet, die auch auf facebook.com zur Implementierung einer Flux-Architektur verwendet werden, da diese speziell für die Verwendung in dieser Konstellation entwickelt wurden.
Als View-Layer wird dabei die ebenfalls von Facebook entwickelte React-Bibliothek eingesetzt, mit der es möglich ist, eigene Komponenten zu definieren, die ausgehend von einer Wurzelkomponente hierarchisch aufeinander aufbauen. Um Änderungen effizient im DOM-Abzubilden, setzt React auf eine eigene Virtual DOM
-Implementierung, die ein Abbild des DOM im Javascript-Heap pflegt, das bei jeder Änderung komplett neu berechnet wird. Anschließend wird ein minimales Set von DOM-Operationen ermittelt, mit dem der echte DOM wieder mit dem virtuellen DOM synchronisiert werden kann. Diese Herangehensweise basiert auf der Überlegung, dass die Javascript-Ausführung in modernen Browsern sehr effizient, Interaktion mit dem DOM aber teuer und aufwändig ist. React fügt sich gut in die Flux-Architektur ein, da es durch die ständige Neuevaluierung des Komponentenbaums ohne Aufwand des Entwicklers synchron mit dem aus den Stores übermittelten State bleibt.
Für den Dispatcher, der Aktionen an Stores weitergibt, kann die Implementierung, die auch Facebook in seinen Web Apps nutzt, aus dem npm-Package flux
genutzt werden. Aktionen können dann mit der dispatch
-Methode des Dispatchers erzeugt werden.
Damit die Stores Zustandsänderungen an die Controller Views weitergeben können, kann eine beliebige Implementierung des Observer Patterns genutzt werden, für mehr Flexibilität bspw. die Klasse EventEmitter
aus dem npm-Package events
.
Außerdem existieren verschiedenste Flux-Implementierungen, die das Erstellen von Stores, Actions und die Anbindung an Views vereinfachen und oft noch zusätzliche Features wie Server-side Rendering anbieten.
Polymer und Flux
Obwohl die Flux-Architektur oft mit React als View-Implementierung genutzt wird, ist dies keinesfalls notwendig. Web Components eignen sich hervorragend als Ersatz für React – sie basieren auf dem gleichen Prinzip der hierarchischen Unterteilung und Komposition und benötigen keine Drittbibliothek, um im Browser ausgeführt zu werden. Die Anbindung an die Daten aus den Stores kann genauso erfolgen wie für React-Komponenten. Um das Erstellen der Komponenten zu vereinfachen, kann Polymer als Hilfsbibliothek eingesetzt werden.
Wenn Polymer eingesetzt wird, muss, um die Unidirektionalität von Flux nicht zu verletzen, darauf geachtet werden, dass Eigenschaften im Komponentenbaum nur von oben nach unten weitergegeben werden und kein Two Way Databinding besteht – dies könnte unkontrollierte und schwer ersichtliche Abhängigkeiten zwischen den Komponenten schaffen, die durch die Action Loop über die Stores als einzige Quelle des Anwendungszustands vermieden werden sollen. Das Binding von Kind-Elementen zu ihren Eltern-Elementen kann explizit deaktiviert werden, indem für alle Properties notify: false
gesetzt wird.
Bei der Verwendung von Polymer bietet es sich an, eine Bibliothek wie ImmutableJS einzusetzen, die für jeden veränderten Zustand auch ein neues Objekt zurückgibt, das aggressive Caching von Polymer-Templates verhindert sonst die Propagierung von geänderten Properties an Kind-Elemente.
1 2 3 4 5 6 7 |
var data = [‚one’, ‚two’]; this.set(‚dataList’, data); data.push(‚three’); this.set(‚dataList’, data); |
So wird im obenstehenden Beispiel das Element three
nicht im Template angezeigt, da data
bei beiden Zuweisungen eine Referenz auf dieselbe Array-Instanz ist, was Polymer Templates nicht zu einem Re-rendering veranlasst.
Flux DOM-ified
Die Idee hinter Polymer ist es, nicht über Frameworks zusätzliche Strukturen zu generieren, sondern den DOM als bereits etablierte und ohne Abhängigkeiten und Overhead in allen Browsern verfügbare Plattform für Anwendungen zu nutzen. Diese Idee kann auf die Flux-Architektur übertragen werden – statt des von Facebook erstellten Dispatchers und der EventEmitter
-Klasse können auch standardisierte Funktionalitäten genutzt werden, die der Browser selbst mitbringt – in diesem Falle DOM-Events.
Wenn eine View eine Action an die Stores weitergeben will, kann sie ein DOM-Event erzeugen, das durch Event Bubbling bis zum document
hochgereicht wird – dort kann der Store mittels addEventListener
ein Callback registrieren, das im Falle eines Events aufgerufen wird. Die View kann das Event entweder über die Browser-API new CustomEvent()
oder im Falle von Polymer einfach per this.fire(eventName, eventPayload)
auslösen. Lösen verschiedene Komponenten dieselben Actions aus, können diese an einer zentralen Stelle deklariert werden und als Behaviors
in den verschiedenen Komponenten verfügbar gemacht werden:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
var TodoActions = { /** * Toggle whether a single ToDo is complete * @param {object} todo */ toggleComplete: function(todo) { var id = todo.id; var actionType = todo.complete ? TodoConstants.TODO_UNDO_COMPLETE : TodoConstants.TODO_COMPLETE; this.fire(actionType, { id: id, }); }, //.... }; |
1 2 3 4 5 6 7 |
behaviors: [TodoActions], _onToggleComplete: function() { this.toggleComplete(this.todo); }, |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
document.addEventListener(TodoConstants.TODO_UNDO_COMPLETE, function(ev) { update(ev.detail.id, {complete: false}); TodoStore.emitChange(); }); document.addEventListener(TodoConstants.TODO_COMPLETE, function(ev) { update(ev.detail.id, {complete: true}); TodoStore.emitChange(); }); |
Im oben stehenden Beispiel ist die Logik zum Erzeugen von Actions in actions.js
ausgelagert, wird in component.js
eingebunden und ausgeführt und in store.js
wieder empfangen. Die Stores können die gleiche Technik nutzen, um Änderungen bei den Controller Views bekannt zu machen – dies ist in der Methode TodoStore.emitChange()
gekapselt:
1 2 3 4 5 |
emitChange: function() { document.dispatchEvent(new CustomEvent(TodoConstants.TODO_STORE_CHANGE)); } |
1 2 3 4 5 6 7 8 9 10 11 |
attached: function() { document.addEventListener(TodoConstants.TODO_STORE_CHANGE, this._onChange.bind(this)); }, _onChange: function() { //get and use data from store } |
Hier wird bei Zustandsänderungen document
als Event-Bus genutzt – der Store emittiert ein Event, das von der Controller View per addEventListener
empfangen wird.
Damit ist der unidirektionale Datenfluss von Flux ohne Hilfsbibliotheken wie AppDispatcher umgesetzt und folgt so der Philosophie von Polymer, die vorhandenen Strukturen des DOM gegenüber künstlichen Javascript-Versionen zu bevorzugen. Auf diese Weise haben die verschiedenen Komponenten einer Web App, die von verschiedenen Entwicklern stammen, eine einheitliche Kommunikationsschnittstelle, die per Standard überall verfügbar ist.
Vergleich React/Polymer Version
Im Rahmen dieses Artikels wurde das von Facebook bereitgestellte Flux-Beispiel, eine exemplarische Todo-App, schrittweise zur oben beschriebenen Polymer/DOM-Version umgebaut. Beim Umbau musste die Struktur des Projektes leicht verändert werden; dabei ging es hauptsächlich um die CSS-Dateien, die auf die jeweiligen Komponenten aufgeteilt wurden, da jede Polymer-Komponente einen eigenen CSS-Scope besitzt.
Das resultierende Projekt hat annähernd die selbe Anzahl an Code-Zeilen, da sich die APIs von React und Polymer stark ähneln und HTML und CSS-Code soweit möglich identisch übernommen wurden. Konkateniert und minifiziert ist die mit Polymer erstellte Version um 10% kleiner, was sich durch die Größe der Polymer- bzw. React-Abhängigkeit erklärt – die anderen Abhängigkeiten und die Anwendung selbst sind für die Dateigröße vernachlässigbar.
Performance-Tests legen nahe, dass die Startup-Time der Bibliotheken (Parsen und Evaluieren des Javascripts) bei den neuesten Versionen nahezu identisch ist.
Während die für den Endnutzer relevanten Kennzahlen sich kaum verändern, ergeben sich für den Entwickler nur subjektive Unterschiede in der Ergonomie der Bibliothek sowie eine zukunftsorientierte Ausrichtung auf wiederverwendbare Web Components im Falle von Polymer.
Serverseitiges Flux
Die Idee eines unidirektionalen Datenflusses kann auch als Server-Architektur angewendet werden – die Views
wären in diesem Fall z.B. zum Client gesendete JSON-Dokumente und Actions würden durch HTTP-Anfragen ausgelöst werden. Die zusätzliche Herausforderung, die hier gelöst werden müsste, ist das Handling von verteilten Systemen. Dies kann durch eine verteilte Message-Queue wie z.B. RabbitMQ angegangen werden, die als Dispatcher fungiert und die Daten zu auf verschiedenen physikalischen Systemen laufenden Stores transportiert. Hier ist die Betrachtungsweise, die Sequenz der Actions als den gültigen State zu verstehen, besonders hilfreich – die Migration von Daten (z.B. durch ein Store-Update) stellt dadurch nur eine neue Aggregation dar, die durch Abspielen der gesamten Sequenz für den neuen Store erreicht werden kann. Da Stores durch diese Methode jederzeit wieder auf den neuesten Stand gebracht werden können, müssen sie keine Daten persistieren und sind damit „stateless“, was das Deployment deutlich vereinfacht und stark verteilte Systeme möglich macht.
Wie effizient dieser Ansatz allerdings ist, muss noch überprüft werden.
Fazit
Abschließend lässt sich sagen, dass Flux eine interessante und vielversprechende Methode ist, Webanwendungen zu strukturieren. Web Components in Verbindung mit Polymer bzw. DOM-Events passen gut zu diesem Ansatz und versprechen eine performante, abhängigkeitsarme, moderne Anwendungsarchitektur, die trotzdem alle Möglichkeiten offen und Feature-Skalierung zulässt.
We’re hiring!
Tapetenwechsel gefällig? Wir sind auf der Suche nach begeisterten Frontend-Entwicklern, die unsere Projektteams im Umfeld von JavaScript, HTML und CSS unterstützen und auch vor innovativen Themen wie AngularJS und Progressive Web Apps nicht zurückschrecken. Jetzt Bewerben!
Weiterlesen
Mehr Informationen zu unseren Dienstleistungen rund um die Web-Entwicklung gibt es auf unserer Website. Unser Portfolio umfasst außerdem die Anwendungsentwicklung für Android & iOS mit speziellem Fokus auf Enterprise-Apps. Für direkten Kontakt schreibt an info@inovex.de oder ruft an unter +49 721 619 021-0.