November 2016

Band 31, Nummer 11

Als Programmierer mit dem MEAN-Stapel arbeiten: Gulp

Von Ted Neward | November 2016

Ted NewardWillkommen zurück, Freunde des MEAN-Stapels.

Wenn Sie meine letzte Kolumne vom Oktober gelesen haben, in der ich über den „Neustart“ der Codebasis gesprochen habe (mithilfe von Yeoman, um die Grundlagen und den Verbindungscode [Glue Code] mit einem Gerüst zu versehen), wird Ihnen vielleicht aufgefallen sein, dass da von einem neuen Tool die Rede war, das nicht weiter erläutert wurde. Ich meine natürlich das Tool Gulp, das zum Starten des Servers und Öffnen des Clientbrowsers mit der erstellten Clientanwendung verwendet wird (msdn.com/magazine/mt742874).

Wenn Sie meine letzte Kolumne verpasst haben, können Sie das Versäumte leicht nachholen. Stellen Sie zunächst sicher, dass sowohl Yeoman als auch die „angular-fullstack“-Generatoren in Ihrer Node.js-Entwicklungsumgebung (sowie eine lokal ausgeführte Kopie von MongoDB) ausgeführt werden:

npm install –g yeoman angular-fullstack-generator
yo angular-fullstack

Nach Beantwortung von Fragen dazu, welche Tools Yeoman einrüsten soll (für diese Kolumne sind die gewählten Optionen meist irrelevant), und nachdem Yeoman hilfreicherweise den Befehl „npm install“ ausgeführt hat, um alle Laufzeit- und Entwicklungsabhängigkeiten abzurufen, meldet der Scaffolder, dass die Anwendung nun zum Testen bereits ist, indem „gulp test“ oder „gulp start:server“ ausgeführt wird.

Bei Gulp handelt es sich also um eine Art von Buildtool vergleichbar mit Make, MSBuild oder Ant. Doch seine Funktionsweise unterscheidet sich von diesen drei Tools, weshalb wir uns näher damit beschäftigen wollen.

Einleitende Worte zu Gulp

Wenngleich es nicht ganz korrekt ist, Gulp als „Buildtool“ für eine Sprache zu bezeichnen, für die es keine Builds gibt (zur Erinnerung: ECMAScript ist im Allgemein als zu interpretierende Sprache vorgesehen), so ist diese Bezeichnung jedoch am besten für ein Tool, das nach (oder während) der Entwicklung ausgeführt werden soll, um sicherzustellen, dass alles in der Reihe und startklar ist. Eine vielleicht bessere Bezeichnung für alle diese Tools wäre „Entwicklungsautomatisierungstool“, da dies treffender ist und auch den Akt der Kompilierung des Codes und seiner Assemblierung zu einem bereitstellbaren Artefakt berücksichtigt. Da dies aber ziemlich lang ist und „Buildtool“ leichter von der Zunge geht, hängen wir uns an die Vorstellung an, das Gulp ein Buildtool ist.

Lassen Sie uns zum Einstieg in Gulp vom vorherigen Gerüstcode lösen und ganz von vorn anfangen, um uns darauf zu konzentrieren, was Gulp genau wie macht. Installieren Sie zuerst die globalen Gulp-Befehlszeilentools (npm install --g gulp-cli). Erstellen Sie dann in einem neuen Verzeichnis ein leeres Node.js-Projekt, indem Sie Folgendes ausführen:

npm init
npm install --save-dev gulp

Dies sorgt dafür, dass auf Gulp in der Datei „package.json“ als Entwicklungsabhängigkeit verwiesen wird. Dies erfolgt so, dass Sie beim Herunterladen des Projekts sich nicht erinnern müssen, Gulp zu installieren, denn es wird zu einem Bestandteil bei der nächsten Ausführung von „npm install“ in einer neuen Umgebung.

Erstellen Sie nun im Text-Editor Ihrer Wahl die neue Datei „gulpfile.js“:

const gulp = require('gulp');
gulp.task('default', function() {
  console.log("Gulp is running!");
});

Rufen Sie anschließend im selben Verzeichnis den Gulp-Standardbefehl auf, der (wenig überraschend) nur „gulp“ lautet. Gulp denkt darüber einen Moment nach und gibt dann das hier zurück:

[18:09:38] Using gulpfile ~/Projects/code/gulpdemo/gulpfile.js
[18:09:38] Starting 'default'...
Gulp is running!
[18:09:38] Finished 'default' after 142 μs

Nicht schlecht. Scheint im Augenblick ein bisschen zu viel des Guten zu sein, aber nicht schlecht.

Ausführen von Aufgaben mit Gulp

Wie die meisten Buildtools arbeitet Gulp mit der Begrifflichkeit von „Aufgaben“, wobei das Nachverfolgen der Abhängigkeiten zwischen diesen Aufgaben im Vordergrund steht. In der einfachen Gulpfile hier erkennt Gulp, dass es eine Aufgabe namens „default“ gibt (dabei wird die wohlverstandene Konvention befolgt, die Aufgabe zu meinen, die ausgeführt werden soll, wenn in der Befehlszeile keine angegeben ist), und führt den Hauptteil des zugehörigen Funktionsliterals aus, wenn die Aufforderung zur Ausführung dieser Aufgabe erfolgt. Das Umbenennen der Aufgabe ist kinderleicht:

const gulp = require('gulp');
gulp.task('echo', function() {
  console.log("Gulp is running!");
});

Es dürfte augenfällig sein, dass Gulp bloß Code ist. Das heißt, dass alles, was mit Code angestellt werden kann, auch im Body einer Gulp-Aufgabe möglich ist. Dies bietet viele unglaubliche Möglichkeiten, wie beispielsweise das Lesen einer Datenbank zum Auffinden von Elementen, die mit Code generiert werden müssen, das Kommunizieren mit anderen Onlinediensten zum Zweck der Konfiguration oder das einfache Ausgeben des aktuellen Datums samt Uhrzeit:

const gulp = require('gulp');
gulp.task('echo', function() {
  console.log("Gulp is running on " + (new Date()));
});

Hiermit wird Folgendes generiert:

Teds-MacBook-Pro:gulpdemo tedneward$ gulp echo
[18:16:24] Using gulpfile ~/Projects/code/gulpdemo/gulpfile.js
[18:16:24] Starting 'echo'...
Gulp is running on Wed Sep 14 2016 18:16:24 GMT-0700 (PDT)
[18:16:24] Finished 'echo' after 227 μs

Abhängige Aufgaben werden einfach als Zeichenfolgenarray zwischen dem Namen der Aufgabe und dem Body ihres Funktionsliterals aufgeführt, sodass wenn „echo“ einer Aufgabe von einer anderen Aufgabe abhängt, dies wie folgt aussieht:

const gulp = require('gulp');
const child_process = require('child_process');
gulp.task('gen-date', function() {
  child_process.exec('sh date > curdate.txt');
});
gulp.task('echo', ['clean', 'gen-date'], function() {
  console.log("Gulp is running on " + (new Date()));
});

Hier nutzt die Aufgabe „gen-date“ das Node.js-Standardpaket „child_process“ zum Starten eines externen Tools zum Schreiben der Daten in eine Datei (nur um zu beweisen, dass das möglich ist). Das ist, ehrlich gesagt, alles schön und gut, doch im Allgemeinen wird von Buildtools etwas mit größerer Bedeutung erwartet, als bloß Daten in der Konsole auszugeben oder das aktuelle Datum samt Uhrzeit zu bestimmen.

Anspruchsvolleres mit Gulp

Lassen Sie uns das Ganze mit mehr Fleisch versehen. Erstellen Sie die Datei „index.js“, die den folgenden ECMAScript-Code einschließlich der nicht ganz so guten Codeabschnitte enthalten soll:

// index.js
function main(args) {
  for (let arg in args) {
    if (arg == "hello")
      console.log("world!");
      console.log("from index.js!");
    }
}
console.log("Hello, from index.js!")
main()

Ja, das ist ein bisschen unsinnig, und ja, da sind eindeutig einige Fehler dabei, doch das ist hier der Punkt. Es wäre schön, wenn es ein Tool gäbe, dass einige dieser Fehler erkennen und diese dann melden könnte. (Wage ich zu sagen „Prüfung zur Kompilierzeit“?) Zum Glück gibt es mit JSHint (jshint.com) ein solches Tool, doch es wird standardmäßig als Befehlszeilentool installiert, und es wäre nervig, wenn man sich die ganze Zeit merken müsste, dass es ausgeführt werden muss.

