Suche
Teil 2: Der Teufel steckt im Detail

Angular Tutorial: Wiederverwendbare Pakete erstellen – so geht’s!

Manfred Steyer, Daniel Schwab

©Shutterstock / Sunny studio

Der weit verbreitete De-facto-Standard hinter npm-Paketen bietet sich zur Strukturierung großer Angular-Anwendungen an. Lösungen wie Nexus helfen beim Aufbau firmeninterner Repositories zur Verteilung solcher Pakete. Wir zeigen anhand eines Beispiels, wie solche Pakete für Angular entwickelt werden können.

Zum Strukturieren von Angular-Anwendungen kommen Angular-Module zum Einsatz. Diese fassen zueinander gehörende Konstrukte zusammen, wie Komponenten oder Pipes. Bei großen Unternehmensanwendungen ist das jedoch zu wenig. Hier möchte man das gesamte Projekt in mehrere Unterprojekte aufteilen, die das damit betraute Team möglichst unabhängig entwickeln und testen kann. Hierzu bieten sich npm-Pakete an. Es handelt sich dabei um eine Spezifikation für Pakete, die über ein Repository angeboten werden. Außerdem liefert das Produktteam Angular selbst in Form solcher Pakete aus. Aufgrund der Vormachtstellung von Node.js stellen npm-Pakete einen De-facto-Standard dar.

Dieser Artikel beschreibt anhand eines Beispiels die Vorgehensweise bei der Entwicklung solcher Pakete für Angular. Das Beispiel basiert auf dem Grundgerüst der Angular-Bibliothek ngx-translate, die sich an der Vorgehensweise des Angular-Teams orientiert und somit auch als Musterbeispiel für eigene Pakete dienen kann. Zusätzlich zeigt dieser Artikel, wie solche Pakete über ein firmeninternes, auf Nexus basierendes Repository veröffentlicht werden können.

Angular im Videotutoriallogo-entwickler tutorials

Im entwickler.tutorial Angular – eine Einführung bereitet Sie Manfred Steyer auf die Entwicklung mit dem Angular Framework vor. Steyer zeigt anhand eines praxisnahen Beispiels, wie man Services und Dependency Injection integriert, Pipes zur Formatierung von Ausgaben nutzt, Komponenten mit Bindings verwendet und professionell mit dem Angular CLI umgeht.

Jetzt anmelden!

 

Artikelserie

Aufbau von npm-Paketen

npm-Pakte sind denkbar einfach strukturiert: Sie bestehen aus einem Ordner mit den zu teilenden Dateien. Die sich im Root befindliche package.json stellt Metadaten zum Paket zur Verfügung, und der Ordner node_modules enthält weitere eingebundene npm-Pakete. So gesehen handelt es sich auch bei den meisten Angular-Projekten um ein npm-Paket. Bei Paketen, die verteilt werden sollen, sind jedoch bestimmte Einstellungen der package.json besonders zu berücksichtigen. Dazu gehört der Paketname, unter dem Konsumenten das Paket im Repository finden. Den selben Namen geben die Konsumenten auch an, wenn sie Inhalte des Pakets über die Anweisungen import und require referenzieren (Listing 1).

{
  "name": "lib-starter",
  "description": "...",
  "version": "0.0.2",
  "scripts": {
    "prepublish": "webpack",
    […]
  },
  "main": "bundles/lib-starter.umd.js",
  "module": "index.js",
  "dependencies": { […] },
  "peerDependencies": {
    "@angular/core": "^2.0.0",
    "@angular/http": "^2.0.0"
  },
  "devDependencies": { [...] }
}

Bei Änderungen gilt es, die Versionsnummer im Feld VERSION zu pflegen, da der Befehl npm update nur dann das Paket für Konsumenten erneut aus dem Repository lädt, wenn die Versionsnummer erhöht wurde. Interessant ist auch das Skript prepublish. Der Packet Manager npm führt es aus, bevor das Paket in einem Repository veröffentlich wird. Deswegen bietet es sich an, damit eventuell benötigte Build-Schritte anzustoßen. Beispiele dafür sind Bundling oder auch die Transpilierung von TypeScript nach ECMAScript 5. Das betrachtete Listing delegiert hierzu an die Build- und Bundling-Lösung webpack.

