原文:https://www.smashingmagazine.com/2014/06/building-with-gulp/
作者: Callum Macrae
简述:Gulp是少数用JavaScript编写的构建工具,但是也有不是用JavaScript编写的构建工具,例如,Rake。我们为什么要选择Gulp呢?
在不同的浏览器优化你的网站资源和测试你的设计,并不是一个设计过程最有趣的部分。幸运的是,由各种重复任务组成的它可以通过工具实现自动化,以便提高你的效率。
Gulp是一个通过自动执行常见的任务而改善你的web开发方式。例如自动编译预编译CSS,压缩JavaScript和自动重新加载浏览器。
通过本文,我们将看到你能使用Gulp去改变开发方式,使得开发更快更有效率。
什么是Gulp
gulp.js是一个构建系统,这意味着你可以在开发网站时使用它自动执行常见的任务。gulp是基于nodejs的。你定义的任务的gulp源文件或者是gulp文件是JavaScript编写的。(你也可以选择像CoffeeScript的语言)。如果你是一个前端开发那就更完美了。当文件发生改变时,你可以编写任务来组织你的JavaScript和CSS,解析模板,编译LESS。(这只是其中的一些例子)。另外,在语言方面,你可能已经对它很熟悉了。
gulp本身不会做太多事情,但是它有数量庞大的插件可供使用。这些插件可以在plugins page或者npm查找得到。例如, run JSHint, compile your CoffeeScript, run Mocha tests甚至可以更新文件版本号。
还有其他的构建工具可以使用,像 Grunt和最近的Broccoli。但是我相信Gulp是最优越的。(看下面“为什么是Gulp”的内容)。我之前写了一个更长的用JavaScript编写的构建工具的列表( list of build tools)。
Gulp是开源的而且能在GitHub上找到。
安装
安装非常简单。首先,全局安装gulp包。
npm install -g gulp
然后,安装它到你的项目。
npm install --save-dev gulp
使用
让我们创建一个压缩JavaScript文件的任务。新建一个名为gulpfile.js的文件。在这里你可以定义Gulp任务,这些任务将用gulp命令执行。把下面的内容放到你的gulpfile.js文件中。
var gulp = require('gulp'),
uglify = require('gulp-uglify');
gulp.task('minify', function () {
gulp.src('js/app.js')
.pipe(uglify())
.pipe(gulp.dest('build'))
});
通过npm命令npm install –save-dev gulp-uglify安装gulp-uglify,然后通过运行gulp minify命令执行任务。假设你的js文件夹有一个app.js的文件,那么build文件夹就会有一个包含js/app.js压缩内容的app.js文件。
这里发生了什么呢?
我们在gulpfile.js做了几件事。首先,我们加载gulp和gulp-uglify模块:
var gulp = require('gulp'),
uglify = require('gulp-uglify');
然后我们定义了名为minify的任务。当这个任务运行时,它会执行一个函数。
gulp.task('minify', function () {
});
最后是最关键的,我们要定义这个任务做什么事情。
gulp.src('js/app.js')
.pipe(uglify())
.pipe(gulp.dest('build'))
除非你熟悉大多数前端都不太熟悉的“流”,不然上面的代码对你没啥意义。
流
流让你能够通过一些通常较小的函数传递一些数据,这些函数将修改数据,然后将修改后的数据传递给下一个函数。
在上面的例子,gulp.src()通过一个字符串匹配一个或数个文件( 称为“glob”),并创建一个流对象代表这些文件。uglify()方法获得一个文件对象并返回一个含有压缩文件的新文件对象。然后新的文件对象通过管道输出到gulp.dest()函数,该函数会把该文件对象保存到指定的文件。
下面的图展示了这个压缩任务的工作过程:
当只有一个任务时,这个函数实际上并不会做太多的工作。然而,请看看下面的代码:
gulp.task('js', function () {
return gulp.src('js/*.js')
.pipe(jshint())
.pipe(jshint.reporter('default'))
.pipe(uglify())
.pipe(concat('app.js'))
.pipe(gulp.dest('build'));
});
要运行上面的代码,请先安装gulp,gulp-jshint,gulp-uglify和gulp-concat。
这个任务会获取所有js路径下的js文件,然后用JSHint检测这些文件然后打印输出这些文件,输出后压缩每一个文件并把每个文件连接起来,最后把它们保存到build/app.js。任务的执行流程如下图所示:
如果你对Grunt熟悉,你会注意到gulp和grunt有很大的差异。Grunt不能使用流,除此之外,每次执行一个任务都会把任务保存到一个新文件,对于每个任务则不断重复这个过程。结果造成多次访问文件系统而使得执行速度比Gulp慢。
更多关于流的阅读,可以参看“Stream Handbook.”
GULP.SRC()
gulp.src()的作用是:获取一个glob(即匹配一个或多个文件的字符串)或一个glob数组,并返回一个可以通过管道传输到插件的流。
gulp 使用node-glob从glob或者是自定义的glob获取文件。使用例子很容易说明这个过程。
- js/app.js 获取特定的文件
- js/*.js 获取js路径下所有以.js结尾的文件
- js/**/*.js 获取js路径下所有以及子目录下以.js结尾的文件
- !js/app.js 不匹配js/app.js。这对于你要获取所有文件但是排除获取特定的文件很有用。
- *.+(js|css) 匹配根目录所有以.js或者.css结尾的文件
还有一起不常用的特性,更多内容可以查看 Minimatch。
我们来看个例子。一个js文件夹包含一些未压缩和已压缩的文件,现在我们想建立一个压缩任务去压缩这个文件夹未压缩的文件但是不包含.min.js结尾的文件。
gulp.src(['js/**/*.js', '!js/**/*.min.js'])
定义任务
使用gulp.task()去定义一个任务。定义一个简单的任务只需两个属性,任务名和一个执行方法。
gulp.task('greet', function () {
console.log('Hello world!');
});
运行gulp greet,‘Hello world’会在控制台打印出来。
一个任务也可以包含一系列的任务。假设我们定义一个包含css,js和imag的build任务。 我们可以通过指定任务数组而不是函数来实现这一点:
gulp.task('build', ['css', 'js', 'imgs']);
它们是异步执行的,所以你没办法知道js开始执行的时候css是否已经执行完毕--事实上,它可能还没完成。要确保某个任务在另一个任务运行之前已完成运行,可以通过将任务数组与函数组合来指定依赖项。例如,定义一个css任务在执行之前必须等待greet执行完毕,你可以这样做:
gulp.task('css', ['greet'], function () {
// Deal with CSS here
});
现在你可以执行css任务。gulp会先执行greet任务,等待greet完成,才会执行你定义的css任务下的执行函数。
默认任务
你可以定义一个默认任务,当你运行gulp命令时,名字为default的任务会被执行。
gulp.task('default', function () {
// Your default task});
插件
你可以使用超过600个的gulp插件。你可以在plugins page或者在npm上搜索 gulpplugin 找到很多插件。 有些插件带有“gulpfriendly”标志,他们不是插件但能够很好得于gulp协同工作。有一点需要知道,直接在npm搜索的插件可能是已被列入黑名单的(滚动页面最底部,你可以看到很多插件可能就是在黑名单中的插件)。
许多插件非常方便使用,它们有很好的文档并且它们的运行方式是一样的( 通过管道将文件对象流传送到插件上)。它们通常会修改文件(有些文件不会修改,例如验证器),然后返回一个新的文件给下一个插件。
让我们使用插件去扩展前面的js任务吧。
var gulp = require('gulp'),
jshint = require('gulp-jshint'),
uglify = require('gulp-uglify'),
concat = require('gulp-concat');
gulp.task('js', function () {
return gulp.src('js/*.js')
.pipe(jshint())
.pipe(jshint.reporter('default'))
.pipe(uglify())
.pipe(concat('app.js'))
.pipe(gulp.dest('build'));});
我们在这段代码中使用了gulp-jshint,gulp-uglify和gulp-concat 插件。你可以通过查看插件中的README文件,了解插件如何使用,插件的可用属性以及默认配置属性。
你可能注意到JSHint插件被使用了两次。这是因为第一次运行JSHint只是为了把jshint的属性绑定到文件对象,这个过程不会输出任何东西。你可以自己读取JShint属性,也可以将其传递给默认的JShint报告程序或其他报告程序,如JShint Style。
其他两个插件看名字就知道是做什么用的。uglify()函数是压缩代码的,concat('app.js')函数是连接所有文件文件,然后把这些文件都集中到app.js文件中。
GULP-LOAD-PLUGINS
我觉很有用的一个插件是gulp-load-plugins。它可以自动从 package.json 中加载其他插件并把这些插件绑定到对象上。下面是一个非常基本的使用方式:
var gulpLoadPlugins = require('gulp-load-plugins'),
plugins = gulpLoadPlugins();
你可以把所有的代码写到一行上(var plugins = require(‘gulp-load-plugins’)();) ,但是我不喜欢内联require调用。
运行这段代码后,变量 plugins 会包含所有你引入的采用驼峰命名的插件(例如,gulp-ruby-sass 可以通过 plugins.rubySass 加载 ) 。如果这些插件需要,你可以想普通插件那样正常使用这些加载进来的插件。举个例子,我们之前的‘js’任务的插件声明就会减少:
var gulp = require('gulp'),
gulpLoadPlugins = require('gulp-load-plugins'),
plugins = gulpLoadPlugins();
gulp.task('js', function () {
return gulp.src('js/*.js')
.pipe(plugins.jshint())
.pipe(plugins.jshint.reporter('default'))
.pipe(plugins.uglify())
.pipe(plugins.concat('app.js'))
.pipe(gulp.dest('build'));});
假设 package.json 的内容如下所示:
{
"devDependencies": {
"gulp-concat": "~2.2.0",
"gulp-uglify": "~0.2.1",
"gulp-jshint": "~1.5.1",
"gulp": "~3.5.6"
}
}
在这个例子,代码实际上并没有变得很多。然而,对于复杂和很长的gulp文件,加载插件的代码将减少一或者两行。
早在3月份发布的0.4.0版本的gulp-load-plugins已经加入了懒加载,这大大提高了性能。在调用之前,插件不会被加载进来。这意味着你不用担心在 package.json没有被用到的插件会影响性能( 尽管这些插件你应该清理干净)。换句话说,如果运行一个只需要两个插件的任务,它就不会加载其他任务所需的所有插件。
监视文件
gulp可以监视文件的变化,并可以根据文件变化运行一个或者多个任务。这个特性非常有用(对于我来说,这可能是gulp最有用的功能)。使用监视功能,当你保存LESS文件时,gulp会把这个文件翻译为CSS并且刷新浏览器的样式,这个过程你无需做
监视一个或多个文件,可以使用gulp.watch()函数,该函数接受一个glob或globs数组(与gulp.src()相同)以及一个要运行的任务数组或回调。
让我们看一个例子,一个名为build的任务负责将模板文件转化为HTML文件。我们定义一个监视任务,当模板文件发生变化时,运行build任务,把模板文件转化为HTML。看下面的代码:
gulp.task('watch', function () {
gulp.watch('templates/*.tmpl.html', ['build']);});
现在我们改变模板文件,build 任务将运行并生成HTML。
你可以给watch 一个回调事件。在下面的例子中,这个回调函数提供一个event 对象,这个对象包含了关于触发回调事件的一些信息。
gulp.watch('templates/*.tmpl.html', function (event) {
console.log('Event type: ' + event.type); // added, changed, or deleted
console.log('Event path: ' + event.path); // The path of the modified file
});
gulp.watch() 另外一个有趣的特性是它可以返回一个watcher(监视程序对象)。使用watcher 可以侦听其他事件或向watch添加文件。 例如,要同时运行任务列表和调用函数,可以向返回的watcher的 change事件添加侦听器:
var watcher = gulp.watch('templates/*.tmpl.html', ['build']);
watcher.on('change', function (event) {
console.log('Event type: ' + event.type); // added, changed, or deleted
console.log('Event path: ' + event.path); // The path of the modified file
});
除了change事件,你还可以监听其他的事件:
- end 文件监听结束(当文件改变的时候,任务及其回调都不会再被调用时)
- error 文件出现了错误时
- ready 当文件被发现并且被监听时
- nomatch 当变量不匹配任何文件文件时
watcher 对象也有一些方法可供调用:
- watcher.end() 停止watcher的监听(所以的任务或者回调都不会被执行)
- watcher.files() 返回被watcher监听的文件列表
- watcher.add(glob) 给匹配指定文件的watcher增加文件 (同时,可以设置一个回调函数作为第二个参数)
- watcher.remove(filepath) 在watcher中移除指定文件
重新加载浏览器的更改
当文件改变时,你可以让Gulp任务重新加载或则更新浏览器。这有两种方法可以做到。第一是使用LiveReload插件,第二是使用BrowserSync。
LIVERELOAD
LiveReload 融合了浏览器的拓展程序,可以重新加载浏览器只要它监测到有文件发生变化。完成这个动作可以使用 gulp-watch 或则使用我前面提到的内置的 gulp.watch() 。以下是从gulp-livereload repository的README 文件里的例子:
var gulp = require('gulp'),
less = require('gulp-less'),
livereload = require('gulp-livereload'),
watch = require('gulp-watch');
gulp.task('less', function() {
gulp.src('less/*.less')
.pipe(watch())
.pipe(less())
.pipe(gulp.dest('css'))
.pipe(livereload());});
上面定义的任务会监测所有匹配less/*.less 的文件 。当这些文件发生变化时,任务会生成CSS保存到css文件夹中并重新加载浏览器。
BROWSERSYNC
BrowserSync是LiveReload 的替代方案,在重新加载浏览器方面和LiveReload 很相似。但是BrowserSync有更多的功能。
当你改变代码时,BrowserSync 要么重新加载页面,要么插入CSS(如果改变的文件是CSS),这意味着页面不需要刷新。如果你的页面不需要刷新,这就非常有用了。假设你正在开发要点击四次才能进到页面的单页面程序,如果刷新页面则整个页面就会回到最开始的页面。使用LiveReload,每次代码发生改变,你都需要点击四次进入到你页面。然而,使用BrowserSync的话,每次改变CSS代码,只会把改变注入到程序,因此你需要不断点击返回到页面。
BrowserSync是更好的方式让你在浏览器测试你的设计。
BrowserSync还同步点击、表单以及浏览器滚动的位置。你可以打开你电脑的或者iPhone的几个浏览器。这些浏览器的变化都会被跟踪,当你滚动其中一个页面,所有设备的这个页面将会滚动。当你在表单输入内容时,这写内容会在所有窗口被输入。当你不想要这样的效果,你可以关掉它。
BrowserSync不需要浏览器插件,因为它只服务于你的文件。
BrowserSync不需要浏览器插件,因为它只服务于你的文件(或者代理它们,如果它们是动态的话)和提供一个在浏览器和服务器之间打开套接字的脚本。
BrowserSync实际上并不是Gulp插件,因为BrowserSync不处理文件,所以它不作为一个插件而工作。然而, NPM的BrowserSync 可以直接在gulp上调用。首先,通过npm安装它:
npm install --save-dev browser-sync
然后,在gulpfile.js上启动BrowserSync并监听一些文件:
var gulp = require('gulp'),
browserSync = require('browser-sync');
gulp.task('browser-sync', function () {
var files = [
'app/**/*.html',
'app/assets/css/**/*.css',
'app/assets/imgs/**/*.png',
'app/assets/js/**/*.js'
];
browserSync.init(files, {
server: {
baseDir: './app'
}
});
});
执行gulp browser-sync命令会监听所有匹配的文件的变化并启动服务于app目录下文件的服务器。
browsersync的开发人员已经在他BrowserSync + Gulp存储库中编写了一些可以文档,这些文档能告诉你browsersync可以做些什么。
为什么是Gulp?
如前所述,Gulp是大多数JavaScript可用的构建工具之一,其他不是JavaScript写的构建工具也可以使用,包括Rake。为什么不选择它呢?
在JavaScript中最受欢迎的构建工具是Grunt和Gulp。Grunt在2013年很流行和完全改变了许多人开发网站的模式。它有数千个插件可供使用。它们能做很多事情,例如检查代码,压缩并合并代码,通过Bower安装包,启动服务器。
Grunt与Gulp的非常不同,Grunt只能用插件来执行处理文件的小任务。但是gulp处理文件不需要插件,因为任务只是JavaScript(与Grunt使用的大型对象不同)。另外,你只需以正常方启动Express服务器。
Grunt往往是对任务配置过度,它会包含大量的属性的对象,即使你根本就不需要这些功能属性。然而,同样的任务,Gulp可能只需要短短的几行代码。让我们看一个例子,定义一个css任务,负责把LESS转化为CSS,然后在上面运行Autoprefixer。
grunt.initConfig({
less: {
development: {
files: {
"build/tmp/app.css": "assets/app.less"
}
}
},
autoprefixer: {
options: {
browsers: ['last 2 version', 'ie 8', 'ie 9']
},
multiple_files: {
expand: true,
flatten: true,
src: 'build/tmp/app.css',
dest: 'build/'
}
}
});
grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-autoprefixer');
grunt.registerTask('css', ['less', 'autoprefixer']);
将上面的代码于下面执行相同任务的gulpfile.js文件做比较:
var gulp = require('gulp'),
less = require('gulp-less'),
autoprefix = require('gulp-autoprefixer');
gulp.task('css',
function() {
gulp.src('assets/app.less').pipe(less()).pipe(autoprefix('last 2 version', 'ie 8', 'ie 9')).pipe(gulp.dest('build'));
});
gulpfile.js 文件看起来可读性更强,代码体积更小。
因为crunt对文件系统的读取远比使用流的gulp来得频繁,所以gulp比grunt快得多。对于轻量的LESS文件,gulpfile.js文件执行只需要6毫秒,gruntfile.js通常需要50毫秒--比gulp多了8倍之多。这只是一个小例子,对于更庞大的文件,grunt执行所需时间增长得更明显。