|
在寫generator-aio-angular的過程中,gulp這一塊發(fā)現(xiàn)了很多非常實(shí)用的插件,大大的增加了能自動(dòng)化的范圍,這篇文章就分門別類的簡單介紹下常用的gulp插件吧。
util工具類
這個(gè)分類下主要介紹一些輔助工具類的插件。
顧名思義,本插件的功能就是幫你自動(dòng)require你在package.json中聲明的依賴。只要一句var
$ = require('gulp-load-plugins')(),則package.json中聲明的gulp-或gulp.開頭的插件就會(huì)自動(dòng)被放在變量$下面。如$.util就等于require('gulp-util'),而有兩個(gè)連字符的插件則會(huì)自動(dòng)命名為駝峰格式,如$.taskListing則等于require('gulp-task-listing')。有了這個(gè)插件,就不用一個(gè)一個(gè)的require了。這個(gè)插件還有一些常用的參數(shù)配置,這里列幾個(gè)常用的:
lazyload: true,用到這個(gè)插件的時(shí)候再去require,默認(rèn)為true。rename: {'gulp-task-listing': 'list'},如果有些插件名字太長,可以使用該參數(shù)重命名。scope: ['dependencies'],本插件默認(rèn)會(huì)掃描package.json里的所有dependence,可以使用該參數(shù)進(jìn)行限制。
要使用這些參數(shù)只要在require的時(shí)候傳入即可,如require('gulp-load-plugins')({lazy:
true})。
這個(gè)插件的作用也很容易猜,它可以打印出gulpfile.js中定義的所有task,這個(gè)插件我們在重構(gòu)你的gulpfile這篇文章的最后介紹過,值得一提的是它還可以根據(jù)task的名字確定它是不是一個(gè)子task,比如帶有:、-、_的task就被認(rèn)為是子task。我一般把這個(gè)插件作為默認(rèn)的task來調(diào)用,如
1 2
|
gulp.task('default', ['help']); gulp.task('help', $.taskListing);
|
這樣,如果只執(zhí)行gulp的話就會(huì)打印出所有定義好的task,非常實(shí)用。
嚴(yán)格的說,yargs不是專門用于gulp的,它是Node中處理命令行參數(shù)的通用解決方案。只要一句代碼var
args = require('yargs').argv;就可以讓命令行的參數(shù)都放在變量args上,非常方便。它可以處理的參數(shù)類型也是多種多樣的:
- 單字符的簡單參數(shù),如傳入
-m=5或-m
5,則可得到args.m = 5。 - 多字符參數(shù)(必須使用雙連字符),如傳入
--test=5或--test
5,則可得到args.test = 5。 - 不帶值的參數(shù),如傳入
--mock,則會(huì)被認(rèn)為是布爾類型的參數(shù),可得到args.mock
= true。
除此之外,還支持很多其他類型的傳參方式,具體可參考它的文檔。
gulp-util帶有很多方便的函數(shù),其中最常用的應(yīng)該就是log了。$.util.log()支持傳入多個(gè)參數(shù),打印結(jié)果會(huì)將多個(gè)參數(shù)用空格連接起來。它與console.log的區(qū)別就是所有$.util.log的結(jié)果會(huì)自動(dòng)帶上時(shí)間前綴。另外,它還支持顏色,如$.util.log($.util.colors.magenta('123'));打印出來的123是品紅色的。其實(shí)$.util.colors就是一個(gè)chalk的實(shí)例,而chalk是專門用來處理命令行打印著色的一個(gè)工具。
grunt自身提供一個(gè)grunt-contrib-clean用來處理支持glob匹配的刪除,而del就是gulp上對(duì)應(yīng)的工具。del支持和gulp.src參數(shù)同樣的匹配,除此之外,它的第二個(gè)參數(shù)還支持一個(gè)回調(diào)函數(shù),當(dāng)刪除完成以后執(zhí)行,所以這是一個(gè)異步的刪除。常用的調(diào)用方法為:del([xxx],
callback)。
這是一個(gè)統(tǒng)計(jì)文件大小變化的工具,通常與壓縮類工具放在一起實(shí)用,比如
1 2 3 4 5 6 7 8 9 10 11 12 13
|
gulp.src('**/*.html') .pipe($.bytediff.start()) .pipe($.minifyHtml({empty: true})) .pipe($.bytediff.stop(bytediffFormatter)) .pipe(gulp.dest('dist')); function bytediffFormatter (data) { var difference = (data.savings > 0) ? ' smaller.' : ' larger.'; return data.fileName + ' went from ' + (data.startSize / 1000).toFixed(2) + ' kB to ' + (data.endSize / 1000).toFixed(2) + ' kB and is ' + formatPercent(1 - data.percent, 2) + '%' + difference; }
|
在壓縮的pipe前后加上$.bytediff.start()和$.bytediff.stop(callback),即可統(tǒng)計(jì)壓縮前后文件的變化。在callback中傳入的參數(shù)data上,可以訪問到很多變量,如文件名,變化前后的大小,變化百分比等等。
這個(gè)插件的作用很簡單,打印出stream里面的所有文件名,通常調(diào)試的時(shí)候比較需要。
這個(gè)插件也可以顧名思義:用來升級(jí)版本用的,廢話不說,直接看例子吧:
1 2 3 4
|
return gulp .src('package.json') .pipe($.bump(options)) .pipe(gulp.dest('dist'));
|
重點(diǎn)來看這里的options,我們可直接傳遞一個(gè)具體的version進(jìn)去,也可以按照Node的版本規(guī)范傳遞一個(gè)type進(jìn)去,讓其自己生成對(duì)應(yīng)的version:
version,直接傳遞要升級(jí)到的版本號(hào),如1.2.3。type,可接受的值包括下面四個(gè),倘若現(xiàn)在的版本號(hào)為1.2.3,則對(duì)應(yīng)的新版本號(hào)為:
- prerelease:
1.2.3-0 - patch:
1.2.4 - minor:
1.3.0 - major:
2.0.0
最終這個(gè)升級(jí)后的版本號(hào)會(huì)反映在package.json中,當(dāng)然,你也可以在gulp.src中傳入更多的文件(如bower.json)來替換更多的文件。
這個(gè)工具用來在壓縮后的JS、CSS文件中添加頭部注釋,你可以包含任意想要的信息,通常就是作者、描述、版本號(hào)、license等,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
function getHeader () { var pkg = require('package.json'); var template = ['/**', ' * <%= pkg.name %> - <%= pkg.description %>', ' * @authors <%= pkg.authors %>', ' * @version v<%= pkg.version %>', ' * @link <%= pkg.homepage %>', ' * @license <%= pkg.license %>', ' */', '' ].join('\n'); return $.header(template, { pkg: pkg }); }
|
這個(gè)函數(shù)將package.json中的各種信息提取出來,變成頭部注釋,只要在壓縮的pipe中調(diào)用.pipe(getHeader())即可。
stream相關(guān)
這個(gè)部分主要介紹一些跟stream操作有關(guān)的插件。
gulp-filter可以把stream里的文件根據(jù)一定的規(guī)則進(jìn)行篩選過濾。比如gulp.src中傳入匹配符匹配了很多文件,可以把這些文件pipe給gulp-filter作二次篩選,如gulp.src('**/*.js').pipe($.filter(**/a/*.js)),本來選中了所有子文件下的js文件,經(jīng)過篩選后變成名為a的子文件夾下的js文件。那有人要問了,為什么不直接將需要的篩選傳入gulp.src,干嘛要多篩選一步呢?這里面有兩種情況:
gulp.src與$.filter中間可能需要?jiǎng)e的處理,比如我對(duì)所有文件做了操作1以后,還需要篩選出一部分做操作2。- 第二種情況就要談到gulp-filter的另外一個(gè)特性:篩選之后還可以restore回去。比如我對(duì)所有文件做了操作1,篩選了一部分做操作2,最后要把所有的文件都拷貝到最終的位置。代碼如下:
1 2 3 4 5 6 7
|
var filter = $.filter('**/a/*.js'); gulp.src('**/*.js') .pipe(action1()) .pipe(filter) .pipe(action2()) .pipe(filter.restore()) .pipe(gulp.dest('dist'))
|
可以看到,如果沒有restore這個(gè)操作,那么拷貝到最終位置的文件將只包含被過濾出來的文件,這樣一restore,所有的文件都被拷貝了。
gulp-flatten非常實(shí)用,可能知道別的庫中flatten函數(shù)的同學(xué)已經(jīng)猜到它是干嘛的了。比如gulp.src('**/*.js')匹配了很多文件,包括a/b/c.js,d/e.js,f/g/h/i/j/k.js,l.js,這些文件的層級(jí)都不一樣,一旦我們將這個(gè)文件pipe給$.flatten(),則所有的文件夾層級(jí)都會(huì)去掉,最終的文件將是c.js,e.js,k.js,l.js,在一些場景下還是非常有用的。
這個(gè)插件的作用簡單來說就是一旦pipe中的某一steam報(bào)錯(cuò)了,保證下面的steam還繼續(xù)執(zhí)行。因?yàn)閜ipe默認(rèn)的onerror函數(shù)會(huì)把剩下的stream給unpipe掉,這個(gè)插件阻止了這種行為。那它一般用于哪種場景呢?比如,代碼每次build之前要跑一遍jshint和jscs來確保所有代碼都符合規(guī)范,但一旦某些代碼不符合規(guī)范,整個(gè)build流程就會(huì)停止,這個(gè)時(shí)候就需要gulp-plumber出場了。如:
1 2 3 4 5 6 7 8
|
gulp.task('build', ['jslint', 'xxxx']); gulp.task('jslint', function () { return gulp .src(config.js.all) .pipe($.plumber()) .pipe($.jshint()) .pipe($.jscs()); });
|
這樣,一旦jshint或jscs報(bào)錯(cuò),整個(gè)build流程還是可以繼續(xù)走下去的,而且gulp-plumber會(huì)給出一個(gè)報(bào)錯(cuò)提醒我們:
1 2 3 4
|
[16:52:36] Plumber found unhandled error: Error in plugin 'gulp-jshint' Message: JSHint failed for: xxxx.js
|
這個(gè)插件的功能也很簡單,可以條件性的添加stream,如.pipe($.if(flag,
action1())),則只會(huì)在flag變量為true時(shí)才會(huì)將action1()添加到stream中去。其實(shí)不用這個(gè)插件也可以達(dá)到類似的效果,那就是gulp-util里有一個(gè)函數(shù)叫做noop(),也就是no
operation,這個(gè)函數(shù)其實(shí)是返回一個(gè)什么都不干的空stream。利用這個(gè)函數(shù)我們可以這么寫:.pipe(flag ? action1() : $.util.noop()),與上例的效果是一樣的。
一個(gè)gulp的task只能返回一個(gè)stream,但有的時(shí)候有這么一種情景:有兩類文件,它們的原始位置和處理后的位置都是不同的,但它們的處理流程相同。由于gulp.src和gulp.dest的參數(shù)不同,我們就需要寫兩個(gè)task來分別完成這個(gè)任務(wù),一方面略顯重復(fù),另一方面邏輯上來講這兩個(gè)task本來就是處理同樣的事情的。這種情況就需要merge-stream登場了,它的作用就是將多個(gè)stream合成一個(gè)返回。比如下面這個(gè)例子:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
var merge = require('merge-stream'); gulp.task('jade', function () { var stream1 = jade(src1, dest1); var stream2 = jade(src2, dest2); return merge(stream1, stream2); }); function jade (src, dest) { return gulp .src(src) .pipe($.jade()) .pipe(gulp.dest(dest)); }
|
可以看到,處理的流程被提取出來放入一個(gè)函數(shù),它接受兩個(gè)參數(shù),分別是src和dest。然后在task中直接調(diào)用這個(gè)函數(shù)生成兩個(gè)stream,然后返回merge-stream合并后的結(jié)果。
gulp里的task都是異步并發(fā)執(zhí)行的,有的時(shí)候我們需要一連串的task按順序執(zhí)行,這時(shí)就需要run-sequence登場了。它的調(diào)用很簡單:runSequence('task1',
'task2', ['task3', 'task4'], 'task5'),這里的task都是gulp定義好的task名稱,task1完成后才會(huì)執(zhí)行task2,以此類推。注意到task3和task4被放在中括號(hào)里了,這表明,task3和task4可以并發(fā)執(zhí)行的,但兩個(gè)都執(zhí)行完后才會(huì)執(zhí)行task5。這里要說明的是,每個(gè)task要么返回一個(gè)stream,即return
gulp.src().pipe().pipe(),要么支持回調(diào)函數(shù),即gulp.task('task1', function (done) { action1(done);
}),滿足了這兩點(diǎn)才能保證正常的執(zhí)行順序,因?yàn)檫@是gulp對(duì)異步task的基本要求。
inject相關(guān)
這個(gè)部分主要介紹一些將JS/CSS自動(dòng)插入到HTML的相關(guān)插件。
wiredep就是wire dependence的意思,它的作用就是把bower.json中聲明的dependence自動(dòng)的包含到HTML中去。要插入文件,wiredep需要解決兩個(gè)問題:
- 插入的位置:wiredep通過識(shí)別HTML中的注釋來識(shí)別插入位置,如
1 2 3 4
|
<!-- bower:css --> <!-- endbower --> <!-- bower:js --> <!-- endbower -->
|
不同類型的文件被插入到不同的區(qū)塊。
- 插入什么文件:要插入的文件列表自然來自
bower.json,每個(gè)bower安裝的依賴庫,根目錄下邊都有一個(gè)自己的bower.json文件,其中的main字段指明了使用這個(gè)庫需要包含的文件,wiredep最終包含的文件列表就來自這個(gè)字段。有些情況下,庫自身的bower.json的main字段可能會(huì)多包含文件或少包含文件,如果想要定制這個(gè)列表,則可以在自己的bower.json中使用overrides字段,如下面的代碼覆蓋了mdi這個(gè)庫的main字段。
1 2 3 4 5 6 7
|
"overrides": { "mdi": { "main": [ "css/materialdesignicons.css" ] } },
|
wiredep插件支持很多參數(shù),常用的主要有兩個(gè):
- bowerJson:指定
bower.json的內(nèi)容,注意這個(gè)字段不是bower.json文件的位置,這個(gè)參數(shù)需要使用require后的結(jié)果賦值:require('bower.json')。 - directory:指定存放bower安裝后的依賴包的路徑,通常是bower_components。注意最終插入到HTML中的文件列表的路徑是index.html文件相對(duì)于本文件夾的相對(duì)路徑。
使用wiredep也比較簡單,直接把它傳入到stream中即可,如gulp.src('index.html').pipe(wiredep(options))。
這個(gè)插件的作用與wiredep類似,不同的是可以自己任意指定需要插入文件的列表。它同樣是利用注釋來尋找插入的位置,它識(shí)別的默認(rèn)注釋為<!--
inject:js -->,但更加智能:
- 支持各種模板語言:可以根據(jù)
gulp.src指定的源文件自動(dòng)識(shí)別注釋和插入內(nèi)容,除了支持HTML外,還支持jade、haml等。若源為jade文件,則識(shí)別的注釋為//-
inject:js,插入的內(nèi)容為:script(src="<filename>.js")。 - 配置非常靈活:
- name:默認(rèn)識(shí)別的注釋標(biāo)簽格式為
<!-- name:ext -->,這里的name默認(rèn)值就是“inject”,而ext的默認(rèn)值是要插入的文件的擴(kuò)展名。那么name屬性可配置意味著可以添加自定義的插入?yún)^(qū)塊,如<!--
production:js -->,這個(gè)標(biāo)簽可以只插入生產(chǎn)環(huán)境需要包含的JS文件。 - starttag和endtag:支持自定義需要識(shí)別的注釋內(nèi)容。
- addPrefix和addSuffix:支持在插入文件的路徑上自定義前綴、后綴。
- relative:指定插入文件的路徑是否為相對(duì)路徑。
- ingorePath:指定插入文件的路徑前面會(huì)忽略掉指定的路徑。
- read:這個(gè)參數(shù)通常給false,不需要真正的去讀取文件。
這個(gè)插件的使用場景通常是,我們需要index里有多個(gè)區(qū)塊,比如上面name的例子,只有當(dāng)為production環(huán)境編譯的時(shí)候才去包含相關(guān)的文件。
這三個(gè)工具之所以放在一起講,是因?yàn)樗鼈円话愣际且黄鹗褂玫摹K鼈円鉀Q什么問題呢?通過上面的wiredep也好,gulp-inject也好,插入了一堆JS、CSS文件到HTML中,一旦部署到生產(chǎn)環(huán)境,這么多文件必然是要合并壓縮的。光是壓縮還不夠,為了解決緩存問題,每次合并壓縮后要給最終的文件加hash,這樣每次文件內(nèi)容一變動(dòng),hash也會(huì)跟著變動(dòng),就不存在瀏覽器依然使用緩存的老文件的問題。這樣得到最終的文件以后,肯定還要將這個(gè)文件替換回HTML中去,一大堆的script和link標(biāo)簽替換成最終合并壓縮帶hash的版本。
前面啰啰嗦嗦的一大堆工作就是這三個(gè)插件要解決的問題了。首先,gulp-useref根據(jù)注釋將HTML中需要合并壓縮的區(qū)塊找出來,對(duì)區(qū)塊內(nèi)的所有文件進(jìn)行合并。注意:它只負(fù)責(zé)合并,不負(fù)責(zé)壓縮!所以合并出來的文件我們要自行壓縮,壓縮以后調(diào)用gulp-rev負(fù)責(zé)在文件名后追加hash。最后調(diào)用gulp-rev-replace負(fù)責(zé)把最終的文件名替換回HTML中去。扯了大半天,還是直接上例子吧。先來看看HTML中的注釋:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
<!-- build:css static/styles/lib.css --> <!-- bower:css --> <!-- endbower --> <!-- endbuild --> <!-- build:css static/styles/app.css --> <!-- inject:css --> <!-- endinject --> <!-- endbuild --> <!-- build:js static/js/lib.js --> <!-- bower:js --> <!-- endbower --> <!-- endbuild --> <!-- build:js static/js/app.js --> <!-- inject:js --> <!-- endinject --> <!-- endbuild -->
|
gulp-useref識(shí)別的就是build開頭的注釋,build后面首先跟的是類型擴(kuò)展名,然后后面的路徑就是build區(qū)塊中的所有文件進(jìn)行合并后的文件路徑,這個(gè)相對(duì)路徑是相對(duì)于這個(gè)HTML的路徑。上面的例子中我們用build區(qū)塊把bower和inject進(jìn)來的文件包起來,這些文件就可以被gulp-useref合并了。再來看gulp中useref相關(guān)task的定義:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
var assets = $.useref.assets({searchPath: 'app/src/'}); var cssFilter = $.filter('**/*.css'); var jsAppFilter = $.filter('**/app.js'); var jslibFilter = $.filter('**/lib.js'); return gulp .src('index.html') .pipe(assets) .pipe(cssFilter) .pipe($.csso()) .pipe(cssFilter.restore()) .pipe(jsAppFilter) .pipe($.uglify()) .pipe(getHeader()) .pipe(jsAppFilter.restore()) .pipe(jslibFilter) .pipe($.uglify()) .pipe(jslibFilter.restore()) .pipe($.rev()) .pipe(assets.restore()) .pipe($.useref()) .pipe($.revReplace()) .pipe(gulp.dest('dist'));
|
首先一上來,先調(diào)用$.useref.assets()函數(shù),這個(gè)函數(shù)返回一個(gè)stream,包含已經(jīng)合并后的文件??梢試L試在第9行后面加上前面介紹過的gulp-print插件.pipe($.print()),打印出stream里的文件,發(fā)現(xiàn)就是前面HTML中4個(gè)build注釋塊后面的4個(gè)文件。注意這里調(diào)用的時(shí)候跟了一個(gè)searchPath的參數(shù),它的用處就是指定從哪個(gè)路徑開始尋找build區(qū)塊底下的文件。比如build區(qū)塊底下有這么一行<script
src="static/js/a.js"></script>,那最終gulp-useref將從這個(gè)路徑app/src/static/js/a.js找到這個(gè)文件。第3到5行定義了3個(gè)filter,這主要是為了后面壓縮準(zhǔn)備的。下面正式看stream的pipe流程。先選出要處理的HTML文件,然后調(diào)用剛才得到的assets得到合并后的4個(gè)文件,第10到12行篩選出合并后的CSS文件進(jìn)行壓縮(壓縮類插件下篇文章再講),第13到16行篩選出app.js進(jìn)行壓縮,第17到19行篩選出lib.js進(jìn)行壓縮。之所以要區(qū)別對(duì)待app.js和lib.js,是因?yàn)閍pp.js是我們自己寫的代碼,壓縮后要加上header(第15行,使用前面介紹過的gulp-header插件),而lib.js是第三方的各種庫,直接壓縮即可。后面調(diào)用gulp-rev給壓縮后的4個(gè)文件加hash,然后調(diào)用assets.restore()將src源換回HTML文件,這是為了后面調(diào)用$.useref(),因?yàn)?code style="font-family:"PT Mono",Consolas,Monaco,Menlo,monospace; font-size:1em; background:rgb(238,238,238); padding:0px 0.3em; white-space:pre-wrap; word-break:break-word">$.useref()做替換的src源是HTML文件,同樣后面調(diào)用gulp-rev-replace將帶hash的文件替換回HTML,它要求的src源也必須是HTML文件。這里的順序很重要,因?yàn)檫@幾個(gè)插件接受的源不一樣,gulp-rev接受的是JS、CSS文件,而gulp-useref和gulp-rev-replace接受的是HTML。還有一個(gè)問題:gulp-rev-replace是怎么知道gulp-rev進(jìn)行hash前后的文件名對(duì)應(yīng)關(guān)系呢?其實(shí)gulp-rev會(huì)生成一個(gè)manifest的文件,內(nèi)容是類似下面的JSON:
1 2 3 4
|
{ "static/styles/lib.css": "static/styles/lib-d41d8cd98f.css" "static/js/lib.js": "static/js/lib-273c2cin3f.js" }
|
當(dāng)然這個(gè)文件默認(rèn)是不會(huì)生成在文件系統(tǒng)里的,可以通過.pipe($.rev.manifest())將這個(gè)文件保存到本地。有了這個(gè)文件,gulp-rev-replace甚至可以脫離gulp-rev獨(dú)立工作哦!
好了,這篇就到這里,還有好多工具沒介紹到,留著給下篇吧。
|