Gulp Under the Hood:構建基於流的任務自動化工具

已發表: 2022-03-11

如今,前端開發人員正在使用多種工具來自動化日常操作。 三個最流行的解決方案是 Grunt、Gulp 和 Webpack。 這些工具中的每一個都建立在不同的理念之上,但它們有著相同的共同目標:簡化前端構建過程。 例如,Grunt 是配置驅動的,而 Gulp 幾乎什麼都不強制。 事實上,Gulp 依賴於開發人員編寫代碼來實現構建流程的流程——各種構建任務。

Gulp Under the Hood:構建基於流的任務自動化工具

在選擇其中一種工具時,我個人最喜歡的是 Gulp。 總而言之,這是一個簡單、快速和可靠的解決方案。 在本文中,我們將通過嘗試實現我們自己的類似 Gulp 的工具來了解 Gulp 是如何工作的。

吞嚥API

Gulp 只有四個簡單的功能:

  • 吞嚥任務
  • 吞嚥.src
  • gulp.dest
  • gulp.watch

這四個簡單的功能,在各種組合中提供了 Gulp 的所有功能和靈活性。 在 4.0 版本中,Gulp 引入了兩個新函數:gulp.series 和 gulp.parallel。 這些 API 允許任務以串行或併行方式運行。

在這四個函數中,前三個對於任何 Gulp 文件都是絕對必要的。 允許從命令行界面定義和調用任務。 第四個是讓 Gulp 真正自動化的原因,它允許在文件更改時運行任務。

吞吐文件

這是一個基本的 gulpfile:

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

它描述了一個簡單的測試任務。 調用時,應將當前工作目錄中的文件test.txt複製到目錄./out中。 運行 Gulp 試試看:

 touch test.txt # Create test.txt gulp test

請注意,方法.pipe不是 Gulp 的一部分,它是節點流 API,它將可讀流(由gulp.src('test.txt')生成)與可寫流(由gulp.dest('out') )。 Gulp 和插件之間的所有通信都基於流。 這讓我們能夠以如此優雅的方式編寫 gulpfile 代碼。

滿足插頭

現在我們對 Gulp 的工作原理有了一些了解,讓我們構建自己的類似 Gulp 的工具:Plug。

我們將從 plug.task API 開始。 它應該讓我們註冊任務,如果在命令參數中傳遞任務名稱,則應該執行任務。

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

這將允許註冊任務。 現在我們需要使這個任務可執行。 為簡單起見,我們不會製作單獨的任務啟動器。 相反,我們將把它包含在我們的插件實現中。

我們需要做的就是運行命令行參數中命名的任務。 我們還需要確保在所有任務都註冊後,我們會嘗試在下一個執行循環中執行此操作。 最簡單的方法是在超時回調中運行任務,或者最好是 process.nextTick:

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

像這樣編寫plugfile.js:

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

......並運行它。

 node plugfile.js test

它將顯示:

 hello plug

子任務

Gulp 還允許在任務註冊時定義子任務。 在這種情況下,plug.task 應該有 3 個參數,名稱、子任務數組和回調函數。 讓我們實現它。

我們需要像這樣更新任務 API:

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

現在,如果我們的 plugfile.js 看起來像這樣:

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

… 運行它

node plugfile.js test

… 應該顯示:

 from sub task 1 from sub task 2 hello plug

請注意,Gulp 並行運行子任務。 但為了簡單起見,在我們的實現中,我們按順序運行子任務。 Gulp 4.0 允許使用它的兩個新 API 函數來控制它,我們將在本文後面實現。

來源和目的地

如果我們不允許讀取和寫入文件,則 Plug 幾乎沒有用處。 所以接下來我們將實現plug.src 。 Gulp 中的這個方法需要一個參數,它可以是文件掩碼、文件名或文件掩碼數組。 它返回一個可讀的節點流。

現在,在我們的src實現中,我們只允許文件名:

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

