为什么用 Make 构建网站绝对不是一个好主意

本文直接联动阮一峰blog的文章《使用 Make 构建网站》。

这篇文章在描述javascript构建工具的弱点上,是牵强附会和夸大的。grunt和gulp的问题远远没有文章所述那么严重。

而文中对make工具,仅有教程式的讲述,而缺乏对弱点和缺陷的讨论。但偏偏原文的make教程就直接暴露了make的若干设计问题。此时如果没有其他观点对make的弱点加以分析,无疑是不全面的。

我觉得这篇文章从本质上来讲,是错误和误导性的,尤其是对新手开发者而言。Make,包括标准的GNU make及其他任何仿品,都不应当作为Web项目的构建工具

make:看似简单实则简陋

隐喻胜于明确

Makefile使用大量的隐喻来表述实在的语法意义。

举一个最简单的例子:无参数的make命令,执行Makefile定义的第一个任务。这就是一个非常不好的语法。这造成了我们修改Makefile时必须自行记忆:“任务是否排在首位是不一样的”。

而这一点在大多数语言中都不存在——例如类的方法写成什么顺序都可以。在grunt和gulp构建系统中,也都使用default任务,明确规定无参时默认(default)的行为。符合语义,无需额外思考(Don't make me think)。

就连Python这样的通用语言,都把用__name__魔术变量标明主流程,作为编写脚本的一种建议实践:

def fun1(): pass
def fun2(): pass
if __name__ == "__main__": fun1(); fun2()

实在的意义,就应当用实在的语法写清楚,这没有任何可以退让的余地。隐喻胜过明确,这是make脱不了的原罪。

丑陋的“workaround”(临时手段)

这里说的例子是那个.PHONY伪文件。

make仅能处理实在的文件依赖文件的关系。但实际构建中,难免出现抽象、不含实体文件的任务,例如clean——人人需要,但不产出实在文件。这时就要把任务表述成文件,然后用.PHONY参数告知make哪些文件是假的。

用伪文件,把“任务”替代成“文件”,存在两点明显的问题:

  1. “任务”与“文件”的命名空间直接产生冲突。
    任务占用的名称,就必须人工注意实在文件不能再用,否则必然产生麻烦。
  2. 增删任务时,必须人工注意维护.PHONY列表。
    如果忘了把任务补到.PHONY中,构建过程就会产生无谓的空文件。空文件本身还不可怕,可怕的是如果没有及时发现,就会造成一次构建之后不能再次构建,白白消耗调试时间。

而对于任何其他构建方法来说,都根本不存在这个问题。所有其他构建系统像看怪物一样,用诡异的眼神鄙视着make。

make提出了伪文件这个东西,并且还在手册中建议了“伪文件充当任务名”的用法,我相信make的开发者当初一定注意到了这个需求。但是任务(流程逻辑)和文件(内容存储)毕竟是相关却不同的两件事,分开管理才是必然的选择。

我不清楚make的开发者是没有想到这一点,还是自认为“借用过来‘文件依赖文件’的已有模型更加‘简洁’”。但结果上看,这个模型的错误是本质性且不可修正的。这个实现懒惰、简陋而不是所谓的“简洁”,最后的结果也是后患大于收益。

再举一个例子例子:UNIX声称“万物皆文件”,到头来还不是为了不同设备的逻辑,而保留了“块文件”、“socket文件”之类的区别?

要替代就替代的聪明一些,把实在、重要的本质逻辑保留住。合理、明确,不回避客观区别的替代,和一时拼凑的“workaround”(临时手段)是两回事。后者一时使用尚可,但绝不应充当作为软件基础的“万灵药”。

Makefile:介于语言和配置之间的“四不像”

考试:请仅用Makefile语法(不依赖shell特性)写一个if/elseif/endif试试?

如果要用某种形式描述一个构建过程,其实:

  • 可以表示成纯粹的配置文件。不可独立运行,不含任何实质的代码,但绝对便于编写、修改。
  • 可以表述成真正的程序代码。绝对灵活,所有语言特性随便用。
  • 但一般都代码和配置文件联合使用,兼取两者之长。(即使是纯代码,其实数据和执行逻辑也会有一定的分离,而不是混在一起搞成“意大利面式编程”)

审查Makefile的本质设计,其实是一种描述依赖关系配置文件,描述了“文件依赖文件”和“文件依赖shell代码”两种关联。但偏偏Makefile也同时提供简单的流程控制、赋值等语句,使得Makefile也是一种可以控制流程走向的程序代码

所以Makefile偏偏落在了配置代码两者之间,既不是倒向一端,也不是两者的联合,最后形成了一个“四不像”的混合品。作为配置文件写起来太费神,作为程序代码又太简陋不够用。

我想问:就从Makefile的设计上来看,那个被奉为圭臬(事实上也确实很优秀)的“UNIX哲学”在哪里?在哪里?

shell:躲不开的雷区

符号胜于语义

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脚本,一般都需要:

  • 系统内有GNU工具链
  • 工具链的详细用法(参数意义等),不能与编写者产生严重的冲突,甚至不能进行造成deprecated(弃用)的升级
  • PATH等基本的环境变量正确
  • 但其他的环境变量还不能与Makefile中用到的变量冲突

更可怕的是以上这些要素,基本上都是隐喻性的。没有明确的版本控制手段去保证不说,甚至连确认都是不现实的。以前能用的脚本可能换个发行版、升级个系统,甚至于换个用户就可能会发生问题。

我们既然已经有了npm版本控制,更何况shell构建本质上也是调用基于node的工具,那我们为什么还要去踩shell缺乏版本控制这个坑?

shell调用js的性能问题

shell调用js,每一个命令都需要启动/停止node进程,并且各个工具是顺序执行的。

而node构建工具,只需要使用同一个node进程,并且各个工具可以异步启停、并行运行。

这个效率区别是不需要具体比较的。

总结

“shell自动化”——邪道之路

从历史上来看,shell本来就是为了方便人类执行命令的小工具。而后人类发现了自动执行命令的便利性,从而将shell扩展成为一门轻量的脚本语言,这个发展历程是可以理解的。

其实简短的shell脚本也可以大大方便人类的工作,是个好用的工具。可是一旦shell脚本庞大起来,shell不适合自动化运行和大型程序管理的各种硬伤就开始暴露:

  • 提倡使用特殊符号,过于强调语法简短(首要问题)。
    这一点对于人类操作shell是优势,谁都想在命令行下少打几个字。
    但如果用于长时间编写、快速运行、长期维护的正式项目,shell的这个特点就会立刻表现为难读、难写、难改的短板。
  • 缺少大型项目所需的版本管理、面向对象等特性
  • 语言功能不足,例如缺乏最基本的数值计算
  • 语言特性诡异,例如bash大多数情况下对空白字符不敏感,可偏偏变量赋值的等号两边不准加空格
  • 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值