JavaScript构建(编绎)系统大比拼:Grunt vs. Gulp vs. NPM

jopen 10年前

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

  译者:kris

  决定采用何种技术总是很难的。一旦遇到问题,你不想推翻你之前的选择。但是你必须选一个,然后让它按照着你的思路做。实施一套构建(编绎)系也是一样的,你应该把它看作一个非常重要的选择,让我们以 Grunt 为例。  

  • Grunt 有一个完善的社区,即使是在 Windows 上
  • 它不仅仅应用在 Node 社区
  • 它简单易学,你可以随便安装插件并配置它们 
  • 你不需要多先进的理念,也不需要任何经验

  这些都是用 Grunt 构建编绎工具的充分理由,但我想澄清一点,我不认为 Grunt 不是唯一最好的选择。还有一些同样流行的选择摆在那里,有些方面可能比 Grunt 做得更好。 

  我写这篇文章,以帮助您了解 Grunt,Gulp 和 npm 之间的差异,这是我在前端开发工作中使用最多的三种构建工具。

  我们先来讨论 Grunt 擅长的方面

  Grunt:好的部分

  Grunt 最好的一个方面是它的易用性。它能使程序员使用 JavaScript 构建编绎工具时,几乎不费吹灰之力。你只需要寻找合适的插件,阅读它们的文档,然后安装和配置它。这种易用性意味着大型开发团队,那些不同技能水平的成 员,也可以没有任何麻烦的调整编绎流程,以满足项目的最新需求。而且团队并不需要精通 Node,他们仅需要配置对象,将不同的任务添加到不同的序列构建编绎流程。

  这里有基础足够大的插件库,你会发现自己几乎不需要开发自己的编译任务,这能使您和您的团队能够快速构建开发工具,如果你要快速完成编绎过程这是至关重要的,你也可以采取小步走,逐步完善编译流程的策略。

  通过 Grunt 管理部署也是可行的,因为有许多包已经可以完成这些任务,如 grunt-git, grunt-rsync, 或 grunt-ec2 等等。

  那么,Grunt 有什么缺陷吗?如果你有一个明显复杂的编绎过程,它可能会变得过于冗长。当开发一段时间以后,它往往很难将编绎过程作为一个整体。一旦你编绎流程任务到达 两位数,几乎可以保证,你会发现自己不得不在多个目标(Targets)中跑同一个 Task,以便你能够正确地执行任务流。由于任务是需要声明配置的,你也很难弄清楚任务真正的执行次序。

  除此之外,你的团队应该致力于编写可维护的代码,当涉及到你的编绎,比如在使用 Grunt 的情况下,这意味着你需要为每个任务(或者每个编绎流)编写一份独立的配置文件,供你的团队使用。

  现在,我们已经了解了 Grunt 好和不好的方面,以及在何种情况下,比较适合作为你项目的编绎工具。我们再来谈谈 npm,它如何被用作构建工具,以及与 Grunt 有何不同。 

  将 npm 视为构建工具

  为了将 NPM 用作构建工具,你需要一个 package.json 和 npm。制定 NPM 任务就像在脚本中添加属性一样简单。该属性的名称将用作任务名和将要执行的命令。下面的这个 build 任务将预先检查我们的 JavaScript 代码中有没有语法错误,例子使用 JSHint 命令行接口来。在命令行中你可以运行任何你需要的 shell。 

{    "scripts": {      "test": "jshint . --exclude node_modules"   },    "devDependencies": {      "jshint": "^2.5.1"   }  }

  一旦定义完成,就可以通过下面的命令来运行

npm run test

  需要注意的是 npm 提供了运行特定任务的快捷方式。比如要运行 test,你可以简单地使用 npm test 并省略动词 run。您可以通过一个命令链来将一系列 npm run 的任务连在一起,构成你的编绎流程:

{    "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"   }  }

  您也可以安排一些后台完成的任务,然后让他们同步。假设我们有以下的包文件, 我们将复制出一个目录用来放 JavaScript 文件,以及将我们用 Stylus 写的样式表文件编绎成 CSS。在这种情况下,多个任务一起运行是比较理想的。也可以实现,使用&分隔符即可。 