請注意,我們在這裡使用objectMode: true ,這是一個可選參數。 這是因為節點流默認使用二進制流。 如果我們需要通過流傳遞/接收 JavaScript 對象,我們必須使用這個參數。

如您所見,我們創建了一個人造對象:

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

…並將其傳遞到流中。

另一方面,plug.dest 方法應該接收一個目標文件夾名稱並返回一個可寫流,該流將從 .src 流中接收對象。 一旦接收到文件對象,它將被存儲到目標文件夾中。

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

讓我們更新我們的plugfile.js:

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

… 創建test.txt

 touch test.txt

…並運行它:

 node plugfile.js test ls ./out

test.txt應該被複製到./out文件夾。

Gulp 本身的工作方式大致相同,但它不是使用我們的人工文件對象,而是使用乙烯基對象。 它更方便,因為它不僅包含文件名和內容,還包含其他元信息,例如當前文件夾名稱、文件的完整路徑等。 它可能不包含整個內容緩衝區,但它具有內容的可讀流。

乙烯基:比文件更好

有一個出色的庫vinyl-fs,它可以讓我們操作表示為乙烯基對象的文件。 它本質上讓我們基於文件掩碼創建可讀、可寫的流。

我們可以使用vinyl-fs 庫重寫插件函數。 但首先我們需要安裝vinyl-fs:

 npm i vinyl-fs

安裝後,我們新的 Plug 實現將如下所示:

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

…並嘗試一下:

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

結果應該還是一樣的。

Gulp 插件

由於我們的 Plug 服務使用 Gulp 流約定,我們可以將原生 Gulp 插件與我們的 Plug 工具一起使用。

讓我們試一試。 安裝 gulp 重命名:

 npm i gulp-rename

...並更新 plugfile.js 以使用它:

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

你猜對了,現在運行 plugfile.js 應該仍然會產生相同的結果。

 node plugFile.js test ls out/renamed.txt

監控變化

最後但並非最不重要的方法是gulp.watch該方法允許我們註冊文件偵聽器並在文件更改時調用已註冊的任務。 讓我們實現它:

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

要嘗試一下,請將此行添加到 plugfile.js:

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

現在,在每次更改test.txt時,文件將被複製到更改名稱的 out 文件夾中。

串聯與並聯

現在 Gulp API 的所有基本功能都已實現,讓我們更進一步。 即將發布的 Gulp 版本將包含更多 API 函數。 這個新的 API 將使 Gulp 更加強大:

  • 吞嚥並行
  • gulp.series

這些方法允許用戶控制任務運行的順序。 要並行註冊子任務,可以使用 gulp.parallel,這是當前的 Gulp 行為。 另一方面,gulp.series 可用於以順序方式運行子任務,一個接一個。

假設我們在當前文件夾中有test1.txttest2.txt 。 為了將這些文件並行複製到 out 文件夾,讓我們製作一個插件文件:

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

為了簡化實現,使子任務回調函數返回其流。 這將幫助我們跟踪流的生命週期。

我們將開始修改我們的 API:

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

我們還需要更新onTask函數,因為我們需要添加額外的任務元信息來幫助我們的任務啟動器正確處理子任務。

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

為簡單起見,我們將使用 async.js,一個用於處理異步函數以並行或串行運行任務的實用程序庫:

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

我們依賴節點流'end',它在流處理完所有消息並關閉時發出,這表明子任務已完成。 使用 async.js,我們不必處理一大堆回調。

要嘗試一下,讓我們首先並行運行子任務:

 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

並連續運行相同的子任務:

 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

結論

就是這樣,我們已經實現了 Gulp 的 API,現在可以使用 Gulp 插件了。 當然,不要在實際項目中使用 Plug,因為 Gulp 不僅僅是我們在這裡實現的。 我希望這個小練習能幫助你了解 Gulp 的底層工作原理,讓我們更流暢地使用它並使用插件對其進行擴展。

相關:使用 Gulp 進行 JavaScript 自動化簡介