今天我們來聊一聊一個框架最基礎的部分:種子模塊。
這個詞是 @司徒正美 在他的《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] // 如果倉庫里已經有這個模塊了,就從倉庫里取出需要的模塊給定義者 }
這樣的話,我們就可以自由的定義模塊而不用害怕模塊重名會覆蓋已有模塊的情況了。
接下來,我們需要使用定義好的模塊。
需要注意的是:我們使用定義好的模塊時,並不是使用模塊本身,而是使用一個它的副本。好處在於,我們不需要每次都調用一次模塊,而是單獨把每個模塊復制給使用者。也即是生產給使用者。
這部分內容我們放到后面的文章去展開。