Dojo 控件性能優化最佳實踐

什麼是 Dojo ?

Dojo 是一個用 JavaScript 語言實現的開源 DHTML 工具包,是基於 CSS 和 JavaScript 實現的一個通用類庫。Dojo 的目標是解決開發 DHTML 應用程序遇到的那些,長期存在 、歷史問題。Dojo 在內部處理了 DHTML 在不同瀏覽器上顯示的差異,這讓程序員大大節省了開發過程針對不同瀏覽器進行調試和容錯的開銷。Dojo 讓你更容易使 web 頁面具有動態能力,或在任何穩健的支持 JavaScript 語言的環境中發揮作用。dojo 是一個很好的基礎架構。它可以非常有效地分離頁面中的 Structure、Presentation、Behaviour,這對於實現 Ajax 組件 unobtrusive 的目標非常有幫助。








什麼是 Dojo Widget?

Widget 是 Dojo 提供組件,將最常用的功能封裝成爲一個現成的對象提供給使用者,並且提供了相關的屬性和基本的方法、事件,使用者還可以根據自己使用的需要添加新的屬性,Dojo 的控件既保持了頁面元素的基本功能,同時還提供了許多令人興奮的高級功能,它可以大大提升你的 web 應用程序交互能力以及功能上的提高,利用它的底層 API 可以使你更容易的建立風格統一的、友好的用戶界面。





回頁首


如何初始化 Dojo 控件?

那麼如何才能在你的 Web 應用中初始化一個 Dojo 控件? 這裏向大家介紹 Dojo 提供的兩種初始化途徑 : 聲明方式和編程方式 .

聲明方式初始化 Dojo 控件

也稱作靜態加載方式,在 HTML 的基本控件上添加 dojoType 屬性,給該屬性賦相應的值,這個屬性值是 Dojo 定義的一個控件的類名,由包名加上類名的形式組成,例如:dijit.form.TextBox, 這樣,在加載頁面的過程中 Dojo 會將指定了 dojoType 的 HTML 控件轉爲一個 dojo 控件 , 如 <input type="text" dojoType="dijit.form.DateTextBox" id="dojoDatetextbox01" value="2009-04-29">

在頁面加載完成後會轉變爲一個 dojo 文本控件。

那麼,一個 Dojo 控件是如何產生的呢?我們在頁面中要創建 Dojo 控件的時候,需要引用「dojo.parser」這樣一個對象,


清單 1. 引入 dojo.parser 對象
				 
 <script type="text/javascript"> 
 dojo.require("dojo.parser"); 
 </script> 

頁面裝載的過程中,使用 djConfig="parseOnLoad:true"來指定在頁面加載完成後確定執行解析功能,dojo.require("dojo.parser") 是用來加載具體執行解析 Dojo 標記的對象,該對象提供了一些的將 Dojo 定義在 HTML 元素上的屬性(比如:dojoType)解析成瀏覽器可以識別執行的代碼的方法。dojo.parser 在生成 Dojo 控件的過程中起到了關鍵性的作用,他會遍歷頁面取出有 dojoType 屬性的 HTML 元素,根據 dojoType 的值來初始化 dojo 控件對象 , 同時會把頁面中該元素的屬性值做爲參數傳遞給初始化方法,Dojo 把參數值轉換爲自己需要的類型。例 :

<input type="text" dojoType="dijit.form.DateTextBox" id=" dojoDatetextbox01" value="2009-04-29"/>

在頁面加載的時候 dojo 會初始化 dijit.form.DateTextBox 類型的控件,同時會把 value="2009-04-29"做爲參數傳給初始化方法 , 但由於 dijit.form.TimeTextBox 對應的 value 屬性的值是 Date 類型,所以 dojo.parser 會對其進行轉換 , 此時會用到 dojo 日期轉換功能,這裏不做詳細介紹 , 代碼如下:


清單 2. 聲明方式初始化 dojo 控件
				 
 <script djConfig="parseOnLoad: true, 
 isDebug: true, src="<%=Context%>/javascript/dojo/dojo.js" > 
 </script> 
 <script type="text/javascript"> 
 dojo.require("dojo.parser"); 
 dojo.require("dijit.form.DateTextBox"); 
 </script> 
 <body class="tundra"> 
 <input type="text" id="text001" dojoType="dijit.form.TimeTextBox" value="T14:22"> 
 </body> 


