学习使用 Dojo JavaScript 工具箱开发 HTML 小部件的基础知识。本文为您提供了一个简介,此外,还给出了几个例子为开发过程提供帮助 — 以简单的示例小部件开始,随后是复杂一些的小部件,同时还突出介绍了在开发过程中可能遇到的一些常见问题。
简介
本文的目标是为您提供使用 Dojo JavaScript 工具箱开发 HTML 小部件的基础知识,首先从版本 1.0 开始。本文还描述了几个示例,以简单的小部件开始,之后是较为复杂的小部件,同时还会解决在小部件开发过程中经常遇到的一些常见问题。
何为 Dojo 工具箱?
Dojo 是一种基于 JavaScript 的开源工具箱,可用来开发动态 HTML Web 应用程序。借助它,可以快速构建较标准 HTML 小部件更为复杂的小部件。使用 Dojo 提供的组件可以让 Web 用户界面的可用性、响应性和功能性都有所提高。由 Dojo 提供的低层 API 和兼容性层可帮助您编写跨浏览器兼容的应用程序。
开始之前
在开始之前,需要设置开发环境。为此:
- 从 Dojo 项目站点下载最新版本的 Dojo 工具箱(dojo-release-1.x.x-src.zip或dojo-release-1.x.x-src.tar.gz)(相关链接,参见参考资料)。
- 将此归档文件的内容解压缩到一个文件夹,请注意 dojo.js 文件解压缩到的位置。在 dojo.js 载入页面后,软件包系统将负责管理所有模块的加载。
完成之后,文件夹的结构应该类似图 1。
图 1. dojo 文件展开后的文件夹结构
Dijit是位于 dojo 之上的一个小部件系统。通过自身的主题tundra,它为其所有的小部件提供了通用的设计和颜色模式。Dojox是一个开发包,由 Dojo 工具箱的扩展组成。它可用于开发常见集合中所没有的那些功能。
Dojo 小部件
在浏览网站时,会看到数百个小部件呈现在屏幕前。Web 浏览器的每个按钮都是一个小部件。每个文本输入框也是一个小部件。标准的 HTML 提供了小部件的有限集合:一个输入框、一个按钮和一个超级链接。
Dojo 小部件接受像文本输入框这样的条目并会添加一些函数来获得更具用户友好性的对象,比如一个便于选择日期的图形日历。并且在这个过程中,不会破坏新功能所基于的那个原始条目。
一个 Dojo 小部件封装了一些可视 Web 组件以便于重用。它由三个文件定义:
- 一个 JavaScript 文件,内含小部件逻辑
- 一个可选 HTML 文件,为小部件提供一个类似 HTML 的模板
- 一个 CSS 文件,通常对所有小部件通用(主题),内含包含可应用到小部件 HTML 模板的可视样式
导入 Dojo 工具箱
清单 1 显示了可用来将小部件导入到一个常规 Web 页面的基本 HTML 骨架。
清单 1. 将小部件导入到 Web 页面所需的 HTML 代码
<html> <head> <title>Dojo Toolkit Test Page</title> <style type="text/css"> /* CSS style code */ </style> <script type="text/javascript" src="js/dojo1.2/dojo/dojo.js" djConfig="parSEOnLoad:true,isDebug:true"></script> <script type="text/javascript"> /* Javascript code */ </script> </head> <body> /* Widgets definition code */ </body> </html>
第一个脚本标记通过加载 dojo.js bootstrap 文件来初始化 Dojo 工具箱。djConfig对象的parSEOnLoad
和isDebug
属性是两个最为常见的配置选项,Dojo 会在运行时对之进行检查。parSEOnLoad
在加载时处理标记(mark-up)解析,而isDebug
启用或禁用调试消息。djConfig
对象还可以在 dojo.js 文件加载前被设置为一个全局变量:
清单 2. 用 djConfig 设置全局变量所需代码
<script type="text/javascript"> var djConfig = { isDebug:true,parSEOnLoad:true }; </script> <script type="text/javascript" src="js/dojo1.2/dojo/dojo.js"></script>
Dojo 包系统
Dojo 是一个包系统,可用来在文件内构造应用程序类并通过dojo.require
函数加载它们。此函数允许加载在基础dojo.js
内尚未提供的 Dojo 工具箱的某些部分。
为了创建一个小部件,必须通过添加清单 3 所示的代码行来导入这个小部件声明。
清单 3. 导入小部件声明所需代码
<script type="text/javascript"> dojo.require("widgets.Button"); </script>
现在,将如下代码插入主体部分:
<body> <div dojoType="widgets.Button">My Button</div> </body>
dojoType
属性的作用是使 Dojo 工具箱以一种特定的方式管理此标记。在页面加载时,Dojo 解析器会查找在dojoType
属性内指定的小部件声明,初始化这个小部件并用所获得的小部件 DOM 节点替换此标记。
声明一个小部件
现在,让我们来看看这个TextBox
小部件示例,定义一个 JavaScript 文件、一个模板文件和一个 CSS 样式文件。
首先,必须创建这个 JavaScript 文件 TextBox.js,内含此小部件的定义和逻辑(参见清单 4)。
清单 4. JavaScript 文件 TextBox.js 的内容
dojo.provide("widgets.TextBox"); dojo.require("dijit._Widget"); dojo.require("dijit._Templated"); dojo.declare( "widgets.TextBox",[dijit._Widget,dijit._Templated],{ /** the template path */ templatePath: dojo.moduleUrl("widgets","templates/TextBox.html"),/** the input DOM node */ inputNode: null,/** the label */ label: "",/** onkeyup handler */ onKeyUp: function() { // give a chance to the browser to update the DOM setTimeout(dojo.hitch(this,this.validate),0); },/** validate function */ validate: function() { if ( this.inputNode.value === "Ok" ) { // the text is correct dojo.addClass(this.inputNode,"inputOk"); dojo.removeClass(this.inputNode,"inputError"); } else { // the text is incorrect dojo.removeClass(this.inputNode,"inputOk"); dojo.addClass(this.inputNode,"inputError"); } } } );
dojo.provide()定义新的小部件的名称并注册此类声明。注意到:
-
dijit._Widget
和dijit._Templated
是面向 TextBox 小部件的超类 -
templatePath
、inputNode
和label
是小部件的属性 -
onKeyUp()
和validate()
是两个函数,定义了小部件的逻辑
现在,我们可以定义模板文件 TextBox.html,如清单 5 所示。
清单 5. TextBox.html 的内容
<span class="textBox"> ${label}: <input type="text" class="inputOk" dojoAttachPoint="inputNode" dojoAttachEvent="onkeyup: onKeyUp"> </input> </span>
${label}将由此小部件实例的 label 属性替代。
dojoAttachPoint声明会导致 inputNode 小部件的属性被设置为这个输入标记所对应的那个 DOM 节点。
dojoAttachEvent声明会导致onkeyup
事件(来自输入节点)触发onKeyUp
小部件的方法调用。
类textBox
和inputOk
代表的是在 TextBox.css 文件内定义的 CSS 类名。参见清单 6。
清单 6. TextBox.css 内的 CSS 类名
.ibm .textBox { margin: 5px; padding: 5px; background-color: #eee; } .ibm .inputOk { border: 1px solid green; } .ibm .inputError { border: 1px solid red; }
由于类名必须在整个项目内惟一,通常它们都具有一个 CSS 选择器(在本例中为ibm)。
最后,这个小部件可以在 HTML 页面上创建,如清单 7 所示。
清单 7. 在 HTML 页面上创建小部件所需代码
<html> <head> <!-- page title --> <title>TextBox Widget</title> <!-- include the DOJO --> <script type="text/javascript" src="../dojo-1.0.0/dojo/dojo.js" djConfig="isDebug: true,parSEOnLoad: true"> </script> <!-- import DOJO base CSS,DIJIT theme,and widget CSS --> <style type="text/css"> @import "../dojo-1.0.0/dojo/resources/dojo.css"; @import "../dojo-1.0.0/dijit/themes/tundra/tundra.css"; @import "templates/TextBox.css"; </style> <!-- import DOJO stuff --> <script type="text/javascript"> dojo.require("dojo.parser"); <!-- register our module path --> dojo.registerModulePath("widgets","../../widget"); <!-- import our stuff --> dojo.require("widgets.TextBox"); </script> </head> <body class="tundra ibm" style="padding:5px"> <br/> <!-- declare the DOJO widget --> <div dojoType="widgets.TextBox" label="Name"> </div> </body> </html>
图 2. TextBox 小部件
声明式和编程式
Dojo 支持两种编程模型:声明式和编程式。如果需要,两个模型均可用在同一个页面上。在清单 7 中,小部件由一个声明式模型创建。
<div dojoType="widgets.TextBox" label="Name"></div>
同样地,这个小部件也可以用编程式模型创建,如清单 8 所示。
清单 8. 用编程式模型创建 TextBox 小部件
Box"); </script> <script type="text/javascript"> function createWidget() { var widget = new widgets.TextBox(); widget.label = "Name"; widget.startup(); } </script> </head> <body class="tundra ibm" style="padding:5px"> <br/> <!-- declare the DOJO widget --> <script type="text/javascript"> createWidget(); </script> </body> </html>
有关 Dojo 小部件的更多内容
如之前所述,一个 Dojo 小部件就是一个继承自某个特定类(dijit._Widget
类)的 Dojo 类声明。该类定义了小部件的基本行为,提供了由所有小部件实现所共享的一些常用函数。由 _Widget 类实现的最为重要的任务是:
- 定义
create
方法,在小部件实例化时自动调用;此方法执行所有所需的创建步骤 - 定义一些
template
方法,提供面向小部件实现者的特殊 hook,使它们能够在某个特定的创建阶段实现特定的初始化动作 - 定义大量
destroy
方法,用来清除所分配的小部件资源 - 定义
event management
方法,用来关联小部件方法与 DOM 节点/方法调用事件
小部件创建阶段
create
方法执行如下的基本步骤来进行小部件初始化:
如果没有 ID,就为这个小部件创建一个惟一 ID。
所有 Dojo 小部件都必须由惟一 ID 标识。在小部件被初始化时,就可以提供小部件 ID。如果不提供,Dojo 就会创建一个具有如下格式的 ID:
packageName_className_number
例如,一个按扭可以有这样一个自动生成的 ID:dijit_form_Button_0。
packageName是小部件包,点被换成了下划线(例如,dijit_form),而number是一个在小部件的基础上不断变大的数字。
随后,可以用dijit.byId
方法来查找小部件,查找将返回与给定 ID 相对应的小部件对象。注册表是一个存在于 dijit.registry_hash 中的全局空间的一个 Object。使用类似 FireBug(参见参考资料)这样的调试工具深入此注册表可能会对跟踪(比如跟踪漏掉的小部件)有用。
映射属性
小部件属性可以被重新映射到小部件模板的用户定义节点。要映射的属性及映射的目标可在attributeMap
对象内列出。这些对象的每个属性都以如下方法定义映射:
清单 9 给出了一个示例
清单 9. 声明一个 dojo 小部件
dojo.declare( "MyWidget",dijit._Widget,{ // the attribute to map (name and value) title: "myTitle",// the DOM node to be used to set the // title attribute to the "myTitle" value titleNode: null,// the attribute map // the title attribute is mapped to the titleNode attributeMap: {title: "titleNode"},// the template string templateString: "<div><span dojoAttachPoint=\"titleNode\"></span></div>",
得到的小部件模板将是:
<div><span title="myTitle"></span></div>
请注意,由于base _Widget
类已经映射了某些基本的 HTML 属性(如 ID、类、样式),因此实现者不能覆盖attributeMap
属性。相反,它们必须混合基础类attributeMap
属性与它们的值。清单 10 显示了从 _FormWidget
类中检索出来的一个示例。
清单 10. 混合基础类 attributeMap 属性与定制值
attributeMap: dojo.mixin( dojo.clone(dijit._Widget.prototype.attributeMap),{id:"focusNode",tabIndex:"focusNode",alt:"focusNode"} ),
模板方法
在小部件创建阶段,我们要在某些特定的初始化阶段调用一些方法来为实现者提供一个执行一些操作的机会。下面的方法会被调用:
-
postMixInProperties
:这个方法会在所有小部件参数均被设置(混合入)为小部件实例属性后调用。如果基于的是由实例化所提供的那些属性,此时非常适合进行进一步的属性初始化。 -
buildRendering
:在需要为小部件生成呈现时,调用此方法。这个 hook 由dijit._Templated
混入类实现,用 HTML 模板扩展原始的小部件。实现者可潜在地提供它们的实现来完善_Templated
类作业。 -
postCreate
:在小部件的 DOM 准备好并被插入到页面后,调用此方法。此时,这个实现者可以在小部件的 DOM 结构之上操作。子小部件尚未启动。 -
startup
:在小部件上操作的最后时机。此时,子小部件已经启动。 -
uninitialize
:在小部件被消毁时调用。实现者有机会执行一些非自动的清理。
模板方法可以由实现者提供,但必须注意的一点是超类有可能已经实现了其模板方法。要确保对链中所有类的正确初始化,实现者必须手动编写超类方法调用的代码,如清单 11 所示。
清单 11. 超类方法调用的示例
startup: function() { // call the superclass' method this.inherited("startup",arguments); // your code here }
作为一个通用约定,对于所有创建方法(除uninitialize
以外的所有模板方法),超类的方法应第一个被调用以确保在处理超类的条目前它们已被正确地初始化。
uninitialize方法则要遵照相反的规则。
最后一个要注意的是startup
方法。别忘了,若小部件通过声明被创建,startup 方法是通过dojo.parser.parse
方法自动调用的。倘若小部件是由编程方式创建,情况就不一样了,必须像下面例子所示的那样手动完成。
var w = new MyWidget({}); w.startup();
小部件销毁阶段
创建后,小部件将一直存在,直到执行一个显式的销毁请求。实现者负责管理小部件的整个生命周期,包括小部件的销毁。否则,将会导致有遗漏的小部件存在,直到整个页面清理发生。这个小部件提供了四种销毁方法:
- destroyRendering:这个方法用来清理小部件的呈现条目。除非实现者需要的是部分清理,否则该方法往往由其他摧毁方法调用。
-
destroyDescendants
:销毁所有的子小部件。除非实现者需要的是部分清理,否则该方法由destroyRecursive
方法调用。 -
destroy
:销毁所有的小部件条目(不包括子小部件)。 -
destroyRecursive
:销毁小部件条目及子小部件。如果小部件包含有内部小部件,那么必须调用此方法。
在本示例中,小部件包含有内部小部件(例如:在其模板中有一些小部件),所以实现者必须要记得触发子小部件,以便销毁这些子小部件并且无需让小部件用户决定调用何种销毁方法。清单 12 给出了实现此目的一种可能方式。
清单 12. 触发小部件销毁的示例
uninitialize: function() { // destroy the descendants this.destroyDescendants(); // call the superclass' method this.inherited("uninitialize",arguments); }
事件管理
Dojo 小部件可以对来自于 DOM 节点或对象的外部事件做出反应。这类事件可通过使用小部件的connect
方法被手动连接,该方法具有如下所示的签名(非常类似于dojo.connect
方法):
connect: function(/*Object|null*/ obj,/*String*/ event,/*String|Function*/ method);
或者,也可以通过使用dojoAttachEvent
属性在模板中声明此连接的方法进行自动连接。在这两种情况下,此连接都可以由小部件在 _connects 数组内内部跟踪。所有连接在销毁时都将会自动断开。这样,小部件与 DOM 节点间的相互引用就被打破了。
如果,在小部件生命周期内一些连接已经不再需要,那么实现者就可以通过调用disconnect
方法来手动断开这些连接以减少事件的处理。
小部件模板
如前面看到的,小部件必须继承自dijit._Widget
类,它定义并提供了 Dojo 小部件的基本行为。这样一个基类定义了负责构建小部件呈现元素的buildRendering
方法。比如,一个小部件实现者可以用这种方法创建小部件标记并将其设置到小部件 DOM 节点。另一种方案是使用 DOM API 创建小部件结构。在这两种情况下,实现者都必须以某种方式编程实现buildRendering
方法。
Dojo 提供了一个强大的抽象,即dijit._Templated
混入类,可以将小部件呈现定义与小部件行为实现分离开来。想要开发这样一个抽象,实现者需要从_Templated
类继承,如清单 13 所示。
清单 13. 从 _Templated 类继承
dojo.declare( "widgets.MyWidget",dijit._Templated { templatePath: dojo.moduleUrl("widgets","templates/MyWidget.html"),} );
_Templated类提供了它自身的buildRendering
实现,该实现利用了一个类似 HTML 的定义。这个定义有可能存在于两个不同的地方。
- 一个外部文件。在这种情况下,该文件由templatePath属性引用。
- 一个内部字符串属性。在这种情况中,模板直接通过
templateString
属性在小部件内定义。如果指定了templateString
,那么templatePath
可被忽略,即便指定,也是如此。
第一种选择是一种组织小部件源代码的最干净的方法,因为这个标记可被写入一个不同的文件,也可被以一种可读的方式格式化,同时不必费心于字符串的串联,并且字符串分界符不可转义。而第二种选择要更好一些,因为无需从浏览器载入第二个文件。然而,您不必担心,因为 Dojo 提供了一个构建工具,能将外部模板内部化到小部件源代码。
一个模板可以通过引用小部件属性而被参数化。在小部件公开某些能直接影响此标记的配置参数时,或是这个标记必须要依照外部首选项而有条件地生成时,这一点将会很有用。小部件属性通过使用这样的语法引用:${propertyName}
。
模板可以包含其他的小部件声明。不过,为了将它们考虑进去,小部件开发人员必须将widgetsInTemplate
属性设为 true,该属性默认情况下被设为 false 以便跳过并非必需的处理。
模板可以包含如下两种特殊的属性声明:
- dojoAttachPoint:如果指定,这个标记属性必须被设置为一个小部件属性名。对应于此标记的 DOM 将被设置为此小部件的属性。模板内的几个标记可以有一个或多个附加点。
- dojoAttachEvent:这个标记属性会列出在触发 DOM 事件时必须被回调的那些小部件方法。
清单 14 给出了一个模板示例。
清单 14. 特殊属性声明的模板示例
<span class="textInputDefault" dojoAttachEvent="onclick:_onClick"> <img class="textInputIcon" src="${constants.DEFAULT_ICON}" dojoAttachPoint="_iconNode"/> <input class="textInputNode" size="${size}" type="${type}" value="" dojoAttachPoint="_inputNode,_focusNode" dojoAttachEvent="onkeyup:_onKeyUp,onkeypress:_onKeyPress,onchange:_onChange" /> <span class="textInputRight"> </span> </span>
Tivoli Dynamic Workload Console 与 Dojo
Tivoli Dynamic Workload Console(TDWC)是 Tivoli Workload Scheduler(TWS)的图形用户界面。这个小节将展示 TDWC v.8.5 是如何利用 Dojo 的能力的。
Tivoli Workload Scheduler 简介
TWS 是一个生产自动化的解决方案,被设计用来在当今繁杂的操作环境里帮助管理工作量。主要的调度元素包括作业和作业流。作业代表的是一个任务,例如一个可执行的文件、程序或命令。作业流代表的是相关作业的容器,并按运行时、顺序、并行限制和重复性组织这些作业。TWS 用来帮助计划作业的执行,解决相互依赖性,启动并跟踪每个作业。一旦作业的依赖性得到满足,这些作业就会开始;这样,就最小化了空闲时间,并显著改进了吞吐量。作业永远不会打破顺序运行,并且如果一个作业失败了,TWS 可以在操作员少量参予甚至不参予的情况下对之进行恢复。
使用 Dojo
TDWC v.8.5 包括一个功能完善的工作量编辑器,这个编辑器是使用 Dojo Toolkit 完全以 JavaScript 编写的。这种实现使 “智能” 和行为更加贴近用户,让代码尽可能多地在浏览器内运行。
让我们看一个由 TDWC 显示的 TWS 作业流的示例属性面板。图 3 显示了针对作业流 PAYROLL 定义的通用属性的 Web 面板。
图 3. 作业流 PAYROLL 的 Web 面板
在前一篇文章中(“The Abstract User Interface Markup Language Web Toolkit: An AUIML renderer for JavaScript and Dojo”,参见参考资料),我们介绍了如何用 AUIML 工具箱设计面板以及如何用 AUIML Web Toolkit(AWT)在 JavaScript 内实现逻辑代码。图 4 显示了 AUIML 面板:
图 4. AUIML 面板
让我们着重看看Valid from字段。它已经在 AUIML 编辑器中被定义为 Edit Box,Date;AWT 通过清单 15 所示的 HTML 代码连接此元素。
清单 15. 用来连接元素的 HTML 代码
<span type='text' dojoType='ajaxcommon.widgets.DateInputBox' id='validFrom'> <script type='dojo/method'event='onValueChanged'>AWT.dispatchOnChange(this.id);</script> </span>
清单 16 显示了小部件ajaxcommon.widgets.DateInputBox是如何定义的:
清单 16. 定义 ajaxcommon.widgets.DateInputBox 所需代码
dojo.provide("ajaxcommon.widgets.DateInputBox"); dojo.require("ajaxcommon.resources.Images"); dojo.require("ajaxcommon.widgets._DateTimePicker"); dojo.require("ajaxcommon.widgets.picker.PickerInputBox"); dojo.require("ajaxcommon.widgets.picker.DatePicker"); dojo.declare( "ajaxcommon.widgets.DateInputBox",[ajaxcommon.widgets.picker.PickerInputBox,ajaxcommon.widgets._DateTimePicker],{ /** the picker icon */ pickerIcon: ajaxcommon.resources.Images.get()["CALENDAR_ICON"],/** the picker disabled icon */ pickerDisabledIcon: ajaxcommon.resources.Images.get()["DISABLED_CALENDAR_ICON"],/** the picker icon title */ pickerIconTitle: ajaxcommon.resources.Labels.get()["PICK_DATE"],/** constraints */ constraints: {selector: "date",formatLength: "short",wideYear: true},/** * Constructor. */ constructor: function() { // even if the format length is short,ensure the wide year (yyyy) // by overriding the datePattern if (this.constraints.formatLength === "short" && this.constraints.wideYear) { this.constraints.datePattern = this._getWideDatePattern(); } // set the regex for the text this.textRegExp = "^(" + dojo.date.locale.regexp(this.constraints) + “){0,1}$"; // set the regex message for the text this.textRegExpMessage = this._labels.format("DATE_INVALID_VALUE",{example: this._getExampleValue()}); // set the picker class this.pickerClass = "ajaxcommon.widgets.picker.DatePicker"; },/** * Returns the date pattern with the wide-year (yyyy) */ _getWideDatePattern: function() { // get the bundle var bundle = dojo.date.locale._getGregorianBundle(); // get the pattern var pattern = bundle["dateFormat-short"]; // replace the yy to yyyy if not yet yyyy if ( pattern.search(/yyyy/gi) === -1 ) { // the year is not in the wide form pattern = pattern.replace(/yy/gi,"yyyy"); } return pattern; },/** * Returns a string containing an example of accepted value. */ _getExampleValue: function() { // get a sample date object (my birthday!!!) var d = new Date(); d.setDate(15); d.setMonth(4); d.setYear(1971); // format the date in the current locale and return return dojo.date.locale.format(d,this.constraints); } } );
这个构造函数(在清单 17中定义)允许初始化在超类小部件ajaxcommon.widgets.picker.PickerInputBox
内所声明的小部件的属性。
清单 18 给出了相关的小部件模板。
清单 18. 面向 ajaxcommon.widgets.picker.PickerInputBox 的小部件模板
<span class="picker" ><span tabindex="${tabindex}" dojoType="${_textBoxWidget}" dojoAttachPoint="_textNode,_focusNode" required="${required}" size="${size}" minLength="${textMinLength}" maxLength="${textMaxLength}" regExp="${textRegExp}" regExpReference="${textRegExpReference}" regExpMessage="${textRegExpMessage}" minWidth="${minWidth}" ></span ><span dojoAttachPoint="_pickerButtonContainer"> <img tabindex="${tabindex}" src="${pickerIcon}" dojoAttachPoint="_pickerButtonNode" dojoAttachEvent="onclick:_onClick,onkeyup:_onKeyUp" class="pickerButton" title="${pickerIconTitle}" /></span> </span>
图 5 显示了将小部件逻辑委派到浏览器端的好处。这个小部件能够显示相关的错误消息,如果在设定其值时发生错误,还能改变观感。
图 5. 显示了浏览器端小部件逻辑的面板
来源:http://www.ibm.com/developerworks/cn/web/wa-aj-dojotool/