因为将要开始的一个项目中会使用AngularJS,所以我最近一直在查看一些JavaScript的构建工具。我需要一个构建工具来编译、打包和压缩我的脚本文件和样式表。查看这些工具的另外一个原因是 Visual Studio 2015 中将增加对 Grunt 和 Gulp 这类任务运行器的支持。
我的探索之旅开始于一个有用的脚手架工具,Yeoman为我创建的 Gruntfile.js 文件。这个文件默认的配置使用 SASS处理 CSS,然而我更倾向于使用LESS。所以,我开始深入地研究有什么替代方案。当时,这个Grunt配置文件被我认为是一个相当典型的构建配置文件。它非常大,有超过450行代码。进一步深入发掘,我发现了一些不喜欢的东西。这导致我去评估其他的替代工具。从 Grunt 离开,我开始查看下一个非常流行的构建工具 Gulp。接着,我研究了较为常见的 Broccoli.js 和 Brunch。我粗略地考察过名不见经传的 Mimosa 和 Jake,但是在它们的网站上,我没有发现任何引人注目的东西足以让我优先考虑它们。这篇博文将详细描述我的发现。
评判标准
在考察这些JavaScript构建工具时,我使用了特殊的视角并带着一些期望。我是一个使用Visual Studio进行.NET网络应用开发多年的程序员。在这之前,我使用过Java和C++以及很多IDE。我使用过许多各种不同的构建工具,包括make、 msbuild、 rake、 NAnt 和 psake。这些使用经验让我更倾向于具有约定大于配置特性的优雅工具。对我来说,一个构建工具的存在让我专注于我的项目工作,而构建工具应该在项目之外。我应该花最少的时间来配置和调试我的构建。大多数IDE在这方面都是不错的。你添加一个文件,而IDE根据文件扩展名做正确的事情(唯一要做的只是偶尔设置一些选项)。
作为一个职业网络应用开发工程师,我期待有这样一种专注于Web的构建工具。它能够为众多复杂的现代Web App功能提供支持。这正是Visual Studio走向衰败的地方。这也是对构建工具来说有趣且独一无二的需求。传统的构建工具,比如Make,几乎不会面临这样的情况。编译一个C++应用基本上就是找到源代码、编译和链接。不会有比这更多的工作了。对于网络应用来说,有许多不同类型的源文件,这些源文件必须通过各自漫长的转换和组合流程。而且,构建的结果不是一个简单的可执行文件,而是一个许多文件的聚集。这使得清理文件的工作非常重要。最后,开发工程师对构建工具的期望也与日俱增。以前,我们很高兴在运行Make命令之后得到一个可执行文件。而现如今,我们希望能触发增量重构建的文件监控和实时浏览器重加载的特性。
这里有一个简明的步骤、任务列表,这是我认为在大多数JavaScript网络应用构建过程中应该存在的。当然,这些要点会因为项目不同而不同,也会随开发人员的喜好改变。
- 转换翻译为JavaScript:CoffeScript、Dart、Bable、Traceur等。
- JavaScript转换:打包成模块或者ng注解等。
- 构建、连接:将脚本和样式表文件合并成多个文件。
- 压缩:脚本、样式表和HTML。
- 源文件映射:脚本和样式表文件都需要。
- CSS预处理器:Less、 Sass、 Stylus等。
- 样式转换:Autoprefixer、 PostCSS等。
- 缓存失败:用hash值来重命名文件以避免不正确的缓存。
- 图像优化
- 编译模板:Mustache或者HandlebarsJS、 Underscore、 EJS、 Jade等。
- 拷贝资源:HTML、 Fav图标等。
- 监控变更
- 增量编译
- 清除构建:在开始构建前删除所有文件或者按需删除。
- 依赖注入:从捆绑的文件中注入脚本和样式标签。
- 构建配置:分离开发、 测试和生产环境的配置,比如在开发构建中不用压缩HTML文件。
- 服务:启动开发服务器。
- 运行单元测试
- 运行 JavaScript 和 CSS 检测:jshint、 csslint等。
- 依赖管理:使用npm和bower,browerfy等管理项目依赖。
发现
所以我发现了什么?在所有的构建工具中都存在一些共同的不足之处。其中之一就是,认为对 npm 和 Bower 包进行恢复的工作在构建范围之外。与诸如Visual Studio这样的IDE不同,IDE会在需要的时候自动运行 Nuget 进行包恢复,没有一个JS构建工具试图去做这件事。这通常没法绕开,因为构建工具需要项目工程中的一个npm包,构建任务只有当这个包被还原后才会执行。许多构建工具的确有插件支持 Bower 包还原。然而,我看到的大多数例子都是假定在构建开始前会先执行 npm install 命令然后是执行 bower install。他们甚至没有用 npm Install 来触发 bwoer install。另外一个不足是,所有构建工具对依赖的处理。这完全不是构建工具的问题,因为JavaScript生态系统一直在努力解决这个问题。显而易见的是,npm 包在 Node.js 中工作良好 (尽管node_modules中创建重复包和深层目录不在我的考虑范围之内)。但是前端包并没有被很好地考虑到。与代码库不同,一个前端开发包中有许多不同语言写成的文件,每个文件又有自己引用其他文件的方法。除此之外,根据项目使用的工具箱,构建可能在编译前或者编译后需要依赖包。举例来说,一些项目需要 Bootstrap 的 Less 文件,但是其他的项目需要编译好的 CSS 文件。所以,如何表示和控制这些资源文件,以一种什么样的方式融入项目中呢?如果你问我,我会说 Bower 不是解决方案(至少现在不是)。引用一个前端开发包需要变成和在.NET中引用一个程序集或者Nuget包一样容易。现如今,配置包括依赖包的构建总是很痛苦。带着对JavaScript构建工具共同的困境,我们现在轮流看看这些构建工具。
Grunt
对许多刚开始接触JS构建工具的人来说,Grunt是一个不错的开始。Grunt绝对是应用最广泛的构建工具。然而,GruntJS.com 却自豪的宣称Grunt是“JavaScirpt任务执行器”,我也认为这是一个不错的说明。Grunt几乎不是一个合格的构建工具。更确切的说,它是一个串行执行命令行或者任务系统。每一个Grunt任务都是一个插件,用来代表一个可能从命令行执行的工具。每一个插件都在 Gruntfile 中定义了必须配置和可选择配置。固定部分的命名意味着,你不能按自己的意愿来组织或者命名这个部分。虽然插件的配置有很多共性的部分,但是实践中你必须强迫自己去阅读每个插件的文档,以弄清楚如何配置它。另一方面,这也意味着可被接受的文档经常被质量差、意义含糊不清的独立插件文档破坏。因为每个插件基本上是一个命令行工具,所以它们都接受一些输入文件并有一些输出文件。为了把过长的构建管道连接起来,通常需要一个插件去管理超出其范围的一些中间临时文件。文件监控和实时重新加载是手工配置一些任务,监控符合文件名规则的文件是否被修改。服务器端配置对我来说还是完全不透明的。增量构建必须通过手工配置 grunt-newer 来实现。实际上,这意味着对每个任务如何使用各种类型的收入文件,以及和哪些临时文件需要被重新构建要有深刻的理解。所有这些手工配置都导致了巨大的、不易理解的配置文件。如果你仅需要运行少量任务,Grunt对你来说是合适的。如果你需要一个真正的网络应用构建工具,我强烈建议你去别处看看。
Gulp
Gulp是比较热门的新工具。它迅速获得了大家的青睐是有原因的。它认识到并尝试着去解决网络应用构建工具独一无二的需求,即长构建管道。在Gulp中,所有的插件以流的方式工作(好吧,时至今日依然有很多插件不是以流的方式工作,这非常烦人)。这种流模式使得Gulp的任务可以链接在一起而不必担心产生临时文件。Gulp鼓吹它的高性能是基于流模式和并行任务执行。是的,很多用户已经抱怨并行任务执行让人很困惑,并且有计划去扩展配置以便获得对并行任务的更多控制。个人观点,我觉得并行任务执行非常的直观,同时我觉得提出的解决方案比当下的问题更糟糕。
虽然流是一个构建工具吸引人的基础,但是最终我还是发现了 Gulp 的不足之处。虽然核心配置是极小的,其目的是需要极少文档,但是我发现文档是不合适的。像 Grunt 一样,它需要依赖那些配套文档很烂的插件。相比Grunt,这种情况一定程度上由插件使用的一致性而得到了缓解。Gulp 的作者们信仰固执软件( opinionated software )和强配置软件。通常情况下,我是很欣赏固执软件的,但是在这种情况下,我必须反对作者的观点。 他们认为一个已经支持流的工具不应该再被包装成一个插件,而应该直接使用。这就违背了构建中必须尽可能简单配置的原则,因为使用者必须学习和适应工具独一无二的 Gulp 系统 API。看一看由此引起的混乱,查看在GitHub上关于 gulp-browserify 插件问题的黑名单。可以看到,许多用户试图去找到 Browserify 的正确使用方法。我不认为有任何尝试找到了官方提供的方法。但是请记住,即使这样也没有成功地在 Browerify 中支持流文件。这个问题太过复杂以至于需要一整篇优秀但是冗长的博文来解释。除此之外,对于那些不支持它的事物来说,流模式成为比较混乱的东西,比如清理。源代码映射某些时候也很奇怪。我曾经试图将外部源代码映射与带有 manifest 文件的缓存清理结合在一起。虽然最终的配置十分简短,但花费了我几个小时和适应额外的插件才能工作。监控依然需要手工配置,增量构建的配置非常晦涩。Plethora的插件。最后,当我自己不理解问题的时候,我发现了许多关于需要做额外的配置才能让Gulp显示有意义的错误消息的讨论。
Broccoli
Broccoli.js是一个较新的构建工具,已经在工作中使用过一段时间了。它目前对我还没有太大的吸引力,但是希望 ember 命令行工具能带来改观。我真的很喜欢这个工具,开发者考虑到了一个JavaScript构建工具所面临的大多数问题。通过自带文件监控和增量构建特性,Broccoli可以自己决定监控哪些文件,并且只在需要重新构建的时候重新构建。它通过一套构建在文件树核心抽象上的复杂缓存系统来实现这个特性。树结构是Broccoli 对 Gulp 流模式的回应,并可以轻易实现链接。为了简化系统,Broccoli 并不像 Gulp 一样并行运行任务,并宣称这不是必要的,因为它们的性能一直不错。我强烈推荐你去看看作者的博文“与其他构建工具的比较” (章节5)。
尽管拥有这些优异的特性,Broccoli还处于beta阶段,不是很成熟。它在文档方面比较欠缺。说明文件中直接说明对Window的支持不够稳定。事实上不支持源代码映射是我最头疼的地方。它看起来还没有为自己伟大的时代做好准备,但是我真的很喜欢看到它成熟并最终替代掉 Grunt 和 Gulp 的一天。
Brunch
Brunch显然已经存在多时,但还是很低调、神秘。Brunch主要依赖配置中的约定,并假设你的构建管道包括了我上面说的几乎所有的步骤。当你加入一个插件,被认为加入到了管道中合理的位置,而需要很少甚至不需要任何配置。这意味着一个配置文件在使用 Grunt 时需要600多行代码,使用 Gulp 的时候只需要150多行,如果使用Brunch的话,最多需要20行。Brunch 使用 CoffeeScript 编写,它的所有示例也用CoffeeScript编写,但是转化为JavaScrpt并不难。它天生就支持监控和增量编译而且不需要任何的配置。Brunch可以通过不同的配置直接进行开发vs、生产构建(其他构建配置也很容易设置)。它甚至可以自动包含JavaScript模块(可配置)。
终上所述,你也许觉得我们找到了解决JavaScript构建之道。然而,总会有问题存在。不考虑文档和手册的长度,我还是发现自己希望能找到更清晰的解释和更多的示例。我很吃惊的发现,没有办法去清除已经完成的构建。希望因为我发现了这个问题会把它修复。我还在尝试找到正确的方式使用 Brunch 运行单元测试。对我来说,真正的问题是Node和Bower包管理。因为 Brunch 试图更深入的管理你的代码,甚至将JavaScript代码包装成模块,但这不能很好的与包一起工作。他们宣称对 Bower 提供支持,但是没有很好的说明文档而且似乎结果也不对。引起这一切的也许是 Brunch 在早期广泛地使用了 NPM 和 Bower,现在他们正努力尝试解决这些问题。同时,要准备好应对 Brunch 和 Browerify 的拙劣设计的补丁。最后,对于那些专注于约定大于配置系统的普遍现象,如果你已经偏移设定好的路线,也许会有麻烦。举例来说,如果我试图编译引用这些图像的模版,还在不断尝试如何正确地将缓存失败应用于像图像这样的资源。
总结
我对JavaScript构建工具在2015的发展态势表示失望。它们都很不成熟,而且没有从其他环境和平台的构建工具长期发展的历史中得到启示。另外,这些工具的作者没有好好坐下来去真正地理解所遇到的问题,从而修复问题和确保他们的工具是容易使用的。只有其中的两个,Brunch 和 Mimosa似乎设定了目标,要使绝大多数构建变得十分容易并且不需要繁冗的配置。
所以,我会推荐哪个构建工具呢?个人观点,我还在使用 Brunch 以便确认它能否为我的项目工作。它应该现在处于我的候选列表首位(如果你喜欢约定大于配置方式的 Brunch,你也可以看看 Mimosa)。然而,如果你不介意大量笨拙的配置,你也许会挑选 Gulp 甚至 Grunt,因为它们有更强的社区支持和更多的插件(虽然我还没有在寻找我需要的 Brunch 插件方面遇到困难)。我真的对 Broccoli 的发展有很大兴趣,也希望它能在未来变成一个可行的选择。最后的回答,似乎当今没有一个符合要求的工具存在。一旦包依赖被正确的移除,我想我们会发现现存的所有工具都没有了意义。当然,没人知道到达这个状态需要多久的时间。