zurück zum Artikel

AngularJS 1.x und 2.0 mit dem Component Router parallel betreiben

Manfred Steyer

Der neue Component Router soll einen Parallelbetrieb von AngularJS 1.x und 2.0 ermöglichen und bietet somit Unterstützung bei einer schrittweisen Migration. Der Einsatz von ECMAScript 2015 in AngularJS-1.x-Anwendungen hilft hierbei ebenfalls.

AngularJS 1.x und 2.0 mit dem Component Router parallel betreiben

Der neue Component Router soll einen Parallelbetrieb von AngularJS 1.x und 2.0 ermöglichen und bietet somit Unterstützung bei einer schrittweisen Migration. Der Einsatz von ECMAScript 2015 in AngularJS-1.x-Anwendungen hilft hierbei ebenfalls.

Dass Angular 2.0 einen Neustart darstellt, ist seit Anfang 2014 bekannt. Entwickler, deren Code mit Version 1.x arbeitet, stellen sich somit die Frage nach einem möglichen Migrationspfad. Eine Antwort darauf liefert der neue Router, den das Produktteam sowohl für AngularJS 1.x als auch für Angular 2.0 zur Verfügung stellen will. Dieser sogenannte Component Router kann beide Versionsstränge von AngularJS parallel betreiben, was eine schrittweise Migration ermöglicht.

Der Einsatz von ECMAScript 6 (ES6, offiziell nun ECMAScript 2015) oder TypeScript hilft zusätzlich, AngularJS-1.x-Code an den für Version 2.0 anzugleichen (siehe "Tipps und Tricks für AngularJS, Teil 2: ES2015 [1]" und "AngularJS mit EcmaScript 6 & Starter-Kit [2]"). Dieser Beitrag betrachtet den Component Router vor dem Hintergrund einer in ECMAScript 6 verfassten AngularJS-1.x-Anwendung und soll vermitteln, dass sich der Weg von 1.x auf 2.0 zwar nicht einfach gestaltet, aber auch nicht so steinig ist, wie vielerorts vermutet. Den Quellcode der Beispielanwendung stellt der Autor auf GitHub zur Verfügung.

Wie bei Angular 2.0 steht auch beim Component Router das Konzept der Komponente im Mittelpunkt. Beim Aktivieren einer Route bringt der Router die mit ihr assoziierten Komponenten zur Ausführung. Während Komponenten in Angular 2.0 sogenannte Web Components zur Grundlage nehmen, wird der Component Router bei AngularJS-1.x-Projekten stattdessen eine Kombination aus einer View und einem dazu passenden Controller nutzen.

Zum Laden dieser Konstrukte greift er auf Konventionen zurück. Hinterlegen Entwickler beispielsweise als Ziel einer Route die Komponente Home, begibt sich der Router auf die Suche nach einem HomeController und einer View components/home/home.html. Zusätzlich würde er in dem Fall eine Instanz des Controllers über die Scope-Variable home bereitstellen. Auf die Weise erzwingt der Router eine einheitliche Anwendungsstruktur und verringert dabei die Länge des notwendigen Konfigurationscodes. Wer sich mit diesen Konventionen nicht anfreunden möchte, kann eigene bereitstellen.

Während sich die Quellcodedateien des Component Router im GitHub-Repository [GitHub-Router] von Angular befinden, kann der Entwickler die letzte freigegebene Version wie folgt via npm beziehen:

npm install angular-new-router

Als der vorliegende Text verfasst wurde, war das die Version 0.5.x. Da es sich dabei nicht um die finale Version handelt, können sich bis zu deren Veröffentlichung noch Änderungen ergeben.

Der neue Component Router sieht vor, dass ein anwendungsweit genutzter Controller die Routen konfiguriert. Der nachfolgende Quellcode demonstriert das anhand der Datei app.js:

// app.js
import angular from 'angular';
import newRouter from 'node_modules/angular-new-router/dist/router.es5';
import { HomeController } from 'home/home';
import { FlugBuchenController } from 'flug-buchen/flug-buchen';
[...]

var app = angular.module('app', ['ngNewRouter']);

class AppController {

constructor($router) {
$router.config([
{ path: '/', component: 'home' },
{ path: '/flugbuchen', component: 'flugBuchen' }
]);
}
}

app.controller('AppController', AppController);
app.controller('HomeController', HomeController);
app.controller('FlugBuchenController', FlugBuchenController);
[...]

app.constant("baseUrl", "http://www.angular.at");

angular.element(document).ready(function() {
angular.bootstrap(document, ['app']);
});

Das in ES6 geschriebene Beispiel importiert zunächst AngularJS, den Component Router sowie die von einzelnen Routen verwendeten Controller. Anschließend definiert es ein neues Angular-Modul für die Anwendung. Es erhält als Abhängigkeit das Modul ngNewRouter, das den Component Router beherbergt. Der AppController bekommt eine Instanz des Component Router injiziert und bildet mit der Methode config einzelne Pfade auf Komponenten ab.

