dojo开发指南


1 Dojo 开发指南 版本:1.0 整理者:严锋 运维产品事业部 2 背景 2005 年 5 月,Ajax 概念被第一次提出。而在此一年之前,Dojo 框架已经写下了第一行代码。 作为 Ajax 之前的“Ajax”框架,Dojo 官网至今一直用着朴素的名字来定义自己:javascript toolkit。看上去仅仅是个工具集,而事实上它却有框架的力量,推进着大型 Web2.0 应用的 开发。这也是 Dojo 一直给人的印象,低调、沉稳,却很强大。如果你仅仅想让 WordPress 页面的下拉菜单效果更加酷,我不反对用 JQuery;但如果你需要以 Web2.0 技术为基础去 架构一整个应用,那么 Dojo 一定是你的最佳选择。 为什么选择 Dojo? 看完前一段,一定会有人说,Dojo 给人的印象没有强大,只有庞大,难用以及糟糕的性能。 这完全可以理解。Dojo 文档的缺乏,社区的不活跃,相对严格的设计模式,让很多初学者 难以上手,于是转投用起来更加友好的其它框架。这是一个偏见导致的恶性循环,造成用 Dojo 的人始终不多。而消除偏见,正是这个专栏的目的。 如果我们相信大公司 CTO 的眼光,不妨先看看 Dojo 的这些用户: 这只是 Dojo 用户的一小部分,实际用 Dojo 的公司更多,甚至中国的腾讯,百度。如果觉 得这些公司的技术离自己很远,那么 Java 的 Struct 2 框架内置 Ajax 模块用的是 Dojo,也 许更能说明 Dojo 的价值。 几乎所有的 javascript 框架或者类库都许诺你可以实现任何功能,但是 Dojo 是最具有此类 话语权的类库。在这篇文章中,我们将从十个不同的角度来分享,为什么你应该在你的 web 项目中开始使用 Dojo。 原因一:模块化和 AMD 模块加载机制 随着 web 技术的发展,客户端的 javascript 越来越复杂,为了更好,更快运行我们的 js 应用, 我们需要模块化我们的程序。不使用异步加载的日子应该结束了。Doj 一直都引以骄傲他们 的模块化设计,使用 dojo.require 来动态的加载页面资源。 虽然有一个跨域选项是异步的饿, 缺省的加载 javascript 资源的方法是同步的。 Dojo 已经迁移到了异步加载器上,由 Rawld Gill 开发,可以方便的异步加载所有资源,很大 程度的提高了速度。为了加载 js 资源,你可以使用如下代码: // require 方法指导加载器去加载第一个数组的资源 3 // 如果资源已经被加载,缓存的对象会被使用 require( // 用来加载的模块 ["dojo/on", "dojo/touch", "dijit/form/Button", "dojo/domReady!"], // 一个回调函数用来使用参数来加载模块 // 必须添加到和加载时一样的顺序 function(on, touch, Button) { // 加载后执行其它 }); 为了定义一个模块,使用如下模式: // Using 'define' instead of 'require' because we're defining a module define( // Again, an array of module dependencies for the module we'd like to build ["dojo/aspect", "dojo/_base/declare", "dijit/layout/BorderContainer"] // Again, a callback function which should return an object function(aspect, declare, BorderContainer) { // Return a module (object, function, or Dojo declared class) return declare("mynamespace.layout.CustomBorderContainer", [BorderContainer], { // Custom attributes and methods here }); }) 这里我们使用简单的 define 方法来定义,基本上所有的 AMD 加载器都使用,非常简单的结 构化方式。很像 require,所以使用非常简单。所有列出的有依赖的数组项目都在 callback 运行之前调用。通常 callback 返回一个方法或者对象用来展现模块。一个简单的模式快速加 载,管理模块,允许开发人员加载他需要的部分。 Dojo 的特性丰富的加载器提供了插件,例如 domReady,用来监听 DOM,并且可以检测是 否有相关 JS。 // This code is featured in the dojo/Deferred module define([ "./has", "./_base/lang", "./errors/CancelError", "./promise/Promise", "./has!config-deferredInstrumentation?./promise/instrumentation" ], function(has, lang, CancelError, Promise, instrumentation){ // ... 4 }); 不单单提供了模块化,而且提供了直接使用的加载器。 相关资源: 1.The Dojo Loader 2.Asynchronous Modules Come to Dojo 1.6 3.Defining Modules 原因二:使用 dojo/declare 来实现 Class 和扩展性 javascript 并不是真正的提供 class 系统,而 Dojo 通过 dojo/declare 提供了一个类似 class 继承 机制。declare 在整个框架中都被使用,所以开发人员可以: 1.剪切或者删除重复的代码 2.使用“混合”方式来在不同 class 中共享功能 3.很简单的扩展已存在的 class 用来扩展自定义 4.不同项目中分享模块代码 5.在出现 bug 的 Dojo class 中安全的创建混合的 class Dojo 的 class 系统使用原型继承,允许原型(prototype)被继承,这样子 class 可以像父 class 一样强大。使用 dojo/declare 非常简单: // Of course we need to use define to create the module define([ // Load dojo/declare dependency "dojo/declare", // Also load dependencies of the class we intend to create "dijit/form/Button", "dojo/on", "mynamespace/_MyButtonMixin" // Mixins start with "_" ], function(declare, Button, on, _MyButtonMixin) { // Return a declare() product, i.e. a class return declare( // First argument is the widget name, if you're creating one // Must be in object syntax format "mynamespace.CustomButton", // The second argument is a single object whose prototype will be used as a base for the new class // An array can also be used, for multiple inheritance [ Button, _MyButtonMixin ], // Lastly, an object which contains new properties and methods, or // different values for inherited properties and methods 5 { myCustomProperty: true, value: "Hello!", myCustomMethod: function() { // Do stuff here! }, methodThatOverridesParent: function(val) { this.myCustomMethod(val); // Calling "this.inherited(arguments)" runs the parent's method // of the same, passing the same params return this.inherited(arguments); } } ); }); 以上代码并不能完成一个现实的任务(只是一个简单例子),通过了继承和混合演示了代码 重用。 另外一个使用dojo class系统的优势在于所有属性和方法都是可以自定义的,这里没有option 来限制属性自定义的数量。任何东西都可以被修改或者扩展。 1.Definitive dojo/_base/declare 2.Classy JavaScript with dojo/_base/declare 3.Writing Your Own Widget 原因三:基于方面(Aspects)和“方法到方法的事件” Aspects 是高级 web 开发力最强大和必要的特性。Dojo 提供了很多年这样的相关功能。允许 你不使用传统的 click,mouseover 和 keyup 来触发功能。 允能够让你在触发 function A 之后或者之前触发 function B 。你可以将方法链接起来,是不 是很棒! 如下: // after(target, methodName, advisingFunction, receiveArguments); aspect.after(myObject, "someMethod", function(arg1, arg2) { // Execute functionality after the myObject.doSomething function fires }, true); 保证 function B 在 function A 之前触发。 aspect.before(myObject, "someMethod", function(arg1, arg2) { // This function fires *before* the original myObject.someMethod does 6 }); Aspect 对于使用 Dijit 来创建高级 UI 非常有帮助。针对事件来监听一个组件或者 class 能够触 发另外一个组件的变化,允许开发人员通过使用很多小组件来创建一个大的可控制的组件: var self = this; aspect.after(this.submitButton, "onClick", function() { // The submit button was clicked, trigger more functionality self.showAjaxSpinner(); }); Aspect 相关资源 1.dojo/aspect Documention and Examples 2.Using dojo/aspect 原因四:Deferreds 和统一的 AJAX 传输 Deferreds 是基于对象的异步操作的表达方式,允许异步操作对象可以方便的从一个地方传 递到另外一个地方。jQuery 最近的最重要的添加就是 Deferred。很巧合的是,Dojo 团队已经 实现了。Dojo 几年前已经添加了这个特性,使用它来简化 AJAX 高级操作,动画及其其它。 除了最前面这里的 Deferred 对象,Dojo 也在 XMLHTTPRequest 之外首次添加了几个 IO 处理 方法,包括:window.name 封装,AJAX 文件上传的 dojo/io/iframe 及其其它。那么什么时候 使用 Deferredd 对象呢?无论什么时候只要一个异步的 AJAX 操作发生!Deferred 对象都会返 回 XHR 请求,dojo/io 请求,动画和其它! // Fire an AJAX request, getting the Deferred in return var def = xhr.get({ url: "/getSomePage" }); // Do lots o' callbacks def.then(function(result) { result.prop = 'Something more'; return result; }).then(function(resultObjWithProp) { // .... }).then(function() { // .... }); 那么 dojo/io/iframe 什么样子呢? 7 require(["dojo/io/iframe"], function(ioIframe){ // Send the request ioIframe.send({ form: "myform", url: "handler.php", handleAs: "json" // Handle the success result }).then(function(data){ // Do something // Handle the error case }, function(err){ // Handle Error }). then(function() { // More callbacks! }) }); Dojo 中使用 Deferred 的好处自傲与每一个 AJAX 操作,不管什么方法,你都能够返回得到一 个 deferred,加速了开发和 API 整合。Dojo1.8 将看到 dojo/request 的介绍,一个新的 AJAX 方法。这里有些例子将展示如何使用 dojo/request API: // The most basic of AJAX requests require(["dojo/request"], function(request){ request("request.html").then(function(response){ // do something with results }, function(err){ // handle an error condition }, function(evt){ // handle a progress event }); }); Deferred 和 AJAX 资源 1.dojo/request 2.Ajax with Dojo 3.Getting Jiggy with JSONP 4.Dojo Deferreds and Promises 8 原因五:Dijit UI framework 毫无疑问,Dojo 相比其它框架最大的优势在于 Dijit UI 框架。这个和其它工具吹嘘的完全不 同: 1.完整,完整的支持本地化 2.完整的 accessiblity 3.先进的布局组件能够帮助你解决 100%高度元素,创建自定义的分割和布局修改 4.内建的表单验证和强化的用户体验 5.很多主题,最新的叫“claro” 6.LESS 文件自定义主题 7.非常模块化的代码,允许自定义和扩展 Dijit 同时允许你宣示性的定义和程序化的创建组件,如下:
传统的 js 创建如下: require(["dijit/form/Button"], function(Button) { // Create the button programmatically var button = new Button({ label: 'Visit GBin1.com!' }, "myNodeId"); }); Dijit UI 资源 1.The Famous Dijit Themetester 2.Creating Template-based Widgets 3.Layout with Dijit 4.Dijit Themes, Buttons, and Textboxes 5.Dijit Tutorials 9 原因六:Dojo Mobile 可以这么说,基本上每一个 web 问题,dojo 都有一个解决方案,Dojo 对于移动客户端的解 决方案就是 dojox/mobile,这个类库在我们以前的 dojo 移动开发文章中我们也牛刀小试了一 把。如果你想看看 dojox/mobile 开发的 UI 界面,请点击这里:在线演示,dojox/mobile 主要 特性如下: 1.自动探测设备类型 2.拥有 iOS,Andriod,Blackberry 和 common 这四种主题 3.mobile 表单组件 4.布局组件和面板 5.支持桌面,允许简单的 debug 移动组件可以使用宣示性或者程序化的方式实现。和 Dijit 组件类似。Mobile 视图可以滞后 加载并且不同的视图都是无缝切换,HTML 很简答如下: 10 GBin1 Mobile Website 使用 dojox/mobile/deviceTheme,你可以自动检测到客户的主机类型并且应用正确的主题: // Will apply the device theme base on UA detection require(["dojox/mobile/deviceTheme"]); 使用主题后,我们选择使用的组件,或者自定义 class,如下: // Pull in a few widgets require([ "dojox/mobile/ScrollableView", "dojox/mobile/Heading", "dojox/mobile/RoundRectList", "dojox/mobile/TabBar", "dojox/parser" ]); 一旦 javascript 资源请求后,我们就开始添加一些列的视图和组件了。

Tweets

  • Tweet item here
11

Mentions

  • Mention tweet item here

Settings

Show

  • Setting item here
使用移动的组件和使用 Dijit 的方法非常类似。而且整个过程非常简单! dojox/mobile 资源 1.Getting Started with dojox/mobile 2.dojox/mobile tests 3.使用最新版本 Dojo1.7 的 dojox/mobile 开发移动设备 web 应用 原因七:GFX 和图表 CSS 动画是不错的视觉工具,既是动画图片,同时也是灵活和强大的矢量图形创建和管理工 具。最流行的客户端的矢量图形生成工具一直是 Raphael JS,但是 Dojo 的 GFX 类库毫无疑问 更将的强大。GFX 可以用来配置 SVG,VML,Silverlight,Canvas 和 webGL 格式的矢量图形。 提供了一个健壮的封装来创建不同的矢量图片形状(线状图等等),包括: 1.改变大小,旋转和偏转 2.动画填入,拉直等属性 3.添加线或者圆圈图形到指定图形中 4.监听和响应鼠标属性 5.组合图形并且更好的去管理 创建代码如下: 12 require(["dojox/gfx", "dojo/domReady"], function(gfx) { gfx.renderer = "canvas"; // Create a GFX surface // Arguments: node, width, height surface = gfx.createSurface("surfaceElement", 400, 400); // Create a circle with a set "blue" color surface.createCircle({ cx: 50, cy: 50, rx: 50, r: 25 }).setFill("blue"); // Crate a circle with a set hex color surface.createCircle({ cx: 300, cy: 300, rx: 50, r: 25 }).setFill("#f00"); // Create a circle with a linear gradient surface.createRect({x: 180, y: 40, width: 200, height: 100 }). setFill({ type:"linear", x1: 0, y1: 0, //x: 0=>0, consistent gradient horizontally x2: 0, //y: 0=>420, changing gradient vertically y2: 420, colors: [ { offset: 0, color: "#003b80" }, { offset: 0.5, color: "#0072e5" }, { offset: 1, color: "#4ea1fc" } ] }); // Create a circle with a radial gradient surface.createEllipse({ cx: 120, cy: 260, rx: 100, ry: 100 }).setFill({ type: "radial", cx: 150, cy: 200, colors: [ { offset: 0, color: "#4ea1fc" }, { offset: 0.5, color: "#0072e5" }, { offset: 1, color: "#003b80" } ] }); }); 13 生成形状如下: 基于 GFX 的强大类库就是 dojox/charting。通过图表来展示视觉化的数据是非常不错的选择。 dojox/charting 提供了如下功能: 1.多个图表 2.动画图形元素 3.插件支持,包括 MoveSlice(动画饼图),提示工具条,缩放和高亮 4.自更新的图表,由 Dojo data store 支持 一个基本的饼图如下:
15 当然这里有很多其它的例子。 dojox/gfx 和 dojox/charting 资源 1.Vector Graphics with Dojo’s GFX 2.Interactive AJAX London Logo 3.Dojo Charting 4.Advanced Charting with Dojo 5.Dojo GFX Demos 16 原因八:SitePen dgrid Sitepen,Dojo 创始人 Dylan Schiemann 的 javascript 咨询公司,打算替换 Dojox 的华而不实的 Grid widget,使用更快,可扩展并且可编辑的 Grid 组件,主要特性如下: 1.支持不同的主题,配置简单 2.支持移动 3.行可排序 4.允许“滞后加载”grid 数据 5.支持树状的 Grid 6.使用 Dijit widget 支持可编辑的 Grid 7.可扩展,支持列宽改变,拖放支持和分页,及其其它 SitePen 已经做了很大的努力,取得了很大成功。 dgrid 资源 1.dgrid Homepage 2.dgrid iTunes Demo 3.SitePen Blog 4.dgrid GitHub repo 5.dgrid Documentation 原因九:DOH 测试框架 测试对于我们来说非常重要,特别是对于客户端而非服务器端的程序来说。随着不同的浏览 器的出现,客户端的互动测试成为一个必须的内容和步骤。Dojo 提供了自己的测试框架。 别名 DOH(Dojo objective Harness)。提供了每一个 Dojo 版本的下载。测试非常简答和明了: // Declare out the name of the test module to make dojo's module loader happy. 17 dojo.provide("my.test.module"); // Register a test suite doh.register("MyTests", [ // Tests can be just a simple function... function assertTrueTest(){ doh.assertTrue(true); doh.assertTrue(1); doh.assertTrue(!false); }, // ... or an object with name, setUp, tearDown, and runTest properties { name: "thingerTest", setUp: function(){ this.thingerToTest = new Thinger(); this.thingerToTest.doStuffToInit(); }, runTest: function(){ doh.assertEqual("blah", this.thingerToTest.blahProp); doh.assertFalse(this.thingerToTest.falseProp); // ... }, tearDown: function(){ } }, // ... ]); 上面的测试是一个非常简单的 Dojo 测试例子,如果面对更复杂的情况,例如,异步操作。 比如是 AJAX 请求,DOH 提供了一个非常简单的方式来测试。Deferred 对象: { name: "Testing deferred interaction", timeout: 5000, runTest: function() { var deferred = new doh.Deferred(); myWidget.doAjaxAction().then(deferred.getTestCallback(function(){ doh.assertTrue(true); }); return deferred; } } 以上测试代码中,getTestCallack 不会调用直到 doAjaxAction 完成,并且返回了成功或者失败。 DOH 资源 1.DOH Tutorial 2.Nightly Tests 18 原因十:Dojo 编译流程 当一个 web 应用准备部署时,对于创建一个压缩版的 javascript 对于加载速度和优化来说非 常有必要。这有效的减少了请求次数,并且缩短了下载时间。Dojo 的编译分析 Define 将调 用并且检测依赖关系。使用 Dojo 编译流程,你需要创建一个 build profile。它可以包含任何 层次或者更加复杂,下面是一个例子: var profile = { releaseDir: "/path/to/releaseDir", basePath: "..", action: "release", cssOptimize: "comments", mini: true, optimize: "closure", layerOptimize: "closure", stripConsole: "all", selectorEngine: "acme", layers: { "dojo/dojo": { include: [ "dojo/dojo", "app/main" ], customBase: true, boot: true } }, resourceTags: { amd: function (filename, mid) { return /\.js$/.test(filename); } } }; Dojo 编译流程可自定义,允许: 1.压缩 2.如果创建组件的话,指定应用到 CSS 的压缩层次 3.输出 4.指定选择器引擎 5.更多 Build profile 通过命令行来运行(最近为 Node.js 重写),提供了不同的选项来重写或者完成 设置,下面一个例子: ./build.sh --profile /path/to/app/app.profile.js --require /path/to/app/boot.js Dojo 编译流提供了超棒的控制编辑文件的功能,帮助你压缩 CSS 和 Javascript,这样你的 Dojo 程序将能够随时准备上线! 1.Creating Builds 2.The Dojo Build System 3.Legacy Build Documentation 19 原因十一:Dojo 的宝藏,更多 Dojox 这里有很多的 Dojo 类库供你使用,相信你能找到你需要的所有功能。 1.extra layout and form widgets for Dijit 2.advanced, localized form validation routines 3.WebSocket and long-polling wrappers 4.image widgets, including lightbox, slideshow, and gallery utilities 5.advanced IO helpers 6.advanced drag and drop libraries 7.Nodelist extensions 8.Basic JavaScript language and helper utilities 9.Advanced Javascript language and AJAX utilties 10.On-demand asynchronous script loading 11.A complete UI framework 12.A comprehensive testing suite 13.Build tools 14.更多 性能其实很优秀 性能很难对比,即使相同的一个 Calendar 控件,功能都会有差别。这里从 2 个点来看性能 问题:DOM 节点的查询速度,以及 Dijit 展现的速度。前者大家的功能完全相同,可以有个 比较;后者则专属 Dojo,看看绝对的性能。 (1)节点查询速度 JQuery 最早引入基于 CSS 选择器进行 DOM 节点查找的机制,也是其最大特征。如今成为 了各个框架都具备的功能。这应该算是最能体现 Javascript 算法设计的一点。下图来自于: http://blog.creonfx.com/javascript/mootools-vs-jquery-vs-prototype-vs-yui-vs-dojo-compar ison-revised 是各个框架查询速度的对比,时间越少表示性能越好。虽然用的版本比较老,但在查询的实 现上各个框架都没有太大的变化。 20 一图胜千言,Dojo 的性能优势一目了然。如果你想亲自做这个测试,可以访问如下地址。 http://mootools.net/slickspeed/ (2)Dijit 展现速度 Dijit 是 Dojo 的界面展现体系,性能好坏直接决定着页面的响应速度。在声明方式下,Dojo 需要遍历页面 Html 找到所有的 Dijit 进而创建并展现它们。这个过程比你的想像要快的多, Dojo 的 ThemeTester 是一个很好的例子: 它的访问地址是: http://archive.dojotoolkit.org/nightly/dojotoolkit/dijit/themes/themeTester.html 21 此页面包含了 399 个 Dijit,全部通过声明方式创建。在 FireFox3.6 下总共的展现时间是 2000ms 左右,平均一个 Dijit 用时 5ms。页面中包括了多个 Tree,多个 TabContainer,多 个 Menu 以及富文本编辑器这样的复杂 Dijit。因此,如果你发现你的界面远没有这么复杂, 却展现的很慢,通常需要检查自定义的 Dijit 设计的是否合理。 综上所述,Dojo 本身,包括自带的 Dijit,拥有着相当不俗的性能。至于如何写出高性能 Dijit, 正如如何写出高性能的 JQuery 插件,需要的是经验和积累。 性能是软件开发的一个永恒话题,无处不在。没有一个统一的解决模式,桌面程序也好, Web 程序也好,要提高性能,合理的设计、高效的算法永远是解决问题的王道。而 Dojo 在 这一点上,绝不会是你的绊脚石。 Dojo 是一个强大的面向对象 JavaScript 框架。主要由三大模块组成:Core、Dijit、DojoX。 Core 提供 Ajax,events,packaging,CSS-based querying,animations,JSON 等相关操作 API。Dijit 是 一个可更换皮肤,基于模板的 WEB UI 控件库。DojoX 包括一些创新/新颖的代码和控件: DateGrid,charts,离线应用,跨浏览器矢量绘图等。 补充 Dojo 有以下的特征: 1、利用 Dojo 提供的组件,你可以提升你的 web 应用程序可用性、交互能力以及功能 上的提高。 2、也可以更容易的建立互动的用户界面。同时 Dojo 提供小巧的动态处理工具。 3、利用它的低级 API 和可兼容的代码,能够写出轻便的、单一风格(复杂)的 JavaScript 代码。Dojo 的事件系统、I/O 的 API 以及通用语言形式是基于一个强大编程环境。 4、通过 Dojo 提供的工具,你可以为你的代码写命令行式的单元测试代码。 5、Dojo 的扩展包能够使你自己的代码更容易维护,耦合性更低。 22 Dojo 支持几乎所有现代的浏览器,官方正式支持并经过测试的浏览器包括: Chrome 5, Firefox 3.5 and 3.6;Internet Explorer 6, 7, and 8; Opera 10.6 (Dojo Core only); Safari 4.1and 5. 其 他的浏览器版本虽然未经官方正式声明支持,在绝大多数情况下 Dojo 也可以在上面运行的 很好。 通过 Dojo 提供的工具,您还可以为代码编写命令行式的单元测试代码。 Dojo 的打包工具可以帮助您优化 JavaScript 代码,并且只生成部署应用程序所需的最 小 Dojo 包集合。 入门简介 Dojo 体系架构 Dojo 的体系架构如图1所示,总体上来看,Dojo 是一个分层的体系架构。最下面的 一层是包系统,Dojo API 的结构与 Java 很类似,它把所有的 API 分成不同的包 (package),当您要使用某个 API 时,只需导入这个 API 所在的包。包系统上面一层 是语言库,这个语言库里包含一些语言工具 API,类似于 Java 的 util 包。再上一层是环 境相关包,这个包的功能是处理跨浏览器的问题。 23 图 1. Dojo 体系架构图 Dojo 大部分代码都位于应用程序支持库,由于太小限制,图 1 中没有列出所有的包。 开发人员大部分时候都在调用这个层中的 API,比如,用 IO 包可以进行 Ajax 调用。 最上面的一层是 Dojo 的 Widget 系统,Widget 指的是用户界面中的一个元素, 比如按钮、进度条和树等。 Dojo 的 Widget 基于 MVC 结构。它的视图作为一个 Template(模板)来进行存放,在 Template 中放置着 HTML 和 CSS 片段,而控制 器来对该 Template 中的元素进行操作。 Widget 不仅支持自定义的样式表,并且能够 对内部元素的事件进行处理。用户在页面中只需要加入简单的标签就可以使用。在这一层中, 存在数百个功能强大的 Widget 方便用户使用,包括表格、树、菜单等。 24 常用包介绍 Dojo 提供了上百个包,这些包分别放入三个一级命名空间:Dojo,Dijit 和 DojoX 。 其中 Dojo 是核心功能包 , Dijit 中存放的是 Dojo 所有的 Widget 组件,而 DojoX 则 是一些扩展或试验功能,DojoX 中的试验功能在成熟之后有可能在后续版本中移入到 Dojo 或 Dijit 命名空间中。 由于 Dojo 包种类繁多,下面只列举了最常用的一些包及其功能,以方便读者有个初 步了解或供以后查阅。 包名 功能 dojo.io 不同的 IO 传输方式。 script、IFrame 等等; dojo.dnd 拖放功能的辅助 API 。 dojo.string 这个包可以对字符串进行如下的处理:修整、转换为大写、编码、esacpe、 填充(pad)等等; dojo.date 解析日期格式的有效助手; dojo.event 事件驱动的 API,支持 AOP 开发,以及主题 / 队列的功能; dojo.back 用来撤销用户操作的栈管理器; dojo.rpc 与后端服务(例如理解 JSON 语法的 Web 服务)进行通信; dojo.colors 颜色工具包; dojo.data Dojo 的统一数据访问接口,可以方便地读取 XML、JSON 等不同格 式的数据文件; dojo.fx 基本动画效果库; dojo.regexp 正则表达式处理函数库; dijit.forms 表单控件相关的 Widget 库; dijit.layout 页面布局 Widget 库; dijit.popup 这个包用于以弹出窗口方式使用 Widget ; dojox.charting 用于在页面上画各种统计图表的工具包; dojox.collections 很有用的集合数据结构(List、Query、Set、Stack、Dictionary...); dojox.encoding 实现加密功能的 API(Blowfish、MD5、Rijndael、SHA...); dojox.math 数学函数(曲线、点、矩阵); dojo.reflect 提供反射功能的函数库; dojox.storage 将数据保存在本地存储中(例如,在浏览器中利用 Flash 的本地存储 来实现); 25 dojox.xml XML 解析工具包; Dojo 的安装 Dojo 的安装不需要特别的环境和配置,只需要将 Dojo 包下载,解压并将其放在自 己喜欢的位置就可以。 第一步,下载 Dojo 包。 图 2. Dojo 包的下载 登陆 Dojo 官方网站的下载页面(参见 参考资料),点击其上如图 2 中所示的 “ Download Now ”按钮。也可以选择点击页面右边的“All releases ”,查看当前 Dojo 所 有的版本,并尝试下载使用。 第二步,解压下载下来的软件包,并将其放在合适的位置。 在 Windows 环境下,可以使用 WinRAR 将下载下来的 Dojo 包解压。在 Linux 环境下,最好是下载后缀名为 tar.gz 的包,可以使用“tar -zxvf Dojo 压缩包文件名”命 令解压 Dojo 包。 如果只是尝试使用 Dojo,不需要牵涉到与服务器端的通信,可以将 Dojo 放在任何 方便的位置。但最好这个位置能方便其测试页面引用 Dojo 。比如假设测试页面 test.html 的放置位置为 D:\test\test.html,则 Dojo 包最好也放置为 D:\test\dojo,以方便 test.html 引用 Dojo 。如果牵涉到 Dojo 在服务器端的放置,则 Dojo 最好放在对应服 务器的 Web 根目录下。比如假设目前 Appach 服务器的 Web 访问目录为 “/home/web/webapp/ ”, 则 Dojo 包最好直接放置于其下,以避免出现跨域问题和方便 服务器上的多个 Web 项目引用 Dojo 。 26 第三步,测试 Dojo 是否运行正常。 图 3. 检测 Dojo 是否运行正常 使用浏览器打开 dojo_path/dijit/themes/themeTester.html,如果页面的运行效 果如图 3 所示,则说明 Dojo 运行正常。 需要说明的是 dojo_path 在本系列文章中有两个不同的代表意义。 第一表示 Dojo 包在系统中所处的绝对位置。例如如果 Dojo 包中 dojo.js 文件在 系统中的位置为 D:\test\dojo\dojo\dojo.js,则此时 dojo_path 所代表的为 D:\test\dojo。 第二表示页面与 Dojo 包的相对位置。例如如果页面的位置为 D:\, 而 Dojo 包的位 置为 D:\test\ 。要在页面中使用 Dojo,首先需要引入 Dojo,其实际引入的语句为 。这 里的 dojo_path 表示的是页面与 Doj 包的相对位置,因此其所代表的为 ./test/ 。对于 本系列后面章节的实例,将会出现很多使用 dojo_path 的情况,如果要运行这些实例, 请将其中的 dojo_path 替换为真实情况的值。 除了通过下载 Dojo 包来使用 Dojo 以外,Dojo 组织还提供了另外一种方法来引入 Dojo 进行使用。如果不希望下载 Dojo 包而尝试对 Dojo 的使用,可以直接通过引入美 国一个在线服务器主机上的 Dojo 来实现。与下载 Dojo 使用的不同点只是 dojo_path 27 应替换为“http://o.aolcdn.com/dojo/1.0.0”。例如要引入 dojo.js 文件,则其实际引 入语句为: Dojo 版的 Hello World 前面,我们对 Dojo 的下载和安装进行了介绍。接下来,我们将通过一个 Dojo 版的 Hello World 示例来了解如何初步使用 Dojo 工具包。本节中,将通过一个客户端登陆验 证的例子来进行讲述。 清单 1. 客户端登陆验证示例 test UserName:
PassWord:
Submit
首先建立一个 test.html 的文件,在将清单 1 中的 dojo_path 根据自己的实际情 况进行修改后,把清单 1 中的代码拷贝到 test.html 中,并双击运行。在输入框中分别 输入“ goodguy ”和“ goodgoodstudy ”,点击 Submit 按钮则会得到一个“ Dojo World Welcome you !”的一个弹出窗口。如果在两个输入框中输入的是其它内容,则点击 Submit 按钮就会得到“ Dojo does not like you! ”的一个弹出窗口。 下面是对关键代码的解释: djConfig="parseOnLoad: true" 表示在页面加载完成以后,启用 Dojo 的解析模 块对页面中的 Dojo 标签属性(Dojo 标签属性是指由 Dojo 定义的一些标记,这些标记 只有在被处理以后,才能为浏览器识别执行)进行解析。djConfig 是使用 Dojo 页面的一 个全局配置参数。通过对这个参数不同的赋值,可以控制页面中 Dojo 的解析模块是否运 行, Dojo 的调试模块是否工作等。 @import "dojo_path/dijit/themes/tundra/tundra.css" 表示引入 Dojo tundra 风格的层叠样式表。 29 dojo.require("dojo.parser") 表示引入 Dijit 的解析功能模块。该模块将会把 Dojo 标签属性替换成浏览器可以识别执行的标记。需要与 djConfig="parseOnLoad:true" 相区别的是,djConfig="parseOnLoad:true" 表示确 定在页面加载完成以后执行解析功能,但解析功能模块的引入要靠 dojo.require("dojo.parser") 来实现。 dojo.require("dijit.form.TextBox") 和 dojo.require("dijit.form.Button") 表 示引入 Dojo 风格的文本输入框和按钮的功能模块。 dojo.connect(dijit.byId("mybutton").domNode, "onclick", "login") 表示将按 钮的点击事件和 login 函数联系起来,当点击 id 为 mybutton 的按钮时,执行 login 函数。 dijit.byId("myname").setValue("") 表示调用 id 为 myname 的 Dojo 文本 框的 setValue 函数,将文本框里面的内容清为空。 中的 dojoType="dijit.form.TextBox" 表示在页 面中文本输入框是 Dojo 风格的。需要注意的是,通过声明 dojoType="dijit.form.TextBox" 这种方式来实现某些 Dojo 功能的使用,其表现形式 上如同声明一个 HTML 标签的属性(如同 width="10px"),因此在本文中称其为 Dojo 标签属性。在页面加载完成以后,Dojo 的解析模块会将 Dojo 标签属性转化为浏览器能 够识别执行的标记。 如果读者希望了解如何写出自己的第一个基于 Dojo 的 Ajax “ Hello World ”,可 参阅本系列文章的第二部分。 Dojo 的调试 就本系列文章写作期间,还没有专门用于支持 Dojo 调试的工具。但考虑到 Dojo 的 本质是一套 Javascript 的 Toolkit,因此使用 Javascript 的调试工具也能较好的满足 Dojo 应用调试的需求。 目前较为常见的浏览器有 IE,Firefox 和 Safari 。但这些浏览器在标准上都有一些 差异,因此就目前的情况找到一个能在各个浏览器中通用的优秀调试工具显然不大可能。不 同的浏览器往往有不同调试工具。因为笔者在实际的工作中最为熟悉的调试工具是 Firefox 下的 Firebug 。本文将以 Firebug 作为最主要的 Dojo 应用调试工具进行讲述。 安装 Firebug 第一步,点击 Firefox 浏览器上的“工具”选项,然后点击“附加软件”,在弹出的小窗 口中,点击右下角的“获取扩展”选项,如图 4 所示。 30 图 4. 获取扩展 第二步,在点击“获取扩展”选项后,打开的页面中搜索 Firebug,在搜索结果的页面 中,下载 Firebug 。需要注意的是,Firebug 的版本要与 Firefox 的版本相兼容。如果 要查看自己 Firefox 的版本,可通过点击浏览器“帮助”选项下“关于 Mozilla Firefox ”选 项。 第三步,由于目前 Firebug 直接支持的是 Firefox3.0,而本书作者的 Firefox 是 Firefox2.0,因此需要从 Firebug 的老版本中找到合适的 Firebug 版本。 Firebug 的 下载网页会判断您的浏览器版本。如果尝试下载安装不合适的 Firebug 版本时,其链接是 失效的。与本书作者浏览器版本相对的最佳 Firebug 下载页面如图 5 所示。其余的一些 老版本的 Firebug,只要可以下载的,一般来说,也就是可以使用的。 图 5. 下载合适的 Firebug 了解 Firebug 调试功能 31 第一步,启动 Firebug 。 Firebug 安装完成以后,有的 Firebug 版本会在浏览器 的右下角产生一个特殊的小图标。点击这个小图标,就可以打开 Firebug 的调试窗口。如 果浏览器的右下角没有小图标,则可以通过浏览器菜单中的“工具”>“Firebug”>“Open Firebug”打开 Firebug 的调试窗口。 Firebug 打开以后,会在浏览器的下端出现如图 6 所示的窗口。 图 6. Firebug 调试窗口 第二步,Console 窗口的使用。 Console 窗口除了显示页面加载的文件以外,还可 以直接显示页面中的代码错误和一些输出信息。 如果将清单 1 代码中的 dojo.require("dijit.form.Button") 删除,就会在 Console 窗口中出现如图 7 所示的红色错误警告。 图 7. Console 窗口中显示的代码错误 32 除了看到页面的运行信息以外,还可以直接在 Console 窗口中输出调试信息。 Firebug 支持的调试语句较为常用的是 console.log 。 在清单 1 的 init 函数中加入一条语句 console.log("I am Firebug!"),然后刷新 页面则可以在 Console 窗口中看到如图 8 所示的输出。 console.log 除了可以直接将 字符串输出以外,还可以使用如 C 语言的 printf 一样的格式控制进行输出。 图 8. 输出调试信息 在清单 1 的 init 函数中加入清单 2 中的代码,然后刷新页面则可以在 Console 窗口中看到如图 9 所示的输出。 清单2 var num = 222; console.log("My test num is %d",num); console.log(num,window,login); 33 图 9. 输出调试信息的高级使用方法 此外,为了方便将不同类别的调试信息输出进行区别(比如错误信息和警告信息), 可以使用另外四种调试输出语句。 在清单 1 的 init 函数中加入清单 3 中的代码,然后刷新页面则可以在 Console 窗口中看到如图 10 所示的输出。 清单 3 console.debug("I am debug"); console.info("I am info"); console.warn("I am warn"); console.error("I am error"); 34 图 10. 另外四种调试输出语句 有 Javascript 经验的读者可能会习惯于使用 alert() 进行调试信息的输出,但是笔 者认为将调试信息在 Console 窗口中输出是一个更优的选择。首先,如果页面有很多 alert(), 则点击“ OK ”让弹出框消失也是一个非常烦人的事情。其次,如果调试信息的量 很大,则是用 alert() 的弹出窗口将无法良好的完整展示调试信息。接着,alert() 无法查 看对象和数组的细节信息。最后,如果在一个循环中使用 alert(), 将很容易造成页面“死 掉”。 第三步,HTML 窗口的使用。在 HTML 窗口中可以查看到页面的源代码。除了可以 查看页面的源代码外,还可以使用 Firebug 的编辑功能直接对页面进行编辑。 图 11. 使用 Firebug 的编辑页面功能 35 如图 11 所示,在处于 HTML 窗口的模式下,点击 Edit 按钮,将切换查看模式到 编辑模式。需要注意的是,在使用 Edit 模式前,最好如图 11 先提前选中页面的 body 代 码区块。 图 12. 使用 Edit 后的页面效果 如图 12 所示,在 Edit 模式下,在页面代码的最后加上了字符串“ bbbb ”,然后在 页面的相应位置也直接显示了字符串“ bbbb ”。需要注意的是,在页面新加完代码后,需 要点击一下页面的其它任何地方,则其效果才会在页面上加载。 36 图 13. HTML 模式下的 Style,Layout,DOM 三窗口 如图 13 所示,在 HTML 窗口模式下,与 HTML 窗口相对应,在其右边有 Style, Layout,DOM 三个用于查看页面对应部分相关属性的窗口。当选中页面中的某个部分时, Style 显示选中部分的 CSS 属性,Layout 显示 CSS 的盒式模型,DOM 显示其选中部 分的所有 DOM 对象。结合使用 Inspect 功能可以方便选择页面中所需要关注的部分。 在图 13 中所显示的是在使用了 Inspect 功能选中一个 Dojo 文本框后,其所显示 的 Dojo 文本框的 CSS 修饰。这些 CSS 修饰是通过加载 Dojo 的 CSS 文件来实现 的。 第四步,Script 窗口的使用。 Script 窗口支持对 Javascript 的断点调试。 在 Script 窗口下,可以选择不同的脚本文件进行调试。在选择好需要调试的脚本文 件以后,直接使用鼠标点击代码行的左端可以添加断点,断点的标志是一个圆点。(此外, 也可以在代码中直接写入 debugger; 来添加断点) 然后刷新页面,则当脚本执行到断点位置的时候,停止执行。此时,可以选择 Script 窗口右边的几种调试按钮对当前代码进行调试。 在图 14 中,在代码的 18 行添加了断点,且此时脚本单步运行到了第 22 行。 37 图 14. Script 窗口的使用 第五步,DOM 窗口和 Net 窗口的使用。 DOM 窗口主要显示的是页面中的所有对 象,及其所包含的属性和方法。 Net 窗口主要是显示当前页面中所加载的各个文件的大小 和加载各个文件所花费的时间。 除了上述五步中所讲述的功能以外,Firebug 还有一些功能本文没有涉及。如果读者 对 Firebug 的使用特别感兴趣,可以在网上搜索相关资料进行了解。 但是在某些情况下,仍然需要在 IE 或 Safari 等其它非 Firefox 浏览器上进行调 试。在这个时候,Dojo 可以提供一些帮助。 Dojo 实现了一个跨平台的调试信息输出的功能。如果需要使用这个功能,则只需要 将 djConfig 的参数值设置为“isDebug: true ”便可以了。 如图 15 所示,在页面的最下面有一个方型区域,其中有三条输出。这三条输出分别 对应的调试语句为 console.debug("This is console debug information"); console.warn("This is console warn information"); console.error("This is console error information"); 。 38 图 15. IE 下使用 Dojo 的跨平台调试信息输出功能 基础知识 http://blog.csdn.net/dojotoolkit dojo 中文博客站点,可以查看部分官网的翻译、dojo 的扩展知识和常用知识。 http://dojotoolkit.org/ dojo 官网,有所有组件的 demo,但是存在某些 demo 运行不了的情况。 Hello Dojo 教程 版本:1.6 我们的起点是一个如下所示的简单 HTML 页面。我们希望在这个页面里添加一些代码来证 明 Dojo 已经成功加载。 [xhtml] view plaincopy 1. 39 2. 3. 4. 5. Tutorial: Hello Dojo! 6. 7. 8. 9. 10.

Hello

11. 12. 这很简洁,不是么?我们在 里放了一个加载 Dojo 的 8. 13. 14. 15.

Hello

16. 17. 我们传给 dojo.ready 的这个函数应该会在页面加载好以后弹出一个 alert 对话框。必须承 认, Dojo 的 version 属性(包含版本信息)对于此类示例程序非常有用。当我们以后更为 深入地学习 Dojo 时,这样的 alert 对话框就会变得越来越讨厌了。于是我们就会想要学习往 浏览器控制台打印日志的方法。不过目前我们暂时略过。 能加载 Dojo 的确不错,但您一定更希望操作这个刚刚载入了 Dojo 的页面。我们将会在其 他教程中深入讲解这方面的内容。而现在,我们只是简单地获取我们的

元素的引用然 后更新其内容。 [xhtml] view plaincopy 1. 2. 3. 4. 5. Tutorial: Hello Dojo! 6. 7. 8. 13. 14. 15.

Hello

16. 17. 这次,在我们的“万事俱备”函数里使用了 dojo.byId 来获取 DOM 树中含有给定 ID 的元素, 然后给它的 innerHTML 属性添加 Dojo 的版本信息字符串。 41 需要特别注意的是,您可以对 dojo.ready 做任意次调用。您每一次传入的这些函数会按照传 入顺序依次执行。实际上,如果您有比较多的代码,常见的做法是定义一个具名函数(非匿 名函数)然后把函数名传给 dojo.ready : [javascript] view plaincopy 1. function init() { 2. alert("Dojo ready, version:" + dojo.version); 3. // More initialization here 4. } 5. dojo.ready(init); 请注意,当我们传入函数的时候,我们只是在传函数名,而不包括其后的任何参数。 如果您是冲着 Dojo 的 Hello World 来的,那么本教程早已结束了。但之前我们曾提到过“声 明依赖模块”——让我们来聊聊这个吧。 模块 可见的东西往往只是冰山一角。Dojo.js 到底给您带来了什么? Dojo 是一个模块化的工具 箱,它有一个“包系统”,能让您只加载您在页面中需要的代码,并使得代码之间的依赖管 理变得非常简单。缺乏语言级别的用于加载代码的包系统(类似于 Java,PHP,Python 等语言 中的 import 或 require ),是一个长久以来困扰 JavaScript 开发人员的问题。 Dojo 利用一种 非常符合直觉的方法组织代码,并提供了一个简单的 API ( dojo.require )用于声明对某个 特定模块的依赖。 这一点对于目前的意义,就是当我们加载 dojo.js 时,并没有去加载整个 dojo 工具箱,而仅 仅是一些基础( base )模块。我们加载 Dojo 的 清单 1 应该是最为 web 开发人员熟知的事件处理方式了,直接把事件处理函数和控 件上的事件属性绑定起来。当用户点击 hello 按钮时,将调用 sayHello() 函数。当然也 可以把事件处理函数的代码作为 onclick 的值,参见清单 2,使用这种方式时,onclick 对 应的处理脚本应比较简单短小,在 onclick 后面写上一大串 javascript 脚本可不是什么 好主意。 清单 2 另一种略微高级的方法是在控件之外绑定控件的事件处理函数,见清单 3 。 清单 3 在清单 3 的例子中,首先通过 document.getElementById 获取需要绑定事件的 控件,再把控件的 onclick 事件设置为事件处理函数,其效果与前面的例子是一样的。需 要注意的是,script 脚本放到了控件后面,因为使用了 document.getElementById 去 获控件,而 javascript 是解释执行的,必须保证控件在执行到 getElementById 之前已 经创建了,否则会出现找不到控件的错误。但 sayHello 为什么会在事件绑定语句的后面 呢?按照刚才的原则,不是必须确保 sayHello 已经预先定义好了吗?其实不然,事件处 理函数的代码直到事件发生时才被调用,此时才会检查变量是否已经定义,函数是否存在, 而页面初次加载时按钮上的 click 事件是不会发生的。页面加载后用户再点击按钮, 57 sayHello 函数已经完全加载到页面中,函数是存在的。当然如果是普通的函数调用,一定 要保证被调用函数出现在调用函数之前。采用清单 3 所示的这种方式时,在 web 应用比 较复杂时,可以把事件处理函数集中放在一起,比如单独存放在一个文件中,方便以后查找, 修改。这个例子也很好的说明了 javascript 是一种解释执行的脚本语言。 前面三种事件处理方式是在 W3C DOM Level0 中定义的,是不是简单易用?但是似 乎太简单了,缺少一些东西。首先一个事件只能绑定一个处理函数,不支持多个事件处理函 数的绑定。如果开发人员被迫把事件处理代码都放在一个函数中,代码的模块性会很差。其 次解除事件处理函数的绑定的方式很不友好,只能把它设为空值或者空串。 document.getElementById("btn").onclick=null; document.getElementById("btn").onclick=""; W3C DOM Level2 标准有了新的事件模型,新模型最大的变化有两点: 首先,事件不再只传播到目标节点,事件的传播被分为三个阶段:捕获阶段,目标节 点阶段,冒泡阶段。一个事件将在 DOM 树中传递两次,首先从 DOM 根节点到目标节点 (捕获阶段),然后从目标节点传递到根节点(冒泡阶段)。在这三个阶段都可以捕获事件 进行处理,也可以阻止事件继续传播。W3C 的官方网站有关于这三个阶段的详细说明。在 DOM Level0 定义的事件模型中,事件只能被目标节点处理,其实这也是大部分支持事件 处理的编程语言采用的机制,比如 Java,C# 。但是这种方式可能并不适合结构比较复杂 的 web 页面。比如很多链接都需要自定义的 tooltip,在 DOM Level0 的方式下,需要 给每个链接的 mouseover,mouseout 事件提供事件处理函数,工作量很大。而在 DOM Level2 模型中,我们可以在这些链接的公共父节点上处理 mouseover,mouseout 事 件,在 mouseover 时显示一个 tooltip,mouseout 时隐藏这个 tooltip 。这样只需要 对一处进行更改即可给每个链接添加上自定义的 tooltip 。所以 DOM Level2 的设计者 定义出分为三个阶段的事件模型也是为了适应复杂的 web 页面,让开发人员在处理事件上 有更大的自由度。 其次,支持一个事件注册多个事件处理函数,也能够删除掉这些注册的事件处理函数。 一个事件可以注册多个事件处理函数同样是大部分的编程语言的事件处理机制支持的方式。 这种方式在面向对象的开发中尤为重要,因为可能很多对象都需要监听某一事件,有了这种 方式,这些对象可以随时为这一事件注册一个事件处理函数,事件处理函数的注册是分散的, 而不像在 DOM Level0 中,事件处理是集中式的,使用这种方式使得事件的“影响力”大大 增强。 清单 4

58 清单 4 是使用 DOM Level2 定义的事件模型的例子,在这个例子中,首先为 hello 按钮的 click 事件注册了两个事件处理函数,分别用来显示“ hello ”和“ world ”警示框。 然后为 remove 按钮的 click 事件处理了一个事件处理函数,用来删除注册在 hello 按 钮上的事件处理函数。例子很简单,但是足够说明 DOM Level2 中的事件处理机制。 addEvenetListener(/*String*/eventName, /*function*/handler, /*bool*/useCapture) 为某一 HTML 元素注册事件处理函数,eventName:该元素上发生的事件名; handler:要注册的事件处理函数,useCapture:是否在捕获阶段调用此事件处理函数, 一般为 false,即只在事件的冒泡阶段调用这一事件处理函数。 reomveEvenetListener(/*String*/eventName, /*function*/handler, /*bool*/useCapture); 删除某一 HTML 元素上注册的事件处理函数,函数声明与 addEventListener 一 样,参数意义也相同,即注册、删除事件处理函数时也需要使用同样的参数。这点不太方便, 比较好的做法是 addEventListener 返回一个句柄,然后把这个句柄传递作为 removeEventListener 的参数。 sayHello, sayWorld 是两个事件处理函数,他们的参数 event 是一个事件对象, 对象的属性包括事件类型(在本例中是 click),事件发生的 X,Y 坐标(这两个属性在实 现 tooltip 时特别有用),事件目标(即事件的最终接收节点)等。 从这个例子中也可以看出事件处理包括三个方面:事件源、事件对象、事件处理函数。 事件处理机制就是把这三个方面有机的联系起来。 注意,清单 4 的例子不能运行在 IE 浏览器里,因为 IE 浏览器采用是一种介乎 DOM level0 和 DOM Level2 之间的事件模型。比如在 IE 中,应该使用 attachEvent(), detachEvent() 来注册、注销事件处理函数。这只是 IE 中的事件模型与标准 DOM 59 Level2 事件模型不一致部分的冰山一角,其他的诸如事件对象的传播方式、事件对象的属 性、阻止事件传播的函数等,IE 与 DOM Level2 都有很大差异。这也是为什么 Dojo 会 再提供一些事件处理的 API 的原因:屏蔽底层浏览器的差异,让开发人员在写编写事件处 理代码时面对的是“透明”的浏览器,即不需要关心浏览器是什么。前面花了很大篇幅来介绍 DOM 事件模型,因为 Dojo 的事件处理机制是基于 DOM Level2 定义的事件模型的, 然后对浏览器不兼容的情况做了很多处理,以保证使用 Dojo 的事件处理机制编写的代码 能在各个浏览器上运行。下面来介绍 Dojo 的事件处理机制。 使用 Dojo 处理 DOM 事件 当 Dojo 运行在支持 DOM Level2 事件模型的浏览器中时,Dojo 只是把事件处理 委托给浏览器来完成。而在与 DOM Level2 的事件模型不兼容的浏览器(比如 IE)中, Dojo 会尽量使用浏览器的 API 模拟 DOM Level2 中的事件处理函数。Dojo 最终提供 给开发者一个称为“简单连接”的事件处理机制来处理 DOM 事件。 为什么叫“简单连接”呢,因为绑定事件处理函数的函数名叫 dojo.connect,相应的 注销的函数是 dojo.disconnect 。 dojo.connect = function(/*Object|null*/ obj, /*String*/ event, /*Object|null*/ context, /*String|Function*/ method, /*Boolean*/ dontFix) 参数 obj 事件源对象,比如 DOM 树中的某一节点; event 参数表示要连接的事 件名,如果是 dojo.global(在浏览器中一般是 window 对象)域上的事件,则 obj 参 数可以置为 null,或者不写。context 指事件处理函数所在的域(比如一个对象); method 表示事件处理函数名,如果是全局函数,则 context 参数可置为 null,或者不写这个参数; dontFix 表示不需要处理浏览器兼容的问题,默认为 false ;如果你的应用只在支持 DOM Level2 事件模型的浏览器上运行,则可以把它设为 true,但是这种几率太小了,因为 IE 就不是完全支持 DOM Level2 事件模型。dojo.connect 函数可以返回一个 handle,在 dojo.disconnect 中会用到。 dojo.disconnect = function(/*Handle*/ handle) dojo.disconnect 函数用来注销已注册的事件处理函数,参数是一个 dojo.connect 时返回的 handle 。 下面来看看如何使用 Dojo 的简单连接机制处理 DOM 事件。 清单 5

  1. C++ primer
  2. Thinking in C++
  3. Inside C++ object model
61
清单 5 的页面中有一个跟 c++ 相关的书的列表,列表的每一项都通过 dojo.connect 绑定了一个或多个事件处理函数。第一项“ c++ primer ”给 mouseover, mousedown,click 三个事件注册了事件处理函数,第二项“ Thinking in c++ ”注册了 两个 click 事件处理函数 handler 和 handler2,第三项“ Inside C++ object model ” 绑定了 click 事件。这个例子可以说包括了“简单连接”机制的方方面面。 首先来看看 dojo.connect 的使用,dojo.connect 能够把多个事件处理函数绑定在 一个事件上,第二项“ Thinking in c++ ”的 click 事件就绑定了两个事件处理函数。在本 例中,并没有给 dojo.connect 函数传递事件处理函数的 context,因为默认的是 dojo.global,而两个事件处理函数 hander 和 hander2 都是全局函数,所以不需要显 示传递 dojo.global 。 再来看事件处理函数 handler 和 handler2 。handler2 只是用来说明 dojo.connect 可以绑定多个事件处理函数,不多说; handler 是主要的事件处理函数, 在 handler 里先输出了事件对象的三个属性,type、target、currentTarget,type 表 示事件的类型,target 表示事件目标节点,currentTarget 表示当前事件传递到哪个节点 了,输出他们三个是为了说明 Dojo 也是在冒泡阶段处理事件的(还记得在 DOM 事件模 型部分对事件的三个阶段的描述吗?)。所以当点击第三项 b3 时,在浏览器的模拟控制 台输出是 eventType=click; node=b3; currentTarget=b3 eventType=click; node=b3; currentTarget=cpp eventType=click; node=b3; currentTarget=book 可以看出,首先会触发 b3 的事件处理函数,然后是 id 为 cpp 的 ol 元素的 click 事件处理函数,最后是 id 为 book 的 div 。所以毫无疑问 Dojo 是在事件的冒泡阶段 处理事件的,capture 阶段并不做任何处理。handler 的最后是关于阻止事件传播的代码, 如果按住 shift 键,再点击第三项时,只会在模拟控制台输出: eventType=click; node=b3; currentTarget=b3 后面两个事件没有发生,因为 click 事件被 stopPropagation 阻止了,没有再往上 冒。事实上,可以在任何一级对象上调用 stopPropagation 阻止事件继续往上传递。 然后是事件对象 eventObj,事件对象是对事件的描述,在前面已经介绍了事件对象 的几个有用的属性。Dojo 的事件对象其实基于 DOM Level2 的事件对象,更详细的属性 信息可以参考 Dojo 的官方文档,这里对用户操作触发的事件和事件的继承结构做些说明。 当用户点击第一项 b1 时,在浏览器输出的是 eventType=mouseover; node=b1; currentTarget=b1 62 eventType=mousedown; node=b1; currentTarget=b1 eventType=click; node=b1; currentTarget=b1 eventType=click; node=b1; currentTarget=cpp eventType=click; node=b1; currentTarget=book 从上面的输出可以看出,在 b1 这个节点上一共监测到了三个事件(事实上产生的事 件不止三个),mouseover、 mousedown、click 。所以表面上一个点击操作背后却藏 着大文章。同理用户点击提交按钮提交一个表单也会触发很多事件,但一般我们只处理了最 上层的 submit 事件。这些现象揭示了事件是有类别,层次的,底层事件可以触发高层事 件。底层事件一般都是与设备有关的事件,比如鼠标移动,按键产生的事件;高层事件一般 指页面元素上的事件,比如链接的 click 事件,表单的 submit 事件等。与设备无关的事 件往往由几个与设备有关的事件触发。比如一个单击页面上按钮的 click 事件,可以分解 为 mouseover, mousedown, mouseup 三个事件,在这三个事件发生之后,将触发按 钮的 click 事件。开发人员应该了解这些知识,因为它有助于写出高效的事件处理程序。 最后是事件目标,在 W3C DOM Level2 的事件模型里,事件目标不仅仅是 DOM 树 种最底层的接收事件的节点,它可以是从这个底节点到跟节点路径上的任何一个节点。 Dojo 目前支持的事件类别包括 UIEvent,HTMLEvent, MouseEvent,每类事件具 有的属性并不一样,比如只能在 MouseEvent 里才能获得事件发生时鼠标的位置等。 使用 Dojo 处理用户自定义事件 既然 W3C 已经定义了标准的 DOM Level2 事件模型,为什么 Dojo 还要提供 connect 函数来注册事件处理函数呢,为何不使用 DOM Level2 的 addEventListener 函数?从前面的叙述中也看不出 connect 与 addEventListener 有明显的不同之处。确 实在处理 DOM 事件上,Dojo 的 connect 与 addEventListener 无甚大的不同,但是 Dojo 的 connect 函数还可以处理用户自定义事件。这是 addEventListener 所不具备 的。下面来看看怎么使用 dojo.connect 来处理用户自定义事件。 用户自定义事件是指用户指定的函数被调用时触发的“事件”,当指定函数被调用时, 将促发监听函数被调用。有点类似于 AOP 的编程思想,但在 Javascript 中实现 AOP 比 起面向对象的编程语言要简单得多。 清单 6 63 运行清单 6 的例子,会在页面中的一个模拟控制台中输出: In userFunction; the arguments are: 1 2 In handler1; the arguments are: 1 2 In handler2; the arguments are: 1 2 调用 userFunction 时,handler1 和 handler2 也被触发了。userFunction 就像 是一个事件源,它的调用像一个事件,而 handler1 和 hander2 就是事件处理函数。那 么这种情况下,事件对象又在哪呢? handler1 事件处理函数没有显式的参数,通过在控 制台的输出可以得知它实际上有两个参数,值分别为 1 和 2 ; handler2 有两个显式参 数,值也为 1 和 2 。所以 Dojo 只是把 userFunction 的两个参数传递给了事件处理函 数,不像在处理 DOM 事件时,提供一个封装好的事件对象。在本例中 userFunction 只 “连接”了两个函数,很显然它还可以连接更多的事件处理函数,这些事件将按连接的先后顺 序来执行。 64 Dojo 的订阅/发布模式 dojo.connect 函数用来处理某一个实体上发生的事件,不管处理的是 DOM 事件还 是用户自定义事件,事件源和事件处理函数是通过 dojo.connect 直接绑定在一起的, Dojo 提供的另一种事件处理模式使得事件源和事件处理函数并不直接关联,这就是“订阅/ 发布”。“订阅/发布”模式可以说是一个预订系统,用户先预定自己感兴趣的主题,当此类主 题发布时,将在第一时间得到通知。这跟我们熟知的网上购物系统不一样,网上购物是先有 物,用户再去买,而在订阅/发布模式下,预订的时候并不确定此类主题是否已存在,以后 是否会发布。只是在主题发布之后,会立即得到通知。订阅/发布模式是靠主题把事件和事 件处理函数联系起来的。在 Dojo 中,跟主题订阅 / 发布有关的函数有三个: dojo.subscribe = function(/*String*/ topic, /*Object|null*/ context, /*String|Function*/ method) subscribe 函数用来订阅某一主题;参数 topic 表示主题名字,是一个字符串; context 是接收到主题后调用的事件处理函数所在的对象,function 是事件处理函数名。 dojo.unsubscribe = function(/*Handle*/ handle) 取消对于某一主题的订阅;参数 handle 是 dojo.subscribe 返回的句柄,跟 dojo.connect 与 dojo.disconnect 的工作方式一样。 dojo.publish = function(/*String*/ topic, /*Array*/ args) 发布某一主题;参数 topic 是主题的名字,args 表示要传递给主题处理函数的参数, 它是一个数组,可以通过它传递多个参数给事件处理函数。 订阅 / 发布模式看上去很神秘,但实现是比较简单的。dojo 维护了一个主题列表, 用户订阅某一主题时,即把此主题及其处理函数添加到主题列表中。当有此类主题发布时, 跟这一主题相关的处理函数会被顺序调用。注意:如果用户使用了相同的处理函数重复订阅 某一主题两次,在主题列表中这是不同的两项,只是他们都对同一主题感兴趣。当此类主题 发布时,这两个处理函数都会被调用,而不会出现第二个处理函数覆盖第一个处理函数的状 况。清单 7 的例子展示了订阅 / 发布模式是如何工作的。 清单 7 在清单 7 的例子中,模拟了一个“新闻记者”(NewsReporter 对象),专门跑体育 和娱乐新闻,任何此类新闻他都不会放过。Dojo 就像一个新闻中心,发布各类新闻。 记者先在新闻中心注册,说自己对体育新闻感兴趣,接着新闻中心发布了一条新闻 “ China will rank first in the 29th Olympic ”,这时新闻记者将立即收到这条消息,并 报道出来(在本例中就是在浏览器的模拟控制台输出这条新闻)。然后记者又再次向新闻中 心注册对体育和娱乐新闻以及跨这两个领域的新闻都感兴趣,然后新闻中心分别发布了这三 个主题的新闻。记者当然不敢懈怠又马上输出了这些新闻,最后新闻记者不打算再跑体育新 闻了,就在新闻中心取消了对体育新闻的注册。这个例子最终将在浏览器的模拟控制台输出: sports:China will rank first in the 29th Olympic sports:America will rank second in the 29th Olympic sports:Russia will third forth in the 29th Olympic sports:America will rank second in the 29th Olympic sports:Russia will third forth in the 29th Olympic entertainment:Red Cliff earns over 200 million in its first week mixed sports: Yao Ming gives Red Cliff high comments entertainment: Jay and S.H.E wish Beijing Olympic success 从这个例子中我们可以得到几个使用订阅/发布模式时的注意事项。 先订阅,再发布。主题发布的时候,订阅了这一主题的事件处理函数会被立即调用。 发布函数的参数为数组,发布第一条新闻时使用的是 [["China will rank first in the 29th Olympic"]],这是一个二维数组,因为事件 处理函数 NewsReporter.sports,NewsReporter.entertainment,以及 NewsReporter.mixed 的参数已经是一个数组,所以在发布时必须把新闻事件这个数组再 放在另一个数组中才能传递给这些事件处理函数。而“ mixed ”新闻的处理函数有两个参数, 所以发布“ mixed ”的新闻时,参数为: [["Yao Ming gives Red Cliff high comments"], ["Jay and S.H.E wish Beijing Olympic success"]] 二维数组中的第一个数组表示体育新闻,第二个数组表示娱乐新闻。 67 取消订阅时,必须把所有的订阅都取消。重复的订阅行为返回的句柄是不一样的,在 本例中 handle1 和 handle2 是不同的,必须都注销。只有在 handle1 和 handle2 都 被注销后,新闻中心发布的体育新闻才不会被这个记者接收到。 结束语 浏览器在事件处理机制上的差异使得 web 开发人员在处理事件时需异常小心,Dojo 的事件处理 API 却能在各个浏览器上工作的很好,减少了开发人员在处理跨浏览器问题上 的工作量。Dojo 参考 W3C DOM Level2 的事件模型实现的事件处理机制,即能处理 DOM 事件,也能处理用户自定义事件,而全新的“订阅 / 发布”模式也给了开发人员在处 理事件时更多的选择。 Dojo 事件补充 版本:1.6 使用 dojo.connect 来轻松的绑定 DOM 事件以及在原生对象上自定义事件。 dojo.connect 的一般用法:dojo.connect(element, event name, handler)。这一用法可用于 所有的窗口(window), 文档(document), 节点(node), 表单(form),鼠标以及键盘 事件上。 dojo.disconnect。将dojo.connect方法的返回值作为参数传递给dojo.disconnect即可解除该 事件处理器与事件之间的连接。 如: var handle = dojo.connect(myButton, "onclick", function(evt){ // Disconnect this event using the handle dojo.disconnect(handle); //写在外面也可以 // Do other stuff here that you only want to happen one time alert("This alert will only happen one time."); }); dojo.NodeList 提供了一个方法用于向多个节点注册事件。除了第一个参数外,其用法与 dojo.connect 方法基本一致。 如: dojo.query(".clickMe").connect("onclick", myObject.onClick); dojo.query(".clickMeAlso").connect("onclick", myObject, "onClick"); 注意:dojo.NodeList.connect 无法注销已连接的事件处理器。 很多的 JavaScript 代码都是围绕着事件的,包括创建新事件或是对事件的响应。这意味着 建立一个交互式的网络应用的关键就是创建有效的事件连接体制。事件连接体制支持你的应 用程序建立与用户的互动及等待接受用户的操作。在浏览器环境下具有原生的 DOM 事件, 68 但同时我们也希望函数也可以具有类似这些事件一样的调用方式:“当某件事发生时,调用 此函数。”dojo.connect——Dojo 事件体制中一个主要方法就提供了这一功能。 DOM 事件 你可以会提出疑问:“DOM 不是已经提供了为事件注册处理函数的机制了吗?”的确如此, 但并非所有浏览器都提供对 DOM 规范的全面支持,纵观主流浏览器的 DOM 实现机制,共 有三种方式来实现对事件处理函数的注册机制(addEventListener, attachEvent,以及 DOM0)。另外还有两种其他的事件实现机制和一个浏览器采用“随机顺序”对处理函数进行注 册,并在注册事件处理器时会导致内存泄露,这对用户的应用角度来说也是一个潜在的灾难 性因素。 幸好,Dojo 为用户提供了统一的 DOM 事件机制,通过使用 Dojo 的 dojo.connectAPI,用 户可以避免各种 DOM API 的分歧,同时 DOJO 也预防了内存泄露问题。 假设我们有如下一段页面代码: [javascript] view plaincopy 1. 2.
Hover over me!
这里假设我们希望在点击按钮时使 div 变为蓝色,而当鼠标悬浮在其上时变为红色,移出时 变回白色。下面的代码示例让我们看到使用 dojo.connect 可以很容易做到这些: [javascript] view plaincopy 1. var myButton = dojo.byId("myButton"), 2. myDiv = dojo.byId("myDiv"); 3. 4. dojo.connect(myButton, "onclick", function(evt){ 5. dojo.style(myDiv, "backgroundColor", "blue"); 6. }); 7. dojo.connect(myDiv, "onmouseenter", function(evt){ 8. dojo.style(myDiv, "backgroundColor", "red"); 9. }); 10. dojo.connect(myDiv, "onmouseleave", function(evt){ 11. dojo.style(myDiv, "backgroundColor", ""); 12. }); 69 通过上面的例子我们得出 dojo.connect 的一般用法:dojo.connect(element, event name, handler)。这一用法可用于所有的窗口(window), 文档(document), 节点(node), 表 单(form),鼠标以及键盘事件上。注意,在这个例子中,所有的事件名都采用了小写,虽 然这并非强制性的,而且 Dojo 会针对不同浏览器对这一参数进行格式化,可对事件名称采 用一致的格式化是一个较好的编码习惯。 dojo.connect方法不仅是一个时间注册API,同时它也可以定义如何对事件处理器进行调用:  事件处理器总是按其注册顺序进行调用  当事件处理器被调用时,第一个参数始终为一个事件对象  事件对象将会有一个 “target”属性,一个"stopPropagation"方法,和一个 “preventDefault”方法 如同 DOM API 一样,Dojo 提供了如何对事件处理器进行注销(解除连接)的方法: dojo.disconnect。将 dojo.connect 方法的返回值作为参数传递给 dojo.disconnect 即可解除 该事件处理器与事件之间的连接。例如,如果你想定义一个只运行一次的事件处理器,可以 如下例所示进行定义: [javascript] view plaincopy 1. var handle = dojo.connect(myButton, "onclick", function(evt){ 2. // Disconnect this event using the handle 3. dojo.disconnect(handle); 4. 5. // Do other stuff here that you only want to happen one time 6. alert("This alert will only happen one time."); 7. }); 最后一项需要注意的是:dojo.connect 方法可在 handler 参数前定义一个可选参数,该参数 用于定义 handler 的上下文。如果该参数未被指定,事件处理器的默认运行上下文将被设置 为所传入的第一个参数 node 或是 window 对象(这一选择依赖于浏览器)。当使用 widget 时, 这一参数是非常重要的。 [javascript] view plaincopy 1. var myScopedButton1 = dojo.byId("myScopedButton1"), 2. myScopedButton2 = dojo.byId("myScopedButton2"), 3. myObject = { 4. id: "myObject", 5. onClick: function(evt){ 70 6. alert("The scope of this handler is " + this.id); 7. } 8. }; 9. 10. // This will alert "myScopedButton1" 11. dojo.connect(myScopedButton1, "onclick", myObject.onClick); 12. // This will alert "myObject" rather than "myScopedButton2" 13. dojo.connect(myScopedButton2, "onclick", myObject, "onClick"); 查看 Demo 当 scope 对象参数被指定后,handler 参数必须为一个 scope 对象中的方法的名称或者是一 个函数对象,如上例中的最后一行,dojo.connect(myDiv, "onclick", myObject, myObject.onClick);。当 handler 参数为一个字符串时,其必须是一个大小写敏感的 scope 对象中的方法名,Dojo 是无法对这一参数进行自动格式化的。 NodeList 事件 如之前提到过的,dojo.NodeList 提供了一个方法用于向多个节点注册事件。除了第一个参 数外,其用法与 dojo.connect 方法基本一致。首先让我们看一个例子: [javascript] view plaincopy 1. 2. 3. 4. 5. My button 2. 这里我们希望能够有一段代码用于通知按钮何时被点击。我们可以连接 myButtonObject 的 onClick 方法而并不需要对按钮的 DOM 节点上再绑定事件处理器: [javascript] view plaincopy 1. dojo.connect(myButtonObject, "onClick", function(evt){ 2. alert("The button was clicked and 'onClick' was called"); 3. }); 这里需要注意的是,如果连接到的是一个原生对象,那么将无法对事件名(dojo.connect 的第二个参数)进行格式化,另外,所有被传入到被连接方法的参数也将作为参数传给处理 器方法: [javascript] view plaincopy 1. var myButtonObject2 = { 2. onClickHandler: function(evt){ 3. this.onClick(evt, "another argument"); 4. }, 5. onClick: function(){} 6. }; 72 7. dojo.connect(dojo.byId("myButton2"), "onclick", 8. myButtonObject2, "onClickHandler"); 9. dojo.connect(myButtonObject2, "onClick", function(evt, another){ 10. alert("The button was clicked, we were given a second argument: " + anot her); 11. }); 由于 DOM 节点的事件处理器方法仅仅有一个参数,即事件对象,那么其连接的事件处理器 方法也仅仅会被传入这一个参数;而连接到原生对象上的处理器方法将接受与被连接方法一 样的多个参数。除了以上两点不同外,对于在 DOM 节点和原生对象上使用 dojo.connect 则再没有其他的区别。 连接到原生对象方法上现在看起来好像不是特别实用,不过接下来我们就会看到这一技术在 小部件(widgets)上面是非常有作用的。另外,这一技术也很适用于特效应用,在其他的 tutorial 中会有关于特效的深入讲解,但在这里我们可以提供一个例子: [javascript] view plaincopy 1. 2.
3. 查看 Demo 这里我们不对特效相关的内容做过多的介绍,只需要知道 dojo.fadeOut 将返回一个带有 onEnd方法的对象,onEnd方法将在特效完成后被触发。在此,我们可以将返回对象的onEnd 方法进行绑定,弹出对话框告诉用户动画特效何时结束。在这一例子里,当红色区域淡出效 果结束后,我们所绑定的处理函数就将会被触发。 73 Publish/Subscribe 到目前为止,以上的例子都是针对已经创建的对象(DOM 节点,某个小部件[widget],或是 某个特效对象),将我们的事件处理器绑定在其上,并以其作为事件发布者。那么,当我们 并没有将事件处理器绑定到某个对象上,或者我们不知道要绑定的对象是否已经被创建时, 我们又该如何去做呢?在这种情况下,我们就会需要用到 Dojo 的 publish 和 subscribe(pub/sub)框架了。pub/sub 使我们可以将某个处理器注册(或称之为“订 阅”[subscribe])到某个“主题”(一个具有多个事件触发源的事件的特定名称,可用字符串表 示),我们所注册的处理器将在该绑定“主题”被发布时被触发调用。 假设我们正在开发某一个应用,其中需要创建一些按钮来告知用户相应的行为。我们既不想 重复的写这一通知程序,也不希望通过在按钮中写入内嵌对象来实现事件注册。那么最好的 方法就是使用 pub/sub: [javascript] view plaincopy 1. 2. 3. 4. 这一事件模式的一个优点是,我们不需要创建任何的 DOM 对象来进行单元测试,通知程序 与事件是完全解耦合的。以下是一些 pub/sub 的一些注意事项:  dojo.subscribe 的调用方式与 dojo.connect 的调用方式相类似(dojo.subscribe(topic, handler)或 dojo.subscribe(topic, scope, handler or method name))  dojo.publish 方法的第二个参数必须是一个数组对象,该数组对象中的元素即为主题 处理器函数的参数。  dojo.sbuscribe 将返回一个对象,该对象可被传入到 dojo.unsubscribe 方法用于注 销该主题中的特定的处理器(作用与 dojo.connect 及 dojo.disconnect 相似) 小结 Dojo 的事件系统十分强大,同时也十分易于使用。dojo.connect 方法可以使用户忽略 DOM 对象与原生对象的事件的区别,以及事件在不同浏览器的不一致。Dojo 的 pub/sub 则提供 给开发人员一种很方便的解耦合事件处理器与事件发布者的方法。一旦你对这些工具有所了 解,它们将成为你开发 Web 应用中的一项利器。 Dojo 中的拖拽 Dojo 支持的两种拖拽方式 在开始尝试了解实现 Dojo 拖拽效果的使用方法以前,首先必须明确拖拽具有两种截 然不同的表现效果。 第一种表现效果是图标被拖拽到哪里,其就会被直接放到哪里,这个拖拽效果是图标 完全紧跟拖拽的动作,与每一个拖拽动作的运动轨迹完全契合,这种效果被称为 “拖动”。 第二种表现效果是当图标被拖拽到一个地方,松开鼠标的时候,图标会以当前位置为基础而 以其它图标为参照系进行位置的自动调整。这种效果被称为 “拖放”。 75 Dojo 的拖动 “拖动”与“拖放”相比较,原理更加容易理解,使用更加简单。而且更加贴近于人们直 观印象中的“拖拽”效果。 最简单的拖动实例 要在 Dojo 的支持下,实现拖动的效果所需要的只是使用 Dojo 所提供的 Dojo 标 签属性标注出希望实现拖动效果的实体。简单的说,就是如果希望一个实体可以拖动,则只 需要在这个实体的标签里面加上 dojoType=“dojo.dnd.Moveable”这个属性。例如要实 现一个表格的拖动,则只需要在这个表格的声明标签“”或“”标签中加上 dojoType=“dojo.dnd.Moveable”,也可以实现对应实体的拖动效果。 清单 1
Haha, I am a good guy.
需要注意的是静态创建可拖动实体需要引入 dojo.require("dojo.parser") 。 动态实现可拖动实体 在清单 1 中,通过在一些实体的标签里面加上相应的 Dojo 标签属性来实现可拖动 实体的创建。这种静态实现可拖动实体的方法简单明了。但是在更多的情况下,往往需要根 据一些实际情况运行得到的数据来动态的创建可拖动实体。在这种情况下,静态实现可拖动 实体的方法就不能满足当下的需求。值得庆幸的是 Dojo 对于所有静态实现的方法都基本 对应有一套相应的动态实现方法。 清单 2
You can cop me "Haha, I am a bad guy."
需要注意的是 dojo.dnd.Moveable("bad",{}) 中的大括号用来设置可拖动实体 “bad”的一些与拖动相关的属性,目前可以暂时设为空,则不设置任何与拖动相关的属性。 在后面的讲述中,一些相关的重要属性将被逐步介绍。 拖动柄 如果运行清单 1 和清单 2 中的代码,然后尝试在其页面中使用鼠标去选择可拖动实 体中的内容。就会发现,无论使用何种方法都无法选择可拖动实体中的内容,当然就更谈不 上复制可拖动实体中的内容了。 仔细分析无法选择可拖动实体中内容的原因,就会发现如果要选择页面中的某一部分 内容,其动作步骤为,按住鼠标左键不放,然后拖动鼠标选择一块区域作为确定选择的内容; 而如果要拖动一个可拖动实体,其动作步骤也为,按住鼠标左键,然后拖动鼠标引起可拖动 实体的移动。 因此如果让某个实体具有了可拖动的功能,则当对这个实体点下鼠标左键,并拖动鼠 标时,就浏览器看来,其将不能理解这个动作的目的是要拖动该实体还是选择该实体里面的 内容。因为这两个具有不同含义的动作就其动作本身来说是一模一样的,浏览器没有办法对 这两个动作进行区分。 但现实的情况往往需要一个实体既可以被拖动,又可以被选择其内部所包含的内容。 Dojo 通过给可拖动实体增加一个拖动柄,实现了选择内容动作和拖动实体动作的区分。 声明拖动柄的方法为在声明可拖动实体的时候,在可拖动实体的标签中再加上一个除 dojoType 之外的另外一个 Dojo 标签属性 handle= “”,handle 后面的双引号中需要 填入的是作为拖动柄部分的那个实体的 id 值。 清单 3 77
You can cop me "Haha, I am a bad guy."
如果要动态声明上面的“拖动柄”,则需要在页面加载完成以后通过执行 dojo.dnd.Moveable("bad", {handle: "dragme"}) 来实现。 handle 后面所跟的值为 将成为可拖动实体“bad”拖动柄的实体 id 。 在清单 2 中,没有设置可拖动实体的任何属性。在清单 3 中设置了与拖动相关的其 中一个属性——拖动柄。 限制可拖动实体拖动的范围 Dojo 限制用户可拖动实体活动范围的方法有两种。第一种方法为在生成可拖动实体 的时候,给其设置一个逻辑上的活动范围空间;第二种方法是依据页面的一个实体,建立可 拖动实体的活动范围空间。需要注意的是,在第一种方法中的活动范围空间是一个不存在相 应页面实体的定义,即这个所谓的活动范围空间在页面是实际不存在的。首先要讨论的是第 一种方法的使用。 清单 4
aaa
bbb
ccc
ddd
在清单 4 中,将动态定义和静态定义的方法都对比着写了出来,前两个可拖动实体是 使用的动态定义的方法,后两个实体所实现的效果和前面一样但使用的是静态定义的方法。 实现可拖动实体活动范围的第一种方法是通过定义一个实际不存在的矩形框来作为可 拖动实体的“监狱”。例如在清单 4 可拖动实体“aaa”中,{box: {l: 100, t: 100, w: 500, h: 500}} 是指在“aaa”外建立一个盒子区域,该盒子区域为“aaa”的活动范围。“l: 100, t: 100”表示该盒子区域距页面左边界和上边界各为 100px,“w: 500”表示盒子区域的宽度 为 500px,“h: 500”表示盒子区域的高度为 500px 。需要注意的是盒子区域的 l 和 t 始终是以页面边界为标准。 图 1 图 1 显示出了清单 4 中可拖动实体“aaa”活动范围的“盒子区域”。 可拖动实体“within”的属性可设为“true”或者“false”,如果设为“true”的话表示“可拖 动实体”的任何一个部分都不能超出其盒子区域的范围,如果设为“false”表示只要拖动实体 的左上角不超出盒子区域的范围便为合法操作。 79 图 2 图 2 中可拖动实体的“within”属性为“false”,因此当前的位置是合法的。但是可能 在某些时候,需要使用到更细致的活动范围控制,或者需要根据可拖动实体的 DOM 父节 点确定其活动范围,而不是距页面的边距。对于这些情况,可以采用 Dojo 提供的第二种 限制用户可拖动实体活动范围的方法。 Dojo 限制用户可拖动实体活动范围的第二种方法是在所有可拖动实体的外面创建一 个 DOM 父节点,然后利用该父节点的“Layout”属性将可拖动实体的运动范围限制在父节 点内。以此为基础,依据父节点四种不同的“Layout”属性分为 “margin”类型的限制, “border”类型的限制,“padding”类型的限制,和“content”类型的限制。 ( “margin”, “border”,“padding”和“content”是 Web 页面中一个实体的 CSS 属性 ) 。 清单 5
I am restricted within my parent's margins.
I am restricted within my parent's border.
I am restricted within my parent's paddings.
I am restricted within my parent's content.
81 在清单 5 中声明了四个可拖动实体,其中两个是以静态 Dojo 标签属性的方式声明, 另外两个是以动态的方式声明。 捕获拖动中的事件 Dojo 通过事件订阅发布机制实现了方便易用的捕获拖动事件 API 。 Dojo 在拖动 开始、结束和过程中会发出一些消息,如果希望监测到页面内可拖动实体的拖动开始事件和 拖动结束事件可以通过订阅“/dnd/move/start” 和“/dnd/move/stop”来实现 清单 6
Haha, I am a bad guy.


Haha, I am boy

清单 6 的运行效果在 Firebug 下最易于观察。在 Firebug 的 Console 窗口中, 每一次拖动可拖动实体,就会在 Console 窗口中有相对应的输出。 如果在某些时候,只希望监测某一个可拖动实体的事件,而不是所有可拖动实体的事 件。则可以采用将可拖动实体的事件和一个函数连接起来以实现只监测某个特定可拖动实体 的事件。 清单 7 82

Haha, I am boy

在清单 7 中将可拖动实体“boy”和一个特定的输出函数连接了起来。这样在捕获可拖 动实体“boy”的时候,不会“惊动”其它可拖放实体。 Dojo 的拖放 拖放是一个复杂而又充满魅力的功能。 Dojo 支持拖放功能的原因,就是因为 Dojo 的使用者在实际项目中开发高级拖拽操作功能的时候提出了这样的需求。 最简单的拖放实例 请读者将下面的代码在自己的机器上运行,并尝试感受拖放的实际效果。 清单 8 test 83
SOURCe
BLUE
RED
TARGET
由清单 8 可以得知,对于拖放来说,可拖放实体必须是从一个容器中拖放到另外一个 容器中。可拖放实体是不能存在于容器之外的任何地方。如果可拖放实体从“容器 A”出来, 放入“容器 B”中,则一般习惯上称 A 为源容器,B 为目标容器。 84 被拖放的实体称之为“可拖放实体”。要实现可拖放实体的拖放,页面中必须要有“拖放 源容器”和“拖放目标容器”。拖放源容器为起初可拖放实体存放的地方,而拖放目标容器为 可拖放实体拖起后可以放的地方。 如果运行清单 8 中的代码,就会发现在拖起可拖放实体的时候,有一个与“原可拖放 实体”很相似的小图标在随着鼠标移动,其被称为可拖放实体的“替身”。“替身”的主要作用 有两个,第一是指明目前操作的实体是哪一个或是有哪几个实体被操作,第二是作为原实体 的替身,帮助用户判断目前选中可拖放实体的鼠标所处的位置为哪里,判断该区域是否为合 法的拖放区域。 替身是由“原可拖放实体”转换而来,包含了“原可拖放实体”的主要外貌特征,同时替 身的细微化,大大减小了如果采用“原拖动实体”作为标识而带来的系统负担和提高了标识的 精确度。 图 3 拖动的本质是可拖动实体象素位置的变化,而拖放的本质是一个页面 DOM 结构的变 化。实体象素位置的变化,可以只通过修改这个实体的属性来实现,不用和页面的任何其它 部分打交道。但是对于拖放,当将一个可拖放实体从源容器拖放到目标容器时,就是将该可 拖放实体先从源容器的 DOM 节点上删除,再在目标容器的 DOM 节点上加上可拖放实 体。 85 因此可拖放实体的存在,在拖放动作前必须依赖于一个父 DOM 节点,这就是源容器, 拖放动作后也必须依赖于一个新的父 DOM 节点,这就是目标容器。 在某些情况下,可能需要的是能在几个容器之间将可拖放实体不断的拖来拖去。那么 所做的修改是只需要将源容器和目标容器都用 dojoType= “dojo.dnd.Source”来进行声 明就能实现可拖放实体从目标容器拖回源容器的操作。 动态生成源容器、目标容器和可拖放实体 动态声明源容器和目标容器的方法比较简单,动态创建源容器的方法为 var foo=new dojo.dnd.Source(Node, Params),而动态创建目标容器的方法为 var foo=new dojo.dnd.Target(Node, Params) 。在声明源容器的方法中,Node 为要声 明为源容器的 DOM 节点 id,Params 所代表的是一些用来确定容器相关属性的参数。 清单 9 function init() { mysource = new dojo.dnd.Source("mysource",{});// 存放可拖放实体的 " 源 容器 " mytarget = new dojo.dnd.Target("mytarget",{});// 存放可拖放实体的 " 目标 容器 " } 86
SOURCe
BLUE
RED
TARGET
在清单 9 中动态声明了源容器和目标容器。由清单 9 中观察可知,容器的声明是建 立在已经存在的页面实体基础之上的。 清单 10 function init() { mysource = new dojo.dnd.Source("mysource");// 存放可拖放实体的 " 源容 器 " mysource.insertNodes(false,["Do you love me?","Good good study","
"]); mytarget = new dojo.dnd.Target("mytarget");// 存放可拖放实体的 " 目标容 器 " } 87
SOURCE
TARGET
在清单 10 中通过 dojo.dnd.Source.insertNodes 来将动态声明的字符串化的实 体插入到“源容器”中。如果回顾一下前面关于拖放基本原理的内容,就可以知道拖放的过程 就是删除一个 DOM 节点和重新创建一个 DOM 节点的过程。删除一个 DOM 节点对于 不同的应用没有太大的差异性,但是创建一个 DOM 节点对于不同的应用情况可能就需要 不同的创建方式。 在大多数情况下,拖放后在目标容器里重建一个可拖放实体的 DOM 节点是调用 Dojo 的一个默认构造函数。这个构造函数能够在目标容器内完全重建一个与源容器一模一 样的可拖放实体。 但如果希望可拖放实体在拖放入目标容器以后发生变化,与在源容器中的可拖放实体 不一样,则可以通过创建自己的构造函数来实现。 如果要创建自己的构造函数,首先要在构建源容器和目标容器的时候,指明自己要创 建的构造函数的函数名。 dojo.dnd.Source("mysource",{creator: sourcenodecreater, copyOnly: false}) 表示创建存放可拖放实体的“源容器”。 dojo.dnd.Target("mytarget",{creator: targetnodecreater, copyOnly: false}) 表示创建存放可拖放实体的“目标容器”。而 “creator: sourcenodecreater” 表示源容器中构建可拖放实体的函数名为 “sourcenodecreater”,“creator: targetnodecreater”表示目标容器中构建可拖放实体 的函数名为“targetnodecreater”。 首先来创建一个最简单的名为“sourcenodecreater”的构造函数。 清单 11 function sourcenodecreater(data, hint) { var myitem = dojo.doc.createElement("div"); myitem.id = dojo.dnd.getUniqueId(); myitem.innerHTML = data; return {node: myitem, data: data}; } 注意清单 11 中源容器的构造函数“sourcenodecreater”只是一个例子,但其包含了 一个构造函数最基本的要素。在这里只是为了阐述清楚最基本的原理。具体的功能和如何构 造出所需要的可拖放实体,需要根据需求来进行分析设计。 对于“sourcenodecreater”函数所接受的两个参数,“data”所代表的是接受到的关于 可拖放实体的一些特征数据。因为可拖放实体是动态构造的,所以在很多情况下,可拖放实 体要根据系统前面所传来的数据来构造相应的可拖放实体,而“data”所包含的就是这些用 来构造可拖放实体的数据。 “hint”是与替身有关的参数。 88 var myitem = dojo.doc.createElement("div") 表示创建一个 div 节点。可拖放 实体将在这个节点上创建。 myitem.id = dojo.dnd.getUniqueId() 表示将会给可拖放实体一个唯一的 id 。 dojo.dnd.getUniqueId() 可以在一次系统运行中每次产生一个绝对独一无二的 id 。因为可拖放实体的拖放是 DOM 节点的销毁和重建,因此,当一个可拖放实体的拖放 完成以后,其 id 将会发生变化。 myitem.innerHTML = data 是用来构建可拖放实体的实际效果,目前这里只是简单 的将数据作为字符串在可拖放实体中展现。 return {node: myitem, data: data} 表示在上面的工作完成以后,将创建的节点 和创建节点所使用的数据返回给 inserNode 函数,由其完成将该可拖放实体插入到 DOM 树中的相应位置。 在实际的情况中,往往需要在一个可拖放实体拖入目标容器后发生变化。例如在线购 买东西时,选中希望购买的东西,将其拖入购物车后,一般代表商品的图片将会变小许多。 那么就其前面所了解的,在可拖放实体进入目标容器的时候,将调用“targetnodecreater” 函数来构造在目标容器中的实体。那么如果希望其发生变化的话,就必须得在 “targetnodecreater”上做些文章。比如希望任何可拖放实体进入目标容器后都变为字符串 “111”。 清单 12 function targetnodecreater(data, hint) { var myitem = dojo.doc.createElement("div"); myitem.id = dojo.dnd.getUniqueId(); myitem.innerHTML = "111"; return {node: myitem, data: data}; } 清单 12 的代码中创建了一个任何可拖放实体进入目标容器后都将变为字符串“111” 的构造函数。 拖放柄 同拖动操作一样,拖放操作也有自己的拖放柄。其拖放柄存在的原因和意义,这里也 不再详细叙述。 要给一个可拖放实体增加一个拖放柄需要完成两步操作。 第一步,是在可拖放实体中增加一个拖放柄实体并将其 Class 声明为 class= “dojoDndHandle”。 89 第二步,是在容器中增加 withHandles= “true”这样一个属性。 在完成了上述两步以后,可拖放实体的拖放操作只能通过抓住拖放柄来实现,对可拖 放实体的其余部分则无法进行拖放操作。 清单 13
Handle
BLUE
清单 13 是通过静态方法实现可拖放实体的拖放柄,紧接着介绍动态创建可拖放实体 拖放柄的方法。 清单 14 function init()\ { mysource = new dojo.dnd.Source("mysource",{creator: sourcenodecreater, copyOnly: false, withHandles: true});// 存放可拖放实体的 " 源容器 ",其中声明 withHandles 的 值为“true” mysource.insertNodes(false,["Do you love me?", "Handle Good good study"," Handle
"]); // 通过直接写入静态标签属性 class='dojoDndHandle' 构建“拖放柄” mytarget = new dojo.dnd.Target("mytarget", {creator: targetnodecreater, copyOnly: false} ); // 存放可拖放实体的 " 目标容器 " } 清单 14 是动态创建拖放柄的一个完整的实例。 可拖放实体的替身 可拖放实体的替身是一个体积细微,但又作用重大的部分。替身分为两个部分,上面 的一个部分是“头”,下面的一个部分是“身体”。 90 图 4 图 4 清楚的表明了替身的头部分和身体部分,其中红色框内的为“头”部分,绿色框内 的为“身体”部分。对于替身身体形式的定义,可以在可拖放实体的构造函数中完成。对于替 身头形式的定义,则需要通过 CSS 来完成。 如果要对替身的身体部分进行修改,则需要 hint 帮助 (hint 为拖放实体构造函数两 个参数中的一个 ) 。替身身体的本质也是调用可拖放实体的构造函数来构建的。因此如果 需要构建特定的替身身体,就需要在可拖放实体的构造函数里面来做文章。 所有的可拖放实体包括替身的身体部分都是通过可拖放实体的构造函数来构建的。那 么带来的一个问题是构造函数如何判断,某次调用可拖放实体的构造函数是构建可拖放实体 还是替身呢?这时候 hint 可以被用来帮助进行区分。如果 hint 的值不等于“avatar”的 话,则说明是构建可拖放实体,反之就是说明构建的是替身。 清单 15 if(hint!="avatar") myitem.innerHTML = data; else myitem.innerHTML = "Haha,my avatar"; 清单 15 表示当 hint 判断不是“avatar”的时候,根据传入的 data 的值构建可拖放 实体,当判断是“avatar”的时候,替身的身体部分就写入“Haha,my avatar”。在能够按 91 照自己的意愿构造替身的身体部分后,下一步想到的是改变替身的头部分。在 Dojo 设计 dnd 替身的时候,其暴露给使用者的只是对身体的修改,但是在个别情况下,可能希望美 化替身,例如想对替身的头进行修改。这时候需要操作的是 CSS 。 在 dojo_path/dojo/resources 有一个 dnd.css 文件,在这个文件中,定义了 dnd 操作的一些页面效果。如果期望对替身的头进行修改的话,就必须在当前页面中重新定义与 其相关的 CSS 定义。例如如果想将替身头部分的背景颜色重新定义为蓝色。则可以在当 前页面的 head 部份写入 “.dojoDndAvatarHeader {background: blue;}”。 清单 16 清单 16 是将替身头部分修改为蓝色的实例。要实现修改后效果,需将其放入当前页 面的 head 部分内。 捕获拖放中的事件 Dojo 将其认为可能常用的一些事件进行了注册,并将这些注册的事件以相对应的名 称发布出来。 /dnd/start:当拖放开始的时候监测到该事件,能获取的相关值包括当前源容器、 拖放的节点和判断这次操作是否是复制操作的布尔值。 /dnd/source/over:当鼠标滑入或滑出一个容器的时候,监测到该事件。需要注 意的是当滑入或滑出一个容器的时候都会获得容器。但如果从一个容器滑入到容器外,得到 的第二个容器值为空。如果从一个容器直接滑入到另外一个容器,得到的第二个容器值不为 空。 /dnd/drop/before:该方法只被 Dojo1.1.0 或更高的版本所支持。在 drop 发 生之前监测到该事件。换句话,可以在该方法中定义一些功能,这些功能将在 drop 发生 之前的一刹那发生。 /dnd/drop:当放下可拖放实体的时候,可监测到该事件。 /dnd/cancel:当拖放动作取消,或者可拖放实体被拖放到一个无效区域时,可监 测到该事件。 清单 17 function init() { mysource = new dojo.dnd.Source("mysource",{creator: sourcenodecreater, copyOnly: 92 false});// 存放可拖放实体的 " 源容器 " mysource.insertNodes(false,["Do you love me?","Good good study","
"]); mytarget = new dojo.dnd.Source("mytarget",{creator: targetnodecreater, copyOnly: false});// 存放可拖放实体的 " 目标容器 " } function sourcenodecreater(data, hint) { var myitem = dojo.doc.createElement("div"); myitem.id = dojo.dnd.getUniqueId(); if(hint!="avatar") myitem.innerHTML = data; else myitem.innerHTML = "Haha,my avatar"; return {node: myitem, data: data}; } function targetnodecreater(data, hint) { var myitem = dojo.doc.createElement("div"); myitem.id = dojo.dnd.getUniqueId(); myitem.innerHTML = "111"; return {node: myitem, data: data}; } dojo.subscribe("/dnd/start", function(source,nodes,iscopy){ console.debug(source);console.debug(nodes);console.debug(iscopy); });// 注册开始事件,当拖放动作开始时,便会有输出 dojo.subscribe("/dnd/source/over", function(source){ console.debug(source);});// 注册鼠标滑过容器事件,当鼠标滑过容器的时候,便会有输出 dojo.subscribe("/dnd/drop/before", function(source,nodes,iscopy){ console.debug(source);console.debug(nodes);console.debug(iscopy); });// 注册结束前事件,当拖放动作接受前时,便会有输出 dojo.subscribe("/dnd/drop", 93 function(source,nodes,iscopy){ console.debug(source); console.debug(nodes); console.debug(iscopy); console.debug("bad"); });// 注册结束事件,当拖放动作结束时,便会有输出 dojo.subscribe("/dnd/cancel", function(){ console.debug("cancel");});// 注册取消事件,当拖放动作取消时,便会有输出 dojo.addOnLoad(init); 在清单 17 中,监测了拖放操作中的开始、结束、取消、结束前和鼠标滑过容器五个 动作,并将这些动作函数接受到的值进行了输出。在实际项目中对拖放事件的操作就是建立 在监测这些事件和这些事件输出值的基础之上的。所不同的只是不同的处理方法和不同的处 理顺序。 对于监听事件函数的输出值,“source”表示源容器;“nodes”表示进行拖放操作的“可 拖放实体们”(“nodes”是一个数组);“iscopy”为“true”或“false”,表示这次操作是否是 复制操作。 结束语 现在你应该了解了如何使用 Dojo 所支持的页面拖拽操作来开发自己的项目,同时也 应该了解了“拖动”和“拖放”的区别。那么,接下来请在你的电脑上创建一个 HTML 格式的 空白文本,去尝试上面的代码,在实践中去感受 Dojo 拖拽功能的强大。 Dojo 的 UI 组件库 – Dijit Dijit 简介 从 Dojo 0.9 开始,Dojo 把 Widget 从 Dojo 的核心包中分离出来,组成 Dijit 。 Dojo 在 Dijit 中为 Widget 家族添加了多位成员,增强了 Widget 的实力的同时也加快 了其在页面中的加载速度。 为改善 Widget 的外观,Dijit 提供了多套样式主题,比如默认绑定的样式主题 Tundra,提高页面可访问性的样式主题 A11y,以及其它两种可供选择的主题 Soria 和 Nihilo 。并且开发人员还可以根据自身项目的需求开发个性化的主题。同时 Dijit 对国际 94 化和针对残障人士的可访问性的支持成度很高。可以说 Dijit 已成为 Dojo 工具包中三辆 马车之一。 本文将会详细介绍 Dijit 中的 Widget 的使用。由于 Dojo Widget 数量众多,不能 一一介绍。为此从功能的角度把 Widget 分为三类:表单 Widget,布局 Widget,高级 Widget 。在每个类别中选择代表性的 Widget 结合示例加以介绍。 表单 Widget 的使用 在有用户概念的 Web 应用中,注册是一项必不可少的功能,同时注册也是有点枯燥 的任务。有个不争的事实就是:没人喜欢填表单——无论是网上还是网下。所以设计有效的 页面表单不是件容易的事情。如果不能改变注册表单存在的事实,那么就要改变表单枯燥令 人生厌的现状,让表单或整个注册过程变得轻松省力。为实现这个目标,Web 开发人员会 把较长的注册表单设计成标签的形式;当用户填错信息时,利用 Javascript 和正则表达式 的结合给用户一些提示信息;设计方便的日期选择组件等等。可是这些提高用户友好性的努 力往往会给开发人员增加很大的工作量,并且确保这些组件的浏览器无关性也不是一件容易 的事情。 有没有更简便的方法来开发 Web 表单呢?当然有!Dijit 为 Web 开发人员提供了 一系列的表单 Widgets,利用这些表单 Widgets,Web 开发人员可以轻松的设计出功能 强大、用户友好性高的表单。 表单 Widget 简介 可以说每一个 HTML 表单控件都可以在 Dijit 找到与其对应的表单 widgets 。下面 列表列出了目前 Dijit 提供的 Form Widgets: Form – 类似于 HTML 的 [form] 控件,同时提供了一些有用的方法和扩展点; Button – 类似于 HTML 的 [button] 风格的控件,同时增加了一些高级的特性; CheckBox RadioButton ToggleButton ComboBox – 类似于 HTML 的 [select] 组合框和 [text] 域控件的结合体。可以 像 [select] 组合框那样提供一列可选值;同时允许用户忽略这些可选值,而像在 [text] 域控件里那样输入自己想要输入的任何值; FilteringSelect – 类似于 HTML 的 [select] 控件,可以动态填充可选项,并且可 以按需设置加载选项的数量; 95 Textbox – 类似于 HTML 的 [text] 控件,同时提供一系列很酷的功能:可以裁空, 改变大小写,设置必填,验证输入合法性,日期组件等; Validation Currency Date, Time Integer Textarea – 类似于 HTML 的 [textarea] 控件,同时可以根据文本的容量动态调 整自己的大小,达到了真正的按需分配空间; Slider – 这个 widget 没有相对应的 HTML 控件,是一个相对独立的图形化的组 件,可以通过鼠标、方向键来控制刻度尺的刻度。 NumberSpinner – 数字转轴,应用此 widget,会让数字的输入更方便,可以通过 输入框右侧的上下按钮(支持键盘上下方向键)来调节数字大小。 所有的这些表单 widgets 都可以放置在 HTML 的 [form] 标签内,也可以放在 dijit.form.Form widget 内,甚至可以放在 [form] 标签外。这些表单 widgets 拥有标 准的 HTML 控件的所有属性和方法,在实际开发中可以完全取代标准的 HTML 控件。同 时它们都继承于 dijit.form._FormWidget,所以这些表单 widgets 还统一拥有一些附 加的属性和方法。下表是它们共有的属性和方法: 表 1. dijit.form._FormWidget 属性和方法 属性 属性类别 描述 disabled Boolean 判断此 widget 是否响应用户的输入,如果为真则此 widget 不响应用户的输入。可以用方法 setAttribute("disabled", true/false)来改变此属性 值。 intermediateChanges Boolean 判断在调用 setValue 方法后是否立即引发 onChange 事件。 tabIndex Integer 当用户点击 tab 键在 widget 中切换时,可以通过此 属性来设置 widget 获得焦点的顺序。 方法 描述 focus 在 widget 上设置 focus getValue 获得 widget 的值 setValue 设置 widget 的值 reset 重置 widget 的值为初始值 undo 恢复 widget 的值为之前最后一次更改的值 96 表单 Widget 使用示例 接下来,我们将通过一个网站注册表单案例的实现来介绍表单 widget 的使用方法。此 案例的场景是一个电子商务类型网站的用户注册页面。表单的设计概要需求为: 表单结构为多步骤,需要给出清晰的导航 使用进度标尺来告诉用户当前的位置和整个步骤 强调几个步骤中的逻辑联系,比如标明:第一步、第二步、第三步等 表单的布局 尽量使用对齐的字段、等长的输入框以及一致的视觉样式来减少视觉干扰 尽量控制在一屏内出现 3-6 个字段或输入框 标明选填和必填的差别 注册过程中的提示 提示信息(tips)尽量在需要帮助的地方和时间出现 填写表单时如有出错,即时显示提醒 / 警示信息,指引用户改正 尽量避免出现弹出框的警示提醒 这个注册页面设计成两步三屏组成:设置用户名和密码,填写个人资料,注册完成。 这三屏由 Slider widget 作为进度标尺串联而成,下面分步介绍这些表单 widgets 。 图 1 在由图 1 可以看到所需要填写的域有用户名、密码、设置密码保护问题、答案等。在 处理表单提交的数据时,经常会碰到非法数据,此时就需要对数据的合法性进行校验。目前 主流应用都会采用服务器端验证和客户端验证相结合的方案以兼顾网站安全性和用户友好 性。Dijit 的 TextBox 家族提供了 Validation, Currency, Number, Date, Time 等 Widgets 。可以说它们是标准 text 标签的功能加强版,在方便用户输入的同时,加强了 数据的合法性校验能力。 97 对于需要用户输入的文本域,ValidationTextBox widget 提供了强大的正则匹配功 能,正好符合此需求;对于设置密码保护问题这一项,需要为用户提供备选项,同时又允许 用户输入自己的希望输入的内容。本案例选择了用 ComboBox widget 来实现。所以在 第一步中应用到了三类 widgets:Form widget,TextBox widget 和 ComboBox widget 。可以说这三个 widget 的用法都相对简单,下面来看一下它们用法。 首先看 Form widget 的使用方法,清单 1 是实例化 Form widget 的代码。 清单 1
从清单 1 中可以看到实例化 Form widget 首先要声明 dojoType="dijit.form.Form",同时这里应用了 Form widget 的扩展点:execute 。 这个扩展点就相当于 HTML 的 form 控件的事件 onSubmit,会在用户提交表单时执行 一些 Javascript 方法。本案例中调用了一个方法 showSteps() 来显示完成页面,同时 把用户填写的表单数据用 alert 的方式打印出来。 设置好 Form widget 之后就需要向其中添加 ValidationTextBox widget 和 ComboBox widget,清单 2 是用户名、密码、确认密码、设置密码保护问题、答案等域 的实现代码。 清单 2
设置用户名和密码

98



在清单 2 中有四处用到了 ValidationTextBox widget,其中用户名和答案这个两 个文本域的验证相对简单,只是通过属性 required="true"来设置此域是必须填写的。同 时通过设置属性 invalidMessage 来设置错误提示信息。不过请读者注意,这里的只是做 了一个简单的 Demo,以演示 Widgets 的用法。在实际应用中应需要从客户端和服务器 端两方面对用户名等表单数据的合法性进行校验。 下图是用户没有填写用户名时,显示的错误提示信息。错误提示信息会以 Dijit 的一 个高级 widget - Tooltip 为载体显示出来,开发人员可以通过 ValidationTextBox widget 的属性 tooltipPosition[] 来设置 Tooltip 显示的位置,关于 Tooltip 的用法会 在本文第四部分:高级 Widget 的使用中介绍。 图 2 看到这里,有的读者可能会问,难道 ValidationTextBox widget 就只能设置文本域 是否必填么?不是的,它还有更炫的利用正则匹配进行验证的机制。通过密码和确认密码这 两个文本域的应用可以初步了解 ValidationTextBox widget 相对高级的验证方式。 ValidationTextBox widget 一般采用正则表达式来验证,利用正则表达式强大的匹 配功能,可以说 ValidationTextBox widget 可以满足目前各种常用输入域的格式要求。 比如 IP 地址的验证,URL 的验证,Email 地址的验证,密码格式的验证等等。清单 2 中 密码输入域的验证就是采用了正则表达式匹配验证。 100 ValidationTextBox widget 提供了两种引用正则表达式的方式:直接引用和通过调 用方法返回正则表达式的方式引用。在清单 2 中 ID 为 password 的 ValidationTextBox widget 采用的是第一种引用正则表达式的方式:直接设置属性 regExp 的值为正则表达式,如 regExp="[a-zA-Z]\w{5,17}"。这里的正则表达式要匹 配的是以字母开头([a-zA-Z]),后接 4 到 16 个任意单一字符(\w 表示任意单一字符 , 同 [a-zA-Z0-9])。ValidationTextBox 另外一种方式:设置属性 regExpGen 的值为 返回正则表达式的方法名,如 regExpGen="dojox.regexp.emailAddress"。方法 dojox.regexp.emailAddress 中提供了域名列表,更明确的限制了域名的合法性。建议 简单的验证可以选择设置 regExp 属性,而较为复杂的验证选择设置 regExpGen 属性,这 样可以提供较为复杂的正则表达式组合。 与用户名输入域相比,密码输入域还增加了一个方法:promptMessage 。这是 ValidationTextBoxes widget 获得焦点时弹出的辅助提示信息,而 invalidMessage 是 在用户输入不合法时的即时提示。图 3 是这两种提示信息的比较图。当密码输入域获得焦 点时,会弹出辅助提示信息:密码必须以字母开头,长度在 6~18 之间,并且只能包含字 符、数字和下划线。此时输入域处在编辑的状态,并且颜色没有改变。当用户进行输入未符 合要求时,会弹出错误提示信息:请确认密码以字母开头,只能包含字符、数字和下划线, 同时长度在 6~18 之间!此时输入域为黄色,并且在输入框末尾有警示图标。 图 3 另外在第一步中还有一个 widget:ComboBox 。可以说 ComboBox widget 一 个自动完成、辅助用户输入的文本输入域。它是 [select] 组合框和 [text] 输入域的结合 体:既可以像 [select] 组合框那样提供一列可选值,也可以像在 [text] 输入域里那样输 入用户想要输入的任何值。首先来了解一下 ComboBox widget 的属性: 表 2. dijit.form.ComboBox 的属性列表 属性 属性类别 描述 autoComplete Boolean true 判断是否自动完成用户输入的内容。当值为真时,用户输入部分 字符串,ComboBox widget 会把能匹配上的可选值列出,如 果光标离开此 widget,会显示第一个匹配的选项值。 hasDownArrow Boolean true 判断是否现实下拉按钮。 ignoreCase Boolean true 判断是否忽略大小写(针对英文的输入)。 101 pageSize Integer Infinity 此属性可以设置下拉列表显示的条数,如果出现多页的情况,会 在下拉列表中显示” Previous choices ”和” More choices ” 按钮。用户可以通过点击这两个按钮来查找选项。 query Object {} 设置查询表达式以过滤’ store ’里的选项。 searchAttr String name 设置查找的匹配表达式 searchDelay boolean true 当用户输入内容后到 Dojo 开始查找用户输入值的匹配项之间 的间隔时间。 store Object 数据提供对象的一个引用。.Dijit 中一般应用 JSON 格式的数 据。 清单 2 中的 ComboBox widget 实例用到了两个属性 autoComplete 和 hasDownArrow 。这两个属性已经表 2 中介绍过了,这里不再多说。ComboBox widget 的另一亮点就是可以从外部文件动态加载选项,并且提供了属性来过滤选项,同时可以设置 下拉列表每页显示选项的数量。 第一步就讲到这里,下面来看一下第二页中用到的 widgets 。 图 4 在图 4 中可以看到需要填写的项为:真实姓名、邮编、手机号。在这里仍然选择 ValidationTextBox widget 。性别项为单选项,本例选择了 RadioButton widget 。 出生日期项需要用户按照 MM/DD/YYYY 的格式输入日期,以往 Web 开发人员可能会需 要写大段的 JS 代码来实现一个日期控件,并且还要写一堆日期格式的验证代码。现在 Dijit 提供了一个 DateTextBox widget,一行代码搞定所有的工作。对于“省”这一项, 102 选择的是 FilteringSelect widget 。可以说 FilteringSelect 跟 ComboBox 非常类似, 不过 FilteringSelect 不允许用户输入可选项之外的值。因为市名特别多,并且不易收集 完整,所以这个 Demo 中选择使用了 ComboBox widget 来实现。在给出用户一定的可 选项的同时,允许用户自己输入。最后同意条款的复选框理所当然的选择了 CheckBox widget 。 由于在第一步中已经介绍过 ValidationTextBox widget,这里不再赘述。 RadioButton widget 和 CheckBox widget 同属于 Button 类型的 widget,其使用 非常方便:只需要在 HTML 标准的 checkbox / radio 控件上加上 dojoType 的属性, 值是” dijit.form.CheckBox ”或者” dijit.form.RadioButton ”。此处要注意的是声明 dojoType 时,大小写敏感。清单 3 中图 4 中使用 CheckBox widget 的代码示例: 清单 3 当 Dojo 解析到控件的属性 dojoType 为 dijit.form.CheckBox 时,会应用 dijit.form.CheckBox 中定义的 templateString(清单 4)替换掉源代码中 [checkbox] 的定义增加了一些属性和方法,然后输出到页面(清单 5)。 清单 4
\n 清单 5
由清单 4 和清单 5 可以看出,Dojo 接管了标准 [checkbox] 控件的三个事件: onmouseover,onmouseout 和 onclick 。分别代替为 _onMouse 和 _onClick,以 此更改 Checkbox 的表现形式。 提示: 如果有兴趣可以在文件 _FormWidget.js 中找到 _onMouse 方法的定义,在 Button.js 文件中找到 _onClick 方法的定义。 图 5 图 5 是完成页面的截图,非常简单,只显示了注册完成的提示信息。不过这步让包括 了一个我们还没有介绍的 widget:Slider widget 。其实 Slider 贯穿了三步,在每屏都 会显示注册的进度,可以说是表单 Widget 中比较独特的一个,下面详细介绍了 Slider Widget 的使用。 Slider Widget 的使用 与 CheckBox widget 相比,Slider widget 较为复杂,至少在标准的 HTML 标签 库中是没有 Slider 控件的。Slider widget 的使用却如同使用 CheckBox widget 一样 104 的方便。用户可以用鼠标点击、拖拉、中键滚动或者用键盘的上、下、左、右按键来选择 Slider widget 的刻度。 Silder 是由六个 widgets 组成,分别是:dijit.form.HorizontalSlider, dijit.form.VerticalSlider,dijit.form.HorizontalRule, dijit.form.VerticalRule, dijit.form.HorizontalRuleLabels, dijit.form.VerticalRuleLabels 。这里可以“望文生 义”一下:从 widgets 的名字可以看出 dijit.form.HorizontalSlider, dijit.form.VerticalSlider 分别是水平方向和垂直方向的 slider,提供可调节大小的标尺。 其属性如下表: 表 3. HorizontalSlider 和 VerticalSlider 的属性 属性 属性类别 描述 clickSelect boolean true 判断是否可以点击进度条来更改刻度值 discreteValues integer Infinity 在最大值与最小值之间可以设置的最大值 intermediateChanges Boolean false 判断在调用 setValue 方法时是否直接触发 onChange 事件。如果为’ true ’,则每次执行 setValue 方法时都会触发 onChange 事件;如果为’ false ’,则 onChange 事件只有在自己被调用时才会被触发。 maximum integer 100 可设置的最大刻度值 minimum integer 0 可设置的最小刻度值 pageIncrement integer 2 点击键盘按键 pageup/pagedown 时一次变化的刻 度值 showButtons boolean true 判断是否在刻度条两端显示增加和减小的按钮 dijit.form.HorizontalRule, dijit.form.VerticalRule 可以为 HorizontalSlider 和 VerticalSlider 设置标识线,其属性如下: 表 4. HorizontalRule 和 VerticalRule 的属性 属性 属性类别 描述 container Node containerNode 设置要连接的父节点 count Integer 3 标识线的数量 105 ruleStyle String 为个别标识线设置 CSS style 当在页面中有多个 dijit.form.HorizontalRule widgets 时,通过设置 container 值来区分他们,比如清单 6 中第一个 dijit.form.HorizontalRule widget 设置 container="topDecoration",而第二个设置 container="bottomDecoration"。这样 用户就可以看到在标尺的上下两方各有一组标识线。同时设置属性 count 的值来确定标识 线的个数,譬如清单 6 中第一个 dijit.form.HorizontalRule widget 的 count 的值为 6,则在标尺上方会有六个标识线,每两个标识线间距是总长的 1/5 。 dijit.form.HorizontalRuleLabels, dijit.form.VerticalRuleLabels 可以为标尺提 供刻度标签,其属性如下: 表 5. HorizontalRuleLabels 和 VerticalRuleLabels 的属性 属性 属性类别 描述 labels Array [] 文本类型的数组,存放要展现的标签值 labelStyle String 为个别标签设置 CSS style dijit.form.HorizontalRuleLabels 是继承 dijit.form.HorizontalRule,所以 RuleLabels 类型的 widgets 都具有 Rule 类型 widgets 的属性,同时也有自己特有的 属性。通过 labels 属性,开发人员可以随心设置刻度标签的值。 Dijit 提供了两种方式来设置标签值,第一种就是直接设置 labels 属性的值为一数组 或者返回数组的 Javascript 方法;第二种方式就是在页面标记为 dijit.form.HorizontalRuleLabels 的标签内部设置
  • ,这也正是清单 6 中采用的方 式。当 labels 属性值为空时,Slider 会自动搜索本节点内部的
  • 标签,取出
  • 标 签节点的 innerHTML 作为单个标签值。 下面通过实例来看 Slider widget 的使用。 清单 6 Dijit HorizontalSlider Example
    1. 20%
    2. 40%
    3. 60%
    4. 80%
    1. 0%
    2. 50%
    3. 107
    4. 100%
    Slider Value: 在清单 6 中可以看到 Slider,Rule,RuleLabels 的应用。 dijit.form.HorizontalSlider 是把 dojoType 属性设置在’ DIV ’标签内的,由于 intermediateChanges="true",onChange 事件会在调用 setValue 方法时自动触发。 这里设置的 onChange 事件是更新 id 为’ sliderinput ’的一个只读域的值,效果如图 6 所示。在实际项目中 Slider widgets 可以应用到多个模块中,比如图片的浏览和缩放, 多页面间的转换,文本长度的控制等等。 图 6 布局 Widget 的使用 Web 应用的页面布局一直是令 web 开发者头疼的一件事情。在 Web 2.0 的时代, Web 页面布局的设计越来越多样化,仅仅依靠 Tables 和 CSS 控制的页面布局已难满足 用户的需求。为此 Dijit 提供了一系列的布局 Widgets 辅助 Web 开发人员实现复杂的 页面布局。 布局 Widget 简介 首先来浏览一下布局 Widgets 的成员。Dijit 中提供的布局 Widgets 可以分为三 类: 对齐方式容器:用以盛放屏面类 widgets,并且可以设置这些 widgets 的排列方式。 这类的布局 Widgets 有 BorderContainer,LayoutContainer,SplitContainer 。其 中 BorderContainer 是在 Dojo1.1 中引进的轻量级组件,有取代 LayoutContainer, SplitContainer widgets 之势。目前 Dojo 不推荐使用 LayoutContainer, SplitContainer widgets ; 108 堆叠容器:此类的 widgets 可以把前两种 widgets 层叠在一起,而一次只显示一个 屏面。这类的的布局 Widgets 有 AccordionContainer,TabContainer, StackContainer 等。 屏面:盛放和显示大块的内容,包括文本、图片、图表,甚至是其它 widgets 。这 类的布局 Widgets 有 ContentPane 等; 在设计页面布局时,首先应选择页面整体的框架:上下两栏、左右两栏、上中下三栏、 左中右三栏、上一栏下两栏、上下左右中五栏等。在以往的 Div + CSS 设计布局时,虽 然可以轻松得做到前五种布局的实现,但如果要实现最后一种五栏的布局,却有些困难。并 且设计后的布局间的比例或者每栏的大小都是固定的,当一栏的内容超出栏宽 / 高时,只 能通过左右拉条或者下拉条的拖动来显示超出的内容。可以说既麻烦又不美观。 在 Dijit 的布局 widgets 中,对齐方式容器类的 BorderContainer widget 提供 了一套简单的 API,可以在页面中设置上下左右中五栏的内容 ―― 也就是设置屏面的内 容,甚至可以嵌套设计。同时在每两个相邻的屏面间有一分割的组件,可以调节屏面的大小。 为解决页面内容多,导致出现左右或者上下拖动条的情况,Dijit 提供了堆叠类容器。 无论是 AccordionContainer,TabContainer,还是 StackContainer 它们实现的功能 是一样的:把内容分成多个屏面,每次只显示一屏的内容,要想显示其它屏的内容,需要点 击那屏的标题栏。 如果说前面这两类布局 widgets 为页面提供了骨架和骨骼的话,那么屏面类 widgets 就是填充这些骨架的真材实料。下面简单介绍这三类布局 widgets 的属性、方 法以及用法等。 BorderContainer widget 首先介绍页面的骨架:对齐方式容器。前面已经说过 LayoutContainer 和 SplitContainer widgets 在 Dojo1.1 中被标注为不推荐使用,所以此类 widget 首推 BorderContainer 。可以说 BorderContainer 是 LayoutContainer 的升级版,同时 集成了 SplitContainer 优点:为用户提供可拖动的边界。 像之前提过的那样,BorderContainer 可以向五个区域输出:上下左右中。同时这五 个区域也有两种不同的摆设,可以通过属性 design 来设置。如果 design 为 “ headline ”,上下两个区域的宽度就会与 BorderContainer 的宽度相同;如果 design 为“ sidebar ”,左右两个区域的高度就会和 BorderContainer 的高度相同。下面是这两 种设计形式的示意图,其中第一张图片的 design 属性为“ headline ”。 图 7 design 属性分别为“headline”和“sidebar”的 BorderContainer 示意图 109 清单 7 是图 7 中 design 属性值为“ headline ”的实现代码,直接更改 design 属 性值为” sidebar ”就可以实现第二张图的效果。同时从清单 7 中还可以注意到 清单 7
    leading
    top bar
    110
    main panel
    bottom bar
    trailing
    通过清单 7 也可以看到样式的定义中设置了属性 width 和 height,这里也是要注 意的地方:BorderContainer 节点需要设置 width 和 height 。同时左右两个子节点可 以设置宽度,而上下两个子节点可以设置高度。中间区域的子节点无需设置大小,刨去四个 边界区域占有的空间,剩下的就是中间区域。 当需要调节区域大小时,spliter 属性就派上用场了。当开发人员希望用户可以自己调 节屏面大小时,可以设置此属性值为“ true ”。这样在两屏面间就会出现一个可以拖拽的边 界。同时如果开发人员不希望用户自己调节屏面大小,就可以设置此属性为“ false ”。 TabContainer widget 当一个页面内容较多,而用户不希望像看十米长卷一样一直向下托动滚动条来浏览页 面时,应用堆叠容器 widgets 是一个比较不错的选择。将功能类似的一些信息放在同一个 标签页内,用户可以方便的在不同的标签页之间切换,并且可以关闭不想要的标签页。 AccordionContainer,TabContainer,StackContaine 三个 widgets 实现的功能相同, 只是表现形式不同,这里通过 TabContainer 的介绍来了解一下堆叠容器 widgets 的使 用。 首先看一下实现一个 TabContainer 所需要的元素。TabContainer 实现的功能就 是包含多个内容面,而一次只显示一个。为了可以选择用户需要的内容面,就需要为每一个 内容面配备一个标签。就像平时自己整理文档一样,每个标签上面都有标题来标注此类文档 的用途。在现实生活中,有时候不需要某份文档了,就会把此文档粉碎。为模仿此操作就需 要为每个标签配备一个关闭按钮(可选的)。那好,到目前为止构建一个 TabContainer 所 需的元素凑齐了:内容面,标签,关闭按钮。那剩下来就是技术活:关联这三个元素, 111 TabContainer 已经为开发者做好了这项工作,开发人员所需要的就是创建 TabContainer 和需要的 ContentPane 。下面来看一代码片段: 清单 8
    The first pane. Can put text,picture or dialog here!
    The second pane! You can close me, Cool ha!
    在清单 8 中的代码片断是一个简单的 TabContainer 例子,声明了一个 TabContainer widget 和两个 ContentPane widget 。其中 TabContainer 的声明很 方便,直接设置 dojoType="dijit.layout.TabContainer"就可以,省下来的工作就是声明 ContentPane 。ContentPane widget 会有一些特殊的属性,比如 closable="true"。 这是标签是否附带关闭按钮的标示,如果为“ true ”则标签上会显示关闭按钮。同时标签的 内容是通过 title 属性来设置的,图 8 是清单 8 在浏览器中的输出。 图 8 TabContainer 示例效果图 如果希望不同的标签显示的是单独的页面文件时,可以设置 dijit.layout.ContentPane 的属性 href 。譬如
    。 ContentPane widget ContentPane 是所有布局 widgets 的基石,其他的任何一个布局 widgets 都可以 用 ContentPane 作为内容或者子 widget 的载体。同时 ContentPane 也可以单独使 112 用,可以盛放文本、图片、图表,甚至其它 widgets 。首先看一下 ContentPane wdiget 的属性和方法。 表 6.ContentPane widget 的属性列表 属性 属性类别 描述 errorMessage String Locale dep. 错误提示信息,可以在 loading.js 文件中更改默认信息。 extractContent Boolean false 当取回的内容是页面时,判断是否抽取页面标签 … 内的可见的内容。 href String 当前实现内容的超链接。如果在构造 ContentPane widget 的时候设置此项,就可以在 widget 显示的时候加载数据。 isLoaded Boolean false 设置加载状态。 loadingMessage String Locale dep. 加载时显示的信息,同 errorMessage 一样可以在 loading.js 文件中更改默认信息。 parseOnLoad Boolean true 解析取回的内容,如果有 widgets 的声明,会实例化 widgets 。 preload Boolean false 强制加载数据。 preventCache Boolean false 判断是否缓存取回的外部数据。 refreshOnShow Boolean false 在本 widget 从隐藏到展现时,判断是否刷新数据。 表 7.ContentPane widget 的方法列表 方法 描述 cancel() 取消进行中的内容下载 refresh() 强制刷新 resize(/* String */size) 此方法可以重新设置 widget 的大 小。 setContent(/*String|DomNode|Nodelist*/data) 代替原有的内容,替换为新的内容。 这个方法经常用到,可以动态向 113 ContentPane 中输入其它 widgets 。 setHref(/*String|Uri*/ href) 替换原有的超链接,通过 XHR 的形 式异步获取数据,然后重置此 widget 中的内容。 表 6 中列出的都是 ContentPane 作为一个单独的 widget 使用时的属性。如果把 ContentPane 作为其它布局 widgets 的子节点,就需要为不同的布局 widget 增加不 同的属性。譬如在清单 7 中的 region="top"和清单 8 中的 closable="true"等。 布局 Widget 使用示例 这部分通过一个简单的页面布局示例把这三中布局 widgets 串连在一起。示例的需 求是这样的:一个页面需要分为四个区域,上下左和中;头部为页面标题,左侧为导航栏, 底部为另外一个导航栏,中间部分显示详细信息。图 9 本示例的效果图。 图 9 布局 Widget 示例效果图 为把页面分为四部分,需要采用 BorderContainer widget 。设置其属性 design 为 “ headline ”,同时创建 4 个子节点,属性 rigion 分别为 “ left ”“ top ”“ buttom ”“ center ”。其中左侧栏为导航栏,选择 AccordionContainer ; 上栏只是显示标题,所以直接安放一个 ContentPane ;底部也是一个导航栏,这里选择 TabContainer 来实现;中间部分显示左侧导航栏的详细信息,选择 ContentPane 。那 么这些 widgets 的层次关系就如下面的列表: BorderContainer ; Top Border Container ContentPane #1 114 Left Border Container Accordion Container ContentPane #2 ContentPane #3 ContentPane #4 Right Border Container ContentPane #5 Bottom Border Container Tab Container ContentPane #6 ContentPane #7 根据这些 widgets 的层次关系就很容易创建这个页面的布局。清单 9 是本示例的实 现代码: 清单 9

    Page Title

    116
    main panel to display different contents according to the selected pane of left bar.
    高级 Widget 的使用 本部分会简单介绍一下高级 Widget 的使用。 高级 Widget 简介 Dijit 在提供一些基本的 widgets 的同时也提供了一些高级功能的 widgets 。比如 说 Editor,ProgressBar,Tooltip,ColorPaletee,Tree,Dialog 等。这里把这些高级 widgets 分为两类:用户辅助 Widget,高级编辑和显示 Widget。 用户辅助 Widget 包括: Dialog:相对应于 HTML 的对话框,是一个模式对话框。用户能通过此 widget 上 的关闭按钮关闭此对话框,同时也可以在此对话框上放置表单 widgets,并且可以在此对 话框上直接提交表单; TooltipDialog:此 widget 必须关联一个 DropDownButton 。用户点击 DropDownButton 时,此对话框显示,用户点击此对话框外任何位置都可以让此对话框 消失; ProgressBar:Dijit 提供的进度条组件; Tooltip:此 widget 的表现形式类似于属性 title,功能却很远比属性 title 强大。 弹出的提示窗口中可以显示文本、图片和页面。同时可以设置提示窗口出现的位置,之后会 详细介绍 Tooltip 。 高级编辑和显示 Widget 包括: ColorPalette:一个颜色选择组件,为用户提供一组可供选择的颜色块。可以跟页面 背景或者局部区域颜色关联起来。通过属性 palette 来设置可供选择的颜色块的数量,目 前提供 "7x10" 和 "3x4" 两种选择。 Editor:一个多文本的编辑器组件,外观类似于 word 编辑器。提供了一系列的编辑 按钮,包括拷贝、粘贴、撤销、各种字体处理、数字编号或者项目符号,文本对齐格式等等。 值得一提的是这个组件的架构是插件架构,支持开发人员自己开发新的命令、按钮等。 117 InlineEditBox:类似于 Dojo 0.9 中的 dijit.form.InlineEditBox widget 。可以 编辑页面显示的输出类型文本,当点击文本时,文本就会从浏览状态转换为可编辑状态。 Tree:此组件可以把有层次关系的数据用树状结构展现出来,就如同 Windows 系统 的资源浏览器。 下面通过 Tooltip 的介绍来帮助读者认识高级 Widget 的使用。 Tooltip Widget 的使用 目前大多数应用中,每个页面的控件都比较多,而仅仅通过控件显示的名称是不足以 让用户了解各种控件的作用和一组相同类型控件间的区别。标准的 HTML 控件会提供 title 属性。当用户鼠标停留在设置好 title 属性值的控件上时,浏览器会弹出一个提示, 内容就是属性 title 的值。 但是 title 属性有自己的不足之处,比如样式单一,只能显示文本,长文本在 Firefox 中不能完全显示等等。所以有不少开发人员自己动手创建自定义的提示工具。其中有的方案 是把提示信息放在层标签中,在 onmouseOver 事件中调用显示层标签的方法,在 onmouseOut 事件中调用隐藏层标签的方法,以达到模仿 title 属性的目的;同时也有方 案在页面初始化的时候收集所有设置了 title 属性的控件,取到 title 的值,经过一定的处 理显示在自定义的提示框中。 Dojo 提供了一个更美观,实用的辅助提示 widget:Dijit.Tooltip 。其样式定义在 单独的样式文件中,开发人员可以自己修改。同时也提供了一些强大的功能,比如可以显示 图片、图表和从服务器请求得到的数据等,可以控制显示的时间和出现持续的时间。 实例化 Tooltip 的方法有多种,除了所有 widget 都具备的 declaratively 和 programmatically方式外,还可以调用 Dijit 提供的方法dijit.showTooltip(/*String*/ innerHTML, /*DomNode*/ aroundNode, /*String[]?*/ position)显示 Tooltip,调 用 dijit.hideTooltip = function(aroundNode)来隐藏 Tooltip 。在后面的实例中会涉及 到这三种方法。 在看实例之前可以先浏览一下 Tooltip 的属性: 表 8. dijit.Tooltip 的属性 属性 属性类别 描述 connectId String 要挂载 Tooltip 的控件的 Id,可以为用逗号分隔的多个 Id 。 label String 要显示的提示信息 showDelay Integer 400 Tooltip 显示之前等待的时间,毫秒级 118 清单 10
    Declaratively

    Programmatically

    Calling Methods
    在清单 10 中分别应用了三种实例化的方法。Declaratively 方式实例化 Tooltip 时 除了要声明 dojoType 为 dijit.Tooltip,还要设置另外两个特殊的属性:connectId 和 label 。如表 4 所示,属性 connectId 的值是要关联 Tooltip 的控件 Id,属性 label 的 值为提示信息的内容。 Programmatically 方式实例化 Tooltip 如 var prTooltip = new dijit.Tooltip( {label:'programmatically',connectId:'programmatically'}, document.createElement('div')); 119 第一个参数是 Tooltip 特殊的属性值:label,connected,positions 等;第二个参数 是要实例化的控件,可以创建一个新的控件,也可以应用已存在的控件(用 dojo.byId(someId) 来定位要实例化为 Tooltip 的控件)。 有时候页面需要实例化的 Tooltip 非常多,曾经遇见过一个数据报表页面有五百多个链接 要挂载 Tooltip !这时无论用 Declaratively 方式还是 Programmatically 方式都会导致页面数据量猛增,达到 MB 级,在 web 应用中这是不可以接受的。在这种情况下选用第三种实例化的方式可以在很大 程度上减少页面的大小。 结束语 在富客户端的时代,web 开发人员都在寻找或创造可以彰显个性的 UI 组件。为避免 web 开发人员重复造轮子,Dojo 提供了大量实用的 Widget 。开发人员所需要做的就是 慷慨接受 Dojo 的这份大礼。本文介绍了各类 Widget 的基本使用方法,如果读者希望了 解创建个性化的 Widget,Dijit 的主题等,请读下篇文章。 Dojo Widget 的高级应用 Dojo Widget 是 Dojo 极为重要的部分,因此在对 Dojo Widget 有了全面的了解 以后,还应该加强对其使用机制的掌握。 创建 Dijit 的两种方式 Dojo 提供了两种方式给系统开发者去使用其所提供的 Dijit(Dijit 是 Dojo Widget 的简称)。第一种方法是通过直接在页面中静态的写入带有 Dijit 属性的标签去实现该 Dijit 的使用;第二种方法是通过 Javascript 语句,动态的在当前使用页面中生成 Dijit。 第一种方法被称为静态创建 Dijit,而第二种方法则被称为动态创建 Dijit。 静态创建 Dijit 静态创建 Dijit 是通过 HTML 标签属性的方式将 Dijit 引入页面的。因此静态创建 Dijit 的方法简单方便,对于初学使用者也无任何认知困难。在页面需要使用 Dijit 的时候, 静态创建 Diijt 往往成为其首选。 静态创建 Dijit 分为三个步骤来完成。 120 第一步,确定要使用 Dijit 的对象名全称。例如,如果想使用 Dijit 按钮,其对象名 全称为 dijit.form.Button。 第二步,在页面的“”和“”标签之间引入即将使用的 Dijit 模块。例 如,如果想使用 Dijit 按钮,则需在该页面的 head 部分引入 Dijit 按钮模块。 清单 1.静态创建 Dijit 示例 dojo.require(“dijit.form.Button”) 表示在页面中引入 Dijit 按钮模块。 dojo.require(“dojo.parser”) 表示在页面中引入解析 Dijit 标签属性的功能模块。 静态创建 Dijit 所使用的 Dijit 标签属性不是标准的 HTML,浏览器不能直接对其进 行解析。因此需要 dojo.parser 在页面加载完成以后,对整个页面的所有 Dijit 标签属性 进行解析,将其转换为浏览器可以识别的标记。 第三步,在页面中需要使用 Dijit 的位置,写入 Dijit 标签属性。例如,如果使用 Dijit 按钮,则为
    OK
    。 动态创建 Dijit 静态创建 Dijit 简单易用,但灵活性差,无法满足实际情况下多变的要求。因此在大 多数情况下,更多的是需要动态创建 Dijit。 例如,需要根据用户的选择,动态的创建一定数目一定类型的 Dijit;或者是在页面生 成后根据后台运算结果动态的创建 Dijit。 动态创建 Dijit 也分为三个步骤来完成。 第一步,明确要动态创建 Dijit 的对象名全称。 第二步,引入即将使用的 Dijit 模块。例如,如果想使用 Dijit 按钮,则需在该页面 的 head 部分引入 Dijit 按钮模块。 清单 2.动态创建 Dijit 示例 1 需要注意的是,清单 2 中没有引入 dojo.require(“dojo.parser”)。因为动态创建 Dijit 的过程中会自动完成向 HTML 的转换。 第三步,通过 Javascript 调用对应 Dijit 的动态构造语句,动态创建 Dijit。 清单 3.动态创建 Dijit 示例 2 121 function init() { var mydiv = document.createElement("div"); dojo.body().appendChild(mydiv); var mybutton = new dijit.form.Button({label:"OK"},mydiv); } 对于绝大部分的 Dijit,第三步动态创建 Dijit 又必须分为两个步骤。 第一个步骤 :动态创建一个“替代层”,并将该层插入到当前页面 DOM 结构中 Dijit 应处的位置。 第二个步骤 :调用该 Dijit 对应的动态构造语句,例如 new dijit.form.Button(params, srcNodeRef) 去创建该 Dijit。其中“params”是 Dijit 构 造时相关的属性参数,“srcNodeRef”是上一步骤中创建的“替代层”。 从 Dojo0.9 开始,Dojo 组织认为创建 Dijit 和确定 Dijit 在页面中的位置(这里 的“位置”是指页面 DOM 结构中的相对位置)是两个不同类型的操作。从代码结构优化和 软件工程化的角度考虑,这两个不同类型的操作在实际的应用中也应该是相对独立的。因此, 在实际创建 Dijit 的时候,首先要动态创建一个“替代层”去将 Dijit 的位置“标注”出来。 在上面的代码中,这个动态创建的“替代层”(mydiv)被插入成为“body”标签的子结 点。 var mybutton = new dijit.form.Button({label:"OK"},mydiv)) 表示调用 Dijit 的动态构造语句,并使用 mydiv 作为“替代层”,将动态创建的 Dijit 插入到页面 DOM 结 构的相应位置。 但也并不是所有的 Dijit 都必须创建“替换层”。一些 Dijit,其在页面中的位置对其功 能没有任何影响。例如 dijit.Tooltip,dijit.TooltipDialog 和 dijit.Dialog 等。因为这类 型 Dijit 的显示位置与其在 DOM 结构中的相对位置没有必然的关系,因此在一般情况下, Dojo 会默认的把这些 Dijit 插入到页面 DOM 结构最后面的位置。 此外需要注意的是,如果在动态创建的 Dijit 中再创建子 Dijit,往往会在页面运行时 出现一些莫名其妙的情况。Dojo 组织建议在动态创建 Dijit 结束以后,调用 startup()。 例如对于上面动态创建的 Dijit 按钮,可在最后加上 mybutton.startup()。加上这段代 码的原因与 Dijit 创建完成以后的解析相关。 使用静态创建的模式动态创建 Dijit 静态创建 Dijit 简单易用,动态创建 Dijit 灵活多变。将两种方法的优点结合起来, 使用静态模式动态创建 Dijit,在某些情况下将会是一种最优的选择。 122 首先回顾一下,静态创建 Dijit 的一个完整的过程。当页面加载完成以后,在引入了 Dojo 解析模块的基础上,Dojo 会自动将页面中所有的 Dijit 标签属性解析为标准的 HTML(动态创建的 Dijit,会在创建的过程中自动完成转换)。 因此如果能将 Dijit 标签属性通过动态的方法直接写入页面中,并手动模仿 Dojo 解 析 Dijit 标签属性的过程,就会实现以静态创建的模式动态创建 Dijit。 假设当前页面中有一个命名为 mydiv 的层实体,现在通过 Javascript 脚本动态的 在这个层实体中写入
    OK
    , 例如 mydiv.innerHTML =
    。那么通过这样的 操作,Dijit 按钮就插入到了页面中。 现在面临的问题就是将 mydiv 层实体中的 Dijit 标签解析为 HTML。静态创建 Dijit 的时候,页面加载完成以后会调用相应的 Dojo 解析模块将整个页面的 Dijit 标签属 性解析为 HTML。如果能够任意调用 Dojo 的解析模块,则上面通过静态方法在一个层实 体中写入的 Dijit 就能被解析为 HTML。通过调用 dojo.parser.parse(), 能够实现对 Dojo 解析模块的调用。dojo.parser.parse() 中填入的参数为需要进行解析的实体。 清单 4.静态模式动态创建 Dijit 示例 Dijit CSS 最后将动态创建 Dijit 的方法和静态创建 Dijit 的方法进行一个全面的比较。 抛开动态创建和静态创建的表象从本质上说,创建一个 Dijit 对于动态和静态都需要 以下相同的几项要素。 Dijit 将插入 DOM 结构中的位置。静态创建 Dijit 是通过直接写入到页面中,表明 其所在 DOM 结构中的位置;而动态创建 Dijit 则需要通过一个“替代层”来实现 Dijit 插 入到 DOM 树中的合适位置。 表明要创建的 Dijit 类型。静态创建 Dijit 是通过使用 dojotype 标签属性来表明; 而动态创建 Dijit 则是通过调用该 dijit 相应的动态构造语句来表明。 在创建一个 Dijit 时,需设置其的相关属性。静态创建 Dijit 是通过标签属性来定义 的;而动态创建 Dijit 则是通过在该 Dijit 相应构造语句中直接设定属性实现的。 表 1.两种方法的比较 比较项 静态创建 Dijit 动态创建 Dijit Dijit 位置 直接在相应的位置写入 Dijit 通过替代层定位 Dijit 类型 通过标签属性 DojoType 确定 通过调用相应 Dijit 的动态构造语句 设置属性 通过标签属性来定义 Dijit 属性 直接在动态构造语句中设定 Dijit 的操控 能通过静态或动态两种方式创建 Dijit 仍然不能够达到在页面中对 Dijit“为所欲为” 的要求。目前的情况是,在创建一个 Dijit 之前,可以尽可能的对 Dijit 进行各种预定义 的操作,一旦 Dijit 被创建以后,就很难再对 Dijit 施加影响。 Dojo 针对页面中已存在 Dijit 的操控给出了以对象操作为基础的解决方法。 要对 Dijit 进行操控,首先是要获得这个 Dijit 实体。正如要将一块玉石打磨成美玉, 首先是要获得那块玉石。在 Dojo 下,有两种方式能获得 Dijit 实体。 第一种,动态创建一个 Dijit 的时候,Dijit 动态构造语句的返回值就为该 Dijit 实体。 将这个 Dijit 动态构造语句的返回值赋值给一个变量,则可以通过这个变量实现对新创建 Dijit 实体的操控。例如 var mybutton = new dijit.form.Button({label:"OK",id:”testid”}, dojo.byId("mydivid")) 新创建了一个 124 Dijit 按钮实体,其动态构造语句的返回值赋给了变量 mybutton,因此可以通过 mybutton 来操控新创建的这个 Dijit 按钮实体。 如果想将新创建的 Dijit 按钮实体的 name 属性,由空字符串修改为“good”,则可 以通过操控 mybutton 来实现。例如 mybutton.name = “good”通过操控 mybutton 的 name 属性,实现了对新创建 Dijit 按钮实体属性的修改。 第二种,在知道某个 Dijit 实体 id 的情况下,则可以通过 dijit.byId() 获得这个 Dijit 实体;或者是在 Dijit 中声明一个属性 jsId,则可以直接通过 jsId 的值来操控这个 Dijit 实体。 例如 var mybutton = new dijit.form.Button({label:"OK",id:”testid”}, dojo.byId("mydivid")) 构建了一个 Dijit 实体。则由其动态构造语句,可以知道该 Dijit 按钮实体的 id 为“testid”。 而后通过 var yourbutton = dijit.byId(“testid”) 就可以获得这个 Dijit 按钮的 实体。 需要注意的是,通过 id 来获得 Dijit 实体没有通过 dojo.byId() 方法而是使用的 dijit.byId() 方法。在 Dojo 体系中,如果想通过 Dijit 的 id 来获得 Dijit 实体,只能 通过 dijit.byId()。而作用与 document.getElementById() 相同的 dojo.byId(),无 法获得任何 Dijit 实体。 Dojo 组织认为 DOM 对象实体和 Dijit 实体是两个完全不同的对象实体类型。通过 id 去获得这两种实体也应该使用不同的方法。dojo.byId() 被分配用来获得 DOM 对象实 体,而 dijit.byId() 则被分配用来获得 Dijit 类型的实体。 从实际情况来看,这种区分也是非常必要的。假如获得 DOM 对象实体和获得 Dijit 实体都使用 dojo.byId(), 且该 Dijit 实体中定义了 width 属性(注意这个 width 属性 与该 Dijit 的宽度没有任何关系,这个 width 属性可能是该 Dijit 中某个小图片的宽度), 那么当尝试修改 width 属性的时候,浏览器将无法明白希望修改的是该 Dijit DOM 对象 属性的 width(即该 Dijit 在页面中的宽度)还是希望修改该 Dijit 实体的 width 属性 (即可能该 Dijit 中某个小图片的宽度)。 Dijit 可以通过 dijit.byId() 方法获得该 Dijit 实体。但在某些情况下,也需要获得 该 Dijit 的 DOM 对象实体。所有的 Dijit 实体都有一个属性 domNode,通过该属性可 以得到该 Dijit 的 DOM 对象实体。 因此某个 Dijit 小图片的 width 属性将为 dijit.byId().width; 而该 Dijit 在某个 页面中的宽度为 dijit.byId().domNode.width。(注意 dojo.byId() 既不能获得 Dijit 实体,也不能获得 Dijit 的 DOM 对象实体) 如果具有 OO 编程的经验,则必然知道对于一个完整定义的对象,其必有自己的属性 和方法。对于 Dijit,作为一种 Dojo 体系下定义的对象,其必然也有自己的属性和方法。 而这些属性和方法就是操控 Dijit 的“钥匙”。 125 但是就其现在来说,完全记住掌握每个 Dijit 的方法和属性是一个“不可能的任务”, 而且随着 Dojo 的发展,其 Dijit 的种类会越来越庞大。因此作为 Dojo 的使用者必须掌 握快速查询 Dijit 方法和属性的技能。 Dojo 的使用者需要的是一个能方便快捷查询 Dijit 属性和方法的手册。Dojo 组织没 有给出一个专门的 Dijit 属性和方法手册。但是仍然可以通过以下两种方法间接得到 Dojo 使用者所期望的信息。 第一种方法,通过使用 Dojo 组织在线 API(http://api.dojotoolkit.org/)。 第二种方法,是利用 Firefox 调试工具 Firebug 的帮助来查看 Dijit 实体的方法和 属性。当获得 Dijit 实体的时候,通过 Firebug 支持的 console.debug() 将已经获得的 Dijit 实体输出。接着再点击 Firebug 的 Console 窗口中输出的 Dijit 实体,将可在 Firebug 的 DOM 窗口中观察到该 Dijit 实体所有的方法和属性。 在实际的工程开发中,建议使用这种利用 Firefox 调试工具 Firebug 的帮助来查看 Dijit 的方法。 清单 5.查看 Dijit 在清单 5 中,动态创建了一个 Dijit 实体,并使用 console.debug() 将其在 Firebug 的 Console 窗口中输出。 图 1.Console 中的 Dijit 126 图 1 显示的是在 Console 窗口中输出的 Dijit 实体。 当点击 Firefox 的 Console 窗口中输出的 Dijit 实体时,Firebug 的 DOM 窗口 会列出该 Dijit 实体的所有属性和方法。 图 2.DOM 窗口中的 Dijit 属性和方法 图 2 显示的是在 DOM 窗口中列出的 Dijit 实体的所有属性和方法。 127 与 Dojo 组织在线 API 中的 Dijit 属性和方法列表进行对比,会发现在 Firebug 的 DOM 窗口中列出的属性和方法比在线 API 上 Dijit 的属性和方法要多得多。同时多 出的属性和方法基本上都是以“_”开头的。 事实上,这些以“_”开头的属性和方法是 Dojo 组织希望隐藏起来的 Dijit 的属性和 方法。从其表现出来的功能角度,可以将这些属性和方法理解为 Dijit 实体的私有属性和 私有方法。但由于 Javascript 不具备面向对象的所有特性,因此 Dijit 的这些属性和方 法也就不能像面向对象中的私有属性和方法一样被禁止访问。 虽然这些属性和方法在 Firebug 中的 DOM 窗口中被列了出来。但建议在一般情况 下,尽量不要使用这些 Dijit 希望被隐藏的属性和方法。 清单 6.Dijit 按钮操控实例 Dijit CSS 在清单 6 中,动态创建了一个 Dijit 按钮实体。在创建 Dijit 按钮实体 3 秒后,弹 出一个提示窗口输出 Dijit 按钮实体属性 label 的值。再经过 3 秒后,调用 Dijit 按钮 实体的 setLabel() 方法修改其属性 label 的值。再经过 3 秒,最后调用 Dijit 按钮实 体的销毁方法 destory() 方法,在当前页面中删除动态创建的 Dijit 按钮实体。 label 属性值的修改不能直接通过给 Dijit 按钮实体 label 属性赋值的方法来完成, 必须通过调用 setLabel() 方法才能成功修改其值。这样就避免了开发人员在无意中修改 Dijit 实体属性的值,增加了代码的安全性和健壮性。 不仅仅 Dijit 按钮实体具有 destory() 方法,所有的 Dijit 实体都可以依靠其自己 的 destory() 方法,将自己在当前页面中的“生命结束”。 Dijit 的样式 Dojo 提供了大量的 Dijit 让开发者选择使用。当这些功能各异的 Dijit 组合在一起 以后,并没有显得纷杂和混乱,反而给人以整齐,清爽的感觉。控制着这些数量众多的 Dijit 以同一种样式进行展现的就是 Dijit 的样式主题。 129 认识 Dijit 的样式主题 Dojo 目前给出的统一样式主题有四个,包括 Tundra,Soria,Nihilo,A11y。其 中 A11y 是特别为残障人士使用浏览器时设计的样式主题(本节所讨论的样式主题以 Dojo1.1.0 版本为准)。 在 dojo_path/dijit/themes 文件夹下有一个 Dijit 的样式主题测试页面 themeTester.html。用浏览器运行该页面,首先将看到默认的以 Tundra 为样式主题的 几乎所有的 Dijit。 在 themeTester.html 页面的右下脚,有一个“Alternate Themes”的按钮选项, 点击这个按钮后,可以依据给出的选项,将页面中的所有 Dijit 转换为 Soria 主题样式或 者是 Nihilo 主题样式。 要让页面中的所有 Dijit 使用同一种样式主题来展现,需要通过两步来完成。 第一步,在页面的 head 部分引入期望使用的样式主题的 CSS 文件。例如,如果使 用 Soria 样式主题。 清单 7.在 head 中引入 CSS 清单 7 中的代码显示了如何引入 Soria 样式主题的 CSS 文件。 第二步,在页面 body 标签中定义属性 class 为所期望使用的样式主题名。如果使 用 Soria 样式主题。 清单 8.使用 CSS 清单 8 显示了如何在页面中使用 Soria 样式主题。 除了让页面中的所有 Dijit 使用同一种样式主题以外,也可以通过给特定的 Dijit 指 定 class 属性来实现特定的 Dijit 使用特定的样式主题。 清单 9.在 Dijit 中指定样式 Dijit CSS
    OK
    BAD
    OK
    在清单 9 中使用 Tundra 作为页面的全局样式主题。但在声明最后一个 Dijit 按钮 实体的时候,通过指定 class 属性为 soria,使得该 Dijit 按钮实体使用 Soria 作为其样 式主题。 需要注意的是在引入样式主题 CSS 文件的时候,需要将 Tundra 的 CSS 文件和 Soria 的 CSS 文件都在页面的 head 部分引入。 修改 Dijit 的样式 在着手掌握修改 Dijit 样式主题的方法之前,非常有必要首先了解一下 Dijit 样式主 题的设计框架。 Dijit 的设计理念遵循逻辑层和表现层相分离的基本原则。其中的表现层又进一步分为 “骨架”和“血肉”。“骨架”是指通过 HTML 构建起整个 Dijit 的 DOM 结构,这些 HTML 像人体的骨架一样构造出了 Dijit 的基本外貌;而“血肉”则是指在基于“骨架”的基础上,通 过 CSS 定义细致的刻画出 Dijit 的最终“形象”,这些 CSS 定义像人体的血肉一样让一 个一个的 Dijit“鲜活”了起来。而要修改 Dijit 的样式主题,就是要修改那些让 Dijit“鲜活” 起来的 CSS 定义(Dijit“骨架”的修改只能靠修改 Dojo 包中 Dijit 定义的源码来完成)。 Dijit CSS 定义的层次结构非常复杂。打开 Dojo 包可以发现在 dojo_path/dojo 文件夹下有 dojo.css 文件,在 dojo_path/dijit 文件夹下有 dijit.css 文件,在 dojo_path/dijit/themes 文件夹下又有每个 Dijit 基于不同样式主题的 CSS 文件。 131 事实上,就其开发者的工作来说,完全没有必要花费大量的时间去弄清楚 Dijit CSS 定义的“支支叶叶”,只需要了解 Dijit CSS 定义的大体层次结构就完全能满足实际工作中 的需要。 Dijit 的 CSS 定义大体上分为三个层次。第一个层次是 Dijit 总体样式的定义,主要 是通过 dijit.css 来完成的;第二个层次是各个样式主题的定义,主要是分别通过 tundra.css、soria.css 和 nihilo.css 三个 CSS 文件分别完成的。第三个层次是各个 Dijit 具体展现形式的定义,主要是通过各个 Dijit 的同名 CSS 文件来完成的(dojo.css 定义的内容与 Dijit 样式不具有直接的关系,因此不纳入 Dijit CSS 定义的体系中)。 然而如果以 Dijit 的三层 CSS 定义体系为依据来查询相关 Dijit 的 css 定义,进而 达到修改相应 Dijit 样式主题的目的,这无疑将是一种愚公移山的行为。 事实上 Firebug 给出了一个非常好的功能。其能帮助开发者快速找到页面中每个 Dijit 样式的全部 CSS 定义,并对应 Dijit CSS 定义的层次结构进行显示。 图 3.Firebug 中的 Dijit 样式 在图 3 中通过 Firebug 查看了 Dijit 按钮的 DOM 结构和 CSS 定义。其中 CSS 定义显示在 Firebug 的 style 窗口中。在 Firebug 的 style 窗口中分别以 Dijit 层次、 Tundra 层次和 Dijit 按钮层次三个不同的等级,将 Dijit “OK”按钮中使用到的 CSS 定 义进行了显示(在选择 Dijit “OK”按钮的时候,可以使用“Inspect”模式,先选中页面上的 Dijit “OK”按钮,然后按回车,则可让 Firebug 的 style 窗口恰好显示 Dijit “OK”按钮 的 CSS 定义)。 132 从 Firebug 的 style 窗口中观察 Dijit 按钮的 CSS 定义,会发现有些 CSS 定义 项是被横线划掉了的。这表示当前的这些被横线划掉的 CSS 定义被后面的 CSS 定义给 重写了,这些被划掉的 CSS 定义在当前 Dijit 按钮上是无效的。 要修改某个 Dijit 实体的 CSS 定义,可以依据 style 窗口中的 CSS 定义,在页面 的 head 部分重新定义相应 CSS 定义的 class。 在图 3 中,查看 Dijit 按钮的 CSS 定义可以发现,Dijit 按钮的 border 宽度是 在 .tundra、.dijitButtonNode 中定义的。要修改 Dijit 按钮的 border 宽度,需要重 新定义 .tundra、.dijitButtonNode 中的 border-width 属性。 清单 10.CSS 定义 .tundra .dijitButtonNode { border-width:10px; } 清单 10 的 CSS 定义将 Dijit 按钮的 border 宽度属性修改为 10px,而其它属性 的值没有变化。 图 4.在 firebug 中查看样式 图 4 是将 Dijit 按钮的 border-width 加粗到 10px 后的效果。可以发现 style 窗口中原来的 .tundra、.dijitButtonNode 中定义的 border-width 属性项被横线划掉 133 了。这是因为在当前页面中重新对其进行了定义,可以在其上的定义项中看见新定义的 border-width 值为 10px。 自定义 Dijit 的样式主题 在一些情况下,仅仅是通过修改 Dijit 已有的样式主题可能不能满足实际的需求,而 是希望 Dijit 能直接使用完全自定义的样式主题。 如果要让当前页面中 Dijit 直接得到自定义的样式主题,最直观的想法是让所有的 Dijit 能直接使用自定义的 CSS,而不是通过修改每个 Dijit 已经存在的 CSS 定义来实 现。修改 Dijit 的样式主题必须针对每一个 Dijit 单独操作,但是通过自定义的样式主题 就可以实现对 Dijit 样式主题的批量操作。这是修改 Dijit 的样式主题和自定义 Dijit 的 样式主题最本质的区别。 自定义 Dijit 样式的主题有三种方法。需要注意的是,在通常情况下,只建议使用前 两种方法。 第一种方法是直接定义相关样式主题属性。
    OK
    直接 定义了 Dijit 按钮 style 属性的 margin 值为 30px。其效果是该 Dijit 按钮样式主题的 margin 值变为 30px。 结合上一节修改 Dijit 样式主题的例子,考虑使用同样的方法将 border-width 值变 为 10px。 在实际运行效果中会发现页面中的 Dijit 按钮没有任何改变。其原因是在于当前直接 定义样式主题属性中的 border-width 所能影响到的层和修改 Dijit 样式主题中 border-width 所影响到的层是不同的层。通过 Firebug 的 HTML 窗口可以发现其区别。 在直接定义样式主题属性中的 border-width 所能影响到的层中,没有定义 border-style 等属性值,所以 border-width 无法起到作用。 图 5.Firebug 中查看样式 134 在知道原先修改失效原因的情况下,可以将代码进行修改,以期得到希望的效果。 清单 11.直接定义相关样式主题
    OK
    清单 11 的代码可以展现出期望的结果。 第二种方法通过定义与 id 相关联的 CSS 属性。 清单 12.定义与 id 相关联的 CSS
    OK
    在清单 12 中定义了 id 为 testme 的实体,其 style 属性的 margin 值为 30px。 第三种方法通过定义与 class 相关联的 CSS 属性。 清单 13.定义与 class 相关联的 CSS
    OK
    在清单 13 中自定义 Dijit 样式主题的方法并不推荐使用,因为在实际情况中,使用 该种方法往往会出现运行结果与预计不符合的情况。其原因是当一个实体使用 CSS 的 class 来进行效果修饰的时候,如果出现多个 class 中定义的属性值冲突的情况,则将会 以最后面的一个属性值为基准来执行。那些被自动屏蔽掉的 CSS 定义就如 Firebug 的 style 窗口中被横线划掉的 CSS 定义一样。 结束语 本文关注的是 Dijit 的高级使用。其目的并不是为了让你的应用程序更加绚丽多彩, 而是帮助你更理性的去看待 Dijit 的强大,更成熟的去使用 Dijit 进行开发,以及更沉着 的去解决 Dijit 使用中出现的问题。 Dojo 的扩展 有时候 Dojo 的一些功能并不能完全满足实际的要求,这时就需要对 Dojo 进行扩展 了,比如可以对 Widget 进行扩展,使它以更加符合项目的要求展现或响应行为。本文将 详细介绍 Dojo 的面向对象特性,以及如何在这个特性上开发新的 Dojo 模块,创建新的 Dijit,定义自己个性化的 Widget。 Dojo 类定义 JavaScript 基于原型的继承 JavaScript 是一门基于对象的语言,对象可以继承自其它对象,但是 JavaScript 采 用的是一种基于原型的 (prototype based) 的继承机制,与开发人员熟知的基于类的 (class based) 继承有很大的差别。在 JavaScript 中,每个函数对象(实际上就是 JavaScript 中 function 定义代码)都有一个属性 prototype,这个属性指向的对象就 是这个函数对象的原型对象,这个原型对象也有 prototype 属性,默认指向一个根原型对 象。如果以某个特定的对象为原型对象,而这个对象的原型对象又是另一个对象,如此反复 将形成一条原型链,原型链的末端是根原型对象。JavaScript 访问一个对象的属性时,首 先检查这个对象是否有同名的属性,如果没有则顺着这条继承链往上找,直到在某一个原型 对象中找到,而如果到达根原型对象都没有找到则表示对象不具备此属性。这样低层对象仿 佛继承了高层对象的某些属性。下面通过一个例子说明基于原型的继承是如何工作的。 清单 1. 基于原型的继承 136 function Plane(w, s) { this.weight = w; this.speed = s; } Plane.prototype.name = ""; function JetPlane() { this.seats = 0; this.construct = function(name, weight, speed, seats) { this.name = name; this.seats = seats; this.weight = weight; this.speed = speed; } } JetPlane.prototype.erased = true; JetPlane.prototype = new Plane(); var p1 = new Plane(2000, 100); p1.name = "Boeing"; var j1 = new JetPlane(500, 300); j1.construct("F-22", 500, 500, 2); console.log("p1.weight:" + p1.weight + ", p1.speed:" + p1.speed + ", p1.name:" + p1.name); console.log("j1.name:"+ j1.name + ", j1.weight:"+ j1.weight + ", j1.speed:"+ j1.speed + ",j1.seats:"+ j1.seats); 在这个例子中声明了两个函数对象 Plane 和 JetPlane,Plane 对象的属性有在构造 函数中定义的 weight, speed,也有在 Plane 的 prototype 对象中定义的 name。这 些属性在使用的时候没有区别,都可以通过【对象.属性】访问。JetPlane 中定义了两个 属性 seats 和 construct,construct 的值是一个函数。JetPlane 的 prototype 对象 增加了一个属性 erased,然后把 JetPlane 的 prototype 设为一个 Plane 对象,这样 JetPlane 就拥有了 Plane 的 prototype 对象(注意不是 Plane 对象)中所有的属性。 随后的代码是使用 Plane 和 JetPlane 构造函数生成了一些对象,并输出对象的属性值。 例子中的对象之间的关系如图 1 所示。 图 1. 对象图 137 绿颜色框表示的函数对象;蓝颜色框代表的是原型对象,每个函数对象都有一个原型 对象,如 Plane 有 PlanPrototype,而 JetPlane 有 JetPlanePrototype。黄颜色框表 示的普通对象。每个对象都有一个 Prototype 属性,指向一个原型对象。从图中可以看到 各个对象的内部属性是如何分布的,Plane 对象中只有在自己的构造函数中定义的属性 weight,spead,name 存在于 Plane 的原型对象 PlanePrototype 中;p1 拷贝了 Plane 中的属性,而不会拷贝 PlanePrototype 中的属性。访 p1 的 name 属性时, JavaScript 解释器发现 p1 没有 name 属性,它会顺着 prototype 属性所指往上找, 然后在 PlanePrototype 中发现了 name,所以实际上访问的是这里的 name。同理 j1 仅拷贝 JetPlane 中的 seats 和 construct,而 j1 的 prototype 有点特别;在语句 JetPlane.prototype = new Plane(); 执行之前,JetPlane 的 prototype 属性是指向 JetPlanePrototype 的,而当此语句执行之后,JetPlane 的 prototype 就被设为一个匿 名的 Plane 对象,原来到 JetPlanePrototype 的链条被“剪断”了。访问 j1 的 weight 和 speed 时,实际上访问的是匿名 Plane 对象 [plane] 中的 weight 和 speed。简 单的说,JavaScript 会在原型链上查找需要访问的属性,这就是 JavaScript 基于原型的 继承的工作原理。 Dojo.declare: Dojo 中定义类的利器 使用 Prototype based 的继承有几个缺点: prototype 只能设为某一个对象,而不能设为多个对象,所以不支持多重继承。 prototype 中的属性为多个子对象共享,如果某个子对象修改了 prototype 中的某 一属性值,则其他的子对象都会受影响,所谓牵一发而动全身。 prototype 的设置只能发生在两个对象都构造完之后,这会造成在子对象的构造函数 中无法修改父对象的属性,而在 class based 的继承中,子类对象在自己的构造函数中可 138 以调用父对象的构造函数。所以在清单 1 中又定义了一个 construct 方法来完成属性的 初始化, 为了解决上述问题,Dojo 对 JavaScript 已有的 prototype based 的继承机制进 行了包装,使其更容易理解,使用。在 Dojo 中可以使用 Dojo.declare 函数来定义普通 类,单继承的类,甚至是多重继承的类(虽然笔者认为 dojo.declare 定义的只是对象, 在 Dojo 的官方文档中把 Dojo.declare 定义为声明为类的函数,所以这里也采用这一定 义),一切都在 Dojo.declare 中。同样我们通过一个例子来说 dojo.declare 是如何工 作的。 清单 2 dojo.declare dojo.declare("Plane", null, { name:"", constructor:function(n, w, s) { this.name = n; this.weight = w; this.speed = s; } }); dojo.declare("JetPlane", Plane, { constructor:function(name, weight, speed, seats) { this.seats = seats; } }) ; dojo.declare("Helicopter", Plane, { propellers : 0, constructor : function (name, weight, speed, placeholder, propellers ) { this.propellers = propellers; } }); dojo.declare("Jetocopter", [JetPlane, Helicopter], { constructor : function(name, weight, speed, seats, propellers) { this.lifelong = 10; 139 } }); var p1 = new Plane("Boeing", 2000, 100); var j1 = new JetPlane("F-22", 500, 500,2); var h1 = new Helicopter("Apache", 200, 200, 0, 3); var jh1 = new Jetocopter("X2", 200, 400, 3, 4); //output every property value pair of obj function output(obj) { var message = ""; for (var p in obj){ if (p[0] != '_' && p!="inherited" && p!="constructor" &&p!="preamble" && p != "declaredClass") message += p + ":" + obj[p] + ", "; } console.log(message); } output(p1);output(j1);output(h1);output(jh1); console.log("jh1 is instance of JetPlane: " + (jh1 instanceof JetPlane)); console.log("jh1 is instance of Helicopter: " + (jh1 instanceof Helicopter)); 这个例子与清单 1 中例子相似,但是使用了 dojo.declare 来定义类。JetPlane 和 Helicopter 继承自 Plane,而 Jetocopter 继承自 JetPlane 和 Helicopter。从这四个 类的定义代码不难看出 dojo.declare 函数的一共有三个参数: className:自定义类的类名 superClass:父类,可以为空,某一个类(单一继承),或者是一个对象数组(多重 继承)。 hash props:类定义代码,比如类有那些属性,方法等。其中名为 constructor 的 属性比较特殊,它是类的构造函数,每次创建一个新的对象时,它都会被调用。 这段代码的输出如下: name:Boeing, weight:2000, speed:100, name:F-22, weight:500, speed:500, seats:2, name:Apache, weight:200, speed:200, propellers:3, name:X2, weight:200, speed:400, seats:3,propellers:4, lifelong:10, jh1 is instance of JetPlane: true 140 jh1 is instance of Helicopter: false 使用 Dojo.declare 可以非常简单高效的定义各种想要的类,再也不用为没有地方初 始化父类属性而发愁,类定义代码具有很好的可读性,类之间的继承关系一望便知,而且继 承关系在声明时就可以确定的,而不是等到类构造完之后再去指定。究竟 Dojo.declare 是 如何实现的,使得能有这么多优点? Dojo.declare 完成的工作是根据它的参数情况构造出一个构造函数对象,并把这个 对象作为 JavaScript Global 对象的一个属性,这样就可以在 JavaScript 代码中调用这 个对象。以清单 2 中的 Plane 定义为例,它和清单 1 中 Plane 的定义代码是等价的; 这种情况下,dojo.declare 做的就是根据第三个参数 hash props 构造出一个临时构造 函数,它的内容和清单 1 中的 Plane 一样。过程大致为首先把 hash props 中的 constructor 函数定义的属性拷贝到临时构造函数中,然后把 hash props 中的其他属性 拷贝到临时构造函数的原型对象中去,最后在 Global 对象中增加一个属性 className, 值为这个临时构造函数。如果是单一继承或者多重继承,过程稍微复杂一点,因为要维护继 承关系。在后续文章中会有更详细的介绍。 Dojo.declare 模拟了 class based 继承机制中大部分特性,但是仍然有些地方还不 够完善,比如清单 2 中最后两行代码是测试 jh1 的类型,按照 class based 继承,jh1 应该即是 JetPlane 类型,又是 Helicopter 类型,但是测试结果中 jh1 只是 JetPlane 型,而不是 Helicopter 型。因为在多重继承时,虽然在 dojo.declare 的第二个参数可 以指定多个父类,但是只有第一个父类是真正的父类,其他的都不是,所以 jh1 只是 JetPlane 型。 此外使用 Dojo.declare 时需要注意第三个参数中的 constructor 函数的参数顺 序,这关系到父类对象的属性是否能被正确初始化。在清单 2 的代码中,JetoHelicopter 并没有显式调用 JetPlane 或 Helicopter 的 constructor 函数,但是父类的属性依然能 被正确的初始化。这是因为 dojo.declare 在调用 JetoHelicopter 的 constructor 函数 时会先调用 JetPlane 和 Helicopter 的 constructor 函数,并把所有的参数传递给他 们,而被调用的函数是按顺序接收所有传进来的参数,多余的参数会被忽略掉。所以 Helicopter 的 constructor 函数的参数中,有一个 placeholder,这个 placeholder 参 数仅仅是为了占个位置,以使得来自 JetoHelicopter 的第 5 个参数 propeller 能正确 的传递给 Helicopter 的 constructor 的第 5 个参数。 Dojo.declare 这些“瑕疵”是由 JavaScript 语言的特性造成的,dojo.declare 提供 的功能已经很强大,开发人员大可不必纠缠于这些细节,重要的是使用 dojo 开发出漂亮 的应用。 141 Dojo 的模块化 在本系列文章的第一部分就提到,Dojo 是基于“包”结构进行组织的,就像面向对象语 言一样。严格意义上来讲,这个“包”并非 java 中 package,Dojo 的官方文档中称之为 模块 (Module),但在使用中,却与 java 或 C# 很像,这样做的好处之一是使得熟悉面 向对象的编程人员能够很快熟悉 Dojo;另外,模块化把 Dojo 的代码按照功能划分为不 同的逻辑单元;最后也是最大的好处是,Dojo 引擎可以实现按需载入,也就是说,Dojo 并 不会一开始就把所有的功能都载入到客户端的浏览器上,它只会把用到的模块发送给客户 端。 通常情况一下,Dojo 的模块结构与 Dojo 的目录结构是一样的,如图一所示,最上 面的有三个目录是 dijit,dojo 和 dojox,目前 Dojo 中所有的模块的前缀都是这三者之 中一个。在 Dojo 中,模块与子模块之间用 “.”进行分隔,对应到目录中,就是目录与子 目录。 图 2. Dojo 目录结构 在代码中使用某一模块前,要先显式地用 dojo.require 导入该模块,用法与 java 中 的 import 非常类似,如清单 3 所示。 清单 3. dojo.require dojo.require("dijit.form.Button"); Dojo 引擎一碰到 require 函数,但会把相应的 js 文件载入,上例中所对应的 js 文 件是 /dijit/form/Button.js。如果所引入的包还依赖于其它包, dojo.require 也会把所依赖的包载入。如果所要求的包已经载入,dojo.require 不会重 复载入,它保证所有了包只会被载入一次。 142 扩展 Dojo 模块 在 Dojo 中,定义一个新的模块是很容易的。我们来看一个简单的例子,假设我们要 创建的新模块是 util.math.Calculator。先在 Dojo 安装目录下创建目录 util/math,如 图 3 所示: 图 3. Dojo 目录结构 在目录 util/math 下,创建一个叫 Calculator.js 的文件,在该文件中写入清单 4 所示的代码。 清单 4. Calculator.js // 注册模块名 dojo.provide("util.math.Calculator"); // 声明 Dojo 类 dojo.declare("util.math.Calculator",null,{ add:function(a,b){ return a+b; }, multiply:function(a,b){ return a*b; } }); 现在你就可以开始使用这个新的模块了,代码如清单 5 所示。 清单 5. 使用新模块 dojo.require("util.math.Calculator"); var c=new util.math.Calculator(); alert(c.add(1,2)); alert(c.multiply(3,2)); 143 在清单 4 中,出现了 dojo.provide 和 dojo.declare 函数。dojo.provide 的功 能是向 dojo 模块注册表中注册一个新的模块,dojo.declare 则是用来声明模块中的类。 通过这个例子可以看出,在 Dojo 中创建一个新的模块是非常简单的。现在让我们来对清 单 4 中的代码作些扩展,在 Calculator.js 中加入清单 6 中的代码。 清单 6. 扩展 Calculator util.math.Calculator.subtract=function(a,b){ return a-b; }; dojo.declare("util.math.Calculator2",null,{ subtract:function(a,b){ return a-b; } }); dojo.declare("a.b",null,{ hello:function(){ alert("Hello"); } }); 新的测试代码如清单 7 所示: 清单 7. 测试代码 dojo.require("util.math.Calculator"); alert((new util.math.Calculator()).add(1,2)); alert((new util.math.Calculator2()).subtract(10,5)); alert(util.math.Calculator.subtract(10,5)); (new a.b()).hello(); 通过这个小修改,我们可以发现很多有趣的现象。第一,在一个模块中,不但可以定 义 Dojo 类,还可以定义一个普通的函数,如本例中的 util.math.Calculator.subtract, 这与 java 中的包是不一样的,java 的包中只能定义类;第二,在一个模块中,可以定义 多个 Dojo 类,如本例中的 util.math.Calculator2 与 a.b,这与 java 的包类似;第 三,模块的名字与 Dojo 中类的命名实际上没有必然的联系,两者在语法上并没有一致性 的要求,但从代码的可维护性来考虑,建议保持模块名字与实际的类名一致。 144 开发自己的 Dijit 现在你应该已经掌握了 Dojo 的模块的概念以及如何创建自己的模块,让我们来进一 步看看 Dijit 的扩展。虽然 Dojo 提供的丰富的 Dijit 库,但有时候还是很难完全满足项 目中的一些特殊需求。这种时候,你可能会很快找到 Dijit 的源文件,然后在上面做些修 改,以满足项目需求,但这并不是个好方法,因为这会使源代码很难维护。好在 Dojo 也 提供了很好的 Dijit 扩展机制,使得开发人员可以创建自己的 Dijit. 也许你已经想到了 Dijit 的扩展也模块的扩展应该非常类似,不错,因为实际上 Dijit 也是模块,只不过 Dijit 带有 UI,可以在界面上展示。所以 Dijit 的扩展也就是在模块扩 展的基础上,加了一些规则。Dijit 扩展中一个最重要的概念就是 Template,Template 是 一段带有可替换标签的 html 代码,当 Dijit 实例化时,Dojo 便把可替换标签替换为相 应的属性值,然后把这段 html 代码插入到 DOM 树中。 为了更好地讲解,我们先来看一个例子。 实例:创建一个 Reminder Dijit 我们想创建这么一个 Dijit,它可以展示一句话,并悬停在屏幕的右上角,用户也可以 把这个 Reminder 关闭。现在 Dojo 的安装目录下创建这个目录:/ibm/dijit/templates。把清单 8 中的 hmtl 片断存为 Reminder.html,放到 刚创建的目录中。 清单 8. Reminder.html
    ${title}
    X
    把清单 9 中的 css 片断保存为 Reminder.css,同样放到刚创建的目录中。 清单 9. Reminder.css .memo { background: yellow;font-family: cursive;width: 10em;position:absolute; top:5px;right:5px;z-index:100;} .title { font-weight: bold;text-decoration: underline;float: left;} .close { float: right;background: black;color: yellow;font-size: x-small; 145 cursor: pointer;}.contents { clear: both;font-style: italic;} .close_focus { float: right;background: red;color: yellow; font-size: x-small;cursor: pointer;} template 文件已经准备好了,现在让我们来看看 Dijit 定义文件,代码如清单 10 所示,把它保存为 Reminder.js,放到目录 /ibm/dijit 中。 清单 10. Reminder.js dojo.provide("ibm.dijit.Reminder"); dojo.require("dijit._Templated"); dojo.require("dijit._Widget"); dojo.declare( "ibm.dijit.Reminder" ,[dijit._Widget,dijit._Templated] ,{ title:"Reminder" ,templatePath: dojo.moduleUrl("ibm.dijit", "templates/Reminder.html") ,_onClose: function(){this.destroy();} }); 到这,一个完整的新 Dijit 已经创建好了,让我们来看看效果,把清单 11 中代码保 存为 newwidget.html,放到 目录下。 清单 11.newwidget.html Dojo toolkit 146
    Pick up milk on the way home.
    在 IE 或 Firefox 中打开该文件,你会看到如下力所示的效果。 图 4 . 运行结果 Template 详解 现在我们来详细分析一下上面的例子。这个 Reminder Dijit 共由三个文件组成,其 中 css 文件不是必须的,Reminder.html 是这个 Dijit 的模板文件。 大部分的的 Dijit 都会有一个模板,这个模板可以独立出来放到一个文件中,就像本 例中一样,也可以直接以字符串的形式放到该 Dijit 的模块文件中。这两种形式在格式和 147 作用上是完全一样的,但前一种方式显然会更好一些,因为前一种方式实现了 UI 与代码 的分离。 下图 5 是 Reminder 在浏览中运行后所生成的 html 代码,对比清单 8 中的模 板,可以发现,最终形成的 html 并没有多大的变化,只是有些地方被实际的值替换掉了。 图 5 .Dijit 生成的 html 代码 这些被替换掉的地方也都不是标准的 html 代码 – 这些都是 Dojo 的模板语言标 签。具体来说,Dojo 的语言标签可以分为以下几类: ${ … } 这会被页面 Dijit 标签中相应的属性值所替换,在本例的测试代码中,有 title=”IBM Reminder”,所以模板中的 ${title} 被替换成”IBM Reminder”。 dojoAttachPoint="…" 在 Dijit 的定义 js 文件中,你经常会想直接访问模板中 dom 节点。假如我们现在 要对 Reminder 作一些调整,我们希望鼠标移动关闭按钮上时,按钮底色变红色。这就需 要访问 X 所在的那个 div 节点,这就要在 div 中添加一个 dojoAttachPoint 属性,比 如 dojoAttachPoint=”focusNode”,然后就可以在 js 中用 focusNode 访问这个 div 节点。 你也许会想,可以直接在 div 中加一个 id,然后用 dojo.byId() 取得这个 div 的 引用。但如果同一个页面上有两个这个 Dijit 的实例时,它们就会有一样的 id,这显然会 有问题。 所以,正确的做法是: 在 Dijit 模板 html 中,在你想要访问的节点上加 dojoAttachPoint 属性: dojoAttachPoint= "yourVariableNameHere"; 在定义此 Dijit 的 js 文件中,直接使用(不用声明)这些变量名。 dojoAttachPoint="containerNode" containerNode 是一个特殊的 attachPoint,页面上 Dijit 声明标签中包含的 html 代码会被拷到这个节点上,如果拷入的这段 html 代码还有其它 Dijit 的声明标签,Dojo 会继续解析这些代码,这是一个递归的过程。在清单 11 中,这段 html 代码就是:Pick up milk on the way home。 dojoAttachEvent="…" 148 dojoAttachEvent 的作用是把 template 中的 dom 事件连接到 Dijit 定义 js 文件中的处理函数上。在清单 8 中,有 dojoAttachEvent="onclick:_onClose",这样, X 所在的那个 div 节点的 onClick 事件便被连接到了清单 10 所示 js 文件中的 _onClose 函数上,_onClose 所要做的事情就是把这个 Dijit 实例销毁。 在我们了解了 template 的原理之后,就可以对 Reminder 进行一些改进了,比如 加上鼠标移动到关闭按钮上时,按钮底色变红色的功能。如上面所说,在 template 上, 我们需要在关闭按钮节点加上一个 attach point,还要把该节点的 onMouseOver 和 OnMouseOut 事件连接到 js 文件中,在 js 文件中,我们要定义两个处理函数来处理这 两个事件。新的 template 和 js 文件如清单 12 和清单 13 所示。 清单 12. 新的 template
    ${title}
    X
    清单 13 . 事件处理 dojo.provide("ibm.dijit.Reminder"); dojo.require("dijit._Templated"); dojo.require("dijit._Widget"); dojo.declare( "ibm.dijit.Reminder" ,[dijit._Widget,dijit._Templated] ,{ title:"Reminder" ,templatePath: dojo.moduleUrl("ibm.dijit", "templates/Reminder.html") ,_onClose: function(){this.destroy();} ,_onMouseOver: function(){ // 鼠标移过时,把关闭节点的 class 设为 close_focus, close_focus 设的 backgroud 是红芭 dojo.removeClass(this.focusNode,"close"); dojo.addClass(this.focusNode,"close_focus"); } 149 ,_onMouseOut: function(){ // 鼠标移出时恢复原状 dojo.removeClass(this.focusNode,"close_focus"); dojo.addClass(this.focusNode,"close"); } }); Dijit 的类文件 Dijit 的类也是一个 Dojo 类,所以 Dijit 类的声明和定义也是用 dojo.declare 函 数,如清单 10 和清单 13 所示。Dijit 类既然是 Dojo 类,自然也可以继承其它类或被 其它类所继承。实际上,一个 Dijit 类区别于其它 Dojo 类最重要的一点是,Dijit 类都直 接或间接地继承于类 dijit._Widget,大部分的 Dijit 类通过 mixin 的方式继承类 dijit._Templated,如清单 13 中的 [dijit._Widget,dijit._Templated]。 让我们回过头来看看清单 13,清单 13 中,有一个属性叫 templatePath,从名字 就可以看出来,这个属性指定了 template 文件的路径。除了指定 template 文件的路径 外,也可以直接把 template 变成一个字符串放到类定义文件中,这种情况下,要用到的 属性就是 templateString 了。 除了 templatePath 和 templateString 以外,还有很多扩展点可以根据实际需要 重载,这些扩展点覆盖了 dijit 的整个生命周期,具体列举如下: constructor: constructor 会在设置参数之前被调用,可以在这里进行一些初始化的工作。 Constructor 结束后,便会开始设置 Dijit 实例的属性值,即把 dijit 标签中定义的属性 值赋给 dijit 实例。 postMixInProperties: 如果你在你的 dijit 中重载这个函数,它会在 dijit 展现之前,并且在 dom 节点生 成之前被调用。如果你需要在 dijit 展现之前,修改实例的属性,可以在这里实现。 buildRendering: 通常情况下这个函数你不需要去重载,因为 _Templated 为在这里为你做好所有的 事情,包括 dom 节点的创建,事情的连接,attach point 的设置。除非你要开发一套完 全不一样的模板系统,否则建议你不要重载这个函数。 postCreate: 这个函数会在 dijit 创建之后,子 dijit 创建之前被调用。 startup: 当你需要确保所有的子 dijit 都被创建出来了,你可以调用这个函数。 150 destroy: 会在 dijit 被销毁时被调用,你可以在这里进行一些资源回收的工作。 通过 Dijit.Declaration 来创建新 Dijit 上面所说的都是能过编程的方式来创建一个新的 dijit,这种方式很灵活,你可以很方 便的控制一切。但这种方式比较麻烦,有时候你想创建的 dijit 也许非常简单,这种情况下, 你可以采用另一种方式来创建 dijit,就是用 Dijit.Declaration 来声明一个 dijit,如清单 14 所示。 清单 14 . 用 Dijit.Declaration 创建 Dijit Introduction to Dojo toolkit
    ${title}
    X 151
    Pick up milk on the way home.
    结束语 本文介绍了 Dojo 与面向对象的关系,Dojo 的模块化原理以及两种开发自己的 dijit 的的方式。通过本文的学习,你应该可以根据自己的需要,对 Dojo 进行扩展了。 理解 Dijit Widget 系统的基础架构 版本:1.6 创建 Dijit (dojo 的 小 部 件 库 ) 和自定义小部件的基础,是由两个基类构成的 : dijit._widgetBase 和 dijit._Widget. 当然 Dijit 系统还包含其他一些重要的工具例如 Dojo 的 解析器,和 Dijit 模板系统. 在这个教程里,你将会学到 Dijit 的基础架构是如何运作的。理 解了这些,使用和创建 Dijit 会变的更加容易。 注意:dijit._Widget 继承自 dijit._WidgetBase; 如果你需要创建自定义的小部件,那么你应 当继承 dijit._Widget 而不是 dijit._WidgetBase, (当然你也可以直接继承其他 Dijit 中现有 的小部件) 也许你已经注意到了,这两个基类前面都有一个下划线“_" ,这实际上是 Dojo 中对内部类 的一种命名规范。表示这个类不应被用户直接使用,而是作为基类被继承的。 学习 Dijit 库,最关键的是要能理解每个小部件的生命周期:从这个小部件被创建,到它能 被程序使用,再到最终它被销毁(生命周期也包含了创建与销毁这个小部件所对应的页面中 的 DOM 元素) 152 Dijit 的生命周期 每个继承自 dijit._Widget 的小部件在实例化时都会经历下面的方法调用过程: ([widget].constructor()); [widget].postscript(); [widget].create(); [widget].postMixinProperties(); [widget].buildRendering(); [widget].postCreate(); // 这个方法对开发者而言是最重要的 [widget].startup(); 查看示例 从这些方法的名字我们大概可以猜测出它们的作用: 1. 何时初始化参数 2. 创建对应这些参数的图形,DOM 元素 3. 确定放置这些元素的位置, 4. 如何处理浏览器相关的一些问题(例如 DOM node measurements) [widget].postCreate() 对于小部件的开发者来说,postCreate()可能是最重要的一个方法了。 这个方法会在小部件的所有属性参数设置好,小部件所使用的 DOM 树节点被创建完成后调 用。而此时该小部件的 DOM 节点还没有被添加到主文档中去。因此这个方法是在小部件被 呈现给终端用户之前,开发者可以做最多定制的地方。(例如设定许多自定义参数和属性). 这 就好像是一出戏的大幕拉开之前,你可以做的最后准备工作的环节. 在开发自定义小部件 时,绝大多数定制的属性和行为都会在这里被加入. [widget].startup() 在 Dijit 一系列生命周期中,另一个重要方法是启动方法 startup. 这个方法会在 DOM 节点 被创建并添加到网页之后执行,同时在这个方法也会等待当前小部件中所包含的子控件被创 建并正确启动之后才执行。 注意: 当你用编程的方法创建一个小部件时,记得一定要调用它的 startup()方法。很多 开发者常犯的错误就是仅仅创建了小部件对象却忘记调用 startup(),结果就会导致小部件在 页面上无法正确显示。 153 析构和销毁方法 除了创建和启动,dijit._WidgetBase 还定义了一系列用于析构和销毁的方法: [widget].destroy(); [widget].destroyDescendants(); [widget].destroyRecursive(); [widget].destroyRendering(); [widget].uninitialize(); 在开发自定义小部件时,你需要覆写[widget].uninitialize 方法,在其中释放你所使用的资 源. Dijit 框架会自动的负责销毁该 Widget 所使用的 DOM 节点,以及大部分的对象. 引用小部件的 DOM 节点 通 常 来 说 , Dijit 小部件都是一些界面元素,因此多数会包含一些 DOM 节点。 Dijit._WidgetBase 中定义了一个属性 domNode ,该属性会指向该小部件中所使用的 DOM 根节点. 当该小部件的 postCreate 方法执行后,domNode 属性就可以使用了。通过使用这 个属性,你可以获取并操纵根节点,例如你可以把这个小部件整体移动到 DOM 树中的另一 个位置上。 除了 domNode 属性外,有些小部件还会定义一个 containerNode 属性. 这个属性指定了小 部件 DOM 中的一个容器节点,用来包含子控件。(参看另一篇 Dijit 模板教程) Getters 和 Setters _WidgetBase 作为基类,除了提供了一些通用的标准属性,它还定义了一套标准的 getter 和 setter 方法来访问开发者自定义属性。这套标准要求你定义一些私有方法(JavaScript 中 并没有真正的私有方法,Dojo 遵循的一种编码规范是使用下划线开头的方法名表示类的私 有方法) [html] view plaincopy 1. // 例如你的小部件中需要对外暴露一个"foo"属性, 则需要定义下列两个私有方法 2. 3. // custom getter 154 4. _getFooAttr: function(){ /* do something */ }, 5. 6. // custom setter 7. _setFooAttr: function(value){ /* do something */ } 一旦你定义了上述两个方法,用户就可以使用 dijit 框架的标准 get 和 set 方法来访问你的自 定义属性. 例如在定义了上述两个私有方法后,现在用户可以直接使用 get 和 set 来访问 foo 属性。 [html] view plaincopy 1. // 假设这个小部件的实例是"myWidget" 2. 3. // get the value of "foo": 4. var value = myWidget.get("foo"); 5. 6. // set the value of "foo": 7. myWidget.set("foo", someValue); 通过定义 Dijit 标准的 getter 和 setter 方法,不仅规范了编码风格,更重要的是它增加了灵 活性。你可以在这些 getter 和 setter 方法中加入自定义的其他动作,例如当一个属性被访 问时,你可以对 DOM 节点做相应的修改,或者是发出某些事件的通知信息. 看下面的例子: [html] view plaincopy 1. // 假设我们需要在 Value 属性发生变化时触发 onChange 2. 3. _setValueAttr: function(value){ 4. this.onChange(this.value, value); 5. //注意这里使用了 Dijit 标准的_set 方法 6. this._set("value", value); 7. }, 8. 9. // a function designed to work with dojo.connect 10. onChange: function(oldValue, newValue){ } 155 注意: 在开发小部件时,请务必使用上述的标准 getter 和 setter 方法来访问存取自定义属 性。另外在实现私有的 setter 方法时,也务必要使用_set 方法。 这是因为通过使用该方法 Dijit 控件就可以使用 Dojo.Stateful 中的 watch 方法。 在 Dijit 中使用事件和消息订阅 _WidgetBase 还定义了 connect 和 subscribe 方法。它们也遵循 Dojo 中通用的事件和消息 订阅机制。因此所有的 Dijit 小部件都自动了拥有了对事件处理能力: [html] view plaincopy 1. // 假设我们有一个 dijit.Button 小部件 btn 2. // 我们希望这个 button 能够监听 foo.bar(): 3. 4. btn.connect(foo, "bar", function(){ 5. // 注意这个回调函数的执行上下文是在 btn 对象中! 6. this.set("something", somethingFromFoo); 7. }); Dijit 的主题消息订阅也是类似的。 通过在_WidgetBase 中定义 connect 和 subscribe 方法,所有的 Dijit 小部件都有了事件处 理功能;另外通过_WidgetBase 中定义的 connect 和 subscribe 方法,所有的订阅和连接都 会被自动记录,因此在小部件销毁时也可以自动的释放和退订相应事件,从而避免了潜在的 内存泄露问题。 Dijit 预定义的属性和事件 最后我们来看看_Widget 预定义了哪些属性。 [html] view plaincopy 1. id, // 唯一标记该小部件实例的标识符 156 2. lang, // 该小部件所使用的语言标记,可以覆盖 Dojo 全局的语言设定。 3. dir, // 双向语言支持的标记(值可以为 rtl 或者 ltr)用来支持阿拉伯语等从右向 左显示的语言 4. class, // domNode(小部件根节点)的 HTML class 属性 5. style, // domNode(小部件根节点)的 HTML style 属性 6. title, // HTML title 属性 7. tooltip, // 可选的指向 dijit.Tooltip 的引用 8. baseClass, // 小部件的根级别的 css 类名 9. srcNodeRef // 在被转换为小部件前的原始 DOM 节点 然后我们也一起了解一下哪些事件是 dijit 所支持的,(你可以连接到这些事件,也可以覆写 这些事件) [html] view plaincopy 1. // 鼠标事件 2. onClick, 3. onDblClick, 4. onMouseMove, 5. onMouseDown, 6. onMouseOut, 7. onMouseOver, 8. onMouseLeave, 9. onMouseEnter, 10. onMouseUp, 11. 12. // 键盘事件 13. onKeyDown, 14. onKeyPress, 15. onKeyUp, 16. 17. // 其他事件 18. onFocus, 19. onBlur, 20. onShow, 21. onHide, 22. onClose 157 总结 本文介绍了 Dijit 的 _widget 和_WidgetBase 中所提供了基础架构方法和属性,这些共同 构成了 Dijit 框架: 生命周期,DOM'节点引用,自定义属性存取方法 getter 和 setter 以及 预定义的属性和事件. 有了这些坚实的基础架构,开发一个自己的 Dijit 小部件也变得很简 单。 XHR 框架与 Dojo XmlHttpRequest 对象的思考 在传统的以页面为单位的浏览器和服务器交互模式中,每一次服务器请求都会导致整 个页面的重新加载,即使需要更新的仅仅是页面的一小部分(比如显示一个登录错误信息)。 Ajax 技术的出现给页面带来了一些变化,其中最直观的莫过于站点的页面上出现越来越多 的“ loading …”,“正在加载中……”等提示信息,有些忽如一夜春风来,loading 加载处处 开的意思。“ loading …”或者“正在加载中……”表示浏览器正在与服务器之间进行交互,交 互完成之后,将对页面进行局部刷新,这种交互模式虽然简单却极大的提高了 Web 应用 的用户体验。实现这种模式的核心就是 XmlHttpRequest(后文简称 XHR)对象。 XHR 对象促使越来越多“单一页面”的 Web 应用的诞生。使用 XHR 对象可以发送 异步 HTTP 请求。因为是异步,在浏览器和服务器交互的过程中,仍然可以操作页面。当 页面中有多个进行异步调用的 XHR 对象时,事情有了质的变化,每一个 XHR 对象都可 以独立于服务器进行通信,浏览器中的页面仿佛是一个多线程的应用程序。这种多线程异步 调用的特性给 Web 应用的开发带来了很大的影响,越来越多像 Google Mail 这种“单一 页面”的应用涌现出来,而且大受欢迎。之所以能做到“单一页面”是因为有很多的 XHR 对 象默默地在背后服务,我们可以通过启用 firebug 来查看每次在 Google Mail 页面上的 操作“生产”了多少个 XHR 对象。 使用 XHR 对象的另一个好处是可以减少服务器返回的数据量,进而提升系统的性能。 在原有的 B/S 交互模式中,服务器返回的是粗粒度的 HTML 页面;使用 XHR 对象之后, 服务器返回的是细粒度的数据,如 HTML,JSON,XML 等,请注意这里返回的是数据而 不是页面,也就是说只返回需要更新的内容,而不返回已经在页面上显示的其他内容,所以 每次从服务器返回的数据量比原来要少。采用 AJAX 技术的 Web 应用在初次加载时花费 的时间比较长,但是加载完成之后,其性能比原来的 Web 应用要好很多。 158 这里介绍了一些 XmlHttpRequest 对象给 Web 开发带来的变化,这些变化是 Ajax 技术能够流行的重要原因,认识这些变化可以帮助开发人员设计、开发高效的 Web 应用。本文并不打算介绍 XmlHttpRequest 的属性、方法,很多文章在这方面已经做得 很好。 XHR 框架 XmlHttpRequest 对象是 Dojo 中的 XHR 框架的基础,目前主流浏览器都已经支 持此对象,但是不同浏览器上实现方式却不一样,IE5、IE6 采用 ActiveX 对象的方式, Firefox 和 Safari 都实现为一个内部对象,所以创建 XHR 对象之前需要先测试浏览器的 类型,清单 1 展示了最简单的创建 XHR 对象的代码。 清单 1 function createXHR(){ if (window.XMLHttpRequest) { // Non Ie return new XMLHttpRequest(); } else if (window.ActiveXObject) { // Ie return new ActiveXObject("Microsoft.XMLHTTP"); } } 或许是认识到 XHR 对象的重要性,微软在 IE7 中已经把它实现为一个窗口对象的 属性。但是判断浏览器类型的代码依然不能消除,因为 IE5, IE6 仍然有大量的使用者。 XHR 对象创建方式不一致是 Dojo 的 XHR 框架诞生的一个原因,更重要的原因是 原始 XHR 对象还不够强大,有些方面不能满足开发的需要:首先 XHR 对象支持的返回 类型有限,原始 XHR 对象只有 responseText 和 responseXML 两个属性代表返回的 数据,重要的数据交换格式 JSON 就不被支持;其次不能设置 HTTP Request 的超时时 间,设置超时时间可以让客户端脚本控制请求存在的时间,而不是被动的等待服务器端的返 回。 基于这些问题,Dojo 组织提供了一组函数来支持各种 HTTP 请求,包括 xhrGet, rawXhrPost,xhrPut,rawXhrPut,xhrPut,xhrDelete,这几个函数与 HTTP 协议中 的四种请求是一一对应的,HTTP 四种请求是:Get(读取),Post(更新),Put(创建), Delete(删除)。 Dojo 组织的发起者 Alex Russell 把这些跟 XHR 对象相关的函数放 在一起称为 XHR 框架。下面我们来看看 Dojo 是如何创建 XHR 对象的。清单 2 是 Dojo 1.1 中创建 XHR 对象的代码片段。 清单 2 159 d._XMLHTTP_PROGIDS = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0']; d._xhrObj= function(){ var http = null; var last_e = null; if(!dojo.isIE || !djConfig.ieForceActiveXXhr){ try{ http = new XMLHttpRequest(); }catch(e){} } if(!http){ for(var i=0; i<3; ++i){ var progid = dojo._XMLHTTP_PROGIDS[i]; try{ http = new ActiveXObject(progid); }catch(e){ last_e = e; } if(http){ dojo._XMLHTTP_PROGIDS = [progid]; break; } } } if(!http){ throw new Error("XMLHTTP not available: "+last_e); } return http; // XMLHTTPRequest instance } _xhrObj 是 Dojo 创建的 XHR 对象。与清单 1 相比,是不是显得有点“冗长”?其 实不然,虽然多了很多 try-catch 语句,但这些 try-catch 块保证了即使创建 XHR 对 象出错时,浏览器依然不会崩溃,增强了代码的健壮性;此外,代码对 IE 浏览器的“照顾” 周到之至,三个 XHR 对象可能存在的命名空间('Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0')都做了判断,只有这样才能保证 XHR 对象在各个不同的浏览器能顺利“诞生”。从这段代码也可以看出要编写健壮、高效的代码, 开发人员必须具有系统性的思维,并能合理使用错误处理机制。下面将对 XHR 框架中的 每个方法进行介绍。 160 使用 xhrGet 请求资源 xhrGet 是 XHR 框架中最重要的函数,使用频率也最高。使用它即可以请求服务器 上的静态文本资源如 txt、xml 等,也可以获取动态页面 php、jsp、asp 等,只要从服 务器返回的是字符数据流即可。首先看一个简单的例子。 清单 3 function helloWorld(){ dojo.xhrGet({ url: "helloworld.txt" , handleAs: "txt", load: function(response, ioArgs){alert(response);}, error: function(error, ioArgs){alert(error.message);} }); } 函数 helloWorld 调用 dojo.xhrGet 获取服务器上与引用此 Javascript 脚本的 页面同一目录下的 helloworld.txt 文件。服务器成功返回之后,使用 alert 显示文件的 内容。如果出错了则使用 alert 显示错误信息。 dojo.xhrGet 的参数是一个 JSON 对象,JSON 对象由很多的属性 / 值对组成,其 中的值可以是任意类型的数据: 整形、字符串、函数……甚至是 JSON 对象,这一点使得 JSON 对象的数据描述能力可以与 XML 匹敌,而且 JSON 对象可以使用“ . ”操作符来直 接访问它的属性,没有任何解析的开销,非常方便。在 Javascript 领域,JSON 大有超 越 XML 成为事实上的数据交换标准的趋势。使用 JSON 对象作为函数参数的情形在 Javascript 中非常普遍,可以看成 Javascript 开发中的一个模式,开发人员应该熟悉它。 再回到作为 xhrGet 参数的 JSON 对象,在清单 3 的例子中的,这一对象有四个属性: url:请求的服务器资源 url,url 标识的只能是文本文件,而不能是二进制文件。 handleAs:返回的数据类型,可以是 text(默认)、json、json-comment-optional, json-comment-filtered、javascript、xml 。 Dojo 将根据 handleAs 设置的数据类 型对从服务器返回的数据进行预处理,再传给 load 属性指向的回调函数。 load:它的值是一个函数,这个函数在请求的资源成功返回之后被调用,实际上就是 一回调函数。 error:它的值也是一个回调函数,但是只在 http 请求出错之后(比如,404 错误: 请求的资源找不到)才被调用。 load, error 所指向的值即可以像清单 3 中所示的那样是无名函数,也可以是一个已 经定义过的函数名,清单 3 的例子可以修改如下: 清单 4 161 function display(response, ioArgs) { alert(response); } function helloWorld2(){ dojo.xhrGet({ url: "helloworld.txt" , handleAs: "txt", load:display, error:display}); } 使用这一方法可以提高代码的复用率,尤其在有多个 xhrGet 对象需要使用相同的 load 回调函数时。 不管 load 的回调函数是无名函数还是预定义的有名函数,它都包含两个参数: response 和 ioArgs(注意:这两个参数的名称可以任意取,这里只是使用了两个常用的 名称。实际上,在 Javascript 中,函数是由函数名唯一声明的,函数参数可以不出现在函 数的声明中,在函数体内可以使用 arguments 引用函数的实际参数)。 response:表示从服务器端返回的数据,Dojo 已经根据 handleAs 设置的数据类 型进行了预处理。 ioArgs: 这是一个对象,包含调用 xhrGet 时使用的一些参数。之所以把这些信息放 在一个对象中并传递给回调函数是为了给回调函数一个执行“上下文”,让 回调函数知道自己 属于哪个 HTTP 请求,请求有哪些参数,返回的数据是什么类型等。这些信息在调试程序 时特别有用。 ioArgs.url:请求的 URL,与调用 xhrGet 时设置的值一样。 ioArgs.query:请求中包含的参数, URL 中“ ? ”后面的内容。 ioArgs.handAs:如何对返回的数据进行预处理,与调用 xhrGet 时设置的值一样。 ioArgs.xhr: xhrGet 函数使用的 XHR 对象。 前面介绍了 xhrGet 函数以及与它关联的回调函数,xhrGet 中的 handleAs 的设 置决定了如何对服务器返回的数据进行预处理,表 1 详细介绍了不同的 handleAs 代表 的不同的预处理方式。 表 1 handleAs VS 预处理方式 handleAs 预处理方式 text 默认值,不对返回的数据做任何处理 xml 返回 XHR 对象的 responseXML javascript 使用 dojo.eval 处理返回的数据,返回处理结果 162 json 使用 dojo.fromJSon 来处理返回的数据,返回生成的 Json 对象 json-comment-optional 如果有数据包含在注释符中,则只使用 dojo.fromJSon 处理 这部分数据,如果没有数据包含在注释符中,则使用 dojo.fromJSon 处理全部数据。 json-comment-filtered 数据应该包含在 /* … */ 中,返回使用 dojo.fromJSon 生成 的 Json 对象,如果数据不是包含在注释符中则不处理。 假设 handleAs 被设置为“ json ”,按照上表,则 load 回调函数的参数 response 为 JSON 对象。如果 handleAs 不是“ json ”,还能不能生成 JSON 对象呢?答案是肯 定的,可以把 handleAs 设为“ text ”,那么返回的是普通的字符串,只要字符串是 JSON 对象的文本形式,则可以简单地使用 eval() 函数把它转换为真正的 JSON 对象,而不再 需要任何其他的 API 完成转换工作。 清单 function jsonDemo() { response = ( "[{ name: 'Joe', age: '30', gender: 'M'}, { name: 'Chandler', age: '32', gender: 'M'}, { name: 'Rose', age: '31', gender: 'M'}]" ); json = eval(response); document.write(json[0].name + "," + json[1].age + "," + json[2].gender); } 清单 5 是一个把文本字符串转换为 JSON 对象的例子。 response 经过 eval 处 理后转换成一个 JSON 对象数组,最后输出每个 JSON 对象的一个属性。调用 jsonDemo 将在浏览器输出: Joe,32,M 这一部分有很大篇幅跟 JSON 相关,因为 JSON 这种数据交换格式应用越来越广, 希望广大开发者能更加重视。 开发人员往往在 xhrGet 的 load 回调函数中处理服务器返回的内容,然后更新页 面。最简单的更新方法是把经过处理的内容设置为页面上某一节点的 innnerHTML 属性。 返回的内容中最好不要包含需要立即执行的 javascript 代码片段,因为这段 javascript 是不起作用的。 163 使用 xhrGet 提交表单 表单的提交在 Web 应用中必不可少,以前 javascript 应用最广的地方是做表单的 验证,今天我们知道 javascript 能做的比这远远要多。使用 xhrGet 提交表单与请求资 源类似,只需要在 xhrGet 的参数对象中增加一个属性,关联需要提交的 form 。使用 xhrGet 异步提交 form 意义重大,在传统的 B/S 交互模式中,提交 form 则意味着页 面的跳转,但很多情况下页面不用跳转,比如用户登录时,用户名或密码错误,这时不跳转 页面而是直接给出错误提示信息用户体验明显要好得多,清单 6 是使用 xhrGet 提交表单 的例子。 清单 6 164 在这个例子中我们看到 xhrGet 的一些新的参数。这些参数不是仅针对提交表单的, 请求资源时也可以使用。之所以在这里介绍,是为了达到循序渐进学习的目的。例子中的 data.php 是服务器端的程序,比较简单,只包含一行代码 echo $_POST[“pwd”],用来 输出表单中的密码字段。 form:需要异步提交的表单的 id 。只有把它设置成想要异步提交的表单的 id,并在 这个表单的 onsubmit 事件中调用自定义的 submitForm() 函数,才能真正做到异步提 交。注意在 submitForm 函数中最后返回了 false,这是为了阻止系统默认的表单提交事 件,让表单提交异步进行,如果不返回 false,会引起页面跳转。 handle:handle 也是一个回调函数,在 xhrGet 返回时被调用,正常和错误返回的 情况都能处理,可以说是 load 和 error 的混合体,但优先级比 load 低,只有在没有设 置 load 时才起作用。 content:在这里可以修改来自表单的信息,在清单 6 所示的例子中,就使用这一属 性修改了用户登录时输入的密码。 sync:设置同步还是异步提交。默认是异步提交,所以在清单 6 以前的例子中并没 有设置这一属性。 需要注意的是:虽然表单提交的默认方法是 POST,但当使用 xhrGet 提交时,表单 提交方式就自动改为 GET,所有表单的数据都会变成查询字符串出现在 URL 中。所以在 服务器端只能从查询字符串中取得这些提交的信息。在 jsp 中是: request.getQueryString(“PWD”),而在 php 中可以使用 $_GET[“PWD”] 在服务器 端获取表单字段。 XHR 框架中的其他方法 除了 xhrGet,Dojo 的 XHR 框架还包含 xhrPost,rawXhrPost,xhrPut, rawXhrPut,xhrDelete 。这几个函数与 xhrGet 类似,使用方法和参数都可以参考 xhrGet 。区别在于他们的 HTTP 请求类型,xhrPost 发送的是 Post 请求,xhrPut 发 送的是 Put 请求,xhrDelete 发生的是 Delete 请求。 xhrPost 一般用来发送表单数据,当然 xhrGet 也可以做到,区别是 xhrPost 把表 单数据封装在 HTTP 请求的 Body 部分。在服务器端只能使用取 POST 信息的方法获取 这些表单数据,假设我们要取清单 6 中的表单的 PWD 密码框中的数据,在 JSP 中可以 是 request.getParameter(“PWD”),在 PHP 中可以是 $_POST[“PWD”] 。 如果使用 xhrDelete 去删除服务器上的资源,比如某一文件,因为它表示删除服务 器上的某一资源,而普通用户是没有权限删除服务器上资源的权限的。所以使用 xhrDelete 165 方法时,一般回返回 405 错误(图 1 是使用 javascript alert 显示的错误信息),表 示“对于请求所标识的资源,不允许使用请求行为中所指定的方法”。 图 1. 405 错误提示 Dojo 提供这些方法的目的当然不是为了方便开发人员增加 / 删除 / 修改服务器上 的物理资源,而是为了支持 REST 架构风格。 REST 架构风格强调使用标准 HTTP 方法, 即前文提到的 Get,Post,Put,Delete 来请求、操作 web 资源,注意不是物理资源。 举个例子,在 REST 架构中,新建订单,应该使用 Put 方法,而删除订单应该使用 Delete 方法,而不像在以前的 Web 应用架构中,开发人员通过额外的参数来确定操作的类型。 Dojo 提供这些方法对 REST 架构风格是很好的支持。 iframe,别样的思路 除了 XHR 框架,Dojo Core 还提供了一个 io 包,Dojo 的官方说明把他们描述成 “高级传输层(advanced ajax transport layer)”,由两个对象组成,dojo.io.iframe 和 dojo.io.script 。 使用 dojo.io.iframe 同样可以跟服务器交互,但是它采用了与 XHR 对象不同的实现思路。清单 7 是一个使用 iframe 方式提交表单的例子。 清单 7
    Name:
    Sex: Male Female
    从这个例子中可以看出,dojo.io.iframe 的使用方式、参数与 xhrGet 非常相似。 其中,from,url,handleAs,load 等在 xhrGet 中也存在,唯一不同的是 method, method 表示 dojo.io.iframe 将以何种 HTTP Method 来发送请求。另外需要注意的一 点是 handleAs 参数,dojo.io.iframe 一般使用 html,因为在 iframe 中存的其实是 另一个 HTML 页面。如果 handleAs 设置为其他值,像 json,text 等,则在服务器端 须使用 把要返回的数据包装起来,比如 hellow, world 要被 包装成 ,所以最后存在 iframe 中的是一个文本 域(textarea),这个文本域包含了从服务器端返回的数据。这么做的原因很简单,就是 为了保持从服务器返回的数据“一成不变”,因为任何字符数据都可以“安全的”放在 HTML 页面的文本域中。想像一下,我们是不是可以在文本域中输入各种字符! dojo.io.iframe 会 对 textarea 包装的数据进行处理:首先把 textarea 标签去掉,然后把数据转换为 handleAs 指定的类型传递给 handle 中设置的回调函数。 dojo.io.iframe 是如何工作的呢?除了 XHR 对象之外还有什么方法可以实现表单 的异步提交?其实这一切都很简单,dojo.io.iframe 首先会创建一个隐藏的 iframe 并插 入到父页面的最后,然后设置此 iframe 的 src 属性为 dojo-module-path/resources/blank.html(dojo-module-path 指 dojo 包所在的目 录),iframe 页面的 onload 事件的处理函数被设置为父窗体的回调函数。接下来就是 在 iframe 页面中发送请求,并接收服务器的响应。当 iframe 接收到服务器的反馈并加 载完之后,父窗体的回调函数即被调用。 dojo.io.iframe 还有其他几个很有用的函数 create: function(/*String*/fname, /*String*/onloadstr, /*String?*/uri)dojo.io.iframe.setSrc() 167 create 函数用来在页面中创建 iframe,参数 fname 表示 iframe 的名字,setSrc 和 doc 函数据此引用创建的 iframe,onloadstr 表示 iframe 加载完成后执行的回调函 数,uri:iframe 请求的资源。后两个参数是可选的,当 uri 为空时,将加载 dojo-module-path/resources/blank.html。 setSrc: function(/*DOMNode*/iframe, /*String*/src, /*Boolean*/replace) 设置指定的 iframe 的 src 属性,这将导致 iframe 页面重新加载。 iframe:需要 刷新的 iframe 的名字,src:用来刷新 iframe 的页面 , replace:是否使用 location.replace 方法来更新 iframe 页面的 url,如果使用 location.replace 方法, 则不会在浏览器上留下历史记录。 doc: function(/*DOMNode*/iframeNode) 获取指定的 iframe 页面的 DOM 根节点,有了它可以对 iframe 页面进行任意的 操作。 dojo.io.iframe 采用了不同的思路实现了“异步”发送请求,但是 dojo.io.iframe 使 用并不多,因为当页面中多处需要异步通信时,在页面中创建很多的 iframe 并不是好的 注意。在能使用 xhr 框架的地方尽量使用 xhr 框架,唯一值得使用 iframe 的地方是发 送文件。 动态 Script,跨域访问 XHR 框架中的函数功能强大,使用方便。但是 XHR 框架的函数有一问题就是不能跨 域访问,浏览器不允许 XHR 对象访问其他域的站点。比如有一个页面属于 a.com,在这 个页面中使用 XHR 对象去访问 b.com 的某一页面,这是被禁止的。如果使用 XHR 对 象来做跨域访问一般需要服务器端的程序做“中转”,先由服务器端的程序获取其他域的数 据,然后浏览器再使用 XHR 对象从服务器上获取这些数据,这种方式即增加了服务器端 的开销,浏览器端的效率也不高。 有没有方法直接在浏览器中实现跨域访问呢?当然有,它就是 script 标签,使用 script 标签可以引用本域或其他域的文件,只要这些文件最后返回的是 javascript 。返 回的 javascript 会立即在浏览器中执行,执行结果存储在本地浏览器。这一点很重要,它 使得各站点可以通过 javascript 来发布自己的服务,像 google 的很多的服务都是通过 这种方式提供的。 script 标签不仅可以静态添加到页面中,也可以被动态插入到页面中, 而且通过 DOM 操作方式动态插入的 script 标签具有与静态 script 标签一样的效果,动 态 script 标签引用的 javascript 文件也会被执行(注意:通过 innerHTML 方式插入的 javascript 是不会被执行的,这一点在前文已经介绍过)。 清单 8 168 function createScript() { var element = document.createElement("script" ); element.type = "text/javascript" ; element.src = url; document.getElementsByTagName("head" )[0].appendChild(element); } 清单 8 中的例子展示了动态创建 script 标签的例子,script 标签即可以放在页面的 head 部分,也可以放在 body 部分。 动态插入 script 标签的一个问题是如何判断返回的 Javascript 执行完了,只有在 执行完之后才能引用 Javascript 中的对象、变量、调用它中间的函数等。最简单的方法是 “标志变量法”,即在脚本中插入标志变量,在脚本最后给这个变量赋值,而在浏览器的脚本 中判断这一变量是否已经被赋值,如果已经被赋值则表示返回的脚本已经执行完。但是这种 方法缺点也很明显,首先如果一个页面有很多的动态 script 标签,而每个 script 标签引 用的 javascript 都使用一个标志变量,那就有很多变量需要判断,而且这些变量的命名可 能冲突,因为这些 Javascript 是由不同的组织、公司提供的,难保不产生冲突。另外在浏 览器本地脚本中需要轮询这些变量的值,虽然可以实现,但实在不是高明的做法。目前被广 泛使用的是另一种方法:JSONP(JSON with Padding)。 JSON 表示返回的 Javascript 其实就是一 JSON 对象,这是使用 JSONP 这种方式的前提条件。 Padding 表示在 JSON 对象前要附加上一些东西,究竟是什么呢?请往下看! JSONP 的思路很简单,与其让浏览器脚本来判断返回的 Javascript 是否执行完毕, 不如让 Javascript 在执行完毕之后自动调用我们想要执行的函数。是不是想起了学习面向 对象设计中的“依赖倒置”原则时的那句名言:“Don't call us, we will call you ”。使用 JSONP 这种方法,只需要在原来的 Javascript 引用链接上加上一个参数,把需要执行的 回调函数传递进去。请看下面的两个 script 标签。 dojo.require("dojo.io.script" ); dojo.io.script.get 函数的使用方式和参数是不是与 xhrGet 很相似?只有 checkString 是 xhrGet 所特有的,checkString 正是“标志变量法”的关键, checkString 表示从服务器返回的 javascript 需要定义的变量。清单 10 展示了使用 PHP 编写的服务器端的脚本,它输出了一段 javascript,在这段 javasript 的最后给变 量 test_01 赋值。而清单 9 中 dojo.io.script.get 函数的 handle 指向的回调函数又 调用了这段 javascript 中定义的函数 greetFromServer() 。只有在 test_01 被赋值 后,调用 greetFromServer 才是安全的。 清单 10 dojo.io.script.get 函数也支持 JSONP 方式,当 dojo.io.script.get 函数的参数 对象使用了 callbackParamName 属性时,表示它工作在 JSONP 方式下。 callbackParamName 表示在 url 中添加回调函数名的参数名称,有点拗口,但是看了下 170 面 dojo.io.script.get 函数在页面中动态创建的 script 标签一切就都清楚了,最终出现 在 URL 中的是 callbackName,而不是 callbackParamName 。 dojo.require("dojo.io.script" ); Dojo 会自动创建一个名为 dojo.io.script.jsonp_dojoIoScript1._jsonpCallback 的 javascript 函数,这个函数 其实什么都不做,只是作为一个回调函数传给服务器端程序。 php 的服务器端程序如清单 12 所示,callbackName 像浏览器和服务器之间的一个“信令”,服务器端必须返回对 callbackName 所代表的函数的调用,因为 Dojo 会检查它是否被调用过。 所以服务器端返回的是 dojo.io.script.jsonp_dojoIoScript1._jsonpCallback({greet:’hello, world’}) 。参 数 {greet:’hello, world’} 正是要返回到浏览器的 JSON 对象。 清单 12 171 清单 11 所示程序的输出为:hello, world,由此可以看出,response 参数就是从 服务器端返回的 JSON 对象,服务器端的 JSON 对象终于成功的传递到浏览器了。前面 介绍了这么多的机制都是为了使这个 JSON 对象安全返回到浏览器中。当然你可以在服务 器端返回任何数据,比如直接返回一个字符串,但此时 response 就变成字符串了,当然 也就不能再叫 JSONP 了,因为 JSONP 特指返回的是 JSON 对象。 dojo.io.script 对象中除了 get 函数之外,还有 attach,和 remove 两个函数 attach: function(/*String*/id, /*String*/url) 创建动态 script 标签,标签的 id 由参数 id 指定,src 由 url 指定。 remove: function(/*String*/id) 删除 id 代表 script 标签。 总结 本文介绍了 Dojo 中三种浏览器与服务器交互的方式,这三种方式各有优缺点,但是 在使用方式却出奇的一致; xhr 框架的函数,dojo.io.iframe、dojo.io.script 对象的函 数使用的 JSON 对象参数也极其相似,而且浅显易懂。 Dojo 设计者的这一良好设计极大 的减轻了开发人员的学习负担,作为框架开发人员应该了解这一理念。表 2 对这三种方式 从三个方面进行了比较。 表 2. 三种方式的比较 支持的 HTTP 请求类型 期望的输出 跨域访问 XHR Get, post, delete, put text, json, xml, javascript … N iframe Get, post html N script Get javascript Y 综上所述,使用上述三种方法时需要遵循一条简单的原则:传送文件则 iframe,跨域 访问则使用动态脚本,其余则选 XHR 框架。 ajax 交互补充 版本:1.7 Dojo 使用 xhrGet、xhrPost 等方法与后台进行异步交互,方法的使用如下: dojo.xhrGet({ url: "users.json", //请求的 url handleAs: "json", //返回结果的格式,可以 json 或 text load: function(res){ //请求成功后执行的方法,res 为返回的结果 // Resolve when content is received 172 def.resolve(res); }, error: function(err){ //请求出错后执行的方法,err 为错误信息. // Reject on error def.reject(err); } , content:{//请求所带的参数 param1:value1, param2:value2 } }); 明日之星 – DojoX Dojo 作为最著名的 Ajax 开源项目之一,不仅让 Web 程序员可以免费获得和使用 其框架进行 Web 应用的开发,更吸引了大量的开发者对其不断的扩充,开发新的组件。 DojoX 就是在这样的开发社区中产生的。DojoX 是一组基于 dojo 的开源项目的集合, 这些开源项目具有很好的创意和很高的实用性。这些 DojoX 项目有可能成长为一个稳定的 版本保留在 DojoX 中,也有些可能会迁移到 Dojo Core 或者 Dijit 中。本文将对 DojoX 中的项目进行一个总体的概述,并结合实例介绍其中较为有特色的项目,本文主要会介绍 DataGrid,Charting,Gfx/Gfx 3D 和 DojoX Widget。 认识 DojoX 目前 DojoX 项目主要扩展了数据结构与算法、数据处理与通信、实用工具、图形 API 以及 Web UI 等。 涉及到数据结构与算法的项目包括了 DojoX Collections、DojoX Encoding 等。 Collections 定义了很多非常有用的数据集合,包括了数组(ArrayList)、二叉树 (BinaryTree)、字典(Dictionary)、迭代器(Iterator)、队列(Queue)、有序列 表(SortedList)、堆栈(Stack)。这些集合的使用将大大提高程序开发的效率以及程序 的质量。Encoding 不仅提供了字符串与字符编码的转换,还提供了对称算法河豚 (Blowfish)和 MD5 数字摘要算法等。 DojoX Data、Embed、I/O、JSON、XML、RPC 等扩展了 Dojo 的数据处理与通 信能力。其中,Data 项目提供了对更多数据格式的支持,包括了对 csv 文件以及 Google、 Picasa 等提供的 API 的支持等等。 DojoX 的图形 API 扩展了 Dojo 的动画效果,并提供了 2D、3D 绘图的支持。 DojoX Fx 通过对 dojo core 以及 dojo fx 的扩展提供了多种动画效果;gfx 提供了一 系列矢量绘图的方法;而 gfx3d 则提供了一些简单的 3D 绘图 API。 173 而更加丰富的 Web UI 以及 Web 小部件也是 DojoX 的一大亮点。功能强大的 Grid、实用的 Charting、以及 DojoX Image 和 DojoX Layout 使得基于 dojo 开发 的 Web UI 更加丰富。DojoX Widgets 中还提供了更加丰富的小部件可以满足大部分应 用开发的需求。 除以上介绍的项目外,DojoX 还收集了很多实用工具,读者可以在 dojo API 网站上 获得更多的信息。http://api.dojotoolkit.org/ 接下来我们就来体验一下 DojoX 给我们带来的精彩吧 注意:本教程使用的 dojo 版本为 1.2.1,由于 1.2.x 版本里 dojo 以及 dojoX 的 部分组件有较大变化,因此本文仅适用于 dojo1.2.x,对于 dojo1.0 的开发者本文仅供 参考,部分代码不能正确运行 DojoX DataGrid Grid 可能是 DojoX 中最受欢迎的部件,比起普通的 Web 表格部件,Grid 更像一 个基于 Web 的 Excel 组件。这使得 Grid 足可以应付较为复杂的数据展示及数据操作。 在 dojox1.2 中,dojox.grid 包中新增了 DataGrid 类,该类是对原 Grid 类的强化和 替代,之所以叫做 DataGrid,是由于该类与 dojo 的数据操作类 store 无缝整合在一起。 而之前的 Grid 需要将 store 对象包装为 model 对象才能使用。下文如果没有特殊声 明,所有 Gird 或是 DataGrid 均指新版 DataGrid,而不是 Grid1.0。 图 1 .DojoX DataGrid 我们为什么需要 Grid 呢?下面列出了 Grid 的特性: 用户只需向下拖动滚动条,Grid 即可加载延迟的记录,省去了翻页操作,减少 Web 与服务器交互,提高了性能; 可以任意的增加和删除单元格、行、或者列; 对行进行统计摘要,Grid 可以生成类似于 OLAP 分析的报表; 174 Grid 超越了二维表格的功能,它可以跨行或跨列合并单元格以满足不同的数据填充的 需求; 行列冻结功能,使得浏览数据更加灵活方便; Grid 事件采用了钩子机制,我们可以通过 onStyle 钩子完成对样式的更改; 单元格具备富操作,所有的 dijit 部件都可以在单元格中使用,并且单元格可以通过 单击转换为编辑状态; 可以为不同的单元格设置不同的上下文菜单; Grid 嵌套,也就是说 Grid 可以在单元格中嵌套其他的 Grid,从而组成更为复杂的 应用; 除此之外,Grid 还有具有其他很多特性,例如,非常实用的偶数行上色、灵活的选取 功能、自动调整列宽、数据的展开 / 合闭等。 DataGrid 基础 要创建一个 DojoX DataGrid,就需要对 DataGrid 的基本工作过程有一个大致的 了解。一个 DataGrid 实例的组成结构如下图所示,DojoX DataGrid 是使用 DataGrid 的基础,因此在使用 Grid 的时候需要加载相关的 dojox 包;一个小部件通常由框架和样 式组成,因此,我们需要指定 DataGrid 的样式表并且声明 DataGrid 实例。DataGrid 实 例会组合一个 Structure 和一个 Store。Structure 是一个表头及数据模型的定义,而 Store 用于承载数据。 图 2 .DataGrid 组成结构 下面开始我们的第一个 DataGrid 应用为了在 Web 页面上创建一个 DataGrid 小 部件,我们从最基本的二维表格开始。首先我们需要加载一些样式,来保证 DataGrid 能 够正常显示,清单 1。 清单 1. 加载样式 175 此处的 css 文件路径为相对于测试页面的相对路径。 在开始创建 Grid 之前,我们还要引入 Dojo 的基础包 dojo.js,以用来加载其他需 要的 dojo 类,并加载 dojo.data.ItemFileReadStore 类以及 dojox.grid.DataGrid 类。接下来我们就可以着手开发第一个 DataGrid 了。首先是布局的定义,如 清单 2 清单 2. 定义布局 var layout = [ {field: 'pro_no', name: 'Product Number' }, {field: 'pro', name: 'Product' }, {field: 'min_amount', name: 'Minimum Amount' }, {field: 'max_amount', name: 'Maximum Amount' }, {field: 'avg_amount', name: 'Average Amount' } ]; 这里定义了一个数组 layout,其中每一个成员表示一个列的定义,其中 field 指定了 使用的数据项,该取值需要遵循 javascript 变量定义规则;name 为该列显示的名称。 接下来是 store 的开发,代码如 清单 3 清单 3. 开发 store var sampleData = { identifier: 'pro_no', label: 'pro_no', items: [ {pro_no:'2100', pro:'A Series', min_amount:346, max_amount:931, avg_amount:647}, {pro_no:'2200', pro:'B Series', min_amount:301, max_amount:894, avg_amount:608}, {pro_no:'2300', pro:'C Series', min_amount:456, max_amount:791, avg_amount:532}, {pro_no:'2400', pro:'D Series', min_amount:859, max_amount:2433, avg_amount:1840}, {pro_no:'2500', pro:'E Series', min_amount:459, max_amount:1433, avg_amount:1040} ] }; var jsonStore = new dojo.data.ItemFileReadStore({ data: sampleData }); 176 在这里,我们首先定义一个 JSON 数据 sampleData,这里 identifier 是对于整行 的唯一标识,因此在数据中不能出现重复;数组 items 是这个表格所显示的数据,其中数 据必须完全符合 JSON 的语法,字符串两端必须使用引号,否则会出现语法错误,保险的 办法是所有的值均用引号括住。 接下来,我们就要在网页的 Body 元素中定义 DataGrid 实例了,如 清单 4 清单 4. 定义 DataGrid 实例
    dojoType 指定了该 Web 部件为 dojox.grid.DataGrid,数据使用 jsonStore, 结构为 layout,自动调整宽度。到此,第一个 Grid 就已开发完毕,完整代码如 清单 5 清单 5. 完整代码 first Grid
    First Grid
    在浏览器中运行,效果如下: 图 3. 第一个 Grid 运行结果 178 DataGrid 开发详解 DataGrid 的创建 在 DataGrid 的开发中,有三种方法创建 DataGrid 实例,第一种是 javascript 创 建结构,html 代码创建实例,我们第一个例子就是使用这种方式实现的; 第二种是由 html 代码创建结构及实例,在这种方法中,我们使用 table 标签,定义 Grid 的结构,而省去了在 javascript 中定义 structure 的部分。具体定义方式与标准的 html 书写方式非常类似,定义方式如 清单 6 清单 6. 由 html 代码创建结构及实例
    Product Number Product Minimum Amount Maximum Amount Average Amount
    第三种方式就是采用纯 javascript 的方式定义 DataGrid 实例,清单 7 声明网页加 载完成后就在 id 为 gridNode 的页面结点上创建一个 DataGrid 实例。 清单 7. 纯 javascript 的方式定义 DataGrid 实例 dojo.addOnLoad(function(){ // 指定页面加载完毕后执行 var grid = new dojox.grid.DataGrid({ query: { pro_no: '*' }, id: 'grid2', store: jsonStore, structure: [ {field: 'pro_no', name: 'Product Number' }, {field: 'pro', name: 'Product' }, {field: 'min_amount', name: 'Minimum Amount' }, {field: 'max_amount', name: 'Maximum Amount' }, 179 {field: 'avg_amount', name: 'Average Amount' } ],rowsPerPage: 20 }, 'gridNode'); // 设置 grid 显示在 id 为 gridNode 的节点下 grid.startup(); // 启动 grid }); Grid1.2 可以通过这种方式很方便的与 dojo 容器结合在一起,动态创建页面布局。 Structure 详解 DataGrid 不仅可以创建简单的二维表格,还可以通过对 structure 的设计创建复杂 的表格应用,同时还可以为每一列进行格式化或是取值。我们将 First Grid 进行简单的修 改,得到 清单 8 的代码。 清单 8. 修改 First Grid function formatAmount(value){ return '$ ' + value; } function getRange(rowIndex, item){ if(!item){return '--';} var grid = dijit.byId('grid'); var max = grid.store.getValue(item, "max_amount"); var min = grid.store.getValue(item, "min_amount"); return max - min; } var subrow1 = [ {field: 'pro_no', name: 'Product Number', rowSpan: 2 }, {field: 'pro', name: 'Product', rowSpan: 2 }, {field: 'min_amount', name: 'Min. Amount',formatter: formatAmount,width: '80px' }, {field: 'avg_amount', name: 'Average Amount',formatter: formatAmount, rowSpan: 2 }, {field: 'range', name: 'Range',get:getRange, rowSpan: 2 } ]; var subrow2 = [ {field: 'max_amount', name: 'Max. Amount',formatter: formatAmount}, 180 ]; var layout = [subrow1,subrow2]; 这里,我们从新定义了 layout,将 layout 分为两个子行,其中子行 1 包含了五个 字段,其中 pro_no、pro、avg_amount、range 具有值为 2 的 rowSpan 属性,也 就表明这三列跨越了两行。第二行仅有 max_amount 一个字段。同时,我们为三个 amount 字段指定了 formatter 函数,在其数值前添加美元符号。为 range 字段指定了 get 方法来自动获取最大值与最小值的差。 显示效果如下: 图 4. DataGrid 布局示例 1 除了 rowSpan 属性外我们还可以使用 colSpan 属性,这两个属性的用法与 html 中的用法一致,并且可以在 html 定义表结构中使用,我们再看这个表头的例子来理解一 下 colSpan 的用法。 清单 9. colSpan 的应用 var structure = [[ {field: 'type', name: 'Type', rowSpan: 2}, {field: 'pro', name: 'Product', rowSpan: 2}, {field: 'Q20071', name: 'Q1',formatter: formatAmount }, {field: 'Q20072', name: 'Q2',formatter: formatAmount }, {field: 'Q20073', name: 'Q3',formatter: formatAmount }, {field: 'Q20074', name: 'Q4',formatter: formatAmount } ],[ {field: 'Y2007', name: 'Year 2007',formatter: formatAmount, colSpan: 4 } ]]; 清单 9 的显示效果如下: 图 5. DataGrid 布局示例 2 181 Store 的使用 DataGrid 使用了 Store 作为数据源,在以上的例子中,我们都是将数据写在 javascript 中然后作为 data 参数值传给 Store 的构造方法。但是在大多数情况下,数 据是要动态的通过 Ajax 请求从服务器端获取的,这同样可以通过 Store 来实现。我们仅 需要将声明 Store 对象时传入请求的 url 地址即可,如:new dojo.data.ItemFileReadStore({url: 'jsondata.txt' }) 。Store 包括 dojo.data.ItemFileReadStore 和 dojo.data.ItemFileWriteStore 两个类。我们在使 用 DataGrid 的编辑功能时需要使用 ItemFileWriteStore 来作为数据源。下面就演示了 一个多功能的 DataGrid,这个 Grid 使用外部数据源,可以对单元格进行编辑,并且可 以通过右击表头弹出列选菜单。为了页面能够正确,清单 10 载入了所需的 CSS。 清单 10. 加载所需 CSS 清单 11 的代码引入了所需的 dojo 包,并创建了可编辑的 DataGrid,将其添加到 了 id 为 gridNode 的页面节点中。为了使列具有编辑功能只需要在 structure 定义中的 表示该列的 JSON 定义中添加值为 true 的 editable 属性。 清单 11. 引入所需 Dojo 包 清单 12 定义了菜单 gridMenu 以及承载 DataGrid 的 DIV。 清单 12. 定义菜单
    Data Grid
    本例使用了外部数据源 dataGrid.txt,该文件的内容类似于 清单 13 清单 13. dataGrid.txt { identifier: 'emp_no', label: 'emp_no', items: [ {emp_no:'2100', name:'Matt', gender:'M', dept_no:730, bonus:647}, {emp_no:'2200', name:'Lisa', gender:'F', dept_no:731, bonus:608}, {emp_no:'2300', name:'Mick', gender:'M', dept_no:732, bonus:532}, {emp_no:'2400', name:'John', gender:'M', dept_no:733, bonus:1840}, {emp_no:'2500', name:'Jan', gender:'M', dept_no:734, bonus:1040}, {emp_no:'2101', name:'Jeff', gender:'M', dept_no:730, bonus:647}, {emp_no:'2202', name:'Frank', gender:'M', dept_no:731, bonus:608}, {emp_no:'2303', name:'Fred', gender:'M', dept_no:732, bonus:532} ]} 运行,结果如下图所示。 图 6. 可编辑的 DataGrid DojoX Charting Charting 是基于 DojoX 绘图包的数据可视化组件,包括了 Chart2D 和 Chart3D 来分别绘制 2D 和 3D 的图表。Chart2D 提供多种样式的饼图、柱状图、折线图、面积 图、网格等图表。Chart3D 目前仅提供了 3D 柱状图和 3D 圆柱图,并且从社区获取的 184 信息表明由于 IE 上的性能问题导致 Chart3D 的开发暂时搁置。Charting 的应用主要分 为如下几个步骤: 首先引入所需要的 dojox 类,如: dojo.require("dojox.charting.Chart2D"); //Chart2D 所需要的 2D 类 dojo.require("dojox.charting.Chart3D"); //Chart3D 所需要的 3D 类 dojo.require("dojox.charting.themes.PlotKit.blue"); // 样式主题 第二,声明 Chart 对象,包括了 Chart2D 或 Char3D, 如:var chart1=new dojox.charting.Charting.Chart2D('chart1'); 这里传入的参数为要在页面中载入 chart1 的元素的 ID,也就是 chart1 显示后的上层 标签的 ID; 使用 Chart 对象的 setTheme 为 Chart 对象设置主题,来保证准确的绘制图表; 使用 Chart 对象的 addPlot 方法为 Chart 对象添加部件,可以添加多个部件; 使用 Chart 对象的 addSeries 方法为 Chart 对象添加数据; 最后,调用 render 方法将 chart 对象添加到页面节点中; 下面我们来看几个应用实例。 2D 饼图 清单 14 的代码为 2D 饼图,我们可以看到该实例加载了类 Chart2D 和 themes.PlotKit.blue 对象。chart1 对象声明在 ID 为 char1 的元素下,并被添加了一 个 Pie 部件作为默认部件,数据为 3、2、5、1、6、4。 清单 14.2D 饼图 ……
    运行结果如下: 图 7. 二维饼图示例 带网格的 2D 面积图 这个例子中我们为 Chart2D 对象添加了两个部件,网格和面积图,如 清单 15。值 得注意的是,我们为 chart 添加了两个部件,Plot1 和 Plot2,其中 Plot1 的类型为 Areas,Plot2 的类型为 Grid,我们给 Plot1 添加了三组数据,并没有给 Plot2 添加数 据。 清单 15. 网格和面积图 chart = new dojox.charting.Chart2D("chart2"); chart.setTheme(dojox.charting.themes.PlotKit.orange); chart.addAxis("x", {origin:"max"}); chart.addAxis("y", {vertical: true, leftBottom: true, min: 5000, max: 8000, majorTickStep: 500, minorTickStep: 100}); chart.addPlot("plot1", {type: "Areas", hAxis:"x", vAxis:"y"}); chart.addPlot("plot2", {type: "Grid", hAxis:"x", vAxis:"y"}); data1 = [{x:10,y:7200}, {x:20,y:6800}, {x:30,y:7000}, {x:40,y:6600}, {x:50,y:7000}, {x:60,y:6800}, {x:70,y:7200}, {x:80,y:6600}, {x:90,y:6800}, {x:100,y:7000}]; 186 data2 = [{x:10,y:6800}, {x:20,y:5800}, {x:30,y:6400}, {x:40,y:5600}, {x:50,y:6000}, {x:60,y:6200}, {x:70,y:6600}, {x:80,y:7200}, {x:90,y:6300}, {x:100,y:6000}]; data3 = [{x:10,y:6000}, {x:20,y:6300}, {x:30,y:6800}, {x:40,y:6200}, {x:50,y:6200}, {x:60,y:6600}, {x:70,y:6300}, {x:80,y:6200}, {x:90,y:6000}, {x:100,y:5900}]; chart.addSeries("series B", data2, {plot: "plot1"}); chart.addSeries("series C", data3, {plot: "plot1"}); chart.addSeries("series A", data1, {plot: "plot1"}); chart.render(); 运行结果如下: 图 8.二维面积图示例 巧用折线图进行函数图像的绘制 我们可以使用折线图或者面积图完成函数图像的绘制,原理就是按照一定的步长循环 将定义域中将 x,y 值计算出来组成一组数据添加的 Chart2D 对象中,下面这个例子就 是使用了折线图绘制了正弦余弦曲线。 清单 16 绘制正弦余弦曲线 dojo.require("dojox.charting.Chart2D"); dojo.require("dojox.charting.themes.PlotKit.blue"); dojo.addOnLoad(function() { var period = 2 * Math.PI; var tick = Math.PI / 180.0; var step = 5*Math.PI / 180.0; var chart = new dojox.charting.Chart2D('chart_area'); chart.setTheme(dojox.charting.themes.PlotKit.blue); 187 chart.addAxis("x", {min: 0, max: period, majorTickStep: tick*30, minorTickStep: tick*10, minorLabels: false, font: '40px bold'}); chart.addAxis("y", {vertical: true, min: -1.01, max: 1, majorTickStep: 0.5, minorTickStep: 0.1, minorLabels: false, font: '40px bold'}); chart.addPlot("default", {type: 'Lines'}); chart.addPlot("grid", {type: "Grid", vMinorLines: true}); var series = {'sin' : [], 'cos' : []}; for(var i = 0; i < period; i+=step) { series.sin.push({'x' : i, 'y' : Math.sin(i)}); series.cos.push({'x' : i, 'y' : Math.cos(i)}); } chart.addSeries('sin', series.sin); chart.addSeries('cos', series.cos); chart.render(); }); 运行结果如下: 图 9. 正余弦曲线图示例 3D 柱状图的绘制 使用 Chart3D 与 Chart2D 略有不同,这主要是因为 3D 绘图比 2D 绘图要复杂 一些,3D 绘图一个很重要的过程就是坐标变换,以及光照和渲染都是很重要的考虑要点。 不过 Chart3D 已经对 gfx3D 进行了封装,我们只需要通过对简单的几个参数的设置就 可以完成一个 3D 图表。我们可以从 清单 17 中看出 Chart3D 对象的声明比 Chart2D 多了一些参数,这主要是两类,光照与摄像机。光照指的是 3D 物体的周围的光环境,其 中 lights 是光源数组,也就是说 3D 物体可以接受多个光源的光照;ambient 为环境光, 影响物体的各个立体面;specular 是镜面反射光,这是光源照射物体特定位置发生镜面反 射所产生的高光。当然了,你甚至可以不用理解这些参数的意义,在实际使用中多次调整参 数以满足自己的使用需求。 188 清单 17. 3D 绘图 dojo.require("dojox.charting.Chart3D"); dojo.require("dojox.charting.plot3d.Bars"); dojo.require("dojox.charting.plot3d.Cylinders"); makeObjects = function(){ var m = dojox.gfx3d.matrix; var chart = new dojox.charting.Chart3D("test", {lights: [{direction: {x: 5, y: 5, z: -5}, color: "white"}], ambient: {color:"white", intensity: 2}, specular: "white"}, [m.cameraRotateXg(10), m.cameraRotateYg(-10), m.scale(0.8), m.cameraTranslate(-50, -50, 0)] ); var plot1 = new dojox.charting.plot3d.Bars(500, 500, {gap: 10, material: "yellow"}); plot1.setData([2,1,2,1,1,1,2,3,5]); chart.addPlot(plot1); var plot2 = new dojox.charting.plot3d.Bars(500, 500, {gap: 10, material: "red"}); plot2.setData([1,2,3,2,1,2,3,4,5]); chart.addPlot(plot2); var plot3 = new dojox.charting.plot3d.Cylinders (500, 500, {gap: 10, material: "#66F"}); plot3.setData([2,3,4,3,2,3,4,5,5]); chart.addPlot(plot3); var plot4 = new dojox.charting.plot3d.Cylinders (500, 500, {gap: 10, material: "#E6F"}); plot4.setData([3,4,5,4,3,4,5,5,5]); chart.addPlot(plot4); chart.generate().render(); }; dojo.addOnLoad(makeObjects); 运行这段代码,显示效果如下: 图 10. 三维柱状图示例 189 这个 3D 柱状图结合了长方体和圆柱体,dojox.charting.plot3d.Bars 是长方体的 声明类,new dojox.charting.plot3d. Cylinders 是圆柱体的声明类。 DojoX Gfx 和 Gfx3D DojoX Gfx 和 Gfx3D 是 DojoX 中进行绘图的两个包,分别提供了 2D 和 3D 的 绘图 API。前面介绍的 DojoX Charting 就是在这两个包的基础上开发的。Gfx 以及 Gfx 3D 是一组矢量绘图 API。对于原生的矢量图形,Gfx 能够支持 SVG、Canvas 和 VML, 新版本中又增加了对 Silverlight 的支持。首先,我们来看看 DojoX Gfx 可以做什么。 在 dojo 官方网站公布的开发包中有这么几个 Gfx 例子,见下图。可以看出,Gfx 对于 简单的 2D 绘图已经绰绰有余了。 图 11.Gfx 绘图 190 下面我们就来使用 Gfx 开始绘图。Gfx 绘图的基本步骤可以简单的归为两步:打开 一个画布(surface),然后“画”。在绘画之前,我们要例行公事,引入 dojox.gfx 包。 然后通过 dojox.gfx 对象的 createSurface 方法建立画布。Surface 提供了很多画笔来 进行作画,这里我们介绍几个比较常用的。 线性画笔:使用方法为 surface.createLine(opt),参数包含 x1、y1、x2、y2 四 个属性,分别表示起点横纵坐标和终点横纵坐标; 矩形画笔:使用方法为 surface.createRect(opt),参数 opt 是一个 JSON 对象, 包括了属性 x、y、width、height,分别表示矩形左上角的横纵坐标以及举行的宽和高; 圆形画笔:使用方法为 surface.createCircle(opt),参数 opt 具有三个属性,分别 是圆心横坐标 cx、圆心纵坐标 cy、圆半径 r; 路径画笔:这个是最强大的画笔,可以绘制任意曲线和图形,当然也是使用最复杂的 画笔,使用方法为 surface.createPath(path),path 是描绘一组路径的字符串,描绘规 则可以参考 SVG 中关于矢量路径的介绍。 除以上介绍的几种外还有点、折线、文字等画笔,并且还提供了组功能,可以使一组 图形一起响应事件等。下面就看个简单的例子来加深理解。 清单 18. Gfx 绘图代码 dojo.require("dojox.gfx"); surface = dojox.gfx.createSurface(dojo.byId("gfx_holder"), 700, 700); surface.createRect({x: 260, y: 260, width: 50, height: 50}).setFill("#AAF"); surface.createLine({x1: 100, y1: 400, x2: 400, y2: 350}) .setStroke({color:"#9F3",width:5}); surface.createCircle({cx: 200, cy: 200, r: 50}) .setFill("#FEF").setStroke({color: "#F9A", width: 3}); var path="M153 334 " + "C153 334 151 334 151 334 C151 339 153 344 156 344 " + "C164 344 171 339 171 334 C171 322 164 314 156 314 " + "C142 314 131 322 131 334 C131 350 142 364 156 364 " + "C175 364 191 350 191 334 C191 311 175 294 156 294 " + "C131 294 111 311 111 334 C111 361 131 384 156 384 " + "C186 384 211 361 211 334 C211 300 186 274 156 274" surface.createPath(path).setFill("rgb(FF,FF,FF)").setStroke({color:"red",width:3}); 清单 18 中的代码将在 ID 为 gfx_holder 的 html 标记内添加这个绘图。这段代 码中的 setFill 方法和 setStroke 方法分别用来设置填充效果和笔触效果。代码运行结果 如下图所示。 图 12. Gfx 简单绘图示例 191 Gfx3D 的工作原理是采用计算机图形学的原理将三维空间中的物体按照透视规则从 三维坐标系转换成二维的坐标系然后通过 SVG 等矢量图显示出来。限于浏览器和 javascript 的性能,目前 Gfx3D 仅能绘制较为简单的 3D 物体和空间曲线,还不能绘制 复杂的空间曲面以及进行纹理等渲染工作,但是 Gfx3D 足以满足大部分 Web 应用的需 要了。Gfx3D 的使用大致分三个步骤:建立画布,在画布上建立视图,在视图上建立 3D 物 体。视图上必不可少的要设置光源以及摄像机方位。设置光源可以通过 setLights 方法来 设定,摄像机方位则需要使用 dojox.gf3d.maxtrix 对象的 cameraRotateX、 cameraRotateY、cameraRotateZ、cameraTranslate 等方法来确定。清单 19 在画 布上绘制了一个立方体和一个圆柱体。 清单 19. Gfx 3D 绘图 dojo.require("dojox.gfx3d"); makeObjects = function(){ var surface = dojox.gfx.createSurface("test", 500, 500); var view = surface.createViewport(); // 建立视图 view.setLights([{direction: {x: 0, y: 0, z: -10}, color: "white"}, {direction: {x: 10, y: 0, z: -10}, color: "#444"}], {color: "white", intensity: 2}, "white"); var m = dojox.gfx3d.matrix; // 建立空间 var l = view.createCube({bottom: {x: 0, y: 0, z: 0}, top: {x: 100, y: 100, z: 100}}) .setFill({type: "plastic", finish: "dull", color: "lime"}); // 绘制立方体 view.createCylinder({}) // 绘制圆柱体 .setTransform([m.translate(200, 100,200), m.rotateZg(60), m.rotateXg(-60)]) .setStroke("black") .setFill({type: "plastic", finish: "glossy", color: "red"}); 192 var camera = [m.cameraRotateXg(20), m.cameraRotateYg(20), m.cameraTranslate(-200, -200, 0)]; // 设置摄像机方位 view.applyCameraTransform(camera); view.render(); }; dojo.addOnLoad(makeObjects); 运行,结果如下图所示。 图 13. Gfx-3D 简单绘图示例 其他 DojoX Widget DojoX 的 widget 包中还提供了更多的小部件,非常的方便易用,本文的最后再来 两个餐后甜点,拾色器和鱼眼。拾色器是我们经常会用到的小部件,用 DojoX 生成拾色器 非常简单,仅需要 清单 20 这一小段代码即可。 清单 20. 拾色器
    除了这段 javascript 代码,为了能正确的显示拾色器的样式,我们还需要导入它的 css 文件:dojox/widget/ColorPicker/ColorPicker.css。代码中,属性 animatePoint 来确定指针是否有滑动动画,默认为 true;属性 showHsv 表示是否显示 HSV 颜色模 式数值;属性 showRgb 表示是否显示 RGB 颜色模式的数值;webSafe 表示是否显示 Web 安全色。清单 20 显示效果如下: 图 14 .DojoX 拾色器 193 鱼眼的实现也很简单,我们需要制作一组图标,命名为 fe1.gif、fe2.gif、fe3.gif 一 直到 fe7.gif。我们将它们和页面放在同一文件夹下。然后在网页中书写如 清单 21 所示 代码。 清单 21. 鱼眼
    运行,鼠标移上去来看看效果。 图 15.DojoX 鱼眼 194 结束语 JavaScript 并不是万能的,同样,使用 JavaScript 开发的 Dojo 也不是万能的, 但是为了满足众多开发者的需要,DojoX 提供了非常丰富的选择,并且提供了对 Flash、 SilverLight、Google Gear 等组件的支持,使得 Dojo 爱好者们可以开发出更好更强大 的 Web 应用。随着 DojoX 项目的不断完善和成熟,Dojo 将给开发者们带来更多的惊喜。 常用例子 dojo dijit dojox dojo 是核心文件。 dijit 是内部定义好的组件,包括各种 form 组件,布局组件等。 dojox 是扩展包,对 dojo 和 dojox 进行扩展的 js。 具体可以查看对应的 API。 常用工具方法 forEach, 它可以对数组中的每个元素执行一个函数, 还可以接受第二个参数,作为回调 函数调用的作用域(Scope). dojo.query(".odd").forEach(function(node, index, nodelist){ // 针对 query 返回的数组中的每个节点,执行本方法 dojo.addClass(node, "red"); }); forEach 的新写法:dojo.query("select").forEach("item.disabled = true"); item 必须固定这么写 dojo.map:用法与 dojo.forEach 类似,设置一个回调函数,然后对每个 Array 元素调 195 用这个函数。不同之处有两点: 1. dojo.map 会返回一个处理后的 Array; 2. dojo.map 的回调函数需要返回一个值,作为 dojo.map 返回的 Array 中的一个元素。 dojo.some:这个函数理解起来很容易, 它设定一个回调函数,对 Array 中的元素进 行判断,如果有返回 true 的元素,dojo.some 就返回 true,否则就返回 false。 dojo.every:与 dojo.some 类似,不同的是回调函数对每个元素都返回 true 的时候, dojo.every 才会返回 true,否则返回 false。 dojo.filter:用于 Array 元素的过滤,返回值为过滤后的 Array,里面包含的元素是回调 函数返回值为 true 的元素。 dojo.indexOf:返回元素在 Array 中的 index 值,如果没有这个元素的话就返回-1。如: dojo.indexOf(array,element)。如果 Array 中有两个元素相等,则返回先找到的那个元素 的 index。 dojo.lastIndexOf:同 dojo.indexOf,不过返回的是找到的最后一个元素的 index 值。 改变节点的样式属性: dojo.style(node, "backgroundColor", "blue"); dijit 组件的使用 举一个简单的例子,toolbar 的使用: 在 js 中定义 toolbar 组件
    ready(function(){ // 声明初始toolbar,并绑定在id为toolbar的dom节点上 var toolbar = new dijit.Toolbar({}, "toolbar"); // 将button放入toolbar中 var button = new dijit.form.Button({ label: "保存", iconClass: "dijitEditorIcon dijitEditorIcon dijitEditorIconSave", onClick: function(){ } }); toolbar.addChild(button); 196 }); 在 HTML 中定义 toolbar 组件 注意一下,有一些组件在 html 中声明的时候没有作用,只有在 js 中初始化才会生效,比 如 toolbar。所以上面的例子并没有在 html 中声明,只是对 toolbar 中的按钮进行了初始化。 gridx 的使用 Gridx 是扩展 dojo 的 grid 的另外一个框架,需要单独引用其 js 文件。 下面举一个简单的例子: require([ "dojo/store/Memory", // 引用所需要的 js "gridx/Grid", "gridx/core/model/cache/Async", ], function(Store, Grid, Cache){ // 列的声明 var columns = [ {field: 'label', name: '权限名称', width: '20%'}, {field: 'module', name: '系统模块', width: '20%'}, {field: 'description', name: '相关描述', width: '60%'} ]; dojo.xhrGet({ url:" /common/getPersistObjects.action",//发送 ajax 请求获取数据 handleAs: "json", // 接收的数据格式为 JSON load: function(data){ // 请求返回后进行的操作 //将 json 数据转化成指定的 store 格式 var store = new dojo.store.Memory({data: data}); var privilegeGrid = new Grid({ cacheClass: Cache, // 缓存 store: store, // 数据 197 structure: columns // 列 }, 'privilegeList'); privilegeGrid.startup(); // 开始构造 grid 表格 }, }, content: {id: id, value:value}); // 请求中带的参数 });
    从后台获取数据后都是存放在 data 或者 store 这 2 个接口的实现类里,然后对数据进行操作显 示出来,增删改查操作实际上操作的是 store 或者 data 中的数据,而不是视图层中显示的数据。 dojo 的 UI 控件(部分) dijit.layout.StackContainer 该组件用于实现导航式多个页面并列,可以来回切换查看不同页面,与 tab 页形式最大的区 别是能对页面中任一块区域实现这样的效果。其中,StackController 用于控制放置到 StackContainer 中的页面切换和导航显示。 用法如下: Js代码: eventStackContainer = new dijit.layout.StackContainer({ style: "height:100%; width: 400px;", id: "eventListStackContainer", region:"center", content:"" }, "eventListPane"); eventStackController = new dijit.layout.StackController({ containerId: "eventListStackContainer", iconClass:"dijitIconConfigure", style:"width:100%;margin:0px;border:0px;" }, "eventPageController"); eventStackContainer.startup(); eventStackController.startup(); 页面元素:
    效果示意图: 198 如图即为 StackController 的显示,点击各个导航标题即实现不同页面的切换。 dojox.layout.ExpandoPane 可展开或收起的布局面板。 用法如下: 声明式:
    Js 编程式: expandoPane = new dojox.layout.ExpandoPane({ region: "leading", maxWidth:"15%", title:"     事件管理", style: "height:100%;width: 15%; margin:0px;padding: 0px" }); 效果如图: 手风琴菜单,使用 dijit.layout.AccordionContainer。 效果如下: 199 A)编程方式实现: require([“dijit/layout/AccordionContainer”, “dojox/layout/ContentPane”], function(AccordionContainer, ContentPane) { var accordion = new AccordionContainer({ style: "width: 100%; height: 100%; overflow: hidden" }, "mapNavAccordion"); accordion.startup(); accordion.addChild(new ContentPane({ title: “网络拓扑图”, href: “../map/navTree.jsp?type=1” }); accordion.addChild(new ContentPane({ title: “业务拓扑图”, href: “../map/navTree.jsp?type=2” }); accordion.addChild(new ContentPane({ title: “影响度视图”, href: “../map/navTree.jsp?type=3” }); accordion.addChild(new ContentPane({ title: “机房导航图”, href: “../map/navTree.jsp?type=4” }); }); 200 B)声明方式实现:
    等待按钮,使用 dojox.form.BusyButton。 效果如下: 正常状态: 等待状态: A) 编程方式 require([“dojox/layout/BusyButton”], function(BusyButton) { var btn = new BusyButton({ label: “上传”, busyLabel: “正在上传”, timeout: 5000, onClick: function(e) {…} }, “upload”); }); B) 声明方式 3)文件上传,直接使用 dijit.form.ValidationTextBox。 效果如下: A)声明方式 使用 Dojo 提供的灵活多样的布局方式 Dojo 提供了多种基本的布局方式,使用这些布局,可以有层次,有意义的组织控件, 使得 web 界面获得更好的用户体验。本文从常见的几种控件出发,介绍 Dojo 常见的布 局方式。 前言 Dojo 提供了多种基本的布局方式,使用这些布局,可以有层次,有意义的组织控件, 使得 web 界面 获得更好的用户体验。 下面将从常见的几种控件出发,介绍 Dojo 常见的布局方式,让我们一起学习 Dojo 灵活而又丰富的 布局方式。 基本布局方式 Dojo 基本布局方式主要体现在下列几种控件: ContentPane (dijit.layout.ConentPane) TitlePane (dijit.TitlePane) FloatingPane (dojox.layout.FloatingPane) ScrollPane(dojox.layout.ScrollPane) BorderContainer (dijit.layout.BorderContainer) ContentPane (dijit.layout.ConentPane) ContentPane,顾名思义,就是用于放置若干内容的面板,是各种布局的基本元素。 ContentPane 的功 能类似于 iFrame。除此之外 ContentPane 还可以与其他 Layout 控件互相嵌套。首先看一个最简单的 ContentPane 的例子: 清单 1. ContentPane 声明法示例
    清单 2. ContentPane 程序生成法示例 …
    在这个例子中,ContentPane 完全实现了 iFrame 的功能,当然,ContentPane 的 功能不止这些。值 得注意的是:1. 在使用声明法时,不要忘记 dojoType 属性,2. 在 使用程序生成法时,不要忘记 startup(),3. 如果 href 页面中 require 的 dojo 控件, 在调用页面中必须再 required 一次,否则 子页面的 dojo 控件会解析 / 创建失败。 203 ContentPane 控件有一些经常会用到的属性和方法,下面予以一一介绍: content – String, DomNode, NodeList content 顾名思义,就是指 ContentPane 中显示的内容。如果没有定义 href 属性, ContentPane 组 件内将显示 content 内容。否则将显示 href 页面的内容。content 可 以是 String, DomNode 和 NodeList 三种类型。下面例子给出了 content 使用方法。 清单 3. ContentPane: content 属性示例 1 … var myFirstContentPane = new dijit.layout.ContentPane({ id: "myFirstContentPane", content: "Hello, Dojo World!" },dojo.byId("myFirstContentPane")); … 浏览器将显示如下信息: 图 1. content 属性示例 1 清单 4. ContentPane: content 属性示例 2 … var myDomNode = document.createElement("table"); // 创建一个 DomNode, table 元素 var myTD1 = document.createElement("td"); myTD1.innerHTML = "Hello, Dojo World!"; var myTR1 = document.createElement("tr"); myTR1.appendChild(myTD1); myDomNode.appendChild(myTR1); //Table 的第一行是”Hello, Dojo World!” var myTD2 = document.createElement("td"); myTD2.innerHTML = "Hello, ContentPane World!"; var myTR2 = document.createElement("tr"); myTR2.appendChild(myTD2); myDomNode.appendChild(myTR2); //Table 的第二行是”Hello, ContentPane World!” myDomNode.border = 1; var myFirstContentPane = new dijit.layout.ContentPane({ 204 id: "myFirstContentPane", //href:"TestContentPane.html" content: myDomNode // 将创建的 DomNode 赋给 content 属性 },dojo.byId("myFirstContentPane")); … 浏览器将显示如下信息: 图 2. content 属性示例 2 title – String title 属性定义了 ContentPane 的标题。这个标题只有当 ContentPane 作为 TabContainer/ StackContainer 等 Layout 控件的子控件时,才可以看到。此时该属性 值显示于该 Tab 页的标题处。下 图展示了这一情形。 图 3. title 属性示例 closable -- boolean closable 属性同 title 属性一样,也是在作为 TabContainers 等 Layout 控件的 子控件时,才有意 义。如在 TabContainer 中,如果 closable=”true”,那么该 tab 页 面的标题处会显示用于关闭 tab 的图标。下图展示了 closable = “true”时的情形: 图 4. closable 属性示例 点击小圆叉,first 页面将会被关闭: 图 5. closable 属性示例 2 loadingMessage / errorMessage -- String 205 当 ContentPane 内容 / 页面加载中,或出现错误时,ContentPane 将会显示的内 容。 清单 5. ContentPane: loadingMessage 属性示例 … myContentPane.attr("loadingMessage", "Still Loading …"); … placeAt() -- function 这是一个常用的方法,dojo 控件都实现了这个方法。运用这个方法可以自由地放置控 件的位置。该方 法有两个参数:reference, 和 position。可以接受的 reference 参数 类型有:String, DomNode 和 _Widget。其中 String 为引用 Dom 节点 (DomNode),或者 dojo 控件 (_Widget) 的 id。而被座位参数 传入的 dojo 控件, 必须是实现了 addChild 方法的类型的。可接受的 position 参数类型有 Int, 和 String。传入的 String 参数必须是”first”,”last”,”before”,”after”中的一个。举一个例 子 : 清单 6. ContentPane: placeAt() 方法示例 … myContentPane.placeAt("OuterContentPane", "first"); … 该例子中,myContentPane 将被嵌套放入 OuterContentPane 的第一个位置。 attr() – function attr 是常用的 dojo 控件通用方法。该方法在前面的例子已经出现过了,用于为 dojo 控件的属性赋 值。如:_Widget.attr(“value”, 3) 相当于 _Widget.setValue(3)。在新 的版本中,许多用于修改属 性的方法被不推荐了,如 setValue, setHref 等,均由 attr(“value”, value), attr(“href”, href) 方法取代。 ContentPane 还支持 dojo 事件响应机制,用 connect 方法链接 dojo 控件,事 件,与事件处理方法 。ContentPane 中定义的很多事件需要与其他布局容器结合才能显 示效果,如在 TabContainer 中,可以 加上对 ContentPane 的 onClose, onShow, onHide 等事件响应。 在这里,先给出一个事件响应的例子: 清单 7. ContentPane: 事件响应示例 … dojo.connect(myContentPane, "onClick", function(event){alert(“Click me!”);}); … TitlePane (dijit.TitlePane) 206 TitlePane 本质上仍然是一个 ContentPane, 不同点是,TitlePane 自带了 Title 的显示。在 TitlePane 中,title 属性定义了显示的标题。显示效果如图所示: 图 6. TitlePane 示例 清单 8. TitlePane: 基本示例 …
    Hello World! First Line.

    Hello World! Last Line.

    该例中展示了构造 TitlePane 的程序法与声明法,以及 placeAt 的使用方法。可以 看出,除了引用 为 dijit.TitlePane 外,其他用法均与 ContentPane 相同。 下面介绍 TitlePane 的一些常用属性: close -- boolean TitlePane 的内容可以被收起。close 属性定义了 TitlePane 是否处于收起状态。 207 清单 9. TitlePane:close 属性示例 … myTP.attr("close", true); … 除了通过程序收起面板,TitlePane 还可以通过点击顶部的标题,打开和收起面板主 体。如下图所示 : 图 7. TitlePane: close 属性示例 1 图 8. TitlePane: close 属性示例 2 duration -- number 收起和打开 TitlePane,是一个动画过程,duration 定义了该动画持续的时间,单 位是毫秒。 清单 10. TitlePane:duration 属性示例 … myTP.attr("duration", 1000); … 下图展示了定义 duration 后收起 TilePane 时展示的动画过程瞬间。 图 9. TitlePane: duration 属性示例 通过例子,可以发现 TitlePane 也可以和 ContentPane 一样相互嵌套。但 TitlePane 不适合被嵌套 在 StackContainer 中,这样会显得不伦不类: 图 10. TitlePane: 在 StackContainer 中的 208 FloatingPane (dojox.layout.FloatingPane) FloatingPane 是 可以随意移动的 TitlePane。 清单 11. FloatingPane: 声明法示例 …
    The Content of the Floating World!
    清单 12. FloatingPane: 程序生成法示例 …
    需要注意的是:1. 必须引用 FloatingPane.css; 2. 如果需要具备改变容器大小 (resize) 功能,需 引用 ResizeHandle.css. 上面创建方法得到的 FloatingPane 显示如下图所示,用户可以自由地在它的外层容 器中移动位置: 图 11. FloatingPane: 移动 1 图 12. FloatingPane 移动 2 下面介绍 FloatingPane 的一些常用属性: closable -- boolean 与 TitlePane 相同,closable 属性定义了该面板是否可以被关闭。 resizable -- boolean 211 该属性定义了 FloatingPane 是否可以在运行时改变大小。当 resizable 为 true 并引用了 ResizeHandle.css 时,用户可以通过拖动控制点来改变面板的大小 图 13. FloatingPane: resizable 属性示例 dockable -- boolean 该属性定义了 FloatingPane 是否可以被最小化到页面的最下方。与 TitlePane 可 以收起面板不同的 是,FloatingPane 可以将面板最小化到页面的最下方,如同 Windows 的工具栏。当 dockable 为 true 时,用户可以通过点击面板标题位置向下的三角来最小 化面板。最小化的结果如图所示: 图 14. FloatingPane: dockable 属性示例 sytle – object 该属性定义了面板的位置。其中 position 定义是绝对位置还是相对位置,top 和 left 定义了面板 左上角的位置,width 和 height 定义了面板的大小。但当面板被拖动或改变 大小时,该值不会跟着变化 。 ScrollPane (dojox.layout.ScrollPane) 顾名思义,ScrollPane 是可以滚动的面板。当面板内容超出了面板显示范围时,使用 ScrollPane 可 以让内容滚动起来。 清单 13. ScrollPane: 声明法示例 …
    title Pane content
    title Pane content
    title Pane content
    title Pane content
    title Pane content
    title Pane content
    title Pane content
    title Pane content
    请注意引用“/dojox/layout/resources/ScrollPane.css”样式,否 则在面板内容 滚动时,不会显示滚动滑块。下面两图,图 15 为引用了 ScrollPane.css 样 式的效果,图 16 为没有引 用 ScrollPane.css 样式的效果。 图 15. ScrollPane: 引用 css 示例 图 16. ScrollPane: 未引用 css 示例 214 图 16 中的滚动条并不能起到滑块的作用。本例在展示 ScrollPane 用法的同时,也 展示了面板嵌套 的方法。 下面介绍 ScrollPane 的一些常用属性: orientation – String 定义了 ScrollPane 的滚动方向,有 vertical 和 horizontal 两种选择。需要注意 的是,滚动的方 向必须出现内容显示不下的情况,否则 SrollPane 的显示效果和 ContentPane 没有区别。 BorderContainer (dijit.layout.BorderContainer) BorderContainer 是一个布局容器,它将容器内容分为 5 个区域:左 (left/leading), 右 (right/trailing), 上 (top), 下 (bottom), 中 (Center),如图所 示。 图 17. BorderContainer: 基本示例 215 清单 14. BorderContainer: 声明法示例 …
    216
    Leading Region
    Tailing Region
    Center Region
    Top Region
    Bottom Region
    五个区域可以分别嵌套各种容器与面板。如下图,顶部嵌套了 ScrollPane 面板 图 18. BorderContainer: 基本示例 2 下面介绍一些 BorderContainer 常用的属性: liveSpliter – Boolean 217 该属性定义了当用户拖动区域边界时,容器内区域大小是随着鼠标的移动改变 (liveSpliter = true) ,还是只有到鼠标松开时,才执行容器内区域大小的修改 (liveSpliter = false)。下图展示了该属性为 false 时拖动区域边界的情况: 图 19. BorderContainer:liveSpliter 属性示例 gutters – Boolean 该属性定义了 BorderContainer 是否具有边界和留白。前面例子都是 gutter 为 true 的情况,下图 展示了 gutter 属性为 false 的情况: 图 20. BorderContainer:gutter 属性示例 design – String 该属性定义了 BorderContainer 使用的布局方式。有“sidebar”, “headline”两种布 局方式。选 用 sidebar 布局方式时,左右区域与 BorderContainer 同高。选用 headline 布局方式时,上下区域与 BorderContainer 同宽。前面例子为 design=“sidebar”的示例,下图为 design=“headline”的示例 。 图 21. BorderContainer:design 属性示例 218 spliter – Boolean 与 region – String 这两个属性是为嵌套在 BorderContainer 内部的区域定义的。region 定义内部区 域位置 (top, bottom, left, right, leading, trailing, center),spliter 定义是否可以通 过拖动区域边界修改区 域大小。 以下简单介绍几种比较高级的布局方式,以便为实际的使用提供更多的选择。 高级布局方式 高级布局的方式的控件有: GridContainer (dojox.layout.GridContainer) AccordionContainer (dijit.layout.LayoutContainer) TabContainer (dijit.layout.TabContainer) GridContainer (dojox.layout.GridContainer) GridContainer 是一种比较灵活的布局方式,允许用户根据自己的需要拖拽 grid 中 的内容,每个 grid 中存放一个布局对象;可以通过一下函数设置容器的列数; acceptTypes 属性用来设置每个行可接 受的对象类型,比如 ContentPane, TitlePane, ColorPalette, Calendar 等。 使用表述的方法创建该布局方式代码如下; 清单 15. GridContainer 声明法示例
    可以通过以下的 setColumns 函数根据用户的输入设置行数 清单 16. GridContainer 程序生成法示例 function setColumns(){ GC1 = dijit.byId("GC1"); var nb = dojo.byId("nbCol").value; if(nb > 0){ GC1.setColumns(nb); } } 图 22. GridContainer 示例 AccordionContainer (dijit.layout.AccordionContainer) 位于 dijit.layout 包中 , 引用方法如下。 清单 17. AccordionContainer 引用方式 dojo.require("dijit.layout.AccordionContainer"); AccordionContainer 顾名思义是像手风琴一样可以收缩的面板,这种方式比较适合 单个 portal 的布 局,小巧易用;也可以用于整个页面的布局。 图 23. AccordionContainer 示例 220 使用表述的方法创建该布局方式代码如下; 清单 18. AccordionContainer 声明法示例
    LayoutContainer (dijit.layout.LayoutContainer) 通过设置 layoutAlign="left"的属性来定义每个对象的具体位置;可以重复使用 layoutAlign = "left",这样的结果根据声明的先后顺序从左至右,从上至下的进行排列。 这种布局方式比较简单,不需 要考虑每个待布局对象的具体位置,只需要考虑对象之间的 相对位置就可以了,比较适合于对整个页面进 行布局。 图 24. LayoutContainer 示例 使用表述的方法创建该布局方式代码如下; 清单 19. LayoutContainer 声明法示例
    221 left
    right
    ……
    StackContainer (dijit.layout.StackContainer) 需要 dijit.layout.StackContainer 和 dijit.layout.StackController,引用如下; 清单 20. StackContainer 引用方法 dojo.require("dijit.layout.StackContainer"); dojo.require("dijit.layout.StackController"); 这种布局分为控制器和面板两部分,控制器就是一组按钮,通过点击负责控制的按钮, 在面板中显示 被关注的内容。 之所以叫做栈,在于这种布局的控制器(也就是按钮)提供了前进和后退的功能,例 如下图中的按钮 page1 之前有一个前进的控制器,点击可以激活 page3,如此循环,保 持一定的顺序。 图 25. StackContainer 示例 使用描述的方法创建该布局方式代码如下; 清单 21. StackContainer 声明法示例 -- 控制器部分 清单 22. StackContainer 声明法示例 -- 布局部分
    ……
    使用程序的方法动态创建该布局方式代码如下; 清单 23. StackContainer 程序生成法示例 -- 布局部分 var container = new dijit.layout.StackContainer({ id: "sc" },"myStackContainer"); container.addChild(new dijit.layout.ContentPane({ title: "Page 1", content: "Page 1" })); container.addChild(new dijit.layout.ContentPane({ title: "Page 2", content: "Page 2" })); // make the controller var controller = new dijit.layout.StackController({containerId: "sc"}, "holder"); // start 'em up controller.startup(); container.startup(); 清单 24. StackContainer 程序生成法示例 -- HTML 部分
    TabContainer (dijit.layout.TabContainer) 223 Tab 目前在网页和应用程序中广泛使用,Dojo 也提供了 tab 的布局方式; TabContainer 位于 dijit.layout 中,引用方法如下。 清单 25. TabContainer 引用方法 dojo.require("dijit.layout.TabContainer"); 使用描述的方法创建该布局方式代码如下; 清单 26. TabContainer 声明法示例
    使用程序的方法动态创建该布局方式代码如下 : 清单 27. TabContainer 程序生成法示例 var tc, cp1, cp2, cp3; tc = new dijit.layout.TabContainer({style:'height:200px;width:500px;'}, dojo.byId('main')); cp1 = new dijit.layout.ContentPane({title: 'Tab 1'}); cp1.domNode.innerHTML = "Contents of Tab 1"; tc.addChild(cp1); cp2 = new dijit.layout.ContentPane({title: 'Tab 2'}); cp2.domNode.innerHTML = "Contents of Tab 2"; tc.addChild(cp2); tc.startup(); 图 26. TabContainer 示例 也提供了纵向的 tab 布局方式;纵向的布局方式是通过设置属性来实现的,如下: 224 清单 28. TabContainer 中 tab 的布局方式示例
    图 27. TabContainer 的 tab 布局方式示例 同样的,如果需要 tab 位于门户的下方,只要设置 tabPosition="bottom"即可。 结束语 Dojo 提供了灵活多样的布局方式,在实际开发过程中,需要根据实际的设计,选择 符合场景的布局控 件,才能使 web 界面更好的用户体验。 使用 Dojo 开发菜单应用 背景介绍 菜单应用是 Web 页面的点睛之笔。当用户在浏览器端右键单击的时候,浏览器会弹 出自带的菜单,显示如“查看源代码”、“复制”、“粘贴”等可用菜单栏。通过使用浏览器自带 的菜单,用户可以方便的进行复制、粘贴等操作。然而很多时候,网站开发人员会考虑禁止 用户通过浏览器自带的菜单进行以上操作,或者是希望用户使用开发人员自定义菜单。一个 简单的自定义菜单如下图所示: 图 1. 自定义菜单 自定义菜单的使用,可以方便用户快速定位到某个操作,增强了用户界面的交互性, 提高用户体验。 225 Dojo 提供的菜单库,除实现了菜单的基本功能外,还加入对弹出式菜单、图标效果、 键盘响应等功能的支持,方便了开发人员的菜单开发过程。本文将首先介绍 Dojo 菜单实 现原理,并从创建最简单右键菜单入手,介绍右键菜单的静态和动态两种菜单创建方式,最 后举例说明如何开发 Dojo 提供的上下文菜单、下拉式菜单、静态菜单三种菜单。 右键菜单实现原理 在默认状况下,用户在浏览器右键单击时,浏览器会触发 document.oncontextmemu 事件,浏览器会采用默认方式对事件进行处理,弹出浏览器 自带的右键菜单。 实现自定义右键菜单的基本原理就是:菜单默认为隐藏;当 document.oncontextmemu 事件触发时,使用 JavaScript 操作菜单节点的 style 属 性,显示该菜单;同时使用 JavaScript 侦听鼠标 onclick 事件,当该事件执行时,判断 鼠标点击位置是否在菜单区域时,若没有,则通过操作菜单的 style 隐藏该菜单。 Dojo 实现右键菜单的方法也是采用了上面的原理,但 Dojo 封装了底层事件的处理 方法,开发人员直接使用 Dojo 提供的简单 API 就能实现复杂的菜单。具体实现方式参见 下文。 Dojo 菜单使用 包括右键菜单在内,Dojo 提供了三种类型菜单:上下文菜单(右键菜单和弹出式菜 单)、下拉式菜单、静态菜单。由于其他菜单使用和右键菜单使用方式基本相同,本文将从 创建一个最简单的右键菜单开始讲解,然后分别介绍上述三种菜单的作用及创建方式。 简单右键菜单示例 在 Dojo 支持的上下文菜单、下拉式菜单、静态菜单三类菜单中,使用最为广泛的是 “上下文菜单”中的右键菜单,一个最简单的右键菜单如下图所示: 图 2. 简单右键菜单 226 用户在“Please Right-click On Me!”上右键单击,即可看到由 Cut、 Copy、 Paste 纵向三栏构成的右键菜单。可以看到,使用 Dojo 创建的“右键菜单”比较漂亮而且 符合用户的使用习惯,下面采用“静态创建”和“动态创建”两种方式实现上述菜单: 静态创建菜单 与 Dojo 静态创建其他 Widget 类似,如果希望一个实体实现菜单的效果,需要在 实体的标签里面加上 dojoType=” dijit.Menu” 属性。 静态创建菜单一般需要如下完整的步骤: 导入所需的 JavaScript 和 CSS 文件后,导入 Dojo 所需要的 dijit.Menu 、 dijit.MenuItem 等模块。 静态创建菜单 Widget 及菜单的各个菜单项 Widget 将该菜单 Widget 静态绑定到现有的 DOM 节点。 清单 1. 静态创建菜单 Menu Learn 227
    Please Right-click On Me!
    dijit.Menu 是 Dojo 中菜单 Widget 的一种,可以理解为是菜单菜单项的容器,一 个 dijit.Menu 通常有若干 dijit.MenuItem 组成,每一个 dijit.MenuItem 即为一条菜 单项。 dijit.Menu 的 targetNodeIds 属性表示与该 Menu 绑定的目标 DOM 节点,即在 该 DOM 节点上右击才会出现右键菜单。contextMenuForWindow 属性表示是否只有在 窗体的任何地方右键单击才会打开菜单,如果该值为 true,用户在窗体的任何地方右击都 会弹出该菜单,若该值为 false,只有在 targetNodeIds 对应的节点上右击才会弹出菜单。 同时,因为右键菜单的在用户右键单击前是不显示的,因此该 Menu Widget 的 style 中 display 属性为 none。 228 动态创建菜单 在清单 1 中,通过在一些实体的标签里面加上相应的 Dojo 标签属性实现了 Menu Widget 创建。这种静态实现 Menu Widge 的方法简单明了。然而某些情况下, 需要根据一些实际情况动态的生成 Menu Widge,或者动态的修改 Menu Widget 的某 些属性。下面代码就是动态实现上述简单右键菜单的方法: 清单 2. 动态创建菜单 Menu Learn
    Please Right-click On Me!
    可以看到,与 Dojo 动态创建普通的 Widget 类似,创建 dojo.menu 的过程也可 分为三步: 导入所需的 JavaScript 和 CSS 文件后,导入 Dojo 所需要的 dijit.Menu、 dijit.MenuItem 等模块。 动态创建菜单 Widget,将该菜单 Widget 动态绑定到现有的某个目标 DOM 节点。 启动菜单。 需要特别注意的是: 在动态创建 dijit.Menu 的时候,dijit.Menu 的 targetNodeIds 属性是一个对象数组,而非特定的对象。 230 上下文菜单 上下文菜单是最常见的菜单,一般会结合上下文环境使用,该菜单典型的应用是右键 菜单和弹出式菜单。上章节设计的菜单即为最简单的右键菜单,而稍微复杂的上下文菜单都 会有键盘响应、图标效果显示、自定义快捷键、分隔符、弹出式菜单、禁用菜单项、复选式 菜单项等功能: 图 3. 上下文菜单 上图所示:键盘响应指用户可以通过 Dojo 已定义的快捷键对菜单进行操作,如使用 “空格键”弹出子菜单;图标效果如上述菜单的“Cut”栏剪刀图标效果所示,而自定义快捷键 则如“Cut”栏对应的 “Ctrl + X”快捷键;分隔符的作用如“Paste”栏下面的横线,将不同栏 目组分隔开;“Paste”栏底色为灰色,即使点击也不会触发任何时间,即为禁用菜单栏功能; 弹出式菜单则是菜单中最经常用到的功能,用户点击 Popup Menu 时会弹出下一级菜单; 而复选菜单的效果则如“Checked”栏所示,用户可以通过点击复选框表示选中该栏或取消 选择。 下面的代码实现了上述功能: 清单 3. 上下文菜单 下面就各个功能对上述代码进行讲解: 键盘响应 键盘响应的功能是不需要开发人员实现的,Dojo 创建的 Menu 自身就已经具备了键 盘响应的功能,Dojo 提供的键盘响应有: 表 1. 键盘响应 功能 快捷键 打开上下文菜单 Windows:shift-f10 或者是在 FireFox 浏览器上右击 Macintosh: ctrl-space Safari 4 或 Mac: VO+shift+m (VO 一般是指 control+opton 组合键 ) 遍历菜单 ↑、↓方向键 弹出子菜单 空格、回车或是→方向键 关闭上下文菜单,或关闭当前子 菜单返回上级菜单 Esc 或者←方向键 关闭上下文菜单和所有子菜单 Tab 键 图标效果显示 dijit.MenuItem 的 iconClass 属性表示了菜单项而使用的 CSS,当菜单项引入该 CSS 后,该菜单项会添加图标效果。Dojo 提供了如 dijitEditorIconCut、 dijitEditorIconCopy、dijitEditorIconPaste 等图标效果的 CSS 类。 233 自定义快捷键 dijit.MenuItem 的 accelKey 属性表示该菜单项对应的快捷键。需要特别注意的是: 尽管菜单项上可以显示该快捷键文本,如上图的第一栏右边显示有“Ctrl+X”,然而当前 Dojo 版本 (1.4) 并没有提供捕捉和执行该快捷键事件的机制,即即使用户键盘输入 “Ctrl+X”,也不会触发剪贴事件。 分隔符 dijit.MenuSeparator 表示菜单菜单项之间的线,用于分割各个菜单项。 弹出式菜单 如果想使用弹出式菜单,会需要如下的代码结构: 清单 4. 弹出式菜单
    Popup Menu 其中,PopupMenuItem 作用类似于 MenuItem,但是它可以显示下一级菜单或者 其他 Widget。一般 PopupMenuItem 都会有两个子节点:显示该菜单项内容的静态文 本的标签(一般是写在 span 里)和一个需要显示的 Widget,该 Widget 一般是 dijit.Menu,也可以是 dijit.ColorPalette(颜色选择框)等 Widget。 禁用菜单项 dijit.MenuItem 的 disabled 属性表示该菜单项是否可用,该属性默认值为“flase”, 表示可用;如果该属性为 true,则该菜单项被禁用,即使点击该菜单项也不会触发点击事 件。 复选菜单项 dijit.CheckedMenuItem 表示复选菜单项,其 checked 属性标识了该菜单项是否 被选中。checked 属性的默认值为 false,即未被选中,每次用户点击该菜单项,就会触 发选中 / 取消选中的事件,菜单项状态就会在“checked”和“unchecked”之间进行切换。 用户可以定义 onchange 函数,用于处理选中 / 取消选中该菜单项事件,onchange 函 数接受的第一个参数即为 checked 属性的值。 234 需要说明的是,以上功能并非只有在“上下文菜单”中才有,“下拉式菜单”和“静态菜单 栏”都具备相同的功能,使用的方法也一样。 下拉式菜单 下拉式菜单指的是点击某个按钮或者菜单项时,会纵向下拉弹出的菜单。Dojo 提供 的下拉式菜单一般会绑定到 dijit.form.ComboButton,dijit.form.DropDownButton 或 dijit.MenuBar Widget 上,点击这些 Widget 或 Widget 的菜单项时,会弹出下拉 式菜单。以 dijit.MenuBar 为例:MenuBar Widget 是经常用到的 Widget,它模拟实 现了一个典型的菜单条,横向列出若干菜单选项,当点击某个菜单项时,会下拉弹出子菜单 或其他 Widget。如下图所示: 图 4. 下拉式菜单 上述功能可由清单 5 实现: 清单 5. 下拉式菜单 一个 dijit.MenuBar Widget 由多个 dijit.PopupMenuBarItem 或 dijit.MenuBarItem Widget 组成: 示例的“File”菜单项就是由 dijit.PopupMenuBarItem Widget 实现的,当鼠标点击 该菜单项时,菜单项会弹出一个子菜单或其他 Widget。同上下文菜单的 dijit.PopupMenuItem 类似,一个 dijit.PopupMenuBarItem Widget 会包含两个子节 点:显示静态文本的标签(一般是写在 span 里)和一个需要显示的 Widget,该 Widget 一般是 digit.Menu Widget。 dijit.MenuBarItem 也是菜单条的菜单项,它不支持下拉弹出 digit.Menu 或其他 Widget。与 dijit.PopupMenuBarItem 不同,当在该 dijit.MenuBarItem Widget 单 击时,会触发 onclick 函数,这点可以通过“Empty”和“Please!”菜单项得到验证: 当点击“Empty”和“Please!”菜单项时,都不会弹出下拉菜单,两者显示效果看起来一 样,但实际触发的事件却不同,观察 firebug 控制台,可以发现:“Empty”菜单栏被单击 后,并没有向控制台进行输出,即并没有真正执行 onclick 函数;而“Please!”菜单栏被单 击后,则向控制台进行输出。 静态菜单 静态菜单是静态定为到窗体某个位置的菜单,如下图所示: 图 5. 静态菜单 237 静态菜单与上下文菜单的显示效果是一样的,然而,静态菜单会在网页加载完后固定 显示于窗体某个位置,并且不会像上下文菜单一样会因鼠标事件的发生而消失或显示,它的 典型应用为作为导航菜单显示在窗体的左侧,用户可以根据其菜单项进行信息的过滤和查 找。 实现上述功能菜单的代码为: 清单 6. 静态菜单
  • 还剩253页未读

    继续阅读

    下载pdf到电脑,查找使用更方便

    pdf的实际排版效果,会与网站的显示效果略有不同!!

    需要 10 金币 [ 分享pdf获得金币 ] 2 人已下载

    下载pdf

    pdf贡献者

    yangzfcool

    贡献于2016-05-05

    下载需要 10 金币 [金币充值 ]
    亲,您也可以通过 分享原创pdf 来获得金币奖励!
    下载pdf