昨天在思否上闲逛,发现了一个有意思的问题(点此传送)。javascript
由于这个问题,我产生了写一个系列文章的想法,试图从站在历史的角度上来看待编程世界中林林总总的问题和解决方案。html
目前中文网络上充斥着大量互相“转载”的内容,基本是某一个技术问题的解决方案(what? how?),却不涉及为何这么作和历史原因(why? when?)。好比你要搜 “JavaScript 有哪些模块化方案?它们有什么区别?”,能获得一万个有用的结果;但要想知道 “为何 JavaScript 有这么多模块化方案?它们是谁建立的?”,却几乎不可能。前端
所以,这一系列文章内会尽量的不涉及具体代码,只谈历史故事。但会在文末提供包含部分代码的参考连接,以供感兴趣的朋友自行阅读。java
这个系列暂定为十篇文章,内容会涉及前端、后端、编程语言、开发工具、操做系统等等。也给本身立个 Flag,在今年年末以前把整个系列写完。若是没完成目标……就当我没说过这句话(逃git
模块化,是前端绕不过去的话题。github
随着 Node.js 和三大框架的流行,愈来愈多的前端开发者们脑海中都会时常浮现一个问题:npm
为何 JavaScript 有这么多模块化方案?编程
自从 1995 年 5 月,Brendan Eich 写下了第一行 JavaScript 代码起,JavaScript 已经诞生了 25 年。segmentfault
但这门语言早期仅仅做为轻量级的脚本语言,用于在 Web 上与用户进行少许的交互,并无依赖管理的概念。后端
随着 AJAX 技术得以普遍使用,Web 2.0 时代迅猛发展,浏览器承载了越来越多的内容与逻辑,JavaScript 代码愈来愈复杂,全局变量冲突、依赖管理混乱等问题始终萦绕在前端开发者们的心头。此时,JavaScript 亟需一种在其余语言中早已获得良好应用的功能 —— 模块化。
其实,JavaScript 自己的标准化版本 ECMAScript 6.0 (ES6/ES2015) 中,已经提供了模块化方案,即 ES Module
。但目前在 Node.js 体系下,最多见的方案实际上是 CommonJS
。再加上你们耳熟能详的 AMD
、CMD
、UMD
,模块化的事实标准如此之多。
那么为何有如此之多的模块化方案?它们又是在怎样的背景下诞生的?为何没有一个方案 “千秋万代,一统江湖”?
接下来,我会按照时间顺序讲述模块化的发展历程,顺带也就回答了上述几个问题。
时间回到 2006 年 1 月,当时仍是国际互联网巨头的 Yahoo(雅虎),开源了其内部使用已久的组件库 YUI Library。
YUI Library 采用了相似于 Java 命名空间的方式,来隔离各个模块之间的变量,避免全局变量形成的冲突。其写法相似于:
YUI.util.module.doSomthing();
这种写法不管是封装仍是调用时都十分繁琐,并且当时的 IDE 对于 JavaScript 来讲智能感知很是弱,开发者很难知道他须要的某个方法存在于哪一个命名空间下,常常须要频繁地查阅开发手册,致使开发体验十分不友好。
在 YUI 发布以后不久,John Resig 发布了 jQuery。当时年仅 23 岁的他,不会知道本身这一时兴起在 BarCamp 会议上写下的代码,将占据将来十几年的 Web 领域。
jQuery 使用了一种新的组织方式,它利用了 JavaScript 的 IIFE(当即执行函数表达式)和闭包的特性,将所依赖的外部变量传给一个包装了自身代码的匿名函数,在函数内部就可使用这些依赖,最后在函数的结尾把自身暴露给 window
。这种写法被不少后来的框架所模仿,其写法相似于:
(function(root){ // balabala root.jQuery = root.$ = jQuery; })(window);
这种写法虽然灵活性大大提高,能够很方便地添加扩展,但它并未解决根本问题:所需依赖仍是得外部提早提供,仍是会增长全局变量。
从以上的尝试中,能够概括出 JavaScript 模块化须要解决哪些问题:
围绕着这些问题,JavaScript 模块化开始了一段曲折的探索之路。
让咱们来到 2009 年 1 月,此时距离 ES6 发布尚有 5 年的时间,但前端领域已经迫切地须要一套真正意义上的模块化方案,以解决全局变量污染和依赖管理混乱等问题。
Mozilla 旗下的工程师 Kevin Dangoor,在工做之余,与同事们一块儿制订了一套 JavaScript 模块化的标准规范,并取名为 ServerJS。
ServerJS 最先用于服务端 JavaScript,旨在为配合自动化测试等工做而提供模块导入功能。
这里插一句题外话,其实早期 1995 年,Netsacpe(网景)公司就提供了有在服务端执行 JavaScript 能力的产品,名为 Netscape Enterprise Server。但此时服务端能作的 JavaScript 仍是基于浏览器来实现的,自己没有脱离其自带的 API 范围。直到 2009 年 5 月,Node.js 诞生,赋予了其文件系统、I/O 流、网络通讯等能力,才真正意义上的成为了一门服务端编程语言。
2009 年年初,Ryan Dahl 产生了创造一个跨平台编程框架的想法,想要基于 Google(谷歌)的 Chromium V8 引擎来实现。通过几个月紧张的开发工做,在 5 月中旬,Node.js 首个预览版本的开发工做已所有结束。同年 8 月,欧洲 JSConf 开发者大会上,Node.js 惊艳亮相。
但在此刻,Node.js 尚未一款包管理工具,外部依赖依然要手动下载到项目目录内再引用。欧洲 JSConf 大会结束后,Isaac Z. Schlueter 注意到了 Node.js,两人一拍即合,决定开发一款包管理工具,也就是后来大名鼎鼎的 Node Package Manager(即 npm)。
在开发之初,摆在二人面前的第一个问题就是,采用何种模块化方案?。二人江目光锁定在了几个月前(2009 年 4 月)在华盛顿特区举办的美国 JSConf 大会上公布的 ServerJS。此时的 ServerJS 已经改名为 CommonJS,并从新制订了标准规范,即Modules/1.0,展示了更大的野心,企图一统全部编程语言的模块化方案。
具体来讲,Modules/1.0标准规范包含如下内容:
require(dependency)
,经过传入模块标识来引入其余依赖模块,执行的结果即为别的模块暴漏出来的 API。require
函数引入的模块中也包含外部依赖,则依次加载这些依赖。require
函数应该抛出一个异常。exports
来向外暴露 API,exports
只能是一个 object
对象,暴漏的 API 须做为该对象的属性。因为这个规范简单而直接,Node.js 和 npm 很快就决定采用这种模块化的方案。至此,第一个 JavaScript 模块化方案正式登上了历史舞台,成为前端开发中必不可少的一环。
须要注意的是,CommonJS 是一系列标准规范的统称,它包含了多个版本,从最先 ServerJS 时的 Modules/0.1,到改名为 CommonJS 后的 Modules/1.0,再到如今成为主流的 Modules/1.1。这些规范有不少具体的实现,且不仅局限于 JavaScript 这一种语言,只要遵循了这一规范,均可以称之为 CommonJS。其中,Node.js 的实现叫作 Common Node Modules。CommonJS 的其余实现,感兴趣的朋友能够阅读本文最下方的参考连接。
值得一提的是,CommonJS 虽然没有进入 ECMAScript 标准范围内,但 CommonJS 项目组的不少成员,也都是 TC39(即制订 ECMAScript 标准的委员会组织)的成员。这也为往后 ES6 引入模块化特性打下了坚实的基础。
在推出 Modules/1.0 规范后,CommonJS 在 Node.js 等环境下取得了很不错的实践。
但此时的 CommonJS 有两个重要问题没能获得解决,因此迟迟不能推广到浏览器上:
function
包裹,被导出的变量会暴露在全局中。require
一个模块,只会有磁盘 I/O,因此同步加载机制没什么问题;但若是是浏览器加载,一是会产生开销更大的网络 I/O,二是自然异步,就会产生时序上的错误。所以,社区意识到,要想在浏览器环境中也能顺利使用 CommonJS,势必从新制订新的标准规范。但新的规范怎么制订,成为了激烈争论的焦点,分歧和冲突由此诞生,逐步造成了三大流派:
require
方式改成回调,将同步加载模块变为异步加载模块,这样就能够经过 ”下载 -> 回调“ 的方式,避免时序问题。咱们能够理解为他们是“激进派”。require
等规范仍是有可取之处,不该该随随便便放弃,而是要尽量的保持一致;但激进派的优势也应该吸取,好比 exports
也能够导出其余类型、而不只局限于 object
对象。咱们能够理解为他们是“中间派”。其中保守派的思路跟今天经过 babel 等工具,将 JavaScript 高版本代码转译为低版本代码一模一样,主要目的就是为了兼容。有了这种想法,这派人马提出了 Modules/Transport 规范,用于规定模块如何转译。browserify 就是这一观点下的产物。
激进派也提出了本身的规范 Modules/AsynchronousDefinition,奈何这一派的观点并无获得 CommonJS 社区的主流承认。
中间派一样也有本身的规范 Modules/Wrappings,但这派人马最后也不了了之,没能掀起什么风浪。
激进派、中间派与保守派的理念不和,最终为 CommonJS 社区分裂埋下伏笔。
激进派的 James Burke 在 2009 年 9 月开发出了 RequireJS 这一模块加载器,以实践证实本身的观点。
但激进派的想法始终得不到 CommonJS 社区主流承认。双方的分歧点主要在于执行时机问题,Modules/1.0 是延迟加载、且同一模块只执行一次,而 Modules/AsynchronousDefinition 倒是提早加载,加之破坏了就近声明(就近依赖)原则,还引入了 define
等新的全局函数,双方的分歧愈来愈大。
最终,在 James Burke、Karl Westin 等人的带领下,激进派于同年年末宣布离开 CommonJS 社区,自立门户。
激进派在离开社区后,起初专一于 RequireJS 的开发工做,并无过多的涉足社区工做,也没有此草新的标准规范。
2011 年 2 月,在 RequireJS 的拥趸们的共同努力下,由 Kris Zyp 起草的 Async Module Definition(简称 AMD)标准规范正式发布,并在 RequireJS 社区的基础上创建了 AMD 社区。
AMD 标准规范主要包含了如下几个内容:
define(id, dependencies, factory)
,用于定义模块。dependencies
为依赖的模块数组,在 factory
中需传入形参与之一一对应。dependencies
的值中有 require
、exports
或module
,则与 CommonJS 中的实现保持一致。dependencies
省略不写,则默认为 ['require', 'exports', 'module']
,factory
中也会默认传入三者。factory
为函数,模块能够经过如下三种方式对外暴漏 API:return
任意类型;exports.XModule = XModule
、module.exports = XModule
。factory
为对象,则该对象即为模块的导出值。其中第3、四两点,即所谓的 Modules/Wrappings,是由于 AMD 社区对于要写一堆回调这种作法很有微辞,最后 RequireJS 团队妥协,搞出这么个部分兼容支持。
由于 AMD 符合在浏览器端开发的习惯方式,也是第一个支持浏览器端的 JavaScript 模块化解决方案,RequireJS 迅速被广大开发者所接受。
但有 CommonJS 珠玉在前,不少开发者对于要写不少回调的方式很有微词。在呼吁高涨声中,RequireJS 团队最终妥协,搞出个 Simplified CommonJS wrapping(简称 CJS)的兼容方式,即上文的第3、四两点。但因为背后实际仍是 AMD,因此只是写法上作了兼容,实际上并无真正作到 CommonJS 的延迟加载。
与 CommonJS 规范有众多实现不一样的是,AMD 只专一于 JavaScript 语言,且实现并很少,目前只有 RequireJS 和 Dojo Toolkit,其中后者已经中止维护。
因为 AMD 的提早加载的问题,被不少开发者担忧会有性能问题而吐槽。
例如,若是一个模块依赖了十个其余模块,那么在本模块的代码执行以前,要先把其余十个模块的代码都执行一遍,无论这些模块是否是立刻会被用到。这个性能消耗是不容忽视的。
为了不这个问题,上文提到,中间派试图保留 CommonJS 书写方式和延迟加载、就近声明(就近依赖)等特性,并引入异步加载机制,以适配浏览器特性。
其中一位中间派的大佬 Wes Garland,自己是 CommonJS 的主要贡献者之一,在社区中很受尊重。他在 CommonJS 的基础之上,起草了 Modules/2.0,并给出了一个名为 BravoJS 的实现。
另外一位中间派大佬 @khs4473 提出了 Modules/Wrappings,并给出了一个名为 FlyScript 的实现。
但 Wes Garland 本人是学院派,理论功底十分扎实,但写出的做品却既不优雅也不实用。而实战派的 @khs4473 则在与 James Burke 发生了一些争论,最后删除了本身的 GitHub 仓库并停掉了 FlyScript 官网。
到此为止,中间一派基本已全军覆灭,空有理论,没有实践。
让咱们前进到 2011 年 4 月,国内阿里巴巴集团的前端大佬玉伯(本名王保平),在给 RequireJS 不断提出建议却被拒绝以后,萌生了本身写一个模块加载器的想法。
在借鉴了 CommonJS、AMD 等模块化方案后,玉伯写出了 SeaJS,不过这一实现并无严格遵照 Modules/Wrappings 的规范,因此严格来讲并不能称之为 Modules/2.0。在此基础上,玉伯提出了 Common Module Definition(简称 CMD)这一标准规范。
CMD 规范的主要内容与 AMD 大体相同,不过保留了 CommonJS 中最重要的延迟加载、就近声明(就近依赖)特性。
随着国内互联网公司之间的技术交流,SeaJS 在国内获得了普遍使用。不过在国外,也许是由于语言障碍等缘由,并无获得很是大范围的推广。
2014 年 9 月,美籍华裔 Homa Wong 提交了 UMD 第一个版本的代码。
UMD 即 Universal Module Definition 的缩写,它本质上并非一个真正的模块化方案,而是将 CommonJS 和 AMD 相结合。
UMD 做出了以下内容的规定:
exports
方法,若是存在,则采用 CommonJS 方式加载模块;define
方法,若是存在,则采用 AMD 方式加载模块;global
对象上是否认义了所需依赖,若是存在,则直接使用;反之,则抛出异常。这样一来,模块开发者就可使本身的模块同时支持 CommonJS 和 AMD 的导出方式,而模块使用者也无需关注本身依赖的模块使用的是哪一种方案。
时间前进到 2016 年 5 月,通过了两年的讨论,ECMAScript 6.0 终于正式经过决议,成为了国际标准。
在这一标准中,首次引入了 import
和 export
两个 JavaScript 关键字,并提供了被称为 ES Module 的模块化方案。
在 JavaScript 出生的第 21 个年头里,JavaScript 终于迎来了属于本身的模块化方案。
但因为历史上的先行者已经占据了优点地位,因此 ES Module 迟迟没有彻底替换上文提到的几种方案,甚至连浏览器自己都没有当即做出支持。
2017 年 9 月上旬,Chrome 61.0 版本发布,首次在浏览器端原生支持了 ES Module。
2017 年 9 月中旬,Node.js 迅速跟随,发布了 8.5.0,以支持原生模块化,这一特性被称之为 ECMAScript Modules(简称 MJS)。不过到目前为止,这一特性还处于试验性阶段。
不过随着 babel、Webpack、TypeScript 等工具的兴起,前端开发者们已经再也不关心以上几种方式的兼容问题,习惯写哪一种就写哪一种,最后由工具统一转译成浏览器所支持的方式。
所以,预计在从此很长的一段时间里,几种模块化方案都会在前端开发中共存。
本文以时间线为基准,从做者、社区、理念等几个维度谈到了 JavaScript 模块化的几大方案。
其实模块化方案远不止提到的这些,但其余的都没有这些流行,这里也就不费笔墨。
文中并无说起各个模块化方案是如何实现的,也没有给出相关的代码示例,感兴趣的朋友能够自行阅读下方的参考阅读连接。
下面咱们再总结梳理一下时间线:
时间 | 事件 |
---|---|
1995.05 | Brendan Eich 开发 JavaScript。 |
2006.01 | Yahoo 开源 YUI Library,采用命名空间方式管理模块。 |
2006.01 | John Resig 开发 jQuery,采用 IIFE + 闭包管理模块。 |
2009.01 | Kevin Dangoor 起草 ServerJS,并公布第一个版本 Modules/0.1。 |
2009.04 | Kevin Dangoor 在美国 JSConf 公布 CommonJS。 |
2009.05 | Ryan Dahl 开发 Node.js。 |
2009.08 | Ryan Dahl 在欧洲 JSConf 公布 Node.js。 |
2009.08 | Kevin Dangoor 将 ServerJS 更名为 CommonJS,并起草第二个版本 Modules/1.0。 |
2009.09 | James Burke 开发 RequireJS。 |
2010.01 | Isaac Z. Schlueter 开发 npm,实现了基于 CommonJS 模块化方案的 Common Node Modules。 |
2010.02 | Kris Zyp 起草 AMD,AMD/RequireJS 社区成立。 |
2011.01 | 玉伯开发 SeaJS,起草 CMD,CMD/SeaJS 社区成立。 |
2014.08 | Homa Wong 开发 UMD。 |
2015.05 | ES6 发布,新增特性 ES Module。 |
2017.09 | Chrome 和 Node.js 开始原生支持 ES Module。 |
注:文章中的全部人物、事件、时间、地点,均来自于互联网公开内容,由本人进行搜集整理,其中若有谬误之处,还请多多指教。
首发于 Segmentfault.com,欢迎转载,转载请注明来源和做者。
RHQYZ, Write at 2020.06.24.