Teil 3: Das Ziel ist das Ziel

Angular-Tutorial: Test und Build

Daniel Schwab, Manfred Steyer

©Shutterstock / roibu

Die Entwicklung von Single Page Applications (SPA) stellt den Entwickler vor viele Aufgaben, um eine produktionsreife Applikation zu erhalten. Dabei sollte auch das automatisierte Testen nicht zu kurz kommen.

Um aus den einzelnen Dateien eines Angular-Projekts eine lauffähige Software für den Browser zu erhalten, sind einige Schritte notwendig. Für diese Aufgabe eignet sich der Module Bundler webpack  besonders gut. Durch einen definierten Einstiegspunkt wie etwa main.ts oder vendor.ts einer typischen Angular-Anwendung löst das Tool alle referenzierten Dateien (Module) durch alle Ebenen auf, die dann verarbeitet, transformiert sowie optimiert und als Bundle bereitgestellt werden.

Artikelserie

Durch die Nutzung von Frameworks wie Angular und seiner Konzepte wandert natürlich auch immer mehr Logik in den Clientteil der Applikation. Manuelles Testen reicht nach kurzer Zeit nicht mehr aus, um die fortlaufende Stabilität der eigenen Anwendung zu gewährleisten und deren Komplexität zu kontrollieren. Automatisierte Tests helfen einem jedoch nicht nur dabei, Funktionalität schnell zu prüfen, sie beeinflussen auch positiv die Art, wie Programmcode geschrieben wird. Dieser Artikel beschreibt anhand eines Beispiels den Umgang mit webpack sowie die Nutzung von Karma und Jasmine, um Unit-Tests zu erstellen und daraus Reports für CI-Umgebungen wie Jenkins zu generieren. Außerdem gehen wir darauf ein, wie diese Tasks über den Node-Package-Manager npm in einen Maven-Build-Prozess integriert werden können, sodass am Ende eine fertige JAR-Datei zur weiteren Verarbeitung vorliegt.

Projekte mit webpack

Als Module werden nicht nur TypeScript-Dateien angesehen, sondern sämtliche Dateien, die über Referenzen erkennbar sind. So kann webpack neben den üblichen, wie etwa import oder require, auch beispielsweise src: url(‚…‘) in CSS-Dateien oder <img src=“…“ /> in HTML auflösen. Damit webpack weiß, wie gefundene Dateien verarbeitet werden müssen, ist es notwendig, so genannte Loader zu definieren. Diese reagieren anhand einer selbst definierten Regex auf passende Dateien.

Oft wird webpack als Task-Runner wie Grunt oder gulp missverstanden. Task-Runner arbeiten konfigurierte Tasks wie das Kopieren oder Komprimieren von Dateien nacheinander ab. Das Gesamtergebnis der einzelnen Tasks ist für den Task-Runner nicht relevant. webpack kann in diesem Zusammenhang als ein Task angesehen werden. Obwohl das Tool durch Plug-ins erweiterbar ist und somit teilweise einen Task-Runner imitiert, besteht die primäre Aufgabe darin, ein lauffähiges Bundle aus einzelnen zusammenhängenden Modulen zu erhalten.

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!

 

Listing 1 zeigt die notwendigen Node Packages sowie den Task build. Damit lässt sich ein Bundle über Webpack erstellen. Loader sind nicht in Webpack enthalten und müssen somit immer extra installiert werden. Das gleiche gilt für die meisten Plug-ins.

{
  "name": "angular-build-test",
  "scripts": {
    "build": "webpack"
      },
  "dependencies": {
    […]
  },
  "devDependencies": {
    "@types/webpack": "2.2.7",
    "awesome-typescript-loader": "3.0.8",
    "html-webpack-plugin": "2.28.0",
    "webpack": "2.2.1",
    "webpack-dev-server": "2.4.1"
  }
}

Einstiegspunkte festlegen