{    "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 并不一定要包含在你的系统中,这里有两种安装它的方式。如果你正在寻找直接从命令行中运行的工具,那么你应该在全局范围内安装,使用g标志,如下所示。

npm install -g jshint

  不过,如果您使用的是包在 npm run 中使用的,那么你就应该把它加到 devDependency 中,如下所示。这将让 npm 自动在系统中寻找它所依赖的 JSHint 安装在了哪里。这方法适用于任何命令行工具中和所有操作系统。 

npm install --save-dev jshint

  你其实不仅局限使用 CLI 工具。事实上,npm 能够运行任何 shell 脚本。让我们来挖一挖! 

  在 npm 中使用 shell 脚本

  下面是一个运行在 node 的脚本,并显示一个随机的绘文字符串(emoji-random)。第一行指定运行环境,该脚本基于 Node。 

#!/usr/bin/env nodevar emoji = require ('emoji-random'); var emo = emoji.random ();  console.log (emo);

  如果你将一个名为 emoji 的脚本文件放到你项目的根目录中,你必须将 emoji-random 申报为依赖关系,并将以下脚本命令添加到包文件中。 

{    "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 的最大的缺点是,在 Windows 环境中的应用可能没那么好。这意味着使用 NPM 运行的开源项目可能遇到问题。这也意味着 Windows 开发人员尝试使用 npm 的替代品。这缺点几乎将 NPM 从 Windows 上排除。

  Gulp,另一个构建工具,提出了与 Grunt 和 npm 相似的功能,一会你就会发现。 

  Gulp 的流式构建工具

  与 Grunt 类似,它依赖插件,并且是跨平台的,它也支持 Windows。Gulp 是一个代码驱动的构建工具,与 Grunt 的声明式定义任务相反,它的任务定义更容易阅读一点。Gulp 也有类似于 npm run 的东西,因为它使用 Node Stream 来转化输入输出。这意味着,Gulp 没有 Grunt 那种磁盘密集型I/O操作的问题。它也是它比 Grunp 更快的原因,更少的时间花在I/O上面。

  在使用 Gulp 的主要缺点是,它在很大程度上依赖于流,管道和异步代码。不要误解我的意思:如果你用在 Node 中,这绝对是一个优势。但是,除非你和你的团队非常精通 Node,你很有可能会遇到处理流的问题,特别是如果你要建立你自己的 Gulp 任务插件。 

  在团队工作的时候,Gulp 不是望而却步的 npm,因为大多数前端团队可能都懂 JavaScript,但是他们可能对 Bash 脚本不那么熟练,其中一些可能是使用 Windows 的!这就是为什么我通常建议你在个人项目中运行 NPM 的原因。如果你的团队很熟悉 Node,你可以使用 Gulp。当然,这是我个人的建议,你应该找到最适合你和你团队的工具。此外,你应该不会把自己限制在 Grunt,Gulp,或者 npm run 中,对你我来说这些都只是工具。尝试做一些小小的研究,也许你会发现,你喜欢的甚至比这三个更好的工具。 

  让我们通过一些例子来看看 Gulp 中的任务看起来是什么样子的。

  在 Gulp 中运行测试

  有一些约定 Gulp 与 Grunt 极为相似。在 Grunt 中有一个定义 Task 的文件 Gruntfile.js,在 Gulp 中叫 Gulpfile.js。另一种微小的差别是,在 Gulp 中,CLI 已经包含在同一个 Gulp 包中,你需要通过 npm 从本地和全局同时安装。 

touch Gulpfile.js  npm install -g gulp  npm install --save-dev gulp

  在开始之前,我将创建一个 Grulp 任务处理一个 JavaScript 文件,就像你已经在 Grunt 和 NPM 中看到的那样使用 JSHint,你需要先安装 gulp-jshint,Gulp 的 JSHint 插件。

npm install --save-dev gulp-jshint

  现在你已经同时在全局和本地中装好 CLI 了,本地已经安装了 gulp 和 gulp-jshint 插件,你可以将这些构建任务合成一个。你可以在 Gulpfile.js 文件中写出来。

  首先,您将使用 gulp.task 定义一个任务和功能。该功能包含了所有必要的代码来运行这项测试。在这里,你应该使用 gulp.src 创建一个读取你源文件的流,这个数据流会被管道输送进 JSHint 插件。然后,所有你需要做的就是管道中的 JSHint 任务打印到终端。下面是 Gulpfile 中展示的结果。

var gulp = require ('gulp'); var jshint = require ('gulp-jshint');  gulp.task ('test', function () {    return gulp      .src ('./sample.js')      .pipe (jshint ())      .pipe (jshint.reporter ('default'));  });
点需要提一下,Grulp 流会在一个任务完全结束之后再转到下一个任务。你可以使用一个 JSHint Reporter 使输出更加简洁,从而更易于阅读。 JSHint Reporter 并不需要 Grulp 插件,例如 jshint-stylish,让我们在本地直接安装。
npm install --save-dev jshint-stylish

  更新后的 Gulpfile 应如下所示。它会加载 jshint-stylish 模块,按报表格式输出。 

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 的任务。它可以使用下面的命令运行,只要你安装了全局的 CLI。

gulp test

  这是一个相当简单的例子。你也可以通过使用 gulp.dest,创建了一个写数据流到磁盘中。让我们看看另外一个构建任务。 

  在 Grulp 中创建一个库

  在开始之前,让我们明确任务:从磁盘 gulp.src 读取源文件并通过磁盘管道写回内容到 gulp.dest,你可以理解成只是将文件复制到另一个目录。 

var gulp = require ('gulp');  gulp.task ('build', function () {    return gulp      .src ('./sample.js')      .pipe (gulp.dest ('./build'));  });

  复制文件完成了,但是它没有压缩这个 JS 文件。要做到这一点,你必须使用一个 Gulp 插件。在这种情况下,你可以使用 gulp-uglify,流行的 UglifyJS 压缩编绎插件。

var gulp = require ('gulp');var uglify = require ('gulp-uglify');  gulp.task ('build', function () {    return gulp      .src ('./sample.js')      .pipe (uglify ())      .pipe (gulp.dest ('./build'));  });

  正如你可能意识到的那样,流使可以让您添加更多的插件,而只需要读取和写入磁盘一次。你也可以指定缓冲器中内容的大小。需要注意的是,如果你在压缩之前添加它,那么你得到的大小是 unminified。

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'));  });

  为了增强这种组合,满足添加或删除管道的需要,让我们添加最后一个插件。这一次,我会用 gulp-header 在头文件添加一段版权信息的代码,如名称,版本和许可证类型。 

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 一样,在 Grulp 中你可以通过传递一组任务到 gulp.task 来定义流程。在这方面,Grunt 和 Grulp 之间的主要区别在于,Grunt 是同步的,而 Grulp 是异步的。 

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

  在 Gulp,如果你要让任务同步运行,你必须声明一个任务。你的任务开始之前执行。 

gulp.task ('build', ['dep'], function () {    // 执行 dep 所依辣的任务 });

  如果你有任何收获,先看看这段话。 

你使用哪种工具并不重要,只要保证:流程构建(编绎)好用就行了,用起来不要太辛苦。
来自: ourjs.com