gulp 前端自動化實踐

艾倫先生發表於2017-12-14

gulp 簡介

gulp是一個基於Nodejs的自動化任務執行器,能自動化地完成javascript/coffee/sass/less/html/image/css 等檔案的的測試、檢查、合併、壓縮、格式化、瀏覽器自動重新整理、部署檔案生成,並監聽檔案在改動後重復指定的這些步驟。

在實現上,它借鑑了Unix作業系統的管道(pipe)思想,前一級的輸出,直接變成後一級的輸入,使得在操作上非常簡單。

gulp的核心設計

核心詞是streaming(流動式)。Gulpjs的精髓在於對Nodejs中Stream API 的利用。

流(stream)的概念來自Unix,核心思想是do one thing well。一個大工程系統應該由各個小且獨立的管子連線而成。

我們以經典的Nodejs讀取檔案邏輯來說明stream和傳統方式的差異。使用fs模組讀取一個json檔案,傳統的方式程式碼如下

var dataJson, fs; 
fs = require('fs'); 
dataJson = 'public/json/test.json'; 
exports.all = function(req, res) { 
  fs.readFile(dataJson,function(err, data){ 
    if (err) { 
      console.log(err); 
    } else { 
      res.end(data); 
    } 
  }); 
};
複製程式碼

fs.readFile()是將檔案全部讀進記憶體,然後觸發回撥。有兩方面的瓶頸。

  • 讀取大檔案時容易造成記憶體洩露
  • 深惡痛絕的回撥大坑

下面我們看看使用流的方式

var fs = require('fs');
var readStream = fs.createReadStream('data.json');
var writeStream = fs.createWriteStream('data1.json');

readStream.on('data', function(chunk) { // 當有資料流出時,寫入資料
    if (writeStream.write(chunk) === false) { // 如果沒有寫完,暫停讀取流
        readStream.pause();
    }
});

writeStream.on('drain', function() { // 寫完後,繼續讀取
    readStream.resume();
});

readStream.on('end', function() { // 當沒有資料時,關閉資料流
    writeStream.end();
});
複製程式碼

或者直接使用

fs.createReadStream('/path/to/source').pipe(fs.createWriteStream('/path/to/dest'));
複製程式碼

先建立一個有狀態的只讀的流,然後呼叫stream.pipe(res)。pipe方法是stream的核心方法。這句話的程式碼可以理解為res物件接收從stream來的資料,並予以處理輸出。所以gulppipe()並不是gulp的方法,而是流物件的方法。切記:pipe()返回的res的返回的物件。

gulp的安裝與使用

gulp 前端自動化實踐

  • 全域性安裝

      npm install gulp -g
    複製程式碼
  • 切換到專案目錄執行npm初始化,生成pakeage.json

      npm init
    複製程式碼
  • 開發依賴安裝

      npm install gulp --save-dev //專案內整合gulp
      npm install gulp-name --save-dev//專案內整合gulp第三方外掛
    複製程式碼
  • 專案根目錄下建立配置檔案

      gulpfile.js
    複製程式碼
  • 在gulpfile.js中引用gulp

      var gulp = require('gulp')
      var name = require('gulp-name')  //外掛呼叫
    複製程式碼
  • 在gulpfile.js中配置gulp任務

      gulp.task('task_name', function () {
          return gulp.src('file source path')
              .pipe(...)
              .pipe(...)
              // till the end
              .pipe(...);
      });
    複製程式碼

gulp task_name可以來執行不同的任務。這個task_name可以是自定義的,也可以是預設的任務(task_name為‘default’),預設任務執行的時候,可以不用拼上task_name ,直接使用gulp來執行

//例子1(預設):
gulp.task('default',function() {
    console.log('我是default')
});
//執行結果:
$ gulp
[21:54:33] Using gulpfile ~/Desktop/nodeJS/gulp/gulpfile.js
[21:54:33] Starting 'default'...
我是default
[21:54:33] Finished 'default' after 120 μs

