Friday, December 5, 2008

Prototype and script.aculo.us终极揭秘 推荐序

推荐序

2005年对于Web开发来说是一个伟大的年份。在这一年中,有两项技术异军突起,一项是Ajax,另一项是Ruby on Rails。这两项技术的出现改变了Web开发的面貌,甚至打乱了JavaEE前进的步伐。多年以来,JavaEE设计者们为自己所设计的无所不包的复杂架构而陶醉,新的buzz word层出不穷,一出来就会得到广泛的关注,相关的图书也会热卖。辉煌的JavaEE版图中居然还有完全被忽略的死角,这是JavaEE设计者们始料不及的。事实上他们在JavaEE的架构中从来就没有考虑过浏览器端的处理能力,浏览器对于JavaEE来说是完全无智能的瘦客户端。他们这样设计情有可原,因为他们只能把设计局限在Java能够控制的区域内,在Java Applet彻底失败之后,Java能够控制的区域退守到了服务器端。毫无疑问,JavaEE取得了巨大的成功,但是这种完全基于服务器端的解决方案很难向用户提供优秀的交互体验,同时也很难实现最佳的可伸缩性(Scalability)。

Ajax是用户和市场的选择,这个由群众创造出来的buzz word时常响在JavaEE设计者的耳边,令他们非常烦躁。甚至有人喊出了"JavaEE without Ajax"的口号,和2004年Rod Johnson所提出的"J2EE without EJB"相对应。但是仔细考察这个"without Ajax",我们却发现它和"without EJB"说的并不是一个意思。"without EJB"的意思就是不使用EJB,而"without Ajax"的意思却不是不使用Ajax,而是设法将Ajax隐藏掉,使得普通的JavaEE开发者不需要学习Ajax。在这个口号的发明者所设计的JSF 框架中,内嵌了一个Ajax组件库ExtJS,看来他们还是需要Ajax的帮助的。所谓的"JavaEE without Ajax"其实是一个伪命题,只是用来吸引眼球的噱头,主要的目的是为了迎合那些讨厌Ajax、对于浏览器端脚本编程没有兴趣的JavaEE开发者。其实无论开发Web应用,还是开发B/S结构的企业应用,交互体验都是非常重要的。在交互体验越来越受到重视的今天,想要把Ajax排除在外,可以说是一件不可能的任务。

尽管Ajax开发确实很重要,但无须讳言的是Ajax开发,特别是DHTML开发至今仍然是一件很复杂的工作,必须具备专业的技能。除了高超的开发技能之外,还需要开发者对于Web可用性和交互设计具有足够的理解。这超出了Web页面设计师和制作人员的能力范围;而对于服务器端开发者来说,这些显然也不是他们的特长(他们的特长是处理业务逻辑和与数据库打交道,而不是界面开发和交互设计)。在Web页面设计师和服务器端开发者之间出现了巨大的断层,这使得高水平的Ajax开发者一下子变得炙手可热。

JavaEE设计者们简化Ajax开发的努力是可以理解的。但是我们需要讨论清楚一个问题:究竟怎样做才能真正简化Ajax开发?

在我看来,JavaEE社区简化Ajax开发的努力都不是很成功。将ExtJS嵌入到JSF框架中,确实可以实现一些简单的交互需求,但是这种做法其实阉割掉了Ajax的主要能力--直接在浏览器端处理用户的事件。JSF的事件模型是完全位于服务器端的,界面的任何改变都需要发送事件到服务器端做处理。即使引入了ExtJS,也不可能改变这一点。在Struts2/WebWork和jMaki中通过使用taglib集成Ajax组件库的做法也会令人感觉笨拙而不自然。之所以集成的效果不理想,是因为传统的JavaEE框架和Ajax组件库的核心架构设计都没有考虑到对方的需求,它们在架构设计上存在着内在的冲突,拉郎配的结果是可想而知的。这些JavaEE表现层框架集成Ajax组件库,是希望将Ajax组件库改造为JavaEE世界的顺民,服从 JavaEE框架的架构约束,但是这样做不可避免地会削弱Ajax组件库的能力。要简化Ajax开发并且充分用好Ajax技术,仅仅使用taglib来对 Ajax组件库做封装是不够的,服务器端的架构也必须加以改造,甚至需要设计出一套全新的架构,这是传统的JavaEE表现层框架无法做到的。 Google的GWT同时具有浏览器端和服务器端,似乎是一个很好的选择,但是GWT主要是为实现一类One Page的Ajax应用而设计的,它模仿的是桌面应用的交互模型,而绝大多数的Web应用是不可能采用这种交互模型的。DWR也是一个跨浏览器端和服务器端的框架,但是DWR的RPC风格的API是我所不喜欢的。RPC架构和REST架构在可伸缩性方面的差距是巨大的,Ajax应用的最佳架构是REST而不是RPC,同时REST在简化编程模型方面的效果也要比RPC更好。
要简化Ajax开发,我认为需要满足以下3个要求:

1. 由专业的DHTML开发者开发出更加全面和强大的Widget库。这部分的开发应该由专业人士来做,这样就可以把负担从普通的Web开发者身上卸掉。开源软件是开发Widget库的必由之路。

2. Ajax组件库要与服务器端框架做更加紧密的集成。服务器端框架的架构需要加以改造以适应Ajax和REST的需求,不能指望以不变应万变。

3. 简化的编程模型。这包括两个方面:a)通过与服务器端框架紧密集成,使得服务器端开发者仅仅使用服务器端编程语言就能够实现一些普通的Ajax需求;b)更加复杂的交互需求如果必须要做DHTML开发,需要大幅降低DHTML开发的难度。

当我把眼光投向了Ruby on Rails之后,包括Ajax开发在内,一切都变得容易了很多。Rails通过其RJS模板集成了两个Ajax组件库Prototype和基于 Prototype的script.aculo.us。Rails集成这两个组件库的方式显得非常自然,因为这两个组件库正是Rails和Ajax相结合的产物,它们都是由Rails的核心开发人员所开发的,充分考虑到了Rails在架构方面的需求。现在这两个组件库已经长大成人,完全可以独立应用在非 Rails环境中。Rails + Prototype/script.aculo.us满足了上述3个要求,因此是目前做Ajax开发的最佳组合。当然,上述3个要求每一个都还有很大的改进余地,但是Rails + Prototype/script.aculo.us走在了正确的道路上,前景是非常光明的。

随着Ajax技术的普及,传统的DHTML组件库恢复了生机,可以加以改造以适应新的交互需求。这些DHTML组件获得了一个时髦的新名称--Widget。我一般是将Ajax组件库分成3部分:

1. 基础库。包括:a)对于JavaScript语言的增强,为目前浏览器所支持的JavaScript 1.x版提供继承、包管理等面向对象编程所需要的支持;b)封装HTML DOM的API,提供编程模型更加简化的API。

2. Web Remoting库,通过XMLHttpRequest/IFrame/JSONP等机制与服务器交互,从服务器获取数据。

3. 丰富的Widget库,可用来实现各种交互需求,以改善用户的交互体验。

Prototype库实现了第1部分和第2部分,script.aculo.us库实现了第3部分。在这3部分中,开发工作量最大的是Widget 库。正是因为市场上对于Widget库有着巨大的需求,在2007年涌现出了大量Widget库,开源的包括script.aculo.us、Dojo、 YUI、ExtJS、Mootools、jQuery UI、Spry、Tibco GI、Qooxdoo;私有的包括ASP.NET Ajax、Dorado、Zimbra,等等,全部列举下来不下二三十个。Apple公司在这一年推出了iPhone手机,其中的Widget全部是使用 DHTML开发的,达到了交互体验的极致,同时也展示出Widget开发的美好前景。很多人说2007年是Widget年,这个说法是有道理的。到了 2008年,Google公司推出了Open Social,其核心是Google所提供的Widget开发API和Widget的部署容器。Google管这些Widget叫做Google Gadget,Gadget将Widget的开发水平推向了新的高度。

无论是基于现有的Widget开发Ajax功能,还是开发新的Widget,Prototype/script.aculo.us都是我们手中的一件利器。网上的很多投票都显示出,Prototype/script.aculo.us组合是最受欢迎的Ajax组件库。因为Prototype大受欢迎,其爱好者基于Prototype还开发了很多其他的组件库,Prototype家族成员的数量还在不断增加。很多Ajax组件库最大的问题是缺乏详尽的文档,API参考并不能代替文档。从文档的数量和质量来说,Prototype/script.aculo.us的文档在所有Ajax组件库中都是最棒的。本书的出版,为爱好者深入学习这两个组件库提供了极大的便利。本书的副标题是"You Never Know JavaScript Could Do This!",确实,正是通过这些优秀的Ajax组件库、这些勇于探索的Ajax顶尖高手们,我们才知道浏览器端脚本编程的世界原来是如此灿烂。基于 Prototype/script.aculo.us做Ajax开发是一种很愉快的体验。学习越深入,这种愉快的感觉就越强烈。那么,你还在等什么呢?

上海印客网首席架构师

2008年7月6日于上海

《JavaScript王者归来》序

       在人类漫漫的历史长河里,很难找到第二个由简单逻辑和抽象符号组合而成的,具有如此宏大信息量和丰富多彩内涵的领域。从某种意义上说,当你翻开这本书的时候,你已经踏入了一个任由你制定规则的未知世界。尽管你面对的仅仅是程序设计领域的冰山一角,但你将透过它,去领悟“道”的奥秘。在接下来的一段时间内,你会同我一起,掌握一种简单而优雅的神秘语言,学会如何将你的意志作用于它。这种语言中所蕴涵着的亘古之力,将为你开启通往神秘世界的大门……

1.1为什么选择JavaScript?

       在一些人眼里,程序设计是一件神秘而浪漫的艺术工作,对他们来说,一旦选定某种编程语言,就会像一个忠贞的信徒一样坚持用它来完成任何事情,然而我不是浪漫的艺匠,大多数人也都不是,很多时候我们学习一种新技术的唯一目的,只是为了把手中的事情做得更好。所以,当你面对一项陌生的技术时,需要问的第一个问题往往是,我为什么选择它,它对我来说,真的如我所想的那么重要吗?

好,让我们带着问题开始。

1.1.1 用户的偏好:B/S模式

       如果你坚持站在专业人员的角度,你就很难理解为什么B/S模式会那么受欢迎。如果你是一个资深的程序员,有时候你甚至会对那些B/S模式的东西有一点点反感。因为在你看来,浏览器、表单、DOM和其他一切与B/S沾边的东西,大多是行为古怪而难以驾驭的。以你的经验,你会发现实现同样的交互,用B/S来做通常会比用任何一种客户端程序来做要困难得多。

       如果你尝试站在用户的角度,你会发现为什么大多数最终用户对B/S模式却是如此的青睐。至少你不必去下载和安装一个额外的程序到你的电脑上,不必为反复执行安装程序而困扰,不必整天被新的升级补丁打断工作,不必理会注册表、磁盘空间和一切对普通用户来说有点头疼的概念。如果你的工作地点不是固定的办公室,你日常工作的PC也不是固定的一台或者两台,那么,B/S的意义对你而言或许比想象的还要大。

