Jenga mit Code: Migration einer komplexen App aus AngularJS zu React

by Juan Cabello
10 Minute Read

Die Aufgabe, einen einzelnen Spielstein aus dem Turm zu ziehen und ihn oben draufzulegen und die mit jedem Zug zunehmende Intensität: Jeder, der schon einmal Jenga gespielt hat, versteht dieses Gefühl. Stellen Sie sich vor, Sie spielen Jenga mit einem riesigen Turm vor einem Live-Publikum, in dem jeder falsche Zug eine totale Katastrophe bedeutet. Die Erfahrung meines Teams mit der Migration einer Anwendung aus AngularJS zu React hat sich wie eine ähnliche Herausforderung angefühlt.

Stellen Sie sich eine Front-End-Anwendung als Jenga-Turm und jeden Stein als mit allen anderen verbundene Komponente vor. Wird ein Stein entfernt, bedeutet das das Beheben eines Programmfehlers im Legacy-Code, während das Ersetzen eines Steins die Programmierung einer neuen Funktion sein könnte. App-Benutzer sollten keine Änderung bemerken, außer einem möglicherweise aktualisierten Design, und die App sollte mindestens genauso gut wie zuvor funktionieren. Da beide Frameworks JavaScript-Frameworks sind, sollte dies keine große Herausforderung sein, oder?

Diese Migration hat sich ehrlich gesagt als recht schwierig erwiesen. Die Planung der Migration, die Entwicklung neuer Funktionen auf unserer Produkt-Roadmap und der Abbau von Technologie-Schulden sind nicht einfach, und wir mussten uns mit all diesen Herausforderungen gleichzeitig auseinandersetzen.

Trotz dieser Hürden beschlossen wir, dass es an der Zeit war, von AngularJS zu React zu wechseln.

Gründe für die Migration

AngularJS markierte einen Neuanfang bei der Frontend-Entwicklung und es zeigte sich, dass Frontend-Entwicklung mehr umfasst als nur die Arbeit an der Benutzeroberfläche. Die Abstraktionsstufen aus AngularJS waren aber für die meisten Frontend-Ingenieure immer noch zu kompliziert. Man konnte nicht einfach eine jQuery-Funktion schreiben, um die Sichtbarkeit von Elementen zu ändern. Jetzt waren richtige Engineering-Arbeiten erforderlich, von der Aktualisierung des DOM basierend auf dem Zustand bis zur Verwaltung von Abhängigkeiten. (Das waren schließlich damals noch die Zeiten vor Module-Bundlern.)

Seit dem Release von AngularJS hat die Beliebtheit vieler UI-Frameworks und Bibliotheken zugenommen. Die Schwierigkeiten von AngularJS wurden schließlich so umfassend, dass Frontend-Entwickler sich für neuere Frameworks entschieden. Dazu kam, dass die letzte Version von AngularJS mit einem LTS von drei Jahren 1.7 war, deren Release im Juli 2018 stattfand. Seriöse Produkte können ohne langfristige Unterstützung nicht auf Frameworks entwickelt werden, daher war dies ein Signal, dass wir bald die Frameworks ändern mussten.

Wir wählten React vor allem wegen seiner Annahme bei Frontend-Ingenieuren und wie einfach es ist, welche einzustellen, die React-Erfahrung haben. Außerdem ist die Leistung besser als bei Angular. Das React-Ökosystem ist riesig und war bereits größer als das von Angular, als wir uns für den Wechsel entschieden.

Nach der Migrationsentscheidung mussten wir ermitteln, wo wir anfangen sollten. Wir suchten nach Geschichten von anderen Unternehmen, die migriert haben, und überraschenderweise gab es dazu nur wenig Informationen. Deshalb bahnten wir uns unseren eigenen Weg.

Frontend-Engineering umfasst das Anzeigen von Datenbits, die auf freundliche Weise für Benutzer optimiert sind, während gleichzeitig ihre Probleme behoben werden. Egal, ob es um die Interaktion mit einem anderen Dienst oder die Präsentation von Informationen über ein Produkt geht, der Hauptzweck eines Frontends besteht darin, dass Benutzer mit den angezeigten Datenbits interagieren. Die Problemlösungsfähigkeit eines Produkts kann mit drei Faktoren gemessen werden: (1) die Zeit, die ein Benutzer für die Durchführung einer bestimmten Aufgabe aufwendet; (2) die Verfügbarkeit des Dienstes und (3) die Einheitlichkeit der Daten. UX (Benutzererfahrung) kombiniert die Effektivität eines Produkts mit dem Gefühl des Benutzers bei der Interaktion damit.