//例子2(自定義)
gulp.task('task1',function () {
    console.log('我是task1')
})
//執行結果:
$ gulp task1  //後面跟上自定義的任務名稱
[21:58:00] Using gulpfile ~/Desktop/nodeJS/gulp/gulpfile.js
[21:58:00] Starting 'task1'...
我是task1
[21:58:00] Finished 'task1' after 121 μs

//例子3(複合)
var gulp = require('gulp')

gulp.task('task', function(){
    console.log('hello,i am task')
})
gulp.task('task2', function(){
    console.log('hello,i am task2')
})
gulp.task('task3', function(){
    console.log('hello,i am task3')
})
gulp.task('task4', function(){
    console.log('hello,i am task4')
})
gulp.task('task5', function(){
    console.log('hello,i am task5')
})
gulp.task('default', ['task','task2','task3','task4','task5'], function(){
    console.log('hello,i am default')
})

//執行結果:
D:\test\gulpTest>gulp   //先按順序執行陣列內依賴項,後執行預設
[10:52:56] Using gulpfile D:\test\gulpTest\gulpfile.js
[10:52:56] Starting 'task'...
hello,i am task
[10:52:56] Finished 'task' after 140 μs
[10:52:56] Starting 'task2'...
hello,i am task2
[10:52:56] Finished 'task2' after 59 μs
[10:52:56] Starting 'task3'...
hello,i am task3
[10:52:56] Finished 'task3' after 52 μs
[10:52:56] Starting 'task4'...
hello,i am task4
[10:52:56] Finished 'task4' after 56 μs
[10:52:56] Starting 'task5'...
hello,i am task5
[10:52:56] Finished 'task5' after 54 μs
[10:52:56] Starting 'default'...
hello,i am default
[10:52:56] Finished 'default' after 55 μs
複製程式碼

gulp的核心API

gulp.task(taskName, deps, callback)

name:任務名稱,不能包含空格; deps: 依賴任務,依賴任務的執行順序按照deps中宣告順序,先於taksName執行; callback,指定任務要執行的一些操作,支援非同步執行。

下面提供幾個特殊用法

  • 接受一個callback

      // 在 shell 中執行一個命令
      var exec = require('child_process').exec;
      gulp.task('jekyll', function(cb) {
        // 編譯 Jekyll
        exec('jekyll build', function(err) {
          if (err) return cb(err); // 返回 error
          cb(); // 完成 task
        });
      });
    複製程式碼
  • 返回一個stream

      gulp.task('somename', function() {
        var stream = gulp.src('client/**/*.js')
          .pipe(minify())
          .pipe(gulp.dest('build'));
        return stream;
      });
    複製程式碼
  • 返回一個promise

      var Q = require('q');
    
      gulp.task('somename', function() {
        var deferred = Q.defer();
      
        // 執行非同步的操作
        setTimeout(function() {
          deferred.resolve();
        }, 1);
      
        return deferred.promise;
      });
    複製程式碼

gulp.src(globs,options)

該函式通過一定的匹配模式,用來取出待處理的原始檔物件

globs作為需要處理的原始檔匹配符路徑

  • src/a.js:指定具體檔案;

  • *:匹配所有檔案 例:src/*.js(包含src下的所有js檔案);

  • **:匹配0個或多個子資料夾 例:src/**/*.js(包含src的0個或多個子資料夾下的js檔案);

  • {}:匹配多個屬性 例:src/{a,b}.js(包含a.js和b.js檔案) src/*.{jpg,png,gif}(src下的所有jpg/png/gif檔案);

  • “!”:排除檔案 例:!src/a.js(不包含src下的a.js檔案);

      gulp.src(['src/js/*.js','!src/js/test.js'])
              .pipe(gulp.dest('dist'))
    複製程式碼

options包括如下內容

  • options.buffer: boolean,預設true,設定為false將返回file.content的流並且不快取檔案,處理大檔案很好用

  • options.read: boolean,預設true,是否讀取檔案

  • options.base: 設定輸出路徑以某個路徑的某個組成部分為基礎向後拼接

      gulp.src('client/js/**/*.js')
        .pipe(minify())
        .pipe(gulp.dest('build'));  // 寫入 'build/somedir/somefile.js'
      
      gulp.src('client/js/**/*.js', { base: 'client' })
        .pipe(minify())
        .pipe(gulp.dest('build'));  // 寫入 'build/js/somedir/somefile.js'
    複製程式碼