圖 1. 聲明方式初始化 dojo 控件效果
圖 1. 聲明方式初始化 dojo 控件效果 

編程方式始化 Dojo 控件

也稱作動態加載方式,Dojo 允許以更加面向對象的方式來創建和使用 Dojo 控件,上面例子中的日期控件,我們可以採用如下方式來初始化

new dijit.form.DateTextBox({"id":"dojoDatetextbox01","value":dojo.date.locale.parse("2009-04-29",{selector:"date"})},dojo.byId("dojotext01"));

該方法有兩個參數 , 第一個爲 dojo 對象屬性值的一個集合,第二個參數指出了 dojo 控件在頁面上的位置,在此例中 id 值爲" dojotext01"的 HTML 元素將被替換爲 Dojo 控件 , 同時原來的 HTML 控件將被移除。

第一個參數中我們同樣有兩個屬性 id 和 value, 由於編程方式定義的 Dojo 控件不再經過 dojo.parser 處理,因此 value 屬性的值必須是日期類型 ,

我們用 dojo.date.locale.parse 方法來將字符串轉換爲 Date 類型,此處我們會看到同樣是把字符串轉換爲 Date 類型,詳細代碼如下 :


清單 3. 編程方式初始化 dojo 控件
				 
 <script djConfig="parseOnLoad: true, isDebug: true, 
 src="<%=Context%>/javascript/dojo/dojo.js" ></script> 
 <script type="text/javascript"> 
 dojo.require("dijit.form.TimeTextBox"); 
 </script> 
 <body class="tundra"> 
 <input type="text" id="dojotext01"> 
 <script> 
 new dijit.form.DateTextBox({"id":"dojoDatetextbox01","value": 
 dojo.date.locale.parse("2009-04-29",{selector:"date"})},dojo.byId("dojotext01")); 
 </script> 
 </body> 

使用編程方式,就不需要 dojo.parser 來參與到 Dojo 控件的初始化過程中,僅用 Dojo 提供的方式構建一個日期控件對象,這一過程類似於 Java 語言中,通過類的構造方法,來實例化對象的過程。

聲明方式和編程方式初始化的 Dojo 控件在頁面中展示的效果是一樣的,沒有任何的區別。聲明方式定義 Dojo 控件實現起來比較簡單,而且 dojo.parser 幫助我們做了很多輔助工作。編程方式稍微複雜些,但相比聲明方式 , 編程方式更靈活,我們可以很方便的控制它的初始化過程。

兩種方式結合初始化 Dojo 控件

當然有些情況下會有這種需求 , 既想擁有編程方式的靈活性,又想擁有聲明方式的簡單性,比如我想自己來控制什麼時候生成 dojo 控件,又不想自己寫方法來進行參數值轉換,可通過如下方式來實現,

在 html 代碼中不添加 dojoType 屬性,在我們需要的時候,通過編程方式來指定 html 控件的 dojoType 屬性值,然後通過調用 dojo.parser.instantiate() 方法,來解析 html 代碼生成 dojo 控件,這樣我們既能動態的控制 Dojo 控件的生成,又能利用 dojo.parser 的強大功能,對於上面提到的例子我們可採用如下代碼實現 :


清單 4. 兩種方式結合頁面代碼
				 
 <script djConfig="parseOnLoad: true, isDebug: true, locale:'zh-cn'" 
 src="<%=Context%>/javascript/dojo/dojo.js" ></script> 
 <script type="text/javascript"> 
 dojo.require("dojo.parser"); 
 dojo.require("dijit.form.DateTextBox"); 
	
 function createDojo(inputId){ 
 var inputObj = dojo.byId(inputId); 
 inputObj.setAttribute("dojoType", "dijit.form.DateTextBox"); 
 dojo.parser.instantiate([inputObj]); 
 } 
 </script> 
 <body class="tundra"> 
 <input type="text" id="dojotext02" onfocus="createDojo('dojotext02')"> 
 </body> 

