继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Gulp基础及其在前端工作流自动化中的应用

种子_fe
关注TA
已关注
手记 28
粉丝 83
获赞 479

鼓捣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基础知识

  1. 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是通过插件来完成每项工作的,这些插件也通过做为项目的开发依赖来安装。

  1. gulp的使用方法
    在项目根目录下创建一个名为 gulpfile.js 的文件,然后在这个文件中定义要执行的一系列任务,包括任务的名字,任务要完成的工作,以及任务执行的顺序等。
    比如:
    var gulp = require('gulp');
    gulp.task('sayHi',function(){
      console.log('hello gulp~');
    });
    

上述代码定义了一个任务,任务名是sayHi,任务的内容在一个匿名函数中,是输出文本,在命令行中执行gulp sayHi,就会看到hello gulp~

上面只是一个简单的例子,它并没有帮我们完成前端开发工作流程中任何实际工作,要想使用gulp,首先要掌握gulp的基本API

  1. 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])。其中globgulp.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的使用。通常,项目的工作流分成开发阶段和发布阶段。

  1. 开发阶段
    我把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.');
});
  1. 发布阶段
    到了发布阶段,要做的事就比较多了,你会看到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.xxxxxx表示插件名称(省去’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。

打开App,阅读手记
2人推荐
发表评论
随时随地看视频慕课网APP