使用 Dojo 的 Ajax 应用开发进阶教程,第 8 部分: Dijit 开发最佳实践

来源:百度文库 编辑:神马文学网 时间:2024/04/26 10:58:49
Dijit 组件(widget)是 Dojo 提供的图形用户界面组件库。它提供了 Ajax 应用开发中会用到的常用组件,可以帮助开发人员快速的构建 Ajax 应用。本文并不会介绍 Dojo 默认提供的组件,而是侧重于介绍 Dijit 组件的编程模型和最佳实践,其目的是帮助开发人员更好的开发自己的 Dijit 组件。下面首先对 Dijit 做概要介绍。
Dijit 组件的存在是 Dojo 框架区别于其它 JavaScript 框架的一个重要特性。在桌面应用开发中,开发人员大量使用图形用户界面组件库来提高开发效率。而在 Web 应用开发中,HTML 语言本身仅提供了少数基本的控件,如按钮、单选框、复选框、文本输入框和下拉列表等。而对于在 Web 应用开发中常见的一些复杂组件,如对话框、菜单、工具栏、进度条、富文本编辑器和树等,并没有提供原生的支持。在这种情况下,开发人员往往需要自己开发这样的复杂组件,这就造成了更长的开发周期和更高的开发和维护的成本。
Dojo 提供了一个种类多样的组件库。开发人员只需要简单的定制就可以在自己的应用中使用这些组件。除此之外,Dojo 还提供了完善的组件编程模型。如果默认提供的组件都不能满足需求,可以自己来开发所需的组件。遵循这个统一的编程模型,会比从头开始创建组件要容易得多。有了组件的概念之后,开发人员在设计 Web 应用的时候,就可以从比较高的抽象层次来对应用的各个部分进行划分。定义好清晰的组件接口之后,团队成员就可以各司其职,并行开发,从而提高开发效率。
在开发 Dijit 组件的时候,需要注意下面几个基本的问题。 组件的粒度问题。一般来说,功能比较复杂的组件不利于复用,也不利于团队开发时的分工合作。但是过多小组件在页面上的时候,会消耗比较多的系统资源,影响性能。而性能对 Web 应用来说是一个非常重要的因素。因此需要进行一定的权衡。比较好的做法是从较大的组件开始,当发现存在代码重复的时候,再把重复的代码提取出来,重构成新的组件。这样就把划分成小组件的决策推迟到了真正需要的时候,避免过度设计。
组件的接口问题。组件的接口定义了代码中的其它部分如何使用该组件。一般来说,组件可以提供三类的接口:公共属性、公共方法和事件绑定点。公共属性指的是组件提供的可以公开访问的简单数据类型属性。一般在创建组件的时候使用,用来对组件进行定制;公共方法指的是可以公开访问的 JavaScript 方法。一般在组件创建完成之后使用,用来改变组件的行为;事件绑定点是组件暴露出来的占位方法。一般由组件使用者通过 dojo.connect()来绑定到该方法上。组件使用该方法来通知使用者其内部状态的变化。这类方法一般以 on作为名称前缀。开发人员应该根据需要定义合适的接口,避免一些不好的实践。比如公共属性的值在组件创建之后,一般不推荐使用者设置其值。如果设置该属性的值是一个合理的场景的话,最好提供相应的公共方法,并以文档的形式告诉使用者正确的用法。
组件之间的交互问题。组件之间如果需要相互通讯的话,最好使用组件的对象引用来完成。比如某个组件在创建另外一个组件的时候,可以把自己的对象引用作为参数传递给其创建出来的组件。后者就可以使用此对象引用来调用前者的方法。另外一种做法是通过 dojo.publish()和 dojo.subscribe()方法来完成。这种做法的使用比较简单,可以避免层次较深的对象引用传递。不好的地方是组件之间的关联关系不够清晰,也比较难维护。推荐的做法是优先使用第一种方式。
 
