Javascript构建工具:Grunt、Gulp、NPM

Nicolas Bevacqua专心研究了JavaScript构建系统,他比较了三大工具:Grunt、Gulp、NPM,讨论了各自的优点和缺点。

原文地址A JavaScript Build System Shootout: Grunt vs. Gulp vs. NPM

决定技术总是困难的,因为你不想反复选择。但最终你将不得不做出选择来满足你需要它做的事情。对于构建技术在这方面没有什么不同:它是一个重要的选择,你应该慎重对待它。例如让我们来看看Grunt:

  • Grunt有一个健康的社区,甚至是在Windows上。

  • 它不完全使用在Node社区

  • 很容易学习,你只是安装插件然后配置它们

  • 没有先进的概念,也不需要大量的经验知识

这些都是选择使用Grunt很好的理由,但是我想弄清楚,Grunt是不是最好的选择,而且还有其他流行的构建工具,是否比Grunt更满足你的需要。

我写这篇短文,可以帮助你理解Grunt、Gulp和npm之间的差异。这是我在前端开发工作流程中最常使用的三个构建工具。当我们理解它们以后,定制一个工具的时候可能会更得心应手。

第一步,我们来讨论Grunt擅长的地方。

Grunt的特点

Grunt的最好的方面是它的易用性。它使程序员在开发使用JavaScript构建流程中几乎毫不费力。你只需要寻找适当的插件,阅读它的文档,然后安装和配置它。这种易用性意味着在大型开发团队中,即便成员们不同的技能水平,也可以没有任何麻烦的调整构建项目的流程来满足最新的需求。团队不需要流畅使用Node,他们只需要添加属性来配置对象,并形成了不同任务名称的构造流程。

有一个足够完善的插件库,你会发现很少需要开发自己的构建任务,这使你和你的团队能够快速投入到开发一个构建过程中,如果你使用Build First的方式来构建你的Javascript应用,这是很重要的。

也可以通过Grunt管理部署,有许多依赖包提供这样的任务,例如grunt-git,grunt-rsyncgrunt-ec2等。

Grunt的不足在哪里?如果你有一个很大的构建流程,它可能过于冗长。一旦任务数在构建流到达两位数,几乎可以肯定,你会发现自己不得不运行相同的任务,这样你才能够组成正确顺序的流程。由于任务配置以声明的方式,你也很难弄清楚任务的执行顺序。

除此之外,你的团队应该编写可维护的代码,在Grunt下,这意味着每个任务的配置或者至少为每个构建流程维护单独的文件。

既然我们已经了解了Grunt的好与坏的方面,这可能已经是一个适合你项目的工具,让我们继续npm,看看如何利用它作为一个构建工具,及其与Grunt的差异。

npm构建工具

使用npm,你需要安装npm,然后建立一个package.json文件。在npm里定义任务一样很容易,你只需要在你的’package.json’里添加属性。属性的名称将被用作任务名称,值为你想要执行的命令。下面所示的示例使用JSHint命令行界面运行我们的JavaScript文件和检查错误。你可以运行任何你需要的命令。

1
2
3
4
5
6
7
8
{
"scripts": {
"test": "jshint . --exclude node_modules"
},
"devDependencies": {
"jshint": "^2.5.1"
}
}

一旦定义了任务,你通过运行以下命令执行命令行。

1
npm run test

注意,npm为特定任务名称提供了捷径。在test这个例子里,你可以简单的输入’npm test’而省略掉’run’来运行。您可以通过在脚本声明里定义’npm run’命令。

1
2
3
4
5
6
7
8
9
10
11
{
"scripts": {
"lint": "jshint . --exclude node_modules",
"unit": "tape test/*",
"test": "npm run lint && npm run unit"
},
"devDependencies": {
"jshint": "^2.5.1",
"tape": "~2.10.2"
}
}

你也可以安排任务作为背景,使他们可以是异步的。假设我们有以下package文件,我们设置build-js为复制一个目录构建流程,build-css编译一个Stylus在我们的CSS样式构建流(Stylus是一个CSS预处理器)。在这种情况下,可以使用&分隔符和命令来实现异步处理。例如下面的build:

1
2
3
4
5
6
7
8
9
10
{
"scripts": {
"build-js": "cp -r src/js/vendor bin/js",
"build-css": "stylus src/css/all.styl -o bin/css",
"build": "npm run build-js & npm run build-css"
},
"devDependencies": {
"stylus": "^0.45.0"
}
}

更多地了解npm,你应该试着学习如何编写Bash命令。

安装npm依赖

你的系统中不一定有JSHint CLI,有两种方法可以去安装它。如果你想从你的命令行工具里直接使用,而不是在一个npm运行任务中,那么你应该使用g全局安装,如下所示:

1
npm install -g jshint

