gulp

gulp構建進階

9月2日更新: gulp.src時exclude的細節. 見文章尾部.javascript

9月2日更新: gulp-livereload模塊更新了, 升級以後提供的接口的用法發生了變化, 詳見模塊文檔css

新興的task-runner gulp給了開發者除grunt之外的選擇,讓咱們能夠更靈活更高效的執行構建.不得不認可gulp有不少grunt拍馬不及的優點,連gulpfile都要遠遠比gruntfile更加node-style.我這個不喜歡配置式的人馬上轉了過來, 本篇文章就深刻介紹一下gulp的使用, 算是進階文, 沒有任何基礎的同窗還請先去看一下官網或者其餘同窗寫的入門文.java

一句話開始

仍是贅述一下入門…就只說下四個API, 流式的處理過程就不說了, 一直pipe下去就行~node

  • gulp.task('taskname', [ taskDep1, taskDep2 ],taskContentFunc) 定義一個任務,聲明它的名稱, 任務依賴, 和任務內容.
  • gulp.src( file[s] ) 讀取文件,參數爲文件路徑字符串或數組, 支持通配符.
  • gulp.dest( destPath ) 寫入文件, 做爲pipe的一個流程.文件夾不存在時會被自動建立.
  • gulp.watch(files, [taskDep, taskDep2]) 監控文件,執行任務.

進階1:dest匹配src

gulp.dest( destPath )會將流中的文件寫入到destPath中, 但並非全部的文件都寫在destPath的根目錄下, 有的多是在其下又建立子目錄,其中的規則是怎樣的呢~?git

dest是與src相匹配的. src讀取文件路徑獲取文件,主要有三種狀況:github

  • 指定的文件:['/foo-1/bar-1.js', '/foo-1/bar-2.js','/foo-2/bar2.js'].
  • 模糊匹配文件名的文件: ['/foo-1/bar-*.js', 'foo-2/bar-*.js'].
  • 模糊匹配路徑下的文件: [/*/bar-*.js]

以上三種狀況讀取的文件, 前兩種會寫入到destPath的根目錄下, 而最後一種狀況, 會在destPath下新建foo-1和foo-2文件夾而後寫三個文件到相應的文件夾裏. 發現規律了嗎~? dest會將src」匹配」到的文件路徑寫出來~(這個匹配必須是純粹的*匹配, 全匹配, 若是是foo*式的半匹配就不會寫文件路徑) 第一和第二種匹配到的是指定文件, 第三種匹配到的是符合規則的路徑下的文件, 因此會出現上面的狀況. 也就是說, 若是想將源文件的路徑也dest到目標路徑, 那就須要將路徑也放在」匹配」中.chrome

下面來看例子: locale目錄下有en, zh-cn, zh-tw三個目錄,三個文件下各有一個lang.js, 把文件夾也輸出到目標路徑的作法以下:shell

$javascript$
gulp.task('locale', function(){
    gulp.src( 'locale/*/lang.js' )
        .pipe( uglify() )
        .pipe( gulp.dest( localeDest ) );
});
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
代碼laycode - v1.1

瞭解這個規律以後,destPath下的目錄結構就徹底可控了.npm

gulp中實現src和dest的模塊, 深刻探查以後發現,能夠當作是gulp -> vinyl-fs ->vinyl -> glob-stream -> node-glob的依賴鏈, 從最底層往上依次來看,node-glob實現的是:gulp

Match files using the patterns the shell uses, like stars and stuff.

而後glob-stream把匹配到的文件流式讀取, vinyl構建出gulp生態系統內很關鍵的File類, 其中的file.relative就是文件匹配時*全匹配出的路徑, 上面的例子中就是 [lang-type]/lang.js.

src完成工做, File一直pipe下去, 直到dest時,vinyl-fs有一套寫文件的規則: 傳入的outFolder 加上 匹配文件時的relative的dirname, 做爲目標目錄. 整個流程下來,就是完整的src到dest的過程.

對整個流程感興趣的同窗, 能夠按照我上面寫出的依賴鏈來讀一下相關的源碼. 其中除了node-glob是npm做者issaacs寫的以外,都是gulp的做者團隊實現的, 他們作好好多工做啊.

