JavaScript模块化开发一瞥

  英文原文:JavaScript Modules

  对于那些初学JavaScript、同时又正用它构建大型应用程序的开发者而言,必须面对的首要挑战是,该如何组织代码。尽管起初通过在<script>标记之间嵌入数百行代码就能跑起来,不过很快代码会变得一塌糊涂。其中的难点在于,对于组织我们的代码,JavaScript并未提供任何明显帮助。从字面上看,C#有using,Java有import——而JavaScript一无所有。这就迫使JavaScript作者去尝试各种不同约定(conventions),并用我们拥有的这种语言创建了一些实践方法来组织大型JavaScript应用程序。

形成现代JavaScript基础的那些模式、工具及实践必将来自语言本身以外的实现

——Rebecca Murphy

模块模式(The Module Pattern)

  解决此问题使用最为广泛的方法是模块模式(Module Pattern)。我尝试在下面解释一个基本示例,并谈论它的一些属性。对于各种不同方法更好的描述和梦幻般的运行,请参阅Ben Cherry的帖子——JavaScript Module Pattern: In-Depth(深入理解JavaScript模块模式)。

(function(lab49) {    
functionprivateAdder(n1, n2) {    
returnn1 + n2;    
}    
lab49.add = function(n1, n2) {    
returnprivateAdder(n1, n2); // 原文代码有误,已修正。    
};    
})(window.lab49 = window.lab49 || {});

  上面的示例中,我们只使用一些来自语言本身的基本功能,就创建了曾在C#和Java等语言中见过的类似结构。

  隔离(Isolation)

  你会注意到这段代码包在一个立即调用的函数里(查看最后一行)。在浏览器中,默认情况下会在全局范围(global scope)级别上对JavaScript文件进行评估(evaluated),因此在我们在文件内声明的任何内容都是随处可用的。想象一下,如果在lib1.js中有句var name = '...',而lib2.js中有另一句var name = '...'。那么第二个var语句会替掉第一句的值——这可不太妙。然而,由于JavaScript拥有函数作用域(function scoping)级别,上例中所声明的一切都在该函数自身作用域中,与全局作用域相脱离。这意味着,无论未来在系统中发生什么,位于该函数中的任何内容都会被隔离开来。

 命名空间(Namespacing)

  在最后一行中,你会发现我们要么把window.lab49赋给其自身,要么把空对象直接量(empty object literal)赋给它。尽管看起来有些奇怪,但是让我们一起看下某个虚构的系统,在那里我们拥有若干js文件,所有文件都用了上例中的函数包装器(function wrapper)。

  首个包含进来的文件会评估OR(逻辑或)语句,并发现左侧表达式为undefined(未定义)。由于undefined是虚假值(falsely value),因此OR语句会继续评估右侧表达式,本例中是个空对象直接量。此OR语句实际上是个会返回评估结果的表达式,然后将结果赋给全局变量window.lab49

  现在,轮到下个文件来使用此模式了,它会获得OR语句,并发现window.lab49当前是对象实例 — — 真值(truthy value)。OR语句会短路并返回这个值,并将此值立即赋给其自身 — — 实际上啥也没做。

  这导致的结果是,首个包含进来的文件会创建我们的lab49命名空间(只是个JavaScript对象),而且每个使用这种结构的后续文件都只不过是重用这个现有实例。

  私有状态(Private State)

  正如我们刚才所说,由于位于函数内部,在其内部声明的一切内容都是处于该函数的范围内,而不是全局范围。对于隔离我们的代码这真太棒了,此外,它还有个影响是,没有人能调用它。中看不中用。

  刚刚我们还谈到,我们创建了window.lab49对象来有效管地理我们内容的命名空间。而且这个lab49变量是全局可用的,因为它被附加到window对象上。要想把我们模块中的内容暴露给外部,你可以公开地说,我们要做的就是把一些值附加到全局变量上。正如我们在上例中对add函数所做的一样。现在,在我们的模块外部可以通过lab49.add(2, 2)来调用我们的add函数了。

  在此函数内声明我们的值的另一结果是,如果某个值不是通过将其附加到我们的全局命名空间或者模块外部的某物的方法来显示公开的,那么外部代码将无法碰到它。事实上,我们刚刚就创建了一些私有值。

 CommonJS模块(CommonJS Modules)

  CommonJS是一个主要由服务端JavaScript运行库(server-side JavaScript runtimes)作者组成的小组,他们一直致力于暴露及访问模块的标准化工作(standardize exposing and accessing modules)。值得注意的是,尽管他们提议的模块系统不是来自于创建JavaScript标准同一小组的一个标准,因此它更多地成为JavaScript运行库作者之间的非正式约定(informal convention)。

