Eine der größten Herausforderung ist es Features, die neu eingeführt werden, je nach verfügbarem Platz auf den verschiedenen Endgeräten in unterschiedlicher Ausprägung zur Verfügung zu stellen. Der Webshop soll sich also adaptiv verhalten. Dadurch soll gewährleistet werden, dass bei allen Endnutzern eine höchstmögliche Akzeptanz hinsichtlich Bedienbarkeit und Benutzererfahrung erreicht wird.

Die Entwicklung eines adaptiven Webshops erfordert eine modulare Sicht auf den Shop und dessen Anforderungen. Es ist zwingend notwendig die Umsetzung der einzelnen Features mit Hilfe eines Komponentensystems durchzuführen. Die einzelnen Komponenten sowie auch das Komponentensystem selbst dürfen nicht starr programmiert sein, sondern müssen dynamisch und unmittelbar auf die Gegebenheiten reagieren können.

Dies bedarf einer erweiterten Sicht auf Komponenten. Im Laufe der bisherigen Arbeiten haben sich in unserem Team Kriterien, die eine Komponente auszeichnen sollten, herauskristallisiert.

Eine Komponente in unserem Sinne hat folgende Eigenschaften:

Die Komponente…

  • …bildet ein Feature vollständig ab
  • …besitzt keine Abhängigkeit zu anderen Komponenten
  • …kommuniziert mit anderen Komponenten über Events
  • …ist nicht verschachtelt
  • …kann auf Device-Größen adaptiv reagieren
  • …wird als einzelne Klasse implementiert
  • …kann mit Optionen gesteuert werden

Im Detail möchte ich auf diese Kriterien in einem gesonderten Artikel eingehen. Hier möchte ich mich eher auf das Zusammenspiel der Komponenten beschränken.

Kommunikation zwischen Komponenten

In unseren Webshops benutzen wir ein Eventing-System, um den Informationsaustausch zwischen den Komponenten zu gewährleisten.
Bei den Events handelt es sich nicht um normale Browser-Events, sondern um ein Publish/Subscribe-System (PubSub), das Events beliebiger Art zulässt und in dem erweiterte Features zur Datenweitergabe implementiert sind.

Solche Systeme sind ein bekanntes Pattern und in vielen Variationen in diversen Bibliotheken und Frameworks implementiert.
Events werden anhand ihres Namens identifiziert.

Aus Komponentensicht betrachtet sendet (published) eine Komponente ein Event mit zusätzlichen Daten an einen Verteiler (Dispatcher), der das Event an andere Komponenten (Member) weiterleitet, die sich vorher für die Verarbeitung dieses Events angemeldet (subscribed) haben.

Eine für ein Event registrierte Komponente wird vom Dispatcher benachrichtigt, sobald das Event eintritt, so dass die Komponente auf dieses Event reagieren kann. Des Weiteren ist sie in der Lage bei einem bestimmten Ereignis (beispielsweise eine Benutzeraktion) selbst ein Event auszulösen.

Eine Komponente hat keinen Einfluss auf das, was andere Komponenten mit einem von ihr emittierten Event machen.

Bereits bei der Implementierung der Komponente wird festgelegt, für welche Events sich die Komponente subscriben soll. Ist das einmal erledigt, kann sich der Entwickler auf die Umsetzung des Features, die Behandlung von empfangenen Events und das Senden eigener Events konzentrieren.
Bei erster Betrachtung ist es unerheblich an welcher Stelle im HTML-Code der Seite sich eine Komponente befindet; sie könnte an beliebiger Stelle platziert sein und würde weiterhin funktionieren.

Das ist in der Realität aber nicht immer gegeben denn hier spielen der Scope und der Kontext der Komponente eine entscheidende Rolle.

Scoping von Events

Betrachten wir als rudimentäres Beispiel eine Datenliste mit der Möglichkeit zu blättern. Die Datenliste wird als Komponente umgesetzt und es gibt zusätzlich eine Komponente zum Blättern am oberen Rand.

Die Blättern-Komponente erzeugt initial die Seitennavigation und markiert darin die aktuelle Seite. Die Datenliste-Komponente subscribed sich auf das Event paging und rendert initial die erste Seite aus den Daten des zugehörigen Models. Danach ist sie erstmal nicht weiter aktiv.

Erst wenn der User auf ein Element der Seitennavigation klickt, erkennt dies die Blättern-Komponente. Sie ermittelt die gewünschte Seite, markiert diese in der Seitennavigation und löst das Event paging aus. Darin gibt sie eine Information zur anzuzeigenden Seite mit.

Auf das paging-Event reagiert die Datenliste. Sie liest die neu anzuzeigende Seite aus den Daten des Events aus und zeigt die entsprechende Seite an.