Wie zu Beginn erwähnt, benötigt webpack einen Einstiegspunkt. Für die Beispielanwendung ist das die Datei main.ts. Als Bootstrap für Angular verbindet sie auch für webpack alle relevanten Dateien. Über die darin enthaltenen import-Statements (Listing 2) kann so ein Dependency-Graph aufgebaut und aufgelöst werden. Der zweite Einstiegspunkt vendor.ts enthält import-Statements externer Libraries sowie Polyfills. Dadurch kann webpack selbst erstellten Code von externen trennen und ihn als eigenes Bundle ausliefern.

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from '@angular/core';
import { AppModule } from './app/app.module';

if (ENV === 'prod') {
    enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule);

Um diese Einstiegspunkte nutzen zu können, wird zunächst eine Konfigurationsdatei im Projekthauptverzeichnis für webpack erstellt. Der Standardname ist hier webpack.config.js. In Listing 3 sind die beiden Dateien nun unter entry konfiguriert. webpack erwartet sich hier ein Node-Modul, das über module.exports angegeben ist. Entweder kann ein Objekt oder – wie hier – eine Funktion zurückgeliefert werden. Der Vorteil einer Funktion liegt darin, dass so über das Kommandozeilentool eine Umgebungsvariable verwendbar ist. Dazu später mehr. Der Bereich output legt fest, wo webpack das Ergebnis speichert.

Lesen Sie auch: Auf dem Weg zu Angular 5: Angular 4.2 bringt Performance Boost

Der Platzhalter [name] steht für die unter entry definierten Keys app und vendor. Der Pfad /target/classes/META-INF/resources stellt sicher, dass Maven die Dateien später auch gleich weiterverarbeiten kann. Die Konfiguration extensions im Bereich resolve weist webpack an, bei Referenzen, die keinen Dateityp aufweisen, nach Dateien mit den Werten des Arrays als Endung zu suchen. Somit muss bei Anweisungen wie import { AppModule } from ‚./app/app.module‘; nicht ‚…module.ts‘; angegeben werden.

module.exports = function (env) {
  return {
    entry: {
      'app': './src/main.ts',
      'vendor': './src/vendor.ts'
    },
    output: {
      path: __dirname + '/target/classes/META-INF/resources',
      filename: '[name].js'
    },
    resolve: {
      extensions: ['.ts', '.js']
    },
    […]
  }
};

Loader und Plug-ins definieren

Das Ausführen von npm run build löst bereits jetzt die Abhängigkeiten auf und legt die Dateien app.js und vendor.js an. Der Durchlauf führt jedoch zu Fehlern in der Konsole, und auch der Inhalt der gebauten Dateien ist nicht korrekt. Das liegt daran, dass noch keine Loader konfiguriert sind. Somit weiß webpack nicht, wie mit gefundenen Dateien wie TypeScript, aber auch CSS, HTML, Bildern oder Fonts umzugehen ist. JavaScript- und JSON-Dateien versteht webpack auch ohne Loader. Über den Bereich module.rules kann ein Array mit Loader-Konfigurationen angelegt werden. Wie in Listing 4 zu sehen ist, benötigt die Konfiguration dabei immer zwei Parameter. Über den Parameter test ist eine Regex angegeben. Hier kann nicht nur auf die Dateiendung, sondern auch auf den gesamten Namen sowie Pfad abgefragt werden. Alle gefundenen TypeScript-Dateien kann somit der Loader awesome-typescript-loader verarbeiten, definiert durch den Parameter use. Für jede Datei, die webpack findet, muss eine entsprechende Loader-Definition existieren.

module.exports = function (env) {
  return {
[…]
    module: {
      rules: [
        {
          test: /\.ts$/,
          use: [
            'awesome-typescript-loader',
            'angular2-template-loader'
          ],
          exclude: /node_modules/
        }
      ]
    },
[…]
  };
};

