原文:https://addyosmani.com/resources/essentialjsdesignpatterns/book/#detailcommonjs
针对 Server 端优化的模块格式
CommonJS 模块提案具体指定了一个用于声明服务端模块的简单 API,它与 AMD 不同,它尝试去覆盖更广泛的问题,如 io、文件系统、promise 等等。这个格式是由 CommonJS 提出的 - 一个志愿者工作组,他们的目标是设计、制作原型并标准化 JavaScript 的 API。迄今为止,他们已经尝试批准 模块 和 包 的标准。
入门指南
从架构的角度来看,CommonJS 的模块是一个可复用的 JavaScript 片段,它导出一个可供任何相关代码使用的特定对象。与 AMD 不同,它的模块通常没有包装函数(所以我们在这里看不到 define
)。
CommonJS 模块基本都包含两个主要的部分:一个名为 exports
的自由变量,它包含一个模块希望暴露给其他模块使用的对象;另一个是 require
函数,模块可以用它来引入其他模块的导出内容。
理解 CommonJS:require() 和 exports
// package/lib 是我们需要的依赖
var lib = require('package/lib');
// 我们模块的行为
function foo() {
lib.log('hello world!');
}
// 导出 foo 给其他模块使用
exports.foo = foo;
exports 的基本用法
// 定义更多我们希望导出的行为
function foobar() {
this.foo = function() {
console.log('Hello foo');
};
this.bar = function() {
console.log('Hello bar');
};
}
// 导出 foobar 给其他模块
exports.foobar = foobar;
// 使用 “foobar” 的应用
// 根据相对路径访问模块,
// 其中两个文件都存在同一个目录中
var foobar = require('./foobar').foobar,
test = new foobar();
// 输出: "hello bar"
test.bar();
与首个 CommonJS 示例等效的 AMD 的实现
define(function(require) {
var lib = require('package/lib');
// 与我们模块中相同的行为
function foo() {
lib.log('hello world!');
}
// 导出 foo 给其他模块
return {
foobar: foo
};
});
上例能够运行是因为 AMD 支持 简化的 CommonJS 包装 的功能
使用多依赖的情况
app.js
var modA = require('./foo');
var modB = require('./bar');
exports.app = function() {
console.log('Im an application!');
};
exports.foo = function() {
return modA.helloWorld();
};
bar.js
exports.name = 'bar';
foo.js
require('./bar');
exports.helloWorld = function() {
return 'Hello World!!';
};
那些加载器或者框架支持 CommonJS?
浏览器端:
- curl.js http://github.com/unscriptable/curl
- SproutCore 1.1 http://sproutcore.com
- PINF http://github.com/pinf/loader-js
服务端:
- Node http://nodejs.org
- Narwhal https://github.com/tlrobinson/narwhal
- Persevere http://www.persvr.org/
- Wakanda http://www.wakandasoft.com/
CommonJS 是否适合浏览器环境?
有些开发者认为 CommonJS 更适合服务端开发,这也是为什么在 Harmony 时代到来之前,哪种格式应该并且实际被应用这个问题一直存在 分歧 的原因之一。反对 CommonJS 的观点中有一条就是许多的 CommonJS API 都是基于服务端的功能,而这些功能在浏览器环境都是无法被实现的,如:io、系统,和 js ,由于其功能的特性而被认为是无法实现。
也就是说,知道如何构建 CommonJS 模块非常有用,这样我们就能更好的在定义模块时意识到它们适合用在什么地方。同时能应用在服务端和客户端的模块包含验证、转换和模板引擎。一些开发者选择的方式是当模块被应用在服务端时使用 CommonJs,其他情况则使用 AMD。
因为 AMD 模块是支持使用插件的,并且可以定义更细粒度的东西,如构造器和函数,这就变得有意义。CommonJS 只能用于定义对象,如果我们视图从中获取构造函数,那么使用它们就会变得很繁琐。
尽管它超出了本节的范围,可能有人已经注意到了当我们讨论 AMD 和 CommonJS时,我们有提到不同的 require
方法。相同命名规则肯定会导致困惑,并且社区在全局 require 函数的优点上还存在分歧。John Hann 对此的建议是不要称之为 ”require”,因为它无法让使用者区分全局和内部的 require,而是将全局的加载器重命名为更有意义的命名(如,包的名称)。这也是想 curl.js 这样的库使用 curl()
而不是 require
的原因。
相关阅读
- Demystifying CommonJS Modules
- JavaScript Growing Up
- The RequireJS Notes On CommonJS
- Taking Baby Steps With Node.js And CommonJS - Creating Custom Modules
- Asynchronous CommonJS Modules for the Browser
-
AMD 和 CommonJS 相互竞争,但都是有效的标准
AMD 和 CommonJS 都是用于不同目的的有效模块格式。AMD 采取的是浏览器优先的策略进行开发,选择异步行为和简单的向后兼容,但是它不涉及任何文件、IO 等概念。它支持对象、函数、构造器、字符串、JSON和其它类型的模块,在浏览器本地运行。它非常的灵活。
另一方面,CommonJS 采用的是服务端优先的策略,假定是同步行为、没有全局 包袱 并且尝试着迎合未来的标准(在服务端)。我们这样说的意思是 CommonJS 支持未包装的模块,它更接近 ES.next/Harmony 规范,避免了 AMD 强制要求我们使用的 define()
包装器。然而 CommonJs 只支持对象作为模块。
UMD:用于 AMD 和 兼容 CommonJS 模块的插件
对于希望能创建既能在浏览器环境运行又能在服务端环境运行的模块的开发者而言,现有的方法都被认为是有不足的。为了解决这个问题,James Burke、我和一些其他开发者创建了 UMD (通用模块定义)。
UMD 还是一个实验性的模块格式,它可以将定义的模块能在客户端和服务端环境运行,与编写本文时流行的大多数流行的脚本加载技术一起使用。尽管又一个新的模块格式(还不算是)可能让人畏惧,但为了全面起见,我们将会简要的介绍它。
我们最初定义 UMD 是因为看到 AMD 规范中支持简化的 CommonJS 包装。对于希望像 CommonJS 模块一样编写模块的开发者,可以使用下面的兼容 CommonJS 格式:
基本的 AMD 混合格式
define(function(require, exports, module) {
var shuffler = require('lib/shuffle');
exports.randomize = function(input) {
return shuffler.shuffle(input);
};
});
但是,请务必注意的是,如果一个模块不包含依赖数组并且定义函数至少包含一个参数,那么它实际上只能被认为是 CommonJS 模块。这在某些设备上(如:PS3)也无法正常运行。关于上述包装器更多的信息可以看这里。
更近一步,我们希望提供一系列不同的模式,它们不仅仅能同 AMD 和 CommonJS 一起使用,并且还能解决开发者在其他环境中希望开发这样的模块所面临的通用的问题。
下面我们要看到的一个变体是能让我们使用 CommonJS、AMD或者浏览器全局对象来创建一个模块。
使用 CommonJS、AMD 或者全局对象创建一个模块
定义一个模块 commonJsStrict
,它依赖另一个模块 b
。模块的名字来自它对应的文件名,最佳的实践是让文件名和导出的全局对象拥有相同的名字。
如果模块 b
在浏览器中使用相同的策略,它会创建一个全局变量 .b
来使用。如果我们不希望支持浏览器的全局作用范围,我们可以将外层函数的第一个参数从 root
换成 this
。
(function(root, factory) {
if (typeof exports === 'object') {
// CommonJS
factory(exports, require('b'));
} else if (typeof define === 'function' && define.amd) {
// AMD。注册成一个匿名模块
define(['exports', 'b'], factory);
} else {
// 浏览器全局对象
factory((root.commonJsStrict = {}), root.b);
}
})(this, function(exports, b) {
// 以某种方式使用 b
// 为 exports 对象的属性赋值,
// 来定义导出模块的属性
exports.action = function() {};
});
UMD 仓库中包含各种变体,涵盖了适合在浏览器中工作的、最适合提供导出的、最适合 CommonJS 运行时和甚至最适合 jQuery 插件的模块,我们即将讨论最后一种。
适合所有环境的 jQuery 插件
UMD 提供了使用 jQuery 插件的两种模式 - 一种是定义的插件可以和 AMD 和浏览器全局对象工作;另一种是也可以在 CommonJS 环境中工作。但是 jQuery 通常不会用在 CommonJS 环境,除非是在一个非常适合它的环境中工作,所以请记住这一点。
现在我们将要定义一个插件,它是由一个核心和对应的拓展组成。核心插件被加载到 $.core
的命名空间内,然后通过命名空间模式使用插件拓展轻松地被拓展。插件通过 script 标签加载,自动在 core
命名空间下添加 plugin
(如 $.core.plugin.methodName()
)。
这个模式能很好地使用是因为插件拓展可以访问基础的属性和方法,或者稍微调整一下,覆盖默认行为,这样它就能拓展它做更多的事情。为了让它能够完全的工作,还需要一个加载器。
想了解其中更多的细节,请查看下面代码示例中的行内注释。
usage.html
<script type="text/javascript" src="jquery-1.7.2.min.js"></script>
<script type="text/javascript" src="pluginCore.js"></script>
<script type="text/javascript" src="pluginExtension.js"></script>
<script type="text/javascript">
$(function() {
// 本例中我们的插件 “core” 暴露在 core 命名空间下
// 它是我们先缓存的
var core = $.core;
// 然后使用一些 core 内置的功能来将页面中所有的 div 高亮为黄色
core.highlightAll();
// 访问加载到我们 core 模块下 “plugin” 命名空间的插件(拓展插件):
// 设置页面中第一个 div 背景色为绿色
core.plugin.setGreen('div:first');
// 这里我们利用 core 的 “highlight” 方法,
// 这个方法源自它后面加载的插件
// 将最后一个 div 设置为我们在 core 模块/插件中定义的 “errorColor” 属性值。
// 如果我们深入的查看代码,我们会发现在 core 和
// 其他插件之间使用属性和方法是多么轻松
core.plugin.setRed('div:last');
});
</script>
pluginCore.js
// 模块/插件 core
// 注意:我们在模块周围看到的包装器代码使我们能够通过将定义的参数映射
// 到期望的特定格式来支持多种模块格式和规范。
// 我们模块的实际功能是定义在最下方,它演示了命名模块和 exports。
//
// 注意如果需要引用依赖的话,可以像之前的 AMD 模块实例演示的那样使用
(function(name, definition) {
var theModule = definition(),
// 它可以被认为是”安全的“:
hasDefine = typeof define === 'function' && define.amd,
// hasDefine = typeof define === "function",
hasExports = typeof module !== 'undefined' && module.exports;
if (hasDefine) {
// AMD Module
define(theModule);
} else if (hasExports) {
// Node.js Module
module.exports = theModule;
} else {
// 分配给公共命名空间或者简单的全局对象(window)
(this.jQuery || this.ender || this.$ || this)[name] = theModule;
}
})('core', function() {
var module = this;
module.plugins = [];
module.highlightColor = 'yellow';
module.errorColor = 'red';
// 在这里定义 core 模块,并返回公开的 API
// 这是通过 core 的 highlightAll() 实现的高亮方法
// 并且所有的插件高亮元素成不同的颜色
module.highlight = function(el, strColor) {
if (this.jQuery) {
jQuery(el).css('background', strColor);
}
};
return {
highlightAll: function() {
module.highlight('div', module.highlightColor);
}
};
});
pluginExtension.js
// core 模块的拓展
(function(name, definition) {
var theModule = definition(),
hasDefine = typeof define === 'function',
hasExports = typeof module !== 'undefined' && module.exports;
if (hasDefine) {
// AMD Module
define(theModule);
} else if (hasExports) {
// Node.js Module
module.exports = theModule;
} else {
// 分配到公共命名空间或者仅全局对象(window)
// account for for flat-file/global module extensions
var obj = null,
namespaces,
scope;
obj = null;
namespaces = name.split('.');
scope = this.jQuery || this.ender || this.$ || this;
for (var i = 0; i < namespaces.length; i++) {
var packageName = namespaces[i];
if (obj && i == namespaces.length - 1) {
obj[packageName] = theModule;
} else if (typeof scope[packageName] === 'undefined') {
scope[packageName] = {};
}
obj = scope[packageName];
}
}
})('core.plugin', function() {
// 在这里定义我们的模块,返回公开的 API。
// 这个代码可以轻松地被 core 接受,以让其覆盖或者拓展 core 的功能
// 来让 hignlight 方法做更多我们期望它做的事。
return {
setGreen: function(el) {
highlight(el, 'green');
},
setRed: function(el) {
highlight(el, errorColor);
}
};
});
UMD 的目的不是取代 AMD 或者 CommonJS,而是为希望他们的代码能在当今更多的环境中运行的开发者提供一些帮助。想了解更多信息或者提供建设性建议给这个实验性格式,请查看 https://github.com/umdjs/umd 。
相关阅读
- “Using AMD Loaders to Write and Manage Modular JavaScript,” John Hann
- “Demystifying CommonJS Modules,” Alex Young
- “AMD Module Patterns: Singleton,” John Hann
- “Run-Anywhere JavaScript Modules Boilerplate Code,” Kris Zyp
- “Standards And Proposals for JavaScript Modules And jQuery,” James Burke