不明白或者不想太深究的同窗,也沒必要擔憂.gulp近期內就應該會有」動態dest」的功能出現, 具體參見這個issue: Allow function for outFolder in dest, 持續集成測試已經經過, 就等merge啦~

不懂爲何gulp的版本號跑的怎麼這麼快, 如今都3.6.3了, 它的前輩還在1.0如下(最新的是0.4.5…)緩步前行.

進階2: src的玄機 ― 遞歸和exclude:* and !

gulp.src方法接收的是源文件路徑, 能夠是string也能夠是array.官方給出了src的具體語法和實現模塊:

$javascript$
gulp.src(globs[, options])
  1. 1
  2. 2
代碼laycode - v1.1

glob refers to node-glob syntax or it can be a direct file path.

通讀一遍node-glob模塊的文檔後再加以實踐,就能掌握gulp.src的玄機,將你想要的文件」吸收」到」管道」中來,而後pipe處理下去:

  • foo.js指明特定某個文件
  • *.js匹配當前目錄下的全部js文件,不指名擴展名則匹配全部類型
  • */*.js匹配全部第一層子文件夾的js文件,第二層請用*/*/.js
  • **/*.js匹配全部文件夾層次下的js文件, 包括當前目錄

上述就是匹配和文件夾深度匹配規則, 當上述兩條或者兩條以上的規則想結合的話, 匹配到的文件會有必定的讀取順序, 以這樣的一個task爲例: 爲構建一個工具庫, 須要將lib文件夾下全部的工具模塊小文件合併成一個文件, 其中index.js中經過下面的代碼將util暴露給全局:

$javascript$
(function( env ){ env.util = {} })(window);
  1. 1
  2. 2
代碼laycode - v1.1

其餘的小模塊文件也都使用IIFE來給util添加不一樣的功能, 如foo.js:

$javascript$
(function(){ window.util.Foo = function(){
    // Implementation of Foo 
    return this;
} })();
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
代碼laycode - v1.1

這樣的代碼結構, 咱們能夠在構建是保證index.js被最早寫入到合併後的文件, 而沒必要在每一個文件裏都判斷util變量存不存在, 咱們讓它在文件頭,必定存在.要達到這樣的目的,咱們的文件匹配數組要這樣寫:

$javascript$
gulp.task( 'build', function(){
    gulp.src( [ 'lib/index.js', 'lib/**/*'] )
        .pipe( concat( 'chenllosUtil.js' ) )
        .pipe( gulp.dest( 'dist/lib/')  );
} )
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
代碼laycode - v1.1

這就是優先匹配到的文件優先讀取.

下面來說exclude.

排除的方法就是在文件匹配pattern前加!, 跟.gitignore相似. 但有些地方須要注意:若是任務須要你使用**/*遞歸匹配, 那麼執行exclude也須要遞歸exclude, 即!exclude/path/**/*.

這個坑是筆者在最近寫atom-shell程序時遇到的: 我將atom-shell.app放在了項目目錄, 須要構建成OSX的app時就把項目目錄中全部的文件都拷貝到atom-shell.app下的contents/resources/app目錄下. 全部文件, 固然是要用**/*遞歸匹配嘍~, 後面再跟!./atom-shell.app, 而OSX下的.app文件其實就是文件夾, 遞歸匹配加非遞歸exclude, 就形成了其實.app也被讀取的情況, 而後就無限引用了… 不過gulp有本身的保護機制, 沒有出死循環這樣的傻事.

正確的做法是['**/*', '!./atom-shell.app', '!./atom-shell.app/**/*']. 這樣就能夠把app徹底exclude掉了.

