Gulp Under the Hood : création d'un outil d'automatisation des tâches basé sur les flux

Publié: 2022-03-11

De nos jours, les développeurs front-end utilisent plusieurs outils pour automatiser les opérations de routine. Trois des solutions les plus populaires sont Grunt, Gulp et Webpack. Chacun de ces outils repose sur des philosophies différentes, mais ils partagent le même objectif commun : rationaliser le processus de création frontal. Par exemple, Grunt est piloté par la configuration alors que Gulp n'applique presque rien. En fait, Gulp s'appuie sur le code d'écriture du développeur pour implémenter le flux des processus de construction - les différentes tâches de construction.

Gulp Under the Hood : création d'un outil d'automatisation des tâches basé sur les flux

Quand il s'agit de choisir l'un de ces outils, mon préféré est Gulp. Dans l'ensemble, c'est une solution simple, rapide et fiable. Dans cet article, nous verrons comment Gulp fonctionne sous le capot en essayant de mettre en œuvre notre propre outil de type Gulp.

API Gulp

Gulp est livré avec seulement quatre fonctions simples :

  • gorgée.tâche
  • gulp.src
  • gulp.dest
  • avaler.regarder

Ces quatre fonctions simples, dans diverses combinaisons offrent toute la puissance et la flexibilité de Gulp. Dans la version 4.0, Gulp a introduit deux nouvelles fonctions : gulp.series et gulp.parallel. Ces API permettent d'exécuter des tâches en série ou en parallèle.

Parmi ces quatre fonctions, les trois premières sont absolument essentielles pour tout fichier Gulp. permettant aux tâches d'être définies et appelées à partir de l'interface de ligne de commande. Le quatrième est ce qui rend Gulp vraiment automatique en permettant l'exécution de tâches lorsque les fichiers changent.

Fichier Gulp

Ceci est un fichier gulp élémentaire :

 gulp.task('test', function{ gulp.src('test.txt') .pipe(gulp.dest('out')); }); 

Il décrit une tâche de test simple. Lorsqu'il est invoqué, le fichier test.txt du répertoire de travail actuel doit être copié dans le répertoire ./out . Essayez-le en exécutant Gulp :

 touch test.txt # Create test.txt gulp test

Notez que la méthode .pipe ne fait pas partie de Gulp, c'est l'API node-stream, elle connecte un flux lisible (généré par gulp.src('test.txt') ) avec un flux inscriptible (généré par gulp.dest('out') ). Toutes les communications entre Gulp et les plugins sont basées sur des flux. Cela nous permet d'écrire du code gulpfile d'une manière aussi élégante.

Rencontrez Plug

Maintenant que nous avons une idée du fonctionnement de Gulp, construisons notre propre outil de type Gulp : Plug.

Nous allons commencer par l'API plug.task. Cela devrait nous permettre d'enregistrer les tâches, et les tâches devraient être exécutées si le nom de la tâche est passé dans les paramètres de la commande.

 var plug = { task: onTask }; module.exports = plug; var tasks = {}; function onTask(name, callback){ tasks[name] = callback; }

Cela permettra aux tâches d'être enregistrées. Maintenant, nous devons rendre cette tâche exécutable. Pour garder les choses simples, nous ne créerons pas de lanceur de tâches séparé. Au lieu de cela, nous l'inclurons dans notre implémentation de plug.

Tout ce que nous avons à faire est d'exécuter les tâches nommées dans les paramètres de ligne de commande. Nous devons également nous assurer que nous essayons de le faire dans la prochaine boucle d'exécution, une fois toutes les tâches enregistrées. La façon la plus simple de le faire est d'exécuter des tâches dans un rappel de délai d'attente, ou de préférence process.nextTick :

 process.nextTick(function(){ var taskName = process.argv[2]; if (taskName && tasks[taskName]) { tasks[taskName](); } else { console.log('unknown task', taskName) } });

Composez plugfile.js comme ceci :

 var plug = require('./plug'); plug.task('test', function(){ console.log('hello plug'); })

… et exécutez-le.

 node plugfile.js test

Il affichera :

 hello plug

Sous-tâches

Gulp permet également de définir des sous-tâches lors de l'enregistrement des tâches. Dans ce cas, plug.task doit prendre 3 paramètres, le nom, le tableau des sous-tâches et la fonction de rappel. Mettons cela en œuvre.

Nous devrons mettre à jour l'API de tâche en tant que telle :

 var tasks = {}; function onTask(name) { if(Array.isArray(arguments[1]) && typeof arguments[2] === "function"){ tasks[name] = { subTasks: arguments[1], callback: arguments[2] }; } else if(typeof arguments[1] === "function"){ tasks[name] = { subTasks: [], callback: arguments[1] }; } else{ console.log('invalid task registration') } } function runTask(name){ if(tasks[name].subTasks){ tasks[name].subTasks.forEach(function(subTaskName){ runTask(subTaskName); }); } if(tasks[name].callback){ tasks[name].callback(); } } process.nextTick(function(){ if (taskName && tasks[taskName]) { runTask(taskName); } });

