Tutoriel pour apprendre à utiliser l'outil JavaScript Grunt - Créer des tâches

Nourdine FALOLA (Soat)

La lecture des précédents tutoriels de cette série fait que l'API Grunt et la configuration des tâches n'ont plus de secret pour vous ! Passons à la création de tâches !

Simple, à cibles multiples, alias, asynchrone : il y en a pour tous les goûts.

Commentez

- Titre : Tutoriel pour apprendre à utiliser l'outil JavaScript Grunt - Créer des tâches

- Auteur : Nourdine FALOLA (Soat)

- Parution : 20 avril 2017

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2016 Nourdine FALOLA. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Que contient ce tutoriel ?

- II. Créer des tâches…

- II-A. Simples

- II-A-1. Contexte : piqûre de rappel

- II-A-2. registerTask

- II-A-3. Comme fonction

- II-A-4. Comme alias

- II-B. À cibles multiples

- II-B-1. Késako ?

- II-B-2. Contexte : piqûre de rappel

- II-B-3. registerMultiTask

- II-B-4. Exemple

- II-C. Asynchrones

- III. Ne réinventez pas la roue…

- IV. Et après ?

- V. Remerciements

I - Que contient ce tutoriel ?

Dans les tutoriels précédents, vous avez eu un aperçu des deux types de tâches proposés par Grunt. Dans ce tutoriel, vous allez en apprendre les bases de l'écriture.

1. tâche simple

2. tâche à cibles multiples ou « multitâche »

Les tâches Grunt mises en file s'exécutent toujours séquentiellement : lorsqu'une se termine avec succès, la suivante peut commencer ; lorsqu'une tâche se termine en erreur, l'exécution de la séquence est interrompue. Ce mécanisme séquentiel fonctionne naturellement si le code des tâches est synchrone. En revanche, s'il comporte une composante asynchrone, cela peut casser la gestion continu-si-succès/stop-si-erreur de la séquence. Grunt fournit un mécanisme permettant de conserver ce mode de fonctionnement.

1. tâche asynchrone

II - Créer des tâches…

Oui, mais quels types de tâches peut-on créer ? Si vous avez lu les premiers articles de cette série consacrée à Grunt, vous connaissez la réponse :

- des tâches « simples » : une configuration ou pas de configuration, une tâche à réaliser ;

- des tâches « à cibles multiples » : un ensemble de configurations, une tâche à réaliser pilotée par la cible.

Une tâche, classiquement, est synchrone. Cela signifie que l'exécution de son code est séquentielle et que l'action suivante ne s'exécutera qu'une fois que l'action courante s'est terminée. Grunt propose cependant un mécanisme permettant à une tâche - simple ou à cibles multiples - de s'exécuter de manière asynchrone. Nous verrons dans une section séparée comment rendre une tâche asynchrone.

Voyons tout de suite ça plus en détail…

II-A - Simples

II-A-1 - Contexte : piqûre de rappel

Une tâche est une fonction JavaScript. En JavaScript, le mot-clé this utilisé dans le corps d'une fonction se rapporte à l'objet contexte de celle-ci. Dans le cas des tâches Grunt, le contexte permet d'accéder à un certain nombre de propriétés et de méthodes utiles. Dans les exemples suivants, vous verrez que l'objet configuration relatif à une tâche est accessible via this.options().

Je vous renvoie aux précédents articles pour plus de détails :

- l'API Grunt : this ;

- configurer les tâches : this.options().

II-A-2 - registerTask

Créer une tâche simple revient à appeler la méthode grunt.task.registerTask en lui passant en paramètre :

1. Un nom de tâche, une description (facultatif) et une fonction avec ou sans paramètre.

grunt.registerTask(taskName, [description, ] taskFunction)

2. Un nom de tâche, une description (facultatif) et une liste de tâches simples et/ou à cibles multiples.

grunt.registerTask(taskName, [description, ] taskList)

II-A-3 - Comme fonction

Vous créez une tâche simple lorsque vous avez besoin d'une tâche répondant au moins à l'un de ces critères :

1. Pas de configuration ;

2. Toujours la même action sur les mêmes objets.

Illustrons ces deux cas :

1. Vous voulez toujours copier un fichier F de A vers B ? Une tâche sans configuration ;

2. Vous voulez copier un fichier F de A vers B, mais vous prévoyez que F, A et B puissent être changés au besoin ? Une configuration définissant F, A et B, une tâche qui fait toujours la même action de copie de F de A vers B.

Illustrons le cas 1) précédent :

