Gulp Under the Hood : création d'un outil d'automatisation des tâches basé sur les flux
Publié: 2022-03-11De 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.
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.