Im Unterschied zu einem Loader haben Plug-ins Zugriff auf den gesamten Ablauf und können so auch allgemeine Funktionalität einbringen. Einige davon kommen direkt mit webpack, bei anderen muss der Entwickler das entsprechende npm-Paket installieren. Das Modul selbst wird dabei über require in die Konfiguration geladen. Plug-ins können auch Loader zur Verfügung stellen, falls die Anforderung dies notwendig macht. Durch den Einsatz von CommonsChunkPlugin (Listing 5) wird webpack angewiesen, alle Referenzen, die über den Einstiegspunkt vendor definiert sind, nur in vendor.js zu integrieren, jedoch nicht nochmals in anderen Dateien wie app.js. Um alle Dateien zu einer lauffähigen Anwendung zu verbinden, brauchen wir zudem noch die Datei index.html. Generiert über das Plug-in html-webpack-plugin, werden die Dateien vendor.js und app.js an die richtige Stelle im Template index.ejs gesetzt. Durch die erneute Ausführung von npm run build wird das Bundle nun korrekt erstellt.

var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = function (env) {
  return {
[…]
    plugins: [
      new webpack.optimize.CommonsChunkPlugin({name: 'vendor'}),
      new HtmlWebpackPlugin({
        title: 'Example',
        template: './src/index.ejs'
      })
    ]
  };
};

Entwicklungs- und Produktivmodus

webpack stellt über das npm-Package webpack-dev-server einen Entwicklungswebserver zur Verfügung. Es handelt sich dabei um einen Node.js-Express-Server, der das Bundle an einen Browser liefern kann. Dieser erkennt zusätzlich Änderungen im Code und baut die Dateien daraufhin inkrementell neu auf. Das bedeutet, dass nur die geänderten Stellen im Ergebnis angepasst werden, was die Verarbeitungszeit wesentlich verringert. Der Browser wird danach ebenfalls neu geladen. Für dieses Standardverhalten ist keine eigene Konfiguration notwendig. Der Webserver kann die zuvor erstellte webpack-Konfiguration nutzen. Zum Starten wird ein neuer npm-Task in der package.json erstellt, der dann durch den Befehl npm start aufrufbar ist:

{
  "scripts": {
    "start": "webpack-dev-server --env=dev"
  }
}

Neben der Entwicklung ist normalerweise auch vorgesehen, ein Bundle für den realen Betrieb zu erhalten, das sich durch Codeoptimierungen auszeichnet. Auch hier kommt ein weiterer npm-Task zum Einsatz. Als Unterschied zum Task build wird hier der webpack-Konfiguration der Wert prod über den Parameter env übergeben:

{
  "scripts": {
    "build:prod": "webpack --env=prod"
  }
}

Über das Plug-in DefinePlugin (Listing 6) kann der Wert jetzt dazu eingesetzt werden, in der eigentlichen Angular-Anwendung Abfragen auszuführen. Somit ist es möglich, in der Datei main.ts den Produktivmodus von Angular über eine Abfrage der jetzt verfügbaren Konstante ENV wie in Listing 2 zu aktivieren. Damit der TypeScript-Compiler die Konstante ENV auch erkennt, muss folgende Typdefinition vorhanden sein:

declare const ENV: string;

Im Beispielprojekt existiert dafür die Datei custom-typings.d.ts unter src/. Zusätzlich sollen die generierten Dateien ebenfalls durch einen Optimierungsprozess laufen. Mit UglifyJsPlugin wird das generierte JavaScript sowie das Bundle als gesamtes durch verschiedene Verfahren verkleinert, wie das Entfernen von Leerzeichen, das Löschen von Source Maps oder Tree Shaking. Sofern vorhanden, aktiviert das Plug-in auch Loader-Optimierungen automatisch. Auch hier wird die Variable env genutzt, um die Optimierung lediglich für den Task build:prod zu aktivieren. Bei größeren Konfigurationen empfiehlt es sich, das npm-Paket webpack-merge einzusetzen.

