JavaScript中的模塊化開發

StephenSlat 7年前發布 | 7K 次閱讀 SeaJS Node.js JavaScript開發

一、為什么會有模塊化

1. 當一個項目開發的越來越復雜的時候,會遇到一些問題,比如:

  • 命名沖突:當項目由團隊進行協作開發的時候,不同開發人員的變量和函數命名可能相同;即使是一個開發,當開發周期比較長的時候,也有可能會忘記之前使用了什么變量,從而導致重復命名,導致命名沖突。

  • 文件依賴:代碼重用時,引入js文件的數目可能少了,或者引入的順序不對,比如使用boostrap的時候,需要引入jQuery,并且jQuery的文件必須要比boostrap的js文件先引入。

2. 當使用模塊化開發的時候可以避免以上的問題,并且讓開發的效率變高,以及方便后期的維護:

  • 提升開發效率:代碼方便重用,別人開發的模塊直接拿過來就可以使用,不需要重復開發法類似的功能。

  • 方便后期維護:代碼方便重用,別人開發的模塊直接拿過來就可以使用,不需要重復開發法類似的功能。

所以總結來說,在生產角度,模塊化開發是一種生產方式,這種方式生產效率高,維護成本低。從軟件開發角度來說,模塊化開發是一種開發模式,寫代碼的一種方式,開發效率高,方便后期維護。

二、模塊化開發的演變過程

1. 全局函數

function add(a , b) {
    return parseFloat(a) + parseFloat(b);
}
function substract(a ,b) {}
function multiply(a ,b) {}
function divide(a ,b) {}

在早期的開發過程中就是將重復的代碼封裝到函數中,再將一系列的函數放到一個文件中,這種情況下全局函數的方式只能認為的認為它們屬于一個模塊,但是程序并不能區分哪些函數是同一個模塊,如果僅僅從代碼的角度來說,這沒有任何模塊的概念。

存在的問題:

  • 污染了全局變量,無法保證不與其他模塊發生變量名沖突。
  • 模塊成員之間看不出直接關系。

2. 對象封裝-命名空間

var calculator = {
  add: function(a, b) {
    return parseFloat(a) + parseFloat(b);
  },
  subtract: function(a, b) {},
  multiply: function(a, b) {},
  divide: function(a, b) {}
};

通過添加命名空間的形式從某種程度上解決了變量命名沖突的問題,但是并不能從根本上解決命名沖突。 不過此時從代碼級別可以明顯區分出哪些函數屬于同一個模塊。

存在的問題:

  • 暴露了所有的模塊成員,內部狀態可以被外部改寫,不安全。
  • 命名空間越來越長。

3. 私有公有成員分離

var calculator = (function () {
    // 這里形成一個單獨的私有的空間
    // 私有成員的作用:
    //   1、將一個成員私有化
    //   2、抽象公共方法(其他成員中會用到的)

// 私有的轉換邏輯
function convert(input){
    return parseInt(input);
}

function add(a, b) {
    return convert(a) + convert(b);
}
function subtract(a, b) {}
function multiply(a, b) {}
function divide(a, b) {}
return {
    add : add,
    subtract : subtract,
    multiply : multiply,
    divide : divide
}

})();</code></pre>

  1. 利用此種方式將函數包裝成一個獨立的作用域,私有空間的變量和函數不會影響到全局作用域。
  2. 以返回值的方式得到模塊的公共成員,公開公有方法,隱藏私有空間內部的屬性、元素,比如注冊方法中可能會記錄日志。
  3. 可以有選擇的對外暴露自身成員。
  4. 從某種意義上來說,解決了變量命名沖突的問題。

4. 模塊的擴展與維護

// 計算模塊
(function (calculator) {
    function convert(input) {
        return parseInt(input);
    }
    calculator.add = function(a, b) {
        return convert(a) + convert(b);
    }
    window.calculator = calculator;
})(window.calculator || {});

// 新增需求 (function (calculator) { calculator.remain = function (a , b) { return a % b; } window.calculator = calculator; })(window.calculator || {});

