Nachdem sich Continuous Integration und agile Entwicklung durchgesetzt haben, ist praktisch ständig ein Release. Da will natürlich niemand mehr den ganzen Prozess händisch erledigen. In diesem Artikel soll es somit um die Automatisierung verschiedener Aufgaben mit Hilfe von SVN-Hooks und Grunt gehen.
Bevor wir loslegen, erkläre ich noch kurz die Begriffe, die wir hier verwenden werden.
Grunt ist ein sogenannter Task-Runner, der mit JavaScript konfiguriert wird. Natürlich gibt es andere (Gulp, Broccoli), die alle nach ähnlichen Prinzipien arbeiten.
SVN (Subversion) ist eine Versionskontrolle, die vor 10 Jahren cool war 😉 Natürlich gibt es auch hier andere (Git, Mercurial), die ebenfalls das Konzept der Hooks unterstützen.
Ein Hook ist ein kleines Programm, dass sich an einem bestimmten Punkt im normalen Ablauf einhängt.
Ablauf eines SVN Commits
- Hook
start-commit
- SVN sammelt die Daten die committed werden sollen
- Hook
pre-commit
- SVN erstellt eine neue Revision und schickt die Daten an den Server
- Hook
post-commit
Aus obigem Ablauf folgt, dass wir in start-commit
und pre-commit
den ganzen Prozess noch abbrechen können, bevor irgendetwas passiert ist.
In start-commit
ist es außerdem möglich noch Dateien zu erstellen oder zu ändern, die noch mit in den Commit einfließen. Diesen Umstand werden wir im Folgenden nutzen.
Vorbereitung
Wir legen also in den SVN-Einstellungen des Projektes einen Hook für start-commit
an. Da wir einen Task-Runner verwenden wollen, ist das Skript sehr einfach:
1 |
grunt |
Das war’s schon. Die Datei speichern wir als start-hook-grunt.bat
und machen sie SVN bekannt. Hooks werden automatisch in der Working Copy ausgeführt.
Was wir dort benötigen ist ein Gruntfile.js
in dem wir die gewünschten Tasks definieren.
Gruntfile
Zu den häufigsten Aufgaben in der Webentwicklung zählen Kombination und Kompression der CSS- und JavaScript-Dateien. Grunt bietet dazu entsprechende Pakete. Im Gruntfile müssen sie konfiguriert und als Task registriert werden.
Das „Drumrum“
Hier sehen wir zunächst was jedes Gruntfile benötigt – quasi das Skelett.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
module.exports = function(grunt) { grunt.initConfig({ // Hier steht die Konfiguration }); // Diese Tasks wollen wir verwenden grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.loadNpmTasks('grunt-file-creator'); // Diese Tasks werden ausgeführt wenn grunt ohne Parameter aufgerufen wird grunt.registerTask('default', ['uglify', 'concat', 'cssmin', 'file-creator']); }; |
JavaScript-Dateien komprimieren
Um die JavaScript-Dateien des Projektes zu komprimieren benötigen wir folgenden Abschnitt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// JavaScript-Dateien minimieren uglify: { options: { compress: true, // Wir wollen die Dateien komprimieren mit Standard-Optionen drop_console: true }, build: { files: [ { expand: true, cwd: 'js/', // Hier sind die Dateien zu finden - relativ zur Working Copy src: [ '*.js', // nimm alle JavaScript-Dateien '!*_all.js', // überspringe Dateien die auf _all.js enden '!skip_me.js' // diese hier wollen wir auch nicht ], dest: 'js/min/', // hierhin werden alle komprimierten Dateien abgelegt ext: '.min.js' // hiermit werden alle Dateien erweitert file.js wird zu file.min.js } // ... weitere Verzeichnisse mit JavaScript-Dateien, falls benötigt ] } }, |
Anschließend haben wir im Unterverzeichnis js/min
Kopien der JavaScript-Dateien in ihrer komprimierten Version. Diese werden automatisch in den Commit übernommen und somit auch auf dem Server ausgespielt.
Dateien zusammen führen
Es ist in dem meisten Fällen ratsam, die Dateien nicht nur zu verkleinern, sondern auch in einer einzigen Datei zu vereinen. Dazu definieren wir einen weiteren Task.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Dateien zusammen führen concat: { options: { separator: ';' // Trennzeichen zw. den Dateien, falls diese nicht sauber abgeschlossen sind }, build: { src: [ 'js/min/*.min.js', // unsere vorher erstellten komprimierten Dateien 'js/jquery/jquery.min.js' // eine Datei, die wir nicht ändern und schon komprimiert vorliegen haben ], dest: 'js/header_all.js' // so soll das Resultat heißen } }, |
Danach haben wir unseren gesamten JavaScript-Code in einer Datei namens header.js
. Diese binden wir auf den Produktiv-Servern des Projekts als einzige ein. Der Benutzer profitiert von schnelleren Ladezeiten und wir müssen uns nicht darum kümmern.
Stylesheet komprimieren
Das gleiche wie bei JavaScript können wir auch für Stylesheets erledigen lassen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Stylesheets komprimieren cssmin: { options: {}, // Standard ist ausreichend build: { files: { // als key benutzen wir Pfad und Dateiname des Ergebnisses 'css/style.min.css': [ 'css/style.css', // unser Stylesheet 'css/bootstrap.css' // wir streuen noch ein Framework mit ein ], // für den IE haben wir uns noch was besonders hübsches überlegt 'css/ie.min.css': ['css/ie.css'] } } }, |
Selbes Spiel wie beim JavaScript, auf den Produktiv-Servern werden nur die verkleinerten Varianten ausgespielt.
Den Browsercache des Anwenders informieren
Da wir immer den gleichen Namen für unsere Skripte verwenden, wird der Browser diese cachen. Das wollen wir allerdings nur so lange, bis wir eine Änderung vornehmen. Auch hier kann uns Grunt helfen.
1 2 3 4 5 6 7 8 9 10 11 12 |
// Erstelle eine PHP-Datei, die den aktuellen Zeitstempel enthält 'file-creator': { build: { 'config_version.php': function(fs, fd, done) { var d = new Date(), dstr, month = d.getMonth() + 1, h = d.getHours(), m = d.getMinutes(); dstr = d.getFullYear().toString() + (month < 10 ? '0' + month : month) + d.getDate().toString(); dstr += (h < 10 ? '0' + h : h).toString() + (m < 10 ? '0' + m : m).toString(); fs.writeSync(fd, '<?php define("CACHEVERSION", ' + dstr + ') ?>'); done(); } } } |
Hier können wir gut erkennen, dass Gruntfile.js nicht nur so aussieht wie JavaScript, sondern tatsächlich auch so interpretiert wird. Was hier passiert ist, dass eine Datei namens config_version.php im Projektverzeichnis erstellt/überschrieben wird. Sie enthält dann beispielsweise:
1 |
<?php define("CACHEVERSION", 201508181615) ?> |
Diese definiert eine Konstante mit dem aktuellen Zeitstempel und kann dann zum Beispiel so verwendet werden:
1 2 |
<link type="text/css" rel="stylesheet" href="/css/style.min.css?<?php echo CACHEVERSION ?>"> <script type="text/javascript" src="/js/header_all.js?<?php echo CACHEVERSION ?>"></script> |
Vollständiges Gruntfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
module.exports = function(grunt) { grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), // JavaScript-Dateien minimieren uglify: { options: { compress: true, // Wir wollen die Dateien komprimieren mit Standard-Optionen drop_console: true }, build: { files: [ { expand: true, // wir verwenden Wildcards (*) in den Dateinamen cwd: 'js/', // Hier sind die Dateien zu finden - relativ zur Working Copy src: [ '*.js', // nimm alle JavaScript-Dateien '!*_all.js', // überspringe Dateien die auf _all.js enden '!skip_me.js' // diese hier wollen wir auch nicht ], dest: 'js/min/', // hierhin werden alle komprimierten Dateien abgelegt ext: '.min.js' // hiermit werden alle Dateien erweitert file.js wird zu file.min.js } // ... weitere Verzeichnisse mit JavaScript-Dateien, falls benötigt ] } }, // Dateien zusammen führen concat: { options: { separator: ';' // Trennzeichen zw. den Dateien, falls diese nicht sauber abgeschlossen sind }, build: { src: [ 'js/min/*.min.js', // unsere vorher erstellten komprimierten Dateien 'js/jquery/jquery.min.js' // eine Datei, die wir nicht ändern und schon komprimiert vorliegen haben ], dest: 'js/header_all.js' // so soll das Resultat heißen } }, // Stylesheets komprimieren cssmin: { options: {}, // Standard ist ausreichend build: { files: { // als key benutzen wir Pfad und Dateiname des Ergebnisses 'css/style.min.css': [ 'css/style.css', // unser Stylesheet 'css/bootstrap.css' // wir streuen noch ein Framework mit ein ], // für den IE haben wir uns noch was besonders hübsches überlegt 'css/ie.min.css': ['css/ie.css'] } } }, // Erstelle eine PHP-Datei, die den aktuellen Zeitstempel enthält 'file-creator': { build: { 'config_version.php': function(fs, fd, done) { var d = new Date(), dstr, month = d.getMonth() + 1, h = d.getHours(), m = d.getMinutes(); dstr = d.getFullYear().toString() + (month < 10 ? '0' + month : month) + d.getDate().toString(); dstr += (h < 10 ? '0' + h : h).toString() + (m < 10 ? '0' + m : m).toString(); fs.writeSync(fd, '<?php define("CACHEVERSION", ' + dstr + ') ?>'); done(); } } } }); // Diese Tasks wollen wir verwenden grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-cssmin'); grunt.loadNpmTasks('grunt-file-creator'); // Diese Tasks werden ausgeführt wenn grunt ohne Parameter aufgerufen wird grunt.registerTask('default', ['uglify', 'concat', 'cssmin', 'file-creator']); }; |