Wie oben beschrieben, erwartet der Router unter Nutzung der standardmäßig vorliegenden Konventionen für die Komponente home einen HomeController sowie für die Komponente flugBuchen einen FlugBuchenController. Damit diese bereit stehen, registriert die gezeigte Anwendung die entsprechenden Klassen mit der Methode controller beim angular-Modul. Die dazugehörigen Views sind aufgrund dieser Konventionen über die Dateien components/home/home.html und components/flug-buchen/flug-buchen.html bereitzustellen.

Erstere erhält über die Scope-Variable home Zugriff auf eine Instanz ihres Controllers. Bei letzterer nutzt der Router laut den vorherrschenden Konventionen die Scope-Variable flugBuchen.

Um den Quellcode noch stärker an Angular 2.0 anzugleichen, besteht die Möglichkeit, die für ECMAScript 7 (ECMAScript 2016) geplanten Dekoratoren zu verwenden, die Angular 2.0 zum Hinterlegen von Metadaten im Quellcode nutzt. Auf die Weise könnte das Registrieren von Controllern analog zu Angular 2.0 deklarativ durch Angabe von @Component() erfolgen:

@Component()
class AppController {
[...]
}

Werkzeuge wie Babel oder auch TypeScript sind bereits in der Lage, das neue Sprachmittel nach ECMAScript 5 zu übersetzen. Nähere Informationen finden sich in einem Artikel zum Thema [3].

Legt der Entwickler die Controller im selben Ordner wie die Views ab, ergibt sich die nachfolgend dargestellte einheitliche Projektstruktur:

Projektstruktur

Projektstruktur

Bei den .map-Dateien handelt es sich um vom Transpiler erzeugte Source Maps. Sie geben Entwicklern die Möglichkeit, ECMAScript-6-Code zu debuggen, obwohl der Browser das daraus erstellte und ECMAScript 5 zugrunde legende Kompilat ausführt.

Nach dem Definieren einer Konstante führt das betrachtete Beispiel ein manuelles Bootstrapping von AngularJS durch. Es ersetzt die Verwendung von ng-app und ist in Fällen notwendig, in denen die Anwendung die einzelnen Skript-Dateien nicht über script-Verweise sondern über einen Modul-Loader lädt.

Ähnlich wie bei ngRoute und UI Router können Routen auch Parameter, die mit einem Doppelpunkt eingeleitet werden, aufweisen:

