通过将完成细节工作的代码提取到通用的函数或者对象中,来减少代码库中重复代码的数量,是大多数重构的目的。顺着这个思路,我们可以将那些通用的功能包装为可以在不同项目中重用的库或者框架。这样做减少了项目中需要定制的代码数量,并且可以提高生产力。更进一步,因为库的代码已经在以前的项目中经过了测试,项目的质量也有望得到提高。
在本书中,我们会开发出一些可以在项目中重用的小JavaScript框架,包括第4章和第5章的ObjectBrowser、第5章的CommandQueue、第6章的通知框架、第8章的StopWatch性能分析工具以及附录A中的调试控制台。从第9章到第13章,我们在每章的末尾都会对教学的例子做重构,以便提供可以重用的组件。
当然,我们并不是这个游戏的唯一玩家,因特网上还有大量可用的JavaScript和Ajax的框架,其中较为成熟的一些框架已经经过了大量开发者的深入测试。
在本节中,我们将考察Ajax社区中的一些第三方库和框架。Ajax框架这一领域目前处在非常活跃的发展阶段,所以无法详细讨论所有的竞争者,但是我们将尽力使你了解,目前已经有哪些类型的框架,以及如何通过使用它们来为你自己的项目引入秩序。
3.5.1 跨浏览器库
正如3.2.1小节中提到的,对于编写Ajax应用,跨浏览器不一致性的问题近在咫尺。很多库都提供了一个令开发者可以在其上进行开发的通用Façade,以此避开跨浏览器的不一致性问题,从而实现非常有用的功能。一些库集中于一些特定的功能,而另外一些库则试图提供更加全面的编程环境。下面提到的库属于后一类库,当编写Ajax代码时,它们很有帮助。
1. x库
x库是一个用来编写DHTML应用的成熟、通用的库。第1版发布于2001年,取代了作者之前开发的CBE(Cross Browser Extensions,跨浏览器扩展)库,而且使用了简单得多的编程风格。它提供了跨浏览器的函数,用来操作DOM元素、为DOM元素设置样式、处理浏览器的事件模型,还包括了支持动画和拖拽的一些开箱即用的库。它支持IE 4和最近版本的Opera和Mozilla浏览器。
x库使用了一种简单的基于函数的编码风格,利用了JavaScript的可变参数列表和弱类型的特征。例如,document.getElementById()方法只接受字符串作为输入,x库将它包装为另外一个函数,既可以接受字符串也可以接受DOM元素。如果传入的是字符串,就将它作为元素的ID来确定DOM元素;但是如果传入的是DOM元素,就将它原封不动地返回。因此,可以调用xGetElementById()来保证参数通过ID确定为DOM节点,而不必测试这个参数是否是已经确定的DOM节点。在创建动态生成的代码时用DOM元素的文本ID代替DOM元素本身的能力特别有用,例如当传递一个字符串给setTimeout()方法或者回调函数的时候。
在操作DOM元素样式的函数中,也使用了类似的简练风格,相同的函数既可以用来获取属性的值,也可以用来设置属性的值。例如语句:
xWidth(myElement)
会返回DOM元素myElement的宽度,其中myElement既可以是DOM元素,也可以是DOM元素的ID。通过像下面这样增加一个额外的参数:
xWidth(myElement,420)
我们设置了元素的宽度。因此,为了将一个元素的宽度设置为与另一个元素的宽度相同,可以这样写:
xWidth(secondElement,xWidth(firstElement))
x库没有包含任何用于创建网络请求的代码。虽然如此,对于建造Ajax应用的用户界面,以及使得代码具有清晰、易懂风格,它仍然是一个很有用的库。
2. Sarissa
与x库相比,Sarissa的目标更加明确,它主要关注于使用JavaScript来操作XML。它可以支持IE的MSXML ActiveX组件(第3版以上)、Mozilla、Opera、Konqueror和Safari的基本功能。不过只有一小部分浏览器可以支持一些较为高级的特征(例如XPath和XSLT)。
对于Ajax开发者来说,它最重要的功能就是为XMLHttpRequest对象提供了跨浏览器的支持。在不提供原生的XMLHttpRequest对象的浏览器上(主要是IE),Sarissa使用Adapter模式创建了一个基于JavaScript的XMLHttpRequest对象,而不是创建一个自己的Façade对象。这个对象的内部实现还是要使用在第2章中描述过的ActiveX对象,但是对于开发者来说,他们只关注在引入了Sarissa之后,下面的代码就可以运行在任何浏览器中:
var xhr = new XMLHttpRequest();
xhr.open("GET", "myData.xml");
xhr.onreadystatechange = function(){
if(xhr.readyState == 4){
alert(xhr.responseXML);
}
}
xhr.send(null);
将这段代码与代码清单2-11中的代码进行比较,我们会注意到这里的API调用与Mozilla和Safari浏览器中原生的XMLHttpRequest对象的API完全一样。
如同已经提到的那样,Sarissa还为操作XML文档提供了很多通用的支持。例如,它能够完成JavaScript对象和XML之间的序列化。如果你的项目使用XML作为返回数据的标记语言,这些机制对于处理发到服务器端的Ajax请求所返回的XML文档是很有用的 (我们会在第5章讨论这个问题以及它的替代方案) 。
3. Prototype
Prototype是为应用JavaScript编程开发的一个通用的帮助库(helper library),其重点在于扩展JavaScript语言本身,以便支持更加面向对象的编程风格。基于它所增加的这些语言特征,Prototype的JavaScript代码有一种与众不同的编码风格。尽管Prototype本身的代码很难读懂,与Java和C#的编码风格大相径庭,但是使用Prototype或者在Prototype之上建造的库却是非常简洁明了的。Prototype可以看作是库的开发人员所使用的库。Ajax应用的开发人员更喜欢使用建造在Prototype之上的库,而不是直接使用Prototype本身。在下面几个小节我们会考察几个这样的库。在此之前,简要地讨论一下Prototype的核心特征有助于介绍它的代码风格,并且对于后面讨论的Scriptaculous、Rico以及Ruby on Rails,也会很有帮助。
Prototype允许通过将父对象的所有属性和方法复制到子对象,将一个对象“扩展”为另一个对象。这个特征最好还是通过例子来展示。假设我们定义了一个父类Vehicle:
function Vehicle(numWheels,maxSpeed){
this.numWheels=numWheels;
this.maxSpeed=maxSpeed;
}
我们需要定义一个特定的实例用来代表一趟旅客列车。在子类中,我们还想要表示车厢的数量,并且提供一种机制来增加和减少车厢。使用普通的JavaScript,可以这样写:
var passTrain=new Vehicle(24,100);
passTrain.carriageCount=12;
passTrain.addCarriage=function(){
this.carriageCount++;
}
passTrain.removeCarriage=function(){
this.carriageCount--;
}
这段代码提供了passTrain对象所需要的功能。然而从设计的角度来考察这段代码,它对于将扩展的功能封装进一个一致的单元几乎没有什么帮助。在这里,Prototype可以帮助我们将这些扩展的行为定义为一个对象,然后再来扩展这个基对象[16]。首先,我们将扩展的功能定义为一个对象:
function CarriagePuller(carriageCount){
this.carriageCount=carriageCount;
this.addCarriage=function(){
this.carriageCount++;
}
this.removeCarriage=function(){
this.carriageCount--;
}
}
然后,可以将这两个对象的功能合并在一个对象之中,以获得所有需要的行为:
var parent=new Vehicle(24,100);
var extension=new CarriagePuller(12);
var passTrain=Object.extend(parent,extension);
注意,我们首先分别定义了父对象和扩展对象,然后将它们合并在一起。父子关系存在于这些实例之间,而不是存在于Vehicle和CarriagePuller类之间。准确地说,这并不是传统的面向对象的用法,但是它允许我们将与某个特定功能相关的代码放在一起(在这里就是与拖动车厢相关的功能),这样就可以很容易地重用这些代码。尽管在一个这样的小例子中似乎没有必要这样做,但是在大的项目中,以这种方式来封装某个功能是非常有用的。
Prototype也以Ajax对象的形式提供了对于Ajax的支持,通过它可以获得一个跨浏览器的XMLHttpRequest对象。Ajax对象可以通过Ajax.Request类别来扩展,该类型使用XMLHttpRequest向服务器发送请求,就像这样:
var req=new Ajax.Request('myData.xml');
这个构造函数使用了一种在很多基于Prototype的库中也能看到的风格。它接受一个关联数组作为可选的参数,允许根据需要设置各种广泛的选项。每个选项都提供有明智的默认值,所以只需要传进那些我们想要覆盖的对象就行了。对于Ajax.Request的构造函数来说,选项数组可以是post数据、请求参数、HTTP方法以及定义的回调处理函数。一个更加自定义的Ajax.Request调用可能像下面这样:

这里的这个选项数组中传进来了4个参数。我们将HTTP方法设置为get,因为Prototype默认使用post方法。因为使用了HTTP的get方法,参数数组会通过请求字符串来传递。如果使用的是post方法,参数会通过请求的主体部分来传递。onLoaded和onComplete是回调事件处理函数,当底层XMLHttpRequest对象的readyState发生变化时将会被调用。onComplete函数中的req.transport变量是到底层XMLHttpRequest对象的引用。
在Ajax.Request之上,Prototype更进一步定义了对象的Ajax.Updater类型,用来获取服务器端生成的一段JavaScript脚本,然后执行它。这遵循了我们在第5章中描述的“以脚本为中心”的模式,已经超出了这里的讨论范围。
到这里,我们关于跨浏览器库的简要介绍就结束了。我们对这些库的选择在某种程度上有点武断,而且是不完整的。正如我们所提到的,目前这一领域正处在非常活跃的发展阶段,我们只能局限在那些较为流行或者更加成熟的库上。在下一小节,我们将考察一些UI组件的框架,它们建造于上面这些库和其他的一些库之上。
3.5.2 UI组件和UI组件套件
到目前为止,我们所讨论的库为一些相当底层的功能提供了跨浏览器的支持,例如操作DOM元素和从服务器获取资源。有了这些工具,确实能够简化了构建用户界面和开发应用逻辑的工作,然而与对应的Swing、MFC或者Qt这些框架相比,我们仍然需要做大量繁重的工作。
供Ajax开发者使用的预制UI组件,甚至是完整的UI组件集,目前才刚刚开始浮出水面。在本小节中,我们来考察一下其中的几个成员,当然更多的是为本书增加一些特色,而不是做全面的综述。
1. Scriptaculous
Scriptaculous库是建造在Prototype(参见上一小节)之上的UI组件,它的当前版本提供了两部分主要的功能,不过目前针对它的开发很活跃,几个其他的特征已经列入了开发计划。
Scriptaculous库的第一部分功能是它的Effects(效果)库,它定义了一些可以应用在DOM元素上的动画效果,用来改变DOM元素的大小、位置和透明度。这些效果可以很容易组合在一起使用,此外还有很多预定义的辅助效果,例如Puff(),可以使一个元素变得越来越大、越来越透明,直到它完全从屏幕上淡出。另外一个有用的核心效果是Parallel(),它能够同时执行多个效果。当需要迅速为Ajax用户界面添加各种视觉反馈时,Effects库是很有用的,在第6章中将会看到这一点。
执行一个预先确定的效果非常简单,只需要调用它的构造函数,并且将目标DOM元素或者它的ID作为参数传递进来,例如:
new Effect.SlideDown(myDOMElement);
在各种效果之下是一个渐变对象的概念。这个对象接受两个参数,一个持续时间和一个当渐变过程结束时会调用的事件处理函数。它提供了一些基本的渐变类型,例如线性、正弦、钟摆和脉冲。创建一种自定义的效果,只需要将一些核心效果组合在一起,并且将适当的参数传递进来即可。创建自定义效果的详细讨论已经超出了简要介绍的范围。在第6章中,当开发一个通知系统的时候,还会用到Scriptaculous库的效果。
Scriptaculous库的第二部分功能是它通过Sortable类提供了一个拖放库。这个类使用一个父DOM节点作为参数,使得它的所有子节点都可以进行拖放操作。传进构造函数的选项可以指定当节点被拖放时的回调处理函数、能够拖动的子元素类型,以及能够作为释放目标的元素列表(即接受用户通过鼠标拖动项的元素)。Effect对象也可以作为选项传递进来,用来在开始拖动、拖动过程中和释放的时候执行这些效果。
2. Rico
Rico和Scriptaculous一样也是基于Prototyp库的,它也提供了一些高度可定制的效果和拖放功能。除此之外,它还给出了一个Behavior对象的概念,也就是一段代码,可以应用在DOM树的一部分,为它增加交互功能。Rico提供了少量示例的Behavior,例如,Accordion(折叠)UI组件,它可以将一组DOM元素嵌套在一个给定的空间内,每次展开其中的一个(这种风格的UI组件常常称作outlook bar,在微软的Outlook中使用了之后变得非常流行)。
我们来建造一个简单的Rico Accordion UI组件。一开始,我们需要一个父DOM元素,它的每个子节点都会成为一个折叠的面板。我们为每个面板定义一个DIV元素,其中还包含有另外两个DIV,表示每个面板的标题和和主体部分:


第一个面板提供了accordion[17]这个词在字典中的定义;第二个面板包含了一个猴子玩手风琴的图片(参见图3-9)。直接呈现上面这段HTML,仅仅是将这两个元素上下显示。然而,我们给顶层的DIV元素分配了一个ID属性,使得可以将它的引用传递给Accordion对象,像这样来构造:
var outer=$('myAccordion');
outer.style.width='320px';
new Rico.Accordion(
outer,
{ panelHeight:400,
expandedBg:'#909090',
collapsedBg:'#404040',
}
);
第一行看起来有点古怪。$实际上是一个合法的JavaScript变量名,它指向核心Prototype库中的一个函数。$()函数以一种类似于上一小节中讨论的x库xGetElementById()函数的方式来确定DOM节点。我们将一个已确定的DOM元素的引用传递给Accordion对象的构造函数,并伴随一个符合源自Prototype库的标准习惯的选项数组。这里仅仅提供了一些设置Accordion UI组件的视觉风格的选项,不过还可以在这里提供当面板打开或关闭时触发的回调处理函数。图3-9展现了使用Accordion对象来设置DOM元素样式之后的效果。Rico的Behavior提供了一种简单的方式来从通常的标记创建可重用的UI组件,并且还将内容与交互分离开。在第4章,我们探讨将良好的设计原则应用在JavaScript用户界面开发中的话题。