上面对开发 Dijit 组件的一些通用问题进行了讨论。下面开始介绍 Dijit 组件的编程模型。在编程模型的介绍过程中,会穿插介绍相关的最佳实践。首先从 Dijit 组件的核心类 dijit._Widget开始。
回页首
dijit._Widget是所有 Dijit 组件的父类。Dijit 默认提供的组件以及自己开发的组件都需要继承自此类。dijit._Widget所提供的方法涉及组件的生命周期、属性设置和获取、事件处理和其它辅助功能等。深入了解该类的这些方法的用法和实现细节,是开发自己的 Dijit 组件的基础。下面分别对 dijit._Widget提供的方法进行分类讨论。
dijit._Widget提供了对组件生命周期的完整管理,包括组件的创建和销毁。Dijit 组件的生命周期管理在实现的时候,使用了模板方法(Template Method)设计模式。dijit._Widget类的 create()方法定义了 Dijit 组件创建时的生命周期的默认模板。该方法会在合适的时机调用模板中包含的其它方法。这些不同的方法构成了组件生命周期中的各个阶段。开发自己的组件的时候,可以覆写其中的某些方法,从而在感兴趣的阶段添加自己的处理逻辑。开发人员也可以覆写 create()方法来提供一套完全不同的生命周期实现。不过这种做法风险太大,不建议使用。绝大多数情况下,覆写默认模板提供的方法就足够了。dijit._Widget中定义的组件生命周期中的创建阶段如图 1 所示。

如图 1 所示,创建 Dijit 组件时的过程中包含如下几个步骤: 以声明式或是编程式的方式创建 Dijit 组件。
创建时传入的参数被混入(mixin)到当前 Dijit 组件对象中。混入完成之后,postMixInProperties()方法被调用。
如果没有为此组件对象提供 ID 的话,则自动生成一个惟一的 ID。把此对象添加到全局的组件注册表中。完成之后,buildRendering()方法被调用。
设置 Dijit 组件的属性。完成之后,postCreate()方法被调用。
当组件被添加到页面上之后,显式调用 startup()方法。
组件创建完成之后,开始正常工作。
 
图 1 中给出了 4 个方法的名称,其中椭圆形中的 postMixInProperties()、buildRendering()和 postCreate()是 dijit._Widget提供的组件生命周期中的扩展点。自己开发的组件应该通过覆写这些方法来实现自己的逻辑。圆角矩形中的 startup()方法并不是创建过程中的一部分,需要在 Dijit 组件创建完成之后显式的调用。下面对这 4 个方法进行详细的说明。 postMixInProperties():在创建 Dijit 组件的时候,可以通过参数来传入一个包含各种属性的 JavaScript 对象。这些属性被混入到 Dijit 组件中,在代码中可以通过 this来引用。当混入完成之后,在创建组件的界面之前,可能还需要执行一些处理。这些处理的逻辑可以添加在 postMixInProperties()方法中。
buildRendering():该方法用来创建 Dijit 组件的用户界面,即 DOM 节点。该方法完成之后,Dijit 组件的 this.domNode指向的是创建完成的 DOM 节点。
postCreate():当 Dijit 组件的 DOM 节点创建完成之后,此方法被调用。需要注意的是,这个时候组件的 DOM 节点可能还没有被添加到当前页面文档树中。
startup():当 Dijit 组件及其子组件被创建完成并添加到当前页面文档树中之后,显式的调用此方法。该方法对于那些包含子组件的 Dijit 组件来说非常有用,可以用来控制子组件和进行布局。startup()方法只需要调用一次即可。调用完成之后,组件的属性 _started的值为 true,可以用来判断 startup()方法是否已经被调用过。
 
与创建 Dijit 组件对应的组件的销毁过程。销毁过程比较复杂,涉及到 5 个方法,如图 2 所示。图 2 中的箭头表示的是方法之间的调用关系。