Verschiedene Architekturen

Falls es in Ihrem AngularJS-Code Technologie-Schulden gibt, die abgebaut werden müssen, sollten Sie sie nicht in Ihren React-Codebase übertragen. Es ist auch nicht zu empfehlen, nicht behobene Programmfehler zu portieren. Um neu anzufangen, müssen Sie mit den Grundlagen beginnen – der Architektur.

Um mit der Entwicklung der richtigen Architektur beginnen zu können, mussten wir uns mit den Unterschieden zwischen der Entwicklung einer App in AngularJS im Vergleich zu React vertraut machen. Das erste große Problem, das wir beheben konnten, war die Tatsache, dass AngularJS Vendor-Lock-In hatte, React hingegen nicht. (Die React-Tagline lautet: „Eine JavaScript-Bibliothek für die Erstellung von Benutzeroberflächen“). Bei Angular hingegen muss man den Angular-Methoden folgen und sein Tooling für API-Aufrufe, Zustand und mehr verwenden. Man konnte sich entscheiden, das nicht zu tun, aber dadurch entstanden Probleme wie beim Aktualisieren der Benutzeroberfläche, wenn die empfangenen nicht im selben Aufruf-Stack waren (z. B. Callbacks, Promises usw.). Bei Angular muss man auch viel Fachjargon erlernen, wie Controller, Direktiven, Module, Services usw. Um all dies effektiv zu verwalten, empfiehlt AngularJS die Weitergabe von Managementvorgängen an den Controller. Der Zustand wird von Services abgerufen. Controller sind der Leim, der den Zustand aus einem Service abruft, den Zustand verwaltet und die Ansicht aktualisiert. Die Ansicht in AngularJS besteht aus Vorlagen, die den aktuellen Zustand anzeigen, wie er von den Controllern gemeldet wird.

In React ist das wichtigste Konzept, mit dem man sich vertraut machen muss, der Datenfluss. Es reicht aus, sich eine React-Anwendung als Datenbaum vorzustellen, bei dem jeder Knoten eine Komponente (oder ein Jenga-Baustein) ist. Jede Komponente verfügt über einen Zustand, der von seiner jeweiligen Komponente verwaltet wird, und erhält Eigenschaften, die sog. Props, die von einer übergeordneten Komponente verwaltet werden. Dieses System reguliert, wie Daten in der Anwendung fließen. Die Props stellen die Kanten zwischen Knoten dar, während der Zustand Daten enthält und von den Knoten selbst repräsentiert wird. Der Zustand kann im Baum über Props weitergegeben oder für die jeweilige Komponente verwendet werden. Alternativ kann man „Kontext“ erstellen, der gemeinsam genutzte Daten über Knoten erstellt, die für diesen Kontext abonniert werden.

Wenn Sie den Architekturstil von Angular beibehalten möchten, können Sie in React analoge Designmuster verwenden. Sie können beispielsweise verschiedene Stateless-Komponenten erstellen, die den Angular-Vorlagen recht ähnlich sind, aber lediglich Daten anzeigen und Ereignisse bearbeiten. Um Angular-Controller zu ersetzen, können Sie eine Stateful-Komponente verwenden, die die entweder von einem Server, einem Kontext, dem lokalen Zustand oder Props von darüberliegenden Komponenten empfangenen Daten verwaltet und diese mehreren Stateless-Komponenten liefert.

Wenn Sie dem React-Konzept folgen möchten, können Sie Ihre Anwendung mit einer Zustands-Container-Bibliothek wie z. B. Redux erweitern.

Migration in Ebenen

