Unsere kleine Reihe Automatisierung mit Grunt für Windows wäre lückenhaft, wenn wir uns nicht noch mit einigen Techniken beschäftigen würden, unseren Automatisierungscode zu organisieren. Ein minimales Grunt-Projekt besteht aus dem Konfigurationsfile package.json und dem Haupt-Skript Gruntfile.js. Werden die Projekte umfangreicher, möchte man den Sourcecode in mehrere Dateien aufsplitten. Eine Technik dafür haben wir in den vorhergehenden Blogbeiträgen schon kennengelernt, aber nicht kommentiert. Das wollen wir hier nun nachholen und um einige weitere wichtige Techniken ergänzen.
require('load-grunt-tasks')(grunt);
Zunächst einmal wird dem einen oder anderen schon aufgefallen sein, dass wir bei der Realisierung eines Grunt-Projekts die Abhängigkeiten unserer installierten Module eigentlich zweimal registrieren. Zum einen erfolgt bei einer Installation mittels npm (z.B. npm install grunt-contrib-watch --save-dev) ein Eintrag in package.json unter devDependencies und zum anderen müssen wir das Modul im Gruntfile laden mit grunt.loadNpmTasks('grunt-contrib-watch');.
Den zweiten Schritt können wir vereinfachen, indem wir im Grunfile statt der langen Liste mit Abhängigkeiten nur noch eine einzelne Zeile schreiben:
// Somewhere in Gruntfile.js: ... // grunt.loadNpmTasks('grunt-contrib-nodeunit'); // grunt.loadNpmTasks('grunt-contrib-jshint'); // grunt.loadNpmTasks('grunt-contrib-watch'); ... // This is no more needed ! // Gruntfile.js module.exports = function (grunt) { // load all grunt tasks matching the `grunt-*` pattern require('load-grunt-tasks')(grunt); grunt.initConfig({}); grunt.registerTask('default', []); grunt.registerTask('... }
load-grunt-task lädt dann alle erforderlichen Module anhand der automatisch generierten Einträge in package.json.
Diese Vorgehensweise hat außerdem noch den Vorteil, dass uns load-grunt-task auf Abhängigkeiten in package.json aufmerksam macht, die veraltet sind bzw. für die Module nicht mehr existieren.
Damit können wir unser Gruntfile.js schon etwas schlanker schreiben.
Auslagerung von Funktionen in Helper-Objekte
Diese Methode haben wir im letzten Blogbeitrag zum Thema schon kennengelernt. Wir benötigen Funktionen manchmal in der initalen Konfiguration, etwa um einen Timestamp für einen Dateinamen zu erzeugen, aber auch in unseren Tasks. Wir könne dafür z.B. in einer Javascript-Datei grunt-helper.js unsere Funktionen in ein Objekt einbetten. Diese Datei findet man im letzten Beitrag (ziemlich genau in der Mitte des Beitrags). Ich wiederhole das Skript hier deshalb nicht.
Wir können dieses Helper-Objekt nun folgendermaßen in unserem Gruntfils.js nutzen:
module.exports = function(grunt) { require('load-grunt-tasks')(grunt); grunt.initConfig({ pkg : grunt.file.readJSON('package.json'), ... util: require('./grunt-helpers.js')(grunt), shell: { ... "localBackup" : { command : [ 'echo localBackup started at: <%= util.timestamp() %>', ... ] }, .. } }); ... var util = grunt.config('util'); function somewhat() { .... var timestamp = util.timestamp(); .... } ...
Man erzeugt also eine Konfigurationsvariable für das Helper-Objekt util, dessen Objekt-Methoden man dann in Platzhalter-Variablen verwenden kann, um Ausdrücke zu berechnen, die man dann etwa so <%= util.timestamp() %> einfügt.
Außerdem kann man das Helper-Objekt direkt in Javascript-Abschnitten verwenden, mit denen man seine Tasks programmiert. Dafür holt man sich das Helper-Objekt mit seinen Methoden mittels var util = grunt.config('util'); dorthin, wo man es benötigt.
Dies ist eine saubere Methode, Libraries bzw. Funktionssammlungen zu organisieren.
Aufsplittung von Tasks in mehrere Dateien
Nun wird gezeigt, wie man umfangreiche Gruntfils.js-Skripte mit vielen Tasks auf mehrere Module aufteilt. Betrachten wir einmal folgendes Gruntfile für diverse Demo-Tasks.
module.exports = function(grunt) { // http://www.thomasboyt.com/2013/09/01/maintainable-grunt.html function loadConfig(path) { var glob = require('glob'); var object = {}; var key; glob.sync('*', {cwd: path}).forEach(function(option) { key = option.replace(/\.js$/,''); object[key] = require(path + option); }); return object; } require('load-grunt-tasks')(grunt); // Aufsplittung der initialen Konfiguration // grunt.initConfig({ var config = { pkg : grunt.file.readJSON('package.json'), env: process.env, /* shell : { "test" : { command : 'echo ***** test, test, test *****', }, }*/ //}); }; grunt.util._.extend(config, loadConfig('./tasks/options/')); grunt.initConfig(config); // Aufteilung der Tasks auf mehrere Dateien grunt.loadTasks('tasks'); grunt.registerTask('hello', ['helloWorld']); grunt.registerTask('default', ['shell:test1', 'shell:test2', 'shell:test2']); };
Damit man den Unterschied zur klassischen Struktur erkennt, habe ich diese aus dem Skript nicht gelöscht, sondern auskommentiert.
Damit dieses Skript läuft, müssen folgende Module installiert werden:
- grunt,
- glob,
- grunt-shell,
- load-grunt-tasks.
Aufsplittung der Tasks auf mehrere Dateien
Betrachten wir zunächst die Zeile grunt.loadTasks('tasks');. Mit dieser Funktion lassen sich Aufgaben in einen Ordner auslagen (hier der Ordner tasks), indem sie dort in beliebigen Javaskripten organisiert werden. Es können mehrere Tasks in einem Skript enthalten sein. Die Namensgebung des Skripts spielt dabei eine untergeordnete Rolle.
Wir legen im Ordner tasks ein Skript myhello.js an mit folgendem Inhalt:
module.exports = function(grunt) { grunt.registerTask('helloWorld', 'Test-Task', function() { grunt.log.writeln("Hello world!"); console.log(grunt.config('env')); }); };
Der Name der Tasks ist helloWorld und nicht myhello, wie man am Namen der Datei vielleicht hätte vermuten könnte. Dieser Taskname taucht dann in einer Alias-Zuweisung in unserem Gruntfile.js wieder auf. Dies zeigt auch, dass wir in myhello.js durchaus mehrere Tasks programmieren können.
Auf diese Weise lassen sich die Javaskripte für Aufgaben beliebig organisieren.
Aufsplittung der Konfigurationsdefinition auf mehrere Dateien
Auch die Konfiguration wird in einem Gruntfile schnell sehr umfangreich und komplex. Hierfür benötigen wir jetzt das Modul glob, mit dessen Hilfe wir über die Funktion loadConfig() die Definitionen aus mehreren Dateien, die wir hier im Ordner ./tasks/options abspeichern, zu einem Konfigurationsobjekt zusammenlinkt.
Hier spielt jetzt der Name der Dateien eine wichtige Rolle. Hatten wir im Konfigurationsobjekt vorher auf der obersten Ebene z.B. die auf dem Modul grunt-shell basierenden Parameter unter shell definiert, werden diese jetzt in die Datei shell.js in unserem Ordner ./tasks/options verlagert. Hier ein Beispiel für shell.js:
module.exports = { "test1" : { command : 'echo ***** test1, test1, test1 *****', }, "test2" : { command : 'echo ***** test2, test2, test2 *****', }, "test3" : { command : 'echo ***** test3, <%= pkg.name %> , test3 *****', }, };
Interessant ist unter Aufgabe test3 die Verwendung der Platzhalter-Variablen <%= pkg.name %>. Diese können wir hier verwenden, weil wir diese Zuweisung im Gruntfile vorgenommen haben.
Damit kennen wir jetzt zusätzlich eine Möglichkeit, auch das häufig sehr umfangreiche initiale Konfigurationsobjekt aufzusplitten.
Natürlich findet diese Methode dort ihre Grenze, wo wir innerhalb der Konfiguration für ein Modul diesen Abschnitt gerne noch weiter aufsplitten würden. Z.B. kann die Definition für das grunt-shell-Modul alleine schon sehr umfangreich werden. Damit müssen wir aber erst einmal leben.
Elegante Grunt-Skripte: Das Modul load-grunt-config
Das Modul load-grunt-config ermöglicht eine sehr elegante Strukturierung der eigenen Grunt-Skripte. Diese Methode eignet sich im Gegensatz zur Methode oben nicht für jeden Einsatzfall. Deshalb wird hier nur darauf verwiesen. In diesem Artikel wird sie sehr schön erläutert.
Zusammenfassung
Wir haben zunächst eine Methode kennengelernt, mit der wir unseren Source-Code etwas schlanker gestalten können.
Der Hauptteil des Beitrags beschäftigte sich dann mit Möglichkeiten:
- Tasks auf mehrere Dateien und
- Konfigurationsdaten auf mehrere Dateien zu verteilen.
Damit stehen vorerst ausreichende Methoden zur Verfügung, den Source-Code für Automatisierungsaufgaben mit Grunt nach Kriterien wie Lesbarkeit und Wartbarkeit zu organisieren.
In einem letzten Abschnitt wurde auf die Möglichkeiten des Moduls load-grunt-config hingewiesen.