Rossi Oddet

Blog d'un artisan développeur

Soirée NantesJS : Découverte De GruntJS

Grunt permet de lancer des tâches afin d’automatiser des traitements récurrents. Il se configure via l’écriture d’un code javascript. Il existe de nombreux outils permettant d’automatiser le lancement des tâches : ANT, Maven, Gradle, SBT. La particularité de Grunt par rapport aux autres outils est qu’il est conçu principalement pour traiter des tâches récurrentes du web : minification, optimisation des images, génération de sprites, compilation Sass/Less, etc.

La soirée NantesJS du 19 novembre dernier a eu comme objectif une présentation et une prise en main de Grunt.

C’est devant un public composé de professionnels que Thomas Moyse (à gauche dans la photo) commence une présentation de Grunt. Xavier Seignard (à droite) prendra de temps en temps la parole pour partager son expérience sur l’utilisation de Grunt.

Thomas va mettre l’accent sur le nombre important de plugins de Grunt : 1688. Dans la page des plugins Grunt, je compte 1847 plugins au moment de l’écriture de l’article. Grunt dispose donc d’un écosystème riche.

Après la présentation succincte de Grunt, la soirée se transforme en Workshop et nous allons découvrir progressivement cet outil.

Installer Grunt

Grunt s’installe via NPM (Node Packaged Modules) le gestionnaire de package de NodeJS.

Télécharger et installer NodeJS. Veuillez vérifier l’accès à la commande npm après l’installation.

Installer le client grunt via la commande :

1
npm install -g grunt-cli

A savoir : chaque projet va posséder son installation de Grunt et grunt-cli permet d’avoir accès à la commande grunt de façon globale quelque soit le projet.

Création d’un projet

Créer un répertoire.

1
2
mkdir nantesjs-grunt
cd nantesjs-grunt

Initialiser un projet avec NPM.

1
npm init

Tapez la touche entrée à toutes les questions. Le fichier package.json est créé avec le contenu suivant :

1
2
3
4
5
6
7
8
9
10
11
12
{
  "name": "nantesjs-grunt",
  "version": "0.0.0",
  "description": "ERROR: No README.md file found!",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": "",
  "author": "",
  "license": "BSD"
}

Ce fichier représente l’identité d’un projet dans l’univers NodeJS.

Installer Grunt pour le projet.

1
npm install grunt --save-dev

Le fichier package.json est mis à jour comme suit :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "name": "nantesjs-grunt",
  "version": "0.0.0",
  "description": "ERROR: No README.md file found!",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": "",
  "author": "",
  "license": "BSD",
  "devDependencies": {
    "grunt": "~0.4.2"
  }
}

Un répertoire node_modules est créé avec un répertoire grunt qui contient une installation de Grunt dans sa version 0.4.2.

Hello World Grunt !

Grunt se configure dans un fichier Gruntfile.js avec du code javasript.

Créer un fichier Gruntfile.js.

Modifiez ce fichier comme suit pour créer un Hello World.

1
2
3
4
5
module.exports = function(grunt) {
  grunt.registerTask('hello', 'Ma tâche à moi', function(){
      console.log('Hello World Grunt !')
  });
}

La fonction registerTask permet la création d’une tâche avec le nom hello, la description Ma tâche à moi et une fonction qui sera exécutée lorsque cette tâche sera appelée.

console.log écrit du texte dans la console.

Pour lancer la tâche hello, exécuter la commande :

1
grunt hello

Le résultat dans la console

1
2
3
4
5
6
>grunt hello
Running "hello" task
---------------------
Hello World Grunt !

Done, without errors.

Une tâche par défaut

Il possible de spécifier une tâche par défaut à Grunt. Il suffit de lui donner le nom default comme suit :

1
2
3
4
5
module.exports = function(grunt) {
  grunt.registerTask('default', 'Ma tâche à moi', function(){
      console.log('Hello World Grunt !')
  });
}

La tâche est lancée via la commande suivante sans avoir à préciser le nom d’une tâche.

1
grunt

Connaitre la liste des tâches disponibles

1
grunt --help

Un alias pour composer des tâches

Un alias permet de composer plusieurs tâches. Par défaut, les tâches sont exécutées de façon séquentielle.

Un alias est créé à l’aide d’une fonction registerTask en précisant un nom d’alias et un tableau de noms des tâches.

1
2
3
4
5
6
7
8
9
10
11
module.exports = function(grunt) {
  grunt.registerTask('step1',function(){
      console.log('Step 1')
  });

  grunt.registerTask('step2', 'ma tâche à moi', function(){
      console.log('Step 2')
  });

  grunt.registerTask('all', ['step1', 'step2']);
}

Lancer l’alias all

1
2
3
4
5
6
7
8
9
10
> grunt all
Running "step1" task
--------------------
Step 1

Running "step2" task
--------------------
Step 2

Done, without errors.

