JavaSript模塊規範 - AMD規範與CMD規範介紹

原文:http://blog.chinaunix.net/uid-26672038-id-4112229.htmljavascript

原文:https://www.imququ.com/post/amd-simplified-commonjs-wrapping.htmlphp

 

JavaSript模塊化


    在瞭解AMD,CMD規範前,仍是須要先來簡單地瞭解下什麼是模塊化,模塊化開發?

    模塊化是指在解決某一個複雜問題或者一系列的雜糅問題時,依照一種分類的思惟把問題進行系統性的分解以之處理。模塊化是一種處理複雜系統分解爲代碼結構更合理,可維護性更高的可管理的模塊的方式。能夠想象一個巨大的系統代碼,被整合優化分割成邏輯性很強的模塊時,對於軟件是一種何等意義的存在。對於軟件行業來講:解耦軟件系統的複雜性,使得無論多麼大的系統,也能夠將管理,開發,維護變得「有理可循」。

    還有一些對於模塊化一些專業的定義爲:模塊化是軟件系統的屬性,這個系統被分解爲一組高內聚,低耦合的模塊。那麼在理想狀態下咱們只須要完成本身部分的核心業務邏輯代碼,其餘方面的依賴能夠經過直接加載被人已經寫好模塊進行使用便可。

首先,既然是模塊化設計,那麼做爲一個模塊化系統所必須的能力:
    1. 定義封裝的模塊。
    2. 定義新模塊對其餘模塊的依賴。
    3. 可對其餘模塊的引入支持。

    好了,思想有了,那麼總要有點什麼來創建一個模塊化的規範制度吧,否則各式各樣的模塊加載方式只會將局攪得更爲混亂。那麼在JavaScript中出現了一些非傳統模塊開發方式的規範 CommonJS的模塊規範,AMD(Asynchronous Module Definition),CMD(Common Module Definition)等。



AMD 與 RequireJS

AMD

    Asynchronous Module Definition,用白話文講就是 異步模塊定義,對於 JSer 來講,異步是再也熟悉不過的詞了,全部的模塊將被異步加載,模塊加載不影響後面語句運行。全部依賴某些模塊的語句均放置在回調函數中。

    AMD規範定義了一個自由變量或者說是全局變量 define 的函數。

    define( id?, dependencies?, factory );     css

    第一個參數 id 爲字符串類型,表示了模塊標識,爲可選參數。若不存在則模塊標識應該默認定義爲在加載器中被請求腳本的標識。若是存在,那麼模塊標識必須爲頂層的或者一個絕對的標識。
    第二個參數,dependencies ,是一個當前模塊依賴的,已被模塊定義的模塊標識的數組字面量。
    第三個參數,factory,是一個須要進行實例化的函數或者一個對象。

    建立模塊標識爲 alpha 的模塊,依賴於 require, export,和標識爲 beta 的模塊  

  1. define("alpha", [ "require", "exports", "beta" ], function( require, exports, beta ){
  2.     export.verb = function(){
  3.         return beta.verb();
  4.         // or:
  5.         return require("beta").verb();
  6.     }
  7. });


    一個返回對象字面量的異步模塊

  1. define(["alpha"], function( alpha ){
  2.     return {
  3.         verb : function(){
  4.             return alpha.verb() + 1 ;
  5.         }
  6.     }
  7. });


    無依賴模塊能夠直接使用對象字面量來定義

  1. define( {
  2.     add : function( x, y ){
  3.         return x + y ;
  4.     }
  5. } );


    相似與 CommonJS 方式定義

  1. define( function( require, exports, module){
  2.     var a = require('a'),
  3.           b = require('b');

  4.     exports.action = function(){};
  5. } );


    require();   html


    在 AMD 規範中的 require 函數與通常的 CommonJS中的 require 不一樣。因爲動態檢測依賴關係使加載異步,對於基於回調的 require 需求強烈。

    局部 與 全局 的require 前端

    局部的 require 須要在AMD模式中的 define 工廠函數中傳入 require。

  1. define( ['require'], function( require ){
  2.   // ...
  3. } );
  4. or:
  5. define( function( require, exports, module ){
  6.   // ...
  7. } );



    局部的 require 須要其餘特定的 API 來實現。
    全局的 require 函數是惟一全局做用域下的變量,像 define同樣。全局的 require 並非規範要求的,可是若是實現全局的 require函數,那麼其須要具備與局部 require 函數 同樣的如下的限定:
    1. 模塊標識視爲絕對的,而不是相對的對應另外一個模塊標識。
    2. 只有在異步狀況下,require的回調方式才被用來做爲交互操做使用。由於他不可能在同步的狀況下經過 require(String) 從頂層加載模塊。
    依賴相關的API會開始模塊加載。若是須要有互操做的多個加載器,那麼全局的 reqiure 應該被加載頂層模塊來代替。


  1. require(String)
  2. define( function( require ){
  3.     var a = require('a'); // 加載模塊a
  4. } );

  5. require(Array, Function)
  6. define( function( require ){
  7.     require( ['a', 'b'], function( a,b ){ // 加載模塊a b 使用
  8.         // 依賴 a b 模塊的運行代碼
  9.     } );
  10. } );

  11. require.toUrl( Url )
  12. define( function( require ){
  13.     var temp = require.toUrl('./temp/a.html'); // 加載頁面
  14. } );


    amdjs 的API    https://github.com/amdjs/amdjs-api/wiki


