1. 程式人生 > >[轉]js模塊化——AMD及require.js

[轉]js模塊化——AMD及require.js

lba ID 三方 ext init src 希望 some pan

  由CommonJS組織提出了許多新的JavaScript架構方案和標準,希望能為前端開發提供統一的指引。AMD規範就是其中比較著名一個,全稱是Asynchronous Module Definition,即異步模塊加載機制,完整描述了模塊的定義,依賴關系,引用關系以及加載機制。而AMD規範的作者親自實現了符合AMD規範的requireJS。本文將詳細介紹AMD及requireJS

AMD規範

  AMD(Asynchronous Module Definition)翻譯為異步模塊定義。異步強調的是,在加載模塊以及模塊所依賴的其它模塊時,都采用異步加載的方式,避免模塊加載阻塞了網頁的渲染進度

  AMD作為一個規範,只需定義其語法API,而不關心其實現。AMD規範簡單到只有一個API,即define函數

define([module-name?], [array-of-dependencies?], [module-factory-or-object]);

  module-name: 模塊標識,可以省略

  array-of-dependencies: 所依賴的模塊,可以省略

  module-factory-or-object: 模塊的實現,或者一個JavaScript對象

  define函數具有異步性。當define函數執行時,首先會異步的去調用第二個參數中列出的依賴模塊

,當所有的模塊被載入完成之後如果第三個參數是一個回調函數則執行;然後告訴系統模塊可用,也就通知了依賴於自己的模塊自己已經可用

加載

  使用require.js的第一步,是先去官方網站下載最新版本。下載後,假定把它放在js子目錄下面,就可以加載了

<script src="js/require.js"></script>

  HTML中的script標簽在加載和執行過程中會阻塞網頁的渲染,所以一般要求盡量將script標簽放置在body元素的底部,以便加快頁面顯示的速度,還有一種方式就是通過異步加載的方式來加載js文件,這樣可以避免js文件對html渲染的阻塞

<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對它的處理遵循如下規則:

  1、在沒有使用data-main和config的情況下,baseUrl默認為當前頁面的目錄

  2、在有data-main的情況下,main.js前面的部分就是baseUrl,比如上面的js/

  3、在有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文檔的腳本:1、以 ".js" 結束;2、包含 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

  前面提到過,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的配置

  1、在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>
技術分享圖片

  2、將配置作為全局變量"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) {
    }
);

[轉]js模塊化——AMD及require.js