上一篇中说到了JavaScript中的modules,书接前文码一下JavaScript中的CommonJS和AMD,接触这些的时间不算短了(从jQuery和UE.js等库),一直没去写点总结性的东西;
上文示例方法首有一个共同点:使用单个全局变量将其代码包装在一个函数中,为其使用的闭包作用域创建一个私有命名空间,虽然每种方法都能有效运行,但却都有缺点;比如,作为开发人员,需要知道正确的依赖关系顺序来加载所需文件,假设项目中使用Backbone,需要在文件中引用Backbone的脚本;而由于Backbone对Underscore.js有依赖,所以Backbone文件的脚本标签不能放在Underscore.js的引用之前,所以众所周知依赖关系的处理是一个麻烦的事情;另一个缺点是脚本之间可能导致命名空间冲突。例如两个模块具有相同的名称,该怎么办?或者如果有两个版本的模块,并且需要两个版本?
所以我们面临的问题是如何设计出一种不经过全局作用域的模块接口方法;当然答案是肯定的,不然也没这篇博文了,就是标题所述的CommonJS和AMD设计模式,目前两种最流行的两种设计模式;
CommonJS
CommonJS是由一个志愿工作组(The project was started by Mozilla engineer Kevin Dangoor in January 2009 and initially named ServerJS)负责设计和实现用于声明模块的JavaScript API。
CommonJS模块本质上是一个可重用的JavaScript脚本,可以导出特定的对象,使其可用于其他模块在其程序中的需求。如果你熟悉Node.js,那么你一定熟悉CommonJS,因为node.js中就使用了CommonJS规范;
使用CommonJS,每个JavaScript文件将模块存储在其独特的模块上下文中(就像将其封装在一个闭包中),在这个作用域中,使用module.exports对象来公开模块,并且'require'导入它们;
当我们定义一个CommonJS模块时,它可能看起来像这样:
function myModule() { this.hello = function() { return 'hello!'; } this.goodbye = function() { return 'goodbye!'; } } module.exports = myModule;
我们使用特殊对象模块,并将我们的函数的引用放在module.exports中,这使得CommonJS模块系统知道我们要公开的内容,以便其他文件可以使用它;那么当有人想要使用myModule时,他们可以在它们的文件中引用,like this:
var myModule = require('myModule'); var myModuleInstance = new myModule(); myModuleInstance.hello(); // 'hello!' myModuleInstance.goodbye(); // 'goodbye!'
对于我们之前讨论的模块模式,这种方法有两个明显的好处:
-
避免全局变量/命名空间污染;
-
依赖关系明确
另一件需要注意的事情是,CommonJS采用服务器优先方式并同步加载模块。如果我们需要另外三个模块,它们将逐个加载它们;所以这种规范下的脚本在服务器上工作得很好,但在为浏览器编写JavaScript时还是有问题的;浏览器中加载网络资源并运行比服务器本身运行过程要显的长的多,只要加载模块的脚本正在运行,它将阻止浏览器运行其他任何东西,直到它完成加载,因为JavaScript本身以单线程的方式运行;
还有就是,这里说的设计模式,不是单针对浏览器环境的,而是是服务器环境,其实JavaScript诞生之初是服务器端在应用,后来网页中发展迅速就给标准化了,具体历史缘由有兴趣的同学可以搜下,大概是有些仓促吧,所以JavaScript本身有很多限制,比如上面的代码,直接放在浏览器中是不兼容的,因为缺少四个node.js变量:module、exports、require、global,当浏览器中提供了完整的变量环境,浏览器中就可以运行CommonJS了;
AMD
CommonJS很不错也很流行,但如上所述如何做到异步加载modules,可以参照异步模块定义或者使用AMD规范,看一下AMD规范是如何做的:
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) { console.log(myModule.hello()); });
这里define将其第一个参数以数组的形式作为模块依赖(What’s happening here is that the define function takes as its first argument an array of each of the module’s dependencies),这些依赖关系在后台加载(以非阻塞的方式),并且一旦加载define就会执行所给的回调方法callback;接下来,回调函数作为参数使用加载的依赖关系(在我们的例子中,myModule和myOtherModule),允许函数使用这些依赖;最后,依赖关系本身也必须使用define关键字定义;
比如,myModule模块是这样写的:
define([], function() { return { hello: function() { console.log('hello'); }, goodbye: function() { console.log('goodbye'); } }; });
所以AMD是采用浏览器优先和异步行为来完成工作,除此之外,AMD的另一优点是AMD中modules可以是Object、function、constructos、strings、JSON或者其他类型,而CommonJS只支持Object作为模块;虽然如此,AMD相对于CommonJS也有不兼容io、filesystem和其他针对服务器的问题,与简单的require语句相比,函数包装语法有点冗长;
国内还流行CMD规范,跟AMD很接近,这里就不展开了,是淘宝大神玉伯提出的理念,有兴趣搜索之;
UMD
如果项目中要兼容CommonJS和AMD,可以使用UMD规范(Universal Module Definition),UMD模块的定义使之能够在客户端和服务器上工作,看个例子:
(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD define(['myModule', 'myOtherModule'], factory); } else if (typeof exports === 'object') { // CommonJS module.exports = factory(require('myModule'), require('myOtherModule')); } else { // Browser globals (Note: root is window) root.returnExports = factory(root.myModule, root.myOtherModule); } }(this, function (myModule, myOtherModule) { // Methods function notHelloOrGoodbye(){}; // A private method function hello(){}; // A public method because it's returned (see below) function goodbye(){}; // A public method because it's returned (see below) // Exposed public methods return { hello: hello, goodbye: goodbye } }));
看一下,你会注意到这段代码前面是处理兼容的,后面的是实体方法,实际上现在非常多的库/框架使用这种写法解决兼容问题;
思考问题:RequireJS和Sea.js的异同之处,你认为什么样的方式才是最优方案?