Gulp Under the Hood: Erstellen eines Stream-basierten Tools zur Aufgabenautomatisierung
Veröffentlicht: 2022-03-11Front-End-Entwickler verwenden heutzutage mehrere Tools, um Routinevorgänge zu automatisieren. Drei der beliebtesten Lösungen sind Grunt, Gulp und Webpack. Jedes dieser Tools basiert auf unterschiedlichen Philosophien, aber sie haben dasselbe gemeinsame Ziel: den Front-End-Build-Prozess zu rationalisieren. Zum Beispiel ist Grunt konfigurationsgesteuert, während Gulp fast nichts erzwingt. Tatsächlich verlässt sich Gulp darauf, dass der Entwickler Code schreibt, um den Ablauf der Build-Prozesse – die verschiedenen Build-Tasks – zu implementieren.
Wenn es um die Wahl eines dieser Tools geht, ist Gulp mein persönlicher Favorit. Alles in allem eine einfache, schnelle und zuverlässige Lösung. In diesem Artikel werden wir sehen, wie Gulp unter der Haube funktioniert, indem wir versuchen, unser eigenes Gulp-ähnliches Tool zu implementieren.
Gulp-API
Gulp kommt mit nur vier einfachen Funktionen:
- schluck.task
- Schluck.src
- Schluck.dest
- schluck.watch
Diese vier einfachen Funktionen bieten in verschiedenen Kombinationen die gesamte Leistung und Flexibilität von Gulp. In Version 4.0 hat Gulp zwei neue Funktionen eingeführt: gulp.series und gulp.parallel. Diese APIs ermöglichen es, Aufgaben nacheinander oder parallel auszuführen.
Von diesen vier Funktionen sind die ersten drei für jede Gulp-Datei absolut unerlässlich. Aufgaben können über die Befehlszeilenschnittstelle definiert und aufgerufen werden. Der vierte macht Gulp wirklich automatisch, indem Aufgaben ausgeführt werden können, wenn sich Dateien ändern.
Gulpfile
Dies ist ein elementares Gulpfile:
gulp.task('test', function{ gulp.src('test.txt') .pipe(gulp.dest('out')); });
Es beschreibt eine einfache Testaufgabe. Beim Aufruf sollte die Datei test.txt im aktuellen Arbeitsverzeichnis in das Verzeichnis ./out kopiert werden. Probieren Sie es aus, indem Sie Gulp ausführen:
touch test.txt # Create test.txt gulp test
Beachten Sie, dass die Methode .pipe
kein Teil von Gulp ist, sondern eine Node-Stream-API, die einen lesbaren Stream (erzeugt von gulp.src('test.txt')
) mit einem beschreibbaren Stream (erzeugt von gulp.dest('out')
). Die gesamte Kommunikation zwischen Gulp und Plugins basiert auf Streams. Dadurch können wir Gulpfile-Code auf so elegante Weise schreiben.
Lernen Sie Stecker kennen
Nachdem wir nun eine Vorstellung davon haben, wie Gulp funktioniert, bauen wir unser eigenes Gulp-ähnliches Tool: Plug.
Wir beginnen mit der Plug.task-API. Es sollte uns erlauben, Aufgaben zu registrieren, und Aufgaben sollten ausgeführt werden, wenn der Aufgabenname in Befehlsparametern übergeben wird.
var plug = { task: onTask }; module.exports = plug; var tasks = {}; function onTask(name, callback){ tasks[name] = callback; }
Dadurch können Aufgaben registriert werden. Nun müssen wir diese Aufgabe ausführbar machen. Um die Dinge einfach zu halten, werden wir keinen separaten Aufgabenstarter erstellen. Stattdessen werden wir es in unsere Plug-Implementierung aufnehmen.
Alles, was wir tun müssen, ist, die in den Befehlszeilenparametern genannten Aufgaben auszuführen. Wir müssen auch sicherstellen, dass wir versuchen, dies in der nächsten Ausführungsschleife zu tun, nachdem alle Aufgaben registriert wurden. Der einfachste Weg, dies zu tun, besteht darin, Aufgaben in einem Timeout-Callback oder vorzugsweise in process.nextTick auszuführen:
process.nextTick(function(){ var taskName = process.argv[2]; if (taskName && tasks[taskName]) { tasks[taskName](); } else { console.log('unknown task', taskName) } });
Erstellen Sie plugfile.js wie folgt:
var plug = require('./plug'); plug.task('test', function(){ console.log('hello plug'); })
… und führen Sie es aus.
node plugfile.js test
Es wird angezeigt:
hello plug
Unteraufgaben
Gulp ermöglicht auch die Definition von Unteraufgaben bei der Aufgabenregistrierung. In diesem Fall sollte plug.task 3 Parameter annehmen, den Namen, ein Array von Unteraufgaben und eine Rückruffunktion. Lassen Sie uns das umsetzen.
Wir müssen die Aufgaben-API als solche aktualisieren:
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); } });
Wenn unsere plugfile.js nun so aussieht:
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'); })
… läuft es
node plugfile.js test
… sollte anzeigen:
from sub task 1 from sub task 2 hello plug
Beachten Sie, dass Gulp Teilaufgaben parallel ausführt. Aber um die Dinge einfach zu halten, führen wir in unserer Implementierung Unteraufgaben nacheinander aus. Gulp 4.0 ermöglicht die Steuerung mithilfe seiner zwei neuen API-Funktionen, die wir später in diesem Artikel implementieren werden.
Quelle und Ziel
Plug ist von geringem Nutzen, wenn wir das Lesen und Schreiben von Dateien nicht zulassen. Als nächstes werden wir plug.src
implementieren. Diese Methode in Gulp erwartet ein Argument, das entweder eine Dateimaske, ein Dateiname oder ein Array von Dateimasken ist. Es gibt einen lesbaren Node-Stream zurück.
Im Moment erlauben wir in unserer Implementierung von src
nur Dateinamen:
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; }
Beachten Sie, dass wir hier objectMode: true
verwenden, einen optionalen Parameter. Dies liegt daran, dass Knotenstreams standardmäßig mit binären Streams arbeiten. Wenn wir JavaScript-Objekte über Streams übergeben/empfangen müssen, müssen wir diesen Parameter verwenden.
Wie Sie sehen können, haben wir ein künstliches Objekt erstellt:
{ name: path, //file name buffer: data //file content }
… und in den Bach geleitet.
Auf der anderen Seite sollte die Methode plug.dest einen Zielordnernamen erhalten und einen beschreibbaren Stream zurückgeben, der Objekte aus dem .src-Stream empfängt. Sobald ein Dateiobjekt empfangen wird, wird es im Zielordner gespeichert.
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; }
Lassen Sie uns unsere plugfile.js aktualisieren:

var plug = require('./plug'); plug.task('test', function(){ plug.src('test.txt') .pipe(plug.dest('out')) })
… test.txt erstellen
touch test.txt
… und führen Sie es aus:
node plugfile.js test ls ./out
test.txt sollte in den Ordner ./out kopiert werden.
Gulp selbst funktioniert ungefähr auf die gleiche Weise, aber anstelle unserer künstlichen Dateiobjekte verwendet es Vinylobjekte. Es ist viel bequemer, da es nicht nur den Dateinamen und den Inhalt enthält, sondern auch zusätzliche Metainformationen, wie z. B. den aktuellen Ordnernamen, den vollständigen Pfad zur Datei und so weiter. Es enthält möglicherweise nicht den gesamten Inhaltspuffer, aber stattdessen einen lesbaren Stream des Inhalts.
Vinyl: Besser als Dateien
Es gibt eine ausgezeichnete Bibliothek vinyl-fs, mit der wir Dateien manipulieren können, die als Vinylobjekte dargestellt werden. Im Wesentlichen können wir damit lesbare, beschreibbare Streams basierend auf der Dateimaske erstellen.
Wir können Plug-Funktionen mit der Vinyl-fs-Bibliothek umschreiben. Aber zuerst müssen wir vinyl-fs installieren:
npm i vinyl-fs
Wenn dies installiert ist, sieht unsere neue Plug-Implementierung in etwa so aus:
var vfs = require('vinyl-fs') function onSrc(fileName){ return vfs.src(fileName); } function onDest(path){ return vfs.dest(path); } // ...
… und zum Ausprobieren:
rm out/test.txt node plugFile.js test ls out/test.txt
Die Ergebnisse sollten immer noch die gleichen sein.
Gulp-Plugins
Da unser Plug-Service die Gulp-Stream-Konvention verwendet, können wir native Gulp-Plugins zusammen mit unserem Plug-Tool verwenden.
Probieren wir eine aus. gulp-rename installieren:
npm i gulp-rename
… und aktualisiere plugfile.js, um es zu verwenden:
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')); });
Das Ausführen von plugfile.js sollte jetzt immer noch, Sie haben es erraten, das gleiche Ergebnis liefern.
node plugFile.js test ls out/renamed.txt
Überwachung von Änderungen
Die letzte Methode ist gulp.watch
Diese Methode ermöglicht es uns, Datei-Listener zu registrieren und registrierte Aufgaben aufzurufen, wenn sich Dateien ändern. Lass es uns implementieren:
var plug = { task: onTask, src: onSrc, dest: onDest, watch: onWatch }; function onWatch(fileName, taskName){ fs.watchFile(fileName, (event, filename) => { if (filename) { tasks[taskName](); } }); }
Um es auszuprobieren, fügen Sie diese Zeile zu plugfile.js hinzu:
plug.watch('test.txt','test');
Jetzt wird bei jeder Änderung von test.txt die Datei mit geändertem Namen in den Ausgangsordner kopiert.
Serie gegen Parallel
Nachdem nun alle grundlegenden Funktionen der API von Gulp implementiert sind, gehen wir noch einen Schritt weiter. Die kommende Version von Gulp wird weitere API-Funktionen enthalten. Diese neue API wird Gulp leistungsfähiger machen:
- schluck.parallel
- Schluck.serie
Mit diesen Methoden kann der Benutzer die Reihenfolge steuern, in der Tasks ausgeführt werden. Um Unteraufgaben parallel zu registrieren, kann gulp.parallel verwendet werden, was das aktuelle Gulp-Verhalten ist. Andererseits kann gulp.series verwendet werden, um Teilaufgaben sequentiell nacheinander auszuführen.
Angenommen, wir haben test1.txt und test2.txt im aktuellen Ordner. Um diese Dateien parallel in den Ausgangsordner zu kopieren, erstellen wir eine Plug-Datei:
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') })
Um die Implementierung zu vereinfachen, werden die Teilaufgaben-Callback-Funktionen dazu gebracht, ihren Stream zurückzugeben. Dies hilft uns, den Lebenszyklus des Streams zu verfolgen.
Wir werden mit der Änderung unserer API beginnen:
var plug = { task: onTask, src: onSrc, dest: onDest, parallel: onParallel, series: onSeries };
Wir müssen auch die onTask- Funktion aktualisieren, da wir zusätzliche Task-Metainformationen hinzufügen müssen, damit unser Task-Launcher Unteraufgaben richtig verarbeiten kann.
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 }; }
Um die Dinge einfach zu halten, verwenden wir async.js, eine Hilfsbibliothek für den Umgang mit asynchronen Funktionen, um Aufgaben parallel oder in Reihe auszuführen:
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(); } } }
Wir verlassen uns auf das Ende des Knotenstroms, der ausgegeben wird, wenn ein Strom alle Nachrichten verarbeitet hat und geschlossen wird, was ein Hinweis darauf ist, dass die Teilaufgabe abgeschlossen ist. Mit async.js müssen wir uns nicht mit einem großen Durcheinander von Rückrufen auseinandersetzen.
Um es auszuprobieren, lassen Sie uns zunächst die Teilaufgaben parallel ausführen:
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
Und führen Sie die gleichen Teilaufgaben nacheinander aus:
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
Fazit
Das war's, wir haben die API von Gulp implementiert und können jetzt Gulp-Plugins verwenden. Verwenden Sie Plug natürlich nicht in echten Projekten, da Gulp mehr ist als nur das, was wir hier implementiert haben. Ich hoffe, diese kleine Übung hilft Ihnen zu verstehen, wie Gulp unter der Haube funktioniert, und lässt uns es flüssiger verwenden und mit Plugins erweitern.