Einsprungpunkte angeben

Die Einsprungpunkte in das Paket geben die Eigenschaften main und module bekannt. Dabei handelt es sich um jene Dateien, die es beim Referenzieren des Pakets mittels import oder require samt Abhängigkeiten zu laden gilt. Die Eigenschaft module sollte aus traditionellen Gründen auf eine ECMAScript-5-Datei verweisen. Das Angular-Team platziert darin den Namen eines auf ECMAScript 5 basierenden Bundles, das den Quellcode des Pakets enthält. Den Quellcode eingebundener npm-Pakete enthält es jedoch nicht. Stattdessen referenziert das Bundle sie mittels require.

Als Modulformat für dieses Bundle nutzt das Angular-Team die Universal Module Definition (UMD), die kompatibel zu den weit verbreitenden CommonJS-Modulen und zur aus der Welt von RequireJS bekannten Asynchronous Module Definition (AMD) ist. Die jüngere Eigenschaft module verweist hingegen auf eine Datei, die vom ECMAScript-Modulsystem Gebrauch macht. Um möglichst viele Konsumenten zu unterstützen, referenziert das Angular-Team damit ebenfalls ECMAScript-5-Kompilate, die jedoch zusätzlich die Anweisungen import und export verwenden, auf denen das ECMAScript-Modulsystem basiert.

Abhängige Pakete verwalten

Auf die eingebundenen Pakete verweist die package.json über die bekannte Eigenschaft dependencies. Diese installiert npm für den Konsumenten zusammen mit dem angeforderten Paket. Das kann natürlich zu Konflikten führen, wenn der Konsument abweichende Versionen davon bereits geladen hat. Aus diesem Grund bietet sich als Alternative die Eigenschaft peerDependencies an. Die darin gelisteten Abhängigkeiten installiert npm nicht mit. Allerdings prüft der Package Manager, ob der Konsument eine dazu kompatible Version bereits installiert hat. Referenziert eine peerDependency ein Paket in der Version ^2.0.0, gibt sich npm auch mit Version 2.0.1 oder Version 2.4.0 zufrieden. Kann keine kompatible Version gefunden werden, weist npm beim Installieren des Pakets darauf mit einer Warnmeldung hin. Die devDependencies enthält daneben sämtliche Pakete, die der Konsument nicht benötigt.

Pakete lokal testen

Um ein npm-Paket zum Testen bei einem Konsumenten zu installieren, bieten sich symbolische Verweise an. npm kommt hierzu mit einer plattformübergreifenden Lösung. Dazu ist zunächst im Root des jeweiligen Pakets npm link aufzurufen, damit sich Node dessen Speicherort notiert. Danach kommt im Root des Konsumenten die Anweisung npm link name-der-bibliothek zum Einsatz. Diese Anweisung erzeugt innerhalb der node_modules des Konsumenten den gewünschten symbolischen Verweis.

Wann manuell nachgerüstet werden sollte

Die Anweisung npm link name-der-bibliothek erzeugt keinen Dependency-Eintrag in der package.json. Somit werden Kollegen, die auch am selben Projekt arbeiten, von der Notwendigkeit dieser Abhängigkeit nicht in Kenntnis gesetzt. Auch die Codevervollständigung in der Entwicklungsumgebung WebStorm und in ihrem großen Bruder IntelliJ leidet unter diesem fehlenden Eintrag. Deswegen ist es sinnvoll, ihn manuell nachzurüsten.

TypeScript und npm-Pakete

