我为何放弃 Gulp 与 Grunt,转投 npm scripts

2,673 阅读18分钟
原文链接: www.sdk.cn

摘要:有人坚持认为Gulp与Grunt等前端构建工具依然是不可或缺的,还有些人则认为Gulp与Grunt是完全没必要使用的,而且还增加了一层抽象,会导致很多问题。近日,Cory撰文谈到了他对于Gulp、Grunt与npm scripts的认识,并且认为在现在的工程中,我们完全可以抛弃Gulp与Grunt,使用npm scripts就可以满足项目之所需。

Cory House是“Building Applications with React and Flux”与“Clean Code: Writing Code for Humans”的作者,同时也是Pluralsight上众多课程的讲师。他是VinSolutions的软件架构师,在全球培训了为数众多的软件开发者,主要领域是前端开发与整洁代码等软件开发实践。Cory是微软MVP、Telerik开发者专家,同时也是outlierdeveloper.com的创始人。目前,围绕着Gulp、Grunt及npm scripts社区展开了很多争论,讨论Gulp与Grunt在项目中是否还有继续使用的必要。有人坚持认为Gulp与Grunt等前端构建工具依然是不可或缺的,还有些人则认为Gulp与Grunt是完全没必要使用的,而且还增加了一层抽象,会导致很多问题。近日,Cory撰文谈到了他对于Gulp、Grunt与npm scripts的认识,并且认为在现在的工程中,我们完全可以抛弃Gulp与Grunt,使用npm scripts就可以满足项目之所需。

众所周知,Gulp与Grunt是很多项目所使用的构建工具,他们也拥有非常丰富的插件。不过,我却认为Gulp与Grunt是完全不必要的抽象,npm scripts更加强大,并且更易于使用。

我本人是Gulp的粉丝。不过在上一个项目中,gulpfile竟然有100多行,而且还使用了不少Gulp插件。我尝试通过Gulp集成Webpack、Browsersync、热加载、Mocha等工具,为什么要这么做呢?这是因为有些插件的文档实在是太不充分了;还有些插件只公开了我所需的部分API。其中有个插件存在一个奇怪的Bug,它只能看到文件的部分内容。另一个插件则在输出到命令行时丢失了颜色。

当然了,这些问题都是可以解决的;不过,当我直接使用这些工具时,所有问题都不复存在了。最近,我注意到有很多开源项目只是使用了npm scripts。因此,我决定重新审视一下自己的做法。我真的需要Gulp么?答案就是:完全不需要。我决定在我新的开源项目中只使用npm scripts。我只使用npm scripts为一个React应用搭建了开发环境与构建流程。想知道这个项目是什么样子的么?看一下React Slingshot吧。现在,相对于Gulp来说,我更倾向于使用npm scripts,下面就来谈谈原因。

Gulp与Grunt怎么了?

随着时间的流逝,我发现诸如Gulp与Grunt等任务运行器都存在以下3个核心问题:

  • 对插件作者的依赖
  • 令人沮丧的调试
  • 脱节的文档

下面就来详细分析上述3个问题。

问题1:对插件作者的依赖

在使用比较新或是不那么流行的技术时,可能根本就没有插件。当有插件可用时,插件可能已经过时了。比如说,Babel 6前一阵发布了。其API变化非常大,这样很多Gulp插件都无法兼容于最新的版本。在使用Gulp时,我就感到深深的受伤,因为我所需要的Gulp插件还没有更新。在使用Gulp或是Grunt时,你不得不等待插件维护者提供更新,或是自己修复。这会阻碍你使用最新版现代化工具的机会。与之相反,在使用npm scripts时,我会直接使用工具,不必再添加一个额外的抽象层。这意味着当新版本的Mocha、Istanbul、Babel、Webpack、Browserify等发布时,我可以立刻就使用上新的版本。对于选择来说,没有什么能够打败npm:

从上图可以看到,Gulp有将近2,100个插件;Grunt有将近5,400个;而npm则提供了227,000多个包,同时还以每天400多个的速度在持续增加。

在使用npm scripts时,你无需再搜索Grunt或是Gulp插件;只需从227,000多个npm包中选择就行了。公平地说,如果所需要的Grunt或是Gulp插件不存在,你当然可以直接使用npm packages。不过,这样就无法再针对这个特定的任务使用Gulp或是Grunt了。

问题2:令人沮丧的调试

如果集成失败了,那么在Grunt和Gulp中调试是一件令人沮丧的事情。因为你面对的是一个额外的抽象层,对于任何Bug来说都有可能存在更多潜在的原因:

  • 基础工具出问题了么?
  • Grunt/Gulp插件出问题了么?
  • 配置出问题了么?
  • 使用的版本是不是不兼容?

