小谈AMD与CMD
命名冲突和文件依赖,是前端开发过程中的两个经典问题。人们尝试通过模块化开发方法和思维来解决这些问题。
Sea.js与CMD模块化规范
简介
Sea.js 是一个适用于 Web 浏览器端的模块加载器。遵循CMD模块化标准。
定义模块
define(function(require, exports, module){
})
引入模块
var foo = require('./foo.js') // .js可以被省略
注意require的参数,即路径必须是字符串直接量,不得是任何形式的表达式。
异步加载模块
require对模块进行同步加载,如果想要异步加载模块,可以使用
require.async('./foo.js', function(foo){
// do something after the module is loaded
foo.doSomething();
})
// 或
require.async(['./foo.js','./bar.js'], function(foo, bar){
})
路径
路径有相对、顶级与普通之分。相对路径相对于当前路径的uri进行解析,特征是以./或../开头。顶级路径前没有/或.,相对于模块系统的基础路径(即 Sea.js 的 base 路径)来解析。也可以设置普通路径,即绝对路径或根路径(/)。此外sea.use()中的路径始终是普通路径。此外,Sea.js会为每个没有后缀名的文件自动加上js后缀,如果不想这么做,要么为文件加上后缀名,要么在文件名后面加#。
导出模块
exports.foo = foo;
//或
module.exports = bar;
// 或
return {
foo:foo,
bar:bar
}
注意:导出语句必须同步执行,不能放在比如setTimeout等函数的回调中。
配置
seajs.config({
// 别名配置
alias: {
'es5-safe': 'gallery/es5-safe/0.9.3/es5-safe',
'json': 'gallery/json/1.0.2/json',
'jquery': 'jquery/jquery/1.10.1/jquery'
},
// 路径配置,paths 配置可以结合 alias 配置一起使用,让模块引用非常方便。
paths: {
'gallery': 'https://a.alipayobjects.com/gallery'
},
// 变量配置,有时路径只有在运行时才能知道,可以通过{locale}获取配置的值
vars: {
'locale': 'zh-cn'
},
// 映射配置
map: [
['http://example.com/js/app/', 'http://localhost/js/app/']
],
// 预加载项
preload: [
Function.prototype.bind ? '' : 'es5-safe',
this.JSON ? '' : 'json'
],
// 调试模式
debug: true,
// Sea.js 的基础路径
base: 'http://example.com/path/to/base/',
// 文件编码
charset: 'utf-8'
});
此外,seajs.config 可以多次运行,每次运行时,会对配置项进行合并操作。
启动
seajs.use(['jquery', './main'], function($, main) {
$(document).ready(function() {
main.init();
});
});
其它
获得文件(模块)的绝对路径
require.resolve('./foo.js'); // =>http://www.hukaihe.cn/static/foo.js
// 或
module.uri('./foo.js')
获得当前模块所依赖的模块
module.dependencies
Seajs可以方便的跑在Nodejs端
// 让 Node 环境可以加载执行 CMD 模块
require('seajs');
var a = require('./a');
设计原则
- 关注度分离。比如书写模块 a.js 时,如果需要引用 b.js,则只需要知道 b.js 相对 a.js 的相对路径即可,无需关注其他。
- 尽量与浏览器的解析规则一致。比如根路径(/xx/zz)、绝对路径、以及传给 use 方法的非顶级标识,都是相对所在页面的 URL 进行解析。
AMD 与requirejs
RequireJS的目标是鼓励代码的模块化,它使用了不同于传统
baseUrl
RequireJS以一个相对于baseUrl的地址来加载所有的代码。 我们可以在require.config中对baseUrl进行设置,但如果未设置之,则默认与data-main所指定的文件为同一目录,如果未指定data-main属性,那么以引入requirejs的html地址为baseUrl。
此外RequireJS默认假定所有的依赖资源都是js脚本,因此无需在module ID上再加”.js”后缀。RequireJS脚本的加载是支持跨域的。
RequireJS使用head.appendChild()将每一个依赖加载为一个script标签。RequireJS等待所有的依赖加载完毕,计算出模块定义函数正确调用顺序,然后依次调用它们。
data-main
<script data-main="scripts/main" src="scripts/require.js"></script>
<script src="scripts/other.js"></script>
data-main指定的文件是异步加载的,所以不能保证main.js文件在other.js文件加载前完成加载。
模块定义与加载
下面是一个最基本的demo:
// zoo.js
define(['./lion','./tiger'], function(lion, tiger){
var perform = function () {
console.log('马戏团开演了');
lion.perform();
tiger.perform();
}
return {
perform: perform
}
})
// main.js
require(['./zoo'], function(zoo){
zoo.perform();
})
requirejs也支持简单的键值对形式
define({
color: "black",
size: "unisize"
});
RequireJS的模块语法允许它尽快地加载多个模块,虽然加载的顺序不定,但依赖的顺序最终是正确的。同时因为无需创建全局变量,甚至可以做到在同一个页面上同时加载同一模块的不同版本。
严重不鼓励模块定义全局变量。遵循此处的定义模式,可以使得同一模块的不同版本并存于同一个页面上(参见 高级用法 )。另外,函参的顺序应与依赖顺序保存一致。
一个文件对应一个模块,但你可以使用优化工具,为每个模块生成模块名以将多个模块打成一个包,加快到浏览器的载人速度。
循环依赖
如果你定义了一个循环依赖(a依赖b,b同时依赖a),则在这种情形下当b的模块函数被调用的时候,它会得到一个undefined的a。b可以在模块已经定义好后用require()方法再获取(记得将require作为依赖注入进来):
//Inside b.js:
define(["require", "a"], function(require, a) {
//"a" in this case will be null if a also asked for b,
//a circular dependency.
return function(title) {
return require("a").doSomething();
}
}
);
一般说来你无需使用require()去获取一个模块,而是应当使用注入到模块函数参数中的依赖。循环依赖比较罕见,它也是一个重构代码重新设计的警示灯。你也可以换成commonjs风格的代码
define(function(require, exports, module) {
var a = require("a");
exports.foo = function () {
return a.bar();
};
});
JSONP
require(["http://example.com/api/data.json?callback=define"],
function (data) {
//The data object will be the API response for the
//JSONP data call.
console.log(data);
}
);
配置选项
配置方式
<script src="scripts/require.js"></script>
<script>
require.config({
baseUrl: "/another/path",
paths: {
"some": "some/v1.0"
},
waitSeconds: 15
});
require( ["some/module", "my/module", "a.js", "b.js"],
function(someModule, myModule) {
}
);
</script>
下面主要对几个require.config支持的配置项进行阐述
paths
设置path时起始位置是相对于baseUrl的,除非该path设置以”/”开头或含有URL协议(如http:)。此外,paths还可以进行备错处理:
jquery: [
'http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min',
//If the CDN location fails, load from this location
'lib/jquery'
]
shim
为那些没有使用define()来声明依赖关系、设置模块的”浏览器全局变量注入”型脚本做依赖和导出配置。注意:设置shim本身不会触发代码的加载。例如:
shim: {
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone',
init: function (bar) {
// 解决一些库的冲突
}
},
}
map
map用来对项目进行一些列加载的版本控制,如下代码,当“some/newmodule”调用了“require(‘foo’)”,它将获取到foo1.2.js文件;而当“some/oldmodule”调用“`require(‘foo’)”时它将获取到foo1.0.js,而其他模块调用foo时,则会获得foo.0.9。请在map配置中仅使用绝对模块ID,“../some/thing”之类的相对ID不能工作。
map: {
'*':{
'foo':'foo 0.9'
}
'some/newmodule': {
'foo': 'foo1.2'
},
'some/oldmodule': {
'foo': 'foo1.0'
}
}
config
设置想要传递给具体模块的信息
requirejs.config({
config: {
'bar': {
size: 'large'
},
'baz': {
color: 'blue'
}
}
});
define(['module'], function (module) {
//Will be the value 'blue'
var color = module.config().color;
});
waitSeconds
在放弃加载一个脚本之前等待的秒数。设为0禁用等待超时。默认为7秒。
插件
domReady是最常见的插件,其作用是保证模块脚本执行之前页面已经完成加载。模块实现了Loader Plugin API,因此你可以使用loader plugin语法(注意domReady依赖的!前缀)来强制require()回调函数在执行之前等待DOM Ready。当用作loader plugin时,domReady会返回当前的document
require(['domReady!'], function (doc) {
});
注意: 如果document需要一段时间来加载(也许是因为页面较大,或加载了较大的js脚本阻塞了DOM计算),使用domReady作为loader plugin可能会导致RequireJS“超时”错。如果这是个问题,则考虑增加waitSeconds配置项的值,或在require()使用domReady()调用(将其当做是一个模块)。
AMD VS CMD比较
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。
二者推崇的代码风格不同,CMD 推崇依赖就近,AMD 推崇依赖前置
对于依赖的模块,AMD 是提前执行,CMD 是延迟执行
二者最大的区别在于factory回调的执行时机不同
/* a.js */
define(factory);
/* b.js */
define(factory);
/* c.js */
define(function(require) {
// BEGIN
if(some_condition) {
require('./a').doSomething();
} else {
require('./b').soSomething();
}
// END
});
在AMD模式下,c模块的 factory 在执行时,会接收 a 和 b 两个参数。这意味着,c 依赖的所有模块,都是在一开始就得执行好,即便有可能不需要执行。,换句话说,在 BEGIN 处,a 和 b 的 factory 都已经执行好。在 CMD 规范里,在 BEGIN 处,a 和 b 的 factory 还没未执行,在 END 处时,根据条件,只会执行其中一个。