由CommonJS組織提出了許多新的JavaScript架構方案和標準,但願能爲前端開發提供統一的指引。AMD規範就是其中比較著名一個,全稱是Asynchronous Module Definition,即異步模塊加載機制,完整描述了模塊的定義,依賴關係,引用關係以及加載機制。而AMD規範的做者親自實現了符合AMD規範的requireJS。本文將詳細介紹AMD及requireJSjavascript
AMD(Asynchronous Module Definition)翻譯爲異步模塊定義。異步強調的是,在加載模塊以及模塊所依賴的其它模塊時,都採用異步加載的方式,避免模塊加載阻塞了網頁的渲染進度css
AMD做爲一個規範,只需定義其語法API,而不關心其實現。AMD規範簡單到只有一個API,即define函數html
define([module-name?], [array-of-dependencies?], [module-factory-or-object]);
module-name: 模塊標識,能夠省略前端
array-of-dependencies: 所依賴的模塊,能夠省略java
module-factory-or-object: 模塊的實現,或者一個JavaScript對象node
define函數具備異步性。當define函數執行時,首先會異步的去調用第二個參數中列出的依賴模塊,當全部的模塊被載入完成以後,若是第三個參數是一個回調函數則執行;而後告訴系統模塊可用,也就通知了依賴於本身的模塊本身已經可用jquery
使用require.js的第一步,是先去官方網站下載最新版本。下載後,假定把它放在js子目錄下面,就能夠加載了git
<script src="js/require.js"></script>
HTML中的script標籤在加載和執行過程當中會阻塞網頁的渲染,因此通常要求儘可能將script標籤放置在body元素的底部,以便加快頁面顯示的速度,還有一種方式就是經過異步加載的方式來加載js文件,這樣能夠避免js文件對html渲染的阻塞github
<script src="js/require.js" defer async></script>
require.js在加載的時候會檢查data-main屬性,當requireJS自身加載執行後,就會再次異步加載data-main屬性指向的main.js。這個main.js是當前網頁全部邏輯的入口,理想狀況下,整個網頁只須要這一個script標記,利用requireJS加載依賴的其它文件編程
<script data-main="scripts/main" src="js/require.js"></script>
[注意]在main.js中所設置的腳本是異步加載的。因此若是在頁面中配置了其它JS加載,則不能保證它們所依賴的JS已經加載成功
<script data-main="scripts/main" src="js/require.js"></script> <script src="js/other.js"></script>
【內部機制】
在RequireJS內部,會使用head.appendChild()將每個模塊依賴加載爲一個script標籤。RequireJS等待全部的依賴加載完畢,計算出模塊定義函數正確調用順序,而後依次調用它們
模塊不一樣於傳統的腳本文件,它良好地定義了一個做用域來避免全局名稱空間污染。它能夠顯式地列出其依賴關係,並以函數(定義此模塊的那個函數)參數的形式將這些依賴進行注入,而無需引用全局變量。RequireJS的模塊是模塊模式的一個擴展,其好處是無需全局地引用其餘模塊
RequireJS的模塊語法容許它儘快地加載多個模塊,雖然加載的順序不定,但依賴的順序最終是正確的。同時由於無需建立全局變量,甚至能夠作到在同一個頁面上同時加載同一模塊的不一樣版本
一個文件應該只定義1個模塊。多個模塊可使用內置優化工具將其組織打包
若是咱們的代碼不依賴任何其餘模塊,那麼能夠直接寫入javascript代碼
//main.js
console.log(1);
但這樣的話,就不必使用require.js了。真正常見的狀況是,主模塊依賴於其餘模塊,這時就要使用AMD規範定義的的require()函數
// main.js require(['moduleA'], function(a){ console.log(a); }); //moduleA.js define(function(){ return 1; })
這裏拋出一個問題,爲何主模塊使用的是require()函數,而模塊moduleA使用define()函數呢?由於define()定義的模塊能夠被調用,而require()不能夠。主模塊main.js是入口文件,須要調用別的模塊,而不須要被別的模塊調用,因此使用require()或define()均可以。而moduleA須要被調用,因此只能使用define()
若是把moduleA.js中的define()方法改成require()方法,則返回undefined
// main.js require(['moduleA'], function(a){ console.log(a); }); //moduleA.js require(function(){ return 1; })
【簡單的值對】
上面的模塊moduleA中,回調函數返回了一個數字。而實際上,模塊能夠有多種形式,好比一個簡單的值對
define({ color: "black", size: "unisize" });
返回的結果以下:
【函數式定義】
若是一個模塊沒有任何依賴,但須要一個作setup工做的函數,則在define()中定義該函數,並將其傳給define()
define(function () { //Do setup work here return { color: "black", size: "unisize" } });
返回的結果以下:
【存在依賴的函數式定義】
若是模塊存在依賴:則第一個參數是依賴的名稱數組;第二個參數是函數,在模塊的全部依賴加載完畢後,該函數會被調用來定義該模塊,所以該模塊應該返回一個定義了本模塊的object。依賴關係會以參數的形式注入到該函數上,參數列表與依賴名稱列表一一對應
//moduleA.js define(['moduleB'], function(b) { var num = 10; return b.add(num); } ); ////moduleB.js define({ add: function(n){ return n+1; } });
【命名模塊】
define()中能夠包含一個模塊名稱做爲首個參數
//moduleA.js define("moduleA",['moduleB'], function(b) { var num = 10; return b.add(num); } );
這些常由優化工具生成。也能夠本身顯式指定模塊名稱,但這使模塊更不具有移植性——就是說若將文件移動到其餘目錄下,就得重命名。通常最好避免對模塊硬編碼,而是交給優化工具去生成。優化工具須要生成模塊名以將多個模塊打成一個包,加快到瀏覽器的載入速度
html中的base元素用於指定文檔裏全部相對URL地址的基礎URL,requireJS的baseUrl跟這個base元素起的做用是相似的,因爲requireJS老是動態地請求依賴的JS文件,因此必然涉及到一個JS文件的路徑解析問題,requireJS默認採用一種baseUrl + moduleID的解析方式,requireJS對它的處理遵循以下規則:
一、在沒有使用data-main和config的狀況下,baseUrl默認爲當前頁面的目錄
二、在有data-main的狀況下,main.js前面的部分就是baseUrl,好比上面的js/
三、在有config的狀況下,baseUrl以config配置的爲準
上述三種方式,優先級由低到高排列
RequireJS以一個相對於baseUrl的地址來加載全部的代碼。頁面頂層script標籤含有一個特殊的屬性data-main,require.js使用它來啓動腳本加載過程,而baseUrl通常設置到與該屬性相一致的目錄
<script data-main="js/main.js" src="scripts/require.js"></script>
在模塊章節的示例中,代碼以下所示
// main.js require(['moduleA'], function(a){ console.log(a); }); //moduleA.js define(function(){ return 1; })
入口文件main.js依賴於moduleA,直接寫成['moduleA'],默認狀況下,require.js假定moduleA與main.js在同一個目錄,即'js/moduleA.js',文件名爲moduleA.js,而後自動加載
使用require.config()方法,咱們能夠對模塊的加載行爲進行自定義。require.config()就寫在主模塊(main.js)的頭部。參數就是一個對象,這個對象的paths屬性指定各個模塊的加載路徑
下面在demo文件夾下新建一個test文件夾,並在test文件夾下新建一個moduleA.js文件,內容以下
//moduleA.js define(function(){ return 2; })
而在原來的js文件夾下,依然存在一個moduleA.js文件,內容以下
//moduleA.js define(function() { return 1; });
當js文件夾下的main.js進行config配置時
// main.js require.config({ baseUrl: 'test' }) require(['moduleA'], function(a){ console.log(a); });
結果爲2,說明識別的是'test/moduleA.js'文件
當js文件夾下的main.js不進行config配置時
// main.js require(['moduleA'], function(a){ console.log(a); });
結果爲1,說明識別的是'js/moduleA.js'文件
RequireJS默認假定全部的依賴資源都是js腳本,所以無需在module ID上再加".js"後綴,RequireJS在進行module ID到path的解析時會自動補上後綴
若是一個模塊的路徑比較深,或者文件名特別長,好比'js/lib/moduleA.min.js',則可使用config配置對象中的paths屬性
// main.js require.config({ paths:{ 'moduleA':'lib/moduleA.min' } }) require(['moduleA'], function(a){ console.log(a); }); //moduleA-min.js define(function(){ return 3; })
結果爲3
要注意的是,這裏的paths的'moduleA'設置的是'lib/moduleA.min',而不是'js/lib/moduleA.min',是由於requireJS中的文件解析是一個"baseUrl + paths"的解析過程
在index.html的入口文件設置的是'js/main',因此baseURL是'js'。所以'baseUrl + paths' = 'js/lib/moduleA.min'
<script src="require.js" data-main="js/main" defer async></script>
若是在config配置對象中設置了baseUrl,則以此爲準
// main.js require.config({ baseUrl: 'js/lib', paths:{ 'moduleA':'moduleA.min' } }) require(['moduleA'], function(a){ console.log(a); });
結果一樣爲3,baseURL是'js/lib',paths是'moduleA.min'。所以'baseUrl + paths' = 'js/lib/moduleA.min'
若是一個module ID符合下述規則之一,其ID解析會避開常規的"baseUrl + paths"配置,而是直接將其加載爲一個相對於當前HTML文檔的腳本:一、以 ".js" 結束;二、包含 URL 協議,如 "http:" or "https:"
以下所示,require()函數所依賴的模塊路徑爲'js/moduleA.js'
// main.js require.config({ baseUrl: 'js/lib', paths:{ 'moduleA':'moduleA.min' } }) require(['js/moduleA.js'], function(a){ console.log(a); });
而該文件的代碼以下,路徑爲'js/moduleA.js',而不是'js/lib/moduleA.min',因此,最終結果爲1
//moduleA.js define(function() { return 1; });
通常來講,最好仍是使用baseUrl及"paths" config去設置module ID。它會帶來額外的靈活性,如便於腳本的重命名、重定位等。 同時,爲了不凌亂的配置,最好不要使用多級嵌套的目錄層次來組織代碼,而是要麼將全部的腳本都放置到baseUrl中,要麼分置爲項目庫/第三方庫的一個扁平結構,以下
www/ index.html js/ app/ sub.js lib/ jquery.js canvas.js main.js
前面提到過,commonJS主要應用於服務器端編程,如nodejs。使用打包工具Browserify能夠對CommonJS進行格式轉換,使其能夠在瀏覽器端進行
而requireJS支持一種簡單包裝CommonJS的方式,只要在commonJS代碼的外層簡單包裹一層函數,就能夠在瀏覽器端直接運行
define(function(require, exports, module) { });
若是該模塊還依賴其餘模塊,如依賴模塊moduleA,則代碼以下
define(['moduleA'],function(require, exports, module) { });
a.js和b.js的commonJS形式的代碼以下
// a.js var a = 100; module.exports.a = a; // b.js var result = require('./a'); console.log(result.a);
index.html直接引用b.js會報錯,提示require沒有被定義
<script src="b.js"></script>
將a.js和b.js進行改造以後,代碼以下
// a.js define(function(require, exports, module) { var a = 100; module.exports.a = a; }); // b.js define(function(require, exports, module) { var result = require('./a'); console.log(result.a); });
index.html將入口文件設置爲'js/b',則結果爲100
<script src="require.js" data-main="js/b" defer async></script>
有以下例子,入口文件main.js代碼以下
// main.js require(['a'], function(a){ console.log('main'); document.onclick = function(){ a.test(); } });
所依賴的模塊a.js的代碼以下
define(function(){ console.log('a'); return { test : function(){ console.log('a.test'); } } })
在瀏覽器端執行時,即便不點擊頁面,瀏覽器也會下載a.js文件。這個性能消耗是不容忽視的
AMD保留了commonjs中的require、exprots、module這三個功能。能夠不把依賴羅列在dependencies數組中。而是在代碼中用require來引入
重寫後的代碼以下
// main.js define(function(){ console.log('main'); document.onclick = function(){ require(['a'],function(a){ a.test(); }); } }); //a.js define(function(){
console.log('a'); return { test : function(){ console.log('a.test'); } } })
在瀏覽器端執行時,若是不點擊頁面,瀏覽器就不會下載a.js文件,這樣就實現懶加載
在requireJS中,除了路徑配置以外,還有一些其餘配置
【配置設置】
在前面的例子中,咱們配置requireJS中的路徑是經過入口文件main.js中的config對象來配置的。實際上,不經過入口文件,也能夠進行requireJS的配置
一、在index.html文件嵌入javascript代碼
在HTML文件中,加載requireJS文件以後,當即對requireJS進行配置,至關於將main.js文件變爲內嵌的javascript文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <script src="require.js"></script> <script> require.config({ baseUrl: 'js/lib', paths:{ 'moduleA':'moduleA.min' } }) require(['moduleA'], function(a){ console.log(a); }); </script> </body> </html>
二、將配置做爲全局變量"require"在require.js加載以前進行定義,它會被自動應用
這裏有一個問題是,若是require做爲全局變量被提早定義,則data-main入口文件,是以baseUrl爲基礎進行設置的
[注意]使用 var require = {} 的形式而不是 window.require = {}的形式。後者在IE中運行不正常
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> <script> var require = { baseUrl: 'js/lib', paths:{ 'moduleA':'moduleA.min' } } </script> <script src="require.js" data-main="../main"></script> </body> </html>
【shim】
shim屬性爲那些沒有使用define()來聲明依賴關係、設置模塊的"瀏覽器全局變量注入"型腳本作依賴和導出配置,即加載非規範的模塊
舉例來講,underscore和backbone這兩個庫,都沒有采用AMD規範編寫。若是要加載它們的話,必須先定義它們的特徵。具體來講,每一個模塊要定義(1)exports值(輸出的變量名),代表這個模塊外部調用時的名稱;(2)deps數組,代表該模塊的依賴性
經過以下配置後,如今能夠經過_調用underscore的api,使用Backbone來調用backbone的api
require.config({ shim: { 'underscore':{ exports: '_' }, 'backbone': { deps: ['underscore', 'jquery'], exports: 'Backbone' } } });
jQuery的插件能夠以下這樣定義,如今能夠經過jQuery.fn.scroll來調用該插件的api
shim: { 'jquery.scroll': { deps: ['jquery'], exports: 'jQuery.fn.scroll' } }
require.js還提供一系列插件,實現一些特定的功能
【dom ready】
RequireJS加載模塊速度很快,頗有可能在頁面DOM Ready以前腳本已經加載完畢。須要與DOM交互的工做應等待DOM Ready。現代的瀏覽器經過DOMContentLoaded事件來知會
可是,不是全部的瀏覽器都支持DOMContentLoaded。domReady模塊實現了一個跨瀏覽器的方法來斷定什麼時候DOM已經ready
// main.js require(['domready!'], function(){ console.log('ready'); });
【text】
text插件能夠用來加載如.html、.css等文本文件,能夠經過該插件來實現完整組件(結構+邏輯+樣式)的組件化開發
require(["some/module", "text!some/module.html", "text!some/module.css"], function(module, html, css) { } );