Take advantage of a micro frontend for legacy web applications
Recently we reported on a new Fragment architecture for building web applications. The solution is fast, cost-effective, scalable for the largest projects and enables a rapid iteration cycle. This approach leverages multiple Cloudflare Workers working together to render and stream micro-frontends into an application that is more interactive than traditional client-side approaches - meaning better user experience and better SEO values.
This approach is great if you are starting a new project or have the capacity to rewrite your current application from scratch. But in reality, most projects are too large for a complete rebuild and can only incorporate architectural changes incrementally.
In this post, we propose a way to replace only selected parts of an old client-side rendered application with server-side rendered fragments. The result is an application where key views are more quickly interactive, can be developed independently, and retain all the benefits of the micro-frontend approach without the need for large-scale rewriting of the legacy codebase. This approach is framework independent; in this post we show fragments created with React, Qwik and SolidJS.
The pain of large front-end applications
Many large front-end applications currently being developed do not provide a good user experience. This is often due to architectures that require large amounts of JavaScript to be downloaded, parsed, and executed before users can interact with the application. Despite efforts to delay non-critical JavaScript code through lazy loading and server-side rendering, these large applications still take too long to become interactive and respond to user input.
Additionally, large monolithic applications can be complex to develop and deploy. Sometimes multiple teams work on a single codebase, and the effort required to coordinate testing and deployment of the project makes it difficult to develop, deploy, and optimize individual features.
As described in our previous post , micro-frontends powered by Cloudflare Workers provide a solution to these problems, but converting a monolithic application to a micro-frontend architecture is often difficult and expensive. It can take months or even years of development work for users or developers to feel the benefits.
Wir brauchen einen Ansatz, bei dem ein Projekt schrittweise Mikro-Frontends in die wichtigsten Teile der Anwendung einführen kann, ohne die gesamte Anwendung auf einmal neu schreiben zu müssen.
Fragmente als Retter in der Not
Das Ziel einer fragmentbasierten Architektur ist eine deutliche Reduzierung der Lade- und Interaktionslatenz für große Webanwendungen (gemessen über die Core Web Vitals). Dafür zerlegen wir die Anwendung in Mikro-Frontends, die schnell in Cloudflare Workers gerendert (und gecacht) werden können. Die Herausforderung dabei: Ein Mikro-Frontend-Fragment in eine veraltete, clientseitig gerenderte Anwendung zu integrieren, und zwar mit minimalen Kosten für das ursprüngliche Projekt.
Wir schlagen eine Technik vor, die es uns ermöglicht, die wertvollsten Teile der Benutzeroberfläche einer veralteten Anwendung zu konvertieren, und zwar isoliert vom Rest der Anwendung.
Tatsächlich sind in vielen Anwendungen die wertvollsten Teile der Benutzeroberfläche eingebettet in eine „Anwendungsschale“, die Header-, Footer- und Navigationselemente enthält. Beispiele hierfür sind das Anmeldeformular, der Bereich mit den Produktdetails in einer E-Commerce-Anwendung oder der Posteingang in einem E-Mail-Client.
Schauen wir uns ein Anmeldeformular genau an. Wenn unsere Anwendung das Anmeldeformular erst nach mehreren Sekunden anzeigt, werden die Nutzer sich nur ungern anmelden, und wir könnten sie verlieren. Wir können das Anmeldeformular jedoch in ein serverseitig gerendertes Fragment umwandeln, das sofort angezeigt wird und interaktiv ist, während der Rest der veralteten Anwendung im Hintergrund hochfährt. Da das Fragment bereits interaktiv ist, kann der Nutzer seine Anmeldedaten sogar übermitteln, bevor die veraltete Anwendung gestartet ist und den Rest der Seite gerendert hat.
Animation, die veranschaulicht, dass das Anmeldeformular vor der Hauptanwendung verfügbar ist
Mit diesem Ansatz bieten Entwicklerteams den Nutzern wertvolle Verbesserungen zu einem Bruchteil der Zeit und der Entwicklungskosten, ganz anders als bei herkömmlichen Ansätzen, bei denen entweder Verbesserungen der Nutzererfahrung auf der Strecke bleiben oder die gesamte Anwendung langwierig und risikoreich neu geschrieben werden muss. Teams mit monolithischen Single-Page-Anwendungen können eine Mikro-Frontend-Architektur schrittweise einführen, die Verbesserungen auf die wertvollsten Teile der Anwendung ausrichten und so die Investitionsrendite vorverlagern.
Bei der Extraktion von Teilen der Benutzeroberfläche in serverseitig gerenderte Fragmente ergibt sich eine interessante Herausforderung: Sobald sie im Browser angezeigt werden, sollen sich die veraltete Anwendung und die Fragmente wie eine einzige Anwendung anfühlen. Die Fragmente sollten sauber in die veraltete Anwendungshülle eingebettet werden, so dass die Anwendung durch die korrekte Bildung der DOM-Hierarchie zugänglich bleibt. Wir möchten aber auch, dass die serverseitig gerenderten Fragmente so schnell wie möglich angezeigt werden und interaktiv sind – noch bevor die veraltete clientseitig gerenderte Anwendungshülle entsteht. Wie betten wir also UI-Fragmente in eine Anwendungshülle ein, die es noch gar nicht gibt? Wir haben dieses Problem mit einer von uns entwickelten Technik gelöst, wir nennen sie „Fragment Piercing“.
Fragment Piercing
Fragment Piercing kombiniert HTML/DOM, das von serverseitig gerenderten Mikro-Frontend-Fragmenten erzeugt wird, mit HTML/DOM, das von einer veralteten clientseitig gerenderten Anwendung erzeugt wird.
Die Mikro-Frontend-Fragmente werden direkt in die oberste Ebene der HTML-Antwort gerendert und sind so konzipiert, dass sie sofort interaktiv sind. Im Hintergrund wird die veraltete Anwendung clientseitig als ein Geschwisterteil dieser Fragmente gerendert. Wenn sie bereit ist, werden die Fragmente in die veraltete Anwendung „gepierct“ – das DOM jedes Fragments wird an die entsprechende Stelle im DOM der veralteten Anwendung verschoben – ohne dass es zu visuellen Nebeneffekten oder zum Verlust des clientseitigen Status kommt, z. B. des Fokus, der Formulardaten oder der Textauswahl. Sobald ein Fragment „durchdrungen“ (pierced) ist, kann es mit der veralteten Anwendung kommunizieren und wird so zu einem integrierten Teil der Anwendung.
Hier sehen Sie ein „Login“-Fragment und das leere „Root“-Element der veralteten Anwendung auf dem Top-Level des DOM, bevor es gepierct wird.
<body>
<div id="root"></div>
<piercing-fragment-host fragment-id="login">
<login q:container...>...</login>
</piercing-fragment-host>
</body>
Und hier sehen Sie, dass das Fragment in das „login-page“-Div in der gerenderten veralteten Anwendung eingefügt wurde.
<body>
<div id="root">
<header>...</header>
<main>
<div class="login-page">
<piercing-fragment-outlet fragment-id="login">
<piercing-fragment-host fragment-id="login">
<login q:container...>...</login>
</piercing-fragment-host>
</piercing-fragment-outlet>
</div>
</main>
<footer>...</footer>
</div>
</body>
Um zu verhindern, dass sich das Fragment bewegt und während dieses Übergangs eine sichtbare Layoutverschiebung verursacht, wenden wir CSS-Styles an, die das Fragment vor und nach dem Piercen auf die gleiche Weise positionieren.
Eine Anwendung kann zu jedem Zeitpunkt eine beliebige Anzahl von gepiercten Fragmenten anzeigen, oder auch gar keine. Diese Technik ist nicht nur auf das erste Laden der veralteten Anwendung beschränkt. Fragmente können auch jederzeit zu einer Anwendung hinzugefügt oder aus ihr entfernt werden. Auf diese Weise können Fragmente als Reaktion auf Interaktionen des Nutzers und clientseitiges Routing gerendert werden.
Mit Fragment-Piercing können Sie Mikro-Frontends schrittweise mit einem Fragment nach dem anderen einführen. Sie entscheiden über die Granularität der Fragmente und darüber, welche Teile der Anwendung in Fragmente umgewandelt werden sollen. Die Fragmente müssen nicht alle dasselbe Web-Framework verwenden, was beim Wechsel des Stacks oder bei der Integration mehrerer Anwendungen nach der Übernahme nützlich sein kann.
Die Demo der „Productivity Suite“
Zur Demonstration von Fragment-Piercing und inkrementeller Übernahme haben wir eine „Productivity Suite“-Demoanwendung entwickelt, Mit ihr können Nutzer unter anderem To-Do-Listen verwalten und Hacker-News lesen. Wir haben die Shell dieser Anwendung als clientseitig gerenderte React-Anwendung implementiert – eine übliche technische Wahl bei Unternehmensanwendungen. Dies ist unsere „veraltete Anwendung“. Es gibt drei Routen in der Anwendung, die für die Verwendung von Mikro-Frontend-Fragmenten aktualisiert worden sind:
/login
– ein einfaches Dummy-Anmeldeformular mit clientseitiger Validierung, das angezeigt wird, wenn Nutzer nicht authentifiziert sind (in Qwik implementiert)./todos
– verwaltet eine oder mehrere Todo-Listen, implementiert als zwei zusammenarbeitende Fragmente:/news
– ein Klon der HackerNews-Demo (implementiert in SolidJS).
Diese Demo zeigt, dass sowohl für die veraltete Anwendung als auch für jedes der Fragmente verschiedene unabhängige Technologien verwendet werden können.
Eine Visualisierung der Fragmente, die in die veraltete Anwendung gepierct werden
Die Anwendung wird bereitgestellt unter https://productivity-suite.web-experiments.workers.dev/.
Um sie auszuprobieren, müssen Sie sich zunächst anmelden – verwenden Sie einfach einen beliebigen Benutzernamen (kein Passwort erforderlich). Die Nutzerdaten werden in einem Cookie gespeichert, so dass Sie sich mit demselben Benutzernamen ab- und wieder anmelden können. Nach der Anmeldung können Sie mit Hilfe der Navigationsleiste oben in der Anwendung durch die verschiedenen Seiten navigieren. Werfen Sie insbesondere einen Blick auf die Seiten „Todo Lists“ und „News“, um das Piercing in Aktion zu sehen.
Sie können jederzeit versuchen, die Seite neu zu laden, um zu sehen, dass die Fragmente sofort gerendert werden, während die veraltete Anwendung langsam im Hintergrund lädt. Interagieren Sie mit den Fragmenten, noch bevor die veraltete Anwendung erschienen ist!
Ganz oben auf der Seite finden Sie Steuerelemente, mit denen Sie die Auswirkungen des Fragment Piercing in Aktion sehen können.
Verwenden Sie den Schieberegler „Legacy app bootstrap delay“, um die simulierte Verzögerung einzustellen, bevor die veraltete Anwendung startet.
Schalten Sie „Piercing Enabled“ ein, um zu sehen, wie die Nutzererfahrung ohne Fragmente in der App aussehen würde.
Schalten Sie „Show Seams“ ein, um zu sehen, wo sich die einzelnen Fragmente auf der aktuellen Seite befinden.
So funktioniert's
Die Anwendung setzt sich aus einer Reihe von Bausteinen zusammen.
Ein Überblick über die kooperierenden Workers und den veralteten Anwendungshost
Der Host der veralteten Anwendung in unserer Demo stellt die Dateien bereit, die die clientseitige React-Anwendung definieren (HTML, JavaScript und Stylesheets). Anwendungen, die mit anderen Tech-Stacks erstellt wurden, würden genauso gut funktionieren. Die Fragment Worker hosten die Mikro-Frontend-Fragmente, wie in unserem vorherigen Beitrag zur Fragment-Architektur beschrieben. Und der Gateway Worker bearbeitet die Anfragen des Browsers, indem er die Antwortströme der veralteten Anwendung und der Mikro-Frontend-Fragmente auswählt, abruft und kombiniert.
Sobald alle diese Teile implementiert sind, arbeiten sie zusammen, um jede Anfrage des Browsers zu bearbeiten. Schauen wir uns einmal an, was passiert, wenn Sie die Route `/login` aufrufen.
Der Anfragefluss bei der Anzeige der Anmeldeseite
Der Nutzer navigiert zu der Anwendung und der Browser stellt eine Anfrage an den Gateway Worker, um den ursprünglichen HTML-Code zu erhalten. Der Gateway Worker erkennt, dass der Browser die Anmeldeseite anfordert. Er stellt dann zwei parallele Unteranfragen – eine, um die index.html der veralteten Anwendung abzurufen, und eine weitere, um das serverseitig gerenderte Login-Fragment anzufordern. Anschließend kombiniert es diese beiden Antworten zu einem einzigen Antwortstrom, er enthält den HTML-Code, der an den Browser übermittelt wird.
Der Browser zeigt die HTML-Antwort an, die das leere Root-Element für die veraltete Anwendung und das serverseitig gerenderte Login-Fragment enthält, das für den Nutzer sofort interaktiv ist.
Der Browser fordert dann das JavaScript der veralteten Anwendung an. Diese Anfrage wird durch den Gateway Worker an den Host der veralteten Anwendung weitergeleitet. In ähnlicher Weise werden auch alle anderen Assets für die veraltete Anwendung oder Fragmente über den Gateway Worker an den Host der veralteten Anwendung oder den entsprechenden Fragment Worker geroutet.
Sobald das JavaScript der veralteten Anwendung heruntergeladen und ausgeführt und dabei die Shell der Anwendung gerendert wurde, setzt das Fragment Piercing ein und verschiebt das Fragment an die richtige Stelle in der veralteten Anwendung, wobei der gesamte Zustand der Benutzeroberfläche erhalten bleibt.
Wir haben uns bei der Erklärung des Fragment Piercing auf das Login-Fragment konzentriert, aber das gleiche Konzept gilt auch für die anderen Fragmente, die in den Routes /todos
und /news
implementiert sind.
Die Piercing-Bibliothek
Obwohl die Fragmente mit unterschiedlichen Web-Frameworks implementiert wurden, werden sie alle auf die gleiche Weise mit Hilfe einer „Piercing-Bibliothek“ in die veraltete Anwendung integriert. Diese Bibliothek ist eine Sammlung von serverseitigen und clientseitigen Utilities, die wir für die Demo entwickelt haben, um die Integration der veralteten Anwendung mit Mikro-Frontend-Fragmenten zu bewältigen. Die wichtigsten Features der Bibliothek sind die Klasse PiercingGateway
, die benutzerdefinierten Elemente Fragment Host und Fragment Outlet sowie die Klasse MessageBus
.
PiercingGateway
Die Klasse PiercingGateway
kann zur Instanziierung eines Gateway Workers verwendet werden, der alle Anfragen nach den HTML-, JavaScript- und anderen Assets unserer Anwendung bearbeitet. Das `PiercingGateway` routet Anfragen an die entsprechenden Fragment Worker oder an den Host der veralteten Anwendung. Außerdem kombiniert es die HTML-Antwortströme dieser Fragmente mit der Antwort der veralteten Anwendung zu einem einzigen HTML-Stream, der an den Browser zurückgegeben wird.
Die Implementierung eines Gateway Workers ist mit der Piercing-Bibliothek unkompliziert. Erstellen Sie eine neue gateway
-Instanz von PiercingGateway
und übergeben Sie ihr die URL des Hosts der veralteten Anwendung sowie eine Funktion, die feststellt, ob Piercing für die gegebene Anfrage aktiviert ist. Exportieren Sie das gateway
als Standardexport aus dem Worker-Skript, damit die Workers-Laufzeit seinen fetch()
-Handler verknüpfen kann.
const gateway = new PiercingGateway<Env>({
// Configure the origin URL for the legacy application.
getLegacyAppBaseUrl: (env) => env.APP_BASE_URL,
shouldPiercingBeEnabled: (request) => ...,
});
...
export default gateway;
Fragmente können durch den Aufruf der Methode registerFragment()
registriert werden, so dass das gateway
Anfragen nach dem HTML-Code und den Assets eines Fragments automatisch an den Fragment Worker routet. Die Registrierung des Login-Fragments würde zum Beispiel so aussehen:
gateway.registerFragment({
fragmentId: "login",
prePiercingStyles: "...",
shouldBeIncluded: async (request) => !(await isUserAuthenticated(request)),
});
Fragment Host und Outlet
Das Routing von Anfragen und die Kombination von HTML-Antworten im Gateway Worker ist nur eine Hälfte davon, was Piercing ermöglicht. Die andere Hälfte muss im Browser stattfinden, wo die Fragmente mithilfe der zuvor beschriebenen Technik in die veraltete Anwendung gepierct werden müssen.
Das Fragment Piercing im Browser wird durch ein Paar benutzerdefinierter Elemente erleichtert, den Fragment Host (<piercing-fragment-host>
) und das Fragment Outlet (<piercing-fragment-outlet>
).
Der Gateway Worker verpackt den HTML-Code für jedes Fragment in einen Fragment Host. Im Browser verwaltet der Fragment Host die Lebensdauer des Fragments und wird verwendet, wenn das DOM des Fragments in der veralteten Anwendung in Position gebracht wird.
<piercing-fragment-host fragment-id="login">
<login q:container...>...</login>
</piercing-fragment-host>
In der veralteten Anwendung markiert der Entwickler, wo ein Fragment beim Piercen erscheinen soll, indem er ein Fragment Outlet hinzufügt. Die Login-Route unserer Demo-Anwendung sieht wie folgt aus:
export function Login() {
…
return (
<div className="login-page" ref={ref}>
<piercing-fragment-outlet fragment-id="login" />
</div>
);
}
Wenn ein Fragment Outlet dem DOM hinzugefügt wird, sucht es im aktuellen Dokument nach dem zugehörigen Fragment Host. Wird er gefunden, werden der Fragment-Host und sein Inhalt in das Outlet verschoben. Wenn der Fragment Host nicht gefunden wird, stellt das Outlet eine Anfrage an den Gateway-Worker, um das Fragment HTML zu holen, das dann direkt in das Fragment Outlet gestreamt wird. Dabei wird die writable-dom-Bibliothek verwendet (eine kleine, aber leistungsstarke Bibliothek des MarkoJS-Teams).
Dieser Fallback-Mechanismus ermöglicht die clientseitige Navigation zu Routen, die neue Fragmente enthalten. Auf diese Weise können Fragmente im Browser sowohl über die anfängliche (harte) Navigation als auch über die clientseitige (weiche) Navigation gerendert werden.
Message Bus
Sofern die Fragmente in unserer Anwendung nicht vollständig präsentativ oder in sich geschlossen sind, müssen sie auch mit der veralteten Anwendung und anderen Fragmenten kommunizieren. Der [MessageBus](https://github.com/cloudflare/workers-web-experiments/blob/df50b60cfff7bc299cf70ecfe8f7826ec9313b84/productivity-suite/piercing-library/src/message-bus/message-bus.ts#L18)
ist ein einfacher asynchroner, isomorpher und rahmenunabhängiger Сommunication Bus, auf den die veraltete Anwendung und jedes der Fragmente zugreifen können.
In unserer Demoanwendung muss das Login-Fragment die veraltete Anwendung informieren, wenn sich der Nutzer authentifiziert hat. Dieser Nachrichtenversand ist in der Qwik LoginForm
Komponente wie folgt implementiert:
const dispatchLoginEvent = $(() => {
getBus(ref.value).dispatch("login", {
username: state.username,
password: state.password,
});
state.loading = true;
});
Die veraltete Anwendung kann dann wie folgt auf diese Nachrichten achten:
useEffect(() => {
return getBus().listen<LoginMessage>("login", async (user) => {
setUser(user);
await addUserDataIfMissing(user.username);
await saveCurrentUser(user.username);
getBus().dispatch("authentication", user);
navigate("/", { replace: true, });
});
}, []);
Wir haben uns für diese Message-Bus-Implementierung entschieden, weil wir eine rahmenunabhängige Lösung brauchten, die sowohl auf dem Server als auch auf dem Client gut funktioniert.
Probieren Sie es aus!
Mit Fragmenten, Fragment Piercing und Cloudflare Workers verbessern Sie sowohl die Performance als auch den Entwicklungszyklus veralteter clientseitig gerenderter Anwendungen. Sie können diese Änderungen schrittweise vornehmen und dabei Fragmente sogar mit einem Web-Framework Ihrer Wahl implementieren.
Die Anwendung "Productivity Suite", die diese Möglichkeiten demonstriert, finden Sie unter https://productivity-suite.web-experiments.workers.dev/.
Der gesamte von uns gezeigte Code ist Open-Source und auf Github veröffentlicht: https://github.com/cloudflare/workers-web-experiments/tree/main/productivity-suite.
Feel free to clone the repository. You can easily run it locally and even deploy your own version (free) to Cloudflare. We tried to make the code as reusable as possible. Most of the core logic is in the Piercing library , which you can try out in your own projects. We really welcome feedback and suggestions and would love to hear about what applications you would like to use it for. Join our conversation on GitHub or write to us in our Discord channel .
We are convinced that the combination of Cloudflare Workers with the latest ideas from frameworks enables the next big steps forward when it comes to improving the experiences of users and developers in web applications. So expect more demos, blog posts and collaborations as we continue to push the boundaries of the internet. And if you would like to accompany us on this journey, we would be happy to inform you that we are looking for new employees !