Gulp.js深入講解
赤溫(開發工程師)說:這東西(Gulp.js與Grunt)差距不大,社群才是王道!
英布(高階前端開發工程師)說:一個是以配置的方式,一個是以程式設計的方式,感覺各有千秋吧,複雜流程還是使用程式設計的方式比較好,簡單的那就隨便了。
誠冉(前端開發工程師)說:曾經苦於將grunt的各個工具黏合在一起的過程,grunt這個東西生出來就不是給小專案用的,必須是一定規模的並且持續更新的專案,才捨得花這麼大力氣來“製造工具”。一點也不誇張,雖然grunt的元件越來越多,但概念就像import一樣,稱不上是框架,頂多是一種合作機制。Nodejs的世界裡,好像不跟stream和pipe沾點邊的概念都不好用似的,現在寫Web服務框架,基本都跟connect一個風格,req和res一層一層的流經各個handler。剛一看到glup下意識的覺得有點俗氣了,但是想想Unix的哲學,標準化的輸入輸出,不需要過多的元資訊描述,簡單實用,元件連線起來產生的巨大威力,任何人都無法拒絕!
獵隼(資深前端開發工程師)說:基於node.js的工具有一點讓人深惡痛絕:會建立一個資料夾,而且裡邊的檔案有大量的冗餘。這對專案檔案是一種汙染,很有可能讓一些不明真相的工具把這些程式碼也一起搜尋了,比如一些查詢替換程式。比較喜歡ruby的管理方式,需要的包在本機只需要安裝一次,而且不會像node那樣建立一個資料夾
Gulp.js處理構建任務如下圖:
Grunt處理任務:
理解Gulp.js的核心設計
streaming很容易聯想到河流,河流有哪些特徵?流動、源源不斷、具有方向、有起點、有終點,而這些特徵也是Gulp.js構建程式碼的特點。
Gulp.js官網上的簡介是The streaming build system
流(stream)的概念來自unix,核心思想是do one thing well,一個大工程系統應該是由各個小且獨立的管子連線而成,如誠冉同學所說:Unix的哲學,標準化的輸入輸出,不需要過多的元資訊描述,簡單實用,元件連線起來產生的巨大威力,任何人都無法拒絕!,如下圖:
我們以經典的Nodejs讀取檔案邏輯來說明Streams與傳統方式的差異。
使用fs模組讀取一個json檔案
傳統的方式:
var dataJson, fs;
fs =require('fs');
dataJson ='./gallery-db/component-info.json';
exports.all =function(req, res){
fs.readFile(dataJson,function(err, data){if(err){
console.log(err);}else{
res.end(data);}});};
fs.readFile()是將檔案全部讀取放進記憶體,然後觸發回撥,這種方式存在二方面的瓶頸。
1.讀取大檔案時容易造成記憶體洩漏
比如一個上傳視屏的服務,面對少者二三百M,多者七八G的檔案,明顯這種方式不可行,所以我們需要分斷上傳,如果使用傳統方式,就會發現非常蛋疼,估計沒人想這麼幹….
2.深惡痛絕的回撥問題
這是我自己寫NodeJs程式碼最深惡痛絕的地方,在提供大的服務時,比如讀取4-5個檔案,回撥層層巢狀,可讀性和維護性非常大,噁心指數陡增。
而且每次你都要對err進行處理:
if(err){
console.log(err);}else{//...}
程式碼囉嗦,不夠簡練。koa已經解決了這個問題,NodeJs的發展史就是跟回撥鬥爭史啊。
流的方式:
var dataJson, fs;
fs =require('fs');
dataJson ='./gallery-db/component-info.json';
exports.all =function(req, res){var stream;
stream = fs.createReadStream(dataJson);
stream.on('error',function(err){return console.log(err);});
stream.on('data',function(chunk){return console.log('got %d bytes of data', chunk.length);});return stream.pipe(res);};
fs.createReadStream()建立一個readable streams(只讀的流),請看《stream-handbook》中的readable streams部分,readable streams是有狀態的,比如可以監聽其data事件,讀取資料後會觸發,比如error事件,讀取資料失敗後會觸發。
stream.pipe(res),pipe方法是流物件的核心方法,這句程式碼可以理解為,res(結果集,實際上是一個writeable streams,寫流)物件接收從stream而來的資料,並予以處理輸出。所以Gulp.js中的pipe()方法並不是gulp的方法,而是流物件的方法。pipe()返回的是res的返回的物件。
上述程式碼會把component-info.json的內容打印出來。
流的種類有:readable streams、writeable streams(比如Gulp.js中的gulp.dest(‘./build’)f方法)、transform(明河很不喜歡這個詞,詞不達意,稱之為through更為合適,表示這是讀/寫流,後面會詳細講解)streams,duplex streams(duplex可以理解為迴圈的流,比如伺服器端和客戶端的實時通訊,由於與Gulp.js無關,不展開講)。
如何開發個Gulp.js外掛?
所有的Gulp.js外掛基本都是through(後面不再使用transform這個詞) streams,即是消費者(接收gulp.src() 傳遞出來的資料,然後進行處理加工處理),又是生產者(將加工後的資料傳遞出去),我們以gulp-clean為例(這個外掛用於清理目錄和檔案)。
先看下gulp-clean的API用法:
var gulp =require('gulp');var clean =require('gulp-clean');
gulp.task('default',function(){
gulp.src('app/tmp').pipe(clean());});
非常簡單的程式碼,目的是殘忍地將tmp目錄幹掉。
gulp-clean的原始碼傳送門。
引入依賴模組
var rimraf =require('rimraf');var es =require('event-stream');var gutil =require('gulp-util');var path =require('path');
rimraf 模組用於刪除目錄。
gulp-clean外掛使用了個through streams的包裝模組:event-stream,用於簡化streams呼叫的api,明河推薦使用through2,會更好用。
path 模組就不說了,大家太熟悉了。
定義供外部呼叫外掛方法
module.exports =function(options){return es.map(function(file, cb){//具體業務邏輯});};
es.map()會建立一個through streams,我們只要關注寫業務邏輯就好,不用關注如何建立streams。
file為上游讀流產生的檔案資料,cb是核心方法用於向下遊傳遞處理後的資料。
return cb(null, file);
具體業務程式碼
// 獲取檔案絕對路徑var filepath = file.path;var cwd = file.cwd;var relative = path.relative(cwd, filepath);// 防止路徑錯誤if(!(relative.substr(0,2)==='..')&& relative !==''||(options ?(options.force &&typeof options.force ==='boolean'):false)){//刪除目錄
rimraf(filepath,function(error){if(!error){return cb(null, file);}else{return cb(newError('Unable to delete "'+ filepath +'" file ('+ error.message +').'), file);}});//列印錯誤訊息}elseif(relative ===''){
gutil.log('gulp-clean: Cannot delete current working directory. ('+ filepath +')');return cb(null, file);}//列印錯誤訊息else{
gutil.log('gulp-clean: Cannot delete files outside the current working directory. ('+ filepath +')');return cb(null, file);}
程式碼非常簡單,獲取檔案(目錄)路徑,呼叫rimraf()刪除之即可。
一個簡單的Gulp.js外掛就完成了,so easy!
總結
Gulp.js的使用和外掛的開發都很簡單,當然裡面還有很多細節,明河就不展開講了,請看Gulp.js的官方文件。