如果你使用的是封装在一个npm的任务,那么你应该将它作为一个devdependency,如下所示。这将允许npm找到jshint的依赖包,适用于任何命令行工具。

1
npm install --save-dev jshint

你不限于只使用命令行工具。事实上,NPM能够运行任何脚本。让我们深入探讨!

在npm任务中使用脚本

下面是一个在Node上运行的脚本,显示了随机的emoji字符串。第一行告诉我们这个脚本的运行环境是Node。

1
2
3
4
5
6
#!/usr/bin/env node
var emoji = require('emoji-random');
var emo = emoji.random();
console.log(emo);

如果你想将脚本放置到名为emoji的文件中,并将这个文件置于项目的根目录下,你需要将emoji-random声明成一个依赖,并向package清单中的scripts对象中添加命令。

1
2
3
4
5
6
7
8
{
"scripts": {
"emoji": "./emoji"
},
"devDependencies": {
"emoji-random": "^0.1.2"
}
}

一旦搞定这些,运行命令不过是在你的终端里调用npm run emoji而已。

优点和缺点

把npm当作构建工具比使用Grunt要多一些优势。你不必拘泥于Grunt的各种插件,可以利用拥有数万的npm包。除了npm你甚至不需要安装任何多余的CLI工具或文件,就可以管理你的依赖和罗列了所有依赖和构建命令的package.json清单。由于npm运行CLI工具和Bash命令更直接,它会比Grunt表现的更好。

需要考虑到Grunt的最大的缺点之一,便是它的I/O限制。这意味着大多数的Grunt任务是从硬盘读取的,然后在写入到硬盘。如果你有几个任务共同使用一个文件,那么这个文件有可能会从硬盘多次读取。在Bash中,命令会连接成一个管道,一个命令输出接着下一个,这样就避免了像Grunt那样的额外I/O开销。

或许npm最大的缺点是Bash命令行在Windows系统环境中无法得到很好的支持。这意味着在Windows系统中把玩使用npm run的开源项目运行起来的项目可能会出各种「小意外」。同样,这也意味着使用Windows的开发者需要尝试使用npm的替代品。这一缺点几乎排除了在Windows中使用npm

Gulp,一个流构建工具

Gulp,另一个构建工具,表现的跟Grunt和npm相似,接下来你会了解到它。

Gulp和Grunt相似,它们都依赖各种各样的插件,并且Gulp是跨平台的,同样支持Windows用户。Gulp是一个代码驱动型的构建工具,相比于Grunt的用声明的方式来给任务定义,Gulp会让你的任务定义更容易阅读。Gulp也跟npm run很相似,她们都使用Node数据流来读取文件,并且通过函数来传递数据,将其转换为输出,在输出结束后将数据写入硬盘。这就使得Gulp没有Grunt那样对硬盘频繁进行I/O操作的缺点。正因消耗更少的I/O操作时间,Gulp才会比Grunt速度快。

Gulp最主要的缺点在于它严重依赖流,管道和异步代码。别误会我:如果你使用Node,那这些都是它的优点。但这些观念的问题是除非你和你的团队精通Node,否则你将会陷入处理「流」的问题中,特别是如果你不得不构建自己的Gulp任务插件时。

在团队协作时,Gulp并不禁止使用npm,因为大多前端团队成员都懂得JavaScript,但也有可能他们并不能流畅的使用Bash脚本,甚至有些人使用的是Windows系统。这就是为什么我通常建议在你自己的项目中保持使用npm run,在对Node熟悉的团队项目中使用Gulp,其它情况下都可以使用Grunt。当然了,这只是我的个人建议,你应该自己想清楚哪一个最适合你的你的团队。还有,你也不要把你自己局限于Grunt,gulp或者npm run,它们仅仅是为我所用的工具。尝试去做一点调查研究,或许你就能发现一个你觉得比这三个都好用的工具。

让我们瞅一些案例,感受一下Gulp任务看起来是个什么东东。

用Gulp运行测试

Gulp有着和Grunt及其相似的约定。在Grunt中使用Gruntfile.js文件来定义你的构建任务,在Gulp中需要把这个文件命名为Gulpfile.js。其它的小区别是当任务运行时,Gulp的CLI包含在相同的包内,所以你将不得不通过npm在本地和全局都安装上gulp包。

1
2
3
touch Gulpfile.js
npm install -g gulp
mpm install --save-dev gulp

一开始,我会创建一个Gulp任务:使用JSHint检查一个JavaScript文件,就像你在Grunt和npm run中已经看到的一样。你需要为JSHint安装gulp-jshint插件。

1
npm install --save-dev gulp-jshint

现在你已经用CLI完全装备好了,全局安装,本地的gulp安装和gulp-jshint插件,你可以将构建任务汇集到一起来运行检查器。用Gulp定义构建任务,你必须将它们写入Gulpfile.js文件中。

