Die Leistung deines Gruntfile

Build-Konfiguration optimal nutzen

Paul Bakaus
Paul Bakaus

Einleitung

Wenn die Grunt-Welt neu für dich ist, ist ein idealer Ausgangspunkt für den Einstieg in Chris Coyiers ausgezeichneten Artikel „Grunt for People Who Think Things Like Things Like Grunt are Weird and Hard“. Nach der Einführung von Chris hast du dein eigenes Grunt-Projekt ins Leben gerufen und einen Teil der Power-Angebote von Grunt probiert.

In diesem Artikel geht es nicht darum, was zahlreiche Grunt-Plug-ins mit Ihrem tatsächlichen Projektcode tun, sondern auf den Grunt-Build-Prozess selbst. Wir geben Ihnen praktische Ideen zu folgenden Themen:

  • So halten Sie Ihr Gruntfile sauber
  • So kannst du deine Build-Zeit erheblich verbessern,
  • Außerdem erfahren Sie, wie Sie sich bei einem Build benachrichtigen lassen können.

Zeit für einen kurzen Haftungsausschluss: Grunt ist nur eines von vielen Tools, mit denen du diese Aufgabe erledigen kannst. Wenn Gulp eher dein Stil ist, großartig! Wenn Sie sich die verschiedenen Möglichkeiten genauer angesehen haben und trotzdem gerne Ihre eigene Toolchain erstellen möchten, ist das auch in Ordnung. Wir haben uns in diesem Artikel auf Grunt konzentriert, da es ein starkes Ökosystem und eine langjährige Nutzerbasis bietet.

Gruntfile organisieren

Ganz gleich, ob Sie viele Grunt-Plug-ins einbinden oder viele manuelle Aufgaben in Ihre Gruntfile-Datei schreiben müssen: Es kann schnell sehr unhandlich und schwer zu verwalten werden. Zum Glück gibt es eine ganze Reihe von Plug-ins, die sich genau auf dieses Problem konzentrieren: Ihr Gruntfile wieder sauber und ordentlich machen.

Gruntfile vor der Optimierung

So sieht die Gruntfile-Datei aus, bevor wir sie optimiert haben:

module.exports = function(grunt) {

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      dist: {
        src: ['src/js/jquery.js','src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
        dest: 'dist/build.js',
      }
    },
    uglify: {
      dist: {
        files: {
          'dist/build.min.js': ['dist/build.js']
        }
      }
    },
    imagemin: {
      options: {
        cache: false
      },

      dist: {
        files: [{
          expand: true,
          cwd: 'src/',
          src: ['**/*.{png,jpg,gif}'],
          dest: 'dist/'
        }]
      }
    }
  });

  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-contrib-imagemin');

  grunt.registerTask('default', ['concat', 'uglify', 'imagemin']);

};

Wenn Sie jetzt sagen: „Hey! Ich hatte viel schlimmeres erwartet! Das ist doch gut zu warten!“, haben Sie wahrscheinlich recht. Der Einfachheit halber haben wir nur drei Plug-ins ohne große Anpassungsmöglichkeiten hinzugefügt. Die Verwendung einer echten Gruntfile-Produktionsdatei, die ein Projekt von mittlerer Größe erstellt, würde in diesem Artikel unendliches Scrollen erfordern. Mal sehen, was wir da tun können!

Grunt-Plug-ins automatisch laden

Wenn Sie Ihrem Projekt ein neues Grunt-Plug-in hinzufügen möchten, müssen Sie es beide als npm-Abhängigkeit in Ihre package.json-Datei einfügen und dann in die Gruntfile laden. Für das Plug-in grunt-contrib-concat könnte dies wie folgt aussehen:

// tell Grunt to load that plugin
grunt.loadNpmTasks('grunt-contrib-concat');

Wenn du jetzt das Plug-in über npm deinstallierst und deine „package.json“-Datei aktualisierst, aber vergisst, deine Gruntfile zu aktualisieren, funktioniert der Build nicht mehr. Hier kommt das raffinierte Plug-in load-grunt-tasks ins Spiel.

Bisher mussten wir unsere Grunt-Plug-ins manuell laden, z. B. so:

grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-imagemin');

