Beliebte Suchanfragen

Cloud Native

DevOps

IT-Security

Agile Methoden

Java

//

Migration von AngularJS zu Angular – eine Lernkurve

5.5.2022 | 7 Minuten Lesezeit

Ich möchte in diesem Blog-Artikel von der erfolgreichen Migration einer Anwendung von AngularJS zu Angular berichten.

Der Hintergrund

Als die erste Version von AngularJS vor fast zwölf Jahren das Licht der Welt erblickte, war es eines der großen Frontend-Frameworks auf dem Markt. Es war ein von Google entwickelter Meilenstein in der Frontend-Entwicklung mit Komponenten-basierter Architektur. Das Framework war jedoch noch nicht sehr ausgereift und hatte so seine Schwächen. So wurden vor allem die zugrunde liegende Architektur und die sog. digest loop (Mechanismus zum Erkennen und Verarbeiten von Änderungen) und damit einhergehende Geschwindigkeitseinbußen kritisiert. Google entschied sich daher, das Framework von Grund auf neu zu bauen und entwickelte Angular , dessen Version 2 im Jahr 2016 erschien und noch heute (aktuell mit Version 13) Bestand hat und weiterentwickelt wird.

Im Januar 2018 wurde bereits verkündet, dass der Support von AngularJS auslaufen würde. Es dauerte tatsächlich dann noch vier weitere Jahre, bis im Januar 2022 der Support endgültig eingestellt war.

Unser Projekt war in AngularJS gebaut und es hatte schon einige Anstrengungen gegeben, AngularJS abzulösen. Als dann die Beendigung jeglicher Weiterentwicklung angekündigt wurde, war dies endlich das ersehnte Argument um im Projekt die Migration hin zur aktuellsten Angular Version zu platzieren und auch Zeit dafür eingeräumt zu bekommen. Konkret war ein Sprung von Version 1.7.2 hin zu Version 11.x zu bewerkstelligen.

Somit begann Ende 2020 der Prozess, dessen Lernkurve ich im weiteren nun ein wenig erläutern möchte. Alle Erkenntnisse habe ich zudem auf GitHub in einem Beispiel gebündelt.

Der große Knall oder Schritt für Schritt?

Selbstverständlich stand ganz zu Anfang die Frage: Bauen wir die Anwendung komplett neu oder machen wir einen Hybridbetrieb, in dem wir den alten Code sukzessive umschreiben. Die Anzahl der Elemente und somit die Größe des Projekts war damals:

411 Modules mit
131 Controllers
25 Routes
2 Directives
399 Components
161 Services
31 Filters

Damit war klar: Das können wir wirtschaftlich nicht anders lösen als einen Hybridbetrieb zu starten. Die Strategie sah so aus, dass wir mit jeder neuen Anforderung die betreffende Seite nicht nur erweitert, sondern immer gleich auch migriert haben. Somit war gewährleistet, dass wir im Projekt neben der rein technischen Ablösung auch fachlichen Mehrwert schaffen.

Varianten eines Hybridbetrieb

Bei einem Hybridbetrieb lässt sich vor allem auch die Variante denken, zwei Anwendungen parallel auf verschiedenen Ports laufen zu lassen. Hier stellt sich jedoch zunächst die Frage, wie man das Routing lösen kann, ohne bei jedem Anwendungswechsel während des Navigierens die Anwendung neu bootstrappen zu müssen. Außerdem besteht auch das Problem, des globalen Anwendungzustands. Wie kann dieser in beiden Anwendungen erreicht werden?

Die zweite, und für uns bessere Variante war aber die von Angular selbst vorgeschlagene. Die Angular-Entwickler hatten schon in Version 2 einen Mechanismus entwickelt, um bei der Migration von AngularJS zu Angular zu unterstützen – das sog. Upgrade-Modul. Mithilfe dieses Moduls ist es möglich, Komponenten und Services wechselseitig in beiden Versionen zu verwenden.

Dateien und Verzeichnisstruktur

Die Basis für das Angular-Projekt haben wir mithilfe des Angular-CLI Tools erstellt. Danach mussten wir uns grundsätzlich überlegen, wie wir die Verzeichnisse schneiden und wo welche Dateien abgelegt werden. Wir entschieden uns dazu, neben dem generierten src/app-Verzeichnis ein src/app-ajs-Verzeichnis anzulegen, das alle alten AngularJS-Dateien enthält. Dieses Verzeichnis enthält also fast das gesamte alte src-Verzeichnis der AngularJS-Anwendung. Glücklicherweise hatten wir bereits zuvor den gesamten AngularJS-Code in TypeScript umgeschrieben, so dass hier keine weiteren Arbeiten mehr nötig waren.

