Die meisten Anwendungen sollen heutzutage im Web verfügbar sein. Server-seitig haben Entwickler dabei die Qual der Wahl zwischen einer ganzen Menge von Programmiersprachen. Auf dem Client kommt heute jedoch niemand an JavaScript vorbei. – Vielleicht ändert sich dies jedoch eines Tages durch WebAssembly.

In den letzten paar Jahren hat sich auch im JavaScript-Universum viel verändert. Vor zehn Jahren schrieb man seinen JavaScript-Code und band diesen, einschließlich aller Abhängigkeiten, per <script>-Tag in die HTML-Seite ein.

Seitdem hat sich jedoch viel getan. Die Sprache JavaScript wurde um neue Sprachelemente und Kernfunktionalitäten ergänzt. Außerdem wurde der Support in Browsern deutlich besser. JavaScript-Engines sind heute performanter, die Kompatibilität zwischen verschiedenen Browsern ist besser und es gibt deutlich mehr standardisierte APIs.

Alle diese Faktoren ermöglichen es heute, deutlich mehr JavaScript im Frontend zu verwenden. Dadurch steigen jedoch auch die Anforderungen an die Qualität, Wiederverwendbarkeit und Wartbarkeit des geschriebenen Codes.

Dieser Artikel stellt Ihnen Tools aus den folgenden vier Bereichen vor:

Package Manager

Im JavaScript-Universum kapselt ein Paket Klassen und Funktionen für die Verwendung in anderen Projekten. jQuery zum Beispiel ist ein Paket, das eine Menge hilfreicher Funktionen zur Verfügung stellt. Während man jedoch vor zehn Jahren die Wiederverwendung dadurch erreichte, dass man sämtliche Pakete über separate -Tags in seiner HTML-Seite lud, so nutzt man hierzu heutzutage einen Package Manager.

Neben der Verwaltung von Abhängigkeiten ist ein Package Manager auch dafür verantwortlich, das Projekt bei Bedarf zu veröffentlichen, damit es von anderen wiederverwendet werden kann. Dazu gibt es, wie auch im JVM-Universum, das Konzept von Repositories. Neben einem öffentlichen Repository gibt es hier natürlich auch die Möglichkeit, eigene private Repositories zu nutzen.

Betrachtet man die aktuell verfügbaren Package Manager, so ist npm der aktuelle De-facto-Standard. npm nutzt eine package.json genannte Datei (s. Listing 1), in der sowohl Metainformationen zum aktuellen Projekt als auch dessen Abhängigkeiten definiert werden.

{
  "name": "package-json-example",
  "version": "1.0.0",
  "description": "Example package.json file",
  "author": "Michael Vitz",
  "license": "ISC",
  "dependencies": {
    "jquery": "^3.2.0"
  },
  "devDependencies": {
    "webpack": "^2.2.1"
  }
}
Listing 1: Beispiel für eine package.json-Datei

Betrachtet man eine solche Datei, so fällt als Erstes auf, dass es zwei Blöcke zur Definition der Abhängigkeiten gibt. Die unter dependencies angegebenen Abhängigkeiten braucht das Projekt dabei zur Laufzeit, die unter devDependencies nur während der Entwicklung. Dies hat vor allem Auswirkungen auf transitive Abhängigkeiten. Hängt beispielsweise ein Projekt vom hier definierten Paket ab, steht auch jQuery als Abhängigkeit zur Verfügung, webpack allerdings nicht.

Die zweite Besonderheit stellt die Syntax für die Versionsnummern der Abhängigkeiten dar. Neben der Angabe einer konkreten Version bietet npm auch die Möglichkeit an, Versionsintervalle zu spezifizieren. Mit Tilde und Caret Ranges (s. Listing 1, Zeile 8 und 11) bietet npm hierfür eine sehr kompakte Syntax an, um Intervalle für Semantische Versionierung anzugeben. Die im Beispiel genutzte Caret Range für jQuery sorgt dafür, dass stets die neueste 3er-Version angezogen wird.

Die Angabe solcher Intervalle führt dazu, dass die konkret genutzte Version nicht mehr nachvollziehbar ist. Im Extremfall kann es somit passieren, dass die gerade noch vom Entwickler eingecheckte Variante bereits beim Build auf einem CI-Server nicht mehr durchläuft, da dieser eine neuere Version irgendeiner Abhängigkeit nutzt.