$router.config([
[...]
{ path: '/passagier/:id, component: 'passagierEdit' }
]);

Zum Auslesen des Parameters innerhalb des adressierten Controllers können Entwickler den Service $routeParams injizieren. Dieser weist für jeden Parameter eine Eigenschaft auf:

var id = $routeParams.id; // Liefert z.B. 7 für /passagier/7

Um anzugeben, wo die View der aktiven Route zu platzieren ist, ist im Markup der Single Page Application ein Platzhalter vorgesehen. In der hier betrachteten Version 0.5.x des Component Router kommt dafür die Direktive ngViewport zum Einsatz. In der endgültigen Version soll sie jedoch wahrscheinlich ngOutlet heißen:

[...]
<body ng-controller="AppController as app">
[...]
<ul class="nav navbar-nav">
<li><a ng-link="home">Home</a></li>
<li><a ng-link="flugBuchen">Flug Buchen</a></li>
</ul>
[...]
<div>
<div ng-viewport></div>
</div>

<script src="jspm_packages/system.js"></script>
<script src="config.js"></script>
<script>
System.import('app').catch(console.error.bind(console));
</script>
[...]

Das betrachtete Markup referenziert über ng-controller den AppController, der die Routen bereitstellt. Die Direktive ngLink kümmert sich um das Einrichten von Links, die zu diesen Routen führen. Am Ende des Markups werden der Modul-Loader System.js sowie die hierfür genutzte Konfiguration config.js geladen. Der Aufruf von System.import sorgt für das Einbinden der zuvor betrachteten Datei app.js samt ihrer direkten und indirekten Abhängigkeiten.

Ähnlich wie der De-facto-Standard UI Router unterstützt Component Router auch hierarchisch organisierte Views. Dabei handelt es sich um Views, die Platzhalter für weitere Views aufweisen. Um dieses Konzept zu nutzen, können Entwickler einen Component Router in einen über das Routing geladenen Controller injizieren. Mit diesem sogenannten Child Router lassen sich weitere untergeordnete Routen festlegen.

Der nachfolgende Codeauszug demonstriert das. In ihm sind über den FlugBuchenController, der über den Pfad /flugbuchen erreichbar ist, weitere Routen festgelegt. Darunter befindet sich beispielsweise eine für den Pfad /passagier. Da es sich hierbei um eine untergeordnete Route handelt, erhält sie den vollständigen Pfad /flugbuchen/passagier. Beim Aktivieren dieser Route lädt der Child Router die Komponente passagier und somit einen PassagierController sowie eine View /components/passagier/passagier.html. Damit er Letztere zur Anzeige bringen kann, muss die View des übergeordneten FlugBuchenController einen Platzhalter aufweisen. Dazu ist abermals die Direktive ngViewport zu nutzen.

export class FlugBuchenController {
constructor($router) {
$router.config([
{ path: '/passagier', component: 'passagier' },
{ path: '/flug', component: 'flug' }
]);
}
[...]
}

Der neue Component Router gibt Entwicklern auch die Möglichkeit, mehrere Platzhalter pro Single Page Application beziehungsweise pro View zu nutzen. In dem Fall sind sie angehalten, den Platzhaltern Namen zu geben, die später an die Direktive ngViewport weiterzureichen sind:

<div ng-viewport="main"></div>
<hr>
<div ng-viewport="info"></div>

Die Routing-Konfiguration legt darüber hinaus fest, welche Komponente in welchen Platzhalter zu laden ist:

export class FlugBuchenController {
constructor($router) {
$router.config([
{ path: '/passagier', components: { main: 'passagier',
info: 'flugBuchenInfo' } },
{ path: '/flug', components: { main: 'flug', info:
'flugBuchenInfo' }, as:'flug' }
]);
}
}

Um auf solch eine Route zu verweisen, sind ngLink der Name eines Platzhalters sowie der dazugehörige Komponentenname zu übergeben:

<a ng-link="main:flug">Flug</a>

In Fällen, in denen die Kombination nicht eindeutig ist, können Entwickler der Route auch durch Angabe der Eigenschaft as einen Alias geben und via ngLink darauf verweisen. Ein Beispiel dafür findet sich in einem GitHub-Repository [4] des Autors.

Um ein Eingreifen in das Routing zu ermöglichen, stellt der Component Router sogenannte Lifecycle Hooks zur Verfügung. Damit sie sich einsetzen lassen, sind die Controller mit einer oder mehrerer der folgenden Methoden zu versehen:

Methode Beschreibung
canActivate wird vor Aktivierung einer Route ausgeführt
activate wird nach Aktivierung einer Route ausgeführt
canDeactivate wird vor Verlassen einer Route ausgeführt
deactivate wird nach dem Verlassen einer Route ausgeführt

Durch das Zurückliefern von false unterbindet der Controller den jeweiligen Routen-Wechsel. Auf die Weise kann er das Verlassen einer Route ohne Speichern ebenso verhindern, wie das Aktivieren einer Route, für die der Benutzer nicht die nötigen Berechtigungen hat. Letzteres dient bei Single Page Applications mehr der Benutzerfreundlichkeit und weniger der Sicherheit.

Ist der Controller nicht sofort in der Lage zu entscheiden, ob der Routenwechsel durchgeführt werden darf, kann er auch ein Promise zurückliefern. Lässt es sich später erfolgreich auflösen (resolve), erfolgt der Routenwechsel. Kommt es dabei hingegen zu einem Fehler (reject), bricht der Router den Vorgang ab. Ein Beispiel für die Nutzung dieser Lifecycle Hooks findet sich im auf GitHub [5] bereitgestellten Code zum vorliegenden Text innerhalb des PassagierEditControllers.

Der neue Component Router stellt einen Migrationspfad für AngularJS-Anwendungen dar, zumal er in der Lage ist, Version 1.x parallel zu Version 2.0 zu betreiben. Dies macht eine schrittweise Migration möglich. Darüber hinaus hilft der Einsatz von ECMAScript 6, vorliegenden AngularJS-1.x-Code an den für Angular-2.0-Anwendungen anzugleichen. Der Einsatz des für ECMAScript 7 geplanten Dekorator-Konzepts, das in Version 2.0 umfangreich zum Einsatz kommt, ermöglicht eine weitere Annäherung.

Neben den hier betrachteten Aspekten plant das Produkt-Team noch weitere Funktionen für den Component Router. Dazu gehören unter anderem die Möglichkeit, Routen erst bei Bedarf zu laden (Lazy Loading), sowie eine Unterstützung für das Rendern von Navigationselementen unter Nutzung der Routing-Konfiguration.

Manfred Steyer
ist Trainer und Berater bei IT-Visions [6] und an der FH CAMPUS 02 tätig. In seinem aktuellen Buch "AngularJS: Moderne Webanwendungen und Single Page Applications mit JavaScript" behandelt er die vielen Seiten von Googles SPA-Framework.

(jul [7])


URL dieses Artikels:
https://www.heise.de/-2679282

Links in diesem Artikel:
[1] https://www.heise.de/hintergrund/Tipps-und-Tricks-fuer-AngularJS-Teil-2-ES2015-2555723.html
[2] http://www.softwarearchitekt.at/post/2015/04/22/angularjs-mit-ecmascript-6-and-starter-kit.aspx
[3] http://www.softwarearchitekt.at/post/2015/05/09/angularjs-1-x-code-mit-es7-dekoratoren-kurzer-und-pragnanter-gestalten-und-dabei-an-2-0-angleichen.aspx
[4] https://github.com/manfredsteyer/component-router-ng-1.x-multi-views
[5] https://github.com/manfredsteyer/component-router-ng-1.x-decorator-sample
[6] http://www.it-visions.at/
[7] mailto:jul@heise.de