首页 > 代码库 > JavaScript 模块化入门Ⅰ:理解模块

JavaScript 模块化入门Ⅰ:理解模块

作为一名JS初学者。假如你听到了一些诸如“模块化构建&模块化载入” “Webpack&Browserify” 或者 “AMD&CMD”之类的术语,肯定瞬间就凌乱了。


JavaScript的模块化听起来挺深奥,可其实理解它对开发者来说特别实用。


在这篇文章里,我会尽量深入浅出地把这些深奥的术语翻译成浅显易懂的人话(加上一些代码示例)。希望你多少能从中学到点东西。


为了避免长篇大论,整个内容会分为两篇文章,这是第一部分,主要介绍模块化是什么,为什么要使用模块。之后的第二部分会介绍如何打包JS模块,以及各类构建工具。


求解释到底什么是模块化


称职的作家会把他的书分章节和段落;好的程序员会把他的代码分成模块。


就好像书籍的一章,模块仅仅是一坨代码而已。


好的代码模块分割的内容一定是很合理的,便于你增加减少或者修改功能,同时又不会影响整个系统。


为什么要使用模块


模块化可以使你的代码低耦合,功能模块直接不相互影响。我个人认为模块化主要有以下几点好处:


1.可维护性:根据定义,每个模块都是独立的。良好设计的模块会尽量与外部的代码撇清关系,以便于独立对其进行改进和维护。维护一个独立的模块比起一团凌乱的代码来说要轻松很多。


2.命名空间:在JavaScript中,最高级别的函数外定义的变量都是全局变量(这意味着所有人都可以访问到它们)。也正因如此,当一些无关的代码碰巧使用到同名变量的时候,我们就会遇到“命名空间污染”的问题。


这样的问题在我们开发过程中是要极力避免的。


后面的内容里我也会举一些具体的例子来说明这一点。


3.可复用性:现实来讲,在日常工作中我们经常会复制自己之前写过的代码到新项目中。


复制粘贴虽然很快很方便,但难道我们找不到更好的办法了么?要是……有一个可以重复利用的模块岂不妙哉?


如何引入模块


引入模块有很多种方式,这里我们先介绍一些:


模块模式


模块模式一般用来模拟类的概念(因为原生JavaScript并不支持类,虽然最新的ES6里引入了Class不过还不普及)这样我们就能把公有和私有方法还有变量存储在一个对象中——这就和我们在Java或Python里使用类的感觉一样。这样我们就能在公开调用API的同时,仍然在一个闭包范围内封装私有变量和方法。


实现模块模式的方法有很多种,下面的例子是通过匿名闭包函数的方法。(在JavaScript中,函数是创建作用域的唯一方式。)


例1:匿名闭包函数

JavaScript 模块化入门Ⅰ:理解模块


通过这种构造,我们的匿名函数有了自己的作用域或“闭包”。 这允许我们从父(全局)命名空间隐藏变量。


这种方法的好处在于,你可以在函数内部使用局部变量,而不会意外覆盖同名全局变量,但仍然能够访问到全局变量,如下所示:

JavaScript 模块化入门Ⅰ:理解模块


要注意的是,一定要用括号把匿名函数包起来,以关键词function开头的语句总是会被解释成函数声明(JS中不允许没有命名的函数声明),而加上括号后,内部的代码就会被识别为函数表达式。其实这个也叫作立即执行函数(IIFE)感兴趣的同学可以在这里了解更多


例2:全局引入


另一种比较受欢迎的方法是一些诸如jQuery的库使用的全局引入。和我们刚才举例的匿名闭包函数很相似,只是传入全局变量的方法不同:

JavaScript 模块化入门Ⅰ:理解模块


在这个例子中,globalVariable 是唯一的全局变量。这种方法的好处是可以预先声明好全局变量,让你的代码更加清晰可读。


例3:对象接口


像下面这样,还有一种创建模块的方法是使用独立的对象接口:

JavaScript 模块化入门Ⅰ:理解模块


例4:揭示模块模式 Revealing module pattern


这和我们之前的实现方法非常相近,除了它会确保,在所有的变量和方法暴露之前都会保持私有:

JavaScript 模块化入门Ⅰ:理解模块


到这里,其实我们只聊了模块模式的冰山一角。感兴趣的朋友可以阅读更详细的资料:

    深入理解JavaScript 模块模式

    深入理解JavaScript系列(3):全面解析Module模式


CommonJS & AMD


上述的所有解决方案都有一个共同点:使用单个全局变量来把所有的代码包含在一个函数内,由此来创建私有的命名空间和闭包作用域。


虽然每种方法都比较有效,但也都有各自的短板。


有一点,作为开发者,你必须清楚地了解引入依赖文件的正确顺序。就拿Backbone.js来举个例子,想要使用Backbone就必须在你的页面里引入Backbone的源文件。


然而Backbone又依赖 Underscore.js,所以Backbone的引入必须在其之后。


而在工作中,这些依赖管理经常会成为让人头疼的问题。


