HTML5-权威指南-九-

104 阅读1小时+

HTML5 权威指南(九)

原文:The Definitive Guide to HTML5

协议:CC BY-NC-SA 4.0

三十、处理事件

我在本书这一部分的例子中使用事件来响应按钮点击。在这一章中,是时候深入细节,解释事件到底是什么,向您展示它们是如何工作的,以及它们如何适应 DOM 的其余部分。简而言之,事件允许您定义 JavaScript 函数来响应元素状态的变化,比如当元素获得和失去焦点时,或者当用户在元素上单击鼠标按钮时。

在这一章中,我将重点介绍事件机制以及由documentHTMLElement对象定义的事件。这些是最常用的事件,适用于所有文档和元素。表 30-1 对本章进行了总结。

Image

Image

使用简单的事件处理器

有几种不同的方法可以处理事件。最直接的方法是使用一个事件属性创建一个简单事件处理程序。元素为它们支持的每个事件定义一个事件属性。例如,onmouseover事件属性是全局mouseover事件的事件属性,当用户将指针移动到元素所占据的浏览器屏幕区域时,就会触发该事件。(这是一般模式;对于大多数事件,都会有一个对应的事件属性定义为on*<eventname>*

实现简单的内联事件处理程序

使用事件属性最直接的方法是给属性分配一组 JavaScript 语句。当事件被触发时,浏览器将执行您提供的语句。清单 30-1 给出了一个简单的例子。

清单 30-1。用内联 JavaScript 处理事件

`

             Example                      p {                 background: gray;                 color:white;                 padding: 10px;                 margin: 5px;                 border: thin solid black             }                                    

            There are lots of different kinds of fruit - there are over             500 varieties of banana alone. By the time we add the countless types of             apples, oranges, and other well-known fruit, we are faced with thousands of             choices.` `        

     `

在这个例子中,我已经指定了两个 JavaScript 语句应该被执行来响应mouseover事件,方法是将它们设置为文档中p元素的onmouseover事件属性的值。以下是声明:

this.style.background='white'; this.style.color='black'

这些是直接应用于元素的style属性的 CSS 属性,如第四章中所解释的。浏览器将特殊变量this的值设置为代表触发事件的元素的HTMLElement对象,style属性返回该元素的CSSStyleDeclaration对象。

Image 提示注意,我使用双引号来分隔整个属性值,使用单引号来指定我想要的颜色作为 JavaScript 字符串文字。如果您愿意,可以按其他顺序使用它们,但这是在属性中嵌入引用值的技术。

如果将文档加载到浏览器中,那么在style元素中定义的初始样式将应用到p元素中。当你将鼠标移动到元素上时,JavaScript 语句将被执行并改变分配给backgroundcolor CSS 属性的值,使用我在第四章中描述的技术。你可以在图 30-1 中看到过渡。

Image

图 30-1。处理鼠标悬停事件

这是单向过渡;当鼠标离开元素的屏幕区域时,样式不会重置。许多事件是成对发生的。与mouseover相对应的事件称为mouseout,您通过onmouseout事件属性来处理该事件,如清单 30-2 所示。

清单 30-2。处理鼠标释放事件

`

             Example                      p {                 background: gray;                 color:white;                 padding: 10px;                 margin: 5px;                 border: thin solid black             }                                    

            There are lots of different kinds of fruit - there are over             500 varieties of banana alone. By the time we add the countless types of             apples, oranges, and other well-known fruit, we are faced with thousands of             choices.         

     `

通过添加这个元素,您拥有了一个响应鼠标进入和退出它所占据的屏幕空间的元素。你可以在图 30-2 中看到新的过渡。

Image

图 30-2。组合对应事件的过渡效果

清单 30-2 显示了内联事件处理程序的两个问题中的第一个:它们很冗长,使得 HTML 很难阅读。第二个问题是 JavaScript 语句只适用于一个元素。我必须在我想要以这种方式表现的每一个其他的p元素上重复这些语句。

实现一个简单的事件处理函数

我们可以通过定义一个函数并将函数名指定为元素中事件属性的值来解决一些冗长和重复的问题。清单 30-3 展示了如何实现这一点。

清单 30-3。使用函数处理事件

`

             Example                      p {                 background: gray;                 color:white;                 padding: 10px;                 margin: 5px;                 border: thin solid black             }                                   elem.style.removeProperty('color');                 elem.style.removeProperty('background');             }                                    <p onmouseover="handleMouseOver(this)" onmouseout="handleMouseOut(this)">             There are lots of different kinds of fruit - there are over             500 varieties of banana alone. By the time we add the countless types of             apples, oranges, and other well-known fruit, we are faced with thousands of             choices.         

        <p onmouseover="handleMouseOver(this)" onmouseout="handleMouseOut(this)">             One of the most interesting aspects of fruit is the variety available in             each country. I live near London, in an area which is known for             its apples.         

    

`

在这个例子中,我定义了 JavaScript 函数,这些函数包含我想要执行的响应鼠标事件的语句,并在onmouseoveronmouseout属性中指定这些函数。特殊值this指的是触发事件的元素。

这种方法是对以前技术的改进。重复较少,代码也更容易阅读。但是我喜欢将我的事件从 HTML 元素中分离出来,为此我需要重新访问我们的老朋友 DOM。

使用 DOM 和事件对象

我在前面几节中演示的简单处理程序适合基本任务,但是如果您想要执行更复杂的处理(以及更优雅的事件处理程序定义),请切换到使用 DOM 和 JavaScript Event对象。清单 30-4 展示了如何使用Event对象以及如何使用 DOM 将一个函数与一个事件关联起来。

清单 30-4。使用 DOM 设置事件处理

`

             Example                      p {                 background: gray;                 color:white;                 padding: 10px;                 margin: 5px;                 border: thin solid black             }                                    

            There are lots of different kinds of fruit - there are over             500 varieties of banana alone. By the time we add the countless types of             apples, oranges, and other well-known fruit, we are faced with thousands of             choices.         

        

            One of the most interesting aspects of fruit is the variety available in             each country. I live near London, in an area which is known for             its apples.         

        **             for (var i = 0; i < pElems.length; i++) {                 pElems[i].onmouseover = handleMouseOver;                 pElems[i].onmouseout = handleMouseOut;             }

            function handleMouseOver(e) {                 e.target.style.background='white';                 e.target.style.color='black';             }

            function handleMouseOut(e) {                 e.target.style.removeProperty('color');                 e.target.style.removeProperty('background');             }              

`

这就是你在前几章的例子中看到的方法。这个脚本(我不得不将它移到页面的底部,因为我正在使用 DOM)找到了我想要处理的所有元素,并为事件处理程序属性设置了一个函数名。所有事件都有这样的属性。都是以同样的方式命名:on,后接事件名称。您可以在本章后面的使用 HTML 事件一节中了解有关可用事件的更多信息。

Image 提示注意,我使用函数的将其注册为事件监听器。一个常见的错误是在函数名后面加括号,所以用handleMouseOver()代替handleMouse。这相当于在脚本执行时调用函数,而不是在事件触发时。

清单中处理事件的函数定义了一个名为e的参数。这将被设置为由浏览器创建的一个Event对象,当事件被触发时,它代表事件。Event对象为您提供关于发生了什么的信息,并让您比在元素属性中包含代码更灵活地响应用户交互。在这个例子中,我使用了target属性来获取触发事件的HTMLElement,这样我就可以使用 style 属性并改变它的外观。

在向您展示 event 对象之前,我想演示一种替代方法来指定使用哪些函数来处理事件。事件属性(名为on*的属性)通常是最简单的方法,但是您也可以使用由HTMLElement对象实现的addEventListener方法。您还可以使用removeEventListener方法来分离一个函数和一个事件。这两种方法都允许你将事件类型和处理它们的函数表示为参数,如清单 30-5 所示。

清单 30-5。使用 addEventListener 和 removeEventListener 方法

`

             Example                      p {                 background: gray;                 color:white;                 padding: 10px;                 margin: 5px;` `                border: thin solid black             }                                    

            There are lots of different kinds of fruit - there are over             500 varieties of banana alone. By the time we add the countless types of             apples, oranges, and other well-known fruit, we are faced with thousands of             choices.         

        

            One of the most interesting aspects of fruit is the variety available in             each country. I live near London, in an area which is known for             its apples.         

        Press Me         

                pElems[i].addEventListener("mouseover", handleMouseOver);                 pElems[i].addEventListener("mouseout", handleMouseOut);             }

            document.getElementById("pressme").onclick = function() {                 document.getElementById("block2").removeEventListener("mouseout",                     handleMouseOut);             }

            function handleMouseOver(e) {                 e.target.style.background='white';                 e.target.style.color='black';             }

            function handleMouseOut(e) {                 e.target.style.removeProperty('color');                 e.target.style.removeProperty('background');             }                      

`

本例中的脚本使用addEventListener方法将handleMouseOverhandleMouseOut函数注册为p元素的事件处理程序。当点击button时,使用removeEventListener方法将p元素的handleMouseOut函数与block2id值分离。请注意,我使用了onclick属性来设置按钮元素上的click事件的处理程序,以演示您可以在同一个脚本中自由地混合和匹配技术。

addEventListener方法的优点是它允许您访问一些高级事件特性,正如我简短描述的那样。Event对象的成员在表 30-2 中描述。

Image 提示Event对象定义了所有事件共有的功能。但是,当我在本章后面向您展示基本事件时,您将会看到,还有其他与事件相关的对象,它们定义了为特定类型的事件指定的额外功能。

按类型区分事件

属性告诉你你正在处理哪种类型的事件。这个值以字符串的形式提供,比如mouseover。能够检测事件的类型允许你使用一个函数来处理多种类型,如清单 30-6 所示。

清单 30-6。使用类型属性

`

             Example                      p {                 background: gray;                 color:white;                 padding: 10px;                 margin: 5px;                 border: thin solid black             }                                    

            There are lots of different kinds of fruit - there are over             500 varieties of banana alone. By the time we add the countless types of             apples, oranges, and other well-known fruit, we are faced with thousands of             choices.         

        

            One of the most interesting aspects of fruit is the variety available in             each country. I live near London, in an area which is known for             its apples.         

        **             for (var i = 0; i < pElems.length; i++) {                 pElems[i].onmouseover = handleMouseEvent;                 pElems[i].onmouseout = handleMouseEvent;             }

            function handleMouseEvent(e) {                 if (e.type == "mouseover") {                     e.target.style.background='white';                     e.target.style.color='black';                 } else {                     e.target.style.removeProperty('color');                     e.target.style.removeProperty('background');                 }             }              

`

在本例的脚本中,我使用了type属性来确定我在一个事件处理函数handleMouseEvent中处理的是哪种事件。

了解事件流程

一个事件的生命周期有三个阶段:捕获目标冒泡。在这一节中,我将解释其中的每一个阶段,并向您展示它们是如何工作的,以及您如何使用事件侦听器函数来控制它们。

了解捕获阶段

当一个事件被触发时,浏览器识别与该事件相关的元素,该元素被称为事件的目标。浏览器识别body元素和目标之间的所有元素,并检查它们中的每一个,看它们是否有任何事件处理程序请求被通知它们后代的事件。浏览器在触发目标本身上的处理程序之前触发任何这样的处理程序。清单 30-7 提供了一个演示。

清单 30-7。捕捉事件

`

             Example                      p {                 background: gray;                 color:white;                 padding: 10px;                 margin: 5px;                 border: thin solid black             }             span {                 background: white;                 color: black;                 padding: 2px;                 cursor: default;             }                                    

            There are lots of different kinds of fruit - there are over             500 varieties of banana alone. By the time we add             the countless types of apples, oranges, and other well-known fruit, we are             faced with thousands of choices.         

` `        

            banana.addEventListener("mouseover", handleMouseEvent);             banana.addEventListener("mouseout", handleMouseEvent);             textblock.addEventListener("mouseover", handleDescendantEvent, true);             textblock.addEventListener("mouseout", handleDescendantEvent, true);

            function handleDescendantEvent(e) {               if (e.type == "mouseover" && e.eventPhase == Event.CAPTURING_PHASE) {                     e.target.style.border = "thick solid red";                     e.currentTarget.style.border = "thick double black";               } else if (e.type == "mouseout" && e.eventPhase == Event.CAPTURING_PHASE) {                     e.target.style.removeProperty("border");                     e.currentTarget.style.removeProperty("border");               }             }

            function handleMouseEvent(e) {                 if (e.type == "mouseover") {                     e.target.style.background='white';                     e.target.style.color='black';                 } else {                     e.target.style.removeProperty('color');                     e.target.style.removeProperty('background');                                     }             }                      

`

在这个例子中,我定义了一个span元素作为p元素的子元素,并为mouseovermouseout事件注册了处理程序。注意,当我向父元素(p元素)注册时,我向addEventListener方法添加了第三个参数,如下所示:

textblock.addEventListener("mouseover", handleDescendantEvent, **true**);

这个额外的参数告诉浏览器,我希望p元素在捕获阶段接收其后代元素的事件。当触发mouseover事件时,浏览器从 HTML 文档的根开始,沿着 DOM 向目标(触发事件的元素)前进。对于层次结构中的每个元素,浏览器会检查它是否对捕获的事件感兴趣。在图 30-3 中可以看到示例文档的顺序。

Image

图 30-3。捕获事件流程

在每个元素中,浏览器调用任何支持捕获的侦听器。在这种情况下,浏览器将找到并调用我用p元素注册的handleDescendantEvent函数。当调用handleDescendantEvent函数时,Event对象包含关于目标元素的信息(通过target属性),以及通过currentTarget属性导致函数被调用的元素。我使用这两个属性,这样我就可以改变p元素和span子元素的样式。在图 30-4 中可以看到效果。

Image

图 30-4。处理事件捕获

事件捕获使元素的每个祖先在事件被传递给元素本身之前有机会对事件作出反应。父元素事件处理程序可以通过调用Event对象上的stopPropagationstopImmediatePropagation函数来停止事件向目标的流动。这些函数的区别在于,stopPropagation将确保为当前元素注册的所有事件侦听器都会被调用,而stopImmediatePropagation会忽略任何未触发的侦听器。清单 30-8 显示了在handleDescendantEvent事件处理程序中添加了stopPropagation函数。

清单 30-8。阻止进一步的事件流

... function handleDescendantEvent(e) {     if (e.type == "mouseover" && e.eventPhase == Event.CAPTURING_PHASE) {         e.target.style.border = "thick solid red";         e.currentTarget.style.border = "thick double black";     } else if (e.type == "mouseout" && e.eventPhase == Event.CAPTURING_PHASE) {         e.target.style.removeProperty("border");         e.currentTarget.style.removeProperty("border");     }     **e.stopPropagation();** } ...

随着这一改变,当调用了p元素上的处理程序时,浏览器捕获阶段结束。没有其他元素将被检查,目标和冒泡阶段(稍后描述)将被跳过。就这个例子而言,这意味着handleMouseEvent函数中的样式改变不会被应用来响应mouseover事件,正如你在图 30-5 中看到的。

Image

图 30-5。停止事件传播

注意,在处理程序中,我通过使用eventPhase属性检查事件类型并确定事件处于哪个阶段,如下所示:

... if (e.type == "mouseover" && **e.eventPhase == Event.CAPTURING_PHASE**) {       ...

注册事件监听器时启用捕获事件不会停止针对元素本身的事件。在这种情况下,p元素占据了浏览器屏幕的空间,也将响应mouseover事件。为了避免这种情况,我检查以确保我只在处理处于捕获阶段的事件时应用样式更改(即,针对后代元素的事件,并且我只是因为注册了一个启用捕获的侦听器而进行处理)。eventPhase属性将返回表 30-3 中所示的三个值之一,代表事件生命周期中的三个阶段。我将在接下来的小节中解释其他两个阶段。

Image

了解目标阶段

目标阶段是三个阶段中最简单的。当捕获阶段结束时,浏览器触发已经添加到目标元素的事件类型的任何监听器,如图 30-6 所示。

Image

图 30-6。目标阶段

在前面的例子中,您已经看到了这个阶段。这里需要注意的唯一一点是,您可以多次调用addEventListener函数,因此对于给定的事件类型,可以有多个侦听器。

Image 提示如果在目标阶段调用stopPropagationstopImmediatePropagation函数,则停止事件流程,冒泡阶段不会执行。

理解泡沫阶段

目标阶段完成后,浏览器开始沿着祖先元素链向上返回到body元素。在每个元素中,浏览器检查是否有未启用捕获的事件类型的侦听器(例如,addEventListener函数的第三个参数是false)。这就是所谓的事件冒泡。清单 30-9 给出了一个例子。

清单 30-9。事件冒泡

`

             Example                      p {                 background: gray;                 color:white;                 padding: 10px;                 margin: 5px;                 border: thin solid black             }             span {                 background: white;                 color: black;                 padding: 2px;                 cursor: default;             }                                    

            There are lots of different kinds of fruit - there are over             500 varieties of banana alone. By the time we add             the countless types of apples, oranges, and other well-known fruit, we are             faced with thousands of choices.         

        

            banana.addEventListener("mouseover", handleMouseEvent);             banana.addEventListener("mouseout", handleMouseEvent);             textblock.addEventListener("mouseover", handleDescendantEvent, true);             textblock.addEventListener("mouseout", handleDescendantEvent, true);             textblock.addEventListener("mouseover", handleBubbleMouseEvent, false);             textblock.addEventListener("mouseout", handleBubbleMouseEvent, false);

            function handleBubbleMouseEvent(e) {                if (e.type == "mouseover" && e.eventPhase == Event.BUBBLING_PHASE) {                     e.target.style.textTransform = "uppercase";                } else if (e.type == "mouseout" && e.eventPhase == Event.BUBBLING_PHASE) {                     e.target.style.textTransform = "none";                }             }

            function handleDescendantEvent(e) {               if (e.type == "mouseover" && e.eventPhase == Event.CAPTURING_PHASE) {                     e.target.style.border = "thick solid red";                     e.currentTarget.style.border = "thick double black";               } else if (e.type == "mouseout" && e.eventPhase == Event.CAPTURING_PHASE) {                     e.target.style.removeProperty("border");                     e.currentTarget.style.removeProperty("border");               }             }

            function handleMouseEvent(e) {                 if (e.type == "mouseover") {                     e.target.style.background='black';                     e.target.style.color='white';                 } else {                     e.target.style.removeProperty('color');                     e.target.style.removeProperty('background');                                     }             }                      

`

我添加了一个名为handleBubbleMouseEvent的新函数,并将其添加到文档中的p元素中。p元素现在有两个事件监听器,一个启用了捕获,另一个启用了冒泡。当您使用addEventListener方法时,您总是处于这些状态中的一种,这意味着除了它自己的事件之外,一个元素的监听器将总是被通知关于继承的元素事件*。选择是在后代事件的目标阶段之前还是之后调用侦听器。*

这个新添加的结果是,您有三个侦听器函数,它们将被文档中的span元素上的mouseover事件触发。在捕获阶段会触发handleDescendantEvent功能,在目标阶段会调用handleMouseEvent功能,在冒泡阶段会调用handleBubbleMouseEvent。你可以在图 30-7 中看到这样做的效果。

Image

图 30-7。泡沫阶段

元素的外观现在受到所有监听器函数中样式变化的影响,如图图 30-8 所示。

Image

图 30-8。为冒泡阶段添加一个处理程序的效果

Image 提示不是所有事件都支持冒泡。您可以使用bubbles属性查看事件是否会冒泡。值true表示事件会冒泡,false表示不会。

处理可取消事件

一些事件定义了触发事件时将执行的默认操作。例如,a元素上的click事件的默认动作是浏览器将在href属性中指定的 URL 处加载内容。当一个事件有一个默认动作时,它的cancelable属性的值将是true。您可以通过调用preventDefault功能来停止执行默认动作。清单 30-10 给出了一个在事件处理函数中处理可取消事件的例子。

清单 30-10。取消默认操作

`

             Example                      a {                 background: gray;                 color:white;                 padding: 10px;                 border: thin solid black             }                            

            Visit Apress             Visit W3C         

        

            function handleClick(e) {                 if (!confirm("Do you want to navigate to " + e.target.href + " ?")) {                     e.preventDefault();                 }             }

            var elems = document.querySelectorAll("a");             for (var i = 0; i < elems.length; i++) {                 elems[i].addEventListener("click", handleClick, false);             }

             

`

在这个例子中,我使用了confirm函数来提示用户查看他们是否真的想要导航到a元素所指向的 URL。如果用户点击Cancel按钮,那么我调用preventDefault函数。这意味着浏览器将不再导航到该 URL。

注意,调用preventDefault函数不会阻止事件流过捕获、目标和冒泡阶段。这些阶段仍将执行,但浏览器不会在冒泡阶段结束时执行默认操作。您可以通过读取defaultPrevented属性来测试查看preventDefault函数是否被早期的事件处理程序调用过;如果它返回true,那么preventDefault函数已经被调用。

处理 HTML 事件

HTML 定义了一组按类型分组的事件,我将在下一节描述这些事件。第一部分,文档和窗口事件,应用于DocumentWindow对象,我在第二十五章和第二十六章中讨论过。

其他事件由所有的HTMLElement对象定义,实际上是通用的。为了支持每种类型事件的独特特征,浏览器调度具有核心Event对象之外的附加属性的对象。当你浏览这些例子时,这是有意义的。

文档和窗口事件

除了你在前面章节看到的特性外,Document对象定义了表 30-4 中描述的事件。你可以在第二十五章中看到这个事件的例子。

Image

window对象定义了广泛的事件,在表 30-5 中有描述。您可以通过body元素处理其中的一些事件,但是对这种方法的支持不太全面,使用window更可靠。

Image

Image

处理鼠标事件

你已经在本章前面看到了mouseovermouseout事件,但是鼠标相关事件的完整集合显示在表 30-6 中。

Image

当鼠标事件被触发时,浏览器会调度一个MouseEvent对象。这是一个Event对象,其附加属性和方法如表 30-7 所示。

清单 30-11 展示了如何使用MouseEvent对象提供的附加功能。

清单 30-11。使用 MouseEvent 对象响应鼠标事件

`

             Example                      p {                 background: gray;                 color:white;                 padding: 10px;                 margin: 5px;                 border: thin solid black             }             table { margin: 5px; border-collapse: collapse; }             th, td {padding: 4px;}                                    

            There are lots of different kinds of fruit - there are over` `            500 varieties of banana alone. By the time we add the countless types of             apples, oranges, and other well-known fruit, we are faced with thousands             of choices.         

                                                        
Type:
X:
Y:

        

            textblock.addEventListener("mouseover", handleMouseEvent, false);             textblock.addEventListener("mouseout", handleMouseEvent, false);             textblock.addEventListener("mousemove", handleMouseEvent, false);

            function handleMouseEvent(e) {                 if (e.eventPhase == Event.AT_TARGET) {                     typeCell.innerHTML = e.type;                     xCell.innerHTML = e.clientX;                     yCell.innerHTML = e.clientY;

                    if (e.type == "mouseover") {                         e.target.style.background='black';                         e.target.style.color='white';                     } else {                         e.target.style.removeProperty('color');                         e.target.style.removeProperty('background');                                         }                 }             }                           

`

本例中的脚本更新表格中的单元格,以响应两种鼠标事件。你可以在图 30-9 中看到效果。

Image

图 30-9。处理老鼠事件

处理焦点事件

焦点相关事件被触发以响应获得和失去焦点的元素。表 30-8 总结了这些事件。

Image

这些事件由一个FocusEvent对象表示,它将表 30-9 中显示的属性添加到核心Event对象功能中。

Image

清单 30-12 展示了焦点事件的使用。

清单 30-12。使用焦点事件

`

             Example                      p {                 background: gray;                 color:white;                 padding: 10px;                 margin: 5px;                 border: thin solid black             }                                         

                Fruit:             

            

                Name:             

            Submit Vote             Reset         

        

            var inputElems = document.getElementsByTagName("input");             for (var i = 0; i < inputElems.length; i++) {                 inputElems[i].onfocus = handleFocusEvent;                 inputElems[i].onblur = handleFocusEvent;             }

            function handleFocusEvent(e) {                 if (e.type == "focus") {                     e.target.style.backgroundColor = "lightgray";                     e.target.style.border = "thick double red";                 } else {                     e.target.style.removeProperty("background-color");                     e.target.style.removeProperty("border");                 }             }              

`

本例中的脚本使用focusblur事件来改变一对输入元素的样式。你可以在图 30-10 中看到效果。

Image

图 30-10。使用聚焦和模糊事件

处理键盘事件

响应按键触发键盘事件。该类别中的事件集合如表 30-10 所示。

Image

这些事件由一个FocusEvent对象表示,它将表 30-11 中显示的属性添加到核心Event对象功能中。

Image

Image

清单 30-13 显示了一些正在使用的键盘事件。

清单 30-13。使用键盘事件

`

             Example                      p {                 background: gray;                 color:white;                 padding: 10px;                 margin: 5px;                 border: thin solid black             }                                         

                Fruit:             

            

                Name:             

            Submit Vote             Reset                  

        

            var inputElems = document.getElementsByTagName("input");             for (var i = 0; i < inputElems.length; i++) {                 inputElems[i].onkeyup = handleKeyboardEvent;             }

            function handleKeyboardEvent(e) {                 document.getElementById("message").innerHTML = "Key pressed: " +                     e.keyCode + " Char: " + String.fromCharCode(e.keyCode);             }              

`

本例中的脚本更改了一个span元素的内容,以显示发送给一对input元素的击键。注意我是如何使用String.fromCharCode函数将keyCode属性的值转换成一个更有用的值的。你可以在图 30-11 中看到这个脚本的效果。

Image

图 30-11。使用按键事件

使用表单事件

form元素定义了该元素特有的两个特殊事件。这些在表 30-12 中描述。

Image

当我展示 Ajax 时,你可以在第 33 和 34 章看到表单事件是如何使用的。

总结

在这一章中,我解释了事件系统如何允许你对文档元素的状态变化做出反应。我向您展示了处理事件的不同方式,从简单的on*属性,使用处理函数,到addEventListener方法,每种方式都有自己的优点。我还解释了事件生命周期的三个阶段——捕获、到达目标和冒泡——以及如何在事件传播时使用这些阶段来拦截事件。我在本章结束时描述了适用于大多数 HTML 元素的事件。

三十一、使用特定于元素的对象

文档对象模型(DOM)定义了一组对象,代表文档中不同类型的 HTML 元素。这些对象可以被视为HTMLElement对象,在很大程度上,这就是你通常在脚本中所做的。但是,如果您想要访问某个元素特有的属性或特性,通常可以使用这些对象之一来实现。

这些物品没有多大用处。它们通常定义与元素支持的属性相对应的属性,元素的值可以通过HTMLElement的特性来访问。有几个例外——表单元素有一些用于输入验证的有用方法,表格元素有一些可用于构建表格内容的方法。

文档和元数据对象

本节描述了表示数据和元数据元素的对象。你可以在第七章中了解更多关于这些元素的信息。

基础元素

base元素由HTMLBaseElement对象表示。该对象没有定义任何附加事件,但是有两个属性,如表 31-1 所示。

Image

身体元素

body元素由HTMLBodyElement对象表示。该对象没有定义任何额外的属性,但是事件集显示在表 31-2 中。

Image 提示一些浏览器通过window对象支持这些事件,我在第二十七章中描述过。

链接元素

link元素由HTMLLinkElement对象表示,该对象定义了表 31-3 中所示的属性。

Image

元元素

meta元素由HTMLMetaElement对象表示,该对象定义了表 31-4 中所示的属性。

脚本元素

script元素在 DOM 中由HTMLScriptElement对象表示,它定义了在表 31-5 中描述的附加属性。

Image

样式元素

style元素在 DOM 中由HTMLStyleElement对象表示,它定义了表 31-6 中显示的附加属性。

Image

Image

标题元素

title元素由 DOM 中的HTMLTitleElement对象表示。该对象定义了表 31-7 中显示的属性。

Image

其他文档和元数据元素

headhtml元素分别由HTMLHeadElementHTMLHtmlElement对象表示。除了HTMLElement之外,这些对象没有定义任何额外的方法、属性或事件。noscript元素没有特殊的 DOM 对象,只由HTMLElement表示。

文本元素

本节描述表示文本元素的对象。你可以在第八章中了解更多关于这些元素的信息。

a 元素

a元素由HTMLAnchorElement对象表示,该对象定义了表 31-8 中所示的属性。除了定义与元素属性相对应的属性之外,该对象还定义了一组方便的属性,允许您轻松地获取或设置由href属性指定的 URL 组件。

Image

Image

del 和 ins 元素

delins元素都由HTMLModElement表示。您可以使用由HTMLElement定义的tagName属性来区分它们。详见第二十六章。HTMLModElement定义的附加属性在表 31-9 中描述。

Image

q 元素

q元素由HTMLQuoteElement对象表示。该对象定义的属性在表 31-10 中有描述。

Image

时间元素

time元素由HTMLTimeElement对象表示。该对象定义的附加属性如表 31-11 所示。

Image

其他文本元素

brspan元素分别由HTMLBRElementHTMLSpanElement对象表示。除了HTMLElement之外,这些对象没有定义任何额外的方法、属性或事件。以下元素仅由HTMLElement表示:abbrbcitecodedfnemiukbdmarkrtrprubyssampsmallstrongsubsupvarwbr

分组元素

本节描述代表分组元素的对象。你可以在第九章中了解更多关于这些元素的信息。

block quote 元素

blockquote元素由HTMLQuoteElement对象表示。这和q元素使用的是同一个对象,我在表 31-10 中描述过。

李元素

li元素由HTMLLIElement对象表示,该对象定义了表 31-12 中显示的属性。

Image

ol 元素

ol元素由HTMLOListElement对象表示,该对象定义了表 31-13 中所示的属性。

Image

其他分组元素

表 31-14 显示了由元素特定对象表示的分组元素集,这些对象除了HTMLElement的功能之外没有定义任何附加功能。

Image

Image

以下元素在 DOM 中没有对应的元素,分别用HTMLElement : dddtfigcaptionfigure表示。

截面元素

本节描述代表截面元素的对象。你可以在第十章中了解更多关于这些元素的信息。

细节元素

details元素由HTMLDetailsElement对象表示。该对象定义的属性在表 31-15 中描述。

Image

其他截面元素

h1 - h6元素由HTMLHeadingElement对象表示,但是这个对象没有定义任何额外的属性。以下部分元素没有用特定的对象表示:addressarticleasidefooterheaderhgroupnavsectionsummary

表元素

本节描述表示表格元素的对象。你可以在第十一章中了解更多关于这些元素的信息。

col 和 colgroup 元素

colcolgroup元素都由HTMLTableColElement对象表示,该对象定义了表 31-16 中显示的属性。

Image

表元素

table元素由HTMLTableElement对象表示。这是最有用的特定于元素的对象之一。该对象定义的属性和方法在表 31-17 中描述。

Image

thead、tbody 和 tfoot 元素

theadtbodytfoot元素都由HTMLTableSectionElement对象表示。该对象定义的属性和方法如表 31-18 所示。

Image

第 th 元素

th元素由HTMLTableHeaderCellElement对象表示。该对象定义的属性在表 31-19 中描述。

Image

tr 元素

tr元素由HTMLTableRowElement对象表示,该对象定义了表 31-20 中所示的属性和方法。

Image

Image

其他表格元素

表 31-21 显示了表格元素的集合,这些元素由元素特定的对象表示,这些对象没有定义任何超出HTMLElement的附加功能。

Image

表单元素

本节描述了表示表单元素的对象。你可以在第十二章–第十四章中了解更多关于这些元素的信息。

按钮元素

button元素由HTMLButtonElement对象表示,该对象定义了表 31-22 中所示的属性和方法。

Image

Image

datalist 元素

datalist元素由HTMLDataListElement对象表示,该对象定义了表 31-23 中显示的属性。

Image

fieldset 元素

fieldset元素由HTMLFieldSetElement对象表示,该对象定义了表 31-24 中所示的属性。

Image

表单元素

form元素由HTMLFormElement对象表示,该对象定义了表 31-25 中所示的属性和方法。

Image

输入元素

input元素由HTMLInputElement对象表示,支持表 31-26 中显示的属性和方法。

Image

标签元素

label元素由HTMLLabelElement对象表示,该对象定义了表 31-27 中所示的属性。

Image

传说元素

legend元素由HTMLLegendElement对象表示,该对象定义了表 31-28 中显示的属性。

Image

opt group 元素

optgroup元素由HTMLOptGroupElement对象表示,该对象定义了表 31-29 中所示的属性。

Image

选项元素

option元素由HTMLOptionElement对象表示,该对象定义了表 31-30 中所示的属性。

输出元素

output元素由HTMLOutputElement对象表示,该对象定义了表 31-31 中所示的属性。

Image

Image

选择元素

select元素由HTMLSelectElement对象表示,该对象实现了表 31-32 中所示的属性和方法。

Image

Image

textarea 元素

textarea元素由HTMLTextAreaElement对象表示,它定义了表 31-33 中描述的方法和属性。

Image

Image

内容要素

本节描述表示用于在文档中嵌入内容的元素的对象。你可以在第十五章中了解更多关于这些元素的信息。

Image 其他内容元素,如canvasvideo,稍后在第三十四章中描述。

面积元素

area元素由HTMLAreaElement表示,它实现了表 31-34 中所示的属性。

Image

Image

嵌入元素

embed元素由HTMLEmbedElement对象表示,该对象实现了表 31-35 中所示的属性。

Image

Image

iframe 元素

iframe元素由HTMLIFrameElement对象表示,该对象实现了表 31-36 中描述的属性。

Image

img 元素

img元素由HTMLImageElement对象表示,该对象实现了表 31-37 中描述的属性。

Image

Image

地图元素

map元素由HTMLMapElement对象表示,该对象实现了表 31-38 中所示的属性。

Image

计元素

meter元素由HTMLMeterElement对象表示,该对象实现了表 31-39 中所示的属性。

Image

物体元素

object元素由HTMLObjectElement对象表示,该对象实现了表 31-40 中所示的属性。

Image

param 元素

param元素由HTMLParamElement对象表示,该对象实现了表 31-41 中所示的属性。

进度元素

progress元素由HTMLProgressElement对象表示,该对象实现了表 31-42 中所示的属性。

Image

总结

在这一章中,我列出了用于表示 DOM 中不同类型元素的对象集。在大多数情况下,这些都不是特别有用——有两个例外。第一个例外是表单元素,它对验证和表单提交提供了一些有用的控制。第二个例外是表格元素,它提供了管理表格内容的方法。除了这些例外,本章中描述的对象很大程度上是代表特定属性的属性集合——这些属性的值可以通过无处不在的HTMLElement对象来访问。

三十二、使用 Ajax——第一部分

Ajax 是现代 web 应用开发中的一个关键工具。它允许您从服务器异步发送和检索数据,并使用 JavaScript 处理数据。Ajax 是异步 JavaScript 和 XML 的缩写。这个名字是在 XML 成为数据传输格式的时候出现的,尽管我将在后面解释,现在情况已经不同了。

Ajax 是另一种有争议的技术。它在创建丰富的 web 应用方面非常有用,以至于设计师和开发人员围绕它的使用创造了一个传说,并定期参与关于如何正确使用 Ajax 的恶意攻击比赛。这很大程度上是垃圾,不需要。当你深入到细节时,Ajax 出奇的简单,你马上就能像大师一样提出请求。我的对付狂热者的标准建议适用于对付 Ajax 狂热者:礼貌地点点头,后退,为你的项目做正确的事情。

Image 提示你会看到 Ajax 有很多不同的用法。“AJAX”似乎是目前使用最广泛的,但 AJaX 相当普遍,有些人甚至使用 Ajax(挑剔的人认为你从来不大写“and”)。它们都是指相同的技术和技巧。我试图在本书中始终如一地使用 Ajax。

Ajax 的关键规范以您用来设置和发出请求的 JavaScript 对象命名:XMLHttpRequest。该规范有两个层次。所有主流浏览器都实现了 1 级,这是基本的功能级别。第 2 级扩展了最初的规范,以包括额外的事件、一些使使用form元素更容易的特性,以及对一些相关规范的支持,比如 CORS(我将在本章后面解释)。

在这一章中,我将解释 Ajax 的基础知识,向您展示如何创建、配置和执行简单的请求。我将向您展示如何通过事件来通知请求的进度,如何处理请求和应用错误,以及如何跨源发出请求。

本章中的所有例子都是关于从服务器获取数据的。下一章是关于发送数据的——特别是表单数据,这是 Ajax 最常见的用途之一。表 32-1 对本章进行了总结。

【Ajax 入门

Ajax 的关键是XMLHttpRequest对象,理解这个对象的最好方法是通过一个例子。清单 32-1 展示了XMLHttpRequest对象的基本用法。

清单 32-1。使用 XMLHttpRequest 对象

`

             Example                   
            Apples             Cherries             Bananas         
        **
**             **Press a button**         **
**                          httpRequest.onreadystatechange = handleResponse;                 httpRequest.open("GET", e.target.innerHTML +  ".html");                 httpRequest.send();             }

            function handleResponse(e) {                 if (e.target.readyState == XMLHttpRequest.DONE &&                     e.target.status == 200) {                         document.getElementById("target").innerHTML                             = e.target.responseText;                 }             }                   

`

在这个例子中,有三个button元素,每个元素被标记为不同的水果:ApplesCherriesBananas。还有一个div元素,当你开始时,它会显示一条简单的消息,告诉用户按下其中一个按钮。你可以在图 32-1 中看到该文件的外观。

Image

图 32-1。一个简单 Ajax 例子的起始状态

当按下其中一个按钮时,示例中的脚本加载另一个 HTML 文档,并将其设置为div元素中的内容。另外还有三个文档,它们对应于button元素上的标签:apples.htmlcherries.htmlbananas.html。图 32-2 显示了这些文档中的一个响应按钮的按下而显示的文档。

Image

图 32-2。显示异步加载的文档

这三个附加文档非常简单——有一张图片和一段取自维基百科页面的相关水果的文本。作为参考,清单 32-2 显示了cherries.html的内容,但所有三个文档遵循相同的结构(包含在本书的源代码下载中,可在apress.com免费获得)。

清单 32-2。cherries.html 的内容

`

             Cherries                      img {                 float: left; padding: 2px; margin: 5px;                 border: medium double black; background-color: lightgrey;             }                                   

            cherry             True cherry fruits are borne by members of the subgenus Cerasus, which is             distinguished by having the flowers in small corymbs of several together             (not singly, nor in racemes), and by having a smooth fruit with only a weak             groove or none along one side. The subgenus is native to the temperate             regions of the Northern Hemisphere, with two species in America,             three in Europe, and the remainder in Asia. The majority of eating cherries             are derived from either Prunus avium, the wild cherry (sometimes called the             sweet cherry), or from Prunus cerasus, the sour cherry.         

` `     `

当用户按下每个水果按钮时,浏览器关闭并异步检索所请求的文档,而无需重新加载主文档。这是典型的 Ajax 行为。

如果你把注意力转向剧本,你就能看到这是如何实现的。从handleButtonPress函数开始,该函数被调用以响应来自button控件的click事件:

function handleButtonPress(e) {     var httpRequest = new XMLHttpRequest();     httpRequest.onreadystatechange = handleResponse;     httpRequest.open("GET", e.target.innerHTML +  ".html");     httpRequest.send(); }

第一步是创建一个新的XMLHttpRequest对象。与您在 DOM 中看到的大多数对象不同,您不能通过浏览器定义的全局变量来访问这种对象。相反,您可以使用new关键字,就像这样:

var httpRequest = new XMLHttpRequest();

下一步是为readystatechange事件设置一个事件处理程序。这个事件在请求过程中被多次触发,为您提供事情进展的最新信息。我将在本章的后面回到这个事件(以及由XMLHttpRequest对象定义的其他事件)。我将onreadystatechange属性的值设置为handleResponse,这个函数我们很快就会用到:

httpRequest.onreadystatechange = handleResponse;

现在你可以告诉XMLHttpRequest对象你想要它做什么。您使用open方法,指定 HTTP 方法(在本例中为GET)和应该请求的 URL:

httpRequest.open("GET", e.target.innerHTML +  ".html");

Image 提示我在这里展示了最简单形式的open方法。您还可以向浏览器提供在向服务器发出请求时使用的凭证,如下所示:httpRequest.open("GET", e.target.innerHTML + ".html", true, "adam", "secret")。最后两个参数是应该发送给服务器的用户名和密码。另一个参数指定请求是否应该异步执行。这应始终设置为true

我正在根据用户按下的button编写请求 URL。如果按下了Apples按钮,我会请求 URL Apples.html。浏览器足够智能,可以处理相对 URL,并根据需要使用当前文档的位置。在这种情况下,我的主文档是从 URL [titan/listings/example.html](http://titan/listings/example.html)加载的,所以Apples.html被认为是指[titan/listings/Apples.html](http://titan/listings/Apples.html)。您的环境的 URL 会有所不同,但效果是一样的。

Image 提示为您的请求选择正确的 HTTP 方法很重要。正如我在第十二章中解释的那样,GET请求是为了安全交互,这样你就可以一遍又一遍地发出相同的请求,而不会引起任何副作用。POST请求是针对不安全的交互,其中每个请求都会导致服务器发生某种变化,重复的请求很可能会有问题。还有其他 HTTP 方法,但 GET 和 POST 是使用最广泛的方法,以至于如果您想使用不同的方法,您必须使用本章“覆盖请求 HTTP 方法”一节中描述的约定来确保您的请求通过防火墙。

这个函数的最后一步是调用send方法,就像这样:

httpRequest.send();

在这个例子中,我没有向服务器发送任何数据,所以没有关于send方法的参数。我将在本章的后面向您展示如何发送数据,但是在这个简单的例子中,您只是从服务器请求 HTML 文档。

应对应对

脚本一调用send方法,浏览器就向服务器发出后台请求。因为请求是在后台处理的,所以 Ajax 依靠事件来通知您请求的进展情况。在这个例子中,我用handleResponse函数处理这些事件:

function handleResponse(e) {     if (e.target.readyState == XMLHttpRequest.DONE && e.target.status == 200) {         document.getElementById("target").innerHTML = e.target.responseText;     } }

readystatechange事件被触发时,浏览器将一个Event对象传递给指定的处理函数。这就是我在第三十章中描述的同一个Event对象,并且target属性被设置为事件所涉及的XMLHttpRequest

许多不同的阶段通过readystatechange事件发出信号,您可以通过读取XMLHttpRequest.readyState属性的值来确定您正在处理哪一个。该属性的一组值如表 32-2 所示。

Image

Image

DONE状态并不表示请求成功——只表示请求已经完成。您通过status属性获得 HTTP 状态代码,该属性返回一个数值——例如,200值表示成功。只有结合readyStatestatus属性值,才能决定请求的结果。

您可以看到我是如何在handleResponse函数中检查这两个属性的。只有当readyState值为DONE并且status值为200时,我才设置div元素的内容。我使用XMLHttpRequest.responseText属性获取服务器发送的数据,如下所示:

document.getElementById("target").innerHTML = **e.target.responseText;**

responseText属性返回一个表示从服务器检索的数据的字符串。我使用这个属性来设置div元素的innerHTML属性的值,以便显示所请求文档的内容。这样,您就有了一个简单的 Ajax 示例——用户单击一个按钮,浏览器在后台向服务器请求一个文档,当文档到达时,您处理一个事件并显示所请求文档的内容。图 32-3 显示了该脚本的效果及其显示的不同文档。

Image

图 32-3。基本 Ajax 示例中脚本的效果

最低级的普通支配者:处理歌剧

在我们继续之前,我们必须花一点时间处理 Opera 对XMLHttpRequest标准的实现,它……嗯,不如其他浏览器好或完整。本章开头的例子对于其他主流浏览器来说非常适用,但是您需要做一些修改来处理 Opera 中的一些问题。清单 32-3 显示了这个例子,它有必要的修改。

清单 32-3。修改示例以支持 Opera

`

             Example                   
            Apples             Cherries             Bananas         
        
            Press a button                         
        

            function handleButtonPress(e) {                 httpRequest = new XMLHttpRequest();                 httpRequest.onreadystatechange = handleResponse;                 httpRequest.open("GET", e.target.innerHTML +  ".html");                 httpRequest.send();             }

            function handleResponse() {                 if (httpRequest.readyState == 4 && httpRequest.status == 200) {                     document.getElementById("target").innerHTML                         = httpRequest.responseText;                 }             }                   

`

第一个问题是 Opera 在触发readystatechange事件时没有调度Event对象。这意味着你必须将XMLHttpRequest对象赋给一个全局变量,以便以后引用它。我定义了一个名为httpRequestvar,当我在handleButtonPress函数中创建对象时引用它,当我在handleResponse函数中处理完成的请求时再次引用它。

这看起来没什么大不了的,但是如果用户在处理请求时按下按钮,一个新的XMLHttpRequest对象将被分配给全局变量,您将失去与原始请求交互的能力。

第二个问题是 Opera 没有在XMLHttpRequest对象上定义就绪状态常数。这意味着您必须使用我在表 32-2 中显示的数值来检查readyState属性的值。代替XMLHttpRequest.DONE,你必须检查值4

我希望在你读这本书的时候,Opera 已经升级和改进了它的XMLHttpRequest实现,但是如果没有,你需要写你的脚本来适应这种不好的行为。

使用 Ajax 事件

既然您已经构建并研究了一个简单的示例,那么您可以开始深入研究XMLHttpRequest对象支持的特性,以及如何在您的请求中使用它们。从 2 级规范中定义的附加事件开始。你已经看到了其中的一个——从第一级延续下来的readystatechange——但是还有其他的,正如表 32-3 中所描述的。

Image

这些事件中的大多数都是在请求中的特定时刻触发的。例外情况是readystatechange(我在前面描述过)和progress,它们可以被触发几次以给出进度更新。

除了readystatechange之外,表中显示的事件在XMLHttpRequest规范的第 2 级中定义。当我写这篇文章时,对这些事件的支持各不相同。例如,Firefox 拥有最全面的支持。Opera 根本不支持它们,Chrome 也支持其中一些,但不是以符合规范的方式。

Image 注意readystatechange事件是此时跟踪请求进度的唯一可靠方法,因为 2 级事件的实现不完整

当分派事件时,浏览器对readystatechange事件使用常规的Event对象(在第三十章的中描述),对其他事件使用ProgressEvent对象。ProgressEvent对象定义了Event对象的所有成员,加上表 32-4 中描述的附加成员。

Image

清单 32-4 显示了如何使用这些事件。我在这里展示过 Firefox,它有最完整和正确的实现。

清单 32-4。使用 XMLHttpRequest 定义的一次性事件

`

             Example                      table { margin: 10px; border-collapse: collapse; float: left}             div {margin: 10px;}             td, th { padding: 4px; }                            
            Apples             Cherries             Bananas         
        

        

        
            Press a button                         
        for (var i = 0; i < buttons.length; i++) {                 buttons[i].onclick = handleButtonPress;             }

            var httpRequest;

            function handleButtonPress(e) {                 clearEventDetails();                 httpRequest = new XMLHttpRequest();                 httpRequest.onreadystatechange = handleResponse;                 httpRequest.onerror = handleError;                 httpRequest.onload = handleLoad;                 httpRequest.onloadend = handleLoadEnd;                 httpRequest.onloadstart = handleLoadStart;                 httpRequest.onprogress = handleProgress;                 httpRequest.open("GET", e.target.innerHTML +  ".html");                 httpRequest.send();             }

            function handleResponse(e) {                 displayEventDetails("readystate(" + httpRequest.readyState + ")");                 if (httpRequest.readyState == 4 && httpRequest.status == 200) {                         document.getElementById("target").innerHTML                             = httpRequest.responseText;                 }             }

            function handleError(e) { displayEventDetails("error", e);}             function handleLoad(e) { displayEventDetails("load", e);}             function handleLoadEnd(e) { displayEventDetails("loadend", e);}             function handleLoadStart(e) { displayEventDetails("loadstart", e);}             function handleProgress(e) { displayEventDetails("progress", e);}

            function clearEventDetails() {                 document.getElementById("events").innerHTML                     = "EventlengthComputable"                     + "loadedtotal"             }

            function displayEventDetails(eventName, e) {                 if (e) {                     document.getElementById("events").innerHTML +=                     "" + eventName + "" + e.lengthComputable                     + "" + e.loaded + "" + e.total                     + "";                 } else {                     document.getElementById("events").innerHTML +=                     "" + eventName                         + "NANANA";                 }             }              

`

这是前一个例子的变体。我为一些事件注册了处理函数,并在一个table元素中为我处理的每个事件创建了一个记录。你可以在图 32-4 中看到 Firefox 如何触发事件。

Image

图 32-4。Firefox 触发的 2 级事件

处理错误

使用 Ajax 时,您必须注意两种错误。他们之间的差异是由不同的视角驱动的。

第一种错误是从XMLHttpRequest对象的角度来看的问题——一些阻止向服务器发出请求的问题,例如 DNS 中没有解析主机名,连接请求被拒绝,或者 URL 无效。

第二种错误从我们应用的角度来看是一个问题,但是不是XMLHttpRequest对象。当成功地向服务器发出请求,并且服务器接受了请求、处理了请求并生成了响应,但是该响应并没有提供您希望的内容时,就会出现这种情况。例如,如果您请求的 URL 不存在,就会出现这种情况。

有三种方法可以处理这些错误,如清单 32-5 所示。

清单 32-5。处理 Ajax 错误

`

             Example` `              
            Apples             Cherries             Bananas             Cucumber             Bad Host             Bad URL         
        
Press a button
        
        
        

            function handleButtonPress(e) {                 clearMessages();                 httpRequest = new XMLHttpRequest();                 httpRequest.onreadystatechange = handleResponse;                 httpRequest.onerror = handleError;                 try {                                     switch (e.target.id) {                         case "badhost":                             httpRequest.open("GET", "a.nodomain/doc.html");                             break;                         case "badurl":                             httpRequest.open("GET", "http://");                             break;                         default:                             httpRequest.open("GET", e.target.innerHTML + ".html");                             break;                     }                     httpRequest.send();                 } catch (error) {                     displayErrorMsg("try/catch", error.message);                 }             }

            function handleError(e) {                 displayErrorMsg("Error event", httpRequest.status                                 + httpRequest.statusText);             }

            function handleResponse() {                 if (httpRequest.readyState == 4) {                     var target = document.getElementById("target");                     if (httpRequest.status == 200) {                             target.innerHTML = httpRequest.responseText;                     } else {                         document.getElementById("statusmsg").innerHTML =                             "Status: " + httpRequest.status + " "                                 + httpRequest.statusText;                     }                 }             }

            function displayErrorMsg(src, msg) {                 document.getElementById("errormsg").innerHTML = src + ": " + msg;             }

            function clearMessages() {                 document.getElementById("errormsg").innerHTML = "";                 document.getElementById("statusmsg").innerHTML = "";             }

                  

`
处理设置错误

您需要处理的第一种错误发生在您向XMLHttpRequest对象传递错误数据时,比如格式错误的 URL。当基于用户输入生成 URL 时,这非常容易做到。为了模拟这种问题,我在示例文档中添加了一个标记为Bad URLbutton。按下这个button会导致下面对open方法的调用:

httpRequest.open("GET", "http://");

我已经记不清我看到这个问题的次数了(很遗憾,也记不清我造成这个问题的次数了)。通常,系统会提示用户在input元素中输入一个值,该元素的内容用于为 Ajax 请求生成一个 URL。当用户在没有输入值的情况下触发请求时,会向open方法传递一个部分 URL,或者像在本例中一样,只传递协议部分。

这是一个阻止请求执行的错误,当这种事情发生时,XMLHttpRequest对象将抛出一个错误。这意味着您需要在设置请求的代码周围使用一个try…catch语句,如下所示:

**try {**     …     httpRequest.open("GET", "http://");     …     httpRequest.send(); **} catch (error) {**     **displayErrorMsg("try/catch", error.message);** **}**

catch子句是你从错误中恢复的机会。您可以选择提示用户输入一个值,返回到默认的 URL,或者干脆放弃请求。对于这个例子,我简单地通过调用displayErrorMsg函数显示错误消息。该函数在示例脚本中定义,显示 ID 为errormsgdiv元素中的Error.message属性。

处理请求错误

第二种错误出现在请求发出后,但是请求出了问题。为了模拟这种问题,我在示例中添加了一个标记为Bad Host的按钮。当按下此按钮时,用一个不能使用的 URL 调用open方法:

httpRequest.open("GET", "http://a.nodomain/doc.html");

这个 URL 有两个问题。第一个问题是主机名无法在 DNS 中解析,因此浏览器无法连接到服务器。这个问题对于XMLHttpRequest对象来说是不明显的,直到它开始发出请求,所以它以两种方式发出问题信号。如果您已经为error事件注册了一个监听器,浏览器将向您的监听器函数发送一个Event对象。以下是我在示例中的函数:

function handleError(e) {     displayErrorMsg("Error event", httpRequest.status + httpRequest.statusText); }

当这种错误发生时,您从XMLHttpRequest对象获得的信息程度会因浏览器而异,可悲的是,您通常会得到一个0status和一个空的statusText值。

第二个问题是 URL 的来源不同于发出请求的脚本——默认情况下这是不允许的。通常,只允许对与脚本加载来源相同的 URL 发出 Ajax 请求。浏览器可以通过抛出一个Error或触发一个error事件来报告这个问题——这在不同的浏览器之间是不同的。不同的浏览器在不同的时间检查原点,这意味着你并不总是看到浏览器突出显示的同一个问题。(您可以使用跨站点资源规范或 CORS 来克服同源限制。请参阅本章后面的“制作跨源 Ajax 请求”一节)。

处理应用错误

XMLHttpRequest对象的角度来看,当请求成功时,最后一种错误出现了,但是它没有给出您想要的数据。为了解决这种问题,我在示例文档中添加了一个标记为Cucumberbutton。除了服务器上没有cucumber.html文档之外,按下该按钮会生成与ApplesCherriesBananas按钮相同的请求 URL。

当这种情况发生时,并不存在错误(因为请求本身成功了),您可以从status属性中确定发生了什么。当您请求一个不存在的文档时,您会得到一个状态代码404,这意味着服务器无法找到所请求的文档。您可以看到我是如何处理任何不是200(意思是OK)的代码的:

if (httpRequest.status == 200) {     target.innerHTML = httpRequest.responseText; } else {     document.getElementById("statusmsg").innerHTML =         "Status: " + httpRequest.status + " " + httpRequest.statusText; }

对于这个例子,我只显示了statusstatusText的值。在实际的应用中,您需要以一种有用且有意义的方式进行恢复——可能是通过显示一些回退内容或提醒用户出现问题,这取决于什么对应用有意义。

获取和设置标题

XMLHttpRequest对象允许您为发送给服务器的请求设置标题,并从服务器的响应中读取标题。表 32-5 描述了与割台相关的方法。

Image

覆盖请求 HTTP 方法

您不需要经常添加或更改 Ajax 请求中的头。浏览器知道它需要发送什么,服务器知道如何响应。但是也有一些例外。第一个是X-HTTP-Method-Override头。

HTTP 标准通常用于在互联网上请求和传输 HTML 文档,它定义了许多方法。大多数人都知道GETPOST,因为它们是目前使用最广泛的。但是还有其他的,包括PUTDELETE,使用这些 HTTP 方法赋予从服务器请求的 URL 以意义的趋势正在增长。举个简单的例子,如果你想查看一个用户记录,你可以发出这样的请求:

httpRequest.open("GET", "http://myserver/records/freeman/adam");

我只是在这里展示了 HTTP 方法和请求 URL。要使这个请求生效,必须有一个服务器端应用知道如何理解这个请求,并将其转换成合适的数据发送回服务器。如果您想要删除数据,可以执行以下操作:

httpRequest.open("DELETE", "http://myserver/records/freeman/adam");

这里的关键是通过 HTTP 方法表达你希望服务器做什么,而不是通过某种方式在 URL 中编码。这是名为 RESTful APIs 趋势的一部分。构成 RESTful API 的其余部分是一个经常激烈争论的话题,我不打算在这里讨论。

以这种方式使用 HTTP 方法的问题是,许多主流 web 技术只支持GETPOST,许多防火墙只允许GETPOST请求通过。有一个约定可以避免这种限制,那就是在实际发送一个POST请求时,使用X-HTTP-Method-Override头来指定您想要使用的 HTTP 方法。清单 32-6 给出了一个演示。

清单 32-6。设置请求头

`

             Example                   
            Apples             Cherries             Bananas         
        
Press a button
                         httpRequest.send();             }

            function handleError(e) {                 displayErrorMsg("Error event", httpRequest.status                                 + httpRequest.statusText);             }

            function handleResponse() {                 if (httpRequest.readyState == 4 && httpRequest.status == 200) {                     document.getElementById("target").innerHTML                         = httpRequest.responseText;                 }             }

                  

`

在这个例子中,我在XMLHttpRequest对象上使用了setRequestHeader方法来表示我希望这个请求像使用 HTTP DELETE方法一样被处理。注意,我在调用 open 方法后设置了标题*。如果您试图在使用open方法之前使用setRequestHeader方法,那么XMLHttpRequest对象将抛出一个错误。*

Image 提示只有当服务器端 web 应用框架理解X-HTTP-Method-Override约定,并且您的服务器端应用被设置为寻找和理解不常用的 HTTP 方法时,覆盖 HTTP 方法才有效。

禁用内容缓存

添加到 Ajax 请求中的第二个有用的头是Cache-Control,尤其是在编写和调试脚本时。有些浏览器会缓存通过 Ajax 请求获得的内容,并且在浏览会话期间不再请求它。在我在本章中使用的例子的上下文中,这意味着对apples.htmlcherries.htmlbananas.html的任何更改都不会立即反映在浏览器中。清单 32-7 展示了如何设置标题来避免这种情况。

清单 32-7。禁用内容缓存

… function handleButtonPress(e) {     httpRequest = new XMLHttpRequest();     httpRequest.onreadystatechange = handleResponse;     httpRequest.open("GET", e.target.innerHTML + ".html");     **httpRequest.setRequestHeader("Cache-Control", "no-cache");**     httpRequest.send(); } …

您以与前一个示例相同的方式设置头值,但是您感兴趣的头是Cache-Control,您想要的值是no-cache。有了这个语句,下次请求文档时,就会显示通过 Ajax 请求的内容的变化。

读取响应标题

您可以通过getResponseHeadergetAllResponseHeaders方法读取服务器在响应 Ajax 请求时发送的 HTTP 头。在很大程度上,您并不关心头的内容,因为它们是浏览器和服务器之间事务的一部分。清单 32-8 展示了如何使用这些属性。

清单 32-8。读取响应头

`

             Example                      #allheaders, #ctheader {                 border: medium solid black;                 padding: 2px; margin: 2px;             }                            
            Apples             Cherries             Bananas         
        
        
        
Press a button
        

            var httpRequest;

            function handleButtonPress(e) {                 httpRequest = new XMLHttpRequest();                 httpRequest.onreadystatechange = handleResponse;                 httpRequest.open("GET", e.target.innerHTML + ".html");                 httpRequest.send();             }

            function handleResponse() {                 if (httpRequest.readyState == 2) {                     document.getElementById("allheaders").innerHTML =                         httpRequest.getAllResponseHeaders();                     document.getElementById("ctheader").innerHTML =                         httpRequest.getResponseHeader("Content-Type");

                } else if (httpRequest.readyState == 4 && httpRequest.status == 200) {                     document.getElementById("target").innerHTML                         = httpRequest.responseText;                 }             }

                  

`

readyState变为HEADERS_RECEIVED(其数值为2)时,响应头可用。头是服务器在响应中发回的第一件事,这就是为什么您可以在内容本身可用之前阅读它们。在这个例子中,我将两个div元素的内容设置为一个头(Content-Type)和所有头的值,这是用getResponseHeadergetAllResponseHeader方法获得的。你可以在图 32-5 中看到结果。

Image

图 32-5。阅读回复标题

从这一点上,您可以看出我的开发服务器 titan 运行的是 IIS web 服务器的 7.5 版本(这是您对经常使用 Windows Server 2008 R2 服务器的期望。我最后一次修改 apples.html 文档是在 8 月 29 日(但是截图是在 9 月 1 日)。

发出跨来源的 Ajax 请求

默认情况下,浏览器将脚本限制为在包含它们的文档源中发出 Ajax 请求。您应该还记得,来源是 URL 的协议、主机名和端口的组合。这意味着当我从http://titan加载一个文档时,文档中包含的脚本通常不能向http://titan:8080发出请求,因为第二个 URL 中的端口不同,因此在文档源之外。从一个来源到另一个来源的 Ajax 请求被称为跨来源请求

Image 提示该策略旨在降低跨站点脚本 (CSS)攻击的风险,在这种攻击中,浏览器(或用户)被诱骗执行恶意脚本。CSS 攻击超出了本书的范围,但是在[en.wikipedia.org/wiki/Cross-site_scripting](http://en.wikipedia.org/wiki/Cross-site_scripting)有一篇很好的维基百科文章,对这个主题做了很好的介绍。

这个政策的问题在于它是一个全面的禁令——没有跨产地的请求。这导致使用一些非常丑陋的伎俩来欺骗浏览器发出违反策略的请求。幸运的是,现在有了一种合法的跨来源请求方式,在跨来源资源共享 (CORS)规范中定义。

Image 注意这是一个高级主题,需要一些关于 HTTP 头的基础知识。因为这是一本关于 HTML5 的书,所以我不打算详细介绍 HTTP。我的建议是,如果你不熟悉 HTTP,你应该跳过这一节。

为了设置场景,让我们看一下我们试图解决的问题。清单 32-9 显示了一个 HTML 文档,其中包含一个想要进行跨来源请求的脚本。

清单 32-9。想要进行跨原点请求的脚本

`

             Example                   
            Apples             Cherries             Bananas         
        
Press a button
        

            function handleButtonPress(e) {                 httpRequest = new XMLHttpRequest();                 httpRequest.onreadystatechange = handleResponse;                 httpRequest.open("GET", "http://titan:8080/" + e.target.innerHTML);                 httpRequest.send();             }

            function handleResponse() {                 if (httpRequest.readyState == 4 && httpRequest.status == 200) {                     document.getElementById("target").innerHTML                         = httpRequest.responseText;                 }             }                    `

本例中的脚本追加用户已经按下的按钮的内容,将其追加到[titan:8080](http://titan:8080),并尝试发出一个 Ajax 请求(例如,[titan:8080/Apples](http://titan:8080/Apples))。我将从[titan/listings/example.html](http://titan/listings/example.html)加载这个文档,这意味着脚本正试图进行跨来源请求。

脚本试图到达的服务器运行在 Node.js 下。清单 32-10 显示了代码,我将它保存在一个名为fruitselector.js的文件中。(获取 Node.js 的详细信息见第二章)

清单 32-10。fruitselector.js Node.js 脚本

`var http = require('http');

http.createServer(function (req, res) {     console.log("[200] " + req.method + " to " + req.url);

    res.writeHead(200, "OK", {"Content-Type": "text/html"});     res.write('Fruit Total');     res.write('

');     res.write('You selected ' + req.url.substring(1));     res.write('

');     res.end();

}).listen(8080);`

这是一个非常简单的服务器——它根据客户机请求的 URL 生成一个简短的 HTML 文档。例如,如果客户端请求[titan:8080/Apples](http://titan:8080/Apples),服务器将生成并返回以下 HTML 文档:

`              Fruit Total                   

You selected Apples

    

`

照目前的情况来看,example.html中的脚本将无法从服务器获得它想要的数据。解决这个问题的方法是给服务器发送回浏览器的响应添加一个头,如清单 32-11 所示。

清单 32-11。添加跨原点表头

`var http = require('http');

http.createServer(function (req, res) {     console.log("[200] " + req.method + " to " + req.url);

    res.writeHead(200, "OK", {                         "Content-Type": "text/html",                         "Access-Control-Allow-Origin": "http://titan"                         });     res.write('Fruit Total');     res.write('

');

    res.write('You selected ' + req.url.substring(1));     res.write('

');     res.end(); }).listen(8080);`

Access-Control-Allow-Origin标题指定了应该允许对该文档进行跨来源请求的来源。如果标头指定的来源与当前文档的来源匹配,浏览器将加载并处理响应中包含的数据。

Image 提示支持 CORS 意味着浏览器在联系服务器并获得响应报头后,必须应用跨来源安全策略,这意味着即使响应因所需报头丢失或指定了不同的域而被丢弃,仍会发出请求。这是一种与不实现 CORS 的浏览器非常不同的方法,这些浏览器只是阻止请求,从不联系服务器。

通过在来自服务器的响应中添加这个报头,example.html文档中的脚本能够从服务器请求和接收数据,如图 32-6 所示。

Image

图 32-6。启用跨源 Ajax 请求

使用原产地请求标题

作为 CORS 的一部分,浏览器将在请求中添加一个Origin头,指定当前文档的来源。您可以使用它来更加灵活地设置Access-Control-Allow-Origin头的值,如清单 32-12 所示。

清单 32-12。使用原产地请求标题

`var http = require('http');

http.createServer(function (req, res) {     console.log("[200] " + req.method + " to " + req.url);

    res.statusCode = 200;     res.setHeader("Content-Type", "text/html");

    var origin = req.headers["origin"];     if (origin.indexOf("titan") > -1) {         res.setHeader("Access-Control-Allow-Origin", origin);     }

    res.write('Fruit Total');     res.write('

');

    res.write('You selected ' + req.url.substring(1));     res.write('

');     res.end();

}).listen(8080);`

我修改了服务器脚本,只在请求包含值包含titanOrigin头时设置Access-Control-Allow-Origin响应头。这是一种非常宽松的检查请求来源的方法,但是您可以在您自己的项目环境中定制这种方法,使其更加严格。

Image 提示你也可以将Access-Control-Allow-Origin头设置为星号(*,这意味着来自任何起点的跨起点请求都将被允许。在使用此设置之前,您应该仔细考虑安全问题。

高级 CORS 功能

CORS 规范定义了许多额外的头,可用于对跨来源请求实现细粒度控制,包括将请求限制到特定的 HTTP 方法。这些高级功能需要一个预检请求,其中浏览器向服务器发出一个请求以确定约束是什么,然后发出第二个请求以获取数据本身。在我写这篇文章时,这些高级特性还没有可靠地实现。

中止请求

XMLHttpRequest对象定义了一个允许你中止请求的方法,如表 32-6 所述。

为了演示这个特性,我修改了fruitselector.js Node.js 脚本,引入了 10 秒的延迟,如清单 32-13 所示。

清单 32-13。在服务器上引入延迟

`var http = require('http');

http.createServer(function (req, res) {     console.log("[200] " + req.method + " to " + req.url);

    res.statusCode = 200;     res.setHeader("Content-Type", "text/html");

    setTimeout(function() {         var origin = req.headers["origin"];         if (origin.indexOf("titan") > -1) {             res.setHeader("Access-Control-Allow-Origin", origin);         }

        res.write('Fruit Total');         res.write('

');         res.write('You selected ' + req.url.substring(1));         res.write('

');         res.end();     }, 10000);

}).listen(8080);`

当服务器收到请求时,它会写入初始响应头,暂停 10 秒钟,然后完成响应。清单 32-14 展示了如何在浏览器中使用XMLHttpRequest的中止功能。

清单 32-14。中止请求

`

             Example                   
            Apples             Cherries             Bananas         
` `        **
**             **Abort**         **
**         
Press a button
        

                    httpRequest.abort();                 } else {                     httpRequest = new XMLHttpRequest();                     httpRequest.onreadystatechange = handleResponse;                     httpRequest.onabort = handleAbort;                     httpRequest.open("GET", "http://titan:8080/" + e.target.innerHTML);                     httpRequest.send();                     document.getElementById("target").innerHTML = "Request Started";                 }             }

            function handleResponse() {                 if (httpRequest.readyState == 4 && httpRequest.status == 200) {                     document.getElementById("target").innerHTML                         = httpRequest.responseText;                 }             }

            function handleAbort() {                 document.getElementById("target").innerHTML = "Request Aborted";             }                   

`

我在文档中添加了一个Abort按钮,它调用XMLHttpRequest对象上的abort方法来中止一个正在进行的请求。既然我已经在服务器端引入了延迟,我们有足够的时间来做这件事。

XMLHttpRequest通过abort事件和readystatechange事件发出中止信号。在这个例子中,我响应abort事件,用targetid更新div元素的内容,以表明请求已经被中止。在图 32-7 中可以看到效果。

Image

图 32-7。中止请求

总结

在本章中,我通过XMLHttpRequest对象向您介绍了 Ajax。Ajax 允许你进行后台请求,为用户创造更流畅的体验。我解释了XMLHttpRequest对象如何通过一系列事件来表示请求的进度,如何检测和处理不同类型的错误,以及如何设置请求头来指示浏览器或服务器您需要的操作类型。作为一个更高级的主题,我介绍了跨源请求规范(CORS)——一组响应头,允许脚本向另一个源发出 Ajax 请求。这是一项有用的技术——只要您能够向来自服务器的响应添加标头。

本章中的所有例子都是关于从服务器检索数据的。在下一章中,我将向您展示如何发送数据。