Wir wissen jetzt, wie die Unterschiede zwischen jedem Framework funktionieren. Gibt es aber Gemeinsamkeiten, die wir nutzen können, um unsere Migration einfacher zu machen? In modernen Web-Anwendungen gibt es oft drei Ebenen, die alle in der gleichen gebündelten Datei verpackt sind: die Zustandsebene, die in erster Linie die Kommunikation mit dem Server übernimmt und als Zwischenspeicher-Ebene fungiert; die Controller-Ebene, die Zustandsübergänge und die Kommunikation mit der Zustandsebene verwaltet, und die Ansichtsebene, die den aktuellen Zustand der Anwendung anzeigt.

Um eine Migration zu planen, müssen Sie alle drei Ebenen der Anwendung berücksichtigen. Dies ist besonders bei der Migration einer Anwendung wichtig, die nicht effizient entwickelt wurde. Dies ist ein guter Punkt, um damit anzufangen, richtig vorzugehen und Probleme zu korrigieren. Wir berücksichtigten drei verschiedene Ansätze bei der Migration einer AngularJS-Anwendung:

  1. Zuerst die Zustandsebene, dann die Controller- und Ansichtsebenen migrieren
  2. Zuerst die Ansichtsebene migrieren, dann die Zustands- und Controller-Ebenen
  3. Alle drei Ebenen gleichzeitig in kontinuierlichen Schritten migrieren

Wir wollen jeden der oben genannten Ansätze untersuchen.

Zuerst die Zustandsebene, dann die Controller- und Ansichtsebenen migrieren

Dieser Ansatz konzentriert sich zuerst auf das Ersetzen von AngularJS-Services. Zu diesem Zweck ist ein Zustands-Container wie Redux, Flux oder MobX eine gute Möglichkeit, um alle Datenvorgänge zu zentralisieren und Ihre Zustandsebene von jeder anderen Ebene zu entkoppeln. Stellen Sie sicher, dass Sie den auswählen, der Ihren Bedürfnissen am besten entspricht. Es gibt viele Ressourcen mit Beschreibungen jedes Zustands-Containers. Durch die Zentralisierung aller Store-Vorgänge und deren Entfernen aus AngularJS erreichen wir also die völlige Freiheit der Benutzeroberfläche. Sie vermeiden, an eine UI-Bibliothek gebunden zu sein, sodass es recht einfach ist, sie je nach Bedarf zu ersetzen. In diesem Fall können wir AngularJS-Controller, Direktiven und Vorlagen einfach durch React-Komponenten ersetzen, bis wir schließlich AngularJS ganz aus unserer Codebase entfernen.

Zuerst die Ansichtsebene migrieren, dann die Zustands- und Controller-Ebenen

Dieser Ansatz konzentriert sich zunächst auf AngularJS-Vorlagen. Weil AngularJS-Vorlagen überwiegend HTML und CSS verwenden, ist es recht einfach, sie in eine React-Komponente aufzunehmen, indem entsprechende Bindungen verwendet werden, um Ihre React-Komponente mit Ihren Angular-Controllern zu verbinden. Bibliotheken wie react2angular oder ngReact können React-Komponenten direkt an Ihre Angular-Vorlagen binden. Es könnten gewisse Performance-Probleme auftreten, aber insgesamt ist die Performance davon nicht signifikant betroffen. Hinsichtlich einer entkoppelten Architektur ist es immer noch möglich, sie auf diese Weise zu erzielen, weil lediglich die Migrationsreihenfolge des ersten Ansatzes umgekehrt wird. Die Controller halten alles zusammen, sie müssen also in beiden Fällen bis zum späteren Schritt beibehalten werden. Dieser spätere Schritt ist der gleiche wie der erste Schritt des ersten Ansatzes, den wir vorgestellt haben.

Alle drei Ebenen gleichzeitig in kontinuierlichen Schritten migrieren

Der dritte und letzte Ansatz, den wir in Betracht ziehen, konzentriert sich auf Teile der Anwendung und zielt darauf ab, sie vertikal zu migrieren. Sie können sich das wie folgt vorstellen: Seiten, die unabhängig gerendert werden, werden auch unabhängig voneinander migriert. Es ist verlockend, diesen Ansatz zu verwenden, es ist aber auch sehr einfach, Ihr System so zu entwerfen, dass ein kontinuierlicher Umbruch stattfindet. So lange die Migration stattfindet kann die Performance betroffen sein. Wird eine gerenderte Ansicht migriert, gibt es versteckte Zustandsanwendungen, die nicht berücksichtigt werden könnten, und am Ende ist die Aufrechterhaltung des Zustands in zwei verschiedenen Teilen Ihrer Anwendung erforderlich.