Soweit ist alles noch recht einfach.

Jetzt aber soll die Blättern-Komponente ein zweites mal unterhalb der Datenliste angezeigt werden, um zum Beispiel bei langen Seitenlisten das Blättern am Ende der Liste zu ermöglichen.
Wir haben nun zwei Komponenten-Instanzen der gleichen Komponenten-Klasse gleichzeitig auf der Seite. Klickt der User auf einer der beiden Seitennavigationen, muss die Zweite synchronisiert werden, damit sie die gewählte neue Seite ebenfalls markiert.

Synchronisieren von Komponenten

Zur Synchronisation zweier gleicher Komponenten gibt es mehrere Wege. Eine Möglichkeit ist, dass die angeklickte Komponente ein neues Event auslöst, das zum Beispiel statusChanged heißen könnte.
Die Blättern-Komponente subscribed sich für ihr eigenes statusChanged Event und reagiert nur auf dieses Event, wenn die auslösende Komponenten-Instanz nicht sie selbst ist, was sonst in einer Endlosschleife enden würde.

Übrigens: Die meisten JavaScript-Frameworks setzen hier den Mechanismus des Data-Bindings ein, was ein solches PubSub-System vor dem Entwickler verbirgt.
Führt man zur Synchronisation neue Events ein, wird es bei größeren Projekten schnell unübersichtlich. Stattdessen bietet sich hier eine andere Methode an, die wir hier als Event-Scoping bezeichnen.

Event-Scoping mit Keys

Einer Komponenten-Instanz wird in der Option scope ein Key mitgegeben. Die Komponente reagiert nur dann auf bestimmte Events, wenn die auslösende Komponente den gleichen Key in ihrer scope-Option gesetzt hat.

Mit diesem Verfahren können beliebig viele Komponenten zueinander in Beziehung gesetzt werden.
Im Dispatcher lässt sich das automatisieren, in dem Events, die mit einem scope-Key versehen sind, nur an Member-Komponenten verteilt werden, die den gleichen scope-Key haben.
Die Komponente muss sich dann nicht selbst darum kümmern, ob ein Event für sie relevant ist, sondern reagiert direkt ohne weitere Prüfung auf die Events die an sie weitergeleitet wurden.

Die Nachteile dieses Verfahrens sind jedoch eine höhere Belastung des Dispatchers sowie, bei Single Page Applications, ein höherer Verwaltungsaufwand der scope-Keys.
In der Praxis hat sich gezeigt, dass beim Einsatz von Komponenten durch ein CMS-System dem Redakteur nicht zuzumuten ist, dass er sich mit scope-Keys auseinandersetzt.
Weiterhin muss gewährleistet werden, dass der scope-Key eindeutig ist.
Für die einfache Synchronisation zum Beispiel zweier Slider oder Kaskaden von Selektboxen ist dieses Verfahren jedoch durchaus einsetzbar.

Erweitertes Event-Scoping mit Kontexten

Event-Scoping kann auch in einem größeren Zusammenhang notwendig sein. Greifen wir unser Beispiel der Datenliste mit Blättern-Funktionalität noch einmal auf. Möglicherweise sollen bei einem Klick auf ein Datenelement Detaildaten in einem Modal angezeigt werden, die wiederum eine Datenliste sind.

Die Blättern-Komponenten beider Datenlisten emittieren die gleichen Events (paging), aber welche Datenliste reagiert nun auf welches Event? Ohne weitere Strukturierung würden beide Datenlisten auf alle paging-Events reagieren.

Man könnte mit scope-Keys arbeiten, aber auch hier müsste man die Keys aufwendig verwalten und jeder Komponente der Datenliste den gleichen scope-Key mitgeben. Das würde bedeuten, dass die Blättern-Komponenten zwei scope-Keys bekämen. Sicherlich ließe sich ein Event-Scoping mit einem Array von scope-Keys verwalten, aber das würde den Dispatcher weiter verlangsamen und einen weiteren Aufwand zur Verwaltung der scope-Keys verursachen.

Wir haben das letztendlich mit mehreren Dispatchern gelöst, den Kontexten. Ein Kontext ist eine Instanz des Dispatchers (Event-Verteilers). Es gibt für jede Seite eine Dispatcher Instanz, den main-Kontext, und für jedes Modal jeweils eine eigene Instanz, den Modal-Kontext. Jede Komponente bekommt bei ihrer Instanziierung ihren aktuellen Kontext übergeben. Fortan subscribed und published sie die Events nur über diesen Kontext. Das Eventing bleibt daher innerhalb eines jeden Kontextes gekapselt, der scope-Key wird nur noch für die Synchronisierung von Komponenten im gleichen Kontext benötigt.

