简介: 当今,各种 JavaScript 框架在前端开发中已经至关普及。Dojo、Ext jQuery 等主流 JavaScript 框架不只提供了一系列核心 API 来屏蔽浏览器差别,简化 DOM 操做、加强 JavaScript 原生 API,还为用户提供了丰富的 UI 控件库来帮助进行敏捷 Web 开发。然而因为各个框架的核心 API 的设计思路不尽相同,其 UI 控件的架构设计也各具特点。您是否想要建立一个本身的 UI 控件,但面对这些框架,而不知如何取舍?您是否长期从事某一种框架的开发,没有时间研究别的框架,却又想了解一样的问题别人是怎么解决的?本文将为您介绍 Dijit、Extjs、jQuery UI 从使用方式到架构实现的异同,展示不一样的设计思路,但愿能为您带来帮助。javascript
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 版本。java
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 版本。node
jQuery UI 是 jQuery 的官方 UI 控件库。jQuery 的大名在业内可谓是无人不知无人不晓。自 2006 年发布了其第一个稳定版以后,其轻量、易用的特色使其深刻人心。jQuery UI 于 2007 年发布,它彻底基于 jQuery 提供的插件机制,提供了底层交互及动画功能以及一些可定制样式的 UI 控件。虽然提供的控件数量很少,但它们都具有了 jQuery 小巧的特色,其使用风格也与 jQuery 核心 API 一致。撰写本文时,其最新的稳定版本为 1.8.16,本文中关于 jQuery UI 的内容都基于该版本。jquery
回页首web
控件的使用方式编程
在讨论各个控件库的架构实现以前,首先让咱们从用户的角度来看看 Dijit、ExtJS、jQuery UI 控件的的使用方式,对它们有一个直观的了解。api
控件的使用无外乎建立控件、操做控件,而在建立控件以前,咱们每每须要先加载控件资源。下面就让咱们从控件资源加载开始聊起(控件 CSS 文件编写与加载不在本文范围内)。数组
Dijit 篇:借助于 Dojo 提供的自动加载模块依赖项的机制,加载 Dijit 控件所需资源很是简单,用户并不须要了解一个控件究竟须要哪些 JS 文件的支持,只需向页面添加 Dojo 核心文件 dojo.js 并使用 dojo.require 函数导入控件对应模块便可。
<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 个文件。
<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 文件。
<!-- 导入 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 控件对象便可。
<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 树中。
<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 属性配置是否自动实例化页面上全部控件。
<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 选项时,能够经过手动方式实例化所须要的控件。
<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 模块。
dojo.require("dojo.parser"); |
然而在使用 Button 控件时,因为咱们已经导入了 dijit.form.Button 模块 ,Dojo 会为咱们自动加载全部该模块依赖的资源,其中就包括 dojo.parser 模块对应的 JS 文件。
ExtJS 篇:ExtJS 控件的生成方式基于使用 new 关键字建立一个 ExtJS 控件对象,将其放入须要渲染的 DOM 节点中。例如:
<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 属性的子控件。
<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 标签的方式建立控件,可是提供了简单的反射机制,使用户可经过描述符来生成控件。
<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 插件经常使用的链式书写方式来建立控件。
<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 等方法获取控件实例。
// 获取 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 方法来获取/设置控件的属性。
// 调用控件方法 widget.someMethod(); // 使用 get 获取控件属性 var value = widget.get(attributeName); // 使用 set 设置控件属性 widget.set(attributeName, value); |
ExtJS 篇:ExtJS 并无像 Dijit 同样提供了经过 DOM 节点查找控件的方法,而是只提供经过控件 id 号获取控件对象的方法。
// 获取控件对象 var button = Ext.getCmp("button"); |
Ext.getCmp 方法返回的就是一个完整的 ExtJS 的控件对象,包含了控件对象的全部变量及方法。与 Dijit 不一样的是,ExtJS 的成员变量大可能是经过使用变量名去获取/设置的,仅对部分属性提供了对应的 get/set 方法(其缘由和内容将在后文的“属性获取/配置方法”章节中具体阐述), 而 ExtJS 的控件方法调用仍是与 Java 的方法调用相似的。
// 获取控件成员变量,直接访问成员变量 var buttonText = button.text; //button 控件为 width 属性添加了 setWidth 方法,以便 width 属性改变后,对 DOM 节点进行处理。 button.setWidth(100); // 调用控件成员函数,相似 Java 的对象方法调用方式 button.focus(); |
jQuery UI 篇:操做 jQuery UI 控件的方式与建立 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 方法,但用户能够经过以下方式获取/设置控件属性:
// 调用 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 控件的声明代码片断
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 为例:
// 其中 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 方法来为该控件类进行全局的注册,只有通过注册的控件类,才能经过描述符由以前提到的反射机制来生成:
Ext.reg('button',Ext.Button); |
Ext.extend 方法除了提供继承的机制来声明新的控件,还能对已有的控件进行扩展,以 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 控件的声明代码片断:
$.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 关键字来建立该控件类的实例了。
<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。该方法在被调用时会根据传入的参数来决定是调用控件的方法,仍是建立控件对象,或是取出已建立的控件对象来修改属性。
在上一节的 Dijit 及 jQuery UI 相关代码实例中,您可能发现了一些名字如下划线"_"开头的方法,这些方法都是私有方法,但在不一样的 UI 库中又略有不一样。
Dijit 篇:Dojo 的代码风格规定如下划线"_"开头的方法为对象私有,然而这仅仅是代码风格上的要求,用户在使用时仍可直接调用 Dijit 控件的这些方法(但不推荐)。
ExtJS 篇: ExtJS 的控件类中,没有直接用来区分私有和公共方法的风格规范,而是经过方法前添加规范的注释进行划分,其形式相似于 Java 中的方法注释。带有注释的成员方法,会被 Ext API 文档收录。经过这种方式,使 API 文档只展现公共变量和方法,以保证普通用户在查询 API 文档时,仅能接触到 ExtJS 提供的公共方法。当须要扩展一个 ExtJS 的控件,对其添加成员变量及方法时,用户可按照 ExtJS 的文档规范进行注释,将须要开放的公共方法收录到 ExtJS 的 API 文档中,有助于他人查询和使用自助开发的控件类。
/** * 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 方法的代码是无效的。
<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 控件:
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 等。
其中最底层的基类是 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 等类来实现拖拽,鼠标事件处理等功能。
除了经过继承的方式,ExtJS 还提供了 Ext.Override 方法供用户重写已声明过的对象,后文的“DOM 树构建”章节中会描述其部分做用。
jQuery UI 篇: jQuery core 并无提供相似 Dojo/ExtJS 的继承机制,可是在 jQuery UI 1.8 中,为了方便扩展控件,开始为控件类引入了相似的继承机制。用户能够通 $.widget 工厂方法在声明控件的同时为其指定父类 ,
$.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 等。
Dijit 篇:前面提到 dijit._WidgetBase 类中定义了一个 Dijit 控件的基本建立流程,在实例化一个控件时会自动执行如下步骤:
其中 dijit._WidgetBase 会为控件暴露出如下三个方法以供覆写。
postMixinProperties:此时控件对象生成完毕,但还未向全局进行注册,且 DOM 节点还未构建。能够经过覆写此方法为控件初始化一些额外的资源。
buildRendering:控件须要实现此方法来为已注册好的控件对象构建 DOM 树。
postCreate:此时控件已经建立完毕,且已绘制到页面中。能够经过覆写此方法来对控件节点进行最后的调整,如绑定事件的回调函数等。
ExtJS 篇: Ext.Component 类做为全部 ExtJS 控件的基类,定义了 ExtJS 控件最基本的建立流程:
其中,提供了 initComponent、onRender、afterrender、initEvents 方法以供子控件重写。其中 onRender 方法是 render 方法中绘制控件默认 DOM 树的环节。
initComponent:经过重写此方法来为控件添加新的初始化参数,添加须要注册的基本事件,并提供接口,容许使用者对该控件基本事件添加的监听器。
onRender:控件须要覆写此方法来提供其对应的 DOM 树模板。
afterrender:此时控件已经建立完毕,并渲染到页面上,能够在此方法中对控件对象进行初始化状态的调整。
initEvents:经过重写此方法来注册用户添加的自定义事件,并为自定义事件添加对应的监听器。
jQuery UI 篇: jQuery UI 控件的建立流程由 $.Widget 定义。具体步骤以下:
其中,控件须要覆写 _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 树。
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 便可。
dojo.declare("dijit.form.Button", dijit.form._FormWidget,{ ... // 模板内容 templateString: dojo.cache("dijit.form", "templates/Button.html"), ... }); |
Dijit 控件的模板中不只仅定义了控件的 DOM 树结构,还容许使用表达式将控件属性直接插入 DOM 树中。如,使用如下模板片断的控件在生成时,会将 id 为 labelNode 的 span 节点的内容替换成控件的 label 属性。
<div> <span id="labelNode">${label}</span> </div> |
此外 ,Dijit 模板还容许使用 dojoAttachNode 属性来为模板中的指定节点设置访问假名。以下面模板片断中 dojoAttachNode 属性为 labelNode 的 span 节点,能够经过控件的 labelNode 属性来获取。
<div> <span dojoAttachNode="labelNode"></span> </div> //widget 为使用上述模板的控件对象 var spanNode = widget.labelNode |
相似的,对模板中的节点添加 dojoAttachEvent 属性能够为其绑定事件回调函数。以下面模板片断中的 dojoAttachEvent 属性为所在的 span 节点的 click 事件绑定了控件的 _onClick 方法做为回调函数。
<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 方法的部分实现:
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 模板类对象中。
在控件使用的过程当中,若是是单次使用自定义摸版,能够直接在创建对象是使用新的模板类:
var textField = new Ext.form.TextField({ tpl: '<div><label>{defaultLabel}</label></div>', tplWriteMode:'insertBefore', data: { defaultLabel: 'Default' } }); |
若是须要对某种控件的全部的模板都进行修改,可使用前文提到过的 Ext.Override 方法重写对应的控件对象:
Ext.Override(Ext.form.TextField, { tpl: '<div><label>{defaultLabel}</label></div>', tplWriteMode: 'insertBefore', data: { defaultLabel: 'Default' } }); |
值得一提的是 ,Ext.XTemplate 类是一个实用性很强的模板类,经过其 tpl 标签配合 for,if 等操做符可绘制出能接受复杂 JSON 对象的模板类,并能在模板类中添加内部方法进行数据处理,举个简单的例子:
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 控件对象而不是普通的页面标签元素), 来选择不一样的模板类进行渲染,举个简单的实例:
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 方法来获取/配置控件的属性。
// 使用 set 设置控件属性 widget.set(attributeName, value); // 使用 get 获取控件属性 var value = widget.get(attributeName); |
这是由于 dijit._WidgetBase 这个基类为控件实现了 get/set 方法。在调用这两个方法时,若是控件是实现过对应的 _getAttributenameAttr/_setAttributenameAttr 方法 (Attributename 为首字母大写后的属性名 ), 则会调用这两个方法,不然便直接使用默认方法获取/设置控件属性。借助这一特性,能够为某个属性定制其获取/配置方法,以下面的代码为控件定制了 label 属性的配置方法。
_setLabelAttr(/*String*/ label){ // 仅为控件设置 label 属性,这一行不是必须的 this._set("label", label); // 这里添加须要额外执行的代码 ... } |
ExtJS 篇: ExtJS 并无为控件的每一个属性提供对应的 get/set 方法,仅对部分改变后会引发控件逻辑或页面元素变化的属性添加了其对应的 get/set 方法,例如 getHeight,setHeight,getWidth,setWidth 等。大多数状况下,ExtJS 提供的 get/set 方法已知足对控件对象的操做。对于未提供 get/set 方法的属性,可由用户重写控件类来添加对应的公共方法。
// 添加对 label 属性的 set 方法 setLabel: function(label){ var label = label || ''; this.label = label; // 添加须要对 DOM 节点改变的额外代码 } |
每一个控件的具体属性的获取及配置,能够经过查询 ExtJS API 文档来深刻了解。
jQuery UI 篇: jQuery UI 也没有为控件属性提供传统的 get/set 方法,而是经过 option 方法来完成对控件属性的获取/设置功能。
// 建立控件 $("#buttonNode").button({label: "button"}); // 获取 Button 控件属性 $("#buttonNode").button("option"); |
尽管在设置控件属性时咱们并无显式地调用 option 方法而是使用了 button 方法,但因为以前已经使用 button 方法在对应节点上建立过一个 $.ui.button 控件实例,所以,第二次调用 button 方法时等价于取出以前建立的 $.ui.button 控件实例并调用其 option 方法。
// 设置 Button 控件属性,等价于 buttonInstance.option({attributeName: value}); $("#buttonNode").button({attributeName: value}); |
option 方法的具体实现是由 $.Widget 这个基类提供的,若是调用时不传入参数,则该方法直接返回控件实例的全部属性。若以一个属性列表为参数调用该方法,则会继续执行 $.Widget 提供的另外一个方法 _setOption 来为控件配置对应的属性。
一样的,用户也能够根据须要对属性的配置方法进行定制。如经过如下方式定制控件 disabled 属性的配置方法。
_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 框架时,提供一些参考意见。