AMD规范:简单而优雅的动态载入JavaScript代码

本文翻译自http://www.sitepen.com/blog/2010/11/04/requirejsamd-module-forms/,并加入部分自己的解释。

CommonJS 提出了一种用于同步或异步动态加载JavaScript代码的API规范,非常简单却很优雅,称之为AMD(Modules/AsynchronousDefinition)。RequireJS和NodeJS的Nodules已经实现了这个API,而Dojo也将马上完全支持(Dojo1.6)。规范本身非常简单,甚至只包含了一个API:
define([module-name?], [array-of-dependencies?], [module-factory-or-object]);

通过参数的排列组合,这个简单的API可以从容应对各种各样的应用场景,如下所述。

 

匿名模块

在这种场景下,无需输入模块名,即省略第一个参数,仅包含后两个参数:依赖模块的列表以及回调函数,例如一个简单的匿名模块可以用如下代码定义:

define(["math"], function(math){
  return {
    addTen: function(x){
      return math.add(x, 10);
    }
  };
});
在这里,第一个参数表示依赖的模块列表,即math模块。一旦所有依赖的模块被载入完成,那么第三个参数定义的回调函数将被执行,依赖模块的引用作为参数传递给回调函数。

如例子中所示,如果模块名被省略不写,那么这是一个匿名模块。通过这种强大的方式,模块的源代码与它的标识可以做到不相关。从而可以在不改变模块代码的情况下移动源码文件的位置。这个技术遵循了基本的DRY(Don't Repeat Yourself)原则,避免了模块标识的多次存储(文件名/路径信息不会在代码中重复)。这不仅使得模块的开发变得更加容易,而且为模块的重用提供了极大的灵活性。

下面我们看如何从一个Web页面载入这个模块。我们假设上面的模块存储在文件adder.js中。使用RequireJS,我们可以用下面方式来载入这个模块:
<script src="require.js"></script>
<script>
require(["adder"], function(adder){
  // ready to use adder
});
</script> 
一旦代码被执行,RequireJS将会自动去调用adder模块所有的依赖模块。载入完毕之后,我们就可以通过回调函数的adder参数来使用前面定义的匿名模块。例子中可以看到,adder.js里存储的是定义的匿名模块,实际上我们可以用任何文件/路径来包含这个模块,为模块的重用提供了方便(Java中的文件名/路径和类名/包的必须一致性实际上就为类级别的重用造成了不便)。require函数用于载入任何一个模块,后面将多次使用。

对于匿名模块的使用有一些注意事项。比如每个文件中只能包含一个匿名模块,而且匿名模块只能被载入器载入,即只能用require来载入。也可以这么理解,实际上匿名模块并不是没有名字,而是在使用时进行命名的模块,例子中就是adder。

数据封装:新的JSON-P

对于一些仅仅提供数据或者独立方法(不依赖于其它模块的方法)的模块,可以简单的用如下方式来定义:
define({
  name:"some data"
});
这个和JSON-P非常像,但是却有一个显著的优点:它使得JSON-P数据可以现在静态文件中,而并不需要动态的回调过程。这也使得内容是可cache的,而且是CDN友好的。

 

封装CommonJS模块

CommonJS也是一套RIA框架,其中的模块可以通过AMD来进行封装,从而可以用define的方式很容易的进行异步装载,在这里我们可以省略前2个参数,仅包含回调函数,但回调函数的第一个参数是require方法,第二个参数是exports对象,它定义了模块本身,回调函数里的require的使用将被自动进行动态加载。例如:
define(function(require, exports){
//math是标准CommonJS模块:
  var math = require("math");
  exports.addTen = function(x){
    return math.add(x, 10);
  };
}); 
需要注意这种形式要求模块载入器扫描require函数。require调用必须写成require(“…”)的形式才能被正确识别从而正常工作。这在一些浏览器不能正常工作(例如默写版本的Opera移动版,以及PS3)。当然,如果在部署前对代码进行了build,这将完全不成问题。你也可以封装CommonJS模块,并手动的指定依赖,这种方式使得我们也可以引用CommonJS变量,从而我们可以包含标准的require和exports变量:
define(["require", "exports", "math"], function(require, exports){
// standard CommonJS module:
  var math = require("math");
  exports.addTen = function(x){
    return math.add(x, 10);
  };
}); 
 

完整的模块定义

一个完整的模块定义包含了模块名,依赖,以及回调函数。这种形式的优点是模块可以包含在另外的文件中,或者可以用script标记载入的地址中。这是build工具自动生成的规范模式,使得多个依赖可以被打包在同一个文件中,这种格式的例子如下:
define("adder", ["math"], function(math){
  return {
    addTen: function(x){
      return math.add(x, 10);
    }
  };
});

最后,我们来看有模块id,但没有模块依赖的情况。这种情况用于你想指定模块id,但是这个模块不依赖于其它模块。这时的参数默认是“require”,“exports”和“module”。从而我们可以这样创建adder模块。
define("adder", function(require, exports){
  exports.addTen = function(x){
      return x + 10;
  };
});
通过这种方式定义的模块可以被RequireJS载入,也可以作为其它模块的依赖被载入,或者直接用require()的形式载入。

综上所述,这种API看似简单,却提供了一种极其灵活的方式来定义模块,适用于各种应用场景,从可被自由移动的匿名模块,到构建后的可被<script>标记载入的模块。当前RequireJS和Dojo实现了这套规范,而JavaScript的Web Server框架NodeJS的Nodules也实现了这个规范。

posted on 2010-12-15 00:07  springside5  阅读(195)  评论(0编辑  收藏  举报