如图 2 所示,最常用的销毁一个 Dijit 组件的方法是 destroyRecursive()。该方法用来销毁一个 Dijit 组件及其包含的子组件。该方法会首先调用 destroyDescendants()方法来销毁子组件。由于子组件也可能包含自己的子组件,destroyDescendants()也会调用 destroyRecursive()方法来删除其子组件,从而形成一个递归的销毁过程。当子组件销毁完成之后,destroyRecursive()会调用 destroy()方法来销毁自己。destroy()方法会首先调用 uninitialize(),接着执行内部的清理工作,最后调用 destroyRendering()方法来销毁组件的 DOM 节点。需要注意的是,destroy()方法会销毁在 Dijit 组件模板中定义的子 Dijit 组件。这 5 个方法中除了 uninitialize()之外的其它 4 个方法,都有一个参数 preserveDom用来表明是否保留 Dijit 组件的 DOM 节点,不过对从模板创建出的 Dijit 组件无效。
在深入理解了 Dijit 组件的生命周期之后,就可以更好的利用 Dijit 库提供的支持来开发自己 Dijit 组件。下面介绍一些与组件生命周期相关的最佳实践。 灵活覆写 Dijit 组件生命周期相关的方法来添加自己的处理逻辑。一般来说,在 postCreate()方法中添加组件相关的处理逻辑即可。如果需要添加与 DOM 节点大小和位置相关的逻辑,应该放在 startup()方法中,并要求组件的使用者显式调用。在覆写方法的时候,要通过 this.inherited(arguments);来调用 dijit._Widget类的原始方法。
如果自己的 Dijit 组件在销毁的时候需要执行额外的处理,应该把相关的逻辑添加在 uninitialize()方法中,并且只包含当前组件相关的逻辑,不需要考虑子组件。destroyRecursive()方法会负责处理子组件的销毁。
总是使用 destroyRecursive()来销毁一个 Dijit 组件。这样可以确保子组件总是被正常销毁,避免内存泄露。
尽量不要覆写 destroyRecursive()、destroyDescendants()、destroy()和 destroyRendering()等 4 个方法。这 4 个方法封装了完整的 Dijit 组件销毁逻辑。一般来说,覆写 uninitialize()方法就已经足够了。
 
Dijit 组件中可能包含各种不同的属性,允许使用者对这些属性进行获取和设置。下面以一个显示电子邮件地址的 Dijit 组件来进行说明。该组件应该允许使用者获取和设置显示的电子邮件地址。一般来说,需要提供 getEmail()和 setEmail(email)两个方法来实现。另外,为了防止暴露邮件地址,一般需要用特殊的字符替换掉电子邮件地址中的“@”符号,如 admin@example.org被替换成 admin#example.org。这样的话就需要提供另外的两个方法。为了简化属性的获取和设置操作,dijit._Widget类中提供了 attr(name, value)方法来统一完成属性的获取和设置。当只传入一个参数的时候,如果该参数是一个字符串,则表示获取属性的值;如果参数是一个 JavaScript 对象,则用该对象中的属性和值来设置组件的属性。当传入两个参数的时候,两个参数分别表示为属性的名称和要设置的值。对于示例 Dijit 组件来说,可以通过 attr("email", "alex@example.com")来设置要显示的电子邮件地址。
对于 Dijit 组件自定义的属性,默认情况下是保持在组件对象实例中的。attr()方法直接读取和设置组件对象中对应属性的值即可。除此之外,还可以通过 attributeMap来提供从属性到 DOM 节点之间的映射。即通过改变属性的值,就可以修改 DOM 节点。如果希望在获取和设置属性的时候添加额外的处理逻辑,则需要提供满足命名规范的对应方法。该命名规范是在首字母大写的属性名称上添加特定的前缀和后缀。如对于属性 email来说,自定义的获取和设置属性的方法分别是 _getEmailAttr()和 _setEmailAttr()。自定义方法的优先级高于默认的方法。attr()会首先尝试在 Dijit 组件对象中查找是否存在自定义的方法。代码清单 1 中给出了一个获取和设置属性的示例。
dojo.declare("emailDisplayer", dijit._Widget, { replaceString : "", _setEmailAttr : function(value) { if (!value) { return; } var originalValue = value; var displayValue = value; if (this.replaceString) { displayValue = displayValue.replace(/@/, this.replaceString); } this.domNode.innerHTML = displayValue; this.email = originalValue; } }); var n = dojo.create("div", null, dojo.body()); var displayer = new emailDisplayer({}, n); displayer.attr("replaceChar", "#"); displayer.attr("email", "alex@example.org"); displayer.attr("email");
如代码清单 1 所示,Dijit 组件 emailDisplayer定义了两个属性 email和 replaceString。对于属性 email提供了自定义的设置方法 _setEmailAttr()。对于属性 replaceString则使用的是默认的实现。
对于统一的 attr()方法接口和自定义的属性获取和设置方法,Dijit 组件既可以方便使用者的使用,又给了开发人员足够的灵活性。在组件开发中,尽量避免提供 Dijit 组件自己的属性获取和设置方法,而是充分利用 Dijit 组件提供的支持。
除了上面提到的两类方法之外,dijit._Widget还提供了其它一些方法。 尽量使用 connect()方法来绑定事件处理方法。其好处是在 Dijit 组件被销毁的时候,会自动调用 disconnect()来取消事件绑定。
尽量使用 subscribe()方法来监听事件通知。其好处是在 Dijit 组件被销毁的时候,会自动调用 unsubscribe()来取消事件监听。
 
