Self-Contained Systems [SCS] bedeuten, dass man Systeme mit jeweils einer eigenen Web-UI baut und sie nach aussen hin wie ein System aussehen lässt. Das bedeutet, man muss diese Systeme an unterschiedlichsten Stellen integrieren. Über die Integrationspunkte unterhalb des Frontends (REST, Messaging, Daten-Replikation, Compile-Zeit, …) wurde von den großen Geistern der IT-Architektur schon viel geschrieben und gesagt. Mir erscheint aktuell die Frage, wie man Systeme im Web-Frontend integriert, etwas spannender und weitaus weniger diskutiert.
Auch hier gibt es vermutlich zig verschiedene Ansätze. Ich möchte hier einen ganz
speziellen Ansatz beleuchten, der mir die meisten Probleme zu bereiten
scheint: Die Transklusion von HTML aus anderen Systemen. Konzeptionell
handelt es sich dabei um die Ersetzung eines Links durch den Inhalt des
<body>
-Tags, den man erhält, wenn man ein HTTP-GET
mit
Accept: text/html
auf eben diesen Link macht.
Wo genau diese Substitution stattfindet, ist damit natürlich noch nicht geklärt. Auch hierfür gibt es wieder mehrere Varianten, die anhand eines kleinen Beispiels beleuchtet werden sollen.
Technische Möglichkeiten der Transklusion
Stellen wir uns ein Web-Portal einer (modernen 😉) Fluglinie vor: Wenn dort ein System beispielsweise für den Status von Flügen (Verspätungen etc.) verantwortlich ist und ein anderes für die persönlichen Buchungen, so erscheint es (mir zumindest) sinnvoll, den Status des jeweiligen Fluges direkt bei der Buchungsübersicht anzuzeigen. Soll diese Integration im Frontend per Transklusion erfolgen, so gibt es hierfür zumindest zwei konzeptionell unterschiedliche Umsetzungsmöglichkeiten:
- Die Transklusion innerhalb eines Webservers oder Reverse-Proxies und
- die Transklusion innerhalb des Browsers mittels Ajax.
Für den ersten Ansatz bieten sich Standardlösungen wie Edge Side
Includes [ESI] oder Server Side Includes [SSI] an.
Hierbei wird in einem vorgeschalteten Webserver oder Reverse-Proxy die
einbettende Seite nach entsprechenden Platzhaltern durchsucht (wie zum
Beispiel <esi:include src='http://mein.server.de/foo/bar'>
), aus diesen
dann jeweils eine URI extrahiert, auf diese URI ein HTTP-GET ausgeführt
und der Platzhalter dann durch die entsprechende HTTP-Response ersetzt.
Der Knackpunkt dieser Ansätze ist natürlich die Reaktionsgeschwindigkeit
der Backends. Wenn z.B. die Buchungsübersicht eine Liste der Buchungen
rendert, in der unterhalb jeder Buchung ein <esi:include>
auf den
jeweiligen Flugstatus steht, so müsste beispielsweise ein Reverse-Proxy
zunächst diese <esi:include>
-Tags ersetzen bevor er die Antwort überhaupt
an den Client weiter schicken kann. Antwortet jetzt das Flugstatussystem auf
nur einen einzigen dieser Requests etwas langsamer, so verzögert dies natürlich
die gesamte Antwort und der Benutzer sieht eine Weile lang keine Reaktion.
Würde man das Problem clientseitig angehen, so reichten dafür ein paar Zeilen jQuery-Code (natürlich nur im Prototypen-Modus):
Dies funktioniert natürlich nur so lange, wie das Linkziel den Anforderungen der Same Origin Policy [[SOP]] genügt. Auf der anderen Seite werden hierbei Transklusionen schön unabhängig voneinander gleichzeitig und asynchron abgearbeitet, wodurch Fehler nur lokale Auswirkungen haben. Ausserdem kann man hier relativ einfach auch nur gezielt einen bestimmten Teilbaum des eingebetteten Dokumentes transkludieren und nicht immer nur den gesamten ``-Inhalt (siehe beispielsweise [[JQL]]).Welche der beiden Varianten wofür besser geeignet ist, muss im Einzelfall entschieden werden. Grundsätzlich scheint serverseitige Transklusion für primäre Inhalte der Seite, ohne die die Darstellung der Inhalte keinen Sinn ergibt, sinnvoll. Allerdings erscheint mir die Notwendigkeit der Transklusion primärer Inhalte ein “Smell” zu sein, da ein SCS den Kern seiner Funktionalität eigentlich selbst beinhalten sollte.
Ich persönlich halte den ersten Ansatz (ESI/SSI/…) für meistens nicht notwendig
und (siehe “Feedback” unten) würde grundsätzlich (im Sinne der
Rechtsprechung)
den zweiten bevorzugen. Typischerweise dadurch nicht abdeckbare
Punkte sind Hauptmenüs oder Footer. Diese würde ich tendenziell zur
Compile-Zeit als statisches Asset (also beispielsweise auch als
HTML-Schnipsel) integrieren und lieber darauf achten, schnell und oft
deployen zu können. Änderungen an diesen Assets sind nämlich typischerweise
- selten,
- oft nicht so kritisch und
- besser durch globale Feature-Toggles pro System schaltbar als durch globale Magie.
Styling und Funktion
Beide Ansätze haben aber ein gemeinsames Problem: Wenn man dem DOM – über welchen Mechanismus auch immer – einfach irgendwelchen neuen Inhalt hinzufügt, so muss man auch dafür sorgen, dass dieser Inhalt ordentlich dargestellt wird (Styling) und auch ordentlich funktioniert (insbesondere im Hinblick auf JavaScript). Unterm Strich geht es also um die Frage, welche statischen Assets (CSS und JavaScript) zu diesem Zweck herangezogen werden und woher sie kommen.
Und, wer hätte es gedacht, auch hierfür gibt es mal wieder verschiedene Optionen.
Gemeinsame Assets
Die sicherlich am einfachsten erscheinende Möglichkeit ist, die Transklusion auf Elemente aus einem zentralen Assets-Repository zu beschränken. Wäre also unsere Flug-Verwaltung beispielsweise Bootstrap-basiert [BS], so könnte es einfach die Regel geben, dass transkludierte Inhalte ausschließlich Elemente aus Bootstrap beinhalten dürfen und kein Custom-Styling haben dürfen.
Jede transkludierende Ansicht (in unserem Beispiel die Buchungsübersicht) müsste also Bootstrap ausliefern, damit die eingebetteten Inhalte sauber dargestellt werden.
Custom-Assets
Wenn das für Sie plausibel klingt, so müssen Sie nicht weiter lesen und sind fertig. Für mich erscheint es etwas unrealistisch, dass Bootstrap komplett ausreichen wird, um die UX/UI-Anforderungen umzusetzen. Wir werden also wohl nicht darum herum kommen, die zentralen Assets selbst zu pflegen. In unserem Beispiel gäbe es also dann ein drittes Spezialsystem, das für die Bereitstellung der zentralen Styles und Scripte verantwortlich ist. Transkludierende Inhalte binden diese dann entsprechend ein.
Eine Gefahr dieses Ansatzes liegt darin, dass dieses “Assets”-Projekt am Anfang des Gesamt-Projektes vermutlich nicht existiert. Es muss also parallel zu den anderen Systemen entwickelt werden und verstößt deswegen gegen etliche Grundsätze der Self-Contained Systems (nur eben im Frontend und nicht im Backend). Weil aber Papier und irgendwelche komischen Architektur-Manifeste bekanntlich geduldig sind, habe ich diesen Ansatz schon öfters in der freien Wildbahn gesehen. Die erfolgreichen Fälle zeichneten sich dann meist durch ein sehr gutes Assets-Team, bestehend aus Mitgliedern der einzelnen Systeme, aus. Nimmt man dieses Problem auf die leichte Schulter, so ist das Ergebnis aber sicherlich projektgefährdend.
Versionierung
Gerade gut gepflegte Custom-Assets ändern sich darüber hinaus auch noch oft. Das ist aber natürlich auch auf den zuerst geschilderten naiven Bootstrap-Fall ausweitbar, wenn man beispielsweise von Bootstrap v3 auf v4 aktualisieren will. Liefert also in unserem Fall die Buchungsübersicht schon Bootstrap 4 aus, der Flugstatus vertraut aber noch auf die Präsenz von Bootstrap 3, so wird der Flugstatus vermutlich kaputt dargestellt und wir haben ein Problem.
Dagegen hilft nur, die transkludierten Inhalte vor den einbettenden Ansichten zu aktualisieren und eine Zeit lang beide Versionen bereit zu stellen. Es gäbe also in unserem Beispiel den Flugstatus in zwei Versionen: Einmal mit Bootstrap-3-kompatiblen Inhalten und einmal mit Bootstrap-4-kompatiblen. Welche Version eingebettet wird, könnte über die URL spezifiziert werden oder über HTTP-Header (je nach Geschmack).
Au weia… Das klingt nach Arbeit (die wir uns bei Backend-APIs ja genauso machen müssen).
Self-Contained Assets
Nimmt man als Gegenentwurf zu zentralen Assets die SCS-Architektur einmal
wörtlich, so müssten die transklusionsrelevanten Assets wohl genauso
Bestandteil des jeweiligen bereitstellenden Systems sein, wie die Inhalte
selbst. In unserem Beispiel würde also die Buchungsübersicht ihre eigenen
Styles beinhalten, um ihre eigenen Inhalte korrekt darzustellen.
Darüber hinaus muss dann noch irgendwie dafür gesorgt werden, dass auch
die Styles und Scripte des Flugstatussystems irgendwie in dem aktuellen
window
vorhanden sind.
Klingt zunächst mal offensichtlich. Insbesondere solange man so tut, als wären Styles und Scripte isoliert. Sind sie aber leider nicht. Wenn man aber entsprechend von Hand für eine möglichst hohe Isolation sorgt, indem man z.B. möglichst verhindert, dass CSS-Selektoren kollidieren (z.B. anhand von systemspezifischen HTML-Klassenpräfixen), ist dieses Problem in den Griff zu bekommen.
Eine Frage aber bleibt: Wie kommen die Styles und Scripte denn dann in das transkludierende System?
Inline
Der einfachste Ansatz wäre vermutlich, <style>
- und <script>
-Inhalte
einfach direkt mit dem Inhalt auszuliefern. Auch echte Inline-Styles
(<div style="...">
) erscheinen denkbar. Da Letzteres aber massive Auswirkungen
auf die CSS-Selektorspezifität hat, ist das eigentlich eher keine Option.
Wie man es auch im Detail macht, bei unserem Beispiel wäre dieser Ansatz spätestens dann etwas nachteilig, wenn die Buchungsübersicht mehr als nur eine Buchung beinhalten würde. Dann würden nämlich dieselben Styles und Scripte mehrmals ausgeliefert. Nichtsdestotrotz erscheint mir dieser Ansatz aktuell etwas verpönter, als er eigentlich sein sollte.
Verlinkung benötigter Styles und Scripte
Wenn jedes System einfach seine Styles und Scripte selbst
bereitstellen würde, so müssten transkludierende Systeme einfach
nur diese Styles und Scripte einbinden. In unserem Beispiel
würde also die Buchungsübersicht ihre eigenen Assets ausliefern und
dazu noch die transklusionsrelevanten Styles des
Flugstatussystems per <link rel="stylesheet" href="...">
im
<head>
einbinden (Scripte analog).
Ein Nachteil dieses Ansatzes zeigt sich insbesondere dann, wenn ein
System viele unterschiedliche Systeme einbettet. Dann würde der Browser relativ
viele Requests zum Laden der fremden Assets abfeuern, was oft nicht akzeptabel
ist (insbesondere ohne HTTP 2). Auch besteht die Frage, wie man in einem solchen
Szenario die Assets mit einer Version in der URL einbinden kann, da man
typischerweise Assets mit einem sehr hohen Cache-Control: max-age
versehen
will. Woher weiß also die Flugbuchungsübersicht, in welcher Version
sie die Assets des Flugstatussystems einbinden soll?
Dieses Problem muss also auf einer anderen Ebene (meine persönliche Präferenz: JSON Home [JSH]) adressiert werden.
Künstliche Zentralisierung
Eine weitere Alternative wäre es, zur Transklusion benötigte Styles und Scripte der einzelnen Systeme beispielsweise im Build einzusammeln und in ein gemeinsames Style- oder Script-Bundle zusammen zu packen. Dieser Ansatz adressiert primär die Probleme des vorher beschriebenen Vorgehens (der Verlinkung), dürfte aber für viele Projekte mangels vorhandener Standard-Produkte eine Nummer zu groß sein.
Mischmasch
Natürlich sind beide Vorgehensweisen (zentrale Assets und Self-Contained Assets) kombinierbar. Beispielsweise wäre ein zentrales (relativ stabiles) Framework, wie Bootstrap oder ein vorher gebautes Custom-Framework, denkbar und dazu die Möglichkeit für transkludierte Systeme, eigene (darauf aufbauende) Anpassungen auszuliefern.
Kommt das Projekt in eine stabile Phase oder stellt man fest, dass mehr als ein System die selben Assets benötigt, so könnte man nach und nach Assets aus den Systemen in die zentralen Assets konsolidieren.
Wat nu?
Sollte ich das besprochene Beispiel umsetzen, so würde ich, wie bereits gesagt, zu einer meist Ajax-basierten Transklusion tendieren. Die Assets würde ich vermutlich von einem Assets-Team (ggf. aus Mitgliedern der einzelnen System-Teams) bauen lassen, aber trotzdem die Verlinkung von systemspezifischen Assets zulassen. Für das zentrale Assets-Team wäre die erste Aufgabe vermutlich mehr die Bereitstellung des Stylings für den gemeinsamen Seitenrahmen, die Transklusions-Logik und einiger wichtiger Komponenten (wie Buttons, Links, Text, …).
Gibt’s 'ne Moral?
Mir fällt bei diesem Themengebiet immer wieder auf, dass man eigentlich oft klassische IFrames nachbaut. Diese haben eine hohe Isolation, können potentiell interagieren [SOP] und man muss fast nichts dafür tun. Allerdings sind sie verpönt, was primär daran liegen dürfte, dass die einbettende Seite die Maße (insbesondere die Höhe) des Einbettungsfensters vorgibt, diese aber eigentlich nicht kennen kann. Die Variante aber wenigstens im Hinterkopf behalten und somit zumindest manchmal das durchaus komplexe Thema der Transklusion vermeiden, sollte man aber schon, finde ich.
Feedback
Im Folgenden einige interessante Reaktionen auf diesen Post bei Twitter.
@tillsc Guter Artikel! Bei einem neuen System würde ich HTTP/2 als gegeben sehen. Antipattern wird Best Practice https://t.co/FyvfjVPiAZ
— Michael Geers (@naltatis) April 11, 2016
We‘d love to show you a tweet right here. To do that, we need your consent to load third party content from twitter.com
Ich stimme absolut zu, dass HTTP/2.0 aktuell die Art wie wir mit Assets umgehen verändert, insbesondre CSS. Ich persönlich bezweifle jedoch, dass HTTP/2.0 beispielsweise Bundling gänzlich überflüssig machen wird. Gerade bei geschachtelten Abhängigkeiten (die es in CSS natürlich nicht so stark gibt) bleibt immer die Verbindungs-Latenz: So kann beispielsweise ein nicht direkt aus dem HTML referenziertes JavaScript-Modul erst dann geladen werden, wenn ein anderes JavaScript-Modul vollständig geladen wurde. Wären beide Module im selben Bundle, so wäre dies nicht der Fall. Die Lade-Latenz summiert sich bei geschachtelten Abhängigkeiten also auf (mal vereinfachend davon ausgehend, dass kein Server Push eingesetzt wird).
@tillsc Schön! Ob client oder serverseitige Integration hängt v.a. von fachlichen Anforderungen ab, meistens SEO,...
— Martin Grotzke (@martin_grotzke) April 16, 2016
We‘d love to show you a tweet right here. To do that, we need your consent to load third party content from twitter.com
@tillsc ... die Implikationen fehlen mir da ein bisschen, weil sie bekannt sein / beachtet werden müssen.
— Martin Grotzke (@martin_grotzke) April 16, 2016
We‘d love to show you a tweet right here. To do that, we need your consent to load third party content from twitter.com
Ich stimme zu und habe meine Aussage abgeschwächt.
@tillsc Eine interessante Kombination von server/clientseitig ist bigpipehttps://t.co/lztFRn97mJhttps://t.co/u8nT8sMwEx
— Martin Grotzke (@martin_grotzke) April 16, 2016
We‘d love to show you a tweet right here. To do that, we need your consent to load third party content from twitter.com
Danke für den Link.
@gustaf_nk @tillsc @stilkov I've references you in my talk. Thanks for your work! #spa #scs https://t.co/ipovs126Di
— Michael Geers (@naltatis) April 20, 2016
We‘d love to show you a tweet right here. To do that, we need your consent to load third party content from twitter.com
Ein super Foliendeck! (Natürlich auch ohne die Referenz auf diesen Post ;-) )