Freigeben über



Dezember 2015

Band 30, Nummer 13

Visual Studio – Moderne Tools für die Webentwicklung: Grunt und Gulp

Von Adam Tuliper | Dezember 2015

Dem modernen Webentwickler stehen viele Tools zur Verfügung. Zwei, die man in den Webprojekten von heute häufig findet, sind die JavaScript-Task Runner Grunt und Gulp. JavaScript zum Ausführen einer Aufgabe zu verwenden, mag sich wie ein sehr seltsames Konzept anhören, wenn man es noch nie gemacht hat oder an die Webentwicklung auf gebahnten Pfaden in Visual Studio Web gewöhnt ist, aber es gibt gute Gründe, es einmal auszuprobieren. JavaScript-Task Runner, die außerhalb des Browsers agieren und normalerweise „Node.js“ auf der Befehlszeile verwenden, ermöglichen Ihnen, auf einfache Weise Aufgaben im Zusammenhang mit der Front-End-Entwicklung auszuführen, einschließlich Minimierung (Minification), Verkettung mehrerer Dateien, Bestimmen von Skriptabhängigkeiten und Injizieren von Skriptverweisen in HTML-Seiten in der richtigen Reihenfolge, Erstellen von Komponententestumgebungen, Verarbeiten von Front End-Buildskripts wie TypeScript oder CoffeeScript und mehr.

Welches – Grunt oder Gulp?

Das Auswählen eines Task Runners kann nach persönlichen Vorlieben oder Gegebenheiten des Projekts erfolgen, es sei denn, Sie möchten ein bestimmtes Plug-In verwenden, das nur einen bestimmten Task Runner unterstützt. Die wichtigsten Unterschiede liegen darin, dass Grunt durch JSON-Konfigurationseinstellungen gesteuert ist und jede Grunt-Aufgabe normalerweise Zwischendateien erstellen muss, um Dinge an andere Aufgaben abzugeben, während Gulp durch ausführbaren JavaScript-Code gesteuert ist (also nicht einfach JSON) und Ergebnisse von einer Aufgabe per Streaming an eine andere Aufgabe übertragen kann, ohne dazu temporäre Dateien zu benötigen. Gulp ist das neuere Produkt und wird daher in vielen neueren Projekten verwendet. Trotzdem verfügt Grunt über eine Vielzahl von bekannten Unterstützern, wie etwa jQuery, das es verwendet, um… jQuery zu erstellen. Sowohl Grunt als auch Gulp funktionieren mithilfe von Plug-Ins, bei denen es sich um Module handelt, die für bestimmte Aufgaben installiert werden. Das Ökosystem der verfügbaren Plug-Ins ist riesig, und häufig findet man ein Aufgabenpaket, das sowohl Grunt als auch Gulp unterstützt, sodass es auch an dieser Stelle wieder vor allem um persönliche Vorlieben geht.

Installieren und Verwenden von Grunt

Das Installationsprogramm für Grunt ebenso wie für Gulp ist der Knotenpaket-Manager (NPM, Node Package Manager), den ich kurz in meinem Artikel im Oktober besprochen habe (msdn.com/magazine/mt573714). Der Befehl zur Installation von Grunt besteht tatsächlich aus zwei Teilen. Der erste ist die ein Mal vorzunehmende Installation der Grunt-Befehlszeilenoberfläche. Der zweite besteht in der Installation von Grunt in Ihrem Projektordner. Aufgrund dieser in zwei Teilen erfolgenden Installation können Sie mehrere Versionen von Grunt auf Ihrem System verwenden und die Grunt-Befehlszeilenoberfläche von jedem Pfad aus verwenden:

#only do this once to globally install the grunt command line runner
npm install –g grunt-cli
#run within your project folder one time to create package.json
#this file will track your installed npm dependencies
#like grunt and the grunt plug-ins, similar to bower.json (see the last article)
npm init
#install grunt as a dev dependency into your project (again see last article)
#we will still need a gruntfile.js though to configure grunt
npm install grunt –save-dev

Grunt-Konfiguration

Die Grunt-Konfigurationsdatei ist einfach eine JavaScript-Datei mit einer Wrapperfunktion, die die Konfiguration, das Laden von Plug-Ins und die Aufgabendefinition enthält, wie in Abbildung 1 dargestellt.

