原文:https://addyosmani.com/writing-modular-js/
模块化 应用程序解耦的重要性
当我们说一个程序是模块化时,通常是指,这个程序是有一组高内聚,低耦合,功能划分清晰的模块组成。就像你知道的那样,低耦合的应用,可以随时随地的剔除依赖,这就保证了自身的易维护性。如果应用是高度的模块化,那么可以轻易的评估部分功能的修改对其余部分的影响程度。
不幸的是,不像传统的编程语言,在ECMA-262的迭代版本中,并未向编程人员提供引入这种模块化结构的实现方式,以至无法简洁明了的实现应用的结构化。直到近些年,对JavaScript代码结构化需求的日益增加,才提出了“模块化”这个明确的概念。
同时,当下的编程人员只能退而求其次,使用module和对象字面量模式(object literal pattern)来变相的实现模块化。大多数方法的实现,都是通过定义全局对象(global object),来规定命名空间(namespace),并将其捆绑在DOM元素上。但在整体架构上仍然有可能发生命名冲突(naming collisions)的问题。而且,如果不通过一些额外的工作或是使用第三方插件,仍然无法间接地管理依赖注入。
好消息是,在即将到来的ES Harmony中,已经在原生方法中提供了对应的解决方案。从现在开始,编写模块化的JavaScript代码将变得前所未有的轻松。
在这篇文章中,我们将了解编写模块化JavaScript代码的三种规范:AMD,CommonJS以及JavaScript的下个版本,Harmony。
序 概览脚本加载器(Script Loader)
如果抛弃脚本加载器而直接讨论AMD和CommonJS模块化,那无异于盲人摸象。脚本加载器的作用是将模块化的JavaScript代码应用于当下的应用之中。因此,使用合适的加载器是必不可少的。为了更好的了解本文介绍的内容,理解模块化规范的原理,我建议读者应首先对一些常见的加载器的工作原理有最基本的认识。
现在已经有很多非常优秀的加载器实现AMD和CommonJS的规范。我个人推荐RequireJS和curl.js。关于这些加载器的完整教程不在本文介绍的范围之内。但是我推荐 John Hann关于curl.js的介绍和James Burke关于RequireJS的API文档。
从产品的角度来说,值得推荐的是,在使用这些模块化工具的同时,利用其自有的优化工具(如RequireJS optimizer)去压缩拼接JavaScript文件。值得注意的是,通过使用AMD插件Almond,RequireJS不需要掺杂在需要部署的网站中,脚本加载器可以简洁的排除在开发之外。
即使如此,James Burke有可能会说,在页面加载完成后动态加载脚本文件仍然有用武之地,所以RequireJS也支持这种方式。有了这些大致的了解,让我们开始对模块化做进一步了解。
AMD 在浏览器环境编写模块化代码的规范
AMD (Asynchronous Module Definition) 规范的根本目的是提供一种模块化JavaScript编程的解决方案,以供当前开发者使用。该规范的灵感是来源于Dojo在现实应用中使用XHR+eval的经验。它的倡导者们希望能够在未来的解决方案中解决和规避那些已经发现的缺陷。
AMD模块化规范自身是对定义模块的建议,实现模块和依赖能够异步加载。它有很多明显的优点,通过移除代码和模块定义的耦合性,以实现异步和高自由度。很多开发人员欣然接受这种模块化方式,并且在ES Harmony的规范制定中被认为是极具参考价值的基石。
最初AMD是作为CommonJS模块化规范备选方案中的一个,但是由于关于它的实现无法保证一致性,因此后续的开发有amdjs小组负责。
现在它已经被Dojo(1.7),MooTools(2.0),FireBug(1.8)以及jQuery(1.7)接受并实现。也因此,CommonJS AMD 规范在某些情况被认为是不合理的术语,更好的应称之为AMD或者异步模块。
从模块开始
首先需要了解两个关键的概念:define
方法用来便捷地定义模块,require
方法用来管理依赖加载。define
使用如下格式来定义命名或匿名的模块。
define(
module_id /*可选*/,
[dependencies] /*可选*/,
definition function /*实例化模块的函数或对象*/
);
从行内注释中可以看到,module_id
是可选参数,它的典型应用是在使用非AMD压缩工具时,保证模块定义的准确性(当然可能还有其他的极限情况需要必须填写)。当省略该参数时,模块被称之为匿名模块。
模块标识的理念是DRY(译者注:原文中只有简写,个人认为是Don’t repeat yourself),在使用匿名模块时,应尽量简洁以避免重复的文件名或编码。因为代码的可移植性,可以将代码轻松的移动到其他位置(或者在文件系统中移动代码文件),而不需更改自身代码或者ID。在使用简单的包或者不使用包时,module_id
等同于文件的路径。通过使用运行在CommonJS环境下的AMD优化器,如r.js,开发者可以在多个不同的环境运行相同的代码。
回到定义语法, dependencies
参数是一个由当前模块所依赖的模块组成的字符串数组,而第三个参数(definition function
),是用于实例化模块的执行函数。一个基本的模块定义如下所示:
理解AMD:define()
//myModule只是为了印证module_id的使用
define('myModule',
['foo', 'bar'],
// 模块定义函数,依赖(foo和bar)被映射到函数的参数上
function ( foo, bar ) {
// 返回定义了模块export的值(例如:我们需要暴露给外部使用的功能)
//在这里创建你的模块
var myModule = {
doStuff:function(){
console.log('Yay! Stuff');
}
}
return myModule;
});
// 另一个例子
define('myModule',
['math', 'graph'],
function ( math, graph ) {
// 注意这是和AMD略微不同的地方,因为它相当的自由,
// 所以可能存在多种定义模块的方法,但是应该符合核心的语法
return {
plot: function(x, y){
return graph.drawPie(math.randomGrid(x,y));
}
}
};
});
另一方面,require是用来从顶层的JavaScript文件中加载代码或者在模块内容希望动态的加载依赖。下面是一个使用的例子:
理解AMD:require()
// 考虑'foo' and 'bar'是两个外部模块
// 在这个例子中,两个外部模块作为回调函数的参数,它们通过'exports'暴露的属性同样可以被访问
require(['foo', 'bar'], function ( foo, bar ) {
//在这里编写自定义逻辑
foo.doSomething();
});
动态加载依赖
define(function ( require ) {
var isReady = false, foobar;
// 注意这里的require在模块定义的内部
require(['foo', 'bar'], function (foo, bar) {
isReady = true;
foobar = foo() + bar();
});
// 我们仍然可以返回一个模块
return {
isReady: isReady,
foobar: foobar
};
});
理解AMD:plugins
下面的例子展示了如何定义AMD-compatible插件:
// With AMD, it's possible to load in assets of almost any kind
// including text-files and HTML. This enables us to have template
// dependencies which can be used to skin components either on
// page-load or dynamically.
define(['./templates', 'text!./template.md','css!./template.css'],
function( templates, template ){
console.log(templates);
// do some fun template stuff here.
}
});
注意:虽然在上面的例子中,为了加载CSS依赖而包含了
css!
,但是这种方式有一些注意事项,首先当CSS完全加载完成时,根据构建方式的不同,可能CSS依赖无法完全加载。而且可能会导致CSS被作为依赖被写入到压缩优化后的代码文件中,所以在将CSS作为被加载的依赖时,需要特别注意。
通过require.js加载AMD模块
require(['app/myModule'],
function( myModule ){
// start the main module which in-turn
// loads other modules
var module = new myModule();
module.doStuff();
});
通过curl.js加载AMD模块
curl(['app/myModule.js'],
function( myModule ){
// start the main module which in-turn
// loads other modules
var module = new myModule();
module.doStuff();
});
存在异步依赖的模块
// This could be compatible with jQuery's Deferred implementation,
// futures.js (slightly different syntax) or any one of a number
// of other implementations
define(['lib/Deferred'], function( Deferred ){
var defer = new Deferred();
require(['lib/templates/?index.html','lib/data/?stats'],
function( template, data ){
defer.resolve({ template: template, data:data });
}
);
return defer.promise();
});
为什么使用AMD编写JavaScript模块化代码是个很好的选择?
- 提供了灵活定义模块的清晰规则
- 相较于使用全局命名空间和
<script>
标签等解决方案显得更为简洁。具有更简洁的方式去声明独立模块和可能需要的依赖。 - 使用封装的模块化定义,有助于避免全局命名空间的污染
- 相较于其他解决方案(例如随后将要介绍的CommonJS)工作的更好。不存在跨域(cross-domain),本地化以及调试(debugging)等问题,同时无需依赖服务端工具的使用。大多数的AMD加载器支持在浏览器环境中不通过构建(build)直接加载模块
- 为多模块包含在同一文件的情况提供输送方式(transport)。像CommonJS等其他解决方式还未实现输送的格式
- 只在需要时实现对脚本的懒加载(lazy load)
相关阅读
1.The RequireJS Guide To AMD
2.What’s the fastest way to load AMD modules?
3.AMD vs. CJS, what’s the better format?
4.AMD Is Better For The Web Than CommonJS Modules
5.The Future Is Modules Not Frameworks
6.AMD No Longer A CommonJS Specification
7.On Inventing JavaScript Module Formats And Script Loaders
8.The AMD Mailing List
AMD模块化和Dojo
通过Dojo定义AMD兼容的模块是相当简单的。像之前所有的例子一样,在定义任意模块时,将依赖数组作为第一个参数,并提供在依赖加载完成后调用一个回调函数(factory)。例如:
define(["dijit/Tooltip"], function( Tooltip ){
//Our dijit tooltip is now available for local use
new Tooltip(...);
});
注意匿名模块现在可以被Dojo异步加载器,RequireJS或者之前习惯使用的标准dojo.require()方法所使用。
对于那些好奇模块引用的开发人员,这里有几个既有趣又有用的知识点可以加以了解。AMD仅仅对AMD适用的加载器有效-它提倡在依赖列表中使用相匹配的参数声明,作为引用模块的方式。而Dojo 1.6的构建系统中并不支持。例如:
define(["dojo/cookie", "dijit/Tooltip"], function( cookie, Tooltip ){
var cookieValue = cookie("cookieName");
new Tree(...);
});
这比嵌套的命名空间更具优越性,因为引用模块无需每次输入完整的命名空间。我们只需要保证'dojo/cookie'
的路径在依赖中,而一旦它被关联到参数时,则可以通过该变量进行引用。这就避免了在程序中重复输入'dojo.'
的麻烦。
注意:虽然Dojo 1.6 官方并未支持基于用户的AMD模块(亦或是异步加载),但是可以通过使用多种不同的脚本加载器使其正常工作。到现在为止,Dojo的核心和Dijit模块已经向AMD语法靠拢,并且即将在1.7和2.0版本之间综合提升对AMD的支持。
最后需要了解的是,如果希望继续使用Dojo构建系统,或者将过时的模块迁移到这种新的AMD形式,下面这种冗长的写法能够更简单的实现兼容。注意dojo
和dijit
同样被作为依赖引用:
define(["dojo", "dijit", "dojo/cookie", "dijit/Tooltip"], function(dojo, dijit){
var cookieValue = dojo.cookie("cookieName");
new dijit.Tooltip(...);
});
AMD模块化设计模式(Dojo)
如果你看过我之前发表的关于设计模式的优势的文章,你就会了解到,在处理普遍的开发问题上,它们有效得改善我们处理结构方案的方式。最近John Hann精彩地陈述了AMD模块化设计模式,包含了单例(Singleton),装饰者(Decorator),中继者(Mediator)以及其他的模式。如果你有机会,我强烈推荐去浏览一下他的幻灯片。
部分模式如下所示:
装饰者模式
// mylib/UpdatableObservable: a decorator for dojo/store/Observable
define(['dojo', 'dojo/store/Observable'], function ( dojo, Observable ) {
return function UpdatableObservable ( store ) {
var observable = dojo.isFunction(store.notify) ? store :
new Observable(store);
observable.updated = function( object ) {
dojo.when(object, function ( itemOrArray) {
dojo.forEach( [].concat(itemOrArray), this.notify, this );
};
};
return observable; // makes `new` optional
};
});
// decorator consumer
// a consumer for mylib/UpdatableObservable
define(['mylib/UpdatableObservable'], function ( makeUpdatable ) {
var observable, updatable, someItem;
// ... here be code to get or create `observable`
// ... make the observable store updatable
updatable = makeUpdatable(observable); // `new` is optional!
// ... later, when a cometd message arrives with new data item
updatable.updated(updatedItem);
});
适配器模式
// 'mylib/Array' adapts `each` function to mimic jQuery's:
define(['dojo/_base/lang', 'dojo/_base/array'], function (lang, array) {
return lang.delegate(array, {
each: function (arr, lambda) {
array.forEach(arr, function (item, i) {
lambda.call(item, i, item); // like jQuery's each
})
}
});
});
// adapter consumer
// 'myapp/my-module':
define(['mylib/Array'], function ( array ) {
array.each(['uno', 'dos', 'tres'], function (i, esp) {
// here, `this` == item
});
});
AMD模块化和jQuery
基础
与Dojo不同,jQuery只提供一个文件,然而考虑到基于插件库的性质,我们可以通过下面的例子,看出定义AMD模块是多么的直接明了。
define(['js/jquery.js','js/jquery.color.js','js/underscore.js'],
function($, colorPlugin, _){
// Here we've passed in jQuery, the color plugin and Underscore
// None of these will be accessible in the global scope, but we
// can easily reference them below.
// Pseudo-randomize an array of colors, selecting the first
// item in the shuffled array
var shuffleColor = _.first(_.shuffle(['#666','#333','#111']));
// Animate the background-color of any elements with the class
// 'item' on the page using the shuffled color
$('.item').animate({'backgroundColor': shuffleColor });
return {};
// What we return can be used by other modules
});
在这个例子中缺少了注册的概念。
将jQuery注册为异步兼容的模块
在jQuery 1.7版本中一个关键的特性是支持将jQuery注册为异步模块。有许多兼用的脚本加载器(包括RequireJS和curl)可以实现对模块的异步加载,这也就意味着不需要使用hack就可以保证其正确运行。
由于jQuery的普遍使用,版本多种多样,但多数情况并不希望不同的版本在同一时间进行加载,因此AMD加载器需要考虑在同一页面加载多个不同版本的库的情况。加载器具备不同的模式,可以特别处理这种问题,或者是通知使用者其第三方脚本和他们的类库存一些已知的问题。
1.7版本的显著提升,是避免了在页面中加载某个第三方代码时,意外加载了某个不符合开发者所预期的jQuery版本。因为你并不希望其他的实例重写你自己的,所以这个提升是很有意义的。
其工作原理是,脚本加载器通过配置属性define.amd.jQuery=true
,指明其支持多个jQuery版本。为了更好的了解实现细节,我们将jQuery注册为一个命名的模块。这存在一个风险,就是有可能使用AMD define()
方法将它和别的文件进行拼接
,而非使用能够理解异步AMD模块定义的正确拼接脚本。
命名的AMD模块为保证多数使用情况鲁棒性和安全性,添加了安全的限制层。
// Account for the existence of more than one global
// instances of jQuery in the document, cater for testing
// .noConflict()
var jQuery = this.jQuery || "jQuery",
$ = this.$ || "$",
originaljQuery = jQuery,
original$ = $,
amdDefined;
define(['jquery'] , function ($) {
$('.items').css('background','green');
return function () {};
});
// The very easy to implement flag stating support which
// would be used by the AMD loader
define.amd = {
jQuery: true
};
更智能的jQuery插件
不久前,我讨论过关于使用UMD(Universal Module Definition )模式编写jQuery插件。就像其他流行的脚本加载器一样,UMD定义的模块同时兼容服务端和客户端。同时,这仍然是新的领域,有许多概念正在被最终确定。请随意查看后续章节AMD && CommonJS中的代码样例,如果有好多建议请及时告诉我。
支持AMD的脚本加载器和框架
浏览器环境
- RequireJS http://requirejs.org
- curl.js http://github.com/unscriptable/curl
- bdLoad http://bdframework.com/bdLoad
- Yabble http://github.com/jbrantly/yabble
- PINF http://github.com/pinf/loader-js
- …
服务端
- RequireJS http://requirejs.org
- PINF http://github.com/pinf/loader-js
AMD 总结
以上是对AMD模块化规范用途的基本示例,但愿能够提供有助于理解AMD工作原理的基础。
也许你有兴趣知道当前许多大型应用和公司已经将AMD模块化作为他们应用结构的一部分。其中包括IBM,BBC iPlayer,这从企业级层面有力的证明了此规范的重要性。
对于为什么更多的开发者愿意将AMD模块化用于自己的应用,若有兴趣的话,可以参看 James Burke的文章《On inventing JS module formats and script loaders》。
未完待续。。。