dest()

該函式用來設定目標流的輸出,一個流可以被多次輸出。如果目標資料夾不存在,則建立之。檔案被寫入的路徑是以所給的相對路徑根據所給的目標目錄計算而來。類似的,相對路徑也可以根據所給的 base來計算

gulp.task('sass', function(){
  return gulp.src('public/sass/*.scss')
    .pipe(concat('style1.js'))
    .pipe(gulp.dest('public/sass/'))//目錄下生成style1.js
    .pipe(sass(
      {'sourcemap=none': true}
    ))
    .pipe(concat('style.css'))
    .pipe(gulp.dest('public/sass/'))//目錄下生成style.css

});
複製程式碼

pipe()

該函式使用類似管道的原理,將上一個函式的輸出傳遞到下一個函式的輸入

watch()

該函式用來監聽原始檔的任何改動。每當更新監聽檔案時,回撥函式會自動執行。

注意:別忘記將watch任務放置到default任務中

gulp.task('watch',function(){
  gulp.watch('public/sass/*.scss',['sass'],function(event){
   console.log(event.type);//added或deleted或changed
   console.log(event.paht);//變化的路徑
  });
 	
});
gulp.task('default', ['sass','watch']);
複製程式碼

run()

該函式能夠儘可能的並行執行多個任務,並且可能不會按照指定的執行順序

gulp.task('end',function(){
    gulp.run('task1','task3','task2');
});
複製程式碼

gulp 工作流

下面展示一下實際專案中,gulp的前端自動化流程。

配置gulpfile.js中的目錄

我們新建一個檔案,名為gulpfileConfig.js來管理靜態資源的目錄

var src = 'public';//預設目錄資料夾

module.exports = {
  sass: {
    src : src + '/sass/*.scss',
    dest : src + '/css/'
  },
  css: {
    src : src + '/css/*.css',
    dest:'dist/css'
  },
  js: {
    src : src + '/js/*.js',
    dest: 'dist/js'
  },
  images: {
    src : src + '/images/**/*',
    dest : 'dist/images'
  },
  zip: {
    src : './**/*',
    dest : './release/'
  }
};
複製程式碼

然後,在gulpfile.js中引入,並使用

var gulpConfig = require('./gulpfileConfig');
var cssConfig = gulpConfig.css;

gulp.src(cssConfig.src)
複製程式碼

下文中預設使用gulpfileConfig.js中的配置項

根據引數配置構建平臺

gulp.task('set-platform', function() {
  console.log('當前構建平臺:' + gulp.env.platform);//當前構建平臺:web
  gulp.env.platform = gulp.env.platform || 'web';

  // 根據傳進來的平臺引數,設定專案public/目錄下的系統icon
  gulp.src('./public/' + gulp.env.platform + '.ico')
    .pipe(rename('favicon.ico'))
    .pipe(gulp.dest('./public'));

  // 根據傳進來的平臺引數,設定平臺相關的主scss檔案(包含對應平臺的主色調)
  gulp.src('./public/sass/mixins/base-' + gulp.env.platform + '.scss')
    .pipe(rename('base.scss'))
    .pipe(gulp.dest('./public/sass/mixins'));

  // 根據傳進來的平臺引數,設定對應平臺的配置檔案
  return gulp.src('./config/' + gulp.env.platform + '.js')
    .pipe(rename('index.js'))
    .pipe(gulp.dest('./config'));
});

//構建入口,傳入特定的引數
gulp publish --platform web
複製程式碼

從上文可以看出,我們可以通過gulp在構建開始時通過不同的引數,給專案變更成對應的檔案內容,是不是很厲害?

gulp && clean

清理構建生成的資料夾。一般在構建開始時清理掉上一次生成的歷史檔案。

