Einführung in TypeScript - Teil 3: Go with the flow

TypeScript Tutorial: Workflow und Aufsetzen eines Projekts

Johannes Dienst

© Shutterstock / Hanss

Dieses TypeScript Tutorial gibt eine ausführliche Einführung in die populärere JavaScript-Variante. In fünf Teilen lernen Sie die sprachlichen Grundlagen und die Möglichkeiten der Typisierung kennen und wenden das Erlernte gleich in der Praxis an. In diesem Teil nehmen wir den Workflow eines TypeScript-Projektes genauer unter die Lupe.

Dieser Artikel widmet sich dem Workflow für ein TypeScript-Projekt. Aufbauend auf dem Package Manager npm und dessen Fähigkeiten als Build-System wird schrittweise ein TypeScript-Projekt aufgesetzt, das sich leicht verwalten lässt. Es umfasst neben der Kompilierung von Sass und TypeScript auch Techniken wie Linting und Unit-Tests. Im Laufe des Artikels wird klar, dass es gar nicht so schwer ist, TypeScript in den eigenen Prozess zu integrieren.

TypeScript: Go with the Flow

Die Einführung einer neuen Technologie ist immer mit Aufwand im Umbau der Arbeitsprozesse verbunden. Ganz besonders trifft das auf eine neue Programmiersprache zu. Im schlimmsten Fall sind alle Build-Skripte und Entwicklungsumgebungen der Entwickler anzupassen und Mitarbeiter zu schulen. Im besten Fall sollte sich die Integration in bereits bestehende Workflows nahtlos gestalten.

Der Erfolg von TypeScript hängt auch damit zusammen, dass es nicht den Weg des befürchteten Vendor-Lock-in eingeschlagen hat, sondern auf maximale Integration in das JavaScript-Ökosystem setzt. Der beliebte Package Manager npm wird, nach einem kleinen Ausflug in JavaScript-basierte Build-Systeme wie Gulp oder Grunt, wieder verstärkt als Build-System verwendet. Mit der zentralen Konfigurationsdatei package.json ist damit die Verwaltung von Abhängigkeiten und Build-Skripten direkt im Projekt angesiedelt. Auch TypeScript wird über npm bereitgestellt und ist somit nur einen Eintrag in dieser Datei von seinem Einsatz entfernt. Mit TypeScript 2.0 wurde die Verwaltung von Definitionsdateien, die der Compiler benötigt, um Abhängigkeiten zu Fremdbibliotheken aufzulösen (Methoden, Klassen und Interfaces), über die package.json handhabbar gemacht. Das war ein wichtiger Schritt hin zu einer vollständigen Integration in das JavaScript-Ökosystem. Im Folgenden wird schrittweise ein TypeScript-Projekt aufgebaut, das sich für die Entwicklung im Frontend eignet. Der Workflow hat sich bei uns in der Praxis bewährt und ist so flexibel, dass er leicht auf die eigenen Bedürfnisse angepasst werden kann.

Statisch starten

Da ein Frontend-Projekt aufgesetzt werden soll, liegt der Start mit einer index.html nahe (Listing 1). Sie dient zur Demonstration und enthält gerade einmal zwei div-Elemente mit jeweils einer ID (Abb. 1). Abgelegt wird sie im Projektordner unter src/index.html.

   <!DOCTYPE HTML>
<html lang="en">
  <head>
    <title>TypeScript</title>
  </head>
  <body>
    <div id="greeting">Hello TypeScript!</div>
    <div id="result">42</div>
  </body>
</html>

Abb. 1: „index.html“ im Rohzustand (bei 300 Prozent Größe)

Das Herzstück: package.json

Jetzt ist es natürlich nervig, bei jeder Änderung von Dateien manuell einen Refresh des Browsers anstoßen zu müssen. Deswegen überlassen wir das einem Package namens browser-sync. Bevor wir es jedoch benutzen können, sollten wir eine package.json in unserem Projektordner anlegen (Listing 2). Diese enthält neben einigen allgemeinen Informationen des Projekts wie Name, Version, Beschreibung, Main-Datei, Contributors und der Lizenz auch einen devDependencies- und scripts-Block. Als Abhängigkeit in devDependencies wird browser-sync eingetragen. Mit npm install –only=dev wird das Package in den node_modules-Ordner heruntergeladen. Im scripts-Block wird der Startbefehl abgelegt, ein einfaches Shell-Kommando, das Browser-Sync im Watch-Modus startet. Die benötigte Konfigurationsdatei bs-config.js steuert z. B., auf welchem Port der Webserver lauscht. Sie kann vollständig im zum Artikel gehörigen GitHub-Repository eingesehen werden. Das Kommando npm run serve startet das Skript. Sobald sich eine in der bs-config.js spezifizierte Datei ändert, wird der Browser automatisch neu geladen.

