对于组件化的软件工程设计,很多开发人员都比较熟悉。组件化的设计适合于复杂的软件系统和团队协作开发。把软件系统划分成若干个组件,组件之间通过预先定义好的接口和协议进行通讯和协作,共同完成整个软件系统的职责。团队中的开发人员可以各自负责不同的组件。组件化的思想在桌面应用和Web应用后台开发中比较流行,相关的技术和实践都比较成熟。
而在Web应用的前端部分,组件化一直进展得比较缓慢。这其中的原因有很多,最主要的是Web应用的前端在开始的时候比较简单,没有组件化和设计的必要。随着Ajax应用的流行,Web前端部分越发复杂,用户对Web应用的要求不断向桌面应用靠拢。HTML语言的基本界面元素不能单独地满足这样的需求。菜单、树形控件、对话框和进度条等组件,在现在的Ajax应用中十分常见,但是并不是HTML默认提供的。HTML 5规范中引入了一些新的元素,但还是不够。组件化对于Web应用本身的代码共享和团队分工也是很有意义的。
Web 应用前端组件化的发展也是渐进的。开始的时候,只是一些简单的HTML、CSS加上JavaScript的代码示例。比如当需要实现一个多级菜单的时候,就下载相关的代码示例,就根据自己的需要进行修改。这样的组件比较难以复用。后来JavaScript框架开始流行的时候,有些框架本身就提供了组件的支持,包括Ext JS、jQuery UI和Dojo等。不过不同框架提供的组件模型不尽相同。
Dijit组件模型概述
Web 应用的前端组件的定义比较宽泛。一个组件占据Web页面上的某个区域,并负责完成某项具体的任务。Web组件有时候也被称为小部件(widget)。在 Dijit组件模型中,一个Dijit组件是一个JavaScript类,可以在页面上通过new操作符来创建组件的实例。每个组件实例都需要与页面上的某个DOM元素绑定在一起。这个DOM元素就是该组件的根节点。在Dijit组件的逻辑中,就可以对该根节点进行操纵来构建用户界面。组件 JavaScript类暴露出来的属性和方法就是该组件的接口。
Dijit组件的使用
Dijit 组件的使用方式非常简单。首先需要在页面上加载组件的JavaScript代码,这通过dojo.require函数就可以完成。接着在页面上找到或创建一个DOM元素作为该组件的根节点。最后通过new操作符创建即可。如new dijit.form.ComboBox({}, node)就可以用node作为根元素创建一个dijit.form.ComboBox组件,即一个下拉列表选择框。可以看到创建Dijit组件的时候,使用了两个参数:第二个参数是组件的根元素,如果创建的时候不指定该根元素,会自动创建一个新的DIV元素作为根元素。
不过该新创建的根元素一般没有加入到当前的文档树中,可以通过组件的placeAt方法来设置该组件在页面文档树中的位置。第一个元素则是一个JavaScript对象,包含了组件的配置属性。通常来说,一个Dijit组件是可以复用的。因此一般都会提供一些属性供使用者进行配置。通过这个参数,就可以修改这些配置。
上面提到的是程序式的方式创建Dijit组件,还有另外的一种方式来进行创建,即通过在HTML代码中以声明式的方式创建,如
<div dojoType="dijit.Dialog" id="myDialog" title="示例对话框">
<h3>对话框标题</h3>
<div>对话框内容</div>
</div>
声明式的方式在一定程度上简化了开发人员使用Dijit组件的方式。声明式的方式与编写HTML代码的形式类似,只需要在一般的HTML元素上添加一些额外的属性就可以把HTML片段转换成Dijit组件。这对于只熟悉HTML语言的人来说非常方便,相当于在HTML语言的基本元素之上,增加了更多的可用组件。
Dijit深入分析
Dijit组件基本类
所有的Dijit组件都继承自dijit._Widget类。dijit._Widget类中定义了与组件相关的一系列方法。这些方法中有一些是与组件生命周期相关的,有一些则是所有组件都需要的通用方法。了解Dijit组件的生命周期有利于理解Dijit组件的运行方式,从而更好的使用已有的组件或开发出自己的组件。
创建Dijit组件的过程开始于dijit._Widget类中的create方法。create方法采用了典型的模板方法设计模式,即在该方法中封装了创建组件的基本流程。该方法会执行一些重要的操作,并依次调用其它的方法来完成整个创建过程。具体的流程包括:
- 把创建时的配置参数混入(mix-in)到组件中。比如在创建组件的时候使用的方式是var myWidget = new TestWidget({prop : "Hello"}, node);,那么在创建完成之后就可以通过myWidget.prop来获取到"Hello",在组件中也可以通过this.prop来访问。
- 调用生命周期方法:postMixInProperties。该方法在配置参数混入之后调用,可以对混入的参数进行修改。
- 把该新创建出来的组件添加到全局的组件对象注册表中。Dijit组件都会被分配一个惟一的标识符。添加到注册表中之后,就可以用dijit.byId来根据标识符获取组件对象。
- 调用生命周期方法:buildRendering。该方法用来完成构建组件的用户界面。该方法负责设置this.domNode的值,表示的是创建完成的组件的根元素。
- 调用生命周期方法:postCreate。该方法在用户界面构建完成之后被调用。一般是组件内部行为逻辑的起点,类似HTML页面中的onload方法。
对于Dijit组件开发人员来说,创建一个新的Dijit非常简单。只需要用dojo.declare声明一个JavaScript类并继承自 dijit._Widget,在该类中覆写感兴趣的JavaScript方法即可。最简单的情况是覆写postCreate方法并添加组件的逻辑。
对于用来包含其它子组件的容器类组件来说,一般会覆写startup方法来让其调用者显式的启动这个组件。这是因为在postCreate被调用的时候,只是保证了组件的DOM节点已经被创建成功了,但是这些DOM节点可能并没有被添加到当前文档树中,因此不能在postCreate中包含与DOM节点大小和位置相关的代码。如果要添加这样的代码,应该在startup中添加。很多容器类组件都使用该方法来对其子节点进行布局。
使用HTML模板
如果只是使用dijit._Widget的话,编写Dijit组件会比较繁琐。比如在构建用户界面的时候,可能会需要很多的DOM操作,编写起来也不方便。 Dijit提供了dijit._Templated用来使用HTML片段来定义组件的内容。HTML片段是作为组件的内容模板。如:
dojo.declare("TempWidget", [dijit._Widget, dijit._Templated], {
templateString : "<div><span>Hello</span></div>"
});
TempWidget继承了两个JavaScript类,除了必需的dijit._Widget之外,还有dijit._Templated的。需要保证 dijit._Widget是父类数组的第一个元素,只有它是真正意义上的父类,其余的是混入类。dijit._Templated类已经覆写了 buildRendering方法来从HTML模板中创建组件内容的DOM元素,并作为组件的this.domNode的值。在HTML模板中,除了可以使用基本的HTML元素和属性之外,还有一些附加的实用功能:
- 在HTML模板中直接引用组件中的属性。比如组件中有个属性叫title,在HTML模板中想引用该属性的值,可以直接写<span>${title}</span>。如果属性title的值是"Hello",那么上述模板在运行时刻会变成<span>Hello</span>。
- 通过dojoAttachPoint来声明在组件对象中可见的DOM节点。当需要在组件中引用某个内部的DOM节点时,不需要再次进行查询,通过 dojoAttachPoint即可。如声明<div><span dojoAttachPoint="myNode"></span></div>,就可以在组件对象中通过 this.myNode来引用该SPAN元素。
- 通过dojoAttachEvent来进行事件绑定。这种方式比先手工查询DOM节点,再通过dojo.connect来绑定要简单得多。如声明<div><button dojoAttachEvent="onclick:test">Test</button></div>,就意味着将组件的test方法绑定到按钮的onclick事件上。
dijit._Templated的模板机制的这些实用功能减少了构建用户界面时的一些繁琐代码。
作为容器
如果组件是作为其它组件的容器来使用的话,就可以混入dijit._Container类。该类提供了对子组件的基本管理功能,包括查询、添加和删除等。使用该类的时候,需要在组件中声明一个containerNode的属性作为子组件的父节点。创建出Dijit组件之后,就可以通过addChild方法来添加子组件了。
销毁过程
组件在创建并运行之后,就可能需要被销毁。销毁一个Dijit组件很简单,只需要调用destroyRecursive方法即可。该方法会负责销毁当前Dijit组件及其包含的子组件。当一个组件被销毁的时候,其uninitialize方法会被调用,类似于析构函数。因此可以把组件特有的销毁逻辑添加在uninitialize方法中。
Dijit组件的接口与交互
前面提到,组件之间通过设计好的接口和协议进行通讯。对于Dijit组件来说,它所提供的接口一般有下面这几类:
- 公开的属性和方法。这些属性和方法类似于Java类中的公开的域和方法,在获取到组件对象之后可以直接使用。
- 通过dojo.connect进行连接。有些组件提供了一些占位符方法用来允许其使用者监听其内部状态的变化,类似于DOM事件的处理。
当组件之间进行通讯和协作的时候,一般有下面几种交互的模式:
- 传递组件对象的引用。这种做法一般是在创建新组件的时候,将其需要引用的组件对象传递进去,如var anotherWidget = new MyWidget({parent : oneWidget}, node);。
- 不传递对象引用,而是进行查找。这种情况适用于所依赖的组件的ID已知的情况。可以通过dijit.byId来直接进行查找。
- 使用Dojo提供的全局通讯机制:dojo.publish和dojo.subscribe。一个组件通过dojo.publish来发布消息,另外一个组件则通过dojo.subscribe来监听相关的消息并做出处理。
一般来说,比较推荐的做法是第一种,即通过传递组件对象的引用来完成。不过当组件之间的关系比较复杂的时候,有可能需要将一个对象的引用进行多次传递。这个时候也可以考虑后两种做法。
Dijit开发最佳实践
编写Dijit组件并不是一件复杂的事情,只需要按照一般的流程依次完成即可。不过Dijit组件本身的设计和实现比较复杂,包含了比较多的内容。下面对一些重点的地方进行讨论。
编程式和声明式的创建方式选择
这两种方式的区别只是在于开发人员的使用方式上。用声明式方式声明的Dijit组件,在运行时刻也是通过程序式的方式来进行创建的,由dojo.parser.parse方法来完成。因此,声明式的方式更像一种语法糖衣。不过声明式方式的一个好处是可以实现优雅地退化(graceful degradation),即当JavaScript不被支持的时候,仍然可以在页面上显示出部分内容。
对于简单的和容器类的组件来说,声明式创建的方式比较好。简单的组件用声明式的方式比较简洁。而容器类的组件在创建的时候一般都需要指定所包含的内容。使用声明式的时候,组件的子节点会自动作为容器类组件的子组件来添加。而如果以程序式的方式来完成的话,需要手工创建子组件,并通过addChild方法来逐个添加。代码会比较繁琐。
组件状态变迁与外观样式
在开发Dijit组件中,经常会遇到的一个场景是根据组件内部的状态变化改变其外观样式。比如对于一个单选按钮组件来说,选中和未被选中的外观样式是不同的。典型的做法是通过切换不同的CSS样式名称来转换外观。比如未被选中适合的CSS样式名称可能是MyRadioButton,被选中之后则变成 MyRadioButtonChecked。Dijiti提供了dijit._CssStateMixin混入类来抽象这种行为。
开发人员的组件只需要继承此类,并通过属性this.baseClass 设置基础的CSS样式名称,就具备根据状态的变化动态修改CSS样式名称的能力。比如设置了baseClass为myWidget,当鼠标移动到该组件上的时候,其根元素的CSS样式名称会自动变成myWidgetHover。该类所支持的状态变化包括鼠标进入/离开、焦点获取/失去、选中/未选中、启用 /禁用等。