JS模塊化開發:使用SeaJs高效構建頁面

一、扯淡部分

 

很久很久以前,也就是剛開始接觸前端的那會兒,腦袋裡壓根沒有什麼架構、重構、性能這些概念,天真地以為前端===好看的頁面,甚至把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 推崇依賴前置。

 

——SeaJs作者玉伯在知乎的回答

 

看代碼理解上面兩點的意思:

 

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 });

 

四、小例子

 

說瞭半天概念應該印象還不深刻,我們就來看一個例子用來演示sea.js的基本用法。首先define傳入的參數是對象和字符串的情況,我先舉一個參數的對象的例子,傳字符串大同小異。來看代碼:

 

1,我先來定義一個模塊m1.js:

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 : 好的,我定義的對象字面量放到接口裡給你瞭,拿去盡管刷~

 

然後……a的值就彈出來瞭。很愉快的一次交易。PS:頁面所調用的模塊就為整個web應用的js入口。本例中js的入口就是m1.js。接下來再來看看如果define的參數是個函數的情況。

 

1,先定義一個模塊m2.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 p1 = $id("p1");

 6     var p2 = $id("p2");

 7     var p3 = $id("p3");//以上是獲取頁面元素的幾隻變量

 8     require('./drag.js').drag(p3);//引入拖拽模塊,執行拖拽模塊接口中的drag方法並傳參

 9     exports.oInput = oInput;

10     oInput.onclick = function(){

11         p1.style.display = "block";

12         require('./scale.js').scale(p1,p2);//引入縮放模塊,執行縮放模塊接口中的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 });

發佈留言