Wie oben erwähnt, stellt das Angular-Team den Quellcode in Form von auf ECMAScript 5 basierenden UMD-Bundles sowie in Form einzelner ECMAScript-5-Dateien, die vom ECMAScript-Modulen Gebrauch machen, zur Verfügung. Während der Einsatz von ECMAScript 5 eine große Reichweite sichert, erleichtert das ECMAScript-Modulsystem den Einsatz von Build-Tools des Konsumenten. Beispielsweise können die davon bereitgestellten import– und export-Anweisungen fürs Bundling eingesetzt werden. Daneben sind sie auch der Schlüssel für Tree Shaking. Dieses Verfahren entfernt Codestrecken des Pakets, die der Konsument nicht benötigt, aus dessen Bundle. Basiert das Paket auf TypeScript, ist es sinnvoll, zusätzlich zu den ECMAScript-Kompilaten Dateien mit Typdeklarationen auszuliefern. Konsumenten, die ebenfalls TypeScript einsetzen, können diese zur Erhöhung der Typsicherheit aber auch für Codevervollständigungen nutzen. Sowohl das Kompilierungsziel als auch das gewünschte Modulformat gibt das Paket dem TypeScript-Compiler über die Datei tsconfig.json bekannt (Listing 2). Mit der Eigenschaft declarations kann es darüber hinaus auch die benötigten Typdeklarationsdateien anfordern.

{
  "compilerOptions": {
    "module": "es2015",
    "target": "es5",
    "declaration": true,
    […]
  }
    […]
}

Damit der TypeScript-Compiler die Typdeklarationen innerhalb des Pakets findet, ist ihm auch dafür ein Einsprungpunkt bekannt zu geben. Den Namen dieser Datei erwartet er in der Eigenschaft typings der package.json des Pakets (Listing 3). Darüber hinaus bietet sich beim Einsatz von TypeScript der Aufruf des TypeScript-Compilers tsc über das Skript prepublish an.

{
  […]
  "main": "bundles/lib-starter.umd.js",
  "module": "index.js",
  "typings": "index.d.ts",
  "scripts": {
    "prepublish": "tsc && webpack",
    […]
  }
  […]
}

Templates vorkompilieren mit dem Angular-Compiler

Der Angular-Compiler erlaubt das Vorkompilieren von Templates im Rahmen des Build-Prozesses. Diese Ahead-of-Time-(AOT-)Kompilierung beschleunigt den Anwendungsstart, da sich Angular nicht mehr im Rahmen dessen darum kümmern muss. Pakete, die der Konsument gemeinsam mit AOT verwenden möchte, müssen ein paar Metadaten bereitstellen, die der Compiler vor dem Ausliefern des Pakets generiert. Das gewünschte Ausmaß an Metadaten steuert der Abschnitt angularCompilerOptions, den der Compiler in der Datei tsconfig.json erwartet (Listing 4).

{
  […]
  "angularCompilerOptions": {
    "strictMetadataEmit": true,
    "skipTemplateCodegen": true
  }
}

Die Option strictMetadataEmit gibt an, dass der Compiler zusätzliche Metadaten generieren soll, die das frühe Erkennen von Fehlern erlauben. Daneben führt der Einsatz von skipTemplateCodegen dazu, dass die Templates der Bibliothek nicht kompiliert werden, sie werden ohnehin beim Kompilieren des Konsumenten berücksichtigt. Um sicherzustellen, dass der Angular-Compiler die Metadaten vor dem Veröffentlichen des Pakets generiert, sollte ihn das prepublish-Skript aufrufen (Listing 5). Dazu ist lediglich die Anweisung ngc aus dem Paket @angular/compiler-cli anzustoßen. Da es sich dabei um einen Fork des TypeScript-Compilers handelt, muss das Skript ihn nicht separat aufrufen.

{
  […]
  "scripts": {
    "prepublish": "ngc && webpack",
    […]
  }
  […]
}

Angular-Module in npm-Paketen

npm-Pakete für Angular bieten in der Regel mindestens ein Angular-Modul an. Damit der Konsument dieses möglichst einfach nutzen kann, empfiehlt es sich, es im Einsprungpunkt zu platzieren. IM hier betrachteten Beispiel handelt es sich dabei um die Datei index.ts, die TypeScript zur Datei index.js kompiliert (Listing 6).

// index.ts
@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HttpModule
  ],
  declarations: [
    DemoComponent
  ],
  providers: [ /* Keine Provider */ ],
  exports: [
    DemoComponent
  ]
})
export class LibStarterModule {
  static forRoot(): ModuleWithProviders {
    return {
      ngModule: LibStarterModule,
      providers: [
        DemoService
      ]
    };
  }
}