alert(calculator.remain(4,3));</code></pre>

  1. 利用此種方式,有利于對龐大的模塊的子模塊劃分。
  2. 實現了開閉原則:對新增開發,對修改關閉。對于已有文件盡量不要修改,通過添加新文件的方式添加新功能。

5. 第三方依賴的管理

(function (calculator , $) {
    // 依賴函數的參數,是屬于模塊內部
    // console.log($);
    calculator.remain = function (a , b) {
        return a % b;
    }
    window.calculator = calculator;
})(window.calculator || {} , jQuery);

模塊最好要保證模塊的 職責單一性 ,最好不要與程序的其他部分直接交互,通過向匿名函數注入依賴項的形式,除了保證模塊的獨立性,還使模塊之間的以來關系變得明顯。

對于模塊的依賴通過自執行函數的參數傳入,這樣做可以做到依賴抽象,本例中使用的jQuery,而當要使用zepto的時候,只要更換傳入的參數即可。

原則:高內聚低耦合,模塊內相關性高,模塊間關聯低。

總結:在什么場景下使用模塊化開發

  • 業務復雜
  • 重用邏輯非常多
  • 擴展性要求較高

三、模塊化規范

服務器端規范主要是 CommonJS , node.js 用的就是 CommonJS 規范。

客戶端規范主要有: AMD (異步模塊定義,推崇依賴前置)、 CMD (通用模塊定義,推崇依賴就近)。 AMD 規范的實現主要有 RequireJS , CMD 規范的主要實現有 SeaJS 。 RequireJS 在國外用的比較多, SeaJS 在國內用的比較多,并且 SeaJS 的創始人為阿里的玉伯,所以 SeaJS 在阿里系用的非常廣泛,包括京東等大廠也在用 SeaJS ,我們詳細介紹的也是 SeaJS 。但是 SeaJS 已經停止維護了,因為在ES6中已經有了模塊化的實現,隨著ES6的普及,第三方的模塊化實現將會慢慢的淘汰(但是這個在國內可能還要很多年)。

四、SeaJs