by the way, 系統的」最大讀取文件數」會限制gulp的執行, 好比**/*將node_module也包含了進來的時候… 具體我是怎麼避免這個問題的, 你們能夠看我正在寫的atom-shell項目的gulpfile.

gulp項目的issue裏也有討論這個問題的, 很多同仁踩了這個坑, 雖然情景不太同樣,但都是」exclude文件夾與其下的全部文件」的問題.

課間休息~

gulp的github repo裏有這樣一個issue:

―: μs: Is it a font issue maybe? ―: http://en.wikipedia.org/wiki/Microsecond ―: Wow, that was fast (pun intended). Thanks!

:-D

呃, 其實gulp沒有那麼那麼快, 只是其中的stream處理的這麼快而已, 真正的讀寫文件,仍是須要多一點時間的, 這也就是爲何看一系列的task都執行完以後進程尚未退出的緣由, 它還在寫文件~! 就筆者的經驗看來, 從less文件編譯出十幾個200~2500行的未壓縮css文件仍是須要個近1秒的時間的( 從敲入gulp less 命令到進程退出), 而輸出顯示less任務處理的時間只有10ms.文件IO的速度是固定的,task-runner能比拼的只有併發+處理速度+優化減小文件讀寫.

gulp之因此這麼快, 是由於:

  • 使用orchestrator任務系統,最大限度的併發運行多個任務.
  • 每一個plugin只作一件事,作好一件事, 提高處理速度
  • 流式處理,極少的文件IO

這裏有一篇slide,Build Wars, 構建工具之戰, 風趣幽默的介紹了grunt的興盛和gulp的崛起, 其中也提到了:

Grunt 1.0 alpha uses Orchestrator! (OMG)

Task-runner未來會更加的百家爭鳴哦~

進階3: watch的高級應用 ― liveReload 和 錯誤處理

gulp中自帶的gulp.watch能幫你監控指定的文件, 隨之觸發執行指定的任務, 或者, 執行一個回調. 注意, 是或者, 不能二者兼具.

gulp.watch(glob [, opts], tasks) or gulp.watch(glob [, opts, cb])

有這麼一個模塊, 叫作gulp-livereload, 它能夠在一個端口(默認爲35729)上起一個server, 而後在你的頁面上注入一個script標籤, 引用它提供的通信腳本, 在文件發生改變後server通知頁面的腳本從新加載這個改變的文件.筆者以前試過好多種相似與livereload(如下簡稱LR)的插件或工具,這個算是最中意的~

注入腳本的工做能夠交給這樣的一個名爲LiveReload的chrome瀏覽器擴展程序,這個擴展的圖標作的略挫,辨識度較低,開啓和沒開啓的區別就只有中間的小圓點是空心和實心… 固然擴展是死的, 它只會注入對應默認server端口的腳本, 有同時開啓兩個LR的需求的同窗,仍是本身定一個端口號而後實例化的時候傳給plugin而後在頁面中也好script tag吧~

LR有兩種用法,做爲stream的一環使用或者在watch中使用, 在它的nom頁面有詳細的介紹, 我就不贅述了. 其中更加靈活的用法確定是在watch中, 好比監控less編譯任務的dest目錄下css文件的變化, 而後使頁面從新加載某個css文件, 經過lr.changed( filePath )來指定. 不必定非得是變動的文件哦~ ). 具體的用法和方便之處, 你用了就會知道. 在開發傳統頁面或者SPA時, 都能提高很多的效率.

並無特別深刻的去研究reload的規則, 用它來reload樣式表和整個page都是很好用的. 運行機制應該就是LR-server和頁面注入的js通信, 文件發生改變時根據規則進行不一樣的reload. 從使用經驗來看, 若是changed的file在瀏覽器頁面中match不到, 注入的腳本就會從新載入整個頁面. 這能夠當作一個feature來使用哦, 好比作nodejs開發, 使用ejs或jade或其餘模板引擎時, 變動的文件爲.ejs或者.jade, LR就會從新載入頁面, 正是我想要的結果.

個人下一篇文會講到jade預編譯和LR配合使用的方法, stay tuned.

gulp任務添加錯誤處理在沒有使用watch時還顯得沒那麼重要,但使用了watch以後, 沒有錯誤處理的task在發生錯誤後會馬上崩潰掉整個gulp進程, 想繼續watch?重啓~! 這一點程序猿確定沒法忍受, 除非你能夠保證每一次的文件保存操做都是能夠build經過的… 我這種保存強迫症卻對作不到.

通過查閱issue和相關blog, 能夠獲得以下知識點:

  • gulp官方推薦的plugin書寫規範中, 推薦在發生錯誤時拋出一個gulp-util的PluginError
  • 這個錯誤會出如今文件處理stream上
  • 想要捕獲這個錯誤, 在stream上添加on('error', handler)處理函數

找到這些資料以後, 就能夠寫出初步的帶有錯誤處理的task了:

$javascript$
// 編譯less, 壓縮
gulp.src( src )
    .pipe( less({ compress: true }) )
    .on( 'error', function(e){console.log(e)} )
    .pipe( gulp.dest(dest) );
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
代碼laycode - v1.1

這樣的錯誤處理, 我用了一段時間. it works well. 後來在處理jade編譯時出現了一些問題, 這裏只簡單說一下: 若是jade編譯任務return stream(爲何return stream下面會講), 錯誤發生時errhandler會避免進程crash可是會致使沒法繼續watch jade compile, 也就是說文件變動後也不會執行任務. 其餘任務不受影響.

這些表現上的差別應該是不一樣插件實現原理不一樣致使的,所以須要有一個能夠處理全部錯誤的通用方法, 我找到了gulp-plumber, 流式任務體系中的水管工.

使用方法:

$javascript$
var gutil = require( 'gulp-util' )
var plumber = require( 'gulp-plumber' );
function errrHandler( e ){
    // 控制檯發聲,錯誤時beep一下
    gutil.beep();
    gutil.log( e );
}
gulp.task( 'less', function(){
    gulp.src( [srcPath] )
        // 在處理前註冊plumber
        .pipe( plumber( errorHandler: errrHandler ) )
        .pipe( less() )
        .pipe( gulp.dest(destPath) );
} )
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
代碼laycode - v1.1

如今的錯誤處理魯棒性就很好了,至今沒有碰見它不能勝任的狀況.

進階3:同步任務/線性任務

gulp比起grunt更加的node-style, 並且其依賴的orchestrator的特性就是最大併發的執行任務, 以上, 在提供了大併發和nodejs異步操做特性的同事,給使用者在gulp中作同步的線性的任務帶來了必定的難度.

根據gulp在github上的文檔的描述, 有三種方法能夠經過任務依賴實現線性的同步的任務.

callback/ return stream / return promise

doc中也給出了三種的範例代碼, 但就筆者看來實際可操做的就只有return stream一項, 由於其餘的兩中方式, 須要的是你準確的知道什麼時候任務結束, 而後callback() 或者 進行promise的revolve. 所以, 筆者的同步任務實踐, 只有return stream一種, 下面是範例:

$javascript$
gulp.task( 'coffee', function(){
    return gulp.src( coffee Src )
        .pipe( coffee() )
        .pipe( gulp.dest( coffeeDest ) )
} );
gulp.task( 'uglify', ['coffee'], function(){
        return gulp.src( jsSrc )
            .pipe( uglify() )
            .pipe( jsMinDest );
} );

gulp.task( 'dev', ['coffee'] );

gulp.task( 'default', ['uglify'] );
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
代碼laycode - v1.1

上述的代碼實現了」開發時代碼不壓縮,部署時執行默認任務進行js壓縮」的需求.雖然這個需求也能夠經過寫兩個coffee編譯任務來實現, 其中一個在dest前進行uglify, 可是這不應是程序猿的作法, 咱們纔不要作重複的勞動~!

固然單純的return stream面對更復雜的task需求時會應付不來,咱們還有更好的方式來實現DRY的同時針對不一樣任務執行不一樣的流程, 那就是下面要講的條件判斷.

進階4:條件判斷, 讓task-runner更懂你

上面提到的需求: 在不增長重複代碼的前提下針對不一樣的command gulp [taskname]執行不一樣的任務流程. 咱們能夠經過獲取命令的參數而後作判斷來實現.

gulp [taskname]最終仍是執行的一個nodejs腳本, 因此能夠經過process.argv[2]來獲取taskname, 若是爲undefined那麼就是default任務, 有值的話就是其餘的任務. 固然也能夠用cli比較標準的方式用cmd file ―param的格式來實行命令, 在gulpfile內部來取得參數值, 具體能夠看這個討論.

在取得任務名稱後, 和gulpfile中預設好的一個tasklist來作對比, 看看是否是某些特定的任務. 咱們仍然以開發和部署來舉例. 預設一個這樣的tasklist: 若是任務名匹配到, 那麼就進行代碼壓縮,jade/less/coffee編譯後的文件都進行壓縮, 固然壓縮方法不同.因此咱們能夠作一個這樣的list:

$javascript$
var proTaskList = [undefined, 'all-compress'];
  1. 1
  2. 2
代碼laycode - v1.1

而後用取得的taskname去匹配:

$javascript$
var taskName = process.argv[2];
var isPro = ( proTaskList.indexOf( taskName ) >=0 )? true: false;
  1. 1
  2. 2
  3. 3
代碼laycode - v1.1

這樣就判斷出了是不是須要壓縮的環境.接下來就是如何作判斷了.

gulp-cond模塊能夠在stream pipe的過程當中,判斷某個變量的true or false, 將stream導向不一樣的處理函數:

$javascript$
var cond = require('gulp-cond');
// codes
.pipe( cond( boolean, trueHanlder() [, falseHandler() ] ) )
.pipe…
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
代碼laycode - v1.1

上述的意思是在判斷boolean的值後,true時執行trueHandler()falseHandler()是可選的.

如此一來就能夠在jade/less/coffee的編譯任務中根據不一樣的命令行任務進行不一樣的流導向, 筆者經常使用的幾句判斷是這樣的, 固然能夠在個人github項目(如個人blog項目)中找到所有的gulpfile.

$javascript$
// jade 編譯
cond( isPro, jade(), jade({ pretty: true }) )

// less編譯 生產時壓縮, 開發時將"哪一個less文件的哪一行生成了該條css規則"
// 輸出在編譯好的css文件裏
cond( isPro,
    less({ compressed: true }),
    less({ dumpLineNumbers: 'comments'})
)

// coffee 編譯, 生產時添加uglify流程
.pipe( coffee({ bare: true }) )
.pipe( cond( isPro, uglify() ) )
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
代碼laycode - v1.1

額外的東西:

  • 使用gulp-changed模塊能夠大大減小重複編譯, 注意extension參數能夠告訴模塊, 當前任務的源文件和產出文件的擴展名是不一樣的,把這點考慮進來以後changed模塊就能精準的判斷編譯類的任務的源文件是否較上次有變動了.
  • 儘管gulpfile中是寫function來定義和執行任務, 可是一點點配置加上靈活的function會讓程序的複用性加強並且更加易讀哦.
  • gulp如今的模塊數大約有700+,遠遠及不上grunt的生態系統. 固然因爲gulp的plugin功能性更加專注, 使得模塊間相互配合更加方便, 插件的編寫也更加的簡單.官方給出的gulp-replace模塊樣例就至關之簡單, 趕快給gulp社區貢獻本身的一份力量吧~

結語

感謝親愛的讀者看完這篇長文,但願能給你平常的開發帶來些許便利. 有任何問題均可以在下面評論中留言, 也能夠到個人github上提issue.

9月2日更新: gulp.src時exclude的細節:

gulp.src匹配, 還有gulp.watch時的匹配, 本質上都是vinyl-fs作的文件系統的匹配. 在寫exclude匹配表達式, 也就是那個gulp.src的參數字符串或者字符串數組時, exclude的模式要和匹配的模式一致:

var file2w = ['js/**/*.js', '!js/bundle.js'];        // 能夠exclude掉bundle.js
file2w = ['js/**/*.js', '!./js/bundle.js'];            // 沒法exclude掉bundle.js
  1. 1
  2. 2
代碼laycode - v1.1

儘管js 和 ./js的含義是同樣的, 都是當前目錄下的js文件夾, 可是匹配和排除的表達式模式不一致的時候就會沒法exclude掉. 這個問題… 我已經提交了issue, 不知道gulp team會怎麼回覆. 你們能夠關注一下這個issue. 渣英語, 勿怪.

相關文章
相關標籤/搜索