图3-9 Rico框架的Behaviors库可以为简单的DOM节点设置样式,使得它变成交互的UI组件,这只需要将顶层节点的引用传给Behavior对象的构造函数就行。在这里,Accordion对象应用在一组DIV元素(图中左边)上,以创建一个交互的菜单UI组件(图中右边),通过鼠标点击来打开和关闭单个面板
需要提到的最后一个Rico框架的功能,是它通过全局的Rico AjaxEngine对象为Ajax风格的服务器请求提供了非常棒的支持。AjaxEngine所提供的不仅仅是XMLHttpRequest对象的一个跨浏览器的包装,它还定义了一种由很多<response>元素组成的XML响应格式。引擎会自动对这些响应进行解码,它内建了对于两种类型的响应的支持:直接更新DOM元素的响应类型和更新JavaScript对象的响应类型。在5.5.3节深入讨论客户/服务器交互的时候,我们还会更详细地考察一个类似的机制。现在,我们转到下一种类型的框架:一种跨越客户端与服务器的框架。
3.5.3 应用框架
到目前为止,我们所考察的框架都是完全在浏览器中执行的,可以作为静态的JavaScript文件从任何Web服务器提供。我们在这里将要介绍的最后一类框架位于服务器端,至少会动态地生成一些JavaScript代码或HTML标记。
在所讨论的框架中,这些框架是最复杂的,我们无法非常详细地讨论它们,而只能对其功能做一些简要的介绍。在第5章中,我们还会回到这个有关服务器端框架的话题。
1. DWR、JSON-RPC和SAJAX
我们首先一起考察一下这三个框架。尽管它们是使用不同的服务器端语言编写的,但是它们其实是一类共同的方法。SAJAX可以与多种服务器端语言共同使用,包括PHP、Python、Perl和Ruby。DWR(直接Web远程调用)是一个以类似方法实现的基于Java的框架,它能够将对象的方法暴露出来,而不是像SAJAX那样将独立的函数暴露出来。JSON-RPC(基于JSON的远程过程调用)在设计上也是相似的,它提供了对于服务器端的JavaScript、Python、Ruby、Perl和Java的支持。
它们都允许将定义在服务器端的对象上的方法直接暴露给Ajax请求。我们常常在这些服务器端的函数中执行那些必须要在服务器端执行的计算(例如,从数据库查询一个值),然后再将有用的结果返回给客户端。这些框架为从Web浏览器中访问这些函数或方法提供了方便的途径,并且也是将服务器端的领域模型暴露给Web浏览器代码的很好方法。
我们来考察一个使用SAJAX的例子,其暴露了一个在服务器端用PHP定义的函数。这个例子非常简单,仅仅返回一个文本字符串,就像下面这样:
<?php
function sayHello(name){
return("Hello! {$name} Ajax in Action!!!!");
?>
为了将这个函数暴露给JavaScript层,我们只需要在PHP中引入SAJAX引擎,然后调用sajax_export函数:
<?php
require('Sajax.php');
sajax_init();
sajax_export("sayHello");
?>
当随后编写动态Web页面的时候,我们使用SAJAX来为这个暴露的函数生成一些JavaScript包装代码。生成的代码创建了一个本地的JavaScript函数,具有和服务器端函数完全相同的签名:
<script type='text/javascript'>
<?
sajax_show_javascript();
?>
...
alert(sayHello("Dave"));
...
</script>
当我们在浏览器中调用sayHello("Dave")时,生成的JavaScript代码会向服务器发送一个Ajax请求,执行服务器端的函数,并且在HTTP响应中返回结果。然后它会解析HTTP响应,从中提出返回值,传给调用它的JavaScript代码。开发者无需接触到任何Ajax技术,所有的底层工作都通过SAJAX库以后台的方式来处理。
这三个框架提供了一个从服务器端函数和对象到客户端的Ajax调用的相当底层的映射。这些枯燥的任务可以自动地完成,但是它们确实也引入了将服务器端的逻辑过多暴露给因特网的危险。我们将在第5章中更加详细地讨论这个问题。
我们在本节中将要考察的其余框架则采用了更为复杂的方法,它们在服务器端根据声明的模型生成整个UI层。虽然在它们内部也使用了标准的Ajax技术,但是从本质上看,这些框架其实是提供了它们自己的编程模型。带来的结果就是,使用这些框架来编程与编写普通的Ajax代码大不相同,我们在这里只做一些概括的介绍。
2. BackBase
BackBase表现服务器(BackBase Presentation Server)提供了一套丰富的UI组件,这些UI组件可以与服务器端生成的、嵌入在HTML文档中的XML标签进行实时绑定。这里的原理类似于Rich的Behavior组件,只不过BackBase使用了自定义的XHTML标签集以标记UI组件,而Behavior使用的是标准HTML标签。
BackBase为Java和.NET提供了服务器端的实现。它是一个商业化的产品,但是也提供了可以免费使用的社区版本。
3. Echo2
NextApp的Echo2框架是一个基于Java的服务器引擎,它可以根据在服务器端声明的用户界面模型生成丰富的UI组件。一旦在浏览器中启动后,UI组件就以一种相当自治的方式来运行,要么使用JavaScript在本地处理用户交互;要么使用一个类似于Rico中使用的请求队列批量地将请求发送回服务器。
Echo2将自己提升为一个基于Ajax的解决方案[18],开发者无需具备HTML、JavaScript或CSS的知识,除非你想要扩展现有组件集合。在大多数情况下,客户端应用的开发只需要使用Java。Echo2是一个开源项目,经Mozilla风格认证许可,可以用于商业应用。
4. Ruby on Rails
Ruby on Rails是一个使用Ruby编程语言编写的Web开发框架。它将一些解决方案打包在一起,支持服务器端对象与数据库中的数据之间的映射[19];还支持使用模板来生成内容,其风格非常类似于3.4节讨论过的服务器端MVC。Ruby on Rails宣称能够非常快速地开发简单的和中等规模的网站,因为它使用代码生成技术来生成大量通用的代码。它还试图用尽可能少的配置数量让应用跑起来。
在最近的版本中,Rails通过Prototype库提供了强大的Ajax支持。Prototype和Ruby on Rails是天生的一对,因为Prototype的JavaScript代码就是使用Ruby程序生成的,并且它们的编程风格也很相似。和Echo2一样,在Rails中使用Ajax也不要求开发者很了解JavaScript等Ajax技术,但是一个确实理解JavaScript的开发者,将能够以新的方式扩展Rails的Ajax支持。
到这里,我们对于第三方Ajax库和框架的介绍就结束了。正如我们已经提到的那样,这是一个目前正在迅速发展的领域,大部分在这里讨论的框架仍然处在活跃的开发阶段。
很多库和框架都有自己的编码习惯和风格。在为本书编写例子代码的过程中,我们尽力使读者体会到Ajax技术覆盖范围之广,而避免使用那些需要对某种特殊框架进行大量学习的技术。尽管如此,在本书的其余部分,你仍然会遇到一些在此讨论过的框架。