大多数情况下,客户更偏好使用浏览器,而不是那些看起来比较专业的软件界面,专业人员则恰恰相反。

       总之,用户的需求让B/S模式有了存在的理由,而迅速发展的互联网技术则加速了B/S应用的普及。随着一些优秀的Web应用产品出现,不但唤起了用户和业内人士对Ajax技术的关注,也令Web领域内的一个曾经被无数人忽视的脚本语言——JavaScript进入了有远见的开发人员和IT经理人的视线。于是在你的手边,也多了现在这本教程。

编写本书的时候,在TIOBE编程社区最新公布的数据中,JavaScript在世界程序开发语言中排名第十,这意味着JavaScript已经正式成为一种被广泛应用的热门语言。

1.1.2 在什么情况下用JavaScript

       一路发展到今天,JavaScript的应用范围已经大大超出一般人的想象,但是,最初的JavaScript是作为嵌入浏览器的脚本语言而存在,而它所提供的那些用以表示Web浏览器窗口及其内容的对象简单实用,功能强大,使得Web应用增色不少,以至于直到今天,在大多数人眼里,JavaScript 表现最出色的领域依然是用户的浏览器,即我们所说的Web应用的客户端。客户端浏览器的JavaScript应用也正是本书讨论的重点内容。

作为一名专业程序员,当你在面对客户的时候,经常需要判断哪些交互需求是适合于JavaScript来实现的。而作为一名程序爱好者或者是网页设计师,你也需要了解哪些能够带给人惊喜的特效是能够由JavaScript来实现的。总之一句话,除了掌握JavaScript本身,我们需要学会的另一项重要技能是,在正确的时候、正确的地方使用JavaScript。对于JavaScript初学者来说学会判断正确使用的时机有时候甚至比学会语言本身更加困难。

作为项目经理,我经常接受来自客户的抱怨。因此我很清楚我们的JavaScript在带给客户好处的同时制造了太多的麻烦,相当多的灾难是由被错误使用的 JavaScript引起的。一些代码本不应该出现在那个位置,而另一些代码则根本就不应当出现。我曾经寻访过问题的根源,发现一个主要的原因由于 JavaScript的过于强大(在后面的小节中我们将会提到,另一个同样重要的原因是“脚本诱惑”),甚至超越了浏览器的制约范围,于是麻烦就不可避免的产生了,这就像你将一个魔鬼放入一个根本就不可能关住它的盒子里,那么你也就无法预料魔鬼会做出任何超出预期的举动。

*  毫无疑问,正确的做法是:不要放出魔鬼。所以,JavaScript程序员需要学会的第一个技巧就是掌握在什么情况下使用JavaScript才是安全的。

       在什么情况下用JavaScript?给出一个简单的答案是:在任何不得不用的场合使用,除此以外,不要在任何场合使用!无懈可击的应用是不用,除非你确实无法找到一个更有效更安全的替代方案。也许这个答案会让读到这里的读者有些郁闷,但是,我要很严肃地提醒各位,由于JavaScript比大多数人想象的要复杂和强大得多,所以它也比大多数人想象得要危险得多。在我的朋友圈子里,许多资深的JavaScript程序员(包括我在内)偶尔也不得不为自己一时疏忽而做出的错误决定让整个项目团队在“脚本泥潭”中挣扎好一阵子。所以这个建议从某种意义上说也是专家们的血泪教训。最后向大家陈述一个令人欣慰的事实,即使是像前面所说的这样,在Web应用领域,JavaScript的应用范围也仍然是相当广泛的。

Ø         在本节的最后三个小节里,我们将进一步展开讨论关于JavaScript使用的话题。

1.1.3 对JavaScript的一些误解

       JavaScript是一个相当容易误解和混淆的主题,因此在对它进一步研究之前,有必要澄清一些长期存在的有关该语言的误解。

1.1.3.1 JavaScript和Java

这是最容易引起误会的一个地方,这个Java-前缀似乎暗示了JavaScript和Java的关系,也就是JavaScript是Java的一个子集。看上去这个名称就故意制造混乱,然后随之而来的是误解。事实上,这两种语言是完全不相干的。

JavaScript和Java的语法很相似,就像Java和C的语法相似一样。但它不是Java的子集就像Java也不是C的子集一样。在应用上,Java要远比原先设想的好得多(Java原称Oak)。

JavaScript 的创造者是Brendan Eich,最早的版本在NetScape 2中实现。在编写本书时,Brendan Eich在Mozilla公司任职,他本人也是JavaScript的主要革新者。而更加有名的Java语言,则是出自Sun Microsystems公司的杰作。

JavaScript最初叫做LiveScript,这个名字本来并不是那样容易混淆,只是到最后才被改名为JavaScript,据说起同Java相似的名字纯粹是一种行销策略。

尽管JavaScript和Java完全不相干,但是事实上从某种程度上说它们是很好的搭档。JavaScript可以控制浏览器的行为和内容,但是却不能绘图和执行连接(这一点事实上并不是绝对的,通过模拟是可以做到的)。而Java虽然不能在总体上控制浏览器,但是却可以绘图、执行连接和多线程。客户端的JavaScript可以和嵌入网页的Java applet 进行交互,并且能够对它执行控制,从这一意义上来说,JavaScript真的可以脚本化Java。

1.1.3.2披着C外衣的Lisp

JavaScript的C风格的语法,包括大括号和复杂的for 语句,让它看起来好像是一个普通的过程式语言。这是一个误导,因为JavaScript和函数式语言如Lisp和Scheme有更多的共同之处。它用数组代替了列表,用对象代替了属性列表。函数是第一型的。而且有闭包。你不需要平衡那些括号就可以用λ算子。

Ø         关于JavaScript闭包和函数式的内容,在本书的第23章中会有更详细的介绍。

1.1.3.3思维定势

JavaScript 是原被设计在Netscape Navigator 中运行的。它的成功让它成为几乎所有浏览器的标准配置。这导致了思维定势。认为JavaScript是依赖于浏览器的脚本语言。其实,这也是一个误解。 JavaScript也适合很多和Web无关的应用程序。

早些年在学校的时候,我和我的实验室搭档曾经研究过将JavaScript作为一种PDA控制芯片的动态脚本语言的可行性,而在我们查阅资料的过程中发现一些对基于嵌入式环境的动态脚本语言实现的尝试,我们有理由相信,JavaScript在某些特定的嵌入式应用领域中也能够表现得相当出色。

1.1.3.4业余爱好者

一个很糟糕的认知是:JavaScript过于简朴,以至于大部分写JavaScript的人都不是专业程序员。他们缺乏写好程序的修养。JavaScript有如此丰富的表达能力,他们可以任意用它来写代码,以任何形式。

事实上,上面这个认知是曾经的现实,不断提升的Web应用要求和Ajax彻底改变了这个现实。通过学习本书,你也会发现,掌握JavaScript依然需要相当高的专业程序员技巧,而不是一件非常简单的事情。不过这个曾经的现实却给JavaScript带来了一个坏名声──它是专门为外行设计的,不适合专业的程序员。这显然是另一个误解。

推广JavaScript最大的困难就在于消除专业程序员对它的偏见,在我的项目团队中许多有经验的J2EE程序员却对JavaScript停留在一知半解甚至茫然的境地,他/她们不愿意去学习和掌握JavaScript,认为这门脚本语言是和浏览器打交道的美工们该干的活儿,不是正经程序员需要掌握的技能。这对于Web应用开发来说,无疑是一个相当不利的因素。

1.1.3.5面向对象

JavaScript 是不是面向对象的?它拥有对象,可以包含数据和处理数据的方法。对象可以包含其它对象。它没有类(在JavaScript 2.0真正实现之前),但它却有构造器可以做类能做的事,包括扮演类变量和方法的容器的角色。它没有基于类的继承,但它有基于原型的继承。两个建立对象系统的方法是通过继承和通过聚合。JavaScript两个都有,但它的动态性质让它可以在聚合上超越。

一些批评说JavaScript不是真正面向对象的因为它不能提供信息的隐藏。也就是,对象不能有私有变量和私有方法:所有的成员都是公共的。但随后有人证明了JavaScript对象可以拥有私有变量和私有方法。另外还有批评说JavaScript不能提供继承,但随后有人证明了JavaScript不仅能支持传统的继承还能应用其它的代码复用模式。

说 JavaScript是一种基于对象的语言,是一种正确而略显保守的判断,而说JavaScript不面向对象,在我看来则是错误的认知。事实上有充足的理由证明JavaScript是一种的面向对象的语言,只是与传统的class-based OO(基于类的面向对象)相比,JavaScript有它与众不同的地方,这种独特性我们称它为prototype-based OO(基于原型的面向对象)。

Ø         关于JavaScript面向对象的内容,在本书的第21章中会有更详细的介绍。

1.1.3.6其他误解

       除了以上提到的几点之外,JavaScript还有许多容易令人迷惑和误解的特性,这些特性使得JavaScript成为世界上最被误解的编程语言。

Ø         如果读者对这方面有兴趣,可以详细阅读下面这篇文章

http://javascript.crockford.com/javascript.html  [Douglas Crockford]

1.1.4 警惕!脚本诱惑

       前面我们提到过,许多专业程序员拒绝去了解如何正确使用JavaScript,另一些则是缺乏对JavaScript足够的认知和应用经验。但是在B/S 应用中,相当多的情况下,要求开发人员不得不采用JavaScript。于是,一个问题产生了,大量的JavaScript代码拷贝出现在页面的这个或者那个地方,其中的大部分是不必要的,另一部分可能有缺陷。我们的开人员没有办法(也没有意识到)去判断这些代码是否必要,以及使用它们会带来哪些问题。

*  如果你的B/S应用中的JavaScript不是由专业的JavaScript程序员来维护的,那么当你对你的开发团队进行一次小小的代码走查时,你甚至可能会发现90%的JavaScript代码被错误地使用,这些错误使用的代码浪费了用户大量的网络带宽、内存和CPU资源,提升了对客户端配置的要求,降低了系统的稳定性,甚至导致许多本来可以避免的安全问题。

       由于浏览器的JavaScript可以方便地被复制粘贴,因此,一个特效或者交互方式往往在真正评估它的必要性之前便被采用——客户想要它,有人使用过它,程序员复制它,而它就出现在那儿,表面上看起来很完美,于是,所谓的脚本诱惑就产生了。

       事实上,在我们真正使用JavaScript之前,需要反复问自己一个重要问题是,究竟是因为有人想要它,还是因为真正有人需要它。在你驾驭 JavaScript马车之前,你必须学会抵制脚本诱惑,把你的脚本用在必要的地方,永远保持你的Web界面简洁,风格一致。

*  在用户眼里,简洁一致的风格与提供强大而不常用的功能和看起来很COOL而实际上没有什么功用的界面特效相比起来,前者更能令他们觉得专业。毕竟,大部分用户和你我一样,掌握一个陌生的环境和新的技能只是为了能够将事情做得更快更好。除非你要提供的是一个类似于Qzone之类的娱乐程序,你永远也不要大量地使用不必要的JavaScript。

