Boostez votre gruntfile

Exploiter pleinement votre configuration de compilation

Introduction

Si le monde Grunt est nouveau pour vous, l'excellent article de Chris Coyier intitulé "Grunt for People Who Think Things Like Grunt are Weird and Hard" (Grunt pour les personnes qui pensent que Grunt sont bizarres et difficiles) est un point de départ idéal. Après la présentation de Christophe, vous pourrez créer votre propre projet Grunt et y avoir goûté une partie des avantages proposés par Grunt.

Dans cet article, nous n'étudierons pas l'effet des nombreux plug-ins Grunt sur le code réel de votre projet, mais nous nous intéresserons au processus de compilation Grunt lui-même. Nous vous donnerons des idées pratiques sur les sujets suivants:

  • Garder votre Gruntfile propre et ordonné
  • Comment améliorer considérablement la durée de la compilation,
  • et comment être averti en cas de compilation.

Voici une brève clause de non-responsabilité: Grunt n'est qu'un des nombreux outils que vous pouvez utiliser pour accomplir la tâche. Si Gulp est plus votre style, c'est parfait ! Si, après avoir examiné les options disponibles, vous souhaitez toujours créer votre propre chaîne d'outils, ce n'est pas un problème. Pour cet article, nous avons choisi de nous concentrer sur Grunt en raison de son écosystème solide et de sa base d'utilisateurs de longue date.

Organiser votre fichier Gruntfile

Que vous incluiez de nombreux plug-ins Grunt ou que vous deviez écrire de nombreuses tâches manuelles dans votre fichier Gruntfile, celui-ci peut rapidement devenir très pénible et difficile à gérer. Heureusement, il existe de nombreux plug-ins qui se focalisent sur ce problème: rendre votre Gruntfile à nouveau clair et ordonné.

Le Gruntfile, avant l'optimisation

Voici à quoi ressemble notre fichier Gruntfile avant de l'optimiser:

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']);

};

Si maintenant vous êtes en train de dire "Hey ! Je m'attendais à bien pire ! C'est en fait facile à gérer !", vous avez sans doute raison. Par souci de simplicité, nous n'avons inclus que trois plug-ins sans grande personnalisation. L'utilisation d'un fichier Gruntfile de production réel pour créer un projet de taille moyenne nécessiterait un défilement infini dans cet article. Voyons ce que nous pouvons faire !

Charger automatiquement vos plug-ins Grunt

Lorsque vous ajoutez un plug-in Grunt que vous souhaitez utiliser à votre projet, vous devez l'ajouter à votre fichier package.json en tant que dépendance npm, puis le charger dans le fichier Gruntfile. Pour le plug-in grunt-contrib-concat, qui pourrait se présenter comme suit:

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

Si vous désinstallez maintenant le plug-in via npm et mettez à jour le fichier package.json, mais oubliez de mettre à jour votre fichier Gruntfile, votre build ne fonctionnera pas. C'est là que le plug-in astucieux load-grunt-tasks est la solution.

Auparavant, nous devions charger manuellement nos plug-ins Grunt, comme ceci:

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

Avec "load-gunt-tasks", vous pouvez le réduire à la ligne suivante:

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

Une fois le plug-in requis, le plug-in analyse votre fichier package.json, détermine les dépendances comme des plug-ins Grunt et les charge automatiquement.

Diviser votre configuration Grunt en différents fichiers

load-grunt-tasks a réduit le code et la complexité de votre Gruntfile, mais lorsque vous configurez une application volumineuse, il devient un fichier très volumineux. C'est là qu'intervient load-grunt-config. load-grunt-config vous permet de diviser votre configuration Gruntfile par tâche. De plus, il encapsule load-grunt-tasks et ses fonctionnalités.