使用npm scripts可以消除上面的第2点,我发现第3点也很少会出现,因为我通常都是直接调用工具的命令行接口。最后,第4点也很少出现,因为我通过直接使用npm而不是任务运行器的抽象减少了项目中包的数量。

问题3:脱节的文档

一般来说,我所需要的核心工具的文档质量总是要比与之相关的Grunt和Gulp插件的好。比如说,如果使用了gulp-eslint,那么我就要在gulp-eslint文档与ESLint网站之间来回切换;不得不在插件与插件所抽象的工具之间来回切换上下文。Gulp与Grunt的问题在于:光理解所用的工具是远远不够的。Gulp与Grunt要求你还得理解插件的抽象。

大多数构建相关的工具都提供了清晰、强大,且具有高质量文档的命令行接口。ESLint的CLI文档就是个很好的例子。我发现在npm scripts中阅读并实现一个简短的命令行调用会更加轻松,阻碍更少,也更易于调试(因为并没有抽象层存在)。既然已经知道了痛点,接下来的问题就在于,为何我们觉得自己还需要诸如Gulp与Grunt之类的任务运行器呢?

我相信个中原因应该是因人而异的。毫无疑问,Gulp与Grunt等任务运行器已经出现很长一段时间了,而且围绕着这些任务运行器的插件生态圈也呈现出欣欣向荣的繁荣景象。依赖于这些插件,很多日常工作都可以实现自动化,并且运行良好。这样,人们就会认为只有通过这些任务运行器才能实现任务的构建、文件的打包、工作流的良好运行等等。另外一个原因就是人们对于npm scripts的认识还远远不够;对于npm scripts所能完成的事情与任务也流于表面。这也进一步造成了很多人并没有发现npm scripts可以实现很多日常开发时的构建任务的结果。我相信随着开发者对于npm scripts认识的进一步深入,大家会逐步发现原来使用npm scripts也可以完成Gulp与Grunt等任务运行器所能完成的任务,而且配置更加简单,也更加直接,因为它会直接使用目标工具而不必再使用对目标工具的包装器了。

在构建时我们为何会忽略掉npm?

我认为有如下4点原因造成Gulp与Grunt等任务运行器变得如此流行:

  • 人们认为npm scripts需要强大的命令行技巧
  • 人们认为npm scripts不够强大
  • 人们认为Gulp的流对于快速构建来说是不可或缺的
  • 人们认为npm scripts无法实现跨平台运行

下面我就来按照顺序解释一下这些误解。

误解1:使用npm scripts需要强大的命令行技巧

体验npm scripts的强大功能其实并不需要对操作系统的命令行了解太多。当然了,grep、sed、awk与管道等是值得你去学习的,令你众生受用的技能;不过,为了使用npm scripts,你不必非得成为Unix或是Windows命令行专家才行。你可以通过npm中1000多个拥有良好文档的脚本来完成工作。

比如说,你可能不知道在Unix中,命令rm -rf会强制删除一个目录,这没问题。你可以使用rimraf完成同样的事情(它也是跨平台的)。大多数npm包都提供了一些接口,这些接口假设你对所用操作系统的命令行了解不多。只需在npm中搜索想要使用的包即可,边做边学就行了。过去,我常常会搜索Gulp插件,不过现在则是搜索npm包了。libraries.io是个非常棒的资源。

误解2:npm scripts不够强大

npm scripts本身其实是非常强大的。它提供了基于约定的pre与post钩子

{
name: "npm-scripts-example",
version: "1.0.0",
description: "npm scripts example",
scripts: {
prebuild: "echo I run before the build script",
build: "cross-env NODE_ENV=production webpack",
postbuild: "echo I run after the build script"
}
}

你所要做的就是遵循约定。上述脚本会根据其前缀按照顺序运行。prebuild脚本会在build脚本之前运行,因为他们的名字相同,但prebuild脚本有“pre”前缀。postbuild脚本会在build脚本之后运行,因为它有“post”前缀。因此,如果创建了名为prebuild、build与postbuild的脚本,那么在我输入“npm run build”时,他们就会自动按照这个顺序运行。

此外,还可以通过在一个脚本中调用另一个脚本来对大的问题进行分解:

{
  "name": "npm-scripts-example",
  "version": "1.0.0",
  "description": "npm scripts example",
  "scripts": {
    "clean": "rimraf ./dist && mkdir dist",
    "prebuild": "npm run clean",
    "build": "cross-env NODE_ENV=production webpack"
  }
}

在上述示例中,prebuild任务调用了clean任务。这样就可以将脚本分解为更小、命名良好、单职责,单行的脚本。