module.exports = function (env) {
  var config = {
    plugins: [
      new webpack.DefinePlugin({
          ENV: JSON.stringify(env)
      })
    ]
  };

  if (env === 'prod') {
    config.plugins.push(
      new webpack.optimize.UglifyJsPlugin({
        output: {comments: false}
      })
    );
  }

  return config;
};

Unit-Tests mit Jasmine, Karma und webpack

Bei Jasmine handelt es sich um ein JavaScript Framework zur Implementierung von automatisierten Tests, das die Ideen des Behavior-driven Developments (BDD) unterstützt. Das bedeutet, dass der Entwickler mit jedem Testfall ein Verhaltensmerkmal der Anwendung beschreibt. Allerdings ist Jasmine nicht von Angular abhängig, sodass man es auch für andere Frameworks heranziehen kann. Um Jasmine-Tests auszuführen, benötigt es einen Test-Runner. Im einfachsten Fall ist ein Test-Runner eine HTML-Datei, die die notwendigen JavaScript-Dateien referenziert. In den meisten Fällen ist eine einfache HTML-Datei jedoch nicht ausreichend. Das Einbinden der JavaScript-Dateien eines geschriebenen Tests möchte man z. B. nicht selbst übernehmen.

Lesen Sie auch: Redux & Angular: Die Architektur-Alternative zur klassischen MVC-Variante

Der Test-Runner Karma, früher unter dem Namen Testacular entwickelt, ist während der Arbeit an AngularJS entstanden und kommt daher wie Angular selbst von Google. Karma verfolgt das Ziel, schnell lauffähig zu sein, weshalb es auch als Kommandozeilentool konzipiert ist. Der Entwickler kann mit einer einfachen Konfiguration alle notwendigen Voraussetzungen schaffen, um Tests starten zu können. Für das kommende Beispiel sind einige Node-Pakete wie folgt zu installieren:

npm install --save-dev @types/jasmine intl istanbul-instrumenter-loader jasmine-core karma karma-coverage karma-jasmine karma-junit-reporter karma-phantomjs-launcher karma-remap-istanbul karma-sourcemap-loader karma-webpack remap-istanbul

Das Paket karma stellt den eigentlichen Test-Runner dar. karma-jasmine und jasmine-core bieten der Testumgebung das nötige Testframework Jasmine an. Mit @types/jasmine kommen die passenden Typings dazu. So kann der TypeScript-Compiler die Library korrekt interpretieren. Damit Karma mit webpack arbeiten kann, benötigt es einen Präprozessor. In diesem Fall wird karma-webpack genutzt, um den vorhandenen Code vorher von TypeScript in JavaScript zu transpilieren und alle notwendigen Abhängigkeiten aufzulösen.

Mit istanbul-instrumenter-loader, karma-coverage, karma-remap-istanbul, karma-sourcemap-loader und remap-istanbul hat Karma die Möglichkeit, einen Report zu erstellen, der Auskunft über die Testabdeckung gibt. Die Anfertigung des Coverage-Reports erfolgt über den bereits transpilierten JavaScript-Code. Daher ist es notwendig, diesen danach über source maps zu konvertieren, um TypeScript- und nicht JavaScript-Dateien anzuzeigen. Deshalb sind hier auch mehrere Module notwendig. Über karma-junit-reporter lässt sich zusätzlich ein JUnit-Report erstellen, der von CI-Umgebungen wie Jenkins weiterverarbeitet werden kann. Zum Schluss benötigt man noch den Launcher karma-phantomjs-launcher sowie intl. Damit lassen sich die Tests im Headless-Modus über PhantomJS ausführen. Listing 7 zeigt eine vollständige Konfigurationsdatei, um die nun vorhandenen Pakate nutzen zu können.

var webpack = require('webpack');