在介绍完 dijit._Widget之后,下面介绍另外一个核心类 dijit._Templated。
回页首
在介绍 Dijit 组件的生命周期的时候,提到过 dijit._Widget类中的 buildRendering()方法用来创建 Dijit 组件的用户界面。如果通过 DOM 操作来创建用户界面的话,一般来说会比较复杂,维护起来也比较麻烦。dijit._Templated提供了一种从 HTML 模板中创建 Dijit 组件用户界面的方式。dijit._Templated一般是作为一个混入类的方式来使用的。它提供了自己的 buildRendering()用来从模板字符串或是文件中创建 DOM 节点。在使用 dijit._Templated的时候,有下面几点需要注意。 通过 dojo.declare()方法定义新的组件的时候,dijit._Widget需要作为基类,而 dijit._Templated只能作为混入类。也就是说在父类声明中,dijit._Widget需要作为第一个出现;否则的话会出现错误。
可以通过 templateString和 templatePath两种方式来指定所使用的模板。从 Dojo 1.4 开始,建议使用 templateString来指定模板字符串。不过在 Dijit 组件开发过程中,把模板存放在单独的 HTML 文件中更加便于开发和调试。因此在开发过程中可以使用 templatePath。在发布的时候,则需要通过 Dojo 的构建过程把 HTML 文件的内容内联到组件代码中,通过 templateString来表示。
通过设置属性 widgetsInTemplate的值为 true可以声明该 Dijit 组件中包含其它的组件。这些包含的组件在 destroy()方法中会被销毁。
 
在模板中可以使用 dojoAttachPoint和 dojoAttachEvent两个特殊的 DOM 节点的属性。dojoAttachPoint属性的值被转换成组件对象中的一个属性的名称,该属性的值是当前的 DOM 节点。dojoAttachEvent属性的值被转换成通过 dojo.connect()完成的事件处理绑定。如
声明了该 DOM 节点可以在组件对象中通过 myDiv来引用,点击该节点会调用 show()方法。这两个属性的存在带来了编程上的简便。在组件对象的方法中,如果需要引用某个 DOM 节点的话,一般需要通过 DOM 查询或是 dojo.query()来完成,这样的话会比较繁琐。通过 dojoAttachPoint就避免了查询操作,使用起来更加简单。如果需要绑定事件处理的话,使用 dojoAttachEvent就免去了对 dojo.connect()方法的显式调用。不过使用这两个属性的话,会造成变量的声明和使用在不同的地方,会在一定程度上影响代码的可读性。比较好的实践如下: 为了方便区分组件对象中通过 dojoAttachPoint声明的属性和一般的属性,最好为 dojoAttachPoint声明的属性名称添加统一的后缀,如 Node或是 Container。其好处是开发人员在发现带某个后缀的属性时,会明白要去模板中查找相关的声明。
在引用 DOM 节点和绑定事件处理方法的时候,尽量使用统一的方式。可以统一使用 dojoAttachPoint和 dojoAttachEvent,也可以统一使用 DOM 查询和 dojo.connect()。最好不要两种方式混用。项目开发团队应该根据团队的意见,制定出相关的代码编写规范。
 