首先你需要使用gulp.task,并给它传递一个任务名称和函数。这个函数包含运行任务所必须的代码。这里你应该用gulp.src来创建一个读入流到你的源文件中,像我们在学习Grunt中一样使用一个全局配置。相同的流应该引导至JSHint插件,在这里你可以自行配置或者使用默认配置。之后你所需做的是通过指示器链接JSHint的结果,然后将结果输出到你的终端。所有我所描述的结果都基于下面呈现的Gulpfile。

1
2
3
4
5
6
7
8
9
var gulp = require('gulp');
var jshint = require('gulp-jshint');
gulp.task('test', function () {
return gulp
.src('./sample.js')
.pipe(jshint())
.pipe(jshint.reporter('default'));
});

我还应该提到我正在返回数据流,使得Gulp能明白在任务完成之前,它应该等待数据停止流动。为了能让输出更容易被人类阅读,你应该使用默认的JSHint指示器。JSHint指示器不需要作为Gulp插件,因此你可以使用jshint-sytlish为例。让我们安装它到本地。

1
npm install --save-dev jshint-stylish

更新后的Gulpfile应该如下所示。它将会读取jshint-stylish模式来格式化指令的输出。

1
2
3
4
5
6
7
8
9
var gulp = require('gulp');
var jshint = require('gulp-jshint');
gulp.task('test', function () {
return gulp
.src('./sample.js')
.pipe(jshint())
.pipe(jshint.reporter('jshint-stylish'));
});

你做到了!!这就是名为test的Gulp任务所有的声明。用下面的命令使之运行,确保你全局安装了gulpCLI。

1
gulp test

这只是一个简单的小案例。就像你通过可以打印检验结果的指示器输出JSHint检验的结果,你甚至可以用gulp.dest将输出写入硬盘。接下来我们探寻另一个构建任务。

在Gulp中构建一个库

在一开始,我们来设定一下最低限度:使用gulp.src从硬盘读取并切通过用管道传输gulp.dest中源文件的内容来写回到硬盘,有效的见文件复制到其它目录中。

1
2
3
4
5
6
7
var gulp = require('gulp');
gulp.task('build', function () {
return gulp
.src('./sample.js')
.pipe(gulp.dest('./build'));
});

复制文件挺不错的,但是它并不能是文件内容最小化。为了达到这个效果,你还需要使用一个Gulp插件。这种情况下可以用gulp-uglify,一个流行的UglifyJs压缩插件。

1
2
3
4
5
6
7
8
9
var gulp = require('gulp');
var uglify = require('gulp-uglify');
gulp.task('build', function () {
return gulp
.src('./sample.js')
.pipe(uglify())
.pipe(gulp.dest('./build'));
});

如你所料,流操作使得你可以添加一堆插件,但是却只需要对硬盘实行一次读写操作。作为一个例子:我们可以使用gulp-size(它能够计算出缓存区内容的大小,并且输出到终端)。注意,如果你把它添加在Uglify之前,你会得到原始大小,而添加到Uglify之后,则会得到压缩后的大小。你也可以在前后都添加。

1
2
3
4
5
6
7
8
9
10
11
var gulp = require('gulp');
var uglify = require('gulp-uglify');
var size = require('gulp-size');
gulp.task('build', function () {
return gulp
.src('./sample.js')
.pipe(uglify())
.pipe(size())
.pipe(gulp.dest('./build'));
});

为了加强可组合型,能够按需添加和移除pipe,我们最后需要添加一个插件。这一次,我将使用gulp-header来向压缩的代码片段中添加一些许可信息,例如名称,包的版本号,和许可的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var gulp = require('gulp');
var uglify = require('gulp-uglify');
var size = require('gulp-size');
var header = require('gulp-header');
var pkg = require('./package.json');
var info = '// <%= pkg.name %>@v<%= pkg.version %>, <%= pkg.license %>\n';
gulp.task('build', function () {
return gulp
.src('./sample.js')
.pipe(uglify())
.pipe(header(info, { pkg : pkg }))
.pipe(size())
.pipe(gulp.dest('./build'));
});

就像在Grunt中一样,在Gulp中你可以通过向gulp.task中传递名称数组来定义流程。而不是通过函数。GruntGulp最主要的区别是Gulp会异步执行这些依赖,而Grunt是同步执行的。

1
gulp.task('build',['builid-js','build-css']);

Gulp中,如果你想同步运行任务,你不得不将任务作为依赖来声明,然后定义你自己的任务。所有依赖会在你的任务开始之前执行。

1
2
3
gulp.task('build', ['dep'], function () {
// here goes the task that depends on 'dep'
});

如果你能从本文中有所收获,那么请记下下面的话:

你用什么工具并不重要,关键在于它能够让你编译构建流程的工作更轻松。

Wow