Um genau dieses Problem zu beseitigen, gibt es für npm shrinkwrap. Es generiert eine Datei, die für alle Abhängigkeiten die aktuell wirklich aufgelöste Version enthält (s. Listing 2). Somit ist sichergestellt, dass auch zu einem anderen Zeitpunkt exakt die gleiche Version genutzt wird.

{
  "name": "package-json-example",
  "version": "1.0.0",
  "dependencies": {
    ...
    "jquery": {
      "version": "3.2.0",
      "from": "jquery@latest",
      "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.0.tgz"
    },
    ...
  }
}
Listing 2: Auszug aus npm-shrinkwrap.json
npm5

npm5

Erst nach der Abgabe dieses Artikels wurde npm in Version 5 veröffentlicht. Highlights dieses Releases sind die verbesserte Geschwindigkeit und die direkte Integration von shrinkwrap.

Die Aussagen zu shrinkwrap und dem Geschwindigkeitsvorteil von Yarn sind somit nicht mehr vollständig gültig.

Seit Ende 2016 gibt es mit Yarn einen weiteren Package Manager, der vollständig kompatibel zu npm ist. Yarn zeichnet sich dadurch aus, dass shrinkwrap bereits von Anfang integriert ist und das er in der Regel deutlich performanter als npm ist.

Der dritte erwähnenswerte Package Manager ist Bower. Im Gegensatz zu npm und Yarn erlaubt Bower lediglich die Installation von Paketen, die speziell für die Frontend-Entwicklung gedacht sind. Dazu benötigt man für Bower eine eigene bower.json-Datei zur Angabe von Metadaten und Abhängigkeiten. Da Bower selbst wiederum per npm installiert wird, muss man anschließend sein Projekt mit zwei Package Managern verwalten. Insbesondere dieser Umstand hat dazu geführt, dass mittlerweile die meisten Frontend-Pakete auch direkt über npm bezogen werden können. Somit kommt man mit npm oder Yarn als Package Manager aus und kann auf die zusätzliche Komplexität durch Bower verzichten.

Linter

Neben der Nutzung eines Package Manager ist auch der Einsatz eines Linters in modernen JavaScript-Frontend-Projekten üblich.

Eventuell kennen Sie auch die Bilder, auf denen die beiden Bücher „JavaScript – The Good Parts“ und „JavaScript – The Definitive Guide“ nebeneinander liegen? Wenn nicht, suchen Sie doch einmal nach „javascript the good parts vs the bad parts“. Dabei werden Sie sehen, dass das zweitgenannte ungefähr fünf bis sechsmal so dick ist. Häufig wird mit dem Bild veranschaulicht, wie schwer es ist, guten JavaScript-Code zu schreiben. Genau um den Entwickler hierbei zu unterstützen, gibt es Werkzeuge zur statischen Codeanalyse, sogenannte Linter. Diese „entfusseln“ den Quellcode, das heißt, sie analysieren ihn und warnen bei offensichtlichen Fehlern/Ungenauigkeiten.

Mit JSLint gibt es bereits seit 2002 einen Linter, der von Douglas Crockford, dem Autor von „JavaScript – The Good Parts“, entwickelt wurde. Die größte Kritik an JSLint besteht darin, dass man nur wenig Einfluss darauf hat, Regeln zu ändern oder zu erweitern.

Als Alternative wurde deswegen JSHint als Fork von JSLint entwickelt. Hauptziel war es, eine Möglichkeit zu schaffen, den Linter zu konfigurieren.

Der dritte und modernste Linter ist ESLint. Neben zahlreichen Konfigurationsmöglichkeiten sticht vor allem die Erweiterbarkeit durch Plug-ins heraus. Zudem ist es mit ihm auch möglich, JSX, eine Erweiterung von JavaScript um eine an XML angelehnte und in React verwendete Syntax, zu „linten“. Dadurch ist er für React-Projekte die aktuell einzig nutzbare der drei Alternativen.

Um ESLint zu nutzen, installieren wir es zuerst mit npm und nutzen anschließend die eingebaute Initialisierung von ESLint, um uns eine einfache Konfiguration erzeugen zu lassen (s. Listing 3).

$ npm install --save-dev eslint
$ ./node_modules/.bin/eslint --init
? How would you like to configure ESLint? Answer questions about your style
? Are you using ECMAScript 6 features? Yes
? Are you using ES6 modules? Yes
? Where will your code run? Browser
? Do you use CommonJS? No
? Do you use JSX? No
? What style of indentation do you use? Spaces
? What quotes do you use for strings? Single
? What line endings do you use? Unix
? Do you require semicolons? No
? What format do you want your config file to be in? JavaScript
Listing 3: Installation und Konfiguration von ESLint

