en
de

Pragmatische Frontend-Builds in drei Varianten

Frontend-Build-Tools für Webapplikationen gibt es wie Sand am Meer. Die Übersicht zu finden und zu behalten ist schwierig. Ein Blick auf deren Einsatzgebiet hilft, die verschiedenen Tools zu ordnen.

Grundsätzlich helfen die Frontend-Build-Tools, jene Aufgaben zu automatisieren, welche zwischen dem Programmieren und dem Veröffentlichen einer Applikation anfallen. Diese Aufgaben lassen sich vier Treibern zuordnen:

  • Funktionalität: Damit die Applikation läuft, wird der Quellcode kompiliert (*.less, *.ts) oder transpiliert (ES6 zu ES5). Zudem werden Abhängigkeiten aufgelöst, heruntergeladen und verlinkt. Idealerweise sind Änderungen jeweils direkt nach einem Speichervorgang im Browser sichtbar.
  • Qualität: Tests werden ausgeführt und der Quellcode wird mit einem Linter auf problematischen Code hin überprüft.
  • Web Performance: Um ein schnelles Benutzererlebnis zu ermöglichen werden die Assets (Bilder, Styles, Code, Dependencies etc.) auf eine performante Auslieferung hin optimiert.
  • Deployment: ein Deployable wird erstellt, konfiguriert und veröffentlicht.

zuehlke_pragmatische_frontend_builds_driver

Was fehlt, ist ein Werkzeug, das all diese Aufgaben zusammen übernimmt. Vielmehr gibt viele Werkzeuge, die auf bestimmte Aufgaben spezialisiert sind. Ein Frontend-Build besteht demnach aus einer ganzen Kette von Werkzeugen. Node.js hat sich hierbei als Ausführungsumgebung etabliert.

Die Einteilung der Werkzeuge in folgende drei Kategorien hilft bei der Übersicht:

  • Package Manager: installieren Dependencies und/oder andere Werkzeuge (Beispiel: npm, yarn).
  • Task Runner: definieren die Abfolge des Builds (Beispiele: Grunt, Gulp, Brunch).
  • Eigentliche Werkzeuge: Hier geschieht die richtige Arbeit.
    • Compiler: wandelt Quellcode in ausführbaren Code um (Beispiel: Typescript, Less).
    • Transpiler: wandelt Quellcode in ausführbaren Code um (Beispiele: Babel).
    • Linter: analysiert den Code (Beispiele: JSLint, ESLint, TSLint).
    • Bundler: optimiert Assets (Beispiele: Rollup.js, Webpack).

Der Build-Einstiegspunkt ist in der Regel ein NPM Script. NPM Scripts erlauben die Definition des Builds in der «package.json» Datei, wo auch die Dependencies definiert werden. Üblicherweise werden Scripts zum Testen, zum Installieren, zum Starten, zum Stoppen usw. erstellt. In der Praxis haben sich drei Varianten zur Implementation dieser Scripts bewährt.

#1 Direkter Aufruf der Node-Module

Ohne zusätzlichen Task-Runner wird der Build durch direkten Aufruf der Node-Module wie Compiler oder Linter implementiert.

{ 
  "scripts": {
    "build:css": "lessc src/app.less dist/app.css",
    "lint:js": "eslint src/*.js",
    "build": "npm run build:css && npm run lint:js",
    "watch": "watch \"npm run build\" src/",
  },
  "dependencies": {
    ...
  },
  "devDependencies": {
    "less": "2.7.1",
    "watch": "1.0.1",
    ...
  }
}

Im Beispiel werden im «scripts»-Abschnitt vier Tasks definiert. «build:css» und «lint:js» führen spezifische Node-Module aus, «build» ist ein Sammeltask (der auch parallel ausgeführt werden könnte) und «watch» zeigt den Einsatz eines File-Watchers. Ein vollständiges und lauffähiges Beispiel findet sich hier.
Die Voraussetzung, um solche Scripts zu schreiben, ist die Kenntnis der verschiedenen Node-Module. Minimal sollte man die oben erwähnten Compiler, Transpiler und Linter kennen. Nachdem diese Hürde aber genommen ist, glänzen NPM-Script-Builds vor allem durch ihre direkte Linie vom Build-Code zu den Node-Modulen. Sie haben keine zusätzliche Schicht zwischen Script und Modulen, deshalb stellt der direkte Aufruf von Node Modulen einen guten Einstieg dar.

#2 Module-Bundler

