鼓捣gulp有一段时间了,最开始接触gulp是在学习通过Browsersync自动刷新浏览器的时候知道了通过在gulpfile.js里编写脚本能更方便地配置Browsersync(见我的另一篇手记:Browsersync结合gulp和nodemon实现express全栈自动刷新),那个时候对gulp的认识还很浅,只知道大概是定义任务然后执行。看gulp文档的时候感觉对初学者也并不是很友好,一开始可能看完了文档还是不知道如何在实际的项目中应用gulp。最近,我查找了一些资料,看了一下gulp在前端工作流中的应用,然后用gulp对之前一个项目(结合慕课网从PSD到HTML和FullPage.js全屏滚动插件等多个课程制作的单页应用)进行了工作流改进的实践,总算感觉初步掌握了gulp。
这里我把我看到的感觉比较好的gulp基础知识的资料整理一下,然后以自己在项目中的工作流为例,简单说明一下gulp的使用。
gulp是干什么的
Gulp.js 是一个自动化构建工具,开发者可以使用它在项目开发过程中自动执行常见任务。Gulp.js 是基于 Node.js 构建的,利用 Node.js 流的威力,你可以快速构建项目并减少频繁的 IO 操作。Gulp.js 源文件和你用来定义任务的 Gulp 文件都是通过 JavaScript(或者 CoffeeScript )源码来实现的。
简单的说,gulp是一个将前端开发流程中的一些工作自动化完成,从而提高开发效率的工具,既然能提高效率,那么学习它对实际做项目是很有帮助的,把一些以前可能需要一步步手动完成的工作(比如合并、压缩js、css等)按你设计的工作流自动完成,就能节省很多时间,从而更专注于项目中更重要的部分。
gulp基础知识
- gulp的安装
因为是基于Node.js的,所以还是要先安装node.js环境,然后以全局方式安装gulp:
npm install -g gulp
要在项目中使用gulp,先要作为项目的开发依赖(devDependencies)安装gulp,首先切换到项目目录下,在命令行中执行:
npm init
创建package.json文件,然后在命令行中执行:
npm install --save-dev gulp
gulp就会出现在package.json的devDependencies项中。gulp是通过插件来完成每项工作的,这些插件也通过做为项目的开发依赖来安装。
- gulp的使用方法
在项目根目录下创建一个名为 gulpfile.js 的文件,然后在这个文件中定义要执行的一系列任务,包括任务的名字,任务要完成的工作,以及任务执行的顺序等。
比如:var gulp = require('gulp'); gulp.task('sayHi',function(){ console.log('hello gulp~'); });
上述代码定义了一个任务,任务名是sayHi
,任务的内容在一个匿名函数中,是输出文本,在命令行中执行gulp sayHi
,就会看到hello gulp~
。
上面只是一个简单的例子,它并没有帮我们完成前端开发工作流程中任何实际工作,要想使用gulp,首先要掌握gulp的基本API
- gulp的基本API
gulp有四个基本的API:gulp.task()
,gulp.src()
,gulp.dest()
,gulp.watch()
详细介绍这些API之前,我们先简单了解一下gulp中任务的常见执行过程:通过gulp.src()
方法获取到我们想要处理的文件流(html、css、js和图片等等) 通过管道(pipe)方法把流依次传递给各种插件(编译、合并、压缩、重命名等等)进行操作 把经过插件处理后的流再通过pipe方法导入到gulp.dest()
中,最后gulp.dest()
方法把流中的内容写入到文件中,完成对文件的操作。
gulp.src()
具体讲解gulp.src()
之前要先提一下grunt,grunt是类似于gulp的工具,不同之处在于grunt是基于文件的。
Grunt主要是以文件为媒介来运行它的工作流的,比如在Grunt中执行完一项任务后,会把结果写入到一个临时文件中,然后可以在这个临时文件内容的基础上执行其它任务,执行完成后又把结果写入到临时文件中,然后又以这个为基础继续执行其它任务…就这样反复下去。
而gulp则是基于Node.js的stream(流)为媒介来运行工作流的,前面已经说了,要对一个文件执行某个操作,要先把这个文件取到流中,这也就是gulp.src()
做的事,其语法为:
gulp.src(globs[, options])
其中比较重要的是globs这个参数,是表示文件的匹配模式,也就是用来匹配选择你要获取的文件的,当需要用到多个匹配模式时,可以把这个参数写成数组的形式,为了正确获取需要的文件,我们需要了解一些简单的匹配规则。
Gulp内部使用了node-glob模块来实现其文件匹配功能。我们可以使用下面这些特殊的字符来匹配我们想要的文件:
* 匹配文件路径中的0个或多个字符,但不会匹配路径分隔符,除非路径分隔符出现在末尾
** 匹配路径中的0个或多个目录及其子目录,需要单独出现,即它左右不能有其他东西了。如果出现在末尾,也能匹配文件。
? 匹配文件路径中的一个字符(不会匹配路径分隔符)
[…] 匹配方括号中出现的字符中的任意一个,当方括号中第一个字符为^或!时,则表示不匹配方括号中出现的其他字符中的任意一个,类似js正则表达式中的用法
!(pattern|pattern|pattern) 匹配任何与括号中给定的任一模式都不匹配的
?(pattern|pattern|pattern) 匹配括号中给定的任一模式0次或1次,类似于js正则中的(pattern|pattern|pattern)?
+(pattern|pattern|pattern) 匹配括号中给定的任一模式至少1次,类似于js正则中的(pattern|pattern|pattern)+
(pattern|pattern|pattern) 匹配括号中给定的任一模式0次或多次,类似于js正则中的(pattern|pattern|pattern)
@(pattern|pattern|pattern) 匹配括号中给定的任一模式1次,类似于js正则中的(pattern|pattern|pattern)
以一系列例子来加深理解:
* 能匹配 a.js,x.y,abc,abc/,但不能匹配a/b.js
* .* 能匹配 a.js,style.css,a.b,x.y
* / * / * .js 能匹配 a/b/c.js,x/y/z.js,不能匹配a/b.js,a/b/c/d.js
** 能匹配 abc,a/b.js,a/b/c.js,x/y/z,x/y/z/a.b,能用来匹配所有的目录和文件
** /* .js 能匹配 foo.js,a/foo.js,a/b/foo.js,a/b/c/foo.js
a/** /z 能匹配 a/z,a/b/z,a/b/c/z,a/d/g/h/j/k/z
a/** b/z 能匹配 a/b/z,a/sb/z,但不能匹配a/x/sb/z,因为只有单**单独出现才能匹配多级目录
?.js 能匹配 a.js,b.js,c.js
a?? 能匹配 a.b,abc,但不能匹配ab/,因为它不会匹配路径分隔符
[xyz].js 只能匹配 x.js,y.js,z.js,不会匹配xy.js,xyz.js等,整个中括号只代表一个字符
[^xyz].js 能匹配 a.js,b.js,c.js等,不能匹配x.js,y.js,z.js
获取到文件之后,就可以用各种插件对文件进行操作了。
gulp.dest()
操作完成之后,就用gulp.dest()
把文件写入输出的路径,语法为:
gulp.dest(path[, options])
其中比较重要的是path
参数,表示文件输出的路径,这里有两点要强调一下:一是这个路径参数path
只能指定路径,不能指定文件名,在没有使用插件对文件名进行修改的情况下,生成文件的文件名和导入文件的文件名相同;二是path参数指定的路径与生成文件的路径的关系,在gulp.src(globs[, options])
的options
参数中有一个base
属性,在没有自己设置的情况下就表示globs参数中的base path 也就是glob匹配规则之前那部分明确的路径,比如gulp.src('src/**/*.css')
能匹配src/
路径下和其任意子目录下的任意css文件,此时的base
就是src/
。在用gulp.dest(path[, options])
输出文件的时候,会用path参数指定的路径替换base
,还是结合实际的例子来具体看看上面提到的两点,比如:
gulp.src('src/**/*.css')
.pipe(gulp.dest('dist'));
就会用dist/
替换src/
,假如匹配到的文件是src/css/a.css
,文件输出后就是dist/css/a.css
,如果我们在获取文件的时候更改base
属性的值,就能在输出文件的时候更改输出路径,因此这一属性能让我们更灵活地修改项目文件的组织结构。
gulp.task()
这个方法用来定义要执行的任务,其语法为gulp.task(name[, deps], fn)
,其中names
是任务名,deps
数组是当前任务的前置依赖任务,如果没有依赖任务就不需要这个参数。
在这个方法里,我们需要明确多个任务执行顺序的问题,特别是当任务中存在异步操作的时候,这样才能让我们在gulp中定义的一系列任务按我们设计的工作流顺序执行。先看一个最简单的例子:
gulp.task('a', function() {
console.log('a');
});
// b不依赖a
gulp.task('b', function() {
console.log('b');
});
gulp.task('c', ['a', 'b'], function() {
console.log('c');
});
上面的代码定义了a、b和c三个任务,其中a和b是c的依赖任务,a和b没有依赖关系,在命令行中执行:gulp c
,会先执行依赖任务a和b,由于a和b没有依赖关系,就按书写顺序,先执行a,打印出’a’,接着执行b,打印出’b’,执行完这两个依赖任务再执行c,打印出’c’。
接着我简单修改一下:
// a依赖b
gulp.task('a', ['b'], function() {
console.log('a');
});
gulp.task('b', function() {
console.log('b');
});
gulp.task('c', ['a', 'b'], function() {
console.log('c');
});
上面我让a依赖b,尽管在后面定义任务c的时候,依赖任务的书写顺序是先a后b,但由于a依赖于b,还是会先执行b,然后a,最后是c。为了避免混淆,我们在写的时候最好把被依赖的前置任务写在前面。
接着再来看看任务中存在异步操作的情况,让a通过setTimeout延时2s执行:
gulp.task('a', function() {
setTimeout(function (){
console.log('a');
},2000);
});
// b不依赖a
gulp.task('b', function() {
console.log('b');
});
gulp.task('c', ['a', 'b'], function() {
console.log('c');
});
上面这段代码运行后实际的打印顺序则是b c a。b和c都在a执行完之前就执行完了,但是原因是不同的,b对a没有依赖,a异步执行,a中setTimeout
所完成的只是等待2s后将回调函数放入任务队列等待执行,b不会等a的回调函数执行,c对a有依赖,但是c没有被告知a执行完毕,所以c也不会等a的回调函数执行,c对b有依赖,所以c会等b。在gulp文档中我们可以看到:
如果你想要创建一个序列化的 task 队列,并以特定的顺序执行,你需要做两件事:
1. 给出一个提示,来告知 task 什么时候执行完毕,
2. 并且再给出一个提示,来告知一个 task 依赖另一个 task 的完成。
告知依赖很简单,只要在定义b任务的时候把a作为它的依赖任务写到参数中,而告知task执行完毕有三种方法:
第一:在异步操作完成后执行一个回调函数来通知gulp这个异步任务已经完成,这个回调函数就是任务函数的第一个参数。
第二:定义任务时返回一个流对象。适用于用gulp.src获取文件到的流并进行操作然后输出的情况。
第三:返回一个promise对象。
这里我用上面的第一种方法,添加一个回调函数,修改后的代码如下:
gulp.task('a', function(cb) {
setTimeout(function (){
console.log('a');
cb();//执行回调,告知异步任务完成
},2000);
});
// 让b依赖a
gulp.task('b', ['a'], function() {
console.log('b');
});
//c依赖b
gulp.task('c', ['b'], function() {
console.log('c');
});
此时再运行,任务执行顺序和打印顺序就是 a b c了。
gulp.watch()
监视文件,并且可以在文件发生改动时候做一些事情。
语法有两种gulp.watch(glob [, opts], tasks)
或 gulp.watch(glob [, opts, cb])
。其中glob
和gulp.src()
里一样是文件匹配模式;opts
是一个可选的配置对象,通常用不到;tasks
是文件变化后要执行的任务,是一个数组;cb
是一个函数,表示每次文件变动后执行的callback,callback 会被传入一个名为 event 的对象。这个对象描述了所监控到的变动信息,包括变动类型(event.type
)和变动文件的路径(event.path
),例如:
gulp.watch('js/**/*.js', function(event) {
console.log('File ' + event.path + ' was ' + event.type + ', running tasks...');
});
会输出有变更的js文件的路径和具体的变更(包括added, changed 或者 deleted)。
API介绍到了这里,内容有点多,不过有些地方比官方文档要详细,可能你会觉得看完了这些API还是不知道如何用gulp在项目中改进工作流,没关系,我原来也不知道,后来实际在项目中用了以后才理解了API中的很多细节。所以相信我,这些都是要用到的,有些地方现在还不懂也没关系,后面结合项目实际会更容易理解。
使用gulp改进工作流的项目实践
这里我就简单结合项目实际来讲讲gulp的使用。通常,项目的工作流分成开发阶段和发布阶段。
- 开发阶段
我把html、css、js和图片文件放在src目录下,这就是我开发阶段的工作目录。
这个阶段我只需要gulp帮我做一件事:监听各种文件并自动刷新浏览器,这个只需要gulp结合Browsersync就能很好完成。
gulp.task('watchdev', function() {
// 启动Browsersync服务。这将启动一个服务器,代理服务器(proxy)或静态服务器(server)
browserSync.init({
// 设置监听的文件,以baseDir设置的目录为起点,单个文件就用字符串,多个文件就用数组
files: ["*.html", "css/*.css", "script/*.js", "images/*.*"],
// 这里是静态服务器,默认监听3000端口,baseDir设置监听文件根路径
server: {
baseDir: "./src"
},
// 在不同浏览器上镜像点击、滚动和表单,即所有浏览器都会同步
ghostMode: {
clicks: true,
scroll: true
},
// 更改控制台日志前缀,可以设为项目名
logPrefix: "from psd to html",
// 设置监听时打开的浏览器
browser: ["firefox", "chrome"],
// 设置服务器监听的端口号
port: 8080
});
console.log('Debugging in dev.');
});
- 发布阶段
到了发布阶段,要做的事就比较多了,你会看到gulp的强大。
我把发布阶段的修改后的文件都放到dist文件夹中。在发布阶段,根据项目的复杂程度不同,要完成的工作是不一样的,gulp提供了丰富的插件能满足各种需求,你可以根据自己的项目情况去学习不同插件的用法,这里因为不想篇幅太长了,我只举例最简单的情况:
- css的压缩
- js的压缩
- 静态资源防缓存
- 图片的压缩
合并压缩比较好理解,就是为了减少http请求和资源大小,从而加快页面加载速度,静态资源防缓存需要解释一下,因为到了发布阶段,可能还是需要对项目的静态资源进行更新,而之前用户的客户端可能已经对静态资源进行了缓存,更新之后,为了防止客户端是用旧的缓存文件,可以给更新后的静态资源文件名加上hash值,从而在客户端下次访问时强制加载更新后的文件。
因为每次修改静态资源文件后添加的hash值是不同的,不能覆盖原先的文件,所以发布阶段工作的一开始,我们要清空发布阶段工作目录dist/里的文件:
gulp.task('clean', function() {
return del([
'dist/css/**/*',
'dist/script/**/*',
'rev/**/*'
]);
});
由于删除文件和文件内容并没有太大关系,所以,我们没必要去用一个 gulp 插件。
这里用到了原生的 node 模块del。rev文件夹下存放了存储着添加了hash值的文件清单的json文件,后面会提到。
接着处理css文件:
当我们在gulp中使用的插件很多的时候,可以使用一个gulp-plugins插件来省去一大堆的require语句,后面使用插件的时候就可以写成plugins.xxx
,xxx
表示插件名称(省去’gulp-’)部分
gulp.task('minifycss', function() {
return gulp.src('src/css/**/*.css')
.pipe(plugins.sourcemaps.init())
.pipe(plugins.autoprefixer({
// 设置支持的浏览器,这里是主要浏览器的最新两个版本
browsers: 'last 2 versions'
}))
.pipe(cleanCSS())
// 添加hash值
.pipe(plugins.rev())
.pipe(plugins.sourcemaps.write('./'))
.pipe(gulp.dest('dist/css/'))
//把添加了哈希值的文件添加到清单中
.pipe(plugins.rev.manifest())
.pipe(gulp.dest('rev/css'));
});
简单介绍一下这里用到的几个插件,详细内容可以根据需要去读插件的文档和Google。
- gulp-sourcemaps:压缩后的css和js代码没有换行符和空白符,很难调试。gulp-sourcemaps插件可以生成一个和文件对应的source map文件,里面存储着转换后的代码的每一个位置,所对应的转换前的位置。有了这个文件,在chrome和firefox的开发者工具的设置选项里启用sourcemap功能就可以用压缩后的文件进行调试了。
- gulp-autoprefixer:给css属性添加浏览器前缀。
- gulp-cleanCSS:压缩css。
- gulp-rev:给文件名添加hash值,插件的manifest方法会生成一个记录添加hash值文件名的json文件rev-manifest.json,用于替换html文件中的link标签。
接着处理js文件:
const handleError = function(err) {
var colors = gutil.colors;
console.log('\n');
gutil.log(colors.red('Error!'));
gutil.log('fileName: ' + colors.red(err.fileName));
gutil.log('lineNumber: ' + colors.red(err.lineNumber));
gutil.log('message: ' + err.message);
gutil.log('plugin: ' + colors.yellow(err.plugin));
};
gulp.task('uglifyjs', function () {
var combined = combiner.obj([
gulp.src('src/script/**/*.js'),
plugins.sourcemaps.init(),
plugins.uglify(),
plugins.rev(),
plugins.sourcemaps.write('./'),
gulp.dest('dist/script/'),
plugins.rev.manifest(),
gulp.dest('rev/script')
]);
combined.on('error', handleError);
return combined;
});
用到的插件:
- stream-combiner2:
默认情况下,在 stream 中发生一个错误的话,它会被直接抛出,gulp停止运行,除非已经有一个时间监听器监听着 error 事件。 这在处理一个比较长的管道操作的时候会显得比较棘手。
通过使用 stream-combiner2,你可以将一系列的 stream 合并成一个,这意味着,你只需要在你的代码中一个地方添加监听器监听 error 就可以了。
此时当要压缩的js文件里有语法错误时,命令行会出现错误提示。而且不会让 gulp 停止运行。
- gulp-uglifyjs:压缩js。
接着压缩图片:
gulp.task('images', function () {
return gulp.src('src/images/**/*')
.pipe(plugins.imagemin({
progressive: true
}))
.pipe(gulp.dest('dist/images'));
});
- gulp-imagemin:压缩图片,针对jpg、png和GIF格式图片都有更细化的插件满足不同的压缩需要。
最后把更改文件名后的静态资源文件引入html文件替换原先的引用并输出处理后的html文件到发布目录dist/覆盖原来的html:
gulp.task('html', ['clean', 'minifycss','uglifyjs', 'images'], function() {
return gulp.src(['rev/**/*.json', 'src/*.html'])
.pipe(plugins.revCollector({
replaceReved: true,
dirReplacements: {
'css': 'css',
'script': 'script'
}
}))
.pipe(gulp.dest('dist'));
});
- gulp-revCollector:从rev-manifest.json中读取更新的文件名信息,和gulp-rev配合使用,注意rev-manifest.json文件中只有文件名,没有路径,所以还要在
dirReplacements
项中注明路径。
经过上面一系列的工作,我们就能在dist/目录下看到所有用于发布的文件,这些文件都完成了压缩、添加hash,替换到html,而这一切工作只需在命令行中执行gulp html
就可以自动完成了。
总结
呼~终于到了总结阶段,如果你还没看晕我挺佩服你的,其实我已经快写晕了,本来想把自己在Browsersync结合gulp和nodemon实现express全栈自动刷新里用到的更复杂的工作流写上来,不过感觉这篇手记的篇幅已经够长了,而且复杂的工作流适用的范围就不广了。
希望能给初学gulp的小伙伴们一点帮助。
参考资料
本作品采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。要查看该许可协议,可访问 http://creativecommons.org/licenses/by-nc-sa/4.0/ 或者写信到 Creative Commons, PO Box 1866, Mountain View, CA 94042, USA。