小談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 處時,根據條件,只會執行其中一個。