Genau hierfür gibt es erfreulicherweise Buildtools. Lassen Sie uns zur Gulpfile zurückkehren und Gulp auffordern, JSHint auf alle Quelldateien (von denen es im Moment nur eine gibt) anzuwenden, indem es über die betreffenden Quelldateien informiert und aufgefordert wird, JSHint auf alle anzuwenden:

// Don't need it in this file, but we need it installed
require('jshint');
require('jshint-stylish');
const gulp = require('gulp');
const jshint = require('gulp-jshint');
gulp.task('jshint', function() {
  return gulp.src('*.js')
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'));
});

Wenn Sie dies ausführen, wird das Tool angeben, dass es einige vorgeschlagene Änderungen an allen JS-Dateien im aktuellen Verzeichnis (einschließlich der Gulpfile selbst) gibt. Oohh. Da Sie die Gulpfile nicht wirklich überprüfen wollen, filtern wir sie aus der Sammlung der auszuführenden Dateien heraus:

gulp.task('jshint', function() {
  return gulp.src(['*.js', '!gulpfile.js'])
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'));
});

Dadurch wird „gulpfile.js“ aus der aktuellen Liste der Dateien entfernt, ehe sie an die nächste Phase in der Pipeline übergeben wird. Freilich möchten Sie wohl am liebsten, dass sich Code in einem Verzeichnis des Typs „src“ (oder in den Verzeichnissen „server“ und „client“ für jede Seite) befindet, weshalb Sie diese und beliebige ihrer Unterverzeichnisse der Liste der zu verarbeitenden Dateien hinzufügen:

gulp.task('jshint', function() {
  return gulp.src(['*.js', '!gulpfile.js', 'server/**/*.js', 'client/**/*.js'])
    .pipe(jshint())
    .pipe(jshint.reporter('jshint-stylish'));
});

Mithilfe des doppelten Sterns in jedem Pfad wird das rekursive Verhalten aktiviert, alle JS-Dateien in allen diesen Unterverzeichnissen auszuwählen.

Oberflächlich betrachtet, ist das toll, doch können Sie damit nicht allzu viel erreichen: Sie müssen immer noch „gulp jshint“ (oder bloß „gulp“, wenn Sie das Tool an die Aufgabe „default“ als Abhängigkeit gebunden haben) immer dann manuell eingeben, wenn Sie sehen möchten, was korrigiert werden muss. Warum kann das Tool nicht immer ausgeführt werden, wenn sich der Code ändert, wie bei IDEs?

Doch, das geht (siehe Abbildung 1).

Abbildung 1: Kontinuierliche Automatisierung mit Gulp

// Don't need it in this file, but you need it installed
require('jshint');
const gulp = require('gulp');
const jshint = require('gulp-jshint');
gulp.task('default', ['watch']);
gulp.task('watch', function() {
  gulp.watch(['*.js', '!gulpfile.js', 'client/**/*.js', 'server/**/*.js'],
    ['jshint']);
});
gulp.task('jshint', function() {
  return gulp.src(['*.js', '!gulpfile.js', 'client/**/*.js', 'server/**/*.js'])
    .pipe(jshint())
    .pipe(jshint.reporter('default'));
});

Wenn Sie „gulp“ jetzt ausführen, wird die Befehlszeile einfach angehalten und wartet. Gulp ist nun im Beobachtungsmodus „watch“, in dem das Tool die Dateien überwacht, die an den Aufruf „gulp.watch“ übergeben werden. Wenn sich beliebige dieser Dateien ändern sollten (was heißt, dass sie gespeichert wurden, denn Gulp kann leider nicht in Text-Editoren hineinblicken), ruft das Tool sofort die Aufgabe „jshint“ für sämtliche Dateien auf. Und setzt die Beobachtung fort.

Mehr über Gulp

Einer der Schlüssel zum Verstehen, wie Dateien mit Gulp-Aufgaben verarbeitet werden, findet sich im Aufruf an „pipe“. Gulp arbeitet mit Datenströmen anstatt mit Aufgaben oder Dateien. Eine einfache Gulp-Aufgabe zum Kopieren von Dateien aus dem Verzeichnis „src“ in das Verzeichnis „dest“ sieht z. B. so aus:

gulp.task('copy-files', function() {
  gulp.src('source/folder/**')
    .pipe( gulp.dest('dest/folder/**') );
});

Im Wesentlichen werden die Dateien von „gulp.src“ einzeln ausgewählt und dann (unbearbeitet) im Ziel abgelegt, das mit „gulp.dest“ angegeben wird. Alles, was mit diesen Dateien passieren muss, fließt in einen Schritt in der Pipeline ein, und jede Datei durchläuft diese Pipeline, ehe mit dem nächsten Schritt in der Pipeline fortgefahren wird. Dies ist der UNIX-Architekturstil von „Pipes und Filtern“, der auf sehr elegante Weise in das Node.js-Ökosystem integriert wurde. Windows PowerShell setzt auf derselben Art von Architektur auf, falls einige von Ihnen meinen sollten, dass sie so etwa schon einmal im .NET-Universum gesehen hätten.

Wenn Sie z. B. nur möchten, dass Gulp jede Datei über die Pipeline anfasst, gibt es ein Plug-In (gulp-filelogger) dafür. Es erfolgt eine Ausgabe in der Konsole für jede Datei, die das Tool anfasst:

gulp.task('copy-files', function () {
  gulp.src(srcFiles)
    .pipe(filelogger())
    .pipe(gulp.dest(destDir));
});

Dies führt zu Folgendem:

Teds-MacBook-Pro:gulpdemo tedneward$ gulp copy-files
[20:14:01] Using gulpfile ~/Projects/code/gulpdemo/gulpfile.js
[20:14:01] Starting 'copy-files'...
[20:14:01] Finished 'copy-files' after 14 ms
[20:14:01] [/Users/tedneward/Projects/code/gulpdemo/index.js]
Teds-MacBook-Pro:gulpdemo tedneward$

Sehen Sie sich an, wie die Ausgabe angezeigt wird, nachdem Gulp seine Arbeit abgeschlossen hat. Gulp verarbeitet diese Datenströme zumeist asynchron, wodurch sich die Buildzeiten verkürzen. Die meiste Zeit wissen Entwickler nicht, dass Aufgaben parallel erledigt werden (oder es ist ihnen egal). Doch für die Fälle, in denen die Ausführung von Aufgaben in einer präzisen Reihenfolge wichtig ist, bietet die Gulp-Plug-In-Community wenig überraschend einige Plug-Ins, die die Ausführung serialisieren und sicherstellen, dass alles in der gewünschten Reihenfolge erfolgt. Gulp 4.0 fügt mit „parallel“ und „serial“ zwei neue Funktionen hinzu, um dies zu verdeutlichen, doch da diese Version noch nicht veröffentlicht wurde, müssen Sie darauf warten.

Im Übrigen besteht Gulp selbst bloß aus den vier Funktionen, die Sie bislang kennengelernt haben: „gulp.task“, „gulp.watch“, „gulp.src“ und „gulp.dest“. Alles andere sind Plug-Ins, npm-Module oder manuell geschriebener Code. Dadurch ist Gulp an und für sich extrem einfach zu verstehen. Schon fast deprimierend einfach für Autoren von Artikeln, die nach Wort bezahlt werden.

Noch mehr zu Gulp

Gulp ist für sich genommen kein besonders kompliziertes Tool. Doch wie bei allen Tools dieser Art liegt seine wirkliche Stärke in der breiten Palette von Plug-Ins und Ergänzungstools, die in der zugehörigen Community entwickelt werden. Die vollständige Liste finden Sie unter gulpjs.com/plugins. Doch Abbildung 2 zeigt ein repräsentatives Beispiel eines Gulp Recipes, das veranschaulicht, wie die Freigabe eines Projekts auf GitHub automatisiert wird, einschließlich der Git-Befehle für „push“ zu „master“.

Abbildung 2: Gulp Recipe