Anschließend können wir es nutzen, um unseren Code (s. Listing 4) überprüfen zu lassen (s. Listing 5). Wie man sieht, verstößt Listing 4 an insgesamt 8 Stellen gegen die vom Linter geprüften Regeln. Diese Verstöße sind neben kleineren stylistischen Dingen (falsche Einrückung, Nutzung von doppelten Anführungszeichen und den zu viel gesetzten Semikolons) auch Variablen, die zwar deklariert, aber nie genutzt werden, und die Nutzung der Variablen console, ohne dass diese deklariert wurden. Listing 6 zeigt eine korrigierte Variante.

function sum (a, b, c) {
  console.log("sum(" + a + ',' + b + ')');
  return a + b;
}
console.log(sum(1, 2))
Listing 4: Beispielcode für ESLint
$ ./node_modules/eslint/bin/eslint.js index.js

/Users/mvitz/Development/new/javaspektrum_1703_js-tooling/package-json/index.js
  1:21  error  'c' is defined but never used                 no-unused-vars
  2:3   error  Expected indentation of 4 spaces but found 2  indent
  2:3   error  Unexpected console statement                  no-console
  2:15  error  Strings must use singlequote                  quotes
  2:42  error  Extra semicolon                               semi
  3:3   error  Expected indentation of 4 spaces but found 2  indent
  3:15  error  Extra semicolon                               semi
  6:1   error  Unexpected console statement                  no-console

✖ 8 problems (8 errors, 0 warnings)
Listing 5: Nutzung des Linters und Anzeige der Fehler
/*eslint no-console: 0*/

function sum (a, b) {
    console.log('sum(' + a + ',' + b + ')')
    return a + b
}
console.log(sum(1, 2))
Listing 6: Korrigierter Beispielcode

Um die Nutzung von console zu erlauben, wurde dem Linter mit einer Direktive (Zeile 1 von Listing 6) mitgeteilt, diese Regel in dieser Datei nicht anzuwenden. Die anderen Fehler wurden behoben.

Task Runner

Neben der Verwaltung der Abhängigkeiten und dem Ausführen eines Linters fallen natürlich noch weitere Schritte in einem JavaScript-Projekt an. Zum Beispiel müssen Tests ausgeführt oder die Quelldateien noch verkleinert werden.

Im JVM-Universum werden sowohl die Verwaltung der Abhängigkeiten als auch das Ausführen von Aufgaben während eines Builds zumeist vom gleichen Tool erledigt. Dies ist im JavaScript-Universum anders. Hier haben sich eigene Tools zum Ausführen von Aufgaben während des Builds entwickelt.

Zu Anfang wurden hierzu häufig Shell-Skripte genutzt oder auf Make zurückgegriffen. Beides schränkt die Nutzung jedoch auf Linux und Mac ein. Zudem muss man eine weitere Programmiersprache oder Konfigurationssyntax lernen. Somit entstand der Wunsch, diese Aufgaben mit JavaScript-basierten Tools zu erledigen, und es entstanden mit der Zeit eine Reihe von JavaScript-basierten Task Runnern.

Grunt ist ein Task Runner, der über ein Gruntfile.js konfiguriert wird, und dabei dateibasiert arbeitet. Die Konfiguration wird hierbei in Form einer JavaScript-Funktion deklariert (s. Listing 7), in der Tasks definiert (Zeile 2 und 9) und Einstellungen (Zeile 4 bis 8) vorgenommen werden können.

module.exports = function(grunt) {
  grunt.loadNpmTasks('grunt-eslint');

  grunt.initConfig({
    eslint: {
      target: ['index.js']
    }
  });
  grunt.registerTask('default', ['eslint']);
};
Listing 7: Grunt-Konfiguration zur Ausführung des Linters

Eine Alternative zu Grunt ist Gulp. Die Konfiguration für Gulp findet man in der Datei gulpfile.js. Wie man am Beispiel in Listing 8 sehen kann, besteht eine solche Konfiguration aus imperativen Anweisungen. Somit kann der Entwickler die volle Ausdrucksstärke von JavaScript für seine Tasks nutzen.

'use strict';

var gulp = require('gulp');
var eslint = require('gulp-eslint');

gulp.task('lint', function() {
  return gulp.src('index.js')
    .pipe(eslint())
    .pipe(eslint.format())
    .pipe(eslint.failAfterError());
});
gulp.task('default', ['lint']);
Listing 8: Gulp-Konfiguration zur Ausführung des Linters