Le multi task

Le multi task sert à créer des tâches qui peuvent s’exécuter suivant plusieurs cibles.

Ci-dessous un exemple d’utilisation de multi task.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = function(grunt) {

  grunt.initConfig({
      multitask1: {
          conf1:[1,2,3],
          conf2: 'hello world'
      }
  })

  grunt.registerMultiTask('multitask1', function(){
      console.log('target = ' + this.target);
      console.log("data = " + this.data);
  })
}

La fonction initConfig crée les différentes configurations possibles. Ici la tâche multitask1 possède 2 contextes d’exécution conf1et conf2. Chaque contexte d’exécution possède son jeu de données.

Dans la fonction d’exécution de la tâche multitask1, this.target contient le nom du contexte d’exécution et this.data contient les données associées.

Lancer la tâche multitask1 entraine son exécution pour les différents contextes d’exécution définis.

1
2
3
4
5
6
7
8
9
10
11
12
>grunt multitask1
Running "multitask1:conf1" (multitask1) task
--------------------------------------------
target = conf1
data = 1,2,3

Running "multitask1:conf2" (multitask1) task
--------------------------------------------
target = conf2
data = hello world

Done, without errors.

Il est possible d’exécuter une tâche de type multi task sur une configuration particulière comme suit :

1
2
3
4
5
6
7
>grunt multitask1:conf2
Running "multitask1:conf2" (multitask1) task
--------------------------------------------
target = conf2
data = hello world

Done, without errors.

Partager de la configuration via l’API options

L’API options offre la possibilité de mutualiser de la configuration Grunt. Cette configuration peut être surchargée par un contexte d’exécution.

Voici un exemple d’utilisation des options via le partage de configuration key pour les différents contextes d’exécution.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = function(grunt) {

  grunt.initConfig({
      multitask1: {
          options:{key:"v1"},
          conf1:[1,2,3],
          conf2:'hello world',
          conf3:{
              options:{key:"v2"}
          }
      }
  });

  grunt.registerMultiTask('multitask1', function(){
      console.log('target = ' + this.target);
      console.log("data = " + this.data);
      console.log("version = " + this.options().key)
  });
}

Dans cet exemple, key vaut

  • v1 pour conf1 et conf2
  • v2 pour conf3

L’appel de la fonction this.options().key renvoie la valeur de la donnée key dans le contexte d’exécution courant.

Le résultat de l’exécution de multitask1 donne :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>grunt multitask1
Running "multitask1:conf1" (multitask1) task
--------------------------------------------
target = conf1
data = 1,2,3
version = v1

Running "multitask1:conf2" (multitask1) task
--------------------------------------------
target = conf2
data = hello world
version = v1

Running "multitask1:conf3" (multitask1) task
--------------------------------------------
target = conf3
data = [object Object]
version = v2

Done, without errors.

Les templates

Il est possible d’utiliser un template avec Grunt par exemple pour partager une valeur entre plusieurs contextes d’exécution.

La partie variable d’un template se déclare via les symboles : <%= XXXX %>.

Un exemple d’utilisation d’un template.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = function(grunt) {

  grunt.initConfig({
      multitask1: {
          options:{key:'<%= version %>'},
          conf1:[1,2,3]
      },
      version : 'v15'
  });

  grunt.registerMultiTask('multitask1', function(){
      console.log("version = " + this.options().key);
  });
}

L’exécution de la tâche multitask1 affiche bien le numéro de version v15.

1
2
3
4
5
6
>grunt multitask1
Running "multitask1:conf1" (multitask1) task
--------------------------------------------
version = v15

Done, without errors.

Plusieurs modes pour déclarer des fichiers

Les différentes tâches utilisées avec Grunt consistent la plupart du temps à manipuler des fichiers.

Il existe plusieurs modes pour désigner des fichiers avec Grunt : mode compact, mode object, mode array et le mode dynamic.

Afin d’illustrer ces différents modes, nous allons utiliser le plugin grunt-contrib-copy qui donne la possibilité de copier des fichiers dans un répertoire.

Installer le plugin

1
npm install grunt-contrib-copy --save-dev

Le plugin est installé dans le répertoire node_modules. Le fichier package.json est mis à jour avec la ligne "grunt-contrib-copy": "~0.4.1" qui vient compléter la section devDependencies.

Mettre à jour le fichier Gruntfile.js comme suit.

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = function(grunt) {

  grunt.initConfig({
      copy: {
          main: {
              ???  
          }
      }
  });

  grunt.loadNpmTasks('grunt-contrib-copy');
}

La fonction grunt.loadNpmTasks charge le plugin grunt-contrib-copy. Ce plugin expose la tâche copy sous la forme d’une multi task configurable dans la fonction grunt.initConfig.

La tâche copy peut être exécutée avec la commande :

1
grunt copy

