Node.js 自动化工具 - Gulp

接着上篇的《 Node.js 自动化工具 - Bower 》我们开始学习另一个强大的自动化工具吧!

打开它的官网 Gulp 中文网,当我点开它的那刻是激动的,居然有中文!浏览完网站后,风格简洁呀!对,不止 display 简洁,文档也真的是很简洁,本来以为能轻松学习的,没想到后面还是要补充很多其他知识点 -_-||

这次 logo 终于是不动物了

“ 一个服务于前端的管道式构建系统 ”
自动化的构建项目,实现 JavaScript 和 CSS 的版本与依赖问题,文件合并,文件压缩,单元测试等。

易于使用
通过代码优于配置的策略,Gulp 让简单的任务简单,复杂的任务可管理。

构建快速
利用 Node.js 流的威力,你可以快速构建项目并减少频繁的 IO 操作。

插件高质
Gulp 严格的插件指南确保插件如你期望的那样简洁高质得工作。

易于学习
通过最少的 API,掌握 Gulp 毫不费力,构建工作尽在掌握:如同一系列流管道。

这上面是官网给予的 Gulp 四个特点,我们来一点一点的解释一下:

1. 易于使用

到底有多简单?看看官网的入门指南,短短四行说明完毕。确实 Gulp 整个的任务逻辑十分简单明了

// 定义插件的引入
var gulp = require('gulp');
// 创建任务
gulp.task('default', function() {
  // 将你的默认的任务代码放在这
});

这一切的一切就基于这两步形成的,很符合我们敲代码的中心思想 , “ 一切为了优雅 ” 。

2. 构建快速

看看这个说明画重点,流 \ IO操作 没错这个就是 Gulp 与 Grunt 的最大差别,也是我什么挑 Gulp 的原因。

举个例子,比如我们要对某个 JS 文件进行校验,压缩,混淆,我们工作流大致是这样的:

Grunt/Gulp 的工作模式

由上图就可以看出,Grunt 与 Gulp 的不同:

  • Grunt 每次处理完一个任务(task)都会产生一个临时文件,下个任务开始前再读取临时文件执行,循环如此,直至输出,这其中的读写操作就是: IO操作

  • Gulp 就跟水流一样从一个 task 到另一个 task 直至输出,这种模式就是我们现在常用的 流模式,跟视频流,图片流有异曲同工之妙。

// 这个就是控制流中最重要的管道
.pipe()

3. 插件高质

这个呢,我也不太好说,他说高就高吧,毕竟哪里都有高手又不是只有你家有。除了 Gulp 的 插件搜索页 比较好看之外我也不知道差别在哪里,反正最后都跳到 npm

4. 易于学习

相对 Grunt 十个类型的,大量的 API 和命令来看,Gulp 更像集天下武功于大乘,以四个 API 就能掌控雷电,毕竟一招 “ 乾坤大挪移 ” 就够它吃尽大半武林了。

// 输入口
gulp.src();

// 输出口
gulp.dest();

// 任务
gulp.task();

// 监察
gulp.watch()

通过上面我们已经可以基本了解到 Gulp 具体是什么样的了,现在动手学习吧~


安装

首先我们在全局安装,方便调用

$ cnpm install --global gulp

接着我们在项目作为依赖再安装一遍

$ cnpm install --save-dev gulp

为什么要在项目中再装一遍呢?这是为了避免每个项目合作者本地的 Gulp 版本不一导致的构建失败,在项目中规定了 Gulp 版本可以有效的避免这样情况。

现在我们假定执行 JS 的文件压缩任务,它依赖的插件为:gulp-uglify
我们继续按照这个插件并依赖到项目 package 中

$ cnpm install --save-dev gulp-uglify

task编写

创建 gulpfile.js 文件,编写 task 用来告诉 Gulp 我们需要完成怎么样的任务

手动创建不够装逼,补充下小知识用命令行创建:

  • 我们知道命令 mkdir 能够创建一文件夹,那创建文件要怎样呢?
  • 输入命令
    $ cd.>gulpfile.js
    这样就可以创建出 gulpfile.js 文件了,至于原理是什么?自己去查 Dos 命令吧~