Das gemeinsame Bootstrapping

Die erste Hürde, die genommen werden muss, ist der gemeinsame Start beider Teilanwendungen beim Laden im Browser. Hier kommt nun das o. g. Upgrade-Modul zum Tragen, welches das Bootstrapping beider Anwendungen erledigt und gleich auch das Routing zwischen den Anwendungsteilen gewährleistet.

1import {ApplicationRef, DoBootstrap, NgModule} from '@angular/core';
2import {setUpLocationSync} from '@angular/router/upgrade';
3import {setAngularJSGlobal, UpgradeModule} from '@angular/upgrade/static';
4import * as angular from 'angular';
5import {app, prepare} from '../app-ajs/app';
6...
7 
8setAngularJSGlobal(angular);
9 
10@NgModule({
11imports: [
UpgradeModule,
 ...
],
12declarations: [...],
13exports: [],

14    providers: [...]

15})
16export class AppModule implements DoBootstrap {


17 
18    constructor(private upgrade: UpgradeModule) {

19    }
20 
21    

ngDoBootstrap = (appRef: ApplicationRef): void => {

22        prepare().then((): void => {

23            this.upgrade.bootstrap(document.body, [app.name], {strictDi: false});

24            appRef.bootstrap(AppComponent);

25            setUpLocationSync(this.upgrade);

26       
});
27    
};
28
}

In der app.module.ts werden beide Anwendungsteile gestartet. Der Aufruf von this.upgrade.bootstrap(...) startetet den AngularJS-Teil, appRef.bootstrap(...) den Angular-Teil. Die Datei ../app-ajs/app.ts enthält alle Befehle zum Konfigurieren von AngularJS (angular.module(...), app.config(...), app.run(...))

AngularJS Services in Angular verwenden

AngularJS-Komponenten können im Angular-Teil verwendet werden. Hierzu müssen die Services als sog. Upgraded Providers registriert werden. Ein Beispiel:

1export const FooServiceProvider = {
2    provide: 'FooService',
3    useFactory: ($injector: Injector): any => $injector.get('FooService'),
4    deps: ['$injector']
5};

Dieser Provider muss in der app.module.ts noch in dem providers-Abschnitt aufgenommen werden. Wichtig ist hierbei, dass der Service auch mit dem Namen FooService in dem AngularJS-Modul als Service registriert ist. Danach kann dieser AngularJS-Service mit @Inject('FooService') in allen Angular-Klassen injected werden.

Angular Services in AngularJS verwenden

Der umgekehrte Fall ist auch möglich. Um neue Angular Services in AngularJS Teil verwenden zu können, werden die Services mithilfe von Funktionen des Upgrade-Moduls im AngularJS-Modul registriert.

1import {downgradeInjectable} from '@angular/upgrade/static';
2import {BarService} from '../app/bar.service';
3import * as angular from 'angular';
4 
5angular.module('foo').service('BarService', downgradeInjectable(BarService));

Somit lässt sich dann der Angular Service BarService mit @Inject('BarService') in den AngularJS-Klassen injecten.

AngularJS-Komponenten in Angular verwenden

Genau wie Services können auch Komponenten (und Direktiven) auch beidseitig verwendet werden. Um AngularJS-Komponenten im Angular-Teil verwenden zu können, muss man diese Komponenten als Direktiven in Angular als sog. UpgradeComponent registrieren.

1import {Directive, ElementRef, Injector, Input} from '@angular/core';
2import {UpgradeComponent} from '@angular/upgrade/static';
3 
4@Directive({
5    selector: 'app-upgraded-foo'
6})
7export class FooDirective extends UpgradeComponent {
8    @Input() name: string;
9    @Input() onUpdate: () => (name: string) => void;
10 
11    constructor(elementRef: ElementRef, injector: Injector) {
12        super('foo', elementRef, injector);
13    }
14}

Diese Direktive FooDirective muss danach noch in der app.module.ts in der declarations– und export-Sektion aufgenommen werden. Die AngularJS-Komponente foo kann dann unter dem Namen im HTML des Angular-Teils verwendet werden. Anzumerken ist hier, dass AngularJS kein @Output-ähnliches Konstrukt kennt. Daher wird das bei dem Upgrade über @Input reingereichte Funktionsreferenzen gelöst.

Angular-Komponenten in AngularJS verwenden

Ebenso können auch Angular-Komponenten im AngularJS-Teil verwendet werden. Hierfür wird die Komponente als Direktive im AnguarJS-Modul registriert.