Dieses Angular-Modul deklariert die angebotenen Komponenten, Direktiven und Pipes. Existieren mehrere Module im Paket, bietet es sich an, das Modul im Einsprungpunkt als Hauptmodul anzusehen. Es könnte sämtliche andere Module einbinden und exportieren. Vorsicht ist beim Einsatz von Services geboten: Normalerweise gelten Services, die auf Modulebene deklariert werden, als globale Singletons. Doch jede Regel hat ihre Ausnahme, und die tritt hier beim Einsatz von Lazy Loading in Kraft. Module, die der Konsument per Lazy Loading bezieht, haben ihren eigenen Wertebereich. Das führt dazu, dass sie eigene Serviceinstanzen erhalten, wenn sie ein Modul einbinden. Um das zu verhindern, gibt das Angular-Team der Community ein Muster zur Hand. Es sieht vor, dass das Modul zunächst keine Services deklariert. Zusätzlich bietet das Modul jedoch eine statische Methode forRoot an. Diese retourniert das Modul mitsamt der Services in Form einer Instanz von ModuleWithProviders. Somit steht das Modul in zwei Formen zur Verfügung: Einmal ohne Services und einmal mit Services. Die letztere, über forRoot angebotene Variante, kommt beim Hauptmodul (Root-Modul) des Konsumenten zum Einsatz. Auf diese Weise stellt er die Services als globale Singletons zur Verfügung. Alle anderen Module – vor allem solche, die die Anwendung per Lazy Loading lädt – binden die Variante ohne Services ein (Abb. 1). Das verhindert, dass sie eine weitere Instanz der Services erhalten und stellt somit die Nutzung der globalen Singletons sicher. Das zuvor betrachtete Listing 6 enthält eine Umsetzung dieses Musters.

Abb. 1: Modul mit und ohne Services

Listing 7 zeigt, wie das Root-Modul des Konsumenten das Paket nutzen kann. Zunächst importiert es dessen Modul. Die from-Klausel der verwendeten import-Anweisung verweist dazu auf den Namen des Pakets. Zusätzlich importiert es die von forRoot gelieferte Variante des Moduls, die auch die Services beinhaltet.

import { LibStarterModule } from 'lib-starter';
[…]

@NgModule({
  imports: [ 
    BrowserModule,
    FormsModule,
    HttpModule,
    LibStarterModule.forRoot(),
    […]
  ],
  […]
})
export class AppModule {
}

Alle anderen Module des Konsumenten würden das Bibliotheksmodul direkt importieren, also ohne Aufruf von forRoot.

Barrels: Alles in einem Fass

Das ECMAScript-Modulsystem sieht vor, dass jede Datei ein Modul mit eigenem Namensraum ist. Das stellt Konsumenten vor eine Herausforderung, zumal sie sich mit der gesamten Dateistruktur des Pakets beschäftigen müssten, um die gebotenen Möglichkeiten zu nutzen. Um diesen Aufwand zu vermeiden, kommen Barrels zum Einsatz – Module, die die Konstrukte mehrerer anderer Module gesammelt anbieten. Dazu importieren sie diese Konstrukte und exportieren sie wieder. Generell bietet sich der Einsprungpunkt eines Pakets als Barrel an. Zusätzlich steht es Paketen natürlich frei, weitere Barrels anzubieten. Ein Beispiel für ein Barrel findet sich in Listing 8. Es nutzt eine spezielle Form des export-Schlüsselsworts, um Konstrukte gleichzeitig zu importieren und zu reexportieren. Auf diese Weise veröffentlicht es sämtliche Konstrukte, die auch das Modul src/demo.service exportiert sowie den OtherDemoService aus dem Modul src/other-demo.service.

// index.ts
export * from './src/demo.service';
export { OtherDemoService } from './src/other-demo.service';

@NgModule({ … })
export class LibStarterModule {
}

Da hier der Einsprungpunkt als Barrel fungiert, kann der Konsument die reexportierten Services importieren, indem er lediglich die Bibliothek in der from-Klausel der import-Anweisung angibt: import { DemoService, OtherDemoService } from ‚lib-starter‘;.

