前端模塊化開發


一、為什么要進行模塊化開發

1.命名沖突

  在實際工作中,相信大家都遇這樣的問題:我自己測試好的代碼和大家合並后怎么起沖突了?明明項目需要引入的包都引進來了怎么還報缺少包?……這些問題總結起來就是命名空間沖突及文件依賴加載順序問題。舉個最簡單的例子來解釋一下命名空間沖突問題,看下面這段代碼:

test.html

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <script src="js/module1.js"></script>
    <script src="js/module2.js"></script>
</head>
<body>

</body>
</html>
<script>
    var module=function(){
        console.log('I am module3');
    };
    module();
</script>

module1.js

/**
 * Created by user on 2016/5/14.
 */
var module=function(){
    cosonle.log('I am module1.js');
}

module2.js

/**
 * Created by user on 2016/5/14.
 */
var module=function(){
    console.log("I am module2.js");
}

當運行test.html時結果輸出:

顯然是因為前兩個JS文件里的函數名與html里面的一致而導致沖突,所以只會執行最后一個module()函數,在團隊合作中你不會知道自己寫的函數或變量等是否會與別人起沖突,為解決此類問題出現了參照於JAVA的命名空間如下:

test.html

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <script src="js/module1.js"></script>
    <script src="js/module2.js"></script>
</head>
<body>

</body>
</html>
<script>
    var module=function(){
        console.log('I am module3');
    };
    module1.fn.Utils.module();
    module2.fn.Utils.module();
    module();
</script>

module1.js

/**
 * Created by user on 2016/5/14.
 */
var module1={};
module1.fn={};
module1.fn.Utils={};
module1.fn.Utils.module=function(){
    console.log("I am module1.js");
}

module2.js

/**
 * Created by user on 2016/5/14.
 */
var module2={};
module2.fn={};
module2.fn.Utils={};
module2.fn.Utils.module=function(){
    console.log("I am module2.js");
}

此時再運行test.html便可以輸入所有的module里的值了

但是,寫那么長的命名空間只為了調用一個方法,有沒有感覺有些啰嗦呢?此處我只是為了盡量還原實際項目開發過程中的問題而起了較長的命名空間名。將命名空間的概念在前端中發揚光大,首推 Yahoo! 的 YUI2 項目。下面是一段真實代碼,來自 Yahoo! 的一個開源項目。

if (org.cometd.Utils.isString(response)) {  
  return org.cometd.JSON.fromJSON(response);  
}  
if (org.cometd.Utils.isArray(response)) {  
  return response;  
}  

作為前端業界的標桿,YUI 團隊下定決心解決這一問題。在 YUI3 項目中,引入了一種新的命名空間機制。

YUI().use('node', function (Y) {  
  // Node 模塊已加載好  
  // 下面可以通過 Y 來調用  
  var foo = Y.one('#foo');  
}); 

YUI3 通過沙箱機制,很好的解決了命名空間過長的問題。然而,也帶來了新問題。

YUI().use('a', 'b', function (Y) {  
  Y.foo();  
  // foo 方法究竟是模塊 a 還是 b 提供的?  
  // 如果模塊 a 和 b 都提供 foo 方法,如何避免沖突?  
});  

暫且先不公布怎么解決此類問題,再看下一個問題。

2.文件依賴

開發最基本的原則就是不要重復,當項目中有多處地方運用同一個功能時,我們就該想辦法把它抽離出來做成util,當需要時直接調用它即可,但是如果你之后的代碼依賴於util.js而你又忘了調用或者調用順序出錯,代碼便報各種錯誤,舉個最簡單的例子,大家都知道Bootstrap依賴jquery,每次引入時都要將jquery放在Bootstrap前面,一兩個類似於這樣的依賴你或許還記得,但如果在龐大的項目中有許多這樣的依賴關系,你還能清晰的記得嗎?當項目越來越復雜,眾多文件之間的依賴經常會讓人抓狂。下面這些問題,我相信每天都在真實地發生着。

1.通用組更新了前端基礎類庫,卻很難推動全站升級。

2.業務組想用某個新的通用組件,但發現無法簡單通過幾行代碼搞定。

3.一個老產品要上新功能,最后評估只能基於老的類庫繼續開發。

4.公司整合業務,某兩個產品線要合並。結果發現前端代碼沖突。

5.……

以上很多問題都是因為文件依賴沒有很好的管理起來。在前端頁面里,大部分腳本的依賴目前依舊是通過人肉的方式保證。當團隊比較小時,這不會有什么問題。當團隊越來越大,公司業務越來越復雜后,依賴問題如果不解決,就會成為大問題。

二、什么是模塊化開發

模塊化開發使代碼耦合度降低,模塊化的意義在於最大化的設計重用,以最少的模塊、零部件,更快速的滿足更多的個性化需求。因為有了模塊,我們就可以更方便地使用別人的代碼,想要什么功能,就加載什么模塊。但總不能隨便寫吧,總得有規范讓大家遵守吧。

1.目前,模塊化開發有:

1.服務器端規范:CommonJs---nodejs使用的規范,

2.瀏覽器端規范:AMD---RequireJS國外相對流行(官網

        CMD--SeaJS國內相對流行(官網

2.SeaJS與RequireJS的對比:

a. 對於依賴的模塊,AMD是提前執行,CMD是延后執行;

b. CMD推崇依賴就近,AMD推崇依賴前置;

c. AMD的API默認是一個當多個用,CMD的API嚴格區分,推崇職責單一。

三、怎么用模塊化開發

直接看下面寫的小型計算機器代碼吧!

test_seajs.html(前提是得去下載sea.js包哦,我是直接用命令npm install seajs下載的。)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Seajs體驗</title>
    <script src="node_modules/seajs/dist/sea.js"></script>
    <script>
        // 在Seajs中模塊的引入需要相對路徑完整寫法,注意不要再用script標簽引入哦,否則用模塊化就沒意義了
        seajs.use('./calculator.js', function(calculator) {
            //calculator其實就是calculator.js中的exports對象,這樣便可以用其方法了
            var ta = document.getElementById('txt_a');
            var tb = document.getElementById('txt_b');
            var tres = document.getElementById('txt_res');
            var btn = document.getElementById('btn');
            var op = document.getElementById('sel_op');

            btn.onclick = function() {
                switch (op.value) {
                    case '+':
                        tres.value = calculator.add(ta.value, tb.value);
                        break;
                    case '-':
                        tres.value = calculator.subtract(ta.value, tb.value);
                        break;
                    case 'x':
                        tres.value = calculator.multiply(ta.value, tb.value);
                        break;
                    case '÷':
                        tres.value = calculator.divide(ta.value, tb.value);
                        break;
                }
            };
        });
    </script>
</head>

<body>
<input type="text" id="txt_a">
<select id="sel_op">
    <option value="+">+</option>
    <option value="-">-</option>
    <option value="x">x</option>
    <option value="÷">÷</option>
</select>
<input type="text" id="txt_b">
<input type="button" id="btn" value=" = ">
<input type="text" id="txt_res">
</body>

</html>
calculator.js文件內容如下:
/**
 * Created by user on 2016/5/14.
 */

// 定義一個模塊,遵循Seajs的寫法
define(function(require, exports, module) {
  // 此處是模塊的私有空間
  // 定義模塊的私有成員
  // 載入convertor.js模塊
  var convertor = require('./convertor.js');

  function add(a, b) {
    return convertor.convertToNumber(a) + convertor.convertToNumber(b);
  }

  function subtract(a, b) {
    return convertor.convertToNumber(a) - convertor.convertToNumber(b);
  }

  function multiply(a, b) {
    return convertor.convertToNumber(a) * convertor.convertToNumber(b);
  }

  function divide(a, b) {
    return convertor.convertToNumber(a) / convertor.convertToNumber(b);
  }
  // 暴露模塊的公共成員
  exports.add = add;
  exports.subtract = subtract;
  exports.multiply = multiply;
  exports.divide = divide;
});
convertor.js內容如下:
/**
 * 轉換模塊,導出成員:convertToNumber
 */
define(function(require, exports, module) {
  // 公開一些轉換邏輯
  exports.convertToNumber = function(input) {
    return parseFloat(input);
  }
});

運行結果:

總結:在test_seajs.html用seajs.use引入calculator.js文件,而在calcultor.js文件中又require了convertor.js文件,這樣就不用關心每個js依賴關系啦,因為在其js內部就已經加載完成了。每引入一個js在其回調函數里執行其js的方法,從而解決了命名沖突問題。

四、seajs暴露接口

細心的同學或許已經發現我在上面的calculator.js中用exports.xx暴露了該JS文件中的方法,如果里面有許多許多的方法,用exports都列出來多麻煩啊,其實還可以用module.exports來暴露其接口。如下:

test-exports.html

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <script src="node_modules/seajs/dist/sea.js"></script>
    <script>
        // 1.當person.js用exports.Person=Person;暴露接口時需要以下方式進行使用其內部的方法
        /*seajs.use('./person.js', function(e) {
            //此時的e為exports對象
            var p=new e.Person();
            p.sayHi();
        });*/

        //2.當person.js用module.exports暴露接口時需要以下方式進行使用其內部的方法
        seajs.use('./person.js', function(Person) {
            //此時function里的參數便直接為Person對象
            var p=new Person();
            p.sayHi();
        });
    </script>
</head>
<body>

</body>
</html>

person.js

/**
 * Created by user on 2016/5/14.
 */

// 定義一個模塊,遵循Seajs的寫法
define(function(require, exports, module) {

     function Person(name, age, gender) {
       this.name = name;
       this.age = age;
       this.gender = gender;
     }
     Person.prototype.sayHi = function() {
       console.log('hi! I\'m a Coder, my name is ' + this.name);
     };

     //exports.Person=Person;
    module.exports=Person;

});

此時問題又來了,如果它倆同時存在以誰為准呢?答案是以module.exports為准,因為exports是module.exports的快捷方式,指向的仍然是原來的地址。看代碼:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
    <script src="node_modules/seajs/dist/sea.js"></script>
    <script>
        seajs.use('./person.js', function(e) {
            console.log(e);
        });
    </script>
</head>
<body>

</body>
</html>

person.js

// 定義一個模塊,遵循Seajs的寫法
define(function(require, exports, module) {
    module.exports={name:'haoxiaoli'};
    exports.name='hxl';
});

結果:

最后,其實還有一個return也可以暴露接口。它們的優先級為:return>module.exports>exports,看案例:

person.js

// 定義一個模塊,遵循Seajs的寫法
define(function(require, exports, module) {
    module.exports={name:'haoxiaoli'};
    exports.name='hxl';
    return {name:'hello world!'};
});

結果:

五、異步加載包

引入JS時難免會遇到需要異步加載文件的時候,此時require.async便可滿足異步加載需求。如下demo

html文件

<script src="node_modules/seajs/dist/sea.js"></script>
    <script>
        // 在Seajs中模塊的引入需要相對路徑完整寫法
        seajs.use('./03-module1.js', function(e) {
            //console.log(e);
        });
</script>

03-module1.js文件

define(function(require,exports,module){
     /*console.log('module1-------start');
     //require必須執行完成后(./module2.js加載完成)才可以拿到返回值
     var module2=require('./03-module2.js');//阻塞代碼執行
     //JS中阻塞現在會造成界面卡頓現象出現
     console.log('module1--------end');*/

    //異步加載便不會出現卡頓現象
    console.log('module1--------start');
    require.async('./03-module2.js',function(module2){
        //等03-module2.js后再做的操作
    });//此處不會阻塞代碼執行
    console.log('module1--------end');
})

六、使用第三方依賴庫

比如當用CMD規范引入jquery時肯定希望它只在該模塊內有效,而不是全局有效。在JQ中有對AMD規范的使用,但由於CMD屬於國內的規范,人家並沒有對其進行適配,所以需要我們手動去改造代碼。在JQ中對AMD規范適配的下面增加如下代碼。

if (typeof define === "function" && !define.amd) {
     // 當前有define函數,並且不是AMD的情況
     // jquery在新版本中如果使用AMD或CMD方式,不會去往全局掛載jquery對象
     define(function() {
         return jQuery.noConflict(true);
     });
 }    

這樣再使用JQ時便做到此模塊內可用了。

define(function(require,exports,module){
    //用JQ做代表第三方庫
    var $=require('./jquery.js');
    $(document.body).css('backgroundColor','red');
});

七、seajs配置

假如你項目中用到許多JS文件,或者引入的JS路徑發生了變化,這樣挨個去文件中修改有點不現實,所以可以把它們集中在某個頁面進行統一管理文件路徑,即config配置文件。如你在某個html文件中寫:

<script src="node_modules/seajs/dist/sea.js"></script>
    <script>
        seajs.config({
            alias:{
                //給引入的包起別名,並放入到配置中
                calc:'./05-calc.js'
            }
        });
        seajs.use('calc');
</script>

每次引入的文件都在此配置后,路徑改變了也只是在這一個文件中修改而已。另外還有map等,在此不再贅述

 
       


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM