JavaScript 模块化开发一瞥

  JavaScript 模块化开发一瞥

  本文作者 / David Padbury

  对于那些正在构建大型应用程序,而对 JavaScript 不甚了解的开发者而言,他们最初必须要面对的挑战之一就是如何着手组织代码。起初只要在 < script > 标记之间嵌入几百行代码就能跑起来,不过很快代码就会变得一塌糊涂。而问题是,JavaScript 没有为组织代码提供任何明显帮助。从字面上看,C# 有 using,Java 有 import——而 JavaScript 一无所有。这迫使 JavaScript 编写者试验不同的约定,并使用现有的语言创建了一些切实可行的方法来组织大型 JavaScript 应用程序。

  模块模式(The Module Pattern)

  用于解决组织代码问题、使用最为广泛的方法之一是模块模式(Module Pattern)。我尝试在下面解释一个基本示例,并讨论其若干特性。要想阅读更精彩的说明,并了解用尽各种不同方法的怪人,那么请参阅 Ben Cherry 的帖子——JavaScript Module Pattern: In-Depth(深入理解 JavaScript 模块模式)。

(function(lab49) {

    function privateAdder(n1, n2) {
        return n1 + n2;
    }

    lab49.add = function(n1, n2) {
        return privateAdder(n1, n2);
    };

})(window.lab49 = window.lab49 || {});

  在上例中,我们使用了一些来自语言的基本功能,从而创造出在 C# 及 Java 等语言中见过的类似结构。

  隔离(Isolation)

  请注意,这段代码包在被立即调用的函数里(仔细看最后一行)。由于在浏览器中,默认情况下会把 JavaScript 文件置于全局作用域级别上进行计算(evaluated),因此在我们的文件中声明的任何内容都是随处可用的。想象一下,要是先在 lib1.js 中声明了 var name = ‘…’,然后又在 lib2.js 声明了 var name = ‘…’。那么后一句 var 声明就会替掉前一句的值——这可不太妙。然而,由于 JavaScript 拥有函数作用域级别,上例中所声明的一切都位于函数自身作用域内,与全局作用域毫无瓜葛。这意味着,无论系统将来如何变化,位于函数中的任何内容都不会受到影响。

  命名空间(Namespacing)

  在最后一行代码中会看到,我们要么将 window.lab49 赋给其自身,要么将空对象 {} 赋给它。尽管看起来有点儿怪,不过让我们一起来看下这样一个虚构系统,系统中的那些 js 文件一律使用了上例中的函数包装器(function wrapper)。

  首个被引入的文件会计算那个或语句(…||…),并发现左侧的表达式 unde?ned(未定义)。由于 undefined 会被判定为假,因此或语句会进一步计算右侧表达式,在本例中就是空对象。或语句实际上是个表达式,它会返回计算结果,进而将结果赋给全局变量 window.lab49。

  现在轮到接下来的文件使用此模式了,它会执行或语句,并发现 window.lab49 目前已是对象实例——真(对象实例会被判定为真)。此时或语句会走捷径,并返回这个会立即赋给其自身的值——其实什么都没做。

  由此导致的结果是,首个被引入的文件会创建 lab49 命名空间(就是个 JavaScript 对象),而且所有使用这种结构的后续文件都只是重用此现有实例。

  私有状态(Private State)

  正如刚才所说,由于位于函数内部,在其内部声明的所有内容都处于该函数的作用域内,而非全局作用域。这对于隔离代码真是棒极了,不过它还带来了一种效果,那就是没人能调用它。真是中看不中用啊!

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

  在此函数中声明一些值的另一结果是,要是某个值没有通过将其附加到全局命名空间或者此模块外部的某个对象上的方式来显示公开,那么外部代码就访问不到该值。实际上,我们恰好创建了一些私有值。

  CommonJS 模块(CommonJS Modules)

  CommonJS 是个社团,主要由服务器端 JavaScript 运行库(server-side JavaScript runtimes)编写者组成,他们致力于将模块的公开及访问标准化的工作。值得注意的是,他们提议的模块系统并非标准,因为它不是出自制定 JavaScript 标准的同一社团,所以它更像是服务器端 JavaScript 运行库编写者彼此之间的非正式约定。

  这份模块规范的核心相当直截了当。所有模块都要在其自身的上下文中进行计算,并且要有个全局变量 exports 供模块使用。而全局变量 exports 只是个普通的 JavaScript 对象,甚至可以自行往上面附加内容,它与上面展示的命名空间对象(lab49)类似。要想访问某个模块,需调用全局函数 require,并指明所请求的包标识符。接着会计算此模块,而且无论返回何值都会将其附加到 exports 上。然后会缓存此模块,以便后来的 require 函数调用。