{
  "name": "JavaMagazin_Workflow_Example",
  "version": "1.0.0",
  "description": "Workflow example",
  "main": "index.js",
  "contributors": [
      "Johannes Dienst <info@johannesdienst.net>"
  ],
  "devDependencies": {
    "browser-sync": "^2.17.6"
    },
    "scripts": {
    "serve": "browser-sync start --config bs-config.js",
    },
  "license": "MIT"
}

Ein bisschen Styling bitte

Die Indexseite sieht noch etwas unschön aus. Zeit, ihr ein Styling zu verpassen. Das geht heutzutage fast nicht mehr ohne CSS-Präprozessor. In diesem Fall ist Sass die erste Wahl. Wir legen eine minimale SCSS-Datei (Listing 3) unter src/scss/default.scss ab.

 
#greeting {
  font-size: 25px;
  text-align: center;
}

#result {
  text-align: center;
}

Da noch kein Watch-Job implementiert ist, ist es ratsam, sich ein wenig Gedanken zu machen, wie nicht nur Sass, sondern auch alle weiteren Builder leicht eingebunden werden können. Das Ziel ist es, mit npm run watch alle Watch-Jobs gleichzeitig anzuwerfen. Zuerst bauen wir dafür ein Build-Target build:css, das die default.scss nach src/css/default.css kompiliert. Da CSS in die Kategorie der statischen Ressourcen fällt, von denen es auch andere Formate (Bilder zum Beispiel) geben kann, wird ein weiteres Target deploy:static-resources angelegt, das im Moment mit npm-run-all build:css lediglich die Sass-Kompilierung anstößt. Das ist aber leicht erweiterbar, indem man zusätzliche Build-Schritte einfach mit einem Leerzeichen getrennt anhängt. Auch deploy:static-resources wird nicht direkt aufgerufen, sondern mit dem letzten Target in dieser Kette watch:static-resources. Dieses lauscht auf Veränderungen von beliebigen Ressourcen und stößt bei Bedarf dann deploy:static-resources an. Am Ende reicht ein npm run watch:static-resources, um auf alle Änderungen von statischen Ressourcen zu reagieren. In den devDependencies ist neben node-sass> für Sass-Kompilierung und nodemon für die Watch-Aufgabe auch npm-run-all für die Ausführung mehrerer Build Targets in einem Befehl einzutragen und mit npm install –only=dev zu installieren.

 
{
  ...
  "devDependencies": {
    ...
    "node-sass": "^3.12.4",
    "nodemon": "^1.9.2",
    "npm-run-all": "^3.1.1"
  },
  "scripts": {
    "build:css": "node-sass --include-path src/scss src/scss/default.scss src/css/default.css",,
    "deploy:static-resources": "npm-run-all build:css",
    "watch:static-resources": "nodemon --verbose -w src/scss/ --on-change-only -d 1 -e \"scss\" --exec \"npm run deploy:static-resources\"",
    "watch": "npm-run-all -s -p watch:static-resources",
    ...
  },
  ...
}

Die kompilierte CSS-Datei wird dann in der index.html im head-Element ergänzt:
<link href="css/default.css" rel="stylesheet">

Das ergibt ein etwas ästhetischeres Ergebnis (Abb. 2). Startet man das Build-Target npm run watch, dann ist eine schnelle Modifizierung des SCSS möglich. Jede Änderung in der default.scss wird kompiliert und direkt danach der Browser aktualisiert (sofern ein weiterer Prozess mit npm run serve läuft).

Abb. 2: „index.html“ mit rudimentärem Styling

TypeScript kommt ins Spiel

