JavaScript-DOM-和-AJAX-入门指南-全-

JavaScript DOM 和 AJAX 入门指南(全)

原文:Beginning JavaScript with DOM Scripting and Ajax

协议:CC BY-NC-SA 4.0

零、简介

JavaScript 已经经历了巨大的转变,从一个很好了解的东西变成了开发功能齐全的网站和充分利用浏览器内置新功能的基本要素。通过库和框架给予 JavaScript 的关注一直在加速增长。

情况并非总是如此。JavaScript 最初是由 Netscape 在 1995 年作为 LiveScript 推出的。随着时间的推移,微软的 Internet Explorer 采用了它,随着 Netscape Navigator 2 的发布,Netscape 将名称改为 JavaScript。随着两家公司继续竞争,支持并没有那么好。最坏的情况是,开发人员不得不为一个网站制作两个不同的版本来支持最流行的浏览器(当时是 Netscape 4 和 Internet Explorer 4)。

浏览器支持有了很大的改进。万维网联盟(W3C)与浏览器制造商合作,开发了一种表示 HTML 文档并与之交互的标准方法,称为文档对象模型(DOM ),以及一种脚本语言,称为 ECMAScript。多年来,对这些标准的遵守情况有所改善。

这本书从基础开始,给你一个很好的基础,告诉你如何以一种不引人注目的方式编写 JavaScript,保持你的结构(HTML)、表示(CSS)和行为(JavaScript)的分离。也有一些例子说明如何让访问者使用屏幕阅读器访问你的站点,还有一些例子说明如果没有 JavaScript 如何开发你的站点。我们还涵盖了对象检测与浏览器检测、AJAX、HTML 5 表单验证以及 jQuery 介绍等主题。

本书的目标是帮助你理解 JavaScipt。如果你已经有一段时间没有看 JavaScript 了,你会很高兴地知道很多东西已经变得更好了。你可以用这本书作为更新技能的指南。如果你是 JavaScript 新手,这本书从一开始就能帮助你。我们从解释什么是变量开始,然后继续讨论如何将谷歌地图添加到你的网站中。

这本书里有很多例子,你可以用它们来增强你的站点,并根据当前浏览器内置的增强功能来添加特性。

一、JavaScript 入门

这本书将教你 JavaScript,以及如何以实用的方式使用它。读完之后,你就可以

  • 理解 JavaScript 语法和结构。
  • 创建易于理解和维护的脚本。
  • 编写不干扰他人的脚本。
  • 编写脚本,使网站更容易使用,同时不阻挡非 JavaScript 用户。
  • 编写独立于浏览器或试图理解它们的用户代理的脚本——这意味着在几年后它们仍然可用,并且不会依赖过时的技术。
  • 用 JavaScript 增强网站,允许没有任何脚本知识的开发人员改变外观和感觉。
  • 使用 JavaScript 增强 web 文档,并允许 HTML 开发人员通过简单地向元素添加 CSS 类来使用您的功能。
  • 只有在用户代理允许的情况下,才使用渐进式增强来使 web 文档变得更好。
  • 使用 Ajax 在后端和客户端之间架起一座桥梁,从而创建更易于维护、对用户来说更简洁的网站。
  • 使用 JavaScript 作为 web 方法的一部分,使您能够独立地维护它,而不会干扰其他开发流。

你在这里找不到的是

  • 特定于浏览器的 JavaScript 应用
  • JavaScript 只是用来证明它可以使用,并不能提升访问者的体验
  • 宣传不想要的内容的 JavaScript,如弹出窗口或其他华而不实的技术,如 tickers 或为动画而动画

JavaScript 在现代 web 开发中是必不可少的,但是你不能想当然地认为访问者能够使用甚至体验到你用 JavaScript 可以实现的所有效果和功能。您可以使用 JavaScript 通过添加和删除或显示和隐藏元素来完全改变网页。您可以为用户提供更丰富的界面,如拖放应用或多级下拉菜单。然而,一些访问者不能使用拖放界面,因为他们只能使用键盘或依靠语音识别来使用我们的网站。其他访问者可能依赖于听到我们的站点而不是看到它们(通过屏幕阅读器),并且可能不会被通知通过 JavaScript 实现的更改。最后但同样重要的是,有些用户就是不能启用 JavaScript 例如,在像银行这样的高安全性环境中。因此,你需要用服务器端的解决方案来备份你在 JavaScript 中做的很多事情。

image 注意网页设计已经成熟了很多年——很久以前我们就不再使用字体标签了,我们不再推荐像 bgcolor 这样的视觉属性,开始将所有的格式和外观属性转移到一个 CSS 文件中。同样的清理过程也发生在 JavaScript 上。现在网站的内容、结构、表现和行为都是相互分离的。现在的 Web 开发是为了商业和帮助用户,而不是为了发布一些东西并希望它能在大多数环境中工作。

JavaScript 现在是整体开发方法的一部分,这意味着你开发它不是为了干扰 HTML 或 CSS 等其他技术,而是为了与它们交互或补充它们。

自 20 世纪 90 年代以来,Web 开发已经取得了长足的进步,创建静态的、大小固定的网站没有多大意义。任何现代的网页设计都应该考虑到成长的需要。它还应该是每个人都可以访问的(这并不意味着每个人都有相同的外观——例如,一个漂亮的多栏布局在高分辨率显示器上可能有意义,但在手机或平板电脑上很难使用)——并为国际化做好准备。你不能再建造一些东西,并认为它会永远存在。因为网络是关于内容和变化的,如果我们不不断升级我们的网络产品,并允许其他数据源向它提供信息或从中获取信息,它就会变得过时。

介绍已经够多了——您已经通过这本书了解了 JavaScript,所以在深入研究之前,让我们先快速讨论一下 JavaScript 的历史和资产。

在本章中,您将学习

  • JavaScript 是什么,它能为你做什么
  • JavaScript 的优点和缺点
  • 如何将 JavaScript 添加到 web 文档中,它的基本语法是什么
  • 与 JavaScript 相关的面向对象编程(OOP)
  • 如何编写和运行一个简单的 JavaScript 程序

您可能已经接触过 JavaScript,并且已经知道它是什么以及它能做什么,所以我们将首先非常快速地浏览一下该语言的一些基础知识及其功能。如果你已经很了解 JavaScript,而你只是想了解更多更新更容易理解的特性和概念,你可以跳到第三章。然而,可能有些信息你已经忘记了,稍微回顾一下也没有坏处。

JavaScript 的原因

在网络的初期,有 HTML 和通用网关接口(CGI)。HTML 定义了文本文档的各个部分,并指示用户代理(通常是 web 浏览器)如何显示它——例如,由标签

包围的文本变成了一个段落。在那个段落中,你可以使用标签来定义主页面标题。请注意,对于大多数开始标记,都有一个以开头的相应结束标记。

HTML 有一个缺点——它有一个固定的状态。如果你想改变一些东西,或者使用访问者输入的数据,你需要往返一次服务器。使用一种(如 ColdFusion、Ruby on Rails、ASP.NET、PHP 或 JSP)动态技术,将表单或参数中的信息发送到服务器,然后服务器执行计算、测试、数据库查找和其他类似任务。然后,与这些技术相关联的应用服务器编写一个 HTML 文档来显示结果,并将生成的 HTML 文档返回给浏览器进行查看。

*这样做的问题是,这意味着每次有变化时,整个过程都必须重复(并且页面必须重新加载)。这既麻烦又慢。诚然,如今至少西方世界受益于快速的互联网连接,但显示一个页面仍然意味着重新加载,这可能是一个缓慢的过程,经常失败。(曾经得到过错误 404 吗?)

某些信息,如执行计算的结果和验证表单上的信息,可能不需要来自服务器。JavaScript 由访问者计算机上的用户代理(通常是浏览器)执行。我们称这个 为客户端代码 。这可以减少访问服务器的次数,提高网站的运行速度。

JavaScript 是什么?

JavaScript 最初的名字是 LiveScript ,但是网景公司把名字改成了 JavaScript——可能是因为 Java 带来的兴奋感。然而,这个名字令人困惑,因为 Java 和 JavaScript 之间没有真正的联系——尽管一些语法看起来很相似。

Java 对于 JavaScript 就像汽车对于地毯一样

—来自一个关于新闻组的 JavaScript 讨论组

网景公司在 1996 年创造了 JavaScript 语言,并通过一个解释器将其包含在他们的网景导航器(NN) 2.0 浏览器中,该解释器读取并执行添加到浏览器中的 JavaScript。html 页面。从那以后,这种语言越来越受欢迎,现在所有的浏览器和一些应用都支持这种语言,以此来定制它们。

好消息是,这意味着 JavaScript 可以在所有浏览器的网页中使用。不太好的消息是,不同浏览器实现 JavaScript 的方式有所不同,尽管核心 JavaScript 语言基本相同。但是,用户可以关闭 JavaScript。我将在本书中进一步讨论这一点。

JavaScript 的伟大之处在于,一旦你学会了如何使用它进行浏览器编程,你就可以继续在其他领域使用它。微软 Windows 8 和 Surface 平板电脑都允许使用 JavaScript 开发应用,PDF 文件使用 JavaScript,Dreamweaver 和 Photoshop 等应用可以使用 JavaScript 编写脚本。现在手机应用可以用 JavaScript 开发,转换成原生代码。JavaScript 甚至可以在服务器端使用 Node.js 之类的东西。

许多大公司也提供软件开发工具包(SDK ),让你在你的网站上访问数据或集成服务。例如,如果你想让你的访问者用他们的脸书 ID 登录,你可以使用位于 http://developers.facebook.com/web/的脸书的 JavaScript SDK。

更好的是,JavaScript 比高级编程语言或服务器端脚本语言更容易开发。它不需要像 Java 或 C++那样的任何编译,也不需要像 Perl、PHP 或 Ruby 那样在服务器或命令行上运行。编写、执行、调试和应用 JavaScript 只需要一个文本编辑器和一个浏览器——任何操作系统都提供这两个工具。当然,有一些工具可以让你轻松很多——比如 Firebug、Opera 蜻蜓和 Chrome 开发者工具等 JavaScript 调试器。

JavaScript 的问题与优点

正如本章开始时提到的,在过去几年中,JavaScript 已经成为 web 开发不可或缺的一部分,但是它也一直被错误地使用。结果,它得到了一个坏名声。这是因为使用了免费的 JavaScript 效果,比如移动页面元素和弹出窗口。第一次看到这些效果时,你可能会印象深刻,但它们很快就变成了“拥有就好”的特征,在某些情况下,甚至成为了“不再拥有就好”的元素。很多这些来自于 DHTML 的时代(我将在第三章中对此进行更多的描述)。

术语 用户代理以及缺乏对用户代理是什么的理解也是一个问题。通常情况下,用户代理是一种浏览器,如微软 Internet Explorer (IE)、Chrome、Firefox (Fx)、Opera 或 Safari。然而,浏览器并不是网络上唯一的用户代理。其他包括

  • 帮助用户克服残疾限制的辅助技术,如文本到语音转换软件或盲文显示器
  • 像 Lynx 这样的纯文本代理
  • 支持 Web 的应用
  • 游戏机中的浏览器
  • 智能手机中的浏览器
  • 平板电脑中的浏览器
  • 互动电视和机顶盒
  • 搜索引擎和其他索引程序
  • 还有更多

这种多种多样的用户代理,具有不同水平的技术技巧(以及没有得到更新的旧用户代理),对于 JavaScript 来说也是一个巨大的危险。

并非所有网站的访问者都会体验到您应用到网站上的 JavaScript 增强功能。出于安全原因,他们中的许多人还将关闭 JavaScript。JavaScript 既可以用来行善,也可以用来作恶。如果操作系统——像未打补丁的 Windows——允许,你可以通过 JavaScript 在电脑上安装 病毒或木马,或者读取用户信息并发送到另一台服务器。

注意没有办法知道访问者使用什么或者他的计算机能做什么。再者,你永远不知道来访者的经验和能力如何。这是网络美好的一面——每个人都可以参与。然而,这会给 JavaScript 程序员带来许多意想不到的后果。

在许多情况下,您可能希望有一个服务器端备份计划。它将测试用户代理是否支持所需的功能,如果不支持,服务器将接管。

脚本语言的独立性是网站的一项法律要求,如英国的数字歧视法案、美国法律的第 508 条以及世界各地的许多地方法律要求中所定义的。这意味着,如果您开发的网站没有 JavaScript 就无法使用,或者您的 JavaScript 增强功能期望用户或他们的用户代理具有某种能力而没有后备能力,您的客户可能会因歧视而被起诉。

然而,JavaScript 不是邪恶或无用的,它是一个很好的工具,可以帮助你的访问者浏览更加流畅、耗时更少的网站。

使用 JavaScript 的优点是

  • 更少的服务器交互您可以在将页面发送到服务器之前验证用户输入。这样可以节省服务器流量,也就是省钱。
  • 给访问者的即时反馈他们不必等待页面重新加载来查看他们是否忘记输入了什么
  • 例如,如果你需要你的访问者填写一个表单,JavaScript 可以提供关于表单填写情况的即时反馈。如果缺少必填字段,网站可以在向服务器提交任何数据之前通知用户。
  • 通过允许访问者在不重新加载页面的情况下更改用户界面并与之进行交互,提高了可用性例如,通过折叠和展开页面的各个部分,或者使用 JavaScript 为访问者提供额外的选项。这方面的一个经典例子是允许即时过滤的选择框,比如只显示某个机场的可用目的地,而不需要重新加载页面并等待结果。
  • 增加互动性你可以创建界面,当用户将指针悬停在其上或通过键盘激活它们时,界面会做出反应。级联样式表(CSS)和 HTML 在一定程度上也可以做到这一点,但是 JavaScript 为您提供了更广泛的选择。
  • 更丰富的界面如果你的用户允许,你可以使用 JavaScript 来包含诸如拖放组件和滑块之类的项目——这原本只能在用户必须安装的胖客户端应用中实现,如 Java 小程序或 Flash 之类的浏览器插件。
  • 轻量级环境与必须下载的大文件 Java 小程序或 Flash 电影不同,脚本文件很小,一旦加载就会被缓存(临时存储)。JavaScript 还使用浏览器控件来实现功能,而不是像 Flash 或 Java 小程序那样使用自己的用户界面。这对用户来说更容易,因为他们已经知道这些控件以及如何使用它们。现代的 Flash 和 Apache Flex 应用确实可以选择流媒体,而且——基于矢量——在视觉上是可伸缩的,这是 JavaScript 和 HTML 控件所不具备的。另一方面,SVG(可缩放矢量图形)是浏览器自带的,可以由 JavaScript 控制。

网页中的 JavaScript 和基本语法

将 JavaScript 应用于 web 文档非常容易;你所需要做的就是使用脚本标签。type 属性在 HTML5 中是可选的,但在 HTML4 中是必需的:

<script type="text/javascript">
  // Your code here
</script>

对于较旧的浏览器,您需要注释掉代码,以确保用户代理不会在页面中显示它,或者试图将其呈现为 HTML 标记。注释掉代码有两种不同的语法,但我们只展示一种。对于 HTML 文档,您可以使用普通的 HTML 注释:

<script type="text/javascript">
<!--
 // Your code here
-->
</script>

从技术上讲,可以在 HTML 文档的任何地方包含 JavaScript,浏览器会解释它。然而,在现代脚本中有理由说明为什么这是一个坏主意。不过现在,我们将在文档主体中添加 JavaScript 示例,让您可以立即看到您的第一个脚本在做什么。这将帮助你更容易地熟悉 JavaScript,而不是第三章中等待你的更现代、更先进的技术。

image 注意脚本标签还有一个“对立面”——noscript——它允许你添加只有在 JavaScript 不可用时才会显示的内容。但是,noscript 在 XHTML 和 strict HTML 中已被弃用,也没有必要使用它——如果您创建的 JavaScript 不引人注目的话。

JavaScript 语法

在我们继续之前,我们应该讨论一些 JavaScript 语法要点:

  • //表示当前行的其余部分是注释而不是要执行的代码,所以解释器不会尝试运行它。注释是在代码中添加注释的一种便捷方式,可以提醒您代码要做什么,或者帮助其他阅读代码的人了解正在发生的事情。
  • /*表示多行注释的开始。
  • */表示覆盖多行的注释的结束。如果您想停止执行某段代码,但又不想永久删除它,多行注释也很有用。例如,如果您的一段代码有问题,并且您不确定是哪一行导致了问题,您可以一次注释一部分来隔离问题。
  • 花括号({和})用于表示代码块。它们确保大括号内的所有行被视为一个块。当我讨论 if 或 f or 之类的结构以及函数时,你会看到更多这样的例子。
  • 分号或换行符定义了一个语句的结尾,一个语句就是一个命令。分号实际上是可选的,但使用它们来明确语句的结束位置仍然是一个好主意,因为这样做会使代码更容易阅读和调试。(虽然你可以把很多语句放在一行,但是最好把它们放在单独的行,这样代码更容易阅读。)

让我们把这个语法放到一个工作代码块中:

<!DOCTYPE html>
<html>
<head>
<body>
<script type="text/JavaScript">
  // One-line comments are useful for reminding us what the code is doing

  /*
     This is a multiline comment. It's useful for longer comments and
     also to block out segments of code when you're testing
  */

  /*
    Script starts here. We're declaring a variable myName, and assigning to it the
    value of whatever the user puts in the prompt box (more on that in Chapter
    2), finishing the instruction with a semicolon because it is a statement
  */
  var myName = prompt ("Enter your name","");

  // If the name the user enters is Chris Heilmann
  if (myName == "Chris Heilmann")
  {
     // then a new window pops up saying hello
     alert("Hello Me");
  }

  // If the name entered isn't Chris Heilmann
  else
  {
     // say hello to someone else
     alert("hello someone else");
  }
</script>
</body>
</html>

根据您以前的 JavaScript 经验,有些代码可能还没有意义。现在重要的是清楚注释是如何使用的,什么是代码块,以及为什么在一些语句的末尾有分号。如果愿意,您可以运行这个脚本——只需将它复制到 HTML 页面中,用文件扩展名保存文档。html,并在浏览器中打开它。

虽然像 if 和 else 这样的语句跨越多行并包含其他语句,但它们被认为是单个语句,后面不需要分号。JavaScript 解释器知道,由于花括号{},与 if 语句链接的行应该被视为一个块。虽然这不是强制性的,但是您应该缩进花括号中的代码。这使得阅读和调试更加容易。我们将在下一章看到变量和条件语句(if 和 else)。

代码执行

浏览器从上到下读取页面,因此代码执行的顺序取决于脚本块的顺序。一个脚本块是<脚本>和</脚本>标签之间的代码。(还要注意,不只是浏览器能读懂你的代码;网站的用户也可以查看你的代码,所以你不应该在里面放任何秘密或敏感的东西。)下一个示例中有三个脚本块:

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
  alert( 'First script Block ');
  alert( 'First script Block - Second Line ');
</script>
</head>
<body>
<h1>Test Page</h1>
<script type="text/javascript">
  alert( 'Second script Block' );
</script>
<p>Some more HTML</p>
<script type="text/JavaScript">
    alert( 'Third script Block' );
  function doSomething() {
    alert( 'Function in Third script Block' );
  }
</script>
</body>
</html>

如果您尝试一下,您会看到第一个脚本块中的 alert()对话框出现并显示消息

First script Block

接下来是第二行显示消息的下一个 alert()对话框

First script Block - Second Line.

解释器继续向下,到达第二个脚本块,alert()函数在这里显示这个对话框:

Second script Block

第三个脚本块后面是 alert()语句,显示

Third script Block

尽管函数中还有另一个 alert 语句,但它并不执行和显示消息。这是因为它在函数定义(function doSomething())中,并且函数中的代码只有在函数被调用时才会执行。

关于函数的旁白

我将在第三章中更深入地讨论函数,但我在这里介绍它们是因为如果不理解函数,你就无法在 JavaScript 中走得很远。一个函数是一个命名的、可重用的代码块,用花括号括起来,你创建它来执行一个任务。JavaScript 包含可供您使用和执行任务的函数,例如向用户显示消息。正确使用函数可以使程序员免于编写大量重复的代码。

您还可以创建我们自己的函数,这就是我们在前面的代码块中所做的。假设您创建了一些代码,将消息写到页面的某个元素中。你可能想在不同的情况下反复使用它。虽然您可以在任何想要使用代码块的地方剪切和粘贴代码块,但是这种方法会使代码过长;如果您希望同一段代码在一个页面中出现三次或四次,那么破译和调试也会变得相当困难。相反,您可以将消息传递代码封装到一个函数中,然后使用参数传入该函数需要的任何信息。函数也可以向最初调用该函数的代码返回值。

要调用这个函数,你只需写下它的名字,后跟括号,()。(注意—使用括号传递参数。但是,即使没有参数,也必须使用括号。)但是,正如您所料,在脚本创建该函数之前,您无法调用它。您可以在这个脚本中调用它,方法是将它添加到第三个脚本块,如下所示:

<script type="text/javascript">
  alert( 'Third script Block ');
function doSomething(){
  alert( 'Function in Third script Block ');
}
// Call the function doSomething
doSomething();
</script>
</body>
</html>

到目前为止,在本章中,您已经了解了 JavaScript 语言的优点和缺点,看到了一些语法规则,了解了该语言的一些主要组成部分(尽管很简单),并运行了一些 JavaScript 脚本。你已经走了相当长的距离。在下一章开始更详细地研究 JavaScript 语言之前,我们先来谈谈成功的 JavaScript 开发的一些关键要素: objects

对象

对象是 JavaScript 使用方式的核心。在许多方面,JavaScript 中的对象就像编程之外的世界中的对象。(确实存在,我只是看了一下。)在现实世界中,一个对象只是一个“东西”(很多关于面向对象编程的书把对象比作名词):一辆车,一张桌子,一把椅子,还有我正在敲的键盘。对象有

  • 属性(类比形容词)这辆车是红色
  • 启动汽车的方法可能是转动点火钥匙
  • 事件转动点火钥匙导致汽车启动事件。

面向对象编程(OOP)试图通过对现实世界的对象建模来简化编程。假设您正在创建一个汽车模拟器。首先,创建一个汽车对象,赋予它类似于颜色当前速度的属性。然后你需要创建方法:也许一个 start 方法来启动汽车,一个 brake 方法来减速汽车,你需要向其中传递关于刹车应该压得多紧的信息,以便你可以确定减速效果。最后,你会想知道你的车什么时候出了问题。在 OOP 中,这被称为事件。例如,当油箱油量不足时,汽车会发出通知(仪表盘上的灯)让你知道该加油了。在这段代码中,您可能希望监听这样的事件,以便能够对此采取措施。

面向对象编程使用这些概念。这种设计软件的方式现在非常普遍,并且影响了编程的许多领域——但是对你来说最重要的是,它是 JavaScript 和 web 浏览器编程的核心。

我们将使用的一些对象是语言规范的一部分:例如,字符串对象、日期对象和数学对象。PDF 文件和 web 服务器上的 JavaScript 也可以使用相同的对象。这些对象提供了许多有用的功能,可以节省您大量的编程时间。例如,您可以使用 Date 对象从客户端(如用户的 PC)获取当前日期和时间。它存储日期并提供许多有用的与日期相关的功能—例如,将日期/时间从一个时区转换到另一个时区。这些对象通常被称为核心对象 ,因为它们独立于实现。浏览器还可以通过对象进行编程,您可以使用这些对象来获取有关浏览器的信息并更改应用的外观。例如,浏览器使 Document 对象可用,该对象表示 JavaScript 可用的网页。您可以在 JavaScript 中使用它来向 web 浏览器的用户正在查看的 web 页面添加新的 HTML。如果您在不同的主机上使用 JavaScript,例如 Node.js 服务器,您会发现托管 JavaScript 的服务器公开了一组非常不同的主机对象,因为它们的功能与您想在 web 服务器上做的事情有关。

你还会在第三章中看到,你可以使用 JavaScript 创建自己的对象。这是一个强大的特性,允许您使用 JavaScript 对现实世界的问题进行建模。要创建一个新对象,您需要使用一个名为的模板来指定它应该具有的属性和方法。一个类有点像建筑师的画,因为它指定了什么应该去哪里,做什么,但是它实际上并不创建对象。

image 注意关于 JavaScript 是基于对象的语言还是面向对象的语言还存在一些争论。区别在于,基于对象的语言使用对象进行编程,但不允许编码者在她的代码设计中使用面向对象的编程。面向对象的编程语言不仅使用对象,而且使开发和设计符合面向对象设计方法的代码变得容易。JavaScript 允许您创建自己的对象,但这并不是像 Java 或 C#这样基于类的语言那样完成的。然而,我们不会在这里集中讨论什么是或不是面向对象,而是在本书中对象在实践中是如何有用的,并且我们会看一些基本的面向对象的编码,在那里它帮助我们的生活变得更容易。

随着本书的深入,您将更深入地了解对象:JavaScript 语言的核心对象、浏览器使用 JavaScript 访问和操作的对象,以及您自己的自定义对象。不过现在,你需要知道的是 JavaScript 中的对象是可以用来给网页添加功能的实体,它们可以有属性和方法。例如,Math 对象在其属性中有一个表示圆周率的值,在其方法中有一个生成随机数的方法。

简单的 JavaScript 示例

我将用一个简单的脚本来结束这一章,它首先确定访问者屏幕的宽度,然后应用一个合适的样式表。(它通过向页面添加一个额外的 LINK 元素来实现这一点。类似这样的事情现在可以使用 CCS 媒体查询来完成,但这仍然是如何使用对象的一个很好的例子。)您将使用 Screen 对象来实现这一点,它是用户屏幕的表示。这个对象有一个 availWidth 属性,您可以检索并使用它来决定加载哪个样式表。

下面是代码:

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>CSS Resolution Demo</title>
    <!-- Basic style with all settings -->
    <link rel="StyleSheet" href="basic.css" type="text/css" />
    <!--
    Extra style (applied via JavaScript) to override default settings
    according to the screen resolution
    -->
    <script type="text/javascript">
      // Define a variable called cssName and a message
      // called resolutionInfo
      var cssName;
      var resolutionInfo;
      // If the width of the screen is less than 650 pixels
      if( screen.availWidth < 650 ) {
      // define the style Variable as the low-resolution style
        cssName = 'lowres.css';
        resolutionInfo = 'low resolution';
      // Or if the width of the screen is less than 1000 pixels
      } else {
        if( screen.availWidth > 1000 ) {
      // define the style Variable as the high-resolution style
          cssName = 'highres.css';
          resolutionInfo = 'high resolution';
      // Otherwise
        } else {
      // define the style Variable as the mid-resolution style
          cssName = 'lowres.css';
          resolutionInfo = 'medium resolution';
        }
      }
      document.write( '<link rel="StyleSheet" href="' +
cssName + '" type="text/css" />' );
    </script>
    </head>
  <body>
    <script type="text/javascript">
      document.write( '<p>Applied Style:' +
resolutionInfo + '</p>' );
    </script>
  </body>
</html>

尽管我们将在下一章看到 if 语句和循环的细节,你可能已经看到它是如何工作的了。第一行的 if 语句询问 screen . avail width 是否小于 650:

if ( screen.availWidth < 650 )

如果用户的屏幕是 640×480,宽度小于 650,则执行花括号内的代码,并定义低分辨率样式和消息。

if ( screen.availWidth < 650 ) {
// define the style Variable as the low-resolution style
  cssName = 'lowres.css';
  resolutionInfo = 'low resolution';
}

代码使用 else 语句继续检查屏幕大小。最后一个 else 只有在其他两个评估都没有导致代码被执行的情况下才会出现,因此假设屏幕为 800×600,并相应地定义媒体样式和消息:

else {
// define the style Variable as the mid-resolution style
  cssName = 'lowres.css';
  resolutionInfo = 'medium resolution';
}

请注意,您在这里测量的是屏幕大小,用户可能有一个 800×600 的屏幕,但这并不意味着他的浏览器窗口被最大化。您可能应用了不合适的样式。

您正在使用另一个对象 document 对象写入页面(一个 HTML 文档)。 document 对象的 write()方法允许您将 HTML 插入页面。请注意,document.write()实际上并没有改变源 HTML 页面,只是改变了用户在计算机上看到的页面。

image 事实上,当你阅读这本书的前几章时,你会发现 document.write()非常有用。对于显示脚本如何工作的小例子,对于与用户交流,甚至对于调试程序中您不确定是否在做您认为它应该做的事情,这都是很好的。它也适用于所有支持 JavaScript 的浏览器。越来越多的现代浏览器有更好的调试工具和方法,但是我会在本书的后面部分详细介绍。

您使用 document.write()写出适当的 link 元素,并在头部使用您定义的样式:

document.write( '<link rel="StyleSheet" href="' +
cssName + '" type="text/css" />' );

在文档的正文中,您写出解释应用了哪种解决方式的消息:

<script type="text/javascript">
  document.write( '<p>Applied Style: '+ resolutionInfo + '</p>' );
</script>

稍后,我们将使用更复杂的例子,使用 JavaScript 来测试用户代理和界面的功能。不过现在,我希望这个简单的例子能让您对使用 JavaScript 为 web 页面增加的灵活性有所了解。

摘要

在这一章中,我们了解了 JavaScript 是什么,它是如何工作的,以及它的优缺点。我注意到最大的缺点是你不能依赖它。然而,我也提到了使用 JavaScript 可以为用户提供更好、更流畅的网站体验。

您运行了一些 JavaScript 代码,了解了如何向代码添加注释,以及如何使用分号分隔 JavaScript 语句。您还看到,您可以使用花括号告诉 JavaScript 将一组代码行视为单个块,例如,在 if 语句之后。您了解了 JavaScript 的执行通常是从上到下,从第一个脚本块到最后一个脚本块,只有在您告诉它们执行时才执行的函数除外。

您还了解了对象,这是编写 JavaScript 的核心。不仅 JavaScript 本身非常依赖于对象,而且浏览器也使用对象和方法使自己和文档可用于脚本。最后,我们看了一个简单的例子,它读取用户的屏幕分辨率并应用合适的样式表。

在下一章,我将介绍 JavaScript 的语言基础。您将看到 JavaScript 如何存储和操作数据,并在计算中使用它。我们还将研究如何使用决策语句创建“智能”JavaScript 程序,这些决策语句允许您评估数据,用数据进行计算,并决定适当的操作过程。有了这一章,你就有了继续进行更令人兴奋和有用的 web 编程所需的大部分基础知识。*

二、数据和决策

数据和决策是每个智能程序的基础。我们将从 JavaScript 如何理解或表示数据开始这一章。这一点很重要,因为 JavaScript 支持多种数据类型,并根据数据类型操作数据。不同类型的数据不匹配可能会产生意外的结果。我们将研究一些更常见的数据类型问题,您将看到如何将一种数据类型转换为另一种数据类型。

我们还将使用条件语句循环:两种最有价值的决策工具。用计算机语言做决策,你需要让程序知道在响应某些条件时应该发生什么,这就是条件语句的用武之地。另一方面,循环只是允许你重复一个动作,直到满足一个特定的条件。例如,您可能希望遍历表单中的每个输入框,并检查其中包含的信息是否有效。

在本章中,我将涉及 JavaScript 的许多不同方面:

  • JavaScript 中的信息分类和操作:数据类型和数据操作符
  • 变量
  • 转换数据类型
  • 数据对象简介:字符串、日期和数学对象
  • 数组:存储有序的数据集,比如购物篮中的商品
  • 使用条件语句、循环和数据评估进行决策

image 注意本章中的例子尽可能保持简单,因此使用 document.write()作为反馈机制,以便您查看结果。在后面的章节中,您将了解到其他更现代、更通用的方法。

数据、数据类型和数据运算符

数据是用来存储信息的,为了更有效地存储信息,JavaScript 需要为每条数据分配一个类型。这种类型规定了可以或不可以对数据做什么。例如,JavaScript 数据类型之一是数字,您可以使用它对它保存的数据执行某些计算。

JavaScript 中存储数据的三种最基本的数据类型是

  • 字符串:一系列字符,例如“某些字符”
  • 数字:数字,包括浮点数
  • 布尔型:可以包含真值或假值

这些有时被称为 原语 数据类型 ,因为它们只存储单个值。还有两种稍微不同的原始数据类型。它们不存储信息,而是在特定情况下向您发出警告:

  • Null :表示即使一个变量已经被创建,它的当前值也是 Null 或者没有。
  • 未定义:表示某事物未被定义并赋予值。当你处理变量时,这很重要。

在本章中,我们将广泛地使用这些数据类型。

字符串数据类型

JavaScript 解释器希望字符串数据 用单引号或双引号括起来(称为分隔符)。例如,下面的脚本将在页面上写入一些字符:

<html>
<body>
<script type="text/javascript">
      document.write("some characters");
</script>
</body>
</html>

引号不会写入页面,因为它们不是字符串的一部分;它们只是告诉 JavaScript 字符串在哪里开始和结束。你可以很容易地使用单引号:

<html>
<body>
<script type='text/javascript'>
      document.write('some characters');
</script>
</body>
</html>

这两种方法都可以,只要你以打开字符串的方式关闭字符串,并且不要试图像这样分隔它:

document.write('some characters");
document.write("some characters');

当然,您可能希望在字符串本身中使用单引号或双引号,在这种情况下,您需要使用不同的分隔符。如果使用双引号,说明将被解释为您想要的含义:

document.write("Paul' s characters");

但是如果你用单引号,它们就不会是:

document.write('Paul' s characters');

这会给你一个语法错误,因为 JavaScript 解释器认为字符串在保罗中的 l 之后结束,并且不理解后面发生了什么。

image 注意 JavaScript 语法和英语语法一样,是一套让语言变得易懂的规则。正如英语中的语法错误会使句子变得毫无意义一样,JavaScript 中的语法错误也会使指令变得毫无意义。

通过使用单引号来分隔任何包含双引号的字符串,可以避免产生如下 JavaScript 语法错误,反之亦然:

document.write("Paul' s numbers are 123");
document.write('some "characters"');

另一方面,如果你想在字符串中同时使用单引号和双引号,你需要使用一个叫做转义序列的东西。事实上,更好的编码实践是使用转义序列,而不是我们到目前为止一直使用的引号,因为它们使你的代码更容易阅读。

转义序列

转义序列 在您想要使用无法通过键盘输入的字符的情况下也很有用(比如在西方键盘上代表日元的符号)。表 2-1 列出了一些最常用的转义序列。

表 2-1。常见转义序列

转义序列 代表的字符
\b 退格。
\f 换页。
\n 换行。
\r 回车。
\t 选项卡。
' 单引号。
" 双引号。
\ 反斜杠。
\xNN NN 是一个十六进制数,标识 Latin-1 字符集中的一个字符。(拉丁语-1 字符是英语国家的标准。)
\ uDDDD DDDD 是标识 Unicode 字符的十六进制数。

让我们修改以下导致语法错误的字符串

document.write( 'Paul' s characters' );

以便它使用转义序列(\ ')并被正确解释:

document.write( 'Paul\' s characters' );

转义序列告诉 JavaScript 解释器单引号属于字符串本身,不是分隔符。

ASCII 是一种字符编码方法,使用从 0 到 254 的值。或者,您可以使用十六进制的 ASCII 值和\xNN 转义序列来指定字符。字母 C 在十进制中是 67,在十六进制中是 43,因此您可以使用转义序列将其写入页面,如下所示:

document.write( "\x43" );

\ uDDDD 转义序列的工作方式非常相似,但是使用 Unicode 字符编码方法,该方法有 65,535 个字符。因为前几百个 ASCII 和 Unicode 字符集是相似的,所以您可以使用如下转义序列来编写字母 C :

document.write( '\u0043' );

ASCII 和 Unicode 信息可以变得非常详细,所以寻找信息的最好地方是在网上。对于 Unicode,尝试一下www.unicode.org

操作员

JavaScript 有许多操作符,可以用来操作程序中的数据;你可能会从数学课上认出他们。 表 2-2 给出了一些最常用的运算符。

表 2-2。 JavaScript 运算符

操作员 它的作用
+ 将两个数字相加或连接两个字符串。
- 从第一个数字中减去第二个数字。
* 将两个数相乘。
/ 将第一个数字除以第二个数字。
% 寻找模数(除法的余数),例如 98 % 10 = 8。
- 将数字减 1;只对变量有用,稍后你会在工作中看到。
++ 将数字增加 1;只对变量有用,稍后你会在工作中看到。

以下是它们的使用情况:

<html>
<body>
<script type="text/javascript">
      document.write( 1 - 1 );
      document.write("<br>" );
      document.write( 1 + 1 );
      document.write("<br>" );
      document.write( 2 * 2 );
      document.write( "<br>" );
      document.write( 12 / 2 );
      document.write("<br>" );
      document.write( 1 + 2 * 3 );
      document.write("<br>" );
      document.write( 98 % 10 );
</script>
</body>
</html>

您应该得到以下输出:

0
2
4
6
7
8

JavaScript 就像数学一样,赋予一些运算符优先级。乘法的优先级高于加法,所以计算 1 + 2 * 3 是这样进行的:

  • 2 * 3 = 6
  • 6 + 1 = 7

所有运算符都有优先顺序。乘法、除法和模数具有相同的优先级,因此当它们都出现在一个等式中时,求和是从左到右计算的。尝试以下计算:

  • 2 * 10 / 5%3

结果是 1,因为计算只是从左向右读取:

  • 2 * 10 = 20
  • 20 / 5 = 4
  • 4%3 = 1

加法和减法也有同样的优先权。

您可以使用括号赋予部分计算更高的优先级。例如,您可以将 1 加 1,然后乘以 5,如下所示:

  • (1 + 1) * 5

结果将是 10,但如果没有括号,结果将是 6。事实上,即使括号不是必需的,也应该使用括号,因为它们有助于使执行顺序清晰。

如果使用多组括号,JavaScript 简单地从左到右工作,或者,如果有内括号,从内向外工作:

document.write( ( 1 + 1 ) * 5 * ( 2 + 3 ) );

前面公式的计算是这样进行的:

  • (1 + 1) = 2
  • (2 + 3) = 5
  • 2 * 5 = 10
  • 10 * 5 = 50

如您所见,JavaScript 的加法运算符将值相加。它对这两个值的实际处理取决于您使用的数据类型。例如,如果您正在处理两个存储为数字数据类型的数字,则+运算符会将它们相加。但是,如果您正在处理的数据类型之一是字符串(如分隔符所示),这两个值将被连接起来。试试这个:

<html>
<body>
<script type="text/javascript">
      document.write( 'Java' + 'Script' );
      document.write( 1 + 1 );
      document.write( 1 + '1' );
</script>
</body>
</html>

能够对字符串使用加法运算符可能会很方便(在本例中称为串联运算符),但是如果您正在处理的某个值恰好与您期望的数据类型不同,也会产生意外的结果。稍后,我们会看到一些类似的例子,并解决它们。

如果您像目前为止所做的那样处理文字值,问题就小多了。然而,您将在程序中使用的大部分数据将由用户输入或由脚本生成,因此您无法提前确切知道您将使用哪些值。这就是变量发挥作用的地方。变量是脚本中数据的占位符,是 JavaScript 的核心。

JavaScript 变量

谈到变量,JavaScript 可能是最宽容的语言。在使用变量之前,你不需要定义变量是什么,你可以在脚本中随时改变变量的类型。然而,如果你的代码运行在严格模式下,隐式创建变量(没有先定义)是行不通的。有关这方面的更多信息,Mozilla 开发者网络有关于严格模式有何不同的完整描述,位于:Developer . Mozilla . org/en-US/docs/JavaScript/Reference/Functions _ and _ function _ scope/Strict _ Mode

通过给变量一个唯一的名称并使用 var 关键字来声明变量。变量名必须以字母表中的一个字母或下划线开头,而名称的其余部分只能由数字、字母、美元符号($)和下划线字符组成。不要使用任何其他字符。

image 注意像 JavaScript 中的大多数东西一样,变量名是区分大小写的——例如,thisVariable 和 ThisVariable 是不同的变量。命名变量时要非常小心;如果你不能始终如一地说出他们的名字,你会遇到各种各样的麻烦。为此,大多数程序员使用骆驼符号,其中变量名以小写字母开头,而后面的单词是大写的,并且没有空格。因此,这个变量的名字。

总是给你的变量取一个有意义的名字。在下一个例子中,我们将构建,我们将编写一个汇率转换程序,所以我们将使用像 euroToDollarRate 和 dollarToPound 这样的变量名。描述性地命名变量有两个好处:如果你以后再来看,更容易记住代码在做什么,对于新接触代码的人来说,更容易看到发生了什么。代码可读性和布局对网页的开发非常重要。它可以更快更容易地发现错误并调试它们,并根据需要修改代码。

image 注意虽然技术上没有必要,但是变量声明应该以关键字 var 开头。不使用它可能会有影响,你会看到你的进步。

说了这么多,让我们开始声明变量。您可以声明一个变量而不初始化它(给它一个值):

var myVariable;

然后它就准备好了,等着你有值的时候。这对于保存用户输入的变量很有用。

您也可以同时声明和初始化变量:

var myVariable = "A String";
var anotherVariable = 123;

或者,您可以通过将 prompt()函数的返回值或计算的和赋给变量来声明和初始化变量:

var eurosToConvert = prompt("How many Euros do you wish toconvert", "");
var dollars = eurosToConvert * euroToDollarRate;

prompt()函数是一个 JavaScript 函数,它要求用户输入一个值,然后将其返回给代码。这里,您将输入的值赋给变量 eurosToConvert。

初始化您的变量是一个非常好的主意,特别是如果您可以给它们一个对应用有用的默认值。甚至将变量初始化为空字符串也是一个好主意,因为您可以检查它,而不会出现错误消息,如果它没有值的话,就会弹出错误消息。

让我们看看变量是如何提高代码的可读性和功能性的。下面是一段没有任何变量的代码:

<html>
<body>
<script type="text/javascript">
      document.write( 0.872 * prompt( "How many Euros do you wish to convert", "" ) );
</script>
</body>
</html>

这段代码是否将欧元转换成美元并不明显,因为没有什么可以告诉你 0.872 就是汇率。尽管代码运行良好;如果您用数字 10 进行试验,您应该会得到以下结果:

8.72

在这个例子中,我们使用 window 对象的 prompt()方法来获取用户反馈。(在这种情况下,窗口是可选的;为了使代码更短,可以省略它。)这个方法有两个参数:一个显示在输入字段上方的标签和该字段的初始值。你将在第四章中了解更多关于 prompt()以及如何使用它。假设您想让结果更具信息性,如下所示:

10 Euros is 8.72 Dollars

如果没有变量,唯一的方法就是让用户输入两次他们想要兑换的欧元金额,这真的对用户不友好。但是,使用变量,您可以临时存储数据,然后根据需要多次调用它:

<html>
<body>
<script type="text/javascript">
      // Declare a variable holding the conversion rate
      var euroToDollarRate = 0.872;
      // Declare a new variable and use it to store the
      // number of euros
      var eurosToConvert = prompt( "How many Euros do you wish to convert", "" );
      // Declare a variable to hold the result of the euros
      // multiplied by the conversion
      var dollars = eurosToConvert * euroToDollarRate;
      // Write the result to the page
      document.write( eurosToConvert + " euros is " + dollars + " dollars" );
</script>
</body>
</html>

您已经使用了三个变量:一个存储从欧元到美元的汇率,另一个存储将要转换的欧元数,最后一个保存转换成美元的结果。然后你需要做的就是用两个变量写出结果。这个脚本不仅功能更强大,而且更容易阅读。

转换不同类型的数据

在很大程度上,JavaScript 解释器可以计算出您希望使用什么数据类型。例如,在下面的代码中,解释器将数字 1 和 2 理解为数字数据类型,并相应地处理它们:

<html>
<body>
<script type="text/javascript">
      var myCalc = 1 + 2;
      document.write( "The calculated number is " + myCalc );
</script>
</body>
</html>

这将被写入您的页面:

The calculated number is 3

但是,如果您重写代码,允许用户使用 prompt()函数输入自己的数字,您将得到完全不同的计算结果:

<html>
<body>
<script type="text/javascript">
      var userEnteredNumber = prompt( "Please enter a number","" );
      var myCalc = 1 + userEnteredNumber;
      var myResponse = "The number you entered + 1 = " + myCalc;
      document.write( myResponse );
</script>
</body>
</html>

如果你在提示符下输入 2,你会被告知

The number you entered + 1 = 12

JavaScript 解释器没有将两个数相加,而是将它们连接起来。这是因为 prompt()函数实际上将用户输入的值作为字符串数据类型返回,即使该字符串包含数字字符。这一行发生串联:

var myCalc = 1 + userEnteredNumber;

实际上,这和你写的一样

var myCalc = 1 + "2";

但是,如果使用减法运算符,如下所示:

var myCalc = 1 - userEnteredNumber;

从 1 中减去 userEnteredNumber。减法运算符不适用于字符串数据,因此 JavaScript 得出结论,您希望将数据视为数字,将字符串转换为数字,然后进行计算。这同样适用于*和/运算符。typeof()运算符返回传递给它的数据类型,因此您可以使用它来查看 JavaScript 解释器正在处理哪些数据类型:

<html>
<body>
<script type="text/javascript">
      var userEnteredNumber = prompt( "Please enter a number","" );
      document.write( typeof( userEnteredNumber ) );
</script>
</body>
</html>

这将把字符串写入页面。确保解释器使用期望的数字数据类型的方法是 显式地 声明该数据是一个数字。您可以使用三个函数来完成此操作:

  • Number(): 尝试将括号内的变量值转换成数字。
  • parseFloat(): 尝试将值转换为浮点。它从左到右逐个字符地解析字符串,直到遇到不能在数字中使用的字符。然后,它在该点停止,并将该字符串计算为一个数字。如果第一个字符不能用在数字中,结果就是 NaN(它代表而不是数字)。
  • parseInt(): 通过删除任何小数部分而不向上或向下舍入数字,将值转换为整数。传递给该函数的任何非数值内容都将被丢弃。如果第一个字符不是+、–或数字,则结果为 NaN。

让我们看看这些函数在实践中是如何工作的:

<html>
<body>
<script type="text/javascript">
      var userEnteredNumber = prompt( "Please enter a number", "" );
      document.write( typeof( userEnteredNumber ) );
      document.write( "<br>" );
      document.write( parseFloat( userEnteredNumber ) );
      document.write( "<br>" );
      document.write( parseInt( userEnteredNumber ) );
      userEnteredNumber = Number( userEnteredNumber )
      document.write( "<br>" );
      document.write( userEnteredNumber );
      document.write( "<br>" );
      document.write( typeof( userEnteredNumber ) );
</script>
</body>
</html>

尝试输入值 23.50。您应该得到以下输出:

string
23.5
23
23.5
number

输入的数据在第一行作为字符串读取。然后 parseFloat()将 23.50 从字符串转换为浮点数,在下一行中,parseInt()去掉小数部分(不向上或向下舍入)。然后,使用 number()函数将变量转换为数字,并存储在 userEnteredNumber 变量本身中(覆盖保存在那里的字符串)。在最后一行,您会看到 userEnteredNumber 的数据类型确实是 Number。

尝试在用户提示符下输入 23.50abc:

string
23.5
23
NaN
number

结果差不多,但是这次 Number()返回了 NaN。parseFloat()和 parseInt()函数仍然返回一个数字,因为它们从左向右工作,尽可能将字符串转换为数字,然后在遇到非数字值时停止。Number()函数拒绝任何包含非数字字符的字符串。(允许使用数字、有效的小数位以及+和–符号,但不允许使用其他符号。)

如果你尝试输入 abc,你只会得到

string
NaN
NaN
NaN
number

没有一个函数能找到有效的数字,所以它们都返回 NaN,您可以看到它是一个数字数据类型,但不是一个有效的数字。这是检查用户输入有效性的好方法,稍后您将使用它来完成这一任务。

所以让我们回到我们开始的问题:使用 prompt()检索一个数字。您只需要告诉解释器,用户输入的数据应该转换为数字数据类型,使用 prompt()函数讨论的函数之一:

<html>
<body>
<script type="text/javascript">
      var userEnteredNumber = Number( prompt( "Please entera number", "" ) );
      var myCalc = 1 + userEnteredNumber;
      var myResponse = "The number you entered + 1 = " + myCalc;
      document.write( myResponse );
</script>
</body>
</html>

这不会抛出任何错误,但对访问者没有太大帮助,因为 NaN 的意思不是常识。稍后,您将处理各种条件,并且您将看到如何防止对不熟悉 JavaScript 的用户来说没有多大意义的输出。

这就是你现在需要知道的关于原始数据类型和变量的全部内容。如您所见,原始数据类型只是保存一个值。然而,JavaScript 也可以处理复杂数据,它使用复合数据类型来处理。

复合数据类型:数组和对象

复合数据类型不同于简单数据类型,因为它们可以保存多个值。有两种复合数据类型:

  • 对象:包含对任何对象的引用,包括浏览器提供的对象
  • 数组:包含一个或多个其他数据类型

我们将首先看一下对象数据类型。您可能还记得第一章的讨论,对象模拟现实世界的实体。这些对象可以保存数据,并为您提供属性和方法。

JavaScript 为您提供的对象:字符串、日期和数学

这不是一个完整的内置对象列表,但是你可以通过下面的例子来感受一下对象是如何工作的

  • String 对象:存储字符串,并提供处理字符串的属性和方法
  • Date 对象:存储一个日期,并提供处理它的方法
  • Math 对象:不存储数据,但是提供了操作数学数据的属性和方法

让我们从字符串对象开始。

字符串对象

在前面,您通过给它们一些字符来创建字符串原语,如下所示:

var myPrimitiveString = "ABC123";

String object 做的事情略有不同,不仅允许您存储字符,还提供了一种操作和更改这些字符的方法。您可以显式或隐式创建 String 对象。

创建字符串对象

让我们首先使用隐式方法:我们首先声明一个新变量,并为它分配一个新的 string 原语来初始化它。现在尝试使用 typeof()确保变量 myStringPrimitive 中的数据是一个字符串原语:

<html>
<body>
<script type="text/javascript">
      var myStringPrimitive= "abc";
      document.write( typeof( myStringPrimitive ) );
</script>
</body>
</html>

但是,您仍然可以对它使用 String 对象的方法。JavaScript 将简单地将 string 原语转换为临时 string 对象,对其使用方法,然后将数据类型改回 String。您可以使用字符串对象的长度属性来尝试一下:

<html>
<body>
<script type="text/javascript">
      var myStringPrimitive= "abc";
      document.write( typeof( myStringPrimitive ) );
      document.write( "<br>" );
      document.write( myStringPrimitive.length );
      document.write( "<br>" );
      document.write( typeof( myStringPrimitive ) );
</script>
</body>
</html>

这是您应该在浏览器窗口中看到的内容:

string
3
String

所以在临时转换之后,myStringPrimitive 仍然保存着一个 string 原语。还可以使用 new 关键字和 String() 构造函数 显式创建 String 对象:

<html>
<body>
<script type="text/javascript">
      var myStringObject = new String( "abc" );
      document.write( typeof( myStringObject ) );
      document.write( "<br>" );
      document.write( myStringObject.length );
      document.write( "<br>" );
      document.write( typeof( myStringObject ) );
</script>
</body>
</html>

加载此页面会显示以下内容:

object
3
object

这个脚本与前一个脚本的唯一区别在于第一行,在这里您创建了一个新的对象,并为 String 对象提供了一些要存储的字符:

var myStringObject = new String( "abc" );

无论是隐式还是显式创建 String 对象,检查 length 属性的结果都是一样的。显式或隐式创建 String 对象之间唯一的真正区别是,如果您要一次又一次地使用同一个 String 对象,那么显式创建它们会稍微高效一些。显式创建 String 对象也有助于防止 JavaScript 解释器混淆数字和字符串,因为它可能会混淆数字和字符串。

使用字符串对象的方法

String 对象有很多方法,所以这里我只讨论其中的两个,indexOf()和 substring()方法。

如您所见,JavaScript 字符串是由字符组成的。每个字符都有一个索引。索引从零开始,因此第一个字符的位置的索引为 0,第二个字符的索引为 1,依此类推。indexOf()方法在索引中查找并返回子字符串开始的位置(lastIndexOf()方法返回子字符串最后一次出现的位置)。例如,如果您希望您的用户输入一个电子邮件地址,您可以检查她是否在条目中包含了@符号。(虽然这不能确保地址有效,但至少会朝着那个方向前进。在本书的后面,我们将处理更复杂的数据检查。)

接下来让我们这样做,使用 prompt()方法获取用户的电子邮件地址,然后检查@符号的输入,使用 index of()返回符号的索引:

<html>
<body>
<script type="text/javascript">
      var userEmail= prompt("Please enter your emailaddress ", "" );
      document.write( userEmail.indexOf( "@" ) );
</script>
</body>
</html>

如果找不到@符号,则将–1 写入页面。只要字符在字符串中的某个位置,它在索引中的位置——换句话说,大于-1的值将被返回。

substring()方法从另一个字符串中抽取一个字符串,将子字符串的起始和结束位置的索引作为参数。通过省略第二个参数,可以返回从第一个索引到字符串末尾的所有内容。

因此,要提取从第三个字符(索引 2)到第六个字符(索引 5)的所有字符,您应该编写

<html>
<body>
<script type="text/javascript">
      var myOldString = "Hello World";
      var myNewString = myOldString.substring( 2, 5 );
      document.write( myNewString );
</script>
</body>
</html>

您应该看到 llo 被写到浏览器中。注意,substring()方法复制它返回的子字符串,并且不改变原始字符串。

当您处理未知值时,substring()方法真正发挥了作用。下面是另一个同时使用 indexOf()和 substring()方法的示例:

<html>
<body>
<script type="text/javascript">
      var characterName = "my name is Simpson,  Homer";
      var firstNameIndex = characterName.indexOf( "Simpson," ) + 9;
      var firstName = characterName.substring( firstNameIndex );
      document.write( firstName );
</script>
</body>
</html>

您将从变量 characterName 的字符串中提取 Homer,使用 indexOf()查找姓氏的开头,并向其中添加 9 以获得名字的开头的索引(因为“Simpson”有 9 个字符长),并将它存储在 firstNameIndex 中。substring()方法使用它来提取从名字开始的所有内容—您还没有指定最后的索引,所以将返回字符串中的其余字符。

现在让我们看看日期对象。这允许您存储日期,并提供一些有用的日期/时间相关功能。

日期对象

JavaScript 没有原始的日期数据类型,所以只能显式地创建日期对象。创建新的 Date 对象的方法与创建 String 对象的方法相同,使用 new 关键字和 Date()构造函数。这一行创建一个包含当前日期和时间的 Date 对象:

var todaysDate = new Date();

要创建存储特定日期或时间的 Date 对象,只需将日期或日期和时间放在括号内:

var newMillennium = new Date( "1 Jan 2000 10:24:00" );

不同的国家以不同的顺序描述日期。例如,在美国,日期用 MM/DD/YY 表示,而在欧洲,日期是 DD/MM/YY ,在中国是 YY/MM/DD 。如果使用缩写名称指定月份,则可以使用任何顺序:

var someDate = new Date( "10 Jan 2013" );
var someDate = new Date( "Jan 10 2013" );
var someDate = new Date( "2013 10 Jan" );

事实上,日期对象可以接受许多参数:

var someDate = new Date( aYear,  aMonth,  aDate,
anHour,  aMinute,  aSecond, aMillisecond )

要使用这些参数,您首先需要指定年份和月份,然后使用您想要的参数——尽管您必须按顺序运行它们,并且不能从中进行选择。例如,您可以指定年、月、日和小时:

var someDate = new Date( 2013,  9,  22,  17 );

但是,您不能指定年、月和小时:

var someDate = new Date( 2013,  9,  ,  17 );

image 注意虽然你通常认为 9 月是 9 月,但是 JavaScript 从 0(1 月)开始计算月份,所以 9 月被表示为 8 月。

使用日期对象

Date 对象有很多方法可以用来获取或设置日期或时间。您可以使用本地时间(您所在时区的电脑时间)或 UTC(协调世界时,曾被称为格林威治标准时间)。虽然这可能非常有用,但您需要注意的是,当您使用 Date 时,许多人没有正确设置他们的时区。

让我们看一个演示一些方法的例子:

<html>
<body>
<script type="text/javascript">
      // Create a new date object
      var someDate = new Date( "31 Jan 2013 11:59" );
      // Retrieve the first four values using the
      // appropriate get methods
      document.write( "Minutes = " + someDate.getMinutes() + "<br>" );
      document.write( "Year = " + someDate.getFullYear() + "<br>" );
      document.write( "Month = " + someDate.getMonth() + "<br>" );
      document.write( "Date = " + someDate.getDate() + "<br>" );
      // Set the minutes to 34
      someDate.setMinutes( 34 );
      document.write( "Minutes = " + someDate.getMinutes() + "<br>" );
      // Reset the date
      someDate.setDate( 32 );
      document.write( "Date = " + someDate.getDate() + "<br>" );
      document.write( "Month = " + someDate.getMonth() + "<br>" );
</script>
</body>
</html>

以下是您应该获得的内容:

Minutes = 59
Year = 2013
Month = 0
Date = 31
Minutes = 34
Date = 1
Month = 1

这行代码乍一看可能有点违反直觉:

someDate.setDate( 32 );

JavaScript 知道一月没有 32 天,所以解释器没有试图将日期设置为 1 月 32 日,而是从 1 月 1 日开始计算 32 天,这样我们就得到了 2 月 1 日。

如果您需要在日期上添加天数,这可能是一个方便的功能。通常,如果您想给日期加上天数,您必须考虑不同月份的天数,以及是否是闰年,但是使用 JavaScript 对日期的理解要容易得多:

<html>
<body>
<script type="text/javascript">
      // Ask the user to enter a date string
      var originalDate = prompt("Enter a date (Day, Name of the Month, Year"), "31 Dec 2013" );
      // Overwrite the originalDate variable with a new Date
      // object
      var originalDate = new Date( originalDate );
      // Ask the user to enter the number of days to be
      // added, and convert to number
      var addDays = Number( prompt( "Enter number of daysto be added", "1" ) )
      // Set a new value for originalDate of originalDate
      // plus the days to be added
      originalDate.setDate( originalDate.getDate( ) + addDays )
      // Write out the date held by the originalDate
      // object using the toString( ) method
      document.write( originalDate.toString( ) )
</script>
</body>
</html>

如果系统提示您输入 2013 年 12 月 31 日,并输入 1 表示要添加的天数,您将得到的答案是 2014 年 1 月 1 日星期四 00:00:00 UTC。

image 注意注意你在脚本的第三行使用了数学对象的 Number()方法。如果你不这样做,程序仍然会运行,但结果不会一样。如果您不想使用该方法,有一个技巧可以转换不同的数据类型:如果您从一个可以使用 parseInt()、parseFloat()或 number()转换为数字的字符串中减去 0,则将其转换为数字,如果您将一个空字符串“”添加到一个数字中,则将其转换为字符串,这是您通常使用 toString()所做的事情。

在第四行,您将日期设置为当月的当天,这是 originalDate.getDate()返回的值加上要添加的天数;接下来是计算,最后一行使用 toString()方法将 date 对象中包含的日期输出为字符串。此外,toDateString()仅使用日期生成一个格式良好的字符串。如果使用 UTC 时间,您可以使用相同的 get 和 set 方法——您需要做的只是在方法名中添加 UTC 。所以 getHours()变成了 getUTCHours(),setMonth()变成了 setUTCMonth(),以此类推。还可以使用 getTimezoneOffset()方法返回计算机的本地时间和 UTC 时间之间的差值(以小时为单位)。(你必须依赖用户正确设置他们的时区,并了解不同国家之间夏令时的差异。)

image 注意对于关键的日期操作,JavaScript 可能不是正确的技术,因为你不能相信客户端计算机被正确设置。但是,您可以通过服务器端语言填充 JavaScript 的初始日期,并从那里开始。

数学对象

Math 对象为您提供了许多数学功能,如求一个数的平方或产生一个随机数。Math 对象在两个方面不同于 Date 和 String 对象:

  • 你不能显式地创建一个数学对象,你只能直接使用它。
  • 与 String 和 Date 对象不同,Math 对象不存储数据。

使用以下格式调用 Math 对象的方法:

Math.methodOfMathObject( aNumber ):
alert( "The value of pi is " + Math.PI );

接下来我们将看看一些常用的方法。(你可以在developer.mozilla.org/en-US/docs/Web_Development搜索找到完整的参考资料。)我们将在这里看一下舍入数字和生成随机数的方法。

舍入数字

您在前面已经看到 parseInt()函数将通过删除小数点后的所有内容来使一个分数变成整数(因此 24.999 变成了 24)。通常,您会想要更精确的数学计算—例如,如果您正在进行财务计算—对于这些计算,您可以使用 Math 对象的三个舍入函数之一:round()、ceil()和 floor()。他们是这样工作的:

  • round():当小数为时,将数字向上舍入。5 或更高
  • ceil()(如 ceiling ):总是向上取整,所以 23.75 变成 24,23.25 也一样
  • floor():总是向下舍入,因此 23.75 变成 23,23.25 也是如此

下面是一个简单的例子:

<html>
<body>
<script type="text/javascript">
      var numberToRound = prompt( "Please enter a number", "" )
      document.write( "round( ) = " + Math.round( numberToRound ) );
      document.write( "<br>" );
      document.write( "floor( ) = " + Math.floor( numberToRound ) );
document.write( "<br>" );
      document.write( "ceil( ) = " + Math.ceil( numberToRound ) );
</script>
</body>
</html>

即使您使用 prompt()从用户那里获得一个值,正如您在前面看到的,它返回一个字符串,返回的数字仍然被视为一个数字。这是因为只要字符串包含可以转换为数字的内容,舍入方法就会为您进行转换。

如果输入 23.75,将得到以下结果:

round() = 24
floor() = 23
ceil() = 24

如果你输入–23.75,你会得到

round() = -24
floor() = -24
ceil() = -23

生成一个随机数

您可以使用 Math 对象的 random()方法生成大于等于 0 但小于 1 的分数随机数。通常,您需要将数字相乘,然后使用一种舍入方法使其变得有用。

例如,要模拟掷骰子,您需要生成一个介于 1 和 6 之间的随机数。您可以通过将随机分数乘以 6,得到一个介于 0 和 6 之间的分数,然后使用 floor()方法将该数字向下舍入为一个整数。代码如下:

<html>
<body>
<script type="text/javascript">
      var diceThrow = Math.floor( Math.random( ) * 6 ) + 1;
      document.write( "You threw a " + diceThrow );
</script>
</body>
</html>

数组

JavaScript 允许您使用一个数组来存储和访问相关数据。数组有点像一排盒子( 元素 ),每个盒子包含一项数据。数组可以处理 JavaScript 支持的任何数据类型。例如,您可以使用数组来处理用户将从中选择的项目列表,或者一组图形坐标,或者引用一组图像。

数组对象,如字符串和日期对象,是使用 new 关键字和构造函数创建的。您可以在创建数组对象时对其进行初始化:

var preInitArray = new Array( "First item", "Second item", "Third Item" );

或者您可以将其设置为保存一定数量的项目:

var preDeterminedSizeArray = new Array( 3 );

或者您可以创建一个空数组:

var anArray = new Array();

通过给元素赋值,可以向数组中添加新项:

anArray[0] = "anItem";
anArray[1] = "anotherItem"
anArray[2] = "andAnother"

image 注意你不必使用 array()构造函数;相反,使用快捷符号是完全有效的。

var myArray = [1, 2, 3];
var yourArray = ["red", "blue", "green"];

一旦填充了数组,就可以使用方括号通过元素的索引或位置(同样是从零开始的)来访问它的元素:

<html>
<body>
<script type="text/javascript">
      var preInitArray = new Array( "First Item" "Second Item", "Third Item" );
      document.write( preInitArray[0] + "<br>" );
      document.write( preInitArray[1] + "<br>" );
      document.write( preInitArray[2] + "<br>" );
</script>
</body>
</html>

如果你想在数组中循环,使用索引号来存储条目是很有用的——我们接下来将讨论循环。

您可以通过使用关键字并给它们赋值来创建关联的数组(在其他语言中称为散列,如下所示:

<html>
<body>
<script type="text/javascript">
      // Creating an array object and setting index
      // position 0 to equal the string Fruit
      var anArray = new Array( );
      anArray[0] = "Fruit";
      // Setting the index using the keyword
      // 'CostOfApple' as the index.
      anArray["CostOfApple"] = 0.75;
      document.write( anArray[0] + "<br>" );
      document.write( anArray["CostOfApple"] );
</script>
</body>
</html>

关键字适用于为数据提供有用标签的情况,或者存储仅在上下文中有意义的条目的情况,如图表坐标列表。但是,如果条目是使用关键字设置的,就不能使用索引号来访问它们(在其他一些语言中可以,比如 PHP)。您也可以为索引使用变量。您可以使用变量(一个保存字符串,另一个保存数字)代替文字值来重写前面的示例:

<html>
<body>
<script type="text/javascript">
      var anArray = new Array( );
      var itemIndex = 0;
      var itemKeyword = "CostOfApple";
      anArray[itemIndex] = "Fruit";
      anArray[itemKeyword] = 0.75;
      document.write( anArray[itemIndex] + "<br>" );
      document.write( anArray[itemKeyword] );
</script>
</body>
</html>

让我们把我们讨论过的数组和数学对象放入一个例子中。我们将编写一个脚本,随机选择一个横幅显示在页面顶部。

我们将使用一个数组对象来保存一些图像源名称,如下所示:

var bannerImages = new Array();
bannerImages[0] = "Banner1.jpg";
bannerImages[1] = "Banner2.jpg";
bannerImages[2] = "Banner3.jpg";
bannerImages[3] = "Banner4.jpg";
bannerImages[4] = "Banner5.jpg";
bannerImages[5] = "Banner6.jpg";
bannerImages[6] = "Banner7.jpg";

然后,您需要七个具有相应名称的图像,与 HTML 页面放在同一个文件夹中。你可以用你自己的或者从 http://www.beginningjavascript.com 下载我的。

接下来,您将初始化一个新变量 randomImageIndex,并使用它来生成一个随机数。您将使用与前面生成随机掷骰子相同的方法,但不在结果上加 1,因为您需要一个从 0 到 6 的随机数:

var randomImageIndex = Math.round( Math.random( ) * 6 );

然后,您将使用 document.write()将随机选择的图像写入页面。以下是完整的脚本:

<html>
<body>
<script type="text/javascript">
      var bannerImages = new Array( );
      bannerImages[0] = "Banner1.jpg";
      bannerImages[1] = "Banner2.jpg";
      bannerImages[2] = "Banner3.jpg";
      bannerImages[3] = "Banner4.jpg";
      bannerImages[4] = "Banner5.jpg";
      bannerImages[5] = "Banner6.jpg";
      bannerImages[6] = "Banner7.jpg";
      var randomImageIndex = Math.round( Math.random( ) * 6 );
      document.write( "<img alt=\"\" src=\"" + bannerImages[randomImageIndex] + "\">" );
</script>
</body>
</html>

这就是全部了。与每次用户访问页面时都显示相同的横幅相比,更改横幅会让访问者更容易注意到它,当然,这会给人一种站点经常更新的印象。

数组对象的方法和属性

Array 对象最常用的属性之一是 length 属性,,它返回比数组中最后一个数组项的索引高一个计数的索引。例如,如果您正在处理一个包含索引为 0、1、2、3 的元素的数组,那么长度将为 4,这对于了解您是否想要添加另一个元素非常有用。

Array 对象提供了许多操作数组的方法,包括从数组中剪切许多项或将两个数组连接在一起的方法。接下来,我们将看看连接、切片和排序的方法。

切割数组的一部分

slice()方法对于数组对象就像 substring()方法对于字符串对象一样。您只需告诉该方法您想要对哪些元素进行切片。这很有用,例如,如果您想要分割使用 URL 传递的信息。

slice()方法接受两个参数:切片的第一个元素的索引(将包含在切片中)和最后一个元素的索引(不会包含在切片中)。要从总共包含五个值的数组中访问第二、第三和第四个值,可以使用索引 1 和 4:

<html>
<body>
<script type="text/javascript">
      // Create and initialize the array
      var fullArray = new Array( "One", "Two", "Three","Four", "Five" );
      // Slice from element 1 to element 4 and store
      // in new variable sliceOfArray
      var sliceOfArray = fullArray.slice( 1, 4 );
      // Write out new ( zero-based ) array of 3 elements
      document.write( sliceOfArray[0] + "<br>" );
      document.write( sliceOfArray[1] + "<br>" );
      document.write( sliceOfArray[2] + "<br>" );
</script>
</body>
</html>

新数组将数字存储在一个新的从零开始的数组中,因此对索引 0、1 和 2 进行切片得到如下结果:

Two
Three
Four

原始数组不受影响,但是如果需要,可以通过将变量中的 array 对象设置为 slice()方法的结果来覆盖它:

fullArray = fullArray.slice( 1, 4 );

连接两个数组

数组对象的 concat()方法允许你连接数组。您可以使用此方法添加两个或更多数组,每个新数组都从上一个数组结束的位置开始。这里你加入了三个数组:arrayOne、arrayTwo 和 arrayThree:

<html>
<body>
<script type="text/javascript">
      var arrayOne = new Array( "One", "Two", "Three","Four", "Five" );
      var arrayTwo = new Array( "ABC", "DEF", "GHI" );
      var arrayThree = new Array( "John", "Paul", "George","Ringo" );
      var joinedArray = arrayOne.concat( arrayTwo, arrayThree );
      document.write( "joinedArray has " + joinedArray.length + " elements<br>" );
      document.write( joinedArray[0] + "<br>" )
      document.write( joinedArray[11] + "<br>" )
</script>
</body>
</html>

新数组 joinedArray 有 12 项。此数组中的项目与它们在以前每个数组中的项目相同;它们只是简单地连接在一起。原始数组保持不变。

将数组转换为字符串,然后再转换回来

当您想遍历数组或选择某些元素时,将数据放在数组中会很方便。但是,当您需要将数据发送到其他地方时,您可能应该将该数据转换为字符串。可以通过遍历数组并将每个元素值添加到一个字符串中来实现。然而,并不需要这样做,因为数组对象有一个名为 join()的方法可以为您完成这项工作。该方法将字符串作为参数。该字符串将被添加到每个元素之间。

<script type="text/javascript">
  var arrayThree = new Array( "John", "Paul", "George","Ringo" );
  var lineUp=arrayThree.join( ',  ' );
  alert( lineUp );
</script>

结果字符串,阵容,具有值“约翰,保罗,乔治,林戈”。join()的反义词是 split(),这是一种将字符串转换为数组的方法。

<script type="text/javascript">
  var lineUp="John,  Paul,  George,  Ringo";
  var members=lineUp.split( ',  ' );
  alert( members.length );
</script>

对数组排序

sort()方法允许你将数组中的条目按字母或数字顺序排序:

<html>
<body>
<script type="text/javascript">
      var arrayToSort = new Array( "Cabbage", "Lemon","Apple", "Pear", "Banana" );
      arrayToSort.sort( );
      document.write(arrayToSort[0] + "<br>" );
      document.write(arrayToSort[1] + "<br>" );
      document.write(arrayToSort[2] + "<br>" );
      document.write(arrayToSort[3] + "<br>" );
      document.write(arrayToSort[4] + "<br>" );
</script>
</body>
</html>

项目排列如下:

Apple
Banana
Cabbage
Lemon
Pear

然而,如果你降低其中一个字母的大小写,例如,苹果A ,那么你会得到一个非常不同的结果。排序是严格数学化的——根据 ASCII 集中字符的数量,而不是像人类那样对单词进行排序。

如果要更改排序元素的显示顺序,可以使用 reverse()方法将字母表中的最后一个元素显示为第一个元素:

<script type="text/javascript">
  var arrayToSort = new Array( "Cabbage", "Lemon", "Apple", "Pear", "Banana" );
   arrayToSort.sort( );
sortedArray.reverse( );
  document.write(sortedArray[0] + "<br>" );
  document.write(sortedArray[1] + "<br>" );
  document.write(sortedArray[2] + "<br>" );
  document.write(sortedArray[3] + "<br>" );
  document.write(sortedArray[4] + "<br>" );
</script>

现在,结果列表的顺序相反:

Pear
Lemon
Cabbage
Banana
Apple

用 JavaScript 做决策

决策赋予程序明显的智能。没有它你就写不出一个好的程序,无论你是在创建一个游戏,检查一个密码,给用户一组基于他之前所做决定的选择,还是其他什么。

决策是基于条件语句的,条件语句就是评估为真或假的简单语句。这就是原始布尔数据类型有用的地方。循环是决策制定的另一个重要工具,例如,它使您能够遍历用户输入或数组,并相应地做出决策。

逻辑和比较运算符

我们将了解两组主要的运算符:

  • 数据比较运算符:比较操作数并返回布尔值
  • 逻辑运算符:测试多个条件

我们将从比较运算符开始。

比较数据

表 2-3 列出了一些更常用的比较运算符。

表 2-3。JavaScript 中的比较

操作员 描述 例子
== 检查左右操作数是否相等 123 == 234 返回 false.123 = = 123 返回 true。
=== 检查左右操作数是否相等,以及数据类型是否相等 123 ==="234 "返回 false.123 === 123 返回 true。
!= 检查左操作数是否不等于右操作数 123 != 123 返回 false.123!= 234 返回 true。
> 检查左操作数是否大于右操作数 123 > 234 返回 false.234 > 123 返回 true。
>= 检查左操作数是否大于或等于右操作数 123 >= 234 返回 false.123 > = 123 返回 true。
< 检查左操作数是否小于右操作数 234 < 123 返回 false.123 < 234 返回 true。
<= 检查左操作数是否小于或等于右操作数 234 <= 123 返回 false.234 < = 234 返回 true。

image 注意当心==等号运算符:在脚本中错误地使用赋值运算符=,很容易产生错误。

这些运算符都处理字符串类型数据和数值数据,并且区分大小写:

<html>
<body>
<script type="text/javascript">
document.write("Apple" == "Apple" )
      document.write("<br>" );
      document.write("Apple"<"Banana" )
      document.write("<br>" );
      document.write("apple"<"Banana" )
</script>
</body>
</html>

这是你应该得到的回报:

true
true
false

当对比较字符串的表达式求值时,JavaScript 解释器依次比较两个字符串中每个字符的 ASCII 码——每个字符串的第一个字符,然后是第二个字符,依此类推。大写的 A 在 ASCII 中用数字 65 表示, B 用 66 表示, C 用 67 表示,以此类推。为了评估表达式“Apple”<“Banana”,JavaScript 解释器通过用 ASCII 代码替换每个字符串中的第一个字符来测试比较:65 < 66,所以 A 先排序,比较为真。在测试表达式“apple”<“Banana”时,JavaScript 解释器也是这么做的;但小写字母 a 的 ASCII 码为 97,所以表达式“a”<“B”就简化为 97 < 66,为假。可以使用<、< =、>、> =运算符进行字母比较。如果需要确保所有字母大小写一致,可以使用 String 对象的 toUpperCase()和 toLowerCase()方法。比较运算符就像数值运算符一样,可以用于变量。如果你想按字母顺序比较苹果和香蕉,你可以这样做:

<html>
<body>
<script type="text/javascript">
      var string1 = "apple";
      var string2 = "Banana";
      string1 = string1.toLowerCase( );
      string2 = string2.toLowerCase( );
      document.write( string1 < string2 )
</script>
</body>
</html>

不过,在使用等式运算符比较字符串对象时,还需要注意其他一些事情。试试这个:

<html>
<body>
<script type="text/javascript">
      var string1 = new String( "Apple" );
      var string2 = new String( "Apple" );
      document.write( string1 == string2 )
</script>
</body>
</html>

你会得到错误的回报。事实上,我们在这里所做的是比较两个字符串对象而不是两个字符串原语的字符,正如返回的 false 所表明的,两个字符串对象不可能是同一个对象,即使它们持有相同的字符。

如果确实需要比较两个对象持有的字符串,可以使用 valueOf()方法来执行数据值的比较:

<html>
<body>
<script type="text/javascript">
      var string1 = new String( "Apple" );
      var string2 = new String( "Apple" );
      document.write( string1.valueOf() == string2.valueOf() );
</script>
</body>
</html>

逻辑运算符

有时您需要将比较组合到一个条件组中。您可能希望检查用户提供的信息是否有意义,或者根据他们之前的回答限制他们可以做出的选择。你可以使用表 2-4 中所示的逻辑运算符来实现。

表 2-4 。JavaScript 中的逻辑运算符

Table2-4.jpg

一旦你评估了数据,你需要能够根据结果做出决定。这就是条件语句和循环有用的地方。你会发现我们在这一章中看到的操作符最常用于条件语句或循环的上下文中。

条件语句

如果...else 结构用于测试条件,如下所示:

if ( condition ) {
// Execute code in here if condition is true
} else {
// Execute code in here if condition is false
}
// After if/else code execution resumes here

如果被测试的条件为真,If 后面的花括号中的代码将被执行,否则不会执行。如果满足 if not 中设定的条件,您还可以使用最终的 else 语句创建一个要执行的代码块。

让我们改进您在本章前面构建的货币兑换转换器,并创建一个循环来处理来自用户的非数字输入:

<html>
<body>
<script type="text/javascript">
      var euroToDollarRate = 0.872;
      // Try to convert the input into a number
      var eurosToConvert = Number( prompt( "How many Euros do you wish to convert", "" ) );
      // If the user hasn't entered a number,  then NaN
      // will be returned
      if ( isNaN( eurosToConvert ) ) {
        // Ask the user to enter a value in numerals
        document.write( "Please enter the number in numerals" );
        // If NaN is not returned,  then we can use the input
      } else {
        // and do the conversion as before
        var dollars = eurosToConvert * euroToDollarRate;
        document.write( eurosToConvert + " euros is " + dollars + " dollars" );
      }
</script>
</body>
</html>

if 语句使用的是 isNaN()函数,如果变量 eurosToConvert 中的值不是数字,该函数将返回 true。

image 注意记得尽可能礼貌和有帮助地保存错误消息。好的错误消息清楚地告诉用户对他们的期望,使得使用应用更加容易。

通过使用逻辑运算符和嵌套 if 语句,可以创建更复杂的条件:

<html>
<body>
<script type="text/javascript">
      // Ask the user for a number and try to convert the
      // input into a number
      var userNumber = Number( prompt( "Enter a number between 1 and 10", "" ) );
        // If the value of userNumber is NaN,  ask the user
        // to try again
      if ( isNaN( userNumber ) ) {
        document.write( "Please ensure a valid number is entered" );
        // If the value is a number but over 10, ask the
        //user to try again
      } else {
        if ( userNumber > 10 || userNumber < 1 ) {
          document.write( "The number you entered is notbetween 1 and 10" );
        // Otherwise the number is between 1 and 10, so
        // write to the page
        } else {
          document.write( "The number you entered was " + userNumber );
        }
      }
</script>
</body>
</html>

你知道这个数字是可以的,只要它是一个数值并且小于 10。

image 注意观察代码的布局。您缩进了 if 和 else 语句和代码块,以便于阅读和查看代码块的开始和结束位置。让你的代码尽可能清晰是很重要的。

试着在没有缩进或空格的情况下阅读这段代码:

<html>
<body>
<script type="text/javascript">
// Ask for a number using the prompt() function and try to make it a number
var userNumber = Number(prompt("Enter a number between 1 and 10",""));
// If the value of userNumber is NaN, ask the user to try again
if (isNaN(userNumber)){
document.write("Please ensure a valid number is entered");
}
// If the value is a number but over 10, ask the user to try again
else {
if (userNumber > 10 || userNumber < 1) {
document.write("The number you entered is not between 1 and 10");
}
// Otherwise the number is between 1 and 10, so write to the screen
else{
document.write("The number you entered was " + userNumber);
}
}
</script>
</body>
</html>

不是不能读,但是即使在这个简短的脚本中,也很难破译哪些代码块属于 if 和 else 语句。在较长的代码中,不一致的缩进或不合逻辑的缩进会使代码非常难以阅读,这反过来会让您有更多的 bug 需要修复,并使您的工作变得不必要的困难。

您也可以使用 else if 语句,其中 else 语句以另一个 if 语句开始,如下所示:

<html>
<body>
<script type="text/javascript">
      var userNumber = Number( prompt( "Enter a number between1 and 10", "" ) );
      if ( isNaN( userNumber ) ){
        document.write( "Please ensure a valid number isentered" );
      } else if ( userNumber > 10 || userNumber < 1 ) {
        document.write( "The number you entered is notbetween 1 and 10" );
      } else {
        document.write( "The number you entered was " +userNumber );
      }
</script>
</body>
</html>

该代码与前面的代码做了同样的事情,但是它使用了一个 else if 语句,而不是嵌套的 if 语句,并且比前面的代码短了两行。

分支或环的断裂

在我们继续之前,还有一件事:您可以使用 break 语句来中断条件语句或循环。这只是终止代码块的运行,并将处理过程转到下一条语句。我们将在下一个例子中使用它。

你可以有尽可能多的 if、else 和 else ifs,尽管如果你使用太多,它们会使你的代码变得非常复杂。如果在一段代码中有许多可能的条件来检查一个值,那么接下来您将看到的 switch 语句会很有帮助。

测试多个值:switch 语句

switch 语句允许您根据变量或表达式的值在代码段之间切换。这是 switch 语句的概要:

switch( expression ) {
  case someValue:
  // Code to execute if expression == someValue;
  break; // End execution
  case someOtherValue:
  // Code to execute if expression == someOtherValue;
  break; // End execution
  case yesAnotherValue:
  // Code to execute if expression == yetAnotherValue;
  break; // End execution
  default:
  // Code to execute if no values matched
}

JavaScript 对 switch(表达式)求值,然后与每种情况进行比较。一旦找到匹配,代码就从该点开始执行,并继续执行所有 case 语句,直到找到断点。包含一个默认的 case 通常很有用,如果没有 case 语句匹配,这个 case 就会执行。这是发现错误的有用工具,例如,您希望出现匹配,但是一个 bug 阻止了它的出现。

例如,事例的值可以是任何数据类型、数字或字符串。您可以有一个箱子,也可以根据需要有多个箱子。让我们看一个简单的例子:

<html>
<body>
<script type="text/javascript">
      // Store user entered number between 1 and 4 in userNumber
      var userNumber = Number( prompt( "Enter a number between 1 and 4", "" ) );
      switch( userNumber ) {
        // If userNumber is 1, write out and carry on
        // executing after case statement
        case 1:
          document.write( "Number 1" );
        break;
        case 2:
          document.write( "Number 2" );
        break;
        case 3:
          document.write( "Number 3" );
        break;
        case 4:
          document.write( "Number 4" );
        break;
        default:
          document.write( "Please enter a numeric value between 1 and 4." );
        break;
      }
      // Code continues executing here
</script>
</body>
</html>

试试看。你只需要把你输入的数字写出来,或者句子“请输入一个 1 到 4 之间的数值。”

这个例子也说明了 break 语句的重要性。如果在每个 case 后不包含 break,则执行将在块内继续,直到切换结束。尝试删除分隔符,然后输入 2。匹配之后的所有内容都将执行,并给出以下输出:

Number 2Number 3Number 4Please enter a numeric value between 1 and 4

您可以在 switch 语句中使用任何有效的表达式,例如计算:

switch( userNumber * 100 + someOtherVariable )

在 case 语句之间还可以有一个或多个语句。

重复的事情:循环

在这一节中,我们来看看如何在设定的条件为真的情况下重复一段代码。例如,您可能希望遍历 HTML 表单上的每个 input 元素或数组中的每个项。

重复设定的次数:循环的

**for 循环旨在多次循环通过一个代码块,如下所示:

for( initial-condition; loop-condition; alter-condition ) {
  //
  // Code to be repeatedly executed
  //
}
// After loop completes,  execution of code continues here

与条件语句一样,for 关键字后面跟有括号。这一次,括号包含由分号分隔的三个部分。

第一部分初始化一个变量,该变量将作为计数器来记录循环次数。第二部分测试条件。只要这个条件为真,循环就会一直运行。在每次循环之后,最后一部分或者递增或者递减在第一部分中创建的计数器。(正如您将看到的,它是 after 的事实是编程中的一个重要因素。)

例如,看看一个循环,只要 loopCounter 小于 10,它就会一直运行:

for( loopCounter = 1; loopCounter <= 10; loopCounter++ )

只要循环条件的计算结果为真,循环就会一直执行,只要 loopCounter 小于或等于 10。一旦达到 11,循环就会停止,代码会在循环的右括号后的下一条语句处继续执行。

让我们看一个使用 for 循环遍历数组的例子。您将使用 for 循环来遍历名为 theBeatles 的数组,使用名为 loopCounter 的变量来保持循环运行,同时 loopCounter 的值小于数组的长度:

<html>
<body>
<script type="text/javascript">
      var theBeatles = new Array( "John", "Paul", "George","Ringo" );
      for ( var loopCounter = 0; loopCounter < theBeatles.length; loopCounter++ ) {
        document.write( theBeatles[loopCounter] + "<br>" );
      }
</script>
</body>
</html>

此示例之所以有效,是因为您使用了一个从零开始的数组,其中的项已按顺序添加到索引中。如果您使用关键字在如下数组中存储项目,则循环不会运行:

theBeatles["Drummer"] = "Ringo";

早些时候,当我讨论数组时,我说过数组对象有一个知道长度(有多少个元素)的属性。在遍历数组时,比如在前面的例子中,您使用数组名称后跟一个点和长度作为条件。这可以防止循环计数超过数组长度,否则会导致“越界”错误。

JavaScript 也支持 for..in loop(从 Netscape Navigator 2 开始就有了,尽管 Internet Explorer 从 IE5 开始才支持它)。的不使用计数器...in 循环使用变量访问数组,遍历数组中的每一项。让我们以这种方式创建一个数组,看看它是如何工作的:

<html>
<body>
<script type="text/javascript">
      // Initialize theBeatles object and store in a variable
      var theBeatles = new Object( );
      // Set the values using keys rather than numbers
      theBeatles["Drummer"] = "Ringo";
      theBeatles["SingerRhythmGuitar"] = "John";
      theBeatles["SingerBassGuitar"] = "Paul";
      theBeatles["SingerLeadGuitar"] = "George";
      var indexKey;
      // Write out each indexKey and the value for that
      // indexKey from the array
      for ( indexKey in theBeatles ) {
        document.write( "indexKey is " + indexKey + "<br>" );
        document.write( "item value is " + theBeatles[indexKey] + "<br><br>" );
      }
</script>
</body>
</html>

在循环的每次迭代中,indexKey 中的项键的结果与使用该键从数组中提取的值一起写出,其顺序与该键在数组中出现的顺序相同:

indexKey is Drummer
item value is Ringo

indexKey is SingerRhythmGuitar
item value is John

indexKey is SingerBassGuitar
item value is Paul

indexKey is SingerLeadGuitar
item value is George

根据决策重复动作:while 循环

到目前为止,您一直在处理的循环从脚本本身内部接受停止循环的指令。有时你可能会希望用户决定何时停止循环,或者当满足某个用户主导的条件时,你希望何时停止循环。一会儿做...而循环就是为这种情况设计的。

最简单的 while 循环如下所示:

while ( some condition true ) {
  // Loop code
}

花括号中的条件可以是 if 语句中使用的任何条件。您可以使用类似这样的代码来允许用户输入数字,并通过键入数字 99 来停止输入过程。

<html>
<body>
<script type="text/javascript">
      var userNumbers = new Array( );
      var userInput = 0;
      var arrayIndex = 0;
      var message = ""
      var total = 0;
      // Loop for as long as the user doesn't input 99
      while ( userInput != 99 ) {
        userInput = prompt( "Enter a number,  or 99 to exit", "99" );
        userNumbers[arrayIndex] = userInput;
        arrayIndex++;
      }
      message += 'You entered the following:\n';
      for ( var i = 0; i < arrayIndex-1; i++ ) {
        message += userNumbers[i] + '\n';
        total += Number( userNumbers[i] );
}
      message += 'Total: ' + total + '\n';
      alert( message );
</script>
</body>
</html>

这里 while 循环的条件是 userInput 不等于 99,所以只要条件为真,循环就会继续。当用户输入 99 并且条件被测试时,它将评估为假并且循环将结束。注意,当用户输入 99 时,循环不会立即结束,而是在循环的另一次迭代开始时再次测试条件。

while 循环和 do 循环之间有一个很小但很重要的区别...while:while 循环在执行代码之前测试条件,只有当条件为真时才执行代码块,而 do...while 循环在测试条件之前执行代码块,仅当条件为真时才进行另一次迭代。简而言之,do...当您知道希望循环代码在测试条件之前至少执行一次时,while loop 非常有用。你可以用 do 写前面的例子...while 像这样循环:

<html>
<body>
<script type="text/javascript">
      var userNumbers = new Array( );
      var message = '';
      var total = 0;
      // Declare the userInput but don't initialize it
      var userInput;
      var arrayIndex = 0;
      do {
        userInput = prompt( "Enter a number,  or 99 to exit", "99" );
        userNumbers[arrayIndex] = userInput;
        arrayIndex++;
      } while ( userInput != 99 )
      message +='You entered the following:\n';
      for ( var i = 0; i < arrayIndex-1; i++ ) {
        message += userNumbers[i] + '\n';
        total += Number( userNumbers[i] );
}
      message += 'Total: ' + total + '\n';
      alert( message );
</script>
</body>
</html>

您不需要初始化 userInput,因为循环内部的代码在第一次测试它之前为它设置了一个值。

继续循环

正如您已经看到的,一旦某个事件发生,break 语句对于跳出任何类型的循环都非常有用。continue 关键字的作用类似于 break,因为它停止循环的执行。然而,continue 并没有退出循环,而是在下一次迭代中继续执行。

让我们首先修改前面的例子,这样如果用户输入的不是数字,就不会记录值,循环结束,使用 break:

<html>
<body>
<script type="text/javascript">
      var userNumbers = new Array( );
      var userInput;
      var arrayIndex = 0;
      do {
        userInput = Number( prompt( "Enter a number, or 99 to exit", "99" ) );
        // Check that user input is a valid number,
        // and if not,  break with error msg
        if ( isNaN( userInput ) ) {
          document.write( "Invalid data entered: please enter a number between 0 and 99 in numerals" );
          break;
        }
        // If break has been activated,  code will continue from here
        userNumbers[arrayIndex] = userInput;
        arrayIndex++;
      } while ( userInput != 99 )
      // Next statement after loop
</script>
</body>
</html>

现在让我们再次修改它,这样您就不会中断循环,而是忽略用户的输入并继续循环,使用 continue 语句:

<html>
<body>
<script type="text/javascript">
      var userNumbers = new Array( );
      var userInput;
      var arrayIndex = 0;
      do {
        userInput = prompt( "Enter a number, or 99 to exit", "99" );
        if ( isNaN( userInput ) ) {
          document.write( "Invalid data entered: please enter a number between 0 and 99 in numerals " );
          continue;
        }
        userNumbers[arrayIndex] = userInput;
        arrayIndex++;
      } while ( userInput != 99 )
      // Next statement after loop
</script>
</body>
</html>

break 语句已被 continue 替换,因此循环中将不再执行任何代码,并将再次计算 while 语句中的条件。如果条件为真,将发生循环的另一次迭代;否则,循环结束。

使用以下经验法则来决定使用哪种循环结构:

  • 如果要将一个动作重复设定的次数,请使用 for 循环。
  • 当您想要重复某个动作直到满足某个条件时,请使用 while 循环。
  • 使用 do...如果希望保证操作至少执行一次,请使用 while 循环。

摘要

我们在这一章中涉及了很多内容:事实上,我们讨论了 JavaScript 语言的大部分基本要素。

您了解了 JavaScript 如何处理数据,并且看到了有许多数据类型:string、number、Boolean 和 object,以及一些特殊类型的数据,如 NaN、null 和 undefined。您已经看到 JavaScript 提供了许多对数据执行操作的操作符,比如数字计算或连接字符串。

然后我们看了 JavaScript 如何允许您使用变量存储值。变量在页面的生存期内有效,如果变量在用户创建的函数中并通过 var 关键字在本地声明,则在函数的生存期内有效。您还了解了如何将一种数据类型转换成另一种数据类型。

接下来,您使用了三个 JavaScript 内置对象:字符串、日期和数学对象。您看到了这些为操作字符串、日期和数字提供了有用的功能。我还向您展示了 Array 对象,它允许在单个变量中存储多项数据。

我们通过查看决策来结束这一章,决策提供了编程语言的逻辑或智能。您使用 if 和 switch 语句进行决策,使用条件测试数据的有效性,并根据结果采取行动。循环还使用条件,并允许您重复一定次数的代码块,或者当条件为真时。***

三、从 DHTML 到 DOM 脚本

在这一章中,你将学习什么是 DHTML,为什么它现在被认为是一种不好的方式,以及应该用什么样的现代技术和思想来代替。您将了解什么是功能以及如何使用它们。您还将了解变量和函数作用域,以及一些最新的最佳实践,这些实践将教会您的脚本如何很好地与他人合作。

如果你对 JavaScript 感兴趣,并且已经在网上搜索过脚本,你肯定会遇到术语。DHTML 是 20 世纪 90 年代末和 21 世纪初 IT 和 web 开发行业的热门词汇之一。

*image 注意 DHTML,或称动态 HTML,从来都不是一项真正的技术或万维网联盟(W3C)标准——它仅仅是营销和广告公司发明的一个术语。

DHTML 是与层叠样式表(CSS)和 web 文档(用 HTML 编写)交互的 JavaScript,用于创建看似动态的页面。这样,开发人员就能够在浏览器中创建以前很难或不可能在浏览器中创建的效果。

当时,微软 Internet Explorer 5 和 Netscape Navigator 4 是主流浏览器。开发人员必须处理的一个问题是微软 DOM 和网景 DOM 是不同的。

这些浏览器也有不同层次的 CSS 支持。例如,做同样事情的属性有不同的名称。访问文档中的元素需要编写两次代码,每个浏览器一次,这已经是一个很大的问题了。因此,编写复杂的浏览器嗅探脚本来确保正确的代码在正确的浏览器中运行。

常见的 DHTML 脚本有几个问题:

  • JavaScript 依赖和缺乏优雅退化 : 关闭了 JavaScript 的访问者(无论是出于选择还是因为他们公司的安全设置)将无法获得该功能;相反,他们会得到激活时什么也不做的元素,甚至是根本无法导航的页面。
  • 浏览器和版本依赖 :测试脚本是否可以执行的一个常用方法是读出 navigator 对象中的浏览器名称。因为许多这些脚本是在网景 4 和 Internet Explorer 5 最先进的时候创建的,它们无法支持较新的浏览器——原因是浏览器检测没有考虑较新的版本,只是针对版本 4 或 5 进行了测试。
  • 代码分叉 :因为不同的浏览器支持不同的 DOM,大量的代码需要复制,一些浏览器的怪癖需要避免。这也使得编写模块化代码变得困难。
  • 高维护 :因为网站或应用的大部分外观和感觉都保留在脚本中,任何改变都意味着你至少需要知道基本的 JavaScript。因为 JavaScript 是为几种不同的浏览器开发的,你需要在针对每种浏览器的所有不同脚本中应用这种变化。
  • 标记依赖 :不是通过 DOM 生成或访问 HTML,很多脚本通过 document.write 指令写出内容并添加到每个文档体中,而不是将所有内容保存在单独的—缓存的—文档中。

所有这些问题与我们目前必须满足的需求形成了鲜明的对比:

  • 代码应该易于维护,并且可以在多个项目中重用。
  • 像英国的数字歧视法案(DDA)和美国的 Section 508 这样的法律要求强烈反对,或者在某些情况下,甚至禁止 web 产品依赖于脚本。
  • 越来越多的浏览器、手机等设备上的用户代理(UAs ),或者帮助残疾用户参与网络的辅助技术,使得我们的脚本不可能依赖于浏览器识别。
  • 新的营销策略要求快速、低成本地改变网站或 web 应用的外观,甚至可以通过内容管理系统来改变。

如果我们还想使用 JavaScript 并将其销售给客户,并跟上不断变化的市场的挑战,显然需要重新思考我们将 JavaScript 作为一种 web 技术的方式。

第一步是通过使 JavaScript 成为一个“最好拥有”的项目而不是一个需求来减少它的阻碍——不再有当 JavaScript 不可用时什么都不做的空白页面或链接。术语不引人注目的 JavaScript 是由斯图尔特·朗里奇在www.kryogenix.org为命名的。

不引人注目的 JavaScript 指的是一种不会把自己强加给用户或妨碍用户的脚本。它测试它是否可以被应用,如果可能的话就这样做。不引人注目的 JavaScript 就像一个舞台工作人员——为了整个作品的利益,在后台做她擅长的事情,而不是成为一个独霸整个舞台的女主角,每当出现问题或不合她的心意时,就对管弦乐队和她的同事大喊大叫。

后来,术语 DOM scripting 被引入,在 2004 年伦敦@media 会议之后,WaSP DOM Scripting Task Force 成立了。该任务组由许多希望看到 JavaScript 以更成熟和以用户为中心的方式使用的程序员、博客作者和设计师组成——你可以在 http://domscripting.webstandards.org 看看它有什么要说的。

因为 JavaScript 在常见的 web 开发方法中没有固定的位置——相反,它被认为是“可以从 web 上下载并更改的东西”或“如果需要的话,将由编辑工具生成的东西”——术语行为层出现在各种 web 出版物中。

JavaScript 作为行为层

Web 开发可以被认为是由几个不同的组成,如图图 3-1 :

  • 行为层 : 在客户端执行,定义不同元素在用户与之交互时的行为方式(Flash 站点的 JavaScript 或 ActionScript)。

  • 表示层 : 显示在客户端,指定网页的外观(CSS,imagery)。

  • 结构层 : 由用户代理转换或显示。这是定义某个文本或媒体是什么的标记(HTML)。

  • 内容层 : 存储在服务器上,由站点上使用的所有文本、图像和多媒体内容(XML、数据库、媒体资产)组成。

  • The business logic layer (or back end) : Runs on the server and determines what is done with incoming data and what gets returned to the user.

    9781430250920_Fig03-01.jpg

    图 3-1 。web 开发的不同层次

请注意,这只是定义了哪些层是可用的,而不是它们如何交互。比如有些东西需要把内容转换成结构(比如 XSLT),有些东西需要把上面四层和业务逻辑连接起来。

如果你设法保持所有这些层是独立的,但又能相互交流,你就成功地开发了一个易于访问和维护的网站。在真实的发展和商业世界中,这几乎是不可能的。然而,你越是将此作为你的目标,你在以后的阶段中不得不面对的恼人的变化就越少。级联样式表非常强大,因为它们允许您在一个文件中定义大量 web 文档的外观,该文件将由用户代理缓存。通过使用脚本标记的 src 属性和一个单独的。js 文件。

在本书的前几章中,我们将 JavaScript 直接嵌入到 HTML 文档中。从现在开始,我们不会这样做;相反,我们将创建单独的 JavaScript 文件,并在文档头中链接到它们:

<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Demo</title>
<link rel="stylesheet" type="text/css" href="styles.css">
<script type="text/javascript" src="scripts.js"></script>
<script type="text/javascript" src="morescripts.js"></script>
</head>
<body>
</body>
</html>

我们还应该尽量不要在文档中使用任何脚本块,主要是因为这样会混淆结构层和行为层,如果发生 JavaScript 错误,会导致用户代理停止显示页面。这也是一个维护的噩梦——将所有 JavaScript 添加到单独的。js 文件意味着我们可以在一个地方维护整个网站的脚本,而不是搜索所有的文档。安全性也是保持 JavaScript 独立的一个原因。内容安全策略(CSP) 将确保只有从独立文件加载的代码才会运行。

image 注意当火狐、Chrome、Safari、Opera 显示时。js 文件作为文本,Microsoft Internet Explorer 试图执行它们。如果文件被分配给一个程序(你可以通过图标来判断),当你双击它时,它将启动那个程序。如果你把它拖进浏览器,它会在试图执行代码之前警告你。如果当你这样做的时候你的文件与一个程序相关联,它将会启动这个程序。

9781430250920_Fig03-02.jpg

图 3-2。当您尝试在本地执行 JavaScript 时,Windows 7 上的 Microsoft Internet Explorer 会显示一条警告消息

这种将 JavaScript 分离到自己的文件中的做法使得开发一个在脚本不可用时仍能工作的网站变得更加简单;如果需要改变站点的行为,很容易只改变脚本文件。

对象检测与浏览器依赖性

确定正在使用哪个浏览器的一种方法是测试 navigator 对象,它在其 appName 和 appVersion 属性中显示浏览器的名称和版本。

例如,以下脚本获取浏览器名称和版本,并将其写入文档:

<script type="text/javascript">
  document.write("You are running " + navigator.appName);
  document.write(" and its version is " + navigator.appVersion);
</script>

在我的电脑上,在 Adobe Dreamweaver 的“设计”视图中,此脚本报告了以下内容(因为 Dreamweaver 使用 WebKit 引擎预览 HTML):

You are running Netscape and its version is 5.0 (Macintosh; U; Intel Mac OS X; en_US)  AppleWebKit/533.19.4 (KHTML, like Gecko) Dreamweaver/12.1.0.5949 Version/5.0.3 Safari/533.19.4

如果我在同一台电脑上运行 Firefox 17.0.2 中的相同脚本,我会得到以下结果:

You are running Netscape and its version is 5.0 (Macintosh)

image 注意在装有 IE 10 的 Windows 7 机器上运行同样的代码。+ navigator.appVersion 显示为版本 5.0

许多较旧的脚本使用此信息来确定浏览器是否能够支持它们的功能:

<script type="text/javascript">
 if(navigator.appName.indexOf('Internet Explorer')!=-1  && browserVersion.indexOf('6')!=-1)
  {
    document.write('<p>This is MSIE! 6</p>');
  }
  else
  {
    document.write('<p>This isn\'t MSIE</p>');
  }
</script>

乍一看,这似乎很聪明,但这并不是一个确定哪个浏览器正在使用的可靠方法。例如,假设您像这样显示 navigator.appName 的结果:

<script type="text/javascript">
 if(navigator.appName.indexOf('Internet Explorer')!=-1  && browserVersion.indexOf('6')!=-1)
  {
    document.write('<p>This is MSIE! 6</p>');
    document.write('<p>navigator.appName</p>');
  }
  else
  {
    document.write('<p>This isn\'t MSIE</p>');
    document.write('<p>'+navigator.appName+'</p>');
  }
</script>

结果将显示,在 Chrome、Safari 和 Firefox 中,appName 显示为“Netscape”,这是一种自 2007 年以来就没有开发过的浏览器。从这里开始只会变得更糟。找 navigator.userAgent 会给你更多的混合结果。例如,IE 在 Windows 7 上的 IE 10 显示为“Mozilla/4.0(兼容,MSIE 7.0)”。

读出浏览器名称和版本—通常称为 浏览器嗅探—是不可取的,不仅因为我刚才指出的不一致,还因为它使您的脚本依赖于某个浏览器,而不是支持任何实际上有能力支持脚本的用户代理。

这个问题的解决方案被称为 对象检测 ,它基本上意味着我们确定一个用户代理是否支持某个对象,并使之成为我们的关键区别点。在非常旧的脚本中,比如第一个图像翻转,您可能会看到类似这样的内容:

<script type="text/javascript">
  // preloading images
  if(document.images)
  {
    // Images are supported
    var home=new Image();
    home.src='home.jpg';
    var aboutus=new Image();
    aboutus.src='home.jpg';
   }
</script>

if 条件检查浏览器是否允许您访问 images 属性,只有在这种情况下,它才运行条件中的代码。很长一段时间,像这样的脚本是处理图像的标准方式。在较新的浏览器中,许多 JavaScript 图像效果可以通过 CSS 实现,有效地使这类脚本过时。然而,JavaScript 可以用 CSS 不能的方式操作图像,我们将在第六章中回到这一点。

每个浏览器都通过一种叫做 文档对象模型 ,或者简称为 DOM 的东西,向我们提供它所显示的文档以供操作。较老的浏览器支持它们自己的 DOM,现在称为遗留 DOMDOM Level 0 。所有现代浏览器都支持 W3C DOM,这是 W3C 定义的标准 DOM。在撰写本文时,最新版本是 DOM Level 3。您过去可能遇到过类似这样的测试脚本:

<script type="text/javascript">
  if(document.all)
  {
    // MSIE
  }
  else if (document.getElementById)
  {
    // W3C DOM (MOZ, Chrome, Safari, Opera and IE)
  }
</script>

document.all DOM 是微软发明的,只受 IE 支持。如果您希望用户使用非常旧的浏览器,您可以通过 document.getElementById 测试 W3C 推荐的 DOM。

可能有这样一种情况,你想尝试一些实验性的东西,但不知道用户的浏览器是否支持它。例如,访问麦克风和摄像头,目前并非所有浏览器都支持。

<script type="text/javascript">
  if (navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia)
  {
document.write('<p>getUserMedia() is supported in your browser</p>');
  }else{
document.write('<p>getUserMedia() is not supported in your browser</p>');
}
</script>

从例子中可以看出,Opera、webkit(本例中为 Chrome)、Mozilla (Firefox)和 Microsoft (Internet Explorer)的实现方式都略有不同。因为您检查的是对象而不是特定的浏览器版本,所以您不必担心有人没有您所针对的确切浏览器版本。而且如果需要的话,以后也很容易更新。

不是迎合特定的用户代理,而是在应用你的功能之前测试 UA 的能力——这个过程是一个更大的现代 web 设计思想的一部分,叫做渐进增强

渐进增强

渐进式改进是这样一种实践,即通过从最小公分母开始,然后测试是否支持连续的改进,只向那些可以看到和使用它的人提供功能。那些没有能力支持这些更高功能的用户仍然可以很好地使用网站。一个类似的现实生活过程是早上穿衣服:

  • 你从一个赤裸的身体开始,希望它处于完全工作状态——或者至少和昨天一样的状态,这样你就不会感到震惊。(为了让这个例子简单,我们不考虑睡衣和/或内衣。)
  • 你可能有一个美妙的裸体身体,但在寒冷的天气里这是不够的,可能对你周围的人没有吸引力——你需要一些东西来覆盖它。
  • 如果有衣服可用,你可以检查哪些衣服适合天气、你的心情、你这一天要见的一群人,以及不同的衣服是否整齐、干净、尺寸合适。
  • 你穿上它们,就能面对一天。如果你愿意,你可以开始搭配配饰,但你应该确保在这样做的时候考虑到其他人。(在拥挤的火车车厢里,喷太多香水可能不是个好主意。)

在 web 开发术语中,这意味着:

  • 您从一个有效的、语义正确的 HTML 文档开始,该文档包含所有的内容——包括相关的图像和作为 alt 属性的文本选项——以及一个有意义的结构。

  • 您可以添加一个样式表来改善这个结构的外观、易读性和清晰性——甚至可以添加一些简单的翻转效果来使它更生动一些。

  • 您添加 JavaScript:

  • 通过使用窗口对象的 onload 事件处理程序,JavaScript 在加载文档时启动。

  • JavaScript 测试当前用户代理是否支持 W3C DOM。

  • 然后,它测试所有必需的元素是否可用,并对它们应用所需的功能。

在 JavaScript 中应用渐进增强的概念之前,您需要学习如何从脚本中访问 HTML 和 CSS 并与之交互。我用这本书的两章来完成这个任务——第四章和第五章。然而,目前,意识到您之前练习的对象检测有助于您实现渐进式增强就足够了——您确保只有那些理解正确对象的浏览器才会尝试访问它们。

JavaScript 和可访问性

网页可访问性 是让每个人都能使用网站的做法,不管他们可能有什么残疾。例如,有视觉障碍的用户可能会使用名为 屏幕阅读器 的特殊软件来为他们读出网页内容,而有运动障碍的用户可能会使用某种工具来操纵键盘,以便在网络上导航,因为他们无法使用鼠标。残疾人是网络用户的重要组成部分,所以选择不允许他们使用网站的公司可能会错过很多业务。在一些国家,法律(如美国的第 508 条)规定,任何提供公共服务的网站都必须是可访问的。

那么 JavaScript 在这里起什么作用呢?过时的 JavaScript 技术对可访问性非常不利,因为它们会弄乱文档流。例如,屏幕阅读器不能正确地将 JavaScript 元素读回给用户。(当基本内容由 JavaScript 生成时,这种情况尤其糟糕——屏幕阅读器可能根本看不到它!).因此,用户被迫使用鼠标来导航他们的站点(例如,在复杂的 DHTML whiz-bang 导航菜单的情况下)。整个问题比这更深入,但这只是让你对这一领域的问题有一个感觉。

image 提示如果你想了解更多关于网页可访问性的信息,拿一本吉姆·撒切尔等人写的网页可访问性:网页标准和法规遵从性(编辑之友,2006)。

JavaScript 和可访问性是圣战的素材。心怀不满的开发人员和易访问性专家之间的许多战斗都是在邮件列表、论坛和聊天中进行的,双方都有自己的——非常好的——论点。

不得不忍受糟糕的浏览器和营销经理不合逻辑的假设的开发人员(“我在我表哥的网站上看到的。当然,您也可以将其用于我们的跨国门户。”)不想看到多年的研究和试错付诸东流,不再使用 JavaScript。

可访问性专家指出,JavaScript 可以被关闭,W3C 的可访问性指南似乎根本不允许这样做(指南中对此有很多混淆),并且许多脚本只是假设访问者拥有并能够像神经外科医生那样精确地使用鼠标。

两者都是对的,两者都可以得到他们的蛋糕: 没有必要从一个可访问的网站 中完全删除 JavaScript。

必须淘汰的是假设太多的 JavaScript。可访问的 JavaScript 必须确保以下几点:

  • 无论有没有 JavaScript,web 文档都必须有相同的内容——不应该阻止或强迫访问者打开 JavaScript(因为访问者不一定能决定他是否能打开 JavaScript)。
  • 如果内容或 HTML 元素只有在 JavaScript 可用时才有意义,那么这些内容和元素必须由 JavaScript 创建。没有什么比一个什么都不做的链接或者解释一些你无法使用的功能的文字更令人沮丧的了。
  • 所有的 JavaScript 功能都必须独立于输入设备——例如,用户可以使用拖放界面,但她也应该能够通过单击或按键来激活元素。
  • 页面中不是交互元素的元素(实际上除了链接和表单元素之外的任何元素)都不应该成为交互元素——除非你提供了一个后退选项。迷惑?想象标题折叠并展开后面的文字。你可以很容易地在 JavaScript 中使它们可点击,但这意味着依赖键盘的访问者将永远无法访问它们。如果你在标题中创建一个链接,并使其可点击,那么即使是访问者也可以通过点击链接并回车来激活效果。
  • 脚本不应该在没有任何用户交互的情况下自动将用户重定向到其他页面或提交表单。这是为了避免过早提交表单——因为一些辅助技术在 onchange 事件处理程序上会有问题。此外,病毒和间谍软件通过 JavaScript 将用户发送到其他页面,因此目前一些软件会阻止这种行为。

这就是用 JavaScript 创建一个可访问的网站的全部内容。当然,这也是可访问 HTML 文档的所有优点,例如允许元素以更大的字体设置调整大小,并为色盲和视力正常的人提供足够的对比度和颜色。

良好的编码实践

既然我已经让你进入了实践向前兼容和可访问脚本的心态,让我们来看看 JavaScript 的一些通用最佳实践。

命名规格

JavaScript 是大小写相关的,这意味着名为 moveOption 的变量或函数不同于 moveoption 或 Moveoption。任何名称,无论是函数、对象、变量还是数组,都只能包含字母、数字、美元符号或下划线字符,并且不能以数字开头。

<script type="text/javascript">
  // Valid examples
  var dynamicFunctionalityId = 'dynamic';
  var parent_element2='mainnav';
  var _base=10;
  var error_Message='You forgot to enter some fields: ';

  // Invalid examples
  var dynamic ID='dynamic';  // Space not allowed!
  var 10base=10; // Starts with a number
  var while=10; // while is a JavaScript statement
</script>

最后一个例子显示了另一个问题:JavaScript 有很多保留字——基本上,所有的 JavaScript 语句都使用保留字,如 while、if、continue、var 或 for。如果您不确定可以使用什么作为变量名,那么获取 JavaScript 参考可能是个好主意。好的编辑还会在你输入保留字时突出显示,以避免这个问题。

JavaScript 中的名字没有长度限制;然而,为了避免庞大的难以阅读和调试的脚本,尽可能保持它们的简单性和描述性是一个好主意。尽量避免使用通用名称,例如:

  • 功能 1
  • 变量 2
  • doSomething()

这些对试图调试或理解代码的其他人(甚至对你接下来的两个月)来说没有多大意义。最好使用描述性名称,确切说明函数的作用或变量是什么:

  • createTOC()
  • 计算差异()
  • getcoordonates_)
  • setcoordonates_)
  • 最大宽度
  • 地址数据文件

如前几章所述,可以使用下划线或 camelCase (即 camel 符号——首字小写,之后每个字首字母大写)来连接单词;然而,camelCase 更常见(DOM 本身就使用它),习惯了它会让您在以后的阶段更容易转向更复杂的编程语言。camelCase 的另一个好处是,您可以在几乎任何编辑器中双击突出显示一个变量,而您需要用鼠标突出显示一个下划线分隔的名称。

image 小心当心小写字母 1 和数字 1 !大多数编辑器会使用像 Courier 这样的字体,在这种情况下,它们看起来是一样的,这可能会造成很多混乱,并使寻找 bug 变得很有趣。

代码布局

首先也是最重要的,代码是要被解释器转换来让计算机做一些事情的——或者至少这是一个很常见的神话。当代码有效时,解释器会毫不迟疑地吞下代码——然而,产生真正好的代码的真正挑战是,人类将能够编辑、调试、修改或扩展代码,而无需花费数小时试图弄清楚您想要实现什么。逻辑的、简洁的变量和函数名是使维护者更容易理解的第一步——下一步是正确的代码布局。

image 注意如果你真的很无聊,去任何一个程序员论坛,说出一句绝对的话,比如“空格比制表符好”或者“每个花括号都应该换一行”你很可能会收到成百上千的帖子,指出你所声称的利弊。代码布局是一个热门话题。下面的例子对我来说很好,似乎是一种很常见的布局代码的方式。在加入一个项目的多开发者团队并使用这里提到的标准之前,检查是否有任何矛盾的标准可以遵循,这可能是一个好主意。

只需检查以下代码示例;你现在可能不明白他们在做什么。(它们提供了一个小函数,可以在一个新窗口中打开每一个有 CSS 类小弹出窗口的链接,并添加一条消息,说明这是将要发生的事情)。然而,只要考虑哪一个更容易调试和更改。

它们没有缩进:

function addPopUpLink(){
var popupClass='smallpopup';
var popupMessage= '(opens in new window)';
var pop,t;
var as=document.getElementsByTagName('a');
for(var i=0;i<as.length;i++){
t=as[i].className;
if(t&&t.toString().indexOf(popupClass)!=-1){
as[i].appendChild(document.createTextNode(popupMessage));
as[i].onclick=function(){
pop=window.open(this.href,'popup','width=400,height=400');
returnfalse;
}}}}
window.onload=addPopUpLink;

这是它们的缩进:

function addPopUpLink(){
  var popupClass='smallpopup';
  var popupMessage= '(opens in new window)';
  var pop,t;
  var as=document.getElementsByTagName('a');
for(var i=0;i<as.length;i++){
    t=as[i].className;
    if(t && t.toString().indexOf(popupClass)!=-1){
      as[i].appendChild(popupMessage);
      as[i].onclick=function(){
        pop=window.open(this.href,'popup','width=400,height=400');
        return false;
      }
    }
  }
}
window.onload=addPopUpLink;

下面是新行上的缩进和花括号:

  function addPopUpLink()
  {
    var popupClass='smallpopup';
    var popupMessage= ' (opens in new window)';
var pop,t;
    var as=document.getElementsByTagName('a');
for(var i=0;i<as.length;i++)
    {
      t=as[i].className;
      if(t && t.toString().indexOf(popupClass)!=-1)
      {
        as[i].appendChild(document.createTextNode(popupMessage));
        as[i].onclick=function()
        {
          pop=window.open(this.href,'popup','width=400,height=400');
          return false;
        }
      }
    }
  }
  window.onload=addPopUpLink;

我认为很明显缩进是一个好主意;然而,有一个很大的争论,是否应该通过制表符或空格缩进。我喜欢标签页,主要是因为它们容易删除,而且打字工作量少。大量使用非常基础(或者非常惊人,如果你知道所有神秘的键盘快捷键)编辑器的开发人员,比如 vi 或 emacs,不赞成这样做,因为选项卡可能会显示为非常大的水平间隙。如果是这样的话,用一个简单的正则表达式将所有制表符替换为双空格不是什么大问题。

左花括号是否应该换一行是另一个你需要自己决定的问题。不使用新行的好处是更容易删除错误块,因为它们少了一行。新行的好处是代码看起来不那么拥挤。在 JavaScript 中,我将开始的一行放在同一行,在 PHP 中放在新的一行——因为这似乎是这两个开发人员社区的标准。

另一个问题是线路长度。如今大多数编辑器都有一个换行选项,确保当你想看代码时不必水平滚动。然而,并不是所有的编辑器都能正确地打印出代码,也许以后维护代码的人不会有像您正在使用的那种花哨的编辑器。因此,您应该保持行的简短,最多大约 80 个字符。

评论

注释是只有人类才能受益的东西——尽管在一些高级编程语言中,注释被索引以生成文档。(一个例子是 PHP 手册,正因为如此,对于非程序员来说,它有时有点晦涩难懂。)虽然注释并不是代码工作的必要条件——如果你使用清晰的名称并缩进代码,它应该是不言自明的——但它可以极大地加快调试速度。前一个示例可能对您更有意义,因为它带有解释性注释:

/*
  addPopUpLink
  opens the linked document of all links with a certain
  class in a pop-up window and adds a message to the
  link text that there will be a new window
*/
function addPopUpLink(){
    // Check for DOM and leave if it is not supported
   // Assets of the link - the class to find out which link should
  // get the functionality and the message to add to the link text
  var popupClass='smallpopup';
  var popupMessage= ' (opens in new window)';
  // Temporary variables to use in a loop
  var pop,t;
  // Get all links in the document
  var as=document.getElementsByTagName('a');
  // Loop over all links
  for(var i=0;i<as.length;i++)
  {
    t=as[i].className;
    // Check if the link has a class and that the class is the right one
    if(t && t.toString().indexOf(popupClass)!=-1)
    {
      // Add the message
      as[i].appendChild(document.createTextNode(popupMessage));
      // Assign a function when the user clicks the link
      as[i].onclick=function()
      {
        // Open a new window with
        pop=window.open(this.href,'popup','width=400,height=400');
        // Don't follow the link (otherwise, the linked document
        // would be opened in the pop-up and the document).
        return false;
      }
    }
  }
}
window.onload=addPopUpLink;

那就容易掌握多了,不是吗?也是矫枉过正。像这样的例子可以在培训文档或自学课程中使用,但在最终产品中有点多。评论的时候,适度永远是关键。在大多数情况下,解释一件事做什么,能改变什么就够了。

/*
  addPopUpLink
  opens the linked document of all links with a certain
  class in a pop-up window and adds a message to the
  link text that there will be a new window
*/
function addPopUpLink()
{

  // Assets of the link - the class to find out which link should
  // get the functionality and the message to add to the link text
  var popupClass='smallpopup';
  var popupMessage=document.createTextNode(' (opens in new window)');
  var pop;
  var as=document.getElementsByTagName('a');
  for(var i=0;i<as.length;i++)
  {
    t=as[i].className;
    if(t && t.toString().indexOf(popupClass)!=-1)
    {
      as[i].appendChild(popupMessage);
      as[i].onclick=function()
      {
        pop=window.open(this.href,'popup','width=400,height=400');
        return false;
      }
    }
  }
}
window.onload=addPopUpLink;

这些注释使我们很容易理解整个函数的作用,并找到可以更改某些设置的地方。这使得快速更改变得更加容易——无论如何,功能的更改需要维护人员更仔细地分析您的代码。

功能

函数 是可重用的代码块,是当今大多数程序不可或缺的一部分,包括用 JavaScript 编写的程序。想象你要做一个计算或者需要反复执行某个条件检查。您可以在需要的地方复制并粘贴相同的代码行;然而,使用函数要高效得多。

函数可以以参数的形式获取值(有时称为参数),并且它们可以在完成测试和更改赋予它们的内容后返回值。

使用 function 关键字创建函数,后跟函数名和参数,参数用逗号分隔在括号内:

function createLink(linkTarget, LinkName)
{
  // Code
}

一个函数可以有多少个参数是没有限制的,但是你不应该使用太多,因为这会变得相当混乱。如果您检查一些 DHTML 代码,您可以找到具有 20 个或更多参数的函数,并且在其他函数中调用这些函数时记住它们的顺序会使您几乎想简单地从头开始编写整个内容。当你这样做的时候,记住使用太多的参数意味着更多的维护工作,并且使调试变得更加困难。

与 PHP 不同,如果参数不可用,JavaScript 没有预设参数的选项。您可以使用一些 if 条件来解决这个问题,这些条件首先检查是否有任何要使用的参数。使用 arguments.length,可以看到传递给函数的参数数量。完成后,您可以检查参数的值:

function createLink(linkTarget, LinkName)
{
   if(arguments.length == 0 ) {return false}
  if (linkTarget === undefined)
  {
    linkTarget = '#';
  }
  if (linkName == null)
  {
    linkName = 'dummy';
  }
}

函数通过 return 关键字报告它们所做的事情。如果由事件处理程序调用的函数返回布尔值 false,通常由该事件触发的事件序列就会停止。当您想要将函数应用到链接并阻止浏览器导航到链接的 href 时,这非常方便。在本章前面的“对象检测与浏览器依赖”一节中,您已经看到了这一点。

return 语句后面的任何其他值都将被发送回调用代码。让我们将 createLink 函数改为创建一个链接,并在函数完成创建后返回它:

function createLink(linkTarget,linkName)
{
  if (linkTarget == null) { linkTarget = '#'; }
  if (linkName == null) { linkName = 'dummy'; }

  var tempLink=document.createElement('a');
  tempLink.setAttribute('href',linkTarget);
  tempLink.appendChild(document.createTextNode(linkName));

  return tempLink;
}

另一个函数可以将这些生成的链接附加到一个元素上。如果没有定义元素 ID,它应该将链接附加到文档主体:

function appendLink(sourceLink,elementId)
{
  var element=false;
  if (elementId==null || !document.getElementById(elementId))
  {
    element=document.body;
  }
  if(!element) {
    element=document.getElementById(elementId);
  }
  element.appendChild(sourceLink);
}

现在,要使用这两个函数,您可以让另一个函数用适当的参数调用它们:

function linksInit()
{
  var openLink=createLink('#','open');
  appendLink(openLink);
  var closeLink=createLink('closed.html','close');
  appendLink(closeLink,'main');
}

函数 linksInit()检查 DOM 是否可用。(因为它是唯一调用其他函数的函数,所以您不需要再次在它们内部检查它。)然后它创建一个目标为#的链接,并作为链接文本打开。

然后,它调用 appendLink()函数,并将新生成的链接作为参数发送。注意,它没有发送目标元素,这意味着 elementId 为 null,appendLink()将链接添加到文档的主体。

initLinks()第二次调用 createLink()时,它发送目标 closed.html 和 close 作为链接文本,并通过 appendLink()函数将链接应用于 ID 为 main 的 HTML 元素。如果有一个 ID 为 main 的元素,appendLink()将链接添加到这个元素中;如果没有,它使用文档正文作为后备选项。

如果现在还不清楚,不要担心——稍后您会看到更多的例子。现在,重要的是记住什么是功能以及它们应该做什么:

  • 函数是用来一遍又一遍地完成一项任务的。将每个任务保持在它自己的功能范围内;不要创建同时做几件事的怪物函数。
  • 函数可以有任意多的参数,每个参数可以是任意类型——字符串、对象、数字、变量或数组。
  • 您不能在函数定义本身中为参数提供默认值,但是您可以检查它们是否被定义,并使用 if 条件设置默认值。你可以通过三元运算符简洁地做到这一点,你将在本章的下一节了解它。
  • 函数应该有一个描述它们做什么的逻辑名称。尽量使名称靠近任务主题,例如,一个通用的 init()可能在任何其他包含的 JavaScript 文件中,并覆盖它们的函数。正如你将在本章后面看到的,对象文字可以提供一种避免这个问题的方法。

通过三元运算符的短代码

查看前面展示的 appendLink()函数,您可能会有一种预感,大量的 if 条件或 switch 语句可能会导致代码变得又长又复杂。避免这种膨胀的一个技巧是使用一种叫做三元运算符 的东西。三元运算符的语法如下:

var variable = condition ? trueValue:falseValue;

这对于布尔条件或非常短的值非常方便。例如,您可以用一行代码替换这个长 if 条件:

// Normal syntax
var direction;
if(x<200)
{
  direction=1;
}
else
{
  direction=-1
}
// Ternary operator
var direction = x < 200 ? 1 : -1;

以下是其他几个例子:

t.className = t.className == 'hide' ? '' : 'hide';
var el = document.getElementById('nav')
       ? document.getElementById('nav')
       : document.body;

您也可以嵌套三元选择器,但是这很难理解:

y = x <20 ? (x > 10 ? 1 : 2) : 3;
// equals
if(x<20)
{
  if(x>10)
{
    y=1;
  }
  else
  {
    y=2;
  }
}
else
{
 y=3
}

函数的排序和重用

如果您有大量的 JavaScript 函数,将它们分开可能是个好主意。js 文件,并且只在需要的地方应用它们。说出。js 文件,具体取决于其中包含的函数的功能,例如 formvalidation.js 或 dynamicmenu.js.

这在一定程度上已经为您做到了,因为有许多预打包的 JavaScript 库(函数和方法的集合)有助于创建特殊的功能。我们将在第十一章中看到其中一些,并在接下来的几章中创建我们自己的。

变量和函数范围

在带有新变量的函数中定义的变量只在函数内部有效,在函数外部无效。这看起来像是一个缺点,但它实际上意味着你的脚本不会干扰他人——当你使用 JavaScript 库或你自己的集合时,这可能是致命的。

函数外定义的变量称为全局变量 ,是危险的。你应该尽量把所有的变量都包含在函数中。这确保了您的脚本可以很好地与可能应用于页面的其他脚本配合使用。许多脚本使用通用变量名,如 navigation 或 currentSection。如果这些被定义为全局变量,脚本将覆盖彼此的设置。尝试运行以下函数,看看省略 var 关键字会导致什么问题:

<script type="text/javascript">
  var demoVar=1 // Global variable
  alert('Before withVar demoVar is' +demoVar);
  function withVar()
  {
    var demoVar=3;
  }
  withVar();
  alert('After withVar demoVar is' +demoVar);
  function withoutVar()
  {
    demoVar=3;
  }
  withoutVar();
  alert('After withoutVar demoVar is' +demoVar);
</script>

withVar 保持变量不变,而 with var 改变它:

Before withVar demoVar is 1
After withVar demoVar is 1
After withoutVar demoVar is 3

用对象文字保持脚本安全

前面,我谈到了通过 var 关键字在本地定义变量来保证变量的安全。原因是为了避免其他函数依赖同名变量,以及两个函数覆盖彼此的值。这同样适用于函数。因为您可以在单独的脚本元素中为同一个 HTML 文档包含几个 JavaScripts,所以您的功能可能会因为另一个包含的文档具有相同名称的函数而中断。您可以通过使用命名约定来避免这个问题,比如为您的函数使用 myscript_init()和 myscript_validate()。然而,这有点麻烦,JavaScript 以对象的形式提供了一种更好的处理方式。

您可以定义一个新对象,并将您的函数用作该对象的方法——这就是 JavaScript 对象(如 Date 和 Math)的工作方式。例如:

<script type="text/javascript">
  myscript=new Object();
  myscript.init=function()
  {
    // Some code
  };
  myscript.validate=function()
  {
   // Some code
  };
</script>

注意,如果您试图调用函数 init()和 validate(),您会得到一个错误,因为它们不再存在了。相反,您需要使用 myscript.init()和 myscript.validate()。

将所有函数作为方法包装在一个对象中,类似于 C++或 Java 等其他语言使用的编程类。在这种语言中,您将应用于同一任务的函数放在同一个类中,这样就可以更容易地创建大量代码,而不会被数百个函数所混淆。

我们使用的语法仍然有点麻烦,因为你必须一遍又一遍地重复对象名。有一种叫做对象字面量 的快捷表示法,这让事情变得简单多了。

object literal 已经存在很长时间了,但是没有得到充分利用。如今,它变得越来越流行,你几乎可以认为你在网上找到的使用它的脚本是好的、现代的 JavaScript。

object literal 所做的是使用快捷符号创建对象,并将每个函数作为对象方法而不是独立的函数来应用。让我们看看动态链接示例中的三个函数,作为一个使用对象文字的大对象:

var dynamicLinks={
  linksInit:function()
  {
    var openLink=dynamicLinks.createLink('#','open');
    dynamicLinks.appendLink(openLink);
    var closeLink=dynamicLinks.createLink('closed.html','close');
    dynamicLinks.appendLink(closeLink,'main');
  },
  createLink:function(linkTarget,linkName)
  {
    if (linkTarget == null) { linkTarget = '#'; }
    if (linkName == null) { linkName = 'dummy'; }
    var tempLink=document.createElement('a');
    tempLink.setAttribute('href',linkTarget);
    tempLink.appendChild(document.createTextNode(linkName));
    return tempLink;
  },
  appendLink:function(sourceLink,elementId)
  {
    var element=false;
    if (elementId==null || !document.getElementById(elementId))
    {
      element=document.body;
    }
    if(!element){element=document.getElementById(elementId)}
    element.appendChild(sourceLink);
  }
}
window.onload=dynamicLinks.linksInit;

正如您所看到的,所有的函数都作为方法包含在 dynamicLinks 对象中,这意味着如果您想要调用它们,您需要在函数名之前添加对象名。处理函数的另一种方法是嵌套函数。外部函数会有一个名字,而内部函数(称为一个匿名函数 )会直接运行。

语法有点不同;不是将 function 关键字放在函数名之前,而是将它添加到前面有冒号的名称之后。此外,除了最后一个花括号外,每个花括号后面都需要跟一个逗号。

如果您想使用对象内所有方法都可以访问的变量,可以使用非常类似的语法:

var myObject=
{
  objMainVar:'preset',
  objSecondaryVar:0,
  objArray:['one','two','three'],
  init:function(){},
  createLinks:function(){},
  appendLinks:function(){}
}

现在可能有很多信息需要消化,但是不要担心。这一章旨在作为你回来时的参考,并提醒你许多好的实践。我们将在下一章继续讨论更具体的例子,并打开一个 HTML 文档来处理不同的部分。

摘要

你做到了:你完成了这一章。当你在网上看到新的和旧的脚本时,你应该能够区分它们。旧的脚本很可能

  • 使用大量的 document.write()。
  • 检查浏览器和版本,而不是对象。
  • 写出大量的 HTML,而不是访问文档中已经存在的内容。
  • 使用专有的 DOM 对象,如用于 Microsoft IE 的 document.all 和用于包括 Microsoft IE 在内的现代浏览器的 document.getElementById。
  • 出现在文档中的任何地方(而不是在中或通过

您已经了解了如何将 JavaScript 独立运行。js 文档,而不是将其嵌入到 HTML 中,从而将行为与结构分开。

然后,您听说了使用对象检测而不是依赖浏览器名称。我解释了渐进增强的含义以及它如何应用于 web 开发。测试用户代理的功能而不是名称和版本,将确保您的脚本也适用于您可能没有亲自测试的用户代理。这也意味着你不必每次发布新版本的浏览器时都担心——如果它支持标准,你就没事了。

我谈到了可访问性以及它对 JavaScript 的意义,您已经看到了许多编码实践。需要记住的一般事项是

  • 测试您想要在脚本中使用的对象。
  • 在没有客户端脚本的情况下,对已经运行良好的现有站点进行改进,而不是先添加脚本,然后再添加非脚本回退选项。
  • 保持代码的自包含性,不要使用任何可能干扰其他脚本的全局变量。
  • 编码时要记住,你必须将这些代码交给其他人来维护。这个人可能是三个月后的你,你应该能立即明白是怎么回事。
  • 对代码的功能进行注释,并使用可读的格式,以便于查找错误或更改功能。

除了一个叫做事件处理程序的东西之外,其他的都在这里了,我谈到过它,但并没有真正定义它。我会在第五章中这么做。但是现在,坐下来,喝杯咖啡或茶,放松一下,直到你准备好继续学习 JavaScript 如何与 HTML 和 CSS 交互。*

四、HTML 和 JavaScript

在这一章中,你最终会接触到真正的 JavaScript 代码。您将了解 JavaScript 如何与 HTML 中定义的页面结构交互,以及如何接收数据并向访问者反馈信息。我首先解释什么是 HTML 文档以及它是如何构造的,然后解释几种通过 JavaScript 创建页面内容的方法。然后,您将了解 JavaScript 开发人员的瑞士军刀——文档对象模型(DOM)——以及如何分离 JavaScript 和 HTML 来创建现在无缝的效果,这是开发人员过去用 d HTML 以一种强迫的方式创建的。

HTML 文档的剖析

用户代理中显示的文档通常是 HTML 文档。即使你使用像 ASP.NET、PHP、ColdFusion 或 Perl 这样的服务器端语言,如果你想充分发挥浏览器的潜力,结果也是 HTML。像 Firefox 或 Safari 这样的现代浏览器也支持 XML、SVG 和其他格式,但是对于 99%的日常 web 工作,您会选择 HTML 路线。

HTML 文档是一个以 DOCTYPE 开始的文本文档,它告诉用户代理文档是什么以及应该如何处理它。HTML 随着时间的推移而发展,当前的 DOCTYPE 告诉浏览器,正在交付的页面应该以标准模式呈现为 HTML5。文档中的下一个元素是 HTML 标签。该元素包含构成文档的所有其他内容。文档中的所有元素都可以有一个可选的 lang 属性。lang 属性定义了页面使用的语言(想想人类可读的语言,而不是计算机语言)——在下面的例子中,“en”代表英语。HTML 元素内部是 HEAD 和 TITLE 元素。与 HEAD 和 TITLE 处于同一级别的可选元素是 META 元素。META 中的 charset 属性描述了字符编码、或者文本在屏幕上显示的方式。这可以在服务器上设置,但是因为大多数人没有访问 web 服务器的权限,所以可以在这里定义。在 HEAD 元素的同一层,但是在结束的< head >标签之后,是 BODY——包含所有页面内容的元素。

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Our HTML Page</title>
</head>
<body>
</body>
</html>

像这样的标记文档由标签 (标签括号中的单词或字母,如< p >)和文本内容组成。文档应该是格式良好的(这意味着每一个像< p >这样的开始标记必须与一个像< /p >这样的结束标记相匹配)。

HTML 元素就是方括号中的所有东西,<>,开始标签如

,后面是内容和相同名称的结束标签—如

。每个元素在开始和结束标记之间都可以有内容。每个元素可能有几个属性。下面的例子是一个 P 元素,它有一个名为 class 的属性。该属性的值为 intro。P 包含文本 Lorem Ipsum

<p class="intro">Lorem Ipsum</p>

浏览器检查它遇到的元素,知道 P 是一个段落,并且 class 属性对这个元素有效。它还意识到 class 属性应该检查链接的级联样式表(CSS)样式表,用该类获取 P 的定义,并相应地呈现它。

有几个原因可以说明为什么您应该努力实现标准遵从性——即使是在通过 JavaScript 生成的 HTML 中:

  • 当您知道 HTML 是有效的时,就更容易跟踪错误。
  • 维护遵循规则的文档更容易——因为您可以使用验证器来衡量其质量。
  • 当你按照约定的标准开发时,用户代理更有可能正确地呈现或转换你的页面。
  • 如果它们是有效的 HTML,最终的文档可以很容易地转换成其他格式。

现在,如果你在示例 HTML 中添加更多的元素,并在浏览器中打开它,你会得到如图 4-1 所示的渲染输出:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>DOM Example</title>
</head>
<body>
<h1>Heading</h1>
<p>Paragraph</p>
<h2>Subheading</h2>
<ul id="eventsList">
<li>List 1</li>
<li>List 2</li>
<li><a href="http://www.google.com">Linked List Item</a></li>
<li>List 4</li>
</ul>
<p>Paragraph</p>
<p>Paragraph</p>
</body>
</html>

9781430250920_Fig04-01.jpg

图 4-1 。由浏览器呈现的 HTML 文档

关于 XHTML 的一句话

浏览器使用 DOCTYPE 来决定如何呈现文档。以前版本的 HTML (XHTML 4.01)有一个更长的 DOCTYPE。它描述的事情之一是它是否应该以怪癖模式标准模式呈现事物。顾名思义,quirks 模式是为了适应早期浏览器中呈现 HTML 的方式而创建的,以保持向后兼容性,本质上保留所有的 Quirks 以便页面能够正确显示。标准模式严格遵循万维网联盟(W3C)制定的标准。

用户代理“看到”的文档略有不同。DOM 将文档建模为一组节点,包括元素节点、文本节点和属性节点。两个元素及其文本内容都是独立的节点。属性节点是元素的属性。DOM 包括用于标记文档其他部分的其他类型的节点,但是如果您继续使用 JavaScript 和 HTML,这三种节点——元素节点、文本节点和属性节点——是非常重要的。如果要通过浏览器查看文档,请右键单击(在 Mac 上按住 control 键单击)并选择“检查元素”。这将打开浏览器的下半部分,以树形结构显示文档,并允许您访问内置调试工具,如图图 4-2 所示。

9781430250920_Fig04-02.jpg

图 4-2 。Mozilla DOM Inspector 中展示的文档的 DOM 表示

image 提示你可以在 Chrome、Safari、Opera 和 Internet Explorer 中使用类似的工具。您不仅可以看到文档是如何表示的,还可以编辑代码并直接在浏览器中看到结果。在附录中,我将介绍验证和调试。

image 注意注意到元素之间的所有#text 节点了吗?这不是我添加到文档中的文本,而是我在每行末尾添加的换行符。一些浏览器将它们视为文本节点,而另一些则不是——当您稍后试图通过 JavaScript 访问文档中的元素时,这可能会非常烦人。

图 4-3 显示了另一种可视化文档树的方式。

9781430250920_Fig04-03.jpg

图 4-3 。HTML 文档的结构

认清 HTML 的本质是非常重要的:HTML 是结构化的内容,而不是像图像那样的视觉结构,图像中的元素放置在不同的坐标上。当你有一个合适的、有效的 HTML 文档时,你可以通过 JavaScript 访问和修改它。不管你做了多少测试,一个无效的 HTML 文档可能会使你的脚本出错。一个典型的错误是在一个文档中两次使用同一个 id 属性值,这违背了拥有惟一标识符(ID)的目的。

通过 JavaScript 在网页中提供反馈:老办法

您已经看到了一种方法——document . write()—通过写出内容在 HTML 文档中向用户提供反馈。我们还讨论了这种方法存在的问题——也就是说,混合了结构和表示层,失去了将所有 JavaScript 代码保存在单独文件中的维护优势。

使用窗口方法:prompt()、alert()和 confirm()

给出反馈和检索用户输入数据的另一种方式是使用浏览器通过窗口对象提供的方法——即 prompt()、alert()和 confirm()。

最常用的窗口方法是 alert(),图 4-4 给出了一个例子。它的作用是在对话框中显示一个值(如果用户的硬件支持的话,可能还会播放声音)。用户必须单击确定或按回车键来删除该消息。

9781430250920_Fig04-04.jpg

图 4-4 。JavaScript 警告(在 Mac OS 的 Firefox 上)

不同浏览器和不同操作系统的警报看起来不同。

作为一种用户反馈机制,alert()具有被大多数用户代理支持的优点,但它也阻止你与页面上的任何其他东西进行交互——你称这样的窗口为模态。一个警报是一个通常带来坏消息或者警告某人前方有危险的消息——这不一定是你的意图。

假设您想告诉访问者在提交表单之前在搜索字段中输入一些内容。你可以用一个警告:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Search example</title>
<script type="text/javascript">
      function checkSearch()
      {

if(!document.getElementById("search")){return;}
var searchValue=document.getElementById("search").value;
        if(searchValue=='')
        {
          alert("Please enter a search term");
          return false;
        }
        else
        {
          return true;

        }
      }
</script>
</head>
<body>
<form action="sitesearch.php" method="post"
 onsubmit="return checkSearch();">
<p>
<label for="search">Search the site:</label>
<input type="text" id="search" name="search" />
<input type="submit" value="Search" />
</p>
</form>
</body>
</html>

如果访问者试图通过 submit 按钮提交表单,他会收到警告,并且在他激活 OK 按钮后,浏览器不会将表单发送到服务器。Mac OS 上的 Chrome 看起来就像你在图 4-5 中看到的一样。

9781430250920_Fig04-05.jpg

图 4-5 。通过警告对表单错误给出反馈

警报不会向脚本返回任何信息,它们只是向用户提供一条消息,并停止任何进一步的代码执行,直到 OK 按钮被激活。

这对于提示()和确认()是不同的。前者允许访问者输入一些东西,后者要求用户确认一个动作。

image 提示作为一种调试手段,alert()简直太得心应手了,不能不使用。您所做的只是在代码中添加一个警告(variableName ),在那里您想知道当时的变量值是什么。您将获得信息并停止执行其余的代码,直到 OK 按钮被激活——这对于追溯脚本失败的位置和原因非常有用。不过,要小心在循环中使用它——没有办法停止循环,而且你可能要按下 Enter 键一百次才能回到编辑状态。还有其他调试工具,如 Opera 和 Safari 的 JavaScript 控制台,以及 Mozilla。这些我会在附录里多说。

您可以扩展前面的示例,如下面的代码示例所示,要求访问者确认对常用术语 JavaScript 的搜索(结果如图图 4-6 所示):

function checkSearch()
{

  if(!document.getElementById("search'"){return;}
  var searchValue=document.getElementById("search").value;
  if(searchValue=='')
  {
   alert("Please enter a search term before sending the form");
   return false;
  }
  else if(searchValue=="JavaScript")
  {
    var really=confirm('"JavaScript" is a very common term.\n' +'Do you really want to search for this?');
    return really;
  }
  else
  {
    return true;
  }
}

9781430250920_Fig04-06.jpg

图 4-6 。通过 confirm()请求用户确认的示例

注意 confirm() 是一个根据访问者激活 OK 还是 Cancel 返回布尔值(true 或 false)的方法。确认对话框是阻止访问者在 web 应用中采取非常糟糕的步骤的简单方法。虽然它们不是让用户确认选择的最好方式,但是它们非常稳定,并且提供了一些你自己的确认功能可能没有的功能,例如,播放提醒声音。

alert()和 confirm()都向用户发送信息,但是检索信息呢?检索用户输入的一种简单方法是通过 prompt()方法。这个方法有两个参数:第一个是作为标签显示在输入字段上方的字符串,第二个是输入字段的预设值。标签为 OK 和 Cancel(或类似的东西)的按钮将显示在字段和标签旁边,如图 4-7 中的所示。

var user=prompt('Please choose a name','User12');

9781430250920_Fig04-07.jpg

图 4-7 。允许用户在提示中输入数据

当访问者激活 OK 按钮时,变量 user 的值将是 User12(当她没有改变预置时)或她输入的任何值。当她激活取消按钮时,该值将为空。

您可以使用此功能允许访问者在向服务器发送表单之前更改值:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Date example</title>
<script type="text/javascript">
      function checkDate()
      {
        if(!document.getElementById("date")){return;}
        // Define a regular expression to check the date format
        var checkPattern=newRegExp("\\d{2}/\\d{2}/\\d{4}");
        // Get the value of the date entry field
        var dateValue=document.getElementById("date").value;
        // If there is no date entered, don't send the form
        if(dateValue=='')
        {
          alert("Please enter a date");
          return false
        }
        else
        {
          // Tell the user to change the date syntax either until
          // she presses Cancel or enters the right syntax
          while(!checkPattern.test(dateValue) && dateValue!=null)
          {
            dateValue=prompt("Your date was not in the right format." + "Please enter it as DD/MM/YYYY.", dateValue);
          }
          return dateValue!=null;
        }
      }
</script>
</head>
<body>
<h1>Events search</h1>
<form action="eventssearch.php" method="post" onsubmit="return checkDate();">
<p>
<label for="date">Date in the format DD/MM/YYYY:</label><br>
<input type="text" id="date" name="date">
<input type="submit" value="Check">
<br>(example 26/04/1975)
</p>
</form>
</body>
</html>

如果正则表达式和 test()方法此刻让您感到困惑,也不用担心;这些将在第九章中介绍。现在重要的是使用一个 while 循环,其中包含一个 prompt()。while 循环反复显示相同的提示,直到访问者按下 Cancel(这意味着 dateValue 变为 null)或者以正确的格式输入日期(这满足正则表达式 checkPattern 的测试条件)。

快速回顾

您可以使用 prompt()、alert()和 confirm()方法创建非常漂亮的 JavaScripts,它们有一些优点:

  • 它们很容易掌握,因为它们使用了浏览器的功能、外观和感觉,并提供了比 HTML 更丰富的界面。(具体来说,当出现提示音时,可以帮助很多用户。)
  • 它们出现在当前文档之外和之上,这赋予了它们最大的重要性。

但是,有些观点反对使用这些方法来检索数据和提供反馈:

  • 你不能设计消息的样式,它们会阻碍网页。这确实给了它们更多的重要性,但是从设计的角度来看,这也让它们显得笨拙。因为它们是用户操作系统或浏览器 UI 的一部分,所以用户很容易识别它们,但是它们打破了产品可能必须遵守的设计惯例和准则。
  • 反馈机制不像网站那样具有相同的外观和感觉——这使得网站设计变得不那么重要,并且阻止了用户通过你的可用性增强设计元素的旅程。
  • 它们依赖于 JavaScript——当 JavaScript 关闭时,反馈也应该可用。

通过 DOM 访问文档

除了您现在知道的窗口方法,您还可以通过 DOM 访问 web 文档。从某种意义上来说,你已经用 document.write()例子做到了。文档对象是您想要更改和添加的对象,使用 write()是一种方法。然而,document.write()向文档中添加一个字符串,而不是一组节点和属性,并且您不能将 JavaScript 分离到一个单独的文件中——document . write()仅在您将其放入 HTML 中的位置起作用。您需要的是一种到达您想要更改或添加内容的地方的方法,这正是 DOM 及其方法为您提供的。前面,您发现用户代理将文档作为一组节点和属性来读取,DOM 为您提供了获取这些节点和属性的工具。您可以通过三种方法访问文档的元素:

  • document . get element sbytag name(' p ')。
  • document.getElementById('id ')文件
  • document . get element sbyclasse name(' cssclass ')。

getElementsByTagName('p') 方法返回名为 p 的所有元素的列表作为对象(其中 p 可以是任何 HTML 元素),getElementById('id ')返回 Id 为的元素作为对象。第三个方法 getElementsByClassName(' CSS class ')返回使用该类名的所有元素。

image 注意关于 getElementsByClassName()的一个有趣的事情是,你可以检索使用多个类的元素。例如,getElementsByClassName("one two ")只检索一起使用这些类的元素。请记住,从版本 9 开始,这种方法在 IE 中可用。

如果回到我们之前使用的 HTML 示例,您可以编写一个小的 JavaScript 示例来展示如何使用这两种方法:

<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>DOM Example</title>
<script src="exampleFindElements.js"></script>
</head>
<body>
<h1>Heading</h1>
<p>Paragraph</p>
<h2>Subheading</h2>
<ul id="eventsList">
<li>List 1</li>
<li>List 2</li>
<li><a href="http://www.google.com">Linked List Item</a></li>
<li>List 4</li>
</ul>
<p class='paraStyle'>Paragraph</p>
<p class='paraStyle'>Paragraph</p>
</body>
</html>

我们的脚本现在可以通过调用 getElementsByTagName()方法读取文档中列表项和段落的数量,并将返回值赋给变量——一次用标记名 li,另一次用标记名 p。

var listElements=document.getElementsByTagName('li');
var paragraphs=document.getElementsByTagName('p');
var msg='This document contains '+listElements.length+' list items\n';
msg+='and '+paragraphs.length+' paragraphs.';
alert(msg);

如图图 4-8 所示,如果在浏览器中打开 HTML 文档,会发现两个值都为零!

9781430250920_Fig04-08.jpg

图 4-8 。在呈现页面之前尝试访问元素时出现不需要的结果

没有任何列表元素,因为当您尝试读取文档内容时,浏览器尚未呈现该文档。您需要延迟读取,直到文档被完全加载和呈现。

当文档完成加载时,您可以通过调用一个函数来实现这一点。当 document 对象的 DOMContentLoaded 事件被触发时,文档已完成加载。这个事件告诉 JavaScript 页面的结构(DOM)已经准备好让代码使用了,当它准备好了,就调用这个函数。与 getElementsByClassName 类似,DOMContentLoaded 从版本 9 开始在 IE 中工作。

functionfindElements()
{
  var listElements = document.getElementsByTagName('li');
  var paragraphs = document.getElementsByTagName('p');
  var msg = 'This document contains ' + listElements.length +
' list items\n';
  msg += 'and ' + paragraphs.length + ' paragraphs.';
  alert(msg);
}

document.addEventListener("DOMContentLoaded", findElements, false);

如果你现在在浏览器中打开 HTML 文档,你会看到一个类似于图 4-9 中的警告,带有正确数量的列表元素和段落。

9781430250920_Fig04-09.jpg

图 4-9 。指示找到的元素数量的输出警报

您可以像访问数组一样访问某个名称的每个元素—再次记住数组的计数器从 0 开始计数,而不是从 1 开始计数:

// Get the first paragraph
var firstpara = document.getElementsByTagName('p')[0];
// Get the second list item
var secondListItem = document.getElementsByTagName('p')[1];

您可以组合几个 getElementsByTagName()方法调用来直接读取子元素。例如,要到达第三个列表项中的第一个链接项,可以使用

var targetLink=document.getElementsByTagName('li')[2].getElementsByTagName('a')[0];

不过,这可能会变得相当混乱,还有更聪明的方法来访问子元素——我们马上就要谈到这些。如果要到达最后一个元素,可以使用数组的 length 属性:

var lastListElement = listElements[listElements.length - 1];

length 属性还允许您循环遍历元素并逐个更改所有元素:

var linkItems = document.getElementsByTagName('li');
for(var i = 0; i < linkItems.length; i++)
{
  // Do something...
}

元素 id 需要对文档是唯一的;因此,getElementById()的返回值是单个对象,而不是对象数组:

var events = document.getElementById('eventsList');

您可以混合使用这两种方法来减少要循环的元素数量。虽然前面的 for 循环访问文档中的所有 LI 元素,但是这个循环将只遍历 ID 为 eventsList 的元素中的元素(ID 为的对象的名称将替换 document 对象):

var events = document.getElementById('eventsList');
var eventlinkItems = events.getElementsByTagName('li');
for(var i = 0; i < eventLinkItems.length; i++)
{
  // Do something...
}

可以使用 getElementsByTagName()来访问基于文档中使用的 CSS 类的元素。就像前面的方法一样,您可以像访问数组一样访问结果:

var firstClass = document.getElementsByClassName('paraStyle')[0];

var classNum = document.getElementsByClassName('paraStyle').length;

在 getElementsByTagName()、getElementById()和 getElementsByClassName()的帮助下,您可以访问文档的每个元素,或者专门针对一个元素。方法 getElementById()和 getElementsByClassName()是 document 的方法,getElementsByTagName()是 any 元素的方法。现在是时候看看到达元素后如何在文档中导航了。

孩子、父母、兄弟姐妹和价值观

您已经知道,可以通过连接 getElementsByTagName 方法来访问其他元素内部的元素。然而,这是相当麻烦的,这意味着你需要知道你正在改变的 HTML。有时这是不可能的,您必须找到一种更通用的方法来浏览 HTML 文档。DOM 已经通过孩子父母兄弟姐妹为此做好了计划。

这些关系描述了当前元素在树中的位置以及它是否包含其他元素。让我们再看一次简单的 HTML 例子,集中在文档的主体上:

<body>
<h1>Heading</h1>
<p>Paragraph</p>
<h2>Subheading</h2>
<ul id="eventsList">
<li>List 1</li>
<li>List 2</li>
<li><a href="http://www.google.com">Linked List Item</a></li>
<li>List 4</li>
</ul>
<p class="paraStyle">Paragraph</p>
<p class="paraStyle">Paragraph</p>
</body>

所有缩进的元素都是主体的子元素。H1、、和 P 是兄弟姐妹,而 LI 元素是元素的子元素——彼此也是兄弟姐妹。链接是第三个 LI 元素的子元素。总而言之,他们是一个快乐的大家庭。

但是,孩子就更多了。段落、标题、列表元素和链接中的文本也由节点组成,你可能还记得前面的图 4-2 中的内容,虽然它们不是元素,但是它们仍然遵循相同的关系规则。

文档中的每个节点都有几个有价值的属性:

  • 最重要的属性是 nodeType,它描述了节点是什么——一个元素、一个属性、一个注释、文本或者其他几种类型中的一种(总共 12 种)。对于我们的 HTML 示例,只有 nodeType 值 1 和 3 是重要的,其中 1 是元素节点,3 是文本节点。
  • 另一个重要的属性是 nodeName,,它是元素的名称,如果是文本节点,则是#text。根据文档类型和用户代理,nodeName 可以是大写或小写,这就是为什么在测试某个名称之前将其转换为小写是个好主意。您可以使用 string 对象的 toLowerCase()方法来实现:if(obj . nodename . toLowerCase()= = ' Li '){ };。对于元素节点,您可以使用 tagName 属性。
  • nodeValue 是节点的值:如果是元素则为 null,如果是文本节点则为文本内容。

对于文本节点,可以读取和设置 nodeValue,这允许您改变元素的文本内容。例如,如果您想要更改第一段的文本,您可能认为设置它的 nodeValue 就足够了:

document.getElementsByTagName('p')[0].nodeValue='Hello World';

然而,这是行不通的(尽管——奇怪的是——它不会导致错误),因为第一段是一个元素节点。如果您想要更改段落内的文本,您需要访问段落内的文本节点,换句话说,就是段落的第一个子节点:

document.getElementsByTagName('p')[0].firstChild.nodeValue='Hello World';

从父母到孩子

firstChild 属性是一个快捷方式。每个元素可以有任意数量的子元素,列在一个名为 childNodes 的属性中。关于子节点,需要记住以下几点:

  • childNodes 是元素的所有第一级子节点的列表,它不会向下级联到更深的级别。
  • 可以通过数组计数器或 item()方法访问当前元素的子元素。
  • 快捷方式属性 yourElement.firstChild 和 yourElement.lastChild 是 yourElement.childNodes[0]和 your element . child nodes[your element . child nodes . length-1]的简化版本,可以更快地访问它们。
  • 您可以通过调用 hasChildNodes()方法来检查元素是否有任何子元素,该方法返回一个布尔值。

回到前面的例子,您可以访问 UL 元素并获取有关其子元素的信息,如下所示:

HTML

<ul id="eventsList">
<li>List 1</li>
<li>List 2</li>
<li><a href="http://www.google.com">Linked List Item</a></li>
<li>List 4</li>
</ul>

JavaScript

function myDOMinspector()
{
  var DOMstring='';
var demoList=document.getElementById('eventsList');
if (!demoList){return;}
  if(demoList.hasChildNodes())
  {
    var ch=demoList.childNodes;
    for(var i=0;i<ch.length;i++)
    {
      DOMstring+=ch[i].nodeName+'\n';
    }
    alert(DOMstring);
  }
}

创建一个名为 DOMstring 的空字符串,并检查 DOM 支持以及是否定义了具有正确 id 属性的 UL 元素。然后测试该元素是否有子节点,如果有,将它们存储在一个名为 ch 的变量中。您循环遍历变量(该变量自动成为一个数组),并将每个子节点的 nodeName 添加到 DOMString,后跟一个换行符(\n)。然后使用 alert()方法查看结果。

如果你在浏览器中运行这个脚本,你会看到四个 LI 元素,元素之间的换行符作为文本节点,如图 4-10 中的所示。

9781430250920_Fig04-10.jpg

图 4-10 。我们的脚本找到的节点,包括实际上是换行符的文本节点

从孩子到父母

您还可以通过 parentNode 属性从子元素导航回其父元素。首先,让我们为您添加一个 ID:,让您更容易地找到链接

<ul id="eventsList">
<li>List</li>
<li>List</li>
<li>
<aid="linkedItem"href="http://www.google.com">
Linked List Item
</a>
</li>
<li>List</li>
</ul>

现在给 link 对象分配一个变量,并读取父节点的名称:

var myLinkItem=document.getElementById("linkedItem");
alert(myLinkItem.parentNode.nodeName);

结果是 LI,如果将另一个 parentNode 添加到对象引用中,将得到,这是链接的祖父元素:

alert(myLinkItem.parentNode.parentNode.nodeName);

您可以根据需要添加任意数量的父元素,也就是说,如果文档树中还有父元素,而您还没有到达顶层。如果在循环中使用 parentNode,那么测试 nodeName 并在循环到达主体时结束循环是很重要的。比方说,你想检查一个对象是否在一个动态类的元素中。您可以使用 while 循环来实现这一点:

var myLinkItem = document.getElementById("linkedItem");
var parentElm = myLinkItem.parentNode;
while(parentElm.className != "dynamic")
{
  parentElm = parentElm.parentNode;
}

但是,当没有具有正确类的元素时,此循环将导致“TypeError:无法读取 null 的属性' className'”错误。如果让循环在主体处停止,就可以避免这个错误:

var myLinkItem = document.getElementById('linkedItem');
var parentElm = myLinkItem.parentNode;
while(!parentElm.className != 'dynamic'&& parentElm != document.body')
{
  parentElm=parentElm.parentNode;
}
alert(parentElm);

在兄弟姐妹中

家族类比继续使用兄弟姐妹,是同一级别上的元素。(不过,它们不会像兄弟姐妹一样有不同的性别。)您可以通过节点的 previousSibling 和 nextSibling 属性到达同一级别的不同子节点。让我们回到我们的列表示例:

<ul id="eventsList">
<li>List Item 1</li>
<li>List Item 2</li>
<li>
<a id="linkedItem" href="http://www.google.com/">
      Linked List Item
</a>
</li>
<li>List Item 4</li>
</ul>

您可以通过 getElementById()获得链接,并通过 parentNode 获得包含链接的 LI。属性 previousSibling 和 nextSibling 允许您分别获取列表项 2 和列表项 3:

var myLinkItem = document.getElementById("linkedItem");
var listItem = myLinkItem.parentNode;
var nextListItem = myLinkItem.nextSibling;
var prevListItem = myLinkItem.previousSibling;

如果当前对象是父元素的最后一个子元素,则 nextSibling 将是未定义的,如果没有正确测试它,将会导致错误。与 childNodes 不同,第一个和最后一个兄弟节点没有快捷方式属性,但是您可以编写实用程序方法来查找它们。例如,假设您想在我们的演示 HTML 中找到第一个和最后一个 LI:

document.addEventListener("DOMContentLoaded",init,false);

function init()
{
  var myLinkItem=document.getElementById("linkedItem");
  var first=firstSibling(myLinkItem.parentNode);
  var last=lastSibling(myLinkItem.parentNode);
  alert(getTextContent(first));
  alert(getTextContent(last));
}
function lastSibling(node){
  var tempObj=node.parentNode.lastChild;
  while(tempObj.nodeType!=1 && tempObj.previousSibling!=null)
  {
    tempObj=tempObj.previousSibling;
  }
  return (tempObj.nodeType==1)?tempObj:false;
}
function firstSibling(node)
{
  var tempObj=node.parentNode.firstChild;
  while(tempObj.nodeType!=1 && tempObj.nextSibling!=null)
  {
    tempObj=tempObj.nextSibling;
  }
  return (tempObj.nodeType==1)?tempObj:false;
}
function getTextContent(node)
{
  return node.firstChild.nodeValue;
}

请注意,您需要检查 nodeType,因为 parentNode 的最后一个或第一个子节点可能是文本节点,而不是元素。

让我们通过使用 DOM 方法来提供文本反馈,而不是向用户发送警告,从而使我们的日期检查脚本不那么显眼。首先,您需要一个容器来显示您的错误消息:

<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Date example</title>
<style type="text/css">
      .error{color:#c00;font-weight:bold;}
</style>
<script type="text/javascript" src="checkdate.js"></script>
</head>
<body>
<h1>Events search</h1>
<form action="eventssearch.php" method="post"onsubmit="return checkDate();">
<p>
<label for="date">Date in the format DD/MM/YYYY:</label><br />
<input type="text" id="date" name="date" />
<input type="submit" value="Check " />
<br />(example 26/04/1975)
<span class="error"></span>
</p>
</form>
</body>
</html>

检查脚本或多或少与以前保持相同——区别在于您使用 SPAN 作为显示错误的手段:

function checkDate(){
  var dateField=document.getElementById('date');
  if(!dateField){return;}
  var errorContainer=dateField.parentNode.getElementsByTagName("span")[0];
  if(!errorContainer){return;}
  var checkPattern=new RegExp("\\d{2}/\\d{2}/\\d{4}");
  var errorMessage='';
  errorContainer.firstChild.nodeValue='"";
  var dateValue=dateField.value;
  if(dateValue=='')
  {
    errorMessage="Please provide a date.";
  }
  else if(!checkPattern.test(dateValue))
  {
    errorMessage="Please provide the date in the defined format.";
  }
  if(errorMessage!='')
  {
    errorContainer.firstChild.nodeValue=errorMessage;
    dateField.focus();
    return false;
  }
  else
  {
    return true;
  }
}

首先,测试是否所有需要的元素都存在:

var dateField=document.getElementById("date");
  if(!dateField){return;}
  var errorContainer=dateField.parentNode.getElementsByTagName("span")[0];
  if(!errorContainer){return;}

然后定义测试模式和空错误信息。将误差范围的文本值设置为一个空格。这对于避免访问者在没有纠正错误的情况下第二次发送表单时显示多个错误消息是必要的:

  var checkPattern=new RegExp("\\d{2}/\\d{2}/\\d{4}");
  var errorMessage='';
  errorContainer.firstChild.nodeValue=" ";

接下来是字段的验证。读取日期字段的值,并检查是否有条目。如果没有条目,错误消息将是访问者应该输入一个日期。如果有一个日期,但是格式错误,消息将会指出。

var dateValue=dateField.value;
  if(dateValue=='')
  {
    errorMessage="Please provide a date.";
  }
  else if(!checkPattern.test(dateValue))
  {
    errorMessage="Please provide the date in the defined format.";
  }

那么剩下的就是检查最初的空错误消息是否被修改了。如果没有改变,脚本应该返回 true,形式为 on submit = " return checkDate();"—从而提交表单并允许后端接管工作。如果更改了错误消息,脚本会将错误消息添加到错误范围的文本内容(第一个子节点的 nodeValue)中,并将文档的焦点设置回日期输入字段,而不提交表单:

if(errorMessage!='')
{
  errorContainer.firstChild.nodeValue+=errorMessage;
  dateField.focus();
  return false;
}
else
{
  return true;
}

如图 4-11 所示,结果比警告信息更具视觉吸引力,你可以随心所欲地设计它。

9781430250920_Fig04-11.jpg

图 4-11 。显示动态错误消息

现在您知道了如何访问和更改现有元素的文本值。但是如果你想改变其他属性或者你需要不一定为你提供的 HTML 呢?

更改元素的属性

一旦您找到了想要更改的元素,您可以通过两种方式读取和更改它的属性:一种较老的方式,让您直接与元素对话;另一方面,使用 DOM 方法。

旧版本和新版本的用户代理允许您将元素属性作为对象属性进行获取和设置:

var firstLink=document.getElementsByTagName("a")[0];
if(firstLink.href=="search.html")
{
  firstLink.href="http://www.google.com";
}
var mainImage=document.getElementById('nav').getElementsByTagName('img')[0];
mainImage.src="dynamiclogo.jpg";
mainImage.alt="Generico Corporation - We do generic stuff";
mainImage.title="Go back to Home";

HTML 规范中定义的所有属性都是可用的,并且可以被访问。出于安全原因,有些是只读的,但大多数是可以设置和读取的。您也可以提出自己的属性——JavaScript 并不介意。有时候,在元素的属性中存储一个值可以省去很多测试和循环。

image 注意当心与 JavaScript 命令同名的属性——例如,for。如果尝试设置 element.for='something ',将会导致错误。浏览器供应商想出了解决办法。对于 for——它是 label 元素的一个有效属性——属性名是 htmlFor。更奇怪的是 class 属性——在下一章中你会用到很多。这是一个保留字;您需要改用类名。

DOM 规范提供了两种读取和设置属性的方法——get attribute()和 setAttribute()。getAttribute()方法有一个参数——属性名。setAttribute()方法有两个参数:属性名和新值。

使用较新方法的早期示例如下所示:

var firstLink=document.getElementsByTagName("a")[0];
if(firstLink.getAttribute('href')=='search.html')
{
  firstLink.setAttribute('href')="http://www.google.com";
}
var mainImage=document.getElementById("nav").getElementsByTagName("img")[0];
mainImage.setAttribute("src")="dynamiclogo.jpg";
mainImage.getAttribute("alt") ="Generico Corporation - We do generic stuff";
mainImage.getAttribute("title"
)="Go back to Home";

这可能看起来有点多余和臃肿,但好处是它与其他更高级的编程语言更加一致。与将属性分配给元素的属性方式相比,它更有可能被未来的用户代理支持,并且它们可以轻松地处理任意的属性名称。

创建、删除和替换元素

DOM 还提供了在 HTML/JavaScript 环境中改变文档结构的方法。(如果通过 JavaScript 做 XML 转换的话还有更多。)您不仅可以更改现有的元素,还可以创建新元素以及替换或删除旧元素。这些方法如下:

  • document . createElement(' element '):创建一个标记名为 element 的新元素节点。
  • document . create text node(' string '):创建一个节点值为 string 的新文本节点。
  • Node.appendChild(newNode):将 newNode 作为新的子节点添加到 node,跟在 node 的任何现有子节点后面。
  • newNode=Node.cloneNode(bool):创建 newNode 作为 node 的副本(克隆)。如果 bool 为 true,则克隆包括原始节点的所有子节点和属性的克隆。
  • node.insertBefore(newNode,oldNode):在 oldNode 之前插入 newNode 作为 node 的新的子节点。
  • node.removeChild(oldNode):从节点中删除子 oldNode。
  • node.replaceChild(newNode,oldNode):用 newNode 替换 node 的子节点 oldNode。
  • node.nodeValue:返回当前节点的值。

image 注意【createElement()和 createTextNode()都是 document 的方法;其他的都是任意节点的方法。

当您想要创建由 JavaScript 增强但不完全依赖它的 web 产品时,所有这些都是不可或缺的。除非您通过警告、确认和提示弹出窗口向所有用户提供反馈,否则您将不得不依赖提供给您的 HTML 元素——就像前面示例中的错误消息 SPAN。然而,因为您的 JavaScript 需要的 HTML 只有在启用 JavaScript 时才有意义,所以当没有脚本支持时它不应该可用。额外的跨度不会伤害任何人——但是,为用户提供强大功能的表单控件(如日期选择器工具)却不能工作,这是一个问题。

让我们在下一个例子中使用这些方法。我们将用一个链接替换一个提交按钮。

链接更好,因为你可以用 CSS 样式化它们。它们可以调整大小,并且可以很容易地从本地化的数据集中填充。然而,链接的问题是,你需要 JavaScript 来提交表单。这就是为什么懒惰或太忙的开发人员对这种困境的答案是一个链接,它使用 javascript:协议简单地提交表单(许多代码生成器或框架都会提供相同的功能):

<a href="javascript:document.forms[0].submit()">Submit</a>

在本书的前面,我已经确定这不是一个选项——因为如果没有 JavaScript,这将是一个死链接,并且没有任何提交表单的方法。如果您想两全其美——为非 JavaScript 用户提供一个简单的提交按钮,为启用脚本的用户提供一个链接——您需要做以下事情:

  1. 遍历文档的所有输入元素。
  2. 测试类型是否为提交。
  3. 如果不是这样,继续循环,跳过其余部分。
  4. 如果是这种情况,创建一个带有文本节点的新链接。
  5. 将文本节点的节点值设置为输入元素的值。
  6. 将链接的 href 设置为 javascript:document.forms[0]。submit(),它允许您在单击链接时提交表单。
  7. 用链接替换输入元素。

image 注意将 href 属性设置为 javascript:构造并不是最干净的方式。在下一章中,您将了解事件处理程序——实现这一解决方案的更好的方法。

在代码中,这可能是

<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Example: Submit buttons to links</title>
<style type="text/css"></style>
<script type="text/javascript" src="submitToLinks.js"></script>
</head>
<body>
<form action="nogo.php" method="post">
<label for="Name">Name:</label>
<input type="text" id="Name" name="Name" />
<input type="submit" value="send" />
</form>
</body>
</html>
function submitToLinks()
{
var inputs,i,newLink,newText;
  inputs=document.getElementsByTagName('input');
  for (i=0;i<inputs.length;i++)
  {
    if(inputs[i].getAttribute('type').toLowerCase()!='submit')
 {continue;i++}
    newLink=document.createElement('a');
    newText=document.createTextNode(inputs[i].getAttribute('value'));
    newLink.appendChild(newText);
    newLink.setAttribute('href','javascript:document.forms[0]
 .submit()');
    inputs[i].parentNode.replaceChild(newLink,inputs[i]);
  }
}
document.addEventListener("DOMContentLoaded", submitToLinks,false);

当 JavaScript 可用时,访问者获得一个提交表单的链接;否则,他会得到一个提交按钮,如图图 4-12 所示。

9781430250920_Fig04-12.jpg

图 4-12 。根据 JavaScript 的可用性,用户可以获得一个链接或按钮来发送表单

然而,该函数有一个主要缺陷:当 Submit 按钮后面有更多的输入元素时,它将失败。将 HTML 更改为在提交按钮后有另一个输入:

<form action="nogo.php" method="post">
<p>
<label for="Name">Name:</label>
<input type="text" id="Name" name="Name" />
<input type="submit" value="check" />
<input type="submit" value="send" />
</p>
<p>
<label for="Email">email:</label>
<input type="text" id="Email" name="Email" />
</p>
</form>

您将看到“发送”提交按钮没有被替换为链接。发生这种情况是因为您删除了一个 input 元素,这改变了数组的大小,并且循环与它应该到达的元素不同步。解决这个问题的方法是每次删除一个项目时减少循环计数器。但是,您需要通过比较循环计数器和数组的长度来检查循环是否已经在最后一次迭代中(简单地减少计数器会导致脚本失败,因为它试图访问一个不存在的元素):

function submitToLinks()
{

  var inputs,i,newLink,newText;
  inputs=document.getElementsByTagName('input');
  for (i=0;i<inputs.length;i++)
  {
    if(inputs[i].getAttribute('type').toLowerCase()!='submit')
 {continue;i++}
    newLink=document.createElement('a');
    newText=document.createTextNode(inputs[i].getAttribute('value'));
    newLink.appendChild(newText);
    newLink.setAttribute('href','javascript:document.forms[0]
 .submit()');
    inputs[i].parentNode.replaceChild(newLink,inputs[i]);
    if(i<inputs.length){i--};
  }
}
document.addEventListener("DOMContentLoaded", submitToLinks,false);

这个版本的脚本不会失败,它用链接替换了两个按钮。

image 注意这个脚本会破坏表单的一个可用性方面:当表单中有提交按钮时,您可以通过按 Enter 按钮来提交表单。当您删除所有提交按钮时,这将不再可能。解决方法是添加一个空白图像按钮或隐藏提交按钮,而不是删除它们。我将在下一章回到这个选项。另一个可用性问题是您是否应该改变表单的外观——因为您失去了表单元素的即时可识别性。访问者已经习惯了表单在他们的浏览器和操作系统上的外观——如果你改变这一点,他们将不得不寻找交互元素,并可能期望其他功能。人们信任包含个人数据和货币交易的表单——任何可能使他们困惑的事情都很容易被认为是安全问题。

避免 NOSCRIPT

SCRIPT 元素在 NOSCRIPT 中有对应的元素。这个元素的初衷是在 JavaScript 不可用时为访问者提供替代内容。语义 HTML 不鼓励在文档中使用脚本块。(正文应该只包含有助于文档结构的元素,而脚本不会这样做。)NOSCRIPT 已被弃用。但是,您会在 Web 上找到许多辅助功能教程,它们提倡使用 NOSCRIPT 作为一种安全措施。简单地在页面的< noscript >标签中添加一条消息,解释你将需要 JavaScript 来最大限度地使用网站,这似乎是解决问题的一个非常简单的方法。这就是为什么许多开发人员不赞成 W3C 反对 NOSCRIPT,或者干脆牺牲 HTML 的有效性。但是,通过使用 DOM 方法,您可以解决这个问题。

在一个完美的世界里,不会有任何网站需要 JavaScript 来工作——只有在脚本可用的情况下工作更快、有时更容易使用的网站。然而,在现实世界中,有时您将不得不使用现成的产品或框架,它们只是生成依赖于脚本的代码。当重新设计或替换那些不太显眼的系统不可行时,你可能想告诉访问者,他们需要脚本来使站点工作。

使用 NOSCRIPT,这可以非常简单地完成:

<script type="text/javascript">myGreatApplication();</script>
<noscript>
  Sorry but you need to have scripting enabled to use this site.
</noscript>

这样的消息没有太大的帮助,至少您应该允许不能启用脚本的访问者(例如,银行和金融公司的工作人员,他们因为脚本会带来安全威胁而关闭脚本)与您联系。

现代脚本从另一方面解决了这个问题:我们给出一些信息,并在脚本可用时替换它。对于依赖脚本的应用,这可能是

<p id="noscripting">
  We are sorry, but this application needs JavaScript
  to be enabled to work. Please <a href="contact.html">contact us</a>
  If you cannot enable scripting and we will try to help you in other
  ways.
</p>

然后编写一个脚本,简单地删除它,甚至利用这个机会同时测试 DOM 支持:

<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Example: Replacing noscript</title>
<script type="text/javascript">
      function noscript()
      {
        if(!document.getElementById || !document.createTextNode){return;}
        // Add more tests as needed (cookies, objects...)
        var noJSmsg=document.getElementById('noscripting');
        if(!noJSmsg){return;}
        var headline='Browser test succeeded';
        replaceMessage='We tested if your browser is capable of ';
        replaceMessage+='supporting the application, and all checkedout fine. ';
        replaceMessage+='Please proceed by activating the following link.';
        var linkMessage='Proceed to application.';
        var head=document.createElement('h1');
        head.appendChild(document.createTextNode(headline));
        noJSmsg.parentNode.insertBefore(head,noJSmsg);
var infoPara=document.createElement('p');
        infoPara.appendChild(document.createTextNode(replaceMessage));
        noJSmsg.parentNode.insertBefore(infoPara,noJSmsg);
        var linkPara=document.createElement('p');
        var appLink=document.createElement('a');
        appLink.setAttribute('href','application.aspx');
        appLink.appendChild(document.createTextNode(linkMessage));
        linkPara.appendChild(appLink);
        noJSmsg.parentNode.replaceChild(linkPara,noJSmsg);
}
      document.addEventListener("DOMContentLoaded", noscript,false);
</script>
</head>
<body>
<p id="noscripting">
      We are sorry, but this application needs JavaScript to be
      enabled to work. Please <a href="contact.html">contact us</a>
      if you cannot enable scripting and we will try to help you in
      other ways
</p>
</body>
</html>

您可以看到,通过 DOM 生成大量内容相当麻烦,这就是为什么在这种情况下——您真的不需要将生成的每个节点都作为变量——许多开发人员使用 innerHTML。

通过 InnerHTML 缩短你的脚本

微软在开发 Internet Explorer 的早期就实现了非标准属性 innerHTML。现在大部分浏览器都支持;甚至有人谈到将它加入 DOM 标准。它允许你做的是定义一个包含 HTML 的字符串并将它赋给一个对象。然后,用户代理会为您完成剩下的工作—所有的节点生成和子节点的添加。使用 innerHTML 的 NOSCRIPT 示例要短得多:

<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Example: Replacing noscript</title>
<script type="text/javascript">
      function noscript()
      {
        if(!document.getElementById || !document.createTextNode){return;}
        // Add more tests as needed (cookies, objects...)
        var noJSmsg=document.getElementById('noscripting');
        if(!noJSmsg){return;}
        var replaceMessage='<h1>Browser test succeeded</h1>';
        replaceMessage='<p>We tested if your browser is capable of ';
        replaceMessage+='supporting the application, and all checkedout fine. ';
        replaceMessage+='Please proceed by activating the following
 link.</p>';
        replaceMessage+='<p><a href="application.aspx">Proceed to application</a></p>';
        noJSmsg.innerHTML=replaceMessage;
      }
document.addEventListener("DOMContentLoaded", noscript,false);

</script>
</head>
<body>
<p id="noscripting">
      We are sorry, but this application needs JavaScript to be
      enabled to work. Please <a href="contact.html">contact us</a>
      if you cannot enable scripting and we will try to help you in
      other ways.
</p>
</body>
</html>

您还可以读出元素的 innerHTML 属性,这在调试代码时非常方便,因为并非所有浏览器都有“视图生成源”特性。用其他 HTML 替换整个 HTML 也很容易,当我们显示通过 Ajax 从后端检索的内容时,我们经常这样做。

DOM 摘要:您的备忘单

那是很难接受的。将您需要的所有 DOM 特性都放在一个地方进行复制并放在手边可能会比较好,所以您可以这样做:

  • 到达文档中的元素

  • document.getElementById('Id '):以对象形式检索具有给定 id 的元素

  • document . getelementsbyTagName(' tagname '):检索标记名为 tagname 的所有元素,并将它们存储在一个类似数组的列表中

  • document . getelementsbyclassname(' cssClass '):检索类名为 CSS class 的所有元素,并将它们存储在类似数组的列表中

  • 读取元素属性、节点值和其他节点数据

  • node.getAttribute('attribute '):检索具有名称属性的属性的值

  • node.setAttribute('Attribute ',' value '):将名称为 attribute 的属性的值设置为 value

  • node.nodeType:读取节点的类型(1 =元素,3 =文本节点)

  • node.nodeName:读取节点的名称(元素名或#textNode)

  • node.nodeValue:读取或设置节点的值(文本节点的文本内容)

  • 在节点间导航

  • 检索前一个同级节点,并将其存储为一个对象。

  • 检索下一个同级节点,并将其存储为一个对象。

  • 检索对象的所有子节点,并将它们存储在一个列表中。第一个和最后一个子节点有快捷方式,名为 node.firstChild 和 node.lastChild。

  • 检索包含节点的节点。

  • 创建新节点

  • document.createElement(Element):创建一个名为 element 的新元素节点。您以字符串形式提供元素名称。

  • document.createTextNode(string):创建一个新的文本节点,节点值为 string。

  • newNode = Node.cloneNode(bool):创建 newNode 作为 node 的副本(克隆)。如果 bool 为 true,则克隆包括原始节点的所有子节点的克隆。

  • node.appendChild(newNode):将 newNode 作为新的(最后一个)子节点添加到 node。

  • node.insertBefore(newNode,oldNode):在 oldNode 之前插入 newNode 作为 node 的新的子节点。

  • node.removeChild(oldNode):从节点中删除子 oldNode。

  • node.replaceChild(newNode,oldNode):用 newNode 替换 node 的子节点 oldNode。

  • element.innerHTML:以字符串形式读取或写入给定元素的 HTML 内容,包括所有子节点及其属性和文本内容。

DOMhelp:您自己的助手库

使用 DOM 时最令人讨厌的事情是浏览器的不一致性——特别是当这意味着您每次想要访问下一个兄弟节点时都必须测试 nodeType 时,因为非常旧的浏览器的用户代理可能会也可能不会将换行符作为它自己的文本节点来读取。因此,您应该准备好一组工具函数来解决这些问题,并允许您专注于主脚本的逻辑。

让我们现在就开始我们自己的助手方法库,来说明如果没有它你将不得不面对的问题。

image 注意你会在代码演示中找到 DOMhelp.js 文件和一个测试 HTML 文件。本书附带的 zip 文件。中的版本。zip 文件的方法比较多,下一章会讨论,不要搞混了。

这个库将由一个名为 DOMhelp 的对象和几个实用方法组成。下面是我们将在本章和下一章中充实的实用程序的框架:

DOMhelp=
{
  // Find the last sibling of the current node
  lastSibling:function(node){},

  // Find the first sibling of the current node
  firstSibling:function(node){},

  // Retrieve the content of the first text node sibling of the current node
  getText:function(node){},

  // Set the content of the first text node sibling of the current node
  setText:function(node,txt){},

  // Find the next or previous sibling that is an element
  //  and not a text node or line break
  closestSibling:function(node,direction){},

  // Create a new link containing the given text
  createLink:function(to,txt){},

  // Create a new element containing the given text
  createTextElm:function(elm,txt){},  // Simulate a debugging console to avoid the need for alerts
  initDebug:function(){},
  setDebug:function(bug){},
  stopDebug:function(){}
}

在本章前面,您已经遇到了最后一个和第一个同级函数;这些例子中唯一缺少的是一个测试,即在试图将它分配给临时对象之前,是否真的有一个上一个或下一个兄弟要检查。这两种方法中的每一种都检查有问题的兄弟是否存在,如果不存在,则返回 false:

lastSibling:function(node)
{
  var tempObj=node.parentNode.lastChild;
  while(tempObj.nodeType!=1 && tempObj.previousSibling!=null)
  {
    tempObj=tempObj.previousSibling;
  }
  return (tempObj.nodeType==1)?tempObj:false;
},
firstSibling:function(node)
{
  var tempObj=node.parentNode.firstChild;
  while(tempObj.nodeType!=1 && tempObj.nextSibling!=null)
  {
    tempObj=tempObj.nextSibling;
  }
  return (tempObj.nodeType==1)?tempObj:false;
},

接下来是 getText 方法,它读取元素的第一个文本节点的文本值:

getText:function(node)
{
  if(!node.hasChildNodes()){return false;}
  var reg=/^\s+$/;
  var tempObj=node.firstChild;
  while(tempObj.nodeType!=3 && tempObj.nextSibling!=null ||reg.test(tempObj.nodeValue))
  {
    tempObj=tempObj.nextSibling;
  }
  return tempObj.nodeType==3?tempObj.nodeValue:false;
},

您可能遇到的第一个问题是该节点没有任何子节点;因此,您需要检查 hasChildNodes。其他问题是节点中的嵌入元素和空白,比如换行符和制表符被当作节点读取。因此,您测试第一个子节点并跳转到下一个兄弟节点,直到 nodeType 是 text (3)并且节点不仅仅由空白字符组成。(这是正则表达式检查的内容。)在尝试将下一个兄弟节点赋给 tempObj 之前,还要测试是否有下一个兄弟节点。如果一切正常,该方法返回第一个文本节点的 nodeValue 否则,它返回 false。

相同的测试模式适用于 setText,它用新文本替换节点的第一个真实文本子节点,并避免任何换行符或制表符:

setText:function(node,txt)
{
  if(!node.hasChildNodes()){return false;}
  var reg=/^\s+$/;
  var tempObj=node.firstChild;
  while(tempObj.nodeType!=3 && tempObj.nextSibling!=null ||reg.test(tempObj.nodeValue))
  {
    tempObj=tempObj.nextSibling;
  }
  if(tempObj.nodeType==3){tempObj.nodeValue=txt}else{return false;}
},

接下来的两个 helper 方法帮助您完成创建包含目标和文本的链接以及创建包含文本的元素的常见任务:

createLink:function(to,txt)
{
  var tempObj=document.createElement('a');
  tempObj.appendChild(document.createTextNode(txt));
  tempObj.setAttribute('href',to);
  return tempObj;
},
createTextElm:function(elm,txt)
{
  var tempObj=document.createElement(elm);
  tempObj.appendChild(document.createTextNode(txt));
  return tempObj;
},

它们不包含您之前在这里没有见过的内容,但是放在一个地方非常方便。

事实上,一些浏览器将换行符作为文本节点读取,而另一些不这样做,这意味着您不能信任 nextSibling 或 previousSibling 返回下一个元素——例如,在一个无序列表中。实用方法 closestSibling()解决了这个问题。它需要节点和方向(1 表示下一个兄弟节点,1 表示上一个兄弟节点)作为参数:

closestSibling:function(node,direction)
{
  var tempObj;
  if(direction==-1 && node.previousSibling!=null)
  {
    tempObj=node.previousSibling;
    while(tempObj.nodeType!=1 && tempObj.previousSibling!=null)
    {
       tempObj=tempObj.previousSibling;
    }
  }
  else if(direction==1 && node.nextSibling!=null)
  {
    tempObj=node.nextSibling;
    while(tempObj.nodeType!=1 && tempObj.nextSibling!=null)
    {
      tempObj=tempObj.nextSibling;
    }
  }
  return tempObj.nodeType==1?tempObj:false;
},

最后一组方法是用来模拟可编程 JavaScript 调试控制台的。使用 alert()作为显示值的手段是很方便的,但是当您想要观察一个大循环内部的变化时,它会变得很麻烦——谁愿意按 200 次 Enter 键呢?不使用 alert(),而是向文档中添加一个新的 DIV,并输出任何想要检查的数据作为该 DIV 的新的子节点。使用合适的样式表,您可以将 DIV 浮动在内容之上。从一个初始化方法开始,该方法检查控制台是否已经存在,如果存在,就删除它。这对于避免几个控制台同时存在是必要的。然后创建一个新的 DIV 元素,给它一个 ID 进行样式化,并将其添加到文档中:

initDebug:function()
{
  if(DOMhelp.debug){DOMhelp.stopDebug();}
  DOMhelp.debug=document.createElement('div');
  DOMhelp.debug.setAttribute('id',DOMhelp.debugWindowId);
  document.body.insertBefore(DOMhelp.debug,document.body.firstChild);
},

setDebug 方法将名为 bug 的字符串作为参数。它测试控制台是否已经存在,并在必要时调用初始化方法来创建控制台。然后,它将后跟换行符的 bug 字符串添加到控制台的 HTML 内容中:

setDebug:function(bug)
{
  if(!DOMhelp.debug){DOMhelp.initDebug();}
  DOMhelp.debug.innerHTML+=bug+'\n';
},

最后一个方法是从文档中删除控制台(如果存在的话)。请注意,您需要移除元素并将 object 属性设置为 null 否则,即使没有可写入的控制台,对 DOMhelp.debug 的测试也将为真。

stopDebug:function()
{
  if(DOMhelp.debug)
  {
    DOMhelp.debug.parentNode.removeChild(DOMhelp.debug);
    DOMhelp.debug=null;
  }
}

我们将在接下来的章节中扩展这个助手库。

摘要

读完这一章后,你应该完全有能力处理任何 HTML 文档,得到你需要的部分,并通过 DOM 改变甚至创建标记。

您了解了 HTML 文档的结构,以及 DOM 如何提供您所看到的元素、属性和文本节点的集合。您还看到了窗口方法 alert()、confirm()和 prompt()。这些都是快速和广泛支持的——尽管不安全和笨拙——检索数据和给出反馈的方法。

然后,您学习了 DOM、如何访问元素、在元素之间导航以及如何创建新内容。

在下一章中,您将学习如何处理表示问题,跟踪访问者如何在浏览器中与文档交互,并通过事件处理做出相应的反应。

五、表现和行为(CSS 和事件处理)

在上一章中,你拆开了一个 HTML 文档,看看里面是什么。你拨弄了一些电缆,更换了一些零件,使发动机处于原始状态。现在是时候看看如何用层叠样式表(CSS)给文档添加新的色彩,并通过事件启动它了。如果你追求的是美,那么你很幸运,因为我们从表示层 开始。

通过 JavaScript 改变表示层

HTML 文档中的每个元素都有一个 style 属性作为其属性之一,该属性是其所有可视属性的集合。您可以读取或写入属性的值,如果您将值写入属性,您将立即改变元素的外观。

image 我们在整章中都使用了我们在前一章中创建的 DOMhelp 库(实际上,在本书的其余部分也是如此)。

对于初学者,请尝试以下脚本:

exampleStyleChange.html

<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Example: Accessing the style collection</title>
<style type="text/css">
</style>
<script type="text/javascript" src="DOMhelp.js"></script>
<script type="text/javascript" src="styleChange.js"></script>
</head>
<body>
<h3>Contact Details</h3>
<address>
  Awesome Web Production Company<br>
  Going Nowhere Lane 0<br>
  Catch 22<br>
N4 2XX<br>
  England<br>
</address>
</body>
</html>

styleChange.js

var sc = {
  init:function(){
    sc.head = document.getElementsByTagName('h3')[0];
    if(!sc.head){return;}
    sc.ad = DOMhelp.closestSibling(sc.head,1);
    sc.ad.style.display='none';
    var t = DOMhelp.getText(sc.head);
    var collapseLink = DOMhelp.createLink('#',t);
    sc.head.replaceChild(collapseLink,sc.head.firstChild);
    DOMhelp.addEvent(collapseLink,'click',sc.peekaboo,false)
    collapseLink.onclick = function(){return;} // Safari fix
  },
  peekaboo:function(e){
    sc.ad.style.display=sc.ad.style.display=='none'? '':'none';
    DOMhelp.cancelClick(e);
}
}
DOMhelp.addEvent(window,'load',sc.init,false);

image 耐心是关键。addEvent()和 cancelClick()部分将在本章的第二部分解释。

该脚本获取文档中的第一个 H3 元素,并通过 DOMhelp 库的 closestSibling helper 方法获取 ADDRESS 元素。(该方法确保检索下一个元素,而不是被视为文本节点的换行符。)然后,它修改其样式集合的 display 属性来隐藏地址。它用一个指向函数 peekaboo 的链接替换标题中的文本。该链接对于允许键盘用户展开和折叠地址是必要的。虽然鼠标用户可以很容易地点击标题,但不能通过在文档中跳 tab 键来访问它。peekaboo()函数读取地址样式集合的显示值,如果 display 设置为 none,则替换为空字符串,如果 display 设置为空字符串以外的值,则替换为 none——有效地隐藏和显示地址,如图图 5-1 所示。

9781430250920_Fig05-01.jpg

图 5-1 。地址的两种状态(折叠和展开)

image 注意你可能在过去遇到过使用 element.style.display='block '作为 none 反义词的脚本。这适用于大多数元素,但是简单地将显示值设置为 nothing 会将其重置为初始显示值——不一定是 block 它可以是内联的或表格行的。如果添加一个空字符串,就让浏览器来设置适当的值;否则,你必须为不同的元素添加一个开关块或 if 条件。

样式集合包含当前元素的所有样式设置,您可以使用不同 CSS 选择器的属性表示法来修改这些设置。属性符号的经验法则是,在 CSS 选择器中删除破折号,并对整个选择器使用 camelCase。例如,行高变成行高,右边变成右边。有一个很长的可用属性列表,但是它们可以分为以下几类:

  • 背景
  • 边框/轮廓
  • 生成的内容
  • 目录
  • 混杂的
  • 边距/填充
  • 定位/布局
  • 印刷
  • 桌子
  • 文本

image 注意https://developer.mozilla.org/en-US/docs/CSS/CSS_Reference 的 Mozilla 开发者网络上有一个包含厂商前缀 CSS 的参考指南。

可以使用 getAttribute()和 setAttribute()读写样式属性;但是,如果您编写它们,使用 JavaScript 对象属性语法将样式属性设置为字符串值可能会更快。对于浏览器来说,下面的两个例子是一样的,但是后者可能渲染得更快一些,并且可以让你的 JavaScript 更短:

var warning=document.createElement('div');

warning.style.borderColor='#c00';
warning.style.borderWidth='1px';
warning.style.borderStyle='solid';
warning.style.backgroundColor='#fcc';
warning.style.padding='5px';
warning.style.color='#c00';
warning.style.fontFamily='Arial';

// is the same as
warning.setAttribute( 'style' ,  'font-family:arial;color:#c00;
padding:5px;border:1px solid #c00;background:#fcc');

尽管在现代 web 设计中直接设置样式属性是不可取的(因为你有效地混合了行为层和表示层,使得维护变得更加困难),但是有些情况下你必须通过 JavaScript 直接设置样式属性——例如:

  • 修复浏览器在 CSS 支持方面的缺点
  • 动态改变元素的尺寸以修复布局故障
  • 制作文档各部分的动画
  • 使用拖放功能创建丰富的用户界面

image 在本章的后面你会听到列表中的前两项。然而,您在这里找不到动画或拖放示例,因为这些是高级 JavaScript 主题,需要大量解释,超出了本书的范围。你会在第十一章中找到现成的例子。

对于简单的样式任务,为了简化脚本的维护,应该避免在 JavaScript 中定义外观。在第三章中,我谈到了现代 web 开发的主要特征:开发层的分离。

如果在 JavaScript 中使用了大量的样式定义,就会混淆表示层和行为层。如果几个月后你的应用的外观和感觉必须改变,你或者一些第三方开发者将不得不重新访问你的脚本代码并改变其中的所有设置。这既不必要也不可取,因为您可以通过将它放在 CSS 文档中来分离外观和感觉。

您可以通过动态更改元素的 class 属性来实现这种分离。这样,您可以应用或移除站点样式表中定义的样式设置。CSS 设计者不必担心你的脚本代码,你也不必知道浏览器在支持 CSS 方面的所有问题。你需要交流的只是这些类的名字。

例如,要将名为 dynamic 的类应用于 ID 为 nav 的元素,可以更改其 className 属性:

var n=document.getElementById('nav');
n.className='dynamic';

image 注意从逻辑上讲,你也应该能够通过 setAttribute()方法改变类,但是浏览器对此的支持是不可靠的(例如,Internet Explorer 不允许 class 或 style 作为属性),这就是为什么目前坚持使用 className 是一个好计划。属性的名称是 className,而不是 class,因为 class 是 JavaScript 中的保留字,当用作属性时会导致错误。

您可以通过将其值设置为空字符串来移除该类。同样,removeAttribute()不能跨不同的浏览器可靠地工作。

正如你可能知道的,HTML 元素可以有多个 CSS 类分配给它们。以下类型的构造是有效的 HTML——有时是个好主意:

<p class="intro special kids">Lorem Ipsum</p>

在 JavaScript 中,只需在 className 值后面附加一个空格就可以实现。但是,存在浏览器不能正确显示您的类设置的危险,特别是当添加或删除导致 className 值中的前导或尾随空格时。下面两个例子可以在当前的浏览器中正常显示(在 IE 7 中也可以正常显示):

<p class="intro special kids ">Lorem Ipsum</p>
<p class=" intro special kids">Lorem Ipsum</p>

您可以使用助手方法来解决这个问题。编写这个 helper 方法来动态地添加和删除类应该很容易:如果 className 属性不为空,则在类值前面附加一个空格,如果为空,则不附加空格。从原始值中删除类名就像从字符串中删除单词一样。然而,因为你需要用孤儿空间来解决浏览器的问题,所以事情要比这复杂一些。下面的工具方法包含在 DOMhelp 中,您可以使用它在元素中动态添加和移除类。您还可以使用此方法来测试某个类是否已经添加到元素中:

function cssjs(a,o,c1,c2){
  switch (a){
    case 'swap':
      if(!DOMhelp.cssjs('check',o,c1)){
       o.className.replace(c2,c1);
      }else{
        o.className.replace(c1,c2);
      }
    break;
    case 'add':
      if(!domtab.cssjs('check',o,c1)){
       o.className+=o.className?''+c1:c1;
      }
    break;
    case 'remove':
      var rep=o.className.match(''+c1)?''+c1:c1;
      o.className=o.className.replace(rep,'');
    break;
    case 'check':
      var found=false;
      var temparray=o.className.split('');
      for(var i=0;i<temparray.length;i++){
        if(temparray[i]==c1){found=true;}
      }
      return found;
    break;
  }
}

不要太担心这个方法的内部工作方式——一旦你掌握了 match()和 replace()方法,你就会明白了,这将在第八章中讲述。现在,你所需要知道的就是如何使用它,为此你需要使用这个方法的四个参数:

  • a 是必须采取的行动,有以下选项:

  • swap 用一个类替换另一个类。

  • 添加一个新类。

  • 删除删除一个类。

  • 检查测试该类是否已经应用。

  • o 是要添加类或从中删除类的对象。

  • c1 和 c2 是类名,只有当动作是 swap 时才需要 c2。

让我们使用方法重新编码前面的例子——这一次通过动态地应用和移除一个类来隐藏和显示地址。

exampleClassChange.html

<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Example: Dynamically changing classes</title>
<link rel="stylesheet" href="classChange.css">
<script type="text/javascript" src="DOMhelp.js"></script>
<script type="text/javascript" src="classChange.js"></script>
</head>
<body>
<h3>Contact Details</h3>
<address>
  Awesome Web Production Company<br>
  Going Nowhere Lane 0<br>
  Catch 22<br>
N4 2XX<br>
  England<br>
</address>
</body>
</html>

样式表中包含一个名为 hide 的类,它将隐藏应用到它的任何元素。这是通过使用 CSS 剪辑来完成的。剪辑定义了这个绝对定位元素的可见区域。为了让屏幕阅读器能够阅读内容,它的大小为 1 像素。那个!重要规则告诉浏览器覆盖 CSS 中的任何其他声明。通过改变可见性或显示属性来隐藏元素的问题是,帮助盲人用户的屏幕阅读器可能不会向他们提供内容,尽管它在浏览器中是可见的。

classChange.css (excerpt)

.hide{
position: absolute !important;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
clip: rect(1px, 1px, 1px, 1px);
padding: 0 !important;
border: 0 !important;
height: 1px !important;
width: 1px !important;
overflow: hidden;
}

您在脚本开始时将类名指定为参数,这意味着如果有人需要在稍后阶段更改名称,他不必检查整个脚本。

如果你开发了一个非常复杂的站点,需要添加和删除许多不同的类,你可以把它们转移到它们自己的 JavaScript include 文件中,并包含它们自己的对象。对于这个例子来说,这样的移动有点过了——但是我稍后会回到这个选项。

image 注意 DOMhelp 已经包含了 cssjs()方法;因此,您不需要将其包含在此示例中。

classChange.js

var sc={

   // CSS classes
   hidingClass:'hide', // Hide elements

  init:function(){
    sc.head=document.getElementsByTagName('h3')[0];
    if(!sc.head){return;}
    sc.ad=DOMhelp.closestSibling(sc.head,1);

    DOMhelp.cssjs('add',sc.ad,sc.hidingClass);

    var t=DOMhelp.getText(sc.head);
    var collapseLink=DOMhelp.createLink('#',t);
    sc.head.replaceChild(collapseLink,sc.head.firstChild);
    DOMhelp.addEvent(collapseLink,'click',sc.peekaboo,false)
    collapseLink.onclick=function(){return;} // Safari fix
  },
  peekaboo:function(e){

    if(DOMhelp.cssjs('check',sc.ad,sc.hidingClass)){
       DOMhelp.cssjs('remove',sc.ad,sc.hidingClass)
    } else {
       DOMhelp.cssjs('add',sc.ad,sc.hidingClass)
    }

    DOMhelp.cancelClick(e);
  }
}
DOMhelp.addEvent(window,'load',sc.init,false);

帮助 CSS 设计者

DOM 脚本化和将 CSS 分成可以动态应用和移除的类,可以让 web 设计人员的工作变得更加轻松。使用 DOM 和 JavaScript,你可以比使用 CSS 选择器更深入地了解文档。例如,一个常见的请求是寻找一种方法来到达 CSS 中的父元素以获得悬停效果。在 CSS 中,这是不可能的;在 JavaScript 中,通过 parentNode 很容易实现。使用 JavaScript 和 DOM,您可以通过修改 HTML 内容来应用类和 id,生成内容,甚至通过添加或删除样式和链接元素来添加和删除整个样式表,从而为设计者提供样式表的动态挂钩。

轻松设计动态页面

让设计者尽可能容易地为网站的脚本增强版本和非脚本版本创建不同的样式是非常重要的。非脚本版本可以更简单,所需的样式也更少。(例如,在 HTML 地址示例中,只有在启用了 JavaScript 时,才需要为 H3 内部定义链接样式,因为链接是通过 JavaScript 生成的。)当启用脚本时,为设计者提供唯一标识符的一个非常简单的方法是将一个类应用于主体或布局的主要元素。

dynamic styling . js—在 exampleDynamicStyling.html 使用

var sc={

  // CSS classes
  hidingClass:'hide', // Hide elements
  DOMClass:'dynamic', // Indicate DOM support

  init:function(){

    // Check for DOM and apply a class to the body if it is supported
    if(!document.getElementById || !document.createElement){return;}
    DOMhelp.cssjs('add',document.body,sc.DOMClass);

    sc.head=document.getElementsByTagName('h3')[0];
    if(!sc.head){return;}
    sc.ad=DOMhelp.closestSibling(sc.head,1);
    DOMhelp.cssjs('add',sc.ad,sc.hidingClass);
    var t=DOMhelp.getText(sc.head);
    var collapseLink=DOMhelp.createLink('#',t);
    sc.head.replaceChild(collapseLink,sc.head.firstChild);
    DOMhelp.addEvent(collapseLink,'click',sc.peekaboo,false)
    collapseLink.onclick=function(){return;} // Safari fix
  },
  peekaboo:function(e){
    if(DOMhelp.cssjs('check',sc.ad,sc.hidingClass)){
       DOMhelp.cssjs('remove',sc.ad,sc.hidingClass)
    } else {
       DOMhelp.cssjs('add',sc.ad,sc.hidingClass)
    }
    DOMhelp.cancelClick(e);
  }
}
DOMhelp.addEvent(window,'load',sc.init,false);

这样,CSS 设计者可以在样式表中定义禁用 JavaScript 时要应用的设置,并在启用 JavaScript 时用其他设置覆盖它们,方法是在后代选择器中使用带有类名的主体:

dynamic drive . CSS

*{
  margin:0;
  padding:0;
}
body{
  font-family:Arial,Sans-Serif;
  font-size:small;
  padding:2em;
}

/* JS disabled */
address{
  background:#ddd;
  border:1px solid #999;
  border-top:none;
  font-style:normal;
  padding:.5em;
  width:15em;
}
h3{
  border:1px solid #000;
  color:#fff;
  background:#369;
  padding:.2em .5em;
  width:15em;
  font-size:1em;
}

/* JS enabled */
body.dynamic address{
  background:#fff;
  border:none;
  font-style:normal;
  padding:.5em;
  border-top:1px solid #ccc;
}
body.dynamic h3{
  padding-bottom:.5em;
  background:#fff;
  border:none;
}
body.dynamic h3 a{
  color:#369;
}

/* dynamic classes */
.hide{
position: absolute !important;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
clip: rect(1px, 1px, 1px, 1px);
padding: 0 !important;
border: 0 !important;
height: 1px !important;
 width: 1px !important;
overflow: hidden;
}

现在的地址示例——取决于 JavaScript 和 DOM 是否可用——可以有两种完全不同的外观(其中一种有两种状态),如图 5-2 所示。

9781430250920_Fig05-02.jpg

图 5-2 。地址的三种状态(非动态版本、折叠和扩展)

如果你的网站不太复杂,没有很多动态元素,这种方式会很好。对于更复杂的站点,您可以对非 JavaScript 版本和 JavaScript 版本使用不同的样式表,并通过 JavaScript 添加后者。这还有一个额外的好处:低级用户不必加载对他没有任何用处的样式表。您可以通过在文档头创建一个新的 LINK 元素来添加动态样式表。在本例中,首先包含一个低级样式表:

exampleStyleSheetChange.html

<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Example: Dynamically applying new Style Sheets </title>
<link href="lowlevel.css" rel="stylesheet">
<script type="text/javascript" src="DOMhelp.js"></script>
<script type="text/javascript" src="styleSheetChange.js"></script>
</head>
<body>
<h3>Contact Details</h3>
<address>
  Awesome Web Production Company<br>
  Going Nowhere Lane 0<br>
  Catch 22<br>
N4 2XX<br>
  England<br>
</adress>
</body>
</html>

该脚本检查 DOM 支持,并添加一个指向高级样式表的新 link 元素:

styleSheetChange.js

sc={
  // CSS classes

  hidingClass:'hide', // Hide elements
  highLevelStyleSheet:'highlevel.css', // Style sheet for dynamic site

  init:function(){

    // Check for DOM and apply a class to the body if it is supported
    if(!document.getElementById || !document.createElement){return;}

    var newStyle=document.createElement('link');
    newStyle.setAttribute('type','text/css');
    newStyle.setAttribute('rel','stylesheet');
    newStyle.setAttribute('href',sc.highLevelStyleSheet);
    document.getElementsByTagName('head')[0].appendChild(newStyle);

    sc.head=document.getElementsByTagName('h3')[0];
    if(!sc.head){return;}
    sc.ad=DOMhelp.closestSibling(sc.head,1);
    DOMhelp.cssjs('add',sc.ad,sc.hidingClass);
    var t=DOMhelp.getText(sc.head);
    var collapseLink=DOMhelp.createLink('#',t);
    sc.head.replaceChild(collapseLink,sc.head.firstChild);
    DOMhelp.addEvent(collapseLink,'click',sc.peekaboo,false)
    collapseLink.onclick=function(){return;} // Safari fix
  },
  peekaboo:function(e){
    if(DOMhelp.cssjs('check',sc.ad,sc.hidingClass)){
       DOMhelp.cssjs('remove',sc.ad,sc.hidingClass)
    } else {
       DOMhelp.cssjs('add',sc.ad,sc.hidingClass)
    }
    DOMhelp.cancelClick(e);
  }
}
DOMhelp.addEvent(window,'load',sc.init,false);

脚本执行后,HTML 示例的头部如下所示。(您可以在 Firefox 中测试这一点,方法是通过 Ctrl+A 或 Cmd+A 选择整个文档,然后右键单击任意位置并选择“查看所选源代码”。)

剧本执行后的 exampleStyleSheetChange.html(节选)

<head>
<meta charset=utf-8">
<title>Example: Using dynamic classes</title>
<link href="lowlevel.css" rel="stylesheet">
<script type="text/javascript" src="DOMhelp.js"></script>
<script type="text/javascript" src="styleSheetChange.js"></script>
<link href="highlevel.css" rel="StyleSheet" type="text/css">
</head>

你可能早就遇到过风格的动态变化。早在 2001 年,所谓的风格转换者 开始流行。这些是小的页面部件,允许用户通过从给定的列表中选择样式来选择页面的外观。一些现代浏览器内置了这个选项——例如,在 Firefox 中,你可以选择视图、页面样式,并获得所有可用的样式供选择。

演示 exampleStyleSwitcher.html 展示了这是如何做到的。在 HTML 中,为大字体和高对比度定义主样式表和备用样式表:

exampleStyleSwitcher.html(节选)

<link href="demoStyles.css" title="Normal"
      rel="stylesheet" type="text/css">
<link href="largePrint.css" title="Large Print"
      rel="alternate stylesheet" type="text/css">
<link href="highContrast.css" title="High Contrast"
      rel="alternate stylesheet" type="text/css">

剧本并不复杂。遍历文档中的所有链接元素,并确定每个元素的属性是样式表还是备用样式表。您创建一个新列表,其中的链接指向一个函数,该函数禁用除当前选择的样式表之外的所有样式表,并将该列表添加到文档中。

您从两个属性开始:一个存储样式菜单的 ID 以允许 CSS 样式化,另一个存储一个标签以显示为所有可用样式之前的第一个列表项。

style switch . js

switcher={
  menuID:'styleswitcher',
  chooseLabel:'Choose Style:',

名为 init()的初始化方法创建一个新的 HTML 列表,并添加一个带有标签的列表项作为文本内容。您将列表的 ID 设置为属性中定义的 ID。

styleSwitcher.js(续)

init:function(){
  var tempLI,tempA,styleTitle;
  var stylemenu=document.createElement('ul');
  tempLI=document.createElement('li');
  tempLI.appendChild(document.createTextNode(switcher.chooseLabel));
  stylemenu.appendChild(tempLI);
  stylemenu.id=switcher.menuID;

遍历文档中的所有链接元素。对于每个元素,测试其 rel 属性的值。如果该值既不是样式表也不是替代样式表,则跳过此链接元素。这对于避免通过链接标签提供的其他替代内容(如 RSS 提要)被禁用是必要的。

styleSwitcher.js(续)

var links=document.getElementsByTagName('link');
for(var i=0;i<links.length;i++){
  if(links[i].getAttribute('rel')!='stylesheet'&& links[i].getAttribute('rel')!='alternate stylesheet'){
    continue;
  }

为每种样式创建一个带有链接的新列表项,并将链接的文本值设置为 link 元素的 title 属性的值。设置一个伪 href 属性,使链接显示为链接;否则,用户可能不会将新链接识别为交互元素。

styleSwitcher.js(续)

tempLI=document.createElement('li');
tempA=document.createElement('a');
styleTitle=links[i].getAttribute('title');
tempA.appendChild(document.createTextNode(styleTitle));
tempA.setAttribute('href','#');

对链接应用一个事件处理程序,该处理程序触发 setSwitch()方法,并通过 this 关键字将链接本身作为参数发送。然后,您可以继续向菜单列表添加新的列表项,并在循环完成时将列表追加到文档正文。

styleSwitcher.js(续)

    tempA.onclick=function(){
    switcher.setSwitch(this);
    }
    tempLI.appendChild(tempA);
    stylemenu.appendChild(tempLI);
  }
  document.body.appendChild(stylemenu);
},

在 setSwitch()方法中,您将检索作为参数 o 激活的链接。遍历所有链接元素,并测试每个元素以查看标题属性是否与链接的文本内容相同。(您可以通过 firstChild.nodeValue 安全地读取文本,而无需测试节点类型,因为您生成了链接。)如果标题不同,则将链接的 disabled 属性设置为 true 如果相同,则将 disable 设置为 false,并将 rel 属性设置为 stylesheet 而不是 alternate stylesheet。然后通过返回 false 来阻止链接被跟踪。

styleSwitcher.js(续)

  setSwitch:function(o){
    var links=document.getElementsByTagName('link');
    for(var i=0;i<links.length;i++){
      if(links[i].getAttribute('rel')!='stylesheet'&&
      links[i].getAttribute('rel')!='alternate stylesheet'){
        continue;
      }
      var title=o.firstChild.nodeValue;
      if(links[i].getAttribute('title')!=title){
        links[i].disabled=true;
      } else {
        links[i].setAttribute('rel','stylesheet');
        links[i].disabled=false;
      }
    }
    return false;
  }
}

您可以通过在启用了 JavaScript 和 CSS 的浏览器中打开 exampleStyleSwitcher.html 来测试功能。

样式转换器可能是一个有用的功能,特别是当您提供的样式可能有助于用户克服视力不好等问题时,例如更大的字体或前景和背景之间的更高对比度。另一方面,如果你只是为了提供不同的风格而使用它们,它们可能是毫无意义的视觉享受。

多年来,转换者经历了许多变化。2005 年,Dustin Diaz 接受了这个想法,将 PHP 切换器的稳定性与 JavaScript 增强界面的光滑性结合起来,使用 Ajax 来弥合这一差距。你可以在他题为“不引人注目的可降解 Ajax 样式表切换器”(24ways.org/advent/introducing-udasss)的博客文章中了解更多信息。

风格转换器思想的演变表明 JavaScript 解决方案从来都不是一成不变的。它们需要在现实世界中进行测试,并从用户和其他开发人员那里获得反馈,以便真正适用于生产环境或现场。如果你最近在网上冲浪,你会看到许多试验性的脚本承诺了很多,但是仔细观察,它们被证明是缓慢的,不稳定的,或者只是一个巧妙的技巧,可以用另一种技术做得更好。在 JavaScript 中你可以做任何事情并不意味着你应该这样做。

简化脚本的维护

将整体的外观和感觉保留在脚本之外和样式表之内(因此这是 CSS 设计者的责任)只是成功的一半。在项目维护期间,CSS 类名可能需要更改——例如,为了支持某个后端或内容管理系统(CMS) 。因此,让设计者容易地更改动态应用的类的名称是很重要的。最基本的技巧是将类名保存在它们自己的变量或参数中。您已经在前面的示例中完成了。您可以直接应用类名:

sc={
  init:function(){
    // Check for DOM and apply a class to the body if it is supported
    if(!document.getElementById || !document.createElement){return;}
    DOMhelp.cssjs('add',document.body, 'dynamic');
    sc.head=document.getElementsByTagName('h3')[0];
    if(!sc.head){return;}
    sc.ad=DOMhelp.closestSibling(sc.head,1);
    DOMhelp.cssjs('add',sc.ad, 'hide');
    var t=DOMhelp.getText(sc.head);
    var collapseLink=DOMhelp.createLink('#',t);
    sc.head.replaceChild(collapseLink,sc.head.firstChild);
    DOMhelp.addEvent(collapseLink,'click',sc.peekaboo,false)
    collapseLink.onclick=function(){return;} // Safari fix
  },
  peekaboo:function(e){
    if(DOMhelp.cssjs('check',sc.ad,sc.hidingClass)){
       DOMhelp.cssjs('remove',sc.ad,sc.hidingClass)
    } else {
       DOMhelp.cssjs('add',sc.ad,sc.hidingClass)
    }
    DOMhelp.cancelClick(e);
  }
}
DOMhelp.addEvent(window,'load',sc.init,false);

相反,您将它们作为主对象的属性移出了方法,并对它们进行了注释,以允许不懂 JavaScript 的人在不危及方法的质量或功能的情况下更改类名:

sc={

  // CSS classes
  hidingClass:'hide',        // Hide elements
  DOMClass:'dynamic',        // Indicate DOM support

  init:function(){
    if(!document.getElementById || !document.createElement){return;}
    DOMhelp.cssjs('add',document.body,sc.DOMClass);
    sc.head=document.getElementsByTagName('h3')[0];
    if(!sc.head){return;}
    sc.ad=DOMhelp.closestSibling(sc.head,1);
    DOMhelp.cssjs('add',sc.ad,sc.hidingClass);
    var t=DOMhelp.getText(sc.head);
    var collapseLink=DOMhelp.createLink('#',t);
    sc.head.replaceChild(collapseLink,sc.head.firstChild);
    DOMhelp.addEvent(collapseLink,'click',sc.peekaboo,false);
    collapseLink.onclick=function(){return;} // Safari fix
  },
  peekaboo:function(e){
    // More code snipped
  }
}
DOMhelp.addEvent(window,'load',sc.init,false);

对于没有很多不同 JavaScript includes 的较小脚本和项目来说,这就足够了——如果附带一些相关文档的话。如果您有许多分散在几个文档中的动态类,或者您非常担心非编码者会更改您的代码,那么您可以使用一个单独的 JavaScript include 文件,其中包含一个名为 CSS 的对象,所有的类都作为参数。给它一个明显的文件名,如 cssClassNames.js,并在项目文档中记录它的存在。

cssclassnames . js-CSS 类别名称

css={
  // Hide elements
  hide:'hide',

  // Indicator for support of dynamic scripting
  // will be added to the body element
  supported:'dynamic'
}

您可以将它应用到文档中,就像正在使用的任何其他脚本一样:

exampleDynamicStylingCSSObject.html

<head>
<meta charset="utf-8">
<title>Example: Importing class names from a CSS names object</title>
<link href="demoStyles.css" title="Normal" rel="stylesheet" type="text/css">
<script type="text/javascript" src="DOMhelp.js"></script>
<script type="text/javascript" src="cssClassNames.js"></script>
<script type="text/javascript" src="dynamicStylingCSSObject.js"></script>
</head>

这种方法的实际结果是,您不必为不同的 CSS 类名(通常包含“class ”,因此会让程序员感到困惑)想出参数名。相反,请使用以下内容:

dynamic cstylingcssobject . js

sc={
  init:function(){
    if(!document.getElementById || !document.createElement){return;}

   DOMhelp.cssjs('add',document.body,
css.supported);

    sc.head=document.getElementsByTagName('h3')[0];
    if(!sc.head){return;}
    sc.ad=DOMhelp.closestSibling(sc.head,1);

    DOMhelp.cssjs('add',sc.ad,
css.hide);

    var t=DOMhelp.getText(sc.head);
    var collapseLink=DOMhelp.createLink('#',t);
    sc.head.replaceChild(collapseLink,sc.head.firstChild);
    DOMhelp.addEvent(collapseLink,'click',sc.peekaboo,false);
    collapseLink.onclick=function(){return;} // Safari fix
  },
  peekaboo:function(e){
    // More code snipped
  }
}
DOMhelp.addEvent(window,'load',sc.init,false);

在此示例中,cssClassNames.js 文件使用对象文字表示法。如果您使用 JSON(www.json.org/),这是一种将数据从一个程序或系统传输到另一个程序或系统的格式,您甚至可以更进一步,去掉注释。你将在第七章中听到更多关于 JSON 及其优点的内容。现在,注意到 JSON 允许您使带有类名的文件更易于阅读就足够了:

cssclassnamejson . js

css={
"hide elements" : "hide",
"dynamic scripting enabled" : "dynamic"
}

现在,您必须像读取关联数组一样读取数据,而不是之前使用的属性符号:

dynamic cstylingjson . js

sc={
  init:function(){
    if(!document.getElementById || !document.createElement){return;}

   DOMhelp.cssjs('add',document.body, css['dynamic scripting enabled']);

    sc.head=document.getElementsByTagName('h3')[0];
    if(!sc.head){return;}
    sc.ad=DOMhelp.closestSibling(sc.head,1);

    DOMhelp.cssjs('add',sc.ad,
css['hide elements']);

    var t=DOMhelp.getText(sc.head);
    var collapseLink=DOMhelp.createLink('#',t);
    sc.head.replaceChild(collapseLink,sc.head.firstChild);
    DOMhelp.addEvent(collapseLink,'click',sc.peekaboo,false);
    collapseLink.onclick=function(){return;} // Safari fix
  },
  peekaboo:function(e){
    // More code snipped
  }
}
DOMhelp.addEvent(window,'load',sc.init,false);

你是否想把表现和行为分开,这完全取决于你。根据项目的复杂性和维护人员的知识,它可能只是防止许多可避免的错误。

克服 CSS 支持问题

近年来,CSS 对于 web 开发变得越来越重要。所有的页面布局现在都由 CSS 处理,将页面内容从设计中分离出来。这种分离的好处之一是,您可以根据站点的显示位置,为站点设置不同的布局。例如,由于桌面、平板电脑和手机的屏幕大小不同,您可以使用不同的 CSS 文件来相应地布局网站。

浏览器对 CSS 的支持有所改进,但是随着新功能的增加,您可能会遇到供应商前缀之类的问题:

background-color:#444444;
background-image: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#999999));
/*Safari 4+,Chrome*/

background-image:-webkit-linear-gradient(top,#444444,#999999);
/*Chrome 10+,Safari5.1+,iOS5+*/

background-image:-moz-linear-gradient(top,#444444,#999999);
/*Firefox 3.6-15*/

background-image:-o-linear-gradient(top,#444444,#999999);
/*Opera 11.10-12.0*/

background-image:linear-gradient(top,bottom,#444444,#999999);
/* Firefox 16+, IE10, Opera 12.50+ */

下面是供应商前缀的一个例子。根据浏览器的版本或类型(移动或桌面),其中一些功能可能需要有前缀。虽然您将获得相同的效果,但浏览器使用 CSS 添加该效果的能力可能取决于您是否使用供应商前缀。

高度相同的多列

对于以前只处理过表格布局的设计师来说,CSS 布局最令人讨厌的一点是,如果你对列使用 CSS 浮动技术,它们就没有相同的高度,如图图 5-3 所示。

9781430250920_Fig05-03.jpg

图 5-3 。多列高度问题

让我们从新闻条目列表开始,每个条目包含一个标题、一个“预告”段落和一个“更多”链接。

exampleColumnHeightIssue.html(带有虚拟内容)

<ul id="news">
<li>
<h3><a href="news.php?item=1">News Title 1</a></h3>
<p>Description 1</p>
<p class="more"><a href="news.php?item=1">more link 1</a></p>
</li>
<li>
<h3><a href="news.php?item=2">News Title 2</a></h3>
<p>Description 2</p>
<p class="more"><a href="news.php?item=2">more link 2</a></p>
</li>
<li>
<h3><a href="news.php?item=3">News Title 3</a></h3>
<p>Description 3</p>
<p class="more"><a href="news.php?item=1">more link 3</a></p>
</li>
<li>
<h3><a href="news.php?item=1">News Title 1</a></h3>
<p>Description 4</p>
<p class="more"><a href="news.php?item=4">more link 4</a></p>
</li>
</ul>

如果现在应用一个将列表项和主列表浮动到左侧的样式表,然后设置更多的文本和布局样式,就会得到一个多列布局。实现这一点的 CSS 非常简单:

蹈腔郪眽. css

#news{
  width:800px;
  float:left;
}
#news li{
  width:190px;
  margin:0 4px;
  float:left;
  background:#eee;
}
#news h3{
  background:#fff;
  padding-bottom:5px;
  border-bottom:2px solid #369;
}
#news li p{
  padding:5px;
}

正如您在示例中看到的,每一列都有不同的高度,段落和“更多”链接都不在同一位置。这使得设计看起来不均匀,会使读者困惑。可能有一种 CSS 方法可以解决这个问题(我总是对人们找到的黑客和变通方法印象深刻),但是让我们使用 JavaScript 来解决这个问题。

以下脚本(在文档头中调用)将修复该问题:

fixcolumnheight . js—在 exampleFixedColumnHeightIssue.html 使用

fixcolumns={

  highest:0,
  moreClass:'more',

  init:function(){
    if(!document.getElementById || !document.createTextNode){return;}
    fixcolumns.n=document.getElementById('news');
    if(!fixcolumns.n){return;}
    fixcolumns.fix('h3');
    fixcolumns.fix('p');
    fixcolumns.fix('li');
  },
  fix:function(elm){
    fixcolumns.getHighest(elm);
    fixcolumns.fixElements(elm);
  },
  getHighest:function(elm){
    fixcolumns.highest=0;
    var temp=fixcolumns.n.getElementsByTagName(elm);
    for(var i=0;i<temp.length;i++){
      if(!temp[i].offsetHeight){continue;}
      if(temp[i].offsetHeight>fixcolumns.highest){
        fixcolumns.highest=temp[i].offsetHeight;
      }
    }
  },
  fixElements:function(elm){
    var temp=fixcolumns.n.getElementsByTagName(elm);
    for(var i=0;i<temp.length;i++){
      if(!DOMhelp.cssjs('check',temp[i],fixcolumns.moreClass)){
        temp[i].style.height=parseInt(fixcolumns.highest)+'px';
      }
    }
  }
}
DOMhelp.addEvent(window, 'load', fixcolumns.init, false);

首先,定义一个参数来存储最高元素的高度和用于“更多”链接的类。(后者很重要,我很快会解释。)在 init()方法中,测试 DOM 支持,并检查 ID 为 news 的必要元素是否可用。将元素存储在属性 n 中,以便在其他方法中重用。然后为列表中包含的每个元素调用 fix()方法——标题、段落,最后是列表项。最后更改列表项很重要,因为当其他元素的最大高度固定时,它们的最大高度可能会改变。

fixColumnHeight.js(节选)

fixcolumns={

  highest:0,
  moreClass:'more',

  init:function(){
    if(!document.getElementById || !document.createTextNode){return;}
    fixcolumns.n=document.getElementById('news');
    if(!fixcolumns.n){return;}
    fixcolumns.fix('h3');
    fixcolumns.fix('p');
    fixcolumns.fix('li');
  },

fix()方法调用两个额外的方法:一个找出应用于每个项目的最大高度,另一个应用这个高度。

fixColumnHeight.js(节选)

fix:function(elm){
  fixcolumns.getHighest(elm);
  fixcolumns.fixElements(elm);
},

getHighest()方法首先将参数 Highest 设置为 0,然后遍历列表中与作为 elm 参数发送的元素名称相匹配的所有元素。然后,它通过读取 offsetHeight 属性来检索元素的高度。该属性存储元素被浏览器呈现后的高度。然后,该方法检查元素的高度是否大于属性 highest,如果是,则将属性设置为新值。这样,你就能找出哪个元素是最高的。

fixColumnHeight.js(节选)

getHighest:function(elm){
  fixcolumns.highest=0;
  var temp=fixcolumns.n.getElementsByTagName(elm);
  for(var i=0;i<temp.length;i++){
    if(!temp[i].offsetHeight){continue;}
    if(temp[i].offsetHeight>fixcolumns.highest){
      fixcolumns.highest=temp[i].offsetHeight;
    }
  }
},

image 注意您需要在这里将最高参数重置为 0,因为 getHighest()需要查找作为参数发送的元素中的最高值,而不是您修复的所有元素中的最高值。如果出于某种反常的意外,H3 高于最高的段落,你会在段落和“更多”链接之间产生间隙。

然后, fixElements()方法将最大高度应用于具有给定名称的所有元素。请注意,您需要测试确定“更多”链接的类;否则,链接的高度将与内容最高的段落相同。

fixElements:function(elm){
  var temp=fixcolumns.n.getElementsByTagName(elm);
  for(var i=0;i<temp.length;i++){
    if(!DOMhelp.cssjs('check',temp[i],fixcolumns.moreClass)){
      temp[i].style.height = fixcolumns.highest +'px';
    }
  }
}

image 注意你需要把最高的参数变成一个数字,加上一个 px 后缀,然后再应用到元素的高度。情况总是如此;当涉及到元素的 CSS 尺寸时,你不能简单地分配一个没有单位的数字。

缺少支持:盘旋

CSS 规范允许您在文档的任何元素上使用:hover 伪类,许多浏览器都支持这一点。这允许设计者突出显示文档的大部分,甚至模拟动态的折叠导航菜单,这在以前只有 JavaScript 才能实现。虽然没有 CSS 或 JavaScript 就不能交互的东西在鼠标悬停时是否应该获得不同的状态值得讨论,但这是设计者可以大量使用的功能——毕竟高亮显示文档的当前部分可能会使其更容易阅读。

要查看示例,再次获取新闻条目列表并应用不同的样式表。如果你想在 CSS-2 兼容的浏览器中突出显示一个完整的列表项,你需要做的就是在列表项上定义一个悬停状态:

与 exampleListItemRollover.html 一起使用的 listItemRolloverCSS.css(节选)

#news{
  font-size:.8em;
  background:#eee;
  width:21em;
  padding:.5em 0;
}
#news li{
  width:20em;
  padding:.5em;
  margin:0;
}
#news li:hover{
  background:#fff;
}

在 Firefox 19.0.2 上,效果出现如图图 5-4 所示。

9781430250920_Fig05-04.jpg

图 5-4 。CSS 中使用:hover 伪选择器的翻转效果

在 IE 6 中,你不会得到这种效果,因为它不支持:列表项悬停。但是,它支持 JavaScript,这意味着当用户将指针悬停在列表项上时,您可以使用 cssjs 方法动态添加一个类:

listItemRollover.css(节选)

#news{
  font-size:.8em;
  background:#eee;
  width:21em;
  padding:.5em 0;
}
#news li{
  width:20em;
  padding:.5em;
  margin:0;
}
#news li:hover{
  background:#fff;
}
#news li.over{
  background:#fff;

}

您可以通过 onmouseover 和 onmouseout 事件处理程序并使用 this 关键字来添加该类——我们将在本章后面更详细地讨论这一点。

listItemRollover.js(节选)

newshl={
  overClass:'over',
  init:function(){
if(!document.getElementById || !document.createTextNode){return;}
    var newsList=document.getElementById('news');
    if(!newsList){return;}
    var newsItems=newsList.getElementsByTagName('li');
    for(var i=0;i<newsItems.length;i++){
      newsItems[i].onmouseover=function(){
        DOMhelp.cssjs('add',this,newshl.overClass);
      }
      newsItems[i].onmouseout=function(){
        DOMhelp.cssjs('remove',this,newshl.overClass);
      }
    }
  }
}
DOMhelp.addEvent(window,'load',newshl.init,false);

如果你在 IE 6 中检查这个例子,你会得到和在更现代的浏览器中一样的效果。

您可以将 CSS 的伪类选择器用于动态效果(:hover,:active 和:focus),但是它们只将它们的设置应用于当前元素中包含的元素。

有了 JavaScript,整个 DOM 家族(包括 parentNode、nextSibling、firstChild 等等)都由您支配。

例如,如果您希望当用户将指针悬停在链接上时有不同的翻转状态,您可以轻松地扩展脚本来实现这一点。首先,您需要一个活动状态的新类:

listdouble 扷梓幂彻. CSS as used in examples double 扷梓幂彻. html

#news{
  font-size:.8em;
  background:#eee;
  width:21em;
  padding:.5em 0;
}
#news li{
  width:20em;
  padding:.5em;
  margin:0;
}
#news li:hover{
  background:#fff;
}
#news li.over{
  background:#fff;
}
#news li.active{
  background:#ffc;

}

然后,您需要将事件应用到列表项中的链接,并更改其父节点的父节点的类(因为本例中的链接要么在标题中,要么在段落中):

上市 double 扷梓幂彻. js

newshl={
  // CSS classes
  overClass:'over',     // Hover state of list item
  activeClass:'active', // Hover state on a link

  init:function(){
    if(!document.getElementById || !document.createTextNode){return;}
    var newsList=document.getElementById('news');
    if(!newsList){return;}
    var newsItems=newsList.getElementsByTagName('li');
    for(var i=0;i<newsItems.length;i++){
      newsItems[i].onmouseover=function(){
        DOMhelp.cssjs('add',this,newshl.overClass);
      }
      newsItems[i].onmouseout=function(){
        DOMhelp.cssjs('remove',this,newshl.overClass);
      }
    }
    var newsItemLinks=newsList.getElementsByTagName('a');
    for(i=0;i<newsItemLinks.length;i++){
      newsItemLinks[i].onmouseover=function(){
        var p=this.parentNode.parentNode;
        DOMhelp.cssjs('add',p,newshl.activeClass);
      }
      newsItemLinks[i].onmouseout=function(){
        var p=this.parentNode.parentNode;
        DOMhelp.cssjs('remove',p,newshl.activeClass);
      }
    }
  }
}
DOMhelp.addEvent(window, 'load', newshl.init, false);

结果是新闻条目有两种不同的状态,如图 5-5 所示,这取决于用户的指针是悬停在文本上还是链接上。

9781430250920_Fig05-05.jpg

图 5-5 。单个元素的不同翻转状态

使用 JavaScript,您还可以使整个新闻项目可点击,这就是更多事件发挥作用的地方。

如果你有一把锤子,所有的东西看起来都像钉子

这几个例子应该让你明白,JavaScript 和 DOM 是非常强大的,可以让浏览器表现出你想要的效果。

然而,问题是这是否值得努力,界限在哪里。一个想法是与您的客户或团队讨论您需要支持多少版本。像 Firefox 和 Chrome 这样的浏览器有六周的发布周期。这对开发人员来说更好,他们希望在针对 DOM 或 CSS3 时有更大的一致性。一些开发公司有一个支持当前浏览器加三个版本的政策。

JavaScript 比 CSS 有一个很大的优势:与文档的交流是双向的。虽然 CSS 只处理已经给定的内容,但是 JavaScript 可以读取值、测试支持、检查元素是否可用以及它们是什么,如果需要的话,甚至可以动态地创建元素。CSS 只能阅读文档,就像你只能阅读报纸一样,但是 JavaScript 也可以改变它。

JavaScript 的这种能力在很多 CSS 技巧中都有使用。许多效果只有在外部标记的情况下才有可能实现——嵌套元素、清除元素等等。因此,开发人员开始通过 JavaScript 生成这些内容,而不是期望它们出现在 HTML 中。这使得源文档看起来更加整洁;然而,对于最终用户来说,最终的 DOM 树——包括所有生成的内容——是他必须处理的事情。膨胀的 HTML 不会因为通过 JavaScript 产生膨胀而变得更好。也许有时候从一开始就简化一个界面或者让它更灵活比试图通过大量 CSS 和 JavaScript 魔法让它表现得像表格更好。

通过事件处理改变文档的行为

事件处理可能是 JavaScript 为关注用户界面的开发者提供的最好的东西。这也是最令人困惑的 JavaScript 主题之一——不是因为它复杂,而是因为有不同的方法来实现它们。我现在将向你解释什么是事件;展示一种古老的、屡试不爽的处理事件的方法;然后解释 W3C 推荐的方法。最后,您将了解如何调整 W3C 兼容方法,以允许不支持它的浏览器理解您的脚本。

事件可以是许多事情,例如:

  • 文档的初始加载和呈现
  • 图像的加载
  • 用户点击按钮
  • 用户按下一个键
  • 用户将鼠标移动到某个元素上

image 注意想象一个事件处理程序,比如一个运动探测器或者门铃的触点——如果有人靠近门,灯就会打开;如果有人按下门铃的按钮,电路闭合,触发响铃机构发出声音。同样,您可以检测用户何时将鼠标悬停在链接上以触发一个功能,以及当用户单击该链接时触发另一个功能。

您可以应用事件处理程序,以几种方式让您的脚本知道正在发生什么。当内容安全策略(CSP)被强制执行时,最引人注目且不起作用的方法是在 HTML:

<a href="moreinfo.html" onclick="return infoWindow(this.href)">more information</a>

在前面的章节中已经描述了一种更简洁的方法:通过类或 ID 来标识元素,然后在脚本中设置事件处理程序。同样重要的是要记住,当 CSP 启用时,此代码将不起作用。最简单且最受支持的方法是将事件处理程序作为属性直接应用于对象:

超文本标记语言

<a href="moreinfo.html" id="info">more information</a>

Java Script 语言

var triggerLink=document.getElementById('info');
triggerlink.onclick=infoWindow;

image 注意你不需要测试 ID 为 info 的元素,因为函数只能被它调用。

以这种方式触发事件引发了几个问题:

  • 你没有把元素发送给函数;相反,您需要再次找到该元素。
  • 一次只能分配一个功能。
  • 您独占地劫持了脚本中该元素的事件——试图将该元素用于其他事件的其他脚本的方法将不再工作。

除非您将 triggerLink 定义为全局变量或对象属性,否则函数 infoWindow()需要找到 trigger 元素才能使用。

function infoWindow(){
  var url=document.getElementById('info').getAttribute('href');
  // Other code
}

这个问题以及多个函数连接到一个事件的问题可以通过应用一个匿名函数来解决,该匿名函数调用您的一个或多个真实函数,这也允许您通过 This 关键字发送当前对象:

var triggerLink=document.getElementById('info');
triggerlink.onclick=function(){
  showInfoWindow(this.href);
  highLight(this);
  setCurrent(this);
}
function showInfoWindow(url){
  // Other code
}

第三个问题依然存在。只要您的脚本是文档中包含的最后一个脚本,它就会覆盖其他脚本的事件触发器,这意味着它不容易与其他脚本一起工作。因此,您需要一种方法来分配事件处理程序,而不覆盖其他脚本。当您希望在加载文档时调用不同的函数时,这一点尤其重要。

符合 W3C 的世界中的事件

W3C DOM-2 规范处理事件的方式略有不同,用 DOM-3 扩展了它们。首先,它们定义了事件发生的不同部分,直到详细使用检索到的数据:

  • 事件就是发生的事情——例如,点击。
  • 事件处理程序——例如 onclick——在 DOM-1 中,这是记录事件的位置。
  • 事件目标是事件发生的地方——在大多数情况下,是一个 HTML 元素。
  • 事件监听器是一个处理该事件的函数。
  • DOM-3 还引入了事件捕获的概念,这是控制事件如何通过 DOM 传播的能力。

应用事件

您可以通过 addEventListener()方法应用事件。这个函数有三个参数:字符串形式的事件,没有前缀上的,事件监听器函数的名称(没有括号),以及一个名为 useCapture 的布尔值,,它定义了是否应该使用事件捕捉。现在,将 useCapture 设置为 false 是安全的。通过使用 false,您的代码将在所有支持 addEventListener 的浏览器上工作(例如 9 版之前的 IE)。

如果希望通过 addEventListener()将函数 infoWindow()应用于链接,可以使用以下代码:

var triggerLink=document.getElementById('info');

triggerLink.addEventListener( 'click', infoWindow, false);

如果您希望通过在鼠标位于链接上方时调用 highlight()函数和在鼠标离开链接时调用 unhighlight()函数来添加悬停效果,您可以再添加几行:

var triggerLink=document.getElementById('info');
triggerLink.addEventListener( 'click', infoWindow, false);
triggerLink.addEventListener( 'mouseout', highlight, false);

triggerLink.addEventListener( 'mouseover', unhighlight, false);

检查哪个事件在哪里以及如何被触发

就开发的容易程度而言,您似乎又回到了起点:您必须再次从 infoWindow()中找到读取 href 的元素。这是真的;然而,通过使用 addEventListener,您可以提示符合标准的浏览器提供给您事件对象,,您可以通过一个参数读出该对象。这个参数可以叫任何你喜欢的名字;你可能会发现大多数开发人员只调用 item。

你可能以前见过这个 e,想知道它是什么,在不知道它来自哪里的情况下,你是否应该相信它。当您应用事件时,最初简单地使用参数而不发送它是非常令人困惑的,但是一旦您了解了事件对象,您将永远不会回到使用 onevent 属性。事件对象有许多可以在事件监听器函数中使用的属性:

  • 目标:触发事件的元素。
  • 类型:触发的事件(例如,click)。
  • 按钮:按下的鼠标按钮:0 表示向左,1 表示中间,2 表示向右。
  • keyCode:被按下的键的字符代码。W3C 规范也有密钥。在 IE 9 和更高版本中,key 将显示被按下的键。此外,webkit 浏览器不显示带有按键事件的箭头键的键码结果;请改用 keydown 或 keyup。
  • shiftKey、ctrlKey 和 Alt key:Boolean-如果分别按下 Shift、Ctrl 或 Alt 键,则为 true。

可用内容的完整列表取决于您正在收听的活动。你可以在 http://www.w3.org/TR/DOM-Level-3-Events/找到 DOM-3 规范中的所有属性。

使用 Event 对象,您可以轻松地使用一个函数来处理几个事件:

var triggerLink=document.getElementById('info');
triggerLink.addEventListener( 'click', infoWindow, false);
triggerLink.addEventListener( 'mouseout', infoWindow, false);
triggerLink.addEventListener( 'mouseover', infoWindow, false);

您可以对所有三个事件使用相同的函数,并检查事件类型:

function infoWindow(e){
  switch(e.type){
    case 'click':
       // Code to deal with the user clicking the link
    break;
    case 'mouseover':
       // Code to deal with the user hovering over the link
    break;
    case 'mouseout':
       // Code to deal with the user leaving the link
    break;
}

您还可以通过检查节点名来检查事件发生的元素。请注意,您必须再次使用 toLowerCase()来避免跨浏览器问题:

function infoWindow(e){
  targetElement=e.target.nodeName.toLowerCase();
  switch(targetElement){
    case 'input':
       // Code to deal with input elements
    break;
    case 'a':
       // Code to deal with links
    break;
    case 'h1':
       // Code to deal with the main heading
    break;
  }
}

停止事件传播

分配事件并使用事件侦听器拦截它们也意味着您需要注意两个问题:一个是许多事件都有默认操作——例如,click 可能会使浏览器跟踪一个链接或提交一个表单,而 keyup 可能会向表单字段添加一个字符。

另一个问题被称为事件冒泡。这个术语基本上意味着当一个事件发生在一个元素上时,它也发生在初始元素的所有父元素上。

事件冒泡

让我们回到新闻列表的 HTML 标记:

exampleEventBubble.html

<ul id="news">
<li>
<h3><a href="news.php?item=1">News Title 1</a></h3>
<p>Description 1</p>
<p class="more"><a href="news.php?item=1">more link 1</a></p>
</li>
<!-- and so on -->
</ul>

如果您现在将 mouseover 事件分配给列表中的链接,将鼠标悬停在它们上面也会触发任何事件侦听器,这些事件侦听器可能位于段落、列表项、列表以及节点树中所有其他元素之上,一直到文档正文。例如,您将看到如何将事件侦听器附加到每个元素,然后指向适当的函数:

event bubble . js

bubbleTest={
  init:function(){
    if(!document.getElementById || !document.createTextNode){return;}
    bubbleTest.n=document.getElementById('news');
    if(!bubbleTest.n){return;}

    bubbleTest.addMyListeners('click',bubbleTest.liTest,'li');
    bubbleTest.addMyListeners('click',bubbleTest.aTest,'a');
    bubbleTest.addMyListeners('click',bubbleTest.pTest,'p');

  },
  addMyListeners:function(eventName,functionName,elements){
    var temp=bubbleTest.n.getElementsByTagName(elements);
    for(var i=0;i<temp.length;i++){
      temp[i].addEventListener(eventName,functionName,false);
    }
  },
  liTest:function(e){
    alert('li was clicked');
  },
  pTest:function(e){
    alert('p was clicked');
  },
  aTest:function (e){
    alert('a was clicked');
  }
}
window.addEventListener('load',bubbleTest.init,false);

现在所有的列表项在被点击时都会触发 liTest()方法,所有的段落都会触发 pTest()方法,所有的链接都会触发 aTest()方法。

但是,如果您单击该段落,您将收到两个警告:

p was clicked
li was clicked

您可以通过使用 e.stopPropagation()方法来防止这种情况,该方法确保只有应用于链接的事件侦听器才会获取事件。此方法在 IE 9 及以上版本中有效;对于其他版本,使用 cancelBubble 属性并将其设置为 true。如果将 pTest()方法更改为以下内容:

stop propagation . js—在 exampleStopPropagation.html 使用

pTest:function(e){
  alert('p was clicked');
  e.stopPropagation();
},

输出将是

p 被点击

事件冒泡实际上没有那么多问题,因为您不太可能将不同的侦听器分配给嵌入式元素,而不是它们的父元素。然而,如果你想了解更多关于事件冒泡和事件发生时的顺序,彼得-保罗·科赫写了一篇精彩的解释,可在www.quirksmode.org/js/events_order.html获得。

防止默认操作

您可能遇到的另一个问题是,某些元素上的事件有默认操作。例如,表单向服务器提交数据。您可能还不希望这种情况发生,所以您可以停止默认操作,然后在数据发送到服务器之前执行您想要的任何工作。

在 DOM-1 事件处理程序模型中,通过在被调用的函数中返回一个 false 值来实现这一点:

element.onclick=function(){
  // Do other code
  return false;
}

如果您单击前面示例中的任何链接,它们将加载链接的文档。您可以通过使用 DOM-2 preventDefault()方法来覆盖它。这种方法在大多数浏览器中也得到广泛支持,包括 IE 版及以上版本。让我们通过将它添加到测试方法中来测试它:

prevent default . js—在 examplePreventDefault.html 使用

aTest:function (e){
  alert('a was clicked');
  e.stopPropagation();
  e.preventDefault();
}

现在单击链接只会显示警告:

a was clicked

另一方面,链接没有被关注,您将停留在同一页面上,对链接数据做一些不同的事情。例如,您可以最初只显示标题,并在单击标题时展开内容。首先,样式表中需要更多的类来支持这些更改:

listItemCollapse.css(节选)

.hide{
position: absolute !important;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
clip: rect(1px, 1px, 1px, 1px);
padding: 0 !important;
border: 0 !important;
height: 1px !important;
 width: 1px !important;
overflow: hidden;

}
li.current{
    background:#ccf;
}
li.current h3{
    background:#69c;
}

折叠元素的脚本并不复杂,但是它使用了我提到的所有事件处理元素:

newsItemCollapse.js

newshl={
  // CSS classes
  overClass:'over', // Rollover effect
  hideClass:'hide', // Hide things
  currentClass:'current', // Open item

  init:function(){
  var ps,i,hl;
  if(!document.getElementById || !document.createTextNode){return;}
    var newsList=document.getElementById('news');
    if(!newsList){return;}
    var newsItems=newsList.getElementsByTagName('li');
    for(i=0;i<newsItems.length;i++){
      hl=newsItems[i].getElementsByTagName('a')[0];
      hl.addEventListener('click',newshl.toggleNews,false);
      hl.addEventListener('mouseover',newshl.hover,false);
      hl.addEventListener('mouseout',newshl.hover,false);
    }
    var ps=newsList.getElementsByTagName('p');
    for(i=0;i<ps.length;i++){
      DOMhelp.cssjs('add',ps[i],newshl.hideClass);
    }
  },
  toggleNews:function(e){
    var section=e.target.parentNode.parentNode;
    var first=section.getElementsByTagName('p')[0];
    var action=DOMhelp.cssjs('check',first,newshl.hideClass)?'remove':'add';
    var sectionAction=action=='remove'?'add':'remove';
    var ps=section.getElementsByTagName('p');
    for(var i=0;i<ps.length;i++){
      DOMhelp.cssjs(action,ps[i],newshl.hideClass);
    }
    DOMhelp.cssjs(sectionAction,section,newshl.currentClass);
    e.preventDefault();
    e.stopPropagation();
  },
  hover:function(e){
    var hl=e.target.parentNode.parentNode;
    var action=e.type=='mouseout'?'remove':'add';
    DOMhelp.cssjs(action,hl,newshl.overClass);
  }
}
window.addEventListener ('load',newshl.init,false);

结果是可点击的新闻标题,当你点击它们时会显示相关的新闻摘要。“更多”链接不受影响,点击后会将访问者发送到完整的新闻文章。(参见图 5-6 。)

9781430250920_Fig05-06.jpg

图 5-6 。通过单击标题展开新项目

让我们一步一步地浏览整个脚本。在定义了 CSS 类属性并检查了必要的元素之后,您遍历列表项,获取第一个链接(标题内的链接),并为 click、mouseover 和 mouseout 分配事件侦听器。click 事件应该触发 newshl.toggleNews()方法,而 mouseout 和 mouseover 都应该触发 newshl.hover()。

newsItemCollapse.js(节选)

for(i=0;i<newsItems.length;i++){
  hl=newsItems[i].getElementsByTagName('a')[0];
  hl.addEventListener('click',newshl.toggleNews,false);
  hl.addEventListener('mouseover',newshl.hover,false);
  hl.addEventListener('mouseout',newshl.hover,false);
}

通过对列表项中的所有段落应用 hiding 类,可以隐藏它们:

newsItemCollapse.js(节选)

var ps=newsList.getElementsByTagName('p');
for(i=0;i<ps.length;i++){
  DOMhelp.cssjs('add',ps[i],newshl.hideClass);
}

toggleNews()方法通过读取事件对象的目标来获取当前部分。目标是链接,也就是说如果要到达列表项,需要两次上行至下一个父节点:

newsItemCollapse.js(节选)

toggleNews:function(e){
  var section=e.target.parentNode.parentNode;

您阅读列表项的第一段,并检查它是否已经分配了隐藏类。如果是这种情况,将变量 action 定义为 remove 否则,将其定义为 add。设置另一个名为 sectionAction 的变量,并使用相同的选项将其定义为 Action 的反义词:

newsItemCollapse.js(节选)

var first=section.getElementsByTagName('p')[0];
var action=DOMhelp.cssjs('check',first,newshl.hideClass)?'remove':'add';
var sectionAction=action=='remove'?'add':'remove';

遍历所有段落,并根据操作移除或添加隐藏类。对 section 和当前类执行相同的操作,但这次使用 sectionAction。这有效地切换了段落的可见性和标题的样式:

newsItemCollapse.js(节选)

var ps=section.getElementsByTagName('p');
for(var i=0;i<ps.length;i++){
  DOMhelp.cssjs(action,ps[i],newshl.hideClass);
}
DOMhelp.cssjs(sectionAction,section,newshl.currentClass);

通过调用 preventDefault()阻止最初单击的链接被跟踪,并通过调用 stopPropagation()禁止事件冒泡:

newsItemCollapse.js(节选)

  e.preventDefault();
  e.stopPropagation();
},

hover 方法通过 parentNode 获取列表项,并检查用于调用该方法的事件的类型。如果事件是 mouseout,它将动作定义为 remove 否则,它将操作定义为 add。然后从列表项中应用或移除该类:

newsItemCollapse.js(节选)

hover:function(e){
  var hl=e.target.parentNode.parentNode;
  var action=e.type=='mouseout'?'remove':'add';
  DOMhelp.cssjs(action,hl,newshl.overClass);
}

最后,向窗口对象添加一个事件监听器,当窗口完成加载时,该监听器触发 newshl.init():

newsItemCollapse.js(节选)

}
window.addEventListener ('load',newshl.init,false);

现在,您知道了在符合 DOM-3 的浏览器中单击后如何进行更改。是时候考虑一下其他浏览器了,并确保它们也能得到支持。

为不符合 W3C 的世界修复事件

现在您已经了解了事件处理的理论,是时候看看违反约定标准的违规者并学习如何处理他们了。

image 注意这里的 helper 方法已经包含在 DOMhelp.js 里面了,如果想不用 DOMhelp 也能在那里找到。

IE 从版本 9 开始支持 addEventListener()。对于更低版本,IE 有 attachEvent(),而不是将事件对象传递给每个监听器, IE 在 window.event 中保存一个全局事件对象。

一位名叫 Scott Andrew 的开发人员提出了一个名为 addEvent()的可移植函数,它解决了添加事件时的差异:

function addEvent(elm, evType, fn, useCapture) {
// Cross-browser event handling for IE5+, NS6+ and Mozilla/Gecko
// By Scott Andrew
  if (elm.addEventListener) {
    elm.addEventListener(evType, fn, useCapture);
    return true;
  } else if (elm.attachEvent) {
    var r = elm.attachEvent('on' + evType, fn);
    return r;
  } else {
    elm['on' + evType] = fn;
  }
}

该函数比 addEventListener()多使用一个参数,即元素本身。它测试是否支持 addEventListener(),并在能够以符合 W3C 的方式附加事件时简单地返回 true。

否则,它会检查是否支持 attachEvent(),并尝试以这种方式附加事件。注意,attachEvent()确实需要事件的前缀上的。对于既不支持 addEventListener()也不支持 attachEvent()的浏览器,像极旧的浏览器,该函数将 DOM-1 属性指向该函数。

image 注意关于如何改进 addEvent()的讨论正在进行中——例如,支持保留通过它将当前元素作为参数发送的选项——到目前为止已经开发了许多聪明的解决方案。因为每个解决方案都有不同的缺点,我在这里就不赘述了,但是如果你有兴趣,可以查看www . quirksmode . org/blog/archives/2005/10/_ and _ the _ winner _ 1 . html的 addEvent() recoding contest 页面上的评论。

因为 IE 使用一个全局事件,所以你不能依赖发送给你的监听器的事件对象。相反,您需要编写一个不同的函数来获取被激活的元素。事情变得更加混乱,因为 window.event 的属性与 W3C event 对象的属性略有不同:

  • 在 Internet Explorer 中,target 被替换为 srcElement。
  • 按钮返回不同的值。在 W3C 模型中,0 是左边的按钮,1 是中间的,2 是右边的;但是,IE 对左按钮返回 1,右按钮返回 2,中间按钮返回 4。当左右按钮同时按下时,它也返回 3,当三个按钮同时按下时,它返回 7。

为了适应这些变化,您可以使用此函数:

function getTarget(e){
  var target;
  if(window.event){
    target = window.event.srcElement;
 } else if (e){
    target = e.target;
} else {
   target = null ;
}
  return target;
}

或者更简单地说,使用三元运算符:

getTarget:function(e){
  var target = window.event ? window.event.srcElement :
      e ? e.target : null;
  if (!target){return false;}
  return target;
}

Safari 有一个讨厌的 bug(或者说特性——一个永远不确定):如果你点击一个链接,它不会把链接作为目标发送;相反,它发送链接中包含的文本节点。一种解决方法是检查元素的节点名是否确实是一个链接:

getTarget:function(e){
  var target = window.event ? window.event.srcElement : e ? e.target : null;
  if (!target){return false;}
  if (target.nodeName.toLowerCase() != 'a'){target = target.parentNode;}
  return target;
}

您防止默认操作和事件冒泡的努力还需要适应不同的浏览器实现:

  • stopPropagation()不是 IE 中的方法,而是名为 cancelBubble 的窗口事件的属性。
  • preventDefault()也不是方法,而是一个名为 returnValue 的属性。

这意味着您必须编写自己的 stopBubble()和 stopDefault()方法:

stopBubble:function(e){
   if(window.event && window.event.cancelBubble){
     window.event.cancelBubble = true;
   }
   if (e && e.stopPropagation){
     e.stopPropagation();
  }
}

image 注意 Safari 在 5.1 之前的版本中支持 stopPropagation(),但没有任何作用。在 5.1 及更高版本中,此问题已得到修复。

stopDefault:function(e){
  if(window.event && window.event.returnValue){
    window.event.cancelBubble = true;
  }
  if (e && e.preventDefault){
    e.preventDefault();
  }
}

因为您通常希望阻止这两种情况的发生,所以将它们收集在一个函数中可能是有意义的:

cancelClick:function(e){
  if (window.event && window.event.cancelBubble && window.event.returnValue){
    window.event.cancelBubble = true;
    window.event.returnValue = false;
    return;
  }
  if (e && e.stopPropagation && e.preventDefault){
    e.stopPropagation();
    e.preventDefault();
  }
}

使用这些助手方法应该允许您不引人注目地跨浏览器处理事件。

对于 Safari 之前的版本,解决方法是通过旧的 onevent 语法添加另一个虚拟函数,阻止链接被跟踪。现在,您将看到此修复的运行情况。让我们再次以折叠标题为例,用跨浏览器助手替换 DOM-3 兼容的方法和属性:

exampleXBrowserListItemCollapse.html 使用的 xBrowserListItemCollapse.js】

newshl = {
  // CSS classes
  overClass:'over',       // Rollover effect
  hideClass:'hide',       // Hide things
  currentClass:'current', // Open item

  init:function(){
  var ps,i,hl;
  if(!document.getElementById || !document.createTextNode){return;}
    var newsList = document.getElementById('news');
    if(!newsList){return;}
    var newsItems = newsList.getElementsByTagName('li');
    for(i = 0;i<newsItems.length;i++){
       hl = newsItems[i].getElementsByTagName('a')[0];
      DOMhelp.addEvent(hl,'click',newshl.toggleNews,false);
      hl.onclick = DOMhelp.safariClickFix;
      DOMhelp.addEvent(hl,'mouseover',newshl.hover,false);
      DOMhelp.addEvent(hl,'mouseout',newshl.hover,false);
    }
    var ps = newsList.getElementsByTagName('p');
    for(i = 0;i<ps.length;i++){
      DOMhelp.cssjs('add',ps[i],newshl.hideClass);
    }
  },
  toggleNews:function(e){
    var section = DOMhelp.getTarget(e).parentNode.parentNode;
    var first = section.getElementsByTagName('p')[0];
    var action = DOMhelp.cssjs('check',first,newshl.hideClass)?'remove':'add';
    var sectionAction = action == 'remove'?'add':'remove';
    var ps = section.getElementsByTagName('p');
    for(var i = 0;i<ps.length;i++){
      DOMhelp.cssjs(action,ps[i],newshl.hideClass);
    }
    DOMhelp.cssjs(sectionAction,section,newshl.currentClass);
    DOMhelp.cancelClick(e);
  },
  hover:function(e){
    var hl = DOMhelp.getTarget(e).parentNode.parentNode;
    var action = e.type == 'mouseout'?'remove':'add';
    DOMhelp.cssjs(action,hl,newshl.overClass);
  }
}
DOMhelp.addEvent(window,'load',newshl.init,false);

image HL . onclick = DOM help . safariclickfix;可能是一个简单的 HL . onclick = function(){ return false;};然而,一旦 Safari 开发团队解决了这个问题,搜索和替换这个修复将变得更加容易。

可点击的标题现在可以在所有现代浏览器上使用;然而,看起来你可以稍微简化一下这个脚本。现在这些例子循环了很多,这并不是真正必要的。简单地向列表项中添加一个类并让 CSS 引擎隐藏所有段落要比单独隐藏列表项中的所有段落容易得多:

listitemcollapseshort . CSS(节选)——在 exampleListItemCollapseShorter.html 使用

#news li.hide p{
  display:none;
}
#news li.current p{
  display:block;
}

这样,您可以通过 init()方法中的所有段落来消除内部循环,并用一行代码来替换它,该代码将 hide 类应用于列表项本身,如下所示:

listitemcollapseshort . js(节选)——在 exampleListItemCollapseShorter.html 使用

newshl={
  // CSS classes
  overClass:'over',       // Rollover effect
  hideClass:'hide',       // Hide things
  currentClass:'current', // Open item

  init:function(){
    var hl;
    if(!document.getElementById || !document.createTextNode){return;}
    var newsList=document.getElementById('news');
    if(!newsList){return;}
    var newsItems=newsList.getElementsByTagName('li');
    for(var i=0;i<newsItems.length;i++){
      hl=newsItems[i].getElementsByTagName('a')[0];
      DOMhelp.addEvent(hl,'click',newshl.toggleNews,false);
      DOMhelp.addEvent(hl,'mouseover',newshl.hover,false);
      DOMhelp.addEvent(hl,'mouseout',newshl.hover,false);
      hl.onclick = DOMhelp.safariClickFix;
      DOMhelp.cssjs('add',newsItems[i],newshl.hideClass);
    }
  },

下一个变化是在 toggleNews()方法中。在这里,用一个简单的 if 条件替换循环,该条件检查当前类是否应用于列表项,如果是,用 current 替换 hide,如果不是,用 hide 替换 current。这将显示或隐藏列表项中的所有段落:

listitemcollapseshort . js(节选)——在 exampleListItemCollapseShorter.html 使用

toggleNews:function(e){
  var section=DOMhelp.getTarget(e).parentNode.parentNode;
  if(DOMhelp.cssjs('check',section,newshl.currentClass)){
    DOMhelp.cssjs('swap',section,newshl.currentClass, newshl.hideClass);
  }else{
    DOMhelp.cssjs('swap',section,newshl.hideClass, newshl.currentClass);
  }
  DOMhelp.cancelClick(e);
},

其余的保持不变:

listitemcollapseshort . js(节选)——在 exampleListItemCollapseShorter.html 使用

  hover:function(e){
    var hl = DOMhelp.getTarget(e).parentNode.parentNode;
    var action = e.type == 'mouseout'?'remove':'add';
    DOMhelp.cssjs(action,hl,newshl.overClass);
  }
}
DOMhelp.addEvent(window,'load',newshl.init,false);

永不停止优化

你应该永远不要停止以这种方式分析你自己的代码,以确定哪些可以优化,即使在最激动的时刻,愉快地编码并创建一些对自己有益的过于复杂的东西是非常诱人的。退一步,分析你想要解决的问题,重新评估已经存在的问题,有时比继续努力要有益得多。在这种情况下,优化是将元素的隐藏留给 CSS 中的级联,而不是遍历子元素并单独隐藏它们。

当你再次审视自己编写的代码时,下面的想法总是值得牢记在心:

  • 任何避免嵌套循环的想法都是好主意。
  • 主对象的属性是存储一些方法感兴趣的信息的好地方,例如,在站点导航中哪个元素是活动的。
  • 如果你发现自己一遍又一遍地重复代码,创建一个新的方法来完成这个任务——如果你将来不得不改变代码,你只需要在一个地方改变它。
  • 不要过多地遍历节点树。如果许多元素需要了解其他元素——只需找到一次,并将其存储在一个属性中。这将大大缩短代码,因为像 contentSection 这样的内容比 elm . parent node . parent node . next sibling 要短得多。
  • 将一长串 if 和 else 语句作为 switch/case 块来处理可能容易得多。
  • 如果某些东西将来可能会改变,比如 Safari stopPropagation() hack,您应该将它放在自己的方法中。下一次当您看到代码并发现这个看似无用的方法时,您会记得发生了什么。
  • 不要太依赖 HTML。它总是第一件要改变的事情(尤其是当涉及到 CMS 的时候)。

丑陋的页面加载问题及其丑陋的解决方案

当开发人员开始广泛使用 CSS 时,他们很快就遇到了一些烦人的浏览器 bug。其中一个是无样式内容的 flash(??),也称为 FOUC(??)??(你可以在 http://www.bluerobot.com/web/css/fouc.asp了解更多)。这种效果在应用样式表之前短暂显示没有样式表的页面。

现在,JavaScript 增强的页面也面临同样的问题。如果您加载折叠新闻条目的示例,您将看到所有新闻在一小段时间内展开。这个短暂的时刻是文档及其所有依赖项(如图像和第三方内容)完成加载所需的时间。

这种行为已经用一个设计者的眼光让脚本爱好者烦恼很久了;当页面和所有包含的媒体(如图像)被加载时,onload 事件被触发,就是这样——直到许多聪明的 DOM 脚本编写人员集思广益,开始尝试。

对此的一个解决方案是使用 DOMContentLoadedevent,document . addevent listener(" DOMContentLoaded ",init,false)。这个事件允许你在 DOM 的所有元素都被加载后调用你的函数。目前所有浏览器都支持这一点(IE 从 9.0 开始,Opera 从 9.0 开始,Safari 从 3.1 开始)。

在 IE 的早期版本中,您可以查找 onReadyStateChange/readyState 事件和属性:

document.onreadystatechange = checkState
function checkState(){
if(document.readyState == "complete"){
//run code
}
}

读取和过滤键盘输入

你可能会用到的最常见的 web 事件是 click,因为它的好处是每个元素都支持它,如果可以通过键盘访问到相关的元素,那么键盘和鼠标都可以触发它。

但是,没有什么可以阻止您使用 keyup 或 keypress 处理程序检查 JavaScript 中的键盘输入。前者是 W3C 标准;后者不在标准中,发生在 keydown 和 keyup 之后,但它在浏览器中得到很好的支持。

作为如何读出和使用键盘输入的一个例子,让我们写一个脚本来检查在一个表单字段中输入的数据是否是纯数字。您已经在第二章的中测试了条目并将其转换成数字,但是这一次您想在条目发生时检查它,而不是在用户提交表单之后。如果用户输入非数字字符,脚本应该禁用提交按钮并显示一条错误消息。

从一个只有一个输入字段的简单 HTML 表单开始:

exampleKeyChecking.html

<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Example: Checking keyboard entry</title>
<link rel="stylesheet" href="keyChecking.css">
<script type="text/javascript" src="DOMhelp.js"></script>
<script type="text/javascript" src="keyChecking.js"></script>
</head>
<body>
<p class="ex">Keychecking example, try to enter anything but
numbers in the form field below.</p>
<h1>Get Chris Heilmann's book cheaper!</h1>
<form action="nothere.php" method="post">
<p>
<label for="Voucher">Voucher Number</label>
<input type="text" name="Voucher" id="Voucher" />
<input type="submit" value="redeem" />
</p>
</form>
</body>
</html>

在应用以下脚本后,浏览器将检查用户输入的内容,当输入的内容不是数字时,会显示一条错误消息并禁用提交按钮,如图 5-7 所示。

9781430250920_Fig05-07.jpg

图 5-7 。在输入条目时对其进行测试

keyChecking.js

voucherCheck={
  errorMessage:'A voucher can contain only numbers.',
  error:false,
  errorClass:'error',
  init:function(){
    if (!document.getElementById || !document.createTextNode) { return; }
    var voucher=document.getElementById('Voucher');
    if(!voucher){return;}
    voucherCheck.v=voucher;
    DOMhelp.addEvent(voucher, 'keyup', voucherCheck.checkKey, false);
  },
  checkKey:function(e){
    if(window.event){
      var key = window.event.keyCode;
    } else if(e){
      var key=e.keyCode;
    }
    var v=document.getElementById('Voucher');
    if(voucherCheck.error){
      v.parentNode.removeChild(v.parentNode.lastChild);
      voucherCheck.error=false;
      DOMhelp.closestSibling(v,1).disabled='';
    }
    if(key<48 || key>57){
      v.value=v.value.substring(0,v.value.length-1);
      voucherCheck.error=document.createElement('span');
      DOMhelp.cssjs('add', voucherCheck.error, voucherCheck.errorClass);
      var message = document.createTextNode(voucherCheck.errorMessage)
      voucherCheck.error.appendChild(msg);
      v.parentNode.appendChild(voucherCheck.error);
      DOMhelp.closestSibling(v,1).disabled='disabled';
   }
  }
}
DOMhelp.addEvent(window, 'load', voucherCheck.init, false);

首先,您将从定义一些属性开始,比如一个错误消息、一个指示是否已经显示了错误的布尔值,以及一个应用于错误消息的类。测试必要的元素,并附加一个指向 checkKey()方法的 keyup 事件:

keyChecking.js(节选)

voucherCheck={
  errorMessage:'A voucher can only contain numbers.',
  error:false,
  errorClass:'error',
  init:function(){
    if (!document.getElementById || !document.createTextNode) { return; }
    var voucher=document.getElementById('Voucher');
    if(!voucher){return;}
    voucherCheck.v=voucher;
    DOMhelp.addEvent(voucher, 'keyup', voucherCheck.checkKey, false);
  },

checkKey 方法确定 window.event 或 event 对象是否正在使用中,并以适当的方式读出键码:

keyChecking.js(节选)

checkKey:function(e){
    if(window.event){
      var key = window.event.keyCode;
    } else if(e){
      var key=e.keyCode;
    }

然后,它检索元素(在本例中是通过 getElementById(),虽然您可以轻松地使用 DOMhelp.getTarget(e),但为什么要使它比需要的更复杂呢?)并检查 error 属性是否为真。如果为真,则已经有一个可见的错误消息,并且提交按钮被禁用。在这种情况下,您需要删除错误消息,将 error 属性设置为 false,并启用 Submit 按钮(它是 input 元素的下一个同级,这里使用 closestSibling()以确保它是按钮而不是换行符)。

keyChecking.js(节选)

var v=document.getElementById('Voucher');
if(voucherCheck.error){
  v.parentNode.removeChild(v.parentNode.lastChild);
  voucherCheck.error=false;
  DOMhelp.closestSibling(v,1).disabled='';
}

您确定按下的键不是 0 到 9 之间的任何数字,也就是说,它的 ASCII 码不在 48 和 57 之间。

Tip You can get the values of each key in any ASCII table, for example, at www.whatasciicode.com/.

如果该键不是数字键,则从字段值中删除最后输入的键,并创建新的错误消息。创建一个新的 span 元素,添加类,添加错误消息,将其作为一个新的子元素添加到文本输入框的父元素中,并禁用 form 按钮。最后缺少的是在页面完成加载时启动 voucherCheck.init()。

keyChecking.js(节选)

    if(key<48 || key>57){
     v.value=v.value.substring(0,v.value.length-1);
     voucherCheck.error=document.createElement('span');
     DOMhelp.cssjs('add', voucherCheck.error, voucherCheck.errorClass);
     var message = document.createTextNode(voucherCheck.errorMessage)
     voucherCheck.error.appendChild(msg);
     v.parentNode.appendChild(voucherCheck.error);
     DOMhelp.closestSibling(v,1).disabled='disabled';
   }
  }
}
DOMhelp.addEvent(window, 'load', voucherCheck.init, false);

image 注意通常,在每个 keyup 事件中检查字段内容是否是一个数字就足够了,但是这展示了键盘事件的强大功能。

如果要读取带有 Shift、Ctrl 或 Alt 的键盘组合,需要在事件侦听器方法中检查 shiftKey、ctrlKey 或 altKey 事件属性,例如:

if(e.shiftKey && key==48){alert('shift and 0');}
if(e.ctrlKey && key==48){alert('ctrl and 0');}
if(e.altKey && key==48){alert('alt and 0');}

事件处理的危险

使用这些功能,您可以监听用户发起的任何事件并对其做出反应。您可以创建对滚动而不是对链接的点击作出反应的导航,您可以添加只对您的页面可用的键盘快捷键,并且您可以使事物对鼠标的移动作出反应。

充分利用事件处理并提出全新的导航、用户旅程流以及表单如何与用户交互的概念是非常诱人的。问题是这是好事还是坏事。

根据你自己对什么是好的和有用的想法,你可能有时会认为拖放界面是最好的,但是对于不能移动鼠标的用户呢?通过附加事件,可以使文档中的任何内容成为交互式元素;然而,并不是所有的用户代理都允许访问者在没有鼠标的情况下访问元素。键盘用户不能点击标题,但嵌入链接的标题是因为用户可以点击链接,但不能点击标题。基本的可访问性指南和法律要求强调,如果您想用 DOM 脚本和 HTML 创建自己的富界面,您必须保持输入设备独立。

拖放式界面没有任何问题,只要你还允许键盘访问它。因为您不应该依赖于可用的 JavaScript,所以无论如何您都需要可拖动元素上的真实链接,这可以通过单击事件甚至键盘访问来增强。

键盘事件处理是另一个棘手的问题。尽管所有的浏览器都支持 keydown,但是你永远不知道你想分配给一个元素的快捷键是否对用户机器上的另一个软件是不必要的。

键盘访问普遍是操作系统的一部分,需要特定组合键才能使用它的访问者不会喜欢你为了你的目的劫持那些组合键而妨碍他们的工作。因此,聪明的 web 应用使它们的键盘快捷键可选,甚至可以由用户定制。

当您使用 accesskey 属性时,HTML 中也会出现同样的问题。该属性告诉浏览器在按下属性值中定义的键时激活元素(在 IE 和 Mozilla 上与 Alt 键一起,通过其他浏览器上的其他组合)。实际上,这是添加一个事件并分配一个事件侦听器,该事件侦听器设置元素的焦点或遵循该元素的默认操作。直到最近,对这些属性使用数字键还是一种常见的做法,并且被认为是安全的——这很有效,直到有一个用户的名字中有特殊字符,并且需要使用 Alt 和字符的 ASCII 码来输入。

摘要

你已经读完了这一章,我希望它不会一下子包含太多的信息。

在前半部分,我谈到了 CSS 和 JavaScript 的交互,包括以下内容:

  • 如何通过样式集合改变 JavaScript 中的表示
  • 如何通过在 CSS 类中保持脚本的外观来帮助 CSS 设计者
  • 如何为 CSS 设计者提供钩子,根据脚本的启用或禁用来设计不同的文档样式
  • 介绍不同的第三方风格转换器,以及已发布的 JavaScript 脚本不是一成不变的,而是可以随着时间的推移而改进和完善的思想
  • 如何通过引入只包含 CSS 名称信息的对象来简化 CSS 和 JavaScript 协同工作的维护
  • 修复 JavaScript 的 CSS 问题—在本章的例子中,多列显示不具有相同的高度
  • 通过应用跨浏览器悬停效果来帮助 CSS 设计者
  • 使用 JavaScript 创建大量 HTML 元素来支持 CSS 效果而不是从一开始就通过 JavaScript 实现这些效果的危险

然后我们继续讨论是什么让网站点击——确切地说是有时——换句话说,是事件处理。我谈到了

  • 如何通过 DOM-1 onevent 属性(如 onclick、onmouseover 等)在旧浏览器中应用事件处理
  • W3C 对 DOM-3 规范中的事件有什么看法,以及如何使用它所推荐的内容
  • 如何让不兼容的浏览器也这样做
  • 如何避免页面未完全加载时的显示问题
  • 如何处理键盘输入
  • 事件处理的危险

就是这样——您现在应该拥有了所有需要的工具,可以用稳定、易于维护、流畅的 JavaScript 让大众惊叹不已。在下一章,我将介绍 JavaScript 的一些最常见的用法,并尝试为它们开发最新的解决方案,以取代您可能已经在使用的旧脚本。

六、JavaScript 的常见用法:图像和窗口

如果你阅读了最后 的几章,你现在应该很好地掌握了 JavaScript 知识及其与层叠样式表(CSS)和 HTML 的交互。现在你将了解 JavaScript 在网络上的一些最常见的用法,我们将通过一些例子。在这些例子中,您将看到如何确保 JavaScript 的这些实现独立于页面上的其他脚本工作,我将解释可能会出现什么问题。我还将谈到一些很容易使用但可能不是最安全的选项的功能。

image 注意这一章有很多代码示例,你会被要求在浏览器中打开其中的一些来自己测试功能。如果你还没有去过www.beginningjavascript.com下载这本书的代码示例,现在可能是个好时机。

这里的大部分完整代码示例使用 DOM-3 事件处理。这使得它们比它们的 DOM-1 对等物更复杂一些,但是这也使得它们能够更好地与其他脚本一起工作,并且它们更有可能在未来的浏览器中工作。请耐心听我说,我保证通过反复使用这些方法,你会很快掌握它们的窍门。

开发这些示例时也考虑到了维护和灵活性。这意味着不熟悉 JavaScript 的人在以后可能会更改的所有内容都存储在属性中,并且您可以很容易地让同一文档的几个部分使用脚本的功能。这也增加了一些脚本的复杂性,但这是大多数客户要求的现实生活中的可交付成果。

图像和 JavaScript

图像的动态变化很可能是 JavaScript 的第一个“惊艳”效果。当浏览器还不支持 CSS 时(公平地说,CSS 仍在定义过程中),当用户将鼠标移到图像上或单击图像时,JavaScript 是改变图像的唯一方法。近年来,越来越多传统上通过 JavaScript 实现的图像效果已经被纯 CSS 解决方案所取代,这使得维护变得更加容易。我将在后面讨论这些;现在,让我们看看 JavaScript 能对图像做些什么的基础知识。

图像脚本基础知识

在 JavaScript 中,您可以通过两种方式访问和修改图像:通过 getElementsByTagName()和 getElementById()的 DOM-2 方式,或者通过涉及存储在 document 对象的属性中的 images 集合的旧方式。举个例子,我们来看一个包含照片列表的 HTML 文档:

<ul class="slides">
  <li><img src="pictures/thumbs/cat2.jpg" alt="Lazy Cat"></li>
  <li><img src="pictures/thumbs/dog10.jpg" alt="Dog using the shade"></li>
  <li><img src="pictures/thumbs/dog12.jpg" alt="Squinting Dog"></li>
  <li><img src="pictures/thumbs/dog63.jpg" alt="Dog cooling off in the sand"></li>
  <li><img src="pictures/thumbs/dog7.jpg"  alt="Very flat dog"></li>
  <li><img src="pictures/thumbs/donkeycloseup.jpg" alt="Curious Donkey"></li>
  <li><img src="pictures/thumbs/donkeyeating.jpg" alt="Hay-eating Donkey"></li>
  <li><img src="pictures/thumbs/kittenflat.jpg" alt="Ginger and White Cat"></li>
</ul>

您可以在 JavaScript 中检索所有这些照片,以两种方式对它们进行处理:

// Old DOM
var photosOldDOM=document.images;
// New DOM
var photos=document.getElementsByTagName('img');

这两种方法都会产生一个包含所有图像作为对象的数组。与任何对象一样,您可以读取并操作它们的属性。比方说,您想知道第三个图像的可选文本。您需要做的只是读出对象的 alt 属性:

// Old DOM alt property
var photosOldDOM=document.images;
alert(photosOldDOM[2].alt);
// W3C DOM-2 alt attribute
var photos=document.getElementsByTagName('img');
alert(photos[2].getAttribute('alt'));

图像有几个属性,其中一些是显而易见的。但是还有一些你可能没听说过的:

  • border:HTML 中 border 属性的值
  • 名称:img 标签的名称属性
  • complete:如果图像已完成加载,则该属性为 true(这是只读的,不能更改该属性)
  • height:图像的高度(以像素为单位,以整数形式返回)
  • width:图像的宽度(以像素为单位,以整数形式返回)
  • hspace:图像周围的水平空间
  • vspace:图像周围的垂直空间
  • lowsrc:同名属性中定义的图像预览
  • src:图像的 URL

您可以使用这些属性来动态地访问和更改图像。如果在浏览器中打开示例文档 exampleImageProperties.html,可以读写演示图像的属性,如图图 6-1 所示。

9781430250920_Fig06-01.jpg

图 6-1 。读取和写入图像的属性

image 注意如果图像的尺寸是通过 HTML 的宽度和高度属性定义的,并且你改变了它的来源,你不会自动改变它的尺寸。例如,激活演示中的“设置其他图片”按钮。这可能会导致另一幅图像难看的扭曲,因为浏览器不会以复杂的方式调整图像的大小。

预加载图像

如果您在页面中动态地使用图像来获得翻转或幻灯片效果,您会希望将图像加载到浏览器的内存缓存中,以给访问者一个流畅的体验。你可以用几种方法做到这一点。一种方法是在初始化页面时为每个要预加载的图像创建一个新的图像对象:

kitten = new Image();
kitten.src = 'pictures/kittenflat.jpg';

在“翻转效果”一节中,您将很快看到一个这样的例子:

function simplePreload() {
  var args = simplePreload.arguments;
  document.imageArray = new Array( args.length );
  for(var i = 0; i < args.length; i++ ) {
    document.imageArray[i] = new Image;
    document.imageArray[i].src = args[i];
  }
}

如果您使用想要预加载的图像调用此函数,它将创建一个包含所有图像的新数组,一个接一个地加载它们,例如:

simplePreload('pictures/cat2.jpg', 'pictures/dog10.jpg');

一种不同的、独立于脚本的预加载图像的方式是将它们作为 1×1 像素的图像放在 HTML 的容器元素中,该元素通过 CSS 隐藏。这混合了结构和行为,与任何图像预加载技术有相同的问题:你强迫访问者下载大量他可能不想立即看到的图像。如果您使用预加载器,您可能希望让它们保持可选,让用户决定是否要预加载所有图像。

我将在这里简短地讨论图像预加载,因为关于图像还有很多东西要学。

翻滚特效

当 JavaScript 首次在最常见的用户代理中得到广泛支持时,翻转或悬停效果是绝对的狂热。编写了许多脚本,出现了许多小工具,允许“无需任何编码的即时翻转生成”

翻转效果的想法很简单:你将鼠标悬停在一个图像上,图像会发生变化,这表明这是一个可点击的图像,而不仅仅是视觉效果。图 6-2 显示了翻转效果。

9781430250920_Fig06-02.jpg

图 6-2 。翻转效果意味着当鼠标悬停在元素上时,元素会改变其外观

使用多个图像的翻转

当鼠标悬停在图像上时,可以通过更改图像的 src 属性来创建翻转效果。老式的翻转效果依赖于标签的 name 属性,并使用了 images 集合。像这样的结构在 20 世纪 90 年代的网页中并不少见:

exampleSimpleRollover.html(节选)

HTML

<a href="contact.html"
   onmouseover="rollover('contact', 'but_contact_on.jpg')"
   onmouseout="rollover('contact', 'but_contact.jpg')">
   <img src="but_contact.jpg" name="contact" width="103" height="28" alt="Contact Us" border="0">
</a>

JavaScript

function rollover( img, url ) {
  document.images[img].src=url;
}

翻转的问题是(现在仍然是)第二个映像可能还没有加载,这是适得其反的。这是一个交互元素的事实并不是显而易见的——只有在显示第二个图像时才变得明显。因此,在这种情况下,这会使用户感到困惑,而不是有所帮助。这就是为什么传统的翻转功能(如 Adobe Dreamweaver 附带的功能)将前面介绍的图像对象预加载技术与 name 属性结合使用。

丹尼尔·诺兰在 2003 年提出了一个非常聪明的解决方案,正如在 http://www.dnolan.com/code/js/rollover/的描述。他的解决方案使用图像的文件名,并假设翻转状态的后缀为“_o”。您只需将一个名为 imgover 的类添加到您希望具有翻转效果的图像中。

您可以使用 DOM-3 处理程序轻松复制相同的功能。首先,您需要一个 HTML 文档,其中包含分配了正确类别的图像:

exampleAutomatedRollover.html(节选)

<ul>
  <li>
    <a href="option1.html">
      <img src="but_1.jpg" class="roll" alt="option one">Option 1
    </a>
  </li>
  <li>
    <a href="option2.html">
      <img src="but_2.jpg" class="roll" alt="option two"> Option 2
    </a>
  </li>
  [... code snipped ...]
</ul>

然后你计划你的剧本。脚本的主要对象将被称为 ro,用于翻转。因为您想让未来的维护者尽可能地简单,所以您保留了主对象属性中可能发生变化的所有细节。

在这个脚本中,这个类定义了哪个图像应该获得翻转状态以及鼠标经过图像的后缀。在这种情况下,您将分别使用“roll”和“_on”。您将需要两个方法:一个初始化效果,一个做翻转。此外,您将需要一个数组来存储预加载的图像。所有这些共同构成了翻转脚本的框架:

自动调速器. js(骨架)

ro = {
  rollClass : 'roll',
  overSrcAddOn : '_on',
  preLoads : [],
  init : function(){},
  roll : function( e ){}
}
DOMhelp.addEvent( window, 'load', ro.init, false );

让我们开始充实骨架吧。首先是属性和 init 方法。在其中,您预定义了一个名为 oversrc 的变量,并将文档的所有图像存储在一个名为 imgs 的数组中。你循环遍历这些图片,跳过那些没有合适的 CSS 类的图片:

自动调速器. js (excerpt)

ro = {
  rollClass : 'roll',
  overSrcAddOn : '_on',
  preLoads : [],
  init : function() {
    var oversrc;
    var imgs = document.images;
    for( var i = 0; i < imgs.length; i++ ) {
      if( !DOMhelp.cssjs('check', imgs[i], ro.rollClass ) ) {
        continue;
      }

如果图像附加了正确的 CSS 类,则读取其 source 属性,用 overSrcAddOn 属性中定义的后缀替换其中的句号,后跟一个句号,并将结果存储在 oversrc 变量中:

automatedRollover.js(续)

oversrc = imgs[i].src.toString().replace('. ',ro.overSrcAddOn + '. ');

image 注意例如,文档中的第一个图像具有 src but_1.jpg。此处定义了后缀属性的 oversrc 的值将是 but_1_on.jpg

然后创建一个新的 image 对象,并将其存储为 preLoads 数组的一个新项。将新图像的 src 属性设置为 oversrc。使用 DOMhelp 库中的 addEvent()为 mouseover 和 mouseout 添加一个指向 roll 方法的事件处理程序。

automatedRollover.js(续)

   ro.preLoads[i] = new Image();
   ro.preLoads[i].src = oversrc;
   DOMhelp.addEvent( imgs[i], 'mouseover', ro.roll, false );
   DOMhelp.addEvent( imgs[i], 'mouseout', ro.roll, false );
  }
},

roll 方法通过 getTarget(e)检索发生事件的图像,并将其 src 属性存储在一个名为 s 的变量中,然后通过读取事件类型来测试发生了哪个事件。如果事件类型是 mouseover,您可以将文件名中的句号替换为 add-on 后跟一个句号,如果事件是 mouseout,则反之亦然。向窗口添加一个事件处理程序,当窗口完成加载时,它调用 ro.init():

automatedRollover.js(续)

  roll : function( e ) {
    var t = DOMhelp.getTarget( e );
    var s = t.src;
    if( e.type == 'mouseover' ) {
      t.src = s.replace('.', ro.overSrcAddOn + '. ' );
    }
    if( e.type == 'mouseout' ) {
      t.src = s.replace( ro.overSrcAddOn + '. ', '. ' );
    }
  }
}
DOMhelp.addEvent( window, 'load', ro.init, false );

演示页面的结果,如图 6-3 所示,突出显示了当用户悬停在原始图像上时已经加载到浏览器缓存中的图像。

9781430250920_Fig06-03.jpg

图 6-3 。预载和自动翻转

尽管您可以尝试使用巧妙的脚本来预加载图像,但它可能并不总是有效。用户的浏览器缓存设置或其连接中的特殊设置可能会使其无法在不真正将图像添加到文档的情况下偷偷预加载某些内容。因此,您可能会发现更安全的选择是为翻转效果使用单个图像。

使用单一图像的翻转效果

当 CSS 设计者开始探索:hover 伪选择器,不仅仅是改变链接的下划线时,CSS 专用翻转就诞生了。这基本上意味着你给链接和链接的悬停状态分配不同的背景图像。

同样的问题也发生了——图像必须在显示之前加载,这使得翻转效果闪烁或者根本不发生。解决方案是为两种状态拍摄一张单独的图像,并使用背景位置属性来改变图像的位置,如图 6-4 所示。

9781430250920_Fig06-04.jpg

图 6-4 。背景位置和 CSS 的翻转效果

您可以通过在浏览器中打开 exampleCSSonlyRollover.html 来查看效果。所讨论的 CSS 将链接限制在某个大小,并通过将处于悬停状态的背景图像向左移动(通过图像宽度一半的负背景位置值)来实现翻转效果:

exampleCSSonlyRollover.html(节选)

#nav a{
  width:103px;
  padding-top:6px;
  height:22px;
  background:url(doublebutton.jpg) top left no-repeat #ccc;
}
#nav a:hover{
  background-position:-103px 0;
}

在 JavaScript 中也可以这样做;然而,让我们更有创造性,做一些 CSS 不能做的事情。

父元素上的翻转效果

让我们用一个 HTML 列表,通过添加一个漂亮的背景图片,把它变成一个时髦的导航栏,然后当鼠标悬停在链接上时,让链接改变背景图片。你首先需要的是一张背景图片,上面有背景的所有状态,如图图 6-5 所示。

9781430250920_Fig06-05.jpg

图 6-5 。导航背景与所有状态(调整大小)

导航栏的 HTML 是一个链接列表。因为基本的网站可用性指南强烈建议永远不要链接当前页面,所以当前链接被替换为一个标签:

exampleParentRollover.html(节选)

<ul id="nav">
  <li><a href="index.html">Home</a></li>
  <li><a href="documentation.html">Documentation</a></li>
  <li><strong>Products</strong></li>
  <li><a href="contact.html">Contact Us</a></li>
</ul>

然而,因为这个导航可能是多级导航菜单中的第一级,高亮可能不是一个强元素,而是列表项上的一个类:

<ul id="nav">
  <li><a href="index.html">Home</a></li>
  <li><a href="documentation.html">Documentation</a></li>
  <li class="current"><a href="products.html">Products</a></li>
  <li><a href="contact.html">Contact Us</a></li>
</ul>

两种情况都必须考虑。解释演示页面中的 CSS 不是本书的目的;可以说,您用 ID nav 固定了列表的维度,将其向左浮动,并浮动其中的所有列表元素。

相反,让我们直接开始计划剧本。您需要为主对象定义几个属性(称为 pr 代表父翻转)、导航列表的 ID、导航的高度(也是每个图像的高度,并且是背景位置所必需的),以及可能用来突出显示当前部分而不是一个<强>标签的可选类:

parentRollover.js(节选)

pr = {
  navId : 'nav',
  navHeight : 50,
  currentLink : 'current',

您从一个初始化方法开始,该方法检查 DOM 支持,以及具有正确 ID 的必要列表是否可用:

parentRollover.js(续)

init : function() {
  if( !document.getElementById || !document.createTextNode ) {
    return;
  }
  pr.nav = document.getElementById( pr.navId );
  if( !pr.nav ){ return; }

下一个任务是遍历这个列表中包含的所有列表项,并检查该项中是否有强元素或者该项是否有“当前”类。如果任一情况为真,脚本应该将循环的计数器存储在主对象的当前属性中。该属性将在 rollover 方法中使用,以将背景重置为原始状态:

parentRollover.js(续)

var lis = document.getElementsByTagName('li');

for(var i = 0; i < lis.length; i++)
{
  if( lis[i].getElementsByTagName('strong').length > 0 || DOMhelp.cssjs('check', lis[i], pr.currentLink) ) {
    pr.current = i;
  }

每个列表项都获得一个名为 index 的新属性,该属性在整个列表数组中包含它的计数器值。使用此属性是一种技巧,可以避免您必须循环遍历所有列表项,并将它们与事件侦听器方法中的目标进行比较。

您分配了两个指向 roll()方法的事件处理程序:一个是当鼠标在列表项上时,另一个是当鼠标离开列表项时。

parentRollover.js(续)

   lis[i].index = i;
   DOMhelp.addEvent( lis[i], 'mouseover', pr.roll, false );
   DOMhelp.addEvent( lis[i], 'mouseout', pr.roll, false );
  }
},

翻转方法始于预定义一个名为 pos 的变量,该变量后来成为显示正确图像所需的偏移值。然后,它调用 getTarget()来确定哪个元素被滚动,并将目标的节点名与 LI 进行比较。这是一种安全措施:尽管您将事件处理程序分配给了 LI,但浏览器实际上可能会将链接作为事件目标发送。对此的一种解释可能是,链接是一种交互式页面元素,而 LI 不是,浏览器的呈现引擎认为链接更重要。您不会知道,但是您应该知道这样一个事实,一些用户代理将链接而不是列表元素视为事件目标。

parentRollover.js(续)

roll : function( e ) {
  var pos;
  var t = DOMhelp.getTarget(e);
  while(t.nodeName.toLowerCase() != 'li' && t.nodeName.toLowerCase() != 'body') {
    t = t.parentNode;
  }

然后定义显示正确背景图像所需的位置。这个位置或者是列表项的索引值,或者是存储的当前属性乘以每个图像的高度。

这两者中的哪一个被应用取决于用户是否将鼠标悬停在列表项上——这可以通过将事件类型与 mouse over 进行比较来找到。相应地设置导航背景位置的样式,然后在页面完成加载后调用 init()方法:

parentRollover.js(节选)

    pos = e.type == 'mouseover' ? t.index : pr.current;
    pos = pos * pr.navHeight;
    pr.nav.style.backgroundPosition = '0 -' + pos + 'px';
  }
}
DOMhelp.addEvent( window, 'load', pr.init, false );

当你在浏览器中打开 exampleParentRollover.html 时,你可以看到滚动导航的不同链接会显示不同的背景图像,如图 6-6 所示。

9781430250920_Fig06-06.jpg

图 6-6 。不同翻转状态下的导航

这是对影响父元素的翻转问题的编程解决方案。但是,它有一个问题:如果菜单项的顺序改变,代码的维护者也必须相应地改变图像。这不是一个非常灵活的解决方案,这就是为什么您最好动态地将类分配给导航列表来定位背景图像。

对脚本的必要更改会影响属性和 roll()方法;初始化保持不变。除了 currentLink 和 navId 属性之外,还需要一个类名来添加到导航列表中。这个新的性质可以称为 dynamicLink。

在 roll()方法中,再次检查触发该方法的事件是否是鼠标悬停,并相应地添加或移除新的动态类。这个动态分配和命名的类由 dynamicLink 属性值和当前索引加一组成(因为对于人类来说,拥有一个名为 item1 class 而不是 item0 的第一个类更容易):

parentCSSrollover.html 使用的 parentCSSrollover.js(略)

pr = {
  navId : 'nav',
  currentLink : 'current',
  dynamicLink : 'item',
  init : function() {
   // [... same as in parentRollover.js ...]
  },
  Roll : function( e ) {
    // [... same as in parentRollover.js ...]
    var action = e.type == 'mouseover' ? 'add' : 'remove';
    DOMhelp.cssjs( action, pr.nav, pr.dynamicLink + ( t.index + 1 ) );
  }
}
DOMhelp.addEvent( window, 'load', pr.init, false );

这样,你就允许 CSS 设计者在 CSS: 中为翻转导航定义不同的状态

parentCSSrollover.html 使用的 parentCSSrollover.css(节选)

#nav.item1{
  background-position:0 0;
}
#nav.item2{
  background-position:0 -50px;
}
#nav.item3{
  background-position:0 -100px;
}
#nav.item4{
  background-position:0 -150px;
}

这也为 CSS 设计者提供了另一个设计导航的钩子:dynamic 类可以用来定义当前的翻转,或者突出显示链接本身的状态,这与不同的项目是不同的。

幻灯片放映

幻灯片是嵌入在页面中的小图像,带有上一页和下一页按钮,有时它们甚至会在一定时间后自动改变图像。它们用于说明文字或提供产品的不同视图。

我们可以区分两种类型的幻灯片放映:在同一文档中包含所有图像的嵌入式幻灯片放映,以及在需要时加载图像的动态幻灯片放映。

嵌入式幻灯片放映

将幻灯片添加到页面的最简单的方法可能是将所有图像作为一个列表添加。然后,您可以使用 JavaScript 通过隐藏和显示带有嵌入图像的不同列表项,将该列表转换成幻灯片。演示文档 examplePhotoListInlineSlideShow.html 正是这样做的,如图 6-7 所示。

9781430250920_Fig06-07.jpg

图 6-7 。带有 JavaScript 的嵌入式幻灯片

底层 HTML 是一个无序列表,所有图像都是列表项。请注意,这也允许您为每个图像设置适当的替代文本:

examplePhotoListInlineSlideShow.html(节选)

<ul class="slides">
  <li>
     <img src="pictures/thumbs/cat2.jpg" alt="Lazy Cat">
  </li>
  <li>
    <img src="pictures/thumbs/dog10.jpg" alt="Dog using the shade">
  </li>
  <li>
    <img src="pictures/thumbs/dog12.jpg" alt="Squinting Dog">
  </li>
  <li>
    <img src="pictures/thumbs/dog63.jpg" alt="Dog cooling off in the sand">
  </li>
  <li>
    <img src="pictures/thumbs/dog7.jpg"  alt="Very flat dog">
  </li>
  <li>
    <img src="pictures/thumbs/donkeyeating.jpg" alt="Hay-eating Donkey">
  </li>
  <li>
    <img src="pictures/thumbs/kittenflat.jpg" alt="Ginger and White Cat">
  </li>
</ul>

所有未来的维护者所要做的就是改变图片的顺序或添加或删除图片来改变 HTML。根本不需要修改 JavaScript。只要你提供一个合适的样式表,没有 JavaScript 的访问者将得到如图图 6-8 所示的所有图像。没有样式表的用户将得到一个带有图像缩略图的列表。

9781430250920_Fig06-08.jpg

图 6-8 。没有 JavaScript 的嵌入式幻灯片放映

在文档中嵌入所有图像的一个效果是,当访问者加载页面时,它们都将被加载。这可能是一件好事,也可能是一件坏事,取决于访问者的连接速度。稍后,我将向您展示一个仅当用户单击较小的图像时才加载较大图像的示例。

让我们看看将这个列表转换成幻灯片的脚本。您将使用在前几章中开发的 DOMhelp 库来解决浏览器问题,并稍微缩短代码。

和往常一样,首先要做的是计划你的剧本。在这种情况下,你应该给 CSS 设计者和 HTML 开发者几个类作为钩子来触发功能或者定义外观和感觉:

  • 一个类,指示列表应该转换为幻灯片放映
  • 定义动态幻灯片列表外观的类
  • 显示先前隐藏的元素的类
  • 定义图像计数器外观的类(例如,图像 1/3)
  • 一个隐藏在特定播放状态下不应该出现的元素的类

您还应该允许维护人员更改前后链接的外观和内容以及图像计数器的文本内容。

至于方法,您所需要的(除了 DOMhelp 中包含的 helper 方法之外)就是一个全局初始化方法、一个初始化每个幻灯片放映的方法和一个放映幻灯片的方法。所有这些共同构成了脚本的框架:

photolistinlineslides . js(skeleton)

inlineSlides = {

  // CSS classes
  slideClass : 'slides',
  dynamicSlideClass : 'dynslides',
  showClass : 'show',
  slideCounterClass : 'slidecounter',
  hideLinkClass : 'hide',

  // Labels
  // Forward and backward links, you can use any HTML here
  forwardsLabel : '<img src="control_fastforward_blue.png" lt="next" >',
  backwardsLabel : '<img src="control_rewind_blue.png"alt="previous" >',
  // Counter text, # will be replaced by the current image count
  // and % by the number of all pictures
  counterLabel : '# of %',

  init : function() {},
  initSlideShow : function( o ) {},
  showSlide : function( e ) {}
}
DOMhelp.addEvent( window, 'load', inlineSlides.init, false );

image 注意在前面的代码中,您在向前和向后链接的标签中提供了一个基于 HTML 的选项。这使得幻灯片的样式更加灵活,因为维护者可以添加自己的 HTML(比如图像)。此外,如果您想让维护人员像更改计数器一样更改动态文本,那么使用#和%这样的占位符并解释它们将被替换是有益的。

让我们一步一步地检查脚本中的方法。首先是全局初始化方法 init():

  1. 测试 DOM 支持。
  2. 如果测试成功,遍历文档的所有 ul 元素。
  3. 对于每个 UL,检查它是否具有将其定义为幻灯片放映的类(存储在 slideClass 属性中),如果没有该类,则跳过该函数执行的其余步骤。(使用“继续”来完成此操作。)
  4. 如果当前的 UL 要变成幻灯片,你用定义动态幻灯片的类替换定义它为幻灯片的类;向列表中添加一个名为 currentSlide 的新属性,并使用列表作为参数调用 initSlideShow 方法。

photoListInlineSlides.js(节选)

init : function() {
  if( !document.getElementById  || !document.createTextNode ) {
    return;
  }
  var uls = document.getElementsByTagName('ul');
  for( var i = 0; i < uls.length; i++ ) {
    if( !DOMhelp.cssjs('check', uls[i], nlineSlides.slideClass ) ) {
     continue;
    }
    DOMhelp.cssjs('swap', uls[i],inlineSlides.slideClass, nlineSlides.dynamicSlideClass );
    uls[i].currentSlide = 0;
    inlineSlides.initSlideShow( uls[i] );
  }
},

使用这些技巧,您可以省去大量的循环和检查工作。首先,仅在 JavaScript 可用时用另一个类替换该类,这允许您隐藏 CSS 中的所有列表项,而不是在 initSlideShow()方法中遍历它们:

photoListInlineSlides.css(节选)

.dynslides li{
  display:none;
  margin:0;
  padding:5px;
}

其他动态分配的 CSS 类是 hide 类(显示第一个图像时删除向后链接,显示最后一个图像时删除向前链接)和 show 类(覆盖使用。动态幻灯片选择器。演示中所有其他的 CSS 选择器和属性都是纯粹装饰性的。

photoListInlineSlides.css(节选)

.dynslides .hide{
  visibility:hidden;
}
.dynslides li.show{
  display:block;
}

通过将当前可见图像存储在列表的属性中,您不需要遍历所有图像并在显示当前图像之前隐藏它们。相反,您需要做的只是确定必须显示哪一个,读取父列表元素的属性,并隐藏存储在该属性中的前一个图像。

然后将属性重置为新图像,下次显示图像时,循环重新开始。您可以将当前图像存储在主 inlineSlides 对象的属性中,但是将它存储在 list 的属性中意味着您允许在同一页面上播放多个幻灯片。

initSlideShow()方法获取每个幻灯片列表作为一个名为 lst 的参数。首先,定义将与 var 关键字一起使用的变量,以确保它们不会覆盖同名的全局变量。然后创建一个新的段落元素来存放前向和后向链接以及图像计数器,并将其直接插入到列表之后(使用 lst.nextSibling):

photoListInlineSlides.js(续)

initSlideShow : function(lst ) {
  var p, temp, count;
  p = document.createElement('p');
  DOMhelp.cssjs('add', p, inlineSlides.slideCounterClass );
  lst.parentNode.insertBefore( p, lst.nextSibling );

接下来,通过 DOMhelp 的 createLink 方法创建向后链接,并使用 innerHTML 添加适当的标签。添加一个事件处理程序来调用 showSlide 方法,通过应用适当的 CSS 类隐藏链接,并将链接添加到新创建的段落中。您将把链接存储在名为 rew 的列表属性中,以便以后更容易找到它:

photoListInlineSlides.js(续)

lst.rew = DOMhelp.createLink('#', ' ' );
lst.rew.innerHTML = inlineSlides.backwardsLabel;
DOMhelp.addEvent(lst.rew, 'click', inlineSlides.showSlide, false );
DOMhelp.cssjs('add', lst.rew,inlineSlides.hideLinkClass );
p.appendChild(lst.rew );

接下来是一个充当图像计数器的新 SPAN 元素。获取主对象的 counterLabel 属性,用当前列表的 currentSlide 属性值替换#字符并加 1(因为人类从 1 开始计数,而不是像计算机那样从 0 开始计数)。将%字符替换为列表中 LI 元素的数量,并将结果字符串作为新的文本节点添加到 SPAN,然后将其作为新的子节点添加到段落:

photoListInlineSlides.js(续)

lst.count = document.createElement('span');
temp = inlineSlides.counterLabel.replace( /#/, lst.currentSlide + 1 );
temp = temp.replace( /%/, lst.getElementsByTagName('li').length );
lst.count.appendChild( document.createTextNode( temp ) );
p.appendChild(lst.count );

image 注意你把计数器的跨度存储在链表的一个属性中,叫做 count。这是纯粹的懒惰,因为它使您不必在以后通过 getElementsByTagName(' span ')[0]来访问它。这也使得脚本不太可能被维护者破坏,维护者可能会在稍后阶段在列表项中添加其他跨度。

添加前向链接与添加后向链接类似,只是 forwardsLabel 属性用作内容,一个名为 fwd 的新属性用作快捷方式。

photoListInlineSlides.js(续)

lst.fwd = DOMhelp.createLink('#', ' ' );
lst.fwd.innerHTML = inlineSlides.forwardsLabel;
DOMhelp.addEvent(lst.fwd, 'click', inlineSlides.showSlide, false );
p.appendChild(lst.fwd );

该方法以获取对应于 currentSlide 属性的列表项并向其中添加 show 类结束。您可以使用 o.firstChild 来代替,但是将来的维护者可能希望最初显示与第一张照片不同的照片:

photoListInlineSlides.js(续)

  temp = lst.getElementsByTagName('li')[lst.currentSlide];
  DOMhelp.cssjs('add', temp, inlineSlides.showClass );
},

showSlide()方法定义了一个名为 action 的变量,并通过 getTarget(e)获取事件目标。因为您不知道维护者是否在链接标签中使用了图像,所以您需要通过测试目标 parentNode 的 nodeName 是否为 a 来找到链接。这也抵消了 Safari 旧版本中的错误,即发送链接中包含的文本作为目标,而不是链接本身。然后,该方法通过读取目标 parentNode 的 closestSibling()来获取触发事件的列表。

photoListInlineSlides.js(续)

showSlide : function( e ) {
  var action;
  var t = DOMhelp.getTarget( e );
  while( t.nodeName.toLowerCase() != 'a' && t.nodeName.toLowerCase() != 'body' ) {
    t=t.parentNode;
  }
  var parentList = DOMhelp.closestSibling( t.parentNode, -1 );

image 注意访问者点击链接的内容可以前进或后退一个图像。事件目标可以是图像(如本例所示)或文本——或者该脚本的维护者放入 forwardsLabel 和 backwardsLabel 属性中的任何东西。因此——因为 Safari 将链接中包含的文本作为目标而不是链接本身发送——您需要检查节点的名称,并将其与 A 进行比较。然后,您获取此 A 的父节点——即新创建的段落——并获取它的前一个兄弟节点,即包含图像的 UL。

接下来,您需要从有问题的列表中找到 currentSlide 属性,并通过检查列表项数组的 length 属性找到图像的总数。通过删除 show 类隐藏以前显示的图像:

photoListInlineSlides.js(续)

var count = parentList.currentSlide;
var photoCount = parentList.getElementsByTagName('li').length - 1;
var photo = parentList.getElementsByTagName('li')[count];
DOMhelp.cssjs('remove', photo, inlineSlides.showClass );

通过比较目标和列表的 fwd 属性来确定被激活的链路是否是前向链路,然后相应地增加或减少计数器。

如果计数器大于 0,则从反向链接中删除隐藏类;否则,添加这个类,有效地隐藏或显示链接。同样的逻辑也适用于前向链接,尽管这次的比较标准是计数器小于列表项的总数。这可以防止在第一张幻灯片上显示向后链接,在最后一张幻灯片上显示向前链接。

photoListInlineSlides.js(续)

count = ( t == parentList.fwd ) ? count+1 : count-1;
action = ( count > 0 ) ? 'remove' : 'add' ;
DOMhelp.cssjs( action, parentList.rew,inlineSlides.hideLinkClass );
action = ( count < photoCount ) ? 'remove' : 'add';
DOMhelp.cssjs( action, parentList.fwd, nlineSlides.hideLinkClass);

它负责链接;现在你需要增加计数器显示。因为计数器存储为列表的一个属性,所以很容易读取该属性的第一个子节点——即 SPAN 中的文本。然后,您可以使用 String 对象的 replace()方法用新的图像编号替换第一个数字条目(这里通过正则表达式),新的图像编号是 count+1——同样,因为人类从 1 开始计数,而不是从 0 开始计数。接下来,重置 currentSlide 属性,获取新照片(记得您更改了 count),并通过添加 show 类显示当前照片。剩下要做的就是在窗口加载后启动 init()方法:

photoListInlineSlides.js(节选)

    photo = parentList.getElementsByTagName('li')[count];
    var counterText = parentList.count.firstChild
    counterText.nodeValue = counterText.nodeValue.replace( /\d/, count + 1 );
    parentList.currentSlide = count;
    photo = parentList.getElementsByTagName('li')[count];
    DOMhelp.cssjs('add', photo, inlineSlides.showClass );
    DOMhelp.cancelClick( e );
  }
}
DOMhelp.addEvent( window, 'load', inlineSlides.init, false );

然而,你还没有完全完成。如果你在旧版本的 Safari 中尝试幻灯片放映,你会意识到向前和向后链接确实被隐藏了,但它们仍然是可点击的,并且当你试图到达不在那里的图像时会导致错误。

image 注意这是动态 web 开发中的一个常见错误——明显隐藏东西并不一定会让它们对所有用户都消失。想想盲人或文本浏览器(如 Lynx)的用户。此外,还要考虑浏览器的漏洞和奇怪之处。

防止这个问题是相当容易的:你需要修改的只是 showSlide()方法,这样当被点击的目标被分配了 hide CSS 类时什么也不做。在解决这个问题时,您还可以添加 Safari 修复来取消新生成的链接的默认操作。演示 examplePhotoListInlineSlideShowSafariFix.html 包含了这些变化:

照片列表中的 slide farifix . js

inlineSlides = {

  // CSS classes
  slideClass : 'slides',
  dynamicSlideClass : 'dynslides',
  showClass : 'show',
  slideCounterClass : 'slidecounter',
  hideLinkClass : 'hide',
  // Labels
  // Forward and backward links, you can use any HTML here
  forwardsLabel : '<img src="control_fastforward_blue.png" alt="next"> ',
  backwardsLabel : '<img src="control_rewind_blue.png" alt="previous">',
  // Counter text, # will be replaced by the current image count
  // and % by the number of all pictures
  counterLabel : '# of %',

  init : function() {
    if( !document.getElementById || !document.createTextNode ) {
      return;
    }
    var uls = document.getElementsByTagName('ul');
    for( var i = 0; i < uls.length; i++ ) {
      if( !DOMhelp.cssjs('check', uls[i],inlineSlides.slideClass ) ) {
       continue;
      }
      DOMhelp.cssjs('swap', uls[i], inlineSlides.slideClass, inlineSlides.dynamicSlideClass );
      uls[i].currentSlide = 0;
      inlineSlides.initSlideShow( uls[i] );
    }
  },
  initSlideShow : function(lst ) {
    var p, temp, count;
    p = document.createElement('p');
    DOMhelp.cssjs('add', p, inlineSlides.slideCounterClass );
    lst.parentNode.insertBefore( p, lst.nextSibling );
    lst.rew = DOMhelp.createLink('#', ' ' );
    lst.rew.innerHTML = inlineSlides.backwardsLabel;
    DOMhelp.addEvent(lst.rew, 'click', inlineSlides.showSlide, false );
    DOMhelp.cssjs('add', lst.rew, inlineSlides.hideLinkClass );
    p.appendChild(lst.rew );
    lst.count = document.createElement('span');
    temp = inlineSlides.counterLabel._
    replace( /#/, lst.currentSlide + 1 );
    temp = temp.replace( /%/, o.getElementsByTagName('li').length );
    lst.count.appendChild( document.createTextNode( temp ) );
    p.appendChild(lst.count );
    lst.fwd=DOMhelp.createLink('#', ' ' );
    lst.fwd.innerHTML = inlineSlides.forwardsLabel;
    DOMhelp.addEvent(lst.fwd, 'click', inlineSlides.showSlide, false );
    p.appendChild(lst.fwd );
    temp = lst.getElementsByTagName('li')[ lst.currentSlide];
    DOMhelp.cssjs('add', temp,inlineSlides.showClass );
    lst.fwd.onclick = DOMhelp.safariClickFix;
    lst.rew.onclick = DOMhelp.safariClickFix;
  },
  showSlide : function( e ) {
    var action;
    var t = DOMhelp.getTarget( e );
    while( t.nodeName.toLowerCase() != 'a && t.nodeName.toLowerCase() != 'body' ) {
      t = t.parentNode;
    }
    if( DOMhelp.cssjs('check', t,_inlineSlides.hideLinkClass ) ){
     return;
    }
    var parentList = DOMhelp.closestSibling( t.parentNode, -1 );
    var count = parentList.currentSlide;
    var photoCount = parentList.getElementsByTagName('li').length-1;
    var photo = parentList.getElementsByTagName('li' )[count];
    DOMhelp.cssjs('remove', photo, inlineSlides.showClass );
    count = ( t == parentList.fwd ) ? count + 1 : count - 1;
    action = ( count > 0 ) ? 'remove' : 'add' ;
    DOMhelp.cssjs( action, parentList.rew, inlineSlides.hideLinkClass );
    action = ( count < photoCount ) ? 'remove' : 'add';
    DOMhelp.cssjs( action, parentList.fwd,inlineSlides.hideLinkClass );
    photo = parentList.getElementsByTagName('li')[count];
    var counterText = parentList.count.firstChild
    counterText.nodeValue = counterText.nodeValue.replace( /\d/, count + 1 );
    parentList.currentSlide = count;
    DOMhelp.cssjs('add', photo, inlineSlides.showClass );
    DOMhelp.cancelClick( e );
  }
}
DOMhelp.addEvent( window, 'load', inlineSlides.init, false );

将嵌入的图像列表转换成幻灯片是一种效果,在非 JavaScript 用户代理上效果很好,尽管它不是真正的图像操作,甚至不是动态的。JavaScript 的真正强大之处在于避免页面重载,并在同一文档中显示更大的图像,而不仅仅是在浏览器中显示。让我们来看一些例子。

动态幻灯片放映

让我们再看一个 HTML 列表,把它变成一个动态幻灯片的例子。从 HTML 开始—这次是包含链接到大图的缩略图的列表:

exampleMiniSlides.html

<ul class="minislides">
<li>
  <a href="pictures/thumbs/cat2.jpg">
     <img src="pictures/minithumbs/cat2.jpg" alt="Lazy Cat">
  </a>
</li>
<li>
  <a href="pictures/thumbs/dog63.jpg">
     <img src="pictures/minithumbs/dog63.jpg" alt="Dog cooling off in the sand"></a>
  </li>
  <li>
    <a href="pictures/thumbs/dog7.jpg">
      <img src="pictures/minithumbs/dog7.jpg"  alt="Very flat dog">
    </a>
  </li>
  <li>
    <a href="pictures/thumbs/kittenflat.jpg">
      <img src="pictures/minithumbs/kittenflat.jpg" alt="Ginger and White Cat">
    </a>
  </li>
</ul>

如果您在启用了 JavaScript 的浏览器中打开示例,您会得到一个小缩略图列表和一个大图像。点击缩略图会用缩略图指向的图像替换大图,如图图 6-9 所示。

9781430250920_Fig06-09.jpg

图 6-9 。带有小预览图像(缩略图)的幻灯片放映

没有 JavaScript 的访问者只会得到一行链接到更大图片的图片,如图 6-10 所示。

9781430250920_Fig06-10.jpg

图 6-10 。没有 JavaScript 的小预览图片幻灯片

同样,让我们来规划脚本的框架:定义一个类来识别哪些列表将变成幻灯片,定义一个类来赋予包含大照片的列表项,定义替换文本来添加到大照片中。

方法和上次一样:一个全局初始化方法,一个初始化每个幻灯片放映的方法,一个显示当前照片的方法。

miniSlides.js (skeleton)

minislides = {
  // CSS classes
  triggerClass : 'minislides',
  largeImgClass : 'photo',
  // Text added to the title attribute of the big picture
  alternativeText : 'large view',

  init : function(){  },
  initShow : function( o ){ },
  showPic : function( e ){  }
}
DOMhelp.addEvent( window, 'load', minislides.init, false );

幻灯片的 CSS 非常简单:

miniSlides.css(节选)

.minislides, .minislides * {
  margin:0;
  padding:0;
  list-style:none;
  border:none;
}
.minislides{
  clear:both;
  margin:10px 0;
  background:#333;
}
.minislides,.minislides li{
  float:left;
}
.minislides li img{
  display:block;
}
.minislides li{
  padding:1px;
}
.minislides li.photo{
  clear:both;
  padding-top:0;
}

首先,使用正确的类和列表本身对列表中的任何内容进行全局重置。全局重置意味着将所有边距和填充设置为 0,并将边框和列表样式设置为无。这避免了必须处理跨浏览器差异,也使 CSS 文档更短,因为您不需要为每个元素重置这些值。

然后将列表和所有列表项浮动到左侧,使它们显示为内联,而不是一个在另一个下面。您需要浮动主列表以确保它包含其他列表。

将图像设置为显示为块元素,以避免它们周围的间隙,并在每个列表项上添加一个像素的填充,以显示背景色。

“照片”列表项需要一个浮动清除来显示在其他项的下面。将其顶部填充设置为 0,以避免较小图像和较大图像之间出现双线。

init()方法的功能类似于上一张幻灯片中的方法。测试 DOM 支持,遍历文档中的所有列表,跳过那些没有正确类的列表。拥有正确类的类作为参数发送给 initShow()方法:

miniSlides.js(节选)

init : function() {
  if( !document.getElementById || !document.createTextNode ) {
    return;
  }
  var lists = document.getElementsByTagName('ul');
  for( var i = 0; i < lists.length; i++ ) {
    if( !DOMhelp.cssjs('check', lists[i],minislides.triggerClass ) ) {
     continue;
    }
    minislides.initShow( lists[i] );
  }
},

initShow()方法通过创建一个新的列表项、创建一个新的图像并将大图像类分配给新的列表项来启动。它将图像作为列表项的子项添加,并将新列表项作为主列表的子项添加。与前面定义的 CSS 一起,它在其他图像下面显示新图像:

miniSlides.js(节选)

initShow : function( o ) {
  var newli = document.createElement('li');
  var newimg = document.createElement('img');
  newli.appendChild( newimg );
  DOMhelp.cssjs('add', newli, minislides.largeImgClass );
  o.appendChild( newli );

然后获取列表中的第一个图像,并读取存储在 alt 属性中的替代文本。将此文本作为替换文本添加到新图像中,将存储在 alternative text 属性中的文本添加到新图像中,并将结果字符串存储为新图像的 title 属性:

miniSlides.js(节选)

var firstPic = o.getElementsByTagName('img')[0];
var alt = firstPic.getAttribute('alt');
newimg.setAttribute('alt', alt );
newimg.setAttribute('title', alt + minislides.alternativeText );

接下来,检索列表中的所有链接,并在用户单击每个链接时应用一个指向 showPic 的事件。将新图像作为名为 photo 的属性存储在 list 对象中,并将新创建的图像的 src 属性设置为第一个链接的目标位置:

miniSlides.js(节选)

  var links = o.getElementsByTagName('a');
  for(i = 0; i < links.length; i++){
   DOMhelp.addEvent( links[i], 'click', minislides.showPic,false );

  }
  o.photo = newimg;
  newimg.setAttribute('src', o.getElementsByTagName('a')[0].href );
},

当链接被点击时,显示链接指向的图片是小菜一碟。在 showPic()方法中,通过 getTarget()检索事件目标,并通过读取列表的 photo 属性获取旧图片。

这一次,您知道访问者将点击的元素是一个图像,这就是为什么您不需要循环和测试元素的名称。相反,您可以转到三个父节点(A、LI 和)并读取先前存储的图像。然后设置可选文本、标题和图像 src,并通过 cancelClick()停止链接的默认行为。添加一个处理程序,在窗口完成加载时触发 init()方法,以完成迷你幻灯片放映。

miniSlides.js(节选)

  showPic : function( e ) {
    var t = DOMhelp.getTarget( e );
    var oldimg = t.parentNode.parentNode.parentNode.photo;
    oldimg.setAttribute('alt', t.getAttribute('alt') );
    oldimg.setAttribute('title', t.getAttribute('alt') +minislides.alternativeText );
    oldimg.setAttribute('src', t.parentNode.getAttribute('href') );
    DOMhelp.cancelClick( e );
  }
}
DOMhelp.addEvent( window, 'load', minislides.init, false );

图像和 JavaScript 概述

对图像和 JavaScript 的介绍到此结束。我希望你不要被可能发生的事情和以这种方式写剧本所需要的东西弄得不知所措。我还希望我已经激发了您使用这些示例的兴趣,看看还有什么是可能的。请记住这些幻灯片以及它们是如何工作的——在本章的最后,我们将回到文档嵌入幻灯片,并让它自动播放。在第十章中,你将看到如何开发一个更大的支持 JavaScript 的图库。

现在,有一些关于图像和 JavaScript 的事情需要记住:

  • 您可以对图像对象进行大量预加载,但这并不是一种万无一失的方法。浏览器缓存设置、损坏的图像链接和代理服务器可能会干扰您的预加载脚本。有一些库可以帮助预加载过程。例如,PreloadJS 可以帮助您预加载图像、声音和其他 JavaScript 文件。如果这些脚本失败,用户可能会卡住或面对不断变化的进度条,这是令人沮丧的。
  • 虽然您可以直接访问每个图像的属性,但这可能不是最好的方法。把视觉效果留给 CSS,你的脚本不会被其他不知道发生了什么的人修改。
  • 自 DHTML 时代以来,CSS 已经走过了漫长的道路,现在,通过提供挂钩来帮助 CSS 设计者可能是一种更好的方法,而不是纯粹通过脚本来实现视觉效果——前面看到的父翻转就是一个例子。

Windows 和 JavaScript

JavaScript 的常见用法是生成新的浏览器窗口和改变当前窗口。这些也是令人讨厌和不安全的做法,因为你永远无法确定你的网页的访问者是否能够处理调整大小的窗口,或者当有新窗口时,她的用户代理是否会通知你。想象一下屏幕阅读器用户在收听你的站点或者文本浏览器用户。

过去,窗口曾被用于未经请求的广告(弹出窗口)和在隐藏窗口中执行代码以进行数据检索(网络钓鱼),这就是为什么浏览器供应商和第三方软件提供商想出了许多软件和浏览器设置来阻止这种滥用。例如,Mozilla Firefox 用户可以选择他们是否想要弹出窗口,以及 JavaScript 可以改变窗口的哪些属性,如图图 6-11 所示。

9781430250920_Fig06-11.jpg

图 6-11 。Mozilla Firefox 中的高级 JavaScript 设置

其他浏览器如 Microsoft Internet Explorer 7 或 Opera 8 不允许在新窗口中隐藏地址栏,并且可以对新窗口施加大小和位置限制。

image 注意这是不同浏览器厂商想要阻止安全漏洞的一个契合点。打开一个没有可见地址栏的新窗口可以让恶意攻击者通过跨站点脚本 (XSS)在第三方站点上打开一个弹出窗口,让它看起来像是属于该站点的,并要求用户输入信息。更多关于 XSS 的信息可以在维基百科上找到:en.wikipedia.org/wiki/Cross-site_scripting

这对于网络冲浪者来说都是好消息,因为他们可以阻止不想要的广告,并且不太可能被愚弄而将他们的数据交给错误的人。这对你来说不是什么好消息,因为这意味着当你想在 JavaScript 中广泛使用 windows 时,你必须测试很多不同的场景。

抛开这些考虑,你仍然可以用 windows 和 JavaScript 做很多事情。每一个支持 JavaScript 的浏览器都为您提供了一个名为 window 的对象,其属性将在下一节中列出,还有一些关于如何使用它们的示例。

窗口属性

以下是窗口对象的属性列表:

image 注意这个部分列表没有显示所有可用的窗口属性。如果某个浏览器不支持某个给定的属性,我会在讨论该属性的地方注明。

  • closed: Boolean,表示窗口是否关闭(只读)
  • defaultStatus:状态栏中的默认状态消息(Safari 不支持)
  • innerHeight:窗口文档部分的高度
  • innerWidth:窗口文档部分的宽度
  • outerHeight:整个窗口的高度
  • 外部宽度:整个窗口的宽度
  • pageXOffset:文档在窗口内的当前水平起始位置(只读)
  • pageYOffset:文档在窗口内的当前垂直起始位置(只读)
  • status:状态栏的文本内容
  • 名称:窗口的名称
  • toolbar:当窗口有工具栏时,返回 visible 属性为 true 的对象的属性(只读)

例如,如果您想要获取窗口的内部大小,您可以使用以下一些属性:

exampleWindowProperties.html(节选)

function winProps() {
  var winWidth = window.outerWidth;
  var winHeight = window.outerHeight;
  var docWidth = window.innerWidth;
  var docHeight = window.innerHeight;
  var message = 'This window is ';
  message += winWidth + ' pixels wide and ';
  message += winHeight + ' pixels high.\n';
  message += 'The inner dimensions are: ';
  message += docWidth + ' * ' + docHeight + ' pixels';
  alert( message );
}

该功能的一些可能输出如图 6-12 中的所示。

9781430250920_Fig06-12.jpg

图 6-12 。读取窗口属性

Internet Explorer 不支持其他属性。这些是滚动条、定位栏、状态栏、菜单栏和个人栏。它们中的每一个都存储了一个具有值为 true 或 false 的只读可见属性的对象。若要测试用户是否打开了菜单栏,请检查对象和属性:

if ( window.menubar && window.menubar.visible == true )
{
 // Code
}

窗口方法

窗口对象也有许多方法,其中一些在前面的章节中已经讨论过了。除了那些提供用户反馈的功能,最常见的功能是打开新窗口和定时执行功能。

用户反馈方式

此处列出的用户反馈方法在第四章中有详细介绍:

  • alert('message '):显示警报
  • 确认('消息'):显示对话框以确认操作
  • 提示('消息','预设'):显示对话框以输入值

打开新窗口

窗户的开关在技术上相当容易。然而,随着浏览器抑制不同的属性和方法,或者编写糟糕的弹出窗口阻止软件甚至阻止“好的”弹出窗口,打开和关闭窗口可能成为一场噩梦,需要适当的测试。打开新窗口的细节非常简单。您有四种方法可供选择:

  • open('url ',' name ',' properties '):打开一个名为“name”的新窗口,在其中加载 url,并设置窗口属性
  • close():关闭窗口(如果窗口不是弹出窗口,这将导致安全警告。)
  • blur():将浏览器的焦点从窗口移开
  • focus():将浏览器的焦点移到窗口

open 方法的属性字符串有一个非常独特的语法:它将窗口的所有属性以字符串的形式列出,每个字符串由一个名称和一个用等号连接的值组成,用逗号分隔:

myWindow = window.open('demo.html', 'my', 'p1=v1,p2=v2,p3=v3' );

并非所有浏览器都支持此处列出的所有属性。例如,移动浏览器可能不具备桌面浏览器的所有功能。但是这个列表会让你知道什么是可用的:

  • height:以像素为单位定义窗口的高度。
  • width:以像素为单位定义窗口的宽度。
  • left:以像素为单位定义窗口在屏幕上的水平位置。
  • top:以像素为单位定义窗口在屏幕上的垂直位置。
  • 位置:定义窗口是否有地址栏(是或否)。出于安全考虑。该选项始终为“是”,并且不可编辑。
  • menubar:定义窗口是否有菜单栏(是或否)。这不适用于 MacOS 上的浏览器,因为菜单总是在屏幕的顶部。
  • resizable:定义当窗口太小或太大时,是否允许用户调整窗口的大小。浏览器已经覆盖了此设置。用户必须更改他们的设置才能使用它。
  • scrollbars:定义窗口是否有滚动条(是或否)。Opera 不允许滚动条被抑制。
  • status:定义窗口是否有状态栏(是或否)。Opera 不允许关闭状态栏。
  • 工具栏:定义窗口是否有工具栏(是或否)。Opera 不支持关闭工具栏。

要打开一个宽 200 像素、高 200 像素、距屏幕左上角 100 像素的窗口,然后将文档 grid.html 加载到其中,您必须设置适当的属性,如下所示:

var windowprops = "width=200,height=200,top=100,left=100";

您可以尝试在页面加载时打开窗口:

examplewindowpopup . html(excerpt)

function popup() {
  var windowprops = "width=200,height=200,top=100,left=100";
  var myWin =  window.open("grid.html", "mynewwin" ,windowprops );
}
window.onload = popup;

请注意,不同浏览器的结果略有不同。Internet Explorer 9 显示没有任何工具栏的窗口;Firefox、Opera 和 Chrome 警告你该页面正试图打开一个新窗口,并询问你是否允许它这样做。Safari 不做任何事情。

当窗口通过链接打开时,处理方式略有不同:

示例链接 WindowPopUp.html

<a href="#" onclick="popup();return false">
  Open grid
</a>

Opera,Firefox,Chrome,Safari 现在都不抱怨弹出窗口了。但是,如果 JavaScript 被禁用,就不会有新窗口,链接也不会做任何事情。因此,您可能希望链接到 href 中的文档,并将 URL 作为参数发送:

exampleParameterLinkWindowPopUp.html(节选)

function popup( url ) {
  var windowprops = "width=200,height=200,top=100,left=100";
  var myWin =  window.open( url, "mynewwin", windowprops );
}

<a href="grid.html" onclick="popup(this.href);return false">Open grid</a>

请注意 window.open()方法的 name 参数。这对于 JavaScript 来说似乎毫无意义,因为它什么也不做。(例如,没有可用于通过 windows.mynewwin 访问窗口的 windows 集合。)但是,它在 HTML target 属性中使用,使链接在弹出窗口而不是主窗口中打开其链接的文档。

在本例中,您将窗口的名称定义为 mynewwin,并将一个链接作为目标,在那里打开www.yahoo.co.uk/:

exampleParameterLinkWindowPopUp.html(节选)

function popup( url ) {
  var windowprops = "width=200,height=200,top=100,left=100";
  var myWin = window.open( url, "mynewwin", windowprops );
}
<a href="http://www.yahoo.co.uk/" target="mynewwin">Open Yahoo</a>

除非您使用非转换的 XHTML 或 strict HTML(不推荐使用 target ),否则您还可以使用值为 _blank 的 target 属性来打开一个新窗口,而不管 JavaScript 是否可用。因此,告诉访问者链接将在链接内的新窗口中打开,以避免混淆或可访问性问题,这是一个很好的做法:

<a href="grid.html" onclick="popup(this.href);return false" target="blank">
Open grid  (opens in a new window)
</a>

然而,因为您可能希望使用 HTML 对使用 JavaScript 的弹出窗口进行更多的控制,所以不依赖于 target 可能是一个更好的解决方案,相反,只有在脚本可用的情况下才把链接变成弹出链接。为此,您需要一些东西来挂钩,并将链接标识为弹出链接。例如,您可以使用名为 popup 的类:

exampleAutomaticPopupLinks.html(节选)

<p><a href="grid.html" class="popup">Open grid</a></p>
<p><a href="http://www.yahoo.co.uk/" class="popup">Open Yahoo</a></p>

计划剧本并不需要太多。您需要该类来触发弹出窗口、文本加载项和作为属性的窗口参数,您需要一个 init()方法来标识链接并添加更改,您需要一个 openPopup()方法来触发弹出窗口:

自动弹出链接. js(骨架)

poplinks = {
  triggerClass : 'popup',
  popupLabel : ' (opens in a new window)',
  windowProps : 'width=200,height=200,top=100,left=100',
  init : function(){ },
  openPopup : function( e ){ },
}

这两种方法非常基本。init()方法检查 DOM 支持,并遍历文档中的所有链接。如果当前链接有 CSS 触发器类,它通过从标签创建新的文本节点并将其作为新的子节点添加到链接来将标签添加到链接。当单击指向 openPopup()方法的链接时,它会添加一个事件,并应用旧版本 Safari 的修复程序来阻止该链接在该浏览器中被跟踪:

automaticPopupLinks.js(节选)

init : function() {
  if( !document.getElementById || !document.createTextNode ) {
   return;
  }
  var label;
  var allLinks = document.getElementsByTagName('a');
  for( var i = 0; i < allLinks.length; i++ ) {
    if( !DOMhelp.cssjs('check', allLinks[i],poplinks.triggerClass ) ) {
      continue;
    }
    label = document.createTextNode( poplinks.popupLabel );
    allLinks[i].appendChild( label );
    DOMhelp.addEvent( allLinks[i], 'click',poplinks.openPopup, false );
    allLinks[i].onclick = DOMhelp.safariClickFix;
  }
},

openPopup()方法检索事件目标,确保它是一个链接,并通过调用 window.open()打开一个新窗口,使用事件目标的 href 属性作为 URL、一个空名称和存储在 windowProps 中的窗口属性。它通过调用 cancelClick()方法停止链接被跟踪来结束:

automaticPopupLinks.js(节选)

openPopup : function( e ) {
  var t = DOMhelp.getTarget( e );
  if( t.nodeName.toLowerCase() != 'a' ) {
    t = t.parentNode;
  }
  var win = window.open( t.getAttribute('href'), '', poplinks.windowProps );
  DOMhelp.cancelClick( e );
}

这个话题还有更多的内容,特别是当涉及到可用性和可访问性问题时,要确保弹出窗口只有在真正打开时才使用,并且不会被某些软件阻止或无法打开。然而,深入这个问题不在当前讨论的范围之内。可以说,在今天的环境中,依靠任何类型的弹出窗口都不容易。

窗口交互

窗口可以使用它们的许多属性和方法与其他窗口进行交互。首先有 focus()和 blur():前者将弹出窗口带到前面;后者将其推到当前窗口的后面。

可以使用 close()方法摆脱窗口,通过 window.opener 属性可以到达打开弹出窗口的窗口。假设您从一个主文档中打开了两个新窗口:

w1 = window.open('document1.html', 'win1');
w2 = window.open('document2.html', 'win2');

通过调用第一个窗口的 blur()方法,可以将第一个窗口隐藏在另一个窗口的后面:

w1.blur();

image 注意你再也看不到它了,但是通过 blur()打开一个不请自来的窗口并立即隐藏它的技巧被称为弹出窗口。这一招被广告商认为没有弹窗那么讨厌,因为它们不覆盖当前页面。如果你曾经发现当你关闭浏览器时,你不记得打开了几个窗口,这可能就是发生过的事情。

您可以通过调用其 close()方法来关闭窗口:

w1.close();

如果您想从弹出窗口中的任何文档进入初始窗口,您可以通过

var parentWin = window.opener;

如果你想从第一个窗口到达第二个窗口,你还需要通过 window.opener,因为第二个窗口是从这个窗口打开的:

var parentWin = window.opener;
var otherWin = parentWin.w2;

请注意,您需要使用分配给窗口的变量名,而不是窗口名。

你可以以这种方式使用任何窗口的任何窗口方法。比方说,你想在 document1.html 关闭 w2;您可以通过调用

var parentWin = window.opener;
var otherWin = parentWin.w2.close();

您也可以调用主窗口的功能。如果主窗口有一个名为 demo()的 JavaScript 函数,您可以从 document1.html 通过

var parentWin = window.opener;
parentWin.demo();

image 警告如果你试图用 window.opener.close()关闭初始窗口,一些浏览器会发出安全警告,询问用户是否允许这样做。这是另一个安全功能,可以防止怀有恶意的网站所有者欺骗不同的网站。许多设计机构过去常常关闭原来的浏览器,转而使用预定义大小的窗口——如果没有前面提到的安全警告,这就不再可能了。这可能是一个好主意,以避免这种行为,除非你想吓唬或骚扰你的访客。

改变窗口和的位置和尺寸

下面列表中的每个方法都有 x 和 y 参数:x 是水平位置,y 是距屏幕左上角以像素为单位的垂直位置。moveBy()、resizeBy()和 scrollBy()方法允许负值,这将使窗口或内容向左上方移动,或者使窗口变小指定的像素数:

  • moveBy(x,y)-将窗口移动 x 和 y 像素
  • moveTo(x,y)-将窗口移动到坐标 x 和 y 处
  • resizeBy(x,y)-按 x 和 y 调整窗口大小
  • resizeTo(x,y)-将窗口大小调整为 x 和 y
  • scrollBy(x,y)-按 x 和 y 方向滚动窗口内容
  • scrollTo(x,y)-将窗口内容滚动到 x 和 y 方向

如果您检查示例文档 exampleWindowPosition.html,您可以测试不同的方法,如图图 6-13 所示。注意,这个例子出现在 Firefox 中。在 Opera 8 或 IE 7 中,小窗口会有一个定位栏。

9781430250920_Fig06-13.jpg

图 6-13 。更改窗口位置和尺寸

带窗口间隔和超时的动画

您可以使用 setInterval()和 setTimeout()窗口方法来允许定时执行代码。setTimeout 表示在执行代码之前等待一定的时间(仅一次);setInterval()在每次经过给定的时间段时执行代码。

  • name = setInterval('someCode ',x):每隔 x 毫秒执行作为 someCode 传递给它的 JavaScript 代码
  • clearInterval( name):取消执行名为 name 的间隔(防止代码再次被执行)
  • name=setTimeout('someCode ',x):等待 x 毫秒后执行一次 JavaScript 代码 someCode
  • clearTimeout( name):如果代码尚未运行,则停止名为 name 的超时

image 注意setInterval()和 setTimeout()中的参数 someCode 是你定义的函数。

使用这些方法的经典例子是新闻收报机、时钟和动画。然而,你也可以用它们来使你的网站不那么突兀,更有益于用户。一个例子是警告消息,它在一定时间后消失。演示 exampleTimeout.html 展示了如何使用 setTimeout()在短时间内显示一个非常明显的警告消息,或者允许用户立即删除它。HTML 有一个段落警告用户文档已经过期:

exampleTimeout.html(节选)

<p id="warning">This document is outdated
 and kept only for archive purposes.</p>

一个基本的样式表将这个警告涂成红色,并为非 JavaScript 用户以粗体显示。对于启用了 JavaScript 的用户,添加一个动态类使警告更加明显。

超时. css

#warning{
  font-weight:bold;
  color:#c00;
}
.warning{
  width:300px;
  padding:2em;
  background:#fcc;
  border:1px solid #c00;
  font-size:2em;
}

两者的区别如图图 6-14 所示。

9781430250920_Fig06-14.jpg

图 6-14 。不含和含 JavaScript 的警告消息

用户可以点击“移除警告”链接来消除警告或等待——它会在 10 秒钟后自动消失。

剧本很简单。您检查 DOM 是否受支持,以及具有正确 ID 的警告消息是否存在。然后将动态警告类添加到消息中,并使用指向 removeWarning()方法的事件处理程序创建一个新链接。您将此链接作为一个新的子节点附加到警告消息中,并定义一个超时,当超过 10 秒时自动触发 removeWarning():

timeout.js(节选)

warn = {
  init : function() {
    if( !document.getElementById || !document.createTextNode ) {
      return;
    }
    warn.w = document.getElementById('warning');
    if( !warn.w ){ return; }
    DOMhelp.cssjs('add', warn.w, 'warning');
    var temp = DOMhelp.createLink('#', 'remove warning');
    DOMhelp.addEvent( temp, 'click', warn.removeWarning, false );
    temp.onclick = DOMhelp.safariClickFix;
    warn.w.appendChild( temp );
    warn.timer = window.setTimeout('warn.removeWarning()', 10000 );
  },

removeWarning()方法需要做的只是从文档中删除警告消息,清除超时,并停止链接的默认操作。

timeout.js(续)

  removeWarning : function( e ){
    warn.w.parentNode.removeChild( warn.w );
    window.clearTimeout( warn.timer );
    DOMhelp.cancelClick( e );
  }
}
DOMhelp.addEvent( window, 'load', warn.init, false )

开创这种效果的一个 web 应用是 Basecamp(可在www.basecamp.com/获得),它在页面加载时用黄色突出显示最近对文档的更改,并逐渐淡出高亮。你可以在 37 个信号(【http://www.37signals.com/svn/archives/000558.php】??)上看到效果原理。

在网站上使用超时很有诱惑力,因为它们给人一种非常动态的网站的印象,并允许你从一种状态平稳地过渡到另一种状态。

image 提示有几个 JavaScript 效果库可以为你提供预制脚本来实现过渡和动画效果。虽然大部分都很过时,但是也有一些例外,像 script . aculo . us(script.aculo.us/),tween js(www.createjs.com/#!/TweenJS)和 tween lite(www.greensock.com/)。

但是,您可能需要重新考虑在您的网站中使用大量动画和过渡。请记住,代码是在用户的计算机上执行的,根据时间的长短或其他任务的繁忙程度,过渡和动画可能看起来非常笨拙,并成为一种麻烦,而不是更丰富的网站体验。

如果网站的功能依赖于动画,动画也可能是一个可访问性问题,因为它们可能会使一些残疾访问者群体(如认知障碍或癫痫患者)无法使用网站。美国第 508 条可访问性法律(你可以在 http://www.section508.gov/的上读到)非常明确地指出,对于软件开发,你需要提供一个关闭动画的选项:

(h)当显示动画时,应根据用户的选择,以至少一种非动画演示模式显示信息。

www.section508.gov/index.cfm?fuseAction = stdsdoc #软件

然而,对于网站来说,这一点并不清楚。另一方面,万维网联盟(W3C)可访问性指南在二级优先级中明确指出,您应该避免网页中的任何移动:

在用户代理允许用户冻结移动内容之前,避免页面移动。

www.w3.org/TR/WCAG10-TECHS/#tech-avoid-movement

让我们尝试一个允许用户开始和停止动画的例子。以本章前面开发的嵌入式幻灯片放映为例,您将使用 setInterval()添加一个开始和停止自动幻灯片放映的链接,而不是提供向前和向后链接。

HTML 和 CSS 将保持不变,但是 JavaScript 必须有很大的改变。

如果你在浏览器中打开 exampleAutoSlideShow.html,你会看到一个带有播放按钮的幻灯片,当你点击它的时候就开始播放。您可以在页面加载时轻松启动动画,但是最好让用户自己选择。当您需要遵守辅助功能指南时尤其如此,因为未经请求的动画可能会给患有癫痫等残疾的用户带来问题。单击后,该按钮会变成停止按钮,激活时会停止幻灯片放映。你可以在图 6-15 中看到它在 Firefox 中的样子。

9781430250920_Fig06-15.jpg

图 6-15 。将播放按钮改为停止按钮的自动幻灯片放映

从必要的 CSS 类开始,除了 hide 类之外,这些类与第一个幻灯片示例中的相同。因为你这次不会隐藏任何按钮,所以没有必要。

autolideslowsh . js(except)的缩写形式

autoSlides = {

  // CSS classes
  slideClass : 'slides',
  dynamicSlideClass : 'dynslides',
  showClass : 'show',
  slideCounterClass : 'slidecounter',

其他属性略有变化。你现在需要播放和停止标签,而不是向后和向前标签。指示图片总数和系列中当前正在显示的图片的计数器保持不变。一个新特性是幻灯片放映的延迟,以毫秒为单位:

autoSlides.js(续)

// Labels
// Play and stop links, you can use any HTML here
playLabel : '<img src="control_play_blue.png" alt="play">',
stopLabel : '<img src="control_stop_blue.png" alt="stop">',
// Counter text, # will be replaced by the current image count
// and % by the number of all pictures
counterLabel : '# of %',

// Animation delay in milliseconds
delay : 1000,

init()方法检查是否支持 DOM,并添加一个名为 slideLists 的新数组,该数组将存储所有要转换成幻灯片的列表。这对于告诉该函数将更改应用到哪个列表是必要的:

autoSlides.js(续)

init : function() {
  if( !document.getElementById || !document.createTextNode ) {
    return;
  }
  var uls = document.getElementsByTagName('ul');
  autoSlides.slideLists = new Array();

首先,遍历文档中的所有列表,并检查该类将它们转换为幻灯片。如果列表具有类,则将 currentSlide 属性初始化为 0,并将循环计数器存储在名为 showCounter 的新列表属性中。同样,这将需要告诉时间间隔要更改哪个列表。您使用 list 作为参数调用 initSlideShow()方法,并将列表添加到 slideLists 数组中:

autoSlides.js(续)

  for( var i = 0; i < uls.length; i++ ) {
    if( !DOMhelp.cssjs('check', uls[i],autoSlides.slideClass ) ){
      continue;
    }
    DOMhelp.cssjs('swap', uls[i], autoSlides.slideClass,_autoSlides.dynamicSlideClass );
    uls[i].currentSlide = 0;
    uls[i]. showIndex = i;
    autoSlides.initSlideShow( uls[i] );
    autoSlides.slideLists.push( uls[i] );
  }
},

initSlideShow()方法与您在 photolistinlineslidessafarifix . js 中使用的同名方法没有太大区别。唯一的区别是您创建了一个链接而不是两个,并将 playLabel 应用为新链接的内容:

autoSlides.js(续)

initSlideShow : function( o ){
  var p, temp ;
  p = document.createElement('p');
  DOMhelp.cssjs('add', p, autoSlides.slideCounterClass );
  o.parentNode.insertBefore( p, o.nextSibling );
  o.play = DOMhelp.createLink('#', ' ' );
  o.play.innerHTML = autoSlides.playLabel;
  DOMhelp.addEvent( o.play, 'click', autoSlides.playSlide, false );
  o.count = document.createElement('span');
  temp = autoSlides.counterLabel.replace( /#/, o.currentSlide + 1 );
  temp = temp.replace( /%/, o.getElementsByTagName('li').length );
  o.count.appendChild( document.createTextNode( temp ) );
  p.appendChild( o.count );
  p.appendChild( o.play );
  temp = o.getElementsByTagName('li')[o.currentSlide];
  DOMhelp.cssjs('add', temp,autoSlides.showClass );
  o.play.onclick = DOMhelp.safariClickFix;
},

playSlide()方法是新的,但是它的开始非常像旧的 showSlide()方法。您检查目标及其节点名称,并检索父列表:

autoSlides.js(续)

playSlide : function( e ) {
  var t = DOMhelp.getTarget( e );
  while( t.nodeName.toLowerCase() != 'a' && t.nodeName.toLowerCase() != 'body' ){
    t = t.parentNode;
  }
  var parentList = DOMhelp.closestSibling( t.parentNode, -1 );

测试父列表是否已经有一个名为 loop 的属性。这是存储 setInterval()实例的属性。您使用列表的属性而不是变量来允许在同一文档中进行多个自动幻灯片放映。

将 setInterval()中使用的字符串定义为 showSlide()方法的调用,并将父列表的 showIndex 属性作为参数。这是必要的,因为 setInterval()是 window 对象的一个方法,不在主 autoSlides 对象的范围内。

使用 setInterval()和 autoSlides.delay 属性中定义的延迟,并将它存储在 loop 属性中,然后将激活的链接内容更改为 Stop 按钮:

autoSlides.js(续)

if( !parentList.loop ) {
  var loopCall = "autoSlides.showSlide('" + parentList.showIndex + " ' )";
  parentList.loop = window.setInterval( loopCall, utoSlides.delay );
  t.innerHTML = autoSlides.stopLabel;

如果列表已经有一个名为 loop 的属性,则幻灯片放映当前正在运行;因此,您清除它,将 loop 属性设置为 null,并将按钮改回 Play 按钮。然后通过调用 cancelClick()来停止默认链接行为。

autoSlides.js(续)

  } else {
    window.clearInterval( parentList.loop );
    parentList.loop = null;
    t.innerHTML = autoSlides.playLabel;
  }
  DOMhelp.cancelClick( e );
},

showSlide()方法发生了很大的变化,但是您将会看到其他方法中一些最初容易混淆的部分(比如 slideLists 数组的优点)使得该方法变得相当简单。

请记住,您在 playSlide()中定义了 interval 应该使用列表的 showIndex 属性作为参数来调用 showSlide()方法。现在,您可以使用这个索引来检索需要循环的列表,只需从 slideLists 数组中检索该列表即可。

autoSlides.js(续)

showSlide : function( showIndex ) {
  var currentShow = autoSlides.slideLists[showIndex];

一旦你有了列表,你就可以读出当前的幻灯片和幻灯片的数量。从当前幻灯片中删除 showClass 以隐藏它:

autoSlides.js(续)

var count = currentShow.currentSlide;
var photoCount = currentShow.getElementsByTagName('li').length;
var photo = currentShow.getElementsByTagName('li')[count];
DOMhelp.cssjs('remove', photo, autoSlides.showClass );

增加计数器显示下一张幻灯片。将计数器与所有幻灯片的数量进行比较,如果已经播放了最后一张幻灯片,则将计数器设置为 0,从而从第一张幻灯片开始重新播放幻灯片。

通过检索列表元素并添加 Show 类来显示幻灯片。更新计数器,并将列表的 currentSlide 属性重置为新的列表元素:

autoSlides.js(续)

    count++;
    if( count == photoCount ){ count = 0 };
    photo = currentShow.getElementsByTagName('li')[count];
    DOMhelp.cssjs('add', photo, autoSlides.showClass );
    var counterText = currentShow.count.firstChild;
    counterText.nodeValue = counterText.nodeValue.replace( /\d/, count + 1 );
    currentShow.currentSlide = count;
  }
}
DOMhelp.addEvent( window, 'load', autoSlides.init, false );

当涉及到动画和代码的定时执行时,这种复杂性只是等待 JavaScript 开发人员的一种体验。创建一个流畅、稳定、跨浏览器的动画现在可以用 JavaScript,以及 CSS3 动画、变换和过渡来完成。幸运的是,有现成的动画库可以帮助您完成这项任务,并且已经由许多使用不同操作系统和浏览器的开发人员进行了稳定性测试。你将通过第十一章中的例子来了解其中一个。

浏览器窗口的导航方法

以下是浏览浏览器窗口的方法列表:

  • back():在浏览器历史记录中后退一页
  • forward():在浏览器历史记录中前进一页
  • home():表现为用户点击了 home 按钮(仅适用于 Firefox 和 Opera 在 IE 中,它的 document.location 是“about:home”)
  • stop():停止在窗口中加载文档(IE 不支持)
  • print():启动浏览器的打印对话框

使用这些方法在页面上提供导航是相当诱人的,这些页面应该通过类似下面的内容简单地链接回上一页:

<a href="javascript:window.back()">Back to previous page</a>

考虑到可访问性和现代脚本,这意味着没有 JavaScript 的用户将得到一些不存在的东西。更好的解决方案是通过服务器端 includes (SSIs) 生成一个真正的“返回上一页”链接,或者提供一个指向正确文档的实际 HTML 超链接。如果两者都不可行,请使用占位符,并在 JavaScript 可用时用生成的链接替换它,如下例所示:

exampleBackLink.html(途经 exampleForBackLink.html)

HTML:
<p id="back">Please use your browser's back button or
keyboard shortcut to go to the previous page</p>
JavaScript:
function backlink() {
  var p = document.getElementById('back');
  if( p ) {
    var newa = document.createElement('a');
    newa.setAttribute('href', '#');
    newa.appendChild( document.createTextNode('back to previous page') );
    newa.onclick = function() { window.back();return false; }
    p.replaceChild( newa, p.firstChild );
  }
}
window.onload = backlink;

image 警告这些方法的危险在于,你提供了浏览器已经提供给用户的功能。不同的是浏览器做得更好,因为它支持更多的输入设备。例如,在 PC 上的 Firefox 中,您可以通过按 Ctrl+P 来打印文档,通过按 Ctrl+W 来关闭窗口或标签,并通过 Alt 和向左或向右箭头键在浏览器历史中向前或向后移动。

更糟糕的是,这些方法提供的功能依赖于脚本支持。由您决定前面的方法——创建调用这些方法的链接,这可能是处理这个问题的最干净的方法——是否值得努力,或者您是否应该让用户决定如何触发浏览器功能。生成链接还具有改变浏览器历史的效果。在这种情况下,您不是导航到浏览器历史记录中的页面,而是向历史记录中添加新页面。

打开新窗口的替代方法:分层广告

有时没有办法避免弹出窗口,因为网站设计或功能需要它们,而由于前面解释的浏览器问题和选项,您不能让它们工作。一个解决方法是层广告,它基本上是放在主要内容之上的绝对定位的页面元素。

让我们试一个例子。假设您的公司希望在页面加载时非常明显地宣传其最新产品。最简单的方法是在文档末尾添加信息,并使用脚本将其转换为层广告。当您使用这种方法时,没有 JavaScript 的访问者仍然可以获得信息,但是如果不给他们机会删除这些信息,他们将无法获得任何覆盖内容的信息。HTML 可以是一个带有 ID 的简单 DIV(为了简洁起见,实际的链接已被替换为“#”):

example layer . html(excerpt)

<div id="layerad">
  <h2>We've got some special offers!</h2>
  <ul>
    <li><a href="#">TDK DVD-R 8x 50 pack $12</a></li>
    <li><a href="#">Datawrite DVD-R 16x 100 pack $50</a></li>
    <li><a href="#">NEC 3500A DVD-RW 16x $30</a></li>
  </ul>
</div>

CSS 设计者可以为非 JavaScript 版本设计广告样式,脚本将添加一个类,允许广告显示在主要内容之上。如果您调用类 dyn,CSS 可能如下所示:

分层. CSS(except)

#layerad{
  margin:.5em;
  padding:.5em;
}
#layerad.dyn{
  position:absolute;
  top:1em;
  left:1em;
  background:#eef;
  border:1px solid #999;
}
#layerad.dyn a.adclose{
  display:block;
  text-align:right;
}

最后一个选择器是动态链接的样式,脚本会将它添加到广告中,允许用户删除它。

剧本本身并不包含任何惊喜。首先,将广告的 ID、动态类以及“关闭”链接的类和文本内容定义为属性:

分层。js】??

ad = {
  adID : 'layerad',
  adDynamicClass : 'dyn',
  closeLinkClass : 'adclose',
  closeLinkLabel : 'close',

init()方法检查 DOM 和 ad,并向其中添加动态类。它创建一个新的链接,并向它添加文本和“关闭”链接的类。它向此链接添加一个指向 killAd()方法的事件处理程序,并在 Ad 的第一个子节点之前插入新链接:

layerAd.js(续)

init : function() {
  if( !document.getElementById || !document.createTextNode ) {
    return;
  }
  ad.offer = document.getElementById( ad.adID );
  if( !ad.offer ) { return; }
  DOMhelp.cssjs('add', ad.offer, ad.adDynamicClass );
  var closeLink = DOMhelp.createLink('#', ad.closeLinkLabel );
  DOMhelp.cssjs('add', closeLink, ad.closeLinkClass );
  DOMhelp.addEvent( closeLink, 'click', ad.killAd,false );
  closeLink.onclick = DOMhelp.safariClickFix;
  ad.offer.insertBefore( closeLink, ad.offer.firstChild );
},

killAd()方法从文档中删除广告并取消链接的默认行为:

layerAd.js(续)

  killAd : function( e ) {
    ad.offer.parentNode.removeChild( ad.offer );
    DOMhelp.cancelClick( e );
  }
}
DOMhelp.addEvent( window, 'load', ad.init, false );

您可以通过在浏览器中打开 exampleLayerAd.html 来测试效果。如果您启用了 JavaScript,您将会看到覆盖内容的广告,如图 6-16 所示。你可以通过使用“关闭”链接来摆脱它。

9781430250920_Fig06-16.jpg

图 6-16 。一个分层广告的例子

弹出窗口的另一个常见用途是在不离开当前页面的情况下显示另一个文档或文件。经典的例子包括一长串无聊的条款和条件或者一张照片。特别是在照片的情况下,弹出窗口是一个次优的解决方案,因为您可以打开一个与图片尺寸相同的窗口,但图像周围会有间隙,因为浏览器的内部样式在正文上有填充设置。您可以通过使用一个带有样式表的空白 HTML 文档来解决这个问题,该样式表将正文边距和填充设置为 0,并通过 JavaScript 将图像添加到窗口中的文档。另一种选择是在覆盖主文档的新生成和定位的元素中显示照片。演示 examplePicturePopup.html 就是这样做的;该脚本需要的只是一个在指向照片的链接上有特定名称的类:

examplePicturePopup.html(节选)

<a class="picturepop" href="pictures/thumbs/dog7.jpg">Sleeping Dog</a>

这个脚本需要做一些之前没有解释过的事情——即读取元素的位置。通过绝对定位元素,用元素覆盖主文档。因为不知道指向照片的链接在文档中的什么位置,所以需要读取它的位置,并在那里显示照片。

但那是以后的事了。首先,您需要预定义属性。您需要一个触发脚本的类,在显示照片时向链接应用一个 link 类,并向包含照片的元素应用另一个类。您还需要定义一个在显示照片时添加到链接的前缀,以及一个充当新创建元素的快捷方式引用的属性:

picturePopup.js (excerpt)

pop={
  triggerClass:'picturepop',
  openPopupLinkClass:'popuplink',
  popupClass:'popup',
  displayPrefix:'Hide',
  popContainer:null,

init()方法检查对 DOM 的支持,并遍历文档的所有链接,测试它们是否有正确的 CSS 类来触发弹出窗口。对于这样做的,该方法添加一个指向 openPopup()方法的事件处理程序,然后将链接的 innerHTML 内容存储在一个预设属性中。

picturePopup.js(续)

init : function() {
  if( !document.getElementById || !document.createTextNode ) {
    return;
  }
  var allLinks = document.getElementsByTagName('a');
  for( var i = 0; i < allLinks.length; i++ ) {
    if( !DOMhelp.cssjs('check', allLinks[i],pop.triggerClass ) ) {
      continue;
    }
    DOMhelp.addEvent( allLinks[i], 'click', pop.openPopup, false );
    allLinks[i].onclick = DOMhelp.safariClickFix;
    allLinks[i].preset = allLinks[i].innerHTML;
  }
},

openPopup()方法检索事件目标并确保它是一个链接。然后它测试是否已经有一个 popContainer,这意味着照片被显示。如果不是这样,该方法将前缀添加到链接的内容中,并添加动态类以使链接看起来不同:

picturePopup.js(续)

openPopup : function( e ) {
  var t = DOMhelp.getTarget( e );
  if( t.nodeName.toLowerCase() != 'a') {
    t = t.parentNode;
  }
  if( !pop.popContainer ) {
    t.innerHTML = pop.displayPrefix + t.preset;
    DOMhelp.cssjs('add', pop.popContainer, pop.popupClass );

然后方法创建一个新的 DIV 作为照片容器,添加适当的类,并添加一个新的图像作为容器 DIV 的子节点。它通过将新图像的 src 属性设置为原始链接的 href 属性的值来显示图像。然后,新创建的照片容器被添加到文档中(作为 body 元素的子元素)。最后,openPopup()调用 positionPopup()方法,将 link 对象作为参数。

picturePopup.js(续)

pop.popContainer = document.createElement('div');
DOMhelp.cssjs('add', t,pop.openPopupLinkClass );
var newimg = document.createElement('img');
pop.popContainer.appendChild( newimg );
newimg.setAttribute('src', t.getAttribute('href') );
document.body.appendChild( pop.popContainer );
pop.positionPopup( t );

如果 popContainer 已经存在,该方法所做的就是调用 killPopup()方法,将链接重置为其原始内容,并删除指示照片已显示的类。调用 cancelClick()可防止链接仅在浏览器中显示照片。

picturePopup.js(续)

  } else {
    pop.killPopup();
    t.innerHTML = t.preset;
    DOMhelp.cssjs('remove', t,pop.openPopupLinkClass );
  }
  DOMhelp.cancelClick( e );
},

positionPopup()方法定义了两个变量(x 和 y),将它们都初始化为 0,然后从元素的 offsetHeight 属性中读取元素的高度。接下来,它读取元素及其所有父元素的垂直和水平位置,并将其与 x 和 y 相加。结果是元素相对于文档的位置。然后,该方法通过将链接的高度与垂直变量 y 相加并改变 popContainer 样式属性,将照片容器定位在原始链接的下方:

picturePopup.js(续)

positionPopup : function( o ) {
  var x = 0;
  var y = 0;
  var h = o.offsetHeight;
  while ( o != null ) {
    x += o.offsetLeft;
    y += o.offsetTop;
    o = o.offsetParent;
  }
  pop.popContainer.style.left = x + 'px';
  pop.popContainer.style.top = y + h + 'px';
},

killPopup()方法从文档中移除 popContainer(通过将该属性的值设置为 null 来清除该属性),并通过调用 cancelClick()来阻止默认链接操作的发生。

image 注意你可以通过调用它的父节点的 removeChild()方法从文档中删除一个节点,把节点本身作为要删除的子节点。但是,因为您使用指向节点的属性,而不是检查节点本身,所以您还需要将此属性设置为 null。

picturePopup.js(续)

  killPopup : function( e ) {
    pop.popContainer.parentNode.removeChild( pop.popContainer );
    pop.popContainer = null;
    DOMhelp.cancelClick( e );
  }
}
DOMhelp.addEvent( window, 'load', pop.init, false );

结果是,你可以点击任何指向正确类别的照片的图像,它会显示下面的图像。图 6-17 显示了一个例子。

9781430250920_Fig06-17.jpg

图 6-17 。一个动态显示照片的例子

这种方法的美妙之处在于,它不仅仅局限于照片。只需简单的修改,您就可以在当前文档中显示其他文档。技巧是向 photoContainer 动态添加一个 IFRAME 元素,并将其 src 属性设置为要嵌入的文档。演示 exampleIframeForPopup.html 正是这样做的,在主文档中显示一个冗长的条款和条件文档。

唯一的区别(除了不同的属性名,因为该方法不显示照片)在于 openPopup 方法,在该方法中添加了新的 IFRAME:

ifrimforpopup . js(excerpt)

var ifr = document.createElement('iframe');
pop.ifrContainer.appendChild( ifr );
ifr.setAttribute('src', t.getAttribute('href') );

图 6-18 显示了这可能是什么样子。

9781430250920_Fig06-18.jpg

图 6-18 。一个动态包含和显示文档的例子

通过 IFRAME 元素包含其他文档是一种简单且受支持的方法,但这不是最容易实现的方法。相反,您可以使用 PHP 之类的服务器端语言来检索文档的内容并将其包含在当前文档中,并使用前面的 layer ad 示例使用的相同技巧。对于更现代的浏览器,你也可以使用 Ajax“动态地”做这件事,但是这将在它自己的章节中解释,第八章。

摘要:Windows 和 JavaScript

传统上,控制当前窗口和打开新窗口是 JavaScript 开发的一大部分,尤其是在 web 应用开发中。然而,近年来,由于对浏览器安全的担忧以及网上冲浪者被大量弹出窗口和安装阻止软件所困扰,使用新窗口变得越来越难——即使你想出于正当理由使用它们。这些和可访问性问题使得使用多个浏览器窗口成为越来越不可靠的 web 通信方法。多亏了这里讨论的一些替换技术(还有 Ajax,我将在第八章讨论)和 Twitter Bootstrap 这样的好框架,现在已经没有必要再使用它们了。以下是关于 Windows 和 JavaScript 需要记住的一些关键事项:

  • 在你尝试做任何事情之前,测试,测试,再测试你打开的窗口是否真的存在。
  • 永远记住,虽然窗口在同一个屏幕上,但它们是浏览器的完全独立的实例。如果您想从一个弹出窗口访问另一个弹出窗口,或者从您打开的任何弹出窗口访问主脚本中的一个功能,您需要通过 window.opener。
  • 不要试图通过拿走工具栏,在屏幕上移动它们,或者通过模糊()和聚焦()来显示和隐藏它们来控制窗口。这些功能大部分现在仍然可用,但很有可能在未来的浏览器中被屏蔽。
  • 您可以使用 window 对象方法模拟许多浏览器行为或交互式元素,如关闭和打印窗口或在浏览器历史记录中向后移动。然而,让用户来选择可能更好。如果您想为用户提供自己的控件,也可以用 JavaScript 创建这些控件。否则,当 JavaScript 不可用时,用户会得到一个你无法遵守的承诺。
  • 如果使用弹出窗口,在打开窗口的链接中告诉访问者,将会有一个新窗口。这就通知了不一定支持多窗口的用户代理的访问者,他们可能需要处理一个变化;它还可以防止访问者意外关闭网站所需的窗口。多年来不请自来的广告和弹出窗口已经让网上冲浪者习惯于立即关闭新窗口,甚至看都不看它们一眼。
  • 通过窗口方法 setTimeout()和 setInterval()使用代码的定时执行有点像化妆:作为一个女孩,你学会如何化妆;作为一个女人,你知道什么时候脱下来。你可以使用这两种方法——它们很容易创造出各种时髦的效果——但是你应该想想你的用户,问问你自己,“当一个静态界面可能更快地导致同样的结果时,真的需要动画吗?”

摘要

干得好!您已经阅读完了这一章,现在应该可以用图像和窗口,或者更好的窗口替换技术来创建自己的 JavaScript 解决方案了。

如果有些例子很难理解,不要沮丧,因为作为 JavaScript 开发人员,你会经常有这种感觉。这并不意味着你不明白。有许多方法可以解决 JavaScript 中的任何问题。虽然有比这里介绍的更简单的方法,但是习惯这种脚本应该会让您为更高级的脚本任务做好准备,比如使用第三方 API 或 web 应用开发。

在下一章,当我们讨论导航和表单时,你有机会更好地熟悉事件和属性处理。

七、JavaScript 和用户交互:导航和表单

在这一章中,我将讨论 JavaScript 的两个更常见的用途:导航和表单。两者都涉及大量的用户交互,因此需要深思熟虑地计划和执行。一个网站的成功与否取决于其导航的难易程度,在 web 上没有什么比一个难以使用或无法填写的表单更令人沮丧的了。

image 注意这一章由大量的代码示例组成,你会被要求在浏览器中打开其中的一些来自己测试功能。因此,如果你还没有去过www.beginningjavascript.com下载这本书的代码示例,现在可能是个好时机。当谈到编码时,我坚定地相信实践培训,当你可以直接体验它或者——更好的是——在你自己的编辑器中摆弄它时,你会更容易理解一些功能。

导航和 JavaScript

自从浏览器开始支持页面元素的外观和感觉的动态变化以来,增加网站导航的趣味一直是 DHTML 的主要任务之一。动画和褪色导航的时代是从 DHTML 开始的。这个想法似乎是,如果网页的导航非常流畅、高科技,并且看起来和操作起来都像星际飞船 Enterprise 上的 LCARS 界面,那么这个网站一定很棒。很多时候,访问者不同意,一旦他们厌倦了导航,他们会使用网站搜索选项,假设提供了一个。

这里就不说华而不实的导航了;相反,我将举例说明如何使用 JavaScript 使页面和站点导航更直观、更简单,同时尽可能保持其可访问性。

页面重载的恐惧

对表单和 web 导航进行了大量 JavaScript 增强,以防止访问者在到达他们想要的信息之前必须重新加载页面或加载大量页面。这是一个令人钦佩的想法,可以奇妙地工作;但是,我们不要忘记,只有当整个文档被加载时,JavaScript 才能到达页面的元素,并且它可以处理文档中已经存在的内容(也就是说,除非您使用 Ajax 动态加载其他内容——下一章将详细介绍)。

这意味着您可以用 JavaScript 创建一个光滑的界面,只显示整个页面内容的一部分,但这也意味着没有 JavaScript 的访问者将不得不处理文档中的全部数据。在您完全沉迷于增强页面之前,请不时关闭 JavaScript,看看您是否能够处理文档中的数据量。

只有少量专门信息的较小文档的好处是,您可以使用浏览器提供给用户的整个工具包:前进和后退、书签和打印。当您使用 JavaScript 进行导航或分页时,可能会破坏这个功能。不太理想的效果是,您必须维护更多的文档,访问者必须分别加载每个文档,因此也增加了服务器流量。

用 JavaScript 来增强网站并不邪恶;这完全是一个适度和了解你的观众的问题。

导航和 JavaScript 基础

导航和 JavaScript 最基本的想法是,你不依赖 JavaScript 来让你的导航工作。根据 JavaScript 的不同,页面或网站导航会将没有 JavaScript 的用户拒之门外,也会将搜索引擎拒之门外。

image 提示如果你必须向一个非技术人员解释为什么一堆 JavaScript:navigate(‘page 2’)链接不是一个好主意,那么后者是一个很好的论据。对于一个甚至不知道如何在浏览器中关闭 JavaScript 的人来说,将禁用 JavaScript 的网站访问者作为一个值得考虑的目标群体是不容易解释的。向他解释“又大又瞎的百万富翁”谷歌不会索引他的网站是一个更容易的卖点。

使用 JavaScript 的一个经典例子是使用选择框来导航,这看起来使导航更容易,但却有可能疏远一大群访问者。选择框很棒,因为它们可以让你在不浪费屏幕空间的情况下提供很多选项。图 7-1 为打开的选择框;对于所有这些选项,封闭的只使用了一行。

9781430250920_Fig07-01.jpg

图 7-1 。使用选择框进行导航

打开演示页面 exampleSelectNavigation.html,并从下拉菜单中选择一个选项。如果您连接到网络并启用了 JavaScript,您将立即被发送到您选择的地址。不幸的是,如果你选择了错误的选项,你没有机会撤销你的选择。如果没有 JavaScript,您可以选择一个选项,但是什么都不会发生。

examples selectnavigation . html

<form>
<p>
  <select onchange="window.location = this.options[this.selectedIndex].value">
    <option value="#">Please select</option>
    <option value="http://www.apress.com">The publisher</option>
    <option value="http://wait-till-i.com">The author's blog</option>
    <option value="http://icant.co.uk">The author's other articles</option>
    <option value="http://onlinetools.org">Scripts and tools by the author</option>
  </select>
</p>
</form>

键盘访问是另一个问题:你通常使用 Tab 键来访问一个选择框,然后按上下键来选择你想要的选项。在本例中,您将不会有这样的机会,因为一旦您按下向下箭头键,您将被转到第一个选项。解决方法是同时按 Alt 和向下箭头键来展开整个列表。然后,您可以使用向上和向下箭头键选择您的选项,并按 Enter 键选择它。但是你知道吗?

解决这些问题的最简单的方法是永远不要使用 change、mouseover 或 focus 事件将数据发送给服务器或将用户发送到不同的 web 位置。很多游客都无法进入,这可能会导致很多挫折。如果没有迹象表明数据正在发送并且页面将会改变,这是特别令人沮丧的。

相反,提供一个真正的提交按钮、一个在服务器端执行相同重定向的脚本,以及一个提交处理程序,以便在 JavaScript 可用时通过 JavaScript 进行重定向。

examples saferselectnavigation . html(excerpt)

<form method="post" action="redir.php">
<p>
  <label for="url">Please select your destination:</label>
  <select id="url" name="url">
    <option value="http://www.apress.com">The publisher</option>
    <option value="http://wait-till-i.com">The author's blog</option>
    <option value="http://icant.co.uk">The author's other articles</option>
    <option value="http://onlinetools.org">Scripts and tools by the author</option>
  </select>
  <input type="submit" value="Make it so!">
</p>
</form>

这个脚本非常简单:您将一个事件处理程序应用到提交时触发方法的第一个表单。该方法从 ID 为 url 的选项列表的 selectedIndex 中读取用户所做的选择,并通过 window.location 对象将浏览器重定向到那里。在下一节中,您将读到更多关于 window.location 对象的内容,在本章的“表单和 JavaScript”一节中,您将读到所有关于 selectedIndex 和 form 对象的内容。

examples saferselectnavigation . js

send = {
  init : function() {
    DOMhelp.addEvent(document.forms[0], 'submit', send.redirect, false);
  },
  redirect : function(e){
    var t = DOMhelp.getTarget(e);
    var url = t.elements['url'];
    window.location.href = url.options[url.selectedIndex].value;
    DOMhelp.cancelClick(e);
  }
}
DOMhelp.addEvent(window, 'load', send.init, false);

将非 JavaScript 用户发送到另一个 URI 的服务器端脚本可以是 PHP 中一个简单的头重定向:

<?php header('Location: ' . $_POST['url']); ?>

如果用户启用了 JavaScript,她就不必往返服务器;相反,她会被立即转到另一个网站。这可以通过设置 window.location.href 属性来实现,该属性是内置浏览器导航的一部分。

浏览器导航

浏览器为您提供了几个对象,您可以使用它们来自动重定向或浏览浏览器的历史记录。在上一章中,您已经遇到了 window.back()方法。窗口对象还提供了窗口.位置和窗口.历史等属性

window.location 对象存储当前元素的 URI,并具有以下属性(在括号中,如果 URI 是www.example.com:8080/index.php?,您会看到提供的返回值 s=JavaScript#searchresults ):

所有这些属性都可以读写。例如,如果您希望将搜索更改为 DOM 脚本,可以通过 window.location.search= '?“DOM 脚本”。浏览器会自动将字符串 URL 编码为 DOM%20scripting。还可以通过更改 window.location.href 属性将用户的浏览器发送到另一个位置。

除了这些属性,window.location 还有两个方法:

  • reload():重新加载当前文档(与单击“重新加载”按钮或同时按 F5 或 Ctrl 和 R 键的效果相同)。
  • replace(URI):将用户发送到 URI,并用另一个替换当前的 URI。当前的 URI 将不再是浏览器历史的一部分。

image 注意注意这与 String 对象的 replace()方法不同,后者用字符串的一部分替换另一部分。

您可以使用 reload()定期刷新页面,从后端加载新内容,而无需用户单击 Reload 按钮。这种功能在基于 JavaScript 的聊天系统中很常见。

使用 replace()可能非常烦人,因为它破坏了用户的后退按钮功能。当她不喜欢你发给她的页面时,她不能回到当前页面。

用户在到达当前页面之前访问过的页面列表存储在 window.history 对象中。这个对象只有一个属性,叫做 length,它存储已经访问过的页面的数量。它有一些您可以使用的方法(下面列表中的最后两个是作为 HTML5 的一部分添加的):

  • back():在浏览器历史中返回上一页。
  • forward():在浏览器历史记录中前进一页。
  • go(n): Go n 根据 n 是正还是负,在浏览器历史中向前或向后移动。您也可以通过 history.go(0)重新加载同一页面。
  • pushState (State,title,url):该方法将数据推入会话历史,其中 state 表示一个充满数据的对象,title 表示页面标题,url 表示添加到历史中的 URL。
  • replaceState (state,title,url):该方法的工作方式与 pushState()相同,但它修改数据,而不是向浏览器的历史记录中添加新数据。

历史对象只允许您导航到其他页面,而不能读出它们的 URIs 或更改它们。该规则的例外是当前页面,当您使用 replace()时,它会从浏览器历史记录中删除。

正如上一章已经解释过的,通过 JavaScript 将用户发送到其他页面,可以有效地模拟浏览器功能,这可能是多余的,或者会彻底迷惑用户。

页面内导航

您可以使用 JavaScript 使同一页面内的导航更有趣,占用的屏幕空间更少。在 HTML 中,您可以通过锚和目标提供页面内导航,它们都是用

image 注意锚的 name 属性在 HTML5 中被弃用,它实际上足以提供一个 ID 来链接锚和目标对。但是,为了确保与旧浏览器的兼容性,在下面的例子中使用它可能是一个好主意。

让我们以一个目录中链接到页面下方不同目标的内部链接列表作为页面内导航的示例:

exampleLinkedAnchors.html(节选)

<h1>X - a tool that does Y</h1>
<div id="toolinfo">
  <ul id="toolinfotoc">
    <li><a href="#info">Information</a></li>
    <li><a href="#demo">Demo</a></li>
    <li><a href="#installation">Installation</a></li>
    <li><a href="#use">Use</a></li>
    <li><a href="#license">License</a></li>
    <li><a href="#download">Download</a></li>
  </ul>
  <div class="infoblock">
    <h2><a id="info" name="info">Information about X</a></h2>
    [... content ...]
    <p class="back">
      <a href="#toolinfotoc">Back to <acronym title="Table of Contents">TOC</acronym></a>
    </p>
  </div>
  <div class="infoblock">
    <h2><a id="demo" name="demo">Demonstration of what X can do</a></h2>
    [... content ...]
    <p class="back">
      <a href="#toolinfotoc">Back to <acronym title="Table of Contents">TOC</acronym></a>
    </p>
  </div>
  [.... more sections ...]
</div>

您可能会想,具有 infoblock 类的 DIV 元素对于页面内导航来说并不是必需的。这只是部分正确,因为微软的 IE 浏览器在命名锚和键盘导航方面有一个非常烦人的错误。

如果您在 Internet Explorer 中打开演示页面 exampleLinkedAnchors.html,通过按 Tab 键浏览不同的菜单项,并通过按 Enter 键选择您想要的部分,浏览器将被发送到您选择的锚点。但是,Internet Explorer 不会将键盘焦点发送到此锚点。如果您再次按 Tab 键,您不会到达文档中的下一个链接;相反,你会被送回菜单。

您可以通过在具有定义宽度的元素中嵌套锚来解决这个问题。这就是 div 的作用。你可以在 exampleLinkedAnchorsFixed.html 的演示页面上测试一下。实际结果是,您可以将这些元素(在本例中是 div)用于 CSS 样式。

image 提示默认情况下,辅助功能在不同的浏览器中可能是关闭的。例如,要在 MacOS 上的 Safari 中重新打开它,请前往“偏好设置”、“高级”、“辅助功能”。选择按下标签以高亮显示网页上的每个项目选项。

现在让我们用一个脚本来复制和改进这个功能。脚本应该做的是显示菜单,但隐藏所有部分,只显示您选择的部分,以使页面更短,不至于让人不知所措。逻辑很简单:

  • 遍历菜单中的链接,并添加 click 事件处理程序来显示与菜单项相关的部分。
  • 在事件侦听器方法中,隐藏前面显示的部分,显示当前部分。
  • 初始化页面时,隐藏所有部分并显示第一个部分。

然而,这并没有考虑到页面内导航的另一个方面:页面可能被来自另一个链接的预定义目标所请求。通过在浏览器中向 URI 添加锚点来尝试一下,例如,examplelinkedanchorsfied . html # use。您将自动向下滚动到使用部分。您的脚本应该考虑这个用例。

让我们从定义脚本的框架开始。这个脚本的主要对象被称为 iv,用于内部导航——因为 in 是 JavaScript 中的保留字,您希望保持简短。你需要几个属性:

  • 一个 CSS 类来定义菜单何时是 JavaScript 增强的
  • 突出显示菜单中当前链接的 CSS 类
  • 显示当前部分的 CSS 类

image 提示你不需要通过 JavaScript 隐藏这些部分,但是你可以使用上一章描述的 CSS 父类技巧。

您需要为要添加 CSS 类的父元素定义属性,并为能够遍历链接的菜单定义 ID。

还需要两个属性:一个存储当前显示的部分,另一个存储当前突出显示的链接。

就方法而言,您需要一个 init()方法、一个获取当前部分的事件监听器以及一个隐藏前一部分并显示当前部分的方法。

内部名称. js(骨架)

iv = {
  // CSS classes
  dynamicClass : 'dyn',
  currentLinkClass : 'current',
  showClass : 'show',

  // IDs
  parentID : 'toolinfo',
  tocID : 'toolinfotoc',

  // Global properties
  current : null,
  currentLink : null,

  init : function(){ },
  getSection : function(e){ },
  showSection : function(o){ }
DOMhelp.addEvent(window, 'load', iv.init, false);

init()方法首先检查 DOM 是否受支持以及所有必要的元素是否可用。只有这样,您才可以通过 CSS 将类添加到父元素中,以自动隐藏所有的 section 元素。

内部名称. js(except)

init : function(){
  if(!document.getElementById || !document.createTextNode) {
    return;
  }
  iv.parent = document.getElementById(iv.parentID);
  iv.toc = document.getElementById(iv.tocID);
  if(!iv.parent || !iv.toc) { return; }
  DOMhelp.cssjs('add', iv.parent, iv.dynamicClass);

在变量 loc 中存储一个可能的 URL 散列,并开始遍历菜单中的所有链接。替换哈希值中的#使得以后使用它更容易,因为您可以在 getElementById()中使用该名称,而不必删除哈希。

innerNav.js(续)

var loc = window.location.hash.replace('#', ' ');
var toclinks = iv.toc.getElementsByTagName('a');
for(var i = 0; i < toclinks.length; i++) {

将当前链接的 href 属性与 loc 进行比较,如果相同,则将链接存储在 current link 属性中。这里使用的 string replace()方法从 href 属性中删除除锚点名称之外的任何内容。这是必要的,因为在 Internet Explorer 等一些浏览器中,getAttribute('href ')会返回包括文件路径在内的整个链接位置(这在 IE 8 中已得到修复),而不仅仅是 HTML href 属性中的内容。

innerNav.js(续)

if(toclinks[i].getAttribute('href').replace(/.*#/, ' ') == loc){
  iv.currentLink = toclinks[i];
}

接下来,添加一个指向 getSection()的 click 事件。请注意,在这个示例脚本中,您不需要停止默认事件—相反,允许浏览器跳转到该部分也会更改地址栏中的 URI,这反过来允许用户将该部分添加为书签。

innerNav.js(续)

 DOMhelp.addEvent(toclinks[i], 'click', iv.getSection, false);
}

仅当其中一个链接与 URI 中的散列相同时,才定义 currentLink 属性。这意味着如果 URI 没有散列,或者它有一个指向不存在的锚的散列,则需要将 currentLink 定义为菜单中的第一个锚。init()方法通过调用 showSection()方法以 currentLink 作为参数来结束。

innerNav.js(续)

if(!iv.currentLink) {
  iv.currentLink = toclinks[0];
  }
  iv.showSection(iv.currentLink);
},

事件侦听器方法 getSection()不需要做太多事情;它需要做的就是确定哪个链接被点击了,并将其作为参数发送给 showSection()。如果不是需要访问 window.location.hash,这两行可能是 showSection()方法的一部分。

innerNav.js(续)

getSection : function(e) {
  var t = DOMhelp.getTarget(e);
  iv.showSection(t);
},

showSection()方法检索在 init()方法中作为参数 o 单击或定义的 link 对象。第一项任务是读取此链接的 href 属性,并通过正则表达式删除散列符号之前和包括散列符号在内的所有内容来检索锚点名称。然后,通过读取带有锚 ID 的元素并在节点树中向上移动两个节点,检索要显示的部分。

innerNav.js(续)

showSection : function(o) {
  var targetName = o.getAttribute('href').replace(/.*#/,'');
  var section = document.getElementById(targetName).parentNode.parentNode;

为什么有两个节点?如果您还记得 HTML,您将链接嵌套在标题中,并将标题和部分的其余部分嵌套在 DIV 元素中:

exampleLinkedAnchors.html(节选)

<li><a href="#demo">Demo</a></li>
 [... code snipped ...]
 <div class="infoblock">
   <h2><a id="demo" name="demo">Demonstration of what X can do</a></h2>

因为 getElementById('demo ')提供了链接,所以一个向上的节点是 H2,另一个向上的节点是 DIV。

然后,您需要检查是否显示了一个旧的部分和一个突出显示的链接,然后通过删除适当的类来删除突出显示并隐藏该部分。然后添加当前链接和当前节的类,并设置属性 current 和 current link,确保下次调用 showSection()时撤销您现在所做的操作。

innerNav.js(续)

    if(iv.current != null){
      DOMhelp.cssjs('remove', iv.current, iv.showClass);
      DOMhelp.cssjs('remove', iv.currentLink, iv.currentLinkClass);
    }
    DOMhelp.cssjs('add', section, iv.showClass);
    DOMhelp.cssjs('add', o,iv.currentLinkClass);
    iv.current = section;
    iv.currentLink = o;
  }
}
DOMhelp.addEvent(window, 'load', iv.init, false);

如果您将这个脚本应用到演示 HTML 页面并添加一个适当的样式表,当您单击链接时,您会得到一个显示不同部分的更短的页面。通过在浏览器中打开 exampleLinkedAnchorsPanel.html,您可以亲自看到这一点。在 Firefox 17.0.1 的 MacOS 上,页面看起来就像你在图 7-2 中看到的那样。

9781430250920_Fig07-02.jpg

图 7-2 。从锚-目标列表创建的面板界面

简单地应用一个不同的样式表将页面变成一个选项卡式界面,正如你在 exampleLinkedAnchorsTabs.html 和图 7-3 中看到的。

9781430250920_Fig07-03.jpg

图 7-3 。从锚-目标列表创建的选项卡式界面

对于一个简短的脚本来说,这是非常简洁的;然而,每当用户单击其中一个选项时,清除链接 href 并检索该部分似乎是重复的。

完成相同任务的另一种方法是将链接和部分存储在两个关联数组中,并简单地为 showSection()提供要显示和突出显示的锚的名称。演示 exampleLinkedAnchorsTabsNamed.html 使用了这种技术,并展示了如何同时应用鼠标悬停处理程序来获得相同的效果。

内联 name . js】??

iv = {
  // CSS classes
  dynamicClass : 'dyn',
  currentLinkClass : 'current',
  showClass : 'show',

  // IDs
  parentID : 'toolinfo',
  tocID : 'toolinfotoc',

第一个变化是您只需要一个当前属性和两个新的数组属性,称为 sections 和 sectionLinks,它们将在以后存储节和链接。

innerNavNamed.js(续)

// Global properties
current : null,
sections : [],
sectionLinks : [],
init : function() {
  var targetName,targetElement;
  if(!document.getElementById || !document.createTextNode){
    return;
  }
  var parent = document.getElementById(iv.parentID);
  var toc = document.getElementById(iv.tocID);
  if(!parent || !toc) { return; }
  DOMhelp.cssjs('add', parent, iv.dynamicClass);
  var toclinks = toc.getElementsByTagName('a');
  for(var i = 0; i < toclinks.length; i++){

除了单击之外,还要添加一个 mouseover 处理程序,并将 href 属性存储在菜单中每个链接的一个名为 targetName 的属性中。

innerNavNamed.js(续)

DOMhelp.addEvent(toclinks[i], 'click', iv.getSection, false);
DOMhelp.addEvent(toclinks[i], 'mouseover', iv.getSection,false);
targetName = toclinks[i].getAttribute('href').replace(/.*#/,'');
toclinks[i].targetName = targetName;

通过将第一个链接存储在 presetLink 变量中,将其定义为当前活动链接,并确定锚是否指向现有元素。如果是,则将元素存储在 sections 数组中,将链接存储在 sectionLinks 数组中。请注意,这会产生一个关联数组,这意味着您可以通过 section['info']到达第一节。

innerNavNamed.js(续)

  if(i == 0){ var presetLink = targetName; }
  targetElement = document.getElementById(targetName);
  if(targetElement) {
    iv.sections[targetName] = targetElement.parentNode.parentNode;
    iv.sectionLinks[targetName] = toclinks[i];
  }
}

然后,您可以从 URI 散列中获得一个可能的锚名,并使用该锚名或 presetLink 中存储的锚名调用 showSection()。

innerNavNamed.js(续)

  var loc = window.location.hash.replace('#', ' ');
  loc = document.getElementById(loc) ? loc : presetLink;
  iv.showSection(loc);
},

getSection()事件用链接的 targetName 属性值调用 showSection()。这个属性是在前面的 init()方法中设置的。

innerNavNamed.js(续)

getSection:function(e){
  var t = DOMhelp.getTarget(e);
  iv.showSection(t.targetName);
},

所有这些都使 showSection()变得轻而易举,因为重置最后一个链接和部分并设置当前链接和部分所需要做的就是使用数组到达正确的元素并添加或删除 CSS 类。当前节存储在一个名为 current 的属性中,而不是存储在节和链接的属性中。

innerNavNamed.js(续)

  showSection : function(sectionName){
    if(iv.current != null){
      DOMhelp.cssjs('remove', iv.sections[iv.current], iv.showClass);
      DOMhelp.cssjs('remove', iv.sectionLinks[iv.current], iv.currentLinkClass);
    }
    DOMhelp.cssjs('add', iv.sections[sectionName], iv.showClass);
    DOMhelp.cssjs('add', iv.sectionLinks[sectionName], iv.currentLinkClass);
    iv.current = sectionName;
  }
}
DOMhelp.addEvent(window, 'load', iv.init, false);

页面内导航有更多的选项,例如,你可以提供“上一页”和“下一页”链接,而不是“上一页”链接来浏览选项。如果你想看到这样的脚本,并且每页提供几个标签导航,你可以在 http://onlinetools.org/tools/domtabdata/查看 DOMtab。

网站导航

网站导航和页面内部导航完全不同。您现在一定已经厌倦了阅读它,但是对于依赖于 JavaScript 的导航来说,没有什么好的理由。是的,您可以使用 JavaScript 自动将用户发送到其他位置,但这不是一种安全的方法,因为像 Opera 和 Mozilla 这样的浏览器允许用户阻止这一点。(恶意网站过去使用重定向将用户发送到垃圾网站。)此外,作为站点维护者,它剥夺了您使用站点度量软件的机会,该软件计算点击次数并记录您的访问者在站点中的行程,因为不是所有的度量包都计算 JavaScript 重定向。

由于这些原因,站点导航基本上被限制为增强菜单的 HTML 结构的功能,并通过事件处理程序添加功能。用户到其他页面的真正重定向仍然需要通过链接或表单提交来实现。

网站菜单的一个非常合乎逻辑的 HTML 结构是嵌套列表:

exampleSiteNavigation.html(节选)

<ul id="nav">
  <li><a href="#">Home</a></li>
  <li><a href="#">Products</a>
    <ul>
      <li><a href="#">CMS solutions</a>
        <ul>
          <li><a href="#">Mini CMS</a></li>
          <li><a href="#">Enterprise CMS</a></li>
        </ul>
      </li>
      <li><a href="#">Company Portal</a></li>
      <li><a href="#">eMail Solutions</a>
        <ul>
          <li><a href="#">Private POP3/SMTP</a></li>
          <li><a href="#">Listservers</a></li>
        </ul>
      </li>
    </ul>
  </li>
  <li><a href="#">Services</a>
    <ul>
      <li><a href="#">Employee Training</a></li>
      <li><a href="#">Auditing</a></li>
      <li><a href="#">Bulk sending/email campaigns</a></li>
    </ul>
  </li>
  <li><a href="#">Pricing</a></li>
  <li><a href="#">About Us</a>
    <ul>
      <li><a href="#">Our offices</a></li>
      <li><a href="#">Our people</a></li>
      <li><a href="#">Jobs</a></li>
      <li><a href="#">Industry Partners</a></li>
    </ul>
  </li>
  <li><a href="#">Contact Us</a>
    <ul>
      <li><a href="#">Postal Addresses</a></li>
      <li><a href="#">Arrange Callback</a></li>
    </ul>
  </li>
</ul>

原因是,即使没有任何样式表,导航的结构和层次对访问者来说也是显而易见的。您还可以轻松地设计导航样式,因为所有元素都包含在更高层次的元素中,这允许使用上下文选择器。

image 注意我们不会在这里讨论在导航中提供网站的每一页是否有意义(因为传统上这是网站地图的工作)。在下一章,我们将再次讨论这个话题,并提供给用户一个选择。

基本的网站可用性和常识决定了当前显示的页面不应该链接到自身。为了防止这种情况发生,用一个强元素替换当前页面链接,这也意味着没有 CSS 的用户知道他们在导航中的位置,并且您有机会在导航中以不同的方式设置当前页面的样式,而不必求助于 CSS 类。使用一个强元素代替一个 SPAN 也意味着没有 CSS 的用户可以得到一个明显的指示,表明哪个项目是当前项目。

例如,在迷你 CMS 页面上,导航如下:

exampleHighlightedSiteNavigation.html(节选)

<ul id="nav">
  <li><a href="#">Home</a></li>
  <li><a href="#">Products</a>
    <ul>
      <li><a href="#">CMS solutions</a>
        <ul>
          <li><strong>Mini CMS</strong></li>
          <li><a href="#">Enterprise CMS</a></li>
        </ul>
      </li>
      </ul>
    </li>

您必须在服务器端这样做,因为在 JavaScript 中突出显示当前页面是没有意义的(当然,通过将所有 link href 属性与 window.location.href 进行比较,这并不难做到)。

期望这个 HTML 结构允许你创建一个类似资源管理器的展开和折叠菜单。当你点击包含其他项目的菜单项时,它应该显示或隐藏它的子项目。然而,脚本的逻辑可能与您预期的有点不同。首先,您不必遍历菜单的所有链接。相反,您需要执行以下操作:

  1. 向隐藏所有嵌套列表的主导航项目添加一个 CSS 类。
  2. 循环浏览导航中的所有 UL 项目(因为它们是嵌套的子菜单)。
  3. 向每个 UL 的父节点添加一个指示该列表项包含其他列表的 CSS 类。
  4. 在父节点内的第一个链接上添加一个 click 事件。
  5. 测试父节点是否包含任何强元素,如果有,添加该类以显示 UL,从而防止当前页面所在的子菜单被隐藏。您用一个打开的类替换父类,以显示该部分已经展开。
  6. click 事件侦听器方法需要检查父节点的第一个嵌套 UL 是否有 show 类,如果有,就删除它。它还应该用父类替换开放类。如果没有显示类,它应该做完全相反的事情。

演示文档 exampleDynamicSiteNavigation.html 做到了这一点,用迷你 CMS 页面定义为当前页面来显示效果。图 7-4 显示了这在 MacOS 上的 Firefox 17.0.1 中的样子。

9781430250920_Fig07-04.jpg

图 7-4 。带有 JavaScript 和 CSS 的树形菜单

剧本的框架相当短;您将所有必需的 CSS 类定义为属性,将导航的 ID 定义为另一个属性,并使用 init()和 changeSection()方法来应用整体功能并相应地展开或折叠各个部分。

site navigation . js(skeleton)

sn = {
  dynamicClass : 'dyn',
  showClass : 'show',
  parentClass : 'parent',
  openClass : 'open',
  navID : 'nav',
  init : function() {},
  changeSection : function(e) {}
}
DOMhelp.addEvent(window, 'load', sn.init, false);

init()方法定义了一个名为 triggerLink 的变量,并在应用动态类隐藏嵌套元素之前检查 DOM 支持以及必要的导航元素是否可用。

siteNavigation.js(节选)

init : function() {
  var triggerLink;
  if(!document.getElementById || !document.createTextNode) {
    return;
  }
  var nav = document.getElementById(sn.navID);
  if(!nav){ return; }
  DOMhelp.cssjs('add', nav, sn.dynamicClass);

然后,它遍历所有嵌套的 UL 元素,并将对父节点中第一个链接的引用存储为 triggerLink。它应用调用 changeSection()方法的 click 事件,并将父类添加到父节点。

siteNavigation.js(续)

var nested = nav.getElementsByTagName('ul');
for(var i = 0; i < nested.length; i++){
  triggerLink = nested[i].parentNode.getElementsByTagName('a')[0];
  DOMhelp.addEvent(triggerLink, 'click', sn.changeSection, false);
  DOMhelp.cssjs('add', triggerLink.parentNode, sn.parentClass);
  triggerLink.onclick = DOMhelp.safariClickFix;

该代码测试父节点是否包含强元素,如果是,则将 show 类添加到 UL,将 open 类添加到父节点。这可以防止当前部分被隐藏。

siteNavigation.js(续)

    if(nested[i].parentNode.getElementsByTagName('strong').length > 0){
      DOMhelp.cssjs('add', triggerLink.parentNode, sn.openClass);
      DOMhelp.cssjs('add', nested[i], sn.showClass);
    }
  }
},

所有事件侦听器方法 changeSection()需要做的就是获取事件目标,测试父节点的第一个嵌套 UL 是否应用了 show 类,如果是,则移除该 UL。此外,它需要将父节点的 open 类更改为 parent,反之亦然。

siteNavigation.js(续)

  changeSection : function(e){
    var t = DOMhelp.getTarget(e);
    var firstList = t.parentNode.getElementsByTagName('ul')[0];
    if(DOMhelp.cssjs('check', firstList, sn.showClass)) {
      DOMhelp.cssjs('remove', firstList, sn.showClass)
      DOMhelp.cssjs('swap', t.parentNode, sn.openClass, sn.parentClass);
    } else {
      DOMhelp.cssjs('add', firstList,sn.showClass)
      DOMhelp.cssjs('swap', t.parentNode, sn.openClass, sn.parentClass);
    }
    DOMhelp.cancelClick(e);
  }
}
DOMhelp.addEvent(window, 'load', sn.init,false);

该脚本应用于正确的 HTML,并使用适当的样式表进行样式化,将为您提供展开和折叠导航。CSS 的相关部分如下:

siteNavigation.css(节选)

#nav.dyn li ul{
  display:none;
}
#nav.dyn li ul.show{
  display:block;
}
#nav.dyn li{
  padding-left:15px;
}
#nav.dyn li.parent{
  background:url(plus.jpg) 0 5px no-repeat #fff;
}
#nav.dyn li.open{
  background:url(minus.jpg) 0 5px no-repeat #fff;
}

通过将嵌套的 UL 元素的显示属性值分别设置为“阻止”和“无”,可以显示和隐藏这些元素。这也使得所包含的链接脱离了正常的跳转顺序:如果键盘用户想要到达同一层次上的下一个元素而不展开该部分,他们不必在嵌套列表中的所有链接中跳转。如果他们先按 Enter 键展开该部分,他们将能够使用 Tab 键浏览子菜单链接。

所有 LI 元素都有一个左填充,以允许指示器图像显示该部分有子链接或者它是打开的。具有 open 类或 parent 类的 LI 元素得到一个背景图像来指示它们的状态。

所有这些都很好,但是如果您想提供一个到嵌套部分的父页面的链接呢?解决方案是在每个父链接之前添加一个新的链接图像,它显示和隐藏链接,并保持链接不变。

演示页面 exampleIndicatorSiteNavigation.html 展示了这是什么样子,它是如何工作的。脚本不需要做太多的修改:

siteNavigationIndicator.js(节选)

sn = {
  dynamicClass : 'dyn',
  showClass : 'show',
  parentClass : 'parent',
  openClass : 'open',

第一个变化是您需要两个新的属性来提供要添加到嵌套列表的父节点中的图像。这些将通过 innerHTML 添加,以便于维护人员在需要时用其他图像甚至文本替换它们。

siteNavigationIndicator.js(续)

parentIndicator : '<img src="plus.jpg" alt="open section" title="open section">',
openIndicator: '<img src="minus.jpg" alt="close section" title="close section">',
navID : 'nav',
init : function() {
  var parentLI, triggerLink;
  if(!document.getElementById || !document.createTextNode){
    return;
  }
  var nav = document.getElementById(sn.navID);
  if(!nav){ return; }
  DOMhelp.cssjs('add', nav,sn.dynamicClass);
  var nested = nav.getElementsByTagName('ul');
  for(var i = 0; i < nested.length; i++) {

您没有将父节点中的第一个链接作为触发链接,而是创建了一个新的 link 元素,将其 href 属性设置为一个简单的 hash,使其可点击,并添加前面定义的父指示器图像作为其内容。然后插入链接的图像作为父节点的第一个子节点。

siteNavigationIndicator.js(续)

parentLI = nested[i].parentNode;
triggerLink = document.createElement('a');
triggerLink.setAttribute('href', '#')
triggerLink.innerHTML = sn.parentIndicator;
parentLI.insertBefore(triggerLink, parentLI.firstChild);

init()方法的其余部分几乎保持不变,不同之处在于,当父节点包含一个强元素时,您不仅要应用这些类,还要用“打开的”指示器图像替换“父”指示器图像。

siteNavigationIndicator.js(续)

    DOMhelp.addEvent(triggerLink, 'click', sn.changeSection, false);
    triggerLink.onclick = DOMhelp.safariClickFix;
    DOMhelp.cssjs('add', parentLI, sn.parentClass);
    if(parentLI.getElementsByTagName('strong').length > 0) {
      DOMhelp.cssjs('add', parentLI, sn.openClass);
      DOMhelp.cssjs('add', nested[i], sn.showClass);
      parentLI.getElementsByTagName('a')[0].innerHTML = sn.openIndicator
    }
  }
},

changeSection()方法的不同之处在于,您需要通过将目标的节点名与。

siteNavigationIndicator.js(续)

changeSection : function(e){
  var t = DOMhelp.getTarget(e);
  while(t.nodeName.toLowerCase() != 'a') {
    t = t.parentNode;
  }

该方法的其余部分保持不变,只有一点不同——除了应用不同的类之外,您还更改了链接的内容。

siteNavigationIndicator.js(续)

    var firstList = t.parentNode.getElementsByTagName('ul')[0];
    if(DOMhelp.cssjs('check', firstList, sn.showClass)) {
      DOMhelp.cssjs('remove', firstList, sn.showClass);
      DOMhelp.cssjs('swap', t.parentNode, sn.openClass, sn.parentClass);
      t.innerHTML = sn.parentIndicator;
    } else {
      DOMhelp.cssjs('add', firstList, sn.showClass)
      DOMhelp.cssjs('swap', t.parentNode, sn.openClass, sn.parentClass);
      t.innerHTML = sn.openIndicator;
    }
    DOMhelp.cancelClick(e);
  }
}
DOMhelp.addEvent(window, 'load', sn.init, false);

所有这些只是增强站点导航的一个例子,而且可能是最容易使用的一个。例如,让一个多级下拉导航菜单变得可访问,同时也适用于鼠标和键盘用户,这是一个巨大的任务,不在本书的范围之内,因为它是非常高级的 DOM 脚本。

页码

分页意味着你将一大堆数据分成几页。这通常在后端完成,但是您可以使用 JavaScript 来更快地检查一长串元素。

分页的一个演示是 examplePagination.html,它出现在 MacOS 上的 Firefox 19.0.2 中,如图图 7-5 所示。

9781430250920_Fig07-05.jpg

图 7-5 。对大量数据行进行分页

要操作的内容由同一个 HTML 表的一组行组成,该表具有分页的类。

examplePagination.html(节选)

<table class="paginated">
<thead>
  <tr>
    <th scope="col">ID</th>
    <th scope="col">Artist</th>
    <th scope="col">Album</th>
    <th scope="col">Comment</th>
  </tr>
</thead>
<tbody>
  <tr>
    <th>1</th>
    <td>Depeche Mode</td>
    <td>Playing the Angel</td>
    <td>They are back and finally up to speed again</td>
  </tr>
  <tr>
    <th>2</th>
    <td>Monty Python</td>
    <td>The final Rip-Off</td>
    <td>Double CD with all the songs</td>
  </tr>
  [... and so on ...]
</tbody>
</table>

您在幻灯片演示示例的最后一章中使用了分页,尽管该示例一次显示一个项目。包含几个项目的分页逻辑要复杂得多,但是这个示例应该让您了解可以使用的技巧:

  • 你通过 CSS 隐藏所有的表格行。
  • 您可以定义每页显示多少行。
  • 您显示第一行并生成分页菜单。
  • 这个菜单有一个“上一个”链接和一个“下一个”链接,它有一个计数器告诉用户要显示哪一部分数据以及总共有多少项。
  • 如果当前切片是第一个,则“前一个”链接应该是不活动的;如果是最后一个,则“下一个”链接应该处于非活动状态。
  • “下一个”链接按定义的量增加切片的起始值,“上一个”链接减少起始值。

在这个示例中,您将使用几个属性和五个方法。对这些属性进行注释是一个好主意,这样将来的维护者就可以更容易地根据他们的需要对它们进行修改。

分页。js(骨架)

pn = {
  // CSS classes
  paginationClass : 'paginated',
  dynamicClass : 'dynamic',
  showClass : 'show',
  paginationNavClass : 'paginatedNav',
  // Pagination counter properties
  // Number of elements shown on one page
  Increase : 5,
  // Counter: _x_ will become the current start position
  //          _y_ the current end position and
  //          _z_ the number of all data rows
  Counter : ' _x_ to _y_ of _z_  ',
  // "previous" and "next" links, only text is allowed
  nextLabel : 'next',
  previousLabel : 'previous',

使用一种方法初始化脚本,一种方法生成所需的额外链接和元素,一种方法浏览“页面”(即隐藏当前结果集并显示下一个),一种方法显示当前页面,另一种方法改变分页菜单。

  init : function(){},
  createPaginationNav : function(table){},
  navigate : function(e){},
  showSection : function(table, start){},
  changePaginationNav : function(table, start){}
}
DOMhelp.addEvent(window, 'load', pn.init, false);

喝杯咖啡,吃点饼干,因为这是摆在你面前的一个剧本。不过不要担心——大部分都是简单的逻辑。

init()方法检查是否支持 DOM,并开始遍历文档中的所有表格元素。它测试表是否有正确的类(在 pn.paginationClass 属性中定义)以及它的行数是否多于您希望在每个“页面”上显示的行数(在 pn.increase 属性中定义)。如果其中一个不是这种情况,它将跳过该方法的其余部分——实际上不添加任何菜单。

pagination.js(节选)

init : function() {
  var tablebody;
  if(!document.getElementById || !document.createTextNode){
    return;
  }
  var ts = document.getElementsByTagName('table');
  for(var i = 0;i < ts.length; i++){
    if(!DOMhelp.cssjs('check', ts[i], pn.paginationClass)){
      continue;
    }
    if(ts[i].getElementsByTagName('tr').length < pn.increase+1){
      continue;
    }

因为您想要隐藏的数据行不包括标题行,而只包括那些包含在表体中的数据行,所以您需要告诉其他方法这一点。

最简单的方法是在表的属性中只存储相关的行。获取表中的第一个 TBODY,并将它的所有行存储在 datarows 属性中。您还将所有行数存储在 datarowsize 中,并将当前属性初始化为 null。

这个属性将存储你想要显示的页面的开始。通过将行和行数存储为属性,其他方法可以更容易地从表中检索信息,而不必再次从 DOM 中读取这些信息。

pagination.js(节选)

tablebody = ts[i].getElementsByTagName('tbody')[0];
ts[i].datarows = tablebody.getElementsByTagName('tr');
ts[i].datarowsize = ts[i].datarows.length;
ts[i].current = null;

将动态类应用于表格,从而隐藏所有表格行。调用 createPaginationNav()方法,将对当前表的引用作为参数来添加“上一个”和“下一个”链接,调用 showSection(),将表引用和 0 作为参数来显示第一个结果集。

pagination.js(节选)

    DOMhelp.cssjs('add', ts[i], pn.dynamicClass);
    pn.createPaginationNav(ts[i]);
    pn.showSection(ts[i], 0);
  }
},

createPaginationNav()方法不包含任何意外;它所做的只是创建链接和计数器,并添加指向 navigate()方法的事件处理程序。首先创建一个新的段落元素,并向其中添加 pagination menu 类。

pagination.js(节选)

createPaginationNav : function(table){
  var navBefore, navAfter;
  navBefore = document.createElement('p');
  DOMhelp.cssjs('add', navBefore, pn.paginationMenuClass);

向段落添加一个新链接,将 previousLabel 属性值作为文本内容,并添加一个新的 SPAN 元素,该元素将显示“上一个”和“下一个”链接之间的当前结果集的编号。您将计数器预设为 1 作为初始值,pn.increase 中定义的每页上显示的元素数作为终值,所有数据行数作为总数。添加到新段落的最后一个元素是“下一个”链接。您可以通过 parentNode 和 insertBefore()在表格前添加新段落。

pagination.js(节选)

navBefore.appendChild(DOMhelp.createLink('#', pn.previousLabel));
navBefore.appendChild(document.createElement('span'));
counter=pn.counter.replace('_x_', 1);
counter=counter.replace('_y_', pn.increase);
counter=counter.replace('_z_', table.datarowsize-1);
navBefore.getElementsByTagName('span')[0].innerHTML = counter;
navBefore.appendChild(DOMhelp.createLink('#', pn.nextLabel));
table.parentNode.insertBefore(navBefore, table);

在桌子下面显示同样的菜单会很好。不需要再次重新创建所有这些元素,只需通过 parentNode、insertBefore()和 nextSibling 克隆段落并将其插入到表格之后即可。然后将每个段落的“上一个”和“下一个”链接存储为它们自己的表格属性,以便于在其他方法中更改它们。

pagination.js(节选)

navAfter = navBefore.cloneNode(true);

table.parentNode.insertBefore(navAfter, table.nextSibling);
table.topPrev = navBefore.getElementsByTagName('a')[0];
table.topNext = navBefore.getElementsByTagName('a')[1];
table.bottomPrev = navAfter.getElementsByTagName('a')[0];
table.bottomNext = navAfter.getElementsByTagName('a')[1];

您不能更早地应用事件处理程序,因为 cloneNode()不克隆任何处理程序。现在,您可以将所有处理程序和旧版本 Safari 的修复程序应用到每个链接。此方法的最后一个变化是将计数器存储在属性中,以便其他方法更容易更新它们。

pagination.js(节选)

    DOMhelp.addEvent(table.topPrev, 'click', pn.navigate, false);
    DOMhelp.addEvent(table.bottomPrev, 'click', pn.navigate, false);
    DOMhelp.addEvent(table.topNext, 'click', pn.navigate, false);
    DOMhelp.addEvent(table.bottomNext, 'click', pn.navigate, false);
    table.bottomNext.onclick = DOMhelp.safariClickFix;
    table.topPrev.onclick = DOMhelp.safariClickFix;
    table.bottomPrev.onclick = DOMhelp.safariClickFix;
    table.topNext.onclick = DOMhelp.safariClickFix;
    table.topCounter = navBefore.getElementsByTagName('span')[0];
    table.bottomCounter = navAfter.getElementsByTagName('span')[0];
},

事件监听器方法 navigate()需要检查哪个链接调用了它。第一步是通过 getTarget()检索事件目标,并通过将其节点名称与 a 进行比较来确保它是一个链接(记住,Safari 喜欢将链接内的文本节点作为事件目标发送。)

pagination.js(节选)

navigate : function(e){
  var start, table;
  var t = DOMhelp.getTarget(e);
  while(t.nodeName.toLowerCase() != 'a'){
    t = t.parentNode;
  }

然后,它需要通过测试链接是否具有 href 属性来检查链接是否是活动的。(稍后,您将通过删除 href 属性来关闭“下一个”或“上一个”链接。)如果没有,那就不应该做什么。下一个任务是从激活的链接中找到表。因为在表的上方和下方都有导航,所以需要检查上一个或下一个兄弟节点是否有 table 的节点名,并相应地定义变量 table。

pagination.js(节选)

if(t.getAttribute('href') == null || t.getAttribute('href') == ' '){ return; }
if(t.parentNode.previousSibling && t.parentNode.previousSibling.nodeName.toLowerCase() == 'table') {
  table = t.parentNode.previousSibling;
} else {
  table = t.parentNode.nextSibling;
}

然后确定激活的链接是“下一个”链接还是“上一个”链接,并将 start 定义为表的当前属性加上或减去定义的增量。您调用 showSection(),将检索到的表和起始值作为参数。

pagination.js(节选)

  if(t == table.topNext || t == table.bottomNext){
    start = table.current + pn.increase;
  } else if (t == table.topPrev || t == table.bottomPrev){
    start = table.current - pn.increase;
  }
  pn.showSection(table, start);
},

showSection()方法调用 changePaginationNav()方法来更新链接和计数器,并测试表上是否已经有一个当前参数。如果有,这意味着存在需要删除的数据行。您可以通过遍历存储在 data rows 属性中的数据行部分并删除 showClass()中定义的 CSS 类来消除它们。

pagination.js(节选)

showSection : function(table, start){
  var i;
  pn.changePaginationNav(table, start);
  if(table.current != null){
    for(i=table.current; i < table.current+pn.increase; i++){
      if(table.datarows[i]) {
        DOMhelp.cssjs('remove', table.datarows[i], pn.showClass);
      }
    }
  }

然后,从开始到开始加上预定义的增量进行循环,并添加 CSS 类以在表中显示这些行。注意,您需要测试这些行是否存在;否则,您可能会尝试找到最后一页上没有的行。(想象一个 22 个元素的列表;点击 16–20 页上的“下一页”链接将尝试显示元素 21 至 25。)为了确保下次调用该方法时显示正确的切片,剩下的工作就是将当前属性定义为起始值。

pagination.js(节选)

  for(i = start; i < start + pn.increase; i++){
    if(table.datarows[i]) {
      DOMhelp.cssjs('add', table.datarows[i], pn.showClass);
    }
  }
  table.current = start;
},

如前所述,changePaginationNav()方法使第一页上的“上一页”链接和最后一页上的“下一页”链接不活动。让链接出现但不可点击的技巧是移除 href 属性。

在第一页上,起始值减去预定义的增量会得到一个负数,这很容易测试。当数字大于 0 时,再次添加 href 属性。

pagination.js(节选)

changePaginationNav : function(table, start){
  if(start - pn.increase < 0) {
    table.bottomPrev.removeAttribute('href');
    table.topPrev.removeAttribute('href');
  } else {
    table.bottomPrev.setAttribute('href', '#');
    table.topPrev.setAttribute('href', '#');
  }

如果开始加增加的行数大于,就需要去掉"下一个"链接;否则,您需要激活它。

pagination.js(节选)

if(start + pn.increase > table.rowsize - 2) {
  table.bottomNext.removeAttribute('href');
  table.topNext.removeAttribute('href');
} else {
  table.bottomNext.setAttribute('href', '#');
  table.topNext.setAttribute('href', '#');
}

用适当的值更新计数器。(请记住,start 需要加 1,以便人类更容易理解,并且您需要测试最后一个值不大于现有行数)。至此,您已经从一个普通的数据表创建了一个分页的接口。

pagination.js(节选)

    var counter = pn.counter.replace('_x_', start+1);
    var last = start + pn.increase;
    if(last > table.datarowsize){ last = table.datarowsize; }
    counter = counter.replace('_y_', last)
    counter = counter.replace('_z_', table.datarowsize)
    table.topCounter.innerHTML = counter;
    table.bottomCounter.innerHTML = counter;
  }
}
DOMhelp.addEvent(window, 'load', pn.init, false);

分页的逻辑保持不变,即使您决定显示和隐藏列表项或其他 HTML 结构。您可以通过在“上一个”和“下一个”链接之间显示编号的步骤而不是计数器来使它变得更加复杂,但是我让您自己去尝试一下。

JavaScript 导航概述

用 JavaScript 为站点导航提供动力是对您技能的一种非常诱人的使用,因为它“就在那里”,并且对于拥有花哨界面的客户来说仍然非常令人惊叹。要记住的主要一点是,你应该时不时地关掉 JavaScript,看看你的界面是否还能工作。这同样适用于不使用鼠标,而是尝试键盘。

您可以使用 JavaScript 使大量数据(如深度嵌套的导航菜单)更容易掌握,并以小块的形式呈现给用户。然而,不要忘记,有些用户将获得所有的导航,而不需要你的脚本将它分割成更小的服务。让使用整个站点地图作为数据源的菜单界面成为可选的而不是给定的,这可能是一个好主意。我们将在下一章看看如何做到这一点。

关于 JavaScript 导航,您需要记住的是:

  • 在用户没有单击或激活界面元素的情况下,不要将用户发送到另一个位置或发送表单数据。与其说它有帮助,不如说它令人困惑,甚至可能被认为是一种安全威胁。(如果你能做到这一点,其他任何人也可以将用户发送到一个站点。)
  • 隐藏数据不会让它消失。尽管你可以用一个漂亮的界面让大量数据变得容易消化,但一些用户仍然会在一次服务中获得全部数据,所有用户——包括那些慢速连接的用户——都必须下载所有数据。
  • 利用现有的网络导航模式比发明新的模式要安全得多。例如,使用链接和锚点,很容易就可以将页面内导航变成选项卡式界面。通过 JavaScript 创建所有必要的选项卡会更加麻烦。

表单和 JavaScript

在接下来的页面中,您将学习如何访问、阅读和更改表单及其元素。我不会在这里讨论验证表单的细节,因为我在第九章中专门讨论了数据验证的主题。

然而,我将触及基本的表单可用性,以及一些不好的做法和为什么应该避免它们。首先,让我们看看本章和本书后面的一些例子中使用的形式:

exampleForm.html(节选)

<form method="post" action="send.php">
<fieldset>
  <legend>About You</legend>
  <p><label for="Name">Your Name</label></p>
  <p><input type="text" id="Name" name="Name" /></p>
  <p><label for="Surname">Your Surname</label></p>
  <p><input type="text" id="Surname" name="Surname" /></p>
  <p><label for="email">Your email</label></p>
  <p><input type="email" id="email" value="you@example.com" name="email"></p>
</fieldset>
<fieldset>
  <legend>Your message</legend>
  <p><label for="subject">Subject</label>
  <select id="subject" name="subject">
    <option value="generalEnquiry" selected="selected">General question</option>
    <option value="Webdesign">Webdesign</option>
    <option value="Hosting">Hosting</option>
    <option value="Training">Training</option>
    <option value="Partnership">Partnership</option>
    <option value="other">Other</option>
  </select></p>
  <p><label for="otherSubject">specify other subject</label>
  <input type="text" id="otherSubject" name="otherSubject" /></p>
  <p><label for="Message">Your Message</label></p>
  <p><textarea id="Message" name="Message" cols="20" rows="5"></textarea></p>
</fieldset>
<fieldset>
  <legend>Email options</legend>
  <p><input type="checkbox" name="copyMeIn" id="copyMeIn">
  <label for="copyMeIn">Send me a copy of this email to the above address</label></p>
  <p><input type="checkbox" name="newsletter" value="yes" id="newsletter">
  <label for="newsletter">Sign me up for the newsletter</label></p>
  <p>Newsletter format:
  <input type="radio" name="newsletterFormat" id="newsHtml" value="html" checked="checked">
  <label for="newsHTML">HTML</label>
  <input type="radio" name="newsletterFormat" id="newsPlain" value="plain">
  <label for="newsPlain">Text</label></p>
  <p class="submit"><input type="submit" value="Send Form"></p>
</fieldset>
</form>

image 注意正如你所看到的,这是一个 HTML 4 STRICT 文档的有效形式,如果你想这样做,所有的元素都被关闭以符合 XHTML。还要注意,在符合 XML 的 HTML 中,您需要分别编写单个属性,如 selected 和 checked,分别为 selected="selected "和 checked="checked "。

该表单具有用于将元素分组为逻辑单元的字段集,以及用于将解释文本与特定表单元素相连接的标签。这对表单的可访问性很有帮助,因为它提供了逻辑组织并避免了歧义。

JavaScript 表单基础

在 JavaScript 中达到和改变表单可以通过几种方式实现。和往常一样,DOM 脚本可以通过 getElementsByTagName()和 getElementById()访问表单及其元素,但也有一个名为 forms 的对象包含当前文档中的所有表单。

该对象允许您以三种方式访问文档中的表单:

  • 在索引为整数的数组中,例如,第三个表单为 document.forms[2]。
  • 通过在 name 属性中定义为对象的名称,例如 document.forms.myForm。
  • 同样的表单数组也可以作为关联数组或散列来访问,例如 document.forms['myForm']。当名称包含特殊字符或空格,并且您不能将其标记为对象时,这是必要的。

表单属性

forms 对象本身只有一个属性 length,它存储文档中表单的数量。

但是,每种形式都有更多可以使用的属性,所有这些属性都可以读取和更改:

  • 动作:提交表单时表单数据发送到的脚本
  • 编码:表单元素的 enctype 属性中定义的表单编码
  • 方法:表单的提交方法 POST 或 GET
  • name:在 name 属性中定义的表单名称(不是在 id!)
  • target:表单数据应该发送到的目标(如果使用框架或多个窗口,这一点很重要)

表单方法

表单对象只有两种方法:

  • reset():将表单重置为初始状态,这意味着用户所做的所有输入和选择都将被撤消,表单将显示在单个元素的 value、selected 或 checked 属性中定义的初始值。请注意不同之处——reset()并不清除表单,而是将其恢复到初始状态。这与用户激活表单中的重置按钮时获得的效果相同。
  • submit():提交表单。

这两种方法都模拟了浏览器的功能——即激活重置或提交按钮——并且您应该确保不要使用它们来剥夺用户必要的交互性。当用户单击提交按钮或按下键盘上的 Enter 键时,表单会被提交,这是一个很好的理由——这是最容易访问的方式,如果您劫持了这个功能并在用户与其他元素交互时提交表单,您可能会迫使他们过早地提交表单。

表单元素

forms 集合中的每个表单都有一个名为 elements,的属性,它实质上是这个表单中所有表单元素的数组(与所有 HTML 元素相反)。您可以像最初访问表单一样访问元素,通过索引号、对象名或关联数组:

  • var elm = document.forms[0]。元素[2];
  • var elm = document . forms . my form . elements . my element;
  • var elm = document . forms . my form . elements[' my element '];

正如您在前面的例子中看到的,您可以混合和匹配符号。您也可以使用变量来代替括号中的索引号或字符串。

elements 集合本身有一个名为 length 的只读属性。例如,您可以使用该属性遍历表单中的所有元素,并读出它们的类型:

var myForm = document.forms[0];
var formElements = myForm.elements;
var all = formElements.length;
for(var i = 0; i < all; i++) {
  alert(formElements[i].type);
}

集合中的每个元素都有几个属性;支持哪些类型取决于元素的类型。我现在将列出所有属性,并在括号中列出支持该属性的元素。我们将在本章后面详细讨论不同的元素:

  • checked:布尔值,表示元素是否被选中(按钮、复选框、单选按钮)
  • defaultChecked:布尔值,表示元素最初是否被选中(复选框、单选按钮)
  • value:value 属性中定义的元素值(除选择框外的所有元素)
  • defaultValue:元素(文本框、文本区域)的初始值
  • 表单:元素所在的表单(只读-所有元素)
  • 名称:元素的名称(所有元素)
  • 类型:元素的类型(只读-所有元素)

一种特殊的元素类型是选择框,它自带一个集合,一个名为 options 的属性——稍后将详细介绍。每个元素都有一系列方法,这些方法也取决于元素的类型。这些方法都不需要任何参数。

  • blur():将用户代理的焦点从元素(所有元素)上移开
  • focus():将用户代理的焦点放在元素上(所有元素)
  • click():模拟用户单击元素(按钮、复选框、文件上传字段、重置和提交按钮)
  • select():选择并突出显示元素的文本内容(密码字段、文本字段和文本区域)

image 注意注意 click()乍一看似乎有点奇怪,但是如果你在 web 应用上工作,并且你的开发环境的中间层处理表单的提交过程,比如 Java Spring 和。NET do。不过,这不是 JS 初学者的环境,所以它超出了本书的范围。

元素集合中不包含的 HTML 属性

除了使用 elements 集合的属性之外,一旦通过 forms 和 elements 集合访问到相关元素,还可以读取和设置(当然,浏览器设置允许)该元素的属性。例如,您可以通过更改文本字段的 cols 和 rows 属性来更改其大小:

var myTextBox = document.forms[0].elements[2];
if (myTextBox.type == 'textarea'){
  myTextBox.rows = 10;
  myTextBox.cols = 30;
}

image 提示在试图设置元素的属性之前,最好检查一下你正在操作的元素的类型,以防它们对这个元素不可用。例如,SELECT 元素没有 cols 或 rows 属性。

全局支持的属性

所有表单元素最初都支持类型、名称、表单和值属性。最近的一个变化是,文件上传字段不再支持设置值,因为当受感染计算机上的用户上传某些内容时,这将允许恶意脚本程序注入她自己的文件以上传到您的服务器。

当您通过 DOM 方法直接访问元素时,使用 form 可以非常方便地到达父表单。例如,通过 DOM: 访问示例表单中的电子邮件字段

var mail = document.getElementById('email');

您可以通过 mail.form 或 mail . parent node . parent node . parent node . parent node 来访问表单以更改其属性或提交表单。

exampleForm.html(节选)

<form method="post" action="send.php">
<fieldset>
  [... code snipped ...]
  <p><input type="text" id="email" value="you@example.com" name="email"></p>
</fieldset>
  [... code snipped ...]
</form>

根据元素在表单中的嵌套深度,使用 form 可以在计算节点数时省去很多麻烦,并且实际上使脚本更容易维护,因为您独立于 HTML。如果您想专门使用节点遍历,也可以使用递归循环来检查父节点的 nodeName,以实现与 HTML 标记相同的独立性:

var mail = document.getElementById('email');
parentForm = mail.parentNode;
while(parentForm.nodeName.toLowerCase() != 'form') {
  parentForm = parentForm.parentNode;
}

使用模糊()和聚焦()

您可以使用 blur()将用户代理的焦点从元素上移开,或者使用 focus()来设置它。这样做的危险在于,blur()并不接受它应该将焦点设置到的任何目标,这意味着用户代理可能会关注下一个元素、浏览器的地址栏或任何他们喜欢的东西。对于使用鼠标的视力正常的用户来说,这不是什么大问题;但是,依赖辅助技术的盲人用户或键盘用户将很难再次找到文档。

当您浏览一些网站的代码时,可能会遇到类似这样的情况:

<a href="#" onclick="dothings();" onfocus="this.blur()">Home</a>

开发人员过去这样做是为了阻止浏览器在当前链接周围显示蓝框或虚线边框。这是一个非常糟糕的想法,因为键盘用户不知道当按下 Enter 键时她当前能够到达哪个元素。

使用 focus()有一些合理的理由;然而,在大多数情况下,改变表单输入的自动顺序并不是一个好主意。不是每个用户都可以看到表单,甚至可以看到表单的用户也可能不会看表单。

特别是对于需要输入大量不同数据的较长表单,您会发现人们不看屏幕,而是在阅读打印输出或信用卡、护照等数据时触摸输入。检查中间的表单并意识到您没有填写正确的字段,或者您仍然停留在先前弹出的错误消息中,这是非常令人沮丧的。

文本字段、文本区域、隐藏字段和密码字段

文本字段、文本区域、隐藏字段和密码字段可能是您必须处理的最常见的字段,因为它们是用户输入文本内容的字段。

除了支持全局表单元素属性,它们还支持元素属性 value 和 defaultValue。不同之处在于,如果用户更改元素的内容,它会更改 value 属性,但不会更改 defaultValue。这也意味着当您更改字段的值时,更改是可见的,但当您更改 defaultValue 时,更改是不可见的。如果您想要更改元素的默认值并使其可见,您需要在之后立即调用 reset()方法。在示例文档中,电子邮件字段有一个默认值:

<p><input type="text" id="email" value="you@example.com"  name="email"></p>

您可以像这样读取值和默认值:

var mail= document.getElementById('email');
alert(mail.defaultValue);
alert(mail.value);

当用户没有在字段上做任何改变时,两个值都是 you@example.com。但是,如果用户在字段中输入 me@otherexample.com 的,这两个值就会不同。

很难找到一个 defaultValue 的例子,它不需要您用 JavaScript 做一些应该是后端工作的事情。一个例子是测试当前域是否是德国的,并将默认的电子邮件字段更改为德国的电子邮件地址。文档加载后,应执行以下代码:

var mail = document.getElementById('email');
if(window.location.host.indexOf('.de') != -1) {
  mail.defaultValue = 'email@adresse.de';
  mail.form.reset();
}

请注意,您需要调用 reset()来使更改可见。你可以看到 exampleFormGermanPreset.html 正在发生的变化。(我在那里作弊,通过排除主机测试使其可见。)

image 注意对于 TEXTAREA 元素,您可以像对任何其他表单文本元素一样读写值和 defaultValue。但是,HTML 标记没有 value 属性—初始值和更改后的值是包含在开始和结束标记之间的文本。

文本元素允许使用名为 select()的方法,该方法突出显示元素中的所有文本,以便于复制和粘贴文本示例。这通常被视为网络杂志或在线文档系统的一个特征。

检查框

复选框是提供明确的“是”或“否”选择的好方法。它们很容易在服务器端读出。(如果有复选框,表单发送带有复选框值的名称,如果没有复选框,表单使用“on ”,当用户没有选中复选框时,表单根本不发送名称。)例如,它比单选按钮组或带有“是”和“否”选项的选择框更容易使用。

除了具有前面描述的全局元素属性之外,复选框还具有 checked 和 defaultChecked 属性,这两个属性都是布尔值,指示是否选择了该选项。您可以读取和写入这两个属性,但是您需要重置窗体以使对 defaultChecked 的更改可见。

JavaScript 与复选框相关的一个常见用途是为用户提供选择所有复选框或撤销在许多复选框中所做选择的机会——一个例子是它们在基于 web 的电子邮件系统中的使用,如图 7-6 所示。这些函数的逻辑非常简单:遍历所有元素,测试每个元素的类型,并相应地更改检查的属性。你可以在 exampleFormCheckboxes.html 看到这样的演示。

9781430250920_Fig07-06.jpg

图 7-6 。通过 JavaScript 批量更改复选框

示例中有三个按钮,当用户单击它们时,它们调用同一个函数—change box()。每个按钮为函数的唯一参数提供不同的数值—1 表示全选,1 表示反转,0 表示不选。

exampleFormCheckboxes.html(节选)

<input type="button" onclick="changeBoxes(1)" value="select all">
<input type="button" onclick="changeBoxes(-1)" value="invert selection">
<input type="button" onclick="changeBoxes(0)" value="select none">

这允许你保持简单的改变复选框的功能。只需遍历页面中第一个表单中的所有元素。如果元素类型不是 checkbox,则继续循环而不执行其余部分。

如果元素是复选框,则确定 action 是否小于 0,并通过在 checked 为 true 时将 checked 更改为 false 来反转复选框状态,反之亦然。如果 action 等于或大于 0,只需将 checked 属性设置为 action 的值。

exampleFormCheckboxes.js(节选)

function changeBoxes(action) {
  var f = document.forms[0];
  var elms = f.elements;
  for(var i = 0; i < elms.length; i++) {
    if(elms[i].type != 'checkbox'){ continue; }
    if(action < 0){
      elms[i].checked = elms[i].checked ? 0 : 1;
    } else {
      elms[i].checked = action;
    }
  }
}

如果这令人困惑,请记住 checked 属性是一个布尔值。这意味着当它为 true 或 1 时,复选框被选中,当它为 false 或 0 时,复选框不被选中。如果只使用 true 或 false 关键字,则必须在 else 条件中添加另一个 case(在本例中通过三元符号):

function changeBoxes(action) {
  var f = document.forms[0];
  var elms = f.elements;
  for(var i = 0; i < elms.length; i++){
    if(elms[i].type != 'checkbox'){ continue; }
    if(action < 0){
      elms[i].checked = elms[i].checked ? false : true;
    } else {
      elms[i].checked = action == 1 ? true : false;
    }
  }
}

使用三元运算符,您甚至可以将脚本的整个复选框逻辑部分缩减为一行:

function changeBoxes(action) {
  var f = document.forms[0];
  var elms = f.elements;
  for(var i = 0; i < elms.length; i++){
    if(elms[i].type != 'checkbox'){ continue; }
    elms[i].checked = action < 0 ? (elms[i].checked ? 0 : 1) :action;
  }
}

因为许多复杂的表单代码不一定是由面向客户端的开发人员创建的,所以您很有可能会遇到这种类型的构造,这也是我在这里向您展示它的原因。

单选按钮

如果你想知道的话,单选按钮之所以这么叫是因为它们看起来像老式收音机上的转盘。它们就像复选框一样,不同之处在于它们属于一个同名的组,用户只能选择一个。对于鼠标和键盘用户来说,单选按钮非常容易使用,如果您在使用选择框时遇到问题,它们是短选择框的很好的替代品。

它们与复选框具有相同的布尔 Checked 和 defaultChecked 属性,但是当您设置一个复选框时,它们会自动将其他选项的 checked 属性设置为 false。同样,您可以读写 checked 和 defaultChecked,并且您需要重置表单以使对 defaultChecked 的更改直观地出现。因为示例 HTML 只有两个选项的单选按钮组,所以让我们使用一个不同的示例:

exampleFormRadioGroup.html(节选)

<form method="post" action="send.php">
  <fieldset>
    <legend>Step 1 of 3 - Your favourite Character </legend>
     <p>
       <input type="radio" name="character" id="charC" value="Calvin" checked="checked">
       <label for="charC">Calvin</label>
     </p>
     <p>
       <input type="radio" name="character" id="charH" value="Hobbes">
       <label for="charH">Hobbes</label>
   </p>
   <p>
      <input type="radio" name="character" id="charSd" value="Susie Derkins">
      <label for="charSd">Susie Derkins</label>
   </p>
   <p>
     <input type="radio" name="character" id="charS" value="Spaceman Spiff">
     <label for="charS">Spaceman Spiff</label>
   </p>
   <p>
     <input type="radio" name="character" id="charSm" value="Stupendous Man">
     <label for="charSm">Stupendous Man</label>
   </p>
  </fieldset>
  <p class="submit"><input type="submit" value="Next Step"></p>
</form>

image 注意这也是展示姓名和 id 区别的好机会。尽管一组单选按钮都有相同的名称(在本例中是 character),但是它们每个都必须有一个惟一的 id,这样标签才能与它们连接起来。标签不仅对于屏幕阅读器这样的辅助技术来说很方便,而且它们还使表单更容易使用,因为用户可以单击复选框旁边的名称来选择它们。

演示 HTML 包括一些显示 JavaScript 输出的按钮;您可以通过在浏览器中打开它来测试它。该脚本展示了如何处理单选按钮:

formRadioGroup.js

function setChoice(n) {
  var f = document.forms[0];
  f.character[n].checked = true;
}
function getChoice() {
  var f = document.forms[0];
  var choices = f.elements.character;
  for(var i = 0; i < choices.length; i++){
    if(choices[i].checked){ break; }
  }
  alert('Favourite Character is: ' + choices[i].value);
}

您可以将单选按钮组作为具有共享名称(在本例中为 character)的数组来访问。设置单选按钮组的选项非常简单:setChoice()函数将一个数字作为参数(n),读取第一个表单(forms[0]),并将第 n 个字符项的 checked 属性设置为 true。

formRadioGroup.js(节选)

function setChoice(n) {
  var f = document.forms[0];
  f.character[n].checked = true;
}

如果你点击例子中的 Set Choice To Hobbes 按钮,你会看到高亮显示的单选按钮发生变化,如图图 7-7 所示。

9781430250920_Fig07-07.jpg

图 7-7 。更改单选按钮组中的选定选项

读取当前选中的选项也很简单:选择第一种形式,将字符列表存储在一个名为 choices 的新变量中,然后遍历它。然后测试数组中每个元素的 checked 属性,当找到一个返回 true 的元素时,中断循环。这是当前选定的单选按钮:在任何同名的单选按钮组中只能选择一个。

formRadioGroup.js(续)

function getChoice() {
  var f = document.forms[0];
  var choices = f.elements.character;
  for(var i = 0; i < choices.length; i++) {
    if(choices[i].checked){ break; }
  }
  alert('Favourite Character is: ' + choices[i].value);
}

小跟班

HTML 中有三种按钮:两种不需要脚本就能工作,一种包含在规范中,只与脚本一起工作。

作者可以创建三种类型的按钮:

提交按钮:当被激活时,提交按钮提交一个表单。一个表单可以包含多个提交按钮。

复位按钮:当被激活时,复位按钮将所有控件复位到初始值。

按钮 : 按钮没有默认行为。每个按钮可能有与元素的事件属性相关联的客户端脚本。当事件发生时(例如,用户按下按钮、释放按钮等)。),关联的脚本被触发。

www.w3.org/TR/REC-html40/interact/forms.html#buttons

这个使得“按钮”——既可以是输入类型的按钮,也可以是按钮元素——成为纯 JavaScript 功能的完美触发元素。

另一方面,重置和提交按钮是表单中非常重要的部分,除非你有充分的理由,否则不应该被篡改。一个重复出现的请求是在提交表单时更改提交按钮的值或状态,以防止不耐烦的用户两次单击按钮。您可以通过点击处理程序来实现这一点;但是,更好的选择是在表单上使用提交处理程序,因为当通过 Enter 键提交表单时,这也会触发更改。图 7-8 显示了这可能是什么样子。

9781430250920_Fig07-08.jpg

图 7-8 。发送表单时更改提交按钮的样式和文本内容

要实现这一功能,您只需为窗口分配一个事件处理程序,在提交表单时调用 init()函数和另一个调用 change()函数。

这个函数遍历所有的表单元素(通过 getTarget()检索表单后),并检查元素是图像还是提交按钮。如果是这种情况,它通过 disabled 属性禁用按钮,并将按钮值更改为 Please wait:

examplechangesmitbutton . js

submitChange = {
  init : function() {
    DOMhelp.addEvent(document.forms[0], 'submit', submitChange.change,false);
  },
  change : function(e){
    var t = DOMhelp.getTarget(e);
    for(var i = 0; i < t.elements.length; i++){
      if(!/submit|image/.test(t.elements[i].type)) { continue; }
        t.elements[i].disabled = true;
        t.elements[i].value = 'Please wait... ';
     }
  }
}
DOMhelp.addEvent(window, 'load', submitChange.init ,false);

通过定义的图像按钮的行为类似于提交按钮,唯一的区别是它没有向后端提交其名称,而是提交两组名称-值对,由原始名称和后面的。x 和。y 和用户单击的坐标作为值。这样,您可以根据按钮被单击的位置执行不同的操作。这些信息不能通过 JavaScript 读取,只能在后端读取。

从 JavaScript 的角度来看,除了可以为图像输入提供翻转状态之外,您对图像输入没有什么可做的了。

选择框

选择框可能是最复杂和最通用的表单元素。设计师喜欢它们,因为它们可以使用选择框在一个小屏幕空间中存储大量选项供用户选择。

每个选择框都有一个名为 options 的列表对象,该对象有几个属性:

  • 长度:该选择框中所有选项的数量。
  • selected:如果用户选择了该选项,则为布尔值。
  • selectedIndex:所选元素的索引号。如果没有选择任何元素,则返回–1(这实际上是 SELECT 元素的一个属性,但适合在这里提及)。
  • 文本:选项的文本内容。
  • 值:选项的值。

image 注意注意文本和值是包含在选择框中的每个选项的属性;您不能通过读取选择框对象本身的 value 属性来读出所选的值,因为根本没有这样的东西。

有两种选择框:单选选择框允许一个独占选择,多选选择框允许用户通过按住 Ctrl 并突出显示所需选项来选择多个选项。

image 注意对于使用辅助技术或键盘的用户来说,多选选择框是一场噩梦,这就是为什么你可能要考虑使用复选框列表的原因。这也将使在服务器端读出选择变得更加容易。

读出单项选择框是相当容易的。例如,以演示表单中的选择框为例:

exampleSelectChoice.html(节选)

<p>
  <label for="subject">Subject</label>
  <select id="subject" name="subject">
    <option value="generalEnquiry" selected="selected">General question</option>
    <option value="Webdesign">Webdesign</option>
    <option value="Hosting">Hosting</option>
    <option value="Training">Training</option>
    <option value="Partnership">Partnership</option>
    <option value="other">Other</option>
  </select>
</p>

到达选择框的最快方法是使用元素的名称而不是索引。原因是选择框的元素类型可以是单选或多选,这取决于是否设置了 multiple 属性。一旦找到正确的对象,就可以使用它的 selectedIndex 属性来读取所选的选项,并通过将 selectedIndex 用作列表计数器来显示选项的值或文本:

考试选择题. js (excerpt)

function checkSingle() {
  var f = document.forms[0];
  var selectBox = f.elements['subject'];
  var choice = selectBox.selectedIndex;
  alert('You chose ' + selectBox.options[choice].text)
}

对于多选选择框,这还不够,因为用户可能选择了多个选项(selectedIndex 将只返回第一个选项)。

不使用 selectedIndex,您必须遍历所有选项并测试每个选项的 selected 属性:

考试选择题. js (excerpt)

function checkMultiple() {
  var f = document.forms[0];
  var selectBox = f.elements['multisubject'];
  var choices=[];
  for(var i = 0; i < selectBox.options.length; i++) {
    if(selectBox.options[i].selected == 1) {
      choices.push(selectBox.options[i].text);
    }
  }
  alert(choices.join(', '));
}

您可以通过 elements 集合中选择框的名称来访问它,并创建一个名为 choices 的新数组。([]是新数组()的快捷表示法。)遍历选择框的每个选项,并检查其 selected 属性是否为 true。在这种情况下,将选项的文本值作为新的数组项推入 choices。然后使用数组的 join()方法将数组转换为字符串并显示它。

这种读取值的方式也适用于单选选择框;然而,根据可用选项的数量,这可能是多余的。根据元素的类型,通过读出选项,可以将这两种方法放在一个更通用的函数中:

考试选择题. js (excerpt)

function getSelectValue(fieldName) {
  var f = document.forms[0];
  var selectBox = f.elements[fieldName];
  if(selectBox.type == 'select-one') {
    var choice = selectBox.selectedIndex;
    alert('You chose ' + selectBox.options[choice].text);
  } else {
    var choices = [];
    for(var i = 0;i < selectBox.options.length; i++){
      if(selectBox.options[i].selected == 1) {
        choices.push(selectBox.options[i].text);
      }
    }
    choices.join(', ');
    alert(choices);
  }
}

在选择框中添加选项

就表单元素而言,选择框是唯一的,因为您可以使用它们以编程方式添加或删除选项。通过使用选项构造函数并将其包含在选项列表中,可以添加新选项:

extraOption = new Option(value, text, defaultSelected, selected);

例如,如果您想将“DOM scripting”作为一个主题添加到列表中,您可以这样做:

考试选择题. js (excerpt)

function addOption(fieldName) {
  var f = document.forms[0];
  var selectBox = f.elements[fieldName];
  var extraOption = new Option('DOM scripting', 'domscripting', 0, 0);
  selectBox.options[ selectBox.options.length ] = extraOption;
}

删除和替换选择框中的选项

您可以通过将选项设置为空来删除它:

考试选择题. js (excerpt)

function removeOption(fieldName,i) {
  var f = document.forms[0];
  var selectBox = f.elements[fieldName];
  selectBox.options[i] = null;
}

替换选项也一样容易;只需将旧选项设置为新选项:

考试选择题. js (excerpt)

function replaceOption(fieldName, i) {
  var f = document.forms[0];
  var selectBox = f.elements[fieldName];
  var extraOption = new Option('DOM scripting', 'domscripting', 0 ,0);
  selectBox.options[i] = extraOption;
}

在另一个选项之前插入一个选项会有一些问题,因为在重写 options 集合之前需要复制所有的选项。函数 insertBeforeOption()接受两个参数:表单元素的名称和要在前面插入新选项的选项的索引。首先定义两个循环计数器 I 和 j,以及一个空白数组 opts,然后找到选择框并创建新选项。

考试选择题. js (excerpt)

function insertBeforeOption(fieldName, n) {
  var i = 0, j = 0, opts = [],
  var f = document.forms[0];
  var selectBox = f.elements[fieldName];
  var extraOption = new Option('DOM scripting', 'domscripting', 0,0);

然后将选择框中的选项存储在一个名为 old 的变量中,并遍历它们,为每个选项创建一个新选项,并将它们的属性分配给新选项。

示例 SelectChoice.js(续)

var old = selectBox.options;
for(i = 0; i < old.length; i++) {
  opts[i] = new Option(old[i].text, old[i].value, old[i].defaultSelected, old[i].selected);
}

新列表将增加一个元素,这就是为什么在遍历新列表之前增加 length 属性的原因。您测试循环计数器是否与发送给函数的参数相同,如果相同,则插入新选项。

示例 SelectChoice.js(续)

old.length++;
for(i = 0; i < old.length; i++) {
  if(i == n) {
    old[i] = extraOption;

否则,将选项设置为旧选项,并增加 j 计数器变量。注意这里需要第二个计数器,因为在循环过程中不能改变变量 I。因为新的选项列表将增加一项,所以您需要使用 j 来获取存储在 opts 数组中的值。

示例 SelectChoice.js(续)

    } else {
      old[i] = opts[j];
      j++;
    }
  }
}

根据选择框中选项的数量,这可能会成为一个相当慢且要求很高的脚本。通过使用 DOM,您可以用更少的代码更快地达到同样的效果:

考试选择题. js (excerpt)

function insertBeforeOptionDOM(fieldName, i) {
  var selectBox = document.getElementById(fieldName);
  if(!selectBox){ return false; }
  var opt = selectBox.getElementsByTagName('option');
  var extraOption = document.createElement('option');
  extraOption.setAttribute('value', 'domscripting');
  extraOption.appendChild(document.createTextNode('DOM Scripting'));
  selectBox.insertBefore(extraOption, selectBox.options[i]);
}

选择框是 web 应用开发的重要组成部分,传统上是通过来回移动元素来排序两个列表的界面。

交互式表单:隐藏和显示依赖元素

JavaScript 和表单的一个很酷的地方是,你可以让表单比开箱即用时更吸引人,更有活力。让所有东西都相互交互并立即发送一个表单,而不需要用户点击提交按钮或按回车键,这很有诱惑力。这样做的危险不仅在于你牺牲了对除可视代理之外的用户代理的支持,而且用户可能会过早地发送数据。

当要简单地改变界面或表单中显示的选项数量时,使用更改处理程序是相当安全的。让我们以演示表单为例。您可能已经注意到,有些字段有逻辑联系:“other subject”文本字段只有在选择了 other 选项时才有意义,只有当用户选择订阅时事通讯时,选择接收 HTML 或纯文本形式的时事通讯才起作用。

exampleDynamicForm.html(节选)

<form method="post" action="send.php">
  [... code snipped ...]
  <p><label for="subject">Subject</label>
  <select id="subject" name="subject">
    <option value="generalEnquiry" selected="selected">General question</option>
    <option value="Webdesign">Webdesign</option>
    <option value="Hosting">Hosting</option>
    <option value="Training">Training</option>
    <option value="Partnership">Partnership</option>
    <option value="other">Other</option>
  </select></p>
  <p><label for="otherSubject">specify other subject</label>
  <input type="text" id="otherSubject" name="otherSubject"></p>
  [... code snipped ...]
  <p><input type="checkbox" name="newsletter" value="yes" id="newsletter">
  <label for="newsletter">Sign me up for the newsletter</label></p>
  <p>Newsletter format:
  <input type="radio" name="newsletterFormat" id="newsHtml" value="html" checked="checked">
  <label for="newsHTML">HTML</label>
  <input type="radio" name="newsletterFormat" id="newsPlain"  value="plain">
  <label for="newsPlain">Text</label></p>

使用脚本,您可以隐藏这些选项,并使它们仅在用户选择适当的选项时才显示。图 7-9 显示了它在浏览器中的样子。

9781430250920_Fig07-09.jpg

图 7-9 。基于用户选择显示和隐藏表单元素

您可以定义一个类,应用于您想要隐藏的元素和两个动态元素的 id,作为名为 df 的主对象的属性。

dynamic form . js

df = {
  hideClass : 'hide',
  letterOption : 'newsletter',
  subjectOption : 'subject',

init()方法检查 DOM 支持以及必要的元素是否可用。

dynamicForm.js(续)

init : function() {
  if(!document.getElementById || !document.createTextNode){
   return;
  }
  df.news = document.getElementById(df.letterOption);
  df.subject = document.getElementById(df.subjectOption);
  if(!df.subject || !df.news){ return; }

接下来,您需要找到要隐藏的元素。通过使用 DOMhelp 方法 closestSibling(),您可以确保不要试图隐藏换行符,而是隐藏您实际想要到达的元素。将元素存储在主对象的属性中,以便事件处理程序方法可以访问它们。

您可以通过向元素添加 hiding 类来隐藏元素,并向名为 letterChange()的复选框分配 click 事件处理程序,向名为 subject Change()dynamic form . js 的选择框分配 change 处理程序(续)

  df.newsOpt = DOMhelp.closestSibling(df.news.parentNode, 1);
  df.subjectOpt = DOMhelp.closestSibling(df.subject.parentNode, 1);
  DOMhelp.cssjs('add', df.newsOpt, df.hideClass);
  DOMhelp.cssjs('add', df.subjectOpt, df.hideClass);
  DOMhelp.addEvent(df.news, 'click', df.letterChange, false);
  DOMhelp.addEvent(df.subject, 'change', df.subjectChange, false);
},

在测试复选框的 checked 属性之前,通过 letterChange()方法中的 getTarget()检索复选框。如果选中该属性,则移除隐藏类;否则,你添加它。

dynamicForm.js(续)

letterChange : function(e){
  var t = DOMhelp.getTarget(e);
  var action = t.checked ? 'remove' : 'add';
  DOMhelp.cssjs(action, df.newsOpt, df.hideClass);
},

subjectChange()方法的工作方式相同:检索目标并检查第五个选项是否是选中的选项(即 selectedIndex 是否等于 4)。如果是,就从可选元素中移除隐藏类;否则,您添加它。另外,该方法将浏览器的焦点设置为新显示的元素,以便用户可以立即开始键入。

dynamicForm.js(续)

  subjectChange : function(e) {
    var t = DOMhelp.getTarget(e);
    var action = t.selectedIndex == 5 ? 'remove' : 'add';
    DOMhelp.cssjs(action, df.subjectOpt, df.hideClass);
    if(action == 'remove') {
      df.subjectOpt.getElementsByTagName('input')[0].focus();
    }
  }
}
DOMhelp.addEvent(window, 'load', df.init, false);

显示和隐藏连接的元素是将部分表单连接到其他选项的一种方式。另一种方法是保持它们可见,但添加一个禁用的属性。这使得用户无法对它们进行更改,并且浏览器将它们显示为灰色。

这比隐藏元素的功能要弱一些,因为 disabled 属性只适用于 input、textarea、select、option、optgroup 和 button。图 7-10 显示了在 Windows 上的 Firefox 中禁用元素后的表单外观。

9781430250920_Fig07-10.jpg

图 7-10 。禁用元素而不是隐藏它们

该脚本的主要区别在于,您必须针对每个想要单独禁用的输入元素。在单选按钮的情况下,这意味着您必须经历一个循环。脚本中的更改以粗体突出显示,应该是不言自明的:

dynamicFormDisable.js

df = {
  hideClass : 'hide',
  letterOption : 'newsletter',
  subjectOption : 'subject',
  init : function() {
    if(!document.getElementById || !document.createTextNode){
     return;
    }
    df.news = document.getElementById(df.letterOption);
    df.subject = document.getElementById(df.subjectOption);
    if(!df.subject || !df.news){ return; }
    df.newsOpt = DOMhelp.closestSibling(df.news.parentNode, 1);
    df.newsOpt = df.newsOpt.getElementsByTagName('input');
    for(var i = 0; i < df.newsOpt.length; i++){
      df.newsOpt[i].disabled = 1;
    }
    df.subjectOpt = DOMhelp.closestSibling(df.subject.parentNode, 1);
    df.subjectOpt = df.subjectOpt.getElementsByTagName('input')[0];
    df.subjectOpt.disabled = 1;
    DOMhelp.addEvent(df.news, 'click', df.letterChange, false);
    DOMhelp.addEvent(df.subject, 'change', df.subjectChange, false);
  },
  letterChange : function(e){
    var i;
    var t = DOMhelp.getTarget(e);
    var disable = t.checked ? false: true ;
    for(i = 0; i < df.newsOpt.length; i++) {
      df.newsOpt[i].disabled = disable;
    }
  },
  subjectChange : function(e){
    var t = DOMhelp.getTarget(e);
    if(t.selectedIndex == 5) {
      df.subjectOpt.disabled = null;
      df.subjectOpt.focus();
    } else {
      df.subjectOpt.disabled = 1;
    }
  }
}
DOMhelp.addEvent(window, 'load', df.init, false);

使用 disabled 的实际结果是,这些元素也不能再通过 tab 键来访问——这对于隐藏的元素仍然是可能的(除非您通过将 display 设置为 none 来隐藏它们,如前面的站点导航部分所示)。

自定义表单元素

如果有足够的技能和测试时间,您可以使用 JavaScript 来扩展浏览器提供的常规表单控件,从而为用户提供您自己的自定义控件,甚至使它们可以通过键盘访问。特别是在 web 应用开发中,这可能是一个真正的必要性。

HTML5 增加了新的输入类型,赋予表单更多的功能。其中一些类型包括电话、搜索和电子邮件。对于 input、select 和 textarea 标签的 required 属性等选项,验证也变得更好了。

表单和 JavaScript 概述

我希望这一章能让你对表单和 JavaScript 有所了解。您了解了表单本身的不同属性和方法,以及它们各自的属性和方法可能包含的每个元素。您详细了解了如何处理选择框,以及如何通过隐藏依赖于其他元素的元素并仅在其他元素被激活或具有正确值时才显示它们,来使表单更加动态。

关于表单和 JavaScript,需要记住的主要内容是:

  • 尽量不要过度使用表单。一旦你完成了调整,看看这个表单是否还能用键盘。特别是,更长的表单更有可能通过从一个字段跳到另一个字段来填写,而不是单击不同的元素然后编辑它们。
  • 不要使用事件处理程序自动提交表单,可以通过让用户单击提交按钮或按 Enter 键来提交表单。不要剥夺用户的这些选择。
  • 虽然旧的表单集合表单和元素不是最新的 DOM 脚本技术(因为它们依赖于 HTML,而所有其他 DOM 方法也可以应用于 XML 字符串),但它们可能是在通用或生成的表单上使用的更容易的选项,因为您无法控制 id 或元素的数量。遍历一个元素列表比遍历一个表单的所有子元素并将它们与可能的元素名称进行比较,或者逐个遍历 input、textarea 和 select 元素集合要容易得多。

摘要

您现在应该能够处理 JavaScript 的最常见用法了。当你需要回忆如何处理图像、窗口、导航和表单时,你可以回到本章和上一章。

在下一章,我们将离开浏览器和客户端脚本的世界,专注于如何让 JavaScript 与后端和服务器端脚本对话。这也将使您能够了解 Ajax。

八、Ajax 和 Node.js 的后端交互

你终于读到了我谈论 Ajax 的那一章。好消息是,您可以使用 Ajax 创建非常漂亮的界面,并且可以将 JavaScript 的应用范围扩展到浏览器和当前显示的文档之外。

不太好的消息是,Ajax 依赖于 XMLHTTPRequest 对象(或简称为 XHR)或其微软对等物,并且它被“HTTP”所覆盖。这意味着没有服务器就不能使用任何 Ajax 示例。此外,要使用 Ajax,您需要一些服务器端脚本的基础知识(除非您使用现成的软件包——在本章的“概述”一节中有更多介绍)。

这也意味着使用 Ajax 剥夺了 JavaScript 的一项优势:创建可以在计算机的文件系统上,甚至从 CD 或记忆棒上离线工作的界面的能力。然而,Ajax 的好处弥补了这一点。

本章的另一部分是对 Node.jsNode 的介绍。Node 使 JavaScript 开发人员能够在服务器端编写代码。与 Ruby 或 PHP 类似,Node 允许您与数据库对话,并向应用的客户端发送信息。在本章的例子中,您将完全用 JavaScript 创建一个 HTTP 服务器。

首先,您需要设置一个本地服务器,因为 Ajax 示例使用 PHP 作为它们的服务器端语言。这并不像看起来那么难,因为有很多预打包的服务器可用。

我最喜欢的是 http://www.apachefriends.org/可以下载的《XAMPP》。你可以找一个安装程序,按照说明在几分钟内安装并运行你自己的服务器。

XAMPP 安装 Apache 2、MySQL、PHP 和所有你需要的附加软件,并且可以在许多平台上使用。它还配备了一个 FTP 和电子邮件服务器、一个统计软件包和许多其他选项,并且由 Apache Friends(www.apachefriends.org)的维护者不断更新。哦,是的,当然是免费的。

image 提示同样,为了避免在阅读本章其余部分时遇到挫折,您应该亲自尝试一下本章中的许多代码示例,看看我在说什么。与其他章节的不同之处在于,代码示例不能在文件系统的本地计算机上运行;它们需要服务器,因为 Ajax 需要 HTTP 协议才能工作。如果你不想安装服务器,但是你已经在线了,你可以去本书的主页www.beginningjavascript.com/,在那里你可以看到所有运行中的代码示例。

当您安装 xamp 时,您可以将章节示例解压缩到服务器安装的 htdocs 目录中的一个目录中,例如,名为 jsbook 的目录,该目录可能是 c:\ xamp \ htdocs \。要查看示例,请打开浏览器并键入localhost/jsbook/作为位置。

image 提示除了在www.apachefriends.org/en/faq-xampp.html阅读官方帮助常见问题外,Mac 用户还可以使用 MAMP,它也做同样的事情。你可以在 www.mamp.info 下载 MAMP。

家用清洁液,足球俱乐部,还是飞侠哥顿的飞船:什么是 Ajax?

Ajax 最初代表异步 JavaScript 和 XML ,这个术语是 Jesse James Garrett 在 2005 年 2 月的 Adaptive Path 上创造的(www . Adaptive Path . com/publications/essays/archives/000385 . PHP)。它描述了一种不同于传统的开发 web 应用的方法。

正如文章中所解释的,传统的 web 应用和网站是同步工作的——每当你点击一个链接或提交一个表单时,浏览器将数据发送给服务器,服务器(希望如此)做出响应,然后整个页面被刷新。

Ajax 应用异步工作,这意味着您可以在用户代理和服务器之间来回发送数据,而无需重新加载整个页面。您只替换页面中发生变化的部分。您还可以发送几个请求,继续滚动和使用页面,而其他部分在后台加载。

*一个很好的比喻是,Ajax 之于传统网页,就像即时消息之于电子邮件一样:即时反馈,没有很长的等待时间,有更多的交流选择。图 8-1 显示了与传统网站和网络应用相比,Ajax 应用的流程。

9781430250920_Fig08-01.jpg

图 8-1 。Ajax 与传统的请求

乍一看,这似乎给整个事情增加了一层额外的复杂性。然而,真正酷的是 Ajax 引擎和浏览器之间的通信是通过 JavaScript 触发的,而不是通过页面重载。

实际上,最终用户等待页面加载和呈现的时间更少,与页面的交互也更容易,因为她可以请求数据,并且仍然可以阅读文本或查看页面上的其他内容。这使得界面更加光滑,因为你可以在不改变整个网站的情况下在登录表单上给出反馈,同时能够在服务器或数据库中测试正确的条目。

让我们看一个简单的例子。演示文件 exampleXHR.html 使用 Ajax(没有 X,因为不涉及 XML)在用户点击链接时从服务器加载并显示文件,如图 8-2 所示。

9781430250920_Fig08-02.jpg

图 8-2 。通过 Ajax 加载外部文件

这一切背后的魔棒是我之前介绍的一个名为 XMLHttpRequest 的对象。这是一个非标准对象,因为它不是万维网联盟(W3C)网站上官方标准的一部分。(目前是工作草案;更多详情见www.w3.org/TR/XMLHttpRequest/。)但是,所有现代浏览器都支持它。如果你需要支持 Internet Explorer 6,你应该寻找能做同样事情的 ActiveX object:ActiveX object(“微软。XMLHTTP”)。

image 警告这样做的问题是,当用户在 Microsoft Internet Explorer 中启用了 JavaScript 但禁用了 ActiveX 时,他将无法体验到您的 Ajax 努力。如果您创建 Ajax 解决方案并获得用户错误报告,请记住这一点。

让我们一步一步地看这个例子,这样你就可以看到不同的部分是做什么的。HTML 包含指向文本文件的链接,并使用两个参数调用 simplexhr.doxhr 方法:文本要发送到的 HTML 元素的 ID 和文本的 URL。

exampleXHR.html(节选)

<li>
    <a href="perfect_day.txt"
       onclick="simplexhr.doxhr('txtcontainer1', this.href ); return false;">Perfect Day</a>
</li>
<li>
    <a href="great_adventure.txt"
       onclick="simplexhr.doxhr('txtcontainer1', this.href ); return false;">Great Adventure</a>
</li>

image 注意这些链接并非完全不引人注目,符合本书中其余代码示例的标准,但至少它们在没有 JavaScript 的情况下也能工作——当脚本不可用时,浏览器将简单地显示文本文件。创建依赖于脚本的链接是非常诱人的,尤其是在使用现成的 Ajax 库时。无论这项技术有多酷,这都不是一个好主意。

simpleXHR.js

simplexhr = {
  doxhr : function( container, url ) {
   if( !document.getElementById || !document.createTextNode) {
     return;
   }
   simplexhr.outputContainer = document.getElementById( container );
   if( !simplexhr.outputContainer ){ return; }

脚本首先检查 DOM,并检查您想要写入内容的元素是否可用。如果元素可用,它将被存储在名为 outputContainer 的属性中,以便脚本中的所有其他方法都可以使用它。

simpleXHR.js(续)

var request;
try{
  request = new XMLHttpRequest();
} catch ( error ) {
  try {
    request = new ActiveXObject("Microsoft.XMLHTTP" );
  } catch ( error ) {
    return true;
  }
}

定义一个名为 request 的新变量,并使用 try 和 catch 构造来查看支持哪个 XHR 版本。尝试分配一个新的 XMLHttpRequest。如果不支持这种情况,则会发生错误,触发 catch 语句。(你可以在本书的附录中了解更多关于 try and catch()的知识。)这个尝试分配 Microsoft ActiveX 对象。如果这也不可用,该方法返回 true,这意味着浏览器将只跟随链接并在浏览器中显示文本。

如果赋值成功,您就拥有了一个新的 XMLHttpRequest 对象。

image 注意要获得 XMLHttpRequest 对象的方法、处理程序和属性的完整列表,您可以参考 W3C 站点的文档:www.w3.org/TR/XMLHttpRequest/。你也可以参考 Mozilla 开发者网络的developer.mozilla.org/en-US/docs/DOM/XMLHttprequest或者微软网站的msdn . Microsoft . com/en-us/library/ms 53587428v = vs . 8529 . aspx

第一步是调用 open()方法启动与服务器的连接,并检索或发送数据。open()方法有五个参数,其中三个是可选的:

request = open(requestMethod,url[,sync,[name,[password]]);

  • requestMethod 参数(以及其他一些超出本章范围的选项)可以是 GET 或 POST,对应于 HTTP 方法。
  • url 参数是文件在服务器上的位置。

image 注意 XMLHttpRequest 不允许你从其他服务器加载内容,因为那会是一个很大的安全问题。想象一下,电子邮件或网站中嵌入的 JavaScript 能够从您的计算机发送任何数据,或者从服务器检索更多代码。这个问题有解决办法。一种是通过在服务器上使用代理脚本来加载第三方内容。另一种方法是在服务器上启用跨源资源共享(CORS)。关于 CORS 的信息可以在 http://www.w3.org/TR/cors/的 W3C 网站上找到。

  • sync 参数是可选的,它是一个布尔值,定义请求应该异步发送还是同步发送。它被硬连接为 true——这意味着请求将被异步发送。同步请求会锁定浏览器。
  • name 和 password 参数是可选的,只有当您尝试调用的文件需要用户身份验证时才是必需的。

在这种情况下,您将只从服务器检索文件。为此,您使用 GET 作为请求方法,使用文件的位置作为 url 参数,省略可选参数。

simpleXHR.js(续)

request.open('get', url );

请求对象的 readyState 属性包含一个数值,它描述了请求发生了什么。它在整个请求和响应过程中递增。readyState 的不同可能值及其对应的请求状态如下:

  • 0: 对象已创建,但尚未调用 open 方法。
  • 1: 发送方法尚未被调用。
  • 发送方法被调用,但是数据还不可用。
  • 3: 服务器正在发送数据。
  • 4: 连接完成—数据已发送并已检索。

每当状态改变时,XHR 触发 readystatechange 事件。您可以使用相应的 onreadystatechange 事件处理程序来调用一个函数,在该函数中,您可以根据 readyState 的可能值进行测试,并采取适当的操作。

simpleXHR.js(续)

request.onreadystatechange = function() {
  if( request.readyState == 1 ) {
    simplexhr.outputContainer.innerHTML = 'loading... ';
  }

一旦请求被初始化(readyState 等于 1),给用户一些反馈是一个非常好的主意,即后台正在发生一些事情。在本例中,脚本显示“正在加载...”HTML 输出元素内的消息,如图图 8-3 所示。

9781430250920_Fig08-03.jpg

图 8-3 。通知用户请求已经发送并且正在进行中

其他状态不能跨浏览器安全地读取,这就是为什么您跳过 2 和 3 并通过比较 readyState 和 4 来检查请求是否完成。

simpleXHR.js(续)

if( request.readyState == 4 ) {
  if ( /200|304/.test( request.status ) ) {
    simplexhr.retrieved(request);
  } else {
    simplexhr.failed(request);
  }
}

当请求完成时,检查另一个名为 status 的属性,它存储请求的状态。状态是响应的标准 HTTP 响应状态代码。当无法建立连接时,在其他错误条件下,或者当请求已被取消时,它为 0,当找不到文件时,它为 404。

image 关于标准 HTTP 响应状态码的完整列表,参见www.w3.org/Protocols/rfc2616-sec10.html

如果状态为 200(一切正常)或 304(未修改),则文件已被检索到,您可以对其进行操作。在这个演示脚本中,您调用 retrieved()方法。如果状态是任何其他值,则调用 failed()。

simpleXHR.js(续)

  }
  request.send( null );
  return false;
},

send()方法将您的请求发送到服务器,并可以将请求参数发送到被调用的服务器端脚本。如果您没有任何要发送的参数,那么将其设置为 null 是最安全的。(Internet Explorer 接受不带任何参数的 send(),但这可能会在较旧的 Mozilla 浏览器中导致问题。)最后,将方法的返回值设置为 false 会阻止链接被跟踪。

simpleXHR.js(续)

failed : function( requester ) {
  alert('The XMLHttpRequest failed. Status: ' + requester.status );
  return true;
},

如果请求没有成功,failed()方法会显示一个 alert()对话框,告诉用户这个问题。(这不是很聪明也不漂亮,但目前应该可以了。)在用户单击对话框的 OK 按钮后返回 true 会导致链接被跟随。您可以通过在浏览器中本地打开文件 exampleXHR.html(没有 protocol )并点击链接来测试这一点。因为没有 HTTP 传输,任何请求都会失败,代码为 0,如图图 8-4 所示。

9781430250920_Fig08-04.jpg

图 8-4 。通知用户 XMLHttpRequest 失败

但是,如果请求一切顺利,那么 retrieved()方法将接管。

simpleXHR.js(续)

  retrieved : function( requester ) {
    var data = requester.responseText;
    data = data.replace( /\n/g, '<br>' );
    simplexhr.outputContainer.innerHTML = data;
    return false;
  }
}

这个方法使您能够获取和使用从 XMLHttpRequest 返回的数据。数据可以根据 responseType 以几种不同的格式读出。两种基于文本的格式是 responseText 和 responseXML。两者的区别在于输出的类型——responseText 返回一个字符串,而 responseXML 返回一个 XML 对象。可以在 responseText 上使用所有常用的字符串属性和方法,比如 length、indexof()、replace()等等,也可以在 responseXML 上使用所有的 DOM 方法,比如 getElementsByTagName()、getAttribute()等等。除了返回基于文本的响应,还可以使用 returnType blob 或 arrayBuffer 接收二进制数据。

在此示例中,您仅检索文本并使用 String.replace()方法将所有换行符转换为 BR 元素。然后,您可以将更改后的字符串作为 innerHTML 写出到 outputContainer,并返回 false 以停止正常的链接行为。

在许多情况下,使用 responseText 并通过 innerHTML 写出数据就足够了。与使用 XML 和 DOM 将对象转换回 HTML 相比,对于用户的浏览器和 CPU 来说,这也要快得多,工作量也少得多。

image 注意Ajax 这个缩写并不真正适用于这些例子,因为这个过程缺少 XML 组件。出于这个原因,这种方法被称为异步 HTML 和 HTTP (AHAH) ,并被定义为微格式,在microformats.org/wiki/rest/ahah有代码示例。

你呢?

通常,浏览器缓存是你的朋友。浏览器在其中存储下载的文件,这意味着用户不必一遍又一遍地下载你的脚本。然而,在 Ajax 的情况下,缓存会导致问题。

Safari 是罪魁祸首,因为它缓存了响应状态(),不再触发更改。(请记住,状态返回 HTTP 代码 200、304 或 404。)然而,避免缓存问题非常简单:在调用 send()方法之前,向请求添加另一个头。这个标题告诉浏览器测试自某个日期以来数据是否发生了变化。你设定的日期并不重要,只要它是在过去——例如,在本文写作时是这样写的:

request.setRequestHeader( ‘If-Modified-Since’, ‘Mon, 12 Jan 2013 00:00:00 GMT’ );
request.send( null );

将 X 放回 Ajax 中

如果使用 responseXML,可以使用 DOM 方法将接收到的 XML 转换成 HTML。演示 exampleXMLxhr.html 就是这样做的。作为数据源,以 XML 格式的上一章分页示例中使用的相册集合为例。

albums.xml(节选)

<?xml version="1.0" encoding="utf-8"?>
<albums>
  <album>
    <id>1</id>
    <artist>Depeche Mode</artist>
    <title>Playing the Angel</title>
    <comment>They are back and finally up to speed again</comment>
  </album>
  <album>
    <id>2</id>
    <artist>Monty Python</artist>
    <title>The final Rip-Off</title>
    <comment>Double CD with all the songs</comment>
  </album>
  [... more albums snipped ...]
</albums>

您希望通过 XHR 检索这些数据,并在页面中以表格的形式显示出来。图 8-5 显示了请求的不同阶段。

9781430250920_Fig08-05.jpg

图 8-5 。以表格形式检索和显示 XML 数据

剧本的主要部分不必改变。

simplexmlxmhr . js】的缩写

simplexhr = {
  doxhr : function( container, url ) {
    if( !document.getElementById || !document.createTextNode ){
      return;
    }
    simplexhr.outputContainer = document.getElementById( container );
    if( !simplexhr.outputContainer ) { return; }
    var request;
    try {
      request = new XMLHttpRequest();
    } catch( error ) {
      try {
        request = new ActiveXObject("Microsoft.XMLHTTP" );
      } catch ( error ) {
        return true;
      }
    }
    request.open('get', url,true );
    request.onreadystatechange = function() {
      if(request.readyState == 1) {
        simplexhr.outputContainer.innerHTML = 'loading... ';
      }
      if(request.readyState == 4) {
        if( request.status && /200|304/.test( request.status ) ) {
          simplexhr.retrieved( request );
        } else {
          simplexhr.failed( request );
        }
      }
    }
    request.setRequestHeader('If-Modified-Since', 'Mon, 12 Jan 2013 00:00:00 GMT');
    request.send( null );
    return false;
  },

不同之处在于 retrieved()方法,该方法通过 responseXML 读取数据,并使用 XML 作为内容源写出数据表。移除加载消息,并使用 DOM createElement()和 createTextNode()方法创建主表。

simpleXMLxhr.js(续)

retrieved : function( requester ) {
  var data = requester.responseXML;
  simplexhr.outputContainer.removeChild(simplexhr.outputContainer.firstChild);
  var i, albumId, artist, albumTitle, comment, td, tr, th;
  var table = document.createElement('table' );
  var tablehead = document.createElement('thead');
  table.appendChild( tablehead );
  tr = document.createElement('tr');
  th = document.createElement('th');
  th.appendChild( document.createTextNode('ID'));
  tr.appendChild( th );
  th=document.createElement('th');
  th.appendChild( document.createTextNode('Artist'));
  tr.appendChild( th );
  th = document.createElement('th');
  th.appendChild( document.createTextNode('Title'));
  tr.appendChild( th );
  th=document.createElement('th');
  th.appendChild( document.createTextNode('Comment'));
  tr.appendChild( th );
  tablehead.appendChild( tr );
  var tablebody = document.createElement('tbody');
  table.appendChild( tablebody );

请注意,当您动态创建表格时,Internet Explorer 不会显示它们,除非您将行和单元格嵌套在 TBODY 元素中。火狐不会介意的。

接下来,循环检索数据的所有相册元素。

simpleXMLxhr.js(续)

var albums = data.getElementsByTagName('album');

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

对于每个相册,通过标记名读取 XML 节点的内容,并通过 firstChild.nodeValue 检索它们的文本内容。

simpleXMLxhr.js(续)

tr = document.createElement('tr');
albumId = data.getElementsByTagName('id')[i].firstChild.nodeValue;
artist = data.getElementsByTagName('artist')[i].firstChild.nodeValue;
albumTitle = data.getElementsByTagName('title')[i].firstChild.nodeValue;
comment = data.getElementsByTagName('comment')[i].firstChild.nodeValue;

通过 createElement()、createTextNode()和 appendChild()使用这些信息将数据单元格添加到表中。

simpleXMLxhr.js(续)

  td = document.createElement('th');
  td.appendChild( document.createTextNode( albumId ) );
  tr.appendChild( td );
  td = document.createElement('td');
  td.appendChild( document.createTextNode( artist ) );
  tr.appendChild( td );
  td = document.createElement('td');
  td.appendChild( document.createTextNode( albumTitle ) );
  tr.appendChild( td );
  td = document.createElement('td');
  td.appendChild( document.createTextNode( comment ) );
  tr.appendChild( td );
  tablebody.appendChild( tr );
}

将结果表作为新的子元素添加到输出容器中,并返回 false 以阻止链接将 XML 作为新文档加载。失败的()方法保持不变。

simpleXMLxhr.js(续)

    simplexhr.outputContainer.appendChild( table );
    return false;
  },
  failed : function( requester ) {
    alert('The XMLHttpRequest failed. Status: ' + requester.status );
    return true;
  }
}

您可以看到,通过在 DOM 脚本方面做“正确的事情”,脚本会变得相当复杂。您可以通过使用工具方法创建表行来减少代码量,但是这意味着更多的处理,因为这些方法必须在一个循环中调用。

如果像本例中那样了解 XML 结构,使用 innerHTML 和 string 方法转换数据可能会更快更容易。演示 exampleXHRxmlCheat.html 正是这样做的。大部分脚本保持不变,但是 retrieved()方法要短得多。

simplexmlxhrchet . js(节选)

retrieved : function( requester ){
  var data = requester.responseText;
  simplexhr.outputContainer.removeChild(simplexhr.outputContainer.firstChild);
  var headrow = '<tr><th>ID</th><th>Artist</th><th>Title</th><th>Comment</th></tr>';
  data = data.replace( /<\?.*\?>/g, ' ' )
  data = data.replace( /<(\/*)id>/g, '<$1th>' )
  data = data.replace( /<(\/*)(artist|title|comment)>/g, '<$1td>' )
  data = data.replace( /<(\/*)albums>/g, '<$1table>' )
  data = data.replace( /<(\/*)album>/g, '<$1tr>' );
  data = data.replace( /<table>/g, '<table>' + headrow );
  simplexhr.outputContainer.innerHTML = data;
  return false;
},

您以 responseText 的形式检索数据,删除“加载。。."消息,然后创建一个标题表行作为字符串,并将其存储在变量 headrow 中。因为 responseText 是一个字符串,所以可以使用 String.replace()方法来更改 XML 元素。

首先通过删除任何以 and ending with ?>开头的内容来删除 XML 序言。

image 注意这个例子使用了正则表达式,你可能还不知道,但我们将在下一章详细讨论。只要说正则表达式用斜线分隔并匹配某种文本模式就够了。如果斜线内有括号,这些字符串将存储在以$开头的变量中;这些可以在替换字符串中用来代替匹配模式的子字符串。例如,正则表达式 pattern / < (/*)id > /g 匹配以<开头、后跟可选/(如果找到,则存储为\(1)、后跟字符串 id 和结束字符>的所有内容。第二个参数<\) 1>,写出第<个>或第</第>个,这取决于原始 id 标签是开始标签还是结束标签。您可以执行简单的字符串替换,而不是使用正则表达式:

data = data.replace('<id>', '<th>');
data = data.replace('</id>', '</th>');

按照这个模式替换其他元素:每个相册元素变成一个表,每个相册变成一个 tr,每个 id 变成一个 th;艺术家、标题和评论各成为一个 td。将 headrow 字符串追加到中,并使用 innerHTML 将最终结果存储在 outputContainer 元素中。

用 JSON 替换 XML

尽管 XML 是一种流行的数据传输格式——它是基于文本的,您可以确保有效性,并且系统能够通过 dtd、XML Schemata 或 RELAX NG 相互通信。Ajax 爱好者已经越来越意识到将 XML 转换成 JavaScript 对象是一件非常麻烦的事情。

与其将 XML 文件作为 XML 读取并通过 DOM 解析它,或者作为文本读取并使用正则表达式,不如将数据转换成 JavaScript 可以直接使用的格式,这样会容易得多,对系统的压力也小得多。这种格式被称为JSON(【http://json.org/】??)。它允许数据集用对象文字符号表示。演示 exampleJSONxhr.html 使用前面例子中的 XML 作为 JSON:

<albums>
  <album>
    <id>1</id>
    <artist>Depeche Mode</artist>
    <title>Playing the Angel</title>
    <comment>They are back and finally up to speed again</comment>
 </album>
  <album>
    <id>2</id>
    <artist>Monty Python</artist>
    <title>The final Rip-Off</title>
    <comment>Double CD with all the songs</comment>
  </album>
  <album>
    <id>3</id>
    <artist>Ms Kittin</artist>
    <title>I.com</title>
    <comment>Good electronica</comment>
  </album>
</albums>

转换成 JSON,如下所示:

专辑。json

{
  "album":
   [
    {
      "id" : "1",
      "artist" : "Depeche Mode",
      "title" : "Playing the Angel",
      "comment" : "They are back and finally up to speed again"
    },
    {
      "id" : "2",
      "artist" : "Monty Python",
      "title" : "The final Rip-Off",
      "comment" : "Double CD wiid all the songs"
    },
    {
      "id" : "3",
      "artist" : "Ms Kittin",
      "title" : "I.com",
      "comment" : "Good electronica"
    }
  ]
}

好处是数据已经是 JavaScript 可以理解的格式。要将其转换为要显示的对象,只需对字符串使用 eval 方法。

examplejsonxhr . js(excerpt)

retrieved : function( requester ) {
  var content = '<table><thead>';
  content += '<tr><th>ID</th><th>Artist</th>';
  content += '<th>Title</th><th>Comment</th>';
  content += '</tr></thead><tbody>';
  var data = JSON.parse(' (' + requester.responseText + ') ' );

这为您提供了作为对象的所有内容,您可以通过属性表示法或关联数组表示法(后者在 id 示例中显示,前者在所有其他示例中显示):

examplejsonxhr . js(excerpt)

  var albums = data.album;
  for( var i = 0; i < albums.length; i++ ) {
    content += '<tr><td>' + albums[i]['id'] + '</td>';
    content += '<td>' + albums[i].artist + '</td>';
    content += '<td>' + albums[i].title + '</td>';
    content += '<td>' + albums[i].comment + '</td></tr>';
  }
  Content += '</tbody></table>';
  simplexhr.outputContainer.innerHTML = content;
  return false;
},

对于您自己服务器上的文件,使用 JSON 而不是 XML 要快得多。(在测试中,它被证明快了十倍。)但是,如果从第三方服务器使用 JSON,使用 eval()可能会很危险,因为它会执行任何 JavaScript 代码,而不仅仅是 JSON 数据。

您可以通过使用一个解析器来避免这种危险,该解析器确保只有数据被转换成对象,而恶意代码不会被执行。http://www.json.org/js.html 有一个开源版本。我们会在第十一章回到 JSON。

使用服务器端脚本访问第三方内容

如前所述,出于安全原因,很难使用 XHR 从其他服务器加载内容。例如,如果您想从其他服务器检索 RSS 提要,您可以使用一个服务器端脚本来为您加载这些提要,或者连接到一个支持 CORS 的服务器。

image 注意这是一个关于 Ajax 的常见神话:它并不取代服务器端代码,而是由服务器端代码提供支持,并为它提供一个更光滑的接口。XHR 本身只能从同一个服务器检索数据,或者向服务器端脚本发送信息。例如,您不能用 JavaScript 访问数据库——除非您使用名为 JSONP(带填充的 JSON)的方法,数据库提供者以 JavaScript 的形式提供输出,并且您将它包含在它自己的脚本标记中。在第十一章中有一个这样的例子。

服务器端组件是一个传递或代理脚本,它获取一个 URL,加载文档内容,然后将其发送回 XHR。该脚本需要设置正确的头来告诉 XHR 它返回的数据是 XML。如果找不到该文件,脚本将返回一个 XML 错误字符串。以下示例使用 PHP,但是任何服务器端语言都可以执行相同的任务。

loadrss.php

<?php
// Set the XML header
header('Content-type: text/xml');
// Define an error message in case the feed cannot be found
$error='<?xml version="1.0"?><error>Cannot find feed</error>';
// Clear the contents
$contents = '';
// Read the url variable from the GET request
$rssurl = $_GET['url'];
// Test if the url starts with http to prevent surfers
// from calling and displaying local files
if( preg_match('/^http:/', $rssurl ) ) {
  // Open the remove file, and store its contents
  $handle = @fopen( $rssurl, "rb" );
    if( $handle == true ){
      while ( !feof($handle ) ) {
        $contents .= fread( $handle, 8192 );
      }
      fclose( $handle );
   }
}
// If the file has no channel element, delete contents
if( !preg_match('/<channel/', $contents ) ){ $contents = ''; }
// Return either the contents or the error
echo $contents == '' ? $error : $contents;
?>

演示 exampleExternalRSS.html 使用这个脚本从 Yahoo 网站上检索 RSS 格式的最新标题。

HTML 中的相关部分是调用 doxhr()方法的链接,该方法带有输出新闻的元素和作为参数的 RSS URI。

examples external RSS . html(excerpt)

<p>
  <a href="http://rss.news.yahoo.com/rss/topstories"
     onclick="return readrss.doxhr('newsContainer',this.href)">
     Get Yahoo news
  </a>
</p>
<div id="newsContainer"></div>

image 注意 RSS 是真正简单聚合的缩写。本质上,它是一个 XML,其中包含您想要与世界共享的内容,通常是新闻标题。RSS 的规范可以在blogs.law.harvard.edu/tech/rss找到,你可以在维基百科上读到更多关于它的好处:en.wikipedia.org/wiki/RSS

本例中的重要细节是,RSS 是一种标准化格式,并且您知道 XML 结构——即使您是从第三方网站获得的。每个有效的 RSS 文档都包含一个 items 元素以及嵌套的 item 元素。每一个都至少包含一个描述完整信息的标题和一个指向完整信息的链接。你可以使用这些来显示一个可点击的标题列表,将用户带到雅虎网站,在那里她可以阅读完整的新闻文章,如图 8-6 所示。

9781430250920_Fig08-06.jpg

图 8-6 。检索和显示 RSS 源数据

这个剧本又是一个简单的 XHR。不同之处在于,您不是直接链接到 URL,而是将它作为 GET 参数传递给 PHP 脚本:

外部 RSS.js

readrss = {
  doxhr:function( container, url ) {
    [... code snipped as it is the same as in the last example ...]
    request.open('get', 'loadrss.php?url=' + encodeURI( url ) );
    request.setRequestHeader('If-Modified-Since', 'Mon, 12 Jan 2013 00:00:00 GMT' );
    request.send( null );
    return false;
  },

检索到的()函数需要更改。首先,它删除了“加载。。."消息,并使用 responseXML 检索 XML 格式的数据。因为 PHP 脚本返回 XML 格式的错误消息,所以您需要测试返回的 XML 是否包含错误元素。如果是这种情况,读取第一个错误元素的第一个子元素的节点值,并将其写入由段落标记包围的 outputContainer。

externalRSS.js(续)

retrieved : function( requester ) {
  readrss.outputContainer.innerHTML = '';
  var data = requester.responseXML;
  if( data.getElementsByTagName('error').length > 0 ) {
    var error = data.getElementsByTagName(‘error’)[0].firstChild.nodeValue;
    readrss.outputContainer.innerHTML = '<p>' + error + '</p>';

如果没有错误元素,则检索返回的 XML 中包含的所有 item 元素,并检查结果列表的长度。如果少于一项,则从方法返回,并允许链接在浏览器中加载 XML 文档。这是确保返回的 RSS 有效的必要步骤——因为您没有在服务器端脚本中检查这一点。

externalRSS.js(续)

} else {
var items = data.getElementsByTagName('item');
var end = items.length;
if( end < 1 ){ return; }

如果有要显示的项目,您可以定义必要的变量并遍历它们。因为有些 RSS 提要有很多条目,所以限制显示多少条目是有意义的;在这种情况下,您选择 5。您阅读了每个条目的链接和标题,并添加了一个新的列表条目,其中嵌入了一个链接,该链接分别作为它的 href 属性和文本内容。注意,这个例子只是简单地组装了一个 HTML 字符串;当然,您可以走“更干净”的路,创建元素并应用文本节点。

externalRSS.js(续)

var item, feedlink, name, description, content = '';
for( var i = 0; i < 5; i++ ) {
  feedlink = items[i].getElementsByTagName('link').item(0).firstChild.nodeValue;
  name = items[i].getElementsByTagName('title').item(0).firstChild.nodeValue;
  item = '<li><a href="' + feedlink+'">' + name + '</a></li>';
  content += item;
}

将最后的内容字符串插入到 outputContainer 的 UL 标签中,这样就有了可点击的新闻标题。

externalRSS.js(续)

  readrss.outputContainer.innerHTML = '<ul>' + content + '</ul>';
  return false;
}

脚本的其余部分保持不变;failed()方法仅在 XHR 不成功时显示警告。

externalRSS.js(续)

  },
  failed : function( requester ) {
    alert('The XMLHttpRequest failed. Status: ' + requester.status );
    return true;
  }
}

XHR 的慢速连接

可能出现的一个问题是,XHR 的连接可能需要很长时间,用户会看到加载消息,但什么也没有发生。您可以通过使用 window.timeout()在一定时间后停止执行来避免此问题。演示 exampleXHRtimeout.html 展示了一个使用这种技术的例子。除了使用窗口对象之外,XHR 级别 2 还包括一个超时属性和一个 ontimeout 事件。目前,Chrome 和 Safari 不支持 XHR 超时,而 Opera、Firefox 和 Internet Explorer 10 支持。

该请求的默认设置是 10 毫秒,这会导致超时,如图 8-7 所示。您可以使用示例中的第二个链接将超时设置为 10 秒并重试,如果您的连接不是非常慢或者 Yahoo 没有停机,您将会获得头条新闻。

9781430250920_Fig08-07.jpg

图 8-7 。允许 XHR 连接超时

该脚本的不同之处在于,您需要一个属性来定义在触发超时之前要等待多长时间,一个属性来存储 window.timeout,还有一个布尔属性来定义是否有超时。Boolean 必须在 doxhr()方法中,因为每次调用 doxhr()时都需要初始化它。

xhrtimeout . js

readrss = {
  timeOutDuration : 10,
  toolong : false,
  doxhr : function( container, url ) {
    readrss.timedout = false;
    if( !document.getElementById || !document.createTextNode ){
      return;
    }
    readrss.outputContainer = document.getElementById( container );
    if( !readrss.outputContainer ){ return; }
    var request;
    try {
      request = new XMLHttpRequest();
    } catch( error ) {
      try {
        request = new ActiveXObject("Microsoft.XMLHTTP");
      } catch( error ) {
        return true;
      }
    }

在 onreadystatechange 事件侦听器中,添加超时并将其分配给主对象的 toolong 属性。在超时内,定义一个匿名函数来检查 readystate 并将其与 1 进行比较。这是一个场景,当定义的时间过去了,请求仍然在第一个阶段,而不是第四个也是最后一个阶段。发生这种情况时,调用请求的 abort()方法,将 timedout 属性设置为 true,并向显示元素写出一条消息,说明请求花费的时间太长。

XHRtimeout.js(续)

request.onreadystatechange = function() {
  if( request.readyState == 1) {
      readrss.toolong = window.setTimeout( function(){
        if( request.readyState == 1 ) {
          readrss.timedout = true;
          request.abort(); // Stop
          readrss.outputContainer.innerHTML = 'The request took too long';
         }
        },
       readrss.timeOutDuration
      );
    readrss.outputContainer.innerHTML = 'loading... ';
  }

当请求成功结束并且没有任何超时(存储在 timedout 属性中)时,清除超时。

XHRtimeout.js(续)

    if( request.readyState == 4 && !readrss.timedout ) {
    window.clearTimeout( readrss.toolong );
    if( /200|304/.test( request.status ) ) {
        readrss.retrieved( request );
      } else {
        readrss.failed( request );
      }
    }
  }
  request.open('get', 'loadrss.php?url='+encodeURI( url ) );
  request.setRequestHeader('If-Modified-Since', 'Mon, 12 Jan 2013 00:00:00 GMT' );
  request.send( null );
  return false;
},

脚本的其余部分保持不变。

一个更大的 Ajax 例子:连接的选择框

让我们来看一个更大的 Ajax 例子——我称它为 Ajax,尽管您不会使用 XML。连接的选择框是一个经典的例子,它展示了 JavaScript 可以如何让界面变得更快。它们的一个常见用途是航班报价网站,在这里您在选择框中选择一个机场,页面会立即在第二个选择框中显示从该机场出发的目的地机场。传统上,这是通过将所有 airport 连接数据保存在 JavaScript 数组中并操纵 select 元素的 options 数组来实现的。更改第一个机场选择框会自动将第二个机场选择框更改为可用目的地。

当你有鼠标和可用的 JavaScript 时,这是非常好的;然而,当两者中的一个缺失时,这可能是非常令人沮丧的,或者当两者都不可用时,这甚至是不可能的。这个例子将展示如何创建相互依赖的选择框,这些选择框在没有鼠标和 JavaScript 的情况下也能工作,并且在 JavaScript 可用时不会重新加载整个页面。

诀窍是让功能在服务器端工作,然后添加 JavaScript 和 XHR 技巧来阻止整个页面重新加载。因为你不知道用户是否真的能应付这个,你甚至可以让它可选而不是给定。

image 注意这种 Ajax 方法比原来的方法更具有可访问性。原因是你不想再一次犯 DHTML 最大的错误——使用一种技术而不考虑那些不能处理它的人。Jeremy Keith 在他的 DOM 脚本书中创造了这种方法 HIJAX ,但是到目前为止,它还没有像 Ajax 这个术语一样得到公众的广泛关注。

第一步是创建一个执行所有功能的服务器端脚本。因为这不是一本关于 PHP 的书,这里就不赘述了。只要说主文档 exampleSelectBoxes.php 包含一个更小的 PHP 脚本 selectBoxes.php 就够了。后者以数组的形式包含所有的机场数据(但也可以很容易地进入数据库来检索这些数据),并根据用户的选择和发送表单写出界面的不同状态,如图 8-8 所示。

9781430250920_Fig08-08.jpg

图 8-8 。连接的选择框

主页展示了带有 DIV 的表单,DIV 的 id 可用于 XHR 输出。

exampleSelectBoxes.php(节选)

<form action="exampleSelectBoxes.php" method="post">
  <div id="formOutput">
   <?php include('selectBoxes.php');?>
  </div>
  <p class="submit"><input type="submit" name="select" id="select" value="Choose" /></p>
</form>

image 注意这个例子使用 POST 作为发送数据的方法。

PHP 脚本返回了一个 HTML 界面,您可以在这个过程的每个阶段进入这个界面:

  • 如果还没有发送任何表单数据,它会显示一个 ID 为 airport 的选择框,列出数据集中的所有机场。
  • 如果选择了一个机场并发送给服务器,脚本会将选择的机场显示在一个强元素中,并显示为一个隐藏的表单字段。它还将此选择的可能目的地机场显示为一个 ID 为 destination 的选择框。此外,它还创建了一个指向主文档的链接,用 ID 开始一个新的选择。
  • 如果用户选择一个机场和一个目的地,并将它们发送回服务器,脚本只是暗示更多的功能,因为在这个例子中不需要更多的功能。但是,它提供了返回初始页面的链接。

如果 JavaScript 可用,该脚本应该执行以下操作:

  • 在表单中创建一个新的复选框,允许用户打开 Ajax 功能——在本例中,只需重新加载由 selectBoxes.php 创建的表单部分。
  • 如果选中了该复选框,脚本应该用事件处理程序调用的函数覆盖表单的正常提交过程。作为一个加载指示器,它应该将 Submit 按钮的文本改为“loading”
  • 它还应该向返回第一阶段的链接添加一个搜索参数,以确保当用户单击该链接时,他不必再次选择复选框。

先说剧本的骨架。您需要一个复选框的标签、一个包含它的段落的类(实际上不是必需的,但是它允许样式化)、表单元素容器的 id 和返回到流程开始的链接。

作为方法,您需要一个 init()方法,带有检索和失败处理程序的主 XHR 方法,以及用于事件处理的 cancelClick()和 addEvent()。

select boxes . js(skeleton)

dynSelect = {
  AJAXlabel : 'Reload only the results, not the whole page',
  AJAXofferClass : 'ajax',
  containerID : 'formOutput',
  backlinkID : 'back',
  init : function(){},
  doxhr : function( e ){},
  retrieved : function( requester, e ){},
  failed : function( requester ){},
  cancelClick : function( e ){},
  addEvent : function(elm, evType, fn, useCapture ){}
}
dynSelect.addEvent( window, 'load', dynSelect.init, false );

现在开始充实骨架。

selectBoxes.js

dynSelect = {
  AJAXlabel : 'Only reload the results, not the whole page',
  AJAXofferClass : 'ajax',
  containerID : 'formOutput',
  backlinkID : 'back',

init()方法测试 W3C DOM 是否受支持,检索第一个表单,并将 ID 为 select 的 Submit 按钮存储在一个属性中——这是在最后一步中删除按钮所必需的。然后,它创建一个新段落,并为前面定义的 Ajax 触发器应用该类。

selectBoxes.js(续)

init : function(){
  if( !document.getElementById || !document.createTextNode ){
   return;
  }
  var f = document.getElementsByTagName('form')[0];
  dynSelect.selectButton = document.getElementById('select');
  var p = document.createElement('p');
  p.className = dynSelect.AJAXofferClass;

议程上的下一步是提供打开 Ajax 选项的复选框。将复选框的名称和 ID 设置为 xhr,并确定当前 URI 是否已经具有?ajax 搜索字符串。如果有,将复选框预设为已选中。(这是必要的,以确保返回第一步的链接不会阻止 Ajax 增强的工作。)

selectBoxes.js(续)

dynSelect.cb = document.createElement('input');
dynSelect.cb.setAttribute('type', 'checkbox');
dynSelect.cb.setAttribute('name', 'xhr');
dynSelect.cb.setAttribute('id', 'xhr');
if( window.location.search != '' ) {
  dynSelect.cb.setAttribute('defaultChecked', 'checked' );
  dynSelect.cb.setAttribute('checked', 'checked');
}

将复选框添加到新段落中,并在其后添加一个带有适当文本的标签。新段落成为表单的第一个子节点,当表单被提交时,您应用一个触发 dohxhr()方法的事件处理程序。

selectBoxes.js(续)

  p.appendChild( dynSelect.cb );
  var lbl = document.createElement('label');
  lbl.htmlFor = 'xhr';
  lbl.appendChild( document.createTextNode( dynSelect.AJAXlabel ) );
  p.appendChild( lbl );
  f.insertBefore( p, f.firstChild );
  dynSelect.addEvent(f, 'submit', dynSelect.doxhr, false );
},

dohxr()方法测试复选框是否被选中,如果没有,则简单地返回。如果是,您为当前机场和当前目的地定义两个变量,并将输出元素存储在一个属性中。测试输出容器是否存在,如果不存在则返回。

selectBoxes.js(续)

doxhr : function( e ) {
  if( !dynSelect.cb.checked ){ return; }
  var airportValue, destinationValue;
  dynSelect.outputContainer = document.getElementById(dynSelect.containerID );
  if( !dynSelect.outputContainer ){ return; }

下面是 XHR 代码,它定义了正确的对象并设置 onreadystatechange 事件侦听器。

selectBoxes.js(续)

var request;
try {
  request = new XMLHttpRequest();
} catch( error ) {
  try {
    request = new ActiveXObject("Microsoft.XMLHTTP");
  } catch( error ) {
    return true;
  }
}
request.onreadystatechange = function() {
  if( request.readyState == 1 ) {
    dynSelect.selectButton.value = 'loading... ';
  }
  if( request.readyState == 4 ) {
    if( request.status && /200|304/.test( request.status ) ) {
      dynSelect.retrieved( request );
    } else{
      dynSelect.failed( request );
    }
  }
}

确定文档是否包含机场和目的地选择框;如果是,将它们的当前状态存储在变量 airportValue 和 destinationValue 中。请注意,您需要在航班选择过程的第二阶段检查 airport 字段的类型,因为它是一个隐藏字段。

selectBoxes.js(续)

var airport = document.getElementById('airport');
if( airport != undefined ) {
  if( airport.nodeName.toLowerCase() == 'select' ) {
    airportValue = airport.options[airport.selectedIndex].value;
  } else {
    airportValue = airport.value;
  }
}
var destination = document.getElementById('destination');
if( destination ) {
  destinationValue = destination.options[destination.selectedIndex].value;
}

因为表单是使用 POST 而不是 GET 发送的,所以您需要稍微不同地定义请求。首先,您需要将请求参数组装成一个字符串。(这是当 send 方法为 GET 时,URI 上变量的轨迹——例如,www.example.com/index.php?search+DOM&values = 20&start = 10。)

selectBoxes.js(续)

var parameters = 'airport=' + airportValue;
if( destinationValue != undefined ) {
  parameters += '&destination=' + destinationValue;
}

接下来,打开请求。除了使用修改后的头防止缓存,还需要告诉服务器内容类型是 application/x-www-form-urlencoded;然后,您将所有请求参数的长度作为伴随 Content-length 的值进行传输。您还需要告诉服务器在检索完所有数据后关闭连接。与 GET 请求不同,send()在发布时需要一个参数,这是 URI 编码的参数。

selectBoxes.js(续)

request.open('POST', 'selectBoxes.php');
request.setRequestHeader('If-Modified-Since', 'Mon, 12 Jan 2013 00:00:00 GMT');
request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
request.setRequestHeader('Content-length', parameters.length );
request.setRequestHeader('Connection', 'close');
request.send( encodeURI( parameters ) );

image 注意如果你不知道这里发生的一切,不要自责;毕竟是服务器和 HTTP 代码,你才刚刚开始用 JavaScript。只要你这样使用它,你可能永远也不会真正理解它的含义。

如果您在最后一个页面之前,并且机场和目的地都可用,请移除提交按钮以防止出错。

image 注意这是这个例子的修饰步骤。一个真正的应用也应该完成下面的步骤,但是你现在不需要走那么远。

最后,调用 cancelClick()来阻止正常的表单提交。

selectBoxes.js(续)

  if( airport && destination ) {
    var sendButton = document.getElementById('select');
    sendButton.parentNode.removeChild( sendButton );
  }
  dynSelect.cancelClick( e );
},

retrieved()方法与其他示例没有太大区别。在检索请求的 responseText 并用新的表单元素替换旧的表单元素之前,通过将 Submit 按钮的值改回 Select 来撤销上一步中所做的操作。补充?指向第一步的链接的 href 的 ajax,以确保激活该链接不会关闭之前选择的功能。(现在,您知道用户想要 Ajax 界面。)

selectBoxes.js(续)

retrieved : function( requester, e ) {
  dynSelect.selectButton.value = 'Select';
  var content = requester.responseText;
  dynSelect.outputContainer.innerHTML = content;
  var backlink = document.getElementById( dynSelect.backlinkID );
  if( backlink ) {
    var url = backlink.getAttribute('href');
    backlink.setAttribute('href', url+'?ajax');
  }
  dynSelect.cancelClick( e );
},

脚本的其余部分由熟悉的 failed()、cancelClick()和 addEvent()实用程序方法组成。

selectBoxes.js(续)

  failed : function( requester ){
    alert('The XMLHttpRequest failed. Status: ' + requester.status);
    return true;
  },
  cancelClick : function( e ){
    [... code snipped ...]
  },
  addEvent: function( elm, evType, fn, useCapture ){
    [... code snipped ...]
  }
}
dynSelect.addEvent( window, 'load', dynSelect.init, false );

这个例子表明 Ajax 非常依赖于服务器代码。如果你知道你会得到什么,就很容易创建一个有用的和有吸引力的界面。

您还可以以一种不引人注目和可选的方式使用 Ajax 方法,使旧的效果更引人注目,并更好地针对那些想要它们的人。

Node.js

本书大部分时间我们都在讨论如何在客户端使用 JavaScript,以及如何用它来增强您的 web 应用。这将是一个简短的介绍,介绍如何使用您新获得的技能来开发将在服务器上运行的应用。因为这不是在您的浏览器中运行,所以首先解释一下正在发生的事情并进行设置会有所帮助。

Node.js 建立在谷歌的 Chrome JavaScript 引擎 V8 之上。正因为如此,Node 让您只需使用 JavaScript 就可以创建一个 web 服务器,而不需要安装任何其他服务器软件。

由 Ryan Dahl 创建的目标是制作一个类似 Gmail 的 web 应用,Node 是一个开源项目,可以在多种环境下运行,可以由 Heroku、亚马逊的 AWS、Nodejitsu 和 Joynet 等公司托管,Joynet 也是该项目的赞助商。最著名的是 LinkedIn 将 Node 用于其移动应用。

安装 Node.js 并开始使用

进入www.nodejs.org/download,然后在你的系统上下载并安装 Node,你就可以快速上手了。之后,你应该准备做一些快速的例子。

我们将立即投入工作,确保一切正常。首先,让我们确保节点已安装,并检查当前版本。在撰写本文时,我使用的是版本 0.10.2。您可以通过在命令行中键入 node-version 来亲自尝试一下。

此时,您应该已经安装了节点,可以开始工作了。让我们跳过“Hello World”的例子,直接进入你将在每个节点教程中看到的另一个例子:如何制作 web 服务器。

httpServer.js

var http = require('http');

var server = http.createServer(function (request, response){
      response.writeHead(200,{"Content-Type": "text/plain"});
      response.end("It's the information age!");
});
server.listen(8080);

从纯代码的角度来看,它看起来像普通的 JavaScript。那么这怎么能创建一个 web 服务器呢?让我们深入细节。

首先,您需要 HTTP 模块。模块为您的应用增加了功能。HTTP 模块内置在节点中;但是,有一个节点包管理器(npm)允许您安装其他模块。下一个创建的变量叫做 server,它是 http object 上调用 createServer 返回的 HttpServer 的一个实例。createServer 方法接受匿名函数,该函数可以接受两个参数:请求和响应。

在我们讨论这两行代码之前,让我们先讨论一下代码中的最后一行:

server.listen(8080);

这告诉服务器监听端口 8080。这意味着,如果你在浏览器中输入“localhost:8080”,它会将你导向当前正在运行的服务器。当这种情况发生时,服务器必须做出响应。每次向服务器发出请求时都会调用这个函数。它被传递一个请求对象,其中包含请求的细节,还有一个响应对象,用于在处理程序处理请求时做出响应。在这种情况下,请求处理程序无条件地用状态代码 200(“OK”状态代码,表示成功)和包含字符串的响应正文来响应每个请求。

模块

我们谈到了让您能够向基于节点的应用添加功能的模块。它们还能让你保持自己的文件井然有序。让我们把当前的文件做成一个模块。

http 服务器模块. js

var http = require('http');

function startUp(){
   function onRequest(request, response){
      response.writeHead(200,{"Content-Type": "text/plain"});
      response.end("It's the information age!");
  }
      http.createServer(onRequest).listen(8080);
}
exports.startup = startUp;

现在让我们创建一个 index.js 文件来启动它。

index.js

var server = require("./httpServerModule");
server.startup();

可以看到模块大部分都是一样的。主要要注意的是最后一行:

exports.startup = startUp;

这里,您将属性 startUp 添加到对象导出中。任何使用 require()访问模块的人都可以使用该对象。模块范围内没有添加到导出中的变量对于模块来说仍然是私有的。

另一个文件 Index.js 类似于我们最初的项目。您需要加载该模块。那个。/告诉 Node 查找相对于包含当前文件的目录的模块。如果没有找到,Node 首先在核心模块中搜索,然后在其他文件夹中寻找匹配。可以在节点站点上找到完整的描述(nodejs . org/API/modules . html # loading _ from _ Node _ modules _ Folders)。第二行只是调用你的启动函数,模块完成工作。如果您愿意,也可以从命令行使用 REPL (Read-Eval-Print-Loop)来运行它。在命令行中,只需键入节点,然后就可以像在索引文件中一样运行每一行。然后检查你的浏览器。

在概念上,它类似于创建对象来保存您的功能。这样,在全局名称空间中就不会有太多的变量。

回调

在前面的例子中,您有匿名函数来处理您的请求的结果。这很好,但是如果有很多事情在进行,就很难阅读。回调让你以一种易于阅读的方式分离一些函数。下一个示例展示了如何使用 Node 向 web 服务器发出请求,并使用回调在命令行中显示结果。

在这里,您将创建一个与之前创建的模块相似的模块。这一次,在发出 HTTP 请求时,您分配了两件事情。第一个选项描述您感兴趣的主机服务器和该服务器上的路径。接下来是回调函数。该函数接收从服务器返回的响应,并为该响应分配了两个事件。

http request . js

var http = require('http');

var options = {
      host: 'www.apress.com',
      path: '/9781430250920'
  };
    callback = function(response) {
      var str = ''
        response.on('data', function (chunk) {
        str += chunk;
   });

   response.on('end', function () {
      console.log(str);
   });
}

function client (){
     var req = http.request(options, callback);
           req.end();
}

exports.client = client;

这类似于常规 JavaScript 中的事件侦听器。您寻找“数据”事件并获取从服务器返回的数据。在本例中,您将它附加到 str 变量。

第二个事件 end 让您知道来自服务器的数据流已经结束。命令行的结果是一个长字符串,由您刚刚请求的网页内容组成。

因为这是一个模块,您可以从这里访问它。

服务器请求索引.js

var server = require("./httpRequest");
server.client ();

调试

Node 简介的最后一部分是谈论如何调试你的应用。运行调试器的最快方法是在命令行节点 debug filename.js。在您的代码中,您可以在想要检查代码的位置添加调试器命令。这可能不是调试应用最直观的方式。像 Eclipse、JetBrain 的 IntelliJ Idea 和 WebStorm 产品这样的 ide 都有调试选项,在这些选项中,您可以设置断点,并像使用其他服务器端语言一样使用 Node.js。你可以在 gitHub 上找到 Eclipse 的详细说明:gitHub . com/joyent/Node/wiki/Using-Eclipse-as-Node-Applications-Debugger。你可以在这里找到 JetBrain 产品的详细信息:www . jetbrains . com/idea/web help/running-and-debugging-node-js . html

使用 Node 的另一种方法是安装节点检查器。请注意,在撰写本文时,它不再被维护。但是,如果您想尝试一下,请转到命令行并键入NPM install–g node-inspector。–g 标志将确保它是一个全局安装。

安装后,您将需要两个命令行窗口来完成这项工作。在第一个窗口中,可以输入节点–debug filename . js或者节点***–debug-brk filename . js***第二个选项告诉调试器在程序开始时使用断点。

在第二个窗口中,键入节点检查器。你会得到消息“访问 0.0.0.0:8080/debug?port=5858 。"将它复制并粘贴到 Chrome 中,您将获得带有断点的 Chrome 调试器。(参见图 8-9 。)如果您没有得到,而是得到“connect ECONNREFUSED”,请确保您没有在端口 5858 上运行任何其他程序。您可能需要关闭任何打开端口的应用。

图 8-10 显示了 JetBrain 的 IntelliJIdea 中的编辑功能。

9781430250920_Fig08-09.jpg

图 8-9 。用于调试 Node.js 应用的 Chrome 调试器

9781430250920_Fig08-10.jpg

图 8-10 。JetBrain 的 IntelliJIdea 具有 Node.js 编辑和调试功能

摘要

我希望这让您了解了使用 JavaScript 和 XMLHttpRequest 可以在后端和浏览器之间创建动态连接,而无需重新加载页面。另外,我希望这一章能帮助你理解如何通过 Node.js 将你的 JavaScript 技能带到服务器端。

尽管 Ajax 很酷,但有一些事情需要记住:

  • 最初,Ajax 是作为开发 web 应用的方法而发明的,而不是网站。将每一个小表单和菜单“Ajax 化”可能有些过头了。
  • Ajax 是客户端脚本和后端之间的连接器;它的功能取决于后端脚本或信息。
  • Ajax 可以用来连接到与使用它的脚本相同的服务器上的数据源。您还可以使用服务器上的脚本来连接到不同的服务器,或者您可以拥有以 JSON 格式提供数据的第三方服务。最后,如果服务器支持 CORS,您可以连接到其他服务器上的数据。
  • 创建一个看起来令人印象深刻但很突兀的 Ajax 应用是非常诱人和容易的——依赖于鼠标和可用的 JavaScript。创建一个可访问的 Ajax 界面是一项非常困难的任务。

Ajax 非常有用,许多有天赋的开发人员正在开发框架和库,它们可以帮助您快速创建 Ajax 应用,而无需了解它的所有细节——使用它们甚至可以防止您重复这些开发人员过去犯过的错误。可用的库的数量是惊人的,并且很难说哪一个是适合手头任务的。

Node.js 本身并不是一个框架,就像 Rails 对于 Ruby 一样。它确实为您提供了使用 JavaScript 开发任何类型的应用的低级权限。有一些框架可以帮助你组织你的代码和构建像 express(expressjs.com/)这样的大规模应用。

在下一章中,我们将最终仔细研究正则表达式以及如何使用它们来验证数据。您将学习如何创建一个联系人表单作为示例应用,并可能重用这里实现的一些 XHR 功能,使其比一般的联系人表单更加光滑。*

九、数据验证技术

在本章中,你将学习如何使用 JavaScript 来验证用户输入的或来自其他系统的数据。你已经在第二章中听到了很多关于这方面的内容,该章讨论了涉及数据的决策,我们将使用其中的一些知识并在这里进行扩展。

随着 HTML5 的引入,数据验证经历了一些变化。以前,JavaScript 是防止错误数据发送到数据库的第一道防线。现在,浏览器内置的新功能帮助开发人员消除了许多负担,因为浏览器自己完成了大部分验证工作。

这并不是说不应该有任何服务器端验证。有很多服务器端框架都内置了验证功能,所以在将数据提交到数据库之前,没有理由不进行第二次检查。

HTML5 引入了一个名为约束验证的概念,它涉及到给你的数据一些特定的规则或约束。这样,浏览器就可以对照您设置的规则进行检查,并确保数据是正确的。

客户端验证的利与弊

在客户端验证用户输入非常重要,原因有几个:

  • 当用户输入不正确的数据时,它可以省去重新加载页面的麻烦;您可以保存变量的状态,这样用户就不必再次输入所有数据,只需输入不正确的数据。
  • 它减少了服务器流量,因为在出错的情况下没有到后端的往返。
  • 它使界面更加灵敏,因为它给了用户即时的反馈。

另一方面,在客户端验证有几个问题:

  • 它不能作为唯一的验证手段。(JavaScript 可能不可用,甚至可能被故意关闭以规避您的验证措施。当 JavaScript 是验证数据的唯一方式时,就会出现这种情况。)
  • 可能发生的情况是,用户代理没有通知用户对文档的动态更改——对于有视觉障碍的用户来说,非常旧的屏幕阅读器就是这种情况。
  • 如果您不想让您的验证规则可见——比方说,为了防止垃圾邮件或出于身份验证的目的——在 JavaScript 中没有办法做到这一点。

关于用 JavaScript 保护内容的快速提示

验证是一回事,用密码保护内容或通过加密混淆内容是另一回事。如果你环顾网络,你会发现很多例子,承诺你可以用 JavaScript 密码保护网页不被访问。通常,这些脚本是这样的:

examplePassword.html

var pw = prompt('Enter Password', ' ');
if(pw != 'password123') {
  alert('Wrong password');
  window.location = 'boo.html' ;
} else {
  window.location = 'creditCardNumbers.html';
}

解决这种保护的唯一技巧是查看页面源代码,或者——如果保护在它自己的 JavaScript 文件中——在浏览器或文本编辑器中打开它。在某些情况下,无法重定向到正确的页面,而只能重定向到错误的页面,只需关闭 JavaScript 就可以了。

似乎有更聪明的保护方法,使用密码作为文件名的一部分:

var pw = prompt('Enter Password' , ' ');
window.location = 'page' + pw + '.html';

这些可以通过找出服务器上可用的文件来破解——因为要么目录列表没有被关闭(令人惊讶的是,这种情况经常发生——为了证明,只需在“index of /mp3”上进行谷歌搜索——包括引号),要么页面可以在计数器统计数据或浏览器或谷歌的缓存中找到。

这同样适用于混淆(通过加密或替换文字使某些内容不可读)内容和脚本。只要有足够的时间和决心,任何受 JavaScript 保护的东西都可以被破解。只是不要浪费你的时间。

image 注意 JavaScript 是一种大部分时间在客户端计算机上执行的语言,这使得恶意攻击者很容易绕过您的保护方法。在右键单击防护脚本保护图像和文本不被复制的情况下,你很可能会疏远正常的访问者,除了真正的攻击者的干笑之外什么也得不到。

然而,用像迪安·爱德华的打包器(dean.edwards.name/packer/)这样的东西来打包 JavaScript 以使真正沉重的脚本变得更短是另一个问题,并且有时可能是一个好主意——例如,如果你想在一个高流量的网站上使用一个大的库脚本。尽管打包 JavaScript 可能会使文件变小,但是在客户端解压缩文件可能会影响性能。雅虎在 http://yui.github.com/yuicompressor/的 ?? 也有压缩 JavaScript 和 CSS 的工具。

正则表达式

正则表达式帮助您将字符串与字符模式进行匹配,非常适合验证用户输入或更改文档内容。它们不局限于 JavaScript,也存在于其他语言中,如 Perl、PHP 和 UNIX 服务器脚本。它们的功能惊人地强大,如果你与 Perl 或 PHP 爱好者和服务器管理员交谈,你会惊讶地发现,它们经常可以用一个正则表达式代替你用 JavaScript 编写的 50 行的 switch / caseif / else 构造。许多编辑环境还具有“查找”和“搜索和替换”功能,允许使用正则表达式。

正则表达式是猫的睡衣,一旦你把头伸向它们;然而,乍一看,像/[1]+(.这样的建筑[\w]+)*@([\w]+\。)+[a-z]{2,7}$/i(检查一个字符串是否是有效的电子邮件语法)可能会让胆小的人感到害怕。好消息是,这并不像看上去那么难。

语法和属性

假设您想在文本中搜索字符串 cat。您可以将它定义为两种不同格式的正则表达式:

// Expression literals; notice that you must not use quotation marks!
var searchTerm = /cat/;
// Object constructor
var searchTerm = new RegExp('cat');

如果通过 match()、search()、exec()或 test()方法对字符串使用此表达式,它将返回任何包含“cat”的内容,而不管它在字符串中的位置如何,比如 cat alogconcatenationscat

如果您只想将单词“cat”作为一个字符串进行匹配,而周围没有任何其他内容,那么您需要使用^来表示开始,并使用$来表示结束:

var searchTerm = /^cat$/;
var searchTerm = new RegExp('^cat$');

您也可以省略开始指示符^或结束指示符美元。这将匹配 天文望远镜:

var searchTerm = /^cat/;
var searchTerm = new RegExp('^cat');

下面的代码会找到猫 或 :**

var searchTerm = /cat$/;
var searchTerm = new RegExp('cat$');

如果您想要查找“cat”而不考虑大小写,例如,为了匹配 catCatherineCAT ,您需要在第二个斜杠后使用 I 属性。这导致该案例被忽略:

var searchTerm=/cat/i;
var searchTerm=new RegExp('cat', 'i');

如果您有一个字符串,其中可能多次出现单词“cat”,并且您希望将所有匹配作为一个数组,那么您需要为全局添加参数 g:

var searchTerm = /cat/g;
var searchTerm = new RegExp('cat', 'g');

默认情况下,正则表达式只匹配单行字符串中的模式。如果您想要匹配多行字符串中的模式,请使用参数 m 来表示多行。您也可以混合使用它们,顺序并不重要:

var searchTerm = /cat/gim;
var searchTerm = new RegExp('cat', 'mig');

通配符搜索、约束范围和替代项

句点字符(。)在正则表达式中扮演小丑牌的角色;它代表“任何字符”(这可能会引起混淆,因为在高级 web 搜索中或在 DOS 和 UNIX 命令行中,它是星号,*。)

var searchTerm = /c.t/gim;
var searchTerm = new RegExp('c.t', 'mig');

这搭配小床CRT ,甚至还有 c#tc 这样的废话串!T ,或者包含空格的,如 c Tc\tt 。(记住\t 是制表符。)

对于您的需求来说,这可能太灵活了,这就是为什么您可以使用方括号将选择范围限制在您想要提供的范围内:

var searchTerm = /c[aou]t/gim;
var searchTerm = new RegExp('c[aou]t', 'mig');

你可以用这个正则表达式匹配 catcot 或者 cut 的所有大小写版本。您还可以在括号内提供 a-z 这样的范围来匹配所有小写字母,A-Z 匹配所有大写字母,0-9 匹配数字。

image 注意注意正则表达式匹配的是数字的字符,而不是它们的值。带有[0-9]的正则表达式将返回 0200 作为有效的四位数。

例如,如果您想查找一个小写字母紧跟着一个大写字母,您将使用

var searchTerm = /[a-z][A-Z]/g;
var searchTerm = new RegExp(' [a-z][A-Z] ', 'g');

您可以使用括号中的^字符从搜索中排除某个选项。例如,如果您想避免“剪切”,您可以使用

var searchTerm = /c[^u]t/g;
var searchTerm = new RegExp('c[^u]t', 'g');

括号一次只能匹配一个字符,这就是为什么你不能用这个表达式匹配像 costcoastcast 这样的东西。如果要匹配几个选项,可以在括号内使用管道字符(|),其功能类似于逻辑 OR:

var searchTerm = /c(^u|a|o|os|oas|as)t/g;
var searchTerm = new RegExp('c(^u|a|o|os|oas|as)t', 'g');

这现在匹配成本滑行,但不匹配(因为^u).

用量词限制字符数

在许多情况下,您希望允许一定范围的字符,如 a 到 z,但是您希望限制它们的数量。为此,可以在正则表达式中使用量词,如表 9-1 所示。

表 9-1 。正则表达式中的量词符号

符号 可能的次数
* 0 或 1 次
+ 一次或多次
0 或 1 次
n 次
n 到 m 次

image 添加问号(?)意味着正则表达式应该尽可能少地匹配它们。

例如,如果您想匹配由两组四个字符组成的序列号的语法,每组字符用破折号分隔,您可以使用

var searchTerm = /[a-z|0-9]{4}\-[a-z|0-9]{4}/gim;
var searchTerm = new RegExp(' [a-z|0-9]{4}\-[a-z|0-9]{4}', 'mig');

image 注意您需要对字面上使用的字符进行转义,而不是在正则表达式模式中具有任何特殊含义的字符,比如本例中的破折号。您可以通过在字符前加一个反斜杠来做到这一点。需要转义的字符有-、+、/、(、)、[、]、*、{、}和?。比如/c.t/匹配 catcotc4t ,而/c\t/仅匹配 c.t

单词边界、空白和其他快捷键

所有这些不同的选项都会导致非常复杂的正则表达式,这就是为什么有一些快捷符号可用的原因。你可能还记得在第二章中空格的特殊符号,比如\n 代表换行符,\t 代表制表符。正则表达式也是如此,如表 9-2 所示。

表 9-2 。正则表达式的快捷表示法

符号 等效符号 意为
\d [0-9] 仅数字(整数)
\D [⁰-9] 除数字(整数)以外的所有字符
\w [a-zA-Z0-9 的声音] 所有字母数字字符和下划线
\W 【阿扎依采夫-Z0-9】【阿扎依采夫-扎依采夫-扎依采夫-扎依采夫-扎依采夫-扎依采夫-扎依采夫-扎依采夫-扎依采夫-扎依采夫 所有非字母数字字符
\b 不适用的 单词边界
\B 不适用的 非单词边界
\s [\t\n\r\f\v] 所有空白
\S [^\t\n\r\f\v] 没有空白

例如,如果您想测试一个美国社会保险号,它是一个九位数字,第三位和第五位后面有破折号(例如,456-33-1234),您可以使用下面的正则表达式,带有可选的破折号(使用?量词),因为用户可能不会输入它们:

var searchTerm = /[0-9]{3}\-?[0-9]{2}\-?[0-9]{4}/;
var searchTerm = new RegExp('[0-9]{3}\-?[0-9]{2}\-?[0-9]{4}', ");

或者,您可以使用数字的快捷表示法:

var searchTerm = /\d{3}\-?\d{2}\-?\d{4}/;
var searchTerm = new RegExp('\\d{3}\-?\\d{2}\-?\\d{4}', ");

请注意,如果您在引号内或构造函数符号中使用快捷符号,您需要在它们前面加上双反斜杠,而不是单反斜杠,因为您需要对它们进行转义!有了这些知识,您应该能够编写自己的正则表达式。作为证明,让我们回到本节开始段落中的例子:

var validEmail = /^[\w]+(\.[\w]+)*@([\w]+\.)+[a-z]{2,7}$/i

电子邮件可以非常简单,比如 me@example.com,或者更复杂,比如 ??。这个正则表达式应该将两者都作为有效的电子邮件返回。

它测试字符串是否以一组由一个或多个单词字符组成的[2]+开头,后跟一组由 0 个或多个单词字符组成的句点(\。[\w]+)*,在@符号之前。在@符号之后,字符串可能有一组或多组一个或多个单词字符,后跟一个句点,([\w]+\。)+,它将以一个包含 2 到 7 个字符的字符串结束。最后一个字符串是域名,可以是短的,如 de,也可以是长的,如 name 或 museum。请注意,通过允许几个单词后跟一个句点,您还可以确保像 user@open.ac.uk 这样的电子邮件能够被识别。

使用正则表达式的方法

有几种方法将正则表达式作为参数。表达式本身——斜杠或 RegExp 构造函数中的内容——被称为模式,因为它匹配您想要检索或测试的内容。

  • instance.test(string):测试字符串是否与模式匹配,并返回 true 或 false。
  • instance.exec(string):匹配字符串和模式一次,并返回匹配数组或 null。
  • instance.match(pattern):匹配字符串和模式,并将匹配结果作为字符串数组或 null 返回。
  • instance.search(pattern):匹配字符串和模式,并返回正确匹配的位置。如果字符串不匹配任何模式,搜索返回–1。
  • instance.replace(pattern,replaceString):根据模式匹配字符串,并用 replaceString 替换每个正匹配。
  • instance.split(pattern,limit):将字符串与模式匹配,并将其拆分为一个数组,模式匹配周围的子字符串作为数组项。可选的 limit 参数减少了数组元素的数量。

括号分组的威力

你可能记得,要对一个表达式进行分组,需要使用圆括号,()。这不仅对模式进行分组,还将结果存储在特殊变量中,供以后使用。当您将它与 replace()方法结合使用时,这尤其方便。结果存储在名为$1 到$9 的变量中,这意味着您可以在每个正则表达式中使用多达九个括号分组。如果要将某个组排除在外,可以在它前面加上?:.

例如,如果您有一个格式为的姓名列表,并且您想要将列表中的每个条目转换为名姓格式,您可以执行以下操作:

示例名称顺序.html

names=[
  'Reznor, Trent',
  'Eldritch, Andrew',
  'Clark, Anne',
  'Almond,Marc'
];
for(i = 0; i < names.length; i++) {
  alert(names[i].replace(/(\w+)+,\s?(\w+)/g, '$2 $1'));
}

该模式匹配逗号前面的任何单词(后跟可选的空白字符)和后面的单词,并将两者都存储在变量中。替换字符串通过使用$变量颠倒顺序。

一个更复杂的例子是打印链接后面的内容区域的每个外部链接的 URL:

exampleShowURL.html

showURLs = function(){
  var ct = document.getElementById('content');
  var searchTerm = '(<a href="((?:http|https|ftp):\/\/ ';
  searchTerm     += '(?:[\\w]+\.)+[a-z]{2,7})">';
  searchTerm     += '(?:\\w|\\s|\\.)+<\/a>)';
  var pattern = new RegExp(searchTerm, 'mgi');
  ct.innerHTML = ct.innerHTML.replace(pattern, '$1 ($2) ');
}

您用一组括号开始这个模式,将整个结构存储在变量$1 中,并匹配一个链接< a href= "。接下来用一组括号将 href 属性中的内容括起来,href 属性是一个以 http、https 或 ftp 开头的 URL,不将这个组存储在变量中(因为这组括号的前面是?😃,后面是一个冒号和两个斜杠(需要转义),后面是一个以域结尾的 URL。(这与电子邮件检查示例中使用的模式相同。)

您关闭括号,将链接的 href 属性中的所有内容存储在$2 中,并匹配链接元素中的所有内容(可以是一个或多个单词、空白或句点),但不将其存储在变量中。然后在关闭后关闭主组,使用 replace 将每一个环节都替换为模式匹配,这样实际上就把www.example.com>例< /a >变成了<a href =www.example.com>例(www.example.com)。

正则表达式资源

与任何编程语言一样,有许多方法可以达到相同的目标。我不想只给出复制和粘贴的例子,因为熟悉正则表达式会让你走得更远。

有许多在线资源根据它们的任务列出了模式:

  • 正则表达式库(regexlib.com/)有一个可搜索的模式数据库。
  • 在 Regular-expressions . info(www.regular-expressions.info/)网站上,你会找到非常广泛的关于正则表达式的教程。
  • RegEx Advice(regexadvice.com/)有一个很好的关于正则表达式的论坛和博客。

在书籍方面,有迈克尔·菲茨杰拉德(O'Reilly,2012)的介绍正则表达式、托尼·斯塔布尔宾(O'Reilly,2007)的正则表达式袖珍参考,以及杰弗里·弗里德尔(O'Reilly,2006)的非常广泛的掌握正则表达式**。

对于更倾向于 UNIX 的用户,有 Nathan A. Good (Apress,2005)的正则表达式方法:问题解决方法

验证方法总结

在真实的脚本环境中,您不可能坚持使用前面的方法,而是混合使用它们来尽快达到您的目标。什么时候使用什么没有固定的规则,但是一些提示和好处可能值得记住:

  • 正则表达式只匹配字符;您不能用它们进行计算(至少在 JavaScript 中不能;PHP 提供了 e 开关,它将匹配作为 PHP 代码进行评估)。
  • 正则表达式具有独立于语言的优势——您可以在服务器端和客户端使用相同的规则。字符串和数学方法都固定在 JavaScript 中,在其他语言中可能不一样。
  • 在正则表达式中匹配大范围的选项非常容易,字符串很快就会变得混乱,除非这些范围遵循简单的规则,如 A 到 Z 或 0 到 9。
  • 如果你必须验证数字,大多数时候使用字符串或正则表达式验证是不值得的;坚持用数学方法测试这些值。字符串太宽容了,因为您不能用它们来比较值和进行计算。您仅有的选择是确定字符串长度和测试特殊字符。
  • 使用他人开发的开箱即用的模式和方法并不可耻。其中许多已经由数十名开发人员在不同的开发环境中进行了测试。

约束验证

我们现在将讨论一些您可以在表单中使用的技术,以发现哪些字段需要验证,并告诉用户有些地方出错了。

HTML5 为表单添加了新的输入类型,并以新的方式添加了不需要 JavaScript 的验证。它还提供了一个 API(应用编程接口),允许开发人员扩展功能以更好地满足他们的需求。约束验证是让浏览器验证 web 表单内容的方法。

指定必填字段

有几种方法可以将表单元素指定为强制的并要求验证。HTML5 通过向表单字段添加必需的属性使这变得简单。Internet Explorer (IE) 10+,Safari 5,Firefox 16,Chrome 23,Opera 21.1 都支持这个。约束验证的一个好处是浏览器会为您进行验证,当字段为空时会显示一个警告。

输入标记具有所需的属性:

<input type="text" id="firstName" required title="First Name is Required! " >

当试图提交您的表单时,内置验证会根据您的浏览器向您发出类似于图 9-1 所示的警告。

9781430250920_Fig09-01.jpg

图 9-1 。Chrome 在使用 validate 属性的字段上向用户显示警告的例子。Safari 5 和 6 部分支持 required 属性,因为它不会显示警告

通过添加 pattern 属性,可以将正则表达式添加到可能需要进行额外验证的字段中。

例如,您可能希望确保提供的 URL 仅适用于特定的域:

<input type="url" id="theURL" required title="URL is Required! "  pattern="https?://(?:www\.)?twitter\.com/.+"/>

这个例子让用户输入她的 twitter 地址(www.twitter.com/username)。尽管该字段是必需的,但 pattern 属性为其提供了在有效之前需要遵循的附加规则。

附加验证属性

一种新的表单输入类型是数字。您可以使用此类型创建具有最小值和最大值的数字步进器。(参见图 9-2 。)如果您键入一个大于最大值的数字,然后尝试提交表单,内置验证将会发生。

9781430250920_Fig09-02.jpg

图 9-2 。Chrome 在值高于 max 属性时显示警告

以下是数字输入类型的示例:

<input type="number" min="1" max="10" step="1">

placeholder 属性不做任何类型的验证,但是在指导用户如何填写表单时它很有用。结合像电子邮件这样的输入类型,它会非常有帮助。(参见图 9-3 。)

9781430250920_Fig09-03.jpg

图 9-3 。Chrome 显示占位符文本和电子邮件地址无效的警告

以下是电子邮件输入类型的示例:

<input type="email"  placeholder="Enter Email Address">

Novalidate 属性

如果您想要在提交时禁用节点验证,请使用以下命令:

<form novalidate>
        <input type="text" required >
        <input type="submit" value="Submit">
</form>

将 formnovalidate 属性添加到提交按钮将阻止表单在任何输入节点上执行任何验证:

<input type="submit" value="Submit" formnovaidate>

使用 CSS 伪类的附加用户反馈

HTML5 带来了一种一致的验证表单数据的方式,但是向用户指出错误的内置方式在不同的浏览器之间是不同的。要创建更加一致的体验,您可以使用新的级联样式表(CSS)类。

用新的伪 CSS 类可以让用户直观地知道某个字段是强制的。

exampleHTML5Required.html(节选)

<style>
 input:required{
    border: 1px solid #F00;
}

:optional{
    background:#CCC;
}
</style>
  <label>Your Email
  <input required id="email" type="email" placeholder="Enter Email Address" >
 </label>

  <label>Message
  <textarea id="message"></textarea>
 </label>
<button>Submit</button>

在前面的例子中,您可以看到:required 和:optional 已经添加到 CSS 中。在第一个实例中,您要确保任何具有所需属性的输入标记都接收样式。在下一个实例中,它可以是任何不带有所需属性的字段。

当用户填写表单时,即时反馈也很有帮助。可以使用:valid 和:invalid 类让某人知道他们刚刚填写的字段的状态。在这个例子中,当用户移动到下一个字段时,您添加了交互的类。在字段“模糊”状态下,它将运行验证并显示红色或绿色边框,具体取决于信息是否有效。

exampleValidBlur.html(节选)

<style>
 .interacted:invalid{
    border: 1px solid red;
}

.interacted:valid {
    border: 1px solid green;
}
</style>

<script>
        function addInteractedClass(){
     var inputs = document.getElementsByTagName("input");

      for (var I = 0; i< inputs.lengh; i++){
        inputs[i].addEventListener("blur", function(event){
                event.target.classList.add("interacted");
                },false);
                   }
        }

 document.addEventListener("DomContentLoaded", addInteractedClass, false);
</script>

<p><label for="name">Your Name</label></p>
<p><input required id="name" type="text" placeholder="Please Enter Your Name" ></p>

<p><label for="email">Your Email</label></p>
<p><input type="email" id="email" required ></p>

<p><input type="submit" id="send" valie="Send Form" ></p>

除了上两个示例中使用的 CSS 类之外,还有其他一些类可用:

  • 在范围内
  • 溢出
  • 只读
  • 读写

检测对 HTML5 表单属性的支持

并非所有的浏览器都支持每一种新的输入类型。不这样做的浏览器会忽略你给它的类型,就好像类型被设置为“文本”

要检查浏览器是否支持您正在寻找的属性,您可以编写两个函数。第一个将在 DOM 加载时运行,并执行 if 语句。这将调用第二个函数,将属性名和字段作为其参数。

第二个函数基于字段类型创建一个元素,如果属性在该元素中,则返回值 true 或 false。结果返回给第一个函数,它将完成条件语句,并知道应该使用内置浏览器函数还是 JavaScript。

function initFormCheck(){
     if(testHTML5Attr("required","input")){
     //use built in browser validation
     }else{
                //use JavaScript fall back
     }
}
function testHTML5Attr(attr,elm){
         return attr in document.createElement(elm);
}
document.addEventListener("DOMContentLoaded" initFormCheck, false);

约束验证 API

因为浏览器正在做大量的验证工作,所以在向服务器发送任何数据之前,检查信息输入是否正确变得很容易。约束验证 API(www . whatwg . org/specs/we b-apps/current-work/# constraint-validation)向 DOM 节点添加属性和方法,这些属性和方法可以在纯 JavaScript 解决方案中使用。

这一点也很重要,因为尽管 Safari 6 支持验证,但当所需的属性存在时,它不会禁止提交数据。

此示例使用 email 字段的 valid 属性来检查电子邮件地址的格式是否正确,并检查地址是否匹配。

exampleCheckEmail.html(节选)

<script>
    function emailCheck(){
        var email1 = document.getElementById("email1");
        var email2 = document.getElementById("email2");
        var resultDiv = document.getElementById("result");
        var form = document.getElementById("emailForm");

        form.addEventListener("submit", function(event){
          if(email1.validity.valid && email2.validity.valid && email1.value && email2.value){
            resultDiv.innerHTML = "<p>Email is valid and they match</p>";
          }else{
                resultDiv.innerHTML = "<p>Email is not valid or they do not match</p>";
          }
            event.preventDefault();
        }, false);
    }
 document.addEventListener("DomContentLoaded", addInteractedClass, false);
</script>

显示错误字段的列表

该方法向用户显示包含错误的字段列表。(参见图 9-4 。)除了添加边框以显示字段是否有效之外,您还将遍历表单字段以生成所有无效字段的列表。

9781430250920_Fig09-04.jpg

图 9-4 。显示错误字段的列表

页面加载后,向表单添加一个事件侦听器。当表单将要提交数据时,您停止表单并调用 checkInputs 函数。

checkInputs 函数首先查看一个名为 resultsDiv 的 Div,在这里向用户提供反馈并清空该 div 中的所有节点。然后,它遍历所有的输入标签,并检查每个标签的有效属性。

如果标记无效,它将获取 id,通过 reformatName 函数请求格式正确的名称,并继续创建一个段落节点,用文本表示字段的名称无效,并将这个新节点添加到 resultsDiv 中。

showInvalidFields.html(节选)

<script>
    function formCheck(){
        var form = document.getElementById("userForm");
        form.addEventListener("submit", function(event){
                  checkInputs();
                  event.preventDefault();
             }, false);
   }

function checkInputs(){
    var resultDiv = document.getElementById("result");
          resultDiv.hidden = true;
    var inputs = document.getElementsByTagName("input");

  while(resultDiv.hasChildNodes()){
      resultDiv.removeChild(resultDiv.firstChild);
   }

for(var i = 0; i < inputs.length; i++){
  inputs[i].classList.add("interacted");
  if(!inputs[i].validity.valid){
    var para = document.createElement("p");
    var formatedName = reformatName(inputs[i].id);
    var msg = document.createTextNode(formatedName + " is invalid.");
          para.appendChild(msg);
          resultDiv.appendChild(para);
          resultDiv.hidden = false;
      }
   }
}

function reformatName(oldName){
   switch(oldName){
         case "firstName": return "First Name";
         break;
         case "lastName": return "Last Name";
         break;
         case "email": return "Email";
        break;
        case "phone": return "Phone";
        break;
}

document.addEventListener("DomContentLoaded", formCheck, false);
</script>

其他动态验证方法

当用户更改字段时,让每个字段立即生效是非常诱人的,使用 Ajax 和适当的后端数据集或功能可以做很多事情。一个很好的例子是,在输入表单时,建议哪些数据是有效的。

谷歌是网络表单做到这一点的第一个例子。它为你提供了其他用户已经完成的搜索,有许多可能的结果,如图 9-5 所示。

9781430250920_Fig09-05.jpg

图 9-5 。当你输入时,谷歌显示可能的结果

使用 HTML5,向您的站点添加自动完成功能变得很容易。首先,我将介绍它是如何工作的,然后创建一个 JavaScript 版本,它可以被更新以从服务器获取数据。

autoComplete.html(节选)

<input type="text" name="srch" id="srch" list="datalist1">
<datalist id="datalist1">
  <option value="Bill Gates">
  <option value="Linus Torvalds">
  <option value="Douglas Coupland">
  <option value="Ridley Scott">
  <option value="George Lucas">
  <option value="Dan Akroyd">
  <option value="Sigourney Weaver">
  <option value="Tim Burton">
  <option value="Katie Jane Garside">
  <option value="Winona Ryder">
  <option value="Vince Clarke">
  <option value="Martin Gore">
  <option value="Kurt Harland Larson">
  <option value="Paul Robb">
  <option value="James Cassidy">
  <option value="David Tennant">
</datalist>

在这个例子中,一个新的属性被添加到输入标签中。list 属性指向新的 datalist 元素。该元素包含预定义的选项,当用户开始在字段中输入时将显示这些选项,如图 9-6 所示。

9781430250920_Fig09-06.jpg

图 9-6 。提供数据列表中的数据

既然我已经介绍了基础知识,那么创建一个动态示例只需要使用 JavaScript 为我们创建 datalist。如果您可以使用服务器端脚本来提供数据,那么可以通过使用以前课程中的一些 AJAX 技术来调整这个示例。

dynamicAutoComplete.html(节选)

<script>
function createDataList(){
     var nameArray = new Array();
           nameArray[0] = "Bill Gates";
           nameArray[1] = "Linus Torvalds";
           nameArray[2] = "Douglas Coupland";
           nameArray[3] = "Ridley Scott";
           nameArray[4] = "George Lucas";
           nameArray[5] = "Dan Akroyd";
           nameArray[6] = "Sigourney Weaver";
           nameArray[7] = "Tim Burton";
           nameArray[8] = "Katie Jane Garside";
           nameArray[9] = "Winona Ryder";
           nameArray[10] = "Martin Gore";
           nameArray[11] = "Kurt Harland Larson";
           nameArray[12] = "Paul Robb";
           nameArray[13] = "James Cassidy";
           nameArray[14] = "David Tennant";

var dataList = document.createElement("datalist");
      dataList.id = "datalist1";
      document.body.appendChild(dataList);

for(var i = 0; i<nameArray.length; i++){
   var option = document.createElement("option");
         option.value = nameArray[i];
         dataList.appendChild(option);
      }
}

document.addEventListener("DOMContentLoaded", createDataList, false);
</script>

不支持约束验证的浏览器

正如你所看到的,让浏览器来做繁重的工作是一件好事。这确实排除了旧的浏览器,但是有办法处理它们。

在服务器端实现验证增加了第二层保护。如果您的浏览器不支持约束验证,那也没关系,因为服务器会处理它。

Polyfills 是 JavaScript 库,允许您继续使用最新的浏览器改进,并且它们增加了对尚未本机使用它们的浏览器的支持。Paul Irish 维护的 polyfills 有一长串:github . com/Modernizr/Modernizr/wiki/html 5-Cross-Browser-poly fills

Webshims 是 polyfills 的集合,也包括约束验证 API。从 github(【http://afarkas.github.com/webshim/demos/】??)下载 webshims 库并将 JavaScript 添加到您的页面中,这将使您能够将 HTML5 语法添加到您的页面中,并且仍然可以让旧浏览器正常响应。

下面是一个向页面添加 webshims 的示例:

<script src="js/jquery-1.8.2.js"></script>
<script src="js/modernizr-yepnope-custom.js"></script>
<script src="js-webshim/minified/polyfiller.js"></script>
<script>jQuery.webshims.polyfill('forms');</script>
<form>
    <input type="text" requred>
    <input type="submit" value="submit">

</form>

摘要

我希望您对编写正则表达式和使用约束验证有足够的信心。HTML5 让您不必编写复杂的脚本来验证数据。

使用约束验证 API,您可以确保数据有效,并使用新的 CSS 伪类提供可视化反馈,除了 JavaScript 之外,这些伪类还可以应用于表单元素。

这并不意味着您不应该在服务器端进行验证。许多服务器端框架都内置了验证,所以没有理由不使用它。

Polyfills 使您能够在不支持新 HTML5 方法的旧浏览器上使用这些新方法。随着人们升级他们的浏览器,polyfill 将把新的功能留给浏览器。所以你有办法为大量用户创造一致的体验。

在下一章中,我们将进行一个更大的项目,创建一个由后端驱动的动态图库,并加入 CSS、JavaScript 和 Ajax。

十、现代 JavaScript 案例研究:动态图库

在本章中,你将学习如何开发一个由 PHP 脚本支持的 JavaScript 增强的缩略图库。您将从学习与静态图库相关的技术以及如何增强它们开始。然后,您将继续学习使用 PHP 和 Ajax 从服务器动态获取图像的图库。

image 你可以在www.beginningjavascript.com下载本章演示代码或者在线查看结果。因为这一章包含了图片库,所以下载在较大的一边,但是它允许你在本地服务器上看到所有的代码——包括服务器端的 PHP。

缩略图基本知识

让我们从基础开始,计划我们的缩略图画廊。我考虑了很长时间是否应该在这本书里包含一个,因为 JavaScript 和 CSS 书籍有图库作为例子几乎已经成为陈词滥调。然而,我写这一章是为了举例说明如何用现代脚本和 CSS 来丰富一个非常普通的解决方案,比如缩略图画廊,并且独立于它们。许多例子——尤其是只有 CSS 的图库——看起来很棒,在现代浏览器中也能工作;然而,它们不能很好地降解,也不能真正提供缩略图画廊应该提供的东西。

什么是缩略图画廊,它应该做什么?

缩略图画廊的想法可以追溯到浏览器开始支持图像的时代,当时网络连接速度可以用千比特来衡量。这种图库的工作过去是,现在仍然是,通过提供图库中每个图像的较小预览,给出可用图像的概述。“更小”意味着尺寸更小,但也是最重要的是文件大小更小。这意味着只对您图库中的一张图片感兴趣的访问者无需下载所有图片,只需下载他感兴趣的那张即可,既节省了他的时间,也节省了您的服务器流量。许多纯 CSS 或 JavaScript/HTML 缩略图画廊没有做到这一点,并假设每个用户都想下载一张图片。你可以提供所有图片的下载,但这应该是一个选项,而不是一个要求。最差的缩略图画廊通过 HTML 属性或 CSS 将照片调整为缩略图,从而迫使访问者下载大图像,以看到质量差的缩略图。通过改变图像在 CSS 中的尺寸、通过 JavaScript 或使用 HTML 属性来调整图像的大小不会产生高质量的缩略图;这简直是懒惰和一个坏主意。

如果你想提供缩略图画廊在他们原来的意义上,你需要为你想显示的大图像生成较小的缩略图。您可以在上传图库之前进行批处理,也可以在服务器上通过脚本运行。

image 提示有很多缩略图生成和批量生成工具可用。好的——最重要的是,免费的——是谷歌的 Picasa(在 http://picasa.google.com/的有售)和 IrfanView(在 http://www.irfanview.com/有售)。使用 PHP 和 GD 库可以很容易地在服务器上生成缩略图。我已经写了一篇关于如何做到这一点的文章,可以在icant.co.uk/articles/phpthumbnails/获得,在phpthumb.sourceforge.net/可以获得一个很棒的预制 PHP 类 phpThumb()。因为这是一本关于 JavaScript 的书,所以我不会深入讨论通过 PHP 生成图像的细节,尽管它对于在线画廊来说非常方便。

静态缩略图库

传统的缩略图画廊提供表格或列表中的小缩略图。每个缩略图链接到一个带有大图像的页面,反过来,链接回缩略图画廊或提供上一个和下一个图像链接。

如果有很多图像,缩略图页面可以分页,一次显示一定数量的缩略图,并提供整个集合的向前和向后导航。对于纯静态图库,这意味着您必须生成所有缩略图页面,每张照片一个缩略图页面,这在开始时需要做大量工作,并且在图库每次更新时需要向服务器传输大量文件。

用 JavaScript 伪造动态图库

通过对所有缩略图应用事件处理程序,您可以使用 JavaScript 将静态缩略图画廊转变为看似动态的画廊。当一个缩略图被点击时,你用一个包含大图像的新元素覆盖缩略图。通过将缩略图链接到大图并简单地在浏览器中显示,保持非 JavaScript 用户可以访问图库:

exampleFakeDynamic.html(节选)

<ul id="thumbs">
  <li>
    <a href="galleries/animals/dog2.jpg">
      <img src="galleries/animals/tn_dog2.jpg" alt="tn_dog2.jpg">
    </a>
  </li>
  <li>
    <a href="galleries/animals/dog3.jpg">
      <img src="galleries/animals/tn_dog3.jpg" alt="tn_dog3.jpg">
    </a>
  </li>
  <li>
    <a href="galleries/animals/dog4.jpg">
      <img src="galleries/animals/tn_dog4.jpg" alt="tn_dog4.jpg">
    </a>
  </li>
  [... more thumbnails ...]
</ul>

image 提示你也可以使用表格或定义列表作为缩略图库,因为表格会降级得更好,因为即使在非 CSS 浏览器中它们仍然是多列结构,而且定义列表在语义上也是正确的。对于本章中的例子,我使用了一个简单的列表来保持简单,并允许缩略图占据屏幕上尽可能多的空间。

您可以通过打开演示 exampleFakeDynamic.html 来测试效果。让我们从脚本的框架开始,一步一步地了解功能:

fakeDynamic.js (skeleton)

fakegal = {
  // IDs
  thumbsListID : 'thumbs',
  largeContainerID : 'photo',
  // CSS classes
  closeClass : 'close',
  nextClass : 'next',
  prevClass : 'prev',
  hideClass : 'hide',
  showClass : 'show',
  // Labels
  closeLabel : 'close',
  prevContent : '<img src="last.jpg" alt="previous photo" >',
  nextContent : '<img src="next.jpg" alt="next photo">',

  init : function(){  },
  createContainer : function(){},
  showPic : function(e){  },
  setPic : function(pic){ },
  navPic : function(e){  }
DOMhelp.addEvent(window, 'load', fakegal.init, false);

你需要

  • 包含所有缩略图的元素的 ID
  • 分配给大图片容器的 ID
  • CSS 类,用于移除大图片的链接
  • 浏览大图的链接
  • 显示和隐藏元素的类
  • 告诉用户链接隐藏了大图的标签
  • 下一个和上一个图片链接的标签

在方法方面,你需要

  • 一种初始化功能的方法
  • 最初创建图像容器的实用程序方法
  • 一种显示图片的方法
  • 设置要显示的图片的方法
  • 导航到下一张或上一张图片的方法

设置要显示的图片的方法 setPic()是必需的,因为显示方法 showPic()和导航方法 navPic()都会更改容器中的图像。

fakeDynamic.js

fakegal = {
  // IDs
  thumbsListID : 'thumbs',
  largeContainerID : 'photo',
  // CSS classes
  closeClass : 'close',
  nextClass : 'next',
  prevClass : 'prev',
  hideClass : 'hide',
  showClass : 'show',
  // Labels
  closeLabel : 'close',
  prevContent : '<img src="last.jpg" alt="previous photo">',
  nextContent : '<img src="next.jpg" alt="next photo" >',
  init:function() {
    if(!document.getElementById || !document.createTextNode) {
      return;
    }
    fakegal.tlist = document.getElementById(fakegal.thumbsListID);
    if(!fakegal.tlist){ return; }
    var thumbsLinks = fakegal.tlist.getElementsByTagName('a');
    fakegal.all = thumbsLinks.length;
    for(var i = 0 ; i < thumbsLinks.length; i++) {
      DOMhelp.addEvent(thumbsLinks[i], 'click', fakegal.showPic, false);
      thumbsLinks[i].onclick = DOMhelp.safariClickFix;
      thumbsLinks[i].i = i;
    }
    fakegal.createContainer();
  },

init()方法测试 DOM 是否受支持,并检索包含缩略图的元素。然后,在将所有链接的数量存储在一个名为 all 的属性中之后,它遍历所有链接。(这在后面是必要的,以避免最后一个图像上的下一个链接。)它将指向 showPic()的事件处理程序应用于缩略图列表中的每个链接,并在调用 createContainer()将必要的图像容器元素添加到文档之前,将其索引号存储在名为 I 的新属性中。

fakeDynamic.js(续)

createContainer : function() {
  fakegal.c = document.createElement('div');
  fakegal.c.id = fakegal.largeContainerID;

通过创建一个新的 DIV 元素,将它存储在一个名为 c 的属性中,并为它分配一个大的图像容器 ID,来启动 createContainer()方法。

fakeDynamic.js(续)

var p = document.createElement('p');
var cl = DOMhelp.createLink('#', fakegal.closeLabel);
cl.className = fakegal.closeClass;
p.appendChild(cl);
DOMhelp.addEvent(cl, 'click', fakegal.setPic, false);
cl.onclick = DOMhelp.safariClickFix;
fakegal.c.appendChild(p);

创建一个新段落,并在其中插入一个链接,将 closeLabel 作为文本内容。将指向 setPic()的事件处理程序分配给链接,应用 Safari 修复,并将段落添加到容器元素。

fakeDynamic.js(续)

var il = DOMhelp.createLink('#', ' ');
DOMhelp.addEvent(il, 'click', fakegal.setPic, false);
il.onclick = DOMhelp.safariClickFix;
fakegal.c.appendChild(il);

现在,使用调用 setPic()的事件处理程序向容器添加另一个空链接。这个链接以后会围绕在大图的周围,使它可以点击,这样键盘用户就可以去掉它。

fakeDynamic.js(续)

fakegal.next = DOMhelp.createLink('#', ' ');
fakegal.next.innerHTML = fakegal.nextContent;
fakegal.next.className = fakegal.nextClass;
DOMhelp.addEvent(fakegal.next, 'click', fakegal.navPic, false);
fakegal.next.onclick = DOMhelp.safariClickFix;
fakegal.c.appendChild(fakegal.next);

fakegal.prev = DOMhelp.createLink('#', ' ');
fakegal.prev.innerHTML = fakegal.prevContent;
fakegal.prev.className = fakegal.prevClass;
DOMhelp.addEvent(fakegal.prev, 'click', fakegal.navPic, false);
fakegal.prev.onclick = DOMhelp.safariClickFix;
fakegal.c.appendChild(fakegal.prev);

还需要添加两个链接来分别显示前一个和下一个图像,两个链接的事件处理程序都指向 navPic()。

fakeDynamic.js(续)

  fakegal.tlist.parentNode.appendChild(fakegal.c);
}

将新容器添加到缩略图列表的父节点,然后就可以开始展示了。

fakeDynamic.js(续)

showPic : function(e) {
  var t = DOMhelp.getTarget(e);
  if(t.nodeName.toLowerCase() != 'a') {
    t = t.parentNode;
  }
  fakegal.current = t.i;
  var largePic = t.getAttribute('href');
  fakegal.setPic(largePic);
  DOMhelp.cancelClick(e);
},

在事件侦听器方法 showPic()中,检索目标并通过测试节点名来确定它是否真的是一个链接。然后将分配给 init()方法中每个缩略图链接的 I 属性存储为主对象的新属性 current 的值,以告知所有其他方法当前显示的是哪张图片。在通过 cancelClick()停止浏览器跟踪链接之前,检索链接的 href 属性,并使用 href 作为参数调用 setPic()方法。

fakeDynamic.js(续)

setPic : function(pic) {
  var a;
  var picLink = fakegal.c.getElementsByTagName('a')[1];
  picLink.innerHTML = ' ';

setPic()方法获取图像容器中的第二个链接(结束链接之后的链接),并通过将其 innerHTML 属性设置为空字符串来删除该链接可能包含的任何内容。这是必要的,以避免有一个以上的图片显示在同一时间。

fakeDynamic.js(续)

if(typeof pic == 'string') {
   fakegal.c.className = fakegal.showClass;
   var i = document.createElement('img');
   i.setAttribute('src' , pic);
   picLink.appendChild(i);

您将参数 pic 的类型与 string 进行比较,因为该方法可以使用 URL 作为参数来调用,也可以不使用 URL。如果有一个参数是有效的字符串,您可以将 show 类添加到容器中,以便向用户显示它,并添加一个新的图像,将 pic 参数作为它的源。

fakeDynamic.js(续)

} else {
  fakegal.c.className = ' ';
}

如果没有 string 类型的参数,则从图片容器中移除任何类,从而隐藏它。

fakeDynamic.js(续)

a = fakegal.current == 0 ? 'add' : 'remove';
  DOMhelp.cssjs(a, fakegal.prev, fakegal.hideClass);
  a = fakegal.current == fakegal.all-1 ? 'add' : 'remove';
  DOMhelp.cssjs(a, fakegal.next, fakegal.hideClass);
},

测试主对象的当前属性是否等于 0,如果是,则隐藏上一个图片链接。对下一个图片链接做同样的操作,并将当前的与所有缩略图的数量(存储在 all 中)进行比较。通过添加或删除 Hide 类来隐藏或显示每个链接。

fakeDynamic.js(续)

  navPic : function(e) {
    var t = DOMhelp.getTarget(e);
    if(t.nodeName.toLowerCase() != 'a') {
      t = t.parentNode;
    }
    var c = fakegal.current;
    if(t == fakegal.prev) {
      c -= 1;
    } else {
      c += 1;
    }
    fakegal.current = c;
    var pic = fakegal.tlist.getElementsByTagName('a')[c];
    fakegal.setPic(pic.getAttribute('href'));
    DOMhelp.cancelClick(e);
  }
}
DOMhelp.addEvent(window, 'load', fakegal.init, false);

检索对所单击链接的引用(通过 DOMhelp 的 getTarget()获取事件目标,并确保 nodeName 是 A),并通过将此节点与 prev 属性中存储的节点进行比较来确定该链接是否是上一个链接。根据激活的链路增加或减少电流。然后用新的当前链接的 href 属性调用 setPic(),通过调用 cancelClick()阻止浏览器跟随激活的链接。

剩下的就是添加一个样式表;结果可能如图 10-1 所示。

9781430250920_Fig10-01.jpg

图 10-1 。使用 JavaScript 模拟服务器控制的动态图库

显示字幕

缩略图画廊是视觉构造,但考虑替代文本和图像标题仍然是一个好主意。这不仅可以让盲人用户访问你的图库,还可以通过搜索引擎搜索缩略图数据并建立索引。

许多工具,如谷歌的 Picasa,允许动态字幕和添加替代文本。您可以使用 XHR 创建类似的东西,但是因为这是一本关于 JavaScript 的书,并且如何在服务器上存储输入的数据需要一些解释,所以这不是一个相关的例子。相反,让我们修改“假”画廊,使其显示标题。

您将使用图像的标题属性作为图像的标题;这意味着静态 HTML 需要适当的替代文本和标题数据。

examplefakedyname . html(excerpt)

<ul id="thumbs">
  <li>
    <a href="galleries/animals/dog2.jpg">
      <img src="galleries/animals/tn_dog2.jpg" title="This square is mine" alt="Dog in a shady square">
    </a>
  </li>
  <li>
    <a href="galleries/animals/dog3.jpg">
      <img src="galleries/animals/tn_dog3.jpg" title="Sleepy bouncer" alt="Dog on the steps of a shop">
    </a>
  </li>
  [... More thumbnails ...]
</ul>

剧本本身没必要改动太多;它所需要的只是在生成的图像容器中添加一个额外的段落,并修改将标题和可选文本数据发送到大图像容器的方法。

fakeddynamically . js

fakegal = {
  // IDs
  thumbsListID : 'thumbs',
  largeContainerID : 'photo',
  // CSS classes
  closeClass : 'close',
  nextClass : 'next',
  prevClass : 'prev',
  hideClass : 'hide',
  closeLabel : 'close',
  captionClass : 'caption',
  // Labels
  showClass : 'show',
  prevContent : '<img src="last.jpg" alt="previous photo">',
  nextContent : '<img src="next.jpg" alt="next photo">',

第一个变化是修饰性的:您添加了一个将应用于标题的新 CSS 类。

fakeDynamicAlt.js(续)

init : function() {
  if(!document.getElementById || !document.createTextNode) {
    return;
  }
  fakegal.tlist = document.getElementById(fakegal.thumbsListID);
  if(!fakegal.tlist) { return; }
  var thumbsLinks = fakegal.tlist.getElementsByTagName('a');
  fakegal.all = thumbsLinks.length;
  for(var i = 0; i < thumbsLinks.length; i++) {
    DOMhelp.addEvent(thumbsLinks[i], 'click', fakegal.showPic, false);
    thumbsLinks[i].onclick = DOMhelp.safariClickFix;
    thumbsLinks[i].i = i;
  }
  fakegal.createContainer();
},
showPic : function(e) {
  var t = DOMhelp.getTarget(e);
  if(t.nodeName.toLowerCase() != 'a') {
    t = t.parentNode;
  }
  fakegal.current = t.i;
  var largePic = t.getAttribute('href');
  var img = t.getElementsByTagName('img')[0];
  var alternative = img.getAttribute('alt');
  var caption = img.getAttribute('title');
  fakegal.setPic(largePic, caption, alternative);
  DOMhelp.cancelClick(e);
},

init()方法保持不变,但是 showPic()方法需要读取图像的可选文本和 title 属性,以及链接的 href 属性,并将这三个属性作为参数发送给 setPic()。

fakeDynamicAlt.js(续)

setPic : function(pic, caption, alternative) {
  var a;
  var picLink = fakegal.c.getElementsByTagName('a')[1];
  picLink.innerHTML = ' ';
  fakegal.caption.innerHTML = ' ';
  if(typeof pic == 'string') {
    fakegal.c.className = fakegal.showClass;
    var i = document.createElement('img');
    i.setAttribute('src', pic);
    i.setAttribute('alt' ,alternative);
    picLink.appendChild(I);
  } else {
    fakegal.c.className = ' ';
  }
  a = fakegal.current == 0 ? 'add' : 'remove';
  DOMhelp.cssjs(a, fakegal.prev, fakegal.hideClass);
  a = fakegal.current == fakegal.all-1 ? 'add' : 'remove';
  DOMhelp.cssjs(a, fakegal.next, fakegal.hideClass);
  if(caption != ' ') {
   var ctext = document.createTextNode(caption);
    fakegal.caption.appendChild(ctext);
  }
},

setPic()方法现在接受三个参数,而不是一个——大图片的来源、标题和可选文本。该方法需要删除任何可能已经可见的标题,设置大图片的可选文本属性,并显示新标题。

fakeDynamicAlt.js(续)

navPic : function(e) {
  var t = DOMhelp.getTarget(e);
  if(t.nodeName.toLowerCase() != 'a') {
    t = t.parentNode;
  }
  var c = fakegal.current;
  if(t == fakegal.prev) {
    c -= 1;
  } else {
    C += 1;
  }
  fakegal.current = c;
  var pic = fakegal.tlist.getElementsByTagName('a')[c];
  var img = pic.getElementsByTagName('img')[0];
  var caption = img.getAttribute('title');
  var alternative = img.getAttribute('alt');
  fakegal.setPic(pic.getAttribute('href'), caption, alternative);
  DOMhelp.cancelClick(e);
},

navPic()方法与 init()方法一样,需要检索大图片的可选文本、标题和来源,并将它们发送给 setPic()。

fakeDynamicAlt.js(续)

createContainer : function() {
  fakegal.c = document.createElement('div');
  fakegal.c.id = fakegal.largeContainerID;

  var p = document.createElement('p');
  var cl = DOMhelp.createLink('#', fakegal.closeLabel);
  cl.className = fakegal.closeClass;
  p.appendChild(cl);
  DOMhelp.addEvent(cl, 'click', fakegal.setPic, false);
  cl.onclick = DOMhelp.safariClickFix;
  fakegal.c.appendChild(p);

  var il = DOMhelp.createLink('#', ' ');
  DOMhelp.addEvent(il, 'click', fakegal.setPic, false);
  il.onclick = DOMhelp.safariClickFix;
  fakegal.c.appendChild(il);

  fakegal.next = DOMhelp.createLink('#', ' ');
  fakegal.next.innerHTML = fakegal.nextContent;
  fakegal.next.className = fakegal.nextClass;
  DOMhelp.addEvent(fakegal.next, 'click', fakegal.navPic, false);
  fakegal.next.onclick = DOMhelp.safariClickFix;
  fakegal.c.appendChild(fakegal.next);

  fakegal.prev = DOMhelp.createLink('#', ' ');
  fakegal.prev.innerHTML = fakegal.prevContent;
  fakegal.prev.className = fakegal.prevClass;
  DOMhelp.addEvent(fakegal.prev, 'click', fakegal.navPic, false);
  fakegal.prev.onclick = DOMhelp.safariClickFix;
  fakegal.c.appendChild(fakegal.prev);

  fakegal.caption = document.createElement('p');
  fakegal.caption.className = fakegal.captionClass;
  fakegal.c.appendChild(fakegal.caption);

  fakegal.tlist.parentNode.appendChild(fakegal.c);
}
}
DOMhelp.addEvent(window, 'load', fakegal.init, false);

createContainer()方法只需要一个小改动,即在容器中创建一个新段落来存放标题。

如您所见,用 JavaScript 创建动态图库意味着生成大量 HTML 元素,并读写大量属性。这是由你来决定是否值得争论。当您想要提供缩略图分页时,情况会变得更糟。

不要用 JavaScript 做所有这些,你可以在后端做(例如,用 PHP 或 Ruby on Rails 或 Node.js ),为所有用户提供一个全功能的图库,并且只通过 XHR 和 JavaScript 改进它。

动态缩略图画廊

真正的动态缩略图画廊使用 URL 参数,而不是大量的静态页面,并根据这些参数创建分页和显示缩略图或大图像。

演示 examplePHPgallery.php 就是这样工作的,图 10-2 显示了它可能的样子。

9781430250920_Fig10-02.jpg

图 10-2 。一个动态的 PHP 驱动的缩略图库示例,具有缩略图分页和大图片页面上的上一张和下一张图像预览

这个图库功能齐全,无需 JavaScript 即可访问,但是您可能不希望每次用户单击缩略图时都重新加载整个页面。使用 XHR,你可以两者兼得。您没有使用原始的 PHP 文档,而是使用了一个精简版本,只生成您需要的内容——在本例中是 gallerytools.php。我不会深入 PHP 脚本的细节;可以说它为您做了以下事情:

  • 它读取主菜单中的链接指向的文件夹的内容,检查它是否有图像,并一次返回 10 个 HTML 列表,缩略图链接到大图像。
  • 它增加了一个分页菜单,显示总共显示了多少张图片中的哪十张,并提供了上一页和下一页的链接。
  • 如果单击任何缩略图,它会返回大图像的 HTML 和显示下一个和上一个缩略图的菜单。

您使用这个输出来覆盖原始 PHP 脚本的 HTML 输出,如演示 examplePHPXHRgallery.php 中所示。没有 JavaScript,它做的和 examplePHPgallery.php 一样;但是,当 JavaScript 可用时,它不会重新加载整个文档,而只会刷新图库本身。您可以通过将内容部分的链接替换为 gallerytools.php 和 XHR 电话的链接来实现这一点,而不是重新加载整个页面。

dyngal_xhr.js

dyngal = {
  contentID : 'content',
  originalPHP : 'examplePHPXHRgallery.php',
  dynamicPHP : 'gallerytools.php',
   init : function() {
     if(!document.getElementById || !document.createTextNode) {
       return;
     }
     dyngal.assignHandlers(dyngal.contentID);
   },

首先定义您的属性:

  • 元素的 ID,该元素包含应该用从 gallerytools.php 返回的 HTML 替换的内容
  • 原始脚本的文件名
  • 返回通过 XHR 调用的数据的脚本的文件名

init()方法测试 DOM 支持,并使用内容元素 ID 作为参数调用 assignHandlers()方法。

image 注意在这种情况下,你只替换一个内容元素;但是,因为可能会出现需要替换页面的许多部分的情况,所以为这样的任务创建单独的方法是一个好主意。

dyngal_xhr.js(续)

assignHandlers : function(o) {
  if(!document.getElementById(o)){ return; }
  o = document.getElementById(o);
  var gLinks = o.getElementsByTagName('a');
  for(var i = 0; i < gLinks.length; i++) {
    DOMhelp.addEvent(gLinks[i], 'click', dyngal.load, false);
    gLinks[i].onclick = DOMhelp.safariClickFix;
  }
},

方法测试 ID 作为参数发送的元素是否存在,然后遍历元素中的所有链接。接下来,它添加一个指向 load 方法的事件处理程序,并应用 Safari 修复程序来阻止浏览器跟踪原始链接。(记住 cancelClick()中使用的 preventDefault()方法是 Safari 支持的,但是由于 Safari 中的一个 bug,它并不能阻止链接被关注。)

dyngal_xhr.js(续)

load : function(e) {
  var t = DOMhelp.getTarget(e);
  if(t.nodeName.toLowerCase() != 'a') {
    t = t.parentNode;
  }
  var h = t.getAttribute('href');
  h = h.replace(dyngal.originalPHP, dyngal.dynamicPHP);
  dyngal.doxhr(h, dyngal.contentID);
  DOMhelp.cancelClick(e);
},

在 load 方法中,检索事件目标并确保它是一个链接。然后读取链接的 href 属性,用只返回所需内容的动态名称替换原来的 PHP 脚本名称。使用 href 值和内容元素 ID 作为参数调用 doxhr()方法,并通过调用 cancelClick()停止链接传播。

dyngal_xhr.js(续)

  doxhr : function(url, container) {
    var request;
    try{
      request = new XMLHttpRequest();
    } catch(error) {
      try {
        request = new ActiveXObject("Microsoft.XMLHTTP");
      } catch (error) {
        return true;
      }
    }
    request.open('get', url, true);
    request.onreadystatechange = function() {
      if(request.readyState == 1) {
        container.innerHTML = 'Loading... ';
      }
      if(request.readyState == 4) {
        if(request.status && /200|304/.test(request.status)) {
          dyngal.retrieved(request, container);
        } else {
          dyngal.failed(request);
        }
      }
    }
    request.setRequestHeader('If-Modified-Since', 'Wed, 05 Apr 2013 00:00:00 GMT');
    request.send(null);
    return false;
  },
  retrieved : function(request, container) {
    var data = request.responseText;
    document.getElementById(container).innerHTML = data;
    dyngal.assignHandlers(container);
  },
  failed : function(request) {
    alert('The XMLHttpRequest failed. Status: ' + requester.status);
    return true;
  }
}
DOMhelp.addEvent(window, 'load', dyngal.init, false);

XHR 方法与你在第八章中使用的方法相同。唯一的区别是您需要再次调用 assignHandlers()方法,因为您替换了原始内容,结果丢失了链接上的事件处理程序。

image 注意拥有服务器端语言和 JavaScript 是一个强大的组合。一旦掌握了 JavaScript,学习 PHP 或 Node.js 之类的语言可能是个好主意,因为不了解服务器端语言 Ajax 就没什么意思了。服务器端语言可以做 JavaScript 做不到的事情,比如访问服务器上的文件并读取它们的名称和属性,甚至可以访问来自第三方服务器的内容。PHP 的语法在某些方面与 JavaScript 相似,Node.js 就是 JavaScript。

从文件夹创建图像徽章

在下一章查看一些现成的第三方代码和在线服务之前,让我们先来看看另一个使用 PHP 和 JavaScript/XHR 的小型图库示例。

这个练习将允许用户通过上一个和下一个链接浏览缩略图,并通过单击缩略图显示大照片。演示 exampleBadge.html 做到了这一点,图 10-3 显示了它在两个徽章图库中的样子。

9781430250920_Fig10-03.jpg

图 10-3 。两个图像文件夹作为徽章图库

当创建这样的脚本时,尽可能使 HTML 简单是一个好主意。你对维护者的期望越低,人们就越有可能使用你的脚本。在这种情况下,维护人员要向 HTML 文档中添加徽章库,只需添加一个元素,该元素包含类徽章和一个指向包含图像的文件夹的链接:

exampleBadge.html(节选)

<p class="badge"><a href="galleries/animals/">Animals</a></p>
<p class="badge"><a href="galleries/buildings/">Buildings</a></p>

因为 JavaScript 无法检查服务器上的文件夹中的文件,所以您需要一个 PHP 脚本来完成这项工作。文件 badge.php 会这样做,并将缩略图作为列表项返回。

image 以下是 PHP 脚本的快速解释。这不是 JavaScript,但是我希望您能够理解即将到来的 badge 脚本所使用的工具的工作原理。

徽章. php

<?php
$c = preg_match('/\d+/', $_GET['c']) ? $_GET['c'] : 5;
$s = preg_match('/\d+/', $_GET['s']) ? $_GET['s'] : 0;
$cd = is_dir($_GET['cd']) ? $_GET['cd'] : ' ';

你定义三个变量:\(c,存储要显示的缩略图数量;\)s,它是所有缩略图列表中当前第一个缩略图的索引;和\(cd,它是服务器上的文件夹 URL。PHP 的\)_GET 数组存储了 URL 的所有参数,这意味着如果 URL 是 badge.php?c = 3&s = 0&CD =动物,\(_GET['c']会是 3,\)_GET['s']会是 0,\(_GET['cd']会是动物。您可以使用正则表达式来确保\)c 和\(s 是整数,并分别预设为 5 和 0,并使用 PHP 的 is_dir()函数来确保\)cd 确实是一个可用的文件夹。

badge.php(续)

if($cd != ' ') {
  $handle = opendir($cd);
   if(preg_match('/^tn_.*(jpg|jpe|jpeg)$/i', $file)) {
      $images[] = $file;
    }
  }
  closedir($handle);

如果文件夹是可用的,您开始使用 opendir()方法读出文件夹中的每个文件,并通过将文件名与模式^tn_.进行匹配来测试该文件是否是缩略图*(jpg|jpe|jpeg)$(以 tn_ 开头,以 jpg、jpe 或 jpeg 结尾)。如果文件是缩略图,将其添加到图像数组中。当文件夹中没有文件时,通过调用 closedir()方法关闭文件夹。

badge.php(续)

$imgs = array_slice($images, $s, $c);
if($s > 0) {
  echo '<li class="badgeprev"> ';
  echo '<a href="badge.php?c='.$c;
  echo '&amp;s=' . ($s-$c) . '&amp;cd='.$cd. ' ">';
  echo 'previous</a></li>';
} else {
  echo '<li class="badgeprev"><span>previous</span></li>';
}

您使用 PHP 的 array_slice()方法将数组缩减到选定的图像(从\(s 开始的 c 图像)并测试\)s 是否大于 0。如果是,用 badgeprev 类写出一个列表元素,它在链接的 href 属性中有正确的参数。如果不是,在列表项中写一个 SPAN,而不是一个链接。

badge.php(续)

for($i=0; $i<sizeof($imgs); $i++) {
  echo '<li><a href="'.str_replace('tn_', ' ',$cd.$imgs[$i]). ' "> '.
  '<img src="' . $cd . $imgs[$i] . ' " alt="' . $imgs[$i] . ' " /></a></li>';
}

遍历图像,并在指向每个数组项的大图像的链接中显示一个 IMG 元素。通过用 str_replace()移除数组元素值的 tn_ string,可以检索到大图像的链接。

badge.php(续)

  if(($c+$s) <= sizeof($images)) {
    echo '<li class="badgenext">';
    echo '<a href="badge.php?c=' . $c . '&amp;s=' . ($s + $c);
    echo '&amp;cd=' . $cd . '">next</a></li>';
  } else {
    echo '<li class="badgenext"><span>next</span></li>';
  }
}
?>

测试\(c 和\)s 的总和是否小于文件夹中所有图像的数量,如果是,则显示一个链接,否则显示一个 SPAN。

如您所见,JavaScript 和 PHP 的编程语法和逻辑非常相似,这也是 PHP 成功的原因之一。现在让我们创建 JavaScript,它使用这个 PHP 脚本将链接转换成图像标记。

badge.js

badge = {
  badgeClass : 'badge',
  containerID : 'badgecontainer',

您可以定义用于指定徽章链接的 CSS 类和显示大图片的图像容器的 ID,作为主对象徽章的属性。

badge.js(续)

init : function() {
  var newUL, parent, dir, loc;
  if(!document.getElementById || !document.createTextNode) {
    return;
  }
  var links = document.getElementsByTagName('a');
  for(var i = 0; i < links.length; i++) {
    parent = links[i].parentNode;
    if(!DOMhelp.cssjs('check', parent, badge.badgeClass)) {
      continue;
    }

测试 DOM 支持并遍历文档中的所有链接,测试特定链接的父节点是否分配了 badge 类。如果没有,你跳过这个链接。

badge.js(续)

  newUL = document.createElement('ul');
  newUL.className = badge.badgeClass;
  dir=links[i].getAttribute('href');
  loc = window.location.toString().match(/(^.*\/)/g);
  dir = dir.replace(loc, ' ');
  badge.doxhr('badge.php?cd=' + dir, newUL);
  parent.parentNode.insertBefore(newUL, parent);
  parent.parentNode.removeChild(parent);
  i--;
}

您创建了一个新的列表元素,并向其中添加了 badge 类。检索链接的 href 属性,读取窗口位置,并从 href 属性值中删除 window.location 中最后一个/之前的任何内容。

您使用正确的 URL 和新创建的列表作为参数调用 doxhr()方法,并将列表添加到当前链接的父元素之前。然后用 DOM 方法 removeChild()删除链接的父元素,并将循环计数器减 1。(你循环遍历文档的所有链接,这意味着当你移除其中一个链接时,计数器需要递减,以阻止循环跳过下一个链接。)

badge.js(续)

  badge.container = document.createElement('div');
  badge.container.id = badge.containerID;
  document.body.appendChild(badge.container);
},

创建一个新的 DIV 作为大图像的容器,设置它的 ID,并将其添加到文档的主体。

badge.js(续)

doxhr : function(url, container) {
  var request;
  try {
    request = new XMLHttpRequest();
  } catch (error) {
    try {
      request = new ActiveXObject("Microsoft.XMLHTTP");
    } catch (error) {
      return true;
    }
  }
  request.open('get', url, true);
  request.onreadystatechange = function() {
    if(request.readyState == 1) {
  }
  if(request.readyState == 4) {
      if(request.status && /200|304/.test(request.status)) {
        badge.retrieved(request, container);
      } else{
        badge.failed(request);
      }
    }
  }
  request.setRequestHeader('If-Modified-Since', 'Wed, 02 Jan 2013 00:00:00 GMT');
  request.send(null);
  return false;
},
retrieved : function(request, container) {
  var data = request.responseText;
  container.innerHTML = data;
  badge.assignHandlers(container);
},
failed : function(requester) {
  alert('The XMLHttpRequest failed. Status: ' + requester.status);
  return true;
},

Ajax/XHR 方法在很大程度上保持不变,唯一的区别是当数据被成功检索时,调用 assignHandlers()方法,并将列表项作为参数。

badge.js(续)

assignHandlers : function(o) {
  var links = o.getElementsByTagName('a');
  for(var i = 0; i < links.length; i++) {
    links[i].parent = o;
    if(/badgeprev|badgenext/.test(links[i].parentNode.className)) {
      DOMhelp.addEvent(links[i], 'click', badge.load, false);
    } else {
      DOMhelp.addEvent(links[i], 'click', badge.show, false);
    }
  }
},

assignHandlers()方法遍历作为参数 o 发送的元素中的所有链接。它将该元素存储为每个名为 parent 的链接中的新属性,并测试该链接是否具有 badgeprev 或 badgenext 类,您可能还记得,这两个类是由 badge.php 添加到上一个和下一个链接中的。如果 CSS 类在那里,assignHandlers()添加一个指向 load 方法的事件处理程序;否则,它会添加一个指向 show 方法的事件处理程序,因为有些链接需要浏览缩略图,而其他链接需要显示大图像。

badge.js(续)

load : function(e) {
  var t = DOMhelp.getTarget(e);
  if(t.nodeName.toLowerCase() != 'a') {
    t = t.parentNode;
  }
  var dir = t.getAttribute('href');
  var loc = window.location.toString().match(/(^.*\/)/g);
  dir = dir.replace(loc, ' ');
  badge.doxhr('badge.php?cd=' + dir, t.parent);
  DOMhelp.cancelClick(e);
},

load 方法检索事件目标并确保它是一个链接。它检索事件目标的 href 属性值,并在调用 doxhr 方法之前对其进行清理,并将链接的 parent 属性中存储的元素作为输出容器。通过调用 DOMhelp 的 cancelClick()来阻止链接被跟踪。

badge.js(续)

show : function(e) {
  var t = DOMhelp.getTarget(e);
  if(t.nodeName.toLowerCase() != 'a') {
    t = t.parentNode;
  }
  var y = 0;
  if(self.pageYOffset) {
    y = self.pageYOffset;
  } else if (document.documentElement && document.documentElement.scrollTop) {
    y = document.documentElement.scrollTop;
  } else if(document.body) {
    y = document.body.scrollTop;
  }
  badge.container.style.top = y + 'px';
  badge.container.style.left = 0 + 'px';

在 show 方法中,您再次检索事件目标并测试它是否是一个链接。然后将大图像容器放在屏幕上。因为您不知道徽章在文档中的位置,所以显示图像最安全的方法是读出文档的滚动位置。为了实现这一点,你需要为不同的浏览器做一些对象检测。

image 注意当前的垂直滚动位置是一个名为 pageYOffset 的窗口对象的属性。这在除 Internet Explorer(IE)9 版之前的所有浏览器中都受支持。如果文档中没有指定 HTML DOCTYPE,则 scrollTop 属性位于 IE、Firefox Opera、Chrome 和 Safari 中文档对象的 body 元素中。

您测试所有这些可能性,并通过设置其样式属性集合的 left 和 top 属性来相应地定位图像容器。这样,您可以始终确保大图像在用户的浏览器窗口中可见。

badge.js(续)

  var source = t.getAttribute('href');
  var newImg = document.createElement('img');
  badge.deletePic();
  newImg.setAttribute('src', source);
  badge.container.appendChild(newImg);
  DOMhelp.addEvent(badge.container, 'click', badge.deletePic, false);
  DOMhelp.cancelClick(e);
},

您读取了链接的 href 属性并创建了一个新的 IMG 元素。通过调用 deletePic()方法删除任何可能已经显示的大图像,并将新图像的 src 属性设置为链接的 href。将新图像作为子节点添加到图像容器中,应用一个在用户单击图像时调用 deletePic()的事件处理程序,并通过调用 cancelClick()阻止链接被跟踪。

badge.js(续)

  deletePic : function() {
    badge.container.innerHTML = ' ';
  }
}
DOMhelp.addEvent(window, 'load', badge.init, false);

deletePic 方法需要做的就是将容器元素的 innerHTML 属性设置为空字符串,从而移除大图像。

摘要

在本章中,您了解了如何使用 JavaScript 增强现有的 HTML 结构或缩略图库的动态服务器端脚本,使其变得动态,或者在用户选择另一个图像或缩略图子集时,通过不加载整个文档来使其看起来更加动态。

创建画廊总是很有趣,为他们想出新的更炫的解决方案也是令人愉快的。我希望通过学习本章介绍的一些技巧,你能自信地运用它们,并想出自己的画廊创意。

十一、使用第三方 JavaScript

现在您可能已经意识到,当您创建一个 JavaScript 应用时,您不需要每次都从头开始重新发明轮子并重新编码所有的功能——JavaScript 有很多很多的函数可供您使用,您也可以创建自己的可重用函数和对象。但是它甚至比那更进一步——你也可以利用第三方代码库和 API,现在在网上有很多。知道在哪里、为什么以及如何使用它们是关键,这就是本章的内容。

在这一章中,我们将看看 jQuery——一个众所周知的强大的 JavaScript 库,它可以帮助你更快地完成工作。我们还将看看如何使用 Twitter 的 REST API 从 Twitter 中检索数据。此外,我们将使用 Google Maps API,最后看看 Twitter Bootstrap。

网络为你提供了什么

作为开发者,我们生活在一个非常激动人心的时代。在过去的几年里,公司对共享内容和技术的态度发生了巨大的变化。过去,每个公司都像保护白金一样保护自己的内容和代码,获得任何关于系统工作情况或如何与之沟通的信息都是一个漫长而痛苦的过程,包括价格谈判、非工作演示、PowerPoint 演示、预览代码和其他营销宣传材料。

这一切都变了。几乎每个公司现在都有某种公共/私人合作关系来吸引开发者。例如,Adobe 有一个名为括号的开源 HTML 编辑器,其中所有的源代码都可以在 GitHub(【https://github.com/adobe/brackets】)上获得。这段代码的一个分叉用来制作 Adobe Edge 代码,它是公司的 creative cloud 包的一部分。

除了为开发者提供软件,公司现在还吹嘘可以提供数据。让开发人员获得数据可以促进创新和对公司的忠诚,尤其是如果开发人员能够找到赚钱的方法。对公司来说,另一个好处是有机会吸引人才,这些人才可以在以后的阶段被聘用。

网络上还有无数的第三方 JavaScript 库,可以下载并插入到您的应用中,让您不费吹灰之力就能获得许多强大的功能。在本章的后面,当我们讨论 jQuery、Google Maps 和 Twitter Bootstrap 时,你会看到一些例子。

REST APIs 和库

image 一个应用编程接口(API)(en . Wikipedia . org/wiki/Application _ programming _ interface)是一组用于构建软件应用的例程和工具。基本上,您有一组方法、对象和属性,用于搭载另一个程序甚至操作系统的功能。从某种意义上说,您在本书中使用了很多 API——您用来访问 window 对象及其所有方法的浏览器 API,以及允许您修改和读取文档的 DOM。

REST API 的一个例子来自 Twitter(细节可以在(dev.twitter.com/)找到),它允许你把一个 URL 放在一起搜索某个用户的推文。结果以 JSON 格式返回。在这个例子中,%40 是@符号的 URL 内码:【http://search.twitter.com/search.json?q=%40twitterapi。

以下是如何使用 API 的其他示例:

关于 REST 的细节可以写满一整本书,所以详细的讨论不在这里讨论范围之内。如果你有兴趣,你可以在维基百科(en . Wikipedia . org/wiki/representative _ State _ Transfer)上详细了解一下。REST APIs 允许你在 URL 中定义任何你想要的信息。这可能很简单,例如,向 URL 添加不同的数据,以访问维基百科的不同条目:

通过将回调函数名作为参数发送给 API,您可以在 JavaScript 中直接使用这些信息:

twitterSearch.html(节选)

<script>
  function results(d) {
          var resultArray = new Array()
          resultArray = d.results;
    for(var i = 0; i< resultArray.length; i++) {
           document.write( resultArray[i].from_user +"<br>" )
           document.write("<img src=”+resultArray[i].profile_image_url +"><br>");
           document.write( resultArray[i].text +"<br><br>" )
    }
}
</script>
<script src="http://search.twitter.com/search.json?q=brooklyn&callback=results"></script>

简而言之,您可以使用 REST API 以最简单的形式(通过组装静态 URL)和最复杂的形式(发送参数以定制数据的输出格式,并调用不同的方法来检索不同种类的数据)从系统中检索信息。

使用库:Short,Shorter,jQuery

拥有代码库的一个主要原因是,开发人员希望让其他开发人员更容易完成日常的编码任务。在本书中,您已经使用 DOMhelp 库通过创建实用程序方法来解决浏览器不一致的问题,并解决重复出现的任务。除了 DOM 之外,您还没有提供自己的编码语法或任何其他方法来访问页面中的元素。如果您这样做了,您可以得到更短的代码,但是您也牺牲了“普通”JavaScript 语法的识别效果,并使开发依赖于对库的了解。

jQuery(jquery.com/)包含一个只有 32K 重的 JavaScript 文件,您可以将它添加到文档的开头。它为您提供了大量实用的方法来完成特定于 web 的任务。对于 JavaScript 初学者或尚未涉足 Ruby、Python 或 Java 等语言的开发人员来说,使用 jQuery 必须编写的代码非常令人困惑。然而,一旦你理解了它,你会发现它非常强大。

jQuery 的概念是提供对文档中任何元素的快速访问,为了获得这种访问,您有一个名为$ (of all things)的实用方法,该方法采用下列方法之一:

  • 一个 DOM 构造,例如$(document.body)
  • CSS 选择器—例如$( ' p a '),它是文档中段落内的每个链接
  • 一个 XPath 表达式,例如$(" //a[@rel='nofollow']"),它将文档中的每个链接与一个名为 rel 且值为 nofollow 的属性进行匹配

image 注意 XPath 是一种万维网联盟(W3C)标准语言,旨在访问 XML 文档的各个部分。它通常与 XSLT 或 XPOINTER 结合使用(www.w3.org/TR/xpath)。因为现代 HTML 应该符合 XML 语法规则(所有标记都是封闭的,所有元素都是小写的,属性值用引号括起来,单个属性定义为名称/值对),所以也可以使用 XPath 来查找 HTML 文档的各个部分。与 XSLT 一起,它是将一种 XML 格式转换成另一种格式的非常强大的工具。

jQuery 实现非常短代码的另一个技巧是一个叫做可链接方法的概念,你已经从 DOM 中知道了。您可以将每个方法添加到最后一个方法中,方法是用句号将它们连接起来。代替

$p = $('p');
$p.addClass('test');
$p.show();
$p.html('example' );

你可以用

$( 'p' ).addClass( 'test' ).show().html( 'example' );

这两个例子做的是相同的事情:它们获取文档的每个段落,添加一个名为 test 的 CSS 类,显示该段落(以防它被隐藏),并将该段落的 HTML 内容更改为“example”。jQuery 为您提供了数量惊人的这些简称方法,它们是为完成日常 web 应用开发任务而定制的。jQuery 网站上有很好的文档和示例(api.jquery.com/)。

让我们看一个例子。如果你是一名开发人员,你写教程,你经常需要在 HTML 页面中显示代码示例。您将它们包装在 PRE 和 CODE 元素中,以使代码中的空白以正确的格式出现,如下所示:

exampleJQuery.html(节选)

<h1>Showing and hiding a code example with jQuery</h1>
<p>The code</p>
<pre><code>
  [... code example ...]
</code></pre>
<p>The CSS</p>
<pre><code>
  [... code example ...]
</code></pre>

现在让我们在 jQuery 中编写一个脚本,在代码示例之前生成链接,允许扩展和折叠示例,而不是简单地显示它们,如图 11-1 所示。

9781430250920_Fig11-01.jpg

图 11-1 。用 jQuery 显示和隐藏代码示例

jqueryTest.js

$(document).ready (
  function() {
    $('pre').before('<p><a class="trigger" href="#">Show code</a></p>');
    $('pre').hide();
    $('a.trigger').toggle (
      function() {
        $(this.parentNode.nextSibling).slideDown('slow');
        $(this).html('Hide Code');
      },
      function() {
        $(this.parentNode.nextSibling).slideUp('slow');
        $(this).html('Show Code');
      }
    )
  }
)

正如您所看到的,代码非常短,但是在语法方面也相当复杂。让我们一点一点地看一下这个例子,这样您就可以理解发生了什么:

jqueryTest.js (excerpt)

$(document).ready (
  function() {

$(文档)。ready () 方法是一个事件处理程序,当文档准备好被操作时,它调用作为参数提供的函数(在本例中是一个匿名函数)。这意味着该脚本中的所有内容都会在文档加载后执行——这意味着只是文档,而不是其中所有的嵌入资源(如图像)。你可能还记得,我们在第五章中讨论过页面内容在隐藏之前显示出来的丑陋效果。这种方法可以解决这个问题。

jqueryTest.js(续)

$('pre').before('<p><a class="trigger" href="#">Show code</a></p>');
$('pre').hide();

您获取文档中的每个 PRE 元素,并使用 before()方法在 DOM 树中的这个元素之前添加一个 HTML 字符串——在本例中,是一个带有类触发器的嵌入式链接的段落。使用 jQuery 的 hide()方法来隐藏所有 PRE 元素。(hide()将 CSS 属性显示设置为无。)

jqueryTest.js(续)

$('a.trigger').toggle (

您使用 CSS 选择器 a.trigger 来匹配带有类触发器的所有链接(应该只是脚本通过 before()方法添加的链接),而使用 toggle()方法。当用户单击元素时,该方法交替执行作为参数提供的两个函数。第一个参数是一个匿名函数,它显示了之前隐藏的代码示例,并将链接文本更改为“Hide Code ”,反之亦然。

jqueryTest.js(续)

function() {
  $(this.parentNode.nextSibling).slideDown('slow');
  $(this).html('Hide Code');
},

可以使用几种 jQuery 方法来显示和隐藏元素,最基本的方法是 show()和 hide()。使用 slideDown()和 slideUp()可以产生更高级的效果,它们以逐行动画的方式显示元素。这两种方法都有一个指示动画速度的参数,可以是慢、正常或快。要显示或隐藏 PRE 元素,需要使用\((this)构造,它返回 toggle()的事件目标。这意味着您可以使用 this.parentNode.nextSibling 到达 PRE,因为链接嵌套在一个段落中。您可以通过\)(this)和 html()方法更改链接本身的内容,该方法将 HTML 字符串作为唯一的参数,并更改元素的 innerHTML 属性。

jqueryTest.js(续)

      function() {
        $(this.parentNode.nextSibling).slideUp('slow');
        $(this).html('Show Code');
      }
    )
  }
)

本例中 toggle()的另一种情况是使用 slideUp()慢慢隐藏代码示例,并将链接的文本改回“显示代码”。

jQuery 也允许简单的 Ajax 请求。jQuery 使用 load(),\(。get()和\)。在api.jquery.com/category/ajax/解释的 post()方法。例如,如果您想要创建 PRE 元素,并在用户单击链接时将真实的代码示例加载到其中,这是非常容易做到的。查看演示 exampleJQueryAjax.html,了解以下脚本的运行情况:

jquery test jax . js

$(document).ready (
  function() {
    $('a.codeExample').each (
      function( i ) {
        $(this).after('<pre class="codeExample"><code></code></pre>');
      }
    )
    $('pre.codeExample').hide();
    $('a.codeExample').toggle (
      function() {
        if(!this.old){
          this.old = $(this).html();
        }
        $(this).html('Hide Code');
        parseCode(this);
      },
      function() {
        $(this).html(this.old);
        $(this.nextSibling).hide();
      }
    )
    function parseCode(o){
      if(!o.nextSibling.hascode){
          $.get (o.href,
            function(code){
              code=code.replace(/&/mg,'&');
              code=code.replace(/</mg,'<');
              code=code.replace(/>/mg,'>');
              code=code.replace(/\"/mg,'"');
              code=code.replace(/\r?\n/g,'<br>');
              code=code.replace(/<br><br>/g,'<br>');
              code=code.replace(/ /g,'&nbsp;');
              o.nextSibling.innerHTML='<code>'+code+'</code>';
              o.nextSibling.hascode=true;
            }
          );
      }
      $(o.nextSibling).show();
    }
  }
)

让我们一步步来看这个脚本:

jquery test jax . js(excerpt)

$(document).ready (
  function() {

您再次从 ready()方法和一个匿名函数开始。(您也可以创建一个命名函数,并通过 ready()方法调用它。)

jqueryTestAjax.js(续)

$('a.codeExample').each (
  function(i) {
    $(this).after( '<pre class="codeExample"><code></code></pre>' );
  }
)
$('pre.codeExample').hide();

使用 jQuery 的迭代器方法 each()遍历所有包含 CSS 类 codeExample 的链接。然后,在使用 jQuery 的 hide()方法隐藏 codeExample 类的所有 PRE 元素之前,通过 after()方法和$(this)选择器,使用 codeExample 类创建 PRE 元素,并在每个链接后嵌入代码元素。

jqueryTestAjax.js(续)

$('a.codeExample').toggle (
  function() {
    if(!this.old){
      this.old = $(this).html();
    }
    $(this).html('Hide Code');
    parseCode(this);
  },
  function() {
    $(this).html(this.old);
    $(this.nextSibling).hide();
  }
)

使用 toggle()来显示和隐藏代码示例;但是,与上一个脚本不同的是,当您显示代码并用“Hide Code”替换链接文本时,您将链接的原始文本存储在一个名为 old 的属性中。然后,在显示代码时,调用函数 parseCode(),将链接作为参数。隐藏代码时,在使用 jQuery 的 hide()方法隐藏 PRE 元素之前,通过将链接文本设置回旧参数中存储的值来恢复原始链接文本。

jqueryTestAjax.js(续)

function parseCode(o){
  if(!o.nextSibling.hascode){
      $.get (o.href,
        function(code){

这个函数测试链接后面的 PRE 元素(它的下一个兄弟元素)是否有一个名为 hascode,的属性,这个属性将在第一次加载代码时设置。这对于避免脚本在用户每次单击链接时加载代码是必要的,因为它只加载一次。然后,您可以使用$。get()方法,将链接的 href 属性值和一个匿名函数作为参数。这实际上发送了一个加载链接文档的请求,并在文档加载后调用函数。您发送一个名为 code 的参数,该参数包含通过 XHR 加载的文档内容。

jqueryTestAjax.js(续)

code=code.replace( /&/mg, '&' );
code=code.replace( /</mg, '<' );
code=code.replace( />/mg, '>' );
code=code.replace( /\"/mg, '"' );
code=code.replace( /\r?\n/g, '<br>' );
code=code.replace( /<br><br>/g, '<br>' );
code=code.replace( / /g, ' ' );
o.nextSibling.innerHTML = '<code>'+code+'</code>';
o.nextSibling.hascode = true;

然后,在将结果作为 PRE 元素的 innerHTML 属性添加到 CODE 元素中之前,使用正则表达式将所有&符号、标记括号和引号替换为它们的编号 HTML 实体,将换行符替换为
并将空格替换为它们的 HTML 实体。您将 hascode 属性设置为 true,以确保下次用户单击链接显示代码时$。跳过 get()构造。

jqueryTestAjax.js(续)

            }
          );
      }
      $(o.nextSibling).show();
    }
  }
)

剩下要做的就是使用 jQuery 的 show()方法显示 PRE 元素。请注意,您需要在$之外这样做。get()该构造确保代码在用户选择显示代码的第二次和后续时间得到显示。

jQuery 和其他库使用自己的语法的危险

令人惊讶的是,使用 jQuery 可以轻松快速地执行许多日常 web 应用和 web 开发任务。然而,如果你把这个文档交给第三方开发人员来维护,她必须知道 jQuery,否则她会完全不知所措。这是使用库的危险之一。您不必依赖 JavaScript 语法和规则,而是在这个过程中添加了一层额外的必要知识。由你来决定图书馆提供的好处是否值得。

库的存在是为了使开发过程更快更容易,而不是让我们依赖它们或者重复我们过去已经使用库犯过的错误——创建没有 JavaScript 就无法运行的应用和网站。

接下来,我们将看看如何使用 Google Maps API 来创建地图应用。

使用 API:用谷歌地图给你的网站添加地图

谷歌地图(maps.google.com)可能是让整个 Ajax 热潮滚滚而来的网络应用。它为用户提供了可以移动、缩放的地图,如果实现允许,甚至可以添加注释。你可以用地图、卫星图片或者两者的混合来显示你想看的地点。

谷歌允许网络开发者通过 API 在他们自己的网站上使用谷歌地图。要使用这个 API,你需要在它的主页上注册一个免费的开发者密钥:developers.google.com/maps/。在这里你还可以找到如何使用谷歌地图的文档和许多例子。该密钥将使您能够在单个域或该域的子文件夹中使用地图。本章中的例子使用了一个适用于 localhost 的密钥,这意味着您需要通过 localhost/ 在本地服务器上运行它们,而不是从文件系统运行。

一旦获得了开发者密钥,就可以链接到包含文档头中所有地图代码的 JavaScript。粗体显示的“您的密钥”应替换为您从 Google 获得的密钥:

<script src="https://maps.googleapis.com/maps/api/js?key=yourkey&sensor=true&callback=initialize
" type="text/javascript">
</script>

image 注意这个 URL 将来可能会改变,所以如果你的例子突然失败了,一定要查看 API 主页。

下一步需要做的是获取想要显示的位置的纬度和经度值。如果你在谷歌中输入你感兴趣的位置,然后添加“纬度”一词(例如“埼玉日本纬度”),你就可以获得你需要添加到地图代码中的信息。

有了这些信息后,您就可以开始在网站上添加自己的地图了。作为一个国际位置的例子,让我们使用日本的埼玉县。坐标是纬度 35.8617,经度 139.6453。使用这些信息和 API 为您提供的方法,很容易显示东京附近一个好地方的地图。

从包含地图的 HTML 元素开始。您可以在这个元素中添加 JavaScript 不可用或浏览器不支持时显示的内容。该内容可以是任何 HTML、文本,甚至是同一地图的静态图像。静态图像是确保向后兼容性的一个很好的选择;您只需要确保不要在文本的任何地方告诉用户地图是动态的,因为它可能不是动态的。

exampleGoogleMaps.html(节选)

<div id="map_canvas" style="width:100%; height:100%">
  <p>Here you should see an interactive map, but you
  either have scripting disabled or your browser
  is not supported by Google Maps.</p>
</div>

Google 提供的示例 CSS 代码将使您的地图全屏显示。

谷歌地图. css

html { height: 100% }
body { height: 100%; margin: 0; padding: 0 }
#map_canvas { height: 100% }

接下来,您需要添加将地图放入文档的 JavaScript】

谷歌地图. js

function loadMapAPI(){
var script = document.createElement("script");
    script.type = "text/javascript";
    script.src = "https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&sensor=true&callback=initialize";
};
  document.body.appendChild(script);
}

function initialize() {
var mapOptions = {
      center: new google.maps.LatLng(35.8617, 139.6453 ),
      zoom: 8,
     mapTypeId: google.maps.MapTypeId.ROADMAP
    var map = new google.maps.Map(document.getElementById("map_canvas"),mapOptions);
  }

document.addEventListener("DOMContentLoaded", loadMapAPI,false);

一旦 DOM 被加载,事件监听器就调用 loadMapAPI 函数。然后,该函数创建一个脚本元素,并添加类型和源属性。在这种情况下,源是地图 API 的 URL。注意,在 URL 中有一个回调参数,它告诉浏览器在哪里返回结果——在我们的例子中,是初始化函数。

initialize 函数创建 mapOptions 对象文本。在这个对象中,您可以添加选项,例如将地图集中在特定点并缩放,这将设置地图的分辨率(0 表示地球视图)。地图类型属性告诉 Google 它应该显示哪种地图:

  • 路线图—默认 2D 地图
  • 卫星摄影图片
  • 混合——路线图和卫星的混合,用于道路和城市等显著特征
  • 地形-显示山脉和河流等高程和水域要素

最后,创建地图对象。创建 map 实例时,需要在 HTML 文档中指定一个包含 map 的 div 元素。此外,您可以传递包含参数的 mapOptions 对象来定制地图。上面例子中的代码应该会给你一张锡塔马县的地图,如图图 11-2 所示。

9781430250920_Fig11-02.jpg

图 11-2 。埼玉县的谷歌地图

地图正在工作,但是在当前的视图下,很难确切地说出你应该注意什么。要解决这个问题,您需要添加一个标记。创建标记类似于创建地图。您创建一个标记对象,并在构造函数中添加一个对象文字,该对象文字具有您希望该标记具有的所有属性:

googleMapsMarker.js(节选)

function initialize() {
var myLatlng = new google.maps.LatLng(35.8617, 139.6453 );
var mapOptions = {
    center: myLatlng,
    zoom: 8,
    mapTypeId: google.maps.MapTypeId.ROADMAP
};
    var map = new google.maps.Map(document.getElementById("map_canvas"),mapOptions);
    var marker = new google.maps.Marker({
        position: myLatlng,
        animation: google.maps.Animation.DROP,
        map: map
       });
}

这一改变在您定义的位置创建了一个红色标记图标,如图图 11-3 所示。代码还添加了动画参数,以便标记在显示时有一个反弹。

9781430250920_Fig11-03.jpg

图 11-3 。有标记的地图

您还可以使用 API 来显示单击标记时出现的窗口。这有助于向用户提供必要的信息以及相关网站的链接。在本例中,您创建了一个名为 contentString 的变量。在这个变量中,添加一个 HTML 格式的文本字符串。这将在信息窗口中正确呈现。

接下来的几个步骤类似于您用来创建标记对象的步骤。您创建一个 InfoWindow 对象并传递一个带有属性 content 的对象文本,它将具有您的变量的值。最后,将一个监听器添加到您之前创建的标记中。因此,当标记接收到一个点击时,就会调用 InfoWindow 对象上的 open 方法,然后您就可以传递这个窗口所关联的地图和标记了。图 11-4 中显示了一个例子。

9781430250920_Fig11-04.jpg

图 11-4 。带有标记和信息窗口的地图

示例谷歌地图标记 event . js(except)

var contentString = '<div id="content">'+
    '<p><b>Saitama</b> is the capital and the most populous city '+
    'of Saitama Prefecture in Japan, situated in the south-east of  the prefecture. '+
    'Being in the Greater Tokyo Area and lying 15 - 30 kilometres north of central Tokyo,'+
    'many of its residents commute into Tokyo.</p>'+
    '<p>Source: <a href="http://en.wikipedia.org/wiki/Saitama,_Saitama">'+
    'http://en.wikipedia.org/wiki/Saitama,_Saitama</a>.</p>'+
    '</div>';

var infowindow = new google.maps.InfoWindow({
content: contentString
});
    var marker = new google.maps.Marker({
    position: myLatlng,
    animation: google.maps.Animation.DROP,
   map: map
});
google.maps.event.addListener(marker, 'click', function() {
   infowindow.open(map,marker);
 });
}
document.addEventListener("DOMContentLoaded", loadMapAPI, false);

您将添加的最后一个功能是平移到另一个位置的能力。在本例中,关闭 infoWindow 后,map 对象使用 panTo()方法将地图移动到另一个位置。

首先,您需要向 infoWindow 对象添加一个侦听器,就像您处理标记一样。单击时,侦听器调用一个函数,该函数创建一个名为 newLating 的新对象,该对象保存要将地图移动到的新位置。然后与 map 对象对话并调用它的 panTo()方法。在 panTo 方法中,使用 newLating 对象传递一个新位置。

接下来,创建一个 newContentString 变量,其段落为“欢迎来到秋田县”。将创建 newInfoWindow 对象。它还将有一个 object literal,带有一个名为 content 的属性,该属性引用 newContentString 变量。

最后,newMarker 对象中添加了一个事件。单击时,它将与 newInfoWindow 对象对话,并使用其 open 方法显示新的 info 窗口:

googleMapsPan.js

google.maps.event.addListener(infowindow, 'closeclick', function() {
var newLatLng = new google.maps.LatLng(39.7158,140.1058);
     map.panTo(newLatLng);

var newMarker = new google.maps.Marker({
    position: newLatLng,
    animation: google.maps.Animation.DROP,
    map: map
    });
var newContentString= '<p>Welcome to Akita Prefecture</p>';

var newInfoWindow = new google.maps.InfoWindow({
     content: newContentString
    });

google.maps.event.addListener(newMarker, 'click', function() {
    newInfoWindow.open(map,newMarker);
    });

 });

结果如图图 11-5 所示。

9781430250920_Fig11-05.jpg

图 11-5 。从一个地图位置平移到另一个位置

全面服务:引入 Twitter Bootstrap

在本章的前面,您看了一下 Twitter 的 REST API 的一部分。除了提供对其数据访问的 REST API,Twitter 还发布了 Twitter Bootstrap(位于twitter.github.com/bootstrap/)。Bootstrap 是一个免费的工具集,用于使用 HTML、CSS 和可选的 JavaScript 扩展创建网站和应用。Bootstrap 是 GitHub 上最受欢迎的项目,大公司和初创公司都使用它。

Twitter Bootstrap 的一些特性包括:

  • 设置全局版式
  • 使用网格系统进行页面布局,并支持响应式设计
  • 包括 HTML 元素的样式,如表单、按钮和表格
  • 提供内置图标
  • 提供可重用的 UI 组件

向 HTML 文档添加引导代码很简单。下载。zip 文件,让我们看看它的内容。

将引导程序添加到您的站点

当您解压缩 bootstrap.zip 时,您会发现一个类似于您组织自己的网站的文件结构。图 11-6 显示了它的样子。

9781430250920_Fig11-06.jpg

图 11-6 。从引导页面解压缩 Twitter 引导后的文件夹布局的屏幕截图

这些文件可以被复制到您的站点中,并在您的 HTML 文档中引用。要开始,请使用以下代码:

exampleBootstrapTemplate.html

<!doctype html>
<html>
  <head>
  <title>Bootstrap Template</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="css/bootstrap.min.css" rel="stylesheet" media="screen">
    <script src="http://code.jquery.com/jquery.js"></script>
    <script src="js/bootstrap.min.js"></script>
   </head>
    <body>
         <h1>This is my header</h1>
    </body>
</html>

当在浏览器中查看您的页面时,您应该会看到 h1 标签的字体有所不同。它现在使用 helvetica 作为默认字体。我们可以看到 h1 标签是左对齐的,因为 bootstrap 删除了文档主体中所有默认的空白空间。图 11-7 向你展示了此时你的应用应该是什么样子。

9781430250920_Fig11-07.jpg

图 11-7 。一旦引导程序开始工作,Helvetica 就是页面的默认字体

现在您已经知道 bootstrap 正在工作,您可以使用它的一些特性了。首先,您可以将标题放在使用容器类的 div 中。接下来你可以添加一个段落,声明接下来是你的按钮。在这个段落中,您使用 text-info 类来赋予它样式。之后,可以用 btn 类添加一个 button 元素。当在浏览器中查看时,你应该把你的文本从左边移过来,你的新副本和按钮准备好了。

exampleBootstrapButton.html

<!doctype html>
<html>
  <head>
  <title>Bootstrap Template</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link href="css/bootstrap.min.css" rel="stylesheet" media="screen">
    <script src="http://code.jquery.com/jquery.js"></script>
    <script src="js/bootstrap.min.js"></script>
   </head>
    <body>
         <div class="container">
             <h1>This is my header</h1>
             <p class="text-info">This is my Button</p>
             <button class="btn">Click Me</button>
       </div>
    </body>
</html>

按钮就位后,你现在可以把它变成一个下拉菜单。因为你下载了默认的引导程序。zip 文件,不需要下载下拉插件。Bootstrap 的插件是为 jQuery 设计的,因为你也把它添加到了你的文档中,所以只需要添加必要的 HTML 和 CSS 就可以了。

exampleBootstrapDropdown.html(节选)

<div class="btn-group">
    <button class="btn ">Click Me</button>
    <button class="btn dropdown-toggle" data-toggle="dropdown ">
        <span class="caret "></span>
    </button>
        <ul class="dropdown-menu">
            <li><a href="#">Depeche Mode</a></li>
            <li><a href="#">Information Society</a></li>
            <li><a href="#">The Cure</a></li>
    <li><a href="#">Erasure</a></li>
</ul>
</div>

在图 11-8 中,你可以看到一个由 Twitter Bootstrap 驱动的下拉菜单。没有编写额外的 JavaScript。

9781430250920_Fig11-08.jpg

图 11-8 。Twitter Bootstrap 支持的下拉菜单

正如您所看到的,Bootstrap 的一个好处是您不需要添加 JavaScript 就可以工作。如果您想对文档有更多的控制,您可以添加自己的 JavaScript。在这个例子中,您将使用内置代码添加一个模态窗口。

exampleBootstrapModalNoJs.html(节选)

<button type="button" data-toggle="modal" data-target="#myModal">Launch modal</button>
    <div id="myModal" class="modal hide fade">
       <div class="modal-header">
          <button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button>
          <h3 id="myModalLabel">My Modal header</h3>
        </div>
        <div class="modal-body">
        <p>The body of the window</p>
        </div>
       <div class="modal-footer">
           <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
           <button class="btn btn-primary">Save changes</button>
       </div>

这将弹出一个模态窗口。按钮标签有一个指向 ID myModal 的数据目标属性。通过使用属性,模式窗口无需任何附加代码即可工作。要禁用数据 API,请在代码中添加一行代码,该代码将查看文档并禁用对数据 API 的所有引用。对于单个组件也可以这样做。之后,您需要使用 jQuery 插件让模型工作。在本例中,您添加一些 jQuery 代码,首先禁用数据 API,然后检测现在称为 launchButton (使用 ID 属性)的按钮何时被单击。当单击 launchButton 时,myModal 将像以前一样出现。

exampleBootstrapModal.html(节选)

<button type="button" data-toggle="modal" id="launchButton" data-target="#myModal">Launch modal</button>

    exampleBootstrapModal.js    $(document).ready(function(e) {
      $(document).off('.data-api');
      $('#launchButton').click(function(){
            $('#myModal').modal({ show: true});  //if you comment this out, the modal will not work
      });
});

通过以这种方式控制模态,您可以更好地控制事物的工作方式。例如,如果您想在模式出现之前进行一些表单验证,您可以运行表单验证代码,然后显示带有自定义消息的窗口。要控制一个手风琴,你有类似的代码。

exampleBootstrapAccordion.html(节选)

<div class="accordion-heading">
  <a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion2" href="#collapseOne" id="header1">
            Menu Item 1
  </a>
</div>
...
    <div class="accordion-heading">
        <a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion2" href="#collapseTwo" id="header2">
         Menu Item 2
      </a>
</div>

示例 BootstrapAccordion.js

  $(document).ready(function(e) {
    $(document).off('.data-api');

       $('#header1').click(function(){
            $('#collapseOne').collapse('toggle');
      });

    $('#header2').click(function(){
        $('#collapseTwo').collapse('toggle');
      });
});

在这里,您关闭了数据 API。然后将 click 事件分配给两个 HTML 元素,称为 header1 和 header2。当单击任一元素时,可以使用 bootstrap API 并调用 collapse 方法。这个方法可以接收一些不同的值。在这种情况下,您发送 toggle。这将自动为您展开或折叠内容。下一个例子展示了如何使用 bootstrap API 来控制 carousel 。

示例 BootstrapCarousel.js

      $(document).ready(function(e) {
        $(document).off('.data-api');

        $('.carousel-control').click(function(e){

           switch(e.target.getAttribute("data-slide")){
               case 'prev':
                $('.carousel').carousel('prev');
        break;
        case "next":
        $('.carousel').carousel('next');
        break;
          }
});

$('.carousel-indicators').click( function(e){

      switch(e.target.getAttribute("data-slide-to")){
          case '0':
          $('.carousel').carousel(0);
          break;
          case '1':
          $('.carousel').carousel(1);
          break;
          case '2':
          $('.carousel').carousel(2);
          break;
       }
    });
});

这里有两个函数,每个函数寻找 carousel 正在使用的 CSS 类。在每种情况下,他们都在寻找正在使用的 HTML 标签中的属性的详细信息。第一个是查看 HTML 标记中的 data-slide 属性。

exampleBootstrapCarousel.html(节选)

<a class="carousel-control left" href="#myCarousel" data-slide="prev">&lsaquo;</a>
<a class="carousel-control right" href="#myCarousel" data-slide="next">&rsaquo;</a>

在 HTML 文档中,该属性与用于向前或向后移动转盘的锚标记相关联。当单击这些元素中的任何一个时,JavaScript 会检查 switch 语句中的值。一旦找到匹配,它就调用 carousel 方法,并传递一个值“prev”“next”

第二个函数以类似的方式工作。它寻找当一个元素使用 CSS 类”。转盘-指示器”已被点击。HTML 文档有一个使用该类的列表:

exampleBootstrapCarousel.html(节选)

<ol class="carousel-indicators">
  <li data-target="#myCarousel" data-slide-to="0" class="active"></li>
  <li data-target="#myCarousel" data-slide-to="1"></li>
  <li data-target="#myCarousel" data-slide-to="2"></li>
</ol>

switch 语句查找数据滑动到的值。与前面的 switch 语句一样,一旦找到值,就调用 carousel 方法,并传递数字(图像计数就像一个从 0 开始的数组)以使图像可见。完成后的转盘如图图 11-9 所示。

9781430250920_Fig11-09.jpg

图 11-9 。完工的旋转木马

摘要

这一章已经让你体验了一些目前对你来说不可行的服务,我确信这仅仅是你使用共享内容、信息和服务的漫长经历的开始。许多开发人员花费大量时间来创建精彩的代码,只是为了意识到已经有另一个产品在做同样的事情,但是做得更好。然而,这并不是什么大问题——正是通过交流和反复试验,我们才能在工作中做得更好。

通过睁大眼睛观察可用的服务,您可以学到很多东西,并对开发社区有很大的帮助。就一些服务或库的易用性给出反馈尤其重要。认为自己的代码完美实在是太诱人了,有时直到别人告诉你如何破解它,你才意识到它并不完美。这是双向的。你不应该因为自己的缺点而退缩——坚持下去,你会变得更好。不要害羞——继续参与 JavaScript 社区。

十二、附录 A:调试 JavaScript

在本附录中,我将向您介绍一些调试 JavaScript 代码的技巧和工具。熟悉调试工具是非常重要的,因为编程在很大程度上是试图找出在特定时间出了什么问题。一些浏览器帮助你解决这个问题;其他人通过隐藏他们的调试工具或者返回含糊的错误消息来增加难度,这些错误消息带来的困惑多于帮助。我最喜欢的一些哲学著作包括“未定义的就是未定义的”或微软 IE 标准“对象不支持这个属性或方法。”

常见的 JavaScript 错误

让我们从可能每个 JavaScript 开发人员都犯过的一些常见错误开始。当你检查一个失败的脚本时,在你的大脑后面有这些可能会让你更快的发现问题。

拼写错误和区分大小写问题

最容易发现的错误是 JavaScript 方法名或属性的拼写错误。经典的有 getElementByTagName()代替 getElementsByTagname(),getElementById()代替 getElementByID(),node.style.colour(针对英国英语作家)。很多时候,问题也可能是区分大小写,例如,用混合大小写而不是小写来写关键字。

If( elm.href ) {
  var url = elm.href;
}

没有一个关键字叫做 if,但是有一个叫做 If。同样的区分大小写问题也适用于变量名:

var FamilyGuy = 'Peter';
var FamilyGuyWife = 'Lois';
alert('The Griffins:\n'+ familyGuy + ' and ' + FamilyGuyWife );

这会导致一条错误消息,指出“familyGuy 未定义”,因为有一个名为 FamilyGuy 的变量,但没有一个名为 familyGuy 的变量。

试图访问未定义的变量

我在这本书的第二章中谈到过:你可以通过声明变量来定义变量,可以使用或不使用额外的 var 关键字。(后者是定义变量范围所必需的。)请记住,严格模式会阻止您隐式声明变量(没有 var 关键字的变量)。因此,建议您对每个变量使用 var 关键字。

Stewie = "Son of Peter and Lois";
var Chris = "Older Son of Peter and Lois";

如果你试图访问一个还没有定义的变量,你会得到一个错误。以下脚本中的 alert()引发了一个错误,因为 Meg 尚未定义:

Peter = "The Family Guy";
Lois = "The Family Guy's Wife";
Brian = "The Dog";
Stewie = "Son of Peter and Lois";
Chris = "Older Son of Peter and Lois";
alert( Meg );
Meg = "The Daughter of Peter and Lois";

当它是一个像这样明显的例子时,这很容易,但是试着猜测下一个例子中的错误在哪里呢?

exampleFamilies.html

function getFamilyData( outptID, isTree, familyName ) {
  var father, mother, child;
  switch( familyName ) {
    case 'Griffin':
      father = "Peter";
      mother = "Lois";
      child = "Chris";
    break;
    case 'Flintstone':
      father = "Fred";
      mother = "Wilma";
      child = "Pebbles";
    break;
  }
  var out = document.getElementById( outputID );
  if( isTree ) {
    var newUL = document.createElement( 'ul' );
    newUL.appendChild( makeLI( father ) );
    newUL.appendChild( makeLI( mother ) );
    newUL.appendChild( makeLI( child ) );
    out.appendChild( newUL );
  } else {
    var str = father + ' ' + mother + ' ' + child;
    out.appendChild( document.createTextNode( str ) );
  }
}
getFamilyData( 'tree', true, 'Griffin' );

Chrome 的开发者工具会告诉你第 23 行有一个错误——“没有定义‘output id’”,如图 A-1 所示。

9781430250920_AppA-01.jpg

图 A-1 。Chrome 在第 23 行显示一个错误

但是,如果你看看第 23 行的代码,如图图 A-2 所示,似乎没什么问题。

9781430250920_AppA-02.jpg

图 A-2 。代码在第 23 行高亮显示

罪魁祸首是函数参数中的拼写错误,在图 A-3 中突出显示,这意味着没有定义 outputID,但定义了 output id。

9781430250920_AppA-03.jpg

图 A-3 。导致错误的拼错的函数参数

参数中的拼写错误是一个非常令人困惑的错误,因为浏览器会告诉你错误发生在使用变量的那一行,而不是你犯错误的地方。

右大括号和圆括号的数量不正确

另一个常见的错误是在删除一些行时,代码中没有使用右花括号或保留一个孤立的右花括号。比方说,您不再需要 isTree 选项,并将其从代码中删除:

exampleCurly.html

function getFamilyData( outputID, familyName ) {
  var father, mother, child;
  switch( familyName ) {
    case 'Griffin':
      father = "Peter";
      mother = "Lois";
      child = "Chris";
    break;
    case 'Flintstone':
      father = "Fred";
      mother = "Wilma";
      child = "Pebbles";
    break;
  }
  var out = document.getElementById( outputID );
  var newUL = document.createElement( 'ul' );
  newUL.appendChild( makeListElement( father ) );
  newUL.appendChild( makeListElement( mother ) );
  newUL.appendChild( makeListElement( child ) );
  out.appendChild( newUL );
  }
}
getFamilyData( 'tree', true, 'Griffin' );

孤立的右大括号导致 Chrome 出现“未捕获的语法错误:意外标记”错误。当您没有关闭一个构造中的所有大括号时,也会出现同样的问题,这是一个在您没有缩进代码时很容易发生的错误:

exampleMissingCurly.html

function testRange( x, start, end ) {
if( x <= end && x >= start ) {
if( x == start ) {
alert( x + ' is the start of the range');
}
if( x == end ) {
alert(x + ' is the end of the range');
}
if( x! = start && x != end ) {
alert(x + ' is in the range');
} else {
alert(x + ' is not in the range');
}
}

运行此示例会导致“未捕获的 SyntaxError:意外的输入结束”错误,这是脚本块的最后一行。这意味着在条件结构中的某个地方,你忘记了添加一个右花括号。缺少的大括号应该在哪里很难找到,但是当代码适当缩进时就容易找到了。

exampleMissingCurlyFixed.html

function testRange( x, start, end ) {

  if( x <= end && x >= start ) {
    if( x == start ) {
      alert(x + ' is the start of the range');
    }
    if( x == end ) {
      alert(x + ' is the end of the range');
    }
    if( x != start && x != end ) {
      alert(x + ' is in the range');
    }
  } else {
    alert( x + ' is not in the range' );
  }
}

先前丢失的花括号以粗体显示(在“在范围内”警告()消息之后)。

缺少/最高级括号是另一个常见问题。当您在 if()条件中嵌套函数,然后删除其中一些函数时,就会发生这种情况。例如:

if (all = parseInt(getTotal()){ doStuff(); }

这将导致一个错误,因为您忘记了关闭条件本身的左括号。它应该是这样的:

if (all = parseInt(getTotal())){ ... }

当您嵌套太多方法和返回时,也会发生这种情况:

var elm=grab(get(file).match(/<id>(\w+)<\/id>/)[1];

这个在[1]后面缺少右括号:

var elm=grab(get(file).match(/<id>(\w+)<\/id>/)[1]);

一般来说,这种函数的串联不是好的编码风格,但是有些情况下你会遇到像这样的例子。诀窍是从左到右计算左括号和右括号——好的编辑器还会自动突出显示左括号和右括号。有些编辑会为您添加右括号、花括号或引号;其他人不知道,所以在你打字的时候要注意。

您也可以编写一个实用函数来帮您完成这项工作,这本身就是对您在编码语法方面对细节关注的一个测试:

exampleTestingCodeLine.html

function testCodeLine( c ) {
  if( c.match( /\(/g ).length !=
     c.match( /\)/g) .length ) {
    alert( 'closing ) missing' );
  }
}
c = "var elm=grab(get('demo.xml')" +
  ".match( /<id>(\w+)<\/id>/ )[1] );";
testCodeLine( c );

串联出错

当你使用 JavaScript 输出 HTML 时,串联经常发生。请确保不要忘记不同部分之间的加号(+)以连接成一个整体:

father = "Peter";
mother = "Lois";
child = "Chris";
family = father+" "+mother+" "child;

前面的代码在子变量前缺少一个加号。相反,它应该如下所示:

father = "Peter";
mother = "Lois";
child = "Chris";
family = father+" "+mother+" "+child;

另一个障碍是确保不要连接错误的数据类型:

father = "Peter";
fAge = 40;
mother = "Lois";
mAge = 38;
child = "Chris";
cAge = 12;
family = father + ", " + mother +  " and " + child + " Total Age: " + fAge + mAge + cAge;
alert( family );

这不会显示预期的结果。相反,它将显示以下内容:

彼得、洛伊斯和克里斯总年龄:403812

错误在于您串联了字符串和数字,因为运算符是从左到右工作的。您需要在年龄术语两边加上括号:

father = "Peter";
fAge = 40;
mother = "Lois";
mAge = 38;
child = "Chris";
cAge = 12;
family = father + ", " + mother + " and " + child + " Total Age: " + (fAge + mAge + cAge);
alert(family);

这导致了预期的结果:

彼得、洛伊斯和克里斯总年龄:90 岁

赋值而不是测试变量的值

当测试一个变量的值时,很容易赋值而不是测试它:你需要做的就是忘记一个等号:

if(Stewie = "talking") {
  Brian.hear();
}

这个代码诱使布莱恩一直听,不仅仅是当 Stewie 有话要说的时候;然而,添加一个等号确实使 Brian 只在 Stewie 说话时听到:

if(Stewie == "talking") {
  Brian.hear();
}

用 alert()和“Console”元素跟踪错误

跟踪错误最简单的方法是在需要测试某个值的地方使用 alert()。alert()方法停止脚本执行(Ajax 调用除外,它可能仍在后台运行),并为您提供关于某个变量的值的信息。您可以推断该值是否正确,或者它是否是错误的原因。在某些情况下,使用 alert()并不是正确的选择,例如,如果您想在遍历数组时跟踪几个值的变化。根据数组的大小,这可能会变得很繁琐,因为每次想要消除警告()并开始下一个数组项时,都需要按 Enter 键。

解决此问题的方法是使用您自己的调试控制台或日志记录元素,或者使用浏览器中的调试功能。我们在本书中放在一起的 DOMhelp 库包含调试特性。您可以使用 initDebug()、setDebug()和 stopDebug()方法来模拟调试控制台。只需为 ID 为 DOMhelpDebug 的元素添加一个样式,并使用这些方法来显示元素并向其中写入内容。例如:

exampleDebugTest.html(节选)

#DOMhelpdebug{
  position:absolute;
  top:0;
  right:0;
  width:300px;
  height:200px;
  overflow:scroll;
  background:#000;
  color:#0F9;
  white-space:pre;
  font-family:courier,monospace;
  padding:1em;
}
html>body #DOMhelpdebug{
  position:fixed;
  min-height:200px;
  height:200px;
  overflow:auto;
}

exampleDebugTest.html(节选)

<script type="text/javascript"
 src="../DOMhelp.js"></script>
<script type="text/javascript">
  function DOMDebugTest(){
    DOMhelp.initDebug();
    for(var i = 0; i < 300; i++ ) {
      DOMhelp.setDebug( i + ' : ' + ( i % 3 == 0 ) + '\n' );
    }
  }
  DOMhelp.addEvent( window, 'load', DOMDebugTest, false );
</script>

本示例遍历数字 0 到 299,并显示该数字能否被 3 整除而不产生浮点数。不用按 300 次 Enter 键,你只需要在你用早期样式创建的“控制台窗口”中滚动就可以看到结果。

使用 try and catch()进行错误处理

您可以使用 try 来测试脚本...catch 构造。只需在 try 条件中添加想要测试的代码,如果有错误,就会执行 catch()中的代码。例如:

示例 TryCatch.js

try{
  isNaN(age);
} catch(error) {
    console.log(error);
    console.log(error.message);
    console.log(error.name);
    console.log(error.fileName);
    console.log(error.lineNumber);
}

当 try 语句中出现错误时,catch()方法将异常对象作为参数进行检索。您可以给这个对象取任何变量名;在这个例子中,我们称之为误差。根据错误和浏览器的不同,该对象将具有不同的属性,并且跨浏览器相同的属性将具有不同的值。例如,Chrome、Firefox、IE 和 Safari 中的 message 属性都返回一个值,但在 Safari 中它有一个不同的结果。要查看结果,请确保在开发人员工具中打开控制台面板。我们将在附录的后面提供更多关于开发者工具的细节。

Chrome:年龄未定义

火狐:年龄未定义

例如:“年龄”未定义

Safari:找不到变量:年龄

在调试过程中使用 try and catch 非常有用,根据浏览器的不同,使用它们可以很容易地发现问题。

顺序取消注释

跟踪错误的另一个简单方法是注释掉整个脚本,并逐个函数地取消注释,或者—如果是单个函数—逐行取消注释。每次通过在浏览器中重新加载来取消对一行的注释时,都要对脚本进行测试,这样可以快速找到导致错误的原因。虽然这需要时间,但是知道错误发生的大致位置会容易得多,这就是为什么您需要依靠浏览器来提供这些信息。

内置开发工具

在浏览器中调试代码已经有了很大的改进。现在所有的浏览器都内置了开发工具,使您能够直接从浏览器中检查、更新和保存代码。但是首先,你如何在每个浏览器中访问开发者工具?

微软互联网浏览器

如果您单击浏览器右上角的齿轮图标,然后选择“F12 开发者工具”,Internet Explorer 将显示其开发者工具您也可以按键盘上的 F12 来显示这些工具。(见图 A-4 。)

9781430250920_AppA-04.jpg

图 A-4 。Microsoft Internet Explorer 中的 F12 开发人员工具

你会注意到内置工具有很多相似的特性。这些特性包括一个面板,用于检查页面的每个部分、HTML、CSS 和 JavaScript,以及一个分析器和一个网络监视器。除了所有这些特性之外,还有一个控制台,您可以在其中直接编写 JavaScript。

Safari

默认情况下,Safari 中的调试工具不可用。若要打开它们,请前往 Safari、偏好设置、高级,然后选择“在菜单栏中显示开发菜单”

这将启用开发下拉菜单。一些可用的特性允许您快速关闭 JavaScript 或更改用户代理来测试您的页面在使用另一个浏览器时的外观。(参见图 A-5 。)Safari 也有开发者工具。通过选择开发,显示 Web 检查器,您可以打开开发工具。

9781430250920_AppA-05.jpg

图 A-5 。Safari 的调试菜单

歌剧

Opera 中的调试工具被称为蜻蜓。(参见图 A-6 。)要访问它们,请进入“工具”、“高级”、“蜻蜓歌剧院”。访问它们的另一种方法是去查看,开发者工具,Opera 蜻蜓。

9781430250920_AppA-06.jpg

图 A-6 。在 Opera 中展示蜻蜓开发者工具

Firefox

Firefox 的工作方式略有不同。工具是有的,但它们看起来与其他浏览器非常不同,这可能会引起一点混乱。要访问它们,请转到“工具”、“Web 开发人员”、“开发人员工具栏”。(见图 A-7 。)一旦你打开工具栏,它就会出现在屏幕的底部。在这里,您可以通过单击 inspect 按钮来检查文档中的 HTML。要检查 CSS 或 JavaScript,请单击 Web 控制台按钮。您可以单击调试器按钮在 JavaScript 中设置断点。

9781430250920_AppA-07.jpg

图 A-7 。展示 Firefox 中的 JavaScript 控制台

铬合金

Chrome 在视图、开发人员、开发人员工具下有自己的工具。(参见图 A-8 。)

9781430250920_AppA-08.jpg

图 A-8 。在 Chrome 中展示开发者工具

检查和调试您的代码

在所有这些浏览器中,你都有能力钻研和查看代码。当试图在你的页面中挑出一些东西时,你可以在你的浏览器中右键单击(或者按住 control 键单击)一些东西,然后选择“检查元素”这将打开大多数浏览器中的调试工具,并将您带到您感兴趣的 HTML 元素。

您也可以直接在开发工具中更新 HTML。通过双击任何 HTML 元素,您可以更新文本,添加 CSS,添加或删除 HTML 元素,或者做任何您想做的事情,并在浏览器中获得实时结果。(参见图 A-9 。)

9781430250920_AppA-09.jpg

图 A-9 。直接在 Chrome 中编辑 HTML

CSS 也是如此。通过使用右侧的样式面板(如图 A-10 所示),您可以直接在浏览器中关闭、编辑或添加 CSS,并立即看到结果。

9781430250920_AppA-10.jpg

图 A-10 。使用样式面板直接在 Chrome 中编辑 CSS

在进行这些更改时,您只需更新浏览器中当前加载的代码。编辑器中的原始代码没有被更新。要覆盖该代码,您可以保存(control/command–S)并用更新后的文件覆盖原始文件。

因为本书的重点是 JavaScript,所以让我们来看看一些调试特性。

在代码中设置断点使您能够实时看到发生了什么。当修复一个问题时,你会发现让浏览器在它正在做的事情中间停下来并向你解释诸如变量的当前值之类的事情会有很大的帮助。

通过在 Chrome 中选择源代码(或者在 Firefox 中选择调试器,在 Safari 的调试器中选择文件),您可以看到 JavaScript 代码。在左边,你有行号,如果你点击其中的任何一个,你会得到一个断点。(参见图 A-11 。)

9781430250920_AppA-11.jpg

图 A-11 。在 Chrome 中设置断点

当您刷新页面时,调试器会运行到该点的所有 JavaScript 并停止。然后,这些工具可以帮助您查看变量的当前值,或者让您查看使用调用堆栈将您带到该点的所有函数调用。它还有很多其他的特性,这些特性是开发者在没有扩展的情况下无法获得的。

控制台

通过查看所有这些浏览器,您可能已经进入了控制台。您在 try/catch 示例中引用了它,现在您将更深入地研究它。

控制台的有趣之处在于,你可以把它想象成命令行 JavaScript——不需要编辑器或外部文件。你可以在你的页面上快速测试一些东西,看看它是否如预期的那样工作。此外,控制台具有代码完成功能(如图图 A-12 所示)。如果您知道某个东西应该如何工作,但不记得具体要写什么,您可以使用控制台来帮助。

9781430250920_AppA-12.jpg

图 A-12 。控制台具有代码完成功能,就像编辑器一样

从控制台,您可以访问文档中的所有 JavaScript 代码。浏览器中内置的每个对象和您在代码中编写的每个对象都是可用的,并且可以从控制台执行。

例如,使用第五章中的一个文件,假设我写了以下内容:

sc.init

组成该功能的代码打印在控制台中。

但是,假设我键入了以下内容:

sc.init();

此时,代码在浏览器中执行。

您可以按需添加事件侦听器、创建警报窗口、创建和更改变量值。(参见图 A-13 。)

9781430250920_AppA-13.jpg

图 A-13 。您可以使用控制台检查从您的站点加载的 JavaScript

当您编写 JavaScript 时,有一个命令您可能会比其他任何命令使用得都多,以确保事情按照您认为应该的方式运行:

console.log();

您在 try/catch 示例中使用了这一点。如果您打开了控制台,您可以在控制台中看到这些语句的结果。在其他语言中,这可能是 ActionScript 中的 print 语句或 trace 。这给了你打印信息的能力。

例如,假设您需要知道您的代码执行到某一点或变量的当前值(显示 image x of 10)。您可以在不停止应用运行的情况下做到这一点,就像使用断点时一样。

JSLint 和 Jasmine

一些独立于浏览器的工具也可以帮助您进行 JavaScript 开发。一个是在线 JavaScript 验证器 JSLint,由道格拉斯·克洛克福特开发,在 http://www.jslint.com/lint.html 可以买到。JSLint 是用 JavaScript 编写的工具,它根据语法有效性来验证脚本,并试图确保良好的编码风格。因为“良好的编码风格”是一个主观的问题,所以您可能希望对 JSLint 报告有所保留。JavaScript 开发大多发生在浏览器环境中,有时您需要变通规则或抄近路来使脚本运行得更快或解决浏览器问题。也就是说,如果你不需要偷工减料的话,它仍然是一个很好的工具,可以用来寻找优化你的脚本的方法。JSLint 的伟大之处在于它为您提供了对脚本的全面分析,包括关于全局变量和不同函数被调用次数的报告。

如果你来自后端编码环境,你可能已经使用过或者至少听说过单元测试(【http://en.wikipedia.org/wiki/Unit_testing】??)。简而言之,单元测试意味着你为你所有的方法和函数编写测试用例,并且当你按下一个按钮时,你使用一个测试工具来连续运行所有这些测试。这样,您可以确保您的代码在开发代码之前,在您定义它必须完成的测试用例时能够正常工作。单元测试框架可用于许多不同的语言,包括 Java、PHP、ActionScript 和 JavaScript。

Pivotal Labs 创建了一个名为 Jasmine 的测试框架,它不依赖于浏览器。你可以从 gitHub 获得关于 Jasmine 的信息:

pivotal.github.io/jasmine/

在 Adobe Developer Connection 上还有一个关于用 Jasmine 进行单元测试的教程,网址是 http://www . Adobe . com/devnet/html 5/articles/unit-test-JavaScript-applications-with-Jasmine . html。

最后

总而言之,现在调试 JavaScript 比几年前容易多了。浏览器内置的开发人员工具使得在您试图追踪某些东西不工作的原因时,这个过程变得容易得多。

也有一些编辑器会在你写代码的时候试图指出潜在的问题。

有一个由谷歌赞助的 Code School 开发的优秀实践课程,教你如何使用 Chrome 中的开发者工具。课程位于discover-devtools.codeschool.com/,免费。

面向移动开发者的调试也得到了改进。Weinre(发音为 wine-erwinery )可以让你在桌面上远程调试移动设备中的浏览器。信息可以在 http://prople.apache.org/∼pmuellr/weinre/docs/latest/的找到。Adobe 有一个类似的产品,叫做 Edge Inspect,它也可以让你在多种设备上调试你的站点。你可以在 http://html.adobe.com/edge/inspect/找到它

.


  1. \w ↩︎

  2. \w ↩︎

posted @ 2024-08-19 15:49  绝不原创的飞龙  阅读(0)  评论(0编辑  收藏  举报