1. module.exports = function(grunt) {

2. // une tâche simple copiant un fichier donné de A vers B

3. grunt.registerTask('copyF', 'Copy file A into folder B', function(logEnabled) {

4. var F = 'the_file.txt',

5. A = 'c:\\',

6. B = 'd:\\';

8. if (logEnabled){

9. grunt.log.writeln('copy of ' + F + ' from ' + A + ' to ' + B);

10. }

12. grunt.file.copy(A + F, B + F);

14. if (logEnabled){

15. grunt.log.writeln('copy done');

16. }

17. });

18. };

Et le cas 2) :

1. module.exports = function(grunt) {

2. grunt.initConfig({

3. copyF: {

4. options: {

5. F: 'the_file.txt',

6. A: 'c:\\',

7. B: 'd:\\'

8. }

9. }

10. });

12. // une tâche simple copiant un fichier donné de A vers B

13. grunt.registerTask('copyF', 'Copy file A into folder B', function(logEnabled) {

14. var F = this.options().F,

15. A = this.options().A,

16. B = this.options().B;

18. if (logEnabled){

19. grunt.log.writeln('copy of ' + F + ' from ' + A + ' to ' + B);

20. }

22. grunt.file.copy(A + F, B + F);

24. if (logEnabled){

25. grunt.log.writeln('copy done');

26. }

27. });

28. };

II-A-4 - Comme alias

Une tâche simple peut aussi n'être qu'un alias permettant de lancer séquentiellement une liste de tâches simples et/ou à cibles multiples.

// la tâche dist lance séquentiellement chacune des tâches de la liste
grunt.registerTask('dist', ['jshint', 'qunit', 'concat:dist', 'uglify:dist']);

II-B - À cibles multiples

II-B-1 - Késako ?

Une tâche à cibles multiples, ou « multitâche », est une tâche pour laquelle les propriétés de l'objet de configuration - à l'exception de la propriété options - sont autant de configurations individuelles.

Chacune de ces configurations est appelée « cible ». Si la tâche s'exécute sur une cible, elle s'exécute avec la configuration de cette cible. Si la tâche s'exécute sans qu'une cible ne soit spécifiée, elle s'exécute séquentiellement pour chacune des cibles définies.

Quel avantage par rapport à une tâche simple ?

Une tâche simple produit le même résultat à chaque exécution, sauf si la fonction qui la définit accepte des arguments permettant de piloter son comportement. Cependant :

- les arguments de la fonction ne permettent pas la même flexibilité que la configuration ;

- les arguments de la fonction ne sont pas gérés par Grunt : cf. Configurer les tâches (options, fichiers src-dest, templates, etc.).

Au crédit des tâches à cibles multiples :

- la même tâche peut être exécutée plusieurs fois au cours d'un même process avec des configurations différentes et donc produire des résultats différents ;

- la même tâche peut être exécutée dans des process différents avec des configurations différentes et donc produire des résultats différents suivant le process ;

- les tâches à cibles multiples fournissent à support à la gestion des fichiers source-destination qui n'existe pas pour les tâches simples.

II-B-2 - Contexte : piqûre de rappel

Le contexte des tâches à cibles multiples fournit les mêmes fonctionnalités que les tâches simples, mais pas que ! Je vous renvoie à la section sur le contexte des tâches à cibles multiples de l'article sur l'API Grunt pour plus d'informations.

II-B-3 - registerMultiTask

Créer une tâche à cibles multiples revient à appeler la méthode grunt.registerMultiTask en lui passant en paramètre :

1. Un nom de tâche ;

2. Une description (facultatif) ;

3. Une fonction avec ou sans paramètre

grunt.registerMultiTask(taskName, [description, ] taskFunction)

II-B-4 - Exemple

Reprenons l'exemple de la tâche copyF et adaptons-le pour en faire une tâche à cibles multiples.

1. module.exports = function(grunt) {

2. grunt.initConfig({

3. deployConfig: {

4. dev: {

5. A: 'c:\\dev.config',

6. B: 'd:\\application.config'

7. },

8. uat: {

9. A: 'c:\\uat.config',

10. B: '\\\\UAT_SERVER\\c$\\application.config'

11. },

12. prod: {

13. A: 'c:\\prod.config',

14. B: '\\\\PROD_SERVER\\c$\\application.config'

15. }

16. }

17. });

19. // une tâche simple copiant un fichier donné de A vers B

20. grunt.registerTask('deployConfig', 'Deploy config from A to B', function(logEnabled) {

21. var A = this.options().A,

22. B = this.options().B;

24. if (logEnabled){

25. grunt.log.writeln('deploy from ' + A + ' to ' + B);

26. }

28. grunt.file.copy(A, B);

30. if (logEnabled){

31. grunt.log.writeln('copy done');

32. }

33. });

34. }

Cette tâche permet de déployer la configuration adaptée à l'environnement ciblé.

II-C - Asynchrones

Une tâche se termine lorsque la fonction retourne. Lorsqu'une vous exécutez une séquence de tâches, par exemple via une tâche alias, une tâche de la séquence ne débutera que lorsque la précédente se termine. Une tâche dans son ensemble est donc synchrone.