Der zweite Unterschied zu Grunt besteht darin, dass Gulp nicht Datei-basiert arbeitet, sondern dem Entwickler streams zur Verfügung stellt, die anschließend einen oder mehrere Verarbeitungsschritte durchlaufen. Somit ist es sehr einfach nachvollziehbar, welche Transformationen in welcher Reihenfolge durchlaufen werden.

Obwohl beide Tools in der Praxis erfolgreich eingesetzt werden, gibt es auch kritische Stimmen (z. B. Keith Cirkel), die dafür plädieren, bereits vorhandene Tools zu nutzen. Aus diesem Grund findet man immer häufiger JavaScript-Projekte, die auf die Nutzung eines speziellen Task Runners verzichten und npm (oder Yarn) auch für die Ausführung von Tasks nutzen (s. Listing 9).

...
  "scripts": {
    "lint": "eslint index.js && echo ✓"
  },
...
Listing 9: Script-Block aus einer package.json-Datei zur Ausführung des Linters

Transpiler

Als Transpiler bezeichnet man ein Programm, das Quellcode in Quellcode einer anderen Programmiersprache übersetzt. Besonders populär sind diese speziellen „Compiler“ im JavaScript-Universum. Einer der Gründe hierfür ist, dass der Browser nur JavaScript interpretieren kann, sich Entwickler jedoch aus diversen Gründen andere Sprachen für die Entwicklung wünschen.

Die erste Kategorie von nach JavaScript transpilierten Sprachen sind Sprachen, die eine Obermenge von JavaScript sind. Das heißt, ein solcher Transpiler akzeptiert auch eine in JavaScript geschriebene Datei. Der Mehrwert einer solchen Sprache liegt darin, zusätzliche Sprachfeatures oder eine leicht andere Syntax anzubieten.

Ein Vertreter dieser Kategorie ist mit CoffeeScript eine der ersten nach JavaScript transpilierten Sprachen überhaupt. Neben dem Verzicht auf die Nutzung von Semikolons und Klammern zeichnet sie sich aber vor allem dadurch aus, dass bereits sprachseitig häufig in JavaScript gemachte Fehler verhindert werden. Hierzu gehört zum Beispiel der korrekte Umgang mit Scoping und Hoisting von genutzten Variablen. Zudem wurden einige der Sprachfeatures von CoffeeScript, wie beispielsweise die Syntax zur kürzeren Definition von Funktionen, dort erprobt und anschließend von JavaScript übernommen.

Weitere Sprachen aus dieser Kategorie sind TypeScript und Flow. Beide zeichnen sich vor allem dadurch aus, dass sie JavaScript um statische Typisierung ergänzen.

Der zweite Grund, Transpiler zu benutzen, besteht darin, bereits heute neuere Versionen von JavaScript zu benutzen. Als Webentwickler hat man häufig keinen Einfluss auf die vom Benutzer eingesetzten Browser. Ältere Browser unterstützen allerdings in der Regel nur ältere Versionen von JavaScript. Somit dienen Transpiler wie Babel oder bublé dazu, den in der aktuellen Version geschriebenen Quelltext in eine ältere JavaScript-Version zu transformieren. Diese Transpiler übersetzen dabei in der Regel lediglich Sprachfeatures, wie „Arrow functions“. Möchte man neue APIs nutzen, fügt der Transpiler diese nicht hinzu. Man setzt stattdessen zusätzlich sogenannte Polyfills ein. Somit lässt sich beispielsweise das Fetch API bereits heute nutzen.

Die dritte Art von Transpilern dient dazu, eine bereits bestehende Sprache nach JavaScript zu übersetzen. Vertreter dieser Art sind zum Beispiel ClojureScript oder Scala.js. Neben der eigentlichen Transformation bringen diese Sprachen häufig auch noch eine eigene Bibliothek mit, die die Kernsprachfunktionen enthält.

Fazit

Dieser Artikel hat mit Package Managern, Lintern, Task Runnern und Transpilern vier Werkzeugkategorien vorgestellt, die Ihnen in Praktisch jedem modernen Projekt begegnen werden, das client-seitiges JavaScript bereitstellt. Zudem haben Sie für jedes Tool mehrere Implementierungen kennengelernt, die Sie nutzen können.

Natürlich bietet dieser Artikel nur einen ersten Einstieg in die gesamte Thematik. Ich hoffe jedoch, dass der Artikel ein wenig Licht ins Dunkel gebracht hat und Sie somit in Zukunft viele der Begriffe rund um das Tooling im JavaScript-Universum verstehen.