module.exports = function (config) {
  config.set({
    // Das benötigte Testframework
    frameworks: ['jasmine'],

    // Ausgabe der Testergebnisse über verschiedene Module
    reporters: ['progress', 'junit', 'coverage', 'karma-remap-istanbul'],

    // Der ausführende Browser
    browsers: ['PhantomJS'],

    // Durch diesen Flag werden die Tests ausgeführt und Karma sowie der Browser danach beendet
    singleRun: true,

    // Konfiguration aller Dateien, die während des Tests benötigt werden
    files: [
      'src/main.spec.ts'
    ],

    // Dateien, die vor dem eigentlichen Test durch ein Präprozessormodul bearbeitet werden müssen
    preprocessors: {
      'src/main.spec.ts': ['webpack', 'sourcemap']
    },

    // Die webpack-Präprozessor-Konfiguration, um TypeScript in JavaScript zu transpilieren
    // Die Loader und Plug-in-Optionen sind notwendig, um korrekte source-map-Dateien für remap-istanbul zu erhalten
    webpack: {
      devtool: 'inline-source-map',
      module: {
        rules: [
          {
            test: /\.ts$/,
            use: [
              'awesome-typescript-loader?compilerOptions={"sourceMap": false,"inlineSourceMap": true}'
            ]
          },
          {
            test: /\.ts$/,
            use: 'istanbul-instrumenter-loader?embedSource=true&noAutoWrap=true',
            exclude: ['node_modules', /\.spec\.ts$/],
            enforce: 'post'
          }
        ]
      },
      resolve: {
        extensions: ['.ts', '.js']
      },
      plugins: [
        new webpack.SourceMapDevToolPlugin({
          filename: null,
          test: /\.(ts|js)($|\?)/i
        })
      ]
    },

    // Der webpack-Präprozessor soll nur relevante Logdateien ausgeben
    webpackMiddleware: {stats: 'errors-only'},

    // JUnit-Report-XML-Ausgabe für Jenkins
    junitReporter: {
      outputDir: 'target/surefire-reports/'
    },

    // Coverage-Report-Generierung über istanbul
    coverageReporter: {
      reporters: [
        {type: 'in-memory'}
      ]
    },

    // Konvertiert Istanbul-Ergebnis so, dass TypeScript und nicht JavaScript angezeigt wird
    // Es können verschiedene Reporttypen wie HTML und Cobertura generiert werden
    remapIstanbulReporter: {
      reports: {
        html: 'target/coverage',
        cobertura: 'target/coverage-reports/cobertura.xml'
      }
    }
  });
};

Zum Starten wird ein neuer npm-Task erstellt, der durch den Befehl npm test aufrufbar ist:

{
  "scripts": {
    "test": "karma start karma.config.js",
  }
}

Unter der Vorrausetzung vorhandener Tests sowie der Einrichtung eines Testbundles des kommenden Abschnitts generiert Karma durch die Ausführung von npm test unter dem Ordner target die in Abbildung 1 gezeigten Testergebnisse.

Abb. 1: Generierte Testergebnisse von Karma

Der Coverage-Report wurde hier durch die Konfiguration in zwei Formaten generiert. Einmal im HTML-Format, wie Abbildung 2 zeigt, wodurch der Entwickler eine schnelle Einsicht über die aktuelle Testabdeckung erhält, sowie ein Cobertura-Report zur weiteren Nutzung z. B. durch SonarQube.

Abb. 2: HTML-Coverage-Report

Nutzung eines Testbundles

Damit Tests mit Angular interagieren können, müssen zunächst einige Libraries eingebunden und Angular dafür konfiguriert werden. Das geschieht, wie in Listing 8 zu sehen ist, über die Datei main.spec.ts. Mit diesem Bundle werden auch alle Testspezifikationen über die webpack-Funktion require.context importiert. In der Karma-Konfiguration (Listing 7) ist zu sehen, dass nur noch diese Datei angegeben werden muss. Somit sind alle Vorausetzungen für den ersten Unit-Test in Kombination mit Angular erfüllt.

import 'core-js/es6';
import 'core-js/es7/reflect';

import 'zone.js/dist/zone';
import 'zone.js/dist/long-stack-trace-zone';
import 'zone.js/dist/proxy';
import 'zone.js/dist/sync-test';
import 'zone.js/dist/jasmine-patch';
import 'zone.js/dist/async-test';
import 'zone.js/dist/fake-async-test';

import 'intl';
import 'intl/locale-data/jsonp/de';

import { TestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';

Error.stackTraceLimit = Infinity;

TestBed.initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting()
);

let app = require.context('./app', true, /\.ts$/);
app.keys().forEach(app);

Angular Testing Utilities

Angular-Code ist über ES6 in Module und Klassen aufgebaut, was besonders für das Unit-Testen hilfreich ist. So hat der Entwickler je nach Anforderung die Möglichkeit, sich zwischen zwei Testaufbauarten zu entscheiden. Die erste Möglichkeit nennt sich Isolated Unit-Tests. Diese zeichnen sich dadurch aus, dass Angular selbst nicht für den Test benötigt wird und deshalb auch bewusst nicht zum Einsatz kommt. Der Entwickler instanziert die Klassen der zu testenden Komponenten, Services oder Pipes über das Keyword new. Die zweite Möglichkeit funktioniert über die Angular-Testing-Utilities, ein Werkzeugkasten, der es ermöglicht, Tests mithilfe von Angular-spezifischer Funktionalität zu schreiben, ohne dabei die Applikation als Gesamtes starten zu müssen. Der Entwickler erhält die zu testenden Komponenten, Services oder Pipes hierbei über die Angular Dependency Injection.

Lesen Sie auch: „Write once, run anywhere“ – Cross-Plattform-Anwendungen mit Angular, Cordova und Electron

In Listing 9 wird die Komponente AppComponent der Applikation geprüft. Der Entwickler möchte wissen, ob das Klassenattribut title im HTML Tag h1 über eine Angular Expression gesetzt wird und ob sich bei einer Änderung von title das HTML aktualisiert.

import { Component } from '@angular/core';

@Component({
  selector: 'example-app',
  template: '<h1>{{title}}</h1>
' }) 
export class AppComponent { 
  title = 'Example'; 
}

Um dies zu bewerkstelligen muss zu Beginn mittels TestBed.configureTestingModule der Angular-Kontext definiert werden (Listing 10). Hier kommt das Attribut declarations zum Einsatz, um die zu testende Komponente zu registrieren. Mit dem Befehl TestBed.createComponent wird eine Instanz der Komponente erstellt und ein ComponentFixture vom Typ AppComponent zurückgeliefert. Das ComponentFixture bietet eine Reihe an Attributen, um mit der Komponente während des Tests interagieren zu können. Sobald dieser Aufruf erfolgt ist, kann TestBed nicht weiter konfiguriert werden. Über die Instanz fixture möchte man nun für die darauffolgenden Tests das DOM Element h1 ermitteln. Dafür kommt das Attribut debugElement zum Einsatz. Dieses spiegelt das HTML der Komponente wider. DebugElement bietet speziell für den HTML-Teil der Komponente Methoden an, um den Entwickler bei seiner Arbeit am Test zu unterstützen. Eine dieser Methoden ist query. Wie im beforeEach-Block gezeigt, kann damit das gewünschte Element h1 abgefragt werden. Hier nutzt query die statische Methode By.css, die das Element per CSS-Selektor findet. Die Methode query liefert selbst wieder eine Instanz vom Typ DebugElement. Das Attribut nativeElement, das das eigentliche DOM-Element repräsentiert, ist wie auch query selbst eine Eigenschaft von DebugElement. Die Funktionen describe, beforeEach und it gehören zu Jasmine und definieren den Aufbau der Test-Suite. Ein beforeEach-Block wird vor jedem Test durchlaufen, der innerhalb eines it-Bereichs definiert ist.

Der eigentliche Test möchte nun feststellen, ob der Titel tatsächlich im HTML angezeigt wird. Dafür wird das zuvor erstellte NativeElement h1 nach dessen Inhalt mittels textContent abgefragt. Hier zeigt sich eine Besonderheit beim Testen mit Angular, die immer beachtet werden muss. Angular ist in Tests passiv eingestellt. Das bedeutet, Änderungen müssen manuell mit dem Befehl detectChanges() bekannt gegeben werden. Im Testbeispiel ist es damit möglich, die Änderung vor und nach dem ersten Rendering von HTML zu prüfen.

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { AppComponent } from './app.component';

let fixture: ComponentFixture;
let h1: HTMLElement;

describe('AppComponent', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [AppComponent]
    });
    fixture = TestBed.createComponent(AppComponent);
    h1 = fixture.debugElement.query(By.css('h1')).nativeElement;
  });
  it('should display title', () => {
    expect(h1.textContent).toBe('');
    fixture.detectChanges();
    expect(h1.textContent).toBe('Example');
  });
});