可以通过&&在一行连续调用多个脚本。上述示例中,clean步骤中的脚本会一个接着一个运行。如果你需要在Gulp中按照顺序一个接着一个地运行任务列表中的任务,那么这种简洁性肯定会吸引到你。

如果一个命令很复杂,那还可以调用一个单独的文件:

{
  "name": "npm-scripts-example",
  "version": "1.0.0",
  "description": "npm scripts example",
  "scripts": {
    "build": "node build.js"
  }
}

我在上述的build任务中调用了一个单独的脚本。该脚本会被Node所运行,这样就可以使用我所需的任何npm包了,同时还可以利用上JavaScript的能力。我还能列出很多,不过感兴趣的读者可以参考这份核心特性文档。此外,Pluralsight上也有一门关于如何将npm作为构建工具的课程。还可以看看React Slingshot以直观了解其使用方式。

误解3:Gulp的流对于快速构建来说是不可或缺的

Gulp出来后,人们之所以很快就被它吸引过去并放弃Grunt的原因在于Gulp的内存流要比Grunt基于文件的方式快很多。不过,要想享受到流的强大功能,实际上并不需要Gulp。事实上,流早就已经被内建到Unix与Windows命令行中了。管道(|)运算符会将一个命令的输出以流的方式作为另一个命令的输入。重定向(>)运算符则会将输出重定向到文件。比如说在Unix中,我可以“grep”一个文件的内容,并将输出重定向到一个新的文件:

grep ‘Cory House’ bigFile.txt > linesThatHaveMyName.txt

上述过程是流式的,并不会被写入到中间文件中(想知道如何以跨平台的方式实现上面的命令么?请继续往下读)。

在Unix中,还可以通过“&”运算符同时运行两个命令:

npm run script1.js & npm run script2.js

上述两个脚本会同时运行。要想以跨平台的方式同时运行脚本,请使用npm-run-all。这就造成了下面这个误解。

误解4:npm scripts无法实现跨平台运行

很多项目都会绑定到特定的操作系统上,因此跨平台是一件并不那么重要的事情。不过,如果需要以跨平台的方式运行,那么npm scripts依然可以工作得很好。无数的开源项目就是佐证。下面来介绍一下实现方式。

操作系统的命令行会运行你的npm scripts。因此,在Linux与OS X上,npm scripts会在Unix命令行中运行。在Windows上,npm scripts则运行在Windows命令行中。这样,如果希望构建脚本能够运行在所有平台上,你需要适配Unix与Windows。下面介绍3种实现方式:

方式1:使用跨平台的命令

有很多跨平台的命令可供我们使用。下面列举一些:

&& 链式任务(一个任务接着一个任务运行)
< 将文件内容输入到一个命令
>  将命令输出重定向到文件
| 将一个命令的输出重定向到另一个命令

方式2:使用node包