其頁面效果同前兩種方式完全一樣。通過這種方式,裝載頁面的時候,頁面中的節點全部是基本的 HTML 元素,當其中的節點響應了預先定義的 onfocus() 事件之後,動態的去調用創建 Dojo 控件的方法,在這個過程中通過調用 dojo.parser() 方法來解析相應的 Dojo 控件。由此可見,使用這種方式在頁面初始化的時候 HTML 代碼與 Dojo 沒有發生任何關係,在裝載過程中在生成 Dojo 控件方面的性能消耗將微乎其微,只是加載到頁面相應的 JavaScript 代碼,這樣可以大大提高頁面裝載的速度。

如果在項目中大量應用 Dojo 控件,頁面解析 widget 的時間因其數量的增加會變得很長,這種情況下就需要仔細考慮頁面裝載的效率問題,頁面裝載所消耗的時間是和頁面中節點的數量成正比的,因爲 Dojo 會遍歷頁面中所有的節點來檢驗是否該節點是一個 Dojo 控件,即使我們沒頁面中沒有定義任何的 Dojo 控件,遍歷頁面的操作依然無法避免,那麼,避免無謂的性能損失和優化 Dojo 控件的創建過程可以顯著的提升頁面加載的性能。








JS 調試工具簡介

在分析頁面加載過程中我們需要引入一些相應的工具來監測頁面執行的過程,相信大家對 Firebug 都已經非常的瞭解了,在本文中我們引用 Firebug 1.4-Alpha 這個版本,以下是下載鏈接的地址:http://getfirebug.com/releases/index.html


圖 2. 下載頁面
圖 2. 下載頁面 

點擊 end-user versions,就可以看到可用版本下載的鏈接提示,新版本給我們帶來了更強大的網絡監視功能,至於其他方面的新體驗,這裏不進行詳述,有興趣的讀者可以深入研究。Firebug 在網絡這個選項中進行了很大的改進,讓我們體驗一下加強的網絡監控功能, 打開 Firebug 窗口,如下圖:


圖 3. Firebug 使用界面
圖 3. Firebug 使用界面 

可以看到新的網絡選項內容更加豐富了,在該視圖中我們可以看到該頁面向服務器端發送了哪些請求以及響應時間,點擊每一個請求,我們可以看到請求的詳細信息,提示給出了請求的路徑、狀態,請求文件的大小,在頁面裝載過程中每一個階段所消耗的時間,下面簡單介紹一下圖示的含義:


圖 4. Firebug 網絡監測界面
圖 4. Firebug 網絡監測界面 

Queuing:是 FireFox 內部一種列隊機制,會將請求到的一些元素按照一定的規則加入到列隊當中,然後裝載到頁面裏。

Waiting For Response:等待服務器響應的時間。

Receiving Data:接收響應元素的時間。

TimeLine:DOMContentLoaded(藍色豎線):頁面中 DOM 元素裝載的時間(僅單純的 DOM 元素,不包括 JavaScript 生成 DOM 元素)。Load(紅色豎線):頁面裝載完成時間,所有頁面元素(DOM,JavaScript,CSS 等)全部加載完畢所消耗的時間。

瞭解這些圖示的含義之後可以幫助我們對頁面加載過程有更加深入的瞭解,如果在頁面加載過程中遇到瓶頸,我們可以通過 Firebug 分析問題出現的位置,從而改進頁面的裝載效率。