Bei der nächsten Variante eines Frontend-Builds wird ein Bundler eingesetzt. Ein Killerkriterium für den Einsatz von Bundlern ist die Verwendung von ES6-Modulen. Desweiteren macht ein Bundler vor allem dann Sinn, wenn entsprechende Anforderungen an die Web Performance bestehen. Bundler lösen Abhängigkeiten innerhalb einer Applikation auf, beispielsweise zwischen ES6-Modulen oder zu externen Libraries. Die einzelnen Module fügen sie dann zu statischen Assets zusammen und optimieren diese für eine schnelle Auslieferung und Laufzeit.

(webpack.js.org)

(webpack.js.org)

Als Teil dieser Aufgabe übernehmen Bundler auch Compiler- oder Transpileraufgaben, die in einem einfachen Build durch direkten Aufruf von Node-Modulen bewerkstelligt werden. Sie können daher grosse Teile eines herkömmlichen Builds übernehmen und so auch als eigenes «Build Systems» angesehen werden.

webpack ./src/app.js ./dist/app.bundle.js

Der Kommandozeilenaufruf im Beispiel weist WebPack an, die Datei app.js mit all ihren Abhängigkeiten in der Datei app.bundle.js zusammenzufassen. Webpack bietet auch die Möglichkeit, die Optionen in einer Konfigurationsdatei detailliert anzugeben.

var webpack = require('webpack');

module.exports = {
    entry: {
        app: './src/app.js'
    },

    output: {
        path: __dirname + '/dist',
  filename: '[name].bundle.js'
    },

    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'babel-loader'
            }
        ],
    },
};

Das Beispiel zeigt eine Webpack-Konfigurationsdatei, die ES6-JavaScript-Code mit Babel zu ES5 transpiliert und zusammenfasst. Das vollständige und lauffähige Beispiel findet sich hier. Zusätzlich werden da die LESS-Dateien kompiliert und die Applikation auf den Einstiegspunkt index.html hin optimiert.

#3 Task Runner

NPM-Scripts und Bundler sind sehr mächtig und decken ein grosses Spektrum an anfallenden Tasks ab. Wenn das Projekt nun so umfangreich wird, dass diese Mittel nicht mehr reichen, kommen Task-Runner ins Spiel. Task-Runner übernehmen alle möglichen Aufgaben innerhalb eines Builds: Sie führen Node-Module und Kommandozeilen-Aufrufe aus, sie erstellen mehrere Applikationen parallel usw. Ein guter Task-Runner zeichnet sich durch eine auf die Erstellung von Builds optimierte Syntax (oder DSL) aus. Zudem lässt er sich einfach erweitern und stellt eine schnellstmögliche Ausführung des Builds sicher.

gulp.task('less', () => {

return gulp.src('src/app.less')

.pipe(sourcemaps.init())

.pipe(less({compress: true}))

.pipe(sourcemaps.write('./'))

.pipe(gulp.dest('./dist'));

});

Das Beispiel zeigt die Definition des Less-Tasks mit Gulp. Über sogenannte Streams lassen sich Dateien durch einen Build “pipen“. Sehr schön sieht man hier die Build-spezifische Syntax. Zudem dient das Stream-Konzept der Build-Performance. Das vollständige und lauffähige Beispiel findet sich hier.

Task-Runner spielen ihre Vorteile dann aus, wenn ein Build sehr gross wird und/oder nicht mit herkömmlichen Node-Modulen bewerkstelligt werden kann. So ergibt der Einsatz eines Task-Runners meines Erachtens dann Sinn, wenn ein Projekt neben dem Frontend weitere Projekte wie zusätzliche Frontends oder Backends in anderen Sprachen beinhaltet.

Fazit

NPM Scripts sind ein guter Start und decken die grundlegenden Anforderungen an einen Build ab. Bundler werden bei ES6 Modulen und/oder entsprechenden Performance-Anforderungen eingesetzt, schieben aber eine zusätzliche Schicht zwischen die Build-Definition und dem ausführenden Node-Modul. Mit dem gleichen Nachteil behaftet sind Task-Runner, die aber in Multi-Projekt-Szenarien gute Dienste leisten.

Es bewährt sich, eine der beschriebenen Varianten zu wählen und bei dieser zu bleiben. Eine Vermischung der Varianten führt zu Verzettelung, höherer Komplexität und letzten Endes einem weniger gut wartbarem Build.

Kommentare (0)

×

Updates

Schreiben Sie sich jetzt ein für unsere zwei-wöchentlichen Updates per E-Mail.

This field is required
This field is required
This field is required

Mich interessiert

Select at least one category
You were signed up successfully.