一、 引言
在第一部分中,我们讨论了AJAX基础——建立从脚本到服务器的通讯的能力,这正是使HTML页面具有动态能力的原因所在。然而,这就意味着我们已准备好抛弃我们自己版本的Yahoo邮件吗?不,还没有。原因在于:AJAX是一个混合的祝福。一方面,它使我们能够在Web上创建丰富的桌面级的应用程序;另一方面,如果我们把"翻页面式"的Web应用程序与客户端/服务器或Swing版本的程序进行比较,那么会看到其开发实践并不很相同。我们将需要习惯于这样的事实:构建一个丰富的UI需要时间。须知,允许用户实现更大的灵活性也就相应地需要付出更多的时间为代价。
最后的答案当然要依赖于大量的组件库、框架以及具有工业力量的开发工具。且不考虑工具,本文集中于讨论在今天对于AJAX热心者有哪些技术是可用的。在强调需要构建可重用的商业组件的同时,本文将重点分析"隐含的"JavaScript中的面向对象的力量。另外,在强调需要构建定制的UI组件的同时,本文将介绍一个简便的方法——用定制的客户端HTML标签来封装描述逻辑。
二、 AJAX语言——对象面向的JavaScript
由定义来看,JavaScript是典型的AJAX语言。不同于Java,JavaScript并不强调OO风格的编码。然而,令人吃惊的是JavaScript居然全面支持所有的OO语言的主要属性:封装、继承和多态性。Douglas Crockford甚至称JavaScript是"世界上最易被误解的编程语言"。让我们回顾一下JavaScript的面向对象的地方吧。
数据类型
在Java中,一个类定义了一个数据和它的相关行为的组合。尽管JavaScript保留了class关键字,但是它不支持与常规OOP语言一样的语义。
这听起来可能觉得奇怪,但是在JavaScript中,对象是用函数来定义的。事实上,通过在下面的示例中定义一个函数,你就定义了一个简单的空类Calculator:
function Calculator() {}
一个新的实例的创建与在Java中相同-使用new操作符:
var myCalculator = new Calculator();
上面这个函数不仅定义一个类,而且还担当了一个构造器。在此,操作符new实现了这一魔术-实例化一个类Calculator的对象并且返回一个对象参考而不是只调用该函数。
创建这样的空类是没错,但在实际中并没有多大用处。下面,我们准备使用一个Java-脚本原型结构来填充类定义。JavaScript使用原型当作创建对象的模板。所有的原型属性和方法被参考引用地复制到一个类的每个对象中,所以它们都具有相同的值。你可以改变一个对象中的原型属性的值,并且该新值会覆盖从原型中复制过来的缺省值,但是这仅对于在一个实例中。下列语句将把一个新属性添加到Calculator对象的原型上:
Calculator.prototype._prop = 0;
既然JavaScript并没有提供一个方法来从句法上表示一个类定义,那么我们将使用with语句来标记该类的定义边界。这也将使得示例代码更为短小,因为该with语句被允许在一个指定的对象上执行一系列的语句而不需要限制属性。
function Calculator() {};
with (Calculator) {
prototype._prop = 0;
prototype.setProp = function(p) {_prop = p};
prototype.getProp = function() {return _prop};
}
到目前为止,我们定义了并且初始化了公共变量_prop,并且为它提供了getter和setter方法。
需要定义一个静态变量?你可以把静态变量当作是为类所拥有的一个变量。因为在JavaScript中的类用函数对象来描述,所以我们只需要把一个新属性添加到该函数上:
Calculator.iCount=0;
现在,既然这个iCount变量是一个Calculator对象的属性,那么它将会被类Calculator的所有实例所共享。
function Calculator() {Calculator.iCount++;};
上面的代码计算类Calculator的所有实例的个数。
封装
通过使用如上面所定义的"Calculator",我们可以存取所有的"class"数据;然而,这增加了派生类中命名冲突的危险性。我们明显地需要封装以把对象看作自包含的实体。
数据封装的一种标准语言机制是使用私有变量。并且一个常用的仿效一个私有变量的JavaScript技术是在构造器中定义一个局部变量;这样以来,该局部变量的存取只能经由getter和setter来实现-它们是该构造器中的内部函数。在下列实例中,_prop变量在Calculator函数中定义并且在函数范围外不可见。其中有两个匿名的内部函数(分别被赋予setProp和getProp属性)让我们存取"私有"变量。另外,请注意,这里this的使用-十分相似于在Java中的用法:
function Calculator() {
var _prop = 0;
this.setProp = function (p){_prop = p};
this.getProp = function() {return _prop};
};
常常被忽视的是在JavaScript中作如此封装所付出的代价。须知,这种代价可能是巨大的,因为内部函数对象对于该"class"的每一个实例被不断地重复创建。
因此,既然基于原型构建对象速度更快并且消费更少些的内存,那么我们在最强调性能的场所特别支持使用公共的变量。请注意,你可以使用命名惯例来避免名称冲突-例如,在公共的变量的前面加上该类名。
继承
乍看之下,JavaScript缺乏对类层次的支持,这很相似于面向对象语言的程序员对于现代语言的期盼。然而,尽管JavaScript句法没有象Java一样支持类继承,但是我们仍然能够在JavaScript中实现继承-通过把已定义类的一个实例拷贝到其派生类的原型当中。
在我们提供举例之前,我们需要介绍一个constructor属性。JavaScript保证每一个原型中包含constructor-它拥有到该构造器函数的一个参考。换句话说,Calculator.prototype.constructor包含一个到Calculator()的参考。
现在,下面的代码显示了怎样从基类Calculator派生类ArithmeticCalculator。其中,"第一行"取得类Calculator的所有的属性,而"第二行"把原型constructor的值恢复成ArithmeticCalculator:
function ArithmeticCalculator() { };
with (ArithmeticCalculator) {
ArithmeticCalculator .prototype = new Calculator();//第一行
prototype.constructor = ArithmeticCalculator;//第二行
}
就算上面的实例看起来象一个合成体而不象是继承,但是JavaScript引擎还是清楚这个原型链的。特别是,instanceof操作符会正确地适用于基类和派生类。假定你创建类ArithmeticCalculator的一个新实例:
var c = new ArithmeticCalculator;
表达式c instanceof Calculator和c instanceof ArithmeticCalculator都会成立。
注意,在上面示例中的基类的constructor是在初始化ArithmeticCalculator原型时被调用的,而在创建派生类的实例时是不被调用的。这可能会带来不想要的负面影响,而且为了实现初始化你应该考虑创建一个独立的函数。由于该构造器并不是一个成员函数,所以它无法通过this参考引用调用。我们将需要一个能调用超类的"Calculator"成员函数:
function Calculator(ops) { ...};
with (Calculator) { prototype.Calculator=Calculator;}
现在,我们可以写一个继承类-它显式地调用基类的构造器:
function ArithmeticCalculator(ops) { this.Calculator(ops);};
with (ArithmeticCalculator) {
ArithmeticCalculator .prototype = new Calculator;
prototype.constructor = ArithmeticCalculator;
prototype.ArithmeticCalculator = ArithmeticCalculator;
}
多态性
JavaScript是一种非类型化的语言-在此,一切都是对象。因此,如果有两个类A和B,它们都定义一个foo(),那么JavaScript将允许在A和B的实例上多态地调用foo()-即使不存在层次关系(虽然是可实现的)。从这一角度来看,JavaScript提供一个比Java更宽的多态性。这种灵活性,象往常一样,也要付出代价。在这种情况中,代价是把类型检查工作代理到应用程序代码。具体地说,如果需要检查一个参考确实指向一个所希望的基类,那么这可以通过instanceof操作符来实现。
另一方面,JavaScript并不检查函数调用中的参数-这可以防止用一样的命名和不同的参数来定义多态函数(并且让编译器选择正确的签名)。代之的是,JavaScript提供了一个Java 5风格的函数范围内的argument对象-它允许你根据参数的类型和数量的不同而实现一个不同的行为。
三、 示例展示
本文所附源码列表1实现了一个计算器-它可以计算以一个逆向波兰式标志的表达式。该示例展示了本文中所介绍的主要技术并且也介绍了一些独特的JavaScript特性的用法,例如在一个动态函数调用中以一个数组元素的方式访问对象属性。
为了使列表1工作,我们需要另外准备一些代码-它们用于实例化该计算器对象并且调用evaluate方法:
var e = new ArithmeticCalcuator([2,2,5,"add","mul"]);
alert(e.evaluate());
四、 AJAX组件授权
所有的AJAX组件授权方案在今天被逻辑地分成两组。具体地说,第一组用于与基于HTML的UI定义的无缝集成。第二组把HTML当作一个UI定义语言以支持某种XML。在本文中,我们从第一组中来展示一种方法-虽然它存在于浏览器之中却是类似于JSP标签。这些浏览器特定的组件授权扩展在IE情形下称作元素行为,而在最近版本的Firefox,Mozilla和Netscape 8情形下称作可扩展的绑定。
五、 定制标签
Internet Explorer,从版本5.5开始,支持定制的客户端HTML元素的JavaScript授权。不象JSP标签,这些对象并没有在服务器端被预处理到HTML中。而是,它们成为一标准HTML对象模型的合法扩展,并且包括构造控件在内的一切事情,都是动态地发生在客户端的。同样,基于Gecko-引擎的浏览器能够用一个可重用功能动态地装饰任何现有的HTML元素。
因此,我们有可能用具有HTML语法的方法、事件和属性来构建一个具有丰富的UI组件的库。这样的组件可以被自由地混合于标准HTML中。内部地,这些组件将会与应用程序服务器进行通讯-以AJAX风格。换句话说,你有可能(并且相对简单地)构建自己的AJAX对象模型。
这种IE风味的方法被称为HTC或HTML组件;其Gecko版本被称为XBL-可扩展的绑定语言(eXtensible Bindings Language)。为了实现本文目的,我们集中于讨论IE。
六、 输入HTML组件-HTC
HTC或HTML组件也被称作行为。它们被划分为两种类型:一种是依附的行为-用一组属性、事件和方法装饰任何现有的HTML元素;另一种是元素行为-看上去象宿主页面的定制的HTML标签的一个扩展集合。依附的行为和元素行为一起提供了开发组件和应用程序的一种简单方案。在此,我们将展示一下最为综合的情形-元素行为。
数据绑定复选框控件
为了展示元素行为,我们将构建一个定制的数据绑定复选框。构建这样一个控件背后的基本原因在于,一个标准HTML复选框具有下面若干显著的缺点:
·需要应用程序编码来把"checked"属性的值映射到商业域值,例如"Y[es]"/"N[o]","M[ale]"/"F[emale]",等等。HTML复选框使用"checked"属性,而许多其它HTML控件使用的则是"value"属性。
·需要应用程序编码来维持该控件的状态(修改过的/未修改过的)。这实际上是在所有的HTML控件普遍存在的一个问题。
·需要应用程序编码才能创建一个关联标签-它应该接受鼠标点击并相应地改变该复选框的状态。
·标准HTML复选框不支持"校验"事件以允许取消一个GUI行为,而这种要求可能存在于某些应用程序中。
现在,让我们看一个正在构建的该控件的用法示例,它的用法可能如下情形:
<checkbox id="cbx_1" value="N" labelonleft="true"
label="Show Details:" onValue="Y" offValue="N"/>
另外,我们的控件将支持可取消的事件onItemChanging和通知事件onItemChanged。
定义定制标签
从结构上讲,一个定制标签是一个具有一个HTC扩展名的文件-它在<PUBLIC:COMPONENT>和</PUBLIC:COMPONENT>标志之间对它的属性,方法和事件加以描述。
为了定义一个定制CHECKBOX标签,我们创建一个如下列代码片断中的文件checkbox.htc-其中,第一行负责设置该组件的标签名:
<PUBLIC:COMPONENT NAME="cbx" tagName="CHECKBOX">
<PROPERTY NAME="value" GET="getValue" PUT="putValue" />
//我们把组件的所有另外的属性放在这里
<METHOD NAME="show" />
//我们把组件的所有另外的方法放在这里
<EVENT NAME="onItemChanging" ID="onItemChanging"/>
//我们把组件将向应用程序激活的所有另外的事件放在这里
<ATTACH EVENT="oncontentready" HANDLER="constructor" />
//我们把组件自己处理的另外的事件放在这里
<SCRIPT>
//我们把所有的方法,属性getters和setters和事件处理器放在这里
</SCRIPT>
</PUBLIC:COMPONENT>
使用定制标签
尽管HTC文件的内容比较重要,但是这与其文件名是什么无关。值得注意的是,指向该HTC文件的URL需要被使用IMPORT指令指定-这必须在相应的定制标签第一次出现之前(在页面上)完成。下面是最简单的可能的页面使用一个定制的复选框可能看上去的样子-假定该页面和HTC文件处理同一个文件夹下:
<HTML xmlns:myns>
<?IMPORT namespace="myns" implementation="checkbox.htc" >
<BODY>
<myns:checkbox id='cbx_1' label='Hello'/>
</BODY>
</HTML>
请注意,定制CHECKBOX是怎样在打开的HTML标签中被映射到一个非缺省的命名空间"myns"的。这个IMPORT指令实现把HTC同步加载到浏览器的内存并且还指示浏览器怎样为适当的命名空间实现名称确定的(HTC到命名空间的关联可能是多对一的)。
定制标签的构造器
最好的初始化HTC的方法是,一旦它被装载就处理oncontentready事件。因此,我们可以定义处理器函数-为了概念清晰起见,我们称之为构造器:
<ATTACH EVENT="oncontentready" HANDLER="constructor" />
constructor()的逻辑是简单的:根据属性labelonleft的值(见下面的属性定义)按顺序连接一个常规HTML复选框和HTML标签:
function constructor() {
//我们将把一个HTML复选框和标签添加到元素体
//详细情形见列表2
}
定义定制标签属性
为了定义属性labelonleft,我们又在部分加上一行:
<PROPERTY NAME="labelonleft" VALUE="true"/>
请注意,这个属性并没有包含getter和/或setter方法。属性onValue和offValue不仅提供了从复选框状态到一个商业值域的映射而且不需要getters和setters:
<PROPERTY NAME="onValue" VALUE="true"/>
<PROPERTY NAME="offValue" VALUE="false" />
然而,属性checked是用两个getter和setter定义的:
<PROPERTY NAME="checked" GET="getChecked" PUT="putChecked" />
因此,我们在<SCRIPT>部分建立了上面两个方法的定义。正如你所见,setter putChecked()-将在每次复选框状态改变时激发-把value属性设置为下面两个变体之一:onValue或OffValue。请注意,putChecked()将不仅可由在复选框-宿主页面中的脚本触发,而且也能通过在checkbox.htc中的相应的任何赋值操作触发。
var _value;
function putChecked( newValue ) {
value = (newValue?onValue:offValue);
}
function getChecked(){
return ( _value == onValue);
}
七、 为定制标签定义事件
让我们看一下onItemChanging和onItemChanged事件的定义以及这些事件是怎样在value属性的setter内部被激发和处理的(见所附源码中的列表2)。方法putValue()有几个让人感兴趣的地方。首先,在分析CHECKBOX标签期间,可以调用这个方法-只要指定这个HTMLvalue属性。这正解释了为什么我们为非构造对象建立一个单独的逻辑分支-为把构造过程与一个对用户击键的反应区别开来。其次,在此我们展示了定制事件onItemChanging的创建和处理-它允许应用程序取消行为。请注意,通过这种方式,无论是击键还是通过编程方式实现赋值都能达到取消的目的。
事件取消
为了取消事件,一个应用程序应该拦截该事件并且把event.returnValue设置为false。下面的代码片断展示了应用程序是怎样实现取消事件过程的:
cbx_1::onItemChanging() {
. . . . .
if (canNotBeAllowed) {
event.returnValue=false;
. . . . .
}
如果事件没被取消,putValue()把内部的普通HTML复选框的checked属性设置为每个相应的当前值-如果它等于onValue,这个内部复选框将被选中;如果它等于offValue(不存在第三种选择),复选框不被选中(完整的列表见本文所附源码中的列表2)。
复选框的HTML内幕
我们控件的绘制是通过助理函数addLabel()和addCheckBox()来实现的并且从一个constructor()内部调用。这些函数把HTML注入进元素的innerHTML。这种注入式HTML的一种简化形式如下所示:
<LABEL for=cb_{uniqueID}>Show Details:</LABEL>
<INPUT id=cb_{uniqueID} type=checkbox />
在此,uniqueID是一个由IE所生成的唯一的(在一个页面内)字符串-它用来识别HTC的实例。
八、 再封装
在我们的CHECKBOX中有一个缺点。按照我们建立它的方式,在constructor()期间被注入的HTML将隶属于宿主该HTC的页面的DOM。而且,全局的JavaScript变量like_value属于它们所在的文档的全局范围。这是危险的,因为我们偶然会遇到命名冲突的可能性:最明显的情形是使用同一个组件的多个实例。另外这还会导致一个可能性-我们的控件可能会偶然地用相同的名称参考其它对象,反之也如此。
为简化起见,需要建立一种专门的机制来为对象授权启动一个真正模块化方法。幸好,HTC技术支持一种智能答案-viewLink。
把一个控件声明为封装的最容易的方法是把一个额外声明放到打开和关闭的PUBLIC:COMPONENT标签之间:
<PUBLIC:DEFAULTS viewLinkContent/>
该控件立即就变成封装性的;而且它有自己的HTML文档树-成为主文档的原子组件。该对象的每个实例有它自己的实例值的集合并且只有公共方法和属性能够从外界代码中加以存取。换句话说,该viewLink机制充分地启动了复杂的Web应用程序的设计和实现-通过使用一种真正的OO的基于组件的方法。
特别地,我们可以简化代码-通过从内部复选框和HTML标签的定义中删除uniqueID后缀,因为我们不再担心命名冲突。因此,我们可以替换下面这一行:
eval( 'cb_'+uniqueID).checked = ( _value == onValue );
用
cb.checked = ( _value == onValue );
并相应地改变addCheckbox()和addLabel()。
九、 结论
既然AJAX竞赛刚刚开始,那么就不存在什么AJAX标准并且没有现成的你可以依赖以构建你的应用程序的可广为接受的RAD工具。虽然软件供应商们可能还需要较长一段时间来创建这种强健的开发平台,AJAX热心者已经开始着手准备-通过一些良好定义的API把可重用的代码块封装为商业组件。
以这种方向导航,本文概括了AJAX语言的OO"力量"-JavaScript。另外,还展示了一种可用的组件-授权策略-客户端定制标签技术。我们在仅描述IE特定的定制标签的同时,还另外提供了一个可下载的实例-适于Mozilla浏览器的可扩展的绑定实例。
源码下载