Łyk pod maską: tworzenie narzędzia do automatyzacji zadań opartego na strumieniu

Opublikowany: 2022-03-11

Obecnie 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.

Łyk pod maską: tworzenie narzędzia do automatyzacji zadań opartego na strumieniu

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.

Powiązane: Wprowadzenie do automatyzacji JavaScript za pomocą Gulp