gulp的流與執行順序
gulp的關鍵在於流,這從它的logo就能看出來。
在node中,流是操作文件時一個重要的概念。流是指什么呢?它包含兩個含義:“水流”和“流水”。 水流蘊含了源源不斷或是一股一股那樣流過的意味;而流水是“流水線”或是“流水作業”里那種讓物件通過各個環節依次對其加工的意思。 我們經常接觸到的“流媒體”主要是前者的含義,當你在線看一部電影時,影音數據從服務器源源不斷地流入你的播放器, 再經過一些處理展現在你眼前;而gulp中的流我覺得含義偏重於后者,因為gulp的任務就是把源文件進行各種加工處理最終輸出到指定位置。 我們說“源文件”而不是“原文件”,在gulp中,它還真是流的源頭。
gulp是基於node的,但是它並沒有直接使用node中fs模塊里的文件系統和流,而是包裝了一層vinyl。 vinyl是一個用來描述文件的簡單的數據格式,通過vinyl-fs可以把node原生的文件系統封裝成vinyl。 這個封裝使得整個流的過程更加簡單。從源頭上,vinyl使用glob語法獲取源,比如通過一個表達式 "src/**/*.js"就獲得到了src目錄下各級目錄中的js文件,這要是用原生的fs恐怕得寫個遍歷樹的算法程序了吧。 在gulp或vinyl-fs的api里,通過一個傳入glob表達式的src方法就獲得到了一個流的源。 很明顯,在多數情況下這個源是由多個文件組成的,可以想象成這些文件構成了一個一股一股的文件流, 都將要通過一系列管道被加工處理。那么接下來就是管道,與原生的fs相同,vinyl使用管道也是用pipe方法。 pipe接受一個函數為參數,將當前流的內容傳給這個函數讓其加工,vinyl把流的內容封裝得更加簡明好用, 而且,對於調用一次pipe方法,其傳入的函數會對這個流的所有文件作用,換句話說,傳入pipe方法的函數實際上是針對一個文件的, 而流中所有的文件都會被這個函數加工一下。這么看,vinyl的流有些並行的感覺,但本質上說javascript是單線程的, 加工的過程還是一個接着一個進行的,所以說成讓文件一個接一個地流過某個管道更確切。
既然是流,就應該有一種順序進行的感覺。不過處理流的代碼是異步的,比如下面的代碼:
gulp = require('gulp')
through = require('through2')
gulp.task 'test', ->
stream = gulp.src('src/js/*.js')
.pipe through.obj (file, enc, cb) ->
console.log 'processing...'
cb null, file
.pipe(gulp.dest('test'))
console.log 'end'
如果在src/js目錄下有兩個js文件。執行gulp task,結果是:
[20:12:32] Starting 'test'...
end
[20:12:32] Finished 'test' after 13 ms
processing...
processing...
很顯然,pipe中的函數是異步執行的。不過對於流中的一個文件,各pipe中的函數一定會按照先后順序執行。 再來看一段代碼,為了方便,我把管道中的處理函數寫成一般gulp插件的形式:
processor = (info) ->
through.obj (file, enc, cb) ->
console.log file.path, info
cb null, file
gulp.task 'test', ->
stream = gulp.src('src/js/*.js')
.pipe processor("in pipe 1")
.pipe processor("in pipe 2")
.pipe processor("in pipe 3")
.pipe(gulp.dest('test'))
console.log 'end'
執行結果是:
[16:31:24] Starting 'test'...
end
[16:31:24] Finished 'test' after 9.02 ms
/src/js/city.js in pipe 1
/src/js/city.js in pipe 2
/src/js/city.js in pipe 3
/src/js/sysUtils.js in pipe 1
/src/js/sysUtils.js in pipe 2
/src/js/sysUtils.js in pipe 3
執行結果的確是像流那樣一個文件挨着一個文件,一個過程接着一個過程處理完成的。盡管pipe中的函數會異步執行, 但它們嚴格按照先注冊先執行的順序進行。看來一個任務的執行順序在一個流中是能夠得以保證的,而且也只能在一個流中得以保證。
那么對於多個任務的情況呢?gulp.task方法可以接受一個任務數組,任務數組中的任務將會並行執行。 而傳入gulp.task的函數將會在任務數組中所有任務執行完畢后開始執行。簡單來說是這樣的,實際要小心。
gulp的api中關於task方法有這么一項注意:“Are your tasks running before the dependencies are complete? Make sure your dependency tasks are correctly using the async run hints: take in a callback or return a promise or event stream.” 中文版本是:“你的任務是否在這些前置依賴的任務完成之前運行了?請一定要確保你所依賴的任務列表中的任務都使用了正確的異步執行方式:使用一個 callback,或者返回一個 promise 或 stream。”
既然任務中的那些處理函數一般都是異步執行的,那么怎么才能知道它們執行完了呢?只能是通過回調了, 可以是直接的回調,也可以是其它約定好的回調,也就是promise或者流的事件。 對於JavaScript異步編程來說這是很常見的事情,然而這也帶來了相應的局限性。來看個例子:
gulp.task 'buildjs', (cb) ->
del.sync('prd/js')
gulp.src('src/js/**/*.js')
.pipe uglify({output:{ascii_only:true}})
.pipe gulp.dest('prd/js')
gulp.src(['src/js/**/*.*', '!src/js/**/*.js'])
.pipe gulp.dest('prd/js')
這是一個很常見的任務,就是把js代碼混淆壓縮,然后把非js代碼原樣拷貝出去。我寫的是coffee版的gulpfile, coffeescript會默認把最后一個表達式作為返回值,所以這里實際上是返回了第二個流,也就是拷貝非js文件的那個流。 如果只執行這個任務倒無所謂,誰先誰后都能完成,但是如果它被作為前置任務呢?
gulp.task zip, ['buildjs'], ->
gulp.src('prd/js/*.js')
.pipe(zip('release.zip'))
.pipe(gulp.dest('prd'))
如果文件比較多的話,會發現壓縮包里的文件不完整。原因就是buildjs這個任務返回的流是拷貝文件那個流, 而zip這個任務也只會等待拷貝文件完成時開始,此時混淆文件那個流還不一定能執行完。如果返回混淆文件那個流, 照常理說這個流會執行的慢一些,但仍不那么靠譜,畢竟沒有邏輯保障,所以應該把它們都拆開,分別作為zip的前置任務:
gulp.task 'buildjs', ->
gulp.src('src/js/**/*.js')
.pipe uglify({output:{ascii_only:true}})
.pipe gulp.dest('prd/js')
gulp.task 'copy', ->
gulp.src(['src/js/**/*.*', '!src/js/**/*.js'])
.pipe gulp.dest('prd/js')
gulp.task 'zip', ['buildjs', 'copy'], ->
gulp.src('prd/js/*.js')
.pipe(zip('release.zip'))
.pipe(gulp.dest('prd'))
這樣,zip一定會等buildjs和copy兩個任務中各自的流全都執行完才會開始執行,從邏輯上也沒問題了。 這樣看好像是把本來可以在一個任務里完成的東西拆開了,不過gulp本身鼓勵短小專一,所有的gulp插件都很小, 且只完成一件事情。這么說的話構建js文件和拷貝非js文件說是兩件事也比較合理。
上例是一個兩級的順序保障,“分-總”的結構。你也許發現我偷偷地把刪除目錄的一句給去掉了。因為我的確不知道該把它放在哪里好。 buildjs和copy是並行的,不能確保誰先,如果放到buildjs里,萬一copy先執行了,誤刪了已經拷貝過去的東西可不好。 所以,我需要多級的順序保障,把刪除目錄的任務放在更高的一級,形成一個“總-分-總的結構”。 然而我並沒找到形成這種結構的方法,貌似只能一個接着一個地進行:
gulp.task 'clean', ->
del.sync('prd/js')
gulp.task 'copy', ['clean'], ->
gulp.src(['src/js/**/*.*', '!src/js/**/*.js'])
.pipe gulp.dest('prd/js')
gulp.task 'buildjs', ['copy'], ->
gulp.src('src/js/**/*.js')
.pipe uglify({output:{ascii_only:true}})
.pipe gulp.dest('prd/js')
gulp.task 'zip', ['buildjs'], ->
gulp.src('prd/js/*.js')
.pipe(zip('release.zip'))
.pipe(gulp.dest('prd'))
這樣順序是沒啥問題了,就是看着挺別扭的,我需要一個構建js文件夾里面內容的任務,卻需要一層又一層地依賴多個任務。 還有一種辦法可以把所有步驟一股腦地放在一個任務里,就是利用流的事件,vinyl的流和原生fs流其實基本一樣, 也有那些事件,所以可以利用end事件來控制順序。我認為上面的copy和buildjs兩個任務不應當拆開,就把他們寫在一起:
gulp.task 'copy', ['clean'], (cb) ->
lastStream = null
gulp.src(['src/js/**/*.*', '!src/js/**/*.js'])
.pipe gulp.dest('prd/js').
.on 'end', ->
lastStream = gulp.src('src/js/**/*.js')
.pipe uglify({output:{ascii_only:true}})
.pipe gulp.dest('prd/js')
.on 'end', cb
要注意的是,下一個任務需要等待這個任務最有一個流執行完再開始,所以這里需要在最后執行的流上加上對end事件的處理,執行參數傳入的回調。