Pakete veröffentlichen

Um Pakete zu veröffentlichen, reicht ein Aufruf von npm publish. Vor dem ersten Veröffentlichen muss sich der Autor ggf. beim npm-Repository mit npm login anmelden. Damit npm eine neue Version des Pakets veröffentlicht, muss die Versionsnummer in der package.json hochgezählt werden. Das erfolgt entweder manuell oder mit npm version: Ein Aufruf von npm version patch erhöht beispielsweise die dritte Stelle der Versionsnummer. Zum Erhöhen der zweiten oder ersten Stelle kommt statt patch die Option minor oder major zum Einsatz. Um Dateien von der Veröffentlichung auszuschließen, muss der Autor deren Namen lediglich in der optionalen Datei .npmignore eintragen. Diese hat sich per Definition im Hauptverzeichnis des Pakets zu befinden und erlaubt auch Wildcards. Kann npm diese Dabei beim Veröffentlichen nicht finden, bezieht es seine Blacklist aus einer eventuell vorliegenden .gitignore.

Lokale Pakete mit Nexus

Nexus ist ein in der Java-Welt gut bekannter Repository-Manager. Primär für Maven bietet er mit Version 3 nun auch die Möglichkeit, Node-Pakete für npm in vollem Umfang im lokalen Umfeld zur Verfügung zu stellen. Einerseits als Proxy, um aus dem Internet stammende Pakete zu speichern, andererseits können auf ihn auch eigene abgelegt und abgerufen werden. Bereits mit Version 2 wurden Node Repositories eingeführt, jedoch sind Scoped Packages noch nicht einsetzbar. Diese kennzeichnen sich durch das @-Symbol zu Beginn des Paketnamens, z. B. @angular/core.

Nexus für Node-Pakete vorbereiten

Die Installation eines Nexus-Servers gestaltet sich sehr einfach und kann mithilfe der Referenz durchgeführt werden. Nach der Installation können Repositories angelegt werden. Abbildung 2 zeigt die für den Node-Paketmanager npm zur Verfügung stehenden Typen. Mit npm (proxy) kann das globale Node Repository abgebildet und lokal zur Verfügung gestellt werden. Bei einer Paketanfrage sieht der Server zuerst Lokal nach, ob das gewünschte Paket vorhanden ist. Sollte das nicht der Fall sein, wird es vom externen Paketserver geladen.

Abb. 2: Auswahl des npm-Repositorys

Für die Erstellung ist lediglich ein frei wählbarer Name, hier npm-external, sowie der URL des externen Node Storages https://registry.npmjs.org anzugeben (Abb. 3). Da der externe Storage selbst wählbar ist, können in größeren Strukturen so auch andere Nexus-Server wiederum als Proxy dienen. Mit Klick auf den Button CREATE REPOSITORY am Ende des Formulars ist der Proxy bereits eingerichtet und einsatzbereit.

Abb. 3: Konfiguration des Proxys

Um auch eigene Pakete mit dem Befehl npm publish ablegen zu können, ist noch ein Repository vom Typ npm (hosted) anzulegen. Hier reicht die Angabe eines Namens wie npm-internal als Konfiguration aus. Damit beide Repositories als eine Quelle nutzbar sind, ist der letzte Typ npm (group) vorhanden. Wie in Abbildung 4 zu sehen, können für diesen Typ die zuvor erstellten Repositories ausgewählt werden.

Abb. 4: Gruppierung der angelegten Repositories

Durch die Auswahl kann npm auf beide Ressourcen unter einem Namen zugreifen. Abbildung 5 zeigt die nun verfügbaren Node Repositories.

Abb. 5: Liste der verfügbaren Repositories

Zum Abschluss muss noch der Security Realm für npm (Abb. 6) aktiviert werden. Dadurch ist es möglich, über die Authentifizierung von Nexus Pakete zu publizieren.

Abb. 6: Aktivierung des Security Realms für npm

Pakete über Nexus nutzen

