【轉載】前端模塊化開發
1、爲何要進行模塊化開發
1.命名衝突
在實際工做中,相信你們都遇這樣的問題:我本身測試好的代碼和你們合併後怎麼起衝突了?明明項目須要引入的包都引進來了怎麼還報缺乏包?……這些問題總結起來就是命名空間衝突及文件依賴加載順序問題。舉個最簡單的例子來解釋一下命名空間衝突問題,看下面這段代碼:css
test.htmlhtml
<!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.jsnode
/** * Created by user on 2016/5/14. */ var module=function(){ console.log("I am module2.js"); }
當運行test.html時結果輸出:jquery
顯然是由於前兩個JS文件裏的函數名與html裏面的一致而致使衝突,因此只會執行最後一個module()函數,在團隊合做中你不會知道本身寫的函數或變量等是否會與別人起衝突,爲解決此類問題出現了參照於JAVA的命名空間以下:git
test.htmlgithub
<!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.jsnpm
/** * 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.jsapi
/** * 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.……
以上不少問題都是由於文件依賴沒有很好的管理起來。在前端頁面裏,大部分腳本的依賴目前依舊是經過人肉的方式保證。當團隊比較小時,這不會有什麼問題。當團隊愈來愈大,公司業務愈來愈複雜後,依賴問題若是不解決,就會成爲大問題。
2、什麼是模塊化開發
模塊化開發使代碼耦合度下降,模塊化的意義在於最大化的設計重用,以最少的模塊、零部件,更快速的知足更多的個性化需求。由於有了模塊,咱們就能夠更方便地使用別人的代碼,想要什麼功能,就加載什麼模塊。但總不能隨便寫吧,總得有規範讓你們遵照吧。
1.目前,模塊化開發有:
1.服務器端規範:CommonJs---nodejs使用的規範,
2.瀏覽器端規範:AMD---RequireJS國外相對流行(官網)
2.SeaJS與RequireJS的對比:
a. 對於依賴的模塊,AMD是提早執行,CMD是延後執行;
b. CMD推崇依賴就近,AMD推崇依賴前置;
c. AMD的API默認是一個當多個用,CMD的API嚴格區分,推崇職責單一。
3、怎麼用模塊化開發
直接看下面寫的小型計算機器代碼吧!
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的方法,從而解決了命名衝突問題。
4、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!'}; });
結果:
5、異步加載包
引入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'); })
6、使用第三方依賴庫
好比當用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'); });
7、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等,在此再也不贅述