一、扯淡部分
很久很久以前,也就是剛開始接觸前端的那會兒,腦袋里壓根沒有什么架構、重構、性能這些概念,天真地以為前端===好看的頁面,甚至把js都划分到除了用來寫一些美美的特效別無它用的陰暗角落里,就更別說會知道js還有面向對象,設計模式,MVC,MVVM,模塊化,構建工具等等這些高大上的概念了。現在想想還真是Too young too naive。前兩天某大神在群里分享他招聘前端的心得的時候就說,就是那些以為能寫兩個頁面就可以自稱前端的人拉低了行業水平。這樣看來前兩年我還真的扯了不少后腿呢
……
后來干這行干得稍久一些,發現水簡直深深深深千尺,而且周圍遍布沼澤。即便爬到岸上,迎接你的又是大大小小各種坑。坑爹的IE6,坑爹的兼容,坑爹的瀏覽器特性……總之,任何一個前端都有被這些大大小小的坑虐到體無完膚的慘痛經歷。但(我覺得這個但字是點睛之筆),生活在繼續,時代在發展,競爭依然殘酷,你不往前走就只能在這片沼澤里不斷下沉,最后掙扎的結果也不過是冒出水面兩個泡泡然后……爆掉。
在經歷了會寫頁面,會用js寫效果的階段后,大多數人都已經慢慢地能夠滿足產品提出的各種奇葩的功能需求,但僅僅是滿足了需求,而沒有考慮性能、團隊協作、開發消耗的各種成本等等這些問題。有時候甚至寫好的js再回頭去看時也會讓自己一頭霧水:各種方法,各種邏輯雜亂無章地糾纏在一起,根本理不清誰調用了誰,誰為誰定義,誰又是誰的誰!更可怕的是當項目被其他小伙伴接管,每修改一處上線前都擔驚受怕:修改這里到底TM對不對啊?
還好前端領域開路者們用他們的智慧朝我們艱難跋涉的水坑里扔了幾塊石頭:嘗試讓你的代碼模塊化吧~
二、js模塊化
為毛要嘗試模塊化開發?
如今的網頁越來越像桌面程序,網頁上加載的javascript也越來越復雜,coder們不得不開始用軟件工程的思維去管理自己的代碼。Javascript模塊化編程,已經成為一個非常迫切的需求。理想情況下,開發者只需要實現核心的業務邏輯,其他都可以加載別人已經寫好的模塊。但是,Javascript不是一種模塊化編程語言,它不支持"類"(class),更遑論"模塊"(module)了。(正在制定中的ECMAScript標准第六版將正式支持"類"和"模塊",但還需要很長時間才能投入實用。)
——來自阮一峰的博文:《Javascript模塊化編程(一):模塊的寫法》
上面其實已經把模塊化的意義和目的已經講述的很清楚了,所以就拿來主義,節省腦細胞留給下面的內容
模塊化的概念出來以后,新的問題又來了:需不需要一個統一的模塊化標准?我們來試想一下如果沒有標准的情況:A以自己的標准寫了模塊Module1,然后B又以自己的標准寫了Module2,恩,在他們看來,這的確是模塊,但當Module1想調用模塊Module2的時候該怎么調用呢?它們之間火星人與地球人交流,沒有同聲傳譯看起來依舊是毫無頭緒。於是模塊化規范便又成了一個問題。
2009年美國的一位大神發明了node.js (具體內容自行腦補,本文不作討論),用來開發服務器端的js。我們都知道,傳統的服務器端開發語言如PHP、JAVA等都必須進行模塊化開發,JS想占據人家的地盤也不例外,模塊化是必須的,於是commomJS模塊化開發規范誕生了,但這貨只是服務器端JS模塊化開發的標准,客戶端又沒用。
—有童鞋:bla了那么多,這跟我在客戶端進行js模塊化開發有毛關系啊?
—PO主:表着急,了解了這玩意兒的前世今生,用起來才能得心應手~
服務器端JS模塊化規范有了,JSer們自然想到了能把commonJS規范拿到客戶端就好啦,而且最好兩者能夠兼容,一個模塊不用修改,在服務器和瀏覽器都可以運行
。爽爆~但(這個但字又是一個點睛之筆),由於一個重大的局限,使得CommonJS規范不適用於瀏覽器環境。服務器端獲取資源的方式是本地讀取,而客戶端拿資源的方式是通過Http來獲取,這是一個大問題,因為模塊都放在服務器端,瀏覽器等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於"假死"狀態。因此,瀏覽器端的模塊,不能采用"同步加載"(synchronous),只能采用"異步加載"(asynchronous),於是誕生了AMD和CMD。
—有童鞋:核心內容終於TMD來了,就是AMD和CMD這二貨
。
—PO主:……
三、AMD和CMD
AMD (Asynchronous Module Definition) : RequireJS 在推廣過程中對模塊定義的規范化產出。
AMD用白話文講就是 異步模塊定義,對於 JSer 來說,異步是再也熟悉不過的詞了,所有的模塊將被異步加載,模塊加載不影響后面語句運行。所有依賴某些模塊的語句均放置在回調函數中,等到依賴的模塊加載完成之后,這個回調函數才會運行。
主要有兩個Javascript庫實現了AMD規范:require.js和curl.js。
(本文主要分享的是SeaJs模塊化構建方式,關於requireJs構建方式請移步至:《Javascript模塊化編程(一):模塊的寫法》)
CMD (Common Module Definition) : SeaJS 在推廣過程中對模塊定義的規范化產出。
實現了CMD規范的主要的Javascript庫:Sea.js。
CMD翻譯來就是 通用模塊定義,與AMD的相同點:
1. 這些規范的目的都是為了 JavaScript 的模塊化開發,特別是在瀏覽器端的。
2. 目前這些規范的實現都能達成瀏覽器端模塊化開發的目的。
當然與AMD也有有兩點區別:
1. 對於依賴的模塊,AMD 是提前執行,CMD 是延遲執行。不過 RequireJS 從 2.0 開始,也改成可以延遲執行(根據寫法不同,處理方式不同)。CMD 推崇 as lazy as possible(PO主:是越懶越好的意思么?
)。
2. CMD 推崇依賴就近,AMD 推崇依賴前置。
看代碼理解上面兩點的意思:
AMD模塊的定義方法
// AMD 默認推薦的是 define(['./a', './b'], function(a, b) { // 依賴必須一開始就寫好,即依賴前置,執行完引入的模塊后才開始執行回調函數 a.doSomething() // 此處略去 100 行 b.doSomething() ... })
CMD模塊的定義方法:
// CMD define(function(require, exports, module) { var a = require('./a') a.doSomething() // 此處略去 100 行 var b = require('./b') // 依賴可以就近書寫,即依賴就近,什么時候用到什么時候才引入 b.doSomething() // ... })
好了,看過兩個例子,對於之前沒有接觸過模塊化開發的童鞋來說依舊是一頭霧水:那個define是什么東東啊?還有那個require,exports,module,都是干什么的?
表捉急,我們一步一步來。
在 CMD 規范中,一個模塊就是一個文件。代碼的書寫格式如下:
define(factory);
來看github上CMD模塊定義規范上的解釋:
define 是一個全局函數,用來定義模塊。
define 接受 factory 參數,
factory可以是一個函數,也可以是一個對象或字符串。factory 為對象、字符串時,表示模塊的接口就是該對象、字符串。比如可以如下定義一個 JSON 數據模塊:
1 define({ "foo": "bar" });也可以通過字符串定義模板模塊:
1 define('I am a template. My name is {{name}}.');factory 為函數時,表示是模塊的構造方法。執行該構造方法,可以得到模塊向外提供的接口。factory 方法在執行時,默認會傳入三個參數:require、exports 和 module:
1 define(function(require, exports, module) { 2 // 模塊代碼 3 });
define({a:"這里是屬性a的值"});
define傳入的是一個對象字面量。現在這個東東就可以叫做一個模塊了~我想在頁面一加載的時候就把a的值alert出來,怎么做呢?繼續往下看。
2,在頁面上引入這個模塊:
1 seajs.use('./m1.js',function(ex){ 2 alert(ex.a); 3 }); //彈出“這里是屬性a的值”
翻譯得直白一點,大意就是:
seajs : Hi~m1.js,我現在要用(use)你了,然后把你的公開接口(exports)存到我回調函數的參數(ex)里,你把想給我調用的東東放到這個參數里吧~么么噠
m1.js : 好的,我定義的對象字面量放到接口里給你了,拿去盡管刷~
1 define(function(require,exports,module){ 2 var var1 = "這是要alert出來的值";//私有變量,沒有通過接口返出去的其他模塊不能訪問 3 function alerts(){ 4 alert(var1); 5 } 6 exports.alerts = alerts;//將需要公開的方法存入exports接口中 7 });
2,在頁面上引入這個模塊並執行模塊m2.js公開的方法:
1 seajs.use('./m2.js',function(ex){ 2 ex.alerts();//ex中存的有m2.js中的公開對象 3 }); //彈出“這是要alert出來的值”
到這里可以簡單地說一下factory方法的三個形參的意義了(個人理解):
require : 提供了引入機制,提供了一種方式來建立依賴,和C中的include和java中的import類似;
exports : 提供了導出機制,提供了私有和共有分離,未使用exports語句導出的變量或者函數,其他模塊即使引用此模塊也不能使用;
module : 提供了模塊信息描述。
是不是思路賤賤清晰了呢?剛才我們的例子中只是從頁面調用模塊的用法,模塊之間互相調用還沒有體現,SO,接下來就以m1.js和m2.js兩個模塊作為例子來嘗試一下 模塊之間互相調用。
1,首先m1.js模塊不變:
1 define({a:"這里是屬性a的值"});
2,m2.js模塊要依賴(require)m1.js:
1 define(function(require,exports,module){ 2 var var1 = "這是要alert出來的值";//私有變量,沒有通過接口返出去的其他模塊不能訪問 3 var var2 = require('./m1.js').a;//這里就是m2.js模塊調用m1.js的方式:var2的值等於當前模塊所依賴的m1.js對外接口中屬性a的值 4 function alerts(){ 5 alert(var2); 6 } 7 exports.alerts = alerts;//將需要公開的方法存入exports接口中 8 });
3,頁面上引入m2.js模塊(同上一個例子),結果就會把a的屬性值給alert出來~
五、實例:模塊化的拖拽個窗口縮放
當然,上面幾個例子是簡單到不能再簡單的例子,估計親們也已經看出來一些道道,但個人感覺還是沒能體現出模塊化開發的優勢。那下面就來看一個實例:模塊化的拖拽個窗口縮放。先看一下效果圖:

PS:效果圖中的紅色區域要先定縮放的范圍,即寬高0px-寬高500px。要寫這樣一個需求的例子,按照之前的編程習慣你會怎么寫?反正在之前,我是會把所有的功能寫到一個js文件里,效果出來就行,隨你們怎么胡攪蠻纏
。而自從認識了模塊化開發,內心不止一次告訴自己,拿到需求bigger一定要高,一定要高(雖然require.js和sea.js這兩個東東在圈內多多少少還是有些爭議)……
廢話少說,首先來分析一下需要划分多少個模塊吧:
1,一開始就要有個入口模塊的吧?恩,必須的!入口模塊Get√~
2,既然是拖拽,要有個拖拽模塊吧?恩,必須的!拖拽模塊Get√~
3,既然要縮放,要有個縮放模塊吧?恩,必須的!縮放模塊Get√~
4,既然限定縮放范圍<=500px,那還要有個限定縮放范圍的模塊吧?恩,這個可以有,但為了以后調整范圍數值方便,還是單列個模塊吧。限定縮放范圍模塊Get√~
到這里我們就把本需求划分成了四個模塊:
· 入口模塊:main.js
· 拖拽模塊:drag.js
· 縮放模塊:scale.js
· 限定縮放范圍模塊:range.js
1 <script> 2 seajs.use('./js/main.js');//沒有callback函數表明引入后直接執行入口模塊 3 </script>
接下來看看入口模塊(main.js)里都應該有些神馬東東吧:
1 //入口模塊 2 define(function(require,exports,module){ 3 var $id = function(_id){return document.getElementById(_id);} 4 var oInput = $id("button1"); 5 var div1 = $id("div1"); 6 var div2 = $id("div2"); 7 var div3 = $id("div3");//以上是獲取頁面元素的幾只變量 8 require('./drag.js').drag(div3);//引入拖拽模塊,執行拖拽模塊接口中的drag方法並傳參 9 exports.oInput = oInput; 10 oInput.onclick = function(){ 11 div1.style.display = "block"; 12 require('./scale.js').scale(div1,div2);//引入縮放模塊,執行縮放模塊接口中的scale方法並傳參 13 } 14 });
恩,還真是全面呢
,把拖拽模塊和縮放模塊都引進來了。看看拖拽模塊(drag.js)吧~
1 //拖拽模塊 2 define(function(require,exports,module){ 3 //這個方法就是實現拖拽的方法,不用詳述了吧? 4 function drag(obj){ 5 var disX = 0; 6 var disY = 0; 7 obj.onmousedown = function(e){ 8 var e = e || window.event; 9 disX = e.clientX - obj.offsetLeft; 10 disY = e.clientY - obj.offsetTop; 11 document.onmousemove = function(e){ 12 var e = e || window.event; 13 var l = require('./range.js').range(e.clientX - disX, document.documentElement.clientWidth - obj.offsetWidth,0); 14 var t = require('./range.js').range(e.clientY - disY, document.documentElement.clientHeight - obj.offsetHeight,0); 15 obj.style.left = l + "px"; 16 obj.style.top = t + "px"; 17 } 18 document.onmouseup = function(){ 19 document.onmousemove = null; 20 document.onmouseup = null; 21 } 22 } 23 } 24 exports.drag = drag;//返回拖拽模塊中想要被公開的對象,也就是在本模塊中定義的drag方法。注意有參數~ 25 });
接下來是縮放模塊(scale.js)。縮放模塊還需要調用 限定縮放范圍模塊 (range.js) 的哦~這點不要搞忘了。
1 //縮放模塊 2 define(function(require,exports,module){ 3 //這個方法就是obj2控制obj1改變大小的方法,也不再詳述啦~ 4 function scale(obj1,obj2){ 5 var disX = 0; 6 var disY = 0; 7 var disW = 0; 8 var disH = 0; 9 obj2.onmousedown = function(e){ 10 var e = e || window.event; 11 disX = e.clientX; 12 disY = e.clientY; 13 disW = obj1.offsetWidth; 14 disH = obj1.offsetHeight; 15 document.onmousemove = function(e){ 16 var e = e || window.event; 17 var w = require('./range.js').range(e.clientX - disX + disW,500,100);//看這里看這里,引入了限定范圍的range.js模塊~ 18 var h = require('./range.js').range(e.clientY - disY + disH,500,100); 19 obj1.style.width = w + "px"; 20 obj1.style.height = h + "px"; 21 } 22 document.onmouseup = function(){ 23 document.onmousemove = null; 24 document.onmouseup = null; 25 } 26 } 27 } 28 exports.scale = scale;//將需要公開的對象存入模塊接口中,以便其他模塊調用~ 29 });
最后就是限定范圍的模塊(range.js)了。
1 //限定拖拽的范圍模塊 2 define(function(require,exports,module){ 3 function range(inum,imax,imin){ 4 if(inum > imax){ 5 return imax; 6 }else if(inum < imin){ 7 return imin; 8 }else{ 9 return inum; 10 } 11 } 12 exports.range = range; 13 });
這就是模塊化,雖然在這個實例中我們用到了4個js,但在頁面上我們只引入了一個入口模塊main.js,其他模塊都會按需自動引入(如下圖所示),而且每個功能模塊的區分特別清晰,再也不用擔心神馬命名沖突啊、依賴混亂啊之類的,而且團隊小伙伴每人負責一個模塊,只要放出當前模塊的公開接口並提供簡要的說明文檔(因為標准統一),其他小伙伴們寫的模塊就能非常方便地調用到你寫的模塊,連修改的時候都不用考慮對其他功能的影響,變得更大膽了呢~

本文最后會為大家列出一些相關的資料,想深入了解的小伙伴們可以果斷收走~
CMD 模塊定義規范:https://github.com/seajs/seajs/issues/242
玉伯:AMD和CMD的區別:http://www.zhihu.com/question/20351507/answer/14859415
AMD and CMD are dead之js模塊化黑魔法 : http://www.cnblogs.com/iamzhanglei/p/3790346.html
(后續會繼續補充……)