Mode Compact

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = function(grunt) {

  grunt.initConfig({
      copy: {
          main: {
              src:['*.js', '*.json'],
              dest:'tmp/'  
          }
      }
  });

  grunt.loadNpmTasks('grunt-contrib-copy');
}

Deux paramètres src (fichiers à copier) et dest (répertoire de destination) sont définis.

Mode Object

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = function(grunt) {

  grunt.initConfig({
      copy: {
          main: {
              files : {'tmp/': ['*.js', '*.json']}
          }
          
      }
  });

  grunt.loadNpmTasks('grunt-contrib-copy');
}

Mode Array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = function(grunt) {

  grunt.initConfig({
      copy: {
          main: {
              files : [
                  {
                      src:['*.js', '*.json'],
                      dest:'tmp/'  
                  }
              ]
          }
          
      }
  });

  grunt.loadNpmTasks('grunt-contrib-copy');
}

Mode Dynamic

Ce mode permet une configuration plus fine.

1
2
3
4
5
6
7
8
9
files: [{
  expand:boolean, // true => mode dynamique sinon mode précédent
  cwd:string, // le répertoire de base
  src: [string], // les fichiers sources
  dest: string, // le répertoire de destination
  ext:string, // ajouter une extension aux fichiers 
  flatten:boolean, // ne pas garder l'arborescence et tout mettre au même endroit.
  rename:function // renommer à la volée
}]

Exemple d’utilisation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = function(grunt) {

  grunt.initConfig({
      copy: {
          main: {
              files: [{
                  expand:true,
                  cwd:'node_modules/',
                  src: ['**/*.js'],
                  dest: 'temp/'
              }]
          }
      }
  });

  grunt.loadNpmTasks('grunt-contrib-copy');
}

Quelques trucs & astuces

Les plugins

Utiliser Grunt consiste surtout à ordonnancer des tâches de plugins existants. Il est rare d’avoir besoin de créer ses propres tâches. Il est conseillé de privilégier l’utilisation des plugins dont le nom commence par grunt-contrib. Il s’agit de plugins maintenus officiellement par l’équipe Grunt.

Asynchronisme

Regarder du côté du projet async pour ajouter de l’asynchronisme dans l’exécution des tâches.

Fonctions utilitaires de Grunt

Grunt vient avec quelques fonctions utilitaires comme : grunt.file.copy, grunt.file.readJSON, grunt.template.today("yyyy-mm-dd"), etc. Ne pas hésiter à les utiliser.

Construire un site statique

Que ce soit pour un site web statique ou pour exposer de la documentation, le plugin grunt-carpenter permet de générer un site web statique à partir de fichier markdown et HTML.

Lancer des commandes

Le plugin grunt-shell donne la possibilité de lancer des commandes dans le shell. Ce qui donne un degré de liberté important pour ordonnancer l’exécution des scripts ou même d’autres systèmes de build ;)

Les slides

Elles sont publiées ici.

En somme

Par la richesse de son écosystème de plugins, Grunt est l’outil du moment pour effectuer du build côté front-end. Il est plutôt simple d’utilisation et offre de vraies opportunités de productivité dans le développement web. En donnant la possibilité d’avoir différents contextes d’exécution, il est pratique pour séparer les tâches de développement des tâches de constructions du livrable de production.

Grunt est très lié à l’environnement NodeJS. Ce qui peut constituer un frein à son adoption pour les personnes évoluant dans d’autres écosystèmes. Cette problématique n’est pas propre à Grunt. Les développeurs Java qui vantent par exemple les mérites de Maven auront également du mal à le faire adopter à des personnes qui n’ont pas de JDK sur leur machine. Alors soyons ouvert d’esprit et n’ayons pas peur des écosystèmes qui peuvent nous être étrangers. Vous aimez Jekyll ? Installer Ruby. Vous trouvez AsciiDoc sympa ? Installer Python. Vous aimez l’emploi ? Installer Java ;)

Je pense que la gestion des erreurs de configuration de Grunt ou de ses plugins est perfectible. Durant le workshop, il m’est souvent arrivé d’écrire une configuration non autorisée sans qu’il n’y ait aucune erreur remontée. Par exemple, lorsque vous utiliser les multi task, il faut absolument déclarer au moins un contexte d’éxécution sinon la tâche déclarée ne sera pas exécutée même si vous l’appelez explicitement. Heureusement les erreurs de syntaxes sont plutôt bien remontées.

Grunt en tant qu’outil de build se prête bien à l’intégration continue. Alors il ne faut pas hésiter !

C’était la première fois que je participais à une soirée du Nantes JS. Les organisateurs et les participants ont été très accueillants, vous aviez une bière dans vos mains en guise de bonsoir. Je vous recommande vivement ces soirées que ce soit pour la bière ou pour votre amour de javascript (oui il y en a qui aime) ;)

La prochaine soirée est prévue au mois de janvier, restez connecter via @NantesJS.

Comments