var gulp = require('gulp');
var runSequence = require('run-sequence');
var conventionalChangelog = require('gulp-conventional-changelog');
var conventionalGithubReleaser = require('conventional-github-releaser');
var bump = require('gulp-bump');
var gutil = require('gulp-util');
var git = require('gulp-git');
var fs = require('fs');
gulp.task('changelog', function () {
  return gulp.src('CHANGELOG.md', {
    buffer: false
  })
    .pipe(conventionalChangelog({
      preset: 'angular' // Or to any other commit message convention you use.
    }))
    .pipe(gulp.dest('./'));
});
gulp.task('github-release', function(done) {
  conventionalGithubReleaser({
    type: "oauth",
    token: '' // Change this to your own GitHub token.
  }, {
    preset: 'angular' // Or to any other commit message convention you use.
  }, done);
});
gulp.task('bump-version', function () {
// Hardcode the version change type to "patch," but it might be a good
// idea to use minimist (bit.ly/2cyPhfa) to determine with a command
// argument whether you're doing a "major," "minor" or a "patch" change.
  return gulp.src(['./bower.json', './package.json'])
    .pipe(bump({type: "patch"}).on('error', gutil.log))
    .pipe(gulp.dest('./'));
});
gulp.task('commit-changes', function () {
  return gulp.src('.')
    .pipe(git.add())
    .pipe(git.commit('[Prerelease] Bumped version number'));
});
gulp.task('push-changes', function (cb) {
  git.push('origin', 'master', cb);
});
gulp.task('create-new-tag', function (cb) {
  var version = getPackageJsonVersion();
  git.tag(version, 'Created Tag for version: ' + version, function (error) {
    if (error) {
      return cb(error);
    }
    git.push('origin', 'master', {args: '--tags'}, cb);
  });
  function getPackageJsonVersion () {
    // Parse the json file instead of using require because require caches
    // multiple calls so the version number won't be updated.
    return JSON.parse(fs.readFileSync('./package.json', 'utf8')).version;
  };
});
gulp.task('release', function (callback) {
  runSequence(
    'bump-version',
    'changelog',
    'commit-changes',
    'push-changes',
    'create-new-tag',
    'github-release',
    function (error) {
      if (error) {
        console.log(error.message);
      } else {
        console.log('RELEASE FINISHED SUCCESSFULLY');
      }
      callback(error);
    });
});

Dieses Beispiel zeigt verschiedene Dinge: Ausführen von Aufgaben in einer bestimmten Reihenfolge, Verwenden des Gulp-Plug-Ins zum Generieren einer Changesetdatei für Konventionen, Auslösen von Freigabenachrichten im GitHub-Stil, Ausführen von „Bump“ für die semantische Version und vieles mehr. Alles bei der Gulp-Freigabe, was von hoher Leistungsfähigkeit zeugt.

Zusammenfassung

Das war kein besonders codelastiger Artikel. Dennoch haben Sie die gesamte Anwendung neu gestartet, sich viel zusätzliche Funktionalität gesichert und die Anwendung im Wesentlichen auf das Niveau (und darüber hinaus) von dem gebracht, was Sie im vergangenen Jahr entwickelt haben. Ein Hoch auf den Gerüstbau!

Noch wichtiger ist, nachdem Sie alle Teile einzeln manuell entwickelt hatten, ehe der Gerüstbau erfolgt ist, es wesentlich einfacher ist, den Code als Ganzes und den jeweiligen Ablauf zu verstehen. Die geöffnete Datei „routes.js“ sieht fast so aus wie die Routingtabelle, die Sie zuvor manuell erstellt haben, und die Datei „package.json“ (im Stammverzeichnis des Projektverzeichnisses) wird größer, bleibt aber dieselbe wie die, die Sie verwendet haben.

Das einzig Neue (über die Nutzung von Yeoman selbst hinaus) ist tatsächlich die Einführung eines Buildtools zum Sammeln aller zugehörigen Teile an der richtigen Stelle. Darüber spreche ich beim nächsten Mal. Bis dahin ... viel Spaß beim Programmieren!


Ted Neward ist ein in Seattle ansässiger Berater, Redner und Mentor für zahlreiche Technologien. Er ist F# MVP und hat mehr als 100 Artikel geschrieben und als Autor und Mitautor ein Dutzend Bücher verfasst. Sie können ihn unter ted@tedneward.com erreichen, wenn sie möchten, dass er in Ihrem Team arbeitet, oder unter blogs.tedneward.com seinen Blog lesen.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Shawn Wildermuth