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 带来的负面影响。