使用Dijit實現界面組件化開發
對于組件化的軟件工程設計,很多開發人員都比較熟悉。組件化的設計適合于復雜的軟件系統和團隊協作開發。把軟件系統劃分成若干個組件,組件之間通過預先定義好的接口和協議進行通訊和協作,共同完成整個軟件系統的職責。團隊中的開發人員可以各自負責不同的組件。組件化的思想在桌面應用和Web應用后臺開發中比較流行,相關的技術和實踐都比較成熟。
而在Web應用的前端部分,組件化一直進展得比較緩慢。這其中的原因有很多,最主要的是Web應用的前端在開始的時候比較簡單,沒有組件化和設計的必要。隨著Ajax應用的流行,Web前端部分越發復雜,用戶對Web應用的要求不斷向桌面應用靠攏。HTML語言的基本界面元素不能單獨地滿足這樣的需求。菜單、樹形控件、對話框和進度條等組件,在現在的Ajax應用中十分常見,但是并不是HTML默認提供的。HTML 5規范中引入了一些新的元素,但還是不夠。組件化對于Web應用本身的代碼共享和團隊分工也是很有意義的。
Web 應用前端組件化的發展也是漸進的。開始的時候,只是一些簡單的HTML、CSS加上JavaScript的代碼示例。比如當需要實現一個多級菜單的時候,就下載相關的代碼示例,就根據自己的需要進行修改。這樣的組件比較難以復用。后來JavaScript框架開始流行的時候,有些框架本身就提供了組件的支持,包括Ext JS、jQuery UI和Dojo等。不過不同框架提供的組件模型不盡相同。
Dijit組件模型概述
Web 應用的前端組件的定義比較寬泛。一個組件占據Web頁面上的某個區域,并負責完成某項具體的任務。Web組件有時候也被稱為小部件(widget)。在 Dijit組件模型中,一個Dijit組件是一個JavaScript類,可以在頁面上通過new操作符來創建組件的實例。每個組件實例都需要與頁面上的某個DOM元素綁定在一起。這個DOM元素就是該組件的根節點。在Dijit組件的邏輯中,就可以對該根節點進行操縱來構建用戶界面。組件 JavaScript類暴露出來的屬性和方法就是該組件的接口。
Dijit組件的使用
Dijit 組件的使用方式非常簡單。首先需要在頁面上加載組件的JavaScript代碼,這通過dojo.require函數就可以完成。接著在頁面上找到或創建一個DOM元素作為該組件的根節點。最后通過new操作符創建即可。如new dijit.form.ComboBox({}, node)就可以用node作為根元素創建一個dijit.form.ComboBox組件,即一個下拉列表選擇框。可以看到創建Dijit組件的時候,使用了兩個參數:第二個參數是組件的根元素,如果創建的時候不指定該根元素,會自動創建一個新的DIV元素作為根元素。
不過該新創建的根元素一般沒有加入到當前的文檔樹中,可以通過組件的placeAt方法來設置該組件在頁面文檔樹中的位置。第一個元素則是一個JavaScript對象,包含了組件的配置屬性。通常來說,一個Dijit組件是可以復用的。因此一般都會提供一些屬性供使用者進行配置。通過這個參數,就可以修改這些配置。
上面提到的是程序式的方式創建Dijit組件,還有另外的一種方式來進行創建,即通過在HTML代碼中以聲明式的方式創建,如
<h3>對話框標題</h3>
<div>對話框內容</div>
</div>
聲明式的方式在一定程度上簡化了開發人員使用Dijit組件的方式。聲明式的方式與編寫HTML代碼的形式類似,只需要在一般的HTML元素上添加一些額外的屬性就可以把HTML片段轉換成Dijit組件。這對于只熟悉HTML語言的人來說非常方便,相當于在HTML語言的基本元素之上,增加了更多的可用組件。
Dijit深入分析
Dijit組件基本類
所有的Dijit組件都繼承自dijit._Widget類。dijit._Widget類中定義了與組件相關的一系列方法。這些方法中有一些是與組件生命周期相關的,有一些則是所有組件都需要的通用方法。了解Dijit組件的生命周期有利于理解Dijit組件的運行方式,從而更好的使用已有的組件或開發出自己的組件。
創建Dijit組件的過程開始于dijit._Widget類中的create方法。create方法采用了典型的模板方法設計模式,即在該方法中封裝了創建組件的基本流程。該方法會執行一些重要的操作,并依次調用其它的方法來完成整個創建過程。具體的流程包括:
- 把創建時的配置參數混入(mix-in)到組件中。比如在創建組件的時候使用的方式是var myWidget = new TestWidget({prop : "Hello"}, node);,那么在創建完成之后就可以通過myWidget.prop來獲取到"Hello",在組件中也可以通過this.prop來訪問。
- 調用生命周期方法:postMixInProperties。該方法在配置參數混入之后調用,可以對混入的參數進行修改。
- 把該新創建出來的組件添加到全局的組件對象注冊表中。Dijit組件都會被分配一個惟一的標識符。添加到注冊表中之后,就可以用dijit.byId來根據標識符獲取組件對象。
- 調用生命周期方法:buildRendering。該方法用來完成構建組件的用戶界面。該方法負責設置this.domNode的值,表示的是創建完成的組件的根元素。
- 調用生命周期方法:postCreate。該方法在用戶界面構建完成之后被調用。一般是組件內部行為邏輯的起點,類似HTML頁面中的onload方法。
對于Dijit組件開發人員來說,創建一個新的Dijit非常簡單。只需要用dojo.declare聲明一個JavaScript類并繼承自 dijit._Widget,在該類中覆寫感興趣的JavaScript方法即可。最簡單的情況是覆寫postCreate方法并添加組件的邏輯。
對于用來包含其它子組件的容器類組件來說,一般會覆寫startup方法來讓其調用者顯式的啟動這個組件。這是因為在postCreate被調用的時候,只是保證了組件的DOM節點已經被創建成功了,但是這些DOM節點可能并沒有被添加到當前文檔樹中,因此不能在postCreate中包含與DOM節點大小和位置相關的代碼。如果要添加這樣的代碼,應該在startup中添加。很多容器類組件都使用該方法來對其子節點進行布局。
使用HTML模板
如果只是使用dijit._Widget的話,編寫Dijit組件會比較繁瑣。比如在構建用戶界面的時候,可能會需要很多的DOM操作,編寫起來也不方便。 Dijit提供了dijit._Templated用來使用HTML片段來定義組件的內容。HTML片段是作為組件的內容模板。如:
templateString : "<div><span>Hello</span></div>"
});
TempWidget繼承了兩個JavaScript類,除了必需的dijit._Widget之外,還有dijit._Templated的。需要保證 dijit._Widget是父類數組的第一個元素,只有它是真正意義上的父類,其余的是混入類。dijit._Templated類已經覆寫了 buildRendering方法來從HTML模板中創建組件內容的DOM元素,并作為組件的this.domNode的值。在HTML模板中,除了可以使用基本的HTML元素和屬性之外,還有一些附加的實用功能:
- 在HTML模板中直接引用組件中的屬性。比如組件中有個屬性叫title,在HTML模板中想引用該屬性的值,可以直接寫<span>${title}</span>。如果屬性title的值是"Hello",那么上述模板在運行時刻會變成<span>Hello</span>。
- 通過dojoAttachPoint來聲明在組件對象中可見的DOM節點。當需要在組件中引用某個內部的DOM節點時,不需要再次進行查詢,通過 dojoAttachPoint即可。如聲明<div><span dojoAttachPoint="myNode"></span></div>,就可以在組件對象中通過 this.myNode來引用該SPAN元素。
- 通過dojoAttachEvent來進行事件綁定。這種方式比先手工查詢DOM節點,再通過dojo.connect來綁定要簡單得多。如聲明<div><button dojoAttachEvent="onclick:test">Test</button></div>,就意味著將組件的test方法綁定到按鈕的onclick事件上。
dijit._Templated的模板機制的這些實用功能減少了構建用戶界面時的一些繁瑣代碼。
作為容器
如果組件是作為其它組件的容器來使用的話,就可以混入dijit._Container類。該類提供了對子組件的基本管理功能,包括查詢、添加和刪除等。使用該類的時候,需要在組件中聲明一個containerNode的屬性作為子組件的父節點。創建出Dijit組件之后,就可以通過addChild方法來添加子組件了。
銷毀過程
組件在創建并運行之后,就可能需要被銷毀。銷毀一個Dijit組件很簡單,只需要調用destroyRecursive方法即可。該方法會負責銷毀當前Dijit組件及其包含的子組件。當一個組件被銷毀的時候,其uninitialize方法會被調用,類似于析構函數。因此可以把組件特有的銷毀邏輯添加在uninitialize方法中。
Dijit組件的接口與交互
前面提到,組件之間通過設計好的接口和協議進行通訊。對于Dijit組件來說,它所提供的接口一般有下面這幾類:
- 公開的屬性和方法。這些屬性和方法類似于Java類中的公開的域和方法,在獲取到組件對象之后可以直接使用。
- 通過dojo.connect進行連接。有些組件提供了一些占位符方法用來允許其使用者監聽其內部狀態的變化,類似于DOM事件的處理。
當組件之間進行通訊和協作的時候,一般有下面幾種交互的模式:
- 傳遞組件對象的引用。這種做法一般是在創建新組件的時候,將其需要引用的組件對象傳遞進去,如var anotherWidget = new MyWidget({parent : oneWidget}, node);。
- 不傳遞對象引用,而是進行查找。這種情況適用于所依賴的組件的ID已知的情況。可以通過dijit.byId來直接進行查找。
- 使用Dojo提供的全局通訊機制:dojo.publish和dojo.subscribe。一個組件通過dojo.publish來發布消息,另外一個組件則通過dojo.subscribe來監聽相關的消息并做出處理。
一般來說,比較推薦的做法是第一種,即通過傳遞組件對象的引用來完成。不過當組件之間的關系比較復雜的時候,有可能需要將一個對象的引用進行多次傳遞。這個時候也可以考慮后兩種做法。
Dijit開發最佳實踐
編寫Dijit組件并不是一件復雜的事情,只需要按照一般的流程依次完成即可。不過Dijit組件本身的設計和實現比較復雜,包含了比較多的內容。下面對一些重點的地方進行討論。
編程式和聲明式的創建方式選擇
這兩種方式的區別只是在于開發人員的使用方式上。用聲明式方式聲明的Dijit組件,在運行時刻也是通過程序式的方式來進行創建的,由dojo.parser.parse方法來完成。因此,聲明式的方式更像一種語法糖衣。不過聲明式方式的一個好處是可以實現優雅地退化(graceful degradation),即當JavaScript不被支持的時候,仍然可以在頁面上顯示出部分內容。
對于簡單的和容器類的組件來說,聲明式創建的方式比較好。簡單的組件用聲明式的方式比較簡潔。而容器類的組件在創建的時候一般都需要指定所包含的內容。使用聲明式的時候,組件的子節點會自動作為容器類組件的子組件來添加。而如果以程序式的方式來完成的話,需要手工創建子組件,并通過addChild方法來逐個添加。代碼會比較繁瑣。
組件狀態變遷與外觀樣式
在開發Dijit組件中,經常會遇到的一個場景是根據組件內部的狀態變化改變其外觀樣式。比如對于一個單選按鈕組件來說,選中和未被選中的外觀樣式是不同的。典型的做法是通過切換不同的CSS樣式名稱來轉換外觀。比如未被選中適合的CSS樣式名稱可能是MyRadioButton,被選中之后則變成 MyRadioButtonChecked。Dijiti提供了dijit._CssStateMixin混入類來抽象這種行為。
開發人員的組件只需要繼承此類,并通過屬性this.baseClass 設置基礎的CSS樣式名稱,就具備根據狀態的變化動態修改CSS樣式名稱的能力。比如設置了baseClass為myWidget,當鼠標移動到該組件上的時候,其根元素的CSS樣式名稱會自動變成myWidgetHover。該類所支持的狀態變化包括鼠標進入/離開、焦點獲取/失去、選中/未選中、啟用 /禁用等。