I. Grunt (1) : The JavaScript Task Runner▲
Dans vos développements Web, vous êtes souvent amené à exécuter des tâches récurrentes en marge de votre cœur de métier comme de la minification ou de la concaténation de fichiers JavaScript ou CSS, de la compilation de LESS ou de SASS, etc. Ces actions prennent du temps et si certaines peuvent être faites à la main, l'intervention humaine augmente le risque d'erreur. C'est là qu'un outil tel que Grunt intervient.
Vous allez découvrir dans cette suite de tutoriels ce qu'est un lanceur de tâches JavaScript et, plus spécifiquement, ce que Grunt peut apporter à vos projets par l'automatisation de toutes ces tâches qui font taches et vous gâchent l'existence.
I-A. En bref, Grunt c'est quoi ?▲
À l'origine de Grunt en 2012, il y a Ben ALMAN, contributeur open source (jQuery, jQuery Mobile, Modernizr) et ambassadeur JavaScript et Bonnes Pratiques jQuery.
Sur le site officiel de Grunt, l'outil est défini comme un « Javascript task-runner » ou lanceur de tâches JavaScript. Qu'entend-on par là ?
- JavaScript : un module Node.js - par définition codé en JavaScript - permettant de créer des tâches codées également en JavaScript ;
- Lanceur de tâches : un outil permettant d'automatiser des tâches répétitives pour simplifier et accélérer les développements front-end.
Actuellement Grunt en est à la version 0.4.5. Cela signifie que l'équipe Grunt estime que l'API ne peut pas encore être considérée comme stable et que tout peut changer à tout moment.
I-B. Quels types de tâches ?▲
Pour comprendre l'intérêt de l'automatisation de tâches, il faut savoir identifier ce que sont ces tâches en développement Web. Par exemple :
- Tests unitaires : vérifier le bon fonctionnement et la non-régression de l'application. Des frameworks de tests comme Jasmine, Mocha et QUnit existent en plugins Grunt ;
- Minification : réduire la taille des fichiers, par exemple en supprimant les espaces et retours à la ligne et en remplaçant les noms de variables et de fonctions par des noms le plus courts possible ;
- Concaténation : réduire le nombre de fichiers à charger en concaténant leur contenu dans un seul fichier ;
-
Transcompilation : convertir d'un code source écrit dans un langage en un autre langage, comme :
- LESS, SASS ou Stylus vers CSS,
- CoffeeScript vers JavaScript,
- Jade ou HAML vers HTML ;
- Linting : analyser du code pour la détection de probables erreurs. Cette détection se base sur un ensemble de règles syntaxiques et de bonnes pratiques comme utiliser les accolades en JavaScript même si le bloc ne contient qu'une instruction ;
- Déploiement : copier les distribuables dans les répertoires cibles des serveurs Web.
À la lecture de ces quelques exemples, vous êtes, je pense, en terrain connu et vous percevez déjà l'intérêt d'un seul outil capable de gérer tout cela pour vous. Comme vous le verrez par la suite, Grunt vous permet de réaliser tout cela et plus.
I-C. Pourquoi utiliser un outil tel que Grunt ?▲
Le temps où un site Web n'était que pages statiques et liens est loin. Aujourd'hui, les interfaces se font riches et l'on parle non plus de sites, mais d'applications Web quand il s'agit de produits évolués, voire professionnels, comme Gmail ou Facebook. Le code front-end sous-jacent est de plus en plus complexe et il est important qu'il reste cohérent et maintenable.
Votre projet doit être DRY - Don't Repeat Yourself - et pour cela il vous faut standardiser certains processus comme :
- la gestion des dépendances web : cf. Bower ;
- l'automatisation des tâches récurrentes : Grunt.
Les principaux avantages de l'utilisation de Grunt :
- Utilité : gestion d'un grand nombre de processus récurrents à réaliser au build de l'application. Ces tâches peuvent être lancées manuellement, en réponse à la modification de certains fichiers ou par une autre tâche ;
- Rendement : gain de temps en automatisant les tâches avec un outil ;
- Consistance : moins d'interventions humaines, un même procédé pour toute l'équipe, une logique de build de l'application découplée du développement ;
- Efficacité : le développeur reste maintenant concentré sur le développement ;
- Communauté : une communauté forte assure un bon support en cas de problème et la mise à disposition d'un nombre substantiel de plugins. À l'heure actuelle, il y plus de 3000 plugins mis en ligne.
I-D. Grunt, tout seul sur le marché ?▲
Non, il y a d'autres lanceurs de tâches JavaScript sur le marché. Le plus sérieux concurrent actuellement étant Gulp.
II. Avant de commencer▲
II-A. Node.js▲
Grunt et ses plugins sont des packages Node.js, c'est pourquoi leur installation passe par npm, le gestionnaire de modules de Node.js. Le prérequis sous-jacent est donc que Node.js et npm doivent être installés sur votre poste de travail.
Pour installer Node.js dans votre environnement préféré (Windows, Mac OS X, Linux, Solaris), visitez le site officiel de Node.js.
Pour vous documenter sur l'utilisation de npm, visitez le site officiel de npm.
II-B. Le CLI de Grunt (Command Line Interface)▲
Vous devez tout d'abord installer globalement le CLI - ou interface ligne de commande - de Grunt.
$ npm install -
g grunt-
cli
L'installation de ce package Node.js met la commande grunt dans votre path système, ce qui permet de l'utiliser depuis n'importe quel répertoire.
Cela n'installe pas le lanceur de tâches lui-même ! La fonction du CLI est de fournir une commande unique - grunt - pour lancer n'importe quelle version du lanceur de tâches Grunt installée près d'un Gruntfile. Cela permet la cohabitation de versions différentes de Grunt sur la même machine.
Chaque fois que la commande grunt est exécutée sur un répertoire, ça recherche le Grunt installé localement en utilisant le système require() de Node.js. Grâce à cela, vous pouvez exécuter la commande depuis n'importe quel sous-répertoire du projet.
Si une version de Grunt est détectée, le CLI charge la version locale de la bibliothèque Grunt, applique la configuration du Gruntfile et exécute les tâches demandées.
III. Mise en place d'un projet Grunt▲
Node.js, npm, grunt-cli sont installés ! Voyons maintenant comment mettre en place l'environnement Grunt dans un projet.
III-A. Lien entre npm et Grunt▲
Comme je vous l'ai déjà dit un peu plus tôt, les plugins grunt sont des modules Node.js et npm est le gestionnaire de modules Node.js.
Il permet de publier des modules dans le dépôt npm sous un nom unique, ses objectifs étant le partage et la récupération de code depuis un dépôt public.
Le système de modules Node.js est une implémentation de la spécification CommonJS. CommonJS décrit une syntaxe simple pour les programmes JavaScript pour importer (require()) d'autres programmes JavaScript dans leur contexte. Cette fonctionnalité aide grandement à la création de systèmes modulaires en simplifiant le processus de SoC (Separation of Concerns). Dans Node.js, tout fichier JavaScript peut être vu comme un module individuel.
Un module est un programme JavaScript pouvant être chargé avec require() dans un programme Node.js. Cela peut être :
- un répertoire contenant un fichier package.json contenant lui-même un objet JSON avec un champ main ;
- un répertoire contenant un fichier index.js ;
- un fichier JavaScript.
Un package peut être :
- Un répertoire contenant un programme décrit par un fichier package.json ;
- Une archive compressée au format gzip contenant (1) ;
- Une URL sur un (2) ;
- Un <nom>@<version> publié dans le registre avec (3) ;
- Un <nom>@<tag> qui pointe sur (4) ;
- Un <nom> dont le dernier tag satisfait (5) ;
- Une URL git qui, quand elle est clonée, donne un (1).
Grunt et ses plugins sont des packages Node.js, et donc des modules Node.js. Les modules sont installés par npm dans des répertoires node_modules.
III-B. Arborescence de répertoires▲
Partons d'un projet Web video-library dont l'arborescence initiale est la suivante :
Votre application est fonctionnelle, mais vous voulez utiliser Grunt pour automatiser diverses tâches. Ces tâches auront pour but de créer un livrable de votre application à partir des fichiers source. Il faut donc organiser votre projet de sorte à clairement identifier le code d'origine du code distribuable généré. L'usage est donc d'organiser son projet de manière à avoir le code d'origine dans un répertoire source et de prévoir au même niveau un répertoire target où seront générés les fichiers distribuables.
L'utilisation de Grunt nécessitera la présence des deux fichiers package.json et Gruntfile.js à la racine de votre projet. L'installation de Grunt et de ses plugins implique la création par npm du répertoire node_modules. La structure du projet va devenir donc celle-ci :
III-C. Recherche et installation de package avec npm▲
Maintenant que l'arborescence de votre projet est en place, vous pouvez rechercher et installer des packages !
npm est un utilitaire en ligne de commande. Vous pouvez l'utiliser avec l'invite de commande Windows, Windows PowerShell, Terminal, Git Bash, etc. L'objectif premier de npm est la recherche et l'installation de modules Node.js.
Utilisez la commande npm search suivie d'un ou plusieurs mots-clés pour obtenir la liste des packages qui correspondent. Pour chercher un plug-in Grunt, utilisez le mot-clé gruntplugin qui est le tag recommandé par l'équipe Grunt. Les mots-clés sont recherchés dans le fichier package.json qui contient les métadonnées d'un package : title, tags, description, dependencies, etc.
$ npm search <
mots-
clés>
Vous avez identifié un package qui vous semble intéressant ? Affichez le contenu de son package.json pour obtenir plus d'informations !
$ npm info <
nom_du_package>
Ce package vous plait ? Visitez son dépôt avec la commande npm repo qui ouvre votre navigateur par défaut.
$ npm repo <
nom_du_package>
Pour installer le package, utilisez la commande npm install. Vous pouvez l'installer globalement dans le sous-répertoire node_modules du répertoire de npm défini dans le PATH pour l'utiliser comme utilitaire en ligne de commande,
$ npm install -
g <
nom_du_package>
ou localement dans le répertoire node_modules d'un projet s'il s'agit d'une dépendance de l'un de vos projets.
$ npm install <
nom_du_package>
$ npm install <
nom_du_package>
--
save-
dev
Pour installer localement un package, l'utilitaire de ligne de commande que vous utilisez doit pointer au choix :
- Sur le répertoire de votre projet qui contient le fichier package.json de votre projet ;
- Sur un répertoire dont le premier parent contenant un fichier package.json est le répertoire de (1) ;
- Sur un répertoire de votre projet sans package.json à son niveau ou dans sa parentèle.
Dans les cas (1) et (2), npm détecte le projet auquel ajouter la dépendance et cet ajout se traduit par :
- le téléchargement du package dans un répertoire node_modules au même niveau que le package.json détecté ;
- l'ajout d'une référence au package téléchargé dans les dépendances du projet par complétion de la variable dependencies du fichier package.json de votre projet si l'option --save-dev est utilisée.
Dans le cas (3), npm :
- télécharge le package dans un répertoire node_modules au niveau du répertoire pointé par l'utilitaire de ligne de commande.
Considérant que l'utilitaire de ligne de commande pointe sur le répertoire sub_nested pour le projet top, illustrons les trois cas possibles pour l'installation du package grunt-contrib-uglify :
$ npm install grunt-
contrib-
uglify
|
|
|
III-D. package.json▲
package.json est le fichier qui décrit un package Node.js. Il contient toutes les métadonnées et les dépendances du projet. Ces informations sont nécessaires à npm pour installer Grunt et ses plugins.
package.json contient un unique objet JSON (JavaScript Object Notation). Ce fichier peut être généré par npm ou créé manuellement. À partir de ce fichier, placé à la racine de votre projet, npm est capable d'installer toutes les dépendances du projet !
Il existe plusieurs façons de créer ce fichier :
- En suivant les directives de la commande npm init ;
-
En partant d'un exemple minimaliste
Sélectionnez1.
2.
3.
4.{
"name"
:
"mon projet"
,
"version"
:
"0.1.0"
}
- En partant d'un template grunt-init.
Voici la liste des principaux champs que l'on trouve dans l'objet JSON du fichier package.json :
- name string : le nom du package ;
- version string : la version du package ;
- description string : la description du package. Ce champ est scruté par npm search;
- keywords string[] : les mots-clés du package. Cette liste est scrutée par npm search;
- license string : la licence pour l'utilisation du package ;
- author string : l'auteur du package ;
- main string : l'ID du module qui est le point d'entrée primaire du programme ;
- dependencies hash : hash des noms de packages/plage de versions (SemVer) requis par le package pour fonctionner ;
- devDependencies hash : hash des noms de packages/plage de versions (SemVer) pour les tests et la transcompilation du package ;
- repository hash : définition du dépôt du package. L'objet est composé des champs type (par exemple git, svn…) et url.
Créons ce fichier dans notre projet video-library avec npm init en suivant les étapes proposées par la commande :
Un fichier package.json basique vient d'être créé dans le projet.
Installons maintenant le lanceur de tâches de Grunt et les plugins grunt-contrib-uglify et grunt-processhtml avec la commande npm install :
L'installation commence par deux messages d'alertes :
- il n'y a pas d'indication de repository pour le package video-library dans package.json ;
- il n'y a pas de fichier README.md à la racine du répertoire du package video-library.
Rien de grave :
- si le projet est sur un dépôt, renseignez le champ repository dans le package.json ;
- mettez à la racine du projet un fichier README.md dans lequel vous expliquez ce qu'est ce projet.
Après ces alertes, vous voyez les packages installés dans leur version courante - puisqu'aucun numéro de version n'a été précisé dans la commande npm install - et l'arbre des dépendances de ceux-ci - qui sont également chargés.
Le répertoire node_modules, s'il n'existait pas, a été créé. Les trois packages grunt, grunt-contrib-uglify et grunt-processhtml ont été chargés dans le répertoire.
Par contre le fichier package.json n'a pas été modifié et aucune mention n'est faite des dépendances du projet video-library sur ces trois packages. Pour que le package.json soit automatiquement modifié par la commande npm install, utilisez l'option --save-dev.
2.
3.
4.
5.
"devDependencies"
:
{
"grunt"
:
"^0.4.5"
,
"grunt-contrib-uglify"
:
"^0.5.1"
,
"grunt-processhtml"
:
"^0.3.3"
}
L'avantage d'enregistrer les dépendances dans le package.json est que si vous prenez en main un projet sans le répertoire node_modules, mais avec le fichier package.json, vous pouvez installer toutes les dépendances avec la commande npm install sans paramètre.
Les packages Grunt ont été identifiés comme devDependencies, ce qui signifie qu'ils ne sont pas nécessaires au package quand celui-ci sera distribué.
III-E. Gruntfile.js▲
Une version de Grunt et deux plugins sont installés dans le projet. Il ne reste plus qu'à les utiliser. Pour orchestrer cela, il y a le fichier Gruntfile.
La commande grunt cherche le fichier Gruntfile de manière similaire à npm avec package.json. Le Gruntfile - .js ou .coffee - est un fichier JavaScript ou CoffeeScript à la racine du projet, au même niveau que le package.json. Il est constitué des éléments suivants :
- Fonction wrapper ;
- Configuration du projet et des tâches ;
- Chargement des plugins et des tâches ;
- Tâches personnalisées.
Dans la suite du tutoriel, le Gruntfile considéré sera un fichier JavaScript Gruntfile.js.
III-E-1. Fonction wrapper▲
Tout fichier Gruntfile doit définir ce wrapper.
module.
exports =
function(
grunt) {
// Code relatif à grunt
};
La fonction module.exports est retournée lorsque le module est requis par un autre module. En l'occurrence, Grunt requiert ce module en lui passant en paramètre un objet grunt utilisé pour interagir avec lui. Grunt expose l'ensemble de ses méthodes et propriétés (API Grunt) via cet objet grunt. Dans l'API, on a notamment les méthodes suivantes :
- grunt.initConfig : initialise l'objet de configuration grunt.config ;
- grunt.registerTask : enregistre une tâche ou un ensemble d'alias de tâches sous un alias ;
- grunt.registerMultiTask : enregistre une « multitâche ». Une multitâche est une tâche avec plusieurs « targets » ;
- grunt.loadNpmTasks : charge les tâches d'un plugin Grunt.
III-E-2. Configuration du projet et des tâches▲
La configuration des tâches est stockée dans l'objet grunt.config. Son initialisation se fait en appelant la méthode grunt.initConfig avec un objet en argument.
Vous pouvez stocker toute donnée arbitraire dans l'objet de configuration tant que cela n'entre pas en conflit avec les propriétés attendues par les tâches. Comme il s'agit de JavaScript, vous n'êtes pas limité au JSON ; vous pouvez utiliser tout JavaScript valide et même générer l'objet par programme.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
//Configuration du projet
grunt.initConfig
({
uglify
:
{
options
:
{
banner
:
'/*Mon package : <%=grunt.template.today("yyyy-mm-dd")%>*/
\n
'
},
build
:
{
src
:
'source/monPackage.js'
,
dest
:
'target/monPackage.min.js'
}
}
}
);
Comme la plupart des tâches, la tâche uglify du plugin grunt-contrib-uglify s'attend à trouver une propriété de même nom uglify dans la configuration du projet. L'option banner sert à paramétrer le texte à afficher dans le fichier build.dest minifié, généré à partir de la source build.src.
III-E-3. Chargement des plugins et des tâches▲
Dès qu'un plugin est déclaré comme dépendance dans le package.json et installé via npm install dans le projet, il peut être chargé dans le Gruntfile avec la fonction grunt.loadNpmTasks :
// Charge le plugin qui fournit la tâche 'uglify'.
grunt.loadNpmTasks
(
'grunt-contrib-uglify'
);
Note : la commande grunt --help liste les tâches disponibles.
III-E-4. Tâches personnalisées▲
Vous pouvez créer des tâches personnalisées. Une tâche personnalisée est soit une tâche que vous définissez vous-même en JavaScript, car vous n'avez pas trouvé votre bonheur parmi les plugins disponibles, soit une tâche regroupant d'autres tâches sous un alias.
Un exemple de tâche simple :
2.
3.
4.
// Une tâche très très simple.
grunt.registerTask
(
'simple'
,
'Elle trace ma tâche.'
,
function(
) {
grunt.
log.write
(
'Trace un truc...'
).ok
(
);
}
);
Un exemple d'alias :
// Tâches par défaut.
grunt.registerTask
(
'default'
,
[
'uglify'
,
'simple'
]
);
L'alias default sert à définir la séquence de tâches exécutées par la commande grunt sans paramètre. Ici, l'exécution de la tâche par défaut lancera séquentiellement les tâches uglify et simple.
III-E-5. Un Gruntfile.js complet▲
En reprenant l'ensemble des parties du Gruntfile.js, voici un fichier complet.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
module.
exports =
function(
grunt) {
//Configuration du projet
grunt.initConfig
({
uglify
:
{
options
:
{
banner
:
'/*Mon package : <%=grunt.template.today("yyyy-mm-dd")%>*/
\n
'
},
build
:
{
src
:
'source/monPackage.js'
,
dest
:
'target/monPackage.min.js'
}
}
}
);
// Charge le plugin qui fournit la tâche 'uglify'.
grunt.loadNpmTasks
(
'grunt-contrib-uglify'
);
// Une tâche très très simple.
grunt.registerTask
(
'simple'
,
'Elle trace ma tâche.'
,
function(
) {
grunt.
log.write
(
'Trace un truc...'
).ok
(
);
}
);
// Tâches par défaut.
grunt.registerTask
(
'default'
,
[
'uglify'
,
'simple'
]
);
};
Avec ce Gruntfile, vous pouvez exécuter les tâches simple et default. Si vous voulez pouvoir exécuter la tâche uglify seule, il faut au préalable l'enregistrer avec grunt.registerTask.
IV. Et après ?▲
Dans ce premier tutoriel, vous avez découvert Grunt et obtenu des réponses aux questions suivantes :
- Qu'est-ce que Grunt ?
- Comment l'intégrer à un projet Web ?
- Comment le configurer ?
Dans le prochain tutoriel, je vous présenterai l'API Grunt.
V. Remerciements▲
Nous remercions la société SOAT qui nous a autorisés à publier ce tutoriel.
Nous remercions également Winjerome pour la mise au gabarit et Claude Leloup pour la relecture orthographique.