Important cependant: la division de votre fichier Gruntfile ne fonctionne pas dans tous les cas. Si vous avez beaucoup de configurations partagées entre vos tâches (c'est-à-dire que vous utilisez beaucoup de modèles Grunt), vous devez être un peu prudent.

Avec load-grunt-config, votre fichier Gruntfile.js se présente comme suit:

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

Oui, c'est tout. Maintenant, où résident nos configurations de tâches ?

Créez un dossier nommé grunt/ dans le répertoire de votre fichier Gruntfile. Par défaut, le plug-in inclut des fichiers dans ce dossier qui correspondent au nom de la tâche que vous souhaitez utiliser. Notre structure de répertoire devrait se présenter comme suit:

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

À présent, plaçons la configuration de chacune de nos tâches directement dans les fichiers correspondants (vous constaterez qu'il s'agit principalement de copier-coller du fichier Gruntfile d'origine dans une nouvelle structure):

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/'
    }]
  }
};

Si les blocs de configuration JavaScript ne vous concernent pas vraiment, load-grunt-tasks vous permet même d'utiliser la syntaxe YAML ou CoffeeScript à la place. Écrivons notre dernier fichier requis en YAML : le fichier aliases. Il s'agit d'un fichier spécial qui enregistre les alias de tâches, ce que nous avons dû faire auparavant avec le fichier Gruntfile via la fonction registerTask. Voici les nôtres:

grunt/aliases.yaml

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

C'est tout ! Exécutez la commande suivante dans votre terminal:

$ grunt

Si tout a fonctionné, la tâche "par défaut" est maintenant affichée et tout est exécuté dans l'ordre. Maintenant que nous avons réduit notre Gruntfile principal à trois lignes de code que nous n'avons plus besoin de manipuler et que nous avons externalisé chaque configuration de tâche, ce n'est plus un problème. Mais bon, c'est encore assez lent de tout construire. Voyons ce que nous pouvons faire pour améliorer cela.

Réduire la durée de compilation

Même si les performances d'exécution et de temps de chargement de votre application Web sont beaucoup plus critiques que le temps nécessaire pour exécuter une compilation, une compilation lente reste problématique. Il est alors difficile d'exécuter des compilations automatiques avec des plug-ins tels que grunt-contrib-watch ou après un commit Git suffisamment rapide, et impose une "pénalité" pour exécuter la compilation : plus la compilation est rapide, plus votre workflow est agile. Si l'exécution de votre build de production prend plus de 10 minutes, vous n'exécuterez la compilation que lorsque c'est absolument nécessaire, et vous quitterez pour prendre un café pendant son exécution. La productivité est au rendez-vous. Nous avons des choses à faire.

Créer uniquement les fichiers qui ont été modifiés: grunt-newer

Après la création initiale de votre site, il est probable que vous n'ayez modifié que quelques fichiers dans le projet lorsque vous reviendrez à la création. Imaginons que, dans notre exemple, vous avez modifié une image dans le répertoire src/img/. Il serait logique d'exécuter imagemin pour réoptimiser les images, mais uniquement pour cette seule image. Par ailleurs, réexécuter concat et uglify ne fait que gaspiller de précieux cycles de processeur.

Bien sûr, vous pouvez toujours exécuter $ grunt imagemin depuis votre terminal au lieu de $ grunt pour n'exécuter qu'une tâche sélective, mais il existe une méthode plus intelligente. Ça s'appelle Grunt-newer.

Grunt-newer dispose d'un cache local dans lequel il stocke des informations sur les fichiers qui ont été modifiés et n'exécute vos tâches que pour les fichiers qui ont été modifiés. Voyons comment l'activer.

Vous vous souvenez de notre fichier aliases.yaml ? Remplacez-la par celle-ci:

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

à ceci:

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

Ajoutez simplement "newer:" au début de l'une de vos tâches pour transmettre vos fichiers source et de destination via le plug-in grunt-newer, qui détermine ensuite les fichiers, le cas échéant, que la tâche doit exécuter.

Exécuter plusieurs tâches en parallèle: grunt-concurrent

grunt-concurrent est un plug-in qui s'avère très utile lorsque de nombreuses tâches sont indépendantes les unes des autres et prennent beaucoup de temps. Il utilise le nombre de processeurs de votre appareil et exécute plusieurs tâches en parallèle.

Et cerise sur le gâteau, sa configuration est très simple. En supposant que vous utilisiez load-grunt-config, créez le fichier suivant:

grunt/concurrent.js

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

