Gulp Under the Hood: creazione di uno strumento di automazione delle attività basato sul flusso

Pubblicato: 2022-03-11

Gli sviluppatori front-end al giorno d'oggi utilizzano più strumenti per automatizzare le operazioni di routine. Tre delle soluzioni più popolari sono Grunt, Gulp e Webpack. Ognuno di questi strumenti si basa su filosofie diverse, ma condividono lo stesso obiettivo comune: semplificare il processo di creazione front-end. Ad esempio, Grunt è basato sulla configurazione mentre Gulp non applica quasi nulla. In effetti, Gulp si affida allo sviluppatore che scrive il codice per implementare il flusso dei processi di compilazione, le varie attività di compilazione.

Gulp Under the Hood: creazione di uno strumento di automazione delle attività basato sul flusso

Quando si tratta di scegliere uno di questi strumenti, il mio preferito è Gulp. Tutto sommato è una soluzione semplice, veloce e affidabile. In questo articolo vedremo come funziona Gulp sotto il cofano provando a implementare il nostro strumento simile a Gulp.

Gulp API

Gulp viene fornito con solo quattro semplici funzioni:

  • gulp.task
  • gulp.src
  • gulp.dest
  • gulp.watch

Queste quattro semplici funzioni, in varie combinazioni, offrono tutta la potenza e la flessibilità di Gulp. Nella versione 4.0, Gulp ha introdotto due nuove funzioni: gulp.series e gulp.parallel. Queste API consentono di eseguire attività in serie o in parallelo.

Di queste quattro funzioni, le prime tre sono assolutamente essenziali per qualsiasi file Gulp. consentendo di definire e richiamare le attività dall'interfaccia della riga di comando. Il quarto è ciò che rende Gulp veramente automatico consentendo l'esecuzione di attività quando i file cambiano.

Gulpfile

Questo è un gulpfile elementare:

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

Descrive un semplice compito di prova. Quando viene richiamato, il file test.txt nella directory di lavoro corrente deve essere copiato nella directory ./out . Provalo eseguendo Gulp:

 touch test.txt # Create test.txt gulp test

Si noti che il metodo .pipe non fa parte di Gulp, è un'API del flusso di nodi, collega un flusso leggibile (generato da gulp.src('test.txt') ) con un flusso scrivibile (generato da gulp.dest('out') ). Tutte le comunicazioni tra Gulp e i plugin si basano su flussi. Questo ci permette di scrivere il codice gulpfile in un modo così elegante.

Incontra Spina

Ora che abbiamo un'idea di come funziona Gulp, costruiamo il nostro strumento simile a Gulp: Plug.

Inizieremo con l'API plug.task. Dovrebbe permetterci di registrare le attività e le attività dovrebbero essere eseguite se il nome dell'attività viene passato nei parametri di comando.

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

Ciò consentirà di registrare le attività. Ora dobbiamo rendere questo compito eseguibile. Per semplificare le cose, non creeremo un lanciatore di attività separato. Invece lo includeremo nella nostra implementazione plug.

Tutto ciò che dobbiamo fare è eseguire le attività indicate nei parametri della riga di comando. Dobbiamo anche assicurarci di provare a farlo nel ciclo di esecuzione successivo, dopo che tutte le attività sono state registrate. Il modo più semplice per farlo è eseguire attività in un callback di timeout, o preferibilmente process.nextTick:

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

Componi plugfile.js in questo modo:

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

... ed eseguilo.

 node plugfile.js test

Verrà visualizzato:

 hello plug

Sottocompiti

Gulp consente anche di definire sottoattività alla registrazione delle attività. In questo caso, plug.task dovrebbe accettare 3 parametri, il nome, l'array di attività secondarie e la funzione di callback. Mettiamo in pratica questo.

Dovremo aggiornare l'API dell'attività in quanto tale:

 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); } });

Ora se il nostro plugfile.js ha questo aspetto:

 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'); })

... eseguendo

 node plugfile.js test

… dovrebbe visualizzare:

 from sub task 1 from sub task 2 hello plug

Si noti che Gulp esegue le attività secondarie in parallelo. Ma per semplificare le cose, nella nostra implementazione eseguiamo le attività secondarie in sequenza. Gulp 4.0 consente di controllarlo utilizzando le sue due nuove funzioni API, che implementeremo più avanti in questo articolo.

Sorgente e Destinazione

Plug sarà di scarsa utilità se non consentiamo la lettura e la scrittura di file. Quindi implementeremo plug.src . Questo metodo in Gulp prevede un argomento che sia una maschera di file, un nome di file o un array di maschere di file. Restituisce un flusso di nodi leggibile.

Per ora, nella nostra implementazione di src , consentiremo solo nomi di file:

 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; }

Nota che usiamo objectMode: true , un parametro opzionale qui. Questo perché i flussi di nodi funzionano con flussi binari per impostazione predefinita. Se abbiamo bisogno di passare/ricevere oggetti JavaScript tramite stream, dobbiamo usare questo parametro.

