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 conf1
et 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
14
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.