Gulp思惟——Gulp高級技巧

本文翻譯自Getting gulpy -- Advanced tips for using gulp.jsgit

感覺過gulp.js帶來的興奮事後,你須要的不只僅是它的光鮮,而是切切實實的實例。這篇文章討論了一些使用gulp.js時常踩的坑,以及一些更加高級和定製化的插件和流的使用技巧。github

基本任務

gulp的基本設置擁有很是友好的語法,讓你可以很是方便的對文件進行轉換:web

gulp.task('scripts', function() {
    return gulp.src('./src/**/*.js')
        .pipe(uglify())
        .pipe(concat('all.min.js'))
        .pipe(gulp.dest('build/'));
});

這種方式可以應付絕大多數狀況,但若是你須要更多的定製,很快就會遇到麻煩了。這篇將介紹這其中的一些狀況並提供解決方案。正則表達式

流不兼容?

使用gulp時,你可能會陷入「流不兼容」的問題。這主要是由於常規流和Vinyl文件對象有差別,或是使用了僅支持buffer(不支持流)庫的gulp插件與常規流不兼容。npm

好比說,你不能直接將常規流與gulp和(或)gulp插件相連。咱們建立一個可讀流,並嘗試使用gulp-uglifygulp-rename來進行轉換,將最後獲得的內容交給gulp.dest()。下面就是個錯誤的例子:gulp

var uglify = require('gulp-uglify'),
    rename = require('gulp-rename');
gulp.task('bundle', function() {
    return fs.createReadStream('app.js')
        .pipe(uglify())
        .pipe(rename('bundle.min.js'))
        .pipe(gulp.dest('dist/'));
});

爲何咱們不能將可讀流和一個gulp插件直接相連?gulp難道不就是一個基於流的構建系統嗎?是的,但上面的例子忽視了一個事實,gulp插件指望的輸入是Vinyl文件對象。你不能直接將一個可讀流與一個以Vinyl文件對象做爲輸入的函數(插件)相連app

Vinyl文件對象

gulp使用了vinyl-fs,它實現了gulp.src()gulp.dest()方法。vinyl-fs使用vinyl文件對象——一種「虛擬文件格式」。若是咱們須要將gulp和(或)gulp插件與常規的可讀流一塊兒使用,咱們就須要先把可讀流轉換爲vinyl。函數

使用vinyl-source-stream是個不錯的選擇,以下:grunt

var source = require('vinyl-source-stream'),
    marked = require('gulp-marked');
fs.createReadStream('*.md')
    .pipe(source())
    .pipe(marked())
    .pipe(gulp.dest('dist/'));

另一個例子首先經過browserify封裝並最終將其轉換爲一個vinyl流:工具

var browserify = require('browserify'),
    uglify = require('gulp-uglify'),
    source = require('vinyl-source-stream');
gulp.task('bundle', function() {
    return browserify('./src/app.js')
        .bundle()
        .pipe(source(‘bundle.min.js))
        .pipe(uglify())
        .pipe(gulp.dest('dist/'));
});

哎呦不錯哦。注意咱們再也不須要使用gulp-rename了,由於vinyl-source-stream建立了一個擁有指定文件名的vinyl文件實例(這樣gulp.dest方法將使用這個文件名)

gulp.dest

這個gulp方法建立了一個可寫流,它真的很方便。它從新使用可讀流中的文件名,而後在必要時建立文件夾(使用mkdirp)。在寫入操做完成後,你可以繼續使用這個流(好比:你須要使用gzip壓縮數據並寫入到其餘文件)

流和buffer

既然你有興趣使用gulp,這篇文章假設你已經瞭解了流的基礎知識。不管是buffer仍是流,vinyl的虛擬文件都能包含在內。使用常規可讀流時,你能夠監聽data事件來檢測數據碎片的到來:

fs.createReadStream('/usr/share/dict/words').on('data', function(chunk) {
    console.log('Read %d bytes of data', chunk.length);
});
> Read 65536 bytes of data
> Read 65536 bytes of data
> Read 65536 bytes of data
> Read 65536 bytes of data
> ...

不一樣的是,使用gulp.src()會將轉換成buffer的vinyl文件對象從新寫入到流中。也就是說,你得到的再也不是數據碎片,而是將內容轉換成buffer後的(虛擬)文件。vinyl文件格式擁有一個屬性來表示裏面是buffer仍是流,gulp默認使用buffer:

gulp.src('/usr/share/dict/words').on('data', function(file) {
    console.log('Read %d bytes of data', file.contents.length);
});
> Read 2493109 bytes of data

這個例子說明了在文件被完整加入到流以前數據會被轉換成buffer。

Gulp默認使用buffer

儘管更加推薦使用流中的數據,但不少插件的底層庫使用的是buffer。有時候必須使用buffer,由於轉換須要完整的文件內容。好比文本替換和正則表達式的情形。若是使用數據碎片,將會面臨匹配失敗的風險。一樣,像UglifyJSTraceur Compiler須要輸入完整的文件內容(至少須要語法完整的JavaScript字符串)

這就是爲何gulp默認使用轉換成buffer的流,由於這更好處理。

