JavaScript 技巧高级教程(二)
六、事件
这是最好的时代;那是最糟糕的时代。那是网景的时代;那是互联网浏览器的时代。我们面前的新事件处理者使它成为希望的春天。浏览器以不同方式实现事件处理的事实让我们陷入了绝望的冬天。但是最近几年,阳光变得清晰明亮,事件处理 API 已经跨浏览器实现了标准化(至少是 API 的大部分方面)。编写可用的 JavaScript 代码的最终目标一直是拥有一个能为用户工作的网页,不管他们使用什么浏览器或在什么平台上。长期以来,这意味着编写管理两种不同事件处理模型的事件处理代码。但是随着现代浏览器的出现,我们开发人员再也不用担心这个问题了。
多年来,JavaScript 中的事件概念已经发展到了我们现在所处的可靠、可用的平台。一旦 Internet Explorer 在版本 8 中实现了事件处理的 W3C 模型,我们就可以停止编写用于管理浏览器之间差异的库,而是专注于用事件做有趣和令人惊奇的事情。最终,这将我们引向 JavaScript 强大的模型-视图-控制器(MVC)模型,我们将在后面的章节中讨论。
在这一章中,我们将首先介绍 JavaScript 中的事件是如何工作的。根据这个理论的实际应用,我们将看看如何将事件绑定到元素。然后,我们将研究事件模型提供的信息,以及如何最好地控制它。当然,我们还需要涵盖我们可用的事件类型。我们以事件委托和一些关于事件和最佳实践的建议结束。
JavaScript 事件简介
如果你观察任何 JavaScript 代码的核心,你会发现事件是把所有东西粘在一起的粘合剂。无论是使用完整的基于 MVC 的单页面应用,还是简单地使用 JavaScript 为一两个页面添加一些功能,事件处理程序都是用户与我们的代码进行通信的方式。我们的数据将被绑定在 JavaScript 中,可能作为对象文字。我们将在 DOM 中表示这些数据,将其用作我们的视图。事件由 DOM 引发,由 JavaScript 代码处理,捕捉用户交互并指导应用的流程。结合使用 DOM 和 JavaScript 事件是使所有现代 web 应用成为可能的基本结合。
堆栈、队列和事件循环
在包括 JavaScript 在内的许多编程语言中,都有描述控制流、内存元素和下一步计划的隐喻。我们运行的代码,无论是从全局上下文,直接作为一个函数,还是作为一个从(或内部)调用的函数!)另一个功能,被称为栈。如果你正在运行一个函数foo,它调用一个函数bar,那么堆栈是三个帧深(全局,foo,然后bar)。这段代码运行后会发生什么?这是队列的职责,它管理当前堆栈解析后运行的下一组代码。每当堆栈清空时,它会进入队列并获取一段新的代码来运行。这些是我们理解事件的关键因素。不过,还有第三个元素:堆。这是变量、函数和其他命名对象存在的地方。当 JavaScript 需要访问一个对象、一个函数或一个变量时,它会进入堆来访问信息。对我们来说,堆不太重要,因为它在事件处理中的作用不如堆栈和队列大。
堆栈和队列是如何影响事件处理的?要回答这个问题,我们需要引入事件循环。这是浏览器中两个线程之间的协作:事件跟踪线程和 JavaScript 线程。
注意记住,除了 web 工作者,JavaScript 是单线程的。
这些线程协同工作来捕获用户事件,然后根据我们注册了事件处理程序的事件对它们进行排序。这个过程统称为事件循环。每次运行时,都会检查用户事件,看是否有针对它们注册的事件处理程序。如果不是,那么什么都不会发生。如果有事件处理程序,循环会将它们推到 JavaScript 队列的顶部,以便在 JavaScript 最方便的时候执行处理程序。
困难就在这里。队列管理“最早方便”的概念通常,这意味着在当前堆栈被解析之后。这可能给事件处理一种异步的感觉,特别是如果堆栈有许多帧深或者包含长时间运行的代码。事件被允许跳到队列的头部,但是它们不能中断堆栈。大多数情况下,这种区别对于开发人员来说并不重要,因为从事件触发、堆栈帧解析到事件处理代码运行之间的持续时间可能是人类无法察觉的。尽管如此,对我们来说重要的是理解事件循环只将事件跳到行的前面;它不会将当前运行的代码推开。
我们现在了解了浏览器、队列和堆栈是如何一起决定事件处理程序何时运行的。很快,我们将研究将事件绑定到事件处理程序的机制。但是有一个架构问题我们需要首先解决。考虑一下:如果你点击了 HTML 文档主体中某个段落的无序列表中的列表项的链接,那么这些元素中的哪一个会处理这个事件呢?一个以上的元素可以处理这个事件吗?如果是,哪个元素先获得事件?要回答这个问题,我们需要看看事件阶段。
事件阶段
JavaScript 事件分两个阶段执行,称为捕获和冒泡 。这意味着当一个事件被一个元素触发时(例如,用户点击一个链接,导致click事件被触发),被允许处理它的元素以及处理的顺序是不同的。你可以在图 6-1 中看到执行顺序的例子。它显示了每当用户单击页面上的第一个<a>元素时,触发了哪些事件处理程序以及触发的顺序。
图 6-1 。事件处理的两个阶段
看看这个简单的例子,有人点击了一个链接,你可以看到一个事件的执行顺序。假设用户点击了<a>元素;首先触发文档的 click 处理程序,然后是<body>的处理程序,然后是<div>的处理程序,依此类推,直到<a>元素,这个循环称为捕获阶段。一旦完成,它再次沿着树向上移动,并且<li>、<ul>、<div>、<body>和文档事件处理程序都被依次触发。
事件处理以这种方式构建有非常具体的历史原因。当 Netscape 引入事件处理时,它决定应该使用事件捕获。当 Internet Explorer 赶上它自己的事件处理版本时,它也随之出现了事件冒泡。那是浏览器战争的时代,像这样截然相反的架构选择是司空见惯的。多年来,它们阻碍了 JavaScript 的开发,因为程序员不得不浪费时间来维护规范事件处理的库(以及一些 DOM、Ajax 和其他一些东西!).
好消息是我们现在生活在未来。现代浏览器允许用户选择在哪个阶段捕捉事件。事实上,如果您愿意,可以在两个阶段都分配事件处理程序。这是一个勇敢的新世界。
不管在哪个阶段绑定事件,有两件事应该是显而易见的。首先,我们讨论了在列表项中点击锚标记的想法。这难道不应该把你送到链接的href属性指向的任何地方吗?也许有某种方法可以克服这种行为。此外,考虑事件阶段的一般前提:无论是捕获还是冒泡,事件都是通过 DOM 层次结构进行通信的。如果我们不希望该事件被传播呢?我们能防止一个事件被向上(或向下)传递吗?
但是我们太超前了。我们甚至还没有讨论如何绑定事件监听器!让我们现在就解决这个问题。
绑定事件侦听器
将事件处理程序绑定到元素的最佳方式一直是 JavaScript 中不断发展的探索。它始于浏览器强迫用户在 HTML 文档中内联编写事件处理程序代码。第一次努力被认为是草案或阿尔法代码是有原因的!后来,当我们想要遵循既定的最佳实践时,比如将逻辑与表示分离,使用内联事件处理程序是次优的。好吧,这是个严重的问题。试着想象管理一个代码库,其中一半的关键路径依赖于嵌入在表示层中的代码。这不是专业 JavaScript 程序员想做的事情!幸运的是,随着浏览器 API 和最佳实践标准的发展,这种技术已经被淘汰了。
当 Netscape 和 Internet Explorer 相互竞争时,它们各自开发了独立但非常相似的事件注册模型。最终,网景的模型被修改成了 W3C 标准,而 Internet Explorer 的模型保持不变。直到 Internet Explorer 9,也就是微软最终屈服并实现了通常所说的 W3C 事件处理。事实上,它走得更远,不再支持旧的事件处理 API。这对开发人员来说是一个福音,因为现在我们不再需要编写和维护库来处理浏览器之间的争论。
今天,有两种可靠地登记事件的方法。传统的方法是附加事件处理程序的旧的内联方式的分支,但是它是可靠的并且一致地工作,甚至在旧的浏览器上。另一种方法是使用 W3C 标准来注册事件。当然,我们会两者都看,因为你很可能两者都会遇到。
传统绑定
绑定事件的传统方式是最简单的绑定事件处理程序的方式。这利用了事件处理程序是 DOM 元素的属性这一事实。要使用这个方法,您需要将一个函数作为属性附加到您希望观察的 DOM 元素上。用document.getElementById检索一个元素(或者我们在第五章的中讨论过的任何其他元素检索函数)。让我们假设您想要观察click事件。只需为检索到的元素的onclick属性分配一个函数。搞定了。
对于本章中的例子,我们将使用一个带有许多可定位元素的标准 HTML 页面。页面的内容显示在清单 6-1 的中。
清单 6-1 。用于事件处理的示例 HTML 代码
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>Event Handling</title>
<link rel="stylesheet" href="school.css"/>
</head>
<body>
<div id="main">
<nav id="navbar">
<ul>
<li>Students
<ul>
<li id="Academics">Academics</li>
<li id="Athletics">Athletics</li>
<li id="Extracurriculars">Extracurriculars</li>
</ul>
</li>
<li>Faculty
<ul>
<li id="Frank Walsh">Frank Walsh</li>
<li id="Diane Walsh">Diane Walsh</li>
<li id="John Mullin">John Mullin</li>
<li id="Lou Garaventa">Lou Garaventa</li>
<li id="Dan Tully">Dan Tully</li>
<li id="Emily Su">Emily Su</li>
</ul>
</li>
</ul>
</nav>
<div id="welcome">
<h1>Welcome to the School of JavaScript</h1>
<h3 id="welcome-header">Click here for a welcome message!</h3>
<p id="welcome-content">Welcome to the School of JavaScript. Here, you will find many
<a href="/examples" id="examples-link">examples</a> of JavaScript,
taught by our most esteemed <a href="/faculty">faculty</a>.
<span id="disclaimer">Please note that these are only examples, and are not
necessarily <a href="/production-ready">production-ready code</a>.</span></p>
</div>
<hr/>
<div id="form-container">
<h2>Contact Form</h2>
<p>Thank you for your interest in the School of JavaScript. Please fill out the form
below so we can send you evenmore materials!</p>
<form id="main-form">
<ul>
<li><label for="firstName">First Name: </label><input id="firstName" type="text"/></li>
<li><label for="lastName">Last Name: </label><input id="lastName" type="text"/></li>
<li><label for="city">City: </label><input id="city" type="text"/></li>
<li><label for="state">State: </label><input id="state" type="text"/></li>
<li><label for="postCode">Postal Code: </label><input id="postCode" type="text"/></li>
<li><label for="comments">Comments: </label><br/>
<textarea name="" id="comments" cols="30" rows="10"></textarea>
</li>
<li><input type="submit"/> <input type="reset"/></li>
</ul>
</form>
</div>
</div>
</body>
</html>
正如你所看到的,首页上有很多我们想象中的 JavaScript 学校的元素。navbar 最终会有适当的事件处理来作为一个菜单,我们将为简单的验证添加事件处理(更复杂的验证将在第八章中介绍),我们还计划在欢迎消息中增加一些交互性。
现在,让我们做一些简单的事情。当我们点击进入firstName字段时,让我们在控制台上记录下来,然后为我们的元素设置一个黄色背景。很明显,我们计划很快做更多的事情,但是先从小事做起!让我们将这个事件与传统的事件处理结合起来(清单 6-2 )。
清单 6-2 。以传统方式绑定点击事件
// Retrieve the firstName element
var firstName = document.getElementById('firstName');
// Attach the event handler
firstName.onclick = function() {
console.log('You clicked in the first name field!');
firstName.style.background = 'yellow';
};
太棒了。有效。但它缺少某种东西。那就是灵活性。在这种情况下,我们必须为每个表单字段编写单独的事件处理函数。繁琐!有没有一种方法可以获得触发事件的元素的引用?
其实有两个办法!第一,也是最直接的,是在你的事件处理函数中提供一个参数,如清单 6-3 所示。这个参数是 event 对象,它包含关于刚刚触发的事件的信息。我们将很快更详细地查看事件对象。现在,我们知道事件对象的target属性指的是最初发出事件的 DOM 元素。
清单 6-3 。带参数的事件绑定
// Retrieve the firstName element
var firstName = document.getElementById('firstName');
// Attach the event handler
firstName.onclick = function(e) {
console.log('You clicked in the ' + e.target.id + ' field!');
e.target.style.background = 'yellow';
};
由于e.target指向firstName字段,并且实际上是对firstName字段的 DOM 元素的引用,我们可以检查它的id属性来查看我们点击了哪个字段。更重要的是,我们还可以改变它的样式属性!这意味着我们可以扩展这个事件处理程序来处理表单中的任何文本字段。
除了显式使用event对象,还有一种替代方法。我们也可以在函数中使用关键字this,如清单 6-4 中的所示。在事件处理函数的上下文中,this指的是事件的发出者。换句话说,event.target和this是同义词,或者,至少,它们指向同一个事物。
清单 6-4 。使用this关键字的事件绑定
// Retrieve the firstName element
var firstName = document.getElementById('firstName');
// Attach the event handler
firstName.onclick = function() {
console.log('You clicked in the ' + this.id + ' field!');
this.style.background = 'yellow';
};
你应该使用哪一个?event对象为您提供了所需的所有信息,而this对象有所限制,因为它只指向发出事件的 DOM 元素。使用其中一个而不是另一个是没有成本的,所以,一般来说,我们推荐使用event对象,因为您总是可以立即获得事件的所有细节。然而,有些情况下this对象仍然有用。目标总是指向发出事件的最近的元素。看看清单 6-1 中 ID 为welcome的<div>。假设我们添加了一个mouseover事件处理程序,用于在鼠标悬停在元素上时更改背景颜色,并添加了一个mouseout事件处理程序,用于在鼠标离开<div>时将背景颜色改回来。如果您在e.target上进行样式更改,事件将为每个子元素(welcome-header、welcome-content等等)触发!另一方面,如果您在this上进行样式更改,则更改仅在欢迎<div>上进行。当我们讨论事件委托时,我们将更详细地讨论这种差异。
传统装订的优势
传统装订有以下优点:
- 使用传统方法的最大优势是它非常简单和一致,因为你可以保证无论你在什么浏览器中使用它都是一样的。
- 当处理一个事件时,
this关键字引用当前元素,这可能是有用的(如清单 6-4 所示)。
传统装订的缺点
然而,它也有一些缺点:
-
传统方法不允许控制事件捕获或冒泡。所有的事件都会冒泡,并且不可能改变为事件捕获。
-
It’s only possible to bind one event handler to an element at a time. This has the potential to cause confusing results when working with the popular
window.onloadproperty (effectively overwriting other pieces of code that have used the same method of binding events). An example of this problem is shown in Listing 6-5, where an event handler overwrites an earlier event handler.清单 6-5 。事件处理程序相互覆盖
// Bind your initial load handler window.onload = myFirstHandler; // somewhere, in another library that you’ve included, // your first handler is overwritten // only 'mySecondHandler' is called when the page finishes loading window.onload = mySecondHandler; -
在 Internet Explorer 8 和更早版本中,
event对象参数不可用。相反,你必须使用window.event。
知道有可能盲目地覆盖其他事件,您可能应该选择仅在简单的情况下使用传统的事件绑定方法,在这种情况下,您可以信任与您的代码一起运行的所有其他代码。然而,避免这种麻烦的一种方法是使用现代浏览器实现的 W3C 事件绑定方法。
DOM 绑定:W3C
W3C 将事件处理程序绑定到 DOM 元素的方法是唯一真正标准化的方法。考虑到这一点,所有现代浏览器都支持这种附加事件的方式。Internet Explorer 8 和更老的版本没有,但旧版本的 Internet Explorer 几乎不是现代浏览器。如果你必须为这些设计,考虑使用传统的绑定。
附加新处理函数的代码很简单。它作为一个函数存在于每个 DOM 元素中。该函数名为addEventListener,带三个参数:事件的名称(如click;注意缺少前缀on、处理事件的函数以及启用或禁用事件捕获的布尔标志。清单 6-6 中的显示了一个使用中的addEventListener示例。
清单 6-6 。示例代码使用 W3C 方式绑定事件处理程序
// Retrieve the firstName element
var firstName = document.getElementById( 'firstName' );
// Attach the event handler
firstName.addEventListener( 'click', function ( e ) {
console.log( 'You clicked in the ' + e.target.id + ' field!' );
e.target.style.background = 'yellow';
} );
注意,在这个例子中,我们没有向addEventListener传递第三个参数。在这种情况下,第三个参数默认为 false,这意味着将使用事件冒泡。如果我们想使用事件捕获,我们可以显式地传递一个真值。
W3C 绑定的优势
W3C 事件绑定方法的优势如下:
- 此方法支持事件处理的捕获和冒泡阶段。通过将
addEventListener的最后一个参数设置为 false(默认值,用于冒泡)或 true(用于捕捉)来切换事件阶段。 - 在事件处理函数内部,
this关键字引用当前元素,就像在传统事件处理中一样。 event对象在处理函数的第一个参数中总是可用的。- 您可以将任意数量的事件绑定到一个元素,而不会覆盖先前绑定的处理程序。JavaScript 在内部堆叠处理程序,并按照它们注册的顺序运行。
W3C 绑定的缺点
W3C 事件绑定方法只有一个缺点:
- 它在 Internet Explorer 8 和更低版本中不工作。IE 使用类似语法的
attachEvent。
取消绑定事件
既然我们已经绑定了事件,那么如果我们想解除事件的绑定呢?也许我们绑定了一个click事件处理程序的按钮现在被禁用了。或者我们不再需要在悬停时高亮显示该 div。断开事件及其处理程序的连接相对简单。
对于传统的事件处理,只需为事件处理程序分配一个空字符串或 null,如下所示:
document.getElementById('welcome-content').onclick = null;
不太难吧?
W3C 事件处理的情况稍微复杂一些。相关功能是removeEventListener。它的三个参数是相同的:要移除的事件的类型、关联的处理程序以及捕获或冒泡模式的 true/false 值。不过,有一个问题。首先也是最重要的,这个函数必须是对同一个被分配了addEventListener 的函数的引用。不只是相同的代码行,而是相同的引用。所以如果你用addEventListener指定了一个匿名的内嵌函数,你就不能删除它。
提示如果你认为你以后可能需要移除事件处理程序,你应该总是为它使用一个命名函数。
同样,如果您在最初调用addEventListener时设置了第三个参数,那么您必须在removeEventListener中再次设置它。如果你不考虑这个参数,或者给它一个错误的值,removeEventListener就会无声无息地失败。清单 6-7 有一个解除绑定事件处理程序的例子。
清单 6-7 。解除事件处理程序的绑定
// Assume we have two buttons 'foo' and 'bar'
var foo = document.getElementById( 'foo' );
var bar = document.getElementById( 'bar' );
// When we click on foo, we want to log to the console "Clicked on foo!"
function fooHandler() {
console.log( 'Clicked on the foo button!' );
}
foo.addEventListener( 'click', fooHandler );
// When we click on bar, we want to _remove_ the event handler for foo.
function barHandler() {
console.log( 'Removing event handler for foo....' );
foo.removeEventListener( 'click', fooHandler );
}
bar.addEventListener( 'click', barHandler );
常见事件特征
JavaScript 事件有许多相对一致的特性,在开发时给你更多的能力和控制。最简单也是最古老的概念是event对象,它为您提供了一组元数据和上下文函数,因此您可以处理诸如鼠标事件和按键之类的事情。此外,还有一些函数可用于修改事件的正常捕获/冒泡流程。从里到外了解这些特性可以让你的生活简单很多。
事件对象
事件处理程序的一个标准特性是以某种方式访问event对象,该对象包含有关当前 eventEvent 对象的上下文信息。这个对象对于某些事件来说是一个非常有价值的资源。例如,在处理键按下时,您可以访问对象的keyCode属性以获取被按下的特定键。event对象之间存在一些微妙的差异,但我们将在本章后面讨论这些。现在,让我们解决两个悬而未决的问题:事件传播和默认行为。
取消事件冒泡
您知道事件捕获/冒泡是如何工作的,所以让我们来探索如何控制它。前一个例子中提到的重要一点是,如果您希望一个事件只发生在它的目标上,而不发生在它的父元素上,您没有办法阻止它。停止事件气泡的流动将导致类似于图 6-2 中所示的情况,其中事件的结果被第一个<a>元素捕获,随后的气泡被取消。
图 6-2 。第一个<a>元素捕获事件的结果
停止事件的冒泡(或捕获)在复杂的应用中非常有用。而且实现起来很简单。调用event对象的stopPropagation 方法,防止事件在层次结构中进一步向上(或向下)遍历。清单 6-8 显示了一个例子。
清单 6-8 。停止事件冒泡的示例
document.getElementById( 'disclaimer' ).addEventListener( 'click', function ( e ) {
// When clicking on the disclaimer, highlight it by making it bold
e.target.style.fontWeight = 'bold';
// The parent element wants to hide itself if this element is clicked on. We need to prevent that behavior
e.stopPropagation();
} );
document.getElementById( 'welcome-content' ).addEventListener( 'click', function ( e ) {
e.target.style.visibility = 'hidden';
} );
清单 6-9 显示了一个简短的代码片段,它在用户悬停的元素周围添加了一个红色边框。您可以通过向每个 DOM 元素添加一个mouseover和一个mouseout事件处理程序来实现这一点。如果您不停止事件冒泡,每次鼠标移动到一个元素上时,该元素及其所有父元素都会有红色边框,这不是您想要的。
清单 6-9 。使用stopPropagation防止所有元素改变颜色
// Event handling functions
function mouseOverHandler( e ) {
e.target.style.border = '1px solid red';
e.stopPropagation();
}
function mouseOutHandler( e ) {
this.style.border = '0px';
e.stopPropagation();
}
// Locate, and traverse, all the elements in the DOM
var all = document.getElementsByTagName( '*' );
for ( var i = 0; i < all.length; i++ ) {
// Watch for when the user moves the mouse over the element
// and add a red border around the element
all[i].addEventListener( 'mouseover', mouseOverHandler );
// Watch for when the user moves back out of the element
// and remove the border that we added
all[i].addEventListener( 'mouseout', mouseOutHandler );
}
有了阻止事件冒泡的能力,您现在可以完全控制哪些元素可以看到和处理事件。这是探索动态 web 应用开发所必需的基本工具。最后一个方面是取消浏览器的默认操作,允许您完全覆盖浏览器的功能并实现新的功能。
覆盖浏览器的默认操作
对于大多数发生的事件,浏览器都有一些默认的总是会发生的动作。例如,点击一个<a>元素将把你带到它的相关网页;这是浏览器中的默认操作。这个动作总是发生在捕获和冒泡事件阶段之后,如图 6-3 中的所示,它展示了用户点击网页中的<a>元素的结果。事件从捕获和冒泡阶段遍历 DOM 开始(如前所述)。但是,一旦事件完成遍历,浏览器就会尝试执行该事件和元素的默认操作。在本例中,它访问/ web 页面。
图 6-3 。一个事件的整个生命周期
默认动作可以概括为浏览器做的任何你没有明确告诉它去做的事情。下面是发生的不同类型的默认操作的示例,以及在什么事件上发生的示例:
- 单击一个
<a>元素会将您重定向到其href属性中提供的 URL。 - 使用您的键盘并按下 Ctrl+S,浏览器将尝试保存站点的物理表示。
- 提交 HTML
<form>会将查询数据提交到指定的 URL,并将浏览器重定向到该位置。 - 将鼠标移动到带有
alt或title属性的<img>上(取决于浏览器)会出现一个工具提示,提供属性的值。
即使您停止了事件冒泡,或者您根本没有绑定事件处理程序,浏览器也会执行前面的所有操作。这可能会导致脚本中出现重大问题。如果您希望提交的表单有不同的表现,该怎么办?或者,如果您希望<a>元素的行为与其预期目的不同,该怎么办?因为取消事件冒泡不足以阻止默认操作,所以您需要一些特定的代码来直接处理它。W3C 事件处理 API 通过event对象的preventDefault方法提供了这一功能(清单 6-10 )。对于许多浏览器来说,你可以选择简单地从你的事件处理器返回 false 作为替代,你可以在一些例子和库中看到这种行为编码。使用preventDefault 是首选,因为它是自文档化的——不像偶尔从事件处理程序返回 false 的晦涩技术。
清单 6-10 。防止默认浏览器动作发生的通用函数
document.getElementById('examples-link').addEventListener('click', function(e) {
e.preventDefault();
console.log("examples-link clicked");
});
使用preventDefault功能,您现在可以停止浏览器显示的任何默认动作。例如,这允许您利用链接的mouseover事件,而不用担心用户意外点击链接并把浏览器发送到其他地方。您可以覆盖在状态栏中显示链接位置的默认行为。或者考虑一个用来启动表单验证的 Submit 按钮。如果验证失败,您现在可以推迟提交表单(默认行为)。
事件委托
我们已经有了几乎所有操作事件处理程序的工具。一个挥之不去的问题是技术问题。假设我们有一个包含 20 个条目的无序列表。我们希望为每个列表项添加一个事件处理程序。更准确地说,我们希望能够不同地处理每个列表项的点击。我们可以用document.querySelectorAll获取所有元素,迭代结果,并附加单独的事件处理程序。无论是作为一个过程还是在浏览器中,这都是低效的。我们设置了 20 个事件处理程序(即使它们都指向同一个处理函数),而我们只能设置一个。
所有列表项都包含在一个无序列表标签中,那么为什么不利用我们可以在<ul>级别捕获点击事件的事实呢?我们唯一需要的是区分不同列表项的方法。回到传统事件绑定的部分,当我们讨论this对象时,我们注意到this指的是捕获事件的元素,而event.target指的是实际发出事件的元素。显然,我们可以使用this和event.target的组合。但是事件处理规范提供了event.currentTarget属性来解决这个问题。
在我们的列表项场景中,我们将一个click事件处理程序附加到无序列表。在事件处理程序中,<ul>是event.currentTarget。每个列表项都将是event.target属性。因此,我们可以检查event.target来查看哪个列表项被点击并被分派到适当的函数。清单 6-11 展示了一个事件委托的例子。
清单 6-11 。事件委托
function clickHandler(e) {
console.log( 'Handled at ' + e.currentTarget.id );
console.log( 'Emitted by ' + e.target.id );
}
var navbar = document.getElementById('navbar');
navbar.addEventListener( 'click', clickHandler );
clickHandler函数处理<nav>级别的事件,但是它接收从<nav>元素下的各种列表项发出的事件。
事件对象
在每个事件处理函数中,都提供了或者可以使用event对象。一般来说,event对象的属性涵盖了您可能想知道的关于某个事件的细节:它是什么类型的事件,它来自哪里,点击了什么坐标,或者可能按了什么键。不过,不同浏览器交流这些信息的方式有一些细微的差别。
常规属性
对于每种被捕获的事件类型,event对象上都有许多属性。所有这些event对象属性都与事件本身直接相关,没有什么是特定于浏览器的。下面是所有event对象属性的列表,并附有解释和示例代码。
类型
该属性包含当前被触发的事件的名称(如click或mouseover)。它可以用来提供一个通用的事件处理函数,然后确定性地执行相关代码。清单 6-12 展示了一个使用这个属性使一个处理程序根据事件类型产生不同效果的例子。
清单 6-12 。使用type属性为元素提供类似悬停的功能
function mouseHandler(e){
// Toggle the background color of the <div>, depending on the
// type of mouse event that occurred
this.style.background = (e.type === 'mouseover') ? '#EEE' : '#FFF';
}
// Locate the <div> that we want to hover over
var div = document.getElementById('welcome');
// Bind a single function to both the mouseover and mouseout events
div.addEventListener( 'mouseover', mouseHandler );
div.addEventListener( 'mouseout', mouseHandler );
目标
此属性包含对触发事件的元素的引用。例如,将一个点击处理程序绑定到一个<a>元素会有一个等于<a>元素本身的目标属性。
停止传播
stopPropagation方法停止事件冒泡(或捕获)过程,使当前元素成为最后一个接收特定事件的元素。
prevent default/return value = false
调用preventDefault方法会阻止浏览器的默认动作在所有现代 W3C 兼容浏览器中发生。
鼠标属性
只有当鼠标相关的事件被启动时,鼠标属性才会存在于event对象中(如click、mousedown、mouseup、mouseover、mousemove、mouseout、mouseenter、mouseleave)。在其他任何时候,您都可以假设返回的值不存在或不可靠。本节列出了鼠标事件期间存在于event对象上的所有属性。
pageX 和 pageY
这些属性包含鼠标光标相对于浏览器窗口绝对左上角的 x 和 y 坐标。无论如何滚动,它们都是一样的。
clientxand 客户〔??〕
这些属性包含鼠标光标相对于浏览器窗口的 x 和 y 坐标。因此,如果您向下(或横向)滚动文档,这些数字是相对于浏览器窗口的边缘的。当您在文档中滚动时,这些数字会发生变化。
layerX/layerY 和 offsetX/offsetY
这些属性应该包含鼠标光标相对于事件目标元素的 x 和 y 坐标。这些属性在 Chrome 和 IE 中有效,但在 Firefox 中无效。火狐支持 l ayerX和layerY,但是它们包含的信息不一样。相反,layer*属性似乎等同于适当的page*属性。
按钮
该属性仅在click、mousedown和mouseup事件上可用,是一个代表当前被点击的鼠标按钮的数字。左键点击为 0(零),中键点击为 1,右键点击为 2。
relatedTarget
此事件属性包含对鼠标刚刚离开的元素的引用。通常情况下,relatedTarget用于需要使用mouseover / mouseout的情况,但是你也需要知道鼠标刚刚在哪里,或者它要去哪里。清单 6-13 显示了一个树形菜单的变体(<ol>元素包含其他<ol>元素),其中子树只在用户第一次将鼠标移动到<li>子元素上时显示。
清单 6-13 。使用relatedTarget属性创建一个可导航的树
// When DOMContent is ready, get the references to the elements.
document.addEventListener('DOMContentLoaded', init);
function init(){
var top = document.getElementById("top");
var bottom = document.getElementById("bottom");
top.addEventListener("mouseover", onMouseOver);
top.addEventListener("mouseout", onMouseOut);
bottom.addEventListener("mouseover", onMouseOver);
bottom.addEventListener("mouseout", onMouseOut);
}
function onMouseOut(event) {
console.log("exited " + event.target.id + " for " + event.relatedTarget.id);
}
function onMouseOver(event) {
console.log("entered " + event.target.id + " from " + event.relatedTarget.id);
}
// Sample HTML:
<style>
div > div {
height: 128px;
width: 128px;
}
#top { background-color: red; }
#bottom { background-color: blue; }
</style>
<title>Untitled Document</title>
</head>
<body>
<div id="outer">
<div id="top"></div>
<div id="bottom"></div>
</div>
键盘属性
键盘属性一般只在键盘相关事件被启动时存在于event对象中(如keydown、keyup和keypress)。这个规则的例外是ctrlKey和shiftKey属性,它们在鼠标事件期间可用(允许用户按住 Ctrl 键并单击一个元素)。在其他任何时候,您都可以假设属性中包含的值不存在或不可靠。
ctrl ley
此属性返回一个布尔值,该值表示键盘 Ctrl 键是否被按住。ctrlKey属性可用于键盘和鼠标事件。
键码
这个属性包含一个对应于键盘上不同键的数字。某些键(如 PageUp 和 Home)的可用性可能会有所不同,但一般来说,所有其他键都工作可靠。表 6-1 是所有常用键盘键及其相关键码的参考。
表 6-1 。常用键码
|
钥匙
|
键码
| | --- | --- | | 退格 | eight | | 标签 | nine | | 进入 | Thirteen | | 空间 | Thirty-two | | 向左箭头 | Thirty-seven | | 向上箭头 | Thirty-eight | | 向右箭头 | Thirty-nine | | 下箭头键 | Forty | | 0–9 | 48–57 | | 阿塞拜疆(Azerbaijan 的缩写) | 65–90 |
shiftKey 键
该属性返回一个布尔值,该值表示键盘上的 Shift 键是否被按住。shiftKey属性可用于键盘和鼠标事件。
事件的类型
常见的 JavaScript 事件可以分为几类。可能最常用的类别是鼠标交互,紧随其后的是键盘和表单事件。下面的列表概括介绍了 web 应用中存在并可以处理的不同事件类别。
- 加载和错误 **事件:**该类的事件与页面本身相关,观察其加载状态。它们发生在用户第一次加载页面时(
load事件)和用户最终离开页面时(unload和beforeunload事件)。此外,使用error事件跟踪 JavaScript 错误,使您能够单独处理错误。 - UI 事件 : 这些用于跟踪用户何时与页面的一个方面而不是另一个方面进行交互。例如,使用这些工具,您可以可靠地知道用户何时开始输入表单元素。用于跟踪这一点的两个事件是
focus和blur(当对象失去焦点时)。 - 鼠标 **事件:**分为两类:跟踪鼠标当前所在位置的事件(
mouseover、mouseout),以及跟踪鼠标点击位置的事件(mouseup、mousedown、click)。 - 键盘 **事件:**这些事件负责跟踪键盘键何时被按下以及在什么上下文中被按下——例如,跟踪表单元素内的击键,而不是整个页面内发生的击键。与鼠标一样,三种事件类型用于跟踪键盘:
keyup、keydown和keypress。 - 表单 **事件:**这些事件直接关系到只与表单和表单输入元素发生的交互。
submit事件用于跟踪表单提交的时间;change事件观察用户对元素的输入;当一个<select>元素被更新时,就会触发select事件。
页面事件
所有页面事件专门处理整个页面的功能和状态。大多数事件类型处理页面的加载和卸载(每当用户访问页面然后再次离开时)。
负荷
一旦页面完全完成加载,就会触发load事件;此事件包括所有图像、外部 JavaScript 文件和外部 CSS 文件。它也适用于大多数具有src属性的元素(img、script、audio、video等等)。加载事件不会冒泡。
卸载前
这个活动有点奇怪,因为它完全不规范,但却得到了广泛的支持。它的行为与unload事件非常相似,但有一个重要的区别。在您的beforeunload 事件的事件处理程序中,如果您返回一个字符串,该字符串将显示在一条确认消息中,询问用户是否希望离开当前页面。如果他们拒绝,他们将能够停留在当前页面。Gmail 等动态 web 应用利用这一点来防止用户丢失任何未保存的数据。
错误
每当 JavaScript 代码中出现错误时,就会触发error事件。它可以作为捕获错误消息并优雅地显示或处理它们的一种方式。该事件处理程序的行为不同于其他事件处理程序,因为它没有传入一个event对象,而是包含一条解释所发生的特定错误的消息。
调整大小
页面事件:resize事件在每次用户调整浏览器窗口大小时发生。当用户调整浏览器窗口大小时,resize事件只会在调整完成后触发,而不是在整个过程中的每一步。
卷起
当用户在浏览器窗口中移动文档的位置时,scroll事件发生。这可以通过按键盘(如使用箭头键、上/下翻页或空格键)或使用滚动条来实现。
卸载
每当用户离开当前页面(例如,通过单击链接、点击“后退”按钮,甚至关闭浏览器窗口)时,都会触发此事件。阻止默认动作对这个事件不起作用(下一个最好的事情是beforeunload事件)。
UI 事件
用户界面事件处理用户如何与浏览器或页面元素交互。UI 事件可以帮助您确定用户当前正在与页面上的哪些元素进行交互,并为它们提供更多的上下文(比如突出显示或帮助菜单)。
集中
focus事件是确定页面光标当前位置的一种方式。默认情况下,焦点在整个文档中;但是,每当使用键盘单击或切换到某个链接或表单输入元素时,它就会转到那个位置。(该事件的一个例子如清单 6-14 所示)。
虚化
当用户将焦点从一个元素转移到另一个元素时,就会发生blur事件(在链接、输入元素或页面本身的上下文中)。(该事件的一个例子如清单 6-14 所示)。
鼠标事件
当用户移动鼠标指针或单击鼠标按钮时,就会发生鼠标事件。
点击
当用户在一个元素上按下鼠标左键(参见mousedown事件)并在同一元素上释放鼠标键(参见mouseup事件)时,就会发生click事件。
dblclick(数据库点击)
dblclick事件发生在用户快速连续完成两个click事件之后。双击的速度取决于操作系统的设置。
老鼠洞
当用户按下鼠标按钮时,mousedown事件发生。与keydown事件不同,这个事件只会在鼠标按下时触发一次。
老鼠!老鼠
当用户释放被按下的鼠标按钮时,mouseup事件发生。如果在按钮被按下的同一元素上释放按钮,也会发生一个click事件。
摩门教徒
每当用户将鼠标指针在页面上移动至少一个像素时,就会发生一个mousemove事件。触发的mousemove事件的数量(对于鼠标的完整移动)取决于用户移动鼠标的速度以及浏览器能够跟上更新的速度。
鼠标悬停
每当用户将鼠标从一个元素移到另一个元素时,就会发生mouseover事件。要查找用户来自哪个元素,请使用relatedTarget属性。此事件是资源密集型的,因为它可以为经过的每个像素或子元素触发一次。更喜欢mouseenter,描述简短。
鼠标移出
每当用户将鼠标移出一个元素时,mouseout事件就会发生。这包括将鼠标从一个父元素移动到一个子元素(乍一看可能不直观)。要找到用户要去的元素,使用relatedTarget属性。这个事件是资源密集型的,因为与mouseover配对,它可以触发很多很多次。偏爱mouseleave,简述。
鼠标输入
功能类似于mouseover,但是智能地注意它在元素中的位置。不会再次触发,直到它离开元素的盒子。
moueleve〔??〕
功能类似于mouseout,但是智能地注意它何时离开一个元素。
清单 6-14 展示了一个将事件对附加到元素上的例子,以允许键盘访问(和鼠标访问)网页。每当用户将鼠标移动到链接上或使用键盘导航到该链接时,该链接将会得到一些额外的颜色突出显示。
清单 6-14 。通过使用mouseover和mouseout事件创建悬停效果
// mouseEnter handler
function mouseEnterHandler() {
this.style.backgroundColor = 'blue';
}
// mouseLeave handler
function mouseLeaveHandler() {
this.style.backgroundColor = 'white';
}
// Find all the <a> elements, to attach the event handlers to them
var a = document.getElementsByTagName('a');
for ( var i = 0; i < a.length; i++ ) {
// Attach a mouseover and focus event handler to the <a> element,
// which changes the <a>s background to blue when the user either
// mouses over the link, or focuses on it (using the keyboard)
a[i].addEventListener('mouseenter', mouseEnterHandler);
a[i].addEventListener('focus', mouseEnterHandler);
// Attach a mouseout and blur event handler to the <a> element
// which changes the <a>s background back to its default white
// when the user moves away from the link
a[i].addEventListener('mouseleave', mouseLeaveHandler);
a[i].addEventListener('blur', mouseLeaveHandler);
}
键盘事件
键盘事件处理用户在键盘上按键的所有实例,无论是在文本输入区域内部还是外部。
击键/按键
keydown事件是按键时发生的第一个键盘事件。如果用户继续按住键,keydown事件将继续触发。keypress事件是keydown事件的常见同义词;它们的行为实际上是一样的,只有一个例外:如果你想阻止一个按键被按下的默认动作,你必须在keypress事件上这样做。
好好享受吧
keyup事件是发生的最后一个键盘事件(在keydown事件之后)。与keydown事件不同,这个事件在释放时只会触发一次(因为你不能长时间释放一个键)。
表单事件
表单事件主要处理 HTML 表单的主要元素<form>、<input>、<select>、<button>和<textarea>。
挑选
每当用户使用鼠标在输入区域中选择不同的文本块时,就会触发select 事件。通过该事件,您可以重新定义用户与表单交互的方式。
变化
当用户修改输入元素(包括<select>和<textarea>元素)的值时,会发生change和事件。该事件仅在用户已经离开元素,使其失去焦点后触发。
使服从
submit 事件只在表单中发生,并且只在用户点击提交按钮(位于表单中)或点击输入元素之一中的 Enter/Return 时发生。通过将绑定到表单的提交处理程序,而不是绑定到提交按钮的点击处理程序,可以确保捕捉到用户提交表单的所有尝试。
重置
reset 事件仅在用户单击表单内的重置按钮时发生(与提交按钮相反,提交按钮可以通过按回车键来复制)。
事件可访问性
在开发一个完全不引人注目的 web 应用时,要考虑的最后一点是确保即使不使用鼠标,您的事件也能工作。通过这样做,你帮助了两类人:需要辅助功能的人(视力受损的用户)和不喜欢使用鼠标的人。(有一天坐下来,断开鼠标和电脑的连接,学习如何只用键盘浏览网页。真是大开眼界的经历)。
为了让您的 JavaScript 事件更容易访问,无论何时使用click、mouseover和mouseout事件,您都应该考虑提供替代的非鼠标绑定。幸运的是,有一些简单的方法可以快速补救这种情况:
click事件 : 浏览器开发人员的一个聪明举措是让click事件在每次按下回车键时都能工作。这完全消除了为该事件提供替代的需要。然而,需要注意的一点是,一些开发人员喜欢绑定点击处理程序来提交表单中的按钮,以便在用户提交网页时进行监视。开发人员应该绑定到表单对象上的submit事件,而不是使用该事件,这是一个可靠的智能替代方法。 ***mouseover事件 : 使用键盘浏览网页时,你实际上是在将焦点转移到不同的元素上。通过将事件处理程序附加到mouseover和focus事件,可以确保为键盘和鼠标用户提供相同的解决方案。***mouseout事件 : 与mouseover事件的the focus事件一样,blur事件在用户的焦点从某个元素移开时发生。然后你可以使用blur事件来模拟键盘上的mouseout事件。****
****实际上,除了典型的鼠标事件之外,添加处理键盘事件的能力完全是微不足道的。如果没有别的,这样做可以帮助依赖键盘的用户更好地使用你的网站,这对每个人来说都是一个巨大的胜利。
摘要
在这一章中,我们首先介绍了 JavaScript 中的事件是如何工作的,并将它们与其他语言中的事件模型进行了比较。然后,您看到了事件模型提供了什么信息,以及如何最好地控制它。然后,我们探讨了将事件绑定到 DOM 元素,以及可用的不同类型的事件。我们最后讨论了event对象属性、事件类型以及如何为可访问性编码。****
七、JavaScript 和表单验证
不可避免的是,当遇到一个表单时,人们会考虑该表单数据的命运。JavaScript 的第一个实际应用之一是提供一种在客户端验证数据的方法,而不是必须忍受与服务器之间的往返。表单验证在当时有点特别,没有实用的 API,也没有与浏览器的真正集成。相反,程序员将事件和基本的文本操作绑定在一起,以提供方便的用户界面增强。
快进到今天,表单验证的情况已经好得多了。对于现代浏览器,我们有一个集成的验证 API,它与 HTML 和 CSS 一起工作,以提供一组广泛的表单验证功能。我们也有正则表达式,尽管它们很复杂,但对于数据验证来说,它比一个字符一个字符地迭代要好得多。
本章我们关注的是 JavaScript 和表单。虽然我们将重点关注表单验证,但我们也将关注 JavaScript 与表单交互方式的总体改进,以及一些新的与表单相关的 API。
HTML 和 CSS 表单验证
如前所述,自 JavaScript 早期以来,表单验证已经走过了漫长的道路。要真正深入表单验证的状态,我们不仅需要了解 JavaScript,还需要了解 HTML5 和 CSS。让我们从事物的 HTML 方面开始。在过去的几年里,由于 Web 超文本应用技术工作组(WHATWG) 的辛勤工作,HTML 已经发展并增加了许多新特性。这个组织推动了 HTML 的发展和更新,使之成为众所周知的 HTML5。虽然 HTML5 规范的范围意味着我们不能在这里讨论细节,但你可以在 Jonathan Reid 的 HTML5 程序员参考中找到更多信息(Apress,2015)。
HTML5 中特别值得注意的是对表单控件集的改进。这些变化大致分为两类:添加新的控件或控件样式(URL 字段、日期选择器等),以及表单验证。最初,我们的重点是后者。简单的表单验证已经转移到 HTML 中,不需要任何 JavaScript。这种验证是通过向表单控件添加某些属性来实现的。一个简单的例子是required属性,它与input元素配对,并在表单提交之前强制字段具有值。清单 7-1 是一个基本的例子。
清单 7-1 。简单的形式
<!DOCTYPE html>
<html>
<head>
<title>A basic form</title>
</head>
<body>
<h2>A basic form</h2>
<p>Please provide your first and last names.</p>
<form>
<fieldset>
<label for="firstName">First Name:</label><br/>
<input id="firstName" name="firstName" type="text" required/><br/>
<label for="lastName">Last Name:</label><br/>
<input type="text" name="lastName" id="lastName"/><br/>
</fieldset>
<input type="submit" value="Submit the form"/> <input type="reset" value="Reset the form"/>
</form>
</body>
</html>
注意,在表单中,我们有一个 ID 为firstName的输入字段,它添加了前面提到的required属性。如果我们试图在没有填写这个字段的情况下提交表单,我们将会看到类似于图 7-1 的结果。
图 7-1 。此基本表单中缺少名字
Chrome 和 IE 11 上的显示看起来大致相同(Chrome 没有用红色边框包围字段,但 IE 有一个更块状、更自信的红色边框)。如果您要使firstName和lastName字段都成为必填字段,边框将出现在每个字段上,但是弹出工具提示将只与第一个有问题的字段相关联。自定义弹出窗口怎么样?我们将很快处理这个问题,但是它需要 JavaScript。
有几种其他类型的验证可以通过 HTML 属性激活。他们是
pattern:该属性采用正则表达式作为参数。正则表达式不需要用斜杠括起来。正则表达式语言引擎与 JavaScript 的相同(也有相同的问题)。这是附加到输入元素上的。请注意,email和url的输入类型分别意味着适用于有效电子邮件地址和 URL 的模式值。模式验证不适用于 Safari 8、iOS Safari 8.1 或 Opera Mini。step:强制值匹配指定步长值的倍数。限制为输入类型number、range或日期时间之一。步骤验证在 Chrome 6.0、Firefox 16.0、IE 10、Opera 10.62 和 Safari 5.0 中有效。min/max:最小值或最大值,不仅适用于数字,也适用于日期时间。这种方法在 Chrome 41、Opera 27 和 Chrome for Android 41 中都有效。maxlength:字段中数据的最大长度,以字符为单位。仅对text、email、search、password、tel或url输入类型有效。这种方法通常不会进行太多的验证,以防止用户在它所附加的字段中输入过多的数据。它可以在所有现代浏览器上运行。
在表单级别,您可以通过以下两种方法之一整体关闭验证。您可以将formnovalidate属性添加到表单的提交按钮,或者将novalidate属性添加到表单元素本身。
半铸钢ˌ钢性铸铁(Cast Semi-Steel)
不满足于让 HTML5 做所有的工作,CSS 规范已经被更新来处理表单验证。处于无效状态的表单元素可以通过:invalid伪类来访问。不幸的是,这个伪类的实现还有待改进。首先,在页面加载时检查表单元素的有效性。因此,如果您有如下样式:
:invalid { background-color: yellow }
当页面加载时,许多字段会有黄色背景。第二,Chrome 和 IE 只对表单元素应用:invalid。如果表单中的任何元素无效,Firefox 会将其应用到整个表单。考虑清单 7-2 中的。
清单 7-2 。使用:invalid伪类
<!DOCTYPE html>
<html>
<head>
<title>A basic form</title>
<style>
:invalid {
background-color: yellow
}
</style>
</head>
<body>
<h2>A basic form</h2>
<p>Please provide your first and last names.</p>
<form>
<fieldset>
<label for="firstName">First Name:</label><br/>
<input id="firstName" name="firstName" type="text" required/><br/>
<label for="lastName">Last Name:</label><br/>
<input type="text" name="lastName" id="lastName"/><br/>
</fieldset>
<input type="submit" value="Submit the form"/> <input type="reset" value="Reset the form"/>
</form>
</body>
</html>:
在这个清单中,Firefox 用黄色背景显示整个表单,因为表单的一个元素处于无效状态。通过将:invalid的样式更改为input:invalid来解决这个问题,这将为您提供跨浏览器的一致行为。
CSS 还提供了一些其他的伪类:
:valid包含处于有效状态的元素。:required获取其required属性设置为 true 的元素。:optional获取没有设置required属性的元素。:in-range用于最小/最大边界内的元素;IE 不支持。:out-of-range用于那些在那些界限之外的;IE 不支持。
最后说一下红光和弹出消息。在 Firefox 中提交后,无效元素周围会出现红色光晕效果。(在 Internet Explorer 中,它是一个简单的红色边框,没有发光效果。)Firefox 将效果公开为:-moz-ui-invalid伪类。您可以按如下方式覆盖它:
:-moz-ui-invalid { box-shadow: none }
唉,Internet Explorer 并没有将它的效果公开为一个伪类。这意味着我们已经达到了单独使用 HTML 和 CSS 的极限。有些功能我们想改变,有些功能我们想实现。这就是 JavaScript 重新发挥作用的地方。>:
JavaScript 表单验证
很大程度上得益于 HTML5 living standard,JavaScript 现在有了一个用于表单验证的连贯 API。这依赖于一个相对简单的验证检查生命周期:这个表单元素有验证例程吗?如果有,是否通过?如果失败了,为什么会失败?与这个过程交织在一起的是 JavaScript 的逻辑访问点,要么通过方法调用,要么通过捕获事件。这是一个好系统,虽然这并不是说它不能忍受一点改进。但是我们不要想太多。
检查表单元素有效性的最简单方法是对其调用 checkValidity。支持每个表单元素的 JavaScript 对象现在可以使用这个checkValidity方法。此方法访问 HTML 中为元素设置的验证约束。每个约束都根据元素的当前值进行测试。如果任何约束失败,checkValidity将返回 false。如果全部通过,checkValidity返回 true。对checkValidity的调用不限于单个元素。它们也可以根据表单标签来制作。如果是这种情况,checkValidity调用将被委托给表单中的每个表单元素。如果所有的子调用都返回 true(即所有的表单元素都有效),那么表单作为一个整体是有效的。相反,如果任何子调用返回 false,则表单无效。
除了获得关于元素有效性的简单布尔答案之外,我们还可以找出元素有效性失败的原因。任何元素的validity属性都是一个对象,它包含所有可能导致验证失败的原因,称为ValidityState对象。我们可以迭代它的属性,如果有一个是真的,我们知道这是元素有效性检查失败的原因之一。其性能见表 7-1 。
表 7-1 。Validity State属性
|
财产
|
说明
|
| --- | --- |
| valid | 元素的值是否有效。先从这个属性开始。 |
| valueMissing | 没有值的必需元素。 |
| patternMismatch | 对模式指定的正则表达式的检查失败。 |
| rangeUnderflow | 值低于最小值。 |
| rangeOverflow | 值高于最大值。 |
| stepMismatch | 值不是有效的步长值。 |
| tooLong | 值大于maxlength允许的值(以字符为单位)。 |
| typeMismatch | 值未通过对email或url输入类型的检查。 |
| customError | 如果引发了自定义错误,则为 True。 |
| badInput | 当浏览器认为值是无效的,但不是因为已经列出的原因之一时,这是一种总括;未在 Internet Explorer 中实现。 |
检查元素的validity属性的动作运行有效性检查。没有必要调用元素。 checkValidity第一。
让我们来看看有效性检查的运行情况。首先,清单 7-3 显示了我们的 HTML 的相关部分。
清单 7-3 。我们的 HTML 表单
<body>
<h2>A basic form</h2>
<p>Please fill in the requested information.</p>
<form id="nameForm">
<div id="fields">
<label for="firstName">First Name:</label><br/>
<input id="firstName" name="firstName" type="text" class="foo" required/><br/>
<label for="lastName">Last Name:</label><br/>
<input type="text" name="lastName" id="lastName" required/><br/>
<label for="phone">Phone</label><br/>
<input type="tel" id="phone"/><br/>
<label for="age">Age (must be over 13):</label><br/>
<input type="number" name="age" id="age" step="2" min="14" max="100"/><br/>
<label for="email">Email</label><br/>
<input type="email" id="email"/><br/>
<label for="url">Website</label><br/>
<input type="url" id="url"/><br/>
</div>
<div id="buttons">
<input id="overallBtn" value="Check overall validity" type="button"/>
<input id="validBtn" type="button" value="Display validity"/>
<input id="submitBtn" type="submit" value="Submit the form"/>
<input type="reset" id="resetBtn" value="Reset the form"/>
</div>
</form>
<div>
<h2>Validation results</h2>
<div id="vResults"></div>
<div id="vDetails"></div>
</div>
</body>
注意submit、reset和validity检查按钮在各自的div中。这使得使用document.querySelectorAll只检索相关的表单字段变得更加容易,这些字段在单独的div中。现在,继续我们的 JavaScript ( 清单 7-4 )。
清单 7-4 。表单验证和有效性
window.addEventListener( 'DOMContentLoaded', function () {
var validBtn = document.getElementById( 'validBtn' );
var overAllBtn = document.getElementById( 'overallBtn' );
var form = document.getElementById( 'nameForm' ); // Or document.forms[0]
var vDetails = document.getElementById( 'vDetails' );
var vResults = document.getElementById( 'vResults' );
overallBtn.addEventListener( 'click', function () {
var formValid = form.checkValidity();
vResults.innerHTML = 'Is the form valid? ' + formValid;
} );
validBtn.addEventListener( 'click', function () {
var output = '';
var inputs = form.querySelectorAll( '#fields > input' );
for ( var x = 0; x < inputs.length; x++ ) {
var el = inputs[x];
output += el.id + ' : ' + el.validity.valid;
if (! el.validity.valid) {
output += ' [';
for (var reason in el.validity) {
if (el.validity[reason]) {
output += reason
}
}
output += ']';
}
output += '<br/>'
}
vDetails.innerHTML = output;
} );
} );
整个代码块是一个绑定到 DOM 加载时间的事件。回想一下,我们不想尝试向可能还没有创建的元素添加事件处理程序。首先,我们将检索页面中的相关元素:两个有效性检查按钮、输出 div 和表单。接下来,我们将为整体有效性检查设置事件处理。注意,在这种情况下,为了简单起见,我们检查整个表单的有效性。我们在vResults div 中显示这个检查的结果。
第二个事件处理程序包括检查每个表单元素的有效性状态。我们通过使用querySelectorAll获取 ID 为fields的div下的所有输入字段来获取适当的元素。(这比编写一个扩展的 CSS 选择器来查找不包括提交、重置和按钮的输入类型更简单。)获得我们想要的元素后,很简单,迭代元素并检查它们的validity属性的valid子属性。如果它们无效(valid为假),那么我们打印出该字段无效的原因。我们鼓励您使用各种不同的输入值进行尝试。
这个演示揭示了一些有趣的事情。首先,如果您加载页面并单击“显示有效性”按钮,firstName和lastName字段无效(正如您所料,因为它们是空的),但是phone、age、email和url字段(也是空的)有效!如果该字段不是必需的,则空值有效。另外,注意 email 字段有两个验证,email 的隐含验证,以及一个模式需求。尝试输入一个不包含“@foo.com”之类的电子邮件,您会发现有可能一次多次验证失败。Firefox 还会告诉你,如果你输入了不完整的电子邮件地址(比如说,只是一个用户名),那么typeMismatchbadInput的值就会失效。您可能倾向于只依赖于valid属性,但是知道字段验证失败的原因对于传达给用户来说是非常重要的信息,毕竟,如果不通过各种验证约束,用户将无法成功提交表单。
验证和用户
到目前为止,我们已经把大部分时间花在了表单验证的技术方面。我们还应该讨论什么时候应该执行表单验证。我们有许多选择。简单地使用表单验证 API 意味着我们可以在提交时自动进行表单验证。多亏了checkValidity方法,我们能够在需要时调用任何给定元素的验证。作为最佳实践,我们应该尽早执行表单验证。这在实践中意味着什么取决于被验证的字段。首先,我们应该验证表单字段中的更改。将 change 事件处理程序附加到您的表单控件,并让它在该控件上调用checkValidity。在表单验证 API 中工作,对于何时进行验证的问题,这是一个相当简单的答案。
但是如果我们不在表单验证 API 中工作呢?在表单验证 API 中工作的一个更重要的限制是它没有定制验证的功能。你不能添加一大块自定义代码,绑定在一个函数中,比如说,作为一个验证例程运行。但是毫无疑问,在某个时候你会想要这么做。当您发现自己处于这种情况时,将验证与变更事件处理程序联系起来仍然具有一般和实际的意义。可能会有例外。考虑一个需要 Ajax 调用来验证其值的字段,可能是基于字段中输入的前几个字符。在这种情况下,您可以将验证与按键事件联系起来,可能还会集成自动建议功能。在下一章讨论 Ajax 时,我们将看一个这样的例子。
无论您选择在哪个阶段进行验证,都要记住您的用户。填写完一张表格,然后发现很多数据由于各种原因无效,这是非常令人沮丧的。用户倾向于更容易接受内嵌的修正,而不是在提交时给出一个错误列表。
验证事件
表单验证 API 的另一个增加是无效表单元素现在抛出一个invalid事件。该事件仅在响应对checkValidity的调用时抛出。可以在元素本身或包含元素的表单上进行checkValidity调用。invalid事件不冒泡。表单本身没有invalid事件,尽管表单可能是无效的。
您可以通过调用发射控件上的addEventListener来捕获事件。一旦进入事件处理程序,事件对象本身不提供任何与验证相关的信息。您必须通过event.target属性检索元素,然后查询它的validity属性,找出元素无效的确切原因。但是你可以用事件的preventDefault方法做一些相当有趣的事情。当您调用preventDefault时,浏览器对无效元素的样式行为将不会被应用。请记住,只有在提交表单时,样式更改才会被一致地应用。(如果您更改了表单控件的值并远离它进行模糊处理,Firefox 将应用样式更改。)这对于不同的浏览器意味着不同的事情:
- Chrome 不会设置无效元素的样式,但会给出一个弹出消息,它会抑制该元素的弹出窗口。
- Firefox 既有弹出窗口又有样式,它会抑制弹出窗口,但不会抑制或阻止元素周围的红光效果。
- Internet Explorer 在元素周围既有弹出窗口又有红色边框,它将取消元素周围的弹出窗口和边框。
让我们看一个展示这种行为的例子。从清单 7-5 中一个相对熟悉的 HTML 表单开始。
清单 7-5 。有效性事件表单
<!DOCTYPE html>
<html>
<head>
<title>A basic form</title>
<style>
input:invalid {
background-color: yellow
}
</style>
</head>
<body>
<h2>A basic form</h2>
<p>Please provide your first and last names.</p>
<form id="nameForm">
<fieldset>
<label for="firstName">First Name:</label><br/>
<input id="firstName" name="firstName" type="text" required/><br/>
<label for="lastName">Last Name:</label><br/>
<input type="text" name="lastName" id="lastName" required/><br/>
</fieldset>
<div>
<input type="submit" value="Submit the form"/> <input type="reset" value="Reset the form"/>
</div>
<div>
<input id="firstNameBtn" type="button" value="Check first name validity."/>
<input id="formBtn" type="button" value="Check form validity"/>
<input id="preventBtn" type="button" value="Prevent default behavior"/>
<input id="restoreBtn" type="button" value="Restore default behavior"/>
</div>
</form>
<div id="vResults"></div>
<script src="listing_7_5.js"></script>
</body>
</html> .
请注意,我们已经为无效的输入元素添加了样式。这种样式与无效事件的默认行为无关。让我们看看支持代码(清单 7-6 )。
清单 7-6 。JavaScript 中的有效性事件
window.addEventListener( 'DOMContentLoaded', function () {
var outputDiv = document.getElementById( 'vResults' );
var firstName = document.getElementById( 'firstName' );
firstName.addEventListener("focus", function(){
outputDiv.innerHTML = '';
});
function preventDefaultHandler( evt ) {
evt.preventDefault();
}
firstName.addEventListener( 'invalid', function ( event ) {
outputDiv.innerHTML = 'firstName is invalid';
} );
document.getElementById( 'firstNameBtn' ).addEventListener( 'click', function () {
firstName.checkValidity();
} );
document.getElementById( 'formBtn' ).addEventListener( 'click', function () {
document.getElementById( 'nameForm' ).checkValidity();
} );
document.getElementById( 'preventBtn' ).addEventListener( 'click', function () {
firstName.addEventListener( 'invalid', preventDefaultHandler );
} );
document.getElementById( 'restoreBtn' ).addEventListener( 'click', function () {
firstName.removeEventListener( 'invalid', preventDefaultHandler );
} );
} ); .
像往常一样,我们所有的代码都在DOMContentLoaded事件触发后被激活。对于firstName字段上的无效事件,我们有一个基本的事件处理程序,它输出到vResults div。然后,我们为专用按钮添加事件处理程序。首先我们创建两个方便的按钮:一个用于检查firstName字段的有效性,另一个用于检查整个表单的有效性。然后我们添加行为来覆盖或恢复与无效事件相关的默认行为。试试吧!
自定义验证
我们现在拥有几乎所有的工具来全面控制表单验证。我们可以选择激活哪些验证。我们可以控制何时执行验证。我们可以捕获无效事件,并防止默认行为(特别是关于样式)触发。如前所述,我们不能定制实际的验证例程(唉)。那么我们还能做些什么呢?我们可能希望控制用户在提交表单时看到的验证弹出窗口中的消息。(弹出窗口的外观也是不可定制的。还记得我们提到过 API 及其实现有一些缺点吗?)
要更改表单域无效时出现的验证消息,请使用与表单控件相关联的setCustomValidity函数。将一个字符串作为参数传递给setCustomValidity ,该字符串将作为弹出窗口的文本出现。不过,这确实有一些其他的副作用。在 Firefox 中,该字段在页面加载时会显示为无效,带有红色光晕效果。在浏览器或 Chrome 上使用setCustomValidity对页面加载没有影响。如前所述,可以通过覆盖:-moz-ui-invalid伪类来关闭 Firefox 样式。但更成问题的是,当使用setCustomValidity时,问题中的表单控件的 validityState的customError属性被设置为 true。这意味着validityState的valid属性为假,这意味着它读取为无效。只需更改与有效性检查相关的消息!这是不幸的,并使setCustomValidity几乎无用。
另一种方法是使用多填充物。有一个很长的多填充列表,不仅用于表单验证,还用于其他 HTML5 项目,这些项目可能不支持您需要使用的每个浏览器。您可以在这里找到它们:
https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills
阻止表单验证
表单验证还有一个方面我们没有探讨:关闭它。本章的大部分内容都集中在使用新的表单验证 API,并探索它的局限性。但是如果有一个 API 的交易破坏者,一些阻止我们大规模使用 API 的特性(或者 bug,或者不一致性)会怎么样呢?如果是这种情况,我们需要做两件事:停止自动表单验证,用我们自己的表单来代替。我们感兴趣的是前者,因为后者只是根据我们的想法重新实现一个 API。要关闭表单验证行为,请将novalidate属性添加到表单元素中。您可以通过向 submit 按钮添加formnovalidate属性来防止每次点击 submit 按钮时的行为,但是这并不能从整体上关闭表单的表单验证。由于我们可能希望用自己定制的 API 来替代表单验证,所以我们希望使用novalidate属性来完全禁用表单验证(针对父元素)。
摘要
在这一章中,我们花了大部分时间来看新的 JavaScript 表单验证 API。在其约束下,它是自动验证用户数据的强大工具。表单提交时会自动进行验证,我们也可以在自己选择的任何时间进行验证。如果需要,可以关闭验证。我们可以定制有效和无效元素的外观,甚至可以定制元素无效时显示的消息。
API 并不是没有问题。它缺乏一些关键的定制能力,比如允许错误消息的样式,或者定制验证例程。在主要的浏览器之间有一些小的实现差异,清理起来可能会有点痛苦。API 的一些官方部分(想想willValidate属性)目前还没有实现,其他部分(setCustomValidity)有严重的问题。
总的来说,API 是 JavaScript、HTML、CSS 和浏览器的一大进步。我们期待着看到它在未来将如何完善。
八、Ajax 简介
Ajax 是 Adaptive Path 的 Jesse James Garrett 创造的一个术语,用来描述使用 XMLHttpRequest 对象实现的客户端到服务器的异步通信,所有现代浏览器都提供了这个对象。Ajax 是 A 同步JavaScriptAndXML 的首字母缩写,它已经发展成为一个术语,用来封装创建动态 web 应用所必需的技术。此外,Ajax 技术的各个组件是完全可以互换的——例如,使用 JSON 代替 XML 是完全有效的。
自从这本书的第一版以来,Ajax 的用法已经发生了显著的变化。Ajax 曾经是一种奇特的 API,现在是专业 JavaScript 程序员工具箱中的标准部分。W3C 已经对 XMLHttpRequest 对象 Ajax 的基础——进行了彻底改革,增加了新的特性并阐明了其他特性的行为。Ajax 的核心规则之一:不要连接到外部域;这是通过使用跨源资源共享标准(也称为 CORS)来实现的。
在这一章中,你将看到构成整个 Ajax 过程的细节。我们将集中讨论 XMLHttpRequest 对象 API,但是我们也将讨论一些辅助问题,比如处理响应、管理 HTTP 状态代码等等。目的是让您全面了解 Ajax 请求/响应周期中发生的事情。
我们不会为 Ajax 交互提供 API。按照管理 Ajax 的各种规范编写代码是一件简单的事情,但是为现实世界编写一个完整的 Ajax 库肯定不是那么简单。考虑 jQuery 库的 Ajax 部分,它有十几个边缘案例,处理 Internet Explorer、Firefox、Chrome 和其他浏览器中 API 的各种奇怪之处。此外,因为 jQuery、Dojo、Ext JS 和其他几个较小的库已经有了 Ajax 实现,我们认为没有理由在这里重新发明这个特殊的轮子。相反,我们将提供 Ajax 交互的例子,这些例子是根据当前(截至发布时)的规范编写的。这些示例旨在提供指导和示范,而非最终结果。我们鼓励您在使用 Ajax 时研究一下实用程序库,如 jQuery、Zepto、Dojo、Ext JS 和 MooTools,或者侧重于 Ajax 的库,如 Fermata 和 reqwest。
这留下了相当多的讨论!本章将涵盖以下内容:
- 检查不同类型的 HTTP 请求
- 确定向服务器发送数据的最佳方式
- 查看整个 HTTP 响应,思考如何不仅处理成功的响应,还处理出错的响应(以某种方式)
- 读取、遍历和操作来自服务器响应的数据结果
- 处理异步响应
- 跨域请求,由 CORS 支持
使用 Ajax
创建一个简单的 Ajax 实现不需要太多代码;然而,这种实现给你带来的好处是巨大的。例如,您的代码可以异步处理提交过程,在完成后加载一小部分期望的结果,而不必强迫用户在提交表单后请求一个全新的网页。事实上,当我们将 Ajax 请求绑定到按键等事件的处理程序时,根本不需要等待表单提交。这就是谷歌自动建议搜索功能“神奇”的背后。当您开始输入一个搜索词时,Google 会根据您的输入发出一个 Ajax 请求。当您细化搜索时,它会发出其他 Ajax 请求。谷歌不仅会显示建议,还会根据第一个可能的选项,显示结果的第一页。图 8-1 显示了这个过程的一个例子。
图 8-1 。一个即时域名搜索的例子 在你输入的时候寻找域名
HTTP 请求
Ajax 过程中最重要也可能是最一致的方面是 HTTP 请求 部分。超文本传输协议(HTTP) 被简单地设计用来传输 HTML 文档和相关文件。幸运的是,所有现代浏览器都支持使用 JavaScript 动态异步建立 HTTP 连接的方法。事实证明,这对于开发响应速度更快的 web 应用非常有用。
Ajax 的最终目的是向服务器异步发送数据并接收额外的数据。数据如何格式化最终取决于您的特定需求。
在下面几节中,您将看到如何使用不同的 HTTP 请求来格式化要传输到服务器的数据。然后,您将看到如何建立与服务器的基本连接,以及在跨浏览器环境中实现这一点所需的细节。
建立连接
所有 Ajax 进程都从连接到服务器开始。到服务器的连接通常通过 XMLHttpRequest 对象来组织。(唯一的例外是在旧版本的 Internet Explorer 中进行跨域请求时。但我们稍后会谈到这一点。现在,我们将依赖 XMLHttpRequest 对象。)
与 XMLHttpRequest 对象的通信遵循一个生命周期:
- 创建 XMLHttpRequest 的实例。
- 使用适当的设置配置对象。
- 通过特定的 HTTP 谓词和目的地打开请求。
- 发送请求。
清单 8-1 显示了如何与服务器建立一个基本的 GET 请求。
清单 8-1 。与服务器建立 HTTP GET 请求的跨浏览器方式
// Create the request object
var xml = new XMLHttpRequest();
// If we needed to do any special configuration, we would do it here
// Open the socket
xml.open('GET', '/some/url.cgi', true);
// Establish the connection to the server and send any additional data
xml.send();
如您所见,与服务器建立连接所需的代码非常简单;真的没什么大不了的。当您想要高级特性(例如检查超时或修改的数据)时,会出现一系列困难;我们将在本章的“HTTP 响应”部分讨论这些细节。当您想将数据从客户机(您的浏览器)传输到服务器时,另一组困难就出现了。这是整个 Ajax 方法最重要的特性之一。我们会在 URL 上发送简单的数据吗?发布的数据呢?更复杂的格式呢?带着这些问题(当然还有其他问题),让我们看看打包一些数据并将其发送到服务器所需的细节。
序列化数据
将一组数据发送到服务器的第一步是对其进行格式化,以便服务器可以轻松读取;这个过程叫做序列化。在序列化数据之前,我们需要问几个问题。首先:
- 我们在发送什么数据?我们是在发送变量和值对吗?大数据集?文件?
- 我们如何发送这些数据,GET?帖子?另一个 HTTP 动词?
- 我们使用的是什么格式的数据?有两个:
application/x-www-form-urlencoded和multipart/form-data。前者有时被称为查询字符串编码,采用常见的形式 var1=val1 & var2=val2...
从 JavaScript 的角度来看,第三个问题是最重要的。第一个和第二个问题是设计的问题。它们会对我们的应用产生影响,但不一定需要不同的代码。但是我们使用哪种数据格式对我们的应用有很大的影响。
在现代浏览器中,处理多部分/表单数据信息实际上更容易。由于有了 FormData 对象,我们可以非常容易地将数据序列化为一个对象,浏览器会自动将其转换为多部分/表单数据格式。不幸的是,并不是所有的浏览器都支持规范中的每个选项。但是现在我们可以做很多事情。
表单数据对象
表单数据对象是 HTML5 中一个相对较新的提议。WHATWG 和 W3C 打算为作为 Ajax(或任何 HTTP)请求的一部分发送的信息提供一种更加面向对象、类似地图的方法。因此,FormData 对象可以被初始化为空,也可以与表单相关联。如果使用表单进行初始化,获取对包含表单 DOM 元素的引用(通常通过 getElementById)并将其传递给 FormData 构造函数。否则,如上所述,FormData 对象将为空。无论哪种方式,您都可以选择通过 append 函数添加新数据(清单 8-2 )。
清单 8-2 。对 FormData 使用 append 方法的示例
// Create the formData object
var formDataObj= new FormData();
//append name/values to be sent to the server
formDataObj.append('first', 'Yakko');
formDataObj.append('second', 'Wakko');
formDataObj.append('third', 'Dot');
// Create the request object
var xml = new XMLHttpRequest();
// Set up a POST to the server
xml.open('POST', '/some/url.cgi');
// Send over the formData
xml.send(formDataObj);
尽管在规格上有一些不同。WHATWG 规范还包括删除、获取和设置对象值的函数。现代的浏览器都没有实现这些功能。在某种程度上,这是因为 W3C 版本的规范只有 append 功能。现代浏览器遵循这个 W3C 规范,至少目前是这样。这意味着 FormData 对象是单向的:数据传入,但只能在 HTTP 请求的另一端访问。
FormData 对象的替代方法是用 JavaScript 序列化。也就是说,将您打算传输到服务器的数据进行 URL 编码,然后作为请求的一部分发送到服务器。这并不太难,尽管有一些需要注意的地方。
让我们来看一些你可以发送给服务器的数据类型的例子,以及它们产生的服务器友好的、序列化的输出,如清单 8-3 所示。
清单 8-3 。原始 JavaScript 对象转换成序列化形式的例子
// A simple object holding key/value pairs
{
name: 'John',
last: 'Resig',
city: 'Cambridge',
zip: 02140
}
// Serialized form
name=John&last=Resig&city=Cambridge&zip=02140
// Another set of data, with multiple values
[
{ name: 'name', value: 'John' },
{ name: 'last', value: 'Resig' },
{ name: 'lang', value: 'JavaScript' },
{ name: 'lang', value: 'Perl' },
{ name: 'lang', value: 'Java' }
]
// And the serialized form of that data
name=John&last=Resig&lang=JavaScript&lang=Perl&lang=Java
// Finally, let's find some input elements
[
document.getElementById( 'name' ),
document.getElementById( 'last' ),
document.getElementById( 'username' ),
document.getElementById( 'password' )
]
// And serialize them into a data string
name=John&last=Resig&username=jeresig&password=test
您用来序列化数据的格式是在 HTTP 请求中传递附加参数的标准格式。您可能在标准的 HTTP GET 请求中看到过它们,如下所示:
http://someurl.com/?name=John&last=Resig
这些数据也可以传递给 POST 请求(比简单的 GET 要多得多)。我们将在下一节中讨论这些差异。
现在,让我们建立一个标准的方法来序列化清单 8-3 中的数据结构。在清单 8-4 中可以找到一个这样做的函数。这个函数能够序列化大多数表单输入元素,多选输入除外。
清单 8-4 。将数据结构序列化为 HTTP 兼容参数方案的标准函数
// Serialize a set of data. It can take two different types of objects:
// - An array of input elements.
// - A hash of key/value pairs
// The function returns a serialized string
function serialize(a) {
// The set of serialize results
var s = [];
// If an array was passed in, assume that it is an array
// of form elements
if ( a.constructor === Array ) {
// Serialize the form elements
for ( var i = 0; i < a.length; i++ )
s.push( a[i].name + '=' + encodeURIComponent( a[i].value ) );
// Otherwise, assume that it's an object of key/value pairs
} else {
// Serialize the key/values
for ( var j in a )
s.push( j + '=' + encodeURIComponent( a[j] ) );
}
// Return the resulting serialization
return s.join('&');
}
现在已经有了数据的序列化形式(在一个简单的字符串中),您可以看看如何使用 GET 或 POST 请求将数据发送到服务器。
建立 GET 请求
让我们使用 XMLHttpRequest 重新建立一个与服务器的 HTTP GET 请求,但是这次发送额外的序列化数据。清单 8-5 展示了一个简单的例子。
清单 8-5 。与服务器建立 HTTP GET 请求的跨浏览器方式(不读取任何结果数据)
// Create the request object
var xml = new XMLHttpRequest();
// Open the asynchronous GET request
xml.open('GET', '/some/url.cgi?' + serialize( data ), true);
// Establish the connection to the server
xml.send();
需要注意的重要部分是,序列化数据被附加到服务器 URL(用?性格)。所有的 web 服务器和应用框架都知道如何解释?作为一组序列化的键/值对。
建立员额请求
使用 XMLHttpRequest 与服务器建立 HTTP 请求的另一种方法是 POST,这涉及到一种完全不同的向服务器发送数据的方法。首先,POST 请求能够发送任何格式和任何长度的数据(不仅限于序列化的数据字符串)。
当传递给服务器时,您一直用于数据的序列化格式通常被赋予内容类型 application/x-www-form-urlencoded。这意味着您还可以向服务器发送纯 XML(内容类型为 text/xml 或 application/xml)或者甚至是 JavaScript 对象(使用内容类型 application/json)。
清单 8-6 中的给出了一个建立请求和发送额外序列化数据的简单例子。
清单 8-6 。跨浏览器意味着与服务器建立 HTTP POST 请求(并且不读取任何结果数据)
// Create the request object
var xml = new XMLHttpRequest();
// Open the asynchronous POST request
xml.open('POST', '/some/url.cgi', true);
// Set the content-type header, so that the server
// knows how to interpret the data that we're sending
xml.setRequestHeader(
'Content-Type', 'application/x-www-form-urlencoded');
// Establish the connection to the server and send the serialized data
xml.send( serialize( data ) );
为了扩展前面的观点,让我们看一个向服务器发送非序列化格式的数据的例子。清单 8-7 显示了一个例子。
清单 8-7 。向服务器发布 XML 数据的示例
// Create the request object
var xml = new XMLHttpRequest();
// Open the asynchronous POST request
xml.open('POST', '/some/url.cgi', true);
// Set the content-type header, so that the server
// knows how to interpret the XML data that we're sending
xml.setRequestHeader( 'Content-Type', 'text/xml');
// Establish the connection to the server and send the serialized data
xml.send( '<items><item id='one'/><item id='two'/></items>' );
发送大量数据的能力(您可以发送的数据量没有限制;相比之下,GET 请求的最大数据量只有几 KB,这取决于浏览器)是非常重要的。有了它,您可以创建不同通信协议的实现,比如 XML-RPC 或 SOAP。
然而,为了简单起见,这里的讨论仅限于一些最常见和最有用的数据格式,它们可以作为 HTTP 响应使用。
HTTP 响应
XMLHttpRequest 类的第 2 级现在提供了更好的控制,告诉浏览器我们希望如何取回数据。我们通过设置 responseType 属性来实现这一点,并使用 response 属性接收请求的数据。
首先,让我们看一个处理来自服务器响应的数据的非常简单的例子,如清单 8-8 所示。
清单 8-8 。与服务器建立连接并读取结果数据
// Create the request object
var request = new XMLHttpRequest();
// Open the asynchronous POST request
request.open('GET', '/some/image.png', true);
//Blob is a Binary Large Object
request.responseType = 'blob';
request.addEventListener('load', downloadFinished, false);
function downloadFinished(evt){
if(this.status == 200){
var blob = new Blob([this.response], {type: 'img/png'});
}
}
在这个示例中,您可以看到如何接收二进制数据并将其转换为 PNG 文件。responseType 属性可以设置为以下任意值:
- 文本:结果以文本字符串的形式返回
- ArrayBuffer:结果以二进制数据数组的形式返回
- 文档:假设结果是 XML 文档,但也可能是 HTML 文档
- Blob:结果作为原始数据的类似文件的对象返回
- JSON:结果作为 JSON 文档返回
现在我们知道了如何设置 responseType,我们可以看看如何监控请求的进度。
监控进度
正如我们之前看到的,使用 addEventListener 使我们的代码易于阅读并且非常灵活。这里,我们在请求对象上使用相同的技术。无论你是从服务器下载数据还是上传数据,你都可以监听这些事件,如清单 8-9 所示。
清单 8-9 。使用 addEventListener 侦听服务器请求的进度
var myRequest = new XMLHttpRequest();
myRequest.addEventListener('loadstart', onLoadStart, false);
myRequest.addEventListener('progress', onProgress, false);
myRequest.addEventListener('load', onLoad, false);
myRequest.addEventListener('error', onError, false);
myRequest.addEventListener('abort', onAbort, false);
//Must add eventListeners before running a send or open method
myRequest.open('GET', '/fileOnServer');
function onLoadStart(evt){
console.log('starting the request');
}
function onProgress(evt){
var currentPercent = (evt.loaded / evt.total) * 100;
console.log(currentPercent);
}
function onLoad(evt){
console.log('transfer is complete');
}
function onError(evt){
console.log('error during transfer');
}
function onAbort(evt){
console.log('the user aborted the transfer');
}
与以前相比,您现在可以更好地了解您的文件发生了什么。使用 loaded 和 total 属性,您可以计算出正在下载的文件的百分比。如果出于某种原因,用户决定停止下载,您将会收到中止事件。如果文件有问题,或者它已经完成加载,您将会收到错误或 load 事件。最后,当您第一次向服务器发出请求时,您会收到 loadstart 事件。现在让我们快速看一下超时。
检查超时和跨源资源共享
简单地说,超时允许您设置一个时间,让应用等待多长时间,直到它停止寻找服务器的响应。很容易设置一个暂停时间,并监听它。
清单 8-10 展示了如何在你自己的应用中检查超时。
清单 8-10 。检查请求超时的示例
// Create the request object
var xml = new XMLHttpRequest();
// We're going to wait for a request for 5 seconds, before giving up
xml.timeout = 5000;
//Listen for the timeout event
xml.addEventListener('timeout', onTimeOut, false);
// Open the asynchronous POST request
xml.open('GET', '/some/url.cgi', true);
// Establish the connection to the server
xml.send();
默认情况下,浏览器不允许应用向服务器发出请求,除非该站点来自该服务器。这可以保护用户免受跨站点脚本攻击。服务器必须允许请求;否则,将出现 INVALID_ACCESS 错误。服务器给出的头看起来像这样:
Access-Control-Allow-Origin:*
//Using a wild card (*) to allow access from anyone.
Access-Control-Allow_origin:http://spaceappleyoshi.com
//Allowing from a certain domain
摘要
我们现在有了在服务器上处理数据的坚实基础。我们可以告诉服务器我们期望得到什么样的结果。我们还可以监听一些事件,这些事件会告诉我们文件传输的进度,或者在传输过程中是否有错误。最后,我们讨论了超时和跨源资源共享(CORS)。在下一章中,我们将看看一些用于网页制作的开发工具。
九、网站开发工具
多年来,开发网站的工具已经成熟。我们从使用记事本这样的简单编辑器发展到了 WebStorm 这样的全面开发环境。我们也有像 JQuery 这样的库。我们可以使用 Handlebars 作为模板引擎,AngularJS 作为完整的 MVC 框架。还有单元测试框架和版本控制系统来帮助我们更好更快地完成工作。那么现在我们有了所有这些可用的东西,我们如何把它们组织起来呢?
为了回答这个问题,我们将把它分成两部分。首先,我们将看看创建网站的工具,然后看看跟踪网站变化的工具。为了创建一个站点,我们将探索 Yeoman、Grunt 、Bower 和节点包管理器(NPM) 。为了跟踪变化,我们将使用 Git 。
所有这些工具都可以协同工作,所以让我们来分析一下每个工具的作用:
- Bower 是一个包管理系统。它的目的是确保您的项目所依赖的所有客户端代码都已经下载并安装。地点:
http://bower.io/ - Grunt 就是所谓的构建工具。它允许您自动化许多类型的任务,包括单元测试、林挺(检查 JavaScript 错误),以及将您的代码添加到版本控制中。它还可以用于将您的代码部署到服务器。地点:
http://gruntjs.com/ - 约曼就是所谓的脚手架工具。它创建了创建项目的基本版本所需的文件和文件夹。然后,它使用 Bower 收集项目所依赖的所有代码。最后,它使用一个构建工具(比如 Grunt)来自动化任务。这是通过使用发电机来实现的。地点:
http://yeoman.io/ - 节点包管理器(NPM) ,顾名思义,管理包。这些包运行在 Node.js 之上。随着 Node 变得越来越流行,其中一些包是为客户端开发而开发的,而不仅仅是服务器端,在服务器端使用 Node.js。地点:
https://nodejs.org/ Git是一个版本控制系统。如果你听说过 Subversion 或 Perforce 这样的工具,Git 也是类似的。它会跟踪你处理的所有文件,并告诉你文件之间的区别。地点:http://git-scm.com/
搭建你的项目
计算机非常擅长做人们不想做的任务,它们可以一遍又一遍地做,而不会感到厌烦。没有人希望每次创建项目时都为图像、CSS 和 JavaScript 文件创建文件夹。有很多我们认为理所当然的小任务现在可以自动化。启动一个项目,只需一个命令就可以完成所有的文件夹,这不是很好吗?
这就是脚手架背后的理念。因为大多数网站都是以同样的方式组织的,所以没有必要手工设计结构。Yeoman 可以让你搭建任何你想要的网络项目。利用来自社区的最佳实践,Yeoman 使用发电机来快速方便地建立我们的项目。
生成器实际上是任何人都可以制作的模板。有一些团队赞助项目来创建“官方”发电机,但是如果那个不做你想要的事情,其他人可能已经做了一个。发电机也是开源的,所以你可以看看一个人的引擎盖下,看看它是如何制造的。为了使用 Yeoman,我们首先需要安装 Node.js。
NPM 是一切的基础
节点包管理器(NPM )让您能够管理应用中的依赖关系。这意味着,如果你的项目需要代码(比如说 JQuery),NPM 可以很容易地添加到你的项目中。这也是我们将要安装的大多数工具在幕后运行的内容。NPM 是 Node.js 的一部分,node . js 是一个开源的跨平台环境,用于使用 JavaScript 制作服务器端应用。尽管我们不打算在这里创建 Node.js 项目,但我们确实需要安装node。有几种方法可以做到这一点;对于我们的例子,我们将尽量使它简单。
当你去nodejs.org的时候,网站会算出你用的是什么操作系统。单击安装按钮下载并运行安装程序。
安装完成后,您可以进入终端模式(Mac、Linux)或命令提示符(Windows)并输入node –version。您应该会看到当前版本的node显示在窗口中。
安装好之后,现在我们可以得到我们需要的一切。安装约曼时,键入npm install –g yo,对于咕噜型npm install –g grunt-cli,对于鲍尔型npm install –g bower。使用-g意味着安装将在全球范围内进行;创建新项目时,可以在任何文件夹中运行这些实用程序。cli代表命令行界面。在我们的练习中,我们将花时间在命令行上。习惯了是好事,值得努力。现在我们可以安装一个发电机,并开始寻找其他工具。
发电机
正如我们之前谈到的,生成器实际上是描述站点结构的模板。您可以通过向 Yeoman 传递不同的参数来调整这些模板。在Yeoman.io你可以找到一个生成器列表和 GitHub 库的链接。存储库有关于如何使用生成器的所有说明。例如,如果您想使用 AngularJS 创建一个站点,您可以输入
npm install –g generator-angular
这将安装 AngularJS 发生器。如果您想要一个 AngularJS 站点,并且还想添加 Karma(一个 JavaScript 测试运行器)来帮助运行您的单元测试,安装应该是这样的:
npm install –g generator-angular generator-karma
现在您已经安装了一个生成器,通过在命令行键入yo,您可以查看已安装的生成器列表并更新它们。从这里你也可以安装新的发电机。
此时,您应该为您的项目创建一个文件夹,当您在该文件夹中时,下一个命令应该是
yo angular
约曼会开始问你关于你的应用的问题。比如它会问你是否愿意使用 Sass,如图图 9-1 所示。
图 9-1 。约曼建立了一个 AngularJS 网站
您将被问及其他一些关于您希望如何设置项目的问题。一旦完成,Bower 将从 GitHub 获取您需要的所有库的最新版本,并为您一起搭建您的项目。安装好所有东西后,输入以下命令来查看站点的运行情况:
grunt serve
这一次我们使用 Grunt 在你当前的文件夹中创建一个本地 web 服务器,并提供主页服务(图 9-2 )。
图 9-2 。约曼在本地服务器上运行 AngularJS
这就是你需要做的。你现在已经有了一个可以运行的站点。这是将我们的代码置于版本控制中的好时机。
版本控制
变化是不断的。我们的文件被一遍又一遍地更新。当我们工作时,东西有时会坏掉。在某些情况下,简单的撤销就可以了。在其他时候,我们可能需要恢复到以前的状态,特别是在与团队合作并且有许多变化的时候。一个改变可能会破坏整个网站,并且很难找到问题所在。这就是版本控制非常有用的地方。
Git 是我们将要使用的版本控制系统。它很受欢迎,也有 GUI 客户端。正如我们对 Node.js 示例所做的那样,我们将采用最快的方式来安装它。为此,请转到git-scm.com下载并安装 Git。
安装完成后,您可以使用命令行对其进行配置。要添加您的身份,请键入git config –global user.name "your name" and git config –global user.email your@email.com。
现在您已经安装并配置了 Git,我们将快速添加文件,然后在本地提交它们。确保您位于项目文件夹中。在该文件夹中键入git init。这样就创建了一个. Git 文件夹,其中包含了项目的所有信息。此文件夹通常是不可见的,因此如果您想看到它,可能需要更改操作系统中的一些设置。接下来,让我们检查提交的状态。键入git status,您可以看到在这一点上,没有文件被添加到版本控制中(图 9-3 )。
图 9-3 。文件尚未添加到 Git
添加文件、更新和首次提交
接下来我们添加文件,这样 Git 就可以跟踪它们。向存储库添加文件就像输入git add file name/folder一样简单。Git 将开始跟踪文件。在图 9-4 中,我们通过输入git add app/来添加应用文件夹。您可以再次检查状态并查看结果。
图 9-4 。应用文件夹已经被添加到 Git
继续添加文件,尤其是bower.json和package.json。这些文件跟踪您的依赖模块以及这些模块的版本。Gruntfile.js会有你能运行的所有任务。我们之前通过输入grunt serve运行了一个任务。那个任务为我们运行一个本地服务器。
作为一个最佳实践,node_modules和bower_components文件夹不会被添加到版本控制中。您可以稍后使用npm install和bower install命令重新安装它们。
您不想添加到 Git 的文件在.gitignore文件中定义。您可以修改该文件,以包含文件类型或任何您希望 Git 忽略的内容。
我们讨论了文件如何随时间变化,所以你可能会问 Git 是如何知道文件何时发生变化的。首先,需要将文件添加到存储库中。我们用git add命令做到了。然后,当发生变化时,您可以再次请求状态。这将列出自上次添加以来已更改的所有文件。请记住,在进行更改之前,您需要 Git 了解该文件,这样就可以随时跟踪更改。在图 9-5 的中,我们可以改变README.md文件来说明我们的观点。
图 9-5 。Git 可以看到文件何时被修改
文件更改后,您可以像以前一样再次添加它。下次您检查状态时,它将回到跟踪项目列表中。
现在 Git 已经跟踪了所有文件,可以提交所有文件了。将提交视为项目当前状态的快照(图 9-6 )。如果项目发生了任何事情,您总是可以恢复到上一个状态。
图 9-6 。提交并显示在日志中的文件
要提交,请键入git commit –m "notes about the commit"。–m标志代表消息。或者,您可以使用文本编辑器来制作您的消息。如果你想查看信息的历史,你可以输入git log。
我们现在有一种方法来跟踪本地机器上的变化。如果你是一个孤独的开发者,这将会很好。
但是,如果您想要共享代码或与团队合作,您将需要添加一个服务器端组件。最流行的两个选项是 GitHub ( https://github.com/)和 bit bucket(https://bitbucket.org/)??。使用这两个选项中的任何一个,除了本地机器上的文件之外,您还可以拥有一个远程存储库。
摘要
我们希望在读完这一章后,你会看到大量的资源可供你使用。使用 Yeoman generators 快速组合一个网站的能力,将为你在组合新项目时节省时间和精力。Yeoman 也可以作为一个学习工具来理解不同框架是如何工作的。
我们只是给出了 Git 的一个基本概述。这个主题本身就可以成为一本书。幸运的是,有斯科特·沙孔和本·施特劳布的 Pro Git 。在 Git 主页上也可以找到它的链接(http://git-scm.com/ ))。它是在线的或者是一个数字文件。现在我们可以很快地把一个网站放在一起,让我们看看第十章中一个非常流行的框架 AngularJS。
十、Angular 和测试
在前一章中,你学习了如何使用当前的一套工具来快速组合一个站点,并使用版本控制来跟踪你使用的所有文件以及它们之间的差异。在这一章中,我们将深入研究并理解像 Angular 这样的框架是如何工作的。
简而言之,框架帮助你以一种更有组织性和更容易维护的方式构建大型应用。使用框架的另一个好处是团队的学习曲线更短。一旦新成员学会了这个框架,他们对整个网站的工作原理就有了更好的理解。在我们的例子中,我们将对 AngularJS 进行高层次的观察。
在撰写本文时,Angular 的当前版本是 1.4.1。信息可以在https://angularjs.org/找到。关于 Angular 2 的信息可以在https://angular.io/找到。
Angular 试图解决的问题之一是使开发动态应用变得容易。HTML 本身并不是为制作单页应用而设计的。Angular 提供了一种以快速学习的方式开发现代 web 应用的方法。它通过将应用的每个部分保持独立来实现这一点,这样每个部分都可以独立于其他部分进行更新。这种架构模式被称为模型-视图-控制器(MVC)。你会发现其他框架,如 Backbone 和 Ember,也是以类似的方式工作的。
在第九章中,我们介绍了一些可以帮助我们提高效率的开发工具。自耕农 ( http://yeomanio/)。)使用社区构建的生成器快速开发基本站点运行所需的所有文件和文件夹。Grunt ( http://gruntjs.com/)用于自动化任务,例如为生产就绪的站点进行单元测试和优化文件。本章假设两种工具都已安装。请参考上一章或列出的站点,了解有关安装它们的信息。
要创建新的 Angular 项目,键入your angular。作为回应,约曼问了一些关于你想如何建立这个项目的问题:
-
Would you like to use Sass (with Compass)? Sass (
http://sass-lang.com/)stands for Syntactically Awesome Style Sheets. Sass gives you features like nesting selectors and using variables to develop style sheets. Compass is a tool written in Ruby that uses Sass files and adds features like generating sprite sheets out of a series of images.同意这个选项,你将得到一个默认使用 Twitter Bootstrap 风格的 SCSS 文件。如果你选择否,Yeoman 会给你一个普通的 CSS 文件,使用相同的 CSS 选择器。
-
Would you like to include Bootstrap? Twitter Bootstrap (
http://getbootstrap.com/``)helps you develop the user interface for your website. Bootstrap can help make your site responsive so it can look good on multiple devices and gives you items like buttons, an image carousel, and other user interface components.如果您选择使用这个工具,那么 Yeoman 会询问是否使用 Sass 版本的 Bootstrap。
-
**你想包括哪些模块?**模块给予角额外的能力。例如,
angular-aria模块提供可访问性特性,而angular-route给你添加深度链接特性的能力。您可以选择添加或删除任何模块。模块也可以在以后手动添加。
一旦你回答了这些问题,所有需要的文件都将被下载,Grunt 将启动一个本地服务器,加载默认的浏览器http://localhost:9000,如图图 10-1 所示。
图 10-1 。港口 9000 开始运行
从这里我们可以看到构成这个项目的文件夹。app文件夹包含了我们的主应用,这是我们将要开始的地方。
这些文件夹是非常标准的,你可以在 HTML 站点中找到。在scripts文件夹中,事情开始变得有趣起来。
scripts f 的根源是app.js年长。这是 Angular 的主要应用文件。打开这个文件来看看 Angular 是如何被引导的。在图 10-2 中,你会发现应用的名称chapter10app (因为这是创建该应用的文件夹的名称)。这与index.html中的ng-app指令一起工作。指令赋予 DOM 元素额外的能力。在这种情况下,它告诉 Angular 应用从这里开始,并被称为chapter10app。
图 10-2 。使用ng-app指令告诉 Angular 应用的根元素在哪里。
当你查看app.js时,你会看到在应用的名字后面加载了许多模块,这给 Angular 提供了额外的能力。其中一个模块被命名为ngRoute;它将允许您处理应用的 URL。.config方法使用$routeProvider来理解 URL,并通过使用一系列when语句用控制器加载正确的模板。
when语句使您能够为应用定制 URL。在这个例子中,如果你输入/about,Angular 会知道加载about.html模板并使用AboutCtrl作为控制器。我们来详细解释一下这是什么意思。
视图和控制器
我们已经讨论过 Angular,像其他框架一样,使用 MVC 模式。这意味着应用被分成三个不同的部分。
- 模型:存储应用的数据。
- 视图:创建模型数据的表示;例如,生成图表来表示数据。
- 控制器:向模型发送命令以更新数据。还向视图发送命令以更新模型数据的显示。
文件夹views包含 HTML 模板,可以用来自模型的数据更新。controllers文件夹包含与模型和视图文件通信的 JavaScript 文件。
我们来看一下about.html文件。这里我们将添加一个按钮,并让控制器使用它。
打开about.html,在views文件夹中找到。在它的内部,添加一个按钮标签。在这个按钮标签中,我们将使用另一个指令,它将让 Angular 知道按钮何时被点击。
我们需要添加指令到按钮上。如清单 10-1 中的所示,键入ng-click='onButtonClick()'。这将由我们的控制器解决,让我们分离应用的可视部分和业务逻辑。
清单 10-1 。使用ng-click指令定义方法
<button ng-click="onButtonClick()">Button</button>
打开controllers文件夹中的about.js。控制器允许您添加应用这一部分工作所需的所有业务逻辑。在controller方法中,你可以看到名字AboutCtrl,它与我们在app.js文件中看到的相匹配。您在控制器方法中看到的另一个东西是 $scope属性。
$scope允许您向正在使用的视图添加方法或属性。这里我们将处理一个在 HTML 文件中声明的函数。因为ng-click="onButtonClick()"是在 HTML 代码中定义的,所以它是这个控制器范围的一部分。
清单 10-2 显示了我们如何让控制器和 HTML 一起工作。
清单 10-2 。使用控制器定义 HTML 文件中声明的函数
angular.module('chapter10App')
.controller('AboutCtrl', function ($scope, $http) {
$scope.awesomeThings = [
'HTML5 Boilerplate',
'AngularJS',
'Karma'
];
$scope.onButtonClick = function(){
console.log('hello world');
};
});
如果应用目前正在浏览器中运行,它应该会看到 JavaScript 文件已经更新,并重新编译一切。完成后,您可以单击按钮,查看控制台,并看到消息。
否则,转到命令行,在应用的主文件夹中键入grunt serve。
这仅仅是能够将应用的视图与其业务逻辑分离的开始。例如,如果您想要显示一个项目列表,该怎么办?
回到控制器,我们将在我们的作用域中添加一个 JSON 对象;清单 10-3 显示了代码。
清单 10-3 。分配给about控制器范围的数据
$scope.bands = [
{'name':"Information Society", 'album':"_hello world"},
{'name':"The Cure", 'album':"Wish"},
{'name':"Depeche Mode", 'album':"Delta Machine"}];
现在我们有了需要的数据,下一步是将数据传递给 HTML 模板。回到about.html文件,我们将使用 ng-repeat指令(清单 10-4 )。
清单 10-4 。ng-repeat指令遍历控制器提供的数据,以显示列表中正确的项目数
<ul>
<li ng-repeat="band in bands">{{band.name}}<p>{{band.album}}</p></li>
</ul>
此时,你的页面看起来应该类似于图 10-3 。
图 10-3 。呈现在页面上的来自控制器的数据
到目前为止,我们已经能够使用指令将控制器连接到 HTML 视图中定义的项目。我们的数据也被定义在控制器中。所以,你可能会问,假设我们想从外部来源获取数据;我们如何调用远程服务器并显示结果?我们将在下一节讨论这个问题。
远程数据源
让我们以按钮方法为例,用它来获取一些远程数据。
我们将使用$http服务,它将为我们处理远程呼叫。这类似于 JQuery 中的 AJAX 方法。为了利用该服务,我们首先需要将其添加到controller方法中。
在controller方法中添加$http服务。它现在看起来应该类似于清单 10-5 。
清单 10-5 。将$http服务添加到AboutCtrl控制器
.controller('AboutCtrl', function($scope, $http){
$scope.awesomeThings = [
'HTML5 Boilerplate',
'AngularJS',
'Karma'
];
});
现在控制器可以使用服务了,我们可以通过onButtonClick方法来使用它。我们将使用get方法,它匹配其余的get动词。有关 REST 方法的更多信息,请查看这篇维基百科文章:
http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods
我们还将使用 JSONPlaceholder,这是一个让您测试 REST 请求并返回假数据的服务。在onButtonClick方法中,删除现有代码并添加清单 10-6 中的代码。
清单 10-6 。进行 HTTP 调用并将结果分配给results属性
$http.get('http://jsonplaceholder.typicode.com/photos').success(function(data){
$scope.results = data;
});
查看这段代码,我们看到我们正在使用get方法,但是我们也在监听success事件。如果调用成功,我们使用一个匿名函数将结果赋给一个名为results的属性。
现在我们有了结果,我们可以更新模板,不仅显示文本,还显示从服务返回的缩略图。我们通过对image标签使用另一个指令来做到这一点。
打开about.html并更新现有代码。删除之前的列表,并将清单 10-7 中的 fcode 添加到模板中。
清单 10-7 。基于 REST 服务返回的结果创建一个无序列表
<ul>
<li ng-repeat="result in results">
<p>ID: {{result.id}}</p>
<p>{{result.title}}</p>
<p><img ng-src="{{result.thumbnailUrl}}"/></p>
</li>
</ul>
这里我们使用了ng-src指令来告诉image标签在哪里可以找到列表中的每张图片。就像前面的例子一样,这将遍历整个结果列表,并将它们全部显示在屏幕上。使用这个指令的一个好处是image标签在收到数据之前不会尝试显示图像。所以我们只需要从 REST 服务中获取结果。
此时,页面看起来应该类似于图 10-4 。
图 10-4 。显示 REST 服务的结果
只需几个步骤,我们就有了一个能够从远程数据源检索数据并在需要时将其呈现给浏览器的站点。Angular 有很多特性,但是我们还会介绍一个。我们的下一课将涵盖路线。
路线
Routes 允许用户为我们的应用创建自定义的 URL,并给我们标记一个页面的能力,以便我们以后可以直接访问它。
我们在app.js文件中看到了一个这样的例子。.config方法使用了$routeProvider API。在一系列的when语句中,我们能够计算出将加载什么样的 HTML 模板,以及哪个控制器将与该模板一起使用。
既然我们已经很好地理解了它是如何工作的,那么如果您想将参数传递给应用呢?例如,假设我们只想显示上一个例子中的一篇文章。在这里,我们将创建一条路由来实现这一目的。
如果您在前面的课程中使用 Yeoman 创建了该应用,请转到命令行并键入:
yo angular:route post
使用该命令,Yeoman 将通过添加一条新路线来更新app.js文件。它还将为您的控制器创建一个新的 JavaScript 文件,并为视图创建一个 HTML 文件。对于一个命令来说还不错。
如果应用正在运行,您可以转到浏览器并键入:
http://localhost:9000/#/post
您应该看到 post 视图已经准备好了。
这里的目标是根据添加到我们的 URL 的数量来加载一篇文章。例如,如果 URL 看起来像这样:
http://localhost:9000/#/post/4
基于我们在上一个例子中使用的服务,您应该会看到列表中的第四篇文章。
路线参数
为了实现这一点,我们需要让我们的路由功能更加灵活。打开app.js;这里我们将更新 post route,这样它就可以在 URL 中加入变量。
它应该从:
/post
致:
/post/:postId
通过添加:postId,我们创建了一个可以在控制器中使用的变量。这个变量将代表 URL 中的数字。为了说明这一点,让我们更新控制器。
打开post.js,你会看到在controller方法中有一个使用$scope的匿名函数。在我们的其他例子中,我们看到$scope给了我们控制 HTML 模板的能力。我们将添加一个名为$ routeParams的额外参数,这样我们就可以在 URL 中访问我们的变量。
现在我们可以从 URL 中获取变量,并将其分配给$scope。这将使我们能够在更新模板后显示它。
在controller方法中键入以下内容:
$scope.postId = $routeParams.postId;
要在屏幕上看到我们的号码,我们可以快速更新模板。
打开views文件夹中的post.html文件。在这里我们可以快速更新这个模板。首先删除段落标记之间的副本,使其看起来像这样:
<p>{{postId}}</p>
完成后,你可以在url中输入一个数字,并看到它显示在屏幕上。此时,浏览器应该看起来像图 10-5 。
图 10-5 。基于 URL 在屏幕上显示变量
还不算太糟。所以现在我们只需要将它连接到一个 GET 请求并显示结果。这里的代码将与我们之前所做的非常相似。在 post 控件中,我们需要添加$HTTP服务,这样我们就可以进行 REST 调用。清单 10-8 显示了代码。
清单 10-8 。添加了http`服务的完整 PostCtrl
.controller('PostCtrl', function($scope, $routeParams, $http){
$scope.awesomeThings = [
'HTML5 Boilerplate',
'AngularJS',
'Karma'
];
});
就在这个下面,我们将像以前一样进行相同的 REST 调用,但是这次添加了postId变量,如清单 10-9 中的所示。
清单 10-9 。使用$http服务和routeParam获得一个结果
$http.get('http://jsonplaceholder.typicode.com/photos/'+$routeParams.postId).
success(function(data){
$scope.results = data;
});
至于 HTML 模板(清单 10-10 ,我们将使用与about示例相同的代码;唯一的区别是,我们将删除ng-repeat指令,并确保我们使用单词results。
清单 10-10 。用于显示文章的 HTML 模板
<ul>
<li>
<p>ID: {{results.id}}</p>
<p>{{results.title}}</p>
<p><img ng-src='{{results.thumbnailUrl}}'/></p>
</li>
</ul>
现在,当你更新地址栏中的数字时,你应该会看到一个新的帖子(图 10-6 )。
图 10-6 。基于URL变量显示的单个帖子
应用测试
当您阅读 Angular 的文档时,您将会看到如何编写覆盖您正在构建的应用的不同部分的测试。
测试有助于确保您编写的代码始终是稳定的。例如,如果您需要对一个函数进行更改,即使您更改了该函数的工作方式,它仍然会给出相同的结果。如果这种改变在应用的其他地方产生了意外的结果,测试应该让你知道。
因此,在这种情况下,如何测试 Angular 应用呢?我们将考虑两种类型的测试:单元测试和端到端(E2E)测试。
单元测试
当你写一个 f 函数时,你要考虑它应该接收什么参数,它用这些信息做什么,以及结果应该是什么。对该代码单元进行测试将确保它按照您期望的方式运行。
如果你一直在使用前几章中的约曼生成的 Angular 版本,我们需要安装一些额外的项目来让测试工作。转到命令行并键入
npm install grunt-karma -–save-dev
这将安装因果报应。
Karma 在 Angular developer guide 中被描述为“一个 JavaScript 命令行工具,可用于生成一个 web 服务器,该服务器加载应用的源代码并执行测试。”简而言之,Karma 将启动浏览器,根据您编写的测试运行代码,并给出结果。如果你没有安装想要测试的浏览器(例如,如果你使用的是 Mac),你可以使用 BrowserStack ( https://www.browserstack.com/)或 Sauce Labs ( https://saucelabs.com/ ))这样的服务。关于因果报应的最新信息,请访问http://karma-runner.github.io/。
接下来我们需要安装 PhantomJS 。类型
npm install karma-phantomjs-launcher -–save-dev
在命令行中。
PhantomJS 是一个无头的 ?? 浏览器,一个没有用户界面的浏览器。通过包含它,您可以在浏览器中运行您的应用,并且所有命令都将从命令行执行。有关幻想曲的最新信息,请访问http://phantomjs.org/。
最后,键入
npm install karma-jasmine -–save-dev
在命令行。这将安装 Jasmine ,我们将使用它来测试我们的应用。您可以在http://jasmine.github.io/找到文档。
现在,让我们通过输入grunt test来确保一切正常。这将运行test文件夹中的测试。
添加新测试
使用 Yeoman 的一个好处是,当您创建新的控制器时,它也会为单元测试创建相应的文件。
看主文件夹。在那里你会找到一个test文件夹。在这个文件夹中将会有一个spec文件夹,包含一个controllers文件夹,其中包含了对已经创建的每个控制器进行单元测试的文件。
打开about.js让我们看看如何测试控制器。
在我们开始编写测试之前,让我们先看看现有的代码,了解一下发生了什么。
在顶部有一个describe方法,用于谈论即将编写的测试。describe方法可以用来在高层次上描述将要测试的一切。
接下来有一个beforeEach方法,它将在每次测试之前运行。这使我们能够通过将应用作为一个模块加载来访问整个应用。
创建两个变量,AboutCtrl和 scope;然后我们创建另一个beforeEach方法,它给变量分配控制器的值和控制器内部的范围,就像我们直接使用控制器一样。
最后,我们可以编写我们的测试,我们用一系列的it方法来描述。这有助于使测试易于文档化。在这里你描述这个函数应该如何工作。
默认测试有消息"should attach a list of awesomeThings to the scope";然后它用一个expect方法运行一个函数。这种方法很重要,因为它给你一个测试预期结果的机会。在这种情况下,我们检查数组awesomeThings的长度,预计它是 3。如果不是,测试将失败。
我们现在可以测试之前创建的数组带的长度。添加一个新的it方法,如清单 10-11 中的所示。
清单 10-11 。对应该在 about控制器中的波段数进行单元测试
it('should have at least 3 bands', function(){
expect(scope.bands.length).toBe(3);
});
如果您在命令行输入grunt test,这个测试应该会通过。如果在expect方法中有不同的数字,例如 2,测试将会失败。你可以在图 10-7 中看到。
图 10-7 。测试预期两个项目,但收到三个项目
我们还可以检查数组中某一项的值(清单 10-12 )。
清单 10-12 。检查数组中的值的单元测试
it('should have the second album be Wish', function(){
expect(scope.bands[1].album).toEqual('Wish');
});
这是一个如何测试控制器的快速概述。从这里,您可以测试已经编写的方法并评估结果。Jasmine 给了你很多方法来确保你写的代码是可靠的。让我们再来看看测试,打开post.js并测试我们的 HTTP 请求。
使用$httpBackend测试 HTTP 请求
在前面的例子中,我们测试了一些与控制器相关的数据。在这种情况下,数据来自控制器内部。在大多数应用中,您将从远程数据源获取数据。那么,如何在无法控制数据源的情况下编写测试呢?在这种情况下,您使用$httpBckend来确保您创建的请求独立于服务工作。
这个测试将重现我们对$routeParams所做的一切。它将是独立的,实际上不会调用服务器。
首先,我们将添加几个变量。除了PostCtrl和scope,再增加httpBackend和routeParams。在这种情况下,我们不引用指令,所以您不需要添加美元($)符号。
第二个beforeEach方法是我们当前初始化控制器的地方。这是我们将添加指令的地方,就像在真正的控制器中一样;这里加上$httpBackend和$routeParams。
现在我们给之前创建的变量赋值。在浏览器中,我们可以通过给 URL 中的postId赋值来获得一篇文章。我们模拟它,如清单 10-13 所示。
清单 10-13 。赋值,这样我们就可以模拟从浏览器中获取值(第一部分)
beforeEach(inject(function($controller,$rootScope,$httpBackend,$routeParams){
scope = $routeScope.$new();
routeParams = $routeParams;
routeParams.postId = 1;
httpBackend = $httpBackend;
httpBackend.expectGet('http://jsonplaceholder.typicode.com/photos/'+routeParams.postId).respond({id:'1', title:'title 1', thumbnailUrl:'myImage.png'});
PostCtrl = $controller('PostCtrl', {
$scope: scope
});
httpBackend.flush();
});
因为我们没有将它加载到浏览器中以确保它能够工作,所以我们将把postId的值硬编码为 1。然后,使用httpBackend,我们以与控制器中相同的方式模拟调用。在这种情况下我们使用expectGet的方法。这将模拟一个 HTTP GET请求。如果你想做一个POST请求,你可以使用expectPost方法。
r esponse方法给了我们可以测试的有效负载。这里我们传回一个简单的对象,就像 API 所传递的一样。
在范围被分配后,我们再次看到使用flush方法的httpBackend对象。这将使响应可用于测试,就像您进行了 HTTP 调用一样。
现在开始测试。正如在另一个例子中一样,一系列的it方法描述了您所期望的。
让我们首先确保我们只从服务器返回一个结果(清单 10-14 )。
清单 10-14 。赋值,这样我们就可以模拟从浏览器中获取值(第二部分)
it('should be a single post', function(){
expect(scope.results).not.toBeGreaterThan(1);
});
Jasmine 使测试易于阅读。我们有结果,只是想确定我们只有一个对象。
如果我们想确保这个对象有一个 ID 属性,我们可以使用清单 10-15 中的代码。
清单 10-15 。检查 ID 属性
it('should have an id', function(){
expect(scope.results.id).toBeDefined();
}
就像以前一样,我们可以添加测试,当我们向控制器添加更多功能时,这些测试将允许我们理解控制器。开发代码时首先编写测试的过程被称为 测试驱动开发。在这种方法中,您首先编写一个测试,知道它会失败,然后返回并编写最少的代码来使测试工作。之后,您可以根据需要进行重构。Jasmine 用于测试代码单元。它还用于测试与浏览器的集成。那么如何在多个浏览器上模拟按钮点击呢?毕竟,从 web 开发的历史来看,我们知道即使是看起来简单的东西有时在某些浏览器中也是行不通的。这就是量角器的用武之地。
用量角器进行端到端测试
量角器 ( http://angular.github.io/protractor)是一个让你运行真正的浏览器并在其中运行测试的工具。例如,您可以确保当一个按钮被单击时,它提交一个表单。与测试小代码单元的单元测试不同,端到端(E2E)测试 让你可以针对 HTTP 服务器测试应用的所有部分。这类似于你在浏览器中打开网站,并确保一切正常。这种方法的好处在于它是自动化的。
像这样的任务应该自动化。量角器能够做到这一点,因为它是建立在 WebDriver 之上的,web driver 是一种用于在浏览器内部自动测试的工具。量角器也有支持 Angular 的特性,所以你只需要很少的配置。
让我们安装量角器并进行一些测试。在命令行中,键入:
npm install –g protractor –-save-dev
和其他节点包一样,该行将在全球范围内安装量角器,因此您可以在其他项目中使用它。
我们需要做一些配置来让它工作。让我们创建config文件。
创建一个新文件,我们将其命名为protractor.conf.js,并保存在紧挨着karma.config.js文件的test文件夹中。
在这个文件中,我们将向量角器提供一些关于在哪里可以找到 Selenium 服务器、运行什么浏览器以及测试文件在哪里的信息。清单 10-16 显示了代码。
清单 10-16 。量角器 r 的基本配置文件
export.config = {
seleniumAddress: 'http://localhost:4444/wd/hub',
multiCapabilities: [{browserName: ‘firefox’},{browserName: ‘chrome’}],
baseUrl: 'http://localhost.9000',
framework: 'jasmine',
specs: ['protractor/*.js']
};
这里有几个东西要解包,我们来看看。
属性告诉量角器 Selenium 服务器在哪里运行。接下来,multiCapabilities属性告诉量角器在哪些浏览器上运行测试。如您所见,这是一个对象数组,列出了每个浏览器的名称。
因为我们是在本地测试,所以我们只能测试安装在机器上的浏览器。所以你运行的是 Mac 就不能测试 IE。如果您需要测试像 IE 或移动浏览器这样的浏览器,您可以添加允许您连接到 SauceLabs 或 BrowserStack 的属性。
接下来我们有baseUrl属性,它告诉量角器哪个服务器正在托管被测试的应用。当您运行测试时,站点运行在本地服务器上是很重要的。属性被设置为Jasmine,因为这是我们测试使用的框架。
属性很重要,因为它告诉量角器哪个文件夹中有测试。在我们的例子中,它在protractor文件夹中,我们使用通配符告诉它查看该文件夹中的任何 JavaScript 文件。
我们现在有量角器设置。是时候写一些测试了。在tests文件夹中创建一个Protractor文件夹。这里我们将编写一个基本的测试。
在Protractor文件夹 ?? 中创建一个名为app-spec.js的文件。格式将与我们在前面的例子中所做的非常相似。
我们从将要运行的测试套件的describe方法开始。紧接着是我们的一组it语句(清单 10-17 )。为了简单起见,我们将使用量角器网站上的例子。
清单 10-17 。It检查站点是否有标题的方法
it('should have a title', function(){
browser.get('http://juliemr.github.io/protractor-demo');
expect(browser.getTitle()).toEqual('Super Calculator');
});
我们现在需要的东西都有了。我们将从命令行运行它。如果您还没有运行serve任务并在本地查看站点,请键入:
grunt serve
这将让你从本地服务器的端口 9000 运行网站,这是我们告诉量角器运行测试时要查看的地方。
现在打字
protractor test/protractor.conf.js
这将在test文件夹中查找,运行配置文件,并启动浏览器,以便它可以运行测试。
如图 10-8 所示,我们应该会得到一个通过的结果。
图 10-8 。火狐和 Chrome 都通过了量角器测试
这将引导浏览器找到 URL 并检查标题。很简单。
现在让我们编写一个测试,我们可以使用同一个计算器将两个值相加,然后检查结果。清单 10-18 显示了代码。
清单 10-18 。键入两个文本字段,然后测试结果
describe('Protractor Demo App', function() {
it('should add one and two', function() {
browser.get('http://juliemr.github.io/protractor-demo/');
element(by.model('first')).sendKeys(1);
element(by.model('second')).sendKeys(2);
element(by.id('gobutton')).click();
expect(element(by.binding('latest')).getText()).
toEqual('3');
});
});
在这里,我们可以直接看到 Angular 的ng-model指令来访问文本字段并给它们赋值。然后我们可以通过它的 ID 找到按钮并点击它。该点击触发方法doAddition。最后,我们能够查看和检查由该方法的结果更新的值。
摘要
这是对 AngularJS 以及编写单元测试和端到端测试的一个非常高层次的审视。这两个主题都可以成为他们自己的书。
随着您的项目变得越来越大,越来越复杂,拥有一个框架可以帮助您保持一切井井有条。此外,能够测试您的代码会让您对自己编写的代码更有信心。
单元测试让您知道您的前端代码正在按预期工作。集成测试让您知道相同的代码是否适用于不同的浏览器。
Moo ( www.yearofmoo.com/2013/09/advanced-testing-and-debugging-in-angularjs.html)的站点年有一个用 Angular 进行测试和调试的优秀细分。它涵盖了诸如何时应该编写测试、在旧浏览器中测试以及测试什么和不测试什么等主题。
现在可以放心重构了,知道自己写的东西不会弄坏 app 如果有,你会尽快知道。