今天我們來聊一聊一個架構最基礎的部分:種子子產品。
這個詞是 @司徒正美 在他的《JavaScript架構設計》一書裡提出的一個詞,意思是:這個子產品就好像一棵大樹的種子一樣,其所有的子產品、方法等,都根植于這個種子子產品中,它包容其他的子產品,使其他的子產品之間聯系緊密起來,并且讓使用者更友善的調用子產品、方法。
種子子產品需要堅持穩定、擴充性高、常用等原則,那麼,下面我們就開始編寫吧。
在@司徒正美的書中,他提到過,種子子產品需要包含如下的功能:對象擴充、數組化、類型判定、簡單的事件綁定與解除安裝、無沖突處理、子產品加載與domReady。
我們隻是完成一個簡單的架構,是以我們不需要包含這些全部功能,隻需要包含一些簡單的功能即可:對象擴充、子產品加載、無沖突處理。恩,大概這麼多就夠了。其其它的功能,有興趣的話,可以購買《JavaScript架構設計》自己去翻看。
這部分内容會些長,是以我們分三部分展開。
好啦,下面開始編寫我們的種子子產品。
1.命名空間
什麼是命名空間呢?來舉幾個例子
比如很多架構都親睐有加的$符号、avalonjs的avalon、vuejs的Vue等等,這些都是子產品的命名空間,它們有的綁定在window對象上(比如JQquey、avalonjs),有的則不綁定在window對象上(比如vuejs,使用的時候用new Vue 的方式來建立新的執行個體調用)。但是他們都有一個共同的特點,命名空間包含了架構的所有子產品與方法,使用者隻需要$.XXX這樣去調用就可以直接調用子產品。
使用命名空間的好處是顯而易見的:首先,它防止了全局作用域的污染并且隻暴漏一個出口給使用者,讓使用者更友善的調用其内部的子產品。
但需要注意的是,有的架構(比如prototypejs)擁有不止一個命名空間,這樣做的目的有很多,但是我們的架構隻需要一個命名空間,是以今天不展開這個話題。
命名空間相當于一個出入口,使用者使用命名空間來告訴架構需要什麼子產品,架構在内部調用子產品後将結果傳回給使用者。
簡單的命名空間的實作是這樣的:
if(window !== 'undefined'){ // 判斷一下window是否為undefined
window.$ = {}; //初始化為一個對象
window.$.css = {} //對象的方法...
window.$.animate = {} //對象的方法...
//.....
}
這樣就可以在命名空間上面注冊各種子產品與方法,使用的時候隻需要$.XXX這樣去使用就可以了。
但是隻是這樣的話,顯然是不夠安全的。
首先,我說過,很多架構都對$符号垂涎三尺(它确實很好用),是以如果在我們的架構之前加載了其他架構,那麼就會發生$符命名空間被占用的情況。即使是我們自己單獨取名的命名空間(比如我把我自己的架構稱作AHjs),也有可能會發生命名空間重名的情況,隻要發生這種情況,就會有後加載的架構覆寫先加載的架構的命名空間的情況。這顯然不是我們想要的結果。
那麼怎麼才能同時保留兩個架構呢?衆所周知,JQquery現在幾乎是(甚至可以把幾乎去掉)最常用的前端架構,其命名空間就是$符,而JQuery發展初期,當時的前端架構領域的霸主是prototypejs,而其命名空間也是$符号,JQuery為了自身的發展,引入了多庫共存機制。而多庫共存機制幾乎是現在前端架構的标配了。
讓我們來看看JQuery是怎麼做到的:
var _jQuery = window.jQuery , _$ = window.$ //先用兩個變量把可能存在的架構儲存起來。
function onConflict(deep){
window.$ = _$ //調用這個方法的時候,再把存好的其他庫、架構的命名空間換回去。
if(deep){ //如果存在第二個命名空間(即jQuery)
window.jQuery = _jQuery; //一并換回去
};
return jQuery;
};
通過這種方式,來對架構重新定義命名空間來解決架構之間沖突的問題。
當然,細心的讀者應該發現了,這種方法有一個條件,就是你的類庫、架構必須最後加載進html,不然無法起到儲存其它架構命名空間的作用。(如果第一個加載,則當時window.$還處于未定的狀态,隻會儲存undefined)。
ok,無沖突處了解決了,現在我們把命名空間也完善一下:
!function(global , target){ //兩個函數,分别是作用域、工廠方法
if(typeof global !== 'undefined'){ //如果作用域不是未定義
global.Cvm = global.$ = target(); //把工廠方法傳回的子產品綁定在命名空間上
}else{
throw new Error("Cvm requires a window with a document") //不然就抛出一個錯誤
}
}(typeof window !== 'undefined' ? window : this , function(){})
// 判斷一下,window沒問題的話,就傳入window 不然的話,傳入this(其實也是window)
ok,到這裡,我們成功的把命名空間綁定在了window對象上,至于工廠方法,我們用它來建立和傳回其它子產品,這就要引申出下一個話題:工廠模式
2.最适合架構建構的模式:工廠模式
工廠模式,顧名思義,就像工廠一樣:使用者發出訂單,告訴工廠需要什麼子產品,什麼功能。工廠接到訂單後,再組裝拼接好使用者需要的子產品,最後傳回給使用者。
它使我們的代碼更加的工業化與規範,即使是大型架構,動辄及萬行代碼量,也能夠做到結構清晰,維護友善。
工廠模式聽起來很複雜,其實實作起來很簡單:
function product(){ // 制定一個産品
return {
name:"sneakers",
state:"new",
size:"44"
}
}
function sneakersFactory() {} // 生産産品的工廠
sneakersFactory.prototype.product = product; // 指向産品
sneakersFactory.prototype.createSneakers = function(options){
if(options.sneakersType === "sneakers"){ //如果訂單是這個類型
this.product = product; //生産一個sneakers
}else{
return options.sneakersType + 'is not defined'; //抱歉,我們工廠暫時沒有這個業務...
}
return new this.product( options ); //傳回生産好的産品
}
var sneakersFactory = new sneakersFactory();
var sneakers = sneakersFactory.createSneakers({
sneakersType: "sneakers",
state: "new",
size: 44
}
);
console.log(sneakers) // 輸出一個運動鞋
這樣就實作了一個簡單的工廠,而我們所有的子產品都會注冊在工廠的生産環境裡,這樣使用者需要的時候,就生産一個子產品送到他的手上。用這種方式有條不紊的搭建我們的架構,讓我們的架構更加的工業化。
接下來我們把工廠模式綁定在種子子產品裡:
!function(global , target){
if(typeof global !== 'undefined'){
global.Cvm = global.$ = target();
}else{
throw new Error("Cvm requires a window with a document")
}
}(typeof window !== 'undefined'? window : this , function(){ //工廠是一個函數
return (function(modules){// 用來注冊和生産使用者調用的子產品和方法,參數為子產品的集合
})([])//所有的子產品在此編寫
})
好了,我們的工廠方法已經成功綁定在種子子產品中了。但是離開始編寫其他功能子產品,還有一定的距離,讓我們接着往下看。
現在我們有了工廠,可是這個工廠是個空的工廠。
恩,簡單說就是它沒有勞工。 哦 先别管産品,我們要先有勞工再去生産産品不是麼。想象一下真正的工廠:要有管道負責銷售、要有庫管負責提貨、有勞工負責生産、還需要一個大倉庫來存儲你擁有的産品。而你就是那個老闆(想想還挺帶的~)。
so,我們需要先為我們的工廠招一些人。
來看看招聘清單:管道(負責接受訂單),庫管(負責運送貨物),勞工(負責生産産品)。除此之外還需要置辦一個倉庫(存儲已有的産品)
為了解決這些,我們需要一個子產品加載機制。
3.子產品加載機制
現在市面上已經有很多完善的專注于子產品管理的架構了,比如commonjs、requirejs。以及由此衍生的AMD規範。
什麼是AMD規範呢?
AMD的全稱是Asynchronous Module Definition 異步子產品加載機制。
可以說是近幾年前端領域的一次很重大的突破。具體的我們不詳細展開,有興趣的朋友可以自己查閱資料。
我們隻需要了解AMD規範是怎麼運作的就好。
第一次接觸AMD規範的朋友,可能會被吓到。因為它是“異步”子產品加載機制。 最重要的就是異步兩個字,js涉及到異步處理,隻能使用回調函數來處理。這就導緻了JS領域著名的callback hell(回調地獄)。 恩,确實非常頭疼,很多個函數回調不停的嵌套會增加函數與函數之間的耦合度,給後期維護與穩定性造成很大的沖擊。
但如果你實際接觸到了AMD規範,會發現它其實并沒有你想象的那麼可怕,即使還是會有回調函數的加入,但已經比之前好很多了。
而且AMD規範已經規定好了子產品的寫入加載機制,讓我們更輕松的使用子產品,并且降低子產品之間的耦合狀态,
好了,不多說,先讓我們看看AMD規範需要怎麼撰寫:
define(id?, dependencies?, factory);
這是AMD規範制定好的撰寫子產品API,它有三個參數,分别是:
1.子產品名 可省略
2.子產品所需的依賴 可省略
3.子產品的實作 必須
依賴可省略,這個好了解,可能這個子產品并不需要額外的依賴就能獨立運作,或者幹脆這個子產品就是用來被其他子產品依賴的。
但是子產品名可省略?這... 實際上,AMD規範恰恰推薦這種匿名子產品的定義方式。而在其子產品加載的API:require裡,已經制定好了可靠的匿名子產品查詢機制,是以完全不用擔心匿名子產品依賴的情況。
好了,具體的我們就不展開聊了,不然可能我今晚都要坐在電腦前打字了。。。
我們隻談最基本的:定義和使用子產品,剩下的留到以後展開。
并且,為了友善,我們不允許使用者定義匿名子產品。因為涉及到require匿名子產品查詢機制,如果展開來聊,篇幅會很大,是以我們這裡隻允許使用者定義具名子產品。
首先需要了解,我們不能讓使用者或者開發者随意的定義自己的子產品:如果我定義了一個子產品a 之後又有其他人定義了一個同名子產品a 這樣的話,就會産生子產品覆寫的情況。是以我們要在使用者定義的時候,判斷一下:如果使用者需要的子產品已經存在了,就直接給使用者傳回已經定義過的子產品。
為此,我們需要一個倉庫來存儲已經定義好的子產品:
var modules = {};
function define(name , dependencies , fn){ //子產品名、依賴、實作
if(!modules[name]){ // 如果要定義的子產品不存在
var module = { // 建立一個對象, 用于儲存在倉庫裡
name:name,
dependencies:dependencies,
fn:fn
} // 把名字、依賴、實作儲存在對象裡
modules[name] = module; //把定義好的子產品放進倉庫裡。
};
return modules[name] // 如果倉庫裡已經有這個子產品了,就從倉庫裡取出需要的子產品給定義者
}
這樣的話,我們就可以自由的定義子產品而不用害怕子產品重名會覆寫已有子產品的情況了。
接下來,我們需要使用定義好的子產品。
需要注意的是:我們使用定義好的子產品時,并不是使用子產品本身,而是使用一個它的副本。好處在于,我們不需要每次都調用一次子產品,而是單獨把每個子產品複制給使用者。也即是生産給使用者。
這部分内容我們放到後面的文章去展開。