/ calculator.js
// 计算器模块——译注
exports.add = function(n1, n2) {

};

// app.js
// 某个需要调用计算器模块的应用程序。
// './calculator' 即包标识符。——译注
var calculator = require('./calculator');

calculator.add(2, 2);

  要是摆弄过 Node.js,或许会对以上代码有种似曾相识的感觉。这种用 Node 来实现 CommonJS 模块的方式真是出奇地简单,在 node-inspector(一款 Node 调试器)中查看模块时,会显示其包装在函数内部的内容,这些内容正是传递给 exports 及 require 的值。非常类似于上面展示的手卷模块内容。

  有几个 node 项目(Stitch 及 Browserify),它们将 CommonJS 模块带进了浏览器。服务器端组件会把这些彼此独立的模块 js 文件打包到单独的 js 文件中,并把那些代码用生成的模块包装器包起来。

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

  require 必须立即返回——要是已经拥有所有内容时这会工作得很好,不过这导致难以使用脚本加载器(script loader)去异步下载脚本。

  每个模块占一个文件——为了合并为 CommonJS 模块,必须把它们以某种风格组织起来,并包裹到一个函数中。要是没有类似于上面所提及的服务器组件,那么就难以使用它们,并且在许多环境(ASP.NET,Java)下这些服务器组件尚不存在。

  异步模块定义(Asynchronous Module Definition)

  异步模块定义(Asynchronous Module Definition,通常称为 AMD)已被设计为适合于浏览器的模块格式。它起初源于 CommonJS 社团的提案,不过自从迁移到 GitHub 上以后,现已加入了配套的测试套件,以便模块系统编写者来验证其代码是否符合 AMD 的 API。

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

// 定义calculator(计算器)模块。——译注
define('calculator', ['adder'], function(adder) {
    // 返回具有 add 方法的匿名对象。——译注
    return {
        add: function(n1, n2) {
            /*
             * 实际调用的是adder(加法器)模块的 add 方法。
             * 而且 adder 模块已在前一参数 ['adder'] 中指明了。——译注
             */
            return adder.add(n1, n2);
        }
    };
});

  由于此模块的定义包在 define 函数的调用中,因此这意味着可以欣然将多个模块都放在单个 js 文件中。此外,由于当调用模块工厂函数 define 时,模块加载器已拥有控制权,因此它可以自行安排时间去解决(模块间的)依赖关系——对于那些需要先异步下载的模块,真可谓得心应手。

  为了与原先的 CommonJS 模块提案保持兼容,已做出了巨大的努力。有些特殊行为是为了能在模块工厂函数中使用 require 及 exports,这意味着,那些传统的 CommonJS 模块可直接拿来用。

  看起来 AMD 正在成为颇受欢迎的组织客户端 JavaScript 应用程序的方式。无论是如 RequireJS 或 curl.js 等模块资源加载器,还是像 Dojo 等最近已支持 AMD 的 JavaScript 应用程序,情况都是如此。

  这是否意味着 JavaScript 很烂?

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

  想象一下,即便在 10 年前就已将此类功能引入语言,那么他们也不可能想到后来的那些需求,例如在服务器上运行大型 JavaScript 应用程序、在浏览器中异步加载资源,或者像 text templates(文本模板,就是些文本加载器,其功能类似于 RequireJS)那样引入资源等等。

  正在考虑将模块(Modules)作为 Harmony/ECMAScript 6 的语言级别功能。这多亏了模块系统编写者们的奇思妙想以及过去数年中所做的辛勤工作。更有可能的是,我们最终将得到适合于构建现代 JavaScript 应用程序的语言。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值