Nachdem der Grundstock gebaut ist, will auch TypeScript eingebunden werden. Dafür wird eine tsconfig.json-Datei im Ordner src/ts erstellt, die den Ordner zum TypeScript-Projekt macht (Listing 5). Aus ihr geht hervor, dass die TypeScript-Dateien im selben Ordner zu finden sind und in das Verzeichnis src/js transpiliert werden sollen – das Ganze im ECMAScript2015/ES6-Sprachstandard. Die transpilierte Modulart soll CommonJS sein, also Module, die hauptsächlich im Backend Verwendung finden.

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "ES6",
    "rootDir": "./",
    "outDir": "./src/js",
    ...
  },
  "exclude": [
      "node_modules"
  ]
}

Anschließend wird wieder die package.json vorbereitet. TypeScript ist genau eine Abhängigkeit unter devDependencies entfernt (Listing 6) und kann anschließend wieder mit npm install –only=dev installiert werden. Die weitere Vorgehensweise gestaltet sich ähnlich zum Kompilieren von SCSS. Es gibt zwei neue Build Targets watch:tsc und deploy:tsc. Ersteres registriert Änderungen an Dateien mit der Endung ts, und Zweiteres ruft den in node_modules abgelegten lokalen TypeScript-Compiler auf. Zu guter Letzt wandert das Watch Target zusätzlich in das Target watch, sodass ein npm run watch sowohl auf Änderungen an statischen Ressourcen als auch auf TypeScript-Dateien reagiert. Man sieht, dieser Ansatz ist leicht erweiterbar.

  
{
  ...
  "devDependencies": {
    ...
    "typescript": "^2.0.9",
  },
  "scripts": {
    "deploy:tsc": "./node_modules/typescript/bin/tsc -p ./src/ts",
    "watch:tsc": "nodemon --verbose -w src/ts/ --on-change-only -d 1 -e ts --exec \"npm run deploy:tsc\"", -d 1 -e \"scss\" --exec \"npm run deploy:static-resources\"",
    "watch": "npm-run-all -s -p watch:tsc watch:static-resources",
  },
  "license": "MIT"
}

Unter src/ts wird als Nächstes die Datei app.ts angelegt (Listing 7), die nichts anderes machen soll als nach dem Laden der index.html im Element mit der ID greeting den Text durch „Hello World!“ auszutauschen. Dazu verwendet es jQuery, das importiert wird. Der TypeScript-Compiler meckert bei einem Kompilierungsversuch aber über die fehlende Abhängigkeit zu jQuery.

import "jquery";
$(document).ready(function() {
  $("#greeting").text("Hello World!");
});

Was ist hier falsch? Dazu muss man wissen, dass vor Typescript 2.0 Definitionsdateien, die die Typdefinitionen für eine Library (in diesem Fall $ von jQuery) enthielten, über das Projekt Typings heruntergeladen werden mussten. Die Referenzierung erfolgte dann über die sogenannte Triple-Slash-Direktive. Damit konnte der Compiler das $ erkennen und den Code kompilieren. Ab TypeScript 2.0 wurde die Vorgehensweise erheblich vereinfacht. Jetzt können die Definitionsdateien direkt über npm bezogen werden. Das geht über eine spezielle Syntax, die zum Beispiel für jQuery folgendermaßen aussieht:

npm install --save @types/jquery

Führt man dieses Kommando in einem Terminal aus, dann wird der package.json eine neue Abhängigkeit hinzugefügt:

"dependencies": {

"@types/jquery": "^2.0.34"

},

Mit ihr kann der Compiler direkt den benötigten Import auflösen – ganz ohne Umweg über externe Tools und Definitionsdateien. Ebenfalls entfällt der Import von jQuery in Zeile 1. Das Ergebnis ist die transpilierte app.js im Verzeichnis src/js (Listing 8).

$(document).ready(function () {

$("#greeting").text("Hello World!");

});

Diese bindet man zusammen mit jQuery in der index.html ein:

 <script src="https://code.jquery.com/jquery-2.0.3.min.js" crossorigin="anonymous"></script>
  <script src="js/app.js"></script>

Dadurch ändert sich der Begrüßungstext von „Hello TypeScript!“ nach „Hello World!“, nachdem der DOM vollständig aufgebaut wurde.

Codeanalyse mit Linten