Integration in Maven

Im Java-Enterprise-Umfeld existiert in den meisten Fällen bereits eine Build-Management-Umgebung über Maven. Durch die zuvor richtige Angabe verschiedener Pfade lassen sich die npm-Tasks und deren Ergebnisse nun leicht über das Plug-in frontend-maven-plugin in eine pom.xml integrieren (Listing 11). Über die XML-Konfiguration sind die einzelnen npm-Tasks install, build:prod und test abgebildet und mit Maven-Phasen verbunden.

Für den Task test ist die Phase test-compile angegeben. So kann auch die CI-Umgebung Jenkins den generieten JUnit-Report automatisch verarbeiten. Da sich der Pfad des JUnit-Reports unter target/surefire-reports/ befindet, ist keine weitere Konfiguration notwendig. Ebenfalls durch die richtige Pfadangabe beim webpack Bundling reicht ein einfaches JAR Packaging aus, um die Dateien index.html, app.js und vendor.js in angular-build-test-1.0.jar zu integrieren. Auch die Voraussetzung einer installierten Node-Version am ausführenden System entfällt hier, da die notwendigen Binaries ebenfalls zuerst geladen und dann genutzt werden.

<artifactId>angular-build-test</artifactId>
<packaging>jar</packaging>
<groupId>at.angular</groupId>
<version>1.0</version>

<build>
  <plugins>
    <plugin>
      <groupId>com.github.eirslett</groupId>
      <artifactId>frontend-maven-plugin</artifactId>
      <version>1.3</version>
      <executions>
        <execution>
          <id>install node and npm</id>
          <goals>
             <goal>install-node-and-npm</goal>
          </goals>
        </execution>
        <execution>
          <id>npm install</id>
          <goals>
            <goal>npm</goal>
          </goals>
          <configuration>
            <arguments>install</arguments>
          </configuration>
        </execution>
        <execution>
          <id>npm test</id>
          <phase>test-compile</phase>
          <goals>
            <goal>npm</goal>
          </goals>
          <configuration>
            <arguments>test</arguments>
          </configuration>
        </execution>
        <execution>
          <id>npm build</id>
          <phase>compile</phase>
          <goals>
            <goal>npm</goal>
          </goals>
          <configuration>
            <arguments>run build:prod</arguments>
          </configuration>
        </execution>
        </executions>
      <configuration>
        <installDirectory>target</installDirectory>
        <nodeVersion>v6.10.0</nodeVersion>
        <npmVersion>3.10.10</npmVersion>
      </configuration>
    </plugin>
  </plugins>
</build>

Ahead-of-Time-Kompilierung