RequireJS

    官網  http://www.requirejs.org/

    RequireJS 是一個前端的模塊化管理的工具庫,遵循AMD規範,它的做者就是AMD規範的創始人 James Burke。因此說RequireJS是對AMD規範的闡述一點也不爲過。

    RequireJS 的基本思想爲:經過一個函數來將全部所須要的或者說所依賴的模塊實現裝載進來,而後返回一個新的函數(模塊),咱們全部的關於新模塊的業務代碼都在這個函數內部操做,其內部也可無限制的使用已經加載進來的以來的模塊。


  1. <script data-main='scripts/main' src='scripts/require.js'></script>

    那麼scripts下的main.js則是指定的主代碼腳本文件,全部的依賴模塊代碼文件都將從該文件開始異步加載進入執行。

    defined用於定義模塊,RequireJS要求每一個模塊均放在獨立的文件之中。按照是否有依賴其餘模塊的狀況分爲獨立模塊和非獨立模塊。
    1. 獨立模塊,不依賴其餘模塊。 直接定義:

  1. define({
  2.     method1: function(){},
  3.     method2: function(){}
  4. });

    也等價於

  1. define(function(){
  2.     return{
  3.         method1: function(){},
  4.         method2: function(){}
  5.     }
  6. });

    2. 非獨立模塊,對其餘模塊有依賴。

  1. define([ 'module1', 'module2' ], function(m1, m2){
  2.     ...
  3. });

    或者:

  1. define( function( require ){
  2.     var m1 = require( 'module1' ),
  3.           m2 = require( 'module2' );
  4.     ...
  5. });


    簡單看了一下RequireJS的實現方式,其 require 實現只不過是將 function 字符串而後提取 require 以後的模塊名,將其放入依賴關係之中。

    require方法調用模塊

    在require進行調用模塊時,其參數與define相似。

  1. require( ['foo', 'bar'], function( foo, bar ){
  2.     foo.func();
  3.     bar.func();
  4. } );

    在加載 foo 與 bar 兩個模塊以後執行回調函數實現具體過程。

    固然還能夠如以前的例子中的,在define定義模塊內部進行require調用模塊

  1. define( function( require ){
  2.     var m1 = require( 'module1' ),
  3.           m2 = require( 'module2' );
  4.     ...
  5. });

    define 和 require 這兩個定義模塊,調用模塊的方法合稱爲AMD模式,定義模塊清晰,不會污染全局變量,清楚的顯示依賴關係。AMD模式能夠用於瀏覽器環境而且容許非同步加載模塊,也能夠按需動態加載模塊。



CMD 與 seaJS