Linten ist eine statische Codeanalyse für Sprachen im Umfeld von JavaScript. Auch TypeScript hat einen eigenen Linter – TSLint, der mit einer tslint.json konfiguriert wird. Diese wird auf der gleichen Ebene wie die Datei tsconfig.json abgelegt. Für eine detaillierte Erläuterung der möglichen Regeln empfiehlt sich die Dokumentation. Das Einbinden in die package.json erfolgt durch eine Abhängigkeit zusammen mit zwei weiteren Build-Targets lint und lint:ts (Listing 9). Da viele IDEs/Editoren TSLint über eigene Plug-ins/Packages unterstützen, entfällt die Integration als Watch-Job. Das Linten wird bei Bedarf direkt über die Konsole angestoßen.

{
  ...
  "devDependencies": {
    ...
    "tslint": "^3.15.1"
  },
  "scripts": {
    "lint": "npm-run-all -s lint:ts",
    "lint:ts": "tslint -e **/definitions/**/* -e **/*.d.ts -c src/ts/tslint.json src/ts/**/*.ts || true"
  },
  ...
}

Testen mit Mocha

Funktionalität wird in JavaScript gern in Module ausgelagert. Der JavaScript-Sprachstandard ES2015/ES6 hat wegen der vielfältigen Modulsysteme für Backend und Frontend eine Standardisierung vorgeschlagen, die im Folgenden benutzt wird. TypeScript selbst bietet keine Möglichkeit, diesen Standard direkt nach JavaScript zu transpilieren, unterstützt aber die Syntax. Es musste sich also für ein Modulsystem entschieden werden. Die Wahl fiel auf CommonJS (tsconfig.json), da es einfacher ist, Module in diesem Format im Browser nutzbar zu machen. Dazu aber später mehr. Zuerst erstellen wir ein einfaches ES2015-Modul Calculator unter src/ts/unittests/source/, das genau eine Klasse mit einer Methode exportiert (Listing 10).

  
export default class Calculator {
   add(x: number, y: number): number {
    return x+y;
   }
}

Dieses Modul soll mit dem Testframework Mocha getestet werden. Als Assertion-Library ist Chai geeignet. Beides erhält einen Eintrag in der package.json unter devDependencies und wird mit npm install –only=dev in den node_modules-Ordner installiert. Damit ein Test ohne Meckereien des TypeScript-Compilers geschrieben werden kann, werden noch die fehlenden Typdeklarationen von Mocha und Chai installiert:

npm install --save @types/mocha

npm install --save @types/chai

Die Änderungen in der package.json zeigt Listing 11. Zur Ausführung der Tests wird npm run test:unit auf der Konsole eingegeben, das die unter src/js/unittests liegenden Tests ausführt. In Produktion sollten die Tests wahrscheinlich separat vom zu testenden Modul liegen. Der Einfachheit halber wird darauf aber in diesem Artikel verzichtet.

{
  ...
  "devDependencies": {
    ...
    "chai": "^3.5.0",
    "mocha": "^3.1.2"
  },
  "scripts": {
    ...
    "test:unit": "./node_modules/mocha/bin/mocha src/js/unittests/**/*",
  },
  "dependencies": {
    ...
    "@types/chai": "^3.4.34",
    "@types/mocha": "^2.2.33"
  },
  ...
}

Der Test in Listing 12 ist mit dem BDD-Interface von Mocha geschrieben, was man an den Funktionen describe(), beforeEach() und it() erkennen kann. Ein Vorteil von BDD ist, dass intuitiv verständlich ist, was passiert. So wird mit describe() ein Test für das Modul Calculator angelegt. Vor jedem Test wird ein neues Objekt vom Typ Calculator erzeugt, und es ist ein Test der Methode add spezifiziert.

import "mocha";
import * as chai from "chai";

import Calculator from './calculator';

describe('Calculator', () => {
  var subject :Calculator;

  beforeEach(function () {
    subject = new Calculator();
  });

  describe('#add', () => {
    it('should add two numbers together', () => {
      var result :number = subject.add(2, 3);
      chai.expect(result).to.equal(5);
    });
  });
});

Nach Aufruf des Testskripts mit npm run test:unit meldet die Konsole einen erfolgreichen Testlauf (Abb. 3).

Abb. 3: Erfolgreicher Testlauf für das Calculator-Modul

Module im Browser

Die Entscheidung, das Modulsystem CommonJS für das Calculator-Modul zu verwenden, führt dazu, dass es nicht ohne zusätzlichen Aufwand im Browser verwendet werden kann. Listing 13 zeigt eine app.ts, welche die Klasse Calculator benutzt, um im Element mit der ID result ein Ergebnis anzuzeigen.

 
import Calculator from './unittests/source/calculator';

