25 January 2014

原文:Authoring CommonJS modules

CommonJS 模块规范源于早期的服务端 JavaScript 环境,例如 node.jsNarwhal。后来,又专为这些环境进行了优化,但是不包括浏览器环境。

在浏览器中,我们已经习惯于尽量减少缓慢的 HTTP 请求,通常还会把大部分代码合并成少数几个文件。服务端引擎则可以忽略这些麻烦,因为文件访问几乎瞬间就可以完成,进而可以约定:每个文件恰好对应一个模块。这种“文件-模块”模式是可取的,原因如下:

  • 每个模块可以单独编写,从而增加团队的可扩展性。
  • 每个模块可以独立调试,从而降低测试成本。
  • 每个模块的范围和内容是可控的,从而实现模块解耦。

最后一点尤其值得进一步深入研究。

服务器端环境没有受到浏览器端的全局共享作用域的影响。它没有向模块作用域中注入全局变量,例如 documentwindow,而是注入一些仅限定于模块作用域的变量,来帮助编写模块。

规范 CommonJS Modules/1.1 标准化了这些模块作用域变量requireexportsmodule。我们用一个非常简单的 CommonJS 模块来讨论这些变量。

// module app/mime-client
var rest, mime, client;

rest = require('rest');
mime = require('rest/interceptor/mime');

client = rest.chain(mime);

// debug
console.log(module.id); // should log "app/mime-client"

exports.client = client;

注意:上面的代码并没有被封装起来,例如用 立即调用的函数表达式(Immediately Invoked Function Expression,IIFE)AMDdefine(factory),看起来似乎全是一些全局变量,但事实上并非如此!因为每个文件运行在它自己的模块作用域中,var 语句声明的变量实际上被限制在了当前模块的作用域中,就像是包裹在一个函数中一样。

因为同样的原因,require, exportsmodule 这三个 CommonJS 变量也被限制在了模块的作用域中。我们下面来探索下每个变量的细节。

require

如果一个模块需要依赖其他模块才能工作,你可以通过 require 函数把其他模块引入到当前模块的作用域中,只需要为每个模块调用一次 require(id)。通常情况下,需要把每个模块赋值给一个局部变量。例如,前面的例子中引用了两个模块:“rest”和 “rest/interceptor/mime”。

请注意:“rest/interceptor/mime”中的斜杠使它看起来像一个文件路径或 URL,但其实两者都不是!就像 AMD 一样,CommonJS 使用斜杠来表示模块的命名空间,其中,第一个斜杠之前的名称是包名。一个 CommonJS 模块与相关的模块组成一个更大的结构,这个结构称为包 package

exports

最关键的 CommonJS 变量是 exports。这个对象是模块的公开 API,其中只包含了需要暴漏给环境中其他模块的那部分。模块提供的所有对象、函数、构造函数等等,都必须声明为 exports 对象的属性。例如,在前面的例子中,设置属性 clientrest.chain(mime) 所返回的 client 函数。模块的剩余部分则不会被暴漏。

module

变量 module 的初衷是提供关于模块的元数据。它持有每个模块的 id 和唯一 uri。但是,为了克服 exports 模式的不便(见下文),node.js 在 module 上扩展了一个属性 exports,来暴漏 exports 对象。许多其他的环境遵循了 node.js 的实现,所以 module.exports 是通用的。

exports vs. module.exports

变量 exports 是一个对象直接量,它持有当前模块所提供的所有方法和属性。要充分说明这一点,对于这篇教程来说太高深了,但是有一点,这种编码模式允许开发人员自然而然地构造可解析的循环依赖。

但是,如果当前模块是一个简单的函数、构造函数或字符串模板,又该如何呢?许多开发人员认为一个模块应该能够输出任意对象,尤其是函数,尽管这可能导致无法解决的循环依赖。就这样,module.exports 诞生了。

我们来对比下这两种输出模式有何不同。

延续前面的代码示例,另一个模块可以用下面的代码获取到 client 函数:

// we have to require the client module and then access the client property
var client = require('app/mime-client').client;

如果当前模块只输出了 client 函数,会如何呢?我们用 module.exports 来重写前面示例中的模块:

// module app/mime-client
var rest, mime, client;

rest = require('rest');
mime = require('rest/interceptor/mime');

client = rest.chain(mime);

// debug
console.log(module.id); // should log "app/mime-client"

// here is the interesting bit:
module.exports = client;

现在,我们可以更直观地使用 client

// this is much cleaner!
var client = require('app/mime-client');

CommonJS 模块的局限性

许多开发人员把 CommonJS 视为一种非常清爽的模块书写格式,但是浏览器无法直接使用它们,因为浏览器不会为 CommonJS 模块创建模块作用域变量(require, exportsmodule)。在任何应用程序中,当浏览器必须加载数十或数百个模块时,性能都会受到严重影响。你可以通过用工具生成传输格式来解决这一问题,工具会把 CommonJS 模块串联(合并)和封装起来,使它们能够在浏览器中运行。许多这样的工具只使用 AMD 作为传输格式,因为 AMD 可以有效地完成这一任务,并且 AMD 被广泛支持。

例如,cujo.js 的 cram.js 把 CommonJS 模块封装为 AMD 模块,并且把所有模块合并在一起,以便高效加载。

不幸的是,大多数这些工具都需要一个构建过程,来把书写格式转换成传输格式。而 cujo.js 的 curl.js 在大多数情况下不需要这一步骤。因为构建使开发过程变得更加复杂,也很难应用到一个新项目中。

为什么不能用某种对服务端和浏览器都友好的格式编写模块呢?其实是可以的!那就是 UMD(Universal Module Definition,通用模块定义),不过那是下一节课的话题了。

关于 CommonJS 模块的更多细节,请访问 http://wiki.commonjs.org/wiki/Modules/1.1



blog comments powered by Disqus