概述
CMD(Common Module Definition) 是国内大牛玉伯在开发 SeaJS 的时候提出来的,属于 CommonJS 的一种规范,根据浏览器的异步环境做了自己的实现。它和 AMD 很相似,尽量保持简单,并与 CommonJS 和 Node.js 的 Modules 规范保持了很大的兼容性。
define 方法:定义模块
在CMD中,一个模块就是一个文件,格式为:define(factory)
;define
是一个全局函数,用来定义模块。
factory 函数
define
接受 factory
参数,factory
可以是一个函数,也可以是一个对象或字符串。
factory
为对象、字符串时,表示模块的接口就是该对象、字符串。比如可以如下定义一个 JSON 数据模块:
define({ "foo": "bar" });
也可以通过字符串定义模板模块:
define('I am a template. My name is {{name}}.');
factory
为函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。factory
方法在执行时,默认会传入三个参数:require
、exports
和 module
:
define(function(require, exports, module) {// 模块代码
});
define
也可以接受两个以上的参数,字符串 id
为模块标识,数组 deps
为模块依赖,格式为:define( id?, deps?, factory )
;具体使用如下:
define('hello', ['jquery'], function(require, exports, module) {// 模块代码
});
但是,带 id
和 deps
参数的 define
用法不属于 CMD 规范。CMD 推崇一个文件一个模块,所以经常就用文件名作为模块 id
。CMD 推崇依赖就近,所以一般不在 define
的参数中写依赖,而在 factory
中写。
require 方法:加载模块
require
是 factory
函数的第一个参数。require
是一个方法,接受模块标识作为唯一参数,用来获取其他模块提供的接口。
define(function(require, exports) {// 获取模块 a 的接口var a = require('./a');// 调用模块 a 的方法a.doSomething();
});
require.async(id, callback)
require.async
方法用来在模块内部异步加载模块,并在加载完成后执行指定回调。callback
参数可选。
define(function(require, exports, module) { // 异步加载模块,在加载完成时,执行回调require.async(['./c', './d'], function(c, d) {c.doSomething();d.doSomething();});
});
require
是同步往下执行,require.async
则是异步回调执行。require.async
一般用来加载可延迟异步加载的模块。
define(function(require, exports, module) { // 异步加载模块,在加载完成时,执行回调require.async(['./c', './d'], function(c, d) {c.doSomething();d.doSomething();});});
require.resolve(id)
使用模块系统内部的路径解析机制来解析并返回模块路径。该函数不会加载模块,只返回解析后的绝对路径。
define(function(require, exports) {console.log(require.resolve('./b'));// ==> http://example.com/path/to/b.js});
这可以用来获取模块路径,一般用在插件环境或需动态拼接模块路径的场景下。
exports 对象:暴露接口对象
exports
是一个对象,用来向外提供模块接口。
define(function(require, exports) {// 对外提供 foo 属性exports.foo = 'bar';// 对外提供 doSomething 方法exports.doSomething = function() {};});
除了给 exports
对象增加成员,还可以使用 return
直接向外提供接口。
define(function(require) {// 通过 return 直接提供接口return {foo: 'bar',doSomething: function() {}};
});
需要特别注的是,不能直接给 exports
赋值,例如下面是错误的写法:
define(function(require, exports) {// 错误用法!!!exports = {foo: 'bar',doSomething: function() {}};
});
exports
仅仅是 module.exports
的一个引用。而模块导出的时候,真正导出的是 module.exports
,而不是 exports
。在 factory
内部给 exports
重新赋值时, exports
就不再指向 module.exports
了。因此给 exports
赋值是无效的,正确的写法是用 return
或者给 module.exports
赋值:
define(function(require, exports, module) {// 正确写法module.exports = {foo: 'bar',doSomething: function() {}};
});
module 对象:本模块对象
module
是一个对象,上面存储了与当前模块相关联的一些属性和方法。
module.id
模块的唯一标识。
define('id', [], function(require, exports, module) {// 模块代码
});
上面代码中,define
的第一个参数就是模块标识。
module.uri
根据模块系统的路径解析规则得到的模块绝对路径。
define(function(require, exports, module) {console.log(module.uri); // ==> http://example.com/path/to/this/file.js
});
一般情况下(没有在 define
中手写 id
参数时),module.id
的值就是 module.uri
,两者完全相同。
module.dependencies
dependencies
是一个数组,表示当前模块的依赖。
module.exports
表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取 module.exports
变量。传给 factory
构造方法的 exports
参数是 module.exports
对象的一个引用。只通过 exports
参数来提供接口,有时无法满足开发者的所有需求。 比如当模块的接口是某个类的实例时,需要通过 module.exports
来实现:
define(function(require, exports, module) {// exports 是 module.exports 的一个引用console.log(module.exports === exports); // true// 重新给 module.exports 赋值module.exports = new SomeClass();// exports 不再等于 module.exportsconsole.log(module.exports === exports); // false
});
注意:对 module.exports
的赋值需要同步执行,不能放在回调函数里。下面这样是不行的:
// x.js
define(function(require, exports, module) {// 错误用法setTimeout(function() {module.exports = { a: "hello" };}, 0);
});
在 y.js 里有调用到上面的 x.js:
// y.js
define(function(require, exports, module) {var x = require('./x');// 无法立刻得到模块 x 的属性 aconsole.log(x.a); // undefined
});
define.cmd 属性
defin.cmd
属性是一个空对象,可用来判定当前页面是否有 CMD 模块加载器,其用法跟 AMD 的 denfine.amd
相似,写法如下:
if (typeof define === "function" && define.cmd) {// 有 Sea.js 等 CMD 模块加载器存在
}
CMD 和 AMD 的区别
-
对于依赖的模块 AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从2.0开始,也改成可以延迟执行(根据写法不同,处理方式不通过)。
-
AMD 推崇依赖前置(在定义模块的时候就要声明其依赖的模块),CMD 推崇依赖就近(只有在用到某个模块的时候再去 require ——按需加载)。
//AMD define(['./a','./b'], function (a, b) {//依赖一开始就写好a.test();b.test(); });//CMD define(function (requie, exports, module) {//依赖可以就近书写var a = require('./a');a.test();...//软依赖if (status) {var b = requie('./b');b.test();} });
-
AMD 的 api 默认是一个当多个用,CMD 严格的区分推崇职责单一。例如:AMD 里
require
分全局的和局部的。CMD 里面没有全局的require
,提供seajs.use()
来实现模块系统的加载启动。CMD 里每个 API 都简单纯粹。
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出,CMD 是 SeaJS 在推广过程中被广泛认知。RequireJs 出自 dojo 加载器的作者 James Burke,SeaJs 出自国内前端大师玉伯。二者的区别,玉伯在 12 年如是说:
RequireJS 和 SeaJS 都是很不错的模块加载器,两者区别如下:
-
两者定位有差异。RequireJS 想成为浏览器端的模块加载器,同时也想成为 Rhino / Node 等环境的模块加载器。SeaJS 则专注于 Web 浏览器端,同时通过 Node 扩展的方式可以很方便跑在 Node 服务器端
-
两者遵循的标准有差异。RequireJS 遵循的是 AMD(异步模块定义)规范,SeaJS 遵循的是 CMD (通用模块定义)规范。规范的不同,导致了两者API 的不同。SeaJS 更简洁优雅,更贴近 CommonJS Modules/1.1 和 Node Modules 规范。
-
两者社区理念有差异。RequireJS 在尝试让第三方类库修改自身来支持 RequireJS,目前只有少数社区采纳。SeaJS 不强推,而采用自主封装的方式来“海纳百川”,目前已有较成熟的封装策略。
-
两者代码质量有差异。RequireJS 是没有明显的 bug,SeaJS 是明显没有 bug。
-
两者对调试等的支持有差异。SeaJS 通过插件,可以实现 Fiddler 中自动映射的功能,还可以实现自动 combo 等功能,非常方便便捷。RequireJS无这方面的支持。
-
两者的插件机制有差异。RequireJS 采取的是在源码中预留接口的形式,源码中留有为插件而写的代码。SeaJS 采取的插件机制则与 Node 的方式一致开放自身,让插件开发者可直接访问或修改,从而非常灵活,可以实现各种类型的插件。
优点: 同样实现了浏览器端的模块化加载。 可以按需加载,依赖就近。
缺点: 依赖SPM打包,模块的加载逻辑偏重。