技术开发 频道

3分钟让你读懂JavaScrip的模块化简史!

  【IT168 评论】提到模块化,可能你联想到最多的就是Java 9,但是对于JavaScript中的模块化你可能会比较陌生。本文我们将带你了解JavaScript中的模块化发展。

  脚本标签

  JavaScript发展早期,内联HTML的<script>标签。充其量被卸载到专用脚本文件中,所有这些文件都共享一个全局范围。任何在这些文件或内联脚本中声明的变量都会被打印在全局Windows对象上,从而在完全不相关的脚本中创建漏洞。这些漏洞可能会导致冲突或破坏,从而导致其中脚本中的变量无意中替换全局具有对象依赖性的脚本。

  但是随着web应用程序的规模以及复杂性的增加,范围的概念和全局范围的危险性也被越来越多的人所熟知。

  立即调用函数表达式IIFE逐渐成为主体。IIFE拥有通过将整个文件或文件的部分包在评估后立即执行的功能。而JavaScript中的每个函数都会创建一个新的范围界定,这就意味着var变量将由IIFE来控制。尽管变量声明被提升到包含范围的最高层,但是由于使用了IIFE封装,因此不会变成隐式全局变量,从而暴露了隐式JavaScript全局变量的缺点。

  以下是几种IIFE示例代码,每个IIFE中的代码都是孤立的,代码只能转义到全局中,例如:

3分钟让你读懂Java 9中的模块化简史!

  使用IIFE模式,库通常会通过对Windows对象进行暴露并使用单个绑定来创建模块,从而避免全局命名空间污染。

  下一段代码片段显示了如何在IIFE库中使用sum方法创建mathlib组件。

  假如要在mathlib中添加更多的模块,可以将每个模块放置在一个单独的IIFE中,IIFE会将方法添加到mathlib公共接口中,而剩余的部分会保留在定义新部分组件中。

  然而这种模式也是对JavaScript工具的开放,第一次允许程序员将IIFE模块安全连接到一个文件中,从而极大的减少了网络压力。

  但是,IIFE方法并没有明确的依赖关系树,这就意味着开发人员必须按照精确的顺序来制造组件文件列表,以便在依赖模块之间加载依赖项。

  RequireJS、AngularJS和依赖注入

  这是自从在AngularJS中使用RequireJS或依赖注入机制之类的模块系统以来,我们几乎从未考虑过的一个问题,但是这两者都允许明确地命名每个模块的依赖关系。

  以下示例显示定义mathlib/sum.js库可以使用RequireJS的函数,而且会被被添加到全局范围中,然后再将定义回调的返回值用作模块的公共接口。

3分钟让你读懂Java 9中的模块化简史!

  这样以来就拥有了汇总库中的所有功能的mathlib.js模块。在以下列举的这个例子中只有 mathlib / sum,但依赖项可以同样的被列出。使用数组中的路径列出每个依赖项,并以相同的顺序将其公共接口作为参数回调。

3分钟让你读懂Java 9中的模块化简史!

  定义库之后就可以使用了。下面的代码片中将会介绍依赖关系的解决办法。

3分钟让你读懂Java 9中的模块化简史!

  这就是RequireJS及其固有依赖关系树的优点。无论应用程序中是否包含数百或上千个模块,RequireJS都可以解决依赖树关系,从而不需要仔细维护的列表。鉴于所需的依赖项已经被列出,所以我们消除了每个组件的长列表以及如何相互关联的必要性。但是消除复杂性只是附带的一个方面不是最主要的好处。

  在一个模块级别的依赖声明中,确定了组件和应用程序的关系,也促进了更大程度的模块化。

  RequireJS本身并非不存在问题,整个模式围绕异步加载模块功能,这对于生产部署来说是非常不明智的,因为执行力太差。使用异步加载机制,在许多代码被执行之前会发出数百个网络请求,但是这样以来就必须使用不同的工具来优化,然后才有了verbosity因素影响。这样以后会得到一个依赖列表、一个RequireJS函数调用和模块回调函数。在这一点上有很多不同的RequireJS函数和几种调用方法,使其使用更加复杂。其实API不是最直观的,因为可以同样用依赖项声明模块。

  AngularJS中的依赖注入系统也有同样的问题。当时有一个很完美的解决方案是依靠字符串解析来避免依赖数组,使用函数参数名来解决依赖关系。这种机制与分选程序不兼容,会将参数重命名为单个字符串,从而使注入中断。

  在AngularJS v1的生命周期中,引入了一个构建任务,转化代码如下:

3分钟让你读懂Java 9中的模块化简史!

  以下代码中包含了显示依赖关系列表,这可以被称为一个小型化的安全机制。

3分钟让你读懂Java 9中的模块化简史!

  毫无疑问,这种工具使用步骤繁琐,而且还要构建额外的步骤来维护原有系统,所以程序员大多数都仍继续选择使用熟悉的RequireJS。

  Node.js和CommonJS的出现

  Node.js的创新中有一个CommonJS模块系统,简称CJS。利用Node.js程序可以访问系统文件,而CommonJS标准更符合传统的模块加载机制。在CommonJS中,每个文件都有自己的范围和模块,使用require可以在模块生命周期中随时动态调用同步来加载依赖关系,如下代码片所示:

3分钟让你读懂Java 9中的模块化简史!

  与RequireJS和AngularJS相似,CommonJS依赖关系也涉及路径。但是唯一的区别在于,样板函数和依赖性数组都已经消失了,来自模块的接口也可以分配给一个变量绑定,或者在任何地方都可以使用JavaScript表达式。

  但CommonJS与RequireJS或AngularJS又不同,CommonJS相当严格。在RequireJS或AngularJS中每个文件都有很多动态模块,而CommonJS中的文件和模块之间却是一对一的映射关系。同时,RequireJS除了依赖注入机制与AngularJS框架本身紧密耦合还有好多种声明模块的方法。相比之下,CommonJS却只有一种声明模块的方式。任何JavaScript文件都是一个模块,调用require来加载依赖项,分配给module.exports的任何内容都是接口,这就使工具更容易了解CommonJS组件系统的层次结构。

  最终,Browserify成为了弥合Node.js服务器和浏览器的CommonJS模块之间的差距工具。使用browserify命令行界面程序并为其提供入口点模块的路径,这样可以将任何数量的模块组合到单个浏览器包中。CommonJS(npm包注册表)的特性在帮助模块加载生态系统方面起了决定性作用。

  不过,npm并不只限于CommonJS模块和JavaScript软件包,它的主要作用还有许多。Web应用程序可以在几分钟之内获得数千个软件包(现在已经超过五十万次,稳步增长),再结合Node.js Web服务器和Web服务器重用系统功能,对于其他系统来说,都是一个竞争优势。

  ES6、importBabel和Webpack

  2015年6月以后ES6逐渐变得标准化,一场新的革命也即将到来。ES6规范包括JavaScript原生的模块系统,这个模块系统通常称为ECMAScript模块(ESM)。

  ESM在很大程度上是受到了CJS的影响,提供来了静态的声明API以及基于诺基亚的动态程式API,如下所示:

3分钟让你读懂Java 9中的模块化简史!

  ESM中的每个文件都具有范围和上下文模块。ESM相比于CJS的一个主要优点是ESM的静态导入依赖关系,静态导入允许从系统中每个模块的抽象语法树(AST)进行静态和词法抽取分析,这样就极大的提高了模块系统的内省能力。ESM中的静态导入被限制在模块的最高层,从而进一步简化了解析和内省。

  在Node.js v8.5.0中,ESM模块引入了一个标志。大多数常规浏览器也支持标志后的ESM模块。

  Webpack是Browserify的继承者,由于具有更广泛的功能,Webify在很大程度上取代了通用模块bundler。就像Babel和ES6一样,WebPack长期支持ESM、import和export语句以及动态import()函数,但是由于采用了“代码分割”机制,因此应用程序可以分割成不同的包,首次加载体验性能也被大大的提高。

  鉴于ESM的语言本土化与CJS相反,预计ESM在几年内完全可以超过模块生态系统。

0