Mit Load-gunt-tasks können Sie dies auf folgende Einzeiler reduzieren:

require('load-grunt-tasks')(grunt);

Wenn das Plug-in erforderlich ist, analysiert es Ihre package.json-Datei, ermittelt, welche der Abhängigkeiten Grunt-Plug-ins sind, und lädt sie alle automatisch.

Grunt-Konfiguration in verschiedene Dateien aufteilen

load-grunt-tasks hat Ihre Gruntfile-Datei an Code und Komplexität etwas reduziert, aber wenn Sie eine große Anwendung konfigurieren, wird daraus eine sehr große Datei. Hier kommt load-grunt-config ins Spiel. Mit load-grunt-config können Sie Ihre Gruntfile-Konfiguration nach Aufgabe aufteilen. Darüber hinaus enthält es load-grunt-tasks und seine Funktionen.

Wichtig: Das Aufteilen Ihrer Gruntfile-Datei funktioniert möglicherweise nicht immer in jeder Situation. Wenn Sie viele gemeinsame Konfigurationen zwischen Ihren Aufgaben haben (d.h. viele Grunt-Vorlagen verwenden), sollten Sie vorsichtig sein.

Mit load-grunt-config sieht Ihre Gruntfile.js-Datei so aus:

module.exports = function(grunt) {
  require('load-grunt-config')(grunt);
};

Ja, das war's auch schon die ganze Datei. Wo befinden sich jetzt unsere Aufgabenkonfigurationen?

Erstellen Sie im Verzeichnis Ihrer Gruntfile einen Ordner mit dem Namen grunt/. Standardmäßig schließt das Plug-in Dateien in diesem Ordner mit, die dem Namen der gewünschten Aufgabe entsprechen. Unsere Verzeichnisstruktur sollte wie folgt aussehen:

- myproject/
-- Gruntfile.js
-- grunt/
--- concat.js
--- uglify.js
--- imagemin.js

Fügen wir nun die Aufgabenkonfiguration jeder unserer Aufgaben direkt in die jeweiligen Dateien ein (wie Sie sehen, dass es sich dabei meistens um Kopieren und Einfügen aus der ursprünglichen Gruntfile-Datei handelt):

grunt/concat.js

module.exports = {
  dist: {
    src: ['src/js/jquery.js', 'src/js/intro.js', 'src/js/main.js', 'src/js/outro.js'],
    dest: 'dist/build.js',
  }
};

grunt/uglify.js

module.exports = {
  dist: {
    files: {
      'dist/build.min.js': ['dist/build.js']
    }
  }
};

grunt/imagemin.js

module.exports = {
  options: {
    cache: false
  },

  dist: {
    files: [{
      expand: true,
      cwd: 'src/',
      src: ['**/*.{png,jpg,gif}'],
      dest: 'dist/'
    }]
  }
};

Falls Sie keine JavaScript-Konfigurationsblöcke verwenden, können Sie mit load-grunt-tasks stattdessen die YAML- oder CoffeeScript-Syntax verwenden. Schreiben wir die letzte erforderliche Datei in YAML – die aliases-Datei. Dies ist eine spezielle Datei, die Aufgabenaliasse registriert, was wir zuvor als Teil der Gruntfile-Funktion über die registerTask-Funktion tun mussten. Unsere Lösung:

grunt/aliases.yaml

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

So einfach ist das. Führen Sie in Ihrem Terminal den folgenden Befehl aus:

$ grunt

Wenn alles funktioniert hat, wird jetzt die Standardaufgabe betrachtet und alles der Reihe nach ausgeführt. Jetzt, da wir die Haupt-Grntfile-Datei auf drei Codezeilen entfernt haben, müssen wir nie mehr jede Aufgabenkonfiguration externalisieren. Damit sind wir hier fertig. Aber Mann, es ist immer noch ziemlich langsam, alles fertigzustellen. Sehen wir uns an, wie wir das verbessern können.

Build-Dauer minimieren

