gulp 前端自动化实践

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的安装与使用

  • 全局安装

      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
      }
    

推荐阅读更多精彩内容