var clean = require('gulp-clean');

gulp.task('clean-dist', function(){
  return gulp.src('dist')
    .pipe(clean());
});
複製程式碼

gulp && sass

編譯sass。一般都是將編譯生成好的CSS檔案輸出到專案CSS目錄,用於下一步進行CSS的自動化操縱。

var sass = require('gulp-ruby-sass');
var sassConfig = gulpConfig.sass;

gulp.task('sass', function () {
  return sass(sassConfig.src)
    .pipe(gulp.dest(sassConfig.dest))
});
複製程式碼

gulp && css

對目標資料夾內的css檔案,進行壓縮,新增md5字尾的操作,同時生成對映檔案,並輸出到指定資料夾內。這個對映檔案,會自動替換掉html檔案的標頭檔案中,引用的這個加了md5字尾的css檔案。

var minifycss = require('gulp-minify-css');			//壓縮CSS
var rev = require('gulp-rev');//對檔名加MD5字尾

gulp.task('publish-css',function(){
    return gulp.src(cssConfig.src)
        .pipe(minifycss())
        .pipe(rev())
        .pipe(gulp.dest(cssConfig.dest))			//輸出到檔案本地
        .pipe(rev.manifest())
        .pipe(gulp.dest(cssConfig.dest));
});
複製程式碼

md5字尾的原因是為了解決瀏覽器快取的問題:希望瀏覽器能夠快取資原始檔,但是有希望當檔案內容變化了的時候,瀏覽器能夠自動替換老檔案。怎麼讓瀏覽器檢測到檔案的變化呢?簡單,對檔案大小進行md5,生成的隨機串拼到檔案後就行啦,這樣檔案內容如果不變的話,瀏覽器依舊快取;如果檔案有變動,md5值發生改變,檔名變化,瀏覽器就引用新的檔案內容。

gulp && js

類似於css的自動化操作,對js檔案進行錯誤檢查並輸出、混淆、生成md5字尾、生成sourceMap檔案並輸出

var sourcemaps = require('gulp-sourcemaps');
var uglify = require('gulp-uglify');//壓縮js
var jsConfig =gulpConfig.js;
var jshint = require('gulp-jshint');//js 程式碼檢查
var rev = require('gulp-rev');
var revCollector = require('gulp-rev-collector');

function jsProcess(toAddComment){
    return gulp.src(jsConfig.src)
        .pipe(jshint('.jshintrc'))//錯誤檢查
        .pipe(jshint.reporter('default'))//對錯誤進行輸出
        .pipe(sourcemaps.init())
        .pipe(uglify())
        .pipe(rev())
        .pipe(sourcemaps.write('/maps',{addComment: toAddComment,sourceMappingURLPrefix: '/js'}))
        .pipe(gulp.dest(jsConfig.dest))
        .pipe(rev.manifest())
        .pipe(gulp.dest(jsConfig.dest));
}

// 用於新增map標記(本地開發使用)
gulp.task('publish-js-addMap', function (){
    return jsProcess(true);
});

// 不新增map標記(線上版本)
gulp.task('publish-js', function (){
    return jsProcess(false);
});
複製程式碼

注1 使用rev()這個工具方法,會對某個檔案,比如ajax.js進行md5加密,在檔名後面拼上md5串,變成ajax-fba6bf63c7.js,而rev.manifest()則會在rev-manifest檔案裡,記錄這組對應關係,

{ "ajax.js": "ajax-fba6bf63c7.js" }

_

注2 sourcemaps.write('/maps',{addComment: toAddComment,sourceMappingURLPrefix: '/js'})這句程式碼,會給每個加密的js檔案生成用於解密的map檔案,同時在檔案末尾標註map檔案的位置

