对于组件化的软件工程设计,很多开发人员都比较熟悉。组件化的设计适合于复杂的软件系统和团队协作开发。把软件系统划分成若干个组件,组件之间通过预先定义好的接口和协议进行通讯和协作,共同完成整个软件系统的职责。团队中的开发人员可以各自负责不同的组件。组件化的思想在桌面应用和Web应用后台开发中比较流行,相关的技术和实践都比较成熟。而在Web应用的前端部分,组件化一直进展得比较缓慢。这其中的原因有很多,最主要的是Web应用的前端在开始的时候比较简单,没有组件化和设计的必要。随着Ajax应用的流行,Web前端部分越发复杂,用户对Web应用的要求不断向桌面应用靠拢。HTML语言的基本界面元素不能单独地满足这样的需求。菜单、树形控件、对话框和进度条等组件,在现在的Ajax应用中十分常见,但是并不是HTML默认提供的。HTML 5规范中引入了一些新的元素,但还是不够。组件化对于Web应用本身的代码共享和团队分工也是很有意义的。
Web 应用前端组件化的发展也是渐进的。开始的时候,只是一些简单的HTML、CSS加上JavaScript的代码示例。比如当需要实现一个多级菜单的时候,就下载相关的代码示例,就根据自己的需要进行修改。这样的组件比较难以复用。后来JavaScript框架开始流行的时候,有些框架本身就提供了组件的支持,包括Ext JS、jQuery UI和Dojo等。不过不同框架提供的组件模型不尽相同。
Web 应用的前端组件的定义比较宽泛。一个组件占据Web页面上的某个区域,并负责完成某项具体的任务。Web组件有时候也被称为小部件(widget)。在 Dijit组件模型中,一个Dijit组件是一个JavaScript类,可以在页面上通过new操作符来创建组件的实例。每个组件实例都需要与页面上的某个DOM元素绑定在一起。这个DOM元素就是该组件的根节点。在Dijit组件的逻辑中,就可以对该根节点进行操纵来构建用户界面。组件 JavaScript类暴露出来的属性和方法就是该组件的接口。
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._Widget类。dijit._Widget类中定义了与组件相关的一系列方法。这些方法中有一些是与组件生命周期相关的,有一些则是所有组件都需要的通用方法。了解Dijit组件的生命周期有利于理解Dijit组件的运行方式,从而更好的使用已有的组件或开发出自己的组件。
创建Dijit组件的过程开始于dijit._Widget类中的create方法。create方法采用了典型的模板方法设计模式,即在该方法中封装了创建组件的基本流程。该方法会执行一些重要的操作,并依次调用其它的方法来完成整个创建过程。具体的流程包括:
对于Dijit组件开发人员来说,创建一个新的Dijit非常简单。只需要用dojo.declare声明一个JavaScript类并继承自 dijit._Widget,在该类中覆写感兴趣的JavaScript方法即可。最简单的情况是覆写postCreate方法并添加组件的逻辑。
对于用来包含其它子组件的容器类组件来说,一般会覆写startup方法来让其调用者显式的启动这个组件。这是因为在postCreate被调用的时候,只是保证了组件的DOM节点已经被创建成功了,但是这些DOM节点可能并没有被添加到当前文档树中,因此不能在postCreate中包含与DOM节点大小和位置相关的代码。如果要添加这样的代码,应该在startup中添加。很多容器类组件都使用该方法来对其子节点进行布局。
如果只是使用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元素和属性之外,还有一些附加的实用功能:
dijit._Templated的模板机制的这些实用功能减少了构建用户界面时的一些繁琐代码。
如果组件是作为其它组件的容器来使用的话,就可以混入dijit._Container类。该类提供了对子组件的基本管理功能,包括查询、添加和删除等。使用该类的时候,需要在组件中声明一个containerNode的属性作为子组件的父节点。创建出Dijit组件之后,就可以通过addChild方法来添加子组件了。
组件在创建并运行之后,就可能需要被销毁。销毁一个Dijit组件很简单,只需要调用destroyRecursive方法即可。该方法会负责销毁当前Dijit组件及其包含的子组件。当一个组件被销毁的时候,其uninitialize方法会被调用,类似于析构函数。因此可以把组件特有的销毁逻辑添加在uninitialize方法中。
前面提到,组件之间通过设计好的接口和协议进行通讯。对于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。该类所支持的状态变化包括鼠标进入/离开、焦点获取/失去、选中/未选中、启用 /禁用等。
感谢张凯峰的策划以及审校。