我通常支持CommonJS的想法,但要搞清楚的是:它并不是一份崇高而神圣的规范(就像ES5一样);它不过是某些人在邮件列表中所讨论的想法。而且这些想法多数都没有付诸实现。

——Ryan Dahl, node.js的创造者

  该模块规范(Modules specification)的核心可谓开门见山。模块(Modules)在它们自己的上下文中进行评估,并且拥有全局变量exports以供模块使用。变量exports只是个普通的JavaScript对象(plain old JavaScript object),甚至你也可以往它上面附加内容,与我们上面展示的命名空间对象类似。为了访问某个模块,你要调用全局函数require,并指明你请求的包的标示符(identifier for the package)。然后评估该模块,并且无论返回什么都会附加到exports上。此模块将会缓存起来,以便后来的require函数调用来使用。

// calculator.js    
exports.add = function(n1, n2) {};    
// app.js    
varcalculator = require('./calculator');    
calculator.add(2, 2);

  如果你曾经玩过Node.js,那么你会发现上面的代码很熟悉。这种用Node来实现CommonJS模块的方式是出奇地简单,在node-inspector(一款Node调试器)中查看某个模块时将显示其包装在某个函数内部的内容,此函数正是传递给exportsrequire的值。非常类似于我们上面展示的手攒模块。

  有几个node项目(Stitch和Browserify),它们将CommonJS模块带进了浏览器。服务器端组件将这些彼此独立的模块js文件塞进一个单独的js文件中,并在那些代码外面包上生成的模块包装器(generated module wrapper)。

  CommonJS主要设计用于服务端JavaScript运行库,而且由于有几个属性使得它们很难在浏览器中进行客户端代码的组织。

  • require必须立即返回——当你已经拥有所有内容时这会工作得非常好,但是当使用脚本加载器(script loader)异步下载脚本时就会有困难。

  • 每个文件一个模块——为了合并为CommonJS模块,必须把它们包裹到一个函数中,然后再组织为某种式样。如果没有某些服务器组件,正如上面提到的那些,就会让它们难以使用,并且在许多环境(ASP.NET,Java)下这些服务器组件尚不存在。

  异步模块定义(Asynchronous Module Definition)

  异步模块定义(Asynchronous Module Definition,通常称为AMD)已设计为适合于浏览器的模块格式。它最初只是一个来自CommonJS小组的提议,但此后移到了GitHub上,而且现在伴有一个适用于模块系统作者的测试套件,以便验证对于AMD API的遵从性(compliance)。

  AMD的核心是define函数。调用define函数最常见的方式是接受三个参数——模块名(也就是说不再与文件名绑定)、该模块依赖的模块标识符数组、以及工厂函数,它将返回该模块的定义。(还有其他的方式调用define函数——详细信息参阅AMD wiki)。

define('calculator', \['adder'\], function(adder) {    
return{    
add: function(n1, n2) {    
returnadder.add(n1, n2);    
}    
};    
});

  由于此模块的定义包在define函数的调用中,因此这就意味着,你可以愉快地在单个js文件内拥有多个模块。此外,由于当调用define模块工厂函数时,模块加载器拥有控制权,因此它可在闲暇之余解决(模块之间的)依赖关系——如果那些模块必须首先异步下载,那就会很方便了。

  为了与原本的CommonJS模块提议保持兼容已作出重大努力。当在模块工厂函数中使用requireexports时会有特殊处理,这意味着,那些传统的CommonJS模块可直接拿来用。

  看起来AMD正在成为颇受欢迎的组织客户端JavaScript应用程序的方式。无论是否通过如RequireJS或curl.js、或是像Dojo等最近已采用AMD的JavaScript应用程序等模块资源加载器来组织代码。

  这是否意味着JavaScript很烂?(Does this mean JavaScript sucks?)

  缺乏将代码组织到模块中的语言级别的结构(language level constructs),这可能会让来自于其他语言的开发者感觉很不爽。然而,正由于此缺陷才迫使JavaScript开发者想出他们自己的模块构造模式,我们已经能够随着JavaScript应用程序的发展进行迭代和改进。欲深入了解此主题请访问Tagneto的博客。

  想象一下,即使这种功能类型(即Module)在10年前就已包括在语言中。那么他们也不可能想到在服务器上运行大型JavaScript应用程序、在浏览器中异步加载资源、或者像文本模板(text templates)(那些载入器就像RequireJS所做的一样)那样包含资源等诸如此类的需求。

  正在考虑将模块(Modules)作为Harmony/ECMAScript 6的语言级别功能。这多亏了模块系统作者们的思想和过去几年的辛勤工作,很可能我们最终得到的语言会适用于构建现代JavaScript应用程序。