可以使用node包来代替shell命令。比如说,使用rimraf来代替“rm -rf`”。使用cross-env以跨平台的方式设置环境变量。搜索Google、npm或是libraries.io,寻找你所需要的,几乎都会有相应的node包以跨平台的方式实现你的目标。如果命令行调用过长,你可以在单独的脚本中调用Node包,就像下面这样:

node scriptName.js

上述脚本就是普通的JavaScript,由Node运行。既然是在命令行调用了脚本,那么你就不会受限于.js文件。你可以运行操作系统所能执行的任何脚本,比如说Bash、Python、Ruby或是Powershell等等。

方式3:使用ShellJS

ShellJS是个通过Node来运行Unix命令的npm包。这样就可以通过它在所有平台上运行Unix命令了,也包括Windows。

我在React Slingshot上同时使用了方式1与2。

上面部分介绍了人们对于npm scripts存在的误解,以及npm scripts自身所提供的强大功能。借助于操作系统提供的各种基础设施、npm scripts以及各种命令,我们完全可以通过npm scripts以更加轻量级的方式实现Gulp与Grunt等任务运行器所提供的功能。接下来介绍npm scripts中存在的一些痛点以及应对之道。

痛点

显然,使用npm scripts也存在着一些问题:JSON规范并不支持注释,因此无法在package.json中添加注释。不过有一些办法可以突破这个限制:

  • 编写小巧、命名良好、单一目的的脚本
  • 分离文档与脚本(比如说放在README.md中)
  • 调用单独的.js文件

我更偏爱第一种解决方案。如果将每个脚本都进行分解,使其保持单一职责,那么注释就变得不那么重要了。脚本的名字应该能完全描述其意图,就像任何短小、命名良好的函数一样。就像我在“Clean Code: Writing Code for Humans”中所说的那样,短小、单一职责的函数几乎是不需要注释的。如果觉得有必要添加注释,那么我会使用第3种方案,即将脚本移到单独的文件中。这样就可以利用JavaScript组合的强大力量了。

Package.json也不支持变量。这看起来貌似是个大问题,但实际上并非如此,原因有二。首先,很多时候我们所需的变量都涉及到环境,这可以通过命令行进行设置。其次,如果出于其他原因而需要变量,那么你可以调用单独的.js文件。感兴趣的读者可以看看React-starter-kit,了解这种做法。

最后,还存在一种风险,那就是使用长长的、复杂的命令行参数,这些参数令人难以理解。代码审查与重构是确保npm脚本保持小巧、命名良好、单一职责,且每个人都能容易理解的好方式。如果脚本复杂到需要注释,那么你应该将单个脚本重构为多个命名良好的脚本,或是将其抽取为单独的文件。

我们需要证明抽象是有意义的

Gulp与Grunt是对我所使用的工具的抽象。抽象是很有用的,不过抽象是有代价的。它让我们过分依赖于插件维护者与文档,同时随着插件数量的不断攀升,他们也不断引入复杂性。我已经决定不再使用诸如Gulp与Grunt之类的任务运行器了。

实际上除了我之外,现在已经有不少开发者与我的观点不谋而合,比如说下面这些:

Cory的文章一经发出立刻得到了众多开发者的广泛回应,人们纷纷表达了自己的观点,这里摘录出其中一些典型观点以飨各位读者:

Jason Trill说到:

另一个好处就是对基于Node的项目的标准化。如果仅仅通过“npm run”即可运行任务就非常棒了,虽然这些任务只不过是Gulp/Grunt的包装器而已。

Dwayne Crooks说到:

太棒了。我最近一直在思考是否需要在我的工作流中使用Gulp,并且在项目中使用得越来越少。这篇文章让我相信Gulp与其他构建工具是完全没必要的,非常感谢。

Vladimir Agafonkin说到:

我们在Mapbox上有大量的JavaScript仓库,他们都使用了npm scripts,完全没有用上Gulp与Grunt。这么做完全没有任何问题,搭建容易,理解与管理起来也易如反掌。

Martin Olsen说到:

我在一年前读过了这篇文章blog.keithcirkel.co.uk/why-we-shou…之后就开始使用npm scripts而逐渐放弃Gulp了。我喜欢npm scripts的简洁性。恕我直言,其唯一的痛点就是无法在脚本中添加注释,并且必须要对双引号进行转义。

Tim Wisniewski说到:

我也是这么做的,文章的观点与我不谋而合。

Akshay Bist说到:

不仅仅是node包,你可以运行操作系统所能执行的任何脚本。因此,还可以运行python、bash脚本等等。

Cecil McGregor说到:

非常感谢。虽然在工作时我不得不使用Grunt,不过在家的时候我大部分时间都在使用npm scripts。很多插件都存在一些问题,浪费了我大量的时间探究问题所在。

Jess Hines说到:

非常感谢。通常,我们都认为抽象会使得事情变得更加简单,不过我发现npm scripts已经足够友好了,并且非常强大。如果需要的话,我会尝试一下文中的做法,加深理解。

adam seldan说到:

完全同意文中的观点。我最近就一直在使用npm package.json脚本,特别是那些大量使用Node.js的项目,完全不需要复杂的转换链。如果感觉不太灵便(现在还没有出现,通常情况下,你会提前知道所工作的项目规模,以及其构建步骤),那么引入和学习Webpack是一种很好的方式,它在某种程度上要胜于Grunt与Gulp。

Dylan J Harris说到:

感谢。作为一名任务运行器新手,我已经遇到了文中提到的3个问题,因此非常厌恶这种抽象。我打算在接下来的项目中直接使用npm scripts,非常棒的文章。

Jason Karns说到:

直接使用npm scripts会让我们拥有更多的配置选项;npm会以环境变量的方式公开package.json对象,前缀是npm_package;npm拥有定义良好的配置查找方式,因此可以在不同地方定义各种选项,在必要的时候这些选项会被覆盖。

各位InfoQ中文站的读者朋友,你在项目中使用过Gulp、Grunt等任务运行器么?是否直接使用过npm scripts?你认为二者之间的差别是什么?npm scripts是否能够完全取代Gulp与Grunt呢?当然,Gulp与Grunt由来已久,并且在很多大型项目中都得到了应用;不过,其对插件的依赖一直都为人所诟病,但插件本身也是其一大优势。那么如果要新开发一个项目,你会使用Gulp与Grunt等任务运行器还是会直接使用npm scripts呢?欢迎在下方的评论中提出你的见解并与其他读者一同探讨和交流。