Ein weiteres Thema, das es beim Build von Angular-Anwendungen für die Produktion zu berücksichtigen gilt, ist die Kompilierung der Templates. Zur Performancesteigerung führt Angular sämtliche Templates in gut optimierbarem JavaScript über. Bei einem einfachen Projekt-Set-up erfolgt dies beim Start der Anwendung. Hierbei ist auch von einer Just-in-Time-(JIT-)Kompilierung die Rede. Um den Anwendungsstart zu beschleunigen, bietet sich eine Vorkompilierung im Zuge des Build-Prozesses an. Das nennt sich Ahead-of-Time-(AOT-)Kompilierung.

Lesen Sie auch: React Fiber versus Angular – wer gewinnt?

Eine sehr einfache vorkonfigurierte Lösung bietet hierfür Angular CLI. Durch das Anhängen von –prod beim Start des Build-Prozesses kümmert es sich um diese Aufgabe (ng build –prod). Intern basiert das CLI auf webpack, und für diese spezielle Aufgabe verwendet es ein AotPlugin, das sich auch mit eigenen webpack-Konfigurationen nutzen lässt. Das CLI-Team stellt dieses Plug-in über das npm-Paket @ngtools/webpack zur Verfügung. Die Erfahrung hat gezeigt, dass es nicht mit jeder Minor-Version von Angular harmoniert. Auf jeden Fall spielt die jeweils neueste Version mit jener Ausgabe von Angular zusammen, die auch das CLI einsetzt. Diese lässt sich durch einen Blick in die package.json eines damit generierten Projekts in Erfahrung bringen.

Um das AotPlugin zu nutzen, ist zunächst der Loader @ngtools/webpack, der nach dem Paket benannt wurde, zur Kompilierung von TypeScript zu konfigurieren. Er stellt eine Alternative zum oben erwähnten awesome-typescript-loader dar:

module: {
  rules: [
    [...]
    { test: /\.ts$/, loaders: ['@ngtools/webpack']}
  ],
}

Anschließend wird das AotPlugin in die Konfiguration eingeklickt. Es erhält den Namen der Datei, die Angular im JIT-Modul startet. Basierend darauf generiert es eine Datei für den AOT-Betrieb. Zusätzlich kompiliert es die Templates mit dem AOT-Compiler, der ein Fork des TypeScript-Compilers ist. Deswegen erhält das Plug-in über die Eigenschaft tsConfigPath auch einen Verweis auf die TypeScript-Konfiguration des Projekts (Listing 12).

var AotPlugin = require('@ngtools/webpack').AotPlugin;

[...]

plugins: [
  new AotPlugin({
    "mainPath": "main.ts",
    "tsConfigPath": "src\\tsconfig.json",
    "skipCodeGeneration": false
  }),    […]
}

Die Einstellung skipCodeGeneration erlaubt ein Deaktivieren der AOT-Kompilierung. Hierzu ist sie auf true zu setzen. Auch wenn dieses Plug-in den Einsatz von AOT drastisch vereinfacht, bringt das Konzept einige Fallstricke mit sich. Eine Übersicht dazu finden Sie auf softwarearchitekt.at.

Fazit

Das populäre Build-Werkzeug webpack arbeitet im Gegensatz zu älteren Ansätzen wie gulp oder Grunt deklarativ, und nicht imperativ. Das bedeutet, dass der Entwickler nicht die nötigen Build-Schritte, sondern den gewünschten Endzustand beschreibt. Im Kern handelt es sich dabei um eine Bundling-Lösung, die durch Verfolgen von Dateireferenzen sämtliche benötigte Dateien berücksichtigt. Durch den Einsatz von Plug-ins kann es jedoch um alle anderen benötigten Build-Schritte erweitert werden. Es spielt mit Werkzeugen zur Testautomatisierung zusammen, und das Team hinter dem CLI bietet ein Plug-in zur Unterstützung von AOT, um die Startgeschwindigkeit zu beschleunigen.

Geschrieben von
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.
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
Kommentare

Schreibe einen Kommentar

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