1. SeaJs簡介

  • SeaJS 是一個基于CMD規范實現的模塊化開發解決方案。
  • 作者:Alibaba 玉伯
  • Alibaba 玉伯
  • 特性:
    • 簡單友好的模塊化定義規范。
    • 自然直觀的代碼組織方式。
    </li>
  • 哲學:一切皆模塊
  • </ul>

    2. 使用步驟

    • 引入sea.js庫
    • 定義模塊
      define(function(require, exports, module){ 模塊代碼 });
    • 暴露接口
      • exports
      • module.exports
      </li>
    • 依賴模塊
      require(‘模塊id’)
    • 啟動模塊系統
      seajs.use(‘模塊id’,function( 模塊對象 ){ 業務代碼 });
    • </ul>

      HelloWorld

      • 01-convertor.js
        /**

        • 轉換模塊,到處成員:convertToNumber */ define(function (require, exports, module) { exports.convertToNumber = function (input) {

          return parseFloat(input);
          

          } });</code></pre> </li>

        • 01-calculator.js
          define(function (require, exports, module) {
          // 此處是模塊的私有空間,定義模塊的私有成員
          // 載入01-convertor模塊
          var convertor = require('./01-convertor');
          function add(a, b) {

          return convertor.convertToNumber(a) + convertor.convertToNumber(b);
          

          } exports.add = add; });</code></pre> </li>

        • 01-helloworld.html
          <!DOCTYPE html>
          <html lang="en">
          <head>
          <meta charset="UTF-8">
          <title>Title</title>
          <script src="node_modules/seajs/dist/sea.js"></script>
          <script>

          seajs.use('./01-calculator.js', function (calculator) {
              alert(calculator.add(1,2));
          });
          

          </script> </head> <body> </body> </html></code></pre> </li> </ul>

          3. 定義模塊define

          • 先有規范,后有實現

          • 在CMD規范中, 一個模塊就是一個js文件

          • define是一個全局函數,用來定義模塊

          • define(factory)

            • 對象{}這種方式,外部會直接獲取到該對象
            • 字符串''同上
            • 函數function( require, exports, module ){ // 模塊代碼 }
              • 注意: 為了減少出錯,定義函數的時候直接把這三個參數寫上

          factory為對象、字符串時,表示模塊的接口就是該對象、字符串。比如可以如下定義一個 JSON 數據模塊:

          define({ "foo": "bar" });

          也可以通過字符串定義模板模塊:

          define('I am a template. My name is {{name}}.');

          factory為函數時,表示是模塊的構造方法。執行該構造方法,可以得到模塊向外提供的接口。factory方法在執行時,默認會傳入三個參數:require、exports和 module:

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

          define define(id?, deps?, factory)

          define也可以接受兩個以上參數。字符串 id表示模塊標識,數組 deps是模塊依賴。比如:

          define('hello', ['jquery'], function(require, exports, module) { // 模塊代碼});

          id和 deps參數可以省略。省略時,可以通過構建工具自動生成。

          注意:帶 id和 deps參數的 define用法不屬于 CMD 規范,而屬于 Modules/Transport 規范。

          define.cmd Object

          一個空對象,可用來判定當前頁面是否有 CMD 模塊加載器:

          if (typeof define === "function" && define.cmd) { 
            // 有 Sea.js 等 CMD 模塊加載器存在
          }

          require Function

          require是 factory函數的第一個參數。

          require require(id)

          require是一個方法,接受 模塊標識 作為唯一參數,用來獲取其他模塊提供的接口。

          define(function(require, exports) { 
          // 獲取模塊 a 的接口 
          var a = require('./a'); 
          // 調用模塊 a 的方法 
          a.doSomething();
          });

          注意:在開發時,require的書寫需要遵循一些 簡單約定 。

          require.async require.async(id, callback?)

          require.async方法用來在模塊內部異步加載模塊,并在加載完成后執行指定回調。callback參數可選。

          define(function(require, exports, module) {
          // 異步加載一個模塊,在加載完成時,執行回調
          require.async('./b', function(b) {
            b.doSomething();
          });
          
          // 異步加載多個模塊,在加載完成時,執行回調
          require.async(['./c', './d'], function(c, d) {
            c.doSomething();
            d.doSomething();
          });
          });

          注意:require是同步往下執行,require.async則是異步回調執行。require.async 一般用來加載可延遲異步加載的模塊。

          require.resolve require.resolve(id)

          使用模塊系統內部的路徑解析機制來解析并返回模塊路徑。該函數不會加載模塊,只返回解析后的絕對路徑。

          define(function(require, exports) {
          console.log(require.resolve('./b'));
          // ==> http://example.com/path/to/b.js
          });

          這可以用來獲取模塊路徑,一般用在插件環境或需動態拼接模塊路徑的場景下。

          4. exports 和 module.exports

          • 功能:通過給 exports或module.exports動態的掛載變量、函數或對象,外部會獲取到該接口
          • exports 等價于 module.exports
          • 可以通過多次給exports 掛載屬性向外暴露
          • 不能直接給 exports 賦值
          • 如果想暴露單個變量、函數或對象可以通過直接給module.exports 賦值 即可

          5. exports Object

          exports是一個對象,用來向外提供模塊接口。

          define(function(require, exports) {
          // 對外提供 foo 屬性
          exports.foo = 'bar';
          
          // 對外提供 doSomething 方法
          exports.doSomething = function() {};
          });

          除了給 exports對象增加成員,還可以使用 return直接向外提供接口。

          define(function(require) {
          // 通過 return 直接提供接口
          return {
            foo: 'bar',
            doSomething: function() {}
          };
          });

          如果 return語句是模塊中的唯一代碼,還可簡化為:

          define({
          foo: 'bar',
          doSomething: function() {}
          });

          上面這種格式特別適合定義 JSONP 模塊。

          特別注意:下面這種寫法是錯誤的!

          define(function(require, exports) {
          // 錯誤用法!!!
          exports = {
            foo: 'bar',
            doSomething: function() {}
          };
          });

          正確的寫法是用 return或者給 module.exports賦值:

          define(function(require, exports, module) {
          // 正確寫法
          module.exports = {
            foo: 'bar',
            doSomething: function() {}
          };
          });

          提示:exports 僅僅是 module.exports 的一個引用。在 factory 內部給 exports 重新賦值時,并不會改變 module.exports 的值。因此給 exports 賦值是無效的,不能用來更改模塊接口。

          6. module Object

          module是一個對象,上面存儲了與當前模塊相關聯的一些屬性和方法。

          6.1 module.id String

          模塊的唯一標識。

          define('id', [], function(require, exports, module) { 
          // 模塊代碼
          });

          上面代碼中,define的第一個參數就是模塊標識。

          6.2 module.uri String

          根據模塊系統的路徑解析規則得到的模塊絕對路徑。

          define(function(require, exports, module) {
          console.log(module.uri); 
          // ==> http://example.com/path/to/this/file.js
          });

          一般情況下(沒有在 define 中手寫 id 參數時),module.id 的值就是 module.uri,兩者完全相同。

          6.3 module.dependencies Array

          dependencies是一個數組,表示當前模塊的依賴。

          6.4 module. exports Object

          當前模塊對外提供的接口。

          傳給 factory 構造方法的 exports 參數是 module.exports 對象的一個引用。只通過 exports 參數來提供接口,有時無法滿足開發者的所有需求。 比如當模塊的接口是某個類的實例時,需要通過 module.exports 來實現:

          define(function(require, exports, module) {
          // exports 是 module.exports 的一個引用
          console.log(module.exports === exports); // true
          
          // 重新給 module.exports 賦值
          module.exports = new SomeClass();
          
          // exports 不再等于 module.exports
          console.log(module.exports === exports); // false
          });

          注意:對 module.exports 的賦值需要同步執行,不能放在回調函數里。下面這樣是不行的:

          // x.js
          define(function(require, exports, module) {
          // 錯誤用法
          setTimeout(function() {
            module.exports = { a: "hello" };
          }, 0);
          });

          在 y.js 里有調用到上面的 x.js:

          // y.js
          define(function(require, exports, module) {
          var x = require('./x');
          
          // 無法立刻得到模塊 x 的屬性 a
          console.log(x.a); // undefined
          });

          7. 小結

          這就是 CMD 模塊定義規范的所有內容。經常使用的 API 只有 define, require, require.async, exports, module.exports 這五個。其他 API 有個印象就好,在需要時再來查文檔,不用刻意去記。

          與 RequireJS 的 AMD 規范相比,CMD 規范盡量保持簡單,并與 CommonJS 和 Node.js 的 Modules 規范保持了很大的兼容性。通過 CMD 規范書寫的模塊,可以很容易在 Node.js 中運行

          五、CommonJs

          其實學完了SeaJs規范之后,CommonJs規范也就差不多會用了,兩者非常的相似,并且在node.js中使用起來更簡單。

          在node.js中直接使用require引包,直接使用exports和module.exports暴露公開成員,并且npm基于CommonJs實現了自動加載和安裝依賴。

          同樣的CommonJs讓node.js變得:1、增加 內聚 性,有助分工協作,2、方便 重構 ,3、提高代碼 質量

          node.js中的實現為:

          (function(exports,require,module,__filename,__dirname){
          return module.exports;
          });

          require

          • 加載模塊后會 緩存 ,多次加載后得到同一對象 require('http')

          • 查看模塊緩存 console.log(require.cache);

          • 查詢模塊絕對路徑 require.resolve('./test.js');

          • 查看單個的模塊緩存 require.cache[require.resolve('./test.js')]

          • 刪除模塊緩存 delete require.cache[require.resolve('./test.js')];

           

           

          來自:http://www.jianshu.com/p/3832c00a44a7

           

 本文由用戶 StephenSlat 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!