Gulp 介绍






4.97/5 (31投票s)
Gulp 是一个现代化的 Web 技术构建系统。它也集成到了 ASP.NET vNext 中,并可在许多场景下使用。
目录
引言
2015 年终于到来了。这是技术取得更多奇妙进展的一年。这也是 Visual Studio 2015 将要发布的一年。又一年,又一个 Visual Studio 版本,大多数人可能会这样说。但这一切背后还有更多!首先,这将是第一个随 .NET Compiler Platform(“Roslyn”)一起发布的版本。这不仅包括了新版本的 C#(和 VB)编程语言,还包括了建立在“编译器即服务”概念上的创新工具。然后,这将影响其他产品...
其中一个产品就是 ASP.NET。该框架已从头开始重写。它已经与 System.Web 依赖解耦,并且可以轻松地跨平台。它还具有即时编译功能,无需触碰硬盘上的二进制文件。这都要归功于 Roslyn 以及在改进 Web 框架方面付出的无数努力。无论您是想开发 Web Forms、MVC 还是一个轻量级的 Web API - 新的 ASP.NET 绝对值得称赞。
这种改变不仅对后端开发有影响,也对前端爱好者有影响。在 ASP.NET MVC 4 中,团队加入了诸如 bundling 之类的功能。这个想法很简单:根据某些规则合并(并在适当的情况下压缩)内容。当涉及到 SASS 或 TypeScript 等高级语言的特殊文件时,事情变得更加复杂。通常的解决方案是在源文件更改时将其编译为较低级别的变体(CSS 或 JavaScript)。然后,已编译的文件将触发分布式包的更改。
几年后,这似乎仍然有效,但与纯前端 Web 开发者所做的工作相去甚远。在这里,我们设置了一个构建过程,它基本上就像以前的 makefile 一样工作。我们声明我们拥有什么以及我们想做什么。最终,我们得到一个可以部署的结果。一旦达到部署阶段,我们就需要运行 makefile 来获取更新版本。没有智能程序会一直检查更改。不需要文件监视器。
因此,ASP.NET 5(即 MVC 6,目前称为“vNext”)将移除 bundling 和相关功能,并依赖于前端构建过程。Makefile 是可能的,但为什么不立即与“酷孩子”们一起开始玩呢?因此,ASP.NET 团队开箱即用地集成了两个重要的构建系统(并带有 IDE 集成):已建立的 Grunt 任务运行器及其较新的竞争对手 Gulp。在本文中,我们将仔细研究 Gulp。本文本身是受 ASP.NET 6 的启发,但不要求、也不使用任何 ASP.NET 6 的功能。每个示例都以静态文件的形式给出。
背景
在过去的 70 年里,计算领域发生了很大变化。编程语言不断发展,硬件不断改进,一切都已连接起来。软件开发改进的最重要驱动力之一是构建自动化。其理念是拥有多个或多或少独立的源文件,这些文件需要构建和链接在一起。独立性很重要,因为构建可能需要一段时间,因此将构建过程仅限于自上次成功构建以来已更改的文件是有益的。
整个描述听起来已经是一个自动化的理想案例。我们应该有一个程序来为我们进行检查,对吧?最初,编写了一系列脚本来完成解耦、集成构建的目标。但是,那些被可重用性理念吸引的聪明人将这个概念进一步发展,并创建了一个名为 make
的系统。它基于依赖的概念。一个目标(文件)有一些依赖项。这些依赖项可能又有一些依赖项。为了解析依赖项,必须遵循一个规则。这个规则可能涉及修改时间的比较等等。
Make 是一个非常好的工具,它超越了 C/C++ 或 Fortran 的编译。它也非常适合 Web 开发。但它高度依赖于其他脚本或可执行文件。那么,为什么不直接基于 Node 来开始 JavaScript(或 Web)的开发呢?最大的优势是同构的架构。人们不会混合语言,构建依赖项也会立即减少。同样吸引人的是包管理器 npm
,它代表了一个与系统级包管理器分离的源。因此,我们有了明确的关注点分离。
现在我们已经讨论了需要 Node 的专用构建系统,我们需要介绍 Grunt 任务运行器。Grunt 是 Node 上最早的构建自动化系统之一,可能是最知名和最成熟的前端构建工具。我们为什么应该考虑 Grunt 而不是其他工具?
Grunt 的优势
Grunt 将自己描述为一个任务运行器。显然,“任务”的概念非常重要。我们将构建过程分组为任务,然后可以将这些任务链接在一起(形成依赖关系)或单独执行。主要思想是完全使用插件。任何代码或脚本形式的东西都必须由插件涵盖。
Grunt 还附带一个漂亮的标志,如下所示
对插件的要求不仅完全省略了脚本,还强调了配置级别。Grunt 被认为是一个“配置优先于脚本”的构建自动化系统。它的插件是专用工具,必须紧密地融入 Grunt 的生态系统。这听起来是个好主意,但主要原则(类似的使用方式、一致性)仍然取决于插件作者。尽管如此,配置方面似乎给了我们一个共同点,这绝对是一件好事。
下一个图显示了使用 Grunt 任务运行器的基本构建过程。我们可以看到每个任务都从磁盘开始,并以磁盘结束。无法在内存中继续处理源文件。因此,中间(临时)目录是强制性的。
配置通常在一个简单的 JavaScript 对象中提供。这不一定是 JSON 对象(JSON 对象比纯 JavaScript 对象具有更严格的语法)。显然,Grunt 会考虑配置。Grunt 会读取特定部分以识别插件。然后会使用提供的选项调用这些插件。
Grunt 有很多插件。这也是必需的。我们已经看到 Grunt 要求脚本适应其生态系统。因此,任何可以从 Grunt 中调用的东西都是 Grunt 插件。没有很多插件,Grunt 会相当无用。Grunt 插件生态系统的另一个有趣之处在于许多插件的丰富性。有不止几个插件做的不仅仅是一件事。另一方面,这也是可以理解的,因为插件是必需的,而且编写一个功能强大的插件似乎比编写多个轻量级插件更容易。这也是绕过基于文件的范例限制的经典方法。
Grunt 最大的优势在于其庞大的社区和丰富的插件库。与一个成熟的产品竞争肯定很困难。因此,需要一些不同的东西,它不仅仅是做得更好,而且其核心是不同的,但又熟悉。
有关 Grunt 的更多信息,请访问官方主页:gruntjs.com。
为什么选择 Gulp?
Grunt 是一个强大的竞争对手。因此,Gulp 专注于几个关键点:首先也是最重要的,速度。这种速度有两种形式。我们不仅拥有卓越的构建系统(通常不那么重要),还拥有卓越的开发速度(这更重要)。为什么构建速度更优?我们将看到 Gulp 使用流式处理,它会将工作版本保留在内存中,直到我们决定在硬盘上创建一个包含当前内容的版本。简单的 CS101 告诉我们,计算机主内存的延迟和传输速率比普通的硬盘驱动器更好。
第二种形式更具争议性,但更为重要。通过允许运行任何代码,Gulp 可以完全摆脱插件。当然,Gulp 也有插件(它们确实是关键且有用的),但它们不是强制性的。因此,Gulp 不需要像 Grunt 那样多的插件。另外,我们通常可以 hack 一些特殊的功能,而不是去搜索(或编写)插件。Gulp 使用“代码优先于配置”,这对于大多数开发者来说可能更好。配置需要文档,编码需要技能。我们已经具备后者,因此应该依赖它。
回到 Gulp 的关键点。流式处理通过一个流畅的 API 来集中处理,这很像 Shell 的工作方式。我们将一个计算的结果传递给下一个。每个计算都使用一个虚拟文件系统(称为 Vinyl),它是用 JavaScript 编写的,并且永远不应该离开内存。
Gulp 非常认真地对待它的名字,这体现在以下标志上
流式处理概念是 Gulp 的核心。所有其他概念都源于此。因此,在本文中需要花费大部分篇幅来讨论它。如前所述,我们不生成临时文件。文件的工件保存在内存中。它也在那里被修改。这很强大,因为它不仅提高了修改速率,还能够将命令链接在一起。
剩下的就是一个函数式框架,具有直接的 API,将(硬盘上的)临时文件数量减少到最少(通常为零)。下图显示了使用 Gulp 构建过程的基本方案。与前面显示的 Grunt 的图像相比,这里的变化是至关重要的。子任务不直接输出,而是返回一个修改后的流。这个流可以通过另一个子任务(由 Gulp 直接提供)转储。
实际上,API 因此减少到只有几个方法。我们需要一个用于定义任务的方法,其中包括潜在的依赖项等。我们还需要一个将现有文件转换为虚拟文件的方法,即加载文件内容到内存或将文件转换为流。最后,我们还需要一个将当前虚拟文件转储到硬盘的方法,即写出实际文件。我们可能还想要像文件系统监视器这样的东西直接在 API 中,但这更多的是一种“糖”,可以由插件提供。
当然,给出的列表不是凭空想象的,而是实际存在的。以下方法是 Gulp API 的核心
- 使用
gulp.task(name, fn)
定义任务 - 使用
gulp.src(glob)
将源转换为流 - 使用
gulp.dest(folder)
转储流 - 使用
gulp.watch(glob, fn)
安装文件系统监视器
使用 Gulp 定义的构建过程称为 gulpfile。它是纯粹的(当然也是有效的)JavaScript 代码。代码通过 node.js 执行,这意味着这里基本上没有包装器魔法。但是,我们不直接通过 Node 执行 gulpfile,而是通过一个名为 gulp 的自定义可执行文件。这很有用,因为它允许我们在不更改 gulpfile 的情况下运行任何任务。在 gulpfile 中,我们可以自由地使用任何我们想要的 Node 模块(或 JS 代码)。我们主要对 Gulp 插件感兴趣,它们共享一个简单的概念:它们都非常基础!
基础插件或代码只做一件事,但做得非常好。它接受任意输入,然后执行其工作并返回一些输出,以便另一个基础插件可以进行一些工作。它是一个小的基于流的黑盒子。
这一切导致了一些直接的后果。Gulpfile 往往更小,更容易阅读,至少对于有 JavaScript 或通用编程背景的人来说是这样。让我们看一个例子。
首先是 Grunt 配置
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']);
相比之下,用 Gulp 可以用以下方式表达相同的代码
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'));
});
当然,这只是一个例子,我们不应该过度依赖代码行数的比较。它们具有误导性,肯定会给我们带来错误的教训。比较可读性也是一个错误的判断,考虑到例子相当小,而且实际的配置可能会更大。废话不多说,让我们开始研究 Gulp,看看这里到底发生了什么。
入门
在接下来的几个小节中,我们将学习掌握 Gulp 所需的一切。我们将安装它,创建我们的第一个 gulpfile,并学习任务、插件以及像 glob 模式和 watches 这样的重要主题。
安装
在我们能够使用 Gulp 之前,应该先安装它。这里我们假设 Node 和 npm 都已经安装并设置好。现在我们只需要全局安装 Gulp,这将启用 gulp
命令。
我们运行(需要管理员权限,即使用 sudo
或提升的 shell)
npm install -g gulp
-g
标志将告诉 npm
全局安装 Gulp。但是,全局安装仅对 gulp
命令是必需的。对于在任何项目中都使用 gulpfile,这还不够。要使用 gulpfile,我们需要一个项目目录,这意味着该目录是 npm 包文件(package.json)的根目录。假设我们要创建一个这样的文件
npm init npm install --save-dev gulp
在这里,我们初始化了一个基于 npm 的项目目录(它将是当前目录,所以要注意在哪里执行命令)。我们还在项目中安装了一个本地版本的 gulp,它已被添加到项目的依赖项中(--save-dev
)。后者很重要,因为它允许我们从 VCS 中省略依赖项,并在稍后键入 npm install
时重新安装所有依赖项。这正是我们通常想要的。
现在我们可以开始工作了!但等等... 为什么我们需要另一个本地版本的 Gulp?原因很简单:为每个项目提供一个本地版本的 Gulp,可以确保每个项目都有它所需的确切版本的 Gulp。package.json
不仅列出了依赖项名称,还列出了依赖项版本。当然,一个人可以允许使用较新版本的包,但通常包倾向于尽可能精确地指定版本。这最大限度地降低了遇到重大更改的风险。
Gulp 意识到这种方法,因此努力允许在同一台机器上使用特定版本的 Gulp。这是通过每个项目都有本地版本来实现的。最终,我们牺牲了一些磁盘空间,但获得了在另一台只做了克隆源代码并安装声明的依赖项(本地)的机器上触发构建系统时其能正常工作的保证。
压缩 JavaScript 文件
让我们从一个例子开始。考虑以下目录结构
. Project Directory |--. bin |--. node_modules |--. src | |--. css | |--. html | |--. js
我们在项目目录中有一个 gulpfile.js。我们的目标是生成可以在 Web 上部署的输出。因此,我们需要一些任务,例如,来最小化(并组合)某些 JavaScript 文件。我们假设 src 目录包含所有原始源代码,而 bin 目录将是我们构建过程的目标。
让我们看一个 gulpfile,它接受一个 JavaScript 文件(名为 app.js),将其最小化并存储在 bin 文件夹中(没有子目录,直接放在那里)。
var gulp = require('gulp'),
uglify = require('gulp-uglify');
gulp.task('minify-js', function() {
gulp.src('src/js/app.js')
.pipe(uglify())
.pipe(gulp.dest('bin'));
});
这里发生了什么?我们开始包含两个模块。第一个是 Gulp 本身。显然,这非常重要。第二个是 gulp-uglify
,它基本上是一个 JavaScript 最小化工具。这里有一个构造函数,这意味着我们需要在某个时候调用 uglify
函数。这是 Gulp 插件的典型模式,我们稍后会看到。
task
方法用于创建一个名为“minify-js”的新任务。这个名字是任意的,但应该是一个好的描述特定任务的名称。任务本身以回调的形式给出。这里我们创建一个新的流,它表示位于 src/js/app.js 的文件的内容,然后将其通过管道传递给由 uglify
构造函数创建的另一个函数。最后,我们将修改后的流通过管道传递给通过提供目标目录参数调用 dest
方法创建的函数。
合并和压缩 CSS 文件
让我们重写前面的例子,对 CSS 执行相同的操作,但不是单个 CSS 文件,而是大量文件。通过连接它们,我们可以将多个 CSS 文件合并到一个文件中。这样,我们可以节省一些请求。客户端优化是让客户满意并获得更多业务价值的绝佳技术。好的,但是 CSS 文件的顺序非常关键,因为某些规则可能会覆盖之前的声明。
因此,我们不能只提供一个匹配文件的字符串,也可以提供一个字符串数组。顺序很重要。最后,我们还将使用另一个最小化插件,因为最小化 CSS 的规则与压缩 JavaScript 代码的规则相当不同。
var gulp = require('gulp'),
concat = require('gulp-concat'),
minifyCss = require('gulp-minify-css');
gulp.task('minify-css', function() {
gulp.src(['src/css/bootstrap.css', 'src/css/jquery.ui.css', 'src/css/all.css'])
.pipe(concat('style.css'))
.pipe(minifyCss())
.pipe(gulp.dest('bin'));
});
基本原理是相同的。我们设置插件(这里我们还使用 concat
插件来合并文件),指定任务,并通过管道传输流来调用各种构造函数。
也许我们甚至想存储多个版本。一个只是连接的输出(style.css),另一个是连接和最小化的输出(style.min.css)。Gulp 让我们轻松做到这一点,因为输出也是另一个管道的目标。因此,我们的方案将更改为看起来类似于下图的内容
这在代码中看起来如何?我们将使用 gulp-rename
插件来重命名我们的扩展名。但其余的应该很熟悉
var gulp = require('gulp'),
concat = require('gulp-concat'),
minifyCss = require('gulp-minify-css'),
rename = require('gulp-rename');
gulp.task('minify-css', function() {
gulp.src(['src/css/bootstrap.css', 'src/css/jquery.ui.css', 'src/css/all.css'])
.pipe(concat('style.css'))
.pipe(gulp.dest('bin'))
.pipe(minifyCss())
.pipe(rename({ extname: '.min.css' }))
.pipe(gulp.dest('bin'));
});
这里,管道的整个概念非常有用,可以让我们轻松地连接子任务。但这也很适合仔细查看实际任务,这些任务代表了这些子任务的组合(可以看作是使用流调用的方法)。
任务和依赖
既然我们已经看到了可以做什么,是时候仔细看看任务和依赖了。如前所述,我们需要将所有内容分组为任务。我们可以有一个执行所有操作的单个任务,但强烈不推荐这样做,而且不会非常敏捷。每个任务基本上是一个构建目标。以前,我们指定了诸如压缩 CSS 或压缩 JavaScript 等目标。
每个目标都可以通过 gulp
命令调用。但每个目标也可以被视为其他目标中的依赖项。Gulp 还有一个非常特殊的名为 default
的目标。默认目标是当调用 gulp
命令时,不带任何附加参数而执行的目标。将它视为“make all”类型的指令是一个好习惯。因此,我们通常会提供对所有其他(子)任务的依赖。
这些依赖项以数组的形式提供,其中包含命名各种所需任务(依赖项)的字符串。这是一个可能的 default
任务定义
gulp.task('default', ['clean', 'styles', 'scripts']);
当构建 default
任务时,它需要先运行 clean
任务,然后是 styles
任务,最后是 scripts
任务。顺序很重要,即使 Gulp 可以并行化构建过程(如果流对象未被返回)。因此,clean
任务必须返回流,否则整个系统可能会因为子任务的进度而变得不确定。
让我们使用必需的依赖项定义 clean
任务。这里我们为 src
方法提供了一个附加参数,它提示不应该读取目录的内容。我们唯一关心的是要清理的目录信息。
var gulp = require('gulp'), clean = require('gulp-clean'); gulp.task('clean', function() { return gulp.src('bin', { read: false }) .pipe(clean()); });
在任何情况下都建议返回一个流。有时省略它可能有利,但大多数情况下这可能导致不确定的行为。因此,如果我们不关心(并且知道)应该发生什么,我们希望确保所有内容都按我们指定的顺序构建。
运行 Gulp
现在我们已经设置了 gulpfile,准备开始工作了。我们需要做什么来触发构建?实际上没多少。最简单的情况是,我们在项目目录中运行以下命令。Gulp 将尝试在当前目录中查找 gulpfile。
gulp
这会触发隐式构建过程。隐式版本会查找 gulpfile 中的一个名为 default
的任务并运行它。如果任务不存在(或者没有找到 gulpfile),将显示错误消息。如果脚本包含任何其他错误,也会显示错误消息。
如果我们想更明确一点,可以命名要运行的任务。例如,要合并和最小化所有 CSS 脚本(第二个示例),我们将不得不运行
gulp minify-css
如果我们想运行这个和第一个例子呢?要么我们运行两个命令,要么我们输入 gulp
命令并带有两个参数,如下所示
gulp minify-css minify-js
重要的是要强调实际运行的是本地版本的 Gulp。gulp
命令可能是全局的,但最终执行的代码始终是本地的。
使用 globs 选择文件
我们已经看到了两种指定文件的方式:单个字符串和字符串数组。但每个字符串实际上都有特殊含义,并使用一个特殊的包进行解析。事实上,我们因此倾向于称这些字符串为 globs,因为它们必须遵循特定的规则。
通常,globbing 指的是基于通配符的模式匹配。“glob”这个名词用于指代特定的模式,例如“使用 glob *.log 来匹配所有日志文件”。它们的表示法比正则表达式简单,并且没有它们那么强大的表达能力。然而,这种格式包含了选择和取消选择基于名称的文件所需的所有功能。
Gulp 中允许的 glob 模式基于 glob 包(可在 npmjs.com/package/glob 找到)。它是常用模式的扩展,并具有额外的类似正则表达式的功能。
例如,在解析路径部分模式之前,字符串中的大括号部分会展开为一组。大括号部分以 {
开头,以 }
结尾。一个大括号部分可以包含任意数量的逗号分隔的部分。大括号部分也可以包含斜杠字符,所以 a{/b/c,bcd}
将展开为 a/b/c
和 abcd
。
以下字符在路径部分中使用时具有特殊的魔术含义
*
:它匹配单个路径部分中的 0 个或多个字符(因此不包括分隔符,即/
)。?
:它匹配单个路径部分中的 1 个字符。[...]
范围:匹配字符范围,类似于正则表达式的范围。|
:它在组(由(...)
表示)中分隔各种可能性。
组本身必须有一个前缀来描述它们的用法。例如,glob
!(pattern1|pattern2|pattern3)
匹配任何不匹配提供的模式(pattern1
、pattern2
、pattern3
)的内容。
此外,以下前缀与组一起使用
?
:它匹配模式零次或一次出现。+
:如果至少有一个模式必须匹配,则此项很有用。*
:用于匹配模式零次或多次出现。@
:匹配提供的模式中的一个。
特殊情况是双星号,用 **
表示。如果这样的组合单独出现在路径部分中,则它匹配零个或多个目录和子目录。
理论之外,这在实践中意味着什么?如果我们使用 "js/app.js"
,那么我们将精确匹配该路径。如果我们想要除了前面给出的文件之外的所有 JavaScript 文件,我们可以指定 "js/!(app.js)"
。如果我们想要给定目录中的所有 TypeScript 和 JavaScript 文件,我们可以自由地运行 "js/*.+(js|ts)
。
重要插件
我们已经认识到 Gulp 功能强大,并且通常不依赖于特定插件。然而,插件非常有益,并且肯定会让我们生活更轻松。一般来说,我们总是会优先选择专门为 Gulp 设计的插件,而不是需要更多工作才能集成的代码。
插件通过 npm
安装,就像 Gulp 本身一样。它们总是本地安装的。例如,安装 uglify
插件将是
npm install --save-dev gulp-uglify
这些插件在使用上往往非常小巧简洁。通常,关于它们的全部知识都可以在它们的 GitHub 存储库(这是大多数插件托管的地方)或 npm 系统中找到。否则,简短地查看源代码也是可以管理的。
在本节中,将介绍用于通用前端 Web 开发任务的最重要插件。
左键单击 gadget 并拖动以移动它。左键单击 gadget 的右下角并拖动以调整其大小。右键单击 gadget 以访问其属性。
这里我们将介绍一些在各种任务中常用的插件。它们可能对 CSS、JavaScript 或其他类型的转换很有用。以下这些似乎非常有用
gulp-concat
(我们已经看到过了)gulp-clean
(我们也看到过了)gulp-notify
(对于通知非常有用)gulp-livereload
(稍后将详细讨论)
以下示例使用 notify 插件在文件合并之前和之后输出消息。
var gulp = require('gulp'),
notify = require('gulp-notify'),
concat = require('gulp-concat');
gulp.task('concat', function() {
gulp.src('*.js')
.pipe(notify("Alright, lets merge these files!"))
.pipe(concat('concat.js'))
.pipe(notify("Files successfully merged ..."))
.pipe(gulp.dest('build'));
});
CSS
CSS 是进行前端构建过程的原始主题之一。毕竟,CSS 预处理器已经存在了一段时间,并且可能比编译为 JavaScript 的语言更有用或被更广泛使用。
在众多插件中,以下插件值得一提
gulp-sass
(将 SASS 转换为 CSS)gulp-less
(将 LESS 转换为 CSS)gulp-minify-css
(我们已经看到过了)gulp-autoprefixer
(自动插入供应商特定前缀)gulp-imagemin
(压缩图像)
Autoprefixer 非常有用,但也很依赖于我们的需求(大多数情况下,调用 prefixer()
而不带任何参数就足够了,但有些人可能需要支持最旧的浏览器 - 因此需要更多选项)。因此,我们将关注图像优化器,它可以真正节省空间。如前所述,前端性能优化永远不会错,并且应该始终应用。
var gulp = require('gulp'),
imagemin = require('gulp-imagemin');
gulp.task('images', function() {
return gulp.src('src/images/**/*')
.pipe(imagemin({ optimizationLevel: 3, progressive: true, interlaced: true }))
.pipe(gulp.dest('bin/images'));
});
JavaScript
CSS 非常重要,但 JavaScript 似乎更重要。我们非常关心这一点。我们不仅想在部署前检测到错误,还想转换代码,甚至提取文档。然而,错误检测是最核心的功能之一。我们不仅想运行潜在的单元测试,还想进行一些 linting(通常称为静态代码分析)。Lint 这个词也可以更广泛地指代语法上的差异,尤其是在 JavaScript 等解释型语言中。
gulp-jshint
(JavaScript 的 linting)gulp-uglify
(我们已经看到过了)gulp-jscs
(检查是否违反了提供的样式指南)gulp-jsdoc
(提取文档)
以下示例对我们 src 文件夹(及其子文件夹)中所有 JavaScript 文件执行一些静态代码分析。Linter 必须调用两次。一次是使用通常的创建者函数,执行分析;另一次是使用一个报告器,然后评估分析结果。默认报告器将输出结果。如果发生错误,这不会导致任何失败,这就是为什么还有一个报告器 - 所谓的 *fail reporter*。我们可以使用那个,通过 jshint.reporter('fail')
。
该示例仅使用默认报告器,但输出附加的(错误代码)信息。
var gulp = require('gulp'),
jshint = require('gulp-jshint');
gulp.task('lint', function() {
return gulp.src('./src/**/*.js')
.pipe(jshint())
.pipe(jshint.reporter('default', { verbose: true }));
});
缓存及更多
到目前为止,一切都是按需构建的。对于习惯于 make 规则系统的人来说,这可能是巨大的倒退,它总是比较上次(目标)创建和上次(源文件)修改时间。但总的来说,这种过程对于 Web 开发来说既不可能,也并非非常有效。当然,如果只有一个 JavaScript 文件发生变化,我们就不需要重新编译整个 CSS。但这就是我们拥有不同任务的原因。另一方面,如果我们合并和最小化 JavaScript 文件,即使只有一个文件发生变化,我们也需要调用整个过程。
然而,有时我们可以减少磁盘访问并插入一些缓存。以下包可用于进行一些过滤并优化构建过程
gulp-cached
(过滤器,用于排除未更改的文件并缓存结果)gulp-remember
(过滤器,用于与之前的,但被省略的结果合并)gulp-changed
(过滤器,用于仅缩减到已更改的文件)
以下示例使用了 gulp-remember
和 gulp-cached
来减少最小化工作量。首先,我们只使用已更改的子集来进行 uglification,然后我们添加之前跳过的内容。想法是结果(合并的文件)无法被缓存或记住,但单个文件(源文件和最小化后的文件)可以。因此,缓存做了两件事:构建缓存并使用缓存(即过滤掉)。Remember 也做了两件事:记住流结果并与之前的结果合并。
var gulp = require('gulp'),
cached = require('gulp-cached'),
uglify = require('gulp-uglify'),
remember = require('gulp-remember'),
concat = require('gulp-concat');
gulp.task('script', function(){
return gulp.src('src/js/*.js')
.pipe(cached())
.pipe(uglify())
.pipe(remember())
.pipe(concat('app.js'))
.pipe(gulp.dest('bin/js'));
});
如果我们省略合并文件的子任务,我们可以使用 gulp-changed
插件来实现这一点,它兼具这两者。然而,它更加激进,因此不能在前面的示例中使用。
var gulp = require('gulp'),
changed = require('gulp-changed'),
uglify = require('gulp-uglify');
gulp.task('script', function(){
return gulp.src('src/js/*.js')
.pipe(changed('bin/js'))
.pipe(uglify())
.pipe(gulp.dest('bin/js'));
});
因此,缓存的整个概念并不简单,而且非常微妙。大多数时候,等待稍长时间但拥有无错误构建过程(不排除必需文件)是更好的选择。
LiveReload 和 BrowserSync
有多个插件可以自动处理用于查看输出的 Web 浏览器。结合 watches(将在下面更详细地介绍),它们构成了一个很棒的组合。想法是启动一个 Web 服务器,通过 WebSocket 连接到 Web 浏览器。一旦 Web 服务器被调用了一个特殊命令,它就会向所有连接的 Web 浏览器发送一条刷新消息。有更高级的场景和用法模型,但对于入门来说,这些知识已经足够了。
在以下示例中,选择了 browser-sync
插件。它不包含 gulp 前缀,因此独立于 gulp。选择这个特定 Node 模块的原因很简单:它不需要任何浏览器插件。提供服务器实现就足够了。
var gulp = require('gulp'),
minify = require('gulp-uglify'),
sync = require('browser-sync');
gulp.task('js', function() {
return gulp.src('src/*.js')
.pipe(minify())
.pipe(gulp.dest('bin'));
});
gulp.task('html', function() {
return gulp.src('src/*.html')
.pipe(gulp.dest('bin'));
});
gulp.task('sync', function() {
sync({ server: { baseDir: 'bin' } });
});
gulp.task('default', ['html', 'js', 'sync'], function() {
gulp.watch(files.js, ['js', sync.reload]);
gulp.watch(files.html, ['html', sync.reload]);
});
在下一节中,我们还将简要介绍 livereload
插件,它很相似,但更重量级。
Watch
文件系统监视器可以实现同步/实时构建过程,非常方便。我们设置一个文件系统监视器,一旦源文件发生更改就会触发。然后,处理程序会调用构建过程,该过程不仅提供即时的构建成功/失败报告,还提供始终最新的输出目录。
这样的系统似乎需求很高,这就是为什么 Gulp 创建者将 watches 集成到开箱即用的功能中的原因。在 Gulp 中,watches 与 glob 对象(就像遵循描述模式的字符串)和依赖任务一起工作。一旦任何匹配 glob 的文件发生更改,就会执行依赖任务。
一个简单的例子如下
gulp.task('watch-js', function() {
gulp.watch('./src/js/*.js', ['js']);
});
在这里,我们创建了一个任务(该任务被命名为 watch-js
),它只将文件监视器绑定到 src/js 文件夹中的所有 *.js 文件。一旦任何文件发生更改,js
任务就会被触发。但我们不限于此用法。更有趣的是已经描述的浏览器同步案例。在这里,我们可以触发浏览器重新加载。当然,这不是一个简单的任务,而是一个回调,但 Gulp 提供了一个丰富的事件系统,可以通过 on
方法访问。事件称为 change
。我们只需绑定 livereload
服务器实例的 changed
方法。
gulp.task('watch', function() {
// Rebuild Tasks
livereload.listen();
gulp.watch(['src/**'])
.on('change', livereload.changed);
});
将文件系统监视器和任务绑定在一起非常简单(而且非常方便)。这实际上是 Gulp 的一个独特卖点。但编写自己的插件也相当简单。
编写插件
编写自己的插件也是可能的。出乎意料的是,创建这样的插件实际上相当容易,特别是遵循给定的样板代码。我们也应该遵循返回一个创建者函数的模式,该函数创建一个新的 through2
对象,并启用 objectMode
。through2
是 Node *streams2 Transform* 的一个微型包装器,用于避免显式的子类化噪音。
在创建自己的插件之前,我们应该仔细阅读插件指南。我们可以在 github.com/gulpjs/gulp/blob/master/docs/writing-a-plugin/guidelines.md 找到该指南。当然,我们应该先查看是否有类似的插件,它可能是一个很好的基础,并且可以搜索贡献。如果没有,那么我们需要检查我们想要的插件是否足够基础。否则,我们可能应该将其分解成更多插件(并重新评估这些较小的插件是否已可用)。
最后,指南还将告诉我们该怎么做。首先,我们应该独立于 Gulp。那是什么?没错!Gulp 插件独立于 Gulp 本身。如果我们仔细想想,这是完全有道理的。毕竟,Gulp 只提供用于定义任务和运行它们的有用方法。但是插件在任务中使用,因此独立于常规赋值。然而,应该包含的一个依赖项是 gulp-util
。它包含了从通知连接到错误处理的所有内容。如果我们抛出一个异常,我们应该通过 gulp-util
模块提供的 PluginError
类来做到。
一旦我们准备发布插件,我们也应该使用 gulpplugin
标签。在我们的 package.json 中添加 gulpplugin
作为关键字是有益的,这样我们的插件也会出现在官方搜索结果中。
那么样板代码是什么样的?
var through = require('through2'), gutil = require('gulp-util'), PluginError = gutil.PluginError; module.exports = function() { return through.obj(function(file, enc, cb) { /* ... */ })); };
那么回调中的选项是什么?首先,我们有虚拟文件,这是最重要的参数。我们应该通过 file.isNull()
测试空集,在这种情况下我们直接返回。否则,我们需要区分缓冲区 file.isBuffer()
和流:file.isStream()
。我们也可以通过 file.isDirectory()
测试给定记录是否为目录。
第二个选项是文件的编码。这只对文本文件有意义,但在某些情况下可能很有用。最后,提供了回调 cb
,它用于返回一个(可能已修改的)流,例如 cb(null, file);
。
使用代码
本文附带的源代码是 GitHub 存储库的转储,该存储库包含大量 Gulp 示例。您可以通过 github.com/FlorianRappl/GulpSamples 找到该存储库。代码是纯粹的,即不包含依赖项。您需要在特定示例的目录中运行 npm install
。在运行任何 gulp
命令之前,您还应该已经安装了 Gulp。其他要求,如 Node 和 npm,是显而易见的。
有可能一两个示例根据您尝试运行的时间而不起作用。如果您直接从 GitHub 获取样本的当前版本,则此几率肯定会降低。但即使那样,Gulp 或某些插件也可能发生重大更改。在这种情况下,样本将需要更新。
Gulp 还是 Grunt?
现在,我当然是使用 Gulp 的坚定支持者。然而,这只是个人观点。如果您正在使用 Grunt(或其他构建系统),并觉得它是适合您的,那么就坚持使用它。构建系统的作用是什么?它应该做好它的工作。它不应该妨碍您。您不应该调试构建系统。
Gulp 在很多方面都表现出色,但它并不完美。然而,Grunt 也并非没有缺陷。两种系统都有其优点和缺点。如果您有机会尝试一下,我唯一的建议就是都试试。如果您完全从事编程,那么 Gulp 可能会感觉更合适。如果您更侧重于设计或管理,那么 Grunt 可能是您的理想选择。
如果您问 Preslav Rachev,Gulp 是否让 Grunt 过时了,您可能会听到以下内容:“不,原因与汉堡王没有让麦当劳过时一样。事实是,如果您开始使用基于 JS 的构建系统,很有可能会立即选择 Gulp。它的前景似乎更光明一些,而且它已经被大量项目采用。但是,如果您仍然在使用 Grunt 并且对使用它感到满意,那么没什么可担心的 - 社区仍然在那里,比 Gulp 大,并且还在增长。Grunt 已经有将近两年的优势,我相信大型项目维护者会尽可能长时间地坚持使用它。使用 Grunt 的另一个优点是,如果您只需要一套简单内置的任务,那么您会觉得它很方便,而 Gulp 尽管非常灵活,却会让你花费比需要更多的时间在理论上。与项目相关的任何事情一样,您应该根据具体情况进行选择。”
一如既往,选择适合这项工作的工具,包括您的个人经验和偏好。
兴趣点
本文是去年在慕尼黑 WebTechConference 上的一次演讲的详细版本。我认为这次演讲留下了深刻的印象,大多数听众至少考虑尝试 Gulp。我收到了关于使用 Gulp 的一些非常积极的反馈。对我个人来说,很明显 Gulp 满足了紧急需求。如果您对 Grunt 或任何其他构建系统感到满意,请继续使用它。但是,如果您对您正在使用的系统感到不适,或者想尝试一些新的东西,那么 Gulp 绝对值得您关注。
Gulp 速度快,非常优雅且易于扩展。我提到它也相当容易了吗?一个通常需要一个小时的演讲将涵盖从基础知识到编写我们自己的插件的所有内容。这是一个了不起的成就。在一个快速发展的世界里,找到既有趣又易于学习,但又足够复杂以满足所有要求的技术是很难的。Gulp 通过回归 CS 的基本原理来取得成功:提供可以组合在一起的小型构建块。
别忘了查看 GitHub 存储库。我会不时添加更多示例,如果您觉得应该包含某些内容,我很乐意接受其他示例的 pull request(当然,贡献会得到提及)。如果一两个示例不再工作,也请随时留言 - Gulp 是一个不断变化的目标,我故意没有限制 Gulp(或任何插件)的版本。如果示例失败,则应更新。示例应始终与最新源代码一起工作。
历史
- v1.0.0 | 初始发布 | 2015年1月15日
- v1.0.1 | 修复了一些拼写错误 | 2015年1月19日
- v1.1.0 | 包含 Gulp vs Grunt 讨论 | 2015年2月2日
- v1.1.1 | 修复了 ASP.NET 版本号 | 2015年2月4日