CMD

    在CMD中,一個模塊就是一個文件,格式爲:
    define( factory );

    全局函數define,用來定義模塊。
    參數 factory  能夠是一個函數,也能夠爲對象或者字符串。
    當 factory 爲對象、字符串時,表示模塊的接口就是該對象、字符串。

    定義JSON數據模塊:
  1. define({ "foo": "bar" });

    經過字符串定義模板模塊:

  1. define('this is {{data}}.');

    factory 爲函數的時候,表示模塊的構造方法,執行構造方法即可以獲得模塊向外提供的接口。

  1. define( function(require, exports, module) {
  2.     // 模塊代碼
  3. });


    define( id?, deps?, factory );
    define也能夠接受兩個以上的參數,字符串id爲模塊標識,數組deps爲模塊依賴:

  1. define( 'module', ['module1', 'module2'], function( require, exports, module ){
  2.     // 模塊代碼
  3. } );
    其與 AMD 規範用法不一樣。

    require 是 factory 的第一個參數。
    require( id );
    接受模塊標識做爲惟一的參數,用來獲取其餘模塊提供的接口:

  1. define(function( require, exports ){
  2.     var a = require('./a');
  3.     a.doSomething();
  4. });

    require.async( id, callback? );
    require是同步往下執行的,須要的異步加載模塊可使用 require.async 來進行加載:

  1. define( function(require, exports, module) {
  2.     require.async('.a', function(a){
  3.         a.doSomething();
  4.     });
  5. });

    require.resolve( id )
    可使用模塊內部的路徑機制來返回模塊路徑,不會加載模塊。

    exports 是 factory 的第二個參數,用來向外提供模塊接口。

  1. define(function( require, exports ){
  2.     exports.foo = 'bar'; // 向外提供的屬性
  3.     exports.do = function(){}; // 向外提供的方法
  4. });

    固然也可使用 return 直接向外提供接口。

  1. define(function( require, exports ){
  2.     return{
  3.         foo : 'bar', // 向外提供的屬性
  4.         do : function(){} // 向外提供的方法
  5.     }
  6. });

    也能夠簡化爲直接對象字面量的形式:

  1. define({
  2.     foo : 'bar', // 向外提供的屬性
  3.     do : function(){} // 向外提供的方法
  4. });


    與nodeJS中同樣須要注意的是,一下方式是錯誤的:

  1. define(function( require, exports ){
  2.     exports = {
  3.         foo : 'bar', // 向外提供的屬性
  4.         do : function(){} // 向外提供的方法
  5.     }
  6. });


    須要這麼作

  1. define(function( require, exports, module ){
  2.     module.exports = {
  3.         foo : 'bar', // 向外提供的屬性
  4.         do : function(){} // 向外提供的方法
  5.     }
  6. });

    傳入的對象引用能夠添加屬性,一旦賦值一個新的對象,那麼值錢傳遞進來的對象引用就會失效了。開始之初,exports 是做爲 module.exports 的一個引用存在,一切行爲只有在這個引用上 factory 才得以正常運行,賦值新的對象後就會斷開引用,exports就只是一個新的對象引用,對於factory來講毫無心義,就會出錯。

    module 是factory的第三個參數,爲一個對象,上面存儲了一些與當前模塊相關聯的屬性與方法。
        module.id 爲模塊的惟一標識。
        module.uri 根據模塊系統的路徑解析規則獲得模塊的絕對路徑。
        module.dependencies 表示模塊的依賴。
        module.exports 當前模塊對外提供的接口。


seaJS

    官網  http://seajs.org/docs/
    sea.js 核心特徵:
        1. 遵循CMD規範,與NodeJS般的書寫模塊代碼。
        2. 依賴自動加載,配置清晰簡潔。
    兼容 Chrome 3+,Firefox 2+,Safari 3.2+,Opera 10+,IE 5.5+。

    seajs.use 
    用來在頁面中加載一個或者多個模塊

  1. // 加載一個模塊
  2. seajs.use('./a');
  3. // 加載模塊,加載完成時執行回調
  4. seajs.use('./a',function(a){
  5.     a.doSomething();
  6. });
  7. // 加載多個模塊執行回調
  8. seajs.use(['./a','./b'],function(a , b){
  9.     a.doSomething();
  10.     b.doSomething();
  11. });

    其define 與 require 使用方式基本就是CMD規範中的示例。

 