在介绍完 dijit._Templated之后,下面介绍另外一个核心类 dijit._Container。
回页首
有些 Dijit 组件是作为其它组件的容器而存在的,如与页面布局相关的组件。作为容器的组件需要对其包含的子组件进行管理,包括查询、添加和删除子组件等。dijit._Container混入类提供了这些管理子组件的功能,自己开发的组件可以直接混入此类。dijit._Container所提供的方法如下所示: addChild(widget, insertIndex):该方法用来添加一个新的子组件到给定位置上。参数 widget表示的是子组件,insertIndex表示的是添加的位置。
removeChild(widget):该方法用来移除一个子组件。参数 widget既可以是子组件的引用,也可以是子组件的序号。
getChildren():该方法返回一个包含所有子组件的数组。
hasChildren():该方法用来判断是否包含子组件。
getIndexOfChild(widget):该方法用来返回一个子组件的序号。
 
通过上面提到的这些方法,就可以完成对子组件的管理。在使用 dijit._Container的时候,有下面几点需要注意: dijit._Container中只能包含 Dijit 组件,也就是必须继承自 dijit._Widget。不能包含普通的 DOM 节点。对于 DOM 节点,可以用一个 dijit.layout.ContentPane封装之后,再添加到 dijit._Container中。
dijit._Container的子组件的 DOM 节点都是属性 containerNode所表示的 DOM 节点的子节点。而 containerNode的值与 domNode不一定相同。只有 containerNode中包含的子组件才会在 destroyDescendants()方法中被销毁。一般来说,在 Dijit 组件的 domNode下指定一个 DOM 节点作为 containerNode。
由于 dijit._Container负责管理其中包含的子组件,其 startup()方法会负责调用子组件的 startup()方法。对于通过 addChild()方法动态添加的子组件,如果子组件的 startup()方法没有被调用过,则会调用此 startup()方法。
removeChild()只是将子组件移除,使其不再受 dijit._Container的管理,并不会销毁该子组件。
 
在介绍完 dijit._Container之后,下面介绍如何对 Dijit 组件进行管理。
回页首
Dojo 会负责维护当前页面上所有 Dijit 组件的一个注册表,里面包含了所有的 Dijit 组件对象。该注册表可以通过 dijit.registry来访问,它是一个 dijit.WidgetSet类的实例。dijit.WidgetSet实际上是一个 Dijit 组件的 ID 及其对象对应的查找表。通过组件的 ID 就可以查询到组件对象。dijit.WidgetSet所包含的方法用来对此查找表进行操作。这些方法包括: add(widget)用来添加新组件 widget;remove(id)用来根据 ID 移除组件。
byId(id)用来通过 ID 查找组件;byClass(cls)用来根据类名来查找组件,如 dijit.registry.byClass("dijit.form.Button")用来查找页面上所有的 dijit.form.Button组件。该方法的返回值是一个新的 dijit.WidgetSet对象。
toArray()返回一个包含所有组件对象的数组。
forEach()、filter()、map()、every()和 some():这些方法的含义与用法与 Dojo 基本库中处理数组的同名方法的含义与用法是相同的,都是用来对所包含的组件对象进行处理。
 
除了全局的组件注册表 dijit.registry之外,Dijit 库还提供了其它的方法。这些方法包括: dijit.byId(id)用来根据 ID 在页面上查找组件。
dijit.getUniqueId(widgetType)用来为指定组件类别 widgetType中的新组件生成惟一的 ID。
dijit.findWidgets(root)用来查找指定 DOM 节点 root中所包含的 Dijit 组件。但是不包括嵌套的 Dijit 组件。
dijit.getEnclosingWidget(node)用来查找包含 DOM 节点 node的 Dijit 组件。
 
