Gulp Under the Hood: Construirea unui instrument de automatizare a sarcinilor bazat pe flux
Publicat: 2022-03-11Dezvoltatorii front-end din zilele noastre folosesc mai multe instrumente pentru a automatiza operațiunile de rutină. Trei dintre cele mai populare soluții sunt Grunt, Gulp și Webpack. Fiecare dintre aceste instrumente este construită pe filozofii diferite, dar împărtășesc același obiectiv comun: eficientizarea procesului de construire front-end. De exemplu, Grunt este bazat pe configurație, în timp ce Gulp nu impune aproape nimic. De fapt, Gulp se bazează pe codul de scriere al dezvoltatorului pentru a implementa fluxul proceselor de construire - diferitele sarcini de construire.
Când vine vorba de alegerea unuia dintre aceste instrumente, preferatul meu personal este Gulp. Per total, este o soluție simplă, rapidă și fiabilă. În acest articol, vom vedea cum funcționează Gulp sub capotă, încercând să implementăm propriul nostru instrument asemănător Gulp.
Gulp API
Gulp vine cu doar patru funcții simple:
- înghiţitură.sarcină
- gulp.src
- înghiţitură.dest
- înghiţitură.ceas
Aceste patru funcții simple, în diverse combinații oferă toată puterea și flexibilitatea Gulp. În versiunea 4.0, Gulp a introdus două funcții noi: gulp.series și gulp.parallel. Aceste API-uri permit rularea sarcinilor în serie sau în paralel.
Dintre aceste patru funcții, primele trei sunt absolut esențiale pentru orice fișier Gulp. permițând definirea și invocarea sarcinilor din interfața liniei de comandă. Al patrulea este ceea ce face ca Gulp să fie cu adevărat automat, permițând rularea sarcinilor atunci când fișierele se schimbă.
Gulpfile
Acesta este un gulpfile elementar:
gulp.task('test', function{ gulp.src('test.txt') .pipe(gulp.dest('out')); });
Descrie o sarcină simplă de testare. Când este invocat, fișierul test.txt din directorul de lucru curent ar trebui copiat în directorul ./out . Încercați rulând Gulp:
touch test.txt # Create test.txt gulp test
Observați că metoda .pipe
nu face parte din Gulp, este API-ul nod-stream, conectează un flux care poate fi citit (generat de gulp.src('test.txt')
) cu un flux care poate fi scris (generat de gulp.dest('out')
). Toată comunicarea dintre Gulp și pluginuri se bazează pe fluxuri. Acest lucru ne permite să scriem codul gulpfile într-un mod atât de elegant.
Faceți cunoștință cu Plug
Acum că avem o idee despre cum funcționează Gulp, să ne construim propriul instrument asemănător Gulp: Plug.
Vom începe cu plug.task API. Ar trebui să ne permită să înregistrăm sarcini, iar sarcinile ar trebui să fie executate dacă numele sarcinii este transmis în parametrii de comandă.
var plug = { task: onTask }; module.exports = plug; var tasks = {}; function onTask(name, callback){ tasks[name] = callback; }
Acest lucru va permite înregistrarea sarcinilor. Acum trebuie să facem această sarcină executabilă. Pentru a menține lucrurile simple, nu vom face un lansator de sarcini separat. În schimb, îl vom include în implementarea plug-ului.
Tot ce trebuie să facem este să rulăm sarcinile numite în parametrii liniei de comandă. De asemenea, trebuie să ne asigurăm că încercăm să o facem în următoarea buclă de execuție, după ce toate sarcinile sunt înregistrate. Cel mai simplu mod de a face acest lucru este să rulați sarcini într-un apel invers de expirare sau, de preferință, process.nextTick:
process.nextTick(function(){ var taskName = process.argv[2]; if (taskName && tasks[taskName]) { tasks[taskName](); } else { console.log('unknown task', taskName) } });
Compuneți plugfile.js astfel:
var plug = require('./plug'); plug.task('test', function(){ console.log('hello plug'); })
… și rulează-l.
node plugfile.js test
Acesta va afișa:
hello plug
Subsarcini
Gulp permite, de asemenea, definirea de subsarcini la înregistrarea sarcinilor. În acest caz, plug.task ar trebui să ia 3 parametri, numele, matricea de sarcini secundare și funcția de apel invers. Să implementăm asta.
Va trebui să actualizăm API-ul sarcinii ca atare:
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); } });
Acum, dacă plugfile.js-ul nostru arată astfel:
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'); })
… rulând-o
node plugfile.js test
… ar trebui să afișeze:
from sub task 1 from sub task 2 hello plug
Rețineți că Gulp rulează subsarcini în paralel. Dar pentru a menține lucrurile simple, în implementarea noastră rulăm subsarcini secvențial. Gulp 4.0 permite ca acest lucru să fie controlat folosind cele două noi funcții API, pe care le vom implementa mai târziu în acest articol.
Sursa si Destinatia
Plug-ul va fi de puțin folos dacă nu permitem citirea și scrierea fișierelor. Deci în continuare vom implementa plug.src
. Această metodă din Gulp așteaptă un argument care este fie o mască de fișier, un nume de fișier sau o matrice de măști de fișier. Returnează un flux Node care poate fi citit.
Pentru moment, în implementarea noastră a src
, vom permite doar nume de fișiere:
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; }
Rețineți că folosim objectMode: true
, un parametru opțional aici. Acest lucru se datorează faptului că fluxurile de noduri funcționează implicit cu fluxuri binare. Dacă trebuie să transmitem/primim obiecte JavaScript prin fluxuri, trebuie să folosim acest parametru.
După cum puteți vedea, am creat un obiect artificial:
{ name: path, //file name buffer: data //file content }
… și l-a trecut în pârâu.
Pe de altă parte, metoda plug.dest ar trebui să primească un nume de folder țintă și să returneze un flux care poate fi scris, care va primi obiecte din fluxul .src. De îndată ce un obiect fișier va fi primit, acesta va fi stocat în folderul țintă.
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; }
Să ne actualizăm plugfile.js:

var plug = require('./plug'); plug.task('test', function(){ plug.src('test.txt') .pipe(plug.dest('out')) })
… creați test.txt
touch test.txt
... și rulați-l:
node plugfile.js test ls ./out
test.txt ar trebui copiat în folderul ./out .
Gulp în sine funcționează aproximativ în același mod, dar în loc de obiectele noastre de fișiere artificiale folosește obiecte de vinil. Este mult mai convenabil, deoarece conține nu doar numele și conținutul fișierului, ci și informații meta suplimentare, cum ar fi numele folderului curent, calea completă către fișier și așa mai departe. Este posibil să nu conțină întregul buffer de conținut, dar are în schimb un flux lizibil al conținutului.
Vinil: Better Than Files
Există o bibliotecă excelentă vinyl-fs care ne permite să manipulăm fișiere reprezentate ca obiecte de vinil. În esență, ne permite să creăm fluxuri care pot fi citite și inscriptibile, bazate pe masca de fișiere.
Putem rescrie funcțiile plug folosind biblioteca vinyl-fs. Dar mai întâi trebuie să instalăm vinyl-fs:
npm i vinyl-fs
Cu aceasta instalată, noua noastră implementare Plug va arăta cam așa:
var vfs = require('vinyl-fs') function onSrc(fileName){ return vfs.src(fileName); } function onDest(path){ return vfs.dest(path); } // ...
... și pentru a-l încerca:
rm out/test.txt node plugFile.js test ls out/test.txt
Rezultatele ar trebui să fie în continuare aceleași.
Pluginuri Gulp
Deoarece serviciul nostru Plug folosește convenția Gulp stream, putem folosi pluginuri Gulp native împreună cu instrumentul nostru Plug.
Să încercăm unul. Instalați gulp-rename:
npm i gulp-rename
… și actualizați plugfile.js pentru a-l folosi:
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')); });
Rularea plugfile.js acum ar trebui, ați ghicit, să producă același rezultat.
node plugFile.js test ls out/renamed.txt
Monitorizarea Schimbărilor
Ultima, dar nu în ultimul rând, este gulp.watch
Această metodă ne permite să înregistrăm ascultătorul de fișiere și să invocăm sarcini înregistrate atunci când fișierele se modifică. Să-l implementăm:
var plug = { task: onTask, src: onSrc, dest: onDest, watch: onWatch }; function onWatch(fileName, taskName){ fs.watchFile(fileName, (event, filename) => { if (filename) { tasks[taskName](); } }); }
Pentru a o încerca, adăugați această linie la plugfile.js:
plug.watch('test.txt','test');
Acum, la fiecare modificare a test.txt , fișierul va fi copiat în folderul out cu numele schimbat.
Seria vs Paralel
Acum că toate funcțiile fundamentale din API-ul Gulp sunt implementate, haideți să facem lucrurile cu un pas mai departe. Versiunea viitoare de Gulp va conține mai multe funcții API. Acest nou API va face Gulp mai puternic:
- înghiţitură.paralel
- înghiţitură.serie
Aceste metode permit utilizatorului să controleze secvența în care sunt executate sarcinile. Pentru a înregistra subsarcini în paralel, se poate folosi gulp.parallel, care este comportamentul curent Gulp. Pe de altă parte, gulp.series poate fi folosit pentru a rula subsarcini într-o manieră secvențială, una după alta.
Să presupunem că avem test1.txt și test2.txt în folderul curent. Pentru a copia acele fișiere în folderul out în paralel, să facem un fișier de conectare:
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') })
Pentru a simplifica implementarea, funcțiile de apel invers pentru subsarcina sunt făcute pentru a returna fluxul său. Acest lucru ne va ajuta să urmărim ciclul de viață al fluxului.
Vom începe să ne modificăm API-ul:
var plug = { task: onTask, src: onSrc, dest: onDest, parallel: onParallel, series: onSeries };
Va trebui să actualizăm și funcția onTask , deoarece trebuie să adăugăm metainformații suplimentare despre sarcină pentru a ajuta lansatorul nostru de sarcini să se ocupe corect de subsarcinile.
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 }; }
Pentru a menține lucrurile simple, vom folosi async.js, o bibliotecă de utilitate pentru a gestiona funcțiile asincrone pentru a rula sarcini în paralel sau în 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(); } } }
Ne bazăm pe „sfârșitul” fluxului de nod, care este emis atunci când un flux a procesat toate mesajele și este închis, ceea ce este un indiciu că subsarcina este finalizată. Cu async.js, nu trebuie să ne confruntăm cu o mare mizerie de apeluri inverse.
Pentru a-l încerca, mai întâi să rulăm subsarcinile în paralel:
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
Și rulați aceleași subsarcini în 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
Concluzie
Gata, am implementat API-ul Gulp și putem folosi pluginurile Gulp acum. Desigur, nu utilizați Plug în proiecte reale, deoarece Gulp este mai mult decât ceea ce am implementat aici. Sper că acest mic exercițiu vă va ajuta să înțelegeți cum funcționează Gulp sub capotă și ne va permite să-l folosim mai fluent și să-l extindem cu pluginuri.