Gulp : Retour d'expérience

Comme promis, Gulp revient à l'ordre du jour de ce Blog avec un retour d'expérience sur l'utilisation de Gulp et le développement de plugins.

Suite à la comparaison entre Gulp et Grunt, vous avez peut-être envie d'aller plus loin dans l'utilisation de Gulp. Dans ce billet, je reviendrais sur les fondamentaux de Gulp, les bonnes pratiques et aussi la création de plugins.

Installation

Pour commencer à utiliser Gulp, deux petites actions doivnet être réalisées. Premièrement, l'installation de Gulp de manière globale :

npm install -g gulp

Ceci nous permettra de démarrer Gulp, quelque soit le dossier dans lequel on se trouve. Ensuite, nous devons installer Gulp localement à chaque projet pour lequel on souhaite l'utiliser.

cd monprojet && npm install gulp --save-dev

Nous sommes prêts à commencer à créer nos tâches Gulp dans le fichier gulpfile.js qui est par convention, le fichier qui doit les contenir :

touch gulpfile.js && vim gulpfile.js

Principe de Gulp

Les tâches

Le principe de Gulp est très simple. Le fichier gulpfile.js contient la déclaration de tâches. Ces dernières sont déclarées de la manière suivante :

Cette tâche est nommée clean et équivaut à une commande rm -rf (d'où le nom du module). Une fois déclarée, notre tâche pourra être appelée à tout moment depuis la console grâce à la commande suivante :

gulp clean

Il est aussi possible de l'appeler directement dans une autre tâche avec la méthode gulp.run :

gulp.run('clean', function() { console.log('Terminé') };

Cependant, je ne vous conseille pas l'utilisation de cette méthode car elle est encore un peu boguée et ne fonctionne pas comme on pourrait s'y attendre.

En réalité, il est préférable d'utiliser les dépendances qui peuvent être déclarées au niveau de la déclaration d'une tâche comme ici pour le gulpfile de ChtiJS.

Les streams

Passons maintenant à la véritable particularité de Gulp. La plupart des tâches dont on a besoin pour un projet sont en réalité des tâches qui sont appliquées sur un ensemble de fichiers contenus dans une répertoire donné.

L'idée de Gulp est de créer un stream d'objets représentant chacun de ces fichiers (avec gulp.src) que l'on pourra modifier au travers de divers plugins. Ces plugins sont en fait des streams d'objets de type Stream.Transform.

Les plugins opèrent des modifications sur le contenu des fichiers et/ou sur leur propriétés (chemin, nom de fichier et/ou extension).

À l'autre bout de la chaîne, on peut utiliser gulp.dest pour sauvegarder les modifications effectuées. Un "pipeline" typique avec Gulp donne ceci :

Il est possible de subordonner un plugin à une condition particulière avec gulp.env et gulp-if. Ici, selon la valeur de gulp.env.prod, on minifie ou non les CSS et on utilise Livereload ou non.

Via la ligne de commande, il nous suffira d'ajouter le paramètre suivant pour que gulp.env.prod soit vrai :

gulp css --prod

Contenu : Buffer ou streams ?

Il existe deux modes différents pour le contenu des fichiers. Le mode buffer comme son nom l'indique traite le contenu du fichier comme un unique buffer (c'est à dire, une zone contigüe de la mémoire virtuelle, ou encore, un objet contenu dans la zone Heap du processus).

En mode buffer, la plupart des transformations sur le contenu des fichiers sont réalisées de manière synchrone et on ne peut pas traiter des fichiers trop volumineux sans une dégradation très importante des performances.

Je ne suis personnellement pas fan du mode buffer, bien qu'il soit activé par défaut. À vrai dire, pour ma part, j'aurais préféré que les objets passés aux plugins soient en réalité directement des streams qui, par convention, auraient une propriété réservée aux méta-données (chemin, nom de fichier, répertoire courant etc.).

Ce qui nous amène au mode stream, celui que j'affectionne le plus. Le contenu des fichiers y est traité cette fois de manière plus fluide, par morceaux. L'avantage de ce mode est qu'il est entièrement asynchrone. Les données sont traitées au fur et à mesure des retours des appels système de lecture et d'écriture sur le disque.

Grâce à la nouvelle API des streams de Node (parfois appelée Streams2, dont je parlerais en détail dans un futur billet), les traitements sont ordonnancés selon la disponibilité des ressources (concept de backpressure). Ainsi, théoriquement, il n'y a aucune limite dans la taille des fichiers traités ou dans leur nombre.

Malheureusement, il y a une certaine incompréhension/difficulté avec l'utilisation des streams. Ainsi, peu de développeurs de plugins implémentent le support de ces derniers. De plus, l'utilisation de event-stream est conseillée malgré l'utilisation de l'évènement data qui dans la nouvelle version de Node n'est pas conseillée.

Le choix du mode buffer ou stream se fait au niveau de gulp.src ou tout autre plugin devant générer de nouveaux fichiers sans qu'il soit possible de détecter le mode courant. Pour utiliser les streams avec gulp.src, il suffit de passer en deuxième paramètre un objet d'options contenant une propriété buffer valant false.

Quelques plugins utiles

Voici une petite liste de plugins qui vous seront bien utile :

Créer un plug-in Gulp

Étant donné le faible nombre de plugins Gulp, il est fort probable que vous souhaitiez en créer un. Voici donc quelques conseils que je vais illustrer avec le code de gulp-cat et gulp-svg2ttf.

Tout d'abord, un plugin est un module NPM qui exporte une fonction qui retourne un stream d'objets. Idéalement, cette fonction doit être nommée pour faciliter le débogage.

Selon moi, il très important d'hériter des interface Stream.* de NodeJS plutôt que d'utiliser des modules comme event-stream ou event-map. C'est certe, un peu plus verbeux, mais ces interfaces ont été pensées pour conserver les bénéfices de l'usage des streams. La plupart du temps, vous utiliserez une instance de l'interface Stream.Transform qui doit être augmentée d'une méthode _transform qui comme son nom l'indique gère la transformation du contenu du stream (ici, les fichiers) et d'une méthode _flush, optionnelle, dans le cas où vous souhaiteriez faire une action particulière à la fin du flux d'objets.

Au sein de la méthode de transformation des fichiers, dans la plupart des cas, les fichier dont le contenu est nul seront ignorés. Puis, selon que le contenu du fichier est un buffer ou un stream, on modifiera le buffer ou, on pipera le stream dans un nouveau stream de transformation.

Si l'on souhaite uniquement lire les données, pour le buffer, il suffit d'accèder à la propriété contents pour en lire le contenu. Pour un stream, il faudra le dédoubler via une instance de Stream.PassThrough afin de garantir aux autre plugins un accès à toute les données du stream.

Enfin, une fois le buffer transformé ou le stream du contenu du fichier "pipé", on passe l'objet représentant le fichier au plugin suivant et on appelle le callback reçu en argument de la méthode _transform.

Si durant ce processus, vous ne traitez que les fichiers d'un genre particulier, il peut-être utile de tester l'extension du fichier et de passer directement au plugin suivant tout fichier ne correspondant pas aux critères souhaités.

De la même façon, une option permettant de cloner les fichiers avant de les transformer peut être très utile pour faciliter l'usage d'un plugin.

Bien-sûr, si une transformation d'un fichier, implique le changement de type de ce dernier, il est de votre responsabilité de changer l'extension de ce dernier.

Enfin, toute erreur doit être signalée par l'émission d'un évènement.

Supportez les streams !

Bien-sûr, créer une API streamable n'est pas la solution de facilité. Mais je vous encourage grandement à le faire. Qui peut le plus, peut le moins. J'ai bien écris trois fois cette maxime sur ce blog, mais en la matière mieux vaut trop que pas assez.

Si vous pensez que votre API ne peut pas utiliser les streams, je vous enjoint à regarder le code de Browserify. Si ils ont réussi à tirer parti des streams pour un module aussi complexe, il y a fort à parier que votre format XML peut le faire également.

Si cependant, vous n'avez pas la main sur la bibliothèque wrappée par votre plugin, utilisez BufferStreams en attendant et créer une issue, ou mieux, faîtes une PR !

Voilà, c'est tout pour aujourd'hui ! Vos retours ou questions sont les bienvenues :).

< Blog