[譯]幕後的gulp:構建一個基於流的任務自動化工具

幕後的gulp:構建一個基於流的任務自動化工具

/**
 * 謹獻給Yoyo
 *
 * 原文出處:https://www.toptal.com/nodejs/gulp-under-the-hood
 * @author dogstar.huang <chanzonghuang@gmail.com> 2016-05-14
 */

前端開發人員如今正在使用多種工具把平常操做自動化。三個最流行的解決方案是Grunt,Gulp和Webpack。每一個工具都創建在不一樣的理念,可是它們共享同一個目標:精簡前端構建過程。例如,Grunt是配置驅動的,而Gulp幾乎不須要配置便可執行。事實上,Gulp依賴於開發人員編寫代碼來實現構建流程 - 各類構建任務。
前端

當談到選擇這些工具之一時,我我的最喜歡的是Gulp。總而言之,這是一個簡單,快速和可靠的解決方案。在這篇文章中,咱們將看到幕後的Gulp是如何工做,經過花點心思在實現咱們本身的像Gulp這樣的工具。node

Gulp API 接口

Gulp自帶的只有四個簡單的功能git

  • gulp.task
  • gulp.src
  • gulp.dest
  • gulp.watch

這四個簡單的功能,經過各類組合提供了Glup所有的強悍和靈活性。在4.0版本中,Gulp引入了兩個新的功能:gulp.series和gulp.parallel。這些API容許任務在串行或並行運行。github

這四個函數,前三個是任何Gulp文件絕對必要的。容許任務能夠被定義以及能在命令行界面調用。第四個是經過容許在文件改變時運行任務使得Gulp真正實現自動化。npm

Gulpfile

這是一個基本的gulpfile:gulp

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的一部分,而是一個node-stream API,它鏈接可讀流(由gulp.src('test.txt')生成)與可寫流(由gulp.dest('out')生成)。Gulp和插件之間的全部通訊都基於流。這讓咱們能夠以一個如此優雅的方式來編寫gulpfile代碼。app

初見Plug

既然咱們已經瞭解了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

子任務

Glup還容許在任務註冊中定義子任務。在這種狀況下,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.src。在Gulp中這個方法須要一個參數,能夠是一個文件掩碼(mask),文件名或文件掩碼數組。它返回一個可讀的Node流。

如今,在咱們的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,一個可選參數。這是由於node流在默認狀況下與二進制流工做。若是須要經過流傳遞/接收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對象。這樣更方便的,由於它不只包含文件名和內容,還包含額外的元信息,例如當前文件夾名稱,完整的文件路徑,等等。它可能沒包含整個內容緩衝區,但它有一個可讀的內容流。

vinyl:比文件更好

有一個很棒的類庫vinyl-fs,它能讓咱們像表示爲vinyl對象那樣處理文件。本質上,它讓咱們基於文件掩碼建立了一個可讀,可寫的流。

咱們可使用vinyl-fs類庫重寫plug函數。但首先咱們須要安裝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流會話,咱們能夠經過Plug工具來一塊兒使用本地Gulp插件。

讓咱們來試一下。安裝gulp-rename:

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,正如你想的那樣,應該產生相同的結果。

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目錄。

串行 vs 並行

既然來從Gulp API的所有基本功能已實現,讓咱們再進一步。即將到來的Gulp版本將包含更多的API函數。這些新的API將使Gulp更增強大:

  • gulp.parallel
  • gulp.series

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

假設在當前目錄咱們有test1.txttest2.txt。爲了將這些文件並行複製到了out目錄,讓咱們作一個plugfile:

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

}

咱們依賴於node流「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是如何工做的,以及讓咱們能更流暢使用它,並經過插件擴展它。


------------------------

相關文章
相關標籤/搜索