Cependant, le code qu'exécute une tâche peut être asynchrone. Typiquement, vous pouvez exécuter une requête AJAX ou tout autre procédé asynchrone. Exécuter du code asynchrone dans une tâche synchrone peut aboutir à la cinématique suivante :

1. Début d'exécution de la tâche A ;

2. Un code asynchrone se lance dans la tâche A ;

3. Fin d'exécution du code synchrone de la tâche A qui retourne avec succès. Le code asynchrone est interrompu ;

4. Début d'exécution de la tâche B ;

5. La tâche B s'exécute sur un contexte potentiellement erroné : des données, des fichiers ou autres n'ont pas encore été traités par le code asynchrone de la tâche A.

À ce stade :

1. Le code asynchrone de la tâche A ne s'est pas exécuté jusqu'à son terme ;

2. La tâche B peut retourner une erreur si elle travaille sur des données erronées du fait du traitement incomplet de la tâche précédente ;

3. La tâche B peut retourner avec succès, mais n'avoir réalisé qu'un traitement partiel : par exemple, si la tâche B copie des fichiers la copie peut n'être que partielle si la tâche A n'a pas fini de les générer au moment où B s'exécute.

Prenons un exemple simple :

1. module.exports = function(grunt) {

2. grunt.registerTask('asyncTask', 'Ma tâche asynchrone.', function(n) {

3. grunt.log.writeln(n + " : avant le code asynchrone")

5. setTimeout(function() {

6. grunt.log.writeln(n + " : code asynchrone");

7. }, n*1000);

9. grunt.log.writeln(n + " : après le code asynchrone");

10. });

12. grunt.registerTask('default', [

13. 'asyncTask:2',

14. 'asyncTask:4',

15. 'asyncTask:1',

16. 'asyncTask:3'

17. ]);

18. }

L'exécution de la tâche default produit le résultat suivant :

Comme vous pouvez le constater dans ce scénario, le code ne s'exécute pas comme on pourrait se l'imaginer. Les tâches s'exécutent séquentiellement, mais aucune ne produit le log de la partie asynchrone. L'exécution du code asynchrone a été interrompue par le retour en succès généré par Grunt à la fin de l'exécution du code synchrone de la tâche.

La solution serait qu'une tâche ayant un code asynchrone ne retourne qu'une fois que ce code a terminé son exécution.

L'objet this expose une fonction this.async qui notifie Grunt que la tâche comporte du code asynchrone. this.async() renvoie une méthode done. L'exécution de done signifiera à Grunt que la composante asynchrone de la tâche s'est terminée. Pour indiquer une fin d'exécution en erreur, il suffit de passer en paramètre de done un objet ou false.

1. module.exports = function(grunt) {

2. grunt.registerTask('asyncTask', 'Ma tâche asynchrone.', function(n) {

3. grunt.log.writeln(n + " : avant le code asynchrone")

5. var done = this.async();

6. setTimeout(function() {

7. grunt.log.writeln(n + " : code asynchrone");

8. done();

9. }, n*1000);

11. grunt.log.writeln(n + " : apres le code asynchrone");

12. });

14. grunt.registerTask('default', [

15. 'asyncTask:2',

16. 'asyncTask:4',

17. 'asyncTask:1',

18. 'asyncTask:3'

19. ]);

20. }

L'exécution de la tâche default produit le résultat suivant :

- les tâches s'exécutent séquentiellement : 2 - 4 - 1 - 3 ;

- pour chaque tâche, le code synchrone s'exécute et la fin du code asynchrone survient en dernier.

III - Ne réinventez pas la roue…

Avant de vous lancer dans la création d'une tâche, demandez-vous si vous n'êtes pas en train de réinventer la roue. Si vous avez besoin de réaliser certaines tâches dans votre projet : n'existe-t-il pas déjà un package qui correspond à votre besoin ?

La communauté Grunt propose à ce jour plus de 3000 plugins permettant de réaliser toutes les actions classiques. Pour n'en citer que quelques-unes (cf. Grunt (1)) :

- tests unitaires ;

- minification ;

- concaténation ;

- transcompilation ;

- linting ;

- file watching ;

- déploiement.

En général, vous trouverez votre bonheur parmi les plugins existants. Le plus gros de votre travail consistera à écrire la configuration adéquate pour utiliser au mieux les packages existants dans vos projets et à créer des tâches « alias » pour lancer un ensemble de tâches avec une seule commande.

IV - Et après ?

À ce stade, vous voilà capable de créer et configurer vos tâches Grunt. Il ne vous manque plus qu'à les exécuter ! Voilà donc tout trouvé le sujet du prochain tutoriel de la série.

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.