好了,进行到这一步项目结构大概这样(多删少补):

gulp/
├── src/ 
│    └── app.js (自己随便敲点 js 代码)         
├── dist/
├── node_modules/ 
├── gulpfile.js/ 
└── package.json 

开打 gulpfile.js 输入

var gulp = require('gulp'),                // 引入 gulp 
      uglify = require('gulp-uglify');     // 引入 gulp-uglify 插件

gulp.task('minjs', function () {           // task 任务定义,'minjs' 自定义任务名
    gulp.src('src/app.js')                 // src 定义任务文件
    .pipe(uglify())                        // .pipe() 链式编码,管道过程执行任务
    .pipe(gulp.dest('dist'));              // dest() 定义成型文件输入地址
});

运行task

$ gulp minjs

返回 task 执行时间为 10 ms

可以看到 dist 文件中已经生成了压缩好的 app.js 文件。
这就是最基本的 Gulp 工作流程,接下来我们研究其 API 深入学习各项配置功能。


API

1. gulp.src(globs[, options])

管道入口,两个参数 [ globs , options ]

  • globs
    可直接写文件路径指定执行文件,但是这样做只能匹配单独文件。在具体的应用场景中我们通常需要匹配特定规则的文件以自动化执行任务,这时候我们就需要 globs 来完成这项任务。

  • globs 的使用
    首先它是属于 node.js 的一个 插件 ,Gulp 中能直接应用,而不需要再次安装,这是它的 GitHub 地址 node-glob,里面有详细的用法介绍。我下面挑一些实用例子说明一下:

字符 官档说明 解释
* Matches 0 or more characters in a single path portion 匹配文件路径中的零个或多个字符(不会匹配路径分隔符)
? Matches 1 character 匹配文件路径中的一个字符(不会匹配路径分隔符)
[...] Matches a range of characters, similar to a RegExp range. If the first character of the range is ! or ^ then it matches any character not in the range. 匹配方括号中出现的字符中的任意一个,当方括号中第一个字符为 ^ 或 ! 时,则表示不匹配方括号中出现的其他字符中的任意一个
! (pattern / pattern / pattern) Matches anything that does not match any of the patterns provided. 匹配任何与括号中给定的任一模式都不匹配的字符
? (pattern / pattern / pattern) Matches zero or one occurrence of the patterns provided. 匹配括号中给定的任一模式0次或1次
+ (pattern / pattern / pattern) Matches one or more occurrences of the patterns provided. 匹配括号中给定的任一模式至少1次
* (a / b / c) Matches zero or more occurrences of the patterns provided 匹配括号中给定的任一模式0次或多次
@ (pattern / pat* / pat?erN) Matches exactly one of the patterns provided 匹配括号中给定的任一模式1次
** If a "globstar" is alone in a path portion, then it matches zero or more directories and subdirectories searching for matches. It does not crawl symlinked directories. 匹配路径中的0个或多个目录及其子目录,需单独出现,混合出现时将不能匹配多级目录。
下面以一系列例子来加深理解

* 能匹配 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.src(['js/*.js','css/*.css','*.html'])

gulp.src([*.js,'!b*.js']) //匹配所有js文件,但排除掉以b开头的js文件
gulp.src(['!b*.js',*.js]) //不会排除任何文件,因为排除模式不能出现在数组的第一个元素中

// 展开模式
a{b,c}d 会展开为 abd,acd
a{b,}c 会展开为 abc,ac
a{0..3}d 会展开为 a0d,a1d,a2d,a3d
a{b,c{d,e}f}g 会展开为 abg,acdfg,acefg
a{b,c}d{e,f}g 会展开为 abdeg,acdeg,abdeg,abdfg

—— 该例子来自 无双 博客 《 前端构建工具gulpjs的使用介绍及技巧 》

  • options
options 用法

后面的 { base: 'client' } 就是 options 其中一个的配置项

options 类型 默认值 说明 备注
options.buffer Boolean true 如果该项被设置为 false,那么将会以 stream 方式返回 file.contents 而不是文件 buffer 的形式。这在处理一些大文件的时候将会很有用。注意:插件可能并不会实现对 stream 的支持 就说 false 时将返回文件目录形式而不是文件缓存形式
options.read Boolean true 如果该项被设置为 false, 那么 file.contents 会返回空值(null) 就说是并不会去读取文件
options.base String 将会加在 glob 之前 指定输出文件位置 就是说不是按原 glob 的路径,可以自己指定路径,具体看上图

2. gulp.dest(path[, options])

管道出口,两个参数 [ path , options ]
能在 pipe 中实现输出(emits)数据,在一个任务流中,你可以将其 pipe 到多个文件夹里。如果某文件夹不存在,将会自动创建它。

gulp.src('./client/templates/*.jade')
  .pipe(jade())
  .pipe(gulp.dest('./build/templates'))
  .pipe(minify())
  .pipe(gulp.dest('./build/minified_templates'));
  • path
    类型: String or Function
    文件将被写入的路径(输出目录)。也可以传入一个函数,在函数中返回相应路径,这个函数一般是利用 vinyl 这个插件。
    同时我们只能指定路径并不能指定文件名,因为生成的文件名是由导入到它的文件流决定的。想改变文件名用 gulp-rename 实现。

  • vinyl
    一个文件包含连个基本信息 【 路径 , 内容 】,vinyl 就是负责描述文件的基本属性的工具

var Vinyl = require('vinyl');
var jsFile = new Vinyl({
      cwd: '/',
      base: '/test/',
      path: '/test/file.js',
      contents: new Buffer('var x = 123')
});
var file = new File({
      cwd: '/',
      base: '/test/',
      path: '/test/file.js'
});
console.log(file.extname); // .js
file.extname = '.txt';
console.log(file.extname); // .txt
console.log(file.path); // /test/file.txt

这里面的 API 跟用法比较多,我在这里就不累述了,开个番外篇 《 Gulp 插件 - vinyl 》

  • options
options 类型 默认值 说明 备注
options.cwd String process.cwd() 输出目录的 cwd 参数,只在所给的输出目录是相对路径时候有效。 为下一个任务服务
options.mode String 0777 八进制权限字符,用以定义所有在输出目录中所创建的目录的权限 这里面就涉及了 linux 权限字符串,有兴趣看开番外篇《 八进制权限字符 》
  • 输出例子
    原则:生成的文件名是由导入到它的文件流决定的

由此我们可以得到以下规则:

序号 规则 例子
1 没有通用匹配符 gulp.src('src/app.js').pipe(gulp.dest('dist')); // 最后生成的文件路径为 dist/app.js'
2 出现通配符 **/* gulp.src('src/**/app.js').pipe(gulp.dest('dist')); // 最后生成的文件路径为 dist/js/app.js'
3 当 gulp.src() 指定 base 值时 gulp.src(script/lib/*.js).pipe(gulp.dest('build')) // 最后生成的文件路径为 build/jquery.js / gulp.src(script/lib/*.js, {base:'script'}) .pipe(gulp.dest('build')) // 最后生成的文件路径为 build/lib/jquery.js

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

它是基于 orchestrator 实现,具体看看它的 Github 吧;

  • name
    规定 task 的名字,当其值为 “default” 时则为默认 task ,可直接 $ gulp 执行;

  • deps
    类型为数组,是当前任务的依赖任务列表,即在执行该任务前会,先执行数组中的任务。但其都是按异步方式执行的,即不会等待依赖任务完成再执行下一个任务,若任务间不存在依赖无妨,若存在依赖需要利用使 callback,或者返回一个 promise 或 stream。

注意默认的,task 将以最大的并发数执行,也就是说,gulp 会一次性运行所有的 task 并且不做任何等待。如果你想要创建一个序列化的 task 队列,并以特定的顺序执行,你需要做两件事:

  1. 给出一个提示,来告知 task 什么时候执行完毕
  1. 并且再给出一个提示,来告知一个 task 依赖另一个 task 的完成

我们先看看官网给的例子

var gulp = require('gulp');

// 返回一个 callback,因此系统可以知道它什么时候完成
gulp.task('one', function(cb) {
    // 做一些事 -- 异步的或者其他的
    cb(err); // 如果 err 不是 null 或 undefined,则会停止执行,且注意,这样代表执行失败了
});

// 定义一个所依赖的 task 必须在这个 task 执行之前完成
gulp.task('two', ['one'], function() {
    // 'one' 完成后
});

gulp.task('default', ['one', 'two']);

其他部分都是司空见惯的,唯一不同是的,task one 中多了一个 cb() 这个就是通过 callback 实现告知 Gulp 该 task 执行完毕。还有另外两种方法:[ promise , stream ]

  • promise
var Q = require('q');
gulp.task('somename', function() {
      var deferred = Q.defer();
     // 执行异步的操作
     setTimeout(function() {
     deferred.resolve();
  }, 1);

  return deferred.promise;
});

promise 是 ECMAScript 6 发布用来传递异步操作的 API 这里的具体我就不说了,可以参考阮老师的文章 《 阮一峰的ES6 教程 》,后续有时间再自己为 ES6 填坑吧。

  • stream
    就我个人来说,我比较推荐用 stream 来处理队列
gulp.task('somename', function() {
  var stream = gulp.src('client/**/*.js')
    .pipe(minify())
    .pipe(gulp.dest('build'));
  return stream;
});