Das Scoping über die Event-Dispatcher main-Kontext und Modal-Kontext reicht aber nicht immer aus. In speziellen Fällen nutzen wir stattdessen eine Composite-Component.

Gezieltes Event-Scoping mit Composite-Components

Nehmen wir wieder unser Beispiel der Datenliste. Die Datenliste ist in unserem Fall eine Ansicht auf die Daten, die nicht zeilenweise angeordnet ist, sondern in sogenannten Kacheln. Diese durch ein Template gesteuerte Sicht auf die dahinter liegenden Tabellendaten bezeichnet man als DataView, nicht zu verwechseln mit dem JavaScript-Standardobjekt DataView, das für die Arbeit mit einem Array-Buffer gedacht ist. Die DataView lässt es zu, dass innerhalb einer Einzelansicht eines Datensatzes (Kachel) komplexe User-Interaktionen stattfinden können, die in einer zeilenorientierten Liste so nicht möglich wären.

In unserem Fall besteht eine Kachel aus einer Image-Slider- und einer Farbwechsel-Komponente, die eine Sicht auf jeweils ein Produkt darstellt. Klickt der User auf eine Farbe, wird ein colorChange-Event von der entsprechenden Farbwechsel-Komponente ausgelöst, worauf der zugehörige Image-Slider sich subscribed hat und reagiert, indem er entsprechend zur gewählten Farbe passende Bilder anzeigt. Da wir 24 Produkte gleichzeitig zeigen haben wir 24 Kacheln mit je 2 Komponenten.

Ohne weitere Strukturierung hätten wir 2 mal 24 gleichartige Komponenten die alle über den gleichen Kontext kommunizieren. Das ist beispielsweise hier der main-Kontext, weil alle Kacheln sich auf der gleichen Seite befinden. Eine Image-Slider-Komponenteninstanz müsste aus 24 verschiedenen Farbwechsel-Komponenten das colorChange-Event herausfiltern, was von seiner Kachel ausgegangen ist.

Auch hier könnte man mit scope-Keys arbeiten, die Verwaltung wäre aber sehr aufwendig.

Hier bietet es sich an Subkontexte innerhalb eines Kontextes zu benutzen. Hat jede Kachel einen eigenen Kontext über den dessen Komponenten kommunizieren, bleibt das Eventing innerhalb des jeweiligen Kontextes und somit innerhalb der Kachel gekapselt.

Sollen Komponenten außerhalb einer Kachel auf Änderungen innerhalb der Kacheln reagieren, wäre das innerhalb dieser Architektur nicht möglich. Hier kommt ein zusätzlicher Aspekt hinzu, der sich durch eine Composite-Component lösen lässt.

Was ist eine Composite-Component

Der Begriff stammt aus dem Bereich der Web-Components und bezeichnet eine Komponente, die ihr View-Model, Bindings, Metadaten, Parameter, Code und CSS enthält.

Wir erweitern diese Betrachtung um den Event-Dispatcher (Kontext). Eine Composite-Component ist eine Komponente, die sowohl als Komponente als auch als Kontext dienen kann. Innerhalb ihres DOM vorhandene Komponenten (Member) stellt sie einen Kontext zur Event-Kommunikation zur Verfügung, kann selbst auf diese inneren Events reagieren und da sie selbst eine Komponente ist, auch als eine Art Proxy zur Kommunikation nach außen dienen. Nach außen wirkt sie wie eine einzelne Komponente.

Wir nutzen die Composite-Component als View-Controller für ein komplexeres Zusammenspiel mehrerer Komponenten als größeres Feature auf der Seite. Grundsätzlich wäre das nichts Anderes als eine Verschachtelung von Komponenten, aber hier gibt es einen entscheidenden Unterschied: Die Member-Komponenten sind nicht abhängig von der Composite-Component und können auch ohne sie funktionieren. Sie ist eher eine Komposition von Komponenten.

Die Composite-Component dient also zur Kapselung eines Feature-Zusammenschlusses und nicht als Parent-Komponente. Daher können auch Komponenten außerhalb ihres DOM sich als Member-Komponente anmelden und kapseln so ihre Events innerhalb des Composite-Component Kontextes. Im Extremfall könnte eine Composite-Component überhaupt keine DOM-Repräsentation haben und nur zur Kapselung von Events diverser Komponenten dienen, die breit gestreut auf der Seite angeordnet sind. Komponenten können so unabhängig vom Layout der Seite als komplexes Ganzes reagieren.

Was für Vorteile bietet diese Konstruktion?