AMD 與 CMD 區別到底在哪裏?


    看了以上 AMD,requireJS 與 CMD, seaJS的簡單介紹會有點感受模糊,總感受較爲類似。由於像 requireJS 其並非只是純粹的AMD固有思想,其也是有CMD規範的思想,只不過是推薦 AMD規範方式而已, seaJS也是同樣。

    下面是玉伯對於 AMD 與 CMD 區別的解釋:

    AMD 是 RequireJS 在推廣過程當中對模塊定義的規範化產出。
    CMD 是 SeaJS 在推廣過程當中對模塊定義的規範化產出。

    相似的還有 CommonJS Modules/2.0 規範,是 BravoJS 在推廣過程當中對模塊定義的規範化產出 還有很多??

 
    這些規範的目的都是爲了 JavaScript 的模塊化開發,特別是在瀏覽器端的。
    目前這些規範的實現都能達成瀏覽器端模塊化開發的目的。

 
    區別:

 
    1. 對於依賴的模塊,AMD 是提早執行,CMD 是延遲執行。不過 RequireJS 從 2.0 開始,也改爲能夠延遲執行(根據寫法不一樣,處理方式不一樣)。CMD 推崇 as lazy as possible.
 
    2. CMD 推崇依賴就近,AMD 推崇依賴前置。看代碼:
 

  1. // CMD
  2. define(function(require, exports, module) {
  3.     var a = require('./a')
  4.     a.doSomething()
  5.     // 此處略去 100 行
  6.     var b = require('./b') // 依賴能夠就近書寫
  7.     b.doSomething()
  8.     // ...
  9. })

  10. // AMD 默認推薦的是
  11. define(['./a', './b'], function(a, b) { // 依賴必須一開始就寫好
  12.     a.doSomething()
  13.     // 此處略去 100 行
  14.     b.doSomething()
  15.     // ...
  16. })

 
雖然 AMD 也支持 CMD 的寫法,同時還支持將 require 做爲依賴項傳遞,但 RequireJS 的做者默認是最喜歡上面的寫法,也是官方文檔裏默認的模塊定義寫法。
 
 
    3. AMD 的 API 默認是一個當多個用,CMD 的 API 嚴格區分,推崇職責單一。好比 AMD 裏,require 分全局 require 和局部 require,都叫 require。CMD 裏,沒有全局 require,而是根據模塊系統的完備性,提供 seajs.use 來實現模塊系統的加載啓動。CMD 裏,每一個 API 都簡單純粹。
 
 
    4. 還有一些細節差別,具體看這個規範的定義就好,就很少說了。
 
另外,SeaJS 和 RequireJS 的差別,能夠參考: https://github.com/seajs/seajs/issues/277

總結

    本文主要是介紹了一下 AMD CMD的規範,順便簡單的講述了一下 requireJS 與 seaJS。講的較爲籠統,下面的擴展閱讀能夠更好的幫助你理解模塊化以及各個規範。


擴展閱讀:

AMD規範文檔  https://github.com/amdjs/amdjs-api/wiki/AMD
amdjs 的 require 接口文檔  https://github.com/amdjs/amdjs-api/wiki/require
amdjs 的接口文檔  https://github.com/amdjs/amdjs-api/wiki
RequireJS官網接口文檔   http://www.requirejs.org/docs/api.html  

前端模塊化開發的價值  https://github.com/seajs/seajs/issues/547
前端模塊化開發那點歷史  https://github.com/seajs/seajs/issues/588
從 CommonJS 到 Sea.js  https://github.com/seajs/seajs/issues/269     

RequireJS和AMD規範   http://javascript.ruanyifeng.com/tool/requirejs.html  

知乎  AMD 和 CMD 的區別有哪些?  http://www.zhihu.com/question/20351507  



不提倡使用AMD 的 CommonJS wrapping

其實本文的標題應該是「爲何我不推薦使用 AMD 的 Simplified CommonJS wrapping」,但太長了很差看,爲了美觀我只能砍掉一截。 node

它是什麼?

爲了複用已有的 CommonJS 模塊,AMD 規定了 Simplified CommonJS wrapping,而後 RequireJS 實現了它(前後順序不必定對)。它提供了相似於 CommonJS 的模塊定義方式,以下:python

