通过对本系列文章的前六个部分的学习,你已经对 Dojo 的强大功能有所了解了。但有时候 Dojo 的一些功能并不能完全满足实际的要求,这时就需要对 Dojo 进行扩展了,比如可以对 Widget 进行扩展,使它以更加符合项目的要求展现或响应行为。本文将详细介绍 Dojo 的面向对象特性,以及如何在这个特性上开发新的 Dojo 模块,创建新的 Dijit,定义自己个性化的 Widget。
JavaScript 基于原型的继承
JavaScript 是一门基于对象的语言,对象可以继承自其它对象,但是 JavaScript 采用的是一种基于原型的 (prototype based) 的继承机制,与开发人员熟知的基于类的 (class based) 继承有很大的差别。在 JavaScript 中,每个函数对象(实际上就是 JavaScript 中 function 定义代码)都有一个属性 prototype,这个属性指向的对象就是这个函数对象的原型对象,这个原型对象也有 prototype 属性,默认指向一个根原型对象。如果以某个特定的对象为原型对象,而这个对象的原型对象又是另一个对象,如此反复将形成一条原型链,原型链的末端是根原型对象。JavaScript 访问一个对象的属性时,首先检查这个对象是否有同名的属性,如果没有则顺着这条继承链往上找,直到在某一个原型对象中找到,而如果到达根原型对象都没有找到则表示对象不具备此属性。这样低层对象仿佛继承了高层对象的某些属性。下面通过一个例子说明基于原型的继承是如何工作的。
清单 1. 基于原型的继承
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,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. 对象图
绿颜色框表示的函数对象;蓝颜色框代表的是原型对象,每个函数对象都有一个原型对象,如 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 的继承中,子类对象在自己的构造函数中可以调用父对象的构造函数。所以在清单 1 中又定义了一个 construct 方法来完成属性的初始化,
为了解决上述问题,Dojo 对 JavaScript 已有的 prototype based 的继承机制进行了包装,使其更容易理解,使用。在 Dojo 中可以使用 Dojo.declare 函数来定义普通类,单继承的类,甚至是多重继承的类(虽然笔者认为 dojo.declare 定义的只是对象,在 Dojo 的官方文档中把 Dojo.declare 定义为声明为类的函数,所以这里也采用这一定义),一切都在 Dojo.declare 中。同样我们通过一个例子来说 dojo.declare 是如何工作的。
清单 2 dojo.declare
这个例子与清单 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,sans-serif; margin-top:0px; margin-bottom:0px; padding:0.3em 5px 0.7em; font-size:0.76em"> name:Apache,weight:200,speed:200,propellers:3,sans-serif; margin-top:0px; margin-bottom:0px; padding:0.3em 5px 0.7em; font-size:0.76em"> name:X2,speed:400,seats:3,propellers:4,lifelong:10,sans-serif; margin-top:0px; margin-bottom:0px; padding:0.3em 5px 0.7em; font-size:0.76em"> jh1 is instance of JetPlane:true
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 开发出漂亮的应用。
在本系列文章的第一部分就提到,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 函数,但会把相应的 js 文件载入,上例中所对应的 js 文件是 <DOJO HOME>/dijit/form/Button.js。如果所引入的包还依赖于其它包,dojo.require 也会把所依赖的包载入。如果所要求的包已经载入,dojo.require 不会重复载入,它保证所有了包只会被载入一次。
在 Dojo 中,定义一个新的模块是很容易的。我们来看一个简单的例子,假设我们要创建的新模块是 util.math.Calculator。先在 Dojo 安装目录下创建目录 util/math,如图 3 所示:
图 3. Dojo 目录结构
在目录 util/math 下,创建一个叫 Calculator.js 的文件,在该文件中写入清单 4 所示的代码。
清单 4. Calculator.js
现在你就可以开始使用这个新的模块了,代码如清单 5 所示。
清单 5. 使用新模块
在清单 4 中,出现了 dojo.provide 和 dojo.declare 函数。dojo.provide 的功能是向 dojo 模块注册表中注册一个新的模块,dojo.declare 则是用来声明模块中的类。通过这个例子可以看出,在 Dojo 中创建一个新的模块是非常简单的。现在让我们来对清单 4 中的代码作些扩展,在 Calculator.js 中加入清单 6 中的代码。
清单 6. 扩展 Calculator
新的测试代码如清单 7 所示:
清单 7. 测试代码
通过这个小修改,我们可以发现很多有趣的现象。第一,在一个模块中,不但可以定义 Dojo 类,还可以定义一个普通的函数,如本例中的 util.math.Calculator.subtract,这与 java 中的包是不一样的,java 的包中只能定义类;第二,在一个模块中,可以定义多个 Dojo 类,如本例中的 util.math.Calculator2 与 a.b,这与 java 的包类似;第三,模块的名字与 Dojo 中类的命名实际上没有必然的联系,两者在语法上并没有一致性的要求,但从代码的可维护性来考虑,建议保持模块名字与实际的类名一致。
现在你应该已经掌握了 Dojo 的模块的概念以及如何创建自己的模块,让我们来进一步看看 Dijit 的扩展。虽然 Dojo 提供的丰富的 Dijit 库,但有时候还是很难完全满足项目中的一些特殊需求。这种时候,你可能会很快找到 Dijit 的源文件,然后在上面做些修改,以满足项目需求,但这并不是个好方法,因为这会使源代码很难维护。好在 Dojo 也提供了很好的 Dijit 扩展机制,使得开发人员可以创建自己的 Dijit.
也许你已经想到了 Dijit 的扩展也模块的扩展应该非常类似,不错,因为实际上 Dijit 也是模块,只不过 Dijit 带有 UI,可以在界面上展示。所以 Dijit 的扩展也就是在模块扩展的基础上,加了一些规则。Dijit 扩展中一个最重要的概念就是 Template,Template 是一段带有可替换标签的 html 代码,当 Dijit 实例化时,Dojo 便把可替换标签替换为相应的属性值,然后把这段 html 代码插入到 DOM 树中。
为了更好地讲解,我们先来看一个例子。
我们想创建这么一个 Dijit,它可以展示一句话,并悬停在屏幕的右上角,用户也可以把这个 Reminder 关闭。现在 Dojo 的安装目录下创建这个目录:<Dojo home>/ibm/dijit/templates。把清单 8 中的 hmtl 片断存为 Reminder.html,放到刚创建的目录中。
清单 8. Reminder.html
把清单 9 中的 css 片断保存为 Reminder.css,同样放到刚创建的目录中。
清单 9. Reminder.css
template 文件已经准备好了,现在让我们来看看 Dijit 定义文件,代码如清单 10 所示,把它保存为 Reminder.js,放到目录 <Dojo home>/ibm/dijit 中。
清单 10. Reminder.js
到这,一个完整的新 Dijit 已经创建好了,让我们来看看效果,把清单 11 中代码保存为 newwidget.html,放到 <Dojo home> 目录下。
清单 11.newwidget.html
在 IE 或 Firefox 中打开该文件,你会看到如下力所示的效果。
图 4 . 运行结果
现在我们来详细分析一下上面的例子。这个 Reminder Dijit 共由三个文件组成,其中 css 文件不是必须的,Reminder.html 是这个 Dijit 的模板文件。
大部分的的 Dijit 都会有一个模板,这个模板可以独立出来放到一个文件中,就像本例中一样,也可以直接以字符串的形式放到该 Dijit 的模块文件中。这两种形式在格式和作用上是完全一样的,但前一种方式显然会更好一些,因为前一种方式实现了 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="…"
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
清单 13 . 事件处理
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 都被创建出来了,你可以调用这个函数。
destroy:
会在 dijit 被销毁时被调用,你可以在这里进行一些资源回收的工作。
通过 Dijit.Declaration 来创建新 Dijit
上面所说的都是能过编程的方式来创建一个新的 dijit,这种方式很灵活,你可以很方便的控制一切。但这种方式比较麻烦,有时候你想创建的 dijit 也许非常简单,这种情况下,你可以采用另一种方式来创建 dijit,就是用 Dijit.Declaration 来声明一个 dijit,如清单 14 所示。
清单 14 . 用 Dijit.Declaration 创建 Dijit
本文介绍了 Dojo 与面向对象的关系,Dojo 的模块化原理以及两种开发自己的 dijit 的的方式。通过本文的学习,你应该可以根据自己的需要,对 Dojo 进行扩展了。