本文直接联动阮一峰blog的文章《使用 Make 构建网站》。javascript
这篇文章在描述javascript构建工具的弱点上,是牵强附会和夸大的。grunt和gulp的问题远远没有文章所述那么严重。html
而文中对make工具,仅有教程式的讲述,而缺少对弱点和缺陷的讨论。但恰恰原文的make教程就直接暴露了make的若干设计问题。此时若是没有其余观点对make的弱点加以分析,无疑是不全面的。java
我以为这篇文章从本质上来说,是错误和误导性的,尤为是对新手开发者而言。Make,包括标准的GNU make及其余任何仿品,都不该看成为Web项目的构建工具。node
Makefile
使用大量的隐喻来表述实在的语法意义。python
举一个最简单的例子:无参数的make
命令,执行Makefile定义的第一个任务。这就是一个很是很差的语法。这形成了咱们修改Makefile时必须自行记忆:“任务是否排在首位是不同的”。jquery
而这一点在大多数语言中都不存在——例如类的方法写成什么顺序均可以。在grunt和gulp构建系统中,也都使用default
任务,明确规定无参时默认(default)的行为。符合语义,无需额外思考(Don't make me think)。web
就连Python这样的通用语言,都把用__name__
魔术变量标明主流程,做为编写脚本的一种建议实践:正则表达式
def fun1(): pass def fun2(): pass if __name__ == "__main__": fun1(); fun2()
实在的意义,就应当用实在的语法写清楚,这没有任何能够退让的余地。隐喻赛过明确,这是make脱不了的原罪。shell
这里说的例子是那个.PHONY
伪文件。npm
make仅能处理实在的文件依赖文件的关系。但实际构建中,不免出现抽象、不含实体文件的任务,例如clean——人人须要,但不产出实在文件。这时就要把任务表述成文件,而后用
.PHONY
参数告知make哪些文件是假的。
用伪文件,把“任务”替代成“文件”,存在两点明显的问题:
.PHONY
列表。.PHONY
中,构建过程就会产生无谓的空文件。空文件自己还不可怕,可怕的是若是没有及时发现,就会形成一次构建以后不能再次构建,白白消耗调试时间。而对于任何其余构建方法来讲,都根本不存在这个问题。全部其余构建系统像看怪物同样,用诡异的眼神鄙视着make。
make提出了伪文件这个东西,而且还在手册中建议了“伪文件充当任务名”的用法,我相信make的开发者当初必定注意到了这个需求。可是任务(流程逻辑)和文件(内容存储)毕竟是相关却不一样的两件事,分开管理才是必然的选择。
我不清楚make的开发者是没有想到这一点,仍是自认为“借用过来‘文件依赖文件’的已有模型更加‘简洁’”。但结果上看,这个模型的错误是本质性且不可修正的。这个实现懒惰、简陋而不是所谓的“简洁”,最后的结果也是后患大于收益。
再举一个例子例子:UNIX声称“万物皆文件”,到头来还不是为了避免同设备的逻辑,而保留了“块文件”、“socket文件”之类的区别?
要替代就替代的聪明一些,把实在、重要的本质逻辑保留住。合理、明确,不回避客观区别的替代,和一时拼凑的“workaround”(临时手段)是两回事。后者一时使用尚可,但毫不应充看成为软件基础的“万灵药”。
考试:请仅用Makefile
语法(不依赖shell特性)写一个if/elseif/endif试试?
若是要用某种形式描述一个构建过程,其实:
审查Makefile
的本质设计,实际上是一种描述依赖关系的配置文件,描述了“文件依赖文件”和“文件依赖shell代码”两种关联。但恰恰Makefile
也同时提供简单的流程控制、赋值等语句,使得Makefile
也是一种能够控制流程走向的程序代码。
因此Makefile
恰恰落在了配置和代码二者之间,既不是倒向一端,也不是二者的联合,最后造成了一个“四不像”的混合品。做为配置文件写起来太费神,做为程序代码又太简陋不够用。
我想问:就从Makefile
的设计上来看,那个被奉为圭臬(事实上也确实很优秀)的“UNIX哲学”在哪里?在哪里?
shell使用各类符号来表达语义,难读难写。也就比那个正则表达式简单点很少。
如下两段构建脚本,你愿意读、写或改哪个?
lib_bundle := build/lib.min.js libraries := node_modules/jquery/dist/jquery.js \ node_modules/underscore/underscore.js \ $(lib_bundle): $(libraries) uglifyjs -cmo $@ $^ # What the heck does "c m o @ ^ $" means ???
var gulp = require('gulp'); var concat = require('gulp-concat'); var uglify = require('gulp-uglify'); gulp.task('lib:bundle', function () { return gulp.src(['node_modules/jquery/dist/jquery.js', 'node_modules/underscore/underscore.js']) .pipe(uglify()) .pipe(concat('lib.min.js')) .pipe(gulp.dest('build/')); });
shell是一个严重依赖系统环境的工具。一个make可以正确调用shell脚本,通常都须要:
Makefile
中用到的变量冲突更可怕的是以上这些要素,基本上都是隐喻性的。没有明确的版本控制手段去保证不说,甚至连确认都是不现实的。之前能用的脚本可能换个发行版、升级个系统,甚至于换个用户就可能会发生问题。
咱们既然已经有了npm版本控制,更况且shell构建本质上也是调用基于node的工具,那咱们为何还要去踩shell缺少版本控制这个坑?
shell调用js,每个命令都须要启动/中止node进程,而且各个工具是顺序执行的。
而node构建工具,只须要使用同一个node进程,而且各个工具能够异步启停、并行运行。
这个效率区别是不须要具体比较的。
从历史上来看,shell原本就是为了方便人类执行命令的小工具。然后人类发现了自动执行命令的便利性,从而将shell扩展成为一门轻量的脚本语言,这个发展历程是能够理解的。
其实简短的shell脚本也能够大大方便人类的工做,是个好用的工具。但是一旦shell脚本庞大起来,shell不适合自动化运行和大型程序管理的各类硬伤就开始暴露:
[
。shell从本质上,是方便人类手打命令的终端软件,而不是可靠的自动化工具。本质如此,未来就会一直如此。shell就是shell,也一直只会是shell,不该当赋予其过深的责任和负担。
除非①没有更好的选择②工做实在太少太简单,不然永远不要在正式项目中依赖shell自动化。
必须认可:shell与make工具备很多“坑”,但一旦调试良好,它们确实可以稳定运行。而且工程师们常常会产生这种心态:解决的问题越难,填平的“坑”越多,最后成功时的成就感就越强。
这是一个思惟陷阱。这个陷阱中用过程的复杂度替代了需求的复杂度,从而容易让人错误的评判和看待本身的工做。
可工程毕竟不是智力题。实际需求才是惟一的,只有需求自己的复杂度才须要尊重。代码只不过是完成任务的一种副产品。代码量越少越好,代码引入的额外复杂度越低越好,代码维护起来越容易越好。至于代码自己解决了多少难题,适配了其余工具多少的“坑”,通常都不值一提。
作黑客自有作黑客的合适场景,就如同业余时间作点智力题实际上是个不错的爱好。但实际工程环境下,请老老实实作工程师,使用简单的工具解决同等的问题,不要炫技。
请使用npm的构建工具。我推荐目前(成文时)仍处于测试状态的Gulp 4。
Gulp 4最赞扬的地方是引入了简洁明确的语法,规定命令之间的串并行关系。今后能够把任意形状的加权森林(权值表明执行顺序)简单地表示成Gulp的代码:
gulp.task('default', gulp.series('clean', 'build', 'deploy')) gulp.task('clean', gulp.parallel('clean:a', 'clean:b')) gulp.task('build', gulp.parallel('less', 'uglify')) gulp.task('deploy', gulp.series('revision', 'copy'))
Gulp 4入门请通读《Gulp 4.0 前瞻》这篇文章,以及Gulp 4源代码目录中的全部recipes
(参考代码),很是容易。
Gulp也有Gulp、Node和JavaScript的麻烦(例如并行代码的编写不良通常不会报错),但起码在Web构建这个环境下,比shell值得拥有。
若是你真的须要一些命令行的工具,那也应该舍弃shell这一层,在js、python等正常语言环境下调用它们。命令行工具是不须要shell的,启动子进程而且传递argc、argv的参数才是本质。
原创发表在 SegmentFault.com 博客,转载请遵照 SegmentFault 相关规定(见页脚),做者为沙渺 sha@miao.im。