另外向大家推薦一個非常小的 FireFox 插件 YSlow,我們在它的官方網站上可以看到更加詳細的功能介紹(參見:http://developer.yahoo.com/YSlow/help/index.html)。YSlow 是由 Yahoo 開發的一個用於測試分析網站優化的 Firefox 工具插件,需要結合 Firefox 的 Firebug 調試工具聯合使用,遺憾的是目前爲止 YSlow 暫時還不能在 IE 中工作,如果您希望僅對頁面進行分析,它也可以不依靠 Firebug 單獨的使用。


圖 5. YSlow 界面
圖 5. YSlow 界面 

YSlow 會通過分析頁面各種元素,請求響應時間以及 CSS 和 JavaScript 的位置、加載方式等等方面給出頁面性能的分析結果,並且會羅列出這些分析的結果並給出相應的提高頁面性能的解決辦法。YSlow 在瀏覽器的狀態欄中顯示出了在頁面加載時對其分析的結果,包括頁面加載所有相關控件的總大小以及頁面加載響應的總時間,右擊 YSlow 圖標可以配置 YSlow 的一些常規的選項。YSLow 幫您分析頁面的響應速度並給出相應的解決方案。

在下面的例子中,我們來利用 Firebug 和 YSlow 提供的這些功能來分析一下 Dojo 控件的三種初始化方式在初始化的時候分別消耗的頁面響應時間,以此來驗證本文的論點。








Dojo 控件初始化性能測試

在這裏準備一個簡單的 Demo,頁面所有的 input 框都使用 Dojo widget 實現,如下圖所示:


圖 6. 僱員信息界面
圖 6. 僱員信息界面 

本文的目的在於分析三種方式裝載頁面效率的差別,頁面中的五個 Dojo widget 的效果不夠明顯,爲了更加清晰的展現差別的效果,我們把頁面中 Dojo widget 的數量擴大,使用一段腳本把 DIV 中包含的元素循環 100 次輸出,通過這種方式把效果放大,我們可以更加清晰的分析每種 Dojo widget 初始化的方式。

我們在進入頁面之前先清理一下瀏覽器的緩存,分別加載每一個頁面,打開 Firebug 面板,可以得到以下的結果,讓我們逐一進行分析:


清單 5. 使用聲明方式初始化 Dojo 控件性能測試
				 
 <script djConfig="parseOnLoad: true, isDebug: true, locale:'zh-cn'" 
 src="<%=Context%>/javascript/dojo/dojo.js" ></script> 
 <style type="text/css"> 
 @import "<%=Context%>/javascript/dijit/themes/tundra/tundra.css"; 
	 @import "<%=Context%>/javascript/dojo/resources/dojo.css"; 
 </style> 
 <script type="text/javascript"> 
 dojo.require("dojo.parser"); 
 dojo.require("dijit.form.DateTextBox"); 
 dojo.require("dijit.form.TextBox"); 
 dojo.require("dijit.form.ValidationTextBox"); 
 </script> 
 <body class="tundra"> 
 <h1 class="testTitle"> 僱員信息 </h1><br /> 
 <div> 
 <label> 編號:</label> 
 <input type="text" dojoType="dijit.form.TextBox" id="eid" value="51246"/><br/> 
 <label> 姓名:</label> 
 <input type="text" dojoType="dijit.form.TextBox" id="ename" value="ThinkVision"/> 
 <br/> 
 <label> 職務:</label> 
 <input type="text" dojoType="dijit.form.TextBox" id="duty" value="工程師" /><br /> 
 <label> 電子郵箱:</label> 
 <input type="text" dojoType="dijit.form.ValidationTextBox" id="email" 

value=[email protected]promptMessage="請輸入 Email 地址" invalidMessage="

請輸入正確格式的 Email" required="true" trim="true" 
 regExp="^[A-Za-z0-9](([_\.\-]?[a-zA-Z0-9]+)*)@([A-Za-z0-9]+) 
 (([\.\-]?[a-zA-Z0-9]+)*)\.([A-Za-z]{2,})$"/><br /> 
 <label> 入職時間:</label> 
 <input type="text" dojoType="dijit.form.DateTextBox" id="onboard" 
 value="2009-4-20" /><br /> 
 </div> 
 </body> 


圖 7. 使用聲明方式初始化的效果及分析:
圖 7. 使用聲明方式初始化的效果及分析: 

我們看到整個頁面加載文件的大小是 448K,完成時間是 1.713S,也就是紅色的 TimeLine 所在的位置,但是頁面的反應在這個時間點上並沒有初始化成功,而後面的三張圖片排隊的時間卻用去了將近 7S 的時間。這是爲什麼呢?在頁面加載的過程中,我們會看到很有意思的現象,開始的初始化的時候頁面中只是普通的 input 框,在一段時間以後才變成 Dojo widget,這是因爲使用聲明方式初始化,dojo.parse 方法會遍歷整個頁面節點,找到具有 dojoType 屬性的元素,再逐個將這些元素轉換成爲相應的 Dojo widget,並且爲他們渲染 CSS 效果,在渲染的過程中才開始將 blank.gif,validationInputBg.png,waring.png 裝載到頁面中,由此可見這三張圖片排隊的時間主要是集中在 dojo.parse 遍歷的過程裏面,頁面中 widget 數量越大,dojo.parse 方法執行的時間越長,從而導致頁面裝載時間變長,效率變慢。


清單 6. 使用編程方式初始化 Dojo 控件性能測試
				 
 <script djConfig="parseOnLoad: true, isDebug: true, locale:'zh-cn'" 
 src="<%=Context%>/javascript/dojo/dojo.js" ></script> 
 <style type="text/css"> 
	 @import "<%=Context%>/javascript/dijit/themes/tundra/tundra.css"; 
	 @import "<%=Context%>/javascript/dojo/resources/dojo.css"; 
 </style> 
 <script type="text/javascript"> 
 dojo.require("dojo.parser"); 
 dojo.require("dijit.form.DateTextBox"); 
 dojo.require("dijit.form.TextBox"); 
 dojo.require("dijit.form.ValidationTextBox"); 
 </script> 
 <body class="tundra"> 
 <h1 class="testTitle"> 僱員信息 </h1><br /> 
 <div> 
 <label> 編號:</label> 
 <input type="text" id="eid"/> 
 <script type="text/javascript"> 
 new dijit.form.TextBox({"id":"eidDojo","value":"51246"}, dojo.byId("eid")); 
 </script> 
 <br /> 
 <label> 姓名:</label> 
 <input type="text" id="ename"/> 
 <script type="text/javascript"> 
 new dijit.form.TextBox({"id":"enameDojo","value":"ThinkVision"
 }, dojo.byId("ename")); 
 </script><br /> 
 <label> 職務:</label> 
 <input type="text" id="duty"/> 
 <script type="text/javascript"> 
 new dijit.form.TextBox({"id":"dutyDojo","value":"工程師"dojo.byId("duty")); 
 </script><br /> 
 <label> 電子郵箱:</label> 
 <input type="text" id="email"/> 
 <script type="text/javascript"> 
 new dijit.form.ValidationTextBox({"id":"emailDojo","value":"[email protected]", 
"promptMessage":"請輸入 Email 地址", "invalidMessage":"請輸入正確格式的 Email ", 
"required":"true","trim":"true","regExp":"^[A-Za-z0-9](([_\.\-]?[a-zA-Z0-9]+)*) 
 @([A-Za-z0-9]+)(([\.\-]?[a-zA-Z0-9]+)*)\.([A-Za-z]{2,})$"}, dojo.byId("email")); 
 </script><br /> 
 <label> 入職時間:</label> 
 <input type="text" id="onboard"/> 
 <script type="text/javascript"> 
 new dijit.form.DateTextBox({"id":"onboardDojo", 
"value":dojo.date.locale.parse("2009-4-22",{selector:"date"})},dojo.byId("onboard"
 )); 
 </script><br /> 
 </div> 

</body>


圖 8. 使用編程方式初始化的效果及分析:
圖 8. 使用編程方式初始化的效果及分析: 

使用這種方式初始化 Dojo widget,每加載一個 input 框之後就會構造一個 widget 對象,在頁面加載完成時,所有的 widget 對象也都構建完畢,使用這種方式依然需要 dojo.parse 方法遍歷所有的頁面元素,在裝載文件完成後出現了一段空白的時間,這段時間就是 dojo.parse 方法所消耗掉的。我們可以看到兩條 TimeLine 幾乎是在同一位置,所有的 DOM 元素裝載和頁面裝載完成時已經消耗掉超過 6S 的時間,而頁面裝載文件的大小和聲明方式是相同的,但是速度確慢了很多。


清單 7. 使用兩種方式結合初始化 Dojo 控件性能測試
				 
 <script djConfig="parseOnLoad: true, isDebug: true, locale:'zh-cn'" 
 src="<%=Context%>/javascript/dojo/dojo.js" ></script> 
 <style type="text/css"> 
	 @import "<%=Context%>/javascript/dijit/themes/tundra/tundra.css"; 
	 @import "<%=Context%>/javascript/dojo/resources/dojo.css"; 
 </style> 
 <script type="text/javascript"> 
	 function createDojoDateBox(inputId) { 
			 var inputObj = dojo.byId(inputId); 
			 inputObj.setAttribute("dojoType", "dijit.form.DateTextBox"); 
			 dojo.parser.instantiate([inputObj]); 
	 } 
	 function createDojoTextBox(inputId) { 
			 var inputObj = dojo.byId(inputId); 
			 inputObj.setAttribute("dojoType", "dijit.form.TextBox"); 
			 dojo.parser.instantiate([inputObj]); 
	 } 
	 function createDojoValidationTextBoxForEmail(inputId){ 
			 var inputObj = dojo.byId(inputId); 
 inputObj.setAttribute("dojoType", "dijit.form.ValidationTextBox"); 
 inputObj.setAttribute("promptMessage", "請輸入 Email 地址"); 
 inputObj.setAttribute("invalidMessage", "請輸入正確格式的 Email"); 
 inputObj.setAttribute("required", "true"); 
 inputObj.setAttribute("trim", "true"); 
 inputObj.setAttribute("regExp",
 "^[A-Za-z0-9](([_\.\-]?[a-zA-Z0-9]+)*)@([A-Za-z0-9]+) \
 (([\.\-]?[a-zA-Z0-9]+)*)\.([A-Za-z]{2,})$"); 
 dojo.parser.instantiate([inputObj]); 
 } 
 </script> 
 <body class="tundra"> 
 <h1 class="testTitle"> 僱員信息 </h1><br /> 
 <div> 
 <label> 編號:</label> 
 <input type="text" id="eid" onfocus="createDojoTextBox('eid')" value="51246"/><br /> 
 <label> 姓名:</label> 
 <input type="text" id="ename" onfocus="createDojoTextBox 
 ('ename')" value="ThinkVision"/><br /> 
 <label> 職務:</label> 
 <input type="text" id="duty" onfocus="createDojoTextBox('duty')" value="工程師"/> 
 <br /> 
 <label> 電子郵箱:</label> 
 <input type="text" id="email" onfocus="createDojoValidationTextBoxForEmail 
 ('email')" value="[email protected]"/><br /> 
 <label> 入職時間:</label> 
 <input type="text" id="onboard" onfocus="createDojoDateBox 
 ('onboard')" value="09-04-29"/><br /> 
 </div> 

</body><style type="text/css">


圖 9. 使用結合方式初始化的效果及分析:
圖 9. 使用結合方式初始化的效果及分析: 

單純從裝載時間上看,可能比聲明方式並沒有節省多少時間,但是這種結合方式省掉了 dojo.parse 方法遍歷頁面節點的過程,會明顯提高用戶體驗。換言之,就是這種方式在頁面裝載的時候,所有的 DOM 節點都是普通的 HTML elements,沒有任何的 Dojo widget,只有當用戶觸發了 input 框的相應事件之後,Dojo 控件初始操作纔會被**。而且,由於沒有在裝載的時候初始化 Dojo widget,裝載時的文件請求也沒有請求 widget 有關的文件,我們看到結合方式只發出了 40 個請求,而其他兩種方式的請求數是 42 個,相差了兩個 ValidationTextBox 控件使用的圖片文件 validationInputBg.png 和 warning.png。所以使用結合方式初始化 Dojo widget 較上面兩種方式大大提高了頁面裝載的效率。




結束語

在控制頁面裝載效率方面,還有很多可以在實踐當中進行優化的地方,我們可以利用 YSlow 等提供的最佳實踐應用到我們的項目中來。

Dojo 控件在項目中的使用還是需要我們慎重考慮的,是否使用,用哪些控件可以解決我們的問題,在使用過程中的風險控制,使用控件比我們自己實現究竟減少了多少我們的工作量,這些問題在使用 Dojo 之前還是要首先權衡一下,避免 Dojo 帶來的負面影響。