为什么?看看上面那段代码,一点违和感都没有呀,多适合 Gulp 的风格,多优雅!

4. gulp.watch(glob [, opts], tasks) 或 gulp.watch(glob [, opts, cb])

监视文件,并且可以在文件发生改动时候做一些事情。它总会返回一个 EventEmitter 来发射(emit) change 事件。

  • gulp.watch(glob[, opts], tasks)
    前面两个参数 [ glob , opts ] 跟 gulp.src 没有什么不同,后面跟一个监控的 task ;
var watcher = gulp.watch('js/**/*.js', ['uglify','reload']);
watcher.on('change', function(event) {
  console.log('File ' + event.path + ' was ' + event.type + ', running tasks...');
});
  • gulp.watch(glob[, opts, cb])
    监控文件是否发生改变,并执行对应函数
gulp.watch('js/**/*.js', function(event) {
  console.log('File ' + event.path + ' was ' + event.type + ', running tasks...');
});

解释一下上面两个方法中的 function(event) 中的 event 对象,这个对象描述了所监控到的变动。

参数 类型 事件
event.type String 发生的变动的类型:added, changed 或者 deleted
event.path String 触发了该事件的文件的路径

这上面就是 Gulp 全部的用法了, 剩下就是其插件的探讨,我列举一些常用的吧~


插件介绍

具体用法自己点击链接研究

插件 介绍 安装 用法
gulp-uglify js文件压缩 $ npm install --save-dev gulp-uglify .pipe(uglify())
gulp-rename 文件重命名 $ npm install --save-dev gulp-rename .pipe(rename('jquery.min.js'))
gulp-minify-html css文件压缩 $ npm install --save-dev gulp-minify-html .pipe(minifyHtml())
gulp-jshint js代码检查 $ npm install --save-dev jshint gulp-jshint .pipe(jshint()) 它依赖于 jshint ,再安装时需要注意
gulp-concat 文件合并 $ npm install --save-dev gulp-concat .pipe(concat('all.js'))
gulp-imagemin 图片压缩 $ npm install --save-dev gulp-imagemin .pipe(imagemin({ progressive: true, use: [pngquant()] })) //使用pngquant来压缩png图片,具体见 npm
gulp-livereload 自动刷新 $ npm install --save-dev gulp-livereload gulp.task('watch', function() { livereload.listen(); gulp.watch('less/*.less', ['less']); }); // 可用 Chrome 的 livereload chrome extension 辅助实现
gulp-load-plugins 自动加载插件 $ npm install --save-dev gulp-load-plugins 类似于 require.js 的 gulp 插件管理

好啦,基本上 Gulp 的学习就是到这里了
下篇我们来学习《 Node.js 自动化工具 - webpack 》


该篇收录于文集:Node教程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容