define( function (require, exports, module) {
     var A = require( 'a' );
 
     return function () {};
});

這樣,模塊的依賴能夠像 CommonJS 同樣「就近定義」。但就是這個看上去一箭雙鵰的作法,給你們帶來了不少困擾。git

它作了什麼?

因爲 RequireJS 是最流行的 AMD 加載器,後續討論都基於 RequireJS 進行。github

直接看 RequireJS 這部分邏輯:

 
//If no name, and callback is a function, then figure out if it a
//CommonJS thing with dependencies.
if (!deps && isFunction(callback)) {
     deps = [];
     if (callback.length) {
         callback
             .toString()
             .replace(commentRegExp, '' )
             .replace(cjsRequireRegExp, function (match, dep) {
                 deps.push(dep);
             });
 
         deps = (callback.length === 1 ? [ 'require' ] : [ 'require' , 'exports' , 'module' ]).concat(deps);
     }
}

能夠看到,爲了支持 CommonJS Wrapper 這種寫法,define 函數裏須要作這些事情:

  1. 經過 factory.toString() 拿到 factory 的源碼;
  2. 去掉源碼中的註釋(避免匹配到註釋掉的依賴模塊);
  3. 經過正則匹配 require 的方式獲得依賴信息;

寫模塊時要把 require 當成保留字。模塊加載器和構建工具都要實現上述邏輯。

對於 RequireJS,本文最開始定義的模塊,最終會變成:

 
define([ 'a' ], function (require, exports, module) {
     var A = require( 'a' );
 
     return function () {};
});

等價於:

 
define([ 'a' ], function (A) {
     return function () {};
});

結論是,CommonJS Wrapper 只是書寫上兼容了 CommonJS 的寫法,模塊運行邏輯並不會改變。

AMD 運行策略

AMD 運行時核心思想是「Early Executing」,也就是提早執行依賴。這個好理解:

 
//main.js
define([ 'a' , 'b' ], function (A, B) {
     //運行至此,a.js 和 b.js 已下載完成(運行於瀏覽器的 Loader 必須如此);
     //A、B 兩個模塊已經執行完,直接可用(這是 AMD 的特性);
 
     return function () {};
});

我的以爲,AMD 的這個特性有好有壞:

首先,儘早執行依賴能夠儘早發現錯誤。上面的代碼中,假如 a 模塊中拋異常,那麼 main.js 在調用 factory 方法以前必定會收到錯誤,factory 不會執行;若是按需執行依賴,結果是:1)沒有進入使用 a 模塊的分支時,不會發生錯誤;2)出錯時,main.js 的 factory 方法極可能執行了一半。

另外,儘早執行依賴一般能夠帶來更好的用戶體驗,也容易產生浪費。例如模塊 a 依賴了另一個須要異步加載數據的模塊 b,儘早執行 b 可讓等待時間更短,同時若是 b 最後沒被用到,帶寬和內存開銷就浪費了;這種場景下,按需執行依賴能夠避免浪費,可是帶來更長的等待時間。

我我的更傾向於 AMD 這種作法。舉一個不太恰當的例子:Chrome 和 Firefox 爲了更好的體驗,對於某些類型的文件,點擊下載地址後會詢問是否保存,這時候實際上已經開始了下載。有時候等了好久才點確認,會開心地發現文件已經下好;若是點取消,瀏覽器會取消下載,已下載的部分就浪費了。

瞭解到 AMD 這個特性後,再來看一段代碼:

 
//mod1.js
define( function () {
     console.log( 'require module: mod1' );
 
     return {
         hello: function () {
             console.log( "hello mod1" );
         }
     };
});
 
//mod2.js
define( function () {
     console.log( 'require module: mod2' );
 
     return {
         hello: function () {
             console.log( "hello mod2" );
         }
     };
});
 
//main.js
define([ 'mod1' , 'mod2' ], function (mod1, mod2) {
     //運行至此,mod1.js 和 mod2.js 已經下載完成;
     //mod一、mod2 兩個模塊已經執行完,直接可用;
 
     console.log( 'require module: main' );
 
     mod1.hello();
     mod2.hello();
 
     return {
         hello: function () {
             console.log( 'hello main' );
         }
     };
});
 