另外一点,这些方法也有可能引起命名空间冲突。举个例子,要是你碰巧写了俩重名的模块怎么办?或者你同时需要一个模块的两个版本时该怎么办?


难道就没有不通过全局作用域来实现的模块方法么?


当然是有的。


接下来介绍两种广受欢迎的解决方案:CommonJS 和 AMD.


CommonJS


CommonJS 扩展了JavaScript声明模块的API.


CommonJS模块可以很方便得将某个对象导出,让他们能够被其他模块通过 require 语句来引入。要是你写过 Node.js 应该很熟悉这些语法。


通过CommonJS,每个JS文件独立地存储它模块的内容(就像一个被括起来的闭包一样)。在这种作用域中,我们通过 module.exports 语句来导出对象为模块,再通过 require 语句来引入。


还是举个直观的例子吧:

JavaScript 模块化入门Ⅰ:理解模块


通过指定导出的对象名称,CommonJS模块系统可以识别在其他文件引入这个模块时应该如何解释。


然后在某个人想要调用 myMoudle 的时候,只需要 require 一下:

JavaScript 模块化入门Ⅰ:理解模块


这种实现比起模块模式有两点好处:

    避免全局命名空间污染

    明确代码之间的依赖关系


并且这种书写方式也非常舒服友好,我自己很喜欢。


需要注意的一点是,CommonJS以服务器优先的方式来同步载入模块,假使我们引入三个模块的话,他们会一个个地被载入。


它在服务器端用起来很爽,可是在浏览器里就不会那么高效了。毕竟读取网络的文件要比本地耗费更多时间。只要它还在读取模块,浏览器载入的页面就会一直卡着不动。(在下一篇第二部分的教程里我们会讨论如何解决这个问题)


AMD


CommonJS已经挺不错了,但假使我们想要实现异步加载模块该怎么办?答案就是Asynchronous Module Definition(异步模块定义规范),简称AMD.


通过AMD载入模块的代码一般这么写:

JavaScript 模块化入门Ⅰ:理解模块


这里我们使用 define 方法,第一个参数是依赖的模块,这些模块都会在后台无阻塞地加载,第二个参数则作为加载完毕的回调函数。


回调函数将会使用载入的模块作为参数。在这个例子里就是 myMoudle 和 myOtherModule.最后,这些模块本身也需要通过 define 关键词来定义。


拿 myModule 来举个例子:

JavaScript 模块化入门Ⅰ:理解模块


重申一下,不像CommonJS,AMD是优先浏览器的一种异步载入模块的解决方案。(记得,很多人认为一个个地载入小文件是很低效的,我们将在下一篇文章理介绍如何打包模块)


除了异步加载以外,AMD的另一个优点是你可以在模块里使用对象、函数、构造函数、字符串、JSON或者别的数据类型,而CommonJS只支持对象。


再补充一点,AMD不支持Node里的一些诸如 IO,文件系统等其他服务器端的功能。另外语法上写起来也比CommonJS麻烦一些。


UMD


在一些同时需要AMD和CommonJS功能的项目中,你需要使用另一种规范:Universal Module Definition(通用模块定义规范)。


UMD创造了一种同时使用两种规范的方法,并且也支持全局变量定义。所以UMD的模块可以同时在客户端和服务端使用。


下面是一个解释其功能的例子:

JavaScript 模块化入门Ⅰ:理解模块


更多有关UMD的例子请看其Github上的官方repo.


原生JS


希望你坚持读到了现在,我们最后再介绍一种定义模块的方式。


你可能注意到了,上述的这几种方法都不是JS原生支持的。要么是通过模块模式来模拟,要么是使用CommonJS或AMD.


幸运的是在JS的最新规范ECMAScript 6 (ES6)中,引入了模块功能。


ES6 的模块功能汲取了CommonJS 和 AMD 的优点,拥有简洁的语法并支持异步加载,并且还有其他诸多更好的支持。


我最喜欢的ES6 模块功能的特性是,导入是实时只读的。(CommonJS 只是相当于把导出的代码复制过来)。


来看例子:

JavaScript 模块化入门Ⅰ:理解模块


上面这个例子中,我们一共创建了两份模块的实例,一个在导出的时候,一个在引入的时候。


在 main.js 当中的实例是和原本模块完全不相干的。这也就解释了为什么调用了 counter.increment() 之后仍然返回1。因为我们引入的 counter 变量和模块里的是两个不同的实例。


所以调用 counter.increment() 方法只会改变模块中的 counter .想要修改引入的 counter 只有手动一下啦:

JavaScript 模块化入门Ⅰ:理解模块


而通过 import 语句,可以引入实时只读的模块:

JavaScript 模块化入门Ⅰ:理解模块

这看起来很酷不是么?这样就实现了我们把模块分隔在不同的文件里,需要的时候又可以合并在一起而且不影响它的功能。

稿源:微信定制开发www2.qixoo.com

JavaScript 模块化入门Ⅰ:理解模块