Zurück zu unserem Beispiel. Jede Kachel einer Produktübersichtsseite ist eine Composite-Component die jeweils nur ein change-Event an den übergeordneten Kontext emittiert und die Member-Komponenten der Kachel abschirmt. Auch haben wir haben eine Produktdetail Seite, in der aus verschiedenen Auswahlmöglichkeiten wie Größe, Farbe und anderen Kriterien letztendlich ein Produkt definiert wird, was der User bestellen kann. Auf der DataView Kachel der Produktübersichts-Seite befinden sich die gleichen Komponenten wie auf der Produktdetail-Seite, die nur zusätzliche Komponenten enthält und deren Position und Größe anders sind. Es ist keine weitere Programmierung notwendig denn die Kachel ist eine kleine Produktdetail-Seite. Daher kann man die Produktdetail-Seite mit den gleichen Komponenten auch in einem Modal anzeigen. Da ein Modal auch einen eigenen Kontext hat, stören sich die Eventings der Komponenten nicht gegenseitig – auch wenn sich das gleiche Produkt gleichzeitig einmal auf einer Produktdetail-Seite und einmal in einem Modal befindet. Mit Hilfe einer Composite-Component, die sich inline einblenden lässt, kann die Produktdetail-Seite auch direkt über oder unterhalb einer Kachel eingeblendet werden, so dass der User direkt in der DataView ein Produkt zu Ende konfigurieren könnte. Weiter könnte man ohne Codeanpassung eine weitere Komponente, die auch in der Produktdetail-Seite benutzt wird, zur Kachel hinzufügen, zum Beispiel eine Größen-Auswahlkomponente.

Wir können eine Produktansicht mit User-Interaktionsmöglichkeit an beliebiger Stelle und in unterschiedlicher Ausprägung einbinden. Dazu ist nur eine eventuelle Style-Anpassung notwendig. Weil die Komponenten nur über Events miteinander kommunizieren und von anderen Komponenten abgekoppelt sind, ist es möglich, für verschiedene Bildschirmgrößen eine unterschiedliche Komponentenauswahl zur Verfügung zu stellen, sie evtl. auch gar nicht zu nutzen, ohne an der Codebasis arbeiten zu müssen. Letztendlich kann man so die gesamte DataView-Funktionalität mit Blättern, Sortierung, Detailansicht und Filtern mehrfach auf der Seite jeweils mit unterschiedlichen Daten anzeigen. Möglich wäre hier eine Tab-Ansicht mit mehreren DataViews zu erzeugen.

Zusammenfassung

Durch die Kommunikation der Komponenten über Events kann man problemlos Komponenten und somit auch Features weglassen oder hinzufügen, ohne am Code Anpassungen vornehmen zu müssen. Weil die Komponenten nicht hierarchisch in Beziehung stehen lassen sich einzelne Komponenten entfernen, an andere Stellen im DOM verschieben und in neuen Komponenten-Kompositionen wieder verwenden. Da Komponenten in sich gekapselt sind, kann das von ihnen repräsentierte Feature an beliebiger Stelle auf der Seite platziert werden und ist unabhängig von anderen Komponenten oder Features.

Das Zusammenspiel von kleineren Features hin zu einem großen Workflow durch deren Kombination (wie zum Beispiel eine DataView mit Blättern, Filtern und Sortieren Features) lässt sich durch geschickt eingesetzte Kontexte und Composite-Components nach außen kapseln und beliebig auf der Seite duplizieren.

Wie und ob eine Komponente mit anderen Komponenten zusammenarbeitet hängt alleine vom Kontext ab, dem sie angehört.

Der Redakteur kann allein schon durch die Position auf der Seite visuell ermitteln, zu welchem Kontext welche Komponente gehört und welche zusammenarbeiten.

Composite-Component-Kontexte können auch Komponenten außerhalb ihres DOM als Member verwalten, was ermöglicht, dass Komponenten, die sich nicht in der gleichen DOM-Hierarchie befinden, zusammenarbeiten können.

Insgesamt bietet das für den Redakteur und dem UX-Team maximal mögliche Freiheiten des Layouts, gepaart mit der Möglichkeit Gruppen von Komponenten zu verbinden.

Ausblick

Wie unsere Komponenten Breakpoint abhängig auf die Bildschirmgröße reagieren können, welche Features sie dazu intern benötigen und wie man sie möglichst Ressourcensparend aufbaut, wird in einem der nächsten Blogartikel beschrieben.

Andreas

techblog@walbusch.de

Kontakt Blog

Wenn Sie Fragen oder Feedback zum Technologie-Blog oder zu einem unserer Beiträge haben, können Sie sich gerne per Mail an unser Technologie-Blog Team wenden!

E-Mail: techblog@walbusch.de