在管理 Dijit 组件的时候,有下面几个问题需要注意: 当需要管理一些 Dijit 组件的时候,可以创建自己的 dijit.WidgetSet对象。这样就可以利用 dijit.WidgetSet所提供的管理功能。
dijit.registry是通过组件的 ID 来进行查找的,因此要求 ID 是全局惟一的。对于系统生成的 ID,是可以保证其惟一性的。不过开发人员也可以为组件提供自己的 ID,这个时候就需要格外注意 ID 的惟一性。如果试图创建一个 ID 为 myId的组件,但是页面上已经存在 ID 相同的组件,Dojo 会抛出一个异常 "Tried to register widget with id==myId but that id is already registered"。造成这种情况的原因比较多:第一种可能是由于编程失误造成了 ID 重复,这种情况下只需要修改重复的 ID 即可;另外的可能是准备复用某个 ID,但是前一个 Dijit 组件的销毁不彻底,并没有从全局组件注册表 dijit.registry中移除掉自己。当再次使用此 ID 创建组件的时候就出现了错误。在 dijit._Widget类的 destroy()方法中包含了从 dijit.registry中删除当前组件的实现。如果自己开发的组件覆写了 destroy()方法,而没有通过 this.herited(arguments)来调用父类的逻辑的话,就很容易出现这样的错误。
 
在介绍完 Dijit 组件管理之后,下面介绍实例化 Dijit 组件的两种方式。
回页首
一般来说,实例化 Dijit 组件有两种方式:声明式和编程式。声明式的方式指的是在 HTML 代码中以描述的方式来定义 Dijit 组件,由 Dojo 在运行时刻把这些声明的组件实例化。编程式的方式是开发人员在代码中显式的通过 new操作符来实例化某个 Dijit 组件。实例化一个 Dijit 组件需要两个参数,第一个是混入到组件实例中的包含配置属性的 JavaScript 对象,第二个则是组件所使用的 DOM 节点。这两种方式的不同之处在于如何提供这两个参数的值。下面首先介绍声明式的方式。
使用声明式的时候,需要在 DOM 节点上添加属性 dojoType,其值是 Dijit 组件类的全名。这样就声明了在运行时刻会从此 DOM 节点上创建一个新的 Dijit 组件。如
声明了会从此 div元素上创建一个 dijit.form.Button组件。而对于混入到组件实例中的属性,则是通过此 DOM 节点上的属性值来声明的。由于在 HTML 代码中声明属性的时候只能使用字符串,而 Dijit 组件中的属性是有数据类型的,因此需要一个转换的过程。了解此过程有助解决一些属性无法设置的问题。这个转换过程具体如下: 对于通过 dojoType声明的组件类,遍历该类的 prototype对象中的属性,去掉以“_”开头的和 Object.prototype中包含的属性,其余的就是可以在声明 Dijit 组件的时候使用的属性。这些属性的值的数据类型也会被记录下来。
接着对于上一步中得到的每个属性,查看 DOM 节点是否包含有同名属性。如果有的话,则得到此属性的值,并根据数据类型进行转换。
完成类型转换之后的值被作为最终的结果。代码清单 2 中给出了一个示例。
 
dojo.declare("myWidget", dijit._Widget, { count : 1, valid : false, names : ["Alex", "Bob"] });

在代码清单 2 给出的例子中,myWidget中定义了 3 个属性:count、valid和 names,其数据类型分别是数字、布尔型和数组。在声明 Dijit 组件的时候,在 DOM 节点上添加了属性 count和 names。属性 count的值被转换成数字,而属性 names的值被转换成数组。
除了通过 DOM 节点的属性来设置 Dijit 组件的属性之外,还可以通过

代码清单 3 中使用了代码清单 1 中给出的显示电子邮件地址的 Dijit 组件,并通过