1import {downgradeComponent} from '@angular/upgrade/static';
2import {BarComponent} from '../app/bar.component'
3import * as angular from 'angular';
4 
5angular.module('foo').directive('appBar', downgradeComponent({component: BarComponent}));

Nun kann man die Angular-Komponente BarComponent mit im HTML des AngularJS-Teils verwenden. @Input– und @Output-Attribute der Komponente werden in AngularJS HTML so verwendet wie in Angular.

CSS

Stylesheets in CSS (oder auch SCSS) lassen sich im Hybridbetrieb auch im AngularJS-Teil nicht mehr über einen Import in der Komponente realisieren. Bisher konnte man Stylesheets so importieren.

import './foo.scss';

Das geht nun nicht mehr. Alle Stylesheets aus dem AngularJS-Teil müssen nun in der zentralen styles.scss von Angular gelistet werden.

URL Routing

Das Routing der URLs in ein weiterer Punkt. Die Anwendung muss ja irgendwo entscheiden, welche Komponente nun für die aufgerufene URL zuständig ist. Der Angular-Router kann allerdings nur Angular-Komponenten ansteuern, der AngularJS-Router nur AngularJS-Komponenten. Dies haben wir so gelöst, dass es in der app.component.html ein *ngIf gibt, das den AngularJS-Router bei Bedarf hinzunimmt. Dies ist immer dann der Fall, wenn die URL keine ist, die im Angular-Router konfiguriert ist. Der Block bleibt in dem Fall dann also leer. Die Funktion isAjsRoutePath() ermittelt, ob die URL eine AngularJS-Route ist. Hierzu wird die Angular-Router Konfiguration durchsucht. Findet sich die URL dort, ist es eine Angular-Route. Ansonsten eben nicht.

</router-outlet></router-outlet>
</app-upgraded-router *ngIf="isAjsRoutePath()"></app-upgraded-router>

Zusätzlich muss auch der Angular-Router noch angepasst werden. Es muss eine UrlHandlingStrategy bereitgestellt werden, die entscheidet, ob der Angular-Router sich um die URL kümmert, oder ob eine andere Komponente dies tut – bei uns der AngularJS-Router.

HTML und Build

Die HTML-Templates der AngularJS-Komponenten wird über ein Import in der Komponente realisiert.

import template from './foo.component.html';

Damit dieses HTML-Template im Build auch gelesen werden kann, muss ein html-loader als Abhängigkeit ergänzt werden. Das alleine reicht leider noch nicht. Der Loader muss auch noch verwendet werden. Dies passiert über eine custom-webpack-Konfiguration (@angular-builders/custom-webpack ), die ebenfalls noch hinzugenommen und konfiguriert werden muss.

angular.json (Ausschnitt)

"build": {
    "builder": "@angular-builders/custom-webpack:browser",
        "options": {
            "customWebpackConfig": {
                "path": "./custom-webpack.config.js"
            },
custom-webpack.config.js (Ausschnitt)

rules: [
            {
                test: /\.html$/,
                loader: 'html-loader'
            }
       ]

Probleme

Ein Problem, auf das wir während der laufenden Migration gestoßen sind, war die ChangeDetection von Angular . Wir haben festgestellt, dass die ChangeDetection mit integrierter AngularJS-Anwendung um ein vielfaches häufiger läuft als üblich – viele Male pro Sekunde. Leider war nicht zu ergründen, wo das genau Problem lag. Somit mussten wir mit dem Umstand umgehen und dafür sorgen, dass die Anwendung trotz häufiger ChangeDetection noch bedienbar bleibt. Dazu haben wir viele Komponenten die z.B. in Listen mehrfach im DOM zu finden waren mit changeDetection: ChangeDetectionStrategy.OnPush konfiguriert, so dass für diese Komponenten das genannte Problem keine Rolle mehr spielte.

Beispiel-Code in GitHub

Da die gesamte Realisierung doch recht komplex und anhand der genannten Beispiele nicht vollständig nachzuvollziehen ist, habe ich unser Projekt auf eine Beispiel-Implementierung reduziert und auf GitHub zum Nachlesen veröffentlicht.

Fazit

Ja, die Migration ist schrittweise möglich.

Mithilfe der oben beschriebenen Anpassungen lässt sich auch ein großes AngularJS-Projekt ohne Ausfallzeit komplett zu Angular migrieren.

P.S. Nach über einem Jahr und vier Monaten Hybridbetrieb und fortlaufender Migration der Anwendung haben wir diese nun erfolgreich abgeschlossen und konnten AngularJS komplett aus dem Projekt entfernen.


Beitrag teilen

Gefällt mir

4

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.