Dojo 是开源 JavaScript 库中起步较早的先行者之一。由 Alex Russell, David Schontzler, Dylan Schiemann 等人于 2004 年创立。Dojo 具有类似 Java 的包机制 (packaging system), 将 JS 代码根据功能进行了模块化。主要包含 Dojo、Dijit 以及 Dojox 三个包。其中 Dojo 包提供稳定的内核 API,Dijit 包提供各类 UI 控件,Dojox 包则囊括了一系列实验性的 API 及控件(其中不乏一些得到长期维护、稳定性已相当高的包,如 dojox.charting 包和 dojox.grid 包等)。在 Dojo 1.7 版本中,Dijit 包的内部结构被进行了更细的模块拆分和重构,但由于撰写本文时其尚未发布,本文中的 Dijit 相关内容仍将基于 Dojo 1.6.1 版本。
ExtJS 是当今一套被广泛应用于前端开发的 Ajax 以及控件框架,其历史可以追溯到 Yahoo! User Interface。在 Jack Slocum 的耕耘下,ExtJS 逐渐成长。自从 ExtJS 的 2.0 版本发布后,其使用范围逐渐扩展到世界各地。3.X 版本中推出了更多易用的控件,ExtJS 的优势在于其强大的控件库,以及其对于各种前台功能的封装,形成了完整的一套面向对象风格的 JS 控件库。随着和 Sencha 合并,ExtJS 也向触摸屏发展,不过其 Ext JS 库的发展从未停止。如今的 ExtJS 4.0 提供了更完整的 JavaScript API 库,减少对 Array、Function、String 等底层类的重写,大大的减少了不同的 JS 库之间的冲突问题。由于 4.0 版本不向下兼容,对升级造成了一定的影响,笔者还没有机会去深入使用 ExtJS 4.0 版本,因此本文着重介绍的是 ExtJS 3.X 版本。
jQuery UI 是 jQuery 的官方 UI 控件库。jQuery 的大名在业内可谓是无人不知无人不晓。自 2006 年发布了其第一个稳定版之后,其轻量、易用的特点使其深入人心。jQuery UI 于 2007 年发布,它完全基于 jQuery 提供的插件机制,提供了底层交互及动画功能以及一些可定制样式的 UI 控件。虽然提供的控件数量不多,但它们都具备了 jQuery 小巧的特点,其使用风格也与 jQuery 核心 API 一致。撰写本文时,其最新的稳定版本为 1.8.16,本文中关于 jQuery UI 的内容都基于该版本。
在讨论各个控件库的架构实现之前,首先让我们从用户的角度来看看 Dijit、ExtJS、jQuery UI 控件的的使用方式,对它们有一个直观的了解。
控件的使用无外乎创建控件、操作控件,而在创建控件之前,我们往往需要先加载控件资源。下面就让我们从控件资源加载开始聊起(控件 CSS 文件编写与加载不在本文范围内)。
Dijit 篇:借助于 Dojo 提供的自动加载模块依赖项的机制,加载 Dijit 控件所需资源非常简单,用户并不需要了解一个控件究竟需要哪些 JS 文件的支持,只需向页面添加 Dojo 核心文件 dojo.js 并使用 dojo.require 函数导入控件对应模块即可。
清单 1. Dijit 资源加载
<script type="text/javascript" src="lib/dojo/dojo.js"></script> <script type="text/javascript"> dojo.require("dijit.form.Button"); </script> |
上述代码将自动加载 Button 控件所依赖的所有 JS 文件。
ExtJS 篇:ExtJS 本身没有一套完整的自动加载依赖资源的机制,在大多数情况下,用户都是使用完整的 ExtJS 库,只需要导入 /ExtJS/adapter/ext/ext-base.js 和 /ExtJS/ext-all.js 这 2 个文件即可。一般情况下为了方便调试,会使用 ext-base-bug.js 和 ext-all-debug.js 这 2 个文件。
清单 2. ExtJS 资源加载
<script type="text/javascript" src="JsLib/ExtJS/adapter/ext/ext-base-debug.js"></script> <script type="text/javascript" src="JsLib/ExtJS/ext-all-debug.js"></script> |
当然为了节省资源也可以只加载部分的 ExtJS 资源库,ExtJS 提供了一个名为 ext.jsb2 的文件(该文件描述了各个 JS 文件之间的依赖情况), 让用户查询各个文件之间的依赖情况,方便用户进行 ExtJS 控件的单个导入。
jQuery UI 篇:由于 jQuery 也没有提供一套完整的自动加载依赖资源的机制,因此用户需要手动将所使用控件的依赖资源逐一导入页面。以 jQuery UI 中的 button 控件为例,需要通过手动为页面添加如下代码导入所需 js 文件。
清单 3. jQuery UI 资源加载
<!-- 导入 jquery core --> <script type="text/javascript" src="lib/jquery/jquery-1.6.2.js"></script> <!-- 导入 Button 控件所依赖的 JS 资源 --> <script type="text/javascript" src="lib/jquery/ui/jquery.ui.core.js"></script> <script type="text/javascript" src="lib/jquery/ui/jquery.ui.widget.js"></script> <script type="text/javascript" src="lib/jquery/ui/jquery.ui.button.js"></script> |
这样手动加载资源的方式需要用户清楚了解一个控件依赖哪些资源项,这在使用某些依赖项较多的控件,如 dialog 时会带来困扰。虽然在最终发布的产品中,往往会将页面中所有使用到的 JS 代码进行合并压缩,用户仅需要在页面中导入合并压缩过的 JS 文件即可,但用户仍需要在了解页面需要哪些资源之后再对其进行合并压缩(当然用户也可以选择一次性将所有 jQuery UI 的代码合并压缩到一个文件中)。
Dijit 篇:Dijit 控件的创建方式有两种,编程式(programmatic)以及声明式(declarative)。
使用编程方式使用 Dijit 控件与使用传统面向对象语言进行 UI 编程非常类似。通常只需要提供一个 DOM 节点、一个散列参数表并使用 new 关键字创建一个所需的 dijit 控件对象即可。
清单 4. Dijit 使用 new 关键字创建 Button 控件
<html> <head> <script type="text/javascript"> dojo.addOnLoad(function(){ var button = new dijit.form.Button({ id: "programmatic", label: "This is a button" }, "buttonNode"); }); </script> </head> <body> <div> <button id="buttonNode"> </button> </div> </body> </html> |
上述代码将会创建一个 Button 控件,并将 id 为 buttonNode 的 button 标签元素替换为实例化的控件的 DOM 树。而 button 变量则指向该控件实例的引用。此外还可以先创建控件实例,再将其插入到页面的 DOM 树中。
清单 5. Dijit 使用 new 关键字创建 Button 控件
<html> <head> <script type="text/javascript"> dojo.addOnLoad(function(){ var button = new dijit.form.Button({ id: "programmatic", label: "This is a button" }); button.placeAt("buttonContainer"); }); </script> </head> <body> <div> <p id="buttonContainer"> </p> </div> </body> </html> |
上述代码会创建一个 Button 控件实例并将其 DOM 树其插入到 id 为 buttonContainer 的 p 标签元素之下。
使用声明方式使用 Dijit 控件时,需要为 HTML 标签添加 data-dojo-type 以及 data-dojo-props 属性,其中 data-dojo-type 表示所要生成控件的名称,data-dojo-props 包含生成控件所需的构造参数。使用此种方法创建 Dijit 控件时,可以在导入 Dojo 核心文件时通过 parseOnLoad 属性配置是否自动实例化页面上所有控件。
清单 6. 开启 parseOnLoad 属性,自动创建控件
<html> <head> <script type="text/javascript" src="lib/dojo/dojo.js" data-dojo-config="parseOnLoad: true"/> </head> <body> <button data-dojo-type="dijit.form.Button" data-dojo-props= 'id: "declarative"; label: "This is a button"’ /> </body> </html> |
上述代码将会在页面加载完毕后自动实例化一个 Button 控件。当用户选择关闭 parseOnLoad 选项时,可以通过手动方式实例化所需要的控件。
清单 7. 关闭 parseOnLoad 属性,手动创建控件
<html> <head> <script type="text/javascript" src="lib/dojo/dojo.js" data-dojo-config="parseOnLoad: false"/> <script type="text/javascript"> dojo.addOnLoad(function(){ dojo.parser.parse(); }); </script> </head> <body> <button data-dojo-type="dijit.form.Button" data-dojo-props= 'id: "declarative"; label: "This is a button"’ /> </body> </html> |
无论是否启用 parseOnLoad 选项,其本质都是使用 dojo.parser 这个对象对页面进行扫描,解析 HTML 标签中的属性,并根据这些属性内容实例化控件。需要注意的是,dojo.parser 并不是 Dojo base 的一部分,也就是说之前导入的 Dojo 核心文件 dojo.js 并不包含 dojo.parser 模块,因此通常情况下使用 dojo.parser 的话需要额外添加代码导入 dojo.parser 模块。
清单 8. 导入 dojo.parser 模块
dojo.require("dojo.parser"); |
然而在使用 Button 控件时,由于我们已经导入了 dijit.form.Button 模块 ,Dojo 会为我们自动加载所有该模块依赖的资源,其中就包括 dojo.parser 模块对应的 JS 文件。
ExtJS 篇:ExtJS 控件的生成方式基于使用 new 关键字创建一个 ExtJS 控件对象,将其放入需要渲染的 DOM 节点中。例如:
清单 9. ExtJS 使用 new 关键字创建 Button 控件
<script type="text/javascript"> var button = new Ext.Button({ id: 'button', text: 'button', renderTo: Ext.getBody() }); </script> |
上述代码中,通过赋予 renderTo 参数一个页面的 DOM 节点,将一个 Ext.Button 对象添加到 body 元素中,至此一个简单的 ExtJS 控件完成了。
此外 ,ExtJS 还允许用户通过 add() 和 doLayout() 方法,向容器控件(继承自 Ext.Container 类的控件)中添加缺省 renderTo 属性的子控件。
清单 10. ExtJS 向容器控件添加子控件
<script type="text/javascript"> var mainPanel = new Ext.Panel({ renderTo: Ext.getBody() }); var button = new Ext.Button({ id:'button' }); mainPanel.add(button); mainPanel.doLayout(); </script> |
上述代码首先将 mainPanel 对象渲染到 body 元素中,之后通过 add() 和 doLayout() 方法将缺省 renderTo 属性的 button 对象添加到 mainPanel 对象中,并重新渲染 mainPanel 对象,此时 button 对象也会被渲染到页面上。
ExtJS 没有像 Dijit 那样提供通过解析 HTML 标签的方式创建控件,但是提供了简单的反射机制,使用户可通过描述符来生成控件。
清单 11. ExtJS 通过描述符生成控件
<script type="text/javascript"> var mainPanel = new Ext.Panel({ items: [{ xtype: 'button', id: 'button' }], renderTo: Ext.getBody() }); </script> |
上述代码首先实例化一个 ExtJS 的 Panel 控件并在其 items 属性中添加了一段关于 button 控件的描述符,在 mainPanel 对象渲染的过程中,会遍历 items 数组中的每一个对象,如果对象没有被实例化,则会寻找描述符对象中的 xtype 属性。而后,在控件创建的过程中,Ext.ComponentMgr 的 create() 方法会根据描述的 xtype 属性寻找在 Ext.reg 中注册过的控件类,通过反射的方式创建一个对应的控件实例。
jQuery UI 篇:jQuery UI 控件的使用方式秉承了 jQuery 一贯简洁的风格。它并不提供类似于 dojo.parser 这样的工具类来扫描页面并根据 HTML 标签自动生成控件实例,也不像传统的面向对象语言那样使用 new 关键字来创建控件,而是通过 jQuery 插件常用的链式书写方式来创建控件。
清单 12. jQuery UI 创建 Button 控件
<html> <head> <script type="text/javascript"> $(function){ $("#buttonNode").button({ label: "button" }); }); </script> </head> <body> <div> <button id="buttonNode"></button> </div> </body> </html> |
上述代码首先使用 jQuery 核心方法 $() 获取页面中所有符合查询条件的 HTML DOM 节点(本例中只有一个 id 为 buttonNode 的 DOM 节点符合条件),并将返回的 DOM 节点数组包装成一个 jQuery 对象。之后调用 $.ui.button 插件为 jQuery 对象提供的 button 方法根据传入的散列参数表为这些节点创建 $.ui.button 控件。
Dijit 篇:在创建 Dijit 控件之后,用户可以通过 dijit.byId、dijit.findWidgets、dijit.byNode、dijit.getEnclosingWidget 等方法获取控件实例。
清单 13. 获取 Dijit 控件对象
// 获取 widget id 为 programmatic 的控件 var widget = dijit.byId("programmatic"); // 获取 body 标签下的所有控件 var widgets = dijit.findWidgets(dojo.body()); // 获取 DOM 树根节点为以 node 的控件 var widget = dijit.byNode(node); // 获取 DOM 树中包含 node 节点的控件 var widget = dijit.getEnclosingWidget(node); |
获取控件实例之后可以像使用 Java 类那样调用控件方法。并使用 get、set 方法来获取/设置控件的属性。
清单 14. Dijit 控件属性获取/设置及方法调用
// 调用控件方法 widget.someMethod(); // 使用 get 获取控件属性 var value = widget.get(attributeName); // 使用 set 设置控件属性 widget.set(attributeName, value); |
ExtJS 篇:ExtJS 并没有像 Dijit 一样提供了通过 DOM 节点查找控件的方法,而是只提供通过控件 id 号获取控件对象的方法。
清单 15. 获取 ExtJS 控件对象
// 获取控件对象 var button = Ext.getCmp("button"); |
Ext.getCmp 方法返回的就是一个完整的 ExtJS 的控件对象,包含了控件对象的所有变量及方法。与 Dijit 不同的是,ExtJS 的成员变量大多是通过使用变量名去获取/设置的,仅对部分属性提供了对应的 get/set 方法(其原因和内容将在后文的“属性获取/配置方法”章节中具体阐述), 而 ExtJS 的控件方法调用还是与 Java 的方法调用类似的。
清单 16. ExtJS 控件属性属性获取 / 设置及方法调用
// 获取控件成员变量,直接访问成员变量 var buttonText = button.text; //button 控件为 width 属性添加了 setWidth 方法,以便 width 属性改变后,对 DOM 节点进行处理。 button.setWidth(100); // 调用控件成员函数,类似 Java 的对象方法调用方式 button.focus(); |
jQuery UI 篇:操作 jQuery UI 控件的方式与创建 jQuery UI 控件的方式非常相似。
清单 17. jQuery UI 控件方法调用
<html> <head> <script type="text/javascript"> $(function){ // 为 button 标签创建 Button 控件 $("#buttonNode").button({ label: "button" }); // 调用 Button 控件的 disable 方法 $("#buttonNode").button("disable"); }); </script> </head> <body> <div> <button id="buttonNode"></button> </div> </body> </html> |
上述代码中,先后调用了两次 button 方法,但其含义完全不同。第一次调用 button 方法时,传入了一个散列参数表,为对应节点创建了 $.ui.button 控件。第二次调用 button 方法时,参数为一个字符串,此时 jQuery 调用了与字符串同名的控件成员方法。
jQuery UI 并没有为控件属性提供默认的 get/set 方法,但用户可以通过如下方式获取/设置控件属性:
清单 18. jQuery UI 控件属性获取 / 设置
// 调用 option 方法获取 $.ui.button 控件的属性表 var options = $("#buttonNode").button("option"); // 设置 buttonNode 节点对应的 $.ui.button 控件的 label 属性 $("#buttonNode").button({ label: "changed label" }); |
上述代码中第二次调用 button 方法时虽然传入的是一个散列参数表,但由于之前已经为 id 号为 buttonNode 的 DOM 节点创建过 $.ui.button 控件,因此不会再次创建控件对象,而是取出已创建的控件对象,并设置其对应的属性。
在了解了 Dijit、ExtJS、jQuery UI 的使用方式之后,让我们从开发者的角度再来看看这些控件背后的架构与实现机制。
Dijit 篇:通过前面的使用范例可以看到 Dijit 控件是面向对象的。每个 Dijit 控件都是由 dojo.declare 方法声明的一个类。下面是 dijit.form.Button 控件的声明代码片段
清单 19. 声明 dijit.form.Button 类
dojo.declare( // 控件名 "dijit.form.Button", // 基类 dijit.form._FormWidget, { // 成员属性 baseClass: "dijitButton", templateString: dojo.cache("dijit.form", "templates/Button.html"), ... // 成员方法 buildRendering: function(){...}, reset: function(){...}, _onClick: function(/*Event*/ e){...} ... }); |
上述代码声明了一个 dijit.form.Button 类,在创建该控件对象时,只需通过 new 关键字进行实例化即可。
如果您了解 dojo.declare 用法(具体用法参见 http://dojotoolkit.org/reference-guide/dojo/declare.html#dojo-declare)的话可以看出,控件类的声明方式与声明一个普通的 Dojo 类并没有什么区别。而一个 Dijit 控件类与普通 Dojo 类的主要区别在于 Dijit 控件类继承了一些特定的基类。在后文中将会为您介绍那些在背后支撑 Dijit 的基类。
ExtJS 篇: ExtJS 通过使用 Ext.extend 方法来实现继承,并声明一个新的控件类。以 Ext.Button 为例:
清单 20. 声明 Ext.Button 类
// 其中 Ext.BoxComponent 是一个继承自 Ext.Component 的类, Ext.Button = Ext.extend( // 基类 Ext.BoxComponent, { // 控件属性 hidden: false, … . // 成员方法 initComponent: function(){ Ext.Button.superClass.iniiComponent.call(this); ... } }); |
上述代码声明了一个基于 Ext.BoxComponent 的 Ext.Button 控件类。
在每一个控件类的声明结束后,还可以通过 Ext.reg 方法来为该控件类进行全局的注册,只有经过注册的控件类,才能通过描述符由之前提到的反射机制来生成:
清单 21. 注册 Ext.Button 类
Ext.reg('button',Ext.Button); |
Ext.extend 方法除了提供继承的机制来声明新的控件,还能对已有的控件进行扩展,以 Ext.Component 为例:
清单 22. 扩展 Ext.Component 类
Ext.extend(Ext.Component, Ext.util.Observable, { // 新的成员变量以及方法 }); |
上述代码将 Ext.util.Observable 类的成员变量以及方法扩展到 Ext.Component 类中,并且将第三个参数对象中的成员变量及方法写入 Ext.Component 类中,这些属性及方法将覆盖 Ext.Component 类中的同名属性及方法。
jQuery UI 篇: jQuery UI 在 jquery.ui.widget.js 文件中提供了 $.widget 这个工厂方法来声明 jQuery UI 控件类。下面是 $.ui.button 控件的声明代码片段:
清单 23. 声明 $.ui.button 类
$.widget("ui.button", { options: { // 成员属性 disabled: null, ... }, // 声明体内只声明成员方法 _create: function{ ... }, refresh: function{ ... }, ... }); |
其书写方法与声明一个 Dijit/ExtJS 控件类较为类似,但需要注意的是,jQuery UI 控件的属性需要写在一个 options 对象中,而不是直接写在对象的声明体内。此外我们并没有像声明 Dijit/ExtJS 控件类那样为其指定一个父类,其原因将在下一节继承体系中说明。
既然 jQuery UI 控件的声明方法和 Dijit/ExtJS 控件如此类似,为什么它们的创建方法又如此不同呢?我们来看一下上面的代码片段背后到底做了哪些工作。
在调用 $.widget 方法时首先会声明一个名为 $.ui.button 类,理论上说,有了这个类,用户就可以通过 new 关键字来创建该控件类的实例了。
清单 24. 使用 new 方法创建 $.ui.button 控件
<html> <head> <script type="text/javascript"> $(function){ // 使用 new 关键字创建一个 label 为 button 的 $.ui.button 控件对象 new $.ui.button({label: "button"}, $("#buttonNode")); }); </script> </head> <body> <div> <button id="buttonNode"></button> </div> </body> </html> |
这样的书写方法和使用 Dijit 以及 ExtJS 的基本一致,但是为了保持和 jQuery 核心 API 一样的链式书写方式,在声明完这个控件类之后 ,$.widget 方法还会调用 $.widget.bridge 方法来向 $.fn(jQuery 对象的 prototype) 添加一个与控件名同名的插件方法,在本例中为 button。该方法在被调用时会根据传入的参数来决定是调用控件的方法,还是创建控件对象,或是取出已创建的控件对象来修改属性。
图 1. $.fn.button 方法运行流程图
在上一节的 Dijit 及 jQuery UI 相关代码实例中,您可能发现了一些名字以下划线"_"开头的方法,这些方法都是私有方法,但在不同的 UI 库中又略有不同。
Dijit 篇:Dojo 的代码风格规定以下划线"_"开头的方法为对象私有,然而这仅仅是代码风格上的要求,用户在使用时仍可直接调用 Dijit 控件的这些方法(但不推荐)。
ExtJS 篇: ExtJS 的控件类中,没有直接用来区分私有和公共方法的风格规范,而是通过方法前添加规范的注释进行划分,其形式类似于 Java 中的方法注释。带有注释的成员方法,会被 Ext API 文档收录。通过这种方式,使 API 文档只展示公共变量和方法,以保证普通用户在查询 API 文档时,仅能接触到 ExtJS 提供的公共方法。当需要扩展一个 ExtJS 的控件,对其添加成员变量及方法时,用户可按照 ExtJS 的文档规范进行注释,将需要开放的公共方法收录到 ExtJS 的 API 文档中,有助于他人查询和使用自助开发的控件类。
清单 25. ExtJS 公共方法的代码注释
/** * Adds a CSS class to the component's underlying element. * @param {string} cls The CSS class name to add * @return {Ext.Component} this */ addClass : function(cls){ // 具体实现略 } |
jQuery UI 篇: jQuery UI 与 Dojo 倡导的代码风格一样,规定以下划线"_"开头的方法为控件的私有方法。但与 Dojo 不同的是 ,jQuery UI 不允许这些内部方法被用户调用,也就是说下例中调用 Button 控件 _create 方法的代码是无效的。
清单 26. 调用 jQuery UI 控件私有方法
<html> <head> <script type="text/javascript"> $(function){ // 为 button 标签创建 Button 控件 $("button").button({ label: "button" }); // 这行代码不会执行控件的任何方法 $("button").button("_create"); }); </script> </head> <body> <div> <button></button> </div> </body> </html> |
Dijit 篇:得益于 Dojo 提供的类继承机制,我们可以通过继承一些特定的基类,方便地扩展出一个标准的 Dijit 控件。这些基类抽象了一个 Dijit 控件所需的底层功能,大大方便了新控件的开发。
需要注意的是,Dojo 的继承机制与 Java 有所不同,它支持多重继承,但类的 prototype 只有一个(Dojo 继承机制的具体实现已超出本文范围,请参阅 Dojo 类机制实现原理分析), 因此一个 Dijit 控件往往也继承了多个父类。如 dijit.form.Select 控件:
清单 27. 声明 dijit.form.Select 控件
dojo.declare( // 控件名 "dijit.form.Select", // 父类 [dijit.form._FormSelectWidget, dijit._HasDropDown], { // 成员属性 baseClass: "dijitButton", templateString: dojo.cache("dijit.form", "templates/Button.html"), ... // 成员方法 buildRendering: function(){...}, reset: function(){...}, _onClick: function(/*Event*/ e){...} ... }) |
一个 Dijit 控件背后一般都需要由以下基类来支撑:dijit._WidgetBase,dijit._Templated,dijit._CssStateMixin 等。
图 2. Dijit 控件继承体系图
其中最底层的基类是 dijit._WidgetBase。所有 Dijit 包中的控件都继承于它。在这个类中定义了一个标准 Dijit 控件的基本创建流程。
大多数 Dijit 控件在继承 dijit_WidgetBase 这个基类之外还继承了 dijit._Templated。故名思意该基类为控件提供了模板功能,在后文中将详细介绍模板功能。
dijit._CssStateMixin 也是大部分 Dijit 所继承的基类。它会监听控件的一系列属性,如"disabled","readOnly","checked","selected","focused", "state","hovering","active"等。当这些属性发生改变时,控件 DOM 节点上的某些 CSS class 名会被同步修改,从而反映出控件的当前状态。
此外,该类还在控件创建的过程中为其鼠标事件(mouseenter, mouseleave, mousedown)绑定了回调函数。该回调函数监听用户的鼠标操作,并及时更新控件的"hovering"、"active"属性,最终对其 DOM 节点上的 CSS class 进行修正。
ExtJS 篇: ExtJS 有着类似于 Dijit 的继承机制,也支持通过 Ext.extend 方法扩展控件的功能(Ext.extend 使用方法已在前文展示,这里不再重复)。
ExtJS 所有的控件类都继承自 Ext.Component 类,这个类定义了 ExtJS 控件的基本创建流程,涵盖了事件定制、模板定制、DOM 节点渲染等方法。因此,普通的 ExtJS 控件仅需继承 Ext.Component 类便能满足所有的功能需求。而有特殊功能需要的控件,通过扩展诸如 Ext.dd.DragDrop,Ext.ClickRepeater 等类来实现拖拽,鼠标事件处理等功能。
图 3. ExtJS 控件继承体系图
除了通过继承的方式,ExtJS 还提供了 Ext.Override 方法供用户重写已声明过的对象,后文的“DOM 树构建”章节中会描述其部分作用。
jQuery UI 篇: jQuery core 并没有提供类似 Dojo/ExtJS 的继承机制,但是在 jQuery UI 1.8 中,为了方便扩展控件,开始为控件类引入了类似的继承机制。用户可以通 $.widget 工厂方法在声明控件的同时为其指定父类 ,
清单 28. 声明 $.ui.myButton 控件
$.widget('ui.myButton', // 父类 $.ui.button,{ // 定制代码 ... }); |
然而 jQuery UI 的这套继承机制并不支持多重继承,一个控件类只能基于一个父类。当其父类缺省时(上例中的第二个参数),jQuery UI 会自动将 $.Widget 类作为控件的父类。
jQuery UI 的继承体系相对简单,一般来说,仅有 $.Widget 这个基类是必须的。$.Widget 类定义了 jQuery UI 控件的基本创建流程及属性配置(.option)、控件销毁 (.destroy) 等基本方法。
此外 ,jQuery UI 还提供了一些可选的基类,如用来监听鼠标行为的 $.ui.mouse, 用来支持控件拖拽的 $.ui.draggable,$.ui.droppable 等。
图 4. jQuery UI 控件继承体系图
Dijit 篇:前面提到 dijit._WidgetBase 类中定义了一个 Dijit 控件的基本创建流程,在实例化一个控件时会自动执行以下步骤:
图 5. Dijit 控件基本创建流程
其中 dijit._WidgetBase 会为控件暴露出以下三个方法以供覆写。
postMixinProperties:此时控件对象生成完毕,但还未向全局进行注册,且 DOM 节点还未构建。可以通过覆写此方法为控件初始化一些额外的资源。
buildRendering:控件需要实现此方法来为已注册好的控件对象构建 DOM 树。
postCreate:此时控件已经创建完毕,且已绘制到页面中。可以通过覆写此方法来对控件节点进行最后的调整,如绑定事件的回调函数等。
ExtJS 篇: Ext.Component 类作为所有 ExtJS 控件的基类,定义了 ExtJS 控件最基本的创建流程:
图 6. ExtJS 控件基本创建流程
其中,提供了 initComponent、onRender、afterrender、initEvents 方法以供子控件重写。其中 onRender 方法是 render 方法中绘制控件默认 DOM 树的环节。
initComponent:通过重写此方法来为控件添加新的初始化参数,添加需要注册的基本事件,并提供接口,允许使用者对该控件基本事件添加的监听器。
onRender:控件需要覆写此方法来提供其对应的 DOM 树模板。
afterrender:此时控件已经创建完毕,并渲染到页面上,可以在此方法中对控件对象进行初始化状态的调整。
initEvents:通过重写此方法来注册用户添加的自定义事件,并为自定义事件添加对应的监听器。
jQuery UI 篇: jQuery UI 控件的创建流程由 $.Widget 定义。具体步骤如下:
图 7. jQuery UI 控件基本创建流程
其中,控件需要覆写 _create 方法以及 _init 方法。
_create:覆写此方法为控件在页面上构建其 DOM 树,并为节点绑定事件的回调函数等。
_init:覆写此方法来根据控件属性对创建完的控件 DOM 树内容进行调整。在控件实例化之后,每当属性发生修改时,该方法也会被调用。
Dijit 篇:上一节中提到,一个 Dijit 控件需要实现 buildRendering 方法来构建控件的 DOM 树。而 dijit._Templated 这个基类为 Dijit 控件提供了 buildRendering 方法的基本实现(基于 dijit._Templated 的控件可以根据需要为 buildRendering 方法追加代码,详细方法请参阅 inherite), 使得控件可以在该阶段通过加载模来构建自己的 DOM 树。继承了 dijit._Templated 的 Dijit 控件类都会有一个名为 templateString 的属性,该属性顾名思义,是用来构建该控件的 HTML 模板字符串。在执行 buildRendering 方法时,该字符串会被自动用来替换页面上的节点,从而构建出控件的 DOM 树。
清单 29. 控件中的 templateString
dojo.declare("myWidget", [dijit._Widget, dijit._Templated],{ ... // 模板字符串 templateString: "<div>This is a simple widget</div>", ... }); |
为了使控件代码和模板内容可以独立开,每一个控件的 templateString 内容往往并不直接写在 JS 文件中,而是保存在一个对应的 HTML 模板文件中。Dijit 包中某个控件对应的模板文件被放在该控件所在包下的 templates 文件夹中,如 dijit.form.Button 控件的模板文件为 dijit/form/templates/Button.html。在使用模板时,只需通过 dojo.cache 方法将其加载并赋给 templateString 即可。
清单 30. 使用 dojo.cache 加载模板
dojo.declare("dijit.form.Button", dijit.form._FormWidget,{ ... // 模板内容 templateString: dojo.cache("dijit.form", "templates/Button.html"), ... }); |
Dijit 控件的模板中不仅仅定义了控件的 DOM 树结构,还允许使用表达式将控件属性直接插入 DOM 树中。如,使用以下模板片段的控件在生成时,会将 id 为 labelNode 的 span 节点的内容替换成控件的 label 属性。
清单 31. 在 Dijit 控件模板中使用 ${} 插入控件属性
<div> <span id="labelNode">${label}</span> </div> |
此外 ,Dijit 模板还允许使用 dojoAttachNode 属性来为模板中的指定节点设置访问假名。如下面模板片段中 dojoAttachNode 属性为 labelNode 的 span 节点,可以通过控件的 labelNode 属性来获取。
清单 32. 在 Dijit 控件模板中使用 dojoAttachNode 属性
<div> <span dojoAttachNode="labelNode"></span> </div> //widget 为使用上述模板的控件对象 var spanNode = widget.labelNode |
类似的,对模板中的节点添加 dojoAttachEvent 属性可以为其绑定事件回调函数。如下面模板片段中的 dojoAttachEvent 属性为所在的 span 节点的 click 事件绑定了控件的 _onClick 方法作为回调函数。
清单 33. 在 Dijit 控件模板中使用 dojoAttachEvent 属性
<span> <span dojoAttachEvent="onclick: _onClick"></span> </span> |
上文的“实例化流程”章节中提到,ExtJS 允许用户在扩展控件类的过程中,通过重写 onRender 方法为控件的构建其 DOM 树,也在 render 方法中提供了对自定义模板绘制控件 DOM 树的支持。
ExtJS 篇:在 ExtJS 的控件设计中,总是将一整个控件拆分为多个小模块,例如 header,toolbar,body,bottombar,footer 等,因此绘制控件 DOM 树时,需要在 onRender 方法中分别绘制控件对象的各个部分,并支持通过配置对象的成员变量,例如 headerCssClass,headerCfg,headerStyle 等,对控件小模块的样式进行微调,增加控件样式的灵活性。
如下代码以 Ext.Panel 为例,展示其 onRender 方法的部分实现:
清单 34. ExtJS 中 Ext.Panel 类 onRender 方法的部分实现
onRender : function(ct, position){ ... // 其他内容略,仅展示部分 DOM 树绘制代码,这里的 dom 为省略代码中获取的一个页面 DOM 节点 this.createElement('header', dom); this.createElement('body', dom); this.createElement('footer', dom); }, createElement: function(name, pnode){ ... // 其他内容略,仅展示根据配置项,改变控件对象的部分 DOM 树的 CSS 样式 if(this[name+'Cfg']){ this[name] = Ext.fly(pnode).createChild(this[name+'Cfg']); }else{ var el = document.createElement('div'); el.className = this[name+'Cls']; this[name] = Ext.get(pnode.appendChild(el)); } if(this[name+'CssClass']){ this[name].addClass(this[name+'CssClass']); } if(this[name+'Style']){ this[name].applyStyles(this[name+'Style']); } } |
以上是从扩展控件的角度,通过重写控件 DOM 树模板来绘制新的控件页面元素。接着让我们来了解下 ExtJS 控件在使用过程中可配置的自定义模板。在创建一个新控件对象时,可以通过配置 tpl,tplWriteMode 以及 data 三个属性来定制控件的 DOM 树。tpl 接收的可以是一个 HTML 模板的 string 对象,也可以是一个基于 string 对象创建的 Ext.XTemplate 对象。在控件的 render 方法中,根据配置不同的 tplWriteMode 属性 ,ExtJS 会选择不同的覆盖模式(前置添加 insertBefore, 后置添加 insertAfter, 整体覆盖 overWrite 等), 最后将 data 参数中的数据填充到 Ext.XTemplate 模板类对象中。
在控件使用的过程中,如果是单次使用自定义摸版,可以直接在建立对象是使用新的模板类:
清单 35. 在创建 ExtJS 控件时,使用模板类的方法
var textField = new Ext.form.TextField({ tpl: '<div><label>{defaultLabel}</label></div>', tplWriteMode:'insertBefore', data: { defaultLabel: 'Default' } }); |
如果需要对某种控件的所有的模板都进行修改,可以使用前文提到过的 Ext.Override 方法重写对应的控件对象:
清单 36. 重写 ExtJS 控件类的模板类相关属性
Ext.Override(Ext.form.TextField, { tpl: '<div><label>{defaultLabel}</label></div>', tplWriteMode: 'insertBefore', data: { defaultLabel: 'Default' } }); |
值得一提的是 ,Ext.XTemplate 类是一个实用性很强的模板类,通过其 tpl 标签配合 for,if 等操作符可绘制出能接受复杂 JSON 对象的模板类,并能在模板类中添加内部方法进行数据处理,举个简单的例子:
清单 37. ExtJS 模板类使用实例
var tpl = new Ext.XTemplate( "<table>", "<tpl for="controls">", "<tr>", // 这里的 {[this.formatValue(values.num)]} 会调用模板类中定义的同名方法 // 其返回值会替换为 input 节点的 value 属性 "<td><span>{values.label}</span> <input value= '{[this.formatValue(values.num)]}' /></td>", "</tr>", </tpl> "</table>", { formatValue: function(num){ var num = num || 0; return new String(num) + "%"; } } ); // 这里的将一个 JSON 数据对象填充到 tpl 模板类中 tpl.update({ controls: [{ label: 'A Percent', num: 10 },{ label:'B Percent', num: 15 }] }); // 生成的 HTML <table> <tr> <td><span>A Percent</span><input value= ’ 10% ’ /></td> <td><span>B Percent</span><input value= ’ 15% ’ /></td> </tr> </table> |
最后,来了解下 ExtJS 控件渲染的核心思想。与 Dijit 不同的是,ExtJS 控件渲染不是通过直接替换 DOM 节点完成的(通过重写 render 方法可以改变这一思想,但不推荐,会破坏 ExtJS 页面元素架构), 大多数情况下都是向控件提供的父节点中添加一个新的 DOM 节点,如果没有指定父节点,或者 ExtJS 找不到控件的父节点,控件就无法被渲染到页面上。正是基于这样一种处理方式,在 ExtJS 控件的 render 过程中,往往会通过寻找并判断控件对象的父对象类型(前提是父对象是一个 ExtJS 控件对象而不是普通的页面标签元素), 来选择不同的模板类进行渲染,举个简单的实例:
清单 37. ExtJS 中不同的父对象对子控件的影响示例
var mainPanel = new Ext.Panel({ layout: 'auto', items: [{ xtype: 'textfield', fieldLabel: 'Field Name' }] }); var formPanel = new Ext.Panel({ layout: 'form', items: [{ xtype: 'textfield', fieldLabel: 'Field Name' }] }) |
上述 2 个 panel 对象看似基本相同,但由于其 panel 的 layout 参数不同,其子对象渲染在 DOM 树中的元素也不尽相同。textfield 对象在渲染时,会查询其父对象的类型,当父对象的 layout 属性为 form 时,其 render 方法会额外添加一套 fieldLabel 模板,放在控件 DOM 节点之前,作为控件的字段标签,而父对象的 layout 属性为 auto 时,该字段标签模板类则不会被加载。
jQuery UI 篇: jQuery UI 没有为控件类提供底层的模板功能支持。一个控件需要在覆写 _create 方法时自行编写构建控件 DOM 树的代码,当然您也可以在该方法中自己实现加载模板文件的功能。其实 jQuery Core 有一个提供类似 Dijit 模板机制的插件 Templates, 然而该插件现已处于 deprecated 状态,且 jQuery UI 并不依赖于该插件,有兴趣的读者可以参阅 http://api.jquery.com/category/plugins/templates/。
最后让我们来看一下各个控件库中控件属性获取/配置方法的实现。
Dijit 篇:在“控件操作”这一节中,可以看到,一个 Dijit 控件可以使用 get/set 方法来获取/配置控件的属性。
清单 38. 获取设置 Dijit 控件属性
// 使用 set 设置控件属性 widget.set(attributeName, value); // 使用 get 获取控件属性 var value = widget.get(attributeName); |
这是因为 dijit._WidgetBase 这个基类为控件实现了 get/set 方法。在调用这两个方法时,如果控件是实现过对应的 _getAttributenameAttr/_setAttributenameAttr 方法 (Attributename 为首字母大写后的属性名 ), 则会调用这两个方法,否则便直接使用默认方法获取/设置控件属性。借助这一特性,可以为某个属性定制其获取/配置方法,如下面的代码为控件定制了 label 属性的配置方法。
清单 39. 定制 Dijit 控件的 label 属性设置方法
_setLabelAttr(/*String*/ label){ // 仅为控件设置 label 属性,这一行不是必须的 this._set("label", label); // 这里添加需要额外执行的代码 ... } |
ExtJS 篇: ExtJS 并没有为控件的每个属性提供对应的 get/set 方法,仅对部分改变后会引起控件逻辑或页面元素变化的属性添加了其对应的 get/set 方法,例如 getHeight,setHeight,getWidth,setWidth 等。大多数情况下,ExtJS 提供的 get/set 方法已满足对控件对象的操作。对于未提供 get/set 方法的属性,可由用户重写控件类来添加对应的公共方法。
清单 40. 为 ExtJS 控件添加 setLabel 方法示例
// 添加对 label 属性的 set 方法 setLabel: function(label){ var label = label || ''; this.label = label; // 添加需要对 DOM 节点改变的额外代码 } |
每个控件的具体属性的获取及配置,可以通过查询 ExtJS API 文档来深入了解。
jQuery UI 篇: jQuery UI 也没有为控件属性提供传统的 get/set 方法,而是通过 option 方法来完成对控件属性的获取/设置功能。
清单 41. 获取 jQuery UI 控件属性
// 创建控件 $("#buttonNode").button({label: "button"}); // 获取 Button 控件属性 $("#buttonNode").button("option"); |
尽管在设置控件属性时我们并没有显式地调用 option 方法而是使用了 button 方法,但由于之前已经使用 button 方法在对应节点上创建过一个 $.ui.button 控件实例,因此,第二次调用 button 方法时等价于取出之前创建的 $.ui.button 控件实例并调用其 option 方法。
清单 42. 设置 jQuery UI 控件属性
// 设置 Button 控件属性,等价于 buttonInstance.option({attributeName: value}); $("#buttonNode").button({attributeName: value}); |
option 方法的具体实现是由 $.Widget 这个基类提供的,如果调用时不传入参数,则该方法直接返回控件实例的所有属性。若以一个属性列表为参数调用该方法,则会继续执行 $.Widget 提供的另一个方法 _setOption 来为控件配置对应的属性。
同样的,用户也可以根据需要对属性的配置方法进行定制。如通过以下方式定制控件 disabled 属性的配置方法。
清单 43. 定制 jQuery UI 控件属性设置方法
_setOption: function( key, value ) { $.Widget.prototype._setOption.apply(this, arguments ); if ( key === "disabled" ) { // 这里添加设置 disabled 属性时需要额外执行的代码 ... } } |
通过本文,相信您已对 Dijit,ExtJS,jQuery UI 有了一定的了解。
与 jQuery UI 相比,Dijit 和 ExtJS 同作为面向对象的 JavaScript UI 库,很多方面的处理方式还是有些类似的,无论是控件的声明,使用方式还是继承体系架构,均能找出两者相似的地方,而 jQuery UI 秉承 jQuery 一贯的传统,无论是使用方式还是代码风格上与前两者都有着较大的差别。深入到实例化流程以及 DOM 树绘制时,我们可以发现,三者均有各自不同的设计思路,从而产生的底层架构上的区别。尤其在 DOM 树绘制部分 ,Dijit 提供了统一的方法为每一个控件类加载其模板,用户仅需在绘制模板时追加部分代码来完成额外的需求,但其样式仅能主题进行统一的控制。 而 ExtJS 中,不同控件的模板类需要用户分别绘制,但其对控件模块的清晰划分,使用户可对控件 CSS 样式进行灵活配置,并有功能强大的自定义模板类支持。相比之下,在 jQuery UI 中,其简便的架构,使用户仅需要考虑 _create 方法的覆写即可,但需要用户通过大量的代码来完成控件的绘制。
虽然由于侧重点的不同使得三者在功能的实现上有所区别,但倘若只是完成用户的某个需求,在 3 个框架中均能找到对应的方法。
笔者希望通过本文,给 3 个框架提供一个横向比较的平台,从各个角度去观察他们在不同方面的表现,也给读者在以后选择 Web UI 框架时,提供一些参考意见。
学习
- 参考 Dijit Reference Guide, 获取更多关于 Dijit 控件的官方文档。
- 参考 Dojo Campus Docs, 获取更多关于 Dojo 的文档。
- 参考 jQuery UI Docs, 获取关于 jQuery UI 的官方文档。
- 参考 ExtJS 3.4 Docs, 获取关于 ExtJS 3.4 的官方文档。
- 查看文章 Upgrade Guide, 了解 jQuery UI 1.7.2 与 jQuery UI 1.8 的区别。
- 查看文章 Understanding jQuery UI widgets: A tutorial, 深入了解 jQuery UI 控件的实现
- developerWorks Web development 专区:通过专门关于 Web 技术的文章和教程,扩展您在网站开发方面的技能。
- developerWorks Ajax 资源中心:这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki 和新闻。任何 Ajax 的新信息都能在这里找到。
- developerWorks Web 2.0 资源中心, 这是有关 Web 2.0 相关信息的一站式中心,包括大量 Web 2.0 技术文章、教程、下载和相关技术资源。您还可以通过 Web 2.0 新手入门栏目,迅速了解 Web 2.0 的相关概念。
- 查看 HTML5 专题, 了解更多和 HTML5 相关的知识和动向。
讨论
- 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。