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])
監控文件,執行任務.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 ) ); });
瞭解這個規律以後,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…)緩步前行.
*
and !
gulp.src方法接收的是源文件路徑, 能夠是string也能夠是array.官方給出了src的具體語法和實現模塊:
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暴露給全局:
其餘的小模塊文件也都使用IIFE來給util添加不一樣的功能, 如foo.js:
$javascript$ (function(){ window.util.Foo = function(){ // Implementation of Foo return this; } })();
這樣的代碼結構, 咱們能夠在構建是保證index.js被最早寫入到合併後的文件, 而沒必要在每一個文件裏都判斷util變量存不存在, 咱們讓它在文件頭,必定存在.要達到這樣的目的,咱們的文件匹配數組要這樣寫:
$javascript$ gulp.task( 'build', function(){ gulp.src( [ 'lib/index.js', 'lib/**/*'] ) .pipe( concat( 'chenllosUtil.js' ) ) .pipe( gulp.dest( 'dist/lib/') ); } )
這就是優先匹配到的文件優先讀取.
下面來說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之因此這麼快, 是由於:
這裏有一篇slide,Build Wars, 構建工具之戰, 風趣幽默的介紹了grunt的興盛和gulp的崛起, 其中也提到了:
Grunt 1.0 alpha uses Orchestrator! (OMG)
Task-runner未來會更加的百家爭鳴哦~
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, 能夠獲得以下知識點:
on('error', handler)
處理函數找到這些資料以後, 就能夠寫出初步的帶有錯誤處理的task了:
$javascript$ // 編譯less, 壓縮 gulp.src( src ) .pipe( less({ compress: true }) ) .on( 'error', function(e){console.log(e)} ) .pipe( gulp.dest(dest) );
這樣的錯誤處理, 我用了一段時間. 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) ); } )
如今的錯誤處理魯棒性就很好了,至今沒有碰見它不能勝任的狀況.
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'] );
上述的代碼實現了」開發時代碼不壓縮,部署時執行默認任務進行js壓縮」的需求.雖然這個需求也能夠經過寫兩個coffee編譯任務來實現, 其中一個在dest前進行uglify, 可是這不應是程序猿的作法, 咱們纔不要作重複的勞動~!
固然單純的return stream
面對更復雜的task需求時會應付不來,咱們還有更好的方式來實現DRY的同時針對不一樣任務執行不一樣的流程, 那就是下面要講的條件判斷.
上面提到的需求: 在不增長重複代碼的前提下針對不一樣的command gulp [taskname]
執行不一樣的任務流程. 咱們能夠經過獲取命令的參數而後作判斷來實現.
gulp [taskname]
最終仍是執行的一個nodejs腳本, 因此能夠經過process.argv[2]來獲取taskname, 若是爲undefined
那麼就是default任務, 有值的話就是其餘的任務. 固然也能夠用cli比較標準的方式用cmd file ―param
的格式來實行命令, 在gulpfile內部來取得參數值, 具體能夠看這個討論.
在取得任務名稱後, 和gulpfile中預設好的一個tasklist來作對比, 看看是否是某些特定的任務. 咱們仍然以開發和部署來舉例. 預設一個這樣的tasklist: 若是任務名匹配到, 那麼就進行代碼壓縮,jade/less/coffee編譯後的文件都進行壓縮, 固然壓縮方法不同.因此咱們能夠作一個這樣的list:
而後用取得的taskname去匹配:
$javascript$ var taskName = process.argv[2]; var isPro = ( proTaskList.indexOf( taskName ) >=0 )? true: false;
這樣就判斷出了是不是須要壓縮的環境.接下來就是如何作判斷了.
gulp-cond模塊能夠在stream pipe的過程當中,判斷某個變量的true or false, 將stream導向不一樣的處理函數:
$javascript$ var cond = require('gulp-cond'); // codes .pipe( cond( boolean, trueHanlder() [, falseHandler() ] ) ) .pipe…
上述的意思是在判斷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() ) )
感謝親愛的讀者看完這篇長文,但願能給你平常的開發帶來些許便利. 有任何問題均可以在下面評論中留言, 也能夠到個人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
儘管js
和 ./js
的含義是同樣的, 都是當前目錄下的js文件夾, 可是匹配和排除的表達式模式不一致的時候就會沒法exclude掉. 這個問題… 我已經提交了issue, 不知道gulp team會怎麼回覆. 你們能夠關注一下這個issue. 渣英語, 勿怪.