使用轉換成buffer的流也有缺點,處理大文件時將很是低效。文件必須徹底讀取,而後才能被加入到流中。那麼問題來了,文件的尺寸多大才會下降性能?對於普通的文本文件,好比JavaScript、CSS、模板等等,這些使用buffer開銷很是小。

在任何狀況下,若是將buffer選項設爲false,你能夠告訴gulp流中傳遞的內容到底是什麼。以下所示:

gulp.src('/usr/share/dict/words', {buffer: false}).on('data', function(file) {
    var stream = file.contents;
    stream.on('data', function(chunk) {
        console.log('Read %d bytes of data', chunk.length);
    });
});
> Read 65536 bytes of data
> Read 65536 bytes of data
> Read 65536 bytes of data
> Read 65536 bytes of data
> ...

從流到buffer

因爲所需的輸入(輸出)流和gulp插件不盡相同,你可能須要將流轉換成buffer(反之亦然)。以前已經有過介紹,大多數插件使用buffer(儘管他們的一部分也支持流)。好比gulp-uglifygulp-traceur。你能夠經過gulp-buffer來轉換成buffer:

var source = require('vinyl-source-stream'),
    buffer = require('gulp-buffer'),
    uglify = require('gulp-uglify');
fs.createReadStream('./src/app.js')
    .pipe(source('app.min.js'))
    .pipe(buffer())
    .pipe(uglify())
    .pipe(gulp.dest('dist/'));

或者另外一個例子:

var buffer = require('gulp-buffer'),
    traceur = require('gulp-traceur');
gulp.src('app.js', {buffer: false})
    .pipe(buffer())
    .pipe(traceur())
    .pipe(gulp.dest('dist/'));

將buffer轉換爲流

你也可使用gulp-streamifygulp-stream將一個使用buffer的插件的輸出轉化爲一個可讀流。這樣處理以後,跟在使用buffer的插件後面的(只能)使用流的插件也能正常工做了。

var wrap = require('gulp-wrap'),
    streamify = require('gulp-streamify'),
    uglify = require('gulp-uglify'),
    gzip = require('gulp-gzip');
gulp.src('app.js', {buffer: false})
    .pipe(wrap('(function(){<%= contents %>}());'))
    .pipe(streamify(uglify()))
    .pipe(gulp.dest('build'))
    .pipe(gzip())
    .pipe(gulp.dest('build'));

不是全部事都須要插件

雖然已經有不少使用且方便的插件,不少任務以及轉換能夠不使用插件而輕易完成。插件會帶來一些問題,你須要依賴一個額外的npm模塊,一個插件接口和(反應遲鈍?)的維護者,等等。若是一個任務能夠不使用插件而使用原生模塊就能輕易完成,絕大多數狀況下,都建議不要使用插件。可以理解上面所說的概念,並可以在所處的狀況下作出正確的決定,這點很是重要。下面來看一些例子:

vinyl-source-stream

以前的例子中,咱們已經直接使用了browserify,而不是使用(現已加入黑名單)gulp-browserify插件。這裏的關鍵是使用vinyl-source-stream(或相似的庫)進行加工,來將常規的可讀流輸入使用vinyl的插件。

文本轉換

另外一個例子就是基於字符串的變換。這裏有一個很是基礎的插件,直接使用了vinyl的buffer:

function modify(modifier) {
    return through2.obj(function(file, encoding, done) {
        var content = modifier(String(file.contents));
        file.contents = new Buffer(content);
        this.push(file);
        done();
    });
}

你能夠像這樣使用這個插件:

gulp.task('modify', function() {
    return gulp.src('app.js')
        .pipe(modify(version))
        .pipe(modify(swapStuff))
        .pipe(gulp.dest('build'));
});
function version(data) {
    return data.replace(/__VERSION__/, pkg.version);
}
function swapStuff(data) {
    return data.replace(/(\w+)\s(\w+)/, '$2, $1');
}

這個插件並無完成,並且也不能處理流(完整版本)。然而,這個例子說明,能夠很輕易地經過一些基本函數來建立新的變換。through2庫提供了很是優秀的Node流封裝,而且容許像上面那樣使用轉換函數。

任務流程

若是你須要去運行一些定製化或動態的任務,瞭解gulp所使用的Orchestrator模塊會頗有幫助。gulp.add方法其實就是Orchestrator.add方法(事實上全部的方法都是從Orchestrator繼承而來的)。但爲何你須要這個?
* 你不想「私有任務」(好比:不暴露給命令行工具)弄亂gulp任務列表。
* 你須要更多的動態的和(或)可重用的子任務。

最後的思考

請注意,gulp(或grunt)並不老是當前情境下的最佳工具。好比說,若是你須要拼接並使用uglify壓縮一系列的JavaScript文件,又或者你須要編譯一些SASS文件,你可能須要考慮使用makefile或npm run,經過命令行來實現。減小依賴,減小配置,纔是正解。

閱讀經過npm run來實現任務自動化來了解更多信息。你須要明確經過一系列的「自定義構建」後須要獲得什麼,而哪一個工具最合適。

不過,我以爲gulp是一個偉大的構建系統,我很喜歡使用它,它展示了Node.js中流的強大。

但願這些可以幫到你!若是你有任何反饋或其餘提議,請在評論中告訴我,或者加個人twitter:@webprolific

小廣告:更多內容歡迎來個人博客,共同探討

相關文章
相關標籤/搜索