Łyk pod maską: tworzenie narzędzia do automatyzacji zadań opartego na strumieniu
Opublikowany: 2022-03-11Obecnie programiści front-end używają wielu narzędzi do automatyzacji rutynowych operacji. Trzy najpopularniejsze rozwiązania to Grunt, Gulp i Webpack. Każde z tych narzędzi opiera się na różnych filozofiach, ale mają ten sam wspólny cel: usprawnienie procesu kompilacji front-endu. Na przykład Grunt jest oparty na konfiguracji, podczas gdy Gulp prawie nic nie wymusza. W rzeczywistości Gulp polega na pisaniu kodu przez programistę w celu zaimplementowania przepływu procesów kompilacji - różnych zadań kompilacji.
Jeśli chodzi o wybór jednego z tych narzędzi, moim osobistym faworytem jest Gulp. W sumie jest to proste, szybkie i niezawodne rozwiązanie. W tym artykule zobaczymy, jak Gulp działa pod maską, próbując wdrożyć nasze własne narzędzie podobne do Gulpa.
Interfejs API łykania
Gulp ma tylko cztery proste funkcje:
- gulp.zadanie
- gulp.src
- gulp.dest
- łyk.oglądaj
Te cztery proste funkcje, w różnych kombinacjach, oferują całą moc i elastyczność Gulp. W wersji 4.0 Gulp wprowadził dwie nowe funkcje: gulp.series i gulp.parallel. Te interfejsy API umożliwiają uruchamianie zadań szeregowo lub równolegle.
Z tych czterech funkcji, pierwsze trzy są absolutnie niezbędne dla każdego pliku Gulp. pozwalając na definiowanie i wywoływanie zadań z interfejsu wiersza poleceń. Czwarty jest tym, co sprawia, że Gulp jest naprawdę automatyczny, umożliwiając uruchamianie zadań po zmianie plików.
Przekąski
To jest podstawowy plik przełyku:
gulp.task('test', function{ gulp.src('test.txt') .pipe(gulp.dest('out')); });
Opisuje proste zadanie testowe. Po wywołaniu plik test.txt z bieżącego katalogu roboczego należy skopiować do katalogu ./out . Spróbuj, uruchamiając Gulp:
touch test.txt # Create test.txt gulp test
Zauważ, że metoda .pipe
nie jest częścią Gulp, jest to API strumienia węzłów, łączy czytelny strumień (generowany przez gulp.src('test.txt')
) z zapisywalnym strumieniem (generowanym przez gulp.dest('out')
). Cała komunikacja między Gulp a wtyczkami opiera się na strumieniach. To pozwala nam pisać kod gulpfile w tak elegancki sposób.
Poznaj wtyczkę
Teraz, gdy mamy już pewne pojęcie o tym, jak działa Gulp, zbudujmy nasze własne narzędzie podobne do Gulpa: Wtyczka.
Zaczniemy od plug.task API. Powinno to pozwolić nam rejestrować zadania, a zadania powinny być wykonywane, jeśli nazwa zadania jest przekazana w parametrach polecenia.
var plug = { task: onTask }; module.exports = plug; var tasks = {}; function onTask(name, callback){ tasks[name] = callback; }
Umożliwi to rejestrację zadań. Teraz musimy uczynić to zadanie wykonalnym. Dla uproszczenia nie stworzymy osobnego programu uruchamiającego zadania. Zamiast tego uwzględnimy go w naszej implementacji wtyczki.
Wszystko, co musimy zrobić, to uruchomić zadania wymienione w parametrach wiersza poleceń. Musimy również upewnić się, że spróbujemy to zrobić w następnej pętli wykonania, po zarejestrowaniu wszystkich zadań. Najprostszym sposobem na to jest uruchamianie zadań w funkcji timeout callback, a najlepiej process.nextTick:
process.nextTick(function(){ var taskName = process.argv[2]; if (taskName && tasks[taskName]) { tasks[taskName](); } else { console.log('unknown task', taskName) } });
Skomponuj plik plugfile.js w ten sposób:
var plug = require('./plug'); plug.task('test', function(){ console.log('hello plug'); })
… i uruchom go.
node plugfile.js test
Wyświetli się:
hello plug
Podzadania
Gulp umożliwia również definiowanie podzadań podczas rejestracji zadań. W tym przypadku plugin.task powinien przyjąć 3 parametry, nazwę, tablicę podzadań i funkcję zwrotną. Zaimplementujmy to.
Będziemy musieli zaktualizować interfejs API zadań w następujący sposób:
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); } });
Teraz, jeśli nasz plik plugfile.js wygląda tak:
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'); })
… uruchamiam to
node plugfile.js test
… powinien zawierać:
from sub task 1 from sub task 2 hello plug
Zauważ, że Gulp uruchamia podzadania równolegle. Ale żeby wszystko było proste, w naszej implementacji wykonujemy podzadania sekwencyjnie. Gulp 4.0 pozwala na kontrolowanie tego za pomocą dwóch nowych funkcji API, które zaimplementujemy w dalszej części tego artykułu.
Źródło i miejsce docelowe
Wtyczka na niewiele się zda, jeśli nie pozwolimy na odczytywanie i zapisywanie plików. Więc w następnej kolejności zaimplementujemy plug.src
. Ta metoda w Gulp oczekuje argumentu, który jest maską pliku, nazwą pliku lub tablicą masek plików. Zwraca czytelny strumień węzła.
Na razie w naszej implementacji src
tylko na nazwy plików:
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; }
Zauważ, że używamy tutaj opcjonalnego parametru objectMode: true
. Dzieje się tak, ponieważ strumienie węzłów domyślnie współpracują ze strumieniami binarnymi. Jeśli musimy przekazywać/odbierać obiekty JavaScript za pośrednictwem strumieni, musimy użyć tego parametru.
Jak widać, stworzyliśmy sztuczny obiekt:
{ name: path, //file name buffer: data //file content }
… i przekazaliśmy go do strumienia.
Z drugiej strony metoda plug.dest powinna otrzymać nazwę folderu docelowego i zwrócić zapisywalny strumień, który otrzyma obiekty ze strumienia .src. Gdy tylko obiekt pliku zostanie odebrany, zostanie on zapisany w folderze docelowym.
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; }
Zaktualizujmy nasz plik plugfile.js:

var plug = require('./plug'); plug.task('test', function(){ plug.src('test.txt') .pipe(plug.dest('out')) })
… utwórz test.txt
touch test.txt
… i uruchom go:
node plugfile.js test ls ./out
test.txt należy skopiować do folderu ./out .
Sam Gulp działa mniej więcej w ten sam sposób, ale zamiast naszych sztucznych obiektów plików używa obiektów winylowych. Jest to o wiele wygodniejsze, ponieważ zawiera nie tylko nazwę pliku i zawartość, ale także dodatkowe informacje meta, takie jak nazwa bieżącego folderu, pełna ścieżka do pliku i tak dalej. Może nie zawierać całego bufora zawartości, ale zamiast tego ma czytelny strumień zawartości.
Winyl: lepszy niż pliki
Istnieje doskonała biblioteka vinyl-fs, która pozwala nam manipulować plikami reprezentowanymi jako obiekty winylowe. Zasadniczo pozwala nam tworzyć czytelne, zapisywalne strumienie w oparciu o maskę pliku.
Możemy przepisać funkcje wtyczek za pomocą biblioteki vinyl-fs. Ale najpierw musimy zainstalować vinyl-fs:
npm i vinyl-fs
Po zainstalowaniu tego, nasza nowa implementacja wtyczki będzie wyglądać mniej więcej tak:
var vfs = require('vinyl-fs') function onSrc(fileName){ return vfs.src(fileName); } function onDest(path){ return vfs.dest(path); } // ...
… i wypróbować:
rm out/test.txt node plugFile.js test ls out/test.txt
Wyniki powinny być nadal takie same.
Wtyczki Gulpa
Ponieważ nasza usługa Plug wykorzystuje konwencję strumienia Gulp, możemy używać natywnych wtyczek Gulp razem z naszym narzędziem Plug.
Wypróbujmy jeden. Zainstaluj gulp-zmień nazwę:
npm i gulp-rename
… i zaktualizuj plik plugfile.js, aby z niego korzystać:
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')); });
Uruchomienie pliku plugfile.js powinno nadal, zgadłeś, dać ten sam wynik.
node plugFile.js test ls out/renamed.txt
Monitorowanie zmian
Ostatnią, ale nie mniej ważną metodą jest gulp.watch
Ta metoda pozwala nam zarejestrować nasłuchiwanie plików i wywoływać zarejestrowane zadania w przypadku zmiany plików. Zaimplementujmy to:
var plug = { task: onTask, src: onSrc, dest: onDest, watch: onWatch }; function onWatch(fileName, taskName){ fs.watchFile(fileName, (event, filename) => { if (filename) { tasks[taskName](); } }); }
Aby to wypróbować, dodaj ten wiersz do plugfile.js:
plug.watch('test.txt','test');
Teraz przy każdej zmianie pliku test.txt plik zostanie skopiowany do folderu out ze zmienioną nazwą.
Seria kontra równoległa
Teraz, gdy wszystkie podstawowe funkcje z API Gulpa zostały zaimplementowane, pójdźmy o krok dalej. Nadchodząca wersja Gulpa będzie zawierać więcej funkcji API. Ten nowy interfejs API sprawi, że Gulp będzie potężniejszy:
- gulp.równoległy
- łyk.seria
Te metody pozwalają użytkownikowi kontrolować kolejność wykonywania zadań. Do równoległej rejestracji podzadań można użyć gulp.parallel, co jest aktualnym zachowaniem Gulpa. Z drugiej strony, gulp.series może być używany do uruchamiania podzadań w sposób sekwencyjny, jedno po drugim.
Załóżmy, że w bieżącym folderze mamy test1.txt i test2.txt . W celu równoległego skopiowania tych plików do folderu out stwórzmy plik wtyczki:
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') })
Aby uprościć implementację, funkcje wywołania zwrotnego podzadań zwracają jego strumień. Pomoże nam to śledzić cykl życia strumienia.
Zaczniemy zmieniać nasze API:
var plug = { task: onTask, src: onSrc, dest: onDest, parallel: onParallel, series: onSeries };
Będziemy musieli również zaktualizować funkcję onTask , ponieważ musimy dodać dodatkowe metainformacje o zadaniu, aby pomóc naszemu programowi uruchamiającemu zadania poprawnie radzić sobie z podzadaniami.
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 }; }
Aby uprościć sprawę, użyjemy async.js, biblioteki narzędziowej do obsługi funkcji asynchronicznych do uruchamiania zadań równolegle lub szeregowo:
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(); } } }
Opieramy się na strumieniu węzłów „end”, który jest emitowany, gdy strumień przetworzył wszystkie wiadomości i zostanie zamknięty, co oznacza, że podzadanie zostało zakończone. Dzięki async.js nie mamy do czynienia z dużym bałaganem wywołań zwrotnych.
Aby to wypróbować, najpierw uruchommy podzadania równolegle:
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 uruchamiaj te same podzadania w serii:
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
Wniosek
To wszystko, zaimplementowaliśmy API Gulpa i możemy teraz korzystać z wtyczek Gulpa. Oczywiście nie używaj Plug w prawdziwych projektach, ponieważ Gulp to coś więcej niż tylko to, co tutaj zaimplementowaliśmy. Mam nadzieję, że to małe ćwiczenie pomoże Ci zrozumieć, jak Gulp działa pod maską i pozwoli nam płynniej go używać i rozszerzać o wtyczki.