Hinweis:
Dieser Blogartikel ist älter als 5 Jahre – die genannten Inhalte sind eventuell überholt.
Dies ist ein sehr alter Beitrag, AngularJS ist seit dem 1. Juli 2018 im Long Term Support (LTS). Diese ursprünglich drei jährige Phase wurde aufgrund von COVID-19 um weitere sechs Monate erweitert auf den 31.12.2021. Ihr solltet eure Webanwendungen deshalb auf mindestens Version 1.8.x anheben, um von den Patches zu profitieren. Generell solltet ihr spätestens jetzt an eine Migration oder eine Ablösung eurer AngularJS-WebApps denken.
AngularJS 1 ist ein gutes, stabiles Framework, um Web-Anwendungen zu erstellen. Durch die Entscheidung der Angular(JS)-Entwickler, die Nachfolgeversion des Frameworks auf komplett neue Füße zu stellen, sind viele Entwickler unsicher, ob sie ihre bestehende, gut funktionierende AngularJS-Anwendung updaten sollen oder nicht. Das Elster-Naturell vieler Entwickler trägt wahrscheinlich seinen Teil dazu bei. Zur Beruhigung dieser Leser kann gesagt werden: AngularJS 1 wird nicht morgen als deprecated gekennzeichnet werden; durch die große Verbreitung von AngularJS 1 wird dieses Framework noch eine ganze Weile eine wichtige Rolle in der Entwicklung von Web-Anwendungen spielen. Ein Upgrade kann dennoch sinnvoll sein, da Angular 2 viele sinnvolle neue Konzepte und Features einführt, von denen die Entwicklung einer wartbaren, sauber strukturierten Anwendung nur profitieren kann.
So kann Angular 2 etwa mit einer guten Unterstützung von Typescript punkten, die bei AngularJS 1 eher aufgesetzt und unpraktisch wirkte. Bei Angular 2 ist Typescript dagegen die bevorzugte Variante der Implementierung, was die Entwicklung von stabileren Anwendungen durch Typsicherheit und sinnvoller Modularisierung verspricht. Das durch Web Components und React bereits bekannte Konzept einer hierarchisch verschachtelten komponentenbasierten Entwicklung von Web Apps wird in Angular 2 ebenfalls umgesetzt – statt flacher Controller-Hierarchien mit angebundenen Templates können Komponenten verschachtelt definiert werden, was die Wiederverwendbarkeit und die strukturelle Ordnung erhöht.
Dieser Beitrag beschreibt den Versuch, eine bestehende AngularJS-Anwendung Schritt für Schritt auf Angular 2 zu upgraden. Dabei werden besonders die verschiedenen Fallstricke erklärt, die auf dem Weg dahin auftreten können. Da es in großen, produktiv genutzten Anwendungen oft der Fall ist, dass ein „Big Bang“ Upgrade, bei dem alle Komponenten der Anwendung auf einmal umgestellt werden, wegen paralleler Feature-Entwicklung nicht möglich ist, wurde ein inkrementeller Ansatz gewählt, bei dem AngularJS- und Angular-2-Komponenten innerhalb einer Web App miteinander interagieren.
Das Ziel ist es, die alte, auf ECMAScript 5 basierende AngularJS 1 App Schritt für Schritt auf eine mit Typescript entwickelte Angular 2 App zu upgraden. Dabei muss einmalig die Interoperabilität zwischen den beiden Systemen hergestellt werden – dann können neue Features als Angular 2 Components implementiert werden, während alte Direktiven und Controller der AngularJS-1-Anwendung Schritt für Schritt nachgezogen werden.
Sind die wichtigsten Teile der AngularJS-1-Anwendung umgezogen, kann auf eine Angular-2-Anwendung gewechselt werden, die die noch nicht umgezogenen Services, Controller und Direktiven in einem Kompatibilitätsmodus ausführen kann. Anschließend werden auch diese letzten Teile noch in Angular 2 überführt. Abschließend kann die Abhängigkeit zu AngularJS 1 komplett entfernt werden und die Migration ist vollständig.
Dependency Management
Während AngularJS 1 und davon abhängende Bibliotheken per Bower installiert werden, muss für Angular 2 npm herangezogen werden. Mit angular2-build gibt es zwar ein Bower Repository für diesen Zweck, mit diesem werden jedoch keine Typing Files ausgeliefert, die später für den Typescript-Kompiliervorgang benötigt werden.
Angular 2 ist eigentlich nicht über Bower verfügbar, weil die mit Bower ausgelieferten Bibliotheken in ECMAScript 5 implementiert sind (gewöhnliches JavaScript), das direkt im Browser ausgeführt werden kann – diese Anforderung besteht für npm nicht.
Da Angular 2 stark auf moderne Features von JavaScript setzt, die erst in ECMAScript 6 und ECMAScript 7 eingeführt wurden und die viele Browser noch nicht unterstützen, werden zusätzlich zu Angular 2 noch die Polyfills es6-shim
und reflect-metadata
benötigt, die Features wie Promises und Annotations nachrüsten.
Da Angular 2 modular aufgebaut ist, wird noch ein Module Loader benötigt, um die einzelnen Teile einer Anwendung zusammenzuführen und zur Ausführung zu bringen. Angular nutzt von Haus aus hierfür den universellen Module Loader System.js, der mit beliebigen Modul-Definitionen funktioniert.
Typescript-Kompilierung
Die Kompilierung von Angular-2-Komponenten funktioniert relativ problemlos mit dem Standard Typescript Compiler, in der Konfigurationsdatei muss nur SystemJS als Module-Loader erwähnt werden:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ target: 'ES5', module: 'system', moduleResolution: 'node', experimentalDecorators: true, outFile: 'tsc.js' } |
Damit korrekt gegen verwendete Angular-Imports kompiliert werden kann, muss Typescript die Typing-Dateien für Angular finden. Diese werden zusammen mit der angular2-Abhängigkeit über NPM ausgeliefert.
Bootstrapping der Hybridanwendung
Normalerweise werden AngularJS-1-Anwendungen über die ng-app
-Direktive gebootstrapt, die einem Container-Element in der ursprünglichen index.html
als Attribut mitgegeben werden muss. Damit AngularJS 1 und Angular 2 koexistieren können, muss die bestehende AngularJS-1-Anwendung nun aus Angular 2 heraus gestartet werden. Dafür wird der Upgrade-Adapter eingesetzt:
1 2 3 |
var upgradeAdapter = new UpgradeAdapter(); upgradeAdapter.bootstrap(document.body, ['myApp'], {strictDi: false}); |
Das obige Code-Snippet startet die AngularJS-1-Anwendung myApp
im Kompatibilitäts-Modus mit eventuellen Angular-2-Komponenten. Um geupgradete Komponenten wieder in die AngularJS-1-Anwendung einzufügen, müssen diese gedowngradet werden:
1 2 3 4 5 6 7 8 9 |
import {MyDirective} from './components/MyDirective'; declare var angular:any; //load angular 2 components angular.module('myApp').directive('myDirective', upgradeAdapter.downgradeNg2Component(myDirective)); |
Die Direktive MyDirective
kann man jetzt wie gehabt in die AngularJS-1-Anwendung einbinden. Es muss nur darauf geachtet werden, dass sich die Syntax für die Übergabe von Parametern geändert hat – die Attributnamen müssen in eckige Klammern gesetzt werden:
1 |
<my-directive [param]=”valueFromScope”></my-directive> |
Achtung! Diese Methode funktioniert nur für Elementdirektiven (restrict: “E“).
Problematisch wird diese Vorgehensweise, wenn die Direktive, die in die Angular-2-Welt übertragen wurde, irgendwelche Abhängigkeiten zu Komponenten hat, die noch nicht von AngularJS 1 geupgradet wurden. Dazu zählen zum Beispiel Provider, die per Dependency Injection in der Direktive geladen wurden oder im Template der Direktive verwendete andere Direktiven oder Filter.
Um die Direktive wieder lauffähig zu machen, müssen diese Abhängigkeiten in Angular 2 verfügbar gemacht werden – dies kann man teilweise mit dem UpgradeAdapter erledigen.
- Verfügbarmachen von Providern: Provider können mit einem einfachen Befehl für Angular-2-Komponenten sichtbar gemacht werden: upgradeNg1Provider(‘myProvider’).
- Verfügbarmachen von Direktiven: Verwendet die eigene Direktive andere Direktiven, z.B. aus Bibliotheken, kann das leicht zu Problemen führen. So lassen sich viele Direktiven der bekannten Bibliothek
angular-material
nicht in Angular 2 verfügbar machen. Die einzige Möglichkeit ist es, auf eine Angular-2-Bibliothek auszuweichen – auf lange Sicht muss diese Aufgabe sowieso erfüllt werden, da alle AngularJS-1-Abhängigkeiten schlussendlich entfernt werden sollen. Dieses Vorgehen funktioniert nur für Elementdirektiven (restrict: “E“), Attributdirektiven können nicht verfügbar gemacht werden und müssen neu implementiert werden. - Verfügbarmachen von Filtern: Filter wie in AngularJS 1 ersetzt Angular 2 durch sogenannte Pipes. Leider gibt es keinen Weg, AngularJS-1-Filter direkt als Angular-2-Pipes zu nutzen und später zu migrieren. Es ist erforderlich, diese zuerst selbstständig upzugraden. Das kann in der Folge zu der unschönen Situation führen, dass es zwei verschiedene Versionen von demselben Pipe/Filter gibt, die getrennt voneinander gepflegt werden müssen. Hier kann durch die Reihenfolge der Komponenten-Upgrades versucht werden, diese Situation zu vermeiden oder zu minimieren.
Upgrade von Controllern und Routes
Das größte Problem beim inkrementellen Upgrade auf Angular 2 ist der Rewrite von Controllern, die an bestimmte Routes gebunden sind, da es das Prinzip von Controllern in Angular 2 nicht mehr gibt. Um dieses Problem zu umgehen, gibt es die Bibliothek “new-router“ für AngularJS 1, das das Routing-Konzept von Angular 2 in AngularJS 1 implementiert. Dadurch ist es möglich, einige Routes mit vorhandenen AngularJS 1-Controllern und andere mit neu implementierten Angular 2-Components zu befüllen. Auf diese Weise können alle Controller Schritt für Schritt in Components umgewandelt werden, bevor am Ende AngularJS 1 entfernt und auf den echten Angular 2 Router gewechselt wird.
Testen der Hybridanwendungen
Möchte man Angular 2 (TypeScript) Tests mit Karma und Jasmine durchführen benötigt man ein erweitertes Setup, da Karma-Tests nicht ohne Anpassung mit Module-Loadern wie SystemJS kompatibel sind. Neben bisherigen AngularJS-1-Tests müssen Angular-2-spezifische Tests hinzugefügt werden. Folgende Schritte sind dabei zu beachten:
- Hinzufügen der Angular-2-Abhängigkeiten in der karma.conf.js
- Hinzufügen einer karma-test-shim.js Datei, um Karma mit system.js verwenden zu können
- Kompilieren der TypeScript-Tests
Hinzufügen der Angular-2-Abhängigkeiten
In der karma.conf.js
müssen die Dateien (Abhängigkeiten) hinzugefügt werden, die im Browser von Karma geladen werden müssen und zur Ausführung von Angular 2 benötigt werden.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
../node_modules/systemjs/dist/system.js ../node_modules/angular2/bundles/angular2-polyfills.js ../node_modules/rxjs/bundles/Rx.js ../node_modules/angular2/bundles/angular2.dev.js ../node_modules/angular2/bundles/testing.dev.js ../node_modules/ng2-translate/bundles/ng2-translate.js ../node_modules/angular2/bundles/http.js ../node_modules/angular2/bundles/upgrade.js |
Außerdem müssen die selbstgeschriebenen Angular-2-Komponenten hinzugefügt werden. Diese werden wie oben beschrieben in die einzelne Bundle-Datei tsc.js
geschrieben. Deshalb muss das kompilierte JavaScript File auch in karma.conf.js
hinzugefügt werden.
1 |
../.tmp/tsc.js |
Um die Angular-2-Tests in einer Continous Integration Pipeline in PhantomJS auszuführen, werden außerdem zusätzliche Polyfills benötigt, da verschiedene Teile von Angular sonst nicht korrekt funktionieren.
1 |
../node_modules/es5-shim/es5-shim.js |
Hinzufügen einer karma-test-shim.js
Die einzelnen Unit tests werden nicht mehr über die karma.conf.js
hinzugefügt, sondern mithilfe einer karma-test-shim.js
geladen. Das ist ein kurzes Skript, das es ermöglicht, system.js
mit Karma zu verwenden. Mithilfe der karma-test-shim.js
werden alle Tests, die .spec
im Namen enthalten, mithilfe von system.js
importiert.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
//... }).then(function() { return Promise.all( Object.keys(window.__karma__.files) // All files served by Karma. .filter(onlySpecFiles) .map(function(path) { return System.import(path); })); }) //... //add only spec files function onlySpecFiles(path) { return /[\.|_]spec\.js$/.test(path); } |
Kompilieren der Typescript-Tests
Um die Typescript-Tests in Javascript zu kompilieren, wird eine zusätzliche Typescript-Config benötigt. Diese kann direkt in einem Gulp-Task ausgeführt werden, der später Karma ansteuert.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//compile typescript test files gulp.task('compiletests', function() { return gulp.src(['test/unit/*.ts', ]) .pipe(plugins.typescript({ target: 'ES5', module: 'system', moduleResolution: 'node', experimentalDecorators: true })) .pipe(gulp.dest('test/unit/')); }); |
Das hier beschriebene Setup von Karma als Testrunner und Jasmine als Framework wird vom Angular 2 Team selbst verwendet und für alle Angular-2-basierten Projekte empfohlen.
Der momentan noch recht aufwändige Aufbau lässt sich dadurch erklären, dass das Angular-2-Ökosystem noch sehr jung ist. Sobald die Community mehr Erfahrung in diesem Bereich gesammelt hat, werden sich hierfür wahrscheinlich elegantere Methoden entwickeln, die auch im Bezug auf Testrunner und Testframework mehr Freiheiten bieten.
Der erste Test
Ein beispielhafter Test kann wie folgt aussehen: Um compile errors zu verhindern müssen die TypeScript Typings von Jasmine importiert werden.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
///<reference path="../typings/tsd.d.ts"/> //dumb test to test whether typescript compilation is working... describe('dumbTest universal truths', () => { it('should do math', () => { expect(1 + 1).toEqual(2); expect(5).toBeGreaterThan(4); }); xit('should skip this', () => { expect(4).toEqual(40); }); }); |
Weitere Resourcen
https://developers.livechatinc.com/blog/testing-angular-2-apps-dependency-injection-and-components/
We’re hiring!
Tapetenwechsel gefällig? Wir sind auf der Suche nach begeisterten Frontend-Entwicklern, die unsere Projektteams im Umfeld von JavaScript, HTML und CSS unterstützen und auch vor innovativen Themen wie AngularJS und Progressive Web Apps nicht zurückschrecken.
Weiterlesen
Mehr Informationen zu unseren Dienstleistungen rund um die Web-Entwicklung gibt es auf unserer Website. Unser Portfolio umfasst außerdem die Anwendungsentwicklung für Android & iOS mit speziellem Fokus auf Enterprise-Apps. Für direkten Kontakt schreibt an info@inovex.de oder ruft an unter +49 721 619 021-0.