var ajax={init:function(){return window.ActiveXObject?new ActiveXObject("... //# sourceMappingURL=/js/maps/ajax-fba6bf63c7.js.map

gulp && image

對所有的圖片進行md5,生成對映檔案

gulp.task('publish-image',function(){
  return gulp.src('public/images/**/*.{jpg,png,gif}')
    .pipe(rev())
    .pipe(gulp.dest('dist/images'))
    .pipe(rev.manifest())
    .pipe(gulp.dest('dist/images'));
});
複製程式碼

gulp && html

將靜態檔案裡的所有檔案引用,根據上文中生成好的對映檔案,都替換成md5字尾的格式。

gulp.task('publish-view', function () {
    return gulp.src(['dist/**/*.json','views/**/*.html'])
        .pipe(revCollector({
            replaceReved:true
        }))
        .pipe(gulp.dest('dist/views'));
});
複製程式碼

比如

<script src="/js/format.js"></script>
複製程式碼

替換成

<script src="/js/format-f02584610e.js"></script>
複製程式碼

gulp && css->image

替換css檔案中引用的圖片名為md5字尾形式。

gulp.task('replace-image-inCss', function() {
  return gulp.src(['dist/images/*.json','dist/css/*.css'])
    .pipe(revCollector({
      replaceReved:true
    }))
    .pipe(gulp.dest('dist/css'));
});
複製程式碼

gulp && copy

檔案複製操作

gulp.task('copy-other-files', function () {
  return gulp.src('public/favicon.ico')
    .pipe(gulp.dest('dist'));
});
複製程式碼

gulp && browerify

編譯reactjs

var gulp = require('gulp')
var browserify = require('browserify')
var reactify = require('reactify')
var source = require('vinyl-source-stream')
var streamify = require('gulp-streamify')

gulp.task('browserify',function(){
  browserify('./public/js/react_main_components.js')
    .transform(reactify)
    .bundle()
    .pipe(source('productList.js'))
    .pipe(streamify(uglify().on('error', gutil.log)))
    .pipe(gulp.dest('./public/js/reactCompoments/dist'))
});
複製程式碼

gulp && zip

var zip = require('gulp-zip');
var zipConfig = gulpConfig.zip;

gulp.task('zip', function() {
  // 打包時,排除掉上次生成過的release裡的壓縮包
  return gulp.src([zipConfig.src,'!./release/*.*'])
    .pipe(zip('demo.zip'))
    .pipe(gulp.dest(zipConfig.dest))
});
複製程式碼

最後

合併工作流,序列執行各類任務。根據不同需求,執行不同的構建流

// gulp publish --platform jinhui或jinfeng
gulp.task('publish',function(callback){
    runSequence('set-platform', 'clean-dist','sass',['publish-js', 'publish-css'],'publish-image','publish-view','replace-image-inCss','copy-other-files','zip',callback);
});
gulp.task('publish-addMap',function(callback){
    runSequence('set-platform', 'clean-dist','sass',['publish-js-addMap', 'publish-css'],'publish-image','publish-view','replace-image-inCss','copy-other-files','zip',callback);
});
複製程式碼
  • gulp 使用小節

  • Gulp CSS合併、壓縮與MD5命名及路徑替換

  • 附錄:常見的jshintrc檔案示例

      {
        //迴圈或者條件語句必須使用花括號包圍
        "curly":false,
        //強制使用三等號
        "eqeqeq":false,
        //禁止重寫原生物件的原型,比如 Array , Date
        "freeze":false,
        //程式碼縮排
        "indent":false,
        //禁止單引號雙引號混用
        "quotmark":false,
        //變數未使用
        "unused":false,
        //嚴格模式
        "strict":false,
        //最大巢狀深度
        "maxdepth": 10,
        //最多引數個數
        "maxparams": 10,
        //複雜度檢測
        "maxcomplexity":false,
        //最大行數檢測
        "maxlen": 1500,
        // 禁止定義之前使用變數,忽略 function 函式宣告
        "latedef":false,
        // 構造器函式首字母大寫
        "newcap":false,
        //禁止使用 arguments.caller 和 arguments.callee ,未來ECM5會被棄用
        "noarg":false,
        //變數未定義
        "undef":false,
        // 相容低階瀏覽器 IE 6/7/8/9
        "es3":false,
        // 控制“缺少分號”的警告
        "boss":false
      }
    複製程式碼

相關文章