Wofür wir uns entschieden haben

Wir setzten bei unserer Migration alle drei Ansätze ein. Unsere ersten Schritte waren die Migration sehr kleiner Komponenten nach dem zweiten Ansatz, d. h. wir verwendeten weiterhin den Controller und die Zustandsebene von Angular, implementierten die UI aber in React. Wir folgerten, dass wir auf diese Weise den Abbau der Technologie-Schulden verzögerten, was schlussendlich kontraproduktiv sein könnte. Die Gelegenheit, die Möglichkeit zu nutzen, um die Dinge richtig zu erledigen, wurde missbraucht. Wir lernten aber daraus. Wir hatten praktische Erfahrung mit Bibliotheken, die später in den anderen von uns ergriffenen Ansätzen wiederverwendet werden würde.

Wir bewerteten neu und entschieden uns für die dritte Option. Wir migrierten unsere UI auf Basis der einzelnen gerenderten Ansichten. Wir verbrachten durchschnittlich einen Monat pro Gesamtansicht, einschließlich des Neuprogrammierens aller Interaktionen, der Migration des Zustands nach Redux und so weiter. Wir machten Fortschritte und konnten eine App ohne zu viel Aufwand zur anderen migrieren. Wir erkannten aber, dass dieser Ansatz zu lange dauerte. Unsere Lieferzeit wurde beeinträchtigt und das Risiko von fehlerhafter Codequalität aufgrund des Zeitdrucks nahm zu. Daher stoppten wir nach der Migration von zwei ganzen Seiten und bewerteten die Situation erneut.

Wir wollten schnellere Lieferzeiten und die Fähigkeit, unsere Schritte so gestalten, dass unsere technischen Ressourcen auf verschiedene Sprints aufgeteilt werden konnten, ohne dabei an Dynamik zu verlieren. Wir wollten diese Chance auch nutzen, um die Arbeit richtig zu erledigen und die Technologie-Schulden dabei abzubauen, oder zumindest keine in unsere neuere Codebasis zu übertragen.

Generell waren wir mit dem ersten Migrationszustand zufriedener. Wir glauben, dass er immer wichtiger werden wird, wenn wir mit der Migration unserer wichtigsten und in puncto Daten umfangreichsten Komponente beginnen. Nicht vergessen: Beim Frontend geht es darum, Datenbits anzuzeigen. Indem sichergestellt wird, dass die Daten nicht Framework-abhängig sind, können Sie sich als Nächstes ganz auf die Optimierung der Ansicht für Ihre Benutzer konzentrieren, um das Problem auf möglichst effektive Weise zu beheben.

Schlussfolgerung

Wir haben einige Lektionen gelernt, die für alle hilfreich sein können, die eine Anwendung aus AngularJS nach React migrieren, eigentlich für alle, die irgendeine Art von Frontend-Migration ausführen. Hier einige der wichtigsten Erkenntnisse:

  • Gute Architektur hilft beim Abbau von Technologie-Schulden

Wenn Sie Ihre Anwendung nicht richtig entwerfen, können leicht auch die Technologie-Schulden migriert werden. Obwohl bereits viel Zeit investiert wird, empfehlen wir, die Technologie-Schulden bei der Migration abzubauen, anstatt sie zu übernehmen, da die zukünftigen Kosten der Behebung dieser Schulden noch höher sind. Beginnen Sie zunächst damit, sich mit der grundlegenden Architektur einer React- und einer AngularJS-Anwendung vertraut zu machen und überlegen Sie sich dann, welchen Ansatz Sie verfolgen werden.

  • Betrachten Sie die Migration als Feature

Mit der Migration einer Anwendung haben Sie die Gelegenheit, das zu korrigieren, was falsch war. Sie sollten diese Gelegenheit nicht verpassen und Ihrem Team helfen, die Produktentwicklung entsprechend zu planen. Es kann mehr Zeit in Anspruch nehmen, aber bei einer guten Refaktorisierung macht sich die Zeitinvestition bezahlt, wenn Ihr Produkt wächst.