一、扯淡部分
很久很久以前,也就是剛開(kāi)始接觸前端的那會(huì)兒,腦袋里壓根沒(méi)有什么架構(gòu)、重構(gòu)、性能這些概念,天真地以為前端===好看的頁(yè)面,甚至把js都劃分到除了用來(lái)寫(xiě)一些美美的特效別無(wú)它用的陰暗角落里,就更別說(shuō)會(huì)知道js還有面向?qū)ο?,設(shè)計(jì)模式,MVC,MVVM,模塊化,構(gòu)建工具等等這些高大上的概念了?,F(xiàn)在想想還真是Too young too naive。前兩天某大神在群里分享他招聘前端的心得的時(shí)候就說(shuō),就是那些以為能寫(xiě)兩個(gè)頁(yè)面就可以自稱前端的人拉低了行業(yè)水平。這樣看來(lái)前兩年我還真的扯了不少后腿呢
……后來(lái)干這行干得稍久一些,發(fā)現(xiàn)水簡(jiǎn)直深深深深千尺,而且周圍遍布沼澤。即便爬到岸上,迎接你的又是大大小小各種坑??拥腎E6,坑爹的兼容,坑爹的瀏覽器特性……總之,任何一個(gè)前端都有被這些大大小小的坑虐到體無(wú)完膚的慘痛經(jīng)歷。但(我覺(jué)得這個(gè)但字是點(diǎn)睛之筆),生活在繼續(xù),時(shí)代在發(fā)展,競(jìng)爭(zhēng)依然殘酷,你不往前走就只能在這片沼澤里不斷下沉,最后掙扎的結(jié)果也不過(guò)是冒出水面兩個(gè)泡泡然后……爆掉。
在經(jīng)歷了會(huì)寫(xiě)頁(yè)面,會(huì)用js寫(xiě)效果的階段后,大多數(shù)人都已經(jīng)慢慢地能夠滿足產(chǎn)品提出的各種奇葩的功能需求,但僅僅是滿足了需求,而沒(méi)有考慮性能、團(tuán)隊(duì)協(xié)作、開(kāi)發(fā)消耗的各種成本等等這些問(wèn)題。有時(shí)候甚至寫(xiě)好的js再回頭去看時(shí)也會(huì)讓自己一頭霧水:各種方法,各種邏輯雜亂無(wú)章地糾纏在一起,根本理不清誰(shuí)調(diào)用了誰(shuí),誰(shuí)為誰(shuí)定義,誰(shuí)又是誰(shuí)的誰(shuí)!更可怕的是當(dāng)項(xiàng)目被其他小伙伴接管,每修改一處上線前都擔(dān)驚受怕:修改這里到底TM對(duì)不對(duì)???
還好前端領(lǐng)域開(kāi)路者們用他們的智慧朝我們艱難跋涉的水坑里扔了幾塊石頭:嘗試讓你的代碼模塊化吧~
二、js模塊化
為毛要嘗試模塊化開(kāi)發(fā)?
如今的網(wǎng)頁(yè)越來(lái)越像桌面程序,網(wǎng)頁(yè)上加載的javascript也越來(lái)越復(fù)雜,coder們不得不開(kāi)始用軟件工程的思維去管理自己的代碼。Javascript模塊化編程,已經(jīng)成為一個(gè)非常迫切的需求。理想情況下,開(kāi)發(fā)者只需要實(shí)現(xiàn)核心的業(yè)務(wù)邏輯,其他都可以加載別人已經(jīng)寫(xiě)好的模塊。但是,Javascript不是一種模塊化編程語(yǔ)言,它不支持"類"(class),更遑論"模塊"(module)了。(正在制定中的 ECMAScript標(biāo)準(zhǔn) 第六版將正式支持"類"和"模塊",但還需要很長(zhǎng)時(shí)間才能投入實(shí)用。)
——來(lái)自阮一峰的博文:《 Javascript模塊化編程(一):模塊的寫(xiě)法 》
上面其實(shí)已經(jīng)把 模塊化的意義和目的 已經(jīng)講述的很清楚了,所以就拿來(lái)主義,節(jié)省腦細(xì)胞留給下面的內(nèi)容
模塊化的概念出來(lái)以后,新的問(wèn)題又來(lái)了:需不需要一個(gè)統(tǒng)一的模塊化標(biāo)準(zhǔn)?我們來(lái)試想一下如果沒(méi)有標(biāo)準(zhǔn)的情況:A以自己的標(biāo)準(zhǔn)寫(xiě)了模塊Module1,然后B又以自己的標(biāo)準(zhǔn)寫(xiě)了Module2,恩,在他們看來(lái),這的確是模塊,但當(dāng)Module1想調(diào)用模塊Module2的時(shí)候該怎么調(diào)用呢?它們之間火星人與地球人交流,沒(méi)有同聲傳譯看起來(lái)依舊是毫無(wú)頭緒。于是模塊化規(guī)范便又成了一個(gè)問(wèn)題。
2009年美國(guó)的一位大神發(fā)明了 node.js (具體內(nèi)容自行腦補(bǔ),本文不作討論),用來(lái)開(kāi)發(fā)服務(wù)器端的js。我們都知道,傳統(tǒng)的服務(wù)器端開(kāi)發(fā)語(yǔ)言如PHP、JAVA等都必須進(jìn)行模塊化開(kāi)發(fā),JS想占據(jù)人家的地盤(pán)也不例外,模塊化是必須的,于是 commomJS 模塊化開(kāi)發(fā)規(guī)范誕生了,但這貨只是服務(wù)器端JS模塊化開(kāi)發(fā)的標(biāo)準(zhǔn),客戶端又沒(méi)用。
—有童鞋:bla了那么多,這跟我在客戶端進(jìn)行js模塊化開(kāi)發(fā)有毛關(guān)系???
—PO主:表著急,了解了這玩意兒的前世今生,用起來(lái)才能得心應(yīng)手~
服務(wù)器端JS模塊化規(guī)范有了,JSer們自然想到了能把commonJS規(guī)范拿到客戶端就好啦,而且最好兩者能夠兼容,一個(gè)模塊不用修改,在服務(wù)器和瀏覽器都可以運(yùn)行
。爽爆~但(這個(gè)但字又是一個(gè)點(diǎn)睛之筆),由于一個(gè)重大的局限,使得CommonJS規(guī)范不適用于瀏覽器環(huán)境。服務(wù)器端獲取資源的方式是本地讀取,而客戶端拿資源的方式是通過(guò)Http來(lái)獲取,這是一個(gè)大問(wèn)題,因?yàn)槟K都放在服務(wù)器端,瀏覽器等待時(shí)間取決于網(wǎng)速的快慢,可能要等很長(zhǎng)時(shí)間,瀏覽器處于"假死"狀態(tài)。因此,瀏覽器端的模塊,不能采用"同步加載"(synchronous),只能采用"異步加載"(asynchronous),于是誕生了AMD和CMD。—有童鞋:核心內(nèi)容終于TMD來(lái)了,就是AMD和CMD這二貨
。—PO主:……
三、AMD和CMD
AMD (Asynchronous Module Definition) : RequireJS 在推廣過(guò)程中對(duì)模塊定義的規(guī)范化產(chǎn)出。
AMD用白話文講就是 異步模塊定義 ,對(duì)于 JSer 來(lái)說(shuō),異步是再也熟悉不過(guò)的詞了,所有的模塊將被異步加載,模塊加載不影響后面語(yǔ)句運(yùn)行。所有依賴某些模塊的語(yǔ)句均放置在回調(diào)函數(shù)中,等到依賴的模塊加載完成之后,這個(gè)回調(diào)函數(shù)才會(huì)運(yùn)行。
主要有兩個(gè)Javascript庫(kù)實(shí)現(xiàn)了AMD規(guī)范: require.js 和 curl.js 。
(本文主要分享的是SeaJs模塊化構(gòu)建方式,關(guān)于requireJs構(gòu)建方式請(qǐng)移步至:《 Javascript模塊化編程(一):模塊的寫(xiě)法 》)
CMD (Common Module Definition) : SeaJS 在推廣過(guò)程中對(duì)模塊定義的規(guī)范化產(chǎn)出。
實(shí)現(xiàn)了CMD規(guī)范的主要的Javascript庫(kù): Sea.js 。
CMD翻譯來(lái)就是 通用模塊定義 ,與AMD的相同點(diǎn):
1. 這些規(guī)范的目的都是為了 JavaScript 的模塊化開(kāi)發(fā),特別是在瀏覽器端的。
2. 目前這些規(guī)范的實(shí)現(xiàn)都能達(dá)成 瀏覽器端模塊化開(kāi)發(fā)的目的 。
當(dāng)然與AMD也有有兩點(diǎn)區(qū)別:
1. 對(duì)于依賴的模塊,AMD 是 提前執(zhí)行 ,CMD 是 延遲執(zhí)行 。不過(guò) RequireJS 從 2.0 開(kāi)始,也改成可以延遲執(zhí)行(根據(jù)寫(xiě)法不同,處理方式不同)。CMD 推崇 as lazy as possible( PO主:是越懶越好的意思么?
)。2. CMD 推崇 依賴就近 ,AMD 推崇 依賴前置 。
看代碼理解上面兩點(diǎn)的意思:
AMD模塊的定義方法
// AMD 默認(rèn)推薦的是define(['./a', './b'], function(a, b) { // 依賴必須一開(kāi)始就寫(xiě)好,即依賴前置,執(zhí)行完引入的模塊后才開(kāi)始執(zhí)行回調(diào)函數(shù) a.doSomething() // 此處略去 100 行 b.doSomething() ...})
CMD模塊的定義方法:
// CMDdefine(function(require, exports, module) { var a = require('./a') a.doSomething() // 此處略去 100 行 var b = require('./b') // 依賴可以就近書(shū)寫(xiě),即依賴就近,什么時(shí)候用到什么時(shí)候才引入 b.doSomething() // ... })
好了,看過(guò)兩個(gè)例子,對(duì)于之前沒(méi)有接觸過(guò)模塊化開(kāi)發(fā)的童鞋來(lái)說(shuō)依舊是一頭霧水:那個(gè)define是什么東東啊?還有那個(gè)require,exports,module,都是干什么的?
表捉急,我們一步一步來(lái)。在 CMD 規(guī)范中,一個(gè)模塊就是一個(gè)文件。代碼的書(shū)寫(xiě)格式如下:
define(factory);
來(lái)看github上CMD模塊定義規(guī)范上的解釋:
define 是一個(gè)全局函數(shù),用來(lái)定義模塊。
define 接受 factory 參數(shù), factory
可以是一個(gè) 函數(shù) ,也可以是一個(gè) 對(duì)象或字符串 。
factory 為對(duì)象、字符串時(shí), 表示模塊的接口就是該對(duì)象、字符串 。比如可以如下定義一個(gè) JSON 數(shù)據(jù)模塊:
1 define({ "foo": "bar" });
也可以通過(guò)字符串定義模板模塊:
1 define('I am a template. My name is {{name}}.');
factory 為函數(shù)時(shí),表示是 模塊的構(gòu)造方法 。執(zhí)行該構(gòu)造方法,可以得到模塊向外提供的接口。factory 方法在執(zhí)行時(shí),默認(rèn)會(huì)傳入三個(gè)參數(shù): require、exports 和 module :
1 define(function(require, exports, module) {2 // 模塊代碼3 });
四、小例子
說(shuō)了半天概念應(yīng)該印象還不深刻,我們就來(lái)看一個(gè)例子用來(lái)演示sea.js的基本用法。首先define傳入的參數(shù)是對(duì)象和字符串的情況,我先舉一個(gè)參數(shù)的對(duì)象的例子,傳字符串大同小異。來(lái)看代碼:
1,我先來(lái)定義一個(gè)模塊m1.js:
define({a:"這里是屬性a的值"});
define傳入的是一個(gè)對(duì)象字面量?,F(xiàn)在這個(gè)東東就可以叫做一個(gè)模塊了~我想在頁(yè)面一加載的時(shí)候就把a(bǔ)的值alert出來(lái),怎么做呢?繼續(xù)往下看。
2,在頁(yè)面上引入這個(gè)模塊:
1 seajs.use('./m1.js',function(ex){2 alert(ex.a);3 }); //彈出“這里是屬性a的值”
翻譯得直白一點(diǎn),大意就是:
seajs : Hi~m1.js,我現(xiàn)在要用(use)你了,然后把你的公開(kāi)接口(exports)存到我回調(diào)函數(shù)的參數(shù)(ex)里,你把想給我調(diào)用的東東放到這個(gè)參數(shù)里吧~么么噠
m1.js : 好的,我定義的對(duì)象字面量放到接口里給你了,拿去盡管刷~
然后……a的值就彈出來(lái)了。很愉快的一次交易。 PS:頁(yè)面所調(diào)用的模塊就為整個(gè)web應(yīng)用的js入口。本例中js的入口就是m1.js。 接下來(lái)再來(lái)看看如果 define的參數(shù)是個(gè)函數(shù) 的情況。
1,先定義一個(gè)模塊m2.js:
1 define(function(require,exports,module){2 var var1 = "這是要alert出來(lái)的值";//私有變量,沒(méi)有通過(guò)接口返出去的其他模塊不能訪問(wèn)3 function alerts(){4 alert(var1);5 }6 exports.alerts = alerts;//將需要公開(kāi)的方法存入exports接口中7 });
2,在頁(yè)面上引入這個(gè)模塊并執(zhí)行模塊m2.js公開(kāi)的方法:
1 seajs.use('./m2.js',function(ex){2 ex.alerts();//ex中存的有m2.js中的公開(kāi)對(duì)象3 }); //彈出“這是要alert出來(lái)的值”
到這里可以簡(jiǎn)單地說(shuō)一下factory方法的三個(gè)形參的意義了(個(gè)人理解):
require : 提供了引入機(jī)制,提供了一種方式來(lái)建立依賴,和C中的include和java中的import類似;
exports : 提供了導(dǎo)出機(jī)制,提供了私有和共有分離,未使用exports語(yǔ)句導(dǎo)出的變量或者函數(shù),其他模塊即使引用此模塊也不能使用;
module : 提供了模塊信息描述。
是不是思路賤賤清晰了呢?剛才我們的例子中只是從頁(yè)面調(diào)用模塊的用法,模塊之間互相調(diào)用還沒(méi)有體現(xiàn),SO,接下來(lái)就以m1.js和m2.js兩個(gè)模塊作為例子來(lái)嘗試一下 模塊之間互相調(diào)用 。
1,首先m1.js模塊不變:
1 define({a:"這里是屬性a的值"});
2,m2.js模塊要依賴(require)m1.js:
1 define(function(require,exports,module){2 var var1 = "這是要alert出來(lái)的值";//私有變量,沒(méi)有通過(guò)接口返出去的其他模塊不能訪問(wèn)3 var var2 = require('./m1.js').a;//這里就是m2.js模塊調(diào)用m1.js的方式:var2的值等于當(dāng)前模塊所依賴的m1.js對(duì)外接口中屬性a的值4 function alerts(){5 alert(var2);6 }7 exports.alerts = alerts;//將需要公開(kāi)的方法存入exports接口中8 });
3,頁(yè)面上引入m2.js模塊(同上一個(gè)例子),結(jié)果就會(huì)把a(bǔ)的屬性值給alert出來(lái)~
五、實(shí)例:模塊化的拖拽個(gè)窗口縮放
當(dāng)然,上面幾個(gè)例子是簡(jiǎn)單到不能再簡(jiǎn)單的例子,估計(jì)親們也已經(jīng)看出來(lái)一些道道,但個(gè)人感覺(jué)還是沒(méi)能體現(xiàn)出 模塊化開(kāi)發(fā)的優(yōu)勢(shì) 。那下面就來(lái)看一個(gè)實(shí)例: 模塊化的拖拽個(gè)窗口縮放 。先看一下效果圖:
PS:效果圖中的紅色區(qū)域要先定縮放的范圍,即寬高0px-寬高500px 。要寫(xiě)這樣一個(gè)需求的例子,按照之前的編程習(xí)慣你會(huì)怎么寫(xiě)?反正在之前,我是會(huì)把所有的功能寫(xiě)到一個(gè)js文件里,效果出來(lái)就行,隨你們?cè)趺春鷶囆U纏
。而自從認(rèn)識(shí)了模塊化開(kāi)發(fā),內(nèi)心不止一次告訴自己,拿到需求bigger一定要高,一定要高(雖然require.js和sea.js這兩個(gè)東東在圈內(nèi)多多少少還是有些爭(zhēng)議)……廢話少說(shuō),首先來(lái)分析一下需要?jiǎng)澐侄嗌賯€(gè)模塊吧:
1,一開(kāi)始就要有個(gè)入口模塊的吧?恩,必須的! 入口模塊 Get√~
2,既然是拖拽,要有個(gè)拖拽模塊吧?恩,必須的! 拖拽模塊 Get√~
3,既然要縮放,要有個(gè)縮放模塊吧?恩,必須的! 縮放模塊 Get√~
4,既然限定縮放范圍<=500px,那還要有個(gè)限定縮放范圍的模塊吧?恩,這個(gè)可以有,但為了以后調(diào)整范圍數(shù)值方便,還是單列個(gè)模塊吧。 限定縮放范圍模塊 Get√~
到這里我們就把本需求劃分成了四個(gè)模塊:
· 入口模塊:main.js
· 拖拽模塊:drag.js
· 縮放模塊:scale.js
· 限定縮放范圍模塊:range.js
首先,是頁(yè)面引入入口模塊(我盡量把注釋都寫(xiě)在代碼中,以便對(duì)照代碼,這樣也就不用寫(xiě)大片大片的文字了~):
1 <script>2 seajs.use('./js/main.js');//沒(méi)有callback函數(shù)表明引入后直接執(zhí)行入口模塊3 </script>
接下來(lái)看看 入口模塊(main.js) 里都應(yīng)該有些神馬東東吧:
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");//以上是獲取頁(yè)面元素的幾只變量 8 require('./drag.js').drag(div3);//引入拖拽模塊,執(zhí)行拖拽模塊接口中的drag方法并傳參 9 exports.oInput = oInput;10 oInput.onclick = function(){11 div1.style.display = "block";12 require('./scale.js').scale(div1,div2);//引入縮放模塊,執(zhí)行縮放模塊接口中的scale方法并傳參13 }14 });
恩,還真是全面呢
,把拖拽模塊和縮放模塊都引進(jìn)來(lái)了??纯?拖拽模塊(drag.js) 吧~1 //拖拽模塊 2 define(function(require,exports,module){ 3 //這個(gè)方法就是實(shí)現(xiàn)拖拽的方法,不用詳述了吧? 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;//返回拖拽模塊中想要被公開(kāi)的對(duì)象,也就是在本模塊中定義的drag方法。注意有參數(shù)~25 });
接下來(lái)是 縮放模塊(scale.js) ??s放模塊還需要調(diào)用 限定縮放范圍模塊 (range.js) 的哦~這點(diǎn)不要搞忘了。
1 //縮放模塊 2 define(function(require,exports,module){ 3 //這個(gè)方法就是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;//將需要公開(kāi)的對(duì)象存入模塊接口中,以便其他模塊調(diào)用~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 });
這就是模塊化,雖然在這個(gè)實(shí)例中我們用到了4個(gè)js,但在頁(yè)面上我們只引入了一個(gè)入口模塊main.js,其他模塊都會(huì)按需自動(dòng)引入(如下圖所示),而且每個(gè)功能模塊的區(qū)分特別清晰,再也不用擔(dān)心神馬命名沖突啊、依賴混亂啊之類的,而且團(tuán)隊(duì)小伙伴每人負(fù)責(zé)一個(gè)模塊,只要放出當(dāng)前模塊的公開(kāi)接口并提供簡(jiǎn)要的說(shuō)明文檔(因?yàn)闃?biāo)準(zhǔn)統(tǒng)一),其他小伙伴們寫(xiě)的模塊就能非常方便地調(diào)用到你寫(xiě)的模塊,連修改的時(shí)候都不用考慮對(duì)其他功能的影響,變得更大膽了呢~
寫(xiě)在最后
其實(shí)本文介紹的模塊化和seajs的使用依舊比較淺顯,但基本的模塊化思想已經(jīng)融入到例子中了。 如果你經(jīng)歷過(guò)前文所述的以前寫(xiě)js邏輯的各種糾結(jié)各種坑爹,不妨嘗試一下將你的代碼模塊化,那將是一種飛一樣的感覺(jué)……
本文最后會(huì)為大家列出一些相關(guān)的資料,想深入了解的小伙伴們可以果斷收走~
SeaJs官網(wǎng) : http://seajs.org/docs/
CMD 模塊定義規(guī)范: https://github.com/seajs/seajs/issues/242
玉伯:AMD和CMD的區(qū)別: http://www.zhihu.com/question/20351507/answer/14859415
AMD and CMD are dead之js模塊化黑魔法 : http://www.cnblogs.com/iamzhanglei/p/3790346.html
(后續(xù)會(huì)繼續(xù)補(bǔ)充……)
聯(lián)系客服