Auch wenn die Leistung der Laufzeit und Ladezeit Ihrer Webanwendung viel geschäftskritischer ist als die Zeit, die für die Ausführung eines Builds erforderlich ist, ist ein langsamer Build dennoch problematisch. Es erschwert die Ausführung automatischer Builds mit Plug-ins wie grunt-contrib-watch oder nach einem Git-Commit und führt zu einer Strafe, um den Build tatsächlich auszuführen – je schneller die Build-Zeit ist, desto agiler ist Ihr Workflow. Wenn die Ausführung Ihres Produktions-Builds länger als 10 Minuten dauert, führen Sie ihn nur dann aus, wenn es unbedingt erforderlich ist, und Sie gehen weiter, um während der Ausführung Kaffee zu holen. Das ist ein Produktivitätskiller. Wir haben etwas, das schneller geht.

Nur Dateien erstellen, die sich tatsächlich geändert haben: grunzen-neuer

Nach der ersten Erstellung Ihrer Website haben Sie wahrscheinlich nur wenige Dateien im Projekt bearbeitet, wenn Sie wieder mit der Erstellung beginnen. Nehmen wir an, Sie haben in unserem Beispiel ein Image im Verzeichnis src/img/ geändert. Das Ausführen von Imagemin zur Neuoptimierung von Images wäre sinnvoll, aber nur für dieses eine Image. Natürlich werden durch das erneute Ausführen von concat und uglify wertvolle CPU-Zyklen verschwendet.

Natürlich können Sie auch immer $ grunt imagemin statt $ grunt über Ihr Terminal ausführen, um eine Aufgabe nur selektiv auszuführen. Es gibt jedoch eine intelligentere Methode. Sie nennt sich grunzneuer.

Grunt-newer verfügt über einen lokalen Cache, in dem Informationen darüber gespeichert werden, welche Dateien sich tatsächlich geändert haben, und führt Ihre Aufgaben nur für die Dateien aus, die sich tatsächlich geändert haben. Sehen wir uns an, wie Sie ihn aktivieren können.

Erinnern Sie sich an die Datei aliases.yaml? Ändern Sie ihn so:

default:
  - 'concat'
  - 'uglify'
  - 'imagemin'

in:

default:
  - 'newer:concat'
  - 'newer:uglify'
  - 'newer:imagemin'

Wenn Sie einer Ihrer Aufgaben einfach „newer:“ voranstellen, leiten Sie Ihre Quell- und Zieldateien zuerst durch das grunt-newer-Plug-in, das dann bestimmt, für welche Dateien die Aufgabe gegebenenfalls ausgeführt werden soll.

Mehrere Aufgaben parallel ausführen: gleichzeitig

grunt-gleichzeitig ist ein Plug-in, das besonders nützlich ist, wenn viele Aufgaben unabhängig voneinander sind und viel Zeit in Anspruch nehmen. Er nutzt die Anzahl der CPUs in Ihrem Gerät und führt mehrere Aufgaben parallel aus.

Das Beste daran ist, dass die Konfiguration ganz einfach ist. Wenn Sie "load-grunt-config" verwenden, erstellen Sie die folgende neue Datei:

grunt/concurrent.js

module.exports = {
  first: ['concat'],
  second: ['uglify', 'imagemin']
};

Wir richten lediglich parallele Ausführungs-Tracks mit den Namen first und second ein. Die Aufgabe concat muss in der Zwischenzeit zuerst ausgeführt werden. Im zweiten Track verwenden wir sowohl uglify als auch imagemin, da diese beiden voneinander unabhängig sind und viel Zeit in Anspruch nehmen.

Diese Funktion allein bewirkt noch nichts. Wir müssen unseren standardmäßigen Aufgabenalias so ändern, dass er auf die gleichzeitigen Jobs anstatt auf die direkten Jobs verweist. Hier ist der neue Inhalt von grunt/aliases.yaml:

default:
  - 'concurrent:first'
  - 'concurrent:second'

Wenn Sie Ihren Gunt-Build nun noch einmal ausführen, führt das gleichzeitige Plug-in zuerst die Verkettungsaufgabe aus und erzeugt dann zwei Threads auf zwei verschiedenen CPU-Kernen, um sowohl Imagemin als auch Uglify parallel auszuführen. Super!

Ein Hinweis: Die Chancen stehen gut, dass in unserem einfachen Beispiel das gleichzeitige Grunzen den Build nicht deutlich schneller macht. Der Grund dafür ist der Mehraufwand, der durch die Erzeugung verschiedener Instanzen von Grunt in verschiedenen Threads entsteht: In meinem Fall wurden mindestens 300 ms pro Woche erzeugt.

Wie lange hat es gedauert?

Jetzt, da wir jede unserer Aufgaben optimieren, wäre es sehr hilfreich zu wissen, wie viel Zeit jede einzelne Aufgabe für die Ausführung benötigt. Glücklicherweise gibt es auch dafür ein Plug-in: time-grunt.

Time-grunt ist kein klassisches Gunt-Plug-in, das Sie als npm-Task laden, sondern ein Plug-in, das Sie direkt einbinden können, ähnlich wie „load-grunt-config“. Wir fügen unserer Gruntfile eine Anforderung für Time-gunt hinzu, genau wie wir es mit „load-grunt-config“ getan haben. Unsere Gruntfile sollte jetzt so aussehen:

module.exports = function(grunt) {

  // measures the time each task takes
  require('time-grunt')(grunt);

  // load grunt config
  require('load-grunt-config')(grunt);

};

Schade, dass Sie enttäuscht sind. Versuchen Sie, Grunt von Ihrem Terminal aus noch einmal auszuführen. Für jede Aufgabe (und zusätzlich für den gesamten Build) sollte ein schön formatiertes Infofeld zur Ausführungszeit angezeigt werden:

Grunt-Zeit

Automatische Systembenachrichtigungen

Sie haben jetzt einen stark optimierten Grunt-Build, der schnell ausgeführt wird und auf irgendeine Weise automatisch erstellt wird (z.B. indem Sie Dateien mit Grun-contrib-Watch oder nach Commits ansehen), wäre es nicht großartig, wenn Ihr System Sie benachrichtigen könnte, wenn Ihr neuer Build einsatzbereit ist oder wenn etwas Schlimmes passiert ist? Dürfen wir vorstellen: grunt-notify.

Standardmäßig sendet grunt-notify automatische Benachrichtigungen über alle Grunt-Fehler und -Warnungen über ein beliebiges Benachrichtigungssystem, das auf deinem Betriebssystem zur Verfügung steht: Growl für OS X oder Windows, das Benachrichtigungscenter von Mountain Lion's und Mavericks und Notify-send. Um diese Funktion zu erhalten, müssen Sie lediglich das Plug-in von npm installieren und in Ihre Gruntfile laden. Wenn Sie grunt-load-config oben verwenden, ist dieser Schritt automatisiert.

So sieht dies je nach Betriebssystem aus:

Benachrichtigen

Zusätzlich zu Fehlern und Warnungen konfigurieren wir sie so, dass sie nach Abschluss der letzten Aufgabe ausgeführt wird. Angenommen, Sie verwenden grunt-load-config, um Aufgaben dateiübergreifend aufzuteilen, benötigen wir die folgende Datei:

grunt/notify.js

module.exports = {
  imagemin: {
    options: {
      title: 'Build complete',  // optional
        message: '<%= pkg.name %> build finished successfully.' //required
      }
    }
  }
}

Auf der ersten Ebene des Konfigurationsobjekts muss der Schlüssel mit dem Namen der Aufgabe übereinstimmen, mit der er verbunden werden soll. In diesem Beispiel wird die Nachricht direkt nach der Ausführung der Aufgabe imagemin angezeigt. Dies ist die letzte Aufgabe in unserer Build-Kette.

Zusammenfassung

Wenn Sie ganz oben gefolgt sind, sind Sie jetzt stolzer Inhaber eines Build-Prozesses, der superaufgeräumt und organisiert ist, aufgrund von Parallelisierung und selektiver Verarbeitung unglaublich schnell ist und Sie benachrichtigt, wenn etwas schiefgeht.

Wenn Sie ein weiteres Juwel entdecken, das Grunt und die zugehörigen Plug-ins weiter verbessert, teilen Sie uns dies bitte mit! Viel Spaß beim Grunzen!

Update (14.02.2014): Klicke hier, um eine Kopie des vollständigen, funktionierenden Grunt-Beispielprojekts zu erhalten.