介绍昨天在四福闲逛,发现一个有趣的问题(点此发送)。因为这个问题,我萌生了写系列文章的想法,试图从历史的角度来看待编程世界中出现的各种问题和解决方案。目前中国互联网充斥着大量相互“转载”的内容,基本上是解决某个技术问题(what?how?),但不涉及why或历史原因(why?when?).例如,如果你想搜索“JavaScript的模块化方案有哪些?它们之间有什么区别?”,你可以得到10,000个有用的结果;但如果你想知道“为什么JavaScript有这么多模块化方案?它们是谁创造的?”,却几乎不可能。所以本系列文章尽量不涉及具体代码,只讲历史故事。不过文末会提供包含部分代码的参考链接,有兴趣的朋友可以自行阅读。这个系列暂定十篇,涵盖前端、后端、编程语言、开发工具、操作系统等。也给自己立个Flag,今年年底前把整个系列写完。如果没有达到目标...就当我没说这句话吧(转义全系列索引:《编程时间简史系列》JavaScript模块化历史进程《编程时间简史系列》WebServer编年史文本模块化是前端搞不定的话题随着Node.js和三大框架的流行,越来越多的前端开发者脑子里经常会有一个疑问:为什么JavaScript会有这么多模块化的解决方案?自1995年5月,BrendanEich编写了第一个JavaScript从JavaScript代码诞生至今已经25年了,然而这门语言早期只是作为一种轻量级的脚本语言,用于在Web上与用户进行少量的交互,并没有依赖管理的概念。随着AJAX技术的广泛应用,Web2.0时代飞速发展,浏览器承载的内容和逻辑越来越多,JavaScript代码越来越复杂,全局变量冲突、依赖管理混乱等问题始终萦绕在人们的脑海中前端开发人员。这时候,JavaScript急需一个在其他语言中已经得到很好应用的特性——模块化。事实上,在JavaScript本身的标准化版本ECMAScript6.0(ES6/ES2015)中,已经提供了模块化的解决方案,即ESModule。但是目前在Node.js体系下,最常见的解决方案其实是CommonJS。再加上大家熟悉的AMD、CMD、UMD,模块化的标准实在是太多了。那么为什么会有这么多的模块化解决方案呢?他们是在什么情况下出生的?为什么没有“千世一统江湖”的打算?下面我将按时间顺序讲述模块化的发展历程,顺便回答一下以上问题。萌芽:说起YUILibrary和jQuery,时间要追溯到2006年1月,当时还是国际互联网巨头的雅虎(Yahoo)开源了其长期的内部组件库YUILibrary。YUILibrary使用类似于Java命名空间的方法来隔离模块之间的变量,避免全局变量引起的冲突。它的写法类似于:YUI.util.module.doSomthing();这种写法无论是封装还是调用都非常繁琐,而且当时的IDE对JavaScript的智能感知非常弱,所以开发者很难知道自己需要什么方法存在于哪个namespace中,往往需要频繁查阅开发手册,导致非常不友好的开发体验。YUI发布后不久,JohnResig发布了jQuery。23岁的他不会知道自己在BarCamp大会上心血来潮写的代码会在接下来的十年左右占据Web领域。jQuery使用了一种新的组织方式,利用了JavaScript的IIFE(立即执行的函数表达式)和闭包的特点,将依赖的外部变量传递给一个包裹了自己代码的匿名函数。这些依赖项可用于在函数结束时最终将自身暴露给窗口。这种写法被后来的很多框架模仿,其写法类似于:(function(root){//balabalaroot.jQuery=root.$=jQuery;})(window);这种写法虽然灵活性大大提高,添加扩展也很方便,但是并没有解决根本问题:仍然必须提前在外部提供所需的依赖,否则会添加全局变量。从上面的尝试,我们可以总结出JavaScript模块化需要解决的问题:如何给一个模块一个唯一的标识符?如何在模块中使用依赖的外部模块?如何安全地包装模块(不污染模块外部的代码)?如何优雅地暴露模块(不添加全局变量)?围绕这些问题,JavaScript模块化开始了曲折的探索之路。探索之路:CommonJS和Node.js的诞生把我们带到了2009年1月,此时距离ES6的发布还有5年的时间,但前端领域已经迫切需要一套模块化的解决方案真正意义上的。解决全局变量污染、依赖管理混乱等问题。Mozilla旗下的工程师KevinDangoor在业余时间与同事一起制定了JavaScript模块化的标准规范,并将其命名为ServerJS。ServerJS最早用于服务端JavaScript,为自动化测试等工作提供模块导入功能。这里插一句题外话。事实上,早在1995年,Netsacpe(网景)公司就提供了一款具有在服务器端执行JavaScript能力的产品,名为NetscapeEnterpriseServer。但此时服务端能做的JavaScript还是基于浏览器实现的,并没有脱离自身的API作用域。直到2009年5月,Node.js诞生,赋予了文件系统、I/O流、网络通信等能力,成为真正意义上的服务器端编程语言。2009年初,RyanDahl有了创建跨平台编程框架的想法,他想基于Google的ChromiumV8引擎来实现。经过几个月紧张的开发工作,5月中旬,Node.js第一个预览版的开发已经完成。同年8月,Node.js惊艳亮相欧洲JSConf开发者大会。但是目前Node.js没有包管理工具,外部依赖还是需要手动下载到项目目录下再引用。在欧洲JSConf会议之后,IsaacZ.Schlueter注意到了RyanDahl的Node.js。两人一拍即合,决定开发一个包管理工具,这就是后来大名鼎鼎的NodePackageManager(npm)。开发之初,摆在两人面前的第一个问题是,应该采用什么样的模块化方案?.两人将目光投向了几个月前(2009年4月)在华盛顿举行的美国JSConf会议上宣布的ServerJS。此时ServerJS更名为CommonJS,并重新制定了标准规范,即Modules/1.0,显示出更大的野心和统一所有编程语言的模块化解决方案的尝试。具体来说,Modules/1.0标准规范包括以下内容:模块标识应遵循一定的书写规则。定义全局函数require(dependency),通过传入模块标识引入其他依赖模块,执行结果为其他模块暴露的API。如果require函数导入的模块还包含外部依赖,则依次加载这些依赖。如果导入模块失败,require函数应该抛出异常。该模块通过变量导出公开API。exports只能是object对象,暴露的API必须作为对象的属性。由于该规范简单明了,Node.js和npm很快决定采用这种模块化方法。至此,第一个JavaScript模块化方案正式登上历史舞台,成为前端开发不可或缺的一部分。需要注意的是,CommonJS是一系列标准规范的统称,包括多个版本,从刚推出ServerJS时的Modules/0.1,到改名为CommonJS后的Modules/1.0,再到现在的Modules/1.1,有了成为主流。这些规范有很多具体的实现,并不局限于JavaScript语言。只要遵循这个规范,就可以称为CommonJS。其中,Node.js的实现称为CommonNodeModules。对于CommonJS的其他实现,感兴趣的朋友可以阅读本文底部的参考链接。值得一提的是,虽然CommonJS没有进入ECMAScript标准的范围,但是CommonJS项目组的很多成员同时也是TC39(制定ECMAScript标准的委员会组织)的成员。这也为以后在ES6中引入模块化特性打下了坚实的基础。分道扬镳:CommonJS历史交汇处的选择在Modules/1.0规范推出后,CommonJS在Node.js等环境中取得了很好的实践。但此时,CommonJS有两个重要的问题没有解决,所以很长一段时间不能扩展到浏览器:因为没有外层的函数包,所以导出的变量会全局暴露。如果在服务器端需要一个模块,只会有磁盘I/O,所以同步加载机制没有问题;但如果由浏览器加载,首先,它会产生更昂贵的网络I/O,其次,它自然是异步的。时间错误。因此,社区意识到,要想在浏览器环境中顺利使用CommonJS,有必要制定一个新的标准规范。然而,如何制定新的规范成为激烈争论的焦点,产生分歧和冲突,逐渐形成三大流派:Modules/1.如果你有很好的实践经验,只需要移植到浏览器端即可。在浏览器加载模块之前,通过工具将模块转换成浏览器可以运行的代码。我们可以理解为他们是“保守派”。Modules/Async派:该派认为,由于浏览器环境与服务器环境差异太大,不应在Modules/1.0的基础上继续小修小补,应遵循浏览器自身特点,摒弃require方法并将其改为callback,将同步加载模块改为异步加载模块,这样可以通过“下载->回调”方法避免时序问题。我们可以理解为他们是“激进分子”。Modules/2.0派:该派也认为不应该使用Modules/1.0,但不像激进派那样过于激进。它认为像require这样的规范还是有可取之处的,不应该随便抛弃,而是尽可能的放弃。始终如一;但是部首的优点也要吸收,比如exports还可以导出其他类型,不局限于object对象。我们可以理解为他们是“中间派”。其中,保守派的想法与今天通过babel等工具将JavaScript高版本代码翻译成低版本代码如出一辙,主要目的是为了兼容性。考虑到这一点,这群人提出了Modules/Transport规范,用于指定模块如何翻译。browserify就是这种观点的产物。Radicals也提出了自己的规范Modules/AsynchronousDefinition,但这一派的观点并未得到CommonJS社区主流的认可。中间派也有自己的规格Modules/Wrappings,但是这群人最后都销声匿迹了,也没能掀起什么波澜。激进派、中间派和保守派之间的不和最终为CommonJS社区的分裂铺平了道路。争议:激进派——AMD的崛起激进派的JamesBurke在2009年9月通过开发模块加载程序RequireJS证明了他的观点。但激进的想法从未被CommonJS社区的主流认可。双方的主要区别在于执行时间。Modules/1.0是延迟加载,同一个模块只执行一次,而Modules/AsynchronousDefinition是提前加载,打破了最近声明(最近依赖)的原则,引入了define等新的定义。双方的全球作用越来越大。最终,激进派在JamesBurke、KarlWestin等人的带领下,于同年年底宣布离开CommonJS社区,组建自己的门户网站。激进分子离开社区后,最初专注于RequireJS的开发,并没有过多的参与社区工作,也没有这个新的标准规范。2011年2月,在RequireJS爱好者的共同努力下,由KrisZyp起草的AsyncModuleDefinition(AMD)标准规范正式发布,AMD社区在RequireJS社区的基础上成立。AMD标准规范主要包括以下内容:ModuleidentificationfollowedCommonJSModuleIdentifiers。定义全局函数define(id,dependencies,factory),用于定义模块。dependencies是一个依赖模块的数组,需要传入其中一个正式参与者,在工厂中进行一一对应。如果dependencies的值中有require、exports或module,则与CommonJS中的实现一致。如果省略dependencies,则默认为['require','exports','module'],工厂默认也会传入这三个。如果工厂是一个函数,模块可以通过以下三种方式暴露API:返回任何类型;exports.XModule=XModule,module.exports=XModule。如果factory是对象,则该对象是模块的导出值。其中,第三点和第四点,也就是所谓的Modules/Wrappings,是因为AMD社区对写一堆回调有一些怨言。最后,RequireJS团队妥协,搞出了这么一个部分兼容的支持。因为AMD符合浏览器端开发的习惯方式,也是第一个支持浏览器端的JavaScript模块化方案,所以RequireJS很快被开发者接受。但是随着CommonJS的发展,许多开发人员抱怨他们必须编写大量回调的方式。在一片呼声中,RequireJS团队最终妥协,提出了兼容SimplifiedCommonJSwrapping(简称CJS)的方法,也就是上面的第三点和第四点。不过由于AMD其实在背后支持,所以只是写上兼容,并没有真正实现CommonJS的懒加载。与CommonJS规范的众多实现不同,AMD只专注于JavaScript语言,实现并不多。目前只有RequireJS和DojoToolkit,后者已经停止维护。一波三折:中间派——CMD的没落是因为AMD的预加载问题,很多开发者担心性能问题。例如,如果一个模块依赖于其他十个模块,则必须在执行该模块的代码之前执行其他十个模块的代码,而不管这些模块是否会被立即使用。这种性能成本不容忽视。为了避免这个问题,如前所述,中间派尝试保留CommonJS的写法、延迟加载、附近声明(最近依赖)等特性,引入异步加载机制来适应浏览器特性。WesGarland是中间派之一,他本人是CommonJS的主要贡献者之一,在社区中备受尊重。基于CommonJS,他起草了Modules/2.0,并给出了一个实现,叫做BravoJS。另一位中间派@khs4473提出了Modules/Wrappings并给出了一个名为FlyScript的实现。但韦斯·加兰本人是学者,理论基础扎实,但写出来的作品既不优雅也不实用。这位@khs4473与JamesBurke发生了一些争执,最后删除了他的GitHub仓库并停止了FlyScript官网。至此,中间派基本被消灭,无理论无实践。时光倒流到2011年4月,国内阿里巴巴集团前端负责人余波(本名王保平)在向RequireJS提出建议被拒绝后萌生了自己写一个模块加载器的想法。在学习了CommonJS、AMD等模块化方案后,余波写了SeaJS,但这个实现并没有严格遵守Modules/Wrappings规范,所以严格来说不能称为Modules/2.0。在此基础上,宇博提出了通用模块定义(CommonModuleDefinition,简称CMD)的标准规范。CMD规范的主要内容与AMD大致相同,但保留了CommonJS中延迟加载和附近声明(最近依赖)最重要的特性。随着国内互联网公司之间的技术交流,SeaJS在国内得到了广泛的应用。但在国外,或许因为语言不通等原因,并未得到广泛推广。兼容性:统一UMD2014年9月,美籍华人HomaWong提交了UMD第一版代码。UMD是UniversalModuleDefinition的缩写。它本质上并不是真正的模块化解决方案,而是CommonJS和AMD的结合。UMD做了如下规定:首先判断是否有exports方法,如果有则使用CommonJS方法加载模块;其次判断是否有define方法,如果存在则使用AMD方法加载模块;最后判断全局对象是否定义,如果需要的依赖存在,则直接使用;否则,将抛出异常。通过这种方式,模块开发者可以让他们的模块同时支持CommonJS和AMD的导出方式,而模块的使用者则无需关注自己所依赖的模块使用的是哪种方案。姗姗来迟:ES6/ES2015正式发布的时间提前到了2016年5月。经过两年的讨论,ECMAScript6.0终于通过了决议,成为国际标准。在这个标准中,首次引入了import和export两个JavaScript关键字,并提供了一种称为ESModule的模块化方案。在它的第21个年头,JavaScript终于有了自己的模块化解决方案。但是,由于历史上的先行者已经占据优势地位,ESModule并没有完全取代上面提到的几种方案,甚至连浏览器本身也没有立即支持。2017年9月上旬,Chrome61.0发布,首次在浏览器端原生支持ESModule。2017年9月中旬,Node.js迅速跟进,发布了8.5.0版本,支持原生模块化。此功能称为ECMAScript模块(简称MJS)。但到目前为止,该功能还处于试验阶段。不过随着babel、Webpack、TypeScript等工具的兴起,前端开发者已经不再关心上述方式的兼容性问题。他们可以写他们习惯的任何东西,最后工具将把它翻译成浏览器支持的东西。方式。因此,预计在未来很长一段时间内,前端开发中将有多种模块化方案并存。最后,本文以时间轴为基准,从作者、社区、哲学等多个维度谈了JavaScript模块化的几大解决方案。其实模块化的方案远不止这几种,只是其他的没那么流行,这里就没必要写了。文章没有提及每种模块化方案是如何实现的,也没有给出相关的代码示例。感兴趣的朋友可以自行阅读下面的参考阅读链接。接下来我们总结梳理一下时间线:时间事件1995.05BrendanEich开发JavaScript。2006.01Yahoo开源YUI库,使用命名空间管理模块。2006.01JohnResig开发jQuery,使用IIFE+闭包管理模块。2009.01KevinDangoor起草了ServerJS并发布了第一个版本Modules/0.1。2009.04KevinDangoor在美国JSConf发表CommonJS。2009.05RyanDahl开发了Node.js。2009.08RyanDahl在JSConfEurope上宣布了Node.js。2009.08KevinDangoor将ServerJS更名为CommonJS,并起草第二版Modules/1.0。2009.09JamesBurke开发RequireJS。2010.01IsaacZ.Schlueter基于CommonJS模块化方案开发npm并实现CommonNodeModules。2010.02KrisZyp选中了AMD,AMD/RequireJS社区成立。2011.01宇博开发SeaJS,起草CMD,CMD/SeaJS社区成立。2014.08HomaWong开发UMD。2015.05ES6发布,增加ESModule特性。2017.09Chrome和Node.js开始原生支持ESModule。注:文中所有人物、事件、时间、地点均来自网络公开内容,自行收集整理,如有错误,请多多指教。参考阅读《Wikipedia - YUI Library》《Wikipedia - jQuery》《Wikipedia - List of server-side JavaScript implementations》《Wikipedia - CommonJS》《Wikipedia - Asynchronous Module Definition》《CommonJS Project History》《RequireJS Project History》《JavaScript Modules: A Brief History》《浅析 JS 模块规范:AMD,CMD,CommonJS》《JavaScript Module Loader - CommonJS,RequireJS,SeaJS 归纳笔记》《前端模块化开发那点历史》首发于Segmentfault.com,欢迎转载,转载请注明出处和作者。RHQYZ,写于2020.06.24。