<!--index.html-->
< script >
     require(['main'], function(main) {
         main.hello();
     });
</ script >

在本地測試,一般結果是這樣的:

 
require module: mod1
require module: mod2
require module: main
hello mod1
hello mod2
hello main

這個結果符合預期。可是這就是所有嗎?用 Fiddler 把 mod1.js 請求 delay 200 再測試,此次輸出:

 
require module: mod2
require module: mod1
require module: main
hello mod1
hello mod2
hello main

這是由於 main.js 中 mod1 和 mod2 兩個模塊並行加載,且加載完就執行,因此前兩行輸出順序取決於哪一個 js 先加載完。若是必定要讓 mod2 在 mod1 以後執行,須要在 define 模塊時申明依賴,或者經過 require.config 配置依賴:

 
require.config({
     shim: {
         'mod2' : {
             deps : [ 'mod1' ]
         }
     }
});

嚴重問題!

咱們再回過頭來看 CommonJS Wrapper 會帶來什麼問題。前面說過,AMD 規範中,上面的 main.js 等價於這樣:

 
//main.js
define( function (require, exports, module) {
     //運行至此,mod1.js 和 mod2.js 已經下載完成;
 
     console.log( 'require module: main' );
 
     var mod1 = require( './mod1' ); //這裏才執行 mod1 ?
     mod1.hello();
     var mod2 = require( './mod2' ); //這裏才執行 mod2 ?
     mod2.hello();
 
     return {
         hello: function () {
             console.log( 'hello main' );
         }
     };
});

這種「就近」書寫的依賴,很是容易讓人認爲 main.js 執行到對應 require 語句時才執行 mod1 或 mod2,但這是錯誤的,由於 CommonJS Wrapper 並不會改變 AMD「儘早執行」依賴的本質!

實際上,對於按需執行依賴的加載器,如 SeaJS,上述代碼結果必定是:

 
require module: main
require module: mod1
hello mod1
require module: mod2
hello mod2
hello main

因而,瞭解過 CommonJS 或 CMD 模塊規範的同窗,看到使用 CommonJS Wrapper 方式寫的 AMD 模塊,容易產生理解誤差,從而誤認爲 RequireJS 有 bug。

我以爲「儘早執行」或「按需執行」兩種策略沒有明顯的優劣之分,但 AMD 這種「模仿別人寫法,卻提供不同的特性」這個作法十分愚蠢。這年頭,作本身最重要!

其餘問題

還有一個小問題也順帶提下:默認狀況下,定義 AMD 模塊時經過參數傳入依賴列表,簡單可依賴。而用了 CommonJS Wrapper 以後,RequireJS 須要經過正則從 factory.toString() 中提取依賴,複雜並容易出錯。如 RequireJS 下這段代碼會出錯:

 
define( function (require, exports, module) {
     '/*' ;
     var mod1 = require( 'mod1' ),
         mod2 = require( 'mod2' );
     '*/' ;
 
     mod1.hello();
});
 
//Uncaught Error: Module name "mod1" has not been loaded yet for context: _

固然,這個由於 RequireJS 的正則沒寫好,把正常語句當註釋給過濾了,SeaJS 用的正則處理上述代碼沒問題,同時複雜了許多。

雖然實際項目中很難出現上面這樣的代碼,但若是放棄對腦殘的 CommonJS Wrapper 支持後,再寫 AMD 加載器就更加簡單可靠。例如雨夜帶刀同窗寫的 seed,代碼十分簡潔;構建工具一般基於字符串分析,仍然須要過濾註釋,但能夠採用 uglifyjs 壓縮等取巧的方法。

考慮到不是每一個 AMD Loader 都支持 CommonJS Wrapper,用參數定義依賴也能保證更好的模塊通用性。至於「就近」定義依賴,我一直以爲無關緊要,咱們寫 php 或 python 時,include 和 import 都會放在頂部,這樣看代碼時能一目瞭然地看到全部依賴,修改起來也方便。

相關文章
相關標籤/搜索