写点什么

使用 Dijit 实现界面组件化开发

  • 2011 年 1 月 04 日
  • 本文字数:4721 字

    阅读完需:约 15 分钟

对于组件化的软件工程设计,很多开发人员都比较熟悉。组件化的设计适合于复杂的软件系统和团队协作开发。把软件系统划分成若干个组件,组件之间通过预先定义好的接口和协议进行通讯和协作,共同完成整个软件系统的职责。团队中的开发人员可以各自负责不同的组件。组件化的思想在桌面应用和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 模板中想引用该属性的值,可以直接写${title}。如果属性 title 的值是"Hello",那么上述模板在运行时刻会变成Hello
  • 通过 dojoAttachPoint 来声明在组件对象中可见的 DOM 节点。当需要在组件中引用某个内部的 DOM 节点时,不需要再次进行查询,通过 dojoAttachPoint 即可。如声明
    ,就可以在组件对象中通过 this.myNode 来引用该 SPAN 元素。
  • 通过 dojoAttachEvent 来进行事件绑定。这种方式比先手工查询 DOM 节点,再通过 dojo.connect 来绑定要简单得多。如声明
    ,就意味着将组件的 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。该类所支持的状态变化包括鼠标进入 / 离开、焦点获取 / 失去、选中 / 未选中、启用 / 禁用等。

参考资料


感谢张凯峰的策划以及审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2011 年 1 月 04 日 17:584835

评论

发布
暂无评论
发现更多内容

架构师训练营 Week13 - 数据应用(2) 总结

极客大学架构师训练营

第九周大作业

小兵

以满天星辰为灯塔,照亮百度“大航海计划”背后的AI棋局

脑极体

第 9 周 系统架构总结

心在那片海

第四周作业

胡益

第四周学习总结

胡益

计算机的时钟(五):混合逻辑时钟

ElvinYang

PageRank算法

天天向上

极客大学架构师训练营

架构师训练营 Week13 - 课后作业

极客大学架构师训练营

第十三周作业

Meow

极客时间架构 1 期:第 13 周 数据应用(二) - 学习总结

Null

第九周作业

Jack

week13学习总结

追风

架构师一期

第 9 周 系统架构作业

心在那片海

第十三周学习笔记

Meow

【第十三周】数据应用(二)

云龙

架构训练营第九周作业

一期一会

大数据应用

garlic

极客大学架构师训练营

架构1期 第十三周作业

haha

极客大学架构师训练营

大数据技术与应用

天天向上

极客大学架构师训练营

数据应用(二)课后练习

ABS

架构师训练营第一期 - 第十三周课后作业

卖猪肉的大叔

极客大学架构师训练营

极客时间架构 1 期:第 13 周 数据应用(二) - 命题作业

Null

第九周总结

孤星

第十三周 数据应用(二)总结

钟杰

架构师训练营 2 期 - 第九周总结

Geek_no_one

极客大学架构师训练营

【第十三周】课后作业

云龙

第九周作业

孤星

第十三周 作业1

Yangjing

极客大学架构师训练营

架构师知识体系

天天向上

极客大学架构师训练营

架构入门感悟之九

莫问

数据cool谈(第2期)寻找下一代企业级数据库

数据cool谈(第2期)寻找下一代企业级数据库

使用Dijit实现界面组件化开发-InfoQ