Il nous suffit de configurer des pistes d'exécution parallèles avec les noms "first" et "second". La tâche concat doit être exécutée en premier. Il n'y a rien d'autre à exécuter en attendant. Dans la deuxième piste, nous ajoutons uglify et imagemin, car ils sont indépendants l'un de l'autre et prennent tous deux beaucoup de temps.

Cette opération n'a aucun effet pour l'instant. Nous devons modifier notre alias de tâche default pour qu'il pointe vers les tâches simultanées plutôt que vers les tâches directes. Voici le nouveau contenu de grunt/aliases.yaml:

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

Si vous réexécutez votre build grunt, le plug-in simultané exécutera d'abord la tâche de concaténation, puis deux threads sur deux cœurs de processeur différents pour exécuter imagemin et uglify en parallèle. C'est super !

Un conseil: il est possible que, dans notre exemple de base, grognement simultané n'accélérera pas considérablement la compilation. Cela s'explique par la surcharge créée par la génération de différentes instances de Grunt dans différents threads. Dans mon cas, au moins +300 ms d'espace professionnel sont générés.

Combien de temps ça a pris ? Gémissements

Maintenant que nous optimisons chacune de nos tâches, il serait vraiment utile de comprendre le temps nécessaire à l'exécution de chaque tâche. Heureusement, il existe également un plug-in pour cela: time-grunt.

time-grunt n'est pas un plug-in grognon classique que vous chargez en tant que tâche npm, mais plutôt un plug-in que vous incluez directement, semblable à load-grunt-config. Nous allons ajouter une expression obligatoire pour les grognements de temps à notre fichier Gruntfile, tout comme nous l'avons fait avec load-grunt-config. Le fichier Gruntfile devrait maintenant se présenter comme suit:

module.exports = function(grunt) {

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

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

};

Désolé de vous décevoir, mais c'est tout. Essayez de réexécuter Grunt depuis votre terminal. Pour chaque tâche (et en plus le build total), vous devriez voir un panneau d'informations correctement formaté sur la durée d'exécution:

Temps de grognement

Notifications automatiques du système

Maintenant que vous disposez d'un build Grunt très optimisé qui s'exécute rapidement et que vous avez fourni la compilation automatique d'une manière ou d'une autre (par exemple, en regardant les fichiers avec grunt-contrib-watch ou après des commits), ne serait-il pas idéal que votre système puisse vous avertir lorsque votre nouvelle version est prête à être utilisée ou en cas de problème ? Découvrez grunt-notify.

Par défaut, grunt-notify fournit des notifications automatiques pour toutes les erreurs et avertissements Grunt à l'aide du système de notification disponible sur votre OS: Growl pour OS X ou Windows, centre de notification de Mountain Lion et Mavericks, et Notifier-send. Pour profiter de cette fonctionnalité, il vous suffit d'installer le plug-in depuis npm et de le charger dans votre fichier Gruntfile (n'oubliez pas que si vous utilisez grunt-load-config ci-dessus, cette étape est automatisée).

Voici à quoi cela ressemblera en fonction de votre système d'exploitation:

M'informer

En plus des erreurs et des avertissements, configurons-le pour qu'il s'exécute une fois la dernière tâche exécutée. En supposant que vous utilisiez grunt-load-config pour répartir les tâches entre les fichiers, voici le fichier dont nous avons besoin:

grunt/notify.js

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

Au premier niveau de notre objet "config", la clé doit correspondre au nom de la tâche à laquelle nous voulons l'associer. Dans cet exemple, le message s'affiche juste après l'exécution de la tâche imagemin, la dernière tâche de notre chaîne de compilation.

En résumé

Si vous avez suivi depuis le début, vous êtes désormais l'heureux propriétaire d'un processus de compilation extrêmement ordonné et organisé, qui est extrêmement rapide grâce à la parallélisation et au traitement sélectif, et vous avertit en cas de problème.

Si vous découvrez un autre joyau qui améliore Grunt et ses plug-ins, n'hésitez pas à nous le faire savoir ! En attendant, bon grognement !

Mise à jour (14/02/2014): Pour obtenir une copie de l'exemple complet et fonctionnel du projet Grunt, cliquez ici.