Gulp是一个优秀的开源项目构建工具,它的核心在于流式操作和足够简单的API,这使得它使用非常广泛。本文介绍Gulp的基本使用及其API,并介绍几个我觉得很有用的插件。之后,我会介绍Gulp的内部原理及如何编写一个简单的Gulp插件。先来看一个简单的Gulp配置文件,领略一下基于流式操作的简洁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/* GULP Simple Introduction */
'use strict';
var util = require('util')
let gulp = require('gulp')
let plugins = require('gulp-load-plugins')()
let livereload = require('gulp-livereload')
const globs = {
script: 'app/script/app.es6',
html: 'app/view/**/*.jade',
less: 'app/less/*.less'
}
gulp.task('less', function() {
return gulp.src(globs.less)
.pipe(plugins.less())
.pipe(gulp.dest('dist'))
.pipe(livereload());
});
gulp.task('script', function () {
return gulp.src(globs.script)
.pipe(plugins.browserify({
debug: true,
transform: ['babelify'],
extensions: ['.es6']
}))
.pipe(plugins.rename('bundle.js'))
.pipe(gulp.dest('dist'))
.pipe(livereload());
});
gulp.task('html', function () {
return gulp.src(globs.html)
.pipe(plugins.jade({
pretty: true
}))
.pipe(gulp.dest('dist'))
.pipe(livereload())
})
gulp.task('server', function () {
var exec = require('child_process').exec;
exec('python -m SimpleHTTPServer 8080', function (error, stdout, stderr) {
if (error) {
console.log('stderr: ' + stdout);
}
});
})
gulp.task('watch', ['script', 'less', 'html'], function() {
livereload.listen();
['less', 'script', 'html'].forEach(function (task) {
gulp.watch(globs[task], [task]);
});
});
gulp.task('default', ['server', 'watch']);

Gulp核心API介绍

Gulp之所以越来越受欢迎,一个很重要的原因在于足够简单。简单到什么程度呢?它的核心API只有4个!这里简单地介绍一下这四个核心API:

gulp.src(globs[, options])

gulp是基于流进行处理的,src则是用于初始化创建一个虚拟文件对象流。其中globs参数用于文件匹配,可以是一个字符串也可以是字符串数组。在gulp内部,使用的node-glob模块来实现这个文件匹配的功能,这里列举一下常用glob模式:

  • *:匹配单个文件或目录
  • *.*:能匹配带后缀的文件
  • */*/*.js:能匹配 a/b/c.js,但不能匹配a/b.jsa/b/c/d.js
  • **能匹配所有的目录和文件
  • **/*.js 能匹配多级目录下的js文件(也包含当前目录下)
  • ?.js能匹配单字符名的JS文件,如a.js
  • [xyz].js匹配x.js|y.js|z.js
  • [^xyz].js表示取反,不能匹配x.js|y.js|z.js文件

gulp.dest(path[,options])

gulp.dest()方法是用来写文件的,第一个参数即最终输出的目录名。注意,它无法允许我们指明最终输出的文件名,只能指定输出文件夹名,而且在文件夹不存在的情况下会自动创建。

前面也提到,gulp是基于流的工作方式,从src开始创建的文件流,经过一系列插件的处理生成中间文件流,最终输出到dest中最后写入文件。而它的文件名,则是由src参数来决定。具体规则是,生成的文件路径由我们传入的path参数后面再加上gulp.src()中有通配符开始出现的那部分路径。这里举个例子特别说明一下:

1
2
3
4
let gulp = reruire('gulp');
//有通配符开始出现的那部分路径为 **/*.js
gulp.src('script/**/*.js')
.pipe(gulp.dest('dist')); //最后生成的文件路径为 dist/**/*.js

通过指定gulp.src()``方法配置参数中的base属性,我们可以更灵活的来改变gulp.dest()`生成的文件路径。当我们没有在gulp.src()`方法中配置base属性时,base的默认值为通配符开始出现之前那部分路径。

1
2
3
4
5
6
7
// 默认的base路径为script/lib/
// 但配置了base参数,此时base路径为script
gulp.src('script/lib/*.js', {
base: 'script'
})
//假设匹配到的文件为script/lib/jquery.js,此时生成的文件路径为 build/lib/jquery.js
.pipe(gulp.dest('build'))

当然,值得一提的是,gulp插件gulp-rename允许我们自定义最终输出的文件名。但此时,base配置项就不起作用了。

gulp.task(name[, deps], fn)

gulp.task用于定义任务,内部基于Orchestrator以最大限度提高并发执行任务的能力。

需要注意的是,在Orchestrator底层实现中,为最大限度提高并发执行任务的能力(实际上JS还是单线程运行,这里所说的并发数实际指的是异步执行数),任务都是立马开始执行。所以如果任务是有异步执行操作,而且需要等到异步操作完成后才可以开始另一个任务的时候,有依赖关系的任务就需要我们再做点别的事情来保证正确的执行顺序。有三种方法可以指明这种任务间的依赖:

  1. 定义任务时,需要在执行函数中接收一个回调参数
  2. 定义任务时,执行函数返回一个Promise对象或Stream流对象

要想让有依赖关系的任务能够配合着按顺序执行,需要有两点值得注意:

  • 设置任务的依赖任务 gulp.task('task', [deps])
  • 被依赖的任务参数需要使用上述三种方法之一

下面是对应的实例:

接收回调参数

1
2
3
4
5
6
7
8
9
gulp.task('baseTask', function (cb) {
setTimeout(() => {
cb(null)
}, 1000)
})
// subTask会在1s后才开始执行
gulp.task('subTask', ['baseTask'], function () {
// do something
})

返回Promise对象

1
2
3
4
5
6
7
8
9
10
11
gulp.task('baseTask', function (cb) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 1000)
})
})
// subTask会在1s后才开始执行
gulp.task('subTask', ['baseTask'], function () {
// do something
})

返回流对象(适用于文件IO操作)

1
2
3
4
5
6
7
gulp.task('baseTask', function () {
return gulp.src('XXX')
.pipe(gulp.dest('YYY'))
})
gulp.task('subTask', ['baseTask'], function () {
})

gulp.watch(glob[, opts], tasks)

开发过程中,我们不可能每次都手工调用gulp task来重新构建我们的修改,这时,监听文件变化并自动重新构建对应文件就显得尤为重要了。gulp提供了watch方法完成这一行为,它底层基于gaze,而opts一般很少用到,主要是提供给gaze底层模块使用,gulp.watch的使用方式非常简单:

1
2
3
4
5
// 当`js/**/*.js`下的文件发生变化时,自动执行uglify和reload任务
var watcher = gulp.watch('js/**/*.js', ['uglify','reload']);
watcher.on('change', function (event) {
console.log('File ' + event.path + ' was ' + event.type + ', running tasks...');
})

另外,watch还有另一种调用方式gulp.watch(glob[, opts, cb]),允许我们传入一个回调函数,当文件发生变化时自动执行回调函数,而回调参数可以告诉我们这个文件到底发生了什么变化,如增加、删除、改变文件等。实质上就是前一种方法里change事件的回调函数。

1
2
3
4
gulp.watch('script/**/*.js', function (event) {
console.log(event.type); //变化类型 added为新增,deleted为删除,changed为改变
console.log(event.path); //变化的文件的路径
});

一些实用的插件

gulp-livereload

gulp-livereload插件用于监听文件变化时实时更新页面内容,这样我们就无需每次修改文件后重新刷新页面来看结果了。为使gulp-livereload正常工作,需要配合chrome插件LiveReload一起使用。使用任意方式开启 Web Server 后,将gulp-livereload初始化并监听对应端口,之后在文件发生改变的时候,手工触发livereload函数。具体用法如下:

1
2
3
4
5
6
7
8
9
10
11
let livereload = require('gulp-livereload')
gulp.task('watch', function () {
livereload.listen()
gulp.watch(globs[task], [task])
})
gulp.task('css', function () {
return gulp.src(globs.css)
.pipe(plugins.less())
.pipe(gulp.dest('dist'))
.pipe(livereload())
})

gulp-plumber

在使用gulp.watch的时候,经常会碰到出错后gulp程序直接挂掉的情况,以至于我们不得不重新手工启动任务或使用进程监听的方式重启 gulp 任务。

有了gulp-plumber这个插件,我们可以直接把这些出错的地方全部收集起来,有点类似于Promise中最后的.fail().throw()方法。

1
2
3
4
5
6
7
gulp.src('./src/*.jsx')
// 开始收集错误,一般放在最前面
.pipe(plumber(function () { /* error handler */ }))
.pipe(coffee())
// 终止错误收集时可以使用stop
.pipe(plumber.stop())
.pipe(gulp.dest('./dist'));

gulp-load-plugins

在使用gulp的插件之前,我们需要require一系列插件,这导致gulpfile文件的开头非常长,gulp-load-plugins即是用于解决这个问题,它会自动加载package.json配置文件中的[dev]dependencies里以gulp开头的插件,并导出以驼峰式命名的变量。这样我们在gulpfile文件中,无需在前面引入对应的插件。

1
2
var plugins = require('gulp-load-plugins')();
plugins.rename // ==> require('gulp-rename')

也许你会担忧它加载一些多余的不必要插件(如我们安装了但并没有引用),事实上,gulp-load-plugins并不会一开始就加载所有package.json里的gulp插件,而是在我们需要用到某个插件的时候,才去加载那个插件。

require-dir

确认地说,require-dir不是一个插件,它只是一个用于简化我们的gulpfile文件的模块。众所周知,如果工程足够复杂,gulpfile会写得非常地长。require-gulp允许我们使用模块式的方法编写gulpfile文件,将它分为多个任务文件,提高代码的可读性。

通过使用require-dir,一个简单的gulp主文件可以简化成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var gulp = require('gulp');
var runSequence = require('run-sequence');
var requireDir = require('require-dir');
// Require all tasks.
requireDir('./gulp/tasks', {
recurse: true
});
// Commonly used tasks defined here.
gulp.task('default', function() {
runSequence(
'clean', [
'jade',
'sass',
'scripts',
'images',
'favicon'
],
'watch',
'connect'
);
});
gulp.task('build', function() {
runSequence(
'clean', [
'build-images',
'build-scripts',
'build-css'
],
'build-html',
'connect'
);
});

下面是文件的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
build/
gulp/
tasks/
base/
clean.js
connect.js
open.js
watch.js
build/
build-css.js
build-html.js
build-images.js
build-scripts.js
default/
assets.js
jade.js
sass.js
scripts.js
error-handler.js
paths.js
gulpfile.js
package.json

gulp工作流程剖析

编写一个自定义的gulp插件