1.1.5 隐藏在简单表象下的复杂度

       专业人员不重视JavaScript的一个重要原因是,他们觉得JavaScript是如此的简单,以至于不愿意花精力去学习(或者认为不用学习就能掌握)。前面提到过的,这实际上是一种误解。事实上,在脚本语言中,JavaScript属于相当复杂的一门语言,它的复杂程度未必逊色于Perl和 Python。

另一个业内的偏见是脚本语言都是比较简单的,实际上,一门语言是否脚本语言往往是它的设计目标决定的,简单与复杂并不是区分脚本语言和非脚本语言的标准。JavaScript即使放到非脚本语言中来衡量,也是一门相当复杂的语言。

       之所以很多人觉得JavaScript过于简单,是因为他们大量使用的是一些JavaScript中看似简单的文法,解决的是一些看似简单的问题,真正复杂而又适合JavaScript的领域却很少有人选择JavaScript,真正强大的用法很少被涉及。JavaScript复杂的本质被一个个简单应用的表象所隐藏。

       我曾经给一些坚持认为JavaScript过于简单的开发人员写过一段小代码,结果令他们中的大部分内行人大惊失色,那段代码看起来大致像下面这个样子:

var a = [-1,-1,1,-3,-3,-3,2,2,-2,-2,3,-1,-1];

function f(s, e)

{

       var ret = [];

       for(var i in s){

              ret.push(e(s[i]));

       }

       return ret;

}

var b = f(a, function(n){return n>0?n:0});

alert(b);

*  这是本书中出现的第一段JavaScript代码,也许现在你看来,它有那么一点点令人迷惑,但是不要紧,在本书后面的章节中,你会慢慢理解这段代码的含义以及它的无穷妙味。而现在你完全可以跳过它的实际内容,只要需要知道这是一段外表看起来简单的魔法代码就够了。

       因为这段代码而尖叫的不仅仅包括我的这些程序员朋友,事实上,更兴奋的是另一些电子领域的朋友,他们写信给我反馈说,在此之前他们从来没有见到过如此形式简洁而优雅的数字高通滤波器,更令人欣喜的是,它的阈值甚至是可调节的:

var b = f(a, function(n){return n>=-1?n:0});

如果你想要,它也很容易支持低通滤波:     

var b = f(a, function(n){return n<0?n:0});

       用一个小小的堆栈或者其他伎俩,你也可以构造出一族差分或者其他更为复杂的数字设备,而它们明显形式相近并且结构优雅。

       总之,不要被简单的表象所迷惑,JavaScript的复杂度往往很大程度上取决于你的设计思路和你的使用技巧。JavaScript的确是一门可以被复杂使用的程序设计语言。

1.1.6 令人迷惑的选择:锦上添花还是雪中送炭

       本节最后的这个话题在前面已经被隐讳地提到过多次,实际上,本小节围绕的话题依然是什么时候使用JavaScript。一种比较极端的观点是在必须的时候采用,也就是前面所说的不得不用的场合,另一种比较温和一点的观点则坚持在需要的时候使用,这种观点认为当我们可以依靠JavaScript令事情变得更好的时候,我们就采用它。

事实上,就我个人而言,比较支持“必须论”,这是因为从我以往的经验来看,JavaScript是难以驾驭的,太多的问题由使用JavaScript不当而产生,其中的一部分相当令人困扰,彻底解决它们的办法就是尽可能降低JavaScript的使用频率,也尽可能将它用在真正适合它的地方。当然万事没有绝对,在何时使用 JavaScript永远是一个难题,然而不管怎么说同“锦上添花”相比,JavaScript程序员也许应当更多考虑的是如何“雪中送炭”。

1.1.7回到问题上来

       本节要解决的问题是为什么选择JavaScript,然而在相当多的篇幅里,我们都在试图寻找一些少用和不用JavaScript的理由,尽管如此,抛开大部分不适合JavaScript的位置和时机,浏览器上依然会经常地见到JavaScript的身影,对于浏览器来说,JavaScript实在是一个不可缺少的修饰。

你再也找不到任何一种优雅简朴的脚本语言如此适合于在浏览器中生存。在本书的第2章,我们将具体接触嵌入浏览器中的JavaScript。

       最后,用一句话小结本节的内容——我们之所以选择JavaScript,是因为:Web应用需要JavaScript,我们的浏览器、我们的程序员和我们的用户离不开它。

为什么Applet不会回来

我想知道为什么Javascript能够战胜Applet,找到了如下答案:

From: http://www.javaeye.com/topic/208508

     在上个世纪90年代,sun和微软的关系曾经十分甜蜜,当时的applet也是前景一片光明。然而随后sun选择了挑战微软,除了指控微软向java添加非平台中立的特性以外,在Java1.2中,sun加入了平台无关的UI界面Swing.而也正是从java1.2开始,这个星球上最大的浏览器厂商决定对Java applet进行抵制,微软的jvm版本再没有超过1.1。而java1.2之后sun的jvm在ie上的拙劣表现,也最终导致applet的陨落。

      来到21世纪,sun想要通过新的java 6 update 10和JavaFX重夺RIA市场。然而这样的尝试最终也会失败,这与applet,flash,silverlight这些技术无关,applet终将失败的原因是:sun不是一个足够强大的公司,而这一点是毫无悬念的。

Ted Neward 的话,我觉得有一定道理,但是,基于很多原因(真的有很多),我认为Applet(JavaFX)还是有机会的。当然因为我本身对Java很有感情,所以也许主观了吧。

Thursday, December 4, 2008

cakephp+AJAX打造多级动态树形菜单~~

说是用cakephp,其实也没用到cakephp的ajax helper,只是喜欢cakephp的MVC和ORM功能~

敏捷开发日益被人关注~比起JAVA的struts、hibernate无比复杂的配置文件,cakephp的mvc和orm功能仅需要满足它的一些约定就行了~就像RoR的“约定大于配置”一样~

都说ajax是过渡时期的产品,我觉得很奇怪,ajax不就只是一个XMLHttpRequest么?难道JAVASCRIPT对DOM的操作也属于AJAX?如果是这样的话AJAX怎么会只是过渡时期的产品?

不说废话了,来看看我们的AJAX+CAKEPHP多级动态树形菜单吧~~

先建立数据库表,mysql下

DROP TABLE IF EXISTS `categorys`;
CREATE TABLE IF NOT EXISTS `categorys` (
`id` int(10) unsigned NOT NULL auto_increment,//主键
`parentid` int(10) unsigned NOT NULL,//树形菜单,父结点ID号
`path` varchar(200) NOT NULL,//访问路径
`ordernum` int(11) NOT NULL,//排序号,可能用得上
`subscount` int(10) unsigned NOT NULL,//子结点个数
`name` varchar(15) NOT NULL,//结点名字
`contentable` tinyint(1) NOT NULL,//该结点下是否有非结点内容标记
`workable` tinyint(1) NOT NULL,//该结点是否工作标记
PRIMARY KEY  (`id`),
UNIQUE KEY `path` (`path`),
UNIQUE KEY `name` (`name`)
) ENGINE=MyISAM  DEFAULT CHARSET=gbk AUTO_INCREMENT=53 ;

将树形菜单存放数据按上面字段注释的要求填入~

下面这个是前台代码:

<script src=”../js/prototype.js”></script>
<script type=”text/javascript”>
var childNodeId;
var childValue;
function gettype(nodeId,nodeValue){
childNodeId = nodeId;
childValue = nodeValue;
var temp;
temp=$(”node”+nodeValue+”"+nodeId).innerHTML;
if (temp==”"){
$(”node”+nodeValue+”"+nodeId).innerHTML=”<span align=\”center\”><img src=’../img/common/tree/load.gif’ />    数据读取中…</span><br>”;
getChildTree();
}
else {
showHide();
}
}

function getChildTree(){
var url = “/admin/listcates/”+childNodeId+”?timestamp=”+new Date().getTime();

var myAjax = new Ajax.Request(
url,{
method:”GET”,
onComplete: showResponse
}
);
}

function showResponse(xmlHttp){
var tmp = “node”+childValue+”"+childNodeId;
var tmpimg = “img”+childValue+”"+childNodeId;
$(tmp).innerHTML = xmlHttp.responseText;
$(tmpimg).src = “../img/common/tree/open.gif”;
}

function showHide(){
var tmp = “node”+childValue+”"+childNodeId;
var tmpimg = “img”+childValue+”"+childNodeId;
if($(tmp).style.display==”block” || $(tmp).style.display==”"){
$(tmp).style.display = “none”;
$(tmpimg).src=”../img/common/tree/close.gif”;
}
else{
$(tmp).style.display =”block”;
$(tmpimg).src=”../img/common/tree/open.gif”;
}
}

function addsubject(parentid){
var tmpvalue=prompt(’请输入新的分类名’,”);
if(tmpvalue){
var url = “/admin/newsubject/”+tmpvalue;
var pars = “parentid=”+parentid+”&timestamp=”+new Date().getTime();
var ajax = new Ajax.Request(
url,{
method: ‘get’,
parameters: pars,
onComplete:viewadd
}
);
}
}
function viewadd(xmlHttp){
alert(xmlHttp.responseText);
}

function delsubject(parentid){
var tmpvalue=confirm(’确定删除这个分类吗?’);
if(tmpvalue){
alert(parentid);
}
}
</script>
<table>
<div id=”treebody”>
<span id=”node00″></span>
<script language=”javascript”>gettype(0,0)</script>
</div>
</table>

后台主要代码如下:

function listcates($parentid){
$condition = array(’parentid’=>$parentid);
$db_cates = $this->Cate->findAll($condition);
//pr($db_cates);
header(’Content-Type:text/html;charset=GB2312′);
//$parentid+=1;
if($db_cates != null){
echo “<ul style=’list-style-type:none;’>”;
foreach($db_cates as $key=>$db_cate){
if($db_cate['Cate']['subscount']!=0){
$tmpimg = “/img/common/tree/close.gif”;
echo ”
<li onclick=’gettype({$db_cate['Cate']['id']},{$parentid})’>
<img id=’img{$parentid}{$db_cate['Cate']['id']}’ src=’{$tmpimg}’ />&nbsp;{$db_cate['Cate']['name']}

</li>
<div id=’node{$parentid}{$db_cate['Cate']['id']}’></div>
“;
}
else{
$tmpimg = “/img/common/tree/file.gif”;
echo ”
<li>
<img id=’img{$parentid}{$db_cate['Cate']['id']}’ src=’{$tmpimg}’ />&nbsp;{$db_cate['Cate']['name']}
&nbsp;<img src=’/img/common/tree/new.gif’ onclick=’addsubject({$db_cate['Cate']['id']})’/>
&nbsp;<img src=’/img/common/tree/del.gif’ onclick=’delsubject({$db_cate['Cate']['id']})’/>
</li>

“;
}
}
if($parentid!=0) echo “<li onclick=’addsubject({$parentid})’><img src=’/img/common/tree/add.gif’/>&nbsp;<small>增加分类< /small></li>”;
echo “</ul>”;
}
exit();
}

Wednesday, December 3, 2008

DOM example 2

<html>
<head>

<script type="text/javascript">
window.onload = function() {
var node = document.getElementById("aassdd");
node.onclick=getEventTrigger;
}

function getEventTrigger(evt)
  {

//  e = e¦¦event;
//alert("The source element's id is: "+ (e.srcElement¦¦e.target).id)

if(evt)
x= evt.target;
else //IE
x = window.event.srcElement;

//  x=e.srcElement¦¦event.target;

  alert("The id of the triggered element: "
  + x.id);
  x = x.nextSibling;
  x = x.lastChild;
  alert("The id of the triggered element: "
  + x.nodeValue);

  }
</script>

</head>
<body >

<p id="aassdd" onclick="getEventTrigger">Click on this paragraph. An alert box will show which element triggered the

event.</p><p id="aa" onclick="getEventTrigger">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.</p><p id="dd"

onclick="getEventTrigger">Click on this paragraph. An alert box willshow which element triggered the event.</p>

</body>
</html>

DOM example 1

<html>
<head>
<title>Page</title>
<script>
window.onload = function() {
var node = document.getElementById("testimage");
node.onclick=testFunction;
}

function testFunction(evt) {

if(evt)
alert("evt.target.id="+evt.target.id);
else //IE
alert("window.event.srcElement.id="+window.event.srcElement.id);
}
</script>

</head>

<body>
<img id="testimage" src='someimage.jpg'/>
</body>
</html>

精通JavaScript

http://www.17xie.com/read-60506.html

在上一次Web开发领域的技术浪潮中,文档对象模型(Document Object Model,DOM)无疑是开发者能用来提升用户体验的最重要技术之一。

用DOM脚本编程为页面增加分离式(unobtrusive)JavaScript,既能为你的用户提供各种各样时髦的增效,同时又不会影响那些使用不支持或禁用了JavaScript的浏览器的人们。此外,DOM脚本编程还能精细地分离和轻松地管理你的代码。

幸运的是,所有的现代浏览器都支持DOM和当前HTML文档的内置HTML DOM表达。有了这些后就可以通过JavaScript轻松访问它们,从而为现代Web开发者提供了巨大的便利。理解如何使用并尽情发挥这些技术,你的下一个Web应用程序将会领先一筹。

这一章探讨关于DOM的一系列话题。假定你是DOM新手,我们将从基础开始,并涉及所有重要的概念。而对已经熟悉了DOM的读者,我保证能提供一批你会喜欢的高级技术。

2.1 DOM简介

DOM是一个表达XML文档的标准(由W3C制定的)。它未必是最快的方式,也未必是最轻量级的或者最容易使用的,但确是应用最广泛的,大部分Web开发的编程语言(比如Java、Perl、PHP、 Ruby、Python和JavaScript)都提供了相应的DOM实现。DOM给开发者提供了一种定位XML层级结构的直观方法。就算你尚未完全熟悉 XML也没关系,HTML文档(实际上从浏览器角度来说就是XML文档)已经有一个可用的DOM表达,你会为此而备受鼓舞。

 

 

2.2 遍历DOM

可以把XML的DOM表达方式看作是一棵导航树。一切术语都跟家谱术语(父、子、兄弟)类似。而与家谱的不同之处在于,XML文档从一个独立的根节点(称作文档元素,document element)开始,它包含指向子节点的指针。每一个子节点都包含指针指向它的父节点、相邻节点和子节点。

DOM还使用了一些特殊的术语来描述XML树里的对象种类。DOM树中的每个对象都是一个节点(node),每个节点可以有一个类型(type),比如元素、文本或者文档。要进一步学习下去,我们必须了解 DOM文档是如何表现和如何定位的。请参考以下简单的HTML片段,让我们来研究研究它的DOM结构是如何工作的:

<p><strong>Hello</strong> how are you doing?</p>

这个片段的每一部分被分解为独立的DOM节点指针,指向其亲戚(父、子、兄弟)。使用图谱来表示的话,它应该如图5-1所示:片断的每部分(圆角框描述元素,直角框描述文本节点)和它的引用。

图5-1 节点间的关系

每个独立的DOM节点都包含指向它的相关节点的一系列指针。你需要使用这些指针来学习如何遍历DOM。所有可用的指针如图5-2所示。DOM节点指针的各个属性,分别是指向另一个DOM元素的指针(如果不存在则为null)。

图5-2 使用指针遍历DOM树

只需使用不同的指针,就可以定位页面上的任何元素或者文本块。要理解它在实际环境中是如何工作的,最好的方式就是实践一下。代码清单5-1是一个简单的HTML页。

代码清单5-1 简单HTML页面,或者说是简单的XML文档

    <title>Introduction to the DOM</title>

    <h1>Introduction to the DOM</h1>

    <p class="test">There are a number of reasons why the

DOM is awesome, here are some:</p>

    <ul>

      <li id="everywhere">It can be found everywhere.</li>

      <li class="test">It's easy to use.</li>

      <li class="test">It can help you to find what you want, really quickly.</li>

    </ul>

在这个文档中,根元素是

document.documentElement

根节点与其他DOM节点一样,同样拥有所有用于定位的指针。使用这些指针你就可以浏览整个文档,定位到任何一个你要找的元素。比如,要获取<h1>元素,你可能会使用如下方法:

//不起作用

document.documentElement.firstChild.nextSibling.firstChild

这是我们遇到的第一个难题:DOM指针不仅可以指向元素,也可以指向文本节点。在这里,代码并不会真正指向<h1>元素,而是指向了<title>元素。为什么会发生这样的事?原因在于XML存在一个有争议的地方:空格。或许你已经注意到,在

l    当试图只使用指针来遍历DOM时,精细编写的HTML标记居然会产生混乱。

l    仅使用DOM指针来定位文档可能会过于烦琐和不切实际。

l    通常,你不需要直接存取文本节点,只存取包含文本节点的元素就行了。

这让我们遇到了难题:还有其他更好的方式来寻找文档中的节点吗?当然有!在你的工具箱中创建一些辅助函数,你就可以有效提升现有的方法,并且让DOM的定位更为方便。

2.2.1 处理DOM中的空格

回到我们的HTML文档例子。之前,你试图定位到一个<h1>元素去,但由于捣乱的文本节点使得困难重重。如果只是一个比较独立的元素还好说,但是要继续找<h1>元素的下一个元素呢?你仍然会遭遇这个麻烦的空格bug,使你必须用.nextSibling.nextSibling 来跳过<h1>和<p>之间的行结束符,空格一个都不能少。代码清单5-2所示是一个用以处理空格bug的补救办法。这个特别的技术删除所有空格——当然只是DOM文档中的文本节点,让DOM的遍历更容易。这样做不仅不会对你的HTML渲染产生副作用,还会让定位DOM变得更容易。但是需要注意的是,这个函数的结果并不是持久的,它需要在HTML文档的每一次加载时都重新执行一遍。

代码清单5-2 XML文档空格bug的补救方案

function cleanWhitespace( element ) {

  // 如果不提供参数,则处理整个HTML文档

  element = element || document;

  // 使用第一个子节点作为开始指针

  var cur = element.firstChild;

  // 一直到没有子节点为止

  while ( cur != null ) {

    // 如果节点是文本节点,并且只包含空格

    if ( cur.nodeType == 3 && ! /\S/.test(cur.nodeValue) ) {

      // 删除这个文本节点

      element.removeChild( cur );

      // 否则,它就是一个元素

    } else if ( cur.nodeType == 1 ) {

      // 递归整个文档

      cleanWhitespace( cur );

    }

    cur = cur.nextSibling; // 遍历子节点

  }

}

假设你要在前面例子中使用这个函数来查找位于<h1>元素后的元素。那么代码应该类似这样:

cleanWhitespace();

// 查找H1元素

document.documentElement

.firstChild // 查找Head元素

.nextSibling // 查找

.firstChild // 得到H1元素

.nextSibling // 得到相邻的段落(p)

该技术有优点也有缺点。最大的优点在于,你可以保证DOM文档的遍历在一定程度上的稳定性。但明显性能太差,想想必须遍历每个DOM元素和文本节点,目的只是为了找出包含空格的文本节点。假设你有一个包含大量内容的文档,它可能会严重降低网站的加载速度。此外,每次为文档注入新的HTML,你都需要重新扫描 DOM中的新内容,确保没有增加新的有空格填充的文本节点。

此外此函数重要的方面是节点类型的使用。节点的类型可以由检查它的nodeType属性来确定。可能会出现好几种值,但你经常会碰到的是以下3个:

l    元素(nodeType=1):匹配XML文件中的大部分元素。比如,<li>、<a>、<p>和

l    文本(nodeType=3):匹配文档内的所有文本块。当使用previousSibling和nextSibling来遍历DOM结构时,你会经常碰到元素内和元素间的文本块。

l    文档(nodeType=9):匹配文档的根元素。比如,在HTML文档内,它是

此外,你可以用常量来表明不同的DOM节点类型(但只是对非IE浏览器有用)。与其去记住1、3或9,还不如直接直观地使用document.ELEMENT_NODE、 document.TEXT_NODE或者document.DOCUMENT_NODE。因为经常清空DOM的空格会让人感到厌烦,所以你应该探索其他遍历DOM结构更有效的方法。

2.2.2 简单的DOM遍历

可以使用纯粹的DOM遍历规则(每个遍历方向都有指针)来开发一些更适合你的HTMLDOM文档遍历函数。大部分Web开发者在大多数情况下仅仅需要遍历DOM元素而非相邻的文本节点,该规则就是基于这样的事实而制定的。以下一系列的辅助函数可以帮助你,它们能够取代标准的previousSibling、nextSibling、firstChild、 lastChild和parentNode。代码清单5-3展示的函数,返回的是当前元素的前一个元素,如果前一个元素不存在则是null,类似于元素的 previousSibling属性。

代码清单5-3 查找相关元素的前一个兄弟元素的函数

function prev( elem ) {

  do {

    elem = elem.previousSibling;

  } while ( elem && elem.nodeType != 1 );

  return elem;

}

代码清单5-4展示的函数,返回的是当前元素的下一个元素,如果下一个元素不存在则是null,类似于元素的nextSibling属性。

代码清单5-4 查找相关元素的下一个兄弟元素的函数

function next( elem ) {

  do {

    elem = elem.nextSibling;

  } while ( elem && elem.nodeType != 1 );

  return elem;

}

代码清单5-5展示的函数,返回的是当前元素的第一个子元素,类似于firstChild元素属性。

代码清单5-5 查找元素第一个子元素的函数

function first( elem ) {

  elem = elem.firstChild;

  return elem && elem.nodeType != 1 ?

next ( elem ) : elem;

}

代码清单5-6展示的函数,返回的是当前元素的最后一个子元素,类似lastChild元素属性。

代码清单5-6 查找元素最后一个子元素的函数

function last( elem ) {

  elem = elem.lastChild;

  return elem && elem.nodeType != 1 ?

  prev ( elem ) : elem;

}

代码清单5-7展示的函数,返回当前元素的父元素,类似parentNode元素属性。你可以一次用一个数字来操纵多个父元素,例如parent(elem,2)就等同于parent(parent(elem))。

代码清单5-7 查找元素父元素的函数

function parent( elem, num ) {

num = num || 1;

for ( var i = 0; i < num; i++ )

if ( elem != null ) elem = elem.parentNode;

return elem;

}

使用这些新函数你就可以迅速遍历DOM文档了,而且不必再为元素间的文本操心。比如,需要查找<h1>元素的下一个元素,现在可以这么做了:

// 查找<h1>元素的下一个元素

next( first( document.body ) )

在这段代码内有两件事值得注意。首先,此处有一个新的引用:document.body。所有的现代浏览器都在HTML DOM文档内通过body属性提供一个对

2.2.3 绑定到每一个HTML元素

Firefox和Opera中存在一个强大的对象原型(object prototype)叫HTMLElement,它允许你为每一个HTML DOM元素绑定函数和数据。上一节所描述的函数显得特别愚钝,但它们应该可以更清晰的。一个完美的方案是,为HTMLElement的原型直接绑定函数,由此每个独立的HTML DOM元素也直接绑定了你的函数。为了能让上一节描述的函数继续有效,你必须做出3个改变:

(1)  你需要在函数的顶部增加一行代码,使用this来引用元素,而不是从参数的变量中获取它。

(2) 你需要删除不再需要的元素参数。

(3)  你需要把函数绑定到HTMLElement原型,由此DOM中的各个HTML元素可以使用该函数。

新的next函数如代码清单5-8所示。

代码清单5-8 为所有HTML DOM元素动态绑定新的DOM遍历函数

HTMLElement.prototype.next = function() {

  var elem = this;

  do {

    elem = elem.nextSibling;

  } while ( elem && elem.nodeType != 1 );

  return elem;

};

现在你可以这样来使用新的next函数(和经过同样调整后的其他函数)了:

// 一个简单的例子 —— 获取<p>元素

document.body.first().next()

这让你的代码更加清晰和易于理解。现在你可以按照自然想法的顺序来编写代码了,JavaScript总体上也更清晰了。如果你对这种编程风格感兴趣,我特别推荐尝试一下jQuery JavaScript库(http://jquery.com),它大量使用到这种技术。

注意 因为只有3个现代浏览器(Firefox、Safari和Opera)支持HTMLElement,要让它也能在IE中工作,必须给予特别的处理。有一个可以直接使用的库,由Jason Karl Davis(http://browser-land.org)编写,它为两个不支持的浏览器提供了访问HTMLElement的方法。可以从这找到该库的具体信息:http://www.browserland.org/scripts/htmlelement/.

2.2.4 标准的DOM方法

所有的现代DOM实现都包含一系列让编程更轻松的方法。结合一些自定义函数,遍历DOM可以是一种平滑的体验。那么让我们从JavaScript DOM的两个强大方法开始:

l    getElementById("everywhere"):该方法只能运行在document对象下,从所有中找出ID是everywhere的元素。这是一个非常强大的函数,并且是迅速的访问一个元素的最快方法。

l    getElementsByTagName("li"):该方法能运行在任何元素下,找出该元素下的所有标签名为li的后代元素,并返回一个NodeList(节点列表)。

警告 getElementById本来会按照你的预期工作:查找所有元素,揪出一个包含id的属性,且值为指定值的元素。但是,如果你加载一个远程XML 文档,并使用getElementById(或是使用除了JavaScript之外的实现DOM的语言),它默认并不会使用id属性。这是由于设计上的问题,一个XML文档如需明确指出id属性是什么,通常使用XML的定义或配置。

警告 getElementsByTagName返回一个节点NodeList.该结构跟普通的JavaScript数组非常相似,但一个重要的不同之处在于:它并不支持.push()、.pop()、.shift()等JavaScript数组的常用方法。使用getElementsByTagName时请务必记住这点,这能把你从很多混乱中解救出来。

这两个方法在所有现代的浏览器都可用,对定位到指定元素有非常大的帮助。回头看看我们前面尝试查找<h1>元素的例子,现在可以这么做了:

document.getElementsByTagName("h1")[0]

这行代码保证有效并返回文档中的第一个<h1>元素。再回头看看例子文档,假设你要获取所有<li>元素并为之加上边框。

var li = document.getElementsByTagName("li");

for ( var j = 0; j < li.length; j++ ) {

  li[j].style.border = "1px solid #000";

}

最后,需要使第一个<li>元素的文本加粗,假设它已经有了一个关联它的id:

document.getElementById("everywhere").style.fontWeight = 'bold';

你可能已经注意到获取指定ID的单个元素需要冗长的代码,依靠标签名获取元素也如此。其实你可以封装一个函数来简化获取的过程:

function id(name) {

  return document.getElementById(name);

}

代码清单5-9展示了一个函数,它依靠HTML DOM内的文档标签来定位元素。该函数带一个或两个参数。如果只传入一个标签名作为参数,函数将会遍历整个文档。另外你可以传入一个DOM上下文元素作为第一个参数的辅助,则函数只遍历该参数元素下的标签。

代码清单5-9 依靠HTML DOM文档标签定位元素的函数

function tag(name, elem) {

  // 如果不提供上下文元素,则遍历整个文档

  return (elem || document).getElementsByTagName(name);

}

让我们重新回到寻找<h1>元素后的第一个元素的问题。十分幸运的是,现在的代码可以比之前任何一次的都要精炼:

// 查找<h1>元素后的第一个元素

next( tag("h1")[0] );

以上这些函数为你在DOM文档中获取元素提供了强大而迅速的解决方法。在进一步学习修改DOM的方法之前,首先需要大致了解脚本首次执行时DOM的加载问题。

 

2.3 等待HTML DOM的加载

处理HTML DOM文档存在的一个难题是,JavaScript可以在DOM完全加载之前执行,这会给你的代码引发不少的潜在问题。浏览器的渲染和操作顺序大致如以下列表:

l    HTML解析完毕。

l    外部脚本和样式表加载完毕。

l    脚本在文档内解析并执行。

l    HTML DOM完全构造起来。

l    图片和外部内容加载。

l    网页完成加载。

在网页头部并且从外部文件加载的脚本会在HTML真正构造之前执行。如前所述,这是个至关重要的问题,因为这两处执行的脚本并不能访问还不存在的DOM。幸好,我们还有若干的补救办法。

2.3.1 等待整个页面的加载

目前,最常用的技术是完全等待整个页面加载完毕才执行DOM操作。这种技术只需利用window对象的load事件来绑定一个函数,页面加载完毕即可触发。我们将会在第6章更详细地讨论事件。代码清单5-10展示了一个页面完成加载后执行DOM相关代码的例子。

代码清单5-10 使用addEvent函数为window.onload属性绑定回调函数

// 直到页面加载完毕

// (使用的addEvent, 下一章会有阐述)

addEvent(window, "load", function() {

    // 执行HTML DOM操作

    next( id("everywhere") ).style.background = 'blue';

});

最简单的操作却是最慢的。在加载过程的顺序列表中,你会注意到页面的加载完毕与否完全被最后一步所掌控。这就是说,如果你页面有很多的图片、视频等,用户可能得等上一段时间JavaScript才执行。

2.3.2 等待大部分DOM的加载

第二种技术相当绕弯子,所以并不是十分推荐使用。如果你还记得,前面说过行内的脚本在DOM构造后就可立即执行。这是准真理。只有在DOM构造后,执行到该位置上脚本才真正执行。这意味着在你在页面中途嵌入的行内脚本只能访问该位置之前的DOM。所以,在页面最后元素之前嵌入脚本,你就可以在DOM中访问这个元素之前全部的元素,成为一条模拟DOM加载的伪路子。该方法典型的实现如代码清单5-11所示。

代码清单5-11 依靠在HTML DOM最后插入一个

    <h1>Testing DOM Loading</h1>

    <!-- 这里是大量的HTML -->

在这个例子中,你必须把行内脚本作为DOM 的最后一个元素,以便它最后一个被解析和执行。它唯一执行的是初始化函数,该函数应该包含你需要操作的DOM的相关代码。这个解决方案的最大问题在于混乱:你为HTML增添无关标记的理由仅仅是检查DOM是否已经执行。该技术通常被认为是不合理的,因为你为页面增加额外的代码的目的只是检查加载状态而已。

2.3.3 判断DOM何时加载完毕

最后一种技术可用以监听DOM加载状态,可能是最复杂的(从实现角度来看),但也是最有效的。在该技术中,你既能像绑定window加载事件那般简单,又能获得行内脚本技术那样的速度。

这项技术在不堵塞浏览器加载的情况下尽可能快地检查HTML DOM文档是否已经加载了执行所必须的属性。以下是检查HTML DOM是否可用的几个要点:

(1) document:你需要知道DOM文档是否已经加载。若能足够快地检查,运气好的话你会看到undefined。

(2) document.getElementsByTagName和documents.getElementById:频繁使用docum- ent.getElementsByTagName和document.getElementById函数检查文档,当存在这些函数则表明已完成加载。

(3) document.body:作为额外补充,检查

使用这些检查就足够判断DOM是否可用了(“足够”在此表示可能会有一定毫秒级的时间差)。这个方法几乎没有瑕疵。单独使用前述检查,脚本应该可以在现代浏览器中运行得相对良好。但是,最近 Firefox实现了缓存改进,使得window 加载事件实际上可以在脚本能检查到DOM是否可用之前触发。为了能发挥这个优势,我同时为window加载事件附加检查,以期能获得更快的执行速度。

最后,domReady函数集合了所有需要在DOM可用时就执行的函数的引用。一旦DOM被认为是可用的,就调用这些引用并按顺序一一执行。代码清单5-12展示了一个监听DOM何时加载完毕的函数。

代码清单5-12 监听DOM是否可用的函数

function domReady( f ) {

  // 假如 DOM 已经加载,马上执行函数

  if ( domReady.done ) return f();

  // 假如我们已经增加了一个函数

  if ( domReady.timer ) {

    // 把它加入待执行函数清单中

    domReady.ready.push( f );

  } else {

    // 为页面加载完毕绑定一个事件,

    // 以防它最先完成。使用addEvent(该函数见下一章)。

    addEvent( window, "load", isDOMReady );

    // 初始化待执行函数的数组

    domReady.ready = [ f ];

   // 尽可能快地检查DOM是否已可用

    domReady.timer = setInterval( isDOMReady, 13 );

  }

}

// 检查DOM是否已可操作

function isDOMReady() {

  // 如果我们能判断出DOM已可用,忽略

  if ( domReady.done ) return false;

  // 检查若干函数和元素是否可用

  if ( document && document.getElementsByTagName &&

document.getElementById && document.body )

{

    // 如果可用,我们可以停止检查

    clearInterval( domReady.timer );

    domReady.timer = null;

    // 执行所有正等待的函数

    for ( var i = 0; i < domReady.ready.length; i++ )

      domReady.ready[i]();

    // 记录我们在此已经完成

    domReady.ready = null;

    domReady.done = true;

  }

}

现在该来看看这在HTML文档中是如何执行的。 domReady函数应该按addEvent函数(第6章将讨论)的方式使用,文档可操作时即绑定你需要执行的函数。代码清单5-13展示了如何使用 domReady函数来监听DOM是否可用。在这个例子中,假设已经将domReady函数写到一个名为domready.js的外部文件中。

代码清单5-13 使用domReady函数来确定何时DOM可操作和修改

    <title>Testing DOM Loading</title>

  <h1>Testing DOM Loading</h1>

  <!--这里是大量的HTML -->

现在你已经掌握若干定位一般XML DOM文档与如何补救HTML DOM文档加载难题的方法,另一个问题也该摆上台面了:还有更好的方法来查找HTML文档中的元素吗?当然,答案是十分肯定的。

2.4 在HTML文档中查找元素

在HTML文档中与在XML文档中查找元素通常情况下还是有很大不同的。现代HTML事实上是XML的一个子集,这么说来似乎有点自相矛盾。但HTML文档包含了许多本质的不同,而这正是你可以充分利用的地方。

对JavaScript/HTML开发者来说,最重要的两个优势是利用类和CSS选择器(selector)。将这熟记于心,你就可创建一系列强大的函数,使DOM的操作更简单、更易理解。

2.4.1 通过类的值查找元素

通过类名字定位元素是一种很普遍的技术,2003年由Simon Willison(http://simon.incutio. com)普及,而原创的则是Andrew Hayward(http://www.mooncalf.me.uk)。该技术直截了当:遍历所有的元素(或者某元素的所有后代元素)直至找到指定类的元素。一种可能的实现方法如代码清单5-14所示。

代码清单5-14 找出全部有指定类值的元素的函数

function hasClass(name,type) {

  var r = [];

  // 定位到类值上(允许多类值)

  var re = new RegExp("(^|\\s)" + name + "(\\s|$)");

  // 限制类型的查找,或者遍历所有元素

  var e = document.getElementsByTagName(type || "*");

  for ( var j = 0; j < e.length; j++ )

    // 如果元素拥有指定类,把它添加到函数的返回值中

    if ( re.test(e[j]) ) r.push( e[j] );

  // 返回符合的元素列表

  return r;

}

现在你就可使用这个函数来在任意的或者指定类型(比如<li>或<p>)的元素中迅速查找有指定类值的元素。指定标签名会比遍历所有元素(*)的查找更快,因为需检索的目标更少。比如,在我们的HTML文档中,如果需要找出类值有test的所有元素你可以这么做:

hasClass("test")

如果只需找类值有test的<li>元素,则可以这么做:

hasClass("test","li")

当然,如果只需找出类值有test的<li>元素的第一个,则可以这么做:

hasClass("test","li")[0]

这个函数本身就已非常强大,一旦结合getElementById和getElementsByTagName,你就可以创建更强大的工具集合解决大部分棘手的DOM问题了。

2.4.2 使用CSS选择器查找元素

作为一个Web开发者,你应该已经知道选择HTML 元素的一种方案:CSS选择器。CSS选择器是用于赋予元素样式的表达式。随着CSS标准(1、2和3)的每次修订,选择器规范增加了越来越多的重要特点,由此开发者更容易精确定位到所需的元素。不幸的是,浏览器对CSS 2和CSS 3的实现慢得不可思议,所以你或许并不知道CSS中某些新奇酷的特点。如果你对这些感兴趣,建议你浏览W3C的这些主题页面:

l    CSS 1选择器:http://www.w3.org/TR/REC-CSS1#basic-concepts。

l    CSS 2选择器:http://www.w3.org/TR/REC-CSS2/selector.html。

l    CSS 3选择器:http://www.w3.org/TR/2005/WD-css3-selectors-20051215/。

每种选择器规范可用的特点基本上都差不多,每一个后续的版本也都包含前一版的所有特点,同时,每次新版都会增加一系列的新特点。举个例子,CSS 2包含了特性(attribute)选择器和子选择器,而CSS 3则提供了额外的语言支持、性质类型选择以及逻辑非等。比如,以下所有这些都是正确的CSS选择器。

l    #main<div>p:该表达式查找一个id为main的元素下所有的<div>元素,然后是这些元素下所有的<p>元素。它们都是CSS 1下的选择器。

l    div.items>p:该表达式查找所有的类值为items的<div>元素,然后定位到所有的子<p>元素。这是正确的CSS 2选择器。

l    div:not(.items):这则定位到所有没有值为items的类的所有<div>元素。这是正确的CSS 3选择器。

现在,你可能会觉得疑惑,实践中如果并不能使用它们来定位元素(而只能赋予样式)的话,为何会在此讨论CSS选择器。实际上很多前卫的开发者已经涉足开发能兼容CSS1甚至完全支持CSS 3的选择器的实现。使用这些库能让你顺利、方便地选择任意元素并对它们进行操作。

1.cssQuery

第一个完全支持CSS 1-3可用的公开库叫cssQuery,由Dean Edwards(http://dean.edwards.name)创立。其背后的动机很简单:你提供CSS选择器,cssQuery帮你找出所有匹配的元素。此外,cssQuery可以分解成多个子库,有一个是每个CSS选择器“管理员”,它甚至能执行CSS 3的语法,如果有必要的话。这个独特的库非常复杂但能运行在所有现代浏览器上(Dean是一位跨浏览器支持的忠实拥趸)。要应用整个库,你需要提供选择器,也可选择加入上下文元素以便加快搜索。以下是例子:

// 查找所有<div>元素的子<p>元素

cssQuery("div > p");

// 查找所有的<div>, <p>和<form>元素

cssQuery("div,p,form");

// 查找所有<p>元素和<div>元素,然后查找在这些元素内的a元素

var p = cssQuery("p,div");

cssQuery("a",p);

执行cssQuery函数会返回匹配的元素列表。由此你就可以像使用getElementsByTagName一样对元素进行操作。比如,为所有指向Google的链接增加边框,可按如下方法做:

// 为所有指向Google的链接增加边框

var g = cssQuery("a[href^='google.com']");

for ( var i = 0; i < g.length; i++ ) {

  g[i].style.border = "1px dashed red";

}

关于cssQuery的更多信息可以在Dean Edwards的网站上获取,那里同时也提供完整的源码下载:http://dean.edwards.name/my/cssQuery/.

提示 Dean Edwards是一位JavaScript魔术师,他的代码令人惊讶。强烈建议你稍加浏览他的cssQuery库,看看他的JavaScript扩展性写得多么好。

2.jQuery

尽管jQuery是JavaScript库世界的新成员,但它提供了一些新颖而引人注目的JavaScript编程方式。我最初只想把jQuery写成一个“简便”的CSS选择器库,类似于 cssQuery,直到Dean Edwards发布了卓越的cssQuery库,迫使我开辟了另一个不同的方向。这个库提供了完整的CSS 1-3的支持,同时也有基本的XPath支持。在此之上,它还提供了进一步定位和操作DOM的能力。跟cssQuery一样,jQuery完全支持现代浏览器。以下是使用混合CSS和XPath的jQuery自定义方式来选择元素的例子:

// 查找所有类值为'links', 同时内有p元素的<div>元素

$("div.links[p]")

// 查找所有<p>, <div>元素的所有子孙元素

$("p,div").find("*")

// 查找指向Google的所有偶数链接

$("a[@href^='google.com']:even")

现在,如需进一步使用jQuery元素选择返回的结果,你有两种方式。第一,你可以运行$("expression").get()来获取匹配元素的列表——这跟cssQuery完全一致。第二,你可以使用jQuery内置的特殊函数来操作CSS和DOM。所以,回到使用cssQuery为指向Google的链接加边框的例子上,你现在就可以这么做:

// 为所有指向Google的链接增加边框

$("a[@href^=google.com]").css("border","1px dashed red");

你可以从jQuery项目网站上找到大量的例子、演示和文档等,此外,还有可定制的下载:http://jquery.com/。

注意 应该指出的是,cssQuery或jQuery实际上并不只是能定位HTML文档而已,它们可以在任何XML文档上使用。至于纯粹的XML形式定位,请继续阅读下面的XPath部分。

2.4.3 XPath

XPath表达式是一种不可思议的定位XML文档的强大的方式。自问世几年来,几乎可以肯定DOM实现的背后必有XPath。尽管相对冗长,但XPath表达式比起CSS选择器来,能做的事情更多也更强大。表5-1并行比较了一些CSS选择器和XPath表达式的某些不同。

表5-1 CSS 3选择器与XPath表达式的比较

目 标

CSS 3

XPath

所有元素

*

//*

所有<p>元素

p

//p

所有子元素

p> *

//p/*

由ID获取元素

#foo

//*[@id='foo']

由类获取元素

.foo

//*[contains(@class, 'foo')]

由特性(attribute)获取元素

*[title]

//*[@title]

<p>的第一个子元素

p >*:first-child

//p/*[0]

所有拥有子元素的<p>

不支持

//p[a]

下一个元素

p + *

//p/下一兄弟元素::*[0]

如果前面这些表达式激发了你的兴趣,推荐你浏览XPath的两个规范(尽管现代浏览器通常只完全支持XPath 1.0)以初步了解它们是如何工作的:

l    XPath 1.0:http://www.w3.org/TR/xpath/。

l    XPath 2.0:http://www.w3.org/TR/xpath20/。

如果需要深入这个主题,我推荐你阅读O’Reilly出版的由Elliotte Harold和Scott Means所著的XML in a Nutshell2004),或者Apress出版的由Jeni Tennison所著的Beginning XSLT 2.0: From Novice to Professional2005)。此外,还有一些精彩的教程帮助你学习使用XPath:

l    W3Schools的XPath教程:http://w3schools.com/xpath/。

l    ZVON XPath 教程:http://zvon.org/xxl/XPathTutorial/General/examples.html。

目前,浏览器对XPath的支持各不相同:IE和 Mozilla都比较完整(虽然不一样)地支持XPath实现,而Safari和Opera的版本还在开发中。作为补救,有几种完全使用 JavaScript写的XPath实现方法。虽然它们通常会比较慢(跟基于浏览器的实现相比),但可以确保所有现代浏览器都能稳定运行:

l    XML for Script:http://xmljs.sf.net/。

l    Google AJAXSLT:http://goog-ajaxslt.sf.net/。

此外,一个名为Sarissa的项目(http://sarissa.sf.net/)打算为各种浏览建立一个通用的实现包装。它可以让你一次写成XML访问代码,同时仍能从支持的浏览器中获得更快的运行速度。这项技术的最大问题在于,Opera和Safari浏览器对XPath支持仍不足,走不出XPath先前实现的困境。

使用浏览器内置的XPath与使用得到广泛支持的纯粹的JavaScript解决方案比起来,通常被认为是试验性质的技术。尽管如此,XPath的使用和普及渐渐成长起来,可把它当作CSS选择器强有力的竞争对手。

现在你已经掌握了定位任意DOM元素或任意DOM元素集合的必要知识和工具,那么我们现在应该利用这些知识了:从特性的操作到增加和删除DOM元素。

2.5 获取元素的内容

所有的DOM元素无外乎包含以下三者之一:文本、元素、文本与元素的混合。一般来说,常见的是第一种和最后一种。在这一节中你将了解现有的获取元素内容的常用方法。

2.5.1 获取元素内的文本

文本框:  
图5-3 包含元素与文本的DOM结构例子获取元素内的文本可能会令DOM新手最容易迷惑,同时它也配合了HTML DOM文档和XML DOM文档。在图5-3所示的DOM结构例子中,有一个<p>主元素,它包含一个<strong>元素和一段文本块。<strong>元素本身同时包含文本块。

让我们来看看如何获取这些元素中的文本。<strong>元素很简单,因为它除了文本节点没别的干扰信息。

应该注意的是,所有非基于Mozilla的浏览器中都有一个叫作innerText的属性,可用它来获取元素内的文本。这十分方便。不幸的是,因为它在一个比较流行的浏览器上并不可用,同时在XML DOM文档中也不行,你还是需要开发一个可行的替代方案。

获取元素内文本内容的一个窍门是,需要记住元素并不是直接包含文本的,而包含在一个子文本节点中,这可能有点让人不习惯。假设变量strongElem包换一个<strong>元素的引用,让我们来看看代码清单5-15是如何使用DOM从元素内抽出文本的。

代码清单5-15 获取<strong>元素的文本内容

// 非Mozilla浏览器:

strongElem.innerTex

// 其他的浏览器:

strongElem.firstChild.nodeValue

了解如何从单个元素中获取文本内容后,你需要继续学习如何从<p>元素中获取经过组合的文本内容。为此,你或许需要开发一个通用的函数来获取任意元素的文本内容,而不用考虑元素究竟嵌套了几层,如代码清单5-16所示。调用text()将返回元素及其所有后代元素包含的文本内容。

代码清单5-16 一个获取元素文本内容的通用函数

function text(e) {

  var t = "";

  // 如果传入的是元素,则继续遍历其子元素,

  // 否则假定它是一个数组

  e = e.childNodes || e;

  // 遍历所有字节点

  for ( var j = 0; j < e.length; j++ ) {

    // 如果不是元素,追加其文本值

    // 否则,递归遍历所有元素的子节点

    t += e[j].nodeType != 1 ?

      e[j].nodeValue : text(e[j].childNodes);

  }

  // 返回匹配的文本

  return t;

}

使用这个能够获取任何元素文本内容的函数,在上面这个例子中,你就可以从<p>元素中获取它的文本内容了,代码大致如下:

// 获取<p>元素的文本内容

text( pElem );

这个函数还有一个显著的好处,那就是它可以确保在HTML和XML DOM文档中都能工作,那就是说你现在拥有了一个能获取任何元素文本内容的兼容方法。

2.5.2 获取元素内的HTML

与获取元素内的文本比起来,获取元素内的HTML是一项更容易上手的DOM任务。令人高兴的是,由于一个由IE小组开发的特色方法,所有的现代浏览器现在都实现了HTML DOM元素的一个额外属性:innerHTML。使用这个属性你就可以从一个元素中提取所有HTML和文本了。此外,使用innerHTML十分快——通常会比递归查找元素内的所有文本内容快得多。但是并非事事称心如意。它取决于浏览器如何去实现innerHTML属性,而且因为并没有标准,每种浏览器都可以返回它自以为是的内容。例如,以下是一些使用innerHTML属性的怪异bug:

l    基于Mozilla的浏览器在innerHTML声明中并不会返回<style>元素。

l    IE返回的元素字符都是大写的,如果你想保持一致性可能会感到失望。

l    innerHTML作为一个只能用在HTML DOM文档的元素中的属性,若在XML DOM文档中使用的话只会返回null值。

使用innerHTML属性是很直观的,存取该属性会返回一个包含该元素的HTML内容的字符串。如果元素仅包含文本而不包含任何子元素,则返回的字符串只包含文本。我们通过调试图5-2中两个元素来看它是如何工作的:

// 获取<strong>元素的innerHTML

// 应返回"Hello"

strongElem.innerHTML

// 获取<p>元素的innerHTML

// 应返回"<strong>Hello</strong> how are you doing?"

pElem.innerHTML

如果你确定元素只包含文本,这个方法也可用来取代前面那个获取元素文本的复杂方法,并且十分简便。另一方面,能获取元素内的HTML内容,你就可以利用就地编辑的特点来构建一些动态的时髦应用。在第10章我们会讨论这个主题。

2.6 操作元素特性

几乎跟获取元素内容一样,获取和设置元素特性(attribute)的值也是最频繁的操作之一。通常,元素自带的特性列表会随着元素自身的XML表示信息预先加载,并存储成关联数组备用,例如以下这个网页的HTML片段:

<form name="myForm" action="/test.cgi" method="POST">

...

</form>

一旦加载到DOM中,表示HTML表单元素的变量formElem就会有一个关联数组,它是名/值特性对。其结果大致如下:

formElem.attributes = {

  name: "myForm",

  action: "/test.cgi",

  method: "POST"

};

检查元素特性是否存在对于使用特性数组来说是最简单不过的,但还是存在一个问题:不知何故Safari不支持。最为严重的是,IE也不支持可能是最有用的hasAttribute函数。那么如何才能检查某个特性是否存在呢?一个可行的办法是使用getAttribute函数(我将会在下一节讲述)并且检查返回值是否为null,如代码清单5-17所示。

代码清单5-17 检查元素是否有用一个指定的特性

function hasAttribute( elem, name ) {

  return elem.getAttribute(name) != null;

}

有了这个函数,并了解特性如何使用,那么你就可以获取和设置特性的值了。

获取和设置特性的值

根据你所使用的DOM文档的类型,从元素中获取特性的信息有两种方法。如果你希望保险并且总是兼容通用XML DOM的方式,那么可用getAttribute和setAttribute。可以使用如下方法:

// 获取特性

id("everywhere").getAttribute("id")

// 设置特性的值

tag("input")[0].setAttribute("value","Your Name");

除了这对标准的 getAttribute/setAttribute外,HTML DOM文档还有作为快速特性获取器(getter)/设置器(setter)的额外属性集合。它们通常在现代浏览器的DOM实现中都可用(但只能给 HTML DOM文档打包票),使用它们十分有助于编写精炼的代码。以下的代码向你展示了如何使用DOM属性(property)同时取得和设置DOM特性:

// 快捷获取特性

tag("input")[0].value

// 快捷设置特性

tag("div")[0].id = "main";

你应该注意到,特性存在某些怪异的情形。最常碰到问题的情形之一是存取类名称特性。为了能在所有的浏览器中都生效,你必须使用elem.className来存取className特性,而不是使用看起来更合适的getAttribute("class")。这种情形也发生在for特性上,而它被另外起名为htmlFor。此外,这也会在一些CSS特性上发生:cssFloat和cssText。导致这些特别的命名约定是因为class、for、float和text等都是JavaScript的保留字。

为了补救这些怪异的情形并且简化获取、设置正确特性的处理流程,你应该使用一个辅助函数来处理这些特殊情形。代码清单5-18展示了一个获取和设置元素特性值的函数。使用两个参数来调用这个函数时,比如 attr(element,id),返回指定元素特性的值;而使用3个参数来调用的话,比如attr(element,class,test),则是设置特性的值并返回它的新值。

代码清单5-18 获取和设置元素特性的值

function attr(elem, name, value) {

  // 确保提供的 name 是正确的

  if ( !name || name.constructor != String ) return '';

  // 检查name是否处在怪异命名的情形中

  name = { 'for': 'htmlFor', 'class': 'className' }[name] || name;

  // 如果用户传入了value参数的话,那么

  if ( typeof value != 'undefined' ) {

    // 首先使用快捷方式

    elem[name] = value;

    // 可以的话,使用setAttribute

    if ( elem.setAttribute )

      elem.setAttribute(name,value);

  }

  // 返回特性的值

  return elem[name] || elem.getAttribute(name) || '';

}

不用关心具体实现,使用标准的方法来同时存取和改变特性是一个强大的工具。代码清单5-19展示了一些例子,从中你可以学习到如何在多种场合中使用attr函数来简化处理特性的流程。

代码清单5-19 在DOM元素中使用attr函数设置和获取特性的值

// 设置<h1>元素的class

attr( tag("h1")[0], "class", "header" );

// 设置各个<input>元素的值

var input = tag("input");

for ( var i = 0; i < input.length; i++ ) {

  attr( input[i], "value", "" );

}

// 为name的值是'invalid'的<input>元素增加边框

var input = tag("input");

for ( var i = 0; i < input.length; i++ ) {

  if ( attr( input[i], "name" ) == 'invalid' ) {

    input[i].style.border = "2px solid red";

  }

}

到目前为止,我们已经讨论了DOM中常用的特性(比如id、class、name等)的获取和设置。但是,还有一个派得上用场的技术,那就是设置和获取非传统的特性。比如,你可以增添一个新特性(它只对存取DOM版本的元素可见)然后再次获取,完全不用修改文档的实际属性。举个例子,假设你有一个术语的自定义列表,并需要在点击术语的时候展开它的描述,HTML的结构大致如代码清单5-20所示。

代码清单5-20 自定义列表的HTML,其中把描述事先隐藏起来

    <title>Expandable Definition List</title>

    <style>dd { display: none; }</style>

    <h1>Expandable Definition List</h1>

    <dl>

      <dt>Cats</dt>

      <dd>A furry, friendly, creature.</dd>

      <dt>Dog</dt>

      <dd>Like to play and run around.</dd>

      <dt>Mice</dt>

      <dd>Cats like to eat them.</dd>

    </dl>

我们将会在第6章详细讲解事件的细节,现在先尽量保持这些事件代码的简单。以下例子是一个能让你点击术语显示(或隐藏)描述的高效脚本。该脚本应在页面的头部或者从一个外部文件中引入。代码清单5-21展示了构建一个可扩展自定义列表的必要代码。

代码清单5-21 允许动态切换的自定义列表

// 直至DOM可用

domReady(function(){

    // 遍历所有的术语

    var dt = tag("dt");

    for ( var i = 0; i < dt.length; i++ ) {

    // 等待用户点击术语

    addEvent( dt[i], "click", function() {

      // 检查描述已经open与否

      var open = attr( this, "open" );

      // 切换描述的display

      next( this ).style.display = open ? 'none' : 'block';

      // 记录描述是否open

      attr( this, "open", open ? '' : 'yes' );

      });

    }

});

现在已经知道如何遍历DOM与如何检查及修改特性,接下来你需要学习如何创建新的DOM元素,插入到所需的地方,并删除不再需要的元素。

2.7 修改DOM

了解如何修改DOM后,你就可以干任何事了,从即时创建自定义的XML文档到建立接受用户键入的动态表单,几乎所有可能的操作。修改DOM有3个步骤:首先要知道怎么创建一个新元素,然后要知道如何把它插入DOM中,最后要学习如何删除它。

2.7.1 使用DOM创建节点

修改DOM的主要方法是使用createElement函数,它可以让你即刻创建新元素。但是,已经创建的新元素并不会马上插入到DOM中(刚开始使用DOM的人常会对此感到迷惑)。所以,我先把重点放在创建DOM元素上。

createElement方法带有元素的标记名称,并返回该元素的实际DOM引用,这里没有特性和样式。如果你要开发使用XSLT驱动的XHTML页面(或者使用正确的MIME侍服的XHTML页面)的应用程序,必须记住,你使用的是XML文档,所以创建的元素必须使用正确的XML命名空间来关联它们。为无缝地解决问题,你可以使用一个简单的函数,用它来测试你正使用的HTML DOM文档是否支持使用命名空间(XHTML DOM文档的一个特点)来创建新的元素。在这种情况下,你必须使用正确的XHTML命名空间来创建新的DOM元素,如代码清单5-22所示。

代码清单5-22 创建新DOM元素的通用函数

function create( elem ) {

  return document.createElementNS ?

    document.createElementNS( 'http://www.w3.org/1999/xhtml', elem ) :

document.createElement( elem );

}

例如,使用这个函数你就可以创建一个简单的<div>元素,并附上一些额外的信息:

var div = create("div");

div.className = "items";

div.id = "all";

此外需要注意的是,一个创建新文本节点的DOM方法叫做createTextNode。它需要传入一个参数,即你需要插入到节点中的文本,并返回已创建的文本节点。

使用新建立的DOM元素和文本节点,就可以把它们插入到DOM文档所需的位置中去了。

2.7.2 插入到DOM中

即使是经验丰富的DOM老手,有时也难免会感到插入元素到DOM中非常麻烦。在你的JavaScript工具库中准备以下两个函数可以让你把事情做好。

第一个函数是insertBefore,可以在另一个子元素前插入一个元素。使用该函数的方法类似如下例子:

parentOfBeforeNode.insertBefore( nodeToInsert, beforeNode );

我使用了一个助记诀窍辅助记忆它的参数顺序:“insert第一个元素,before第二个。”没错吧,你只需片刻就记住了它。

有了一个能在其他节点前插入节点(可以是元素,也可以是文本节点),你是否该考虑考虑:“如何插入一个父节点中最后一个子节点?”一个名为appendChild的函数可以做到这个。 appendChild调用一个元素参数,追加指定的节点到子节点列表中的最后一个。这个函数的用法大致如下:

parentElem.appendChild( nodeToInsert );

为避免死记硬背insertBefore和 appendChild的参数顺序,你可以使用我写的两个辅助函数解决问题,如代码清单5-23和代码清单5-24所示,参数以相关的元素/节点然后是需插入的元素/节点的顺序调用。此外,before函数还有一个传入父节点的可选项,它可以为你节约一些代码。最后,两个函数都允许你传入需要插入/追加的字符串并自动地帮你转化为文本节点。建议你提供父元素作为引用(防止elem变成null)。

代码清单5-23 在另一个元素之前插入元素的函数

function before( parent, before, elem ) {

  // 检查parent是否传入

  if ( elem == null ) {

    elem = before;

    before = parent;

    parent = before.parentNode;

  }

  parent.insertBefore( checkElem( elem ), before );

}

代码清单5-24 为另一个元素追加一个子元素的函数

function append( parent, elem ) {

  parent.appendChild( checkElem( elem ) );

}

代码清单5-25的辅助函数允许你插入元素和文本(自动转化为正确的文本节点)。

代码清单5-25 before和append的辅助函数

function checkElem( elem ) {

  // 如果只提供字符串,则把它转化成文本节点

  return elem && elem.constructor == String ? document.createTextNode( elem ) : elem;

}

现在,使用before和append函数,通过创建新的DOM元素,你就可以在DOM中增添更多的信息呈现给用户了,如代码清单5-26所示。

代码清单5-26 使用append和before函数

// 创建一个新的<li>元素

var li = create("li");

attr( li, "class", "new" );

// 创建文本内容并添加到<li>中

append( li, "Thanks for visiting!" );

// 把<li>插入到有序列表的顶端

before( first( tag("ol")[0] ), li );

// 运行这些语句会转化空<ol>

<ol></ol>

// 为以下:

<ol>

<li class='new'>Thanks for visiting!</li>

</ol>

一将这些信息“插入”到DOM(包括使用insertBefore和appendChild),它马上就会渲染并呈现在用户前。因此,你可用之作实时的反馈。这对于需要用户输入的交互应用尤其有用。

现在你看到的只是基于DOM的方法来创建和插入节点,进一步了解其他向DOM注入内容的方法将更有用。

2.7.3 注入HTML到DOM

比创建普通DOM元素并插入到DOM中更为流行的技术是,直接向文档注入HTML。这个最为简便的实现方法使用了前面讨论的innerHTML。除了从元素获取HTML之外,它同时也是设置元素内的HTML的一条途径。作为一个展示其简单性的例子,假设有一个空的<ol>元素并需要往里加入<li>,那么的代码大致如下:

// 给ol 元素加入部分li

tag("ol")[0].innerHTML = "<li>Cats.</li><li>Dogs.</li><li>Mice.</li>";

难道这不比繁复地创建大量的DOM元素和相应的文本节点要简单得多?更好的是,它还比使用DOM方法要快得多(根据http://quirksmode.org的说法)。但世事无完美,使用innerHTML注入方法也伴随着一些棘手的问题:

l    如前所述,XML DOM文档中并不存在innerHTML方法,就是说你得继续使用通用的DOM 创建方法。

l    使用客户端XSLT创建的XHTML文档也不存在innerHTML方法,因为它们也是纯粹的XML文档。

l    innerHTML完全删除了元素内容的任何节点,意味着没有像DOM方法一样追加或者插入的简便方法。

最后一条尤为棘手,因为在另一个元素之前插入元素或者追加到子元素列表末尾特别有用。但是,编织一些DOM魔法,去改造append和before方法,除了常规的DOM元素,还可以让它跟常规的HTML 字符串和平共处。改造过程分两步走。第一步建立一个checkElem函数,它可以处理HTML字符串、DOM元素以及DOM元素数组,如代码清单 5-27所示。

代码清单5-27 转化一个DOM节点/HTML字符串混合型参数数组为纯粹的DOM节点数组

function checkElem(a) {

  var r = [];

  // 如果参数不是数组, 则强行转换

  if ( a.constructor != Array ) a = [ a ];

  for ( var i = 0; i < a.length; i++ ) {

    // 如果是字符串

    if ( a[i].constructor == String ) {

      // 用一个临时元素来存放HTML

      var div = document.createElement("div");

     // 注入HTML, 转换成DOM结构

      div.innerHTML = a[i];

      // 提取DOM结构到临时 div 中

      for ( var j = 0; j < div.childNodes.length; j++ )

        r[r.length] = div.childNodes[j];

    } else if ( a[i].length ) { // If it's an array

      // 假定是DOM节点数组

      for ( var j = 0; j < a[i].length; j++ )

        r[r.length] = a[i][j];

    } else { // 否则,假定是DOM节点

      r[r.length] = a[i];

    }

  }

  return r;

}

第二步,你需要修改这两个插入函数以适应新的checkElem,接受数组元素,如代码清单5-28所示。

代码清单5-28 插入和追加内容到DOM的改进函数

function before( parent, before, elem ) {

  // 检查是否提供 parent 节点参数

  if ( elem == null ) {

    elem = before;

    before = parent;

    parent = before.parentNode;

  }

  // 获取元素的新数组

  var elems = checkElem( elem );

  // 向后遍历数组,

  // 因为我们向前插入元素

  for ( var i = elems.length - 1; i >= 0; i-- ) {

    parent.insertBefore( elems[i], before );

  }

}

function append( parent, elem ) {

  // 获取元素数组

  var elems = checkElem( elem );

  // 把它们所有都追加到元素中

  for ( var i = 0; i <= elems.length; i++ ) {

    parent.appendChild( elems[i] );

  }

}

现在,使用这些新函数为有序列表追加<li>就非常简单了:

append( tag("ol")[0], "<li>Mouse trap.</li>" );

// 运行该简单的一行就能追加HTML到这个<ol>中

<ol>

<li>Cats.</li>

<li>Dogs.</li>

<li>Mice.</li>

</ol>

// 把它变成了这样:

<ol>

<li>Cats.</li>

<li>Dogs.</li>

<li>Mice.</li>

<li>Mouse trap.</li>

</ol>

// 而用before()运行一个类似的语句

before( last( tag("ol")[0] ), "<li>Zebra.</li>" );

// 则会变成这样:

<ol>

<li>Cats.</li>

<li>Dogs.</li>

<li>Zebra.</li>

<li>Mice.</li>

</ol>

这将能帮助你开发简洁和清晰的代码。但是,如果从DOM中移动元素和删除节点要怎么办呢?当然,肯定也会有其他方法处理这些问题。

2.7.4 删除DOM节点

删除DOM节点的操作几乎与创建和插入一样频繁。根据用户的需求允许创建无限量的项目,那么允许用户能够删除它们之中再也不需要处理的部分也就变得很重要了。删除节点的能力可封装成一个函数:removeChild。它跟appendChild用法一致效果相反。该函数的用法大致如下:

NodeParent.removeChild( NodeToRemove );

记住这点,你就可以创建两个独立的函数来删除节点了,如代码清单5-29所示。

代码清单5-29 删除DOM节点的函数

// 删除一个独立的DOM节点

function remove( elem ) {

  if ( elem ) elem.parentNode.removeChild( elem );

}

代码清单5-30展示了从一个元素中删除所有子节点的函数,仅需要DOM元素的引用作为参数。

代码清单5-30 从一个元素中删除所有子节点的函数

// 在DOM中删除一个元素的所有子节点

function empty( elem ) {

  while ( elem.firstChild )

    remove( elem.firstChild );

}

举个例子,要删除上一节例子中添加的<li>,同时假设你已经给用户充足的时间来浏览<li>了,在没有提示的情况下被删除。可以使用以下的JavaScirpt代码来完成这个操作:

// 删除<ol>的最后一个<li>

remove( last( tag("ol")[0] ) )

// 将会把

<ol>

<li>Learn Javascript.</li>

<li>???</li>

<li>Profit!</li>

</ol>

// 转换成:

<ol>

<li>Learn Javascript.</li>

<li>???</li>

</ol>

// 如果要求执行empty()函数而不是remove()

empty( last( tag("ol")[0] ) )

// 它将会简单地清空<ol>, 只留下:

<ol></ol>

学习完删除DOM节点,你已经清楚了我们的文档对象模型是如何运作的,并学会如何充分运用它。

2.8 小结

在这一章里讨论了与文档对象模型相关的大量东西。不幸的是,其中有些主题比其他的要复杂得多(比如等待DOM加载),而且在一段时间里都一直会这么复杂。不过仅使用目前所学你也能搭建出任何动态的Web应用程序了。

如果想看看一些DOM脚本编程的实际运用,在附录A中包含了大量补充代码。此外,还可以从本书的网站http://jspro.org或Apress网站 http://www.apress.com的源代码/下载部分找到更多DOM脚本编程例子。接下来,你要把注意力转向分离式DOM脚本编程的下一个组成部分——事件。