$(document).ready(function() {
  $("#greeting").text("Hello World!");

  let calc = new Calculator();
  $("#result").text(calc.add(21, 6));
});

Die daraus transpilierte app.js in Listing 14 enthält einen require-Aufruf, der das CommonJS-Modul laden soll. Leider wird require eigentlich nur im Backend verwendet und konnte nur mit hohem Aufwand auch im Frontend benutzt werden. Es musste ein anderer Ansatz gefunden werden.

 
const calculator_1 = require('./unittests/source/calculator');
$(document).ready(function () {
 $("#greeting").text("Hello World!");
 let calc = new calculator_1.default();
 $("#result").text(calc.add(21, 6));
});

Die Suche nach einer Lösung für dieses Problem führte zu webpack. Wie der Name schon andeutet, ist es in der Lage, Module so zu verpacken, dass der Browser sie nutzen kann. Durch die leichte Erweiterbarkeit mit Plug-ins übernimmt es auch weitere Aufgaben, wie die Minifizierung von JavaScript-Dateien. Listing 15 beschreibt die nötigen Erweiterungen an der package.json. Es wird ein weiterer Watch-Job für webpack eingebunden, der speziell auf Änderungen in der app.js lauscht. Sollte sie sich ändern, wird sie mit webpack verpackt und in der Datei app_bundle.js abgelegt.

{
  ...
  "devDependencies": {
    ...
    "webpack": "^1.13.3"
  },
  "scripts": {
    "deploy:webpack": "./node_modules/webpack/bin/webpack.js src/js/app.js src/js/app_bundle.js",
    "watch:webpack": "nodemon --verbose -w src/js/app.js --on-change-only -d 1 --exec \"npm run deploy:webpack\"",
    "watch": "npm-run-all -s -p watch:tsc watch:static-resources watch:webpack",
  },
  ...
}

Die app_bundle.js muss anschließend noch in der index.html eingebunden werden. Listing 16 stellt diese noch einmal in ihrer Endform dar. Das Ergebnis ist in Abbildung 4 zu sehen.

 
<html lang="en">
  <head>
    <title>TypeScript</title>
    <link href="css/default.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-2.0.3.min.js" crossorigin="anonymous"></script>
    <script src="js/app_bundle.js"></script>
  </head>
  <body>
    <div id="greeting">Hello TypeScript!</div>
    <div id="result"></div>
  </body>
</html>

Abb. 4: Abschließende Form der „index.html“

Fazit

Die Integration von TypeScript in das JavaScript-Ökosystem schreitet mit TypeScript 2 weiter voran. Die Entscheidung zu diesem Vorgehen kann nur begrüßt werden, denn nur so kann eine reibungslose Einbindung in bestehende Arbeitsabläufe gelingen. Der vorgestellte Workflow zeigt, dass es nicht aufwendig ist, eine vollständige Pipeline mit npm herzustellen und effizient zu nutzen. So können mit verschiedenen Skript-Targets Watch-Jobs, Builder für CSS-Präprozessoren, TypeScript-Compiler, Linter und Tests gebaut werden.

Die zentrale Steuerung von Abhängigkeiten und Skript-Targets über die Konfigurationsdatei package.json ermöglicht die schnelle Ausbreitung des Projekts auf mehrere Entwickler. Dazu ist es nur notwendig, das Projekt aus der Versionskontrolle auszuchecken und mit npm install (–only=dev) die Typdefinitionen (Abhängigkeiten) zu installieren. Die Möglichkeit, ab TypeScript 2 Definitionsdateien für Fremdbibliotheken wie jQuery direkt über npm zu beziehen und sie in der package.json abzulegen, vereinfacht deren Verwaltung erheblich. Dadurch verhalten sie sich auch wie ES2015-Module. Sie können mit der standardisierten Syntax importiert werden, weil der TypeScript-Compiler sie direkt aus den node_modules auflösen kann. TypeScript macht damit einen großen Schritt, um seine eigene Zukunftsfähigkeit zu sichern.

Geschrieben von
Johannes Dienst
Johannes Dienst
Johannes Dienst ist Clean Coder aus Leidenschaft bei der DB Systel GmbH. Seine Tätigkeitsschwerpunkte sind die Wartung und Gestaltung von serverseitigen Java- und JavaScript-basierten Applikationen. Twitter: @JohannesDienst
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: