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

86 阅读1小时+

JavaScript DOM 和 AJAX 入门指南(二)

原文:Beginning JavaScript with DOM Scripting and Ajax

协议:CC BY-NC-SA 4.0

三、从 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 以更成熟和以用户为中心的方式使用的程序员、博客作者和设计师组成——你可以在 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 注意developer.mozilla.org/en-US/docs/… 的 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。

可用内容的完整列表取决于您正在收听的活动。你可以在 www.w3.org/TR/DOM-Leve… 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(??)??(你可以在 www.bluerobot.com/web/css/fou…了解更多)。这种效果在应用样式表之前短暂显示没有样式表的页面。

现在,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 的一些最常见的用法,并尝试为它们开发最新的解决方案,以取代您可能已经在使用的旧脚本。