Abbildung 1 Die Grunt-Konfigurationsdatei

module.exports = function (grunt) {
  // Where does uglify come from? Installing it via:
  // npm install grunt-contrib-uglify --save-dev
  grunt.initConfig({
    uglify: {
      my_target: {
        files: {
          'dest/output.min.js': '*.js'
        }
      }
    }
  });
  // Warning: make sure you load your tasks from the
  // packages you've installed!
  grunt.loadNpmTasks('grunt-contrib-uglify');
  // When running Grunt at cmd line with no params,
  // you need a default task registered, so use uglify
  grunt.registerTask('default', ['uglify']);
  // You can include custom code right inside your own task,
  // as well as use the above plug-ins
  grunt.registerTask('customtask', function () {
    console.log("\r\nRunning a custom task");
  });
};

Sie können die Aufgaben in Abbildung 1 einfach durch diesen Aufruf auf der Befehlszeile ausführen:

#no params means choose the 'default' task (which happens to be uglify)
grunt
#or you can specify a particular task by name
grunt customtask

Anschließend finden Sie das (per Uglification) minimierte und verkettete Ergebnis unter „wwwroot/output-min.js“. Wenn Sie ASP.NET-Minimierung und -Bündelung verwendet haben, werden Sie bemerken, dass dieser Vorgang ein anderer ist – er ist nicht an die Ausführung Ihrer App oder auch nur die Kompilierung gebunden, und es gibt viele weitere Optionen für Aufgaben auszuwählen, wie etwa Uglifying. Ich persönlich finde diesen Workflow geradliniger und besser verständlich.

Sie können Aufgaben mit Grunt so verketten, dass sie voneinander abhängig sind. Diese Aufgaben werden synchron ausgeführt, sodass eine abgeschlossen sein muss, bevor mit der nächsten fortgefahren wird.

#Specify uglify must run first and then concat. Because grunt works off
#temp files, many tasks will need to wait until a prior one is done
grunt.registerTask('default', ['uglify', 'concat']);

Installieren und Verwenden von Gulp

Die Installation von Gulp ist ähnlich der Installation von Grunt. Ich gehe bei Gulp etwas mehr ins Detail, beachten Sie aber, dass mit beiden ähnliche Dinge möglich sind; ich möchte lediglich Wiederholungen vermeiden. Gulp verfügt sowohl über eine globale Installation, dank derer Sie es von jedem beliebigen Pfad auf ihrem System aus verwenden können, als auch eine lokale Installation in Ihrem Projektordner, die Versionsinformationen für das jeweilige Projekt enthält. Globale Gulp-Installationen übergeben die Steuerung an das lokale Projekt, wenn sie es finden, und tragen so Ihrer Projektversion von Gulp Rechnung:

#Only do this once to globally install gulp
npm install –g gulp
#Run within your project folder one time to create package.json
#this file will track your installed npm dependencies
#like gulp and the gulp plug-ins
npm init
#Install gulp as a dev dependency into your project
#we will still need a gulpfile.js to configure gulp
npm install gulp --save-dev

Gulp-Konfiguration und API

Die Konfiguration von Gulp unterscheidet sich stark von der von Grunt. Die Konfigurationsdatei „gulpfile.js“, die normalerweise die in Abbildung 2 dargestellte Struktur aufweist, enthält die Voraussetzungen (Requires) für das Laden von Plug-Ins und das anschließende Definieren von Aufgaben. Beachten Sie, dass ich hier keine JSON-Konfigurationseinstellungen verwende; die Aufgaben sind vielmehr codegesteuert.

Abbildung 2 Die Gulp-Konfigurationsdatei

// All the 'requires' to load your
// various plug-ins and gulp itself
var gulp = require('gulp');
var concat = require('gulp-concat');
// A custom task, run via: gulp customtask
gulp.task('customtask', function(){
  // Some custom task
});
// Define a default task, run simply via: gulp
gulp.task('default', function () {
  gulp.src('./lib/scripts/*.js')
    .pipe(concat('all-scripts.js'))
    .pipe(gulp.dest('./wwwroot/scripts'));
});

Gulp arbeitet mithilfe verschiedener Schlüssel-APIs und -Konzepte: „src“, „dest“, „pipe“, „task“ und „globs“. Die „gulp.src“-API teilt Gulp mit, welche Dateien für die Arbeit geöffnet werden müssen, und diese Dateien werden dann normalerweise per Pipebefehl an eine andere Funktion weitergereicht, statt dass temporäre Dateien erstellt werden. Das ist ein wichtiger Unterschied zu Grunt. Das folgende Beispiel gibt ein paar einfache Beispiele für „gulp.src“ ohne Umleitung der Ergebnisse, worauf ich bald eingehe. Dieser API-Aufruf nimmt als Parameter das an, was als Glob bezeichnet wird. Ein Glob ist im Wesentlichen ein Muster, das Sie eingeben können (so etwa wie ein regulärer Ausdruck), um beispielsweise einen Pfad zu einer oder mehreren Dateien anzugeben (mehr über Globs erfahren Sie unter github.com/isaacs/node-glob):

#Tell gulp about some files to work with
gulp.src('./input1.js');
#This will represent every html file in the wwwroot folder
gulp.src('./wwwroot/*.html')
#You can specify multiple expressions
gulp.src(['./app/**/*.js', './app/*.css']

Die „dest“-API (destination) legt ein Ziel fest und akzeptiert ebenfalls ein Glob. Da Globs eine so große Flexibilität beim Definieren von Pfaden bieten, können Sie entweder einzelne Dateien ausgeben oder die Ausgabe in einen Ordner festlegen:

#Tell dest you'll be using a file as your output
gulp.dest ('./myfile.js');
#Or maybe you'll write out results to a folder
gulp.dest ('./wwwroot');

Aufgaben sind in Gulp einfach der Code, den Sie erstellen, um etwas auszuführen. Das Format für Aufgaben ist ziemlich einfach, aber Aufgaben können auf verschiedene Weise verwendet werden. Das einfachste Verfahren verwendet eine Standardaufgabe und eine oder mehr weitere Aufgaben:

gulp.task('customtask', function(){
  // Some custom task to ex. read files, add headers, concat
});
gulp.task('default', function () {
  // Some default task that runs when you call gulp with no params
});

Aufgaben können parallel ausgeführt werden oder voneinander abhängen. Wenn Ihnen die Reihenfolge gleichgültig ist, können Sie die Aufgaben einfach zur Kette verbinden, wie hier:

gulp.task('default', ['concatjs', 'compileLess'], function(){});

Dieses Beispiel definiert die Standardaufgabe, die nichts weiter tut als einzelne Aufgaben auszuführen, um JavaScript-Dateien zu verketten und LESS-Dateien zu kompilieren (unter der Annahme, dass sich in den hier genannten Aufgaben Code befand). Wenn sich aus den Anforderungen ergibt, dass eine Aufgabe vor der Ausführung einer anderen abgeschlossen sein muss, müssen sie eine Aufgabe als von einer anderen abhängig festlegen, bevor Sie mehrere Aufgaben ausführen. Im folgenden Quellcode wartet die Standardaufgabe auf den Abschluss von „concat“, das seinerseits auf den Abschluss von „uglify“ wartet:

gulp.task('default', ['concat']);
gulp.task('concat', ['uglify'], function(){
  // Concat
});
gulp.task('uglify', function(){
  // Uglify
});

Die „pipe“-API wird verwendet, um Ergebnisse mithilfe der „Node.js“-Datenstrom-API von einer Funktion an eine andere weiterzureichen. Der Workflow sieht normalerweise so aus: „src“ lesen, Ergebnis an eine Aufgabe weiterreichen, deren Ergebnisse an „dest“ weiterreichen. Dieses vollständige „gulpfile.js“-Beispiel liest alle JavaScript-Dateien in „/scripts“, verkettet sie zu einer einzelnen Datei, und schreibt die Ausgabe in einen anderen Ordner:

// Define plug-ins – must first run: npm install gulp-concat --save-dev
var gulp = require('gulp');
var concat = require('gulp-concat');
gulp.task('default', function () {
  #Get all .js files in /scripts, pipe to concatenate, and write to folder
  gulp.src('./lib/scripts/*.js')
    .pipe(concat('all-scripts.js'))
    .pipe(gulp.dest('./wwwroot/scripts'));
}

Hier ein praktisches Beispiel aus der Wirklichkeit. Häufig möchten Sie Dateien verketten und/oder Ihren Quellcodedateien Header mit Informationen hinzufügen. Das können Sie leicht in wenigen Schritten erreichen, indem Sie dem Stamm Ihrer Website ein paar Dateien hinzufügen (Sie könnten diesen Vorgang ebenso gut in Code erledigen – in der gulp-header-Dokumentation finden Sie mehr dazu). Erstellen Sie zuerst eine Datei mit dem Namen „copyright.txt“, die die hinzuzufügenden Header enthält, etwa so:

/*
MyWebSite Version <%= version %>
https://twitter.com/adamtuliper
Copyright 2015, licensing, etc
*/

Erstellen Sie als Nächstes eine Datei mit dem Namen „version.txt“, die die aktuelle Versionsnummer enthält (es gibt auch Plug-Ins, wie gulp-bump und grunt-bump, zum Hochzählen von Versionsnummern ):

1.0.0

Installieren Sie jetzt die Plug-Ins gulp-header und gulp-concat in Ihrem Projektstamm:

npm install gulp-concat gulp-header --save-dev

Alternativ können Sie sie der „package.json“-Datei manuell hinzufügen und die Paketwiederherstellung von Visual Studio ausführen lassen.

Schließlich benötigen Sie noch „gulpfile.js“, um Gulp mitzuteilen, was zu tun ist, wie in Abbildung 3 dargestellt. Wenn Sie nicht alle Ihre Skripts verketten und stattdessen jeder Datei die Header hinzufügen möchten, können Sie einfach die Zeile „pipe(concat)“ auskommentieren. Ganz einfach, oder?

Abbildung 3 Gulpfile.js

var gulp = require('gulp');
var fs = require('fs');
var concat = require("gulp-concat");
var header = require("gulp-header");
// Read *.js, concat to one file, write headers, output to /processed
gulp.task('concat-header', function () {
  var appVersion = fs.readFileSync('version.txt');
  var copyright =fs.readFileSync('copyright.txt');
  gulp.src('./scripts/*.js')
  .pipe(concat('all-scripts.js'))
  .pipe(header(copyright, {version: appVersion}))
  .pipe(gulp.dest('./scripts/processed'));
});

Anschließend führen Sie die Aufgabe einfach mit dem folgenden Befehl aus, und, tataa, Sie haben alle JS-Dateien verkettet, einen benutzerdefinierten Header hinzugefügt und die Ausgabe in den Ordner „./scripts/processed“ geschrieben:

gulp concat-header

Der Task Runner-Explorer

Visual Studio stellt Unterstützung für Gulp und Grunt in Form des Task Runner-Explorers bereit, der in Visual Studio 2015 enthalten und als Visual Studio-Erweiterung verfügbar ist. Sie finden ihn unter „Ansicht“ | „Weitere Fenster“ | „Task Runner-Explorer“. Der Task Runner-Explorer ist ziemlich cool, denn er erkennt, ob im Projekt ein „gulpfile.js“ oder „gruntfile.js“ vorhanden ist, analysiert die Aufgaben und stellt eine Benutzeroberfläche zum Ausführen der gefundenen Aufgaben bereit, wie in Abbildung 4 zu sehen. Außerdem können Sie festlegen, dass Aufgaben ausgeführt werden, wenn im Projekt vordefinierte Aktionen auftreten, worauf ich als Nächstes eingehe, da ASP.NET 5 diese Funktionalität in seinen Standardvorlagen verwendet. Klicken Sie einfach mit der rechten Maustaste auf eine Aufgabe, um sie auszuführen oder sie an eine bestimmte Aktion zu binden, um beispielsweise bei „Project Open“ eine Aufgabe auszuführen.

Task Runner-Explorer mit Grunt- und Gulp-Aufgaben mit Optionen
Abbildung 4 Task Runner-Explorer mit Grunt- und Gulp-Aufgaben mit Optionen

Wie in Abbildung 5 dargestellt, bietet Grunt Optionen, die beim Hervorheben einer Grunt-Aufgabe angezeigt werden, die bei Gulp nicht angezeigt werden, insbesondere die Optionen „Force“ (zum Ignorieren von Warnungen) und „Verbose“ (das „F“ und „V“ oben links). Das sind einfach Parameter, die an die Grunt-Befehlszeile übergeben werden. Zu den großen Vorteilen beim Task Runner-Explorer zählt, dass er die Befehle, die an die Grunt- und Gulp-Befehlszeile übergeben werden, anzeigt (im Ausgabefenster der Aufgabe), sodass es kein Rätselraten über die Vorgänge hinter den Kulissen gibt.

Zusätzliche Grunt-Optionen und die Befehlszeile
Abbildung 5 Zusätzliche Grunt-Optionen und die Befehlszeile

Gulp und ASP.NET 5

Die in Visual Studio 2015 enthaltenen ASP.NET 5-Vorlagen verwenden Gulp und installieren Gulp im Ordner „node_components“ Ihres Projekts, sodass Sie es in Ihrem Projekt sofort verwenden können. Natürlich können Sie in einem ASP.NET 5-Projekt trotzdem Grunt verwenden; Sie müssen nur daran denken, es mithilfe von NPM in Ihrem Projekt zu installieren oder es „packages.json“ in den „devDependencies“ hinzuzufügen und die Funktion zur automatischen Paketwiederherstellung in Visual Studio ihre Arbeit tun zu lassen. Ich möchte explizit darauf hinweisen: Sie können alle diese Aktionen auf der Befehlszeile oder in Visual Studio ausführen, was immer Ihnen lieber ist.

Zum Zeitpunkt, da dieser Artikel entsteht, enthält die aktuelle ASP.NET 5-Vorlage zwei Aufgaben zum Minimieren und Verketten von CSS- und JS-Dateien. In früheren Versionen von ASP.NET wurden diese Aufgaben zur Laufzeit im kompilierten Code ausgeführt, was nicht der richtige Ort und nicht der richtige Zeitpunkt für diese Art von Aufgaben ist. Wie Sie in Abbildung 6 sehen können, rufen die Aufgaben „clean“ und „min“ ihre CSS- und JS-Methoden auf, um diese Dateien zu minimieren oder zuvor minimierte Dateien zu bereinigen.

Vorkonfigurierte Aufgaben in den Vorlagen von ASP.NET 5 Vorschau
Abbildung 6 Vorkonfigurierte Aufgaben in den Vorlagen von ASP.NET 5 Vorschau

Die folgende „Gruntfile.js“-Zeile zeigt ein weiteres Beispiel für das gleichzeitige Ausführen mehrerer Aufgaben:

gulp.task("clean", ["clean:js", "clean:css"]);

Sie können Grunt- und Gulp-Aufgaben in Visual Studio an vier verschiedene Aktionen binden. Bei MSBuild war es üblich, für die Zeitpunkte vor dem Erstellen und nach dem Erstellen getrennte Befehlszeilen zu definieren, um verschiedene Aufgaben auszuführen. Im Task Runner-Explorer können Sie die Ereignisse „Before Build“, „After Build“, „Clean“ und „Project Open“ definieren, um diese Aufgaben auszuführen. Dabei werden den „gulpfile.js“- oder „gruntfile.js“-Dateien einfach Kommentare hinzugefügt, die sich nicht auf die Ausführung auswirken, nach denen der Task Runner-Explorer aber Ausschau hält. Um z. B. die „clean“-Bindung in der ASP.NET 5-„gulpfile.js“ zu sehen, werfen Sie einen Blick auf diese Zeile oben in der Datei:

// <binding Clean='clean' />

Mehr ist nicht erforderlich, um sich in das Ereignis einzuklinken.

Zusammenfassung

Sowohl Grunt als auch Gulp stellen tolle Erweiterungen Ihres Webarsenals dar. Beide werden gut unterstützt und bieten ein riesiges Ökosystem an verfügbaren Plug-Ins. Jedes Webprojekt kann von einigen ihrer Features profitieren. Für weitere Informationen sollten Sie sich unbedingt die folgenden Quellen ansehen:


Adam Tuliper ist ein Senior Technical Evangelist bei Microsoft und lebt im sonnigen Südkalifornien. Er ist ein Web- und Spieleentwickler, Pluralsight-Autor und ein vielseitig interessierter Techniknarr. So finden Sie ihn auf Twitter: @AdamTuliper, oder besuchen Sie seinen Adam’s Garage-Blog unter bit.ly/1NSAYxK.

Unser Dank gilt dem folgenden technischen Experten bei Microsoft für die Durchsicht dieses Artikels: Michael Palermo