Maintenant, si notre plugfile.js ressemble à ceci :

 plug.task('subTask1', function(){ console.log('from sub task 1'); }) plug.task('subTask2', function(){ console.log('from sub task 2'); }) plug.task('test', ['subTask1', 'subTask2'], function(){ console.log('hello plug'); })

… le faire fonctionner

 node plugfile.js test

… doit afficher :

 from sub task 1 from sub task 2 hello plug

Notez que Gulp exécute des sous-tâches en parallèle. Mais pour garder les choses simples, dans notre implémentation, nous exécutons les sous-tâches de manière séquentielle. Gulp 4.0 permet de contrôler cela à l'aide de ses deux nouvelles fonctions API, que nous implémenterons plus loin dans cet article.

Origine et destination

Plug sera de peu d'utilité si nous n'autorisons pas la lecture et l'écriture de fichiers. Ensuite, nous allons implémenter plug.src . Cette méthode dans Gulp attend un argument qui est soit un masque de fichier, un nom de fichier ou un tableau de masques de fichiers. Il renvoie un flux de nœud lisible.

Pour l'instant, dans notre implémentation de src , nous n'autoriserons que les noms de fichiers :

 var plug = { task: onTask, src: onSrc }; var stream = require('stream'); var fs = require('fs'); function onSrc(fileName){ var src = new stream.Readable({ read: function (chunk) { }, objectMode: true }); //read file and send it to the stream fs.readFile(path, 'utf8', (e,data)=> { src.push({ name: path, buffer: data }); src.push(null); }); return src; }

Notez que nous utilisons objectMode: true , un paramètre facultatif ici. En effet, les flux de nœud fonctionnent avec des flux binaires par défaut. Si nous devons transmettre/recevoir des objets JavaScript via des flux, nous devons utiliser ce paramètre.

Comme vous pouvez le voir, nous avons créé un objet artificiel :

 { name: path, //file name buffer: data //file content }

… et l'a passé dans le ruisseau.

À l'autre extrémité, la méthode plug.dest doit recevoir un nom de dossier cible et renvoyer un flux inscriptible qui recevra les objets du flux .src. Dès qu'un objet fichier sera reçu, il sera stocké dans le dossier cible.

 function onDest(path){ var writer = new stream.Writable({ write: function (chunk, encoding, next) { if (!fs.existsSync(path)) fs.mkdirSync(path); fs.writeFile(path +'/'+ chunk.name, chunk.buffer, (e)=> { next() }); }, objectMode: true }); return writer; }

Mettons à jour notre plugfile.js :

 var plug = require('./plug'); plug.task('test', function(){ plug.src('test.txt') .pipe(plug.dest('out')) })

… créer test.txt

 touch test.txt

… et lancez-le :

 node plugfile.js test ls ./out

test.txt doit être copié dans le dossier ./out .

Gulp lui-même fonctionne à peu près de la même manière, mais au lieu de nos objets de fichiers artificiels, il utilise des objets en vinyle. C'est beaucoup plus pratique, car il contient non seulement le nom du fichier et son contenu, mais également des méta-informations supplémentaires, telles que le nom du dossier actuel, le chemin d'accès complet au fichier, etc. Il peut ne pas contenir l'intégralité du tampon de contenu, mais il contient à la place un flux lisible du contenu.

Vinyle : mieux que les fichiers

Il existe une excellente bibliothèque vinyl-fs qui nous permet de manipuler des fichiers représentés comme des objets en vinyle. Il nous permet essentiellement de créer des flux lisibles et inscriptibles basés sur le masque de fichier.

Nous pouvons réécrire les fonctions de plug en utilisant la bibliothèque vinyl-fs. Mais nous devons d'abord installer vinyl-fs :

 npm i vinyl-fs

Une fois installé, notre nouvelle implémentation Plug ressemblera à ceci :

 var vfs = require('vinyl-fs') function onSrc(fileName){ return vfs.src(fileName); } function onDest(path){ return vfs.dest(path); } // ...

… et pour l'essayer :

 rm out/test.txt node plugFile.js test ls out/test.txt

Les résultats devraient toujours être les mêmes.

Plugins Gulp

Étant donné que notre service Plug utilise la convention de flux Gulp, nous pouvons utiliser des plugins Gulp natifs avec notre outil Plug.

Essayons-en un. Installez gulp-rename :

 npm i gulp-rename

… et mettez à jour plugfile.js pour l'utiliser :

 var plug = require('./app.js'); var rename = require('gulp-rename'); plug.task('test', function () { return plug.src('test.txt') .pipe(rename('renamed.txt')) .pipe(plug.dest('out')); });

L'exécution de plugfile.js maintenant devrait toujours, vous l'avez deviné, produire le même résultat.

 node plugFile.js test ls out/renamed.txt

Suivi des modifications

La dernière méthode, mais non la moindre, est gulp.watch Cette méthode nous permet d'enregistrer un écouteur de fichiers et d'invoquer des tâches enregistrées lorsque les fichiers changent. Mettons-le en œuvre :

 var plug = { task: onTask, src: onSrc, dest: onDest, watch: onWatch }; function onWatch(fileName, taskName){ fs.watchFile(fileName, (event, filename) => { if (filename) { tasks[taskName](); } }); }

Pour l'essayer, ajoutez cette ligne à plugfile.js :

 plug.watch('test.txt','test');

Désormais, à chaque changement de test.txt , le fichier sera copié dans le dossier out avec son nom modifié.

Série vs parallèle

Maintenant que toutes les fonctions fondamentales de l'API de Gulp sont implémentées, allons plus loin. La prochaine version de Gulp contiendra plus de fonctions API. Cette nouvelle API rendra Gulp plus puissant :

  • gorgée.parallèle
  • gulp.series

Ces méthodes permettent à l'utilisateur de contrôler l'ordre dans lequel les tâches sont exécutées. Pour enregistrer des sous-tâches en parallèle, gulp.parallel peut être utilisé, qui est le comportement actuel de Gulp. D'autre part, gulp.series peut être utilisé pour exécuter des sous-tâches de manière séquentielle, l'une après l'autre.

Supposons que nous ayons test1.txt et test2.txt dans le dossier actuel. Afin de copier ces fichiers dans notre dossier en parallèle, créons un plugfile :

 var plug = require('./plug'); plug.task('subTask1', function(){ return plug.src('test1.txt') .pipe(plug.dest('out')) }) plug.task('subTask2', function(){ return plug.src('test2.txt') .pipe(plug.dest('out')) }) plug.task('test-parallel', plug.parallel(['subTask1', 'subTask2']), function(){ console.log('done') }) plug.task('test-series', plug.series(['subTask1', 'subTask2']), function(){ console.log('done') })

Pour simplifier la mise en œuvre, les fonctions de rappel de la sous-tâche sont conçues pour renvoyer son flux. Cela nous aidera à suivre le cycle de vie du flux.

Nous allons commencer à modifier notre API :

 var plug = { task: onTask, src: onSrc, dest: onDest, parallel: onParallel, series: onSeries };

Nous devrons également mettre à jour la fonction onTask , car nous devons ajouter des méta-informations supplémentaires sur les tâches pour aider notre lanceur de tâches à gérer correctement les sous-tâches.

 function onTask(name, subTasks, callback){ if(arguments.length < 2){ console.error('invalid task registration',arguments); return; } if(arguments.length === 2){ if(typeof arguments[1] === 'function'){ callback = subTasks; subTasks = {series: []}; } } tasks[name] = subTasks; tasks[name].callback = function(){ if(callback) return callback(); }; } function onParallel(tasks){ return { parallel: tasks }; } function onSeries(tasks){ return { series: tasks }; }

Pour simplifier les choses, nous utiliserons async.js, une bibliothèque utilitaire permettant de gérer les fonctions asynchrones pour exécuter des tâches en parallèle ou en série :

 var async = require('async') function _processTask(taskName, callback){ var taskInfo = tasks[taskName]; console.log('task ' + taskName + ' is started'); var subTaskNames = taskInfo.series || taskInfo.parallel || []; var subTasks = subTaskNames.map(function(subTask){ return function(cb){ _processTask(subTask, cb); } }); if(subTasks.length>0){ if(taskInfo.series){ async.series(subTasks, taskInfo.callback); }else{ async.parallel(subTasks, taskInfo.callback); } }else{ var stream = taskInfo.callback(); if(stream){ stream.on('end', function(){ console.log('stream ' + taskName + ' is ended'); callback() }) }else{ console.log('task ' + taskName +' is completed'); callback(); } } }

Nous nous appuyons sur le flux de nœud 'end' qui est émis lorsqu'un flux a traité tous les messages et est fermé, ce qui indique que la sous-tâche est terminée. Avec async.js, nous n'avons pas à faire face à un gros gâchis de rappels.

Pour l'essayer, exécutons d'abord les sous-tâches en parallèle :

 node plugFile.js test-parallel
 task test-parallel is started task subTask1 is started task subTask2 is started stream subTask2 is ended stream subTask1 is ended done

Et exécutez les mêmes sous-tâches en série :

 node plugFile.js test-series
 task test-series is started task subTask1 is started stream subTask1 is ended task subTask2 is started stream subTask2 is ended done

Conclusion

Ça y est, nous avons implémenté l'API de Gulp et pouvons maintenant utiliser les plugins Gulp. Bien sûr, n'utilisez pas Plug dans de vrais projets, car Gulp est plus que ce que nous avons implémenté ici. J'espère que ce petit exercice vous aidera à comprendre comment Gulp fonctionne sous le capot et nous permettra de l'utiliser plus couramment et de l'étendre avec des plugins.

En relation : Une introduction à l'automatisation JavaScript avec Gulp