Der Nexus-Server ist jetzt bereit, Pakete auszuliefern und selbsterstellte aufzunehmen. Damit npm den Nexus-Server nutzt, ist die Datei .npmrc, falls noch nicht vorhanden, am lokalen System anzulegen. Die Datei kann entweder im gewünschten Node-Projekt auf der Ebene der package.json oder im Benutzerverzeichnis des Betriebssystems erstellt werden. Darin ist das zuvor erstellte Gruppen-Repository npm-all zu registrieren: registry=http://localhost:8081/repository/npm-all/.

Der Befehl npm install im Node-Projekt holt nun die bereits über Nexus in der package.json definierten Pakete. Am Server können die dadurch heruntergeladenen Pakete eingesehen werden. Abbildung 7 zeigt das Repository npm-external mit einem Auszug der verfügbaren Pakete.

Abb. 7: Pakete im Repository „npm-external“

Pakete publizieren

Um eigene Pakete oder Projekte auf den Nexus-Server zu publizieren, sind zwei Schritte notwendig. Zuerst muss ein Nexus-Benutzer registriert werden. Das geschieht mit dem Befehl npm login, der Benutzername, Passwort und E-Mail des gewünschten Benutzers abfragt: npm login –registry=http://localhost:8081/repository/npm-internal/. Sollte der Server nur die Basiskonfiguration der Installation aufweisen, können zum Test die Log-in-Daten des Nexus-Admin angegeben werden. In der Datei .npmrc wird dadurch ein neuer Eintrag angelegt:

registry=http://localhost:8081/repository/npm-all/
//localhost:8081/repository/npm-internal/:_authToken=ae5394a9-46e7-3f0f-baab-5bb0215d8c20

Zukünftige Publizierungen auf das Repository npm-internal laufen nun über diesen Benutzer oder über den dadurch erstellten Autorisierungstoken. Zum Schluss ist in der package.json des zu publizierenden Projekts die Sektion publishConfig anzulegen:

{
  "name": "lib-starter",
  "version": "1.0.0",
  "publishConfig": {
    "registry": "http://localhost:8081/repository/npm-internal/"
  },
  […]
}

Nun kann das Projekt mit dem Befehl npm publish am Nexus publiziert werden. Auch dieses Paket lässt sich am Nexus einsehen (Abb. 8).

Abb. 8: Selbsterstelltes Node-Paket am Nexus

Fazit

npm-Pakete bieten sich zum Bereitstellen von Subprojekten und Bibliotheken an. Lösungen wie Nexus erlauben den Betrieb eines hausinternen Repositories zur Verteilung dieser Pakete. Während npm-Pakete prinzipiell sehr einfach aufgebaut sind, steckt der Teufel doch im Detail. So gilt es, einige Einstellungen in der Datei package.json zu berücksichtigen, damit der Konsument das Paket ohne Probleme nutzen kann. Im Umfeld von Angular kommen noch einige Details dazu, wie die Bereitstellung von Metadaten für den Compiler sowie Module und Barrels. Um sich in diesen Details nicht zu verlieren, empfiehlt sich die Nutzung eines bewährten Grundgerüsts wie jenes von ngx-translate.

Verwandte Themen:

Geschrieben von
Manfred Steyer
Manfred Steyer
Manfred Steyer ist selbstständiger Trainer und Berater mit Fokus auf Angular 2. In seinem aktuellen Buch "AngularJS: Moderne Webanwendungen und Single Page Applications mit JavaScript" behandelt er die vielen Seiten des populären JavaScript-Frameworks aus der Feder von Google. Web: www.softwarearchitekt.at
Daniel Schwab
Daniel Schwab
Daniel Schwab arbeitet bei Infonova GmbH als Frontend-Architekt. Dort beschäftigt er sich mit der Konzeption und Entwicklung von webbasierten Anwendungen sowie deren Integration im Enterprise-Umfeld. Seit über zehn Jahren als Entwickler tätig, gilt sein besonderes Augenmerk momentan dem Thema Angular und TypeScript.
Kommentare
  1. Johnny2017-06-06 16:06:51

    Wann gibt's den 3. Teil - Build?

  2. Melanie Feldmann2017-06-07 11:35:30

    Hallo Johnny,

    der dritte Teil erscheint in den kommenden Tagen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.