Come puoi vedere, abbiamo creato un oggetto artificiale:

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

... e l'ha passato nel ruscello.

Dall'altra parte, il metodo plug.dest dovrebbe ricevere un nome di cartella di destinazione e restituire un flusso scrivibile che riceverà oggetti dal flusso .src. Non appena un oggetto file verrà ricevuto, verrà archiviato nella cartella di destinazione.

 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; }

Aggiorniamo il nostro plugfile.js:

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

… crea test.txt

 touch test.txt

... ed eseguilo:

 node plugfile.js test ls ./out

test.txt deve essere copiato nella cartella ./out .

Gulp stesso funziona più o meno allo stesso modo, ma invece dei nostri oggetti file artificiali usa oggetti in vinile. È molto più conveniente, poiché contiene non solo il nome del file e il contenuto, ma anche meta informazioni aggiuntive, come il nome della cartella corrente, il percorso completo del file e così via. Potrebbe non contenere l'intero buffer del contenuto, ma ha invece un flusso leggibile del contenuto.

Vinile: meglio dei file

Esiste un'eccellente libreria vinyl-fs che ci consente di manipolare i file rappresentati come oggetti in vinile. In sostanza ci consente di creare flussi leggibili e scrivibili basati sulla maschera del file.

Possiamo riscrivere le funzioni plug usando la libreria vinyl-fs. Ma prima dobbiamo installare vinyl-fs:

 npm i vinyl-fs

Con questo installato, la nostra nuova implementazione Plug sarà simile a questa:

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

… e per provarlo:

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

I risultati dovrebbero essere sempre gli stessi.

Plugin Gulp

Poiché il nostro servizio Plug utilizza la convenzione di flusso Gulp, possiamo utilizzare plug-in Gulp nativi insieme al nostro strumento Plug.

Proviamone uno. Installa Gulp-Rename:

 npm i gulp-rename

… e aggiorna plugfile.js per usarlo:

 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'esecuzione di plugfile.js ora dovrebbe comunque, hai indovinato, produrre lo stesso risultato.

 node plugFile.js test ls out/renamed.txt

Monitoraggio delle modifiche

L'ultimo ma non meno importante metodo è gulp.watch Questo metodo ci consente di registrare il listener di file e richiamare attività registrate quando i file cambiano. Mettiamolo in pratica:

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

Per provarlo, aggiungi questa riga a plugfile.js:

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

Ora ad ogni modifica di test.txt , il file verrà copiato nella cartella out con il nome cambiato.

Serie vs Parallela

Ora che tutte le funzioni fondamentali dell'API di Gulp sono state implementate, facciamo un ulteriore passo avanti. La prossima versione di Gulp conterrà più funzioni API. Questa nuova API renderà Gulp più potente:

  • gulp.parallelo
  • gulp.series

Questi metodi consentono all'utente di controllare la sequenza in cui vengono eseguite le attività. Per registrare le attività secondarie in parallelo, è possibile utilizzare gulp.parallel, che è il comportamento di Gulp corrente. D'altra parte, gulp.series può essere utilizzato per eseguire attività secondarie in modo sequenziale, una dopo l'altra.

Supponiamo di avere test1.txt e test2.txt nella cartella corrente. Per copiare quei file nella cartella out in parallelo, creiamo 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') })

Per semplificare l'implementazione, le funzioni di callback della sottoattività vengono fatte per restituire il suo flusso. Questo ci aiuterà a monitorare il ciclo di vita del flusso.

Inizieremo a modificare la nostra API:

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

Avremo bisogno di aggiornare anche la funzione onTask , dal momento che dobbiamo aggiungere ulteriori informazioni sulle meta attività per aiutare il nostro programma di avvio delle attività a gestire correttamente le sottoattività.

 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 }; }

Per semplificare le cose, utilizzeremo async.js, una libreria di utilità per gestire funzioni asincrone per eseguire attività in parallelo o in serie:

 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(); } } }

Facciamo affidamento sul flusso del nodo 'end' che viene emesso quando un flusso ha elaborato tutti i messaggi e viene chiuso, il che indica che l'attività secondaria è completa. Con async.js, non dobbiamo affrontare un gran pasticcio di callback.

Per provarlo, eseguiamo prima le attività secondarie in parallelo:

 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

Ed eseguire le stesse attività secondarie in serie:

 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

Conclusione

Ecco fatto, abbiamo implementato l'API di Gulp e ora possiamo utilizzare i plugin di Gulp. Naturalmente, non utilizzare Plug in progetti reali, poiché Gulp è più di ciò che abbiamo implementato qui. Spero che questo piccolo esercizio ti aiuti a capire come funziona Gulp sotto il cofano e permetterci di usarlo in modo più fluido ed estenderlo con i plugin.

Correlati: Introduzione all'automazione JavaScript con Gulp