一、扯淡部分
很久很久以前,也就是剛開始接觸前端的那會兒,腦袋裡壓根沒有什麼架構、重構、性能這些概念,天真地以為前端===好看的頁面,甚至把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主:是越懶越好的意思麼?![]()
JS子產品化開發:使用SeaJs高效建構頁面 )。
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" });
factory 為函數時,表示是子產品的構造方法。執行該構造方法,可以得到子產品向外提供的接口。factory 方法在執行時,預設會傳入三個參數:require、exports 和 module:1 define('I am a template. My name is {{name}}.');
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 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,其他子產品都會按需自動引入(如下圖所示),而且每個功能子產品的區分特别清晰,再也不用擔心神馬命名沖突啊、依賴混亂啊之類的,而且團隊小夥伴每人負責一個子產品,隻要放出目前子產品的公開接口并提供簡要的說明文檔(因為标準統一),其他小夥伴們寫的子產品就能非常友善地調用到你寫的子產品,連修改的時候都不用考慮對其他功能的影響,變得更大膽了呢~
檢視完整DEMO請猛戳
寫在最後
其實本文介紹的子產品化和seajs的使用依舊比較淺顯,但基本的子產品化思想已經融入到例子中了。 如果你經曆過前文所述的以前寫js邏輯的各種糾結各種坑爹,不妨嘗試一下将你的代碼子產品化,那将是一種飛一樣的感覺……
本文最後會為大家列出一些相關的資料,想深入了解的小夥伴們可以果斷收走~
SeaJs官網 : http://seajs.org/docs/
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
(後續會繼續補充……)