HTML5-编程高级教程-全-

HTML5 编程高级教程(全)

原文:Pro HTML5 Programming

协议:CC BY-NC-SA 4.0

零、简介

HTML5 是全新的。事实上,它甚至还没有完全完成。如果你听一些坏脾气的专家的话,他们会告诉你 HTML5 在十年或更长时间内都不会准备好!

那么,为什么会有人认为现在是出版一本名为 Pro HTML5 编程的书的时候了呢?那很简单。因为对于那些正在寻找额外优势使你的 web 应用鹤立鸡群的人来说,HTML5 的时机到了。这本书的作者已经研究、开发和教授 HTML5 技术两年多了,可以肯定地说,新标准的采用正以令人眩晕的速度加速。即使在写这本书的过程中,我们也被迫不断更新章节,并重新评估我们关于什么可以使用的假设。

大多数用户并没有真正理解他们正在使用的浏览器的强大功能。是的,在他们最喜欢的浏览器自动更新后,他们可能会注意到一些微小的界面增强。但他们可能不知道这个新的浏览器版本只是引入了自由形式的绘图画布或实时网络通信,或任何其他潜在的升级。

通过这本书,我们旨在帮助你打开 HTML5 的力量。

这本书是给谁的

本书的内容面向熟悉 JavaScript 编程的有经验的 web 应用开发人员。换句话说,我们不会在这篇文章中讨论 web 开发的基础知识。有许多现有的资源可以帮助您快速掌握 web 编程的基础知识。也就是说,如果你在以下任何一个要点中看到了自己,这本书可能会为你提供你正在寻找的有用的见解和信息:

  • 你有时会想,“要是我的浏览器能做到就好了。。."
  • 您发现自己正在使用页面源代码和开发工具来剖析一个特别令人印象深刻的网站。
  • 您喜欢阅读最新浏览器更新的发行说明来了解新功能。
  • 您正在寻找优化或简化应用的方法。
  • 你愿意定制你的网站,在相对较新的浏览器上为用户提供最好的体验。

如果这些中的任何一个适用于你,这本书可能很适合你的兴趣。

虽然我们会在适当的地方指出浏览器支持的局限性,但我们的目的不是为您提供复杂的变通方法,让您的 HTML5 应用在十年前的浏览器上无缝运行。经验表明,变通方法和基准浏览器支持发展如此之快,以至于像这样的书不是这类信息的最佳载体。相反,我们将重点讨论 HTML5 的规范以及如何使用它。详细的解决办法可以在互联网上找到,随着时间的推移,将变得不那么必要。

本书概述

这本书的十三个章节涵盖了一系列流行、有用和强大的 HTML5 APIs。在某些情况下,我们在前面章节中介绍的功能的基础上为您提供了更丰富的演示。

第一章“HTML 5 简介”从 HTML 规范的过去和当前版本的背景开始。介绍了新的高级语义标签,以及 HTML5 中所有最新发展背后的基本变化和基本原理。了解这个地形就好。

第二章、第三章、第四章、【音频和视频】描述了新的视觉和媒体元素。在这些章节中,重点是寻找更简单的方法来美化你的用户界面,而不需要插件或者服务器端的交互。

第五章“使用地理定位 API”介绍了一个真正的新功能,这是以前不容易模仿的——应用能够识别用户的当前位置,并使用它来定制体验。隐私在这里很重要,所以我们也包括一些注意事项。

接下来的两章,“使用通信 API”和“使用 WebSocket API”,展示了 HTML5 越来越强大的功能,让您可以与其他网站通信,并以简单和最小的开销将实时数据传输到应用。这些章节中的技术将使你能够简化当今网络上部署的许多过于复杂的架构。

第八章“使用表单应用编程接口”向您介绍了目前您可以对桌面或移动 web 应用进行的最小调整,以提高可用性,以及您可以进行的更基本的更改,以在非常常见的使用场景中检测页面输入错误。第九章“使用拖放 API”,详细阐述了新的拖放 API 特性,并展示了如何使用它们。

第十章、第十一章和第十二章——“使用 Web 工作器 API”、“使用存储 API”和“创建离线 Web 应用”——处理应用的内部管道。在这里,您将找到优化现有功能的方法,以获得更好的性能和更好的数据管理。

最后,第十三章“html 5 的未来”将给你一个即将到来的美味预览。

示例代码和配套网站

本书中给出的示例代码可以在 Apress 网站的源代码部分在线获得。访问[www.apress.com](http://www.apress.com),点击源代码,寻找这本书的书名。你可以从这本书的主页下载源代码。此外,我们在[www.prohtml5.com](http://www.prohtml5.com)为这本书主办了一个伙伴网站,你也可以从那里下载示例代码和一些实用的附加内容。

联系作者

谢谢你买这本书。我们希望你喜欢阅读它,并发现它是一个有价值的资源。尽管我们尽了最大努力避免错误,但我们意识到事情有时会从缝隙中溜走,我们希望提前对任何此类失误表示歉意。我们欢迎您对本书的内容和源代码提出个人反馈、问题和评论。您可以通过发送电子邮件至[prohtml5@gmail.com](http://prohtml5@gmail.com)与我们联系。

一、HTML5 概述

这本书是关于 HTML5 编程的。然而,在理解 HTML5 编程之前,您需要后退一步,理解 HTML5 是什么,它背后的一些历史,以及 HTML 4 和 HTML5 之间的差异。

在这一章中,我们直奔每个人都想得到答案的实际问题。为什么是 HTML5,为什么刚才那么激动?有哪些新的设计原则让 HTML5 真正具有革命性,同时又高度兼容?无插件范例的含义是什么;什么流行什么不流行?HTML 的新特性是什么,它如何为 web 开发人员开启一个全新的时代?我们开始吧。

迄今为止的故事 HTML5 的历史

HTML 可以追溯到很久以前。它于 1993 年首次作为互联网草稿发表。90 年代见证了围绕 HTML 的大量活动,出现了 2.0 版、3.2 版和 4.0 版(同一年!),最后在 1999 年,4.01 版本。在其发展过程中,万维网联盟(W3C)控制了该规范。

在这四个版本快速发布后,HTML 被广泛认为是死胡同;web 标准的焦点转移到了 XML 和 XHTML 上,而 HTML 被放在了次要位置。与此同时,HTML 拒绝消亡,web 上的大多数内容继续作为 HTML 提供服务。为了启用新的 web 应用并解决 HTML 的缺点,HTML 需要新的特性和规范。

为了将网络平台提升到一个新的高度,一小群人在 2004 年成立了网络超文本应用工作组(WHATWG)。他们创建了 HTML5 规范。他们还开始开发专门针对网络应用的新功能——他们认为这是最缺乏的领域。大约在这个时候,Web 2.0 这个术语被创造出来。这真的像是第二个新网站,静态网站让位于需要更多功能的更动态的社交网站——更多的功能。

W3C 在 2006 年再次参与 HTML,并在 2008 年发布了 HTML5 的第一个工作草案,XHTML 2 工作组在 2009 年停止了工作。又过了两年,这就是我们今天的处境。因为 HTML5 解决了非常实际的问题(您将在后面看到),浏览器供应商正在狂热地实现它的新特性,尽管规范还没有完全锁定。浏览器的实验反馈并改进了规范。HTML5 正在快速发展,以解决 web 平台的真正和实际的改进。

HTML 中的时刻

Brian 说:“嗨,我是 Brian,我是一个 HTML 守财奴。

早在 1995 年,我就创作了我的第一个主页。在那个时候,“主页”是你用来介绍自己的东西。它通常由糟糕的扫描图片、<blink>标签、关于你住在哪里和你在读什么的信息,以及你目前正在从事的与计算机相关的项目组成。我和我的大多数“万维网开发人员”都在大学上学或受雇于大学。

当时,HTML 还很原始,工具也不可用。除了一些原始的文本处理脚本,Web 应用几乎不存在。页面使用您最喜欢的文本编辑器手工编码。它们每隔几周或几个月就会更新一次。

十五年来,我们走过了漫长的道路。

如今,用户一天多次更新在线个人资料并不罕见。如果没有建立在每一代人基础上的在线工具的稳步发展,这种类型的互动是不可能的。

当你读这本书时,请记住这一点。我们在这里展示的例子有时可能看起来过于简单,但是潜力是无限的。我们这些在 20 世纪 90 年代中期第一次使用<img>标签的人可能不知道在十年之内,许多人会在网上存储和编辑他们的照片,但我们应该预测到这一点。

我们希望本书中的例子能启发你超越基础知识,为下一个十年创造新的网络基础。"

2022 年的神话和为什么没关系

我们今天看到的 HTML5 规范已经作为工作草案发布了——它还不是最终版本。那么什么时候它会被固定下来呢?以下是你需要知道的关键日期。第一个是 2012 年,这是推荐候选人的目标日期。第二个日期是 2022 年,也就是提出的推荐。等等!别这么快!在你考虑这两个日期实际上意味着什么之前,不要合上书把它放在一边十年。

第一个也是最近的日期可以说是最重要的一个,因为一旦我们到达那个阶段,HTML5 就完成了。那就在拐角处。所提出的建议(我们都同意这有点遥远)的意义在于将会有两个可互操作的实现。换句话说,两个浏览器配备了完整规范的完全可互操作的实现——一个崇高的目标,实际上使 2022 年的最后期限显得雄心勃勃。毕竟,我们甚至还没有在 HTML4 中实现这一点,只是最近才在 CSS2 中实现!

现在重要的是,浏览器供应商正在积极增加对许多非常酷的新功能的支持,其中一些已经处于最终征求意见阶段。根据您的受众,您现在就可以开始使用其中的许多功能。当然,在前进的道路上需要做出许多微小的改变,但这是享受生活在前沿的好处的小小代价。当然,如果你的观众使用的是 Internet Explorer 6.0,许多新功能将无法工作,需要模拟——但这仍然不是放弃 HTML5 的好理由。毕竟,这些用户最终也会跳到更高的版本。他们中的许多人可能会马上转向 IE 浏览器 9.0,而那个版本的 IE 浏览器支持更多的 HTML5 特性。实际上,新的浏览器和改进的仿真技术的结合意味着你可以在今天或不久的将来使用许多 HTML5 特性。

谁在开发 HTML5?

我们都知道需要一定程度的结构,显然需要有人负责 HTML5 的规范。这一挑战是三个重要组织的工作:

  • web 超文本应用技术工作组(WHATWG):WHATWG 成立于 2004 年,由为苹果、Mozilla、谷歌和 Opera 等浏览器厂商工作的个人创立,为 Web 应用开发开发 HTML 和 API,并为浏览器厂商和其他相关方提供开放协作。
  • 万维网联盟(W3C):W3C 包含 HTML 工作组,目前负责交付他们的 HTML5 规范。
  • 互联网工程任务组(IETF) :该任务组包含负责互联网协议(如 HTTP)的小组。HTML5 定义了一个新的 WebSocket API,它依赖于一个新的 WebSocket 协议,该协议正在 IETF 工作组中开发。

新的愿景

HTML5 基于各种设计原则,在 WHATWG 规范中有详细说明,真正体现了可能性和实用性的新愿景。

  • 和睦相处
  • 效用
  • 互用性
  • 普及高等教育

兼容性和铺设牛道

不用担心;HTML5 不是一场令人不安的革命。事实上,它的核心原则之一是保持一切顺利进行。如果不支持 HTML5 特性,行为必须适度降级。此外,由于 HTML 内容已经存在了大约 20 年,所以支持所有现有的内容非常重要。

在研究普通行为方面已经投入了很多努力。例如,谷歌分析了数百万个页面,以发现DIV标签的通用 ID 和Class名称,并发现了大量重复。例如,许多人使用DIV id="header"来标记标题内容。HTML5 都是解决现实问题的,对吧?那么为什么不简单地创建一个<header>元素呢?

虽然 HTML5 标准的一些特性相当具有革命性,但游戏的名称是进化而不是革命。毕竟,为什么要重新发明轮子呢?(或者,如果一定要,那至少做一个更好的吧!)

效用和选区的优先级

HTML5 规范是基于明确的选区优先级编写的。就优先级而言,“用户为王”这意味着,当有疑问时,规范更看重用户,而不是作者、实现者(浏览器)、说明符(W3C/WHATWG)和理论纯度。因此,HTML5 非常实用,尽管在某些情况下并不完美。

考虑这个例子。以下代码片段在 HTML5 中同样有效:

id="prohtml5" id=prohtml5 ID="prohtml5"

当然,有些人会反对这种宽松的语法,但底线是最终用户并不真正关心。我们并不是建议您开始编写草率的代码,但最终,当前面的任何示例生成错误并且不能呈现页面的其余部分时,最终用户会受到影响。

HTML5 还催生了 XHTML5 的创建,使 XML 工具链能够生成有效的 HTML5 代码。HTML 或 XHTML 版本的序列化应该产生差别最小的相同 DOM 树。显然,XHTML 语法要严格得多,最后两个例子中的代码是无效的。

通过设计确保安全

从一开始就非常强调 HTML5 的安全性。规范的每一部分都有关于安全性考虑的章节,安全性已经被预先考虑了。HTML5 引入了一个新的基于起源的安全模型,它不仅易于使用,而且可以被不同的 API 一致地使用。这种安全模式允许我们以过去不可能的方式做事。例如,它允许我们安全地跨域通信,而不必回到各种聪明的、创造性的、但最终不安全的黑客攻击。在这方面,我们肯定不会回顾过去的美好时光。

演示和内容的分离

HTML5 向表示和内容的彻底分离迈出了一大步。HTML5 尽可能地创建这种分离,并且使用 CSS 来实现。事实上,由于前面提到的兼容性设计原则,早期版本的 HTML 的大多数表示特性不再受支持,但仍然可以工作。然而,这个想法并不完全是新的;它已经在 HTML4 Transitional 和 XHTML1.1 中出现了。Web 设计人员长期以来一直将此作为最佳实践,但是现在,更重要的是将两者清楚地分开。表示标记的问题是:

  • 可达性差
  • 不必要的复杂性(使用所有的内联样式更难阅读您的代码)
  • 较大的文档大小(由于样式内容的重复),这意味着页面加载速度较慢

互通简化

HTML5 是关于简化和避免不必要的复杂性。HTML5 口头禅?“简单一点比较好。尽可能简化。”以下是这方面的一些例子:

  • 原生浏览器功能,而不是复杂的 JavaScript 代码
  • 一个新的,简化的DOCTYPE
  • 新的简化字符集声明
  • 强大而简单的 HTML5 APIs

我们稍后会详细讨论其中的一些。

为了实现所有这些简单性,规范变得更大,因为它需要更精确——事实上,比以前任何版本的 HTML 规范都更精确。它规定了大量定义明确的行为,以努力在 2022 年前实现真正的浏览器互操作性。含糊不清根本不会让这种情况发生。

HTML5 规范也比以前的规范更详细,以防止误解。它旨在彻底定义事物,尤其是 web 应用。难怪该规范长达 900 多页!

HTML5 的设计也是为了很好地处理错误,有各种改进的和雄心勃勃的错误处理计划。实际上,它更喜欢优雅的错误恢复,而不是硬故障,再次让 A-1 优先考虑最终用户的利益。例如,文档中的错误不会导致页面无法显示的灾难性故障。相反,错误恢复是精确定义的,因此浏览器可以以标准方式显示“损坏的”标记。

普遍接入

这一原则分为三个概念:

  • 可访问性(Accessibility):为了支持残疾用户,HTML5 与一个名为 Web Accessibility Initiative (WAI)的可访问富互联网应用(ARIA)的相关标准紧密合作。屏幕阅读器支持的 WAI-ARIA 角色已经可以添加到 HTML 元素中了。
  • 媒体独立性:如果可能的话,HTML5 功能应该在所有不同的设备和平台上工作。
  • 支持所有世界语言:例如,新的<ruby>元素支持在东亚排版中使用的 Ruby 注释。

无插件的范例

HTML5 提供了对许多功能的原生支持,这些功能过去只能通过插件或复杂的攻击来实现(原生绘图 API、原生视频、原生套接字等等)。

当然,插件会带来许多问题:

  • 插件不能总是被安装。
  • 可以禁用或阻止插件(例如,苹果 iPad 不附带 Flash 插件)。
  • 插件是一种独立的攻击媒介。
  • 插件很难与 HTML 文档的其余部分集成在一起(因为插件边界、剪裁和透明度问题)。

虽然有些插件的安装率很高(例如 Adobe Flash),但它们在受控的企业环境中经常被屏蔽。此外,一些用户选择禁用这些插件,因为它们支持不受欢迎的广告显示。然而,如果用户禁用了你的插件,他们也禁用了你用来显示内容的程序。

插件通常也很难将它们的显示与浏览器的其他内容集成在一起,这导致了某些网站设计的剪辑或透明度问题。因为插件使用一个独立的呈现模型,这个模型不同于基础网页,如果弹出菜单或其他视觉元素需要跨越页面上的插件边界,开发者会面临困难。这就是 HTML5 登场的地方,微笑着,挥舞着它的魔法棒,展示着原生功能。你可以用 CSS 设计元素的样式,用 JavaScript 编写脚本。事实上,这是 HTML5 展示其最大力量的地方,向我们展示了 HTML 以前版本中不存在的力量。不仅仅是新元素提供了新的功能。它还增加了与脚本和样式的本地交互,使我们能够做比以前更多的事情。

以新的 canvas 元素为例。它使我们能够做一些非常基本的事情,这在以前是不可能的(试着用 HTML 4 在网页上画一条对角线)。然而,最有趣的是我们可以用 API 释放的力量,以及我们可以用几行 CSS 代码应用的样式。像表现良好的孩子一样,HTML5 元素也可以很好地配合使用。例如,您可以从视频元素中抓取一帧并显示在画布上,用户只需单击画布就可以从您刚才显示的帧回放视频。这只是一个本地代码相对于插件所能提供的一个例子。事实上,当你不使用黑盒时,几乎所有事情都会变得更容易。所有这些加起来就是一个真正强大的新媒体,这就是为什么我们决定写一本关于 HTML5 编程的书,而不仅仅是关于新元素!

什么流行,什么不流行?

那么,到底是 HTML5 的什么部分呢?如果您仔细阅读规范,您可能不会发现我们在本书中描述的所有特性。例如,你不会在那里找到地理位置和网络工作者。所以这是我们瞎编的吗?都是炒作吗?不,一点也不!

HTML5 工作的许多部分最初是 HTML5 规范的一部分,然后被转移到单独的标准文档中,以保持规范的重点。人们认为,在将其中一些特性纳入官方规范之前,在单独的轨道上对它们进行讨论和编辑是更明智的做法。这样,一个小的有争议的标记问题不会阻碍整个规范的展示。

特定领域的专家可以在邮件列表上一起讨论给定的特性,而不会有太多的争论。业界仍然将包括地理定位等在内的原始功能称为 HTML5。那么,可以把 HTML5 想象成一个涵盖核心标记以及许多很酷的新 API 的总称。在撰写本文时,这些特性是 HTML5 的一部分:

  • 画布(2D 和 3D)
  • 跨文档消息传递
  • 地理定位
  • 音频和视频
  • 形式
  • 数学公式
  • 微观数据
  • 服务器发送的事件
  • 可缩放矢量图形(SVG)
  • WebSocket API 和协议
  • 网络起源概念
  • 网络存储
  • 索引数据库
  • 应用缓存(离线 Web 应用)
  • 网络工作者
  • 拖放
  • XMLHttpRequest 级别 2

如你所见,我们在本书中涉及的许多 API 都在这个列表中。我们如何选择覆盖哪些 API?我们选择覆盖至少有些烘焙的特性。翻译?它们以某种形式存在于不止一个浏览器中。其他(不太成熟的)功能可能只在一个特殊的浏览器测试版中起作用,而其他功能目前还只是想法。

就浏览器支持而言,有一些优秀的在线资源可以用来检查当前(和未来)的浏览器支持。站点[www.caniuse.com](http://www.caniuse.com)提供了按浏览器版本细分的功能和浏览器支持的详尽列表,站点[www.html5test.com](http://www.html5test.com)检查您用来访问它的浏览器对 HTML5 功能的支持。

此外,这本书并不关注为您提供仿真解决方案,让您的 HTML5 应用在老式浏览器上无缝运行。相反,我们将主要关注 HTML5 的规范以及如何使用它。也就是说,对于每个 API,我们都提供了一些示例代码,您可以使用它们来检测其可用性。我们不使用通常不可靠的用户代理检测,而是使用特性检测。为此,你也可以使用Modernizr——一个 JavaScript 库,提供非常高级的 HTML5 和 CSS3 特性检测。我们强烈建议您在应用中使用 Modernizr,因为它无疑是最好的工具。

HTML 中的更多时刻

弗兰克说:“你好,我是弗兰克,我有时会画画。

我看到的第一个 HTML 画布演示是一个基本的绘画应用,它模仿了 Microsoft Paint 的用户界面。尽管它比数字绘画的艺术水平落后了几十年,而且当时只能在现有浏览器的一小部分上运行,但它让我开始思考它所代表的可能性。

当我进行数码绘画时,我通常使用本地安装的桌面软件。虽然其中一些程序非常优秀,但它们缺乏让 web 应用如此出色的特性。简而言之,它们是脱节的。迄今为止,共享数字绘画涉及从绘画应用中导出图像并将其上传到网络。在现场画布上合作或评论是不可能的。HTML5 应用可以缩短导出周期,并使创作过程与完成的图像一起融入在线世界。

不能用 HTML5 实现的应用数量正在减少。对于文本来说,网络已经是终极的双向交流媒介。基于文本的应用有完全基于网络的形式。他们的图形对应物,如绘画、视频编辑和 3D 建模软件,现在才刚刚出现。

我们现在可以构建优秀的软件来创建和欣赏图像、音乐、电影等等。更好的是,我们制作的软件可以在网上或网下使用:一个无处不在、授权和在线的平台。"

HTML5 有什么新功能?

在开始编写 HTML5 之前,我们先来快速了解一下 HTML5 的新特性。

新文档类型和字符集

首先,网页的DOCTYPE被大大简化了。例如,比较下面的 HTML4 DOCTYPE:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"![Image](https://gitee.com/OpenDocCN/vkdoc-html-css-zh/raw/master/docs/pro-h5-prog/img/U002.jpg)  "http://www.w3.org/TR/html4/loose.dtd">

谁会记得这些呢?我们当然不能。我们总是将一些冗长的DOCTYPE复制并粘贴到页面上,内心深处总是担心,“你绝对确定你粘贴的是正确的吗?”HTML5 巧妙地解决了这个问题,如下所示:

<!DOCTYPE html>

现在那是 a DOCTYPE你可能只记得。和新的DOCTYPE一样,字符集声明也被缩写了。曾经是

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

现在,它是:

<meta charset="utf-8">

如果您愿意,甚至可以去掉“utf-8”两边的引号。使用新的DOCTYPE触发浏览器以标准模式显示页面。例如,图 1-1 显示了如果你在 Firefox 中打开一个 HTML5 页面,点击工具Image页面信息,你会看到的信息。在此示例中,页面以标准模式呈现。

Image

图 1-1。以符合标准的模式呈现的页面

当您使用新的 HTML5 DOCTYPE时,它会触发浏览器以符合标准的模式呈现页面。如您所知,Web 页面可以有不同的呈现模式,比如古怪、近乎标准和标准(或无古怪)模式。DOCTYPE向浏览器指示使用哪种模式以及使用什么规则来验证您的页面。在 Quirks 模式下,浏览器试图避免破坏页面,即使它们不完全有效也要呈现它们。HTML5 引入了新元素,并将其他元素标记为过时元素(下一节将详细介绍)。如果您使用这些过时的元素,您的页面将无效。然而,浏览器会像以前一样继续呈现它们。

新的和废弃的元素

HTML5 引入了许多新的标记元素,并将其分为七种不同的内容类型。这些在下面的表 1-1 中显示。

Image

Image

这些元素中的大多数都可以用 CSS 进行样式化。此外,它们中的一些,比如canvasaudiovideo,可以单独使用,尽管它们伴随着允许细粒度本地编程控制的 API。这些 API 将在本书后面更详细地讨论。

讨论所有这些新元素超出了本书的范围,但是大多数剖切元素(在下一节中讨论)都是新的。在 HTML5 中,canvasaudiovideo元素也是新的。

同样,我们也不打算提供一个所有不推荐使用的标签的详尽列表(网上有很多关于这方面的好的在线资源),但是许多执行内联样式的元素已经被标记为过时,以利于使用 CSS,比如bigcenterfontbasefont

语义标记

包含许多新 HTML5 元素的一种内容类型是 sectioning 内容类型。HTML5 定义了一个新的语义标记来描述元素的内容。使用语义标记不会给最终用户带来任何直接的好处,但是它确实简化了 HTML 页面的设计。更重要的是,它将使你的网页更易于机器阅读和访问。例如,搜索和联合引擎在抓取和索引页面时肯定会利用这些元素。

正如我们之前说过的,HTML5 就是要铺好牛路。谷歌和 Opera 分析了数百万个页面,以发现DIV标签的通用 ID 名称,并发现了大量重复。例如,由于许多人使用DIV id="footer"来标记页脚内容,HTML5 提供了一组新的分节元素,您现在可以在现代浏览器中使用。表 1-2 显示了不同的语义标记元素。

Image

Image

所有这些元素都可以用 CSS 样式化。事实上,正如我们在前面的实用程序设计原则中所描述的,HTML5 推动了内容和表示的分离,所以您必须在 HTML5 中使用 CSS 样式来设计您的页面。清单 1-1 展示了一个 HTML5 页面可能的样子。它使用新的DOCTYPE、字符集和语义标记元素——简而言之,新的分段内容。代码文件(sample.html)在code/intro文件夹中。

清单 1-1。一个 HTML5 页面的例子

`

<head>   <meta charset="utf-8" >   <title>HTML5</title>   <link rel="stylesheet" href="html5.css"> </head> <body>    <header>      <h1>Header</h1>      <h2>Subtitle</h2>      <h4>HTML5 Rocks!</h4>    </header>


        
            

            

              

                

Article Header


              

              

Lorem ipsum dolor HTML5 nunc aut nunquam sit amet, consectetur adipiscing
elit. Vivamus at  
                      est eros, vel fringilla urna.


              

Per inceptos himenaeos. Quisque feugiat, justo at vehicula pellentesque,
turpis
                     lorem dictum nunc.


              

                

Article Footer

              

            

            

              

                

Article Header


              

              

HTML5: "Lorem ipsum dolor nunc aut nunquam sit amet, consectetur
                     adipiscing elit. Vivamus at est eros, vel fringilla urna. Pellentesque
odio


     

                

Article Footer


              

            

        

        
        

          

Footer


        

  

`

如果没有样式,页面看起来会很乏味。清单 1-2 显示了一些可用于样式化内容的 CSS 代码。代码文件(html5.css)位于code/intro文件夹中。这个样式表使用了一些新的 CSS3 特性,比如圆角(border-radius)和旋转变换(transform: rotate();)。CSS3——就像 HTML5 本身一样——仍在开发中,为了更容易被浏览器接受,它使用子规范进行了模块化(例如,转换、动画和过渡都在单独的子规范中)。

实验性的 CSS3 特性以供应商字符串为前缀,以避免规范改变时的命名空间冲突。为了显示圆角、渐变、阴影和变换,目前需要在声明中使用前缀,如-moz-(Mozilla)、o-(Opera)、-webkit-(Safari 和 Chrome 等基于 WebKit 的浏览器)和-ms-(Internet Explorer)。

清单 1-2。html 5 页面的 CSS 文件

`body {
        background-color:#CCCCCC;
        font-family:Geneva,Arial,Helvetica,sans-serif;
        margin: 0px auto;
        max-width:900px;
        border:solid;
        border-color:#FFFFFF;
}

header {
        background-color: #F47D31;
        display:block; color:#FFFFFF;
        text-align:center;
}

header h2 {
        margin: 0px;
}

h1 {
        font-size: 72px;
        margin: 0px;
}

h2 {
        font-size: 24px;
        margin: 0px;
        text-align:center;
        color: #F47D31;
}

h3 {
        font-size: 18px;
        margin: 0px;
        text-align:center;
        color: #F47D31;
}

h4 {
        color: #F47D31;
        background-color: #fff;
        -webkit-box-shadow: 2px 2px 20px #888;
        -webkit-transform: rotate(-45deg);
        -moz-box-shadow: 2px 2px 20px #888;
        -moz-transform: rotate(-45deg);
        position: absolute;
        padding: 0px 150px;
        top: 50px;
        left: -120px;
        text-align:center;

}

nav {
        display:block;
        width:25%;
        float:left;
}

nav a:link, nav a:visited {
        display: block;
        border-bottom: 3px solid #fff;
        padding: 10px; text-decoration: none;
        font-weight: bold;
        margin: 5px;
}

nav a:hover {
        color: white;
        background-color: #F47D31;
}

nav h3 {
        margin: 15px;
        color: white;
}

container {

background-color: #888;
}

section {
        display:block;
        width:50%;
        float:left;
}

article {
        background-color: #eee;
        display:block;
        margin: 10px;
        padding: 10px;
        -webkit-border-radius: 10px;
        -moz-border-radius: 10px;
        border-radius: 10px;
        -webkit-box-shadow: 2px 2px 20px #888;
        -webkit-transform: rotate(-10deg);
        -moz-box-shadow: 2px 2px 20px #888;
        -moz-transform: rotate(-10deg);
}

article header {
        -webkit-border-radius: 10px;
        -moz-border-radius: 10px;
        border-radius: 10px;
        padding: 5px;

}

article footer {
        -webkit-border-radius: 10px;
        -moz-border-radius: 10px;
        border-radius: 10px;
        padding: 5px; }

article h1 {
        font-size: 18px;
}

aside {
        display:block;
        width:25%;
        float:left;
}

aside h3 {
        margin: 15px;
        color: white;
}

aside p {
        margin: 15px;
        color: white;
        font-weight: bold;
        font-style: italic;
}

footer {
        clear: both;
        display: block;
        background-color: #F47D31;
        color:#FFFFFF;
        text-align:center;
        padding: 15px;
}

footer h2 {
        font-size: 14px;
        color: white;
}

/* links */
a {
        color: #F47D31;
}

a:hover {
        text-decoration: underline;
}`

图 1-2 显示了清单 1-1 中页面的一个例子,使用 CSS(和一些 CSS3)样式。但是,请记住,不存在典型的 HTML5 页面。任何事情都有可能发生,这个例子使用了许多新的标签,主要是为了演示。

Image

图 1-2。一个包含所有新语义标记元素的 HTML5 页面

最后要记住的一点是,浏览器可能看起来好像它们实际上理解这些新元素。然而,事实是,这些元素本来可以被重命名为foobar,然后进行样式化,它们也会以同样的方式呈现(但是当然,它们在搜索引擎优化中不会有任何好处)。唯一的例外是 Internet Explorer,它要求元素是 DOM 的一部分。因此,如果您想在 IE 中看到这些元素,您必须以编程方式将它们插入到 DOM 中,并将它们显示为块元素。一个方便的脚本就是 html5shiv ( [code.google.com/p/html5shiv/](http://code.google.com/p/html5shiv/))。

使用选择器 API 简化选择

除了新的语义元素,HTML5 还引入了在页面 DOM 中查找元素的新的简单方法。表 1-3 显示了先前版本的文档对象,它允许开发人员进行一些调用来找到页面中的特定元素。

Image

有了新的选择器 API,现在有了更精确的方法来指定想要检索哪些元素,而不需要使用标准 DOM 在文档中循环和迭代。选择器 API 公开了与 CSS 中相同的选择器规则,作为在页面中查找一个或多个元素的方法。例如,CSS 已经有了基于嵌套、兄弟和子模式选择元素的便利规则。CSS 的最新版本增加了对更多伪类的支持——例如,是否启用、禁用或检查一个对象——以及您可以想象的任何属性和层次结构的组合。要使用 CSS 规则选择 DOM 中的元素,只需利用表 1-4 中所示的函数之一。

Image

还可以向选择器 API 函数发送多个选择器规则,例如:

// select the first element in the document with the // style class highClass or the style class lowClass var x = document.querySelector(“.highClass”, “.lowClass”);

querySelector()的情况下,选择匹配任一规则的第一个元素。在querySelectorAll()的情况下,将返回与任何列出的规则匹配的任何元素。多个规则以逗号分隔。

新的选择器 API 使得选择以前很难跟踪的文档部分变得容易。例如,假设您希望能够找到当前鼠标悬停在其上的表格中的任意单元格。清单 1-3 展示了使用选择器是多么简单。这个示例文件(querySelector.htmlquerySelectorAll.html)位于code/intro目录中。

清单 1-3。使用选择器 API

`

<head>   <meta charset="utf-8" />   <title>Query Selector Demo</title>

</head> <body>   <section>     <!-- create a table with a 3 by 3 cell display -->    <table>       <tr>         <td>A1</td> <td>A2</td> <td>A3</td>       </tr>       <tr>         <td>B1</td> <td>B2</td> <td>B3</td>       </tr>       <tr>         <td>C1</td> <td>C2</td> <td>C3</td>       </tr>     </table>

Focus the button, hover over the table cells, and hit Enter to identify them  using querySelector('td:hover').

    
    


  

`

从这个例子中可以看出,查找用户悬停的元素是一个简单的练习,使用:

var hovered = document.querySelector("td:hover");

Image 注意选择器 API 不仅方便,而且通常比使用遗留子检索 API 遍历 DOM 更快。为了实现快速样式表,浏览器针对选择器匹配进行了高度优化。

在 W3C,选择器的正式规范与 CSS 的规范是分开的,这并不奇怪。正如您在这里看到的,选择器通常在样式之外很有用。新选择器的全部细节超出了本书的范围,但是如果您是一名正在寻找操作 DOM 的最佳方法的开发人员,我们鼓励您使用新的选择器 API 来快速导航您的应用结构。

JavaScript 日志记录和调试

尽管从技术上讲,JavaScript 日志和浏览器内调试工具并不是 HTML5 的特性,但在过去几年中,它们已经得到了很大的改进。第一个分析网页和其中运行的代码的伟大工具是 Firefox 插件 Firebug。

类似的功能现在可以在所有其他浏览器的内置开发工具中找到:Safari 的 Web Inspector、谷歌的 Chrome 开发者工具、Internet Explorer 的开发者工具和 Opera 的蜻蜓。图 1-3 展示了谷歌 Chrome 开发者工具(在 Windows 上使用快捷键 CTRL + Shift + J 或者在 Mac 上使用快捷键 Command + Option + J 来访问这个),这些工具提供了关于你的网页的大量信息;这些视图包括调试控制台、元素视图、资源视图和脚本视图,仅举几个例子。

Image

图 1-3。Chrome 中的开发者工具视图

许多调试工具都提供了一种设置断点来暂停代码执行以及分析程序状态和变量当前状态的方法。API 已经成为 JavaScript 开发者事实上的日志标准。许多浏览器都提供了分窗格视图,允许您查看记录到控制台的消息。使用console.log比调用alert()好得多,因为它不会暂停程序执行。

窗口。数据

JSON 是一种相对较新且越来越流行的表示数据的方式。它是将数据表示为对象文字的 JavaScript 语法的子集。由于其简单性和对 JavaScript 编程的天然适应性,JSON 已经成为 HTML5 应用中数据交换的事实上的标准。JSON 的规范 API 有两个函数,parse()stringify()(意思是序列化或者转换成字符串)。

要在旧浏览器中使用 JSON,需要一个 JavaScript 库(可以在[json.org](http://json.org)找到几个)。JavaScript 中的解析和序列化并不总是像您希望的那样快,所以为了加快速度,新的浏览器现在有了 JSON 的本机实现,可以从 JavaScript 调用。本机 JSON 对象被指定为 ECMAScript 5 标准的一部分,涵盖了下一代 JavaScript 语言。它是 ECMAScript 5 第一批被广泛实现的部分之一。现在每个现代浏览器都有window.JSON,你可以期待在 HTML5 应用中看到相当多的 JSON。

DOM 三级

web 应用开发中最受诟病的部分之一是事件处理。虽然大多数浏览器支持事件和元素的标准 API,但 Internet Explorer 有所不同。早期,Internet Explorer 实现了一个不同于最终标准的事件模型。Internet Explorer 9 (IE9)现在支持 DOM Level 2 和 3 特性,因此您最终可以在所有 HTML5 浏览器中使用相同的代码进行 DOM 操作和事件处理。这包括非常重要的addEventListener()dispatchEvent()方法。

猴子、松鼠和其他速度奇快的动物

最新一轮的浏览器创新不仅仅是新标签和新 API。最近最重要的变化之一是主流浏览器中 JavaScript/ECMAScript 引擎的快速发展。正如新的 API 开放了在上一代浏览器中不可能实现的功能一样,整个脚本引擎的执行速度加快有利于现有的 web 应用和那些使用最新 HTML5 功能的应用。认为您的浏览器无法处理复杂的图像或数据处理,或者冗长文稿的编辑?再想想。

在过去的几年里,浏览器供应商一直在进行虚拟军备竞赛,看谁能开发出最快的 JavaScript 引擎。虽然 JavaScript 最早的迭代是纯解释的,但最新的引擎将脚本代码直接编译成本机代码,与 2000 年代中期的浏览器相比,速度提高了几个数量级。

2006 年,Adobe 向 Mozilla 项目捐赠了 ECMAScript 的实时(JIT)编译引擎和虚拟机——代号为 Tamarin——时,这一行动就开始了。尽管只有少量的 Tamarin 技术保留在最新版本的 Mozilla 中,但 Tamarin 的贡献帮助在每种浏览器中产生了新的脚本引擎,它们的名字就像它们声称的性能一样有趣。

Image

总而言之,浏览器厂商之间的良性竞争使得 JavaScript 的性能越来越接近原生桌面应用代码。

HTML 中的更多时刻

彼得说:“说到竞争和速度奇快,我的名字叫彼得,跑步是我的爱好——经常跑步。

超跑是一项很棒的运动,在这里你会遇到很棒的人。在跑完 100 英里赛跑或 165 英里越野跑的最后几英里时,你真的会以一种全新的方式去了解别人。在那一点上,你真的被剥离到你的本质,伟大的友谊可以发生的地方。当然,仍然有竞争的因素,但最重要的是有一种深厚的同志情谊。但是我在这里跑题了。

为了跟踪我的朋友在我不能参加的比赛中的表现(例如,当我在写一本 HTML5 的书时),我通常会在比赛网站上关注。不足为奇的是,“实时跟踪”选项通常很不可靠。

几年前,我偶然发现了一个欧洲竞赛的网站,里面有所有正确的想法。他们给跑在前面的人发放 GPS 追踪器,然后在地图上显示这些参赛者(我们将在本书中使用地理定位和 WebSocket 构建一些类似的演示)。尽管这是一个相当原始的实现(用户必须点击“刷新页面”才能看到更新!),我可以立即看到不可思议的潜力。

现在,仅仅几年后,HTML5 为我们提供了工具来构建这种现场比赛跟踪网站,其中包括用于位置感知应用的地理定位和用于实时更新的 WebSockets 等 API。毫无疑问,HTML5 已经以胜利者的姿态冲过了终点线!"

总结

在这一章中,我们已经给了你一个 HTML5 的基本概要。

我们绘制了它的发展历史和一些即将到来的重要日期。我们还概述了 HTML5 时代背后的四个新的设计原则:兼容性、实用性、互操作性和通用访问。这些原则中的每一条都为一个充满可能性的世界打开了大门,并关闭了一系列现在已经过时的实践和惯例。然后我们介绍了 HTML5 令人吃惊的新的无插件范例,我们回顾了 HTML5 的新特性,比如新的DOCTYPE和字符集,大量新的标记元素,我们还讨论了 JavaScript 霸权的竞争。

在下一章,我们将从探索 HTML5 的编程方面开始,从 Canvas API 开始。

二、使用画布 API

在这一章中,我们将探索你可以用 Canvas API 做什么——一个很酷的 API,使你能够动态地生成和渲染图形、图表、图像和动画。我们将指导您使用渲染 API 的基础知识来创建一个可以根据浏览器环境进行缩放和调整的绘图。我们将向您展示如何在热图显示中基于用户输入创建动态图片。当然,我们也会提醒您使用 Canvas 的陷阱,并分享克服它们的技巧。

这一章只假设了少量的图形专业知识,所以不要害怕尝试 HTML5 最强大的特性之一。

html 5 画布概述

关于 Canvas API 的使用可以写一整本书(而且不会是一本小书)。因为我们只有一章,所以我们将涵盖(我们认为是)这个非常广泛的 API 中最常用的功能。

历史

canvas 的概念最初是由苹果公司引入的,用于 Mac OS X WebKit 来创建仪表板小部件。在 canvas 出现之前,你只能通过插件在浏览器中使用绘图 API,例如用于 Flash 和可缩放矢量图形(SVG)的 Adobe 插件,仅在 Internet Explorer 中使用的矢量标记语言(VML),或者其他聪明的 JavaScript 黑客。

例如,尝试在没有canvas元素的情况下绘制一条简单的对角线——这听起来很容易,但如果您没有简单的二维绘图 API,这将是一项相当复杂的任务。Canvas 正好提供了这一点,因为它在浏览器中非常有用,所以被添加到 HTML5 规范中。

早些时候,苹果暗示可能会在 WHATWG 的 canvas 规范草案中保留知识产权,这在当时引起了一些 web 标准化追随者的担忧。然而,最终苹果还是根据 W3C 的免版税专利许可条款披露了这些专利。

SVG 对 Canvas

Peter 说:“画布本质上是一个位图画布,因此在画布上绘制的图像是最终的,不能像可缩放矢量图形(SVG)图像那样调整大小。此外,绘制在画布上的对象不是页面 DOM 的一部分,也不是任何名称空间的一部分——如果您需要点击检测或基于对象的更新,这被认为是一个弱点。另一方面,SVG 图像可以在不同的分辨率下无缝缩放,并允许点击检测(精确知道图像被点击的位置)。

那么,WHATWG HTML5 规范为什么不专门使用 SVG 呢?尽管 HTML5 Canvas API 有明显的缺点,但它有两个优点:它的性能很好,因为它不必为它绘制的每个图元存储对象;基于其他编程语言中常见的许多二维绘图 API 实现 Canvas API 相对容易。归根结底,一鸟在手总比双鸟在林好。"

什么是画布?

当你在网页中使用一个canvas元素时,它会在页面上创建一个矩形区域。默认情况下,这个矩形区域是 300 像素宽和 150 像素高,但是您可以为您的canvas元素指定确切的大小和设置其他属性。清单 2-1 展示了可以添加到 HTML 页面的最基本的canvas元素。

清单 2-1。一个基本的画布元素

<canvas></canvas>

一旦您将一个canvas元素添加到页面中,您就可以使用 JavaScript 以任何您想要的方式操作它。您可以向其中添加图形、线条和文本。你可以在上面画画;你甚至可以给它添加高级动画。

Canvas API 支持大多数现代操作系统和框架支持的相同的二维绘制操作。如果您在最近几年中曾经编写过二维图形,您可能会对 Canvas API 如鱼得水,因为它的设计与现有系统非常相似。如果您还没有,您将会发现一个渲染系统比以前的图像和 CSS 技巧强大得多,这些是开发人员多年来用来创建 web 图形的。

要以编程方式使用画布,您必须首先获得它的上下文。然后,您可以对上下文执行操作,并最终将这些操作应用于上下文。您可以将画布修改视为类似于数据库事务:您启动一个事务,执行某些操作,然后提交该事务。

画布坐标

如图 2-1 中的所示,画布中的坐标从左上角的x=0,y=0开始——我们称之为原点——并在 x 轴的水平方向和 y 轴的垂直方向增加(以像素为单位)。

Image

图 2-1。画布上的 x 和 y 坐标

何时不用帆布

虽然canvas元素很棒并且非常有用,但是当另一个元素足够时,你应该而不是使用canvas元素。例如,在画布上动态绘制 HTML 文档的所有不同标题,而不是简单地使用为此目的而设计的标题样式(H1、H2 等),这并不是一个好主意。

回退内容

如果您的网页是由不支持canvas元素或 Canvas API 特性子集的浏览器访问的,那么提供一个替代源是一个好主意。例如,您可以提供一个替代图像或一些文本来解释如果用户真的使用现代浏览器,他们会喜欢什么。清单 2-2 展示了如何在canvas元素中指定替换文本。不支持canvas元素的浏览器将简单地呈现这个后备内容。

清单 2-2。在画布元素中使用回退文本

<canvas>   Update your browser to enjoy canvas! </canvas>

如果浏览器不支持canvas元素,您也可以指向一个可以显示的图像,而不是前面显示的文本。

画布可访问性呢?

Peter 说:“提供替代图像或替代文本引发了可访问性的问题——不幸的是,画布规范在这个领域仍然严重不足。例如,对于插入到画布中的图像,没有本地方法来插入文本替换,也没有本地方法来提供替换文本以匹配使用画布文本 API 生成的文本。在撰写本文时,还没有可用于画布中动态生成内容的可访问性挂钩,但是一个工作组正在设计它们。希望这种情况会随着时间的推移而改善。”

HTML5 设计者对于处理可替换的、可访问的画布内容的当前提议之一是使用这个回退内容部分。然而,为了让它对屏幕阅读器和其他辅助工具有用,即使支持和显示画布,回退内容也需要是键盘可导航的。虽然现在有些浏览器支持这种功能,但是您不应该依赖它来支持有特殊需求的用户。目前推荐使用页面的一个单独部分来显示画布替代。额外的好处是,许多用户可能喜欢使用替代控件或显示,作为快速理解和导航页面或应用的更好方式。

Canvas API 的未来版本可能还会包含 Canvas 显示的可聚焦子区域以及与之交互的控件。但是,如果您的图像显示需要大量的交互,可以考虑使用 SVG 作为 Canvas API 的替代。SVG 也允许绘图,但是它也集成了浏览器 DOM。

CSS 和画布

与大多数 HTML 元素一样,CSS 可以应用于canvas元素本身来添加边框、填充、边距等。另外,一些 CSS 值被canvas的内容继承;字体就是一个很好的例子,因为绘制到canvas中的字体默认为canvas元素本身的设置。

此外,在canvas操作中使用的context上设置的属性遵循您可能已经从 CSS 中熟悉的语法。例如,颜色和字体在context上使用的符号与它们在任何 HTML 或 CSS 文档中使用的符号相同。

浏览器支持 HTML5 画布

随着 Internet Explorer 9 的到来,现在所有的浏览器供应商都提供了对 HTML5 Canvas 的支持,并且它已经掌握在大多数用户的手中。这是网络发展的一个重要里程碑,让 2D 绘画在现代网络上蓬勃发展。

尽管以前版本的 Internet Explorer 的市场份额不断减少,但在使用 API 之前,先测试 HTML5 Canvas 是否受支持仍然是一个好主意。本章后面的“检查浏览器支持”一节将向您展示如何以编程方式检查浏览器支持。

使用 HTML5 画布 API

在这一节中,我们将更详细地探索 Canvas APIs 的使用。为了便于说明——没有双关语的意思——我们将使用各种 Canvas APIs 构建一个类似于徽标的森林场景显示,其中有树木和一条适合长距离比赛的美丽的小径。虽然我们的例子不会赢得任何图形设计的奖项,但它应该有助于以合理的顺序说明 HTML5 Canvas 的各种功能。

检查浏览器支持

在使用canvas元素之前,您会希望确保浏览器中有支持。这样,您可以提供一些替代文本,以防他们的老式浏览器不支持。清单 2-3 展示了一种测试浏览器支持的方法。

清单 2-3。检查浏览器支持

try {   document.createElement("canvas").getContext("2d");   document.getElementById("support").innerHTML =     "HTML5 Canvas is supported in your browser."; } catch (e) {   document.getElementById("support").innerHTML = "HTML5 Canvas is not supported ![Image](https://gitee.com/OpenDocCN/vkdoc-html-css-zh/raw/master/docs/pro-h5-prog/img/U002.jpg)                                                   in your browser."; }

在本例中,您尝试创建一个画布对象并访问其上下文。如果有错误,您将捕捉到它,并知道 Canvas 不受支持。页面上先前定义的支持元素用合适的消息更新,以反映是否有浏览器支持。

该测试将表明浏览器是否支持canvas元素本身。它不会指出支持画布的哪些功能。在撰写本文时,该 API 是稳定的并且得到了很好的支持,所以这通常不是一个需要担心的问题。

此外,给你的canvas元素提供后备内容也是一个好主意,如清单 2-3 所示。

向页面添加画布

在 HTML 页面中添加一个canvas元素非常简单。清单 2-4 显示了可以添加到 HTML 页面的canvas元素。

清单 2-4。画布元素

<canvas height="200" width="200"></canvas>

生成的画布将在页面上显示为“不可见”的 200 × 200 像素矩形。如果你想在它周围添加一个边框,你可以使用清单 2-5 所示的 HTML 代码,用普通的 CSS 边框来设置画布的样式。

清单 2-5。带实线边框的画布元素

<canvas id="diagonal" style="border: 1px solid;" width="200" height="200"> </canvas>

请注意添加了 ID diagonal以便于以编程方式定位这个canvas元素。ID 属性对于任何画布都是至关重要的,因为这个元素上所有有用的操作都必须通过脚本来完成。如果没有 ID,您将很难定位元素以与其进行互操作。

图 2-2 显示了清单 2-5 中的画布在浏览器中的样子。

Image

图 2-2。一个简单的 HTML 页面上的 HTML5 画布元素

不是很令人兴奋,但是任何艺术家都会告诉你,它充满了潜力。现在,让我们用这块原始的画布做点什么。如前所述,如果没有 HTML5 Canvas,在网页上画对角线并不容易。让我们看看现在可以使用 Canvas 有多简单。清单 2-6 展示了如何用几行代码,在我们之前添加到页面的画布上画一条对角线。

清单 2-6。在画布上画一条对角线

``

让我们检查一下用于创建对角线的 JavaScript 代码。这是一个简单的例子,但是它抓住了使用 Canvas API 的基本流程:

首先,通过引用特定画布的 ID 值来访问画布对象。在这个例子中,ID 是diagonal。接下来,您创建一个上下文变量,并调用 canvas 对象的getContext方法,传入您正在寻找的画布类型。您传入字符串“2d”来获得一个二维上下文——此时唯一可用的上下文类型。

Image 注意很多工作已经在三维版本的画布上完成。WebGL 规范的 1.0 版本是浏览器供应商和 Khronos Group 的共同努力,于 2011 年初发布。WebGL 基于与流行的 OpenGL 库相同的概念和设计,为 JavaScript 和 HTML5 带来了类似的 API。要在支持的浏览器中创建三维绘图上下文,只需使用字符串"webgl"作为getContext的参数。最终的上下文拥有一套全新的绘图 API:对他们自己的书来说足够全面和复杂的能力。尽管现在一些浏览器已经发布了 WebGL 的实现,但是并不是所有的供应商都已经发布了。然而,网络上三维渲染的潜力是如此引人注目,以至于我们预计在未来几年内会得到快速的支持。欲了解更多信息,请访问 Khronos 集团的 WebGL 网站([www.khronos.org/webgl](http://www.khronos.org/webgl))。我们将在本书的最后一章更详细地讨论 WebGL。

然后,您可以使用该上下文来执行绘图操作。在这种情况下,您可以通过调用三个方法来创建对角线,这三个方法是:beginPathmoveTolineTo,传递直线的起点和终点坐标。

绘图方法moveTolineTo并不实际创建线条;您通过调用context.stroke();方法来完成画布操作并画线。图 2-3 显示了用示例代码创建的对角线。

Image

图 2-3。画布上的对角线

凯旋!尽管这条简单的线可能看起来并不是一场革命的开始,但请记住,使用经典的 HTML 技术在任意两点之间绘制对角线是一个非常困难的操作,涉及到拉伸的图像、奇怪的 CSS 和 DOM 对象,或者其他形式的魔法。让我们再也不要提起他们。

从这个例子的代码中可以看出,画布上的所有操作都是通过 context 对象执行的。这将适用于您与画布的其他交互,因为可视化输出的所有重要功能都只能从上下文中访问,而不能从画布对象本身访问。这种灵活性允许画布在将来基于从画布中检索的上下文类型支持不同类型的绘制模型。虽然在本章中我们会经常提到我们将在画布上采取的动作,但是请记住,这实际上意味着我们将使用画布提供的上下文对象。

如前面的示例所示,上下文中的许多操作不会立即更新绘图图面。诸如beginPathmoveTolineTo等功能不会立即修改画布外观。许多设置画布样式和偏好的函数也是如此。只有当路径被敲击填充时,它才会出现在显示屏上。否则,只有在显示图像、显示文本或绘制、填充或清除矩形时,画布才会立即更新。

对图纸应用变换

现在让我们看看使用变换在画布上绘图的另一种方法。在下面的示例中,结果与前面的示例相同,但是用于绘制对角线的代码不同。对于这个简单的例子,您可能会认为转换的使用增加了不必要的复杂性。然而,您可以将转换作为更复杂的画布操作的最佳实践。您将会看到,在余下的示例中,我们会大量使用它,它对于理解 Canvas API 的复杂功能至关重要。

也许最容易想到转换系统的方法——至少,不涉及大量数学公式和手势的最简单方法——是作为一个修改层,位于您发出的命令和画布显示上的输出之间。这个修改层总是存在的,即使你选择不与它交互。

修改,或者用绘图系统的术语来说是转换,可以按顺序应用、组合和随意修改。每个绘图操作在出现在画布上之前都要经过要修改的修改层。虽然这增加了一层额外的复杂性,但也给绘图系统增加了巨大的能力。它允许对现代图像编辑工具实时支持的强大修改进行访问,而 API 的复杂程度只是绝对需要的。

如果在代码中不使用转换调用,不要误以为你在优化性能。canvas 实现在其呈现引擎中隐式地使用和应用转换,无论您是否直接调用它们。更明智的做法是预先了解系统,因为知道是否超出最基本的绘图操作是至关重要的。

对可重用代码的一个关键建议是,你通常希望在原点(坐标 0,0)处绘制,并应用变换——缩放、平移、旋转等等——将你的绘制代码修改成它的最终外观,如图图 2-4 所示。

Image

图 2-4。原点变换绘制概述

清单 2-7 使用最简单的转换translate展示了这个最佳实践。

清单 2-7。使用平移在画布上创建一条对角线

<script> `  function drawDiagonal() {
    var canvas = document.getElementById('diagonal');
    var context = canvas.getContext('2d');

// Save a copy of the current drawing state
    context.save();

// Move the drawing context to the right, and down
    context.translate(70, 140);

// Draw the same line as before, but using the origin as a start
    context.beginPath();
    context.moveTo(0, 0);
    context.lineTo(70, -70);
    context.stroke();

// Restore the old drawing state
    context.restore();
  }

window.addEventListener("load", drawDiagonal, true);
`

让我们检查一下用来创建第二条翻译后的对角线的 JavaScript 代码。

  1. First, access the canvas object by referring to its ID value (in this case, diagonal).
  2. Then retrieve a context variable by calling the getContext function of the canvas object.
  3. Next, you want to save the context that is still unmodified, so that you can return to its original state at the end of the drawing and conversion operation. If the state is not saved, the modifications (translation, scaling, etc.) made during the operation will continue to be applied to the context in future operations, which may not be desirable. Saving the context state before the conversion will allow us to restore it later. The next step is to apply the translate method to the context. With this operation, when drawing any figure, the translation coordinate you provided will be added to the final figure coordinate (diagonal), thus moving the line to its final position, but only after the drawing operation is completed.
  4. After applying translation, you can perform normal drawing operations to create diagonal lines. In this case, you can create diagonal lines by calling three methods (beginPath, moveTo and lineTo), this time drawing at the origin (0,0) instead of coordinates 70,140.
  5. After drawing a line, you can render it to the canvas (such as drawing a line) by calling the context.stroke method.
  6. Finally, you restore the context to its clean original state so that the translation applied in this operation will not be used when performing future canvas operations. Figure 2-5 shows the diagonal created with the sample code.

Image

图 2-5。翻译画布上的对角线

尽管您的新行看起来与旧行非常相似,但是您使用转换的力量创建了它,这一点随着我们在本章剩余部分的学习会变得更加明显。

使用路径

虽然我们可以提供更多令人兴奋的画线的例子,但我们现在准备进行一些更复杂的东西:路径。HTML5 Canvas API 中的路径表示您想要呈现的任何形状。我们最初的 line 示例是一个路径,您可能已经从用来启动它的那个明显的beginPath调用中收集到了这一点。但是路径可以如你所愿的那样复杂,有多条直线和曲线段,甚至还有子路径。如果您希望在画布上绘制几乎任何形状,path API 将是您的关注点。

当开始任何绘制形状或路径的例行程序时,您首先调用的是beginPath。这个简单的函数没有参数,但是它向画布发出信号,表示您希望开始一个新的形状描述。此函数对画布非常有用,它可以计算您为以后的填充和描边创建的形状的内部和外部。

路径总是跟踪当前位置的概念,默认为原点。画布在内部跟踪当前位置,但是您将使用您的绘图例程修改它。

形状开始后,您可以使用上下文中的各种功能来绘制形状的布局。您已经看到了最简单的上下文路径函数:

  • moveTo(x, y):将当前位置移动到新的目的地(x,y)而不绘制。
  • lineTo(x, y):将当前位置移动到一个新的终点(x,y),从当前位置到新的终点画一条直线。

本质上,这两个调用之间的区别在于,第一个调用类似于提起绘图笔并移动到新的位置,而第二个调用告诉画布将笔留在纸上,并以直线移动到新的目的地。然而,值得再次指出的是在你描边或填充路径之前,没有实际的绘图发生。目前,我们仅仅是在我们的道路上确定位置,以便以后可以绘制它。

下一个特殊的路径函数是对closePath的调用。该命令的行为与lineTo功能非常相似,不同之处在于目的地被自动假定为路径的起点。然而,closePath也通知画布当前形状已经闭合或者形成了一个完全包含的区域。这对将来的填充和描边很有用。

在这一点上,您可以继续在您的路径中创建更多的子路径。或者您可以随时beginPath重新开始并完全清除路径列表。

对于大多数复杂的系统来说,看到它们的运行通常会更好。让我们离开我们的 line 示例,使用 Canvas API 开始创建一个新的场景,展示一个有小径的森林。这个场景将作为我们比赛的标志。和任何图片一样,我们将从一个基本元素开始,在这个例子中是一棵简单松树的树冠。清单 2-8 显示了如何绘制松树的树冠。

清单 2-8。为树冠创建路径的功能

`function createCanopyPath(context) {
  // Draw the tree canopy
  context.beginPath();

context.moveTo(-25, -50);
  context.lineTo(-10, -80);
  context.lineTo(-20, -80);
  context.lineTo(-5, -110);
  context.lineTo(-15, -110);

// Top of the tree
  context.lineTo(0, -140);

context.lineTo(15, -110);
  context.lineTo(5, -110);
  context.lineTo(20, -80);
  context.lineTo(10, -80);
  context.lineTo(25, -50);

// Close the path back to its start point
  context.closePath();
}`

从代码中可以看出,我们使用了与之前相同的 move 和 line 命令,但是使用了更多的命令。这些线条形成了一个简单树形结构的分支,我们在末端封闭了路径。我们的树将在底部留下一个明显的缺口,我们将在以后的章节中使用它来绘制树干。清单 2-9 展示了如何使用树冠绘制函数将我们简单的树的形状绘制到画布上。

清单 2-9。在画布上画一棵树的功能

`function drawTrails() {
  var canvas = document.getElementById('trails');
  var context = canvas.getContext('2d');

context.save();
  context.translate(130, 250);

// Create the shape for our canopy path
  createCanopyPath(context); // Stroke the current path
  context.stroke();
  context.restore();
}`

这个例程中的所有调用您应该已经很熟悉了。我们获取画布上下文,保存它以供将来参考,将我们的位置转换到一个新的位置,绘制天篷,将其绘制到画布上,然后恢复我们的状态。图 2-6 显示了我们手工制作的结果,一个简单的树冠线条图。随着我们的前进,我们将对此进行扩展,但这是一个良好的开端。

Image

图 2-6。一个树冠的简单路径

使用笔画样式

如果开发人员坚持使用简单的简笔画和黑线,Canvas API 就不会强大或受欢迎。让我们使用笔画造型功能,使我们的树冠更像树。清单 2-10 显示了一些基本命令,这些命令可以修改上下文的属性,以使描边形状看起来更有吸引力。

清单 2-10。使用笔画样式

`// Increase the line width
context.lineWidth = 4;

// Round the corners at path joints
context.lineJoin = 'round';

// Change the color to brown
context.strokeStyle = '#663300';

// Finally, stroke the canopy
context.stroke();`

通过在描边之前添加上述属性,我们可以更改任何未来描边形状的外观——至少在我们将上下文恢复到以前的状态之前。

首先,我们将描边线条的宽度增加到四个像素。

接下来,我们将lineJoin属性设置为round,这使得我们的形状的线段的接合处呈现出更加圆滑的拐角形状。我们也可以将lineJoin设置为bevelmiter(以及相应的context.miterLimit值来调整它)来选择其他角选项。

最后,我们通过使用strokeStyle属性来改变笔画的颜色。在我们的例子中,我们将颜色设置为一个 CSS 值,但是正如您将在后面的章节中看到的,也可以将strokeStyle设置为一个图像模式或一个渐变,用于更好的显示。

虽然我们在这里没有使用它,但是我们也可以将lineCap属性设置为buttsquareround来指定线条应该如何在端点显示。唉,我们的例子没有悬空的线端。图 2-7 显示了我们修整过的树冠,现在用一条更宽、更平滑的棕色线条代替了之前的黑色线条。

Image

图 2-7。时髦的抚摸着树冠

使用填充样式

如您所料,描边并不是影响画布形状外观的唯一方式。修改形状的下一个常用方法是指定如何填充其路径和子路径。清单 2-11 展示了用令人愉快的绿色填充我们的树冠是多么简单。

清单 2-11 。使用填充样式

// Set the fill color to green and fill the canopy context.fillStyle = '#339900'; context.fill();

首先,我们将fillStyle设置为适当的颜色。正如我们将在后面看到的,也可以将填充设置为渐变或图像模式。然后,我们简单地调用上下文的fill函数,让画布填充我们当前形状的所有闭合路径内的所有像素,如图图 2-8 所示。

Image

图 2-8。满树的树冠

因为我们在填充树冠之前对其进行了描边,所以填充覆盖了描边路径的一部分。这是因为宽笔划(在我们的例子中,四个像素宽)位于路径形状的中心。填充应用于形状内部的所有像素,因此它将覆盖一半的描边线像素。如果您希望显示完整的笔画,您可以在笔画路径之前简单地填充

填充矩形内容

每棵树都应该有一个坚实的基础。谢天谢地,我们在原始形状的路径上为我们的树干留了空间。清单 2-12 展示了我们如何通过使用fillRect便利函数来添加一个树干的最简单的渲染。

清单 2-12。使用 fillRect 便捷功能

`// Change fill color to brown
context.fillStyle = '#663300';

// Fill a rectangle for the tree trunk
context.fillRect(-5, -50, 10, 50);`

在这里,我们再次设置了棕色填充样式。但是,我们将使用fillRect一步完成整个躯干的绘制,而不是使用lineTo功能显式绘制躯干矩形的角。fillRect调用获取 x 和 y 位置,以及宽度和高度,然后立即用当前填充样式填充。

虽然我们在这里没有使用它们,但是strokeRectclearRect有相应的函数。前者将根据给定的位置和尺寸绘制矩形的轮廓,而后者将从矩形区域中删除任何内容,并将其重置为原始的透明颜色。

画布动画

Brian 说:“在画布中清除矩形的能力是使用画布 API 创建动画和游戏的核心。通过重复绘制和清除画布的部分,有可能呈现动画的幻觉,并且许多这样的例子已经存在于网络上。然而,为了创建流畅的动画,你需要利用剪辑功能,甚至可能需要一个二级缓冲画布来最小化频繁的画布清除所导致的闪烁。虽然动画并不是本书的重点,但请查看本章的“实用附加”部分,了解一些使用 HTML5 制作页面动画的技巧。”

图 2-9 显示了我们简单的、扁平填充的树干,连接到我们之前的树冠路径上。

Image

图 2-9。矩形树干的树

绘制曲线

世界,尤其是自然界,并不是充满了直线和矩形。幸运的是,画布提供了各种函数来在我们的路径中创建曲线。我们将展示最简单的选择——一条二次曲线——来形成一条穿过虚拟森林的路径。清单 2-13 展示了两条二次曲线的添加。

清单 2-13。画曲线

`// Save the canvas state and draw the path
context.save();

context.translate(-10, 350);
context.beginPath();

// The first curve bends up and right context.moveTo(0, 0);
context.quadraticCurveTo(170, -50, 260, -190);

// The second curve continues down and right
context.quadraticCurveTo(310, -250, 410,-250);

// Draw the path in a wide brown stroke
context.strokeStyle = '#663300';
context.lineWidth = 20;
context.stroke();

// Restore the previous canvas state
context.restore();`

像以前一样,我们要做的第一件事是保存我们的画布上下文状态,因为我们将在这里修改翻译和笔画选项。对于我们的森林路径,我们将从回到原点开始,向右上方画第一条二次曲线。

如图 2-10 所示,quadraticCurveTo函数从当前绘图位置开始,以两个 x,y 点位置为参数。第二个是我们曲线的最后一站。第一个代表一个控制点。控制点位于曲线的一侧(不在曲线上),对曲线路径上的点几乎起着引力的作用。通过调整控制点的位置,可以调整正在绘制的路径的曲率。我们向右上方画第二条二次曲线来完成我们的路径;然后抚摸它,就像我们之前对树冠做的那样(只是更宽)。

Image

图 2-10。二次曲线起点、终点和控制点

HTML5 Canvas API 中的其他曲线选项包括bezierCurveToarcToarc函数。这些曲线采用额外的控制点、半径或角度来确定曲线的特征。图 2-11 显示了两条二次曲线在我们的画布上画出了一条穿过树林的路径。

Image

图 2-11。路径的二次曲线

将图像插入画布

在画布中显示图像非常方便。它们可以被压印、拉伸、变换修改,并且通常是整个画布的焦点。幸运的是,Canvas API 包含一些简单的命令,用于向画布添加图像内容。

但是图像也增加了画布操作的复杂性:您必须等待它们被加载。浏览器通常会在页面脚本呈现时异步加载图像。但是,如果您试图在画布完全加载之前将图像渲染到画布上,画布将根本无法渲染任何图像。因此,在尝试渲染图像之前,您应该小心确保图像已完全加载。

为了在我们简单的森林小径示例中解决这个问题,我们将加载一个树皮纹理的图像,以便直接在画布中使用。为了确保在我们渲染之前图像已经完成加载,我们将把加载代码切换为只作为图像加载完成的回调来执行,如清单 2-14 所示。

清单 2-14。加载图像

`// Load the bark image
var bark = new Image();
bark.src = "bark.jpg";

// Once the image is loaded, draw on the canvas bark.onload = function () {
  drawTrails();
}`

正如您所看到的,我们已经向bark.jpg图像添加了一个onload处理程序,以便仅在图像加载完成时调用主drawTrails函数。这保证了我们添加到画布渲染的下一个调用可以使用这个图像,如清单 2-15 中的所示。

清单 2-15。在画布上绘制图像

// Draw the bark pattern image where //  the filled rectangle was before context.drawImage(bark, -5, -50, 10, 50);

这里,我们用一个简单的例程替换了前面对fillRect的调用,将我们的树皮图像显示为我们的树的新树干。虽然图像是一个微妙的替代,但它为我们的显示提供了更多的纹理。注意,在这个调用中,除了图像本身,我们还指定了 x、y、width 和 height 参数。该选项将缩放图像,以适合我们为主干分配的 10 × 50 像素空间。我们还可以传入源维度,以便更好地控制要显示的传入图像的剪辑区域。

正如你在图 2-12 中所看到的,我们躯干外观的变化与我们之前使用的填充矩形仅略有不同。

Image

图 2-12。树干使用图像的树

使用渐变

对树干不满意?我们也不是。让我们用另一种更巧妙的方法来绘制我们的树干:渐变。渐变允许你应用一个渐进的算法采样颜色作为一个笔画或填充样式,就像在上一节中应用的模式一样。创建渐变需要三个步骤:

  1. Create the gradient object itself.
  2. Apply the color stop point to the gradient object to signal that the color changes along the transition.
  3. Set the gradient to fillStyle or strokeStyle in context.

也许最容易把渐变想象成沿着一条线移动的平滑的颜色变化。例如,如果您提供 A 点和 B 点作为创建渐变的参数,则任何从 A 点向 B 点移动的描边或填色都将转换颜色。

要确定显示什么颜色,只需对渐变对象本身使用addColorStop函数。此功能允许您指定偏移量和颜色。color 参数是要在偏移位置应用于描边或填充的颜色。偏移位置是一个介于 0.0 和 1.0 之间的值,表示颜色应该沿着渐变线到达多远。

如果创建从点(0,0)到点(0,100)的渐变,并在偏移量 0.0 处指定白色色标,在偏移量 1.0 处指定黑色色标,则当发生描边或填充时,随着渲染从点(0,0)到点(0,100),您会看到颜色逐渐从白色(开始色标)变为黑色(结束色标)。

与其他颜色值一样,可以提供一个 alpha(例如,透明度)值作为颜色的一部分,并使该 alpha 值过渡。为此,您需要使用颜色值的另一种文本表示,比如包含 alpha 组件的 CSS rgba函数。

让我们通过一个代码示例来更详细地了解这一点,该示例将两个渐变应用于代表我们最终树干的fillRect,如清单 2-16 中的所示。

清单 2-16。使用渐变

`// Create a 3 stop gradient horizontally across the trunk
var trunkGradient = context.createLinearGradient(-5, -50, 5, -50);

// The beginning of the trunk is medium brown
trunkGradient.addColorStop(0, '#663300');

// The middle-left of the trunk is lighter in color
trunkGradient.addColorStop(0.4, '#996600');

// The right edge of the trunk is darkest
trunkGradient.addColorStop(1, '#552200');

// Apply the gradient as the fill style, and draw the trunk
context.fillStyle = trunkGradient;
context.fillRect(-5, -50, 10, 50);

// A second, vertical gradient creates a shadow from the
//  canopy on the trunk
var canopyShadow = context.createLinearGradient(0, -50, 0, 0); // The beginning of the shadow gradient is black, but with
//  a 50% alpha value
canopyShadow.addColorStop(0, 'rgba(0, 0, 0, 0.5)');

// Slightly further down, the gradient completely fades to
//  fully transparent. The rest of the trunk gets no shadow.
canopyShadow.addColorStop(0.2, 'rgba(0, 0, 0, 0.0)');

// Draw the shadow gradient on top of the trunk gradient
context.fillStyle = canopyShadow;
context.fillRect(-5, -50, 10, 50);`

如图 2-13 所示,应用这两个渐变在我们渲染的树上创建了一个漂亮、平滑的光源,使它看起来弯曲,并被上面树冠的轻微阴影覆盖。我们留着吧。

Image

图 2-13。树干渐变的树

除了我们示例中使用的线性渐变,Canvas API 还支持径向渐变选项,该选项允许您指定两个圆形表示,在这两个圆形表示中,色标应用于两个圆形之间的圆锥体。径向渐变使用与线性渐变相同的颜色停止点,但是以清单 2-17 所示的形式获取其参数。

清单 2-17。应用径向渐变的例子

createRadialGradient(x0, y0, r0, x1, y1, r1)

在此示例中,前三个参数表示以(x0,y0)为中心、半径为 r0 的圆,后三个参数表示以(x1,y1)为中心、半径为 r1 的第二个圆。渐变在两个圆之间的区域上绘制。

使用背景图案

图像的直接呈现有许多用途,但在某些情况下,使用图像作为背景图块是有益的,类似于 CSS 中可用的功能。我们已经看到了如何将笔触或填充样式设置为纯色。HTML5 Canvas API 还包括一个选项,用于将图像设置为路径描边或填充的可重复模式。

为了使我们的森林小径看起来更加崎岖,我们将通过用一个使用背景图像填充的曲线替换先前的描边小径曲线来展示这一能力。在这样做的时候,我们将把现在没有使用的树皮图像换成砾石图像,我们将在这里使用。清单 2-18 显示我们用对createPattern的调用替换了对drawImage的调用。

清单 2-18。使用背景图案

`// Replace the bark image with
// a trail gravel image
var gravel = new Image();
gravel.src = "gravel.jpg";
gravel.onload = function () {
    drawTrails();
}

// Replace the solid stroke with a repeated
// background pattern
context.strokeStyle = context.createPattern(gravel, 'repeat');
con text.lineWidth = 20;
context.stroke();`

如你所见,我们仍在为我们的道路呼叫stroke()。然而,这一次我们首先在上下文上设置了一个strokeStyle属性,将调用context.createPattern的结果传入。哦,同样,为了让画布执行操作,需要预先加载图像。第二个参数是重复模式,它可以是表 2-1 中显示的选项之一。

Image

图 2-14 显示了使用背景图像而不是明确绘制的图像来表示我们的踪迹的结果。

Image

图 2-14。背景图案重复的痕迹

缩放画布对象

什么样的森林只有一棵树?让我们马上修理那个。为了使这变得简单一点,我们将调整我们的代码样本,将树的绘制操作隔离到一个叫做drawTree的例程中,如清单 2-19 中的所示。

清单 2-19。绘制树对象的功能

`// Move tree drawing into its own function for reuse
function drawTree(context) {
  var trunkGradient = context.createLinearGradient(-5, -50, 5, -50);
  trunkGradient.addColorStop(0, '#663300');
  trunkGradient.addColorStop(0.4, '#996600');
  trunkGradient.addColorStop(1, '#552200');
  context.fillStyle = trunkGradient;
  context.fillRect(-5, -50, 10, 50);

var canopyShadow = context.createLinearGradient(0, -50, 0, 0);
  canopyShadow.addColorStop(0, 'rgba(0, 0, 0, 0.5)');
  canopyShadow.addColorStop(0.2, 'rgba(0, 0, 0, 0.0)');
  context.fillStyle = canopyShadow;
  context.fillRect(-5, -50, 10, 50);

createCanopyPath(context);

context.lineWidth = 4;
  context.lineJoin = 'round';
  context.strokeStyle = '#663300';   context.stroke();

context.fillStyle = '#339900';
  context.fill();
}`

正如你所看到的,drawTree函数包含了我们之前创建的绘制树冠、树干和树干渐变的所有代码。现在我们将使用一个转换例程——context.scale——在一个新的位置画第二棵树,并且尺寸更大,如清单 2-20 所示。

清单 2-20。绘制树对象

`// Draw the first tree at X=130, Y=250
context.save();
context.translate(130, 250);
drawTree(context);
context.restore();

// Draw the second tree at X=260, Y=500
context.save();
context.translate(260, 500);

// Scale this tree twice normal in both dimensions
context.scale(2, 2);
drawTree(context);
context.restore();`

scale函数将 x 和 y 维度的两个因子作为其参数。每个因素都告诉 canvas 实现在那个维度上使尺寸变大(或变小)多少;X 因子为 2 将使所有后续绘制例程的宽度加倍,而 Y 因子为 0.5 将使所有后续操作的高度减半。使用这些例程,我们现在有一个简单的方法在我们的路径画布上创建第二棵树,如图 2-15 所示。

Image

图 2-15。大树

总是在原点执行形状和路径程序

Brian 说(这次是认真的):“这个例子说明了为什么在原点执行形状和路径例程是个好主意的原因之一;然后在完成时翻译它们,就像我们在代码中所做的那样。原因是像scalerotate这样的变换是从原点开始操作的。

如果对偏离原点绘制的形状执行rotate变换,则rotate变换将围绕原点旋转形状,而不是原地旋转。同样,如果在将形状转换到正确位置之前对其执行了缩放操作,则路径坐标的所有位置也会乘以缩放因子。根据应用的比例因子,这个新位置甚至可以完全离开画布,让您想知道为什么您的缩放操作只是“删除”了图像。"

使用画布变换

变换操作不限于缩放和平移。也可以使用context.rotate(angle)功能旋转绘图上下文,甚至直接修改底层转换,进行更高级的操作,如剪切渲染路径。如果你想旋转图像的显示,你只需要调用清单 2-21 中的一系列操作。

清单 2-21。旋转后的图像

`context.save();

// rotation angle is specified in radians
context.rotate(1.57);
context.drawImage(myImage, 0, 0, 100, 100);

context.restore();`

然而,在清单 2-22 中,我们将展示如何将任意变换应用到路径坐标上,以彻底改变现有树路径的显示,从而创建阴影效果。

清单 2-22。使用变换

`// Create a 3 stop gradient horizontally across the trunk
// Save the current canvas state for later
context.save();

// Create a slanted tree as the shadow by applying
//  a shear transform, changing X values to increase
//  as Y values increase
// With this transform applied, all coordinates are
//  multiplied by the matrix.
context.transform(1, 0,-0.5, 1, 0, 0);

// Shrink the shadow down to 60% height in the Y dimension
context.scale(1, 0.6);

// Set the tree fill to be black, but at only 20% alpha
context.fillStyle = 'rgba(0, 0, 0, 0.2)';
context.fillRect(-5, -50, 10, 50);

// Redraw the tree with the shadow effects applied
createCanopyPath(context);
context.fill();

// Restore the canvas state
context.restore();`

像我们在这里所做的那样直接修改上下文转换是你应该尝试的事情,只有你熟悉支撑二维绘图系统的矩阵数学。如果你检查这个变换背后的数学,你会看到我们正在把我们的画的 X 值移动一个相应的 Y 值的系数,以便剪切被用作阴影的灰色树。然后,通过应用 60%的比例因子,被剪切的树的大小被减小。

请注意,剪切的“阴影”树首先被渲染,因此实际的树以 Z 顺序(画布对象重叠的顺序)出现在它上面。此外,阴影树是使用 RGBA 的 CSS 符号绘制的,这允许我们将 alpha 值设置为正常值的 20%。这为阴影树创建了光亮的半透明外观。一旦应用到我们的缩放树,输出呈现如图图 2-16 所示。

Image

图 2-16。阴影变形的树

使用画布文本

当我们接近踪迹创建的结尾时,让我们通过在显示的顶部添加一个有趣的标题来展示 Canvas API 文本函数的强大功能。请务必注意,画布上的文本渲染与任何其他 path 对象的处理方式相同:文本可以描边或填充,所有渲染转换和样式都可以应用于文本,就像它们应用于任何其他形状一样。

如您所料,文本绘制例程由上下文对象上的两个函数组成:

  • fillText (text, x, y, maxwidth)
  • strokeText (text, x, y, maxwidth)

这两个函数都接受文本以及应该绘制的位置。可选地,可以提供一个maxwidth参数,通过自动缩小字体以适合给定的大小来约束文本的大小。此外,measureText函数可用于返回包含给定文本宽度的 metrics 对象,如果它是使用当前上下文设置呈现的话。

与所有浏览器文本显示的情况一样,文本的实际外观是高度可配置的,使用类似于它们的 CSS 对应物的上下文属性,如表 2-2 所示。

所有这些上下文属性都可以被设置来改变上下文,或者被访问来查询当前值。在清单 2-23 中,我们将创建一个字体为Impact的大文本消息,并用我们现有的树皮图像的背景图案填充它。为了使文本在画布顶部居中,我们将声明一个最大宽度和一个center对齐方式。

清单 2-23。使用画布文本

`// Draw title text on our canvas
context.save();

// The font will be 60 pixel, Impact face
context.font = "60px impact";

// Use a brown fill for our text
context.fillStyle = '#996600';
// Text can be aligned when displayed
context.textAlign = 'center';

// Draw the text in the middle of the canvas with a max
//  width set to center properly
context.fillText('Happy Trails!', 200, 60, 400);
context.restore();`

正如你在图 2-17 中看到的结果,轨迹绘制变得更加快乐——你猜对了。

Image

图 2-17。背景图案填充文本

应用阴影

最后,我们将使用内置的 canvas shadow API 为新的文本显示添加模糊的阴影效果。像许多图形效果一样,阴影最好适度应用,即使 Canvas API 允许您将阴影应用到我们已经介绍过的任何操作中。

同样,阴影由一些全局context属性控制,如表 2-3 所示。

Image

如果shadowColor和至少一个其他属性被设置为非默认值,阴影效果将在任何路径、文本或图像渲染中触发。清单 2-24 展示了我们如何给新的轨迹标题文本添加阴影。

清单 2-24。应用阴影

`// Set some shadow on our text, black with 20% alpha
context.shadowColor = 'rgba(0, 0, 0, 0.2)';

// Move the shadow to the right 15 pixels, up 10
context.shadowOffsetX = 15;
context.shadowOffsetY = -10;

// Blur the shadow slightly
context.shadowBlur = 2;`

通过这些简单的添加,画布渲染器将自动应用阴影,直到画布状态恢复或阴影属性重置。图 2-18 显示了新应用的阴影。

Image

图 2-18。带阴影文字的标题

如你所见,CSS 生成的阴影只是位置性的,与我们为树创建的变换阴影不同步。出于一致性的考虑,在给定的画布场景中,你应该只使用一种方法来绘制阴影。

处理像素数据

Canvas API 最有用的方面之一——尽管不是很明显——是开发人员能够轻松访问画布中的底层像素。这种访问是双向的:以数字数组的形式访问像素值很容易,修改这些值并将其应用到画布上也同样容易。事实上,完全可以通过像素值调用来操纵画布,而不用我们在本章中讨论的渲染调用。这是因为context API 上存在三个函数。

首先出场的是context.getImageData(sx, sy, sw, sh)。这个函数以整数集合的形式返回画布显示的当前状态。具体来说,它返回一个包含三个属性的对象:

  • width:像素数据的每行的像素数
  • height:像素数据的每一列中的像素数
  • data:一维数组,包含从画布中检索的每个像素的实际 RGBA 值。该数组包含每个像素的四个值,即红色、绿色、蓝色和 alpha 分量,每个值从 0 到 255。因此,从画布中检索到的每个像素都变成了数据数组中的四个整数值。数据数组由像素从左到右、从上到下填充(例如,穿过第一行,然后穿过第二行,等等),如图 2-19 中的所示。

Image

图 2-19。像素数据和代表它的内部数据结构

调用getImageData返回的数据限于四个参数定义的区域。只有包含在由源参数xywidthheight包围的矩形区域中的画布像素将被检索。因此,要将所有像素值作为数据访问,应该传入getImageData(0, 0, canvas.width, canvas.height)

因为每个像素有四个图像数据值,所以准确计算哪个索引代表给定像素的值可能有点棘手。公式如下。

对于具有给定宽度和高度的画布中坐标(x,y)处的任何像素,您可以定位组件值:

  • 红色成分😦(宽度* y) + x) * 4
  • 绿色成分😦(宽度* y) + x) * 4 + 1
  • 蓝色成分😦(宽度* y) + x) * 4 + 2
  • Alpha 分量😦(宽度* y) + x) * 4 + 3

一旦访问了包含图像数据的对象,就很容易从数学上修改数据数组中的像素值,因为它们都是从 0 到 255 的简单整数。通过使用第二个函数:context.putImageData(imagedata, dx, dy),改变一个或多个像素的红色、绿色、蓝色或 alpha 值可以很容易地更新画布显示。

putImageData允许您以与最初检索时相同的格式传入一组图像数据;这很方便,因为你可以修改画布最初给你的值,并把它们放回去。一旦调用了这个函数,画布将立即更新,以反映您作为图像数据传入的像素的新值。如果您选择使用数据数组,那么dxdy参数允许您指定将数据数组应用到现有画布的偏移量。

最后,如果您想从一组空白的画布数据开始,您可以调用context.createImageData(sw, sh)来创建一组绑定到画布对象的新图像数据。这组数据可以像以前一样以编程方式更改,即使它在检索时不代表画布的当前状态。

还有另一种从画布中获取数据的方法:canvas.toDataURL API。该函数为您提供了一种以文本格式检索画布的当前渲染数据的编程方式,但在这种情况下,该格式是浏览器可以解释为图像的数据的标准表示。

数据 URL 是包含图像数据(如 PNG)的字符串,浏览器可以像显示普通图像文件一样显示这些数据。数据 URL 的格式最好用一个例子来说明:

data:image/png;base64, WCAYAAABkY9jZxn…

这个例子显示了格式是字符串data:后跟一个 MIME 类型(比如image/png),后跟一个指示数据是否以 base64 格式编码的标志,然后是表示数据本身的文本。

不要担心格式,因为您不会自己生成它。重要的一点是,通过一个简单的调用,您可以在这些特殊的 URL 之一中获得交付给您的画布内容。当您调用canvas.toDataURL(type)时,您可以传入您希望在其中生成画布数据的图像类型,例如image/png(默认)或image/jpeg。返回给您的数据 URL 可以用作页面或 CSS 样式中图像元素的来源,如清单 2-25 中的所示。

清单 2-25。从画布上创作图像

`var myCanvas = document.getElementById("myCanvas");

// draw operations into the canvas... // get the canvas data as a data URL
var canvasData = myCanvas.toDataURL();

// set the data as the source of a new image
var img = new Image();
img.src = canvasData;`

您不必马上使用数据 URL。您甚至可以将 URL 存储在浏览器的本地存储器中,以便以后检索和操作。浏览器存储将在本书的后面讨论。

实现画布安全性

如前一节所述,使用像素操作有一个重要的注意事项。尽管大多数开发人员将像素操作用于合法的目的,但是从画布中获取和更新数据的能力很可能被用于不正当的目的。出于这个原因,指定了一个原点清洁画布的概念,这样被污染了的画布就不能从包含页面的源之外的原点获取它们的数据。

如图 2-20 中的所示,如果从[www.example.com](http://www.example.com)提供的页面包含一个canvas元素,那么页面中的代码完全有可能试图在画布中呈现来自[www.remote.com](http://www.remote.com)的图像。毕竟,在任何给定的网页中呈现来自远程站点的图像是完全可以接受的。

Image

图 2-20。本地和远程图像源

然而,在 Canvas API 出现之前,不可能以编程方式检索下载图像的像素值。其他网站的私人图片可以显示在页面上,但不能被阅读或复制。允许脚本从其他来源读取图像数据将有效地与整个网络共享用户的照片和其他敏感的在线图像文件。

为了防止这种情况,如果调用getImageDatatoDataURL函数,任何包含从远程源渲染的图像的画布都将抛出安全异常。只要您(或任何其他编剧)在画布被污染后不试图从该画布获取数据,将远程图像渲染到另一个来源的画布中是完全可以接受的。请注意这一限制,并练习安全渲染。

用 HTML5 Canvas 构建应用

使用 Canvas API 有许多不同的应用可能性:图形、图表、图像编辑等等。然而,画布最有趣的用途之一是修改或覆盖现有内容。一种流行的叠加类型被称为热图。虽然这个名字意味着温度测量,但在这种情况下,热量可以指任何水平的可测量活动。地图上活跃程度高的区域被标记为热色(例如,红色、黄色或白色)。活动较少的区域没有显示任何颜色变化,或者显示最少的黑色和灰色。

例如,热图可用于在城市地图上指示交通状况,或在全球地图上指示风暴活动。在 HTML5 中,通过将画布显示与底层地图源结合起来,这样的情况很容易实现。本质上,画布可用于覆盖地图,并根据适当的活动数据绘制热量水平。

让我们使用我们在 Canvas API 中学到的功能构建一个简单的热图。在这种情况下,我们的热度数据源将不是外部数据,而是鼠标在地图上的移动。在地图的一部分上移动鼠标会导致温度升高,将鼠标保持在给定位置会使温度迅速升高到最高水平。我们可以将这样的热图显示(如图 2-21 所示)叠加在一张普通的地形图上,这只是提供一个例子。

Image

图 2-21。热图应用

现在,您已经看到了我们的热图应用的最终结果,让我们来浏览一下代码示例。像往常一样,工作示例可以在线下载和阅读。

让我们从这个例子中声明的 HTML 元素开始。对于这个显示,HTML 只包含一个标题、一个画布和一个按钮,我们可以用它来重置热图。画布的背景显示由一个简单的通过 CSS 应用到画布的mapbg.jpg组成,如清单 2-26 中的所示。

清单 2-26。热图画布元素

<style type="text/css">   #heatmap {       background-image: url("mapbg.jpg");   } `

Heatmap

`

我们还声明了一些将在后面的例子中使用的初始变量。

  var points = {};   var SCALE = 3;   var x = -1;   var y = -1;

接下来,我们将为画布的全局绘制操作设置一个高透明度值,并设置合成模式以使新绘制的像素变亮,而不是替换它们。

然后,如清单 2-27 中的所示,我们将设置一个处理程序来改变显示—addToPoint—每当鼠标移动或十分之一秒过去时。

清单 2-27。load demo 功能

`function loadDemo() {
  document.getElementById("resetButton").onclick = reset;

canvas = document.getElementById("heatmap");
  context = canvas.getContext('2d');
  context.globalAlpha = 0.2;
  context.globalCompositeOperation = "lighter"

function sample() {
  if (x != -1) {
    addToPoint(x,y)
  }
  setTimeout(sample, 100);
}

canvas.onmousemove = function(e) {
  x = e.clientX - e.target.offsetLeft;
  y = e.clientY - e.target.offsetTop;
  addToPoint(x,y)
}

sample();
}`

如果用户点击重置,则整个画布区域被清空,并使用画布的clearRect函数重置为初始状态,如清单 2-28 所示。

清单 2-28。复位功能

function reset() {   points = {};   context.clearRect(0,0,300,300);   x = -1;   y = -1; }

接下来,我们创建一个颜色查找表,在画布上绘制热量时使用。清单 2-29 显示了颜色在亮度上如何从最小到最大变化,它们将被用来代表显示器上不同的热量水平。intensity的值越大,返回的颜色越亮。

清单 2-29。getColor 函数

function getColor(intensity) {   var colors = ["#072933", "#2E4045", "#8C593B", "#B2814E", "#FAC268", "#FAD237"];   return colors[Math.floor(intensity/2)]; }

每当鼠标移动或悬停在画布的某个区域上时,就会绘制一个点。鼠标在邻近区域停留的时间越长,点的大小(和亮度)就越大。如清单 2-30 所示,我们使用context.arc函数绘制一个给定半径的圆,对于更大的半径值,我们通过将半径传递给我们的getColor函数来绘制一个更亮、更热的颜色。

清单 2-30。拉点功能

`function drawPoint(x, y, radius) {
  context.fillStyle = getColor(radius);
  radius = Math.sqrt(radius)*6;

context.beginPath();
  context.arc(x, y, radius, 0, Math.PI*2, true)

context.closePath();
  context.fill();
}`

addToPoint函数中——您会记得每次鼠标移动或悬停在某个点上时都会访问该函数——会增加并存储画布上该特定点的热度值。清单 2-31 显示最大点数值为 10。一旦找到给定像素的当前热度值,适当的像素及其相应的热度/半径值被传递给drawPoint

清单 2-31。addToPoint 函数

`function addToPoint(x, y) {
  x = Math.floor(x/SCALE);
  y = Math.floor(y/SCALE);

if (!points[[x,y]]) {
    points[[x,y]] = 1;
  } else if (points[[x,y]]==10) {
    return
  } else {
    points[[x,y]]++;
  }
  drawPoint(xSCALE,ySCALE, points[[x,y]]);
}`

最后,初始的loadDemo函数被注册,以便在窗口完成加载时调用。

window.addEventListener("load", loadDemo, true);

总之,这一百多行代码说明了在不使用任何插件或外部渲染技术的情况下,您可以在短时间内使用 Canvas API 做多少事情。有了无限多的可用数据源,很容易看出如何简单有效地可视化它们。

实用额外功能:整页玻璃窗格

在示例应用中,您看到了如何在图形上应用画布。您也可以在整个浏览器窗口或部分窗口的顶部应用画布,这种技术通常被称为玻璃面板。一旦你在网页上放置了玻璃面板画布,你就可以用它做各种又酷又方便的事情。

例如,您可以使用一个例程来检索页面上所有 DOM 元素的绝对位置,并创建一个分步帮助函数,该函数可以指导 web 应用的用户完成启动和使用该应用所必须执行的步骤。

或者,您可以使用 glass pane canvas 在某人的 web 页面上使用鼠标事件来绘制输入。如果你试图在这种情况下使用画布,请记住以下几点:

  • 您需要将画布定位设置为absolute,并给它一个特定的位置、宽度和高度。如果没有明确的宽度和高度设置,画布将保持零像素大小。
  • 不要忘记在画布上设置一个高的 Z-index,这样它就可以浮动在所有可见内容之上。在所有现有内容下呈现的画布没有多少机会发光。
  • 您的玻璃窗格画布可能会阻止访问以下内容中的事件,因此请节约使用它,并在不必要时将其移除。

实用附加:为你的画布动画计时

在本章的前面,我们提到了在画布上制作元素动画是一种常见的做法。这可以用于游戏、过渡效果,或者简单地替换现有网页中的动画 gif。但是 JavaScript 缺少的一个方面是一种可靠的方式来安排动画更新。

今天,大多数开发人员使用经典的setTimeoutsetInterval调用来安排对网页或应用的更改。这两个调用都允许您在一定数量的毫秒之后安排一个回调,然后允许您在回调期间对页面进行更改。然而,使用这种方法存在一些重大问题:

  • 作为开发人员,您需要猜测未来合适的毫秒数来安排下一次更新。随着现代网络在比以往更多种类的设备上运行,很难知道高性能桌面设备和移动电话的建议帧速率。即使您猜测每秒调度多少帧,您也可能会与其他页面或机器负载竞争。
  • 用户使用多个窗口或标签进行浏览比以往任何时候都更加普遍,甚至在移动设备上也是如此。如果您使用setTimeoutsetInterval来安排您的页面更新,即使页面在后台,这些更新也会继续发生。在脚本不可见的情况下运行脚本是让用户相信你的 web 应用正在耗尽他们的手机电池的好方法!

作为替代,许多浏览器现在在window对象上提供了一个requestAnimationFrame功能。该函数将回调作为其参数,只要浏览器认为适合更新动画,就会调用回调。

让我们添加另一个例子(清单 2-32 )我们的赛道场景,这个用一个粗糙的动画暴雨来表示我们即将到来的比赛的取消。这段代码建立在前面的例子之上,多余的代码不在这里列出。

清单 2-32。基本动画帧要求

`// create an image for our rain texture
var rain = new Image();
rain.src = "rain.png";
rain.onload = function () {
  // Start off the animation with a single frame request
  // once the rain is loaded
  window.requestAnimFrame(loopAnimation, canvas);
}

// Previous code omitted…

// this function allows us to cover all browsers
// by aliasing the different browser-specific
// versions of the function to a single function
window.requestAnimFrame = (function(){
  return  window.requestAnimationFrame       ||
          window.webkitRequestAnimationFrame ||
          window.mozRequestAnimationFrame    ||
          window.oRequestAnimationFrame      ||
          window.msRequestAnimationFrame     ||
          // fall back to the old setTimeout technique if nothing
          // else is available
          function(/* function / callback, / DOMElement */ element){
            window.setTimeout(callback, 1000 / 60);
          };
})();

// This function is where we update the content of our canvas
function drawAFrame() {
  var context = canvas.getContext('2d');

// do some drawing on the canvas, using the elapsedTime
  // as a guide for changes.
  context.save();

// draw the existing trails picture first
  drawTrails(); // Darken the canvas for an eerie sky.
  // By only darkening most of the time, we create lightning flashes
  if (Math.random() > .01) {
    context.globalAlpha = 0.65;
    context.fillStyle = '#000000';
    context.fillRect(0, 0, 400, 600);
    context.globalAlpha = 1.0;
  }

// then draw a rain image, adjusted by the current time
  var now = Date.now();
  context.fillStyle = context.createPattern(rain, 'repeat');

// We'll draw two translated rain images at different rates to
  // show thick rain and snow
  // Our rectangle will be bigger than the display size, and
  // repositioned based on the time
  context.save();
  context.translate(-256 + (0.1 * now) % 256, -256 + (0.5 * now) % 256);
  context.fillRect(0, 0, 400 + 256, 600 + 256);
  context.restore();

// The second rectangle translates at a different rate for
  // thicker rain appearance
  context.save();
  context.translate(-256 + (0.08 * now) % 256, -256 + (0.2 * now) % 256);
  context.fillRect(0, 0, 400 + 256, 600 + 256);
  context.restore();

// draw some explanatory text
  context.font = '32px san-serif';
  context.textAlign = 'center';
  context.fillStyle = '#990000';
  context.fillText('Event canceled due to weather!', 200, 550, 400);
  context.restore();
}

// This function will be called whenever the browser is ready
// for our application to render another frame.
function loopAnimation(currentTime) {
  // Draw a single frame of animation on our canvas
  drawAFrame();

// After this frame is drawn, let the browser schedule
  // the next one
  window.requestAnimFrame(loopAnimation, canvas);
}`

一旦我们更新了我们的绘图,我们可以在我们的轨迹上看到动画雨(见图 2-22 )。

Image

图 2-22。带雨动画的画布静止镜头

由浏览器决定多久调用一次动画帧回调。后台页面的调用频率将会降低,浏览器可能会将呈现剪辑到提供给requestAnimationFrame调用(在我们的示例中为“canvas”)的元素,以优化绘图资源。你不能保证一个帧速率,但是你不用为不同的环境安排时间了!

这种技术并不局限于 Canvas API。您可以使用requestAnimationFrame在页面内容或 CSS 的任何地方进行修改。还有其他方法可以在网页上产生移动效果——我想到了 CSS 动画——但是如果你正在处理基于脚本的变化,那么requestAnimationFrame函数是最好的方法。

总结

正如您所看到的,Canvas API 提供了一种非常强大的方法来修改 web 应用的外观,而无需求助于奇怪的文档攻击。图像、渐变和复杂路径可以组合在一起,以创建您想要呈现的几乎任何类型的显示。请记住,您通常需要在原点绘制,在尝试绘制之前加载您想要显示的任何图像,并注意不要让外来图像源污染您的画布。然而,如果你学会利用画布的力量,你就可以创建以前在网页中不可能实现的应用。

三、可缩放矢量图形

在这一章中,我们将探索 HTML5 中的另一个图形特性可以做什么:可缩放矢量图形。可缩放矢量图形(SVG)是一种表达二维图形的语言。

SVG 概述

在这一节中,我们将了解 HTML5 浏览器中的标准矢量图形支持,但首先,让我们回顾几个图形概念:光栅和矢量图形。

在光栅图形中,图像由二维像素网格表示。HTML5 Canvas 2d API 是光栅图形 API 的一个例子。使用画布 API 进行绘制会更新画布的像素。PNG 和 JPEG 是光栅图像格式的示例。PNG 和 JPEG 图像中的数据也表示像素。

矢量图形则完全不同。矢量图形用几何的数学描述来表示图像。矢量图像包含从高级几何对象(如线条和形状)绘制图像所需的所有信息。从名字就可以看出,SVG 是矢量图形的一个例子。像 HTML 一样,SVG 也是一种具有 API 的文件格式。SVG 与 DOM APIs 结合形成了矢量图形 API。可以在 SVG 中嵌入光栅图形,比如 PNG 图像,但是 SVG 主要是一种矢量格式。

历史

SVG 已经存在几年了。SVG 1.0 于 2001 年作为 W3C 推荐标准发布。SVG 最初是通过插件在浏览器中使用的。不久之后,浏览器增加了对 SVG 图像的本地支持。

HTML 中的内联 SVG 历史较短。SVG 的一个定义性特征是它基于 XML。当然,HTML 有不同的语法,你不能简单地将 XML 语法嵌入到 HTML 文档中。相反,它对 SVG 有特殊的规则。在 HTML5 之前,可以将 SVG 作为元素嵌入 HTML 页面或链接到自包含页面。svg 文档。HTML5 引入了内联 SVG,其中 SVG 元素本身可以出现在 HTML 标记中。当然,在 HTML 中,语法规则比 XML 更宽松。可以有未加引号的属性、混合大写等等。在适当的时候,您仍然需要使用自结束标记。例如,您可以在 HTML 文档中嵌入一个圆圈,只需一点标记:

<svg height=100 width=100><circle cx=50 cy=50 r=50 /></svg>

了解 SVG

图 3-1 显示了一个带有快乐小径的 HTML5 文档!我们在第二章中用画布 API 绘制的图像。如果你看了这一章的标题,你大概可以猜到这个版本是用 SVG 绘制的。SVG 允许您进行许多与 canvas API 相同的绘图操作。很多时候,结果在视觉上是一样的。然而,有一些重要的看不见的差异。首先,文本是可选的。你不能用画布得到它!当您在画布元素上绘制文本时,字符被冻结为像素。它们成为图像的一部分,并且不能改变,除非您重画画布的一个区域。因此,搜索引擎看不到绘制在画布上的文本。另一方面,SVG 是可搜索的。例如,Google 将网页上 SVG 内容中的文本编入索引。

Image

图 3-1 。SVG 版的快乐步道!

SVG 与 HTML 密切相关。如果您愿意,可以用标记定义 SVG 文档的内容。HTML 是一种用于构建页面的声明性语言。SVG 是一种创建可视化结构的辅助语言。您可以使用 DOM APIs 与 SVG 和 HTML 进行交互。SVG 文档是动态的元素树,您可以像 HTML 一样编写脚本和设计样式。您可以将事件处理程序附加到 SVG 元素。例如,您可以使用 click 事件处理程序来制作 SVG 按钮或有形状的可点击区域。这对于构建使用鼠标输入的交互式应用至关重要。

此外,您可以在浏览器的开发工具中查看和编辑 SVG 的结构。正如你在图 3-2 中看到的,内嵌 SVG 直接嵌入到 HTML DOM 中。它有一个你可以在运行时观察和改变的结构。您可以深入研究 SVG 并查看其来源,不像图像只是一个像素网格。

Image

图 3-2 。查看 ChromeWeb Inspector 中的 SVG 元素

在图 3-2 中,高亮显示的文本元素包含以下代码:

< text y="60" x="200" font-family="impact" font-size="60px"   fill="#996600" text-anchor="middle">     Happy Trails </text>

在开发环境中,您可以添加、删除和编辑 SVG 元素。这些更改会在活动页面中立即生效。这对于调试和实验来说极其方便。

保留模式图形

Frank 说:“在图形 API 设计中有两个学派。canvas 等即时模式图形提供了一个绘图界面。API 调用导致一个绘制动作立即发生,因此得名。与即时模式图形相对应的样式称为保留模式。在保留模式图形中,场景中有一个随时间保留的视觉对象模型。有一个 API 操纵场景图形,图形引擎在场景发生变化时重绘场景。SVG 是保留模式图形,其场景图是文档。操纵 SVG 的 API 是 W3C DOM API。

*有一些 JavaScript 库在 canvas 之上构建保留模式 API。有些还提供精灵、输入处理和层。您可以选择使用这样的库,但是请记住这些特性以及更多特性都是 SVG 固有的!"

可扩展图形

当您放大、旋转或以其他方式变换 SVG 内容时,组成图像的所有线条都会清晰地重新绘制。SVG 在不损失质量的情况下扩展。构成 SVG 文档的矢量信息在呈现时会保留下来。与像素图形形成对比。如果您像放大画布或图像一样放大像素图形,它会变得模糊。这是因为图像是由只能以更高分辨率重新采样的像素组成的。潜在信息——制作图像的路径和形状——在绘制后会丢失(见图 3-3 )。

Image

图 3-3。放大 500%的 SVG 和画布特写

用 SVG 创建 2D 图形

让我们再来看看快乐小径!图片来自图 3-1 。这个 SVG 图形的每个可见部分都有一些相应的标记。完整的 SVG 语言非常广泛,它的所有细节和细微差别都不适合在本章讲述。然而,为了对 SVG 词汇的广度有所了解,这里有一些用于绘制愉快轨迹的特性:

  • 形状
  • 小路
  • 转换
  • 图案和渐变
  • 可重用内容
  • 文本

在我们将它们组合成一个完整的场景之前,让我们依次看看每一个。但是,在我们这样做之前,我们需要了解如何将 SVG 添加到页面中。

给页面添加 SVG

将内联 SVG 添加到 HTML 页面就像添加任何其他元素一样简单。

在 Web 上使用 SVG 有几种方式,包括作为元素。我们将在 HTML 中使用内联 SVG,因为它将集成到 HTML 文档中。这将让我们以后编写一个无缝结合 HTML、JavaScript 和 SVG 的交互式应用(参见清单 3-1 )。

清单 3-1。包含红色矩形的 SVG

<!doctype html> <svg width="200" height="200"> </svg>

就这样!不需要 XML 名称空间。现在,在开始和结束 svg 标记之间,我们可以添加形状和其他可视对象。如果您想将 SVG 内容拆分成一个单独的。svg 文件,您需要像这样更改它:

<svg width="400" height="600"     xmlns:xlink="http://www.w3.org/1999/xlink"> </svg>

现在它是一个有效的 XML 文档,具有适当的名称空间属性。您将能够使用各种图像查看器和编辑器打开该文档。你也可以从 HTML 中引用一个 SVG 文件,作为带有代码的静态图像,比如<img src="example.svg">。这种方法的一个缺点是,SVG 文档没有像内联 SVG 内容那样集成到 DOM 中。您将无法编写与 SVG 元素交互的脚本。

简单的形状

SVG 语言包括基本的形状元素,如矩形、圆形和椭圆形。形状元素的大小和位置由属性定义。对于矩形,这些是widthheight。对于圆,半径有一个r属性。所有这些都使用距离的 CSS 语法,所以它们可以是像素、点、单位等等。清单 3-2 是一个非常短的包含内嵌 SVG 的 HTML 文档。它只是一个带有红色轮廓的灰色矩形,大小为 100 像素乘 80 像素,显示在图 3-4 中。

清单 3-2。包含红色矩形的 SVG

<!doctype html> <svg width="200" height="200">   <rect x="10" y="20" width="100" height="80" stroke="red" fill="#ccc" /> </svg> Image

图 3-4。HTML 文档中的 SVG 矩形

SVG 按照对象在文档中出现的顺序绘制对象。如果我们在矩形后添加一个圆,它会出现在第一个形状的顶部。我们将给这个圆一个 8 像素宽的蓝色笔触,没有填充样式(见清单 3-3 ,所以它很突出,如图图 3-5 所示。

清单 3-3。一个长方形和一个圆形

<!doctype html> <svg width="200" height="200">   <rect x="10" y="20" width="100" height="80" stroke="red" fill="#ccc" />   <circle cx="120" cy="80" r="40" stroke="#00f" fill="none" stroke-width="8" /> </svg> Image

图 3-5。一个长方形和一个圆形

注意,x 和 y 属性定义了矩形左上角的位置。另一方面,圆具有 cx 和 cy 属性,它们是圆心的 x 和 y 值。SVG 使用与 canvas API 相同的坐标系。svg 元素的左上角是位置 0,0。画布坐标系详见第二章。

转换 SVG 元素

SVG 中的组织元素旨在组合多个元素,以便它们可以作为单元进行转换或链接。<g>元素代表“组”组可用于组合多个相关元素。作为一个组,它们可以通过一个公共 ID 来引用。一个组也可以作为一个单元进行转换。如果将变换属性添加到组中,该组的所有内容都会被变换。变换属性可以包括旋转(见清单 3-4 和图 3-6 )、平移、缩放和倾斜的命令。您还可以指定一个转换矩阵,就像您使用 canvas API 一样。

清单 3-4。旋转组中的矩形和圆形

<svg width="200" height="200">     <g transform="translate(60,0) rotate(30) scale(0.75)" id="ShapeGroup">       <rect x="10" y="20" width="100" height="80" stroke="red" fill="#ccc" />       <circle cx="120" cy="80" r="40" stroke="#00f" fill="none" stroke-width="8" />     </g> </svg> Image

图 3-6。一个旋转组

重复使用内容

SVG 有一个<defs>元素,用于定义将来使用的内容。它还有一个名为<use>的元素,可以链接到您的定义。这使您可以多次重用相同的内容,并消除冗余。图 3-7 显示了在不同的变换位置和比例下使用了三次的组。这个组的 id 是ShapeGroup,它包含一个矩形和一个圆形。实际的矩形和圆形只在<defs>元素中定义了一次。定义的组本身是不可见的。相反,有三个<use>元素链接到形状组,所以三个矩形和三个圆形呈现在页面上(见清单 3-5 )。

清单 3-5。使用一组三次

`
  
    
      
      
    

  


  
  
` Image

图 3-7。三个使用元素引用同一个组

图案和渐变

图 3-7 中的圆形和矩形具有简单的填充和描边样式。物体可以被绘制成更复杂的样式,包括渐变和图案(见清单 3-6 )。渐变可以是线性的,也可以是放射状的。模式可以由像素图形甚至其他 SVG 元素组成。图 3-8 显示了一个带有线性颜色渐变的矩形和一个带有砾石纹理的圆形。纹理来自链接到 SVG 图像元素的 JPEG 图像。

清单 3-6。给矩形和圆形添加纹理

`<!doctype html>

  
    
      
    


        
        
    

  


  
` Image

图 3-8。渐变填充的矩形和图案填充的圆形

SVG 路径

SVG 有自由形式的路径和简单的形状。路径元素有d属性。“d”代表数据。在d属性的值中,您可以指定一系列路径绘制命令。每个命令都可能带有坐标参数。有些命令是 M 代表 moveto,L 代表 lineto,Q 代表二次曲线,Z 代表闭合路径。如果这些让你想起了画布绘制 API,那就不是巧合了。清单 3-7 使用一系列 lineto 命令,使用一个路径元素绘制一个封闭的树冠形状。

清单 3-7。定义树冠的 SVG 路径

    <path d="M-25, -50             L-10, -80             L-20, -80             L-5, -110             L-15, -110             L0, -140             L15, -110             L5, -110             L20, -80             L10, -80             L25, -50             Z" id="Canopy"></path>

你可以用 Z 命令关闭一个路径并给它一个填充属性来填充它,就像我们之前画的矩形一样。图 3-9 展示了如何结合描边封闭路径和填充封闭路径来绘制一棵树。

Image

图 3-9。描边路径、填充路径和两种路径

同样,我们可以用两条二次曲线创建一条开放路径,形成一条小径。我们甚至可以赋予它质感。注意清单 3-8 中的stroke-linejoin属性。这在两条二次曲线之间形成了圆形连接。图 3-10 显示了一条被绘制成开放路径的山路。

清单 3-8。定义扭曲轨迹的 SVG 路径

  <g transform="translate(-10, 350)" stroke-width="20" stroke="url(#GravelPattern)" stroke-linejoin="round">         <path d="M0,0 Q170,-50 260, -190 Q310, -250 410,-250" fill="none"></path>   </g> Image

图 3-10。包含两条二次曲线的开放路径

使用 SVG 文本

SVG 也支持文本。SVG 格式的文本可在浏览器中选择(参见图 3-11 )。如果用户愿意,浏览器和搜索引擎也可以允许用户在 SVG 文本元素中搜索文本。这在可用性和可访问性方面有很大的好处。

SVG 文本的属性类似于 HTML 的 CSS 样式规则。清单 3-9 显示了一个具有font-weightfont-family属性的文本元素。和 CSS 一样,font-family 可以是一个单独的字体名称,如“sans-serif ”,也可以是一系列备用名称,如“Droid Sans,sans-serif ”,按照您喜欢的顺序排列。

清单 3-9。 SVG 文本

<svg width="600" height="200">   <text     x="10" y="80"     font-family="Droid Sans"     stroke="#00f"     fill="#0ff"     font-size="40px"     font-weight="bold">     Select this text!   </text> </svg> Image

图 3-11。选择 SVG 文本

把场景组合在一起

我们可以把前面所有的元素结合起来,形成一幅快乐小径的图像。文本自然是一个文本元素。树干由两个长方形组成。树冠是两条路。树木投射阴影,使用相同的几何图形给定一个灰色填充颜色和一个向下向右倾斜的变换。穿过图像的弯曲路径是另一个具有纹理图像图案的路径。还有一点 CSS 给场景一个轮廓。

清单 3-10 提供了trails-static.html的完整代码。

清单 3-10。完整代码为trails-static.html

`Happy Trails in SVG


        
        
        

        
        
        
        
        


        
        
        
        
        
        
        
        
        
        
        


        
        
        

  


        
  


        Happy Trails!
  


  


`

使用 SVG 构建交互式应用

在这一节中,我们将扩展静态示例。我们将添加 HTML 和 JavaScript 来使文档具有交互性。我们将在一个应用中利用 SVG 的功能,这个应用需要更多的代码来实现 canvas API。

添加树木

在这个交互式应用中,我们只需要一个按钮元素。按钮的 click 处理程序在 600x400 像素的 SVG 区域内的随机位置添加一个新树。新树也随机缩放 50%到 150%之间的量。每个新树实际上是一个引用包含多条路径的“树”组的<use>元素。代码使用命名空间document.createElementNS()调用来创建一个<use>元素。它用xlink:href属性将它链接到先前定义的树组。然后它把新元素添加到 SVG 元素树中(见清单 3-11 )。

清单 3-11。添加树功能

`  document.getElementById("AddTreeButton").onclick = function() {
    var x = Math.floor(Math.random() * 400);
    var y = Math.floor(Math.random() * 600);
    var scale = Math.random() + .5;
    var translate = "translate(" +x+ "," +y+ ") ";

var tree = document.createElementNS("http://www.w3.org/2000/svg", "use");
    tree.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", "#Tree");
    tree.setAttribute("transform", translate + "scale(" + scale + ")");
    document.querySelector("svg").appendChild(tree);
    updateTrees();
  }`

元素按照它们在 DOM 中出现的顺序呈现。这个函数总是将树作为新的子节点添加到 SVG 元素的子节点列表的末尾。这意味着新的树会出现在老的树的上面。

这个函数以调用updateTrees()结束,我们接下来会看到。

增加更新树功能

updateTrees函数在文档最初加载时以及添加或删除树时运行。它负责更新显示森林中树木数量的文本。它还为每棵树附加了一个点击处理函数(见清单 3-12 )。

清单 3-12 更新树函数

  function updateTrees() {     var list = document.querySelectorAll("use");     var treeCount = 0;     for (var i=0; i<list.length; i++) {       if(list[i].getAttribute("xlink:href")=="#Tree") {         treeCount++;         list[i].onclick = removeTree;       }     }     var counter = document.getElementById("TreeCounter");     counter.textContent = treeCount + " trees in the forest";   }

关于这段代码,需要注意的一件重要事情是,它在 JavaScript 中没有保留关于树计数的状态。每次发生更新时,这段代码都会选择并过滤 live 文档中的所有树,以获得最新的计数。

增加 removeTree 功能

现在,让我们添加当树被点击时移除它们的函数(见清单 3-13 )。

清单 3-13。移除树功能

  function removeTree(e) {     var elt = e.target;     if (elt.correspondingUseElement) {       elt = elt.correspondingUseElement;     }     elt.parentNode.removeChild(elt);     updateTrees();   }

我们在这里做的第一件事是检查点击事件的目标。由于 DOM 实现的不同,事件目标可以是树组,也可以是链接到该组的 use 元素。不管怎样,这个函数只是从 DOM 中删除那个元素,并调用updateTrees()函数。

如果您删除了位于另一棵树顶部的树,您不必做任何事情来重新绘制较低的内容。这是使用保留模式 API 进行开发的好处之一。您只需操作元素树(没有双关的意思),浏览器就会负责绘制必要的像素。同样,当文本更新以显示最新的树数时,它会停留在树的下方。如果希望文本出现在树的上方,就必须在文本元素之前将树附加到文档中。

添加 CSS 样式

为了使交互更容易被发现,我们将添加一些 CSS 来改变鼠标光标下的树的外观:

g[id=Tree]:hover  {         opacity: 0.9;         cursor: crosshair;   }

每当您将鼠标悬停在 id 属性等于“Tree”的元素上时,该元素将变为部分透明,并且鼠标光标将变为十字准线。

CSS 中也定义了围绕整个 SVG 元素的一个像素的黑色边框。

  svg {     border: 1px solid black;   }

就这样!现在你有了一个在 HTML5 中使用内嵌 SVG 的交互式应用(见图 3-12 )。

Image

图 3-12。最后的文件加上了几棵树

最终代码

为了完整起见,清单 3-14 提供了完整的trails-dynamic.html文件。它包含了静态版本的所有 SVG 以及使其具有交互性的脚本。

清单 3-14。整个trails-dynamic.html代码

`<!doctype html>

<title>Happy Trails in SVG</title>
  ` `


    
      
    

    
        
        
        
    


    
    
        
        
    

    
      
      
      
      
    

  


        
  

    Happy Trails!
  

  
  


    You can remove a
    tree by clicking on it.
  


  

`

【SVG 工具】??㎡

Frank 说:“由于 SVG 作为矢量图形的标准格式有着悠久的历史,因此有许多有用的工具可以用来处理 SVG 图像。甚至还有一个运行在浏览器中的开源编辑器 SVG-edit。你可以把它嵌入到你自己的应用中!在桌面上,Adobe Illustrator 和 Inkscape 是两个强大的矢量图形应用,可以导入和导出 SVG。我发现 Inkscape 对于创建新图形非常有用(见图 3-13 )。

SVG 工具倾向于独立工作。svg 文件,而不是嵌入在 HTML 中的 SVG,所以您可能需要在这两种格式之间进行转换。"

Image

图 3-13。在 Inkscape 中修改文本元素的笔画

总结

在这一章中,你已经看到了 HTML5 中的 SVG 是如何提供一种强大的方法来创建具有交互式二维图形的应用的。

首先,我们看一个使用嵌入在 HTML5 文档中的 SVG 绘制的场景。我们检查了构成绘图的元素和属性。我们看到了如何定义和重用内容定义、分组和转换元素,以及使用形状、路径和文本进行绘图。

最后,我们将 JavaScript 添加到一个 SVG 文档中,以制作一个交互式应用。我们使用 CSS、DOM 操作和事件来利用 SVG 作为动态文档的特性。

现在我们已经看到了 SVG 如何将矢量图形引入 HTML5,我们将把注意力转向为应用带来更复杂媒体的视听元素。*

四、使用音频和视频

在这一章中,我们将探索你能用两个重要的 HTML5 元素做什么——音频和视频并且我们将向你展示它们如何被用来创建引人注目的应用。音频和视频元素为 HTML5 应用添加了新的媒体选项,允许您在没有插件的情况下使用音频和视频,同时提供一个通用的、集成的、可脚本化的 API。**

首先,我们将讨论音频和视频容器文件和编解码器,以及为什么我们最终得到了今天支持的编解码器。我们将继续描述缺乏通用编解码器支持——这是使用媒体元素的最大缺点——并且我们将讨论我们如何希望这在未来不会成为如此大的问题。我们还将向您展示一种切换到最适合浏览器显示的内容类型的机制。

接下来,我们将向您展示如何使用 API 以编程方式使用音频和视频控件,最后我们将探索音频和视频在您的应用中的使用。

音频和视频概述

在下面的章节中,我们将讨论一些与音频和视频相关的关键概念:容器和编解码器。

视频容器

音频或视频文件实际上只是一个容器文件,类似于包含许多文件的 ZIP 存档文件。图 4-1 显示了一个视频文件(一个视频容器)是如何包含音频轨道、视频轨道和附加元数据的。音频和视频轨道在运行时被组合以播放视频。元数据包含有关视频的信息,如封面、标题和副标题、字幕信息等。

Image

图 4-1。视频容器概述

一些流行的视频容器格式包括:

  • 音频视频交错(。avi)
  • Flash 视频(。flv)
  • MPEG 4 (.mp4)
  • 型芯型腔(. mkv)
  • ogg(ogv)

音频和视频编解码器

音频和视频编码器/解码器 ( 编解码器)是用于编码和解码特定音频或视频流的算法,以便可以播放它们。原始媒体文件非常庞大,因此如果不进行编码,视频或音频剪辑将包含大量数据,这些数据可能太大,无法在合理的时间内通过互联网传输。如果没有解码器,接收方将无法从编码形式中重建原始媒体源。编解码器能够理解特定的容器格式,并对其包含的音频和视频轨道进行解码。

以下是一些示例音频编解码器:

  • 加气混凝土
  • MPEG-3
  • 还有沃比斯

视频编解码器示例如下:

  • H.264
  • VP8
  • Ogg Theora
编解码器大战和暂时休战

一些编解码器受专利保护,而另一些则免费提供。例如,Vorbis 音频编解码器和 Theora 视频编解码器是免费提供的,而 MPEG-4 和 H.264 编解码器的使用需要支付许可费。

最初,HTML5 规范要求支持某些编解码器。然而,一些供应商不希望包括 Ogg Theora,因为它不是他们现有的硬件和软件堆栈的一部分。例如,苹果的 iPhone 包括 h264 视频的硬件加速解码,但没有 Theora。另一方面,免费系统不能在不损害下游分发的情况下包含专有的付费编解码器。最重要的是,某些专有编解码器提供的性能是浏览器采用免费编解码器的一个因素。这种情况导致了僵局;似乎没有一个单一的编解码器,所有的浏览器供应商都愿意实现。

目前,编解码器要求已从规范中删除。然而,这一决定可能会在未来重新审议。现在,了解当前的浏览器支持,并了解您可能需要为不同的环境重新编码您的媒体。(您可能已经开始这样做了。)

我们确实希望对不同编解码器的支持会随着时间的推移而增加和融合,使常见媒体类型的选择变得容易和普遍。一种编解码器也有可能发展成为 Web 的事实上的标准编解码器。此外,媒体标签具有一种内置机制,可以切换到最适合浏览器显示的内容类型,从而简化对不同环境的支持。

韦伯来了

Frank 说:“谷歌在 2010 年 5 月推出了 WebM 视频格式。WebM 是一种新的音频和视频格式,旨在清除网络上模糊的媒体格式。WebM 文件的扩展名为.webm,在基于 Matroska 的容器中包含 VP8 视频和 Ogg Vorbis 音频。Google 在涵盖源代码和专利权的许可许可下发布了 WebM 规范和软件。作为一种对实施者和发布者都免费的高质量格式,WebM 代表了编解码器领域的重大发展。”

音频和视频限制

音频和视频规范中有一些不支持的内容:

  • 流媒体音频和视频。即 HTML5 视频目前没有码率切换的标准;当前的实现仅支持完整的媒体文件。然而,一旦支持流媒体格式,该规范的某些方面将在将来支持流媒体。
  • 媒体受到 HTTP 跨源资源共享的限制。参见第六章了解更多关于跨产地资源共享的信息(CORS)。
  • 全屏视频是不可脚本化的,因为让可脚本化的元素接管全屏会被认为是违反安全的。然而,浏览器可以让用户通过附加控件选择全屏观看视频。

支持音频和视频的浏览器

由于支离破碎的编解码器支持,仅仅知道哪些浏览器支持新的audiovideo元素是不够的;您还需要知道支持哪些编解码器。表 4-1 显示了在撰写本文时哪些浏览器支持哪些编解码器。

还要注意的是,谷歌宣布将放弃对 MP4 格式的支持,但这还没有发生。此外,还有一个插件可以用来在 Internet Explorer 9 中播放 WebM。首先测试是否支持音频和视频总是一个好主意。本章后面的“检查浏览器支持”一节将向您展示如何以编程方式检查浏览器支持。

使用音频和视频 API

在本节中,我们将探讨音频和视频在您的应用中的使用。与以前的视频嵌入技术(通常使用 Flash、QuickTime 或 Windows Media 插件嵌入视频)相比,使用新的媒体标签有两个主要优势,旨在使用户和开发人员的生活更加轻松:

  • 作为原生浏览器环境的一部分,新的音频和视频标签消除了部署障碍。虽然有些插件的安装率很高,但在受控的企业环境中却经常被屏蔽。一些用户选择禁用这些插件,因为...招摇…广告显示那些插件也能够,这也删除了他们的能力,用于媒体播放。插件也是安全问题的独立攻击媒介。插件通常很难将它们的显示与浏览器的其他内容整合在一起,导致某些网站设计的剪辑或透明度问题。因为插件使用一个独立的呈现模型,这个模型不同于基本网页的呈现模型,如果弹出菜单或其他可视元素需要跨越页面中的插件边界,开发人员会遇到困难。
  • 媒体元素向文档展示了一个通用的、集成的、可脚本化的 API。作为一名开发人员,您对新媒体元素的使用允许以非常简单的方式编写内容的控制和回放。在这一章的后面,我们将会看到许多这样的例子。

当然,使用媒体标签有一个主要的缺点:缺乏通用的编解码器支持,这一点在本章前面已经讨论过了。然而,我们预计对编解码器的支持将会增加,并随着时间的推移而融合,使常见媒体类型的选择变得容易和普遍。此外,媒体标签有一个内置机制,可以切换到最适合浏览器显示的内容类型,您很快就会看到这一点。

检查浏览器支持

检查对videoaudio标签支持的最简单方法是用脚本动态创建一个或两个标签,并检查函数的存在:

var hasVideo = !!(document.createElement('video').canPlayType);

这个简单的代码行将动态创建一个video元素,并检查canPlayType()函数是否存在。通过使用!!操作符,结果被转换成一个布尔值,该值表示是否可以创建一个视频对象。

但是,如果不支持视频或音频,您可以选择使用一个启用脚本,该脚本将媒体脚本标记引入旧浏览器,允许相同的脚本能力,但使用 Flash 等技术进行回放。

或者,您可以选择在您的audiovideo标签之间包含替代内容,替代内容将代替不支持的标签显示。如果浏览器不支持 HTML5 标签,这种替代内容可以用于 Flash 插件来显示相同的视频。如果你仅仅希望在不支持的浏览器上显示文本消息,在videoaudio元素中添加内容是很容易的,如清单 4-1 所示。

清单 4-1。简单视频元素

<video src="video.webm" controls>   Your browser does not support HTML5 video. </video>

然而,如果您选择使用一种替代方法来为不支持 HTML5 媒体的浏览器呈现视频,您可以使用相同的元素内容部分来提供对显示相同媒体的外部插件的引用,如清单 4-2 所示。

清单 4-2。带闪光后退的视频元素

<video src="video.webm" controls>   <object data="videoplayer.swf" type="application/x-shockwave-flash">     <param name="movie" value="video.swf"/>   </object>   Your browser does not support HTML5 video. </video>

通过在video元素中嵌入一个显示 Flash 视频的object元素,如果 HTML5 视频可用,它将是首选,Flash 视频将用作后备。不幸的是,这需要提供多个版本的视频,直到 HTML5 支持无处不在。

无障碍

让每个人都能访问你的 web 应用不仅仅是正确的事情;这是一笔好生意,在某些情况下,这是法律!应该为视力或听力有限的用户提供满足他们需求的替代内容。请记住,位于视频和音频元素之间的替代内容只有在浏览器支持这些元素时才会显示,因此不适用于浏览器可能支持 HTML5 媒体但用户可能不支持的可访问显示。

视频可访问性的新兴标准是 Web 视频文本轨道(WebVTT),以前称为 Web 子片段文本(WebSRT)格式。在撰写本文时,它才刚刚开始出现在一些早期版本的浏览器中。WebVTT 使用一个简单的文本文件(*.vtt),在第一行以单词WEBVTT开始。vtt文件必须以 mime 类型text/vtt提供。清单 4-3 显示了一个示例vtt文件的内容。

清单 4-3。 WebVTT 文件

`WEBVTT

1
00:00:01,000 --> 00:00:03,000
What do you think about HTML5 Video and WebVTT?...

2
00:00:04,000 --> 00:00:08,000
I think it’s great. I can’t wait for all the browsers to support it!`

要在video元素中使用vtt文件,添加指向vtt文件的track元素,如下例所示:

<video src="video.webm" controls>   <track label="English" kind="subtitles" srclang="en" src="subtitles_en.vtt" default>   Your browser does not support HTML5 video. </video>

您可以添加多个轨道元素。清单 4-4 展示了如何使用指向vtt文件的轨道元素来支持英语和荷兰语字幕。

清单 4-4。在视频元素中使用 WebVTT 轨道

<video src="video.ogg" controls>   <track label="English" kind="subtitles" srclang="en" src="subtitles_en.vtt">   <track label="Dutch" kind="subtitles" srclang="nl" src="subtitles_nl.vtt">   Your browser does not support HTML5 video. </video>

WebVTT 标准支持的不仅仅是字幕。它还允许标题和提示设置(关于如何呈现文本的说明)。完整的 WebVTT 语法超出了本书的范围。更多细节见[www.whatwg.org/specs/web-apps/current-work/webvtt.html](http://www.whatwg.org/specs/web-apps/current-work/webvtt.html)的 WHATWG 规范。

了解媒体元素

由于一个明智的设计决策,HTML5 中的audiovideo元素之间有很多共性。音频和视频都支持许多相同的操作——播放、暂停、静音/取消静音、加载等——因此,通用行为被分离到规范的媒体元素部分。让我们通过观察它们的共同点来开始研究媒体元素。

基础知识:声明你的媒体元素

为了举例,我们将使用一个audio标签来尝试 HTML5 媒体的常见行为。本节中的示例将会是非常媒体化的(惊喜!),它们包含在本书附带的支持文件的code/av文件夹中。

举一个最简单的例子(示例文件audio.html),让我们创建一个页面,显示一个舒缓、令人满意、非常公开的领域音频剪辑的音频播放器:约翰·塞巴斯蒂安·巴赫的“空气”(如清单 4-5 所示)。

清单 4-5。带有音频元素的 HTML 页面

`

  HTML5 Audio    `

这个片段假设 HTML 文档和音频文件(在本例中为johann_sebastian_bach_air.ogg)来自同一个目录。如图图 4-2 所示,在支持audio标签的浏览器中查看,会显示一个简单的控制和播放栏,代表要播放的音频。当用户单击播放按钮时,音轨会按预期开始播放。

Image

图 4-2。简单的音频控制

controls属性告诉浏览器显示用于在媒体剪辑中开始、停止和查找的常见用户控件,以及音量控件。省略controls属性会隐藏它们,并且让用户无法开始播放剪辑。

标签之间的内容是浏览器在不支持媒体标签时将显示的文本表示。如果您和您的用户运行的是旧版本的浏览器,他们将会看到这种情况。它还提供了包含媒体的替代呈现器的机会,例如 Flash player 插件或媒体文件的直接链接。

使用源

最后,我们来看最重要的属性:src。在最简单的设置中,单个src属性指向包含媒体剪辑的文件。但是,如果有问题的浏览器不支持该容器或编解码器(在这种情况下,Ogg 和 Vorbis)呢?然后,另一个声明显示在清单 4-6 中;它包括多个来源,浏览器可以从中选择(参见示例文件audio_multisource.html)。

清单 4-6。具有多个源元素的音频元素

<audio controls>     <source src="johann_sebastian_bach_air.ogg">     <source src="johann_sebastian_bach_air.mp3">     An audio clip from Johann Sebastian Bach. </audio>

在这种情况下,我们在audio标签上包含了两个新的source元素,而不是src属性。这允许浏览器选择最适合其回放能力的源,并将最适合的源用作实际的媒体剪辑。源是按顺序处理的,因此可以播放多个列出的源类型的浏览器将使用它遇到的第一个。

Image 注意将用户体验最好或服务器负载最低的媒体源文件放在任何source列表的最前面。

在支持的浏览器中运行此剪辑可能不会改变您看到的内容。但是如果浏览器支持 MP3 格式而不支持 Ogg Vorbis 格式,那么现在将支持媒体播放。这种声明模型的优点在于,当您编写代码与媒体文件交互时,实际使用的是哪个容器或编解码器并不重要。浏览器为您提供了一个统一的界面来操作媒体,无论哪个源匹配回放。

但是,还有另一种方法可以提示浏览器使用哪种媒体源。回想一下,媒体容器可以支持许多不同的编解码器类型,您将会理解,浏览器可能会被误导,根据所声明的源文件的扩展名,它支持或不支持哪些类型。如果您指定的类型属性与您的源不匹配,浏览器可能会拒绝播放媒体。只有在您确实知道的情况下,包含类型才是明智的。否则,最好省略这个属性,让浏览器检测编码,如清单 4-7 (在示例文件audio_type.html中)所示。还要注意,WebM 格式只允许一个音频编解码器和一个视频编解码器。这意味着.webm扩展名或 video/webm 内容类型会告诉您有关该文件的所有信息。如果一个浏览器可以播放。webm,它应该可以播放任何有效的.webm文件。

清单 4-7。在音频元素中包含类型和编解码器信息

<audio controls>     <source src="johann_sebastian_bach_air.ogg" type="audio/ogg; codecs=vorbis">     <source src="johann_sebastian_bach_air.mp3" type="audio/mpeg">     An audio clip from Johann Sebastian Bach. </audio>

如您所见,type属性可以声明容器和编解码器类型。这里的值分别代表 Ogg Vorbis 和 MP3。完整的列表由 RFC 4281 管理,RFC 4281 是由互联网工程任务组(IETF)维护的文档,但是一些常见的组合在表 4-2 中列出。

Image

取得控制权

您已经看到,默认的回放控件可以通过使用videoaudio标签中的controls属性来显示。正如您所料,当显示媒体时,省略该属性将不会显示控件,但是对于音频文件,它也不会显示任何内容,因为音频元素的唯一可视表示是它的控件。(没有控件的视频仍会显示视频内容。)省略controls属性不应该显示任何影响页面正常呈现的内容。让媒体播放的一种方法是在标签中设置另一个属性:autoplay(参见清单 4-8 和示例文件audio_no_control.html)。

清单 4-8。使用自动播放属性

<audio **autoplay**>     <source src="johann_sebastian_bach_air.ogg" type="audio/ogg; codecs=vorbis">     <source src="johann_sebastian_bach_air.mp3" type="audio/mpeg">     An audio clip from Johann Sebastian Bach. </audio>

通过包含autoplay属性,媒体文件将在加载后立即播放,无需任何用户交互。(注意,并非所有地方都支持自动播放。比如在 iOS 上是禁用的。)然而,大多数用户会觉得这非常令人讨厌,所以谨慎使用autoplay。播放音频而不进行提示可能是为了营造一种氛围效果,或者更糟的是,向用户强加一个广告。但它也会干扰用户机器上的其他音频播放,并且对依赖音频屏幕阅读器来浏览网页内容的用户非常不利。还要注意的是,有些设备,比如 iPad,会阻止自动播放,甚至是自动播放媒体文件(例如,由页面加载事件触发)。

如果内置控件不适合用户界面的布局,或者如果您需要使用默认控件中没有公开的计算或行为来控制媒体元素,那么还有许多内置 JavaScript 函数和属性来帮助您。表 4-3 列出了一些最常见的功能。

Image

canPlayType(type)方法有一个不明显的用例:通过将任意视频剪辑的 MIME 类型传递给动态创建的video元素,您可以使用一个简单的脚本来确定当前浏览器是否支持该类型。例如,以下代码提供了一种快速的方法来确定当前浏览器是否支持播放 MIME 类型为fooType的视频,而不在浏览器窗口中显示任何可见内容:

var supportsFooVideo = !!(document.createElement('video').canPlayType(‘fooType’));

请注意,该函数返回非常非二进制的“null”、“maybe”或“possible”,其中“possible”是可能的最佳场景。

表 4-4 显示了媒体元素的一些只读属性。

Image

表 4-5 显示了媒体元素上的一些属性,这些属性允许脚本修改它们并直接影响回放。因此,它们的行为类似于函数。

Image

在各种功能和属性之间,开发人员可以创建任何媒体回放用户界面,并使用它来控制浏览器支持的任何音频或视频剪辑。

使用音频

如果你理解了audiovideo媒体元素的共享属性,你基本上已经看到了audio标签所能提供的一切。因此,让我们来看一个简单的例子,它展示了控件脚本的运行。

音频激活

如果您的用户界面需要为用户播放音频剪辑,但您不希望播放时间线或控件影响显示,您可以创建一个不可见的audio元素,该元素的controls属性未设置或设置为false,并呈现您自己的音频播放控件。考虑一下清单 4-9 中的简单代码,它也可以在样本代码文件audioCue.html中找到。

清单 4-9。添加自己的播放按钮控制音频

`

<html>   <link rel="stylesheet" href="styles.css">   <title>Audio cue</title>

`

我们再次使用audio元素来演奏我们最喜欢的巴赫曲子。然而,在这个例子中,我们隐藏了用户控件,并且没有将剪辑设置为加载时自动播放。相反,我们创建了一个切换按钮来控制脚本的音频回放:

<button id="toggle" onclick="toggleSound()">Play</button>

我们的简单按钮被初始化来通知用户点击它将开始回放。并且每按一次按钮,就会触发toggleSound()功能。在toggleSound()函数中,我们首先访问 DOM 中的audiobutton元素:

if (music.paused) {     music.play();     toggle.innerHTML = "Pause"; }

通过访问audio元素上的paused属性,我们可以查看用户是否已经暂停了回放。如果没有开始播放,该属性默认为true,所以在第一次点击时将满足该条件。在这种情况下,我们调用剪辑上的play()函数,并更改按钮的文本,以指示下一次单击将暂停剪辑:

else {     music.pause();     toggle.innerHTML ="Play"; }

相反,如果音乐剪辑没有暂停(如果正在播放),我们将主动pause()它,并更改按钮文本以指示下次点击将重新开始播放。似乎很简单,不是吗?这就是 HTML5 中媒体元素的作用:在曾经存在无数插件的地方创建简单的跨媒体类型的显示和控制。简单是它自己的奖励。

处理视频

简单就够了。让我们试试更复杂的。HTML5 video元素非常类似于audio元素,但是加入了一些额外的属性。表 4-6 显示了其中的一些属性。

Image

video元素还有一个不适用于audio元素的关键特性:它可以提供给 HTML5 Canvas 的许多功能(参见第二章)。

创建视频时间轴浏览器

在这个更复杂的例子中,我们将展示一个video元素如何在动态画布中抓取并显示它的帧。为了演示这一功能,我们将构建一个简单的视频时间轴查看器。当视频播放时,来自显示器的周期性图像帧将被绘制到附近的画布上。当用户点击画布中显示的任何一帧时,我们将视频回放跳转到那个精确的时刻。只需几行代码,我们就可以创建一个时间轴浏览器,用户可以使用它在一个冗长的视频中跳转。

我们的视频剪辑样本是 20 世纪中期电影院诱人的特许广告,所以让我们都去大厅犒劳一下自己(见图 4-3 )。

Image

图 4-3。视频时间轴应用

添加视频和画布元素

我们从显示视频剪辑的简单声明开始:

<video id="movies" autoplay oncanplay="startVideo()" onended="stopTimeline()" autobuffer="true" width="400px" height="300px">     <source src="Intermission-Walk-in.ogv">     <source src="Intermission-Walk-in_512kb.mp4"> </video>

由于音频示例中的大多数标记对您来说都很熟悉,所以让我们来关注一下它们的区别。很明显,<audio>元素已经被<video>取代,<source>元素指向了浏览器将要选择的 Ogg 和 MPEG 电影。

在这种情况下,视频被声明为具有autoplay,这样页面一加载它就开始播放。注册了两个额外的事件处理函数。当视频被加载并准备开始播放时,oncanplay功能将触发并启动我们的例程。同样,当视频结束时,onended回调将允许我们停止创建视频帧。

接下来,我们将添加一个名为timeline的画布,我们将在其中定期绘制视频帧。

<canvas id="timeline" width="400px" height="300px">

添加变量

在演示的下一部分中,我们通过声明一些值来开始我们的脚本,这些值将使我们能够轻松地调整演示并使代码更具可读性:

// # of milliseconds between timeline frame updates var updateInterval = 5000; `// size of the timeline frames
var frameWidth = 100;
var frameHeight = 75;

// number of timeline frames
var frameRows = 4;
var frameColumns = 4;
var frameGrid = frameRows * frameColumns;`

updateInterval控制我们捕获视频帧的频率——在本例中,每五秒钟。frameWidthframeHeight设置小时间轴视频帧在画布中显示时的大小。类似地,frameRowsframeColumnsframeGrid决定了我们将在时间轴中显示多少帧:

`// current frame
var frameCount = 0;

// to cancel the timer at end of play
var intervalId;

var videoStarted = false;`

为了跟踪我们正在观看的视频帧,所有演示功能都可以访问一个frameCount。(为了我们的演示,一帧是我们每五秒钟拍摄的视频样本之一。)这个intervalId是用来停止我们将用来抓取帧的计时器。最后,我们添加了一个videoStarted标志来确保每个演示只创建一个计时器。

添加 updateFrame 函数

我们演示的核心功能——视频与画布相遇的地方——是我们抓取一个视频帧并将其绘制到画布上的地方:

`// paint a representation of the video frame into our canvas
function updateFrame() {
    var video = document.getElementById("movies");
    var timeline = document.getElementById("timeline");

var ctx = timeline.getContext("2d");

// calculate out the current position based on frame
    // count, then draw the image there using the video
    // as a source
    var framePosition = frameCount % frameGrid;
    var frameX = (framePosition % frameColumns) * frameWidth;
    var frameY = (Math.floor(framePosition / frameRows)) * frameHeight;
    ctx.drawImage(video, 0, 0, 400, 300, frameX, frameY, frameWidth, frameHeight);

frameCount++;
}`

正如你在第二章中看到的,对于任何画布,首先要做的是从中获取二维绘图上下文:

var ctx = timeline.getContext("2d");

因为我们想用从左到右、从上到下的帧填充我们的画布网格,所以我们需要根据我们捕获的帧的数量,准确地计算出哪个网格槽将用于我们的帧。根据每个框架的宽度和高度,我们可以确定开始绘图的精确 X 和 Y 坐标:

var framePosition = frameCount % frameGrid; var frameX = (framePosition % frameColumns) * frameWidth; var frameY = (Math.floor(framePosition / frameRows)) * frameHeight;

最后,我们到达在画布上绘制图像的按键调用。我们之前在 canvas 演示中已经看到了位置和缩放参数,但是这里我们没有将图像传递给drawImage例程,而是传递视频对象本身:

ctx.drawImage(video, 0, 0, 400, 300, frameX, frameY, frameWidth, frameHeight);

画布绘制例程可以将视频源作为图像或模式,这为您提供了一种修改视频并在另一个位置重新显示视频的便捷方式。

Image 注意当画布使用视频作为输入源时,它只绘制当前显示的视频帧。画布显示不会随着视频播放而动态更新。相反,如果您希望画布内容更新,您必须在视频播放时重新绘制图像。

增加启动视频功能

最后,我们更新frameCount以反映我们已经为我们的时间线拍摄了新的快照。现在,我们只需要一个例程来定期更新我们的时间轴帧:

`function startVideo() {

// only set up the timer the first time the
    // video is started
    if (videoStarted)
        return;

videoStarted = true;

// calculate an initial frame, then create
        // additional frames on a regular timer
        updateFrame();
        intervalId = setInterval(updateFrame, updateInterval);`

回想一下,一旦视频加载足够开始播放,就会触发startVideo()功能。首先,我们确保每次页面加载只处理一次视频开始,以防视频重新开始:

// only set up the timer the first time the     // video is started `    if (videoStarted)
        return;

videoStarted = true;`

当视频开始时,我们将捕捉我们的第一帧。然后,我们将启动一个间隔计时器——一个在指定的更新间隔持续重复的计时器——它将定期调用我们的updateFrame()函数。最终结果是每五秒钟捕获一个新帧:

        // calculate an initial frame, then create         // additional frames on a regular timer         updateFrame();         intervalId = setInterval(updateFrame, updateInterval);

处理用户输入

现在,我们需要做的就是处理单个时间轴帧的用户点击:

`// set up a handler to seek the video when a frame
// is clicked
var timeline = document.getElementById("timeline");
timeline.onclick = function(evt) {
    var offX = evt.layerX - timeline.offsetLeft;
    var offY = evt.layerY - timeline.offsetTop;

// calculate which frame in the grid was clicked
    // from a zero-based index
    var clickedFrame = Math.floor(offY / frameHeight) * frameRows;
    clickedFrame += Math.floor(offX / frameWidth);

// find the actual frame since the video started
    var seekedFrame = (((Math.floor(frameCount / frameGrid)) *
                                frameGrid) + clickedFrame);

// if the user clicked ahead of the current frame
    // then assume it was the last round of frames
    if (clickedFrame > (frameCount % 16))
        seekedFrame -= frameGrid;

// can't seek before the video
        if (seekedFrame < 0)
            return;`

事情变得有点复杂了。我们检索时间轴画布,并在其上设置一个点击处理函数。处理程序将使用事件来确定用户单击了哪个 X 和 Y 坐标:

    var timeline = document.getElementById("timeline");     timeline.onclick = function(evt) {         var offX = evt.layerX - timeline.offsetLeft;         var offY = evt.layerY - timeline.offsetTop;

然后,我们使用框架尺寸来计算用户点击了 16 个框架中的哪一个:

            // calculate which frame in the grid was clicked             // from a zero-based index             var clickedFrame = Math.floor(offY / frameHeight) * frameRows;             clickedFrame += Math.floor(offX / frameWidth);

单击的帧应该只是最近的视频帧之一,因此确定对应于该网格索引的最近的帧:

   // find the actual frame since the video started     var seekedFrame = (((Math.floor(frameCount / frameGrid)) *                                                frameGrid) + clickedFrame);

如果用户在当前帧之前单击,则跳回一个完整的网格帧周期以找到实际时间:

    // if the user clicked ahead of the current frame     // then assume it was the last round of frames     if (clickedFrame > (frameCount % 16))         seekedFrame -= frameGrid;

最后,我们必须防止用户点击视频剪辑开始前的帧:

    // can't seek before the video     if (seekedFrame < 0)         return;

现在我们知道了用户想要寻找的时间点,我们可以使用该知识来更改当前的回放时间。尽管这是关键的演示函数,但例程本身非常简单:

`    // seek the video to that frame (in seconds)
    var video = document.getElementById("movies");
    video.currentTime = seekedFrame * updateInterval / 1000;

// then set the frame count to our destination
    frameCount = seekedFrame;`

通过在我们的视频元素上设置currentTime属性,我们使视频搜索到指定的时间,并将我们当前的帧计数重置为新选择的帧。

Image 注意与许多处理毫秒的 JavaScript 计时器不同,视频的currentTime是以秒为单位指定的。

添加 stopTimeline 功能

对于我们的视频时间轴演示来说,剩下的就是当视频结束播放时停止捕捉帧。虽然不是必需的,但如果我们不执行这一步,演示将继续捕获已完成的演示的帧,过一会儿整个时间轴将会消失:

// stop gathering the timeline frames function stopTimeline() {     clearInterval(intervalId); }

当我们的另一个视频处理程序——onended——被视频播放完成触发时,将调用stopTimeline处理程序。

我们的视频时间轴的功能可能还不足以让高级用户满意,但它只用了很少的代码就完成了。现在,继续表演。

最终代码

清单 4-10 显示了视频时间线页面的完整代码。

清单 4-10。完整的视频时间轴代码

`

<html>   <link rel="stylesheet" href="styles.css">   <title>Video Timeline</title>

`

实用的临时演员

有时有些技术不适合我们的常规例子,但仍然适用于许多类型的 HTML5 应用。我们在这里向你展示一些简短但常见的实用附加功能。

页面中的背景噪音

许多网站试图通过默认为任何访问者播放音频来娱乐观众。虽然我们不容忍这种做法,但是音频支持使得实现这一点非常容易,如清单 4-11 所示。

清单 4-11。使用循环和自动播放属性

`

<html>   <link rel="stylesheet" href="styles.css">   <title>Background Music</title>

You're hooked on Bach!

`

正如你所看到的,播放一个循环的背景声音就像声明一个带有autoplayloop属性集的audio标签一样简单(参见图 4-4 )。

Image

图 4-4。使用自动播放功能在页面加载时播放音乐

在<眨眼>中失去观众

布莱恩对说:“权力越大,责任越大,仅仅因为你,并不意味着你就应该。如果你想要一个例子,只要记住<blink>标签!”

不要让简单的音频和视频播放诱惑你在不合适的地方使用它。如果您有令人信服的理由来启用带有autoplay的媒体—可能是用户期望内容在加载时启动的媒体浏览器—请确保提供禁用该功能的明确方法。没有什么比讨厌的内容更能让用户迅速离开你的网站,因为这些内容是他们不容易关掉的。"

鼠标悬停视频播放

对视频剪辑有效使用简单脚本的另一种方法是根据鼠标在视频上的移动触发playpause例程。这在需要显示许多视频剪辑并让用户选择播放的站点中可能很有用。当用户将鼠标移动到视频剪辑上时,视频剪辑库可以显示简短的预览剪辑,当用户单击时,可以显示完整的视频。使用类似于清单 4-12 的代码样本很容易达到这种效果(参见示例文件mouseoverVideo.html)。

清单 4-12。鼠标检测到一个视频元素上

`

<html>` `  <link rel="stylesheet" href="styles.css">   <title>Mouseover Video</title>

`

通过简单地设置一些额外的属性,当用户指向视频时可以触发预览回放,如图 4-5 所示。

Image

图 4-5。鼠标悬停视频播放

总结

在这一章中,我们探索了你可以用两个重要的 HTML5 元素audiovideo做什么。我们已经向您展示了如何使用它们来创建引人注目的 web 应用。audiovideo元素为 HTML5 应用添加了新的媒体选项,允许您在没有插件的情况下使用音频和视频,同时提供一个通用的、集成的、可脚本化的 API。

首先,我们讨论了音频和视频容器文件和编解码器,以及为什么我们最终选择了目前支持的编解码器。然后,我们向您展示了一种切换到最适合浏览器显示的内容类型的机制,并向您展示了如何使用 WebVTT 访问视频。

接下来,我们向您展示了如何使用 API 以编程方式使用控件音频和视频,最后我们看了如何在您的应用中使用 HTML5 音频和视频。

在下一章,我们将展示如何用最少的代码使用地理定位来定制应用的输出以适应用户的位置。

五、使用地理定位 API

假设您想要创建一个 web 应用,在应用用户步行(或跑步)即可到达的商店中提供跑鞋折扣和特价。使用地理定位 API,您可以请求用户共享他们的位置,如果他们同意,您可以向他们提供如何去附近的商店以折扣价购买一双新鞋的说明。

使用地理定位的另一个例子是一个追踪你跑了(或走了)多远的应用。你可以想象在开始跑步时打开手机浏览器中的应用。当你在移动时,应用会跟踪你跑了多远。跑步的坐标甚至可以覆盖在地图上,甚至可能带有高程剖面图。如果你在和其他对手赛跑,这个应用甚至可以显示对手的位置。

其他地理定位应用的想法可能是逐圈 GPS 风格的导航,社交网络应用,让你可以看到你的朋友在哪里,这样你就可以选择你想去的咖啡店,以及许多不寻常的应用。

在这一章中,我们将探索使用地理定位可以做些什么,这是一个令人兴奋的 API,它允许用户与 web 应用共享他们的位置,以便他们可以享受位置感知服务。首先,我们来看看地理位置信息的来源——纬度、经度和其他属性——以及它们来自哪里(GPS、Wi-Fi、蜂窝三角测量等等)。然后,我们将讨论使用地理位置数据的隐私问题,以及浏览器如何处理这些数据。

之后,我们将深入讨论地理定位 API 中两种不同的位置请求函数(方法):一次性位置请求和重复位置更新,我们将向您展示如何以及何时使用它们。接下来,我们将向您展示如何使用相同的 API 构建一个实用的地理定位应用,最后我们将讨论一些额外的用例及技巧。

关于位置信息

使用地理定位 API 相当简单。您请求一个位置,如果用户同意,浏览器将返回位置信息。位置由运行支持地理定位的浏览器的底层设备(例如,膝上型电脑或移动电话)提供给浏览器。位置信息作为一组纬度和经度坐标以及附加元数据提供。有了这些位置信息,您就可以构建一个引人注目的位置感知应用。

经纬度坐标

位置信息主要由一对纬度和经度坐标组成,如下例所示,显示了美丽的太浩城的坐标,该城位于太浩湖(美国最美丽的山湖)的岸边:

Latitude: 39.17222, Longitude: -120.13778

在前面的示例中,纬度(表示赤道以北或以南距离的数值为 39.17222)和经度(表示英格兰格林威治以东或以西距离的数值)为-120.13778。

纬度和经度坐标可以用不同的方式表示:

  • 十进制格式(例如,39.17222)
  • 度分秒(DMS)格式(例如,39° 10′20′)

Image 注意当你使用地理定位 API 时,坐标总是以十进制格式返回。

除了纬度和经度坐标,地理定位总是提供位置坐标的精度。根据运行浏览器的设备,可能还会提供其他元数据。这些包括高度高度精度航向速度。如果此附加元数据不可用,它将作为空值返回。

位置信息从哪里来?

地理定位 API 没有指定设备必须使用哪种底层技术来定位应用的用户。相反,它只是公开了一个用于检索位置信息的 API。然而,暴露出来的是精确定位的程度。不能保证设备的实际位置会返回准确的位置。

位置,位置

彼得说:“这是一个有趣的例子。在家里,我使用无线网络。我在 Firefox 中打开了本章中显示的地理定位示例应用,它计算出我在萨克拉门托(距离我的实际物理位置大约 75 英里)。错了,但不要太惊讶,因为我的互联网服务提供商位于萨克拉门托市中心。

然后,我让我的儿子 Sean 和 Rocky 在他们的 iPhones 上浏览相同的页面(使用相同的 Wi-Fi 网络)。在 Safari 中,它们看起来像是位于加利福尼亚州的马里斯维尔——一个距离萨克拉门托 30 英里的小镇。真不敢相信"

设备可以使用以下任何来源:

  • 国际电脑互联网地址
  • 坐标三角测量
    • 全球定位系统
    • 带有来自 RFID、Wi-Fi 和蓝牙的 MAC 地址的 Wi-Fi
    • GSM 或 CDMA 手机 id
  • 用户定义的

许多设备使用一个或多个信号源的组合来确保更高的精度。每种方法都有自己的优点和缺点,这将在下一节中解释。

IP 地址地理位置数据

在过去,基于 IP 地址的地理定位是获得可能位置的唯一方法,但返回的位置往往被证明是不可靠的。基于 IP 地址的地理定位的工作原理是自动查找用户的 IP 地址,然后检索注册人的物理地址。因此,如果您的 ISP 为您提供 IP 地址,您的位置通常会被解析为服务提供商的物理地址,而该地址可能在数英里之外。表 5-1 显示了基于 IP 地址的地理定位数据的优缺点。

Image

许多网站基于 IP 地址位置做广告。当你到另一个国家旅行,突然看到当地服务的广告(基于你所访问的国家或地区的 IP 地址)时,你可以看到这一点。

GPS 地理定位数据

只要能看到天空,GPS 就能提供非常精确的定位结果。通过从围绕地球飞行的多个 GPS 卫星获取信号来获取 GPS 定位。然而,修复需要一段时间,这对于必须快速启动的应用来说不是特别好。

因为获取 GPS 定位可能需要很长时间,所以您可能希望异步查询用户的位置。要向应用的用户显示正在获取修复,您可以添加一个状态栏。表 5-2 显示了基于 GPS 的地理定位数据的优缺点。

无线地理定位数据

基于 Wi-Fi 的地理定位信息是通过根据用户与许多已知 Wi-Fi 接入点的距离对位置进行三角测量来获取的,这些接入点大多位于城市地区。与 GPS 不同,Wi-Fi 在室内和市区都非常准确。表 5-3 显示了基于 Wi-Fi 的地理定位数据的利与弊。

Image

手机地理定位数据

基于手机的地理定位信息是通过基于用户与多个手机信号塔的距离对位置进行三角测量而获得的。这种方法提供了相当精确的一般定位结果。这种方法通常与基于 Wi-Fi 和 GPS 的地理定位信息结合使用。表 5-4 显示了基于手机的地理定位数据的利与弊。

用户定义的地理位置数据

您也可以允许用户自己定义他们的位置,而不是通过编程来确定用户的位置。一个应用可能允许用户输入他们的地址、邮政编码或其他一些细节;然后,您的应用可以使用这些信息来提供位置感知服务。表 5-5 显示了用户定义的地理位置数据的优缺点。

Image

支持地理定位的浏览器

地理定位是第一批被完全接受和实现的 HTML5 特性之一,现在它在所有主流浏览器中都可用。有关当前浏览器支持的完整概述,包括移动支持,请参阅[caniuse.com](http://caniuse.com)并搜索地理位置。

如果您必须支持较旧的浏览器,那么在使用 API 之前,最好先看看是否支持地理定位。本章后面的“检查浏览器支持”一节将向您展示如何以编程方式检查浏览器支持。

隐私

地理位置规范要求提供一种机制来保护用户的隐私。此外,除非应用的用户明确许可,否则位置信息不应公开。

这很有意义,并且解决了用户经常提出的关于地理定位应用的“老大哥”问题。然而,正如您从 HTML 5 地理定位应用的一些可能的用例中所看到的,用户通常会有分享这些信息的动机。例如,用户可能会同意分享他们的位置,如果这可以让他们知道一双跑鞋有罕见的 50%折扣,而这双跑鞋就在离他们碰巧喝咖啡的地方几个街区远的商店里。让我们仔细看看图 5-1 所示的浏览器和设备隐私架构。

Image

图 5-1。地理定位浏览器和设备隐私架构

图表中显示了以下步骤:

  1. 用户在浏览器中导航到位置感知应用。
  2. 应用网页通过进行地理定位功能调用从浏览器加载并请求坐标。浏览器拦截这一请求并请求用户许可。让我们假设,在这种情况下,许可被授予。
  3. 浏览器从运行它的设备上检索坐标信息。例如,IP 地址、Wi-Fi 和可能的 GPS 坐标的组合。这是浏览器的内部功能。
  4. 浏览器将这些坐标发送给可信的外部位置服务,后者返回位置坐标,现在可以将这些坐标发送回地理定位应用的主机。

Image 重要应用是否而非直接访问设备;它只能查询浏览器来代表它访问设备。

触发隐私保护机制

当你访问一个使用地理定位 API 的网页时,隐私保护机制就会发挥作用。图 5-2 显示了这在 Firefox 中的样子。

Image

图 5-2。当使用地理定位 API 时,Firefox 中的通知栏被触发。

当地理定位代码被执行时,该机制被触发。简单地添加不在任何地方调用的地理位置代码(例如,在一个onload方法中)不会做任何事情。然而,如果地理位置代码被执行,例如,在对navigator.geolocation.getCurrentPosition的调用中(稍后更详细地解释),用户被提示与应用共享他们的位置。图 5-3 显示了在 iPhone 上运行 Safari 时会发生什么。

Image

图 5-3。使用地理定位 API 时,Safari 中会触发通知对话框。

除了提供必要的机制来请求共享你的位置,一些实现(例如 Firefox)还允许你在下次登录时记住授予该站点的权限。这类似于你在浏览器中记住某些网站的密码。

Image 注意如果你已经允许在 Firefox 中始终向某个网站提供你的位置,但后来又改变了主意,你可以很容易地撤销这个许可,方法是返回该网站,从工具菜单中选择页面信息。然后在权限选项卡上更改共享位置的设置。

处理位置信息

位置数据是敏感信息,因此当您收到它时,必须小心处理、存储和重新传输数据。除非用户授予存储数据的权限,否则您应该始终在需要数据的任务完成后处置数据。

因此,如果您重新传输位置数据,建议您首先加密数据。关于地理位置数据的收集,您的应用应该突出显示以下内容:

  • 你正在收集位置数据
  • 您收集位置数据的原因
  • 位置数据保留多长时间
  • 您如何保护数据
  • 位置数据如何共享以及与谁共享(如果共享)
  • 用户如何检查和更新他们的位置数据

使用地理定位 API

在这一节中,我们将更详细地探索地理定位 API 的使用。为了便于说明,我们创建了一个简单的浏览器页面— geolocation.html。记住,你可以从本书的页面apress.com或配套网站[prohtml5.com](http://prohtml5.com)上下载所有代码。

检查浏览器支持

在调用地理定位 API 函数之前,您需要确保浏览器支持您将要做的事情。这样,您可以提供一些替代文本,提示您的应用的用户放弃他们的恐龙般的浏览器或安装一个插件,如 Gears,它增强了现有的浏览器功能。清单 5-1 显示了一种测试浏览器支持的方法。

清单 5-1。检查浏览器支持

`function loadDemo() {
  if(navigator.geolocation) {
    document.getElementById("support").innerHTML = "Geolocation supported.";

} else {
     document.getElementById("support").innerHTML = "Geolocation is not supported in
                                        your browser.";
    }
}`

在这个例子中,您在loadDemo函数中测试浏览器支持,这个函数可能在应用的页面加载时被调用。对navigator.geolocation(也可以使用 Modernizr)的调用将返回地理位置对象(如果它存在的话),或者如果它不存在就触发失败案例。在这种情况下,通过用合适的消息更新页面上先前定义的support元素,页面被更新以反映是否有浏览器支持。

位置请求

有两种类型的职位请求:

  • 一次性位置请求
  • 重复位置更新
一次性位置请求

在许多应用中,只检索一次用户的位置,或者只根据请求检索,这是可以接受的。例如,如果有人正在寻找最近的电影院,放映今天的热门电影,可以使用清单 5-2 中最简单的地理定位 API。

清单 5-2。一次性位置请求

void getCurrentPosition(in PositionCallback successCallback,                  in optional PositionErrorCallback errorCallback,                  in optional PositionOptions options);

让我们更详细地看看这个核心函数调用。

首先,这是一个在navigator.geolocation对象上可用的函数,所以您需要已经在脚本中检索了这个对象。如前所述,如果您的浏览器不支持地理定位,请确保您有一个好的后备处理程序。

该函数有一个必需的参数和两个可选的参数。

  • successCallback函数参数告诉浏览器,当位置数据可用时,您希望调用哪个函数。这一点很重要,因为提取位置数据等操作可能需要很长时间才能完成。没有用户希望在检索位置时浏览器被锁定,也没有开发人员希望他的程序无限期暂停——特别是因为获取位置数据通常要等待用户的许可。successCallback 是您接收实际位置信息并对其进行操作的地方。
  • 然而,与大多数编程场景一样,最好为失败情况做好计划。对于位置信息的请求很有可能因为超出您控制的原因而无法完成,对于这些情况,您将希望提供一个errorCallback函数,该函数可以向用户提供一个解释,或者尝试再试一次。虽然是可选的,但建议您提供一个。
  • 最后,可以向地理定位服务提供一个options对象来微调它收集数据的方式。这是一个可选参数,我们将在后面进行研究。

假设您在我们的页面上创建了一个名为updateLocation()的 JavaScript 函数,在这个函数中,您用新的位置数据更新页面的内容。类似地,您已经创建了一个handleLocationError()函数来处理错误情况。接下来我们将研究这些函数的细节,但这意味着您访问用户位置的核心请求将如下所示:

navigator.geolocation.getCurrentPosition(updateLocation, handleLocationError);

update location()函数

那么,在我们的updateLocation()通话中会发生什么呢?其实挺简单的。一旦浏览器访问到位置信息,它将调用带有单个参数的updateLocation():一个位置对象。位置将包含坐标(作为属性coords)和收集位置数据时的时间戳。虽然您可能需要也可能不需要时间戳,但是coords属性包含位置的关键值。

坐标上总是有多个属性,但是它们是否有有意义的值取决于浏览器和用户设备的硬件。以下是前三个属性:

  • latitude
  • longitude
  • accuracy

这些属性保证有值,并且是不言自明的。latitudelongitude将包含以十进制度数指定的用户位置的地理定位服务的最佳确定值。accuracy将包含一个以米为单位的值,该值指定纬度和经度值与实际位置的接近程度,置信度为 95%。因此,它可用于显示位置周围的邻近半径,为人们提供关于精确度的视觉线索。由于地理定位实现的性质,近似将是常见和粗略的。在您有把握地呈现返回值之前,请确保检查它们的准确性。推荐用户去一家“附近”的鞋店,而这家鞋店实际上有几个小时的路程,这可能会产生意想不到的后果。

坐标的其他属性不能保证得到支持,但是如果它们不可用,它们将返回一个null值(例如,如果您在台式计算机上,您不太可能访问这些信息):

  • altitude—用户所在位置的高度,单位为米
  • altitudeAccuracy—再次以米为单位,如果没有提供高度,则为null
  • heading—相对于正北的行进方向,单位为度
  • speed——地面速度,单位为米/秒

除非您确定您的用户拥有能够访问此类信息的设备,否则建议您不要依赖它们作为您的应用的关键。虽然全球定位设备可能提供这种级别的细节,但简单的网络三角测量无法提供。

现在让我们来看看我们的updateLocation()函数的代码实现,它用坐标执行一些琐碎的更新(见清单 5-3 )。

清单 5-3。使用 updateLocation()函数的例子

`function updateLocation(position) {
  var latitude = position.coords.latitude;
  var longitude = position.coords.longitude;
  var accuracy = position.coords.accuracy;
  var timestamp = position.timestamp;

document.getElementById("latitude").innerHTML = latitude;
  document.getElementById("longitude").innerHTML = longitude;   document.getElementById(“accuracy”).innerHTML = accuracy
  document.getElementById("timestamp").innerHTML = timestamp;
}`

在这个例子中,updateLocation()回调用于更新页面不同元素中的文本;我们将longitude属性的值放在经度元素中,将latitude属性放在纬度元素中,并将精确度和时间戳放在它们对应的字段中。

handleLocationError()函数

处理错误对于地理定位应用非常重要,因为有许多移动部件,因此位置计算服务有许多出错的可能性。幸运的是,API 为您需要处理的所有情况定义了错误代码,并将它们设置在作为code属性传递给错误处理程序的错误对象上。让我们依次看看它们:

  • PERMISSION_DENIED(错误代码 1)—用户选择不让浏览器访问位置信息。
  • POSITION_UNAVAILABLE(错误代码 2)—尝试了用于确定用户位置的技术,但失败了。
  • TIMEOUT(错误代码 3)—超时值被设置为一个选项,确定位置的尝试超过了该限制。

在这些情况下,您可能希望让用户知道有什么地方出错了。在请求不可用或超时的情况下,您可能希望重试获取值。

清单 5-4 展示了一个错误处理程序的例子。

清单 5-4。使用错误处理器

    function handleLocationError(error) {         switch(error.code){         case 0:           updateStatus("There was an error while retrieving your location: " +                                        error.message);         break;         case 1:         updateStatus("The user prevented this page from retrieving a location.");         break;         case 2:         updateStatus("The browser was unable to determine your location: " +                                      error.message);         break;         case 3:         updateStatus("The browser timed out before retrieving the location.");         break;         }     }

错误代码是从提供的error对象的code属性中访问的,而message属性将提供对错误的更详细描述。在所有情况下,我们调用自己的例程用必要的信息更新页面的状态。

可选地理定位请求属性

处理了正常情况和错误情况后,您应该将注意力转向可以传递给地理定位服务的三个可选属性,以便微调它收集数据的方式。请注意,这三个属性可以使用速记对象符号来传递,这使得将它们添加到地理位置请求调用变得很简单。

  • enableHighAccuracy—This is a hint to the browser that, if available, you would like the Geolocation service to use a higher accuracy-detection mode. This defaults to false, but when turned on, it may not cause any difference, or it may cause the machine to take more time or power to determine location. Use with caution.

    Image 奇怪的是,高精度设定只有一个拨动开关:truefalse。创建 API 不是为了允许将精度设置为各种值或数值范围。也许这将在规范的未来版本中得到解决。

  • timeout—此可选值以毫秒为单位,告知浏览器允许计算当前位置的最大时间。如果计算没有在这段时间内完成,则调用错误处理程序。该值默认为无穷大,即无限制。

  • maximumAge—This value indicates how old a location value can be before the browser must attempt to recalculate. Again, it is a value in milliseconds. This value defaults to zero, meaning that the browser must attempt to recalculate a value immediately.

    Image 注意你可能想知道timeoutmaximumAge选项之间的区别。虽然名字相似,但它们确实有不同的用途。timeout值是指计算位置值所需的持续时间,而maximumAge是指位置计算的频率。如果任何一次计算花费的时间超过timeout值,就会触发错误。但是,如果浏览器没有比maximumAge更早的最新位置值,它必须重新获取另一个值。特殊值在这里适用:将maximumAge设置为“0”要求值总是被重新提取,而将其设置为Infinity意味着它永远不会被重新提取。

地理位置 API 不允许您告诉浏览器重新计算位置的频率。这完全取决于浏览器的实现。我们所能做的就是告诉浏览器maximumAge是它返回的值的什么。实际频率是我们无法控制的细节。

让我们使用简写符号更新我们的位置请求,以包含一个可选参数,如以下示例所示:

navigator.geolocation.getCurrentPosition(updateLocation,handleLocationError,                                          {timeout:10000});

这个新的调用确保任何耗时超过 10 秒(10,000 毫秒)的定位请求都会触发一个错误,在这种情况下,将使用TIMEOUT错误代码调用handleLocationError函数。我们可以将我们到目前为止讨论过的地理定位调用组合起来,并在一个页面上显示相关数据,如图 5-4 所示。

Image

图 5-4。移动设备上显示的地理位置数据

重复位置更新

有时你不得不反复提出职位要求。幸运的是,地理定位 API 的设计者使得从一次性请求用户位置的应用切换到定期请求位置的应用变得很容易。事实上,这很大程度上就像切换请求调用一样简单,如以下示例所示:

  • 一次性更新:navigator.geolocation.**getCurrentPosition**(updateLocation, handleLocationError);
  • 重复更新:navigator.geolocation.**watchPosition**(updateLocation, handleLocationError);

这个简单的改变将导致地理定位服务随着用户位置的改变而重复调用您的updateLocation处理程序,而不是一次。它的作用就好像你的程序正在监视位置,并且会让你知道位置的变化。

你为什么想这么做?

考虑这样一个网页,当浏览者在城市中四处走动时,它会给出一个一个转弯的方向。或者是一个不断更新的页面,在您驾车行驶在高速公路上时向您显示最近的加油站。或者甚至是一个记录并发送你的位置的网页,这样你就可以追溯你的脚步。一旦位置更新在发生变化时流入您的应用,所有这些服务都变得易于构建。

关闭更新也很简单。如果您的应用不再需要接收关于用户位置的定期更新,您只需要调用clearWatch()函数,如下例所示:

navigator.geolocation.clearWatch(watchId);

此功能将通知地理定位服务您不再希望接收用户位置的更新。但是watchID是什么,它是从哪里来的?它实际上是来自watchPosition()调用的返回值。它标识了唯一的监视器请求,以便我们稍后取消它。因此,如果你的应用需要停止接收位置更新,你可以写一些代码,如清单 5-5 所示。

清单 5-5。使用观察位置

`var watchId = navigator.geolocation.watchPosition(updateLocation,
                                                  handleLocationError);
// do something fun with the location updates!

// OK, now we are ready to stop receiving location updates
navigator.geolocation.clearWatch(watchId);`

构建地理定位应用

到目前为止,我们主要关注单次定位请求。让我们通过使用它的 multirequest 特性来构建一个小而有用的应用:一个带有距离跟踪器的网页,来看看地理定位 API 到底有多强大。

如果你曾经想要一个快速的方法来确定你在一定时间内走了多远,你通常会使用一个专用的设备,如 GPS 导航系统或计步器。使用地理定位服务的强大功能,您可以创建一个网页来跟踪您从最初加载页面的位置移动了多远。尽管在台式电脑上用处不大,但这个页面对于今天数百万带有地理定位支持的网络电话来说是理想的。只需将您的智能手机浏览器指向该示例页面,授予该页面访问您的位置的权限,每隔几秒钟它就会更新您刚刚行驶的距离,并将其添加到累计里程中(参见图 5-5 )。

Image

图 5-5。我们的地理定位应用实例

这个示例通过使用我们在上一节中讨论的watchPosition()功能来工作。每次有新的位置发送给我们,我们都会将其与最后一个已知位置进行比较,并计算行进的距离。这是通过一个众所周知的计算方法来实现的,即哈弗辛公式,它允许我们计算球体上两个经度和纬度位置之间的距离。清单 5-6 展示了哈弗辛公式告诉我们的东西。

清单 5-6。哈弗辛公式

Image

如果你希望了解哈弗辛公式是如何工作的,你会非常失望。相反,我们将向您展示该公式的 JavaScript 实现,它允许任何人使用它来计算两个位置之间的距离(参见清单 5-7 )。

清单 5-7。一个 JavaScript 哈弗辛实现

`    Number.prototype.toRadians = function() {
      return this * Math.PI / 180;
    }

function distance(latitude1, longitude1, latitude2, longitude2) {
      // R is the radius of the earth in kilometers
      var R = 6371;

var deltaLatitude = (latitude2-latitude1).toRadians();
      var deltaLongitude = (longitude2-longitude1).toRadians();
      latitude1 = latitude1.toRadians(), latitude2 = latitude2.toRadians();

var a = Math.sin(deltaLatitude/2) *
              Math.sin(deltaLatitude/2) +
              Math.cos(latitude1) *
              Math.cos(latitude2) *
              Math.sin(deltaLongitude/2) *
              Math.sin(deltaLongitude/2);
      var c = 2 * Math.atan2(Math.sqrt(a),
                             Math.sqrt(1-a));
      var d = R * c;
      return d;
    }`

如果你想知道这个公式为什么或如何工作,请查阅青少年的数学教科书。出于我们的目的,我们编写了一个从角度到弧度的转换,并提供了一个distance()函数来计算两个纬度和经度位置值之间的距离。

如果我们检查用户的位置,并以频繁和有规律的时间间隔计算行进的距离,它会给出一个随着时间推移行进的距离的合理近似值。这假设用户在每个时间间隔都在直线运动,但是为了我们的例子,我们将做这样的假设。

编写 HTML 显示

让我们从 HTML 显示开始。在这个练习中,我们保持它非常简单,因为真正感兴趣的是驱动数据的脚本。我们会显示一个包含相关地理位置数据的页面。此外,我们将在适当的位置放置一些状态文本指示器,以便用户可以看到行进距离的摘要(参见清单 5-8 )。

清单 5-8。距离跟踪器 HTML 页面的代码

`

<head>   <meta charset="utf-8" >   <title>Geolocation</title>   <link rel="stylesheet" href="geo-html5.css" > </head>


    

Odometer Demo


    

Live Race Data!


  


    

      

        

Your Location


      

Geolocation is not supported in your browser.


        

Latitude:


        

Longitude:


        

Accuracy:


        

Timestamp:


        

Current distance traveled:


        

Total distance traveled:


      


  


    

Powered by HTML5, and your feet!


  

.
.
.
 

`

这些值目前都是默认的,一旦数据开始流入应用,就会填充这些值。

处理地理位置数据

我们的第一个 JavaScript 代码部分应该看起来很熟悉。我们已经设置了一个处理程序——loadDemo()——它将在页面完成加载后立即执行。该脚本将检测浏览器中是否支持地理定位,并使用状态更新功能来更改页面顶部的状态消息,以指示找到的内容。然后它会请求监视用户的位置,如清单 5-9 中的所示。

清单 5-9。增加 loadDemo()和状态更新功能

`    var totalDistance = 0.0;
    var lastLat;
   var lastLong;

function updateErrorStatus(message) {
      document.getElementById("status").style.background = "papayaWhip";
      document.getElementById("status").innerHTML = "Error: " + message;
    }

function updateStatus(message) {
      document.getElementById("status").style.background = "paleGreen";
      document.getElementById("status").innerHTML = message;
    }

function loadDemo() {  
      if(navigator.geolocation) {
        document.getElementById("status").innerHTML = "HTML5 Geolocation is supported in your browser.";
        navigator.geolocation.watchPosition(updateLocation, handleLocationError,
                                                {timeout:20000});
      }
    }`

请注意,我们在我们的位置监视上设置了一个maximumAge选项:{maximumAge:20000}。这将告诉位置服务,我们不想要任何超过 20 秒(或 20,000 毫秒)的缓存位置值。设置这个选项将使我们的页面定期更新,但是您可以随意调整这个数字,尝试更大或更小的缓存大小。

对于错误处理,我们将使用我们之前确定的相同例程,因为它对于我们的距离跟踪器来说足够通用。在其中,我们将检查收到的任何错误的错误代码,并相应地更新页面上的状态消息,如清单 5-10 所示。

清单 5-10。添加错误处理代码

    function handleLocationError(error) {       switch(error.code)       {       case 0:         updateErrorStatus("There was an error while retrieving your location. Additional details: " +                                                error.message);         break;       case 1:         updateErrorStatus("The user opted not to share his or her location.");         break;       case 2:         updateErrorStatus("The browser was unable to determine your location. Additional details: " +                                                 error.message);         break;       case 3:         updateErrorStatus("The browser timed out before retrieving the location.");         break;       }     }

我们的大部分工作将在我们的updateLocation()函数中完成。这里我们将使用最近的值更新页面,并计算行进的距离,如清单 5-11 所示。

清单 5-11。添加 updateLocation()函数

`    function updateLocation(position) {
      var latitude = position.coords.latitude;
      var longitude = position.coords.longitude;
      var accuracy = position.coords.accuracy;
      var timestamp = position.timestamp;

document.getElementById("latitude").innerHTML = "Latitude: " + latitude;
      document.getElementById("longitude").innerHTML = "Longitude: " +  longitude;
      document.getElementById("accuracy").innerHTML = "Accuracy: " + accuracy + " meters";
      document.getElementById("timestamp").innerHTML = "Timestamp: " + timestamp;`

如您所料,当我们收到一组更新的位置坐标时,我们要做的第一件事就是记录所有信息。我们收集纬度、经度、精确度和时间戳,然后用新数据更新表值。

您可能不会选择在自己的应用中显示时间戳。这里使用的时间戳主要是对计算机有用的形式,对最终用户没有意义。你可以随意用一个更方便用户的时间指示器来代替它,或者干脆把它去掉。

精度值是以米为单位给我们的,乍一看似乎没有必要。但是,任何数据都取决于它的准确性。即使您没有向用户提供精度值,您也应该在自己的代码中考虑它们。显示不准确的值可能会让用户对他或她的位置产生误解。因此,我们将丢弃任何不合理的低精度位置更新,如清单 5-12 所示。

清单 5-12。忽略不准确的精度更新

      // sanity test... don't calculate distance if accuracy       // value too large       if (accuracy >= 30000) {         updateStatus("Need more accurate values to calculate distance.");         return;       }

最简单的旅行方式

Brian 说:“保持位置准确性至关重要。作为一名开发人员,您将无法访问浏览器用来计算位置的方法,但是您可以访问精确度属性。用它!

一个慵懒的下午,我坐在后院的吊床上,通过一个支持地理定位的手机浏览器监控自己的位置。我惊讶地发现,仅仅过了几分钟,据报道我倾斜的身体以不同的速度行进了半公里的距离。尽管这听起来令人兴奋,但它提醒我们,数据只有在来源允许的情况下才是准确的。"

最后,我们将计算行进的距离,假设我们之前已经接收了至少一个准确的位置值。我们将更新旅行距离的总和并显示给用户,我们将存储当前值以备将来比较。为了让我们的界面不那么混乱,对计算值进行舍入或截断是个好主意,如清单 5-13 所示。

清单 5-13。添加距离计算代码

`      // calculate distance
      if ((lastLat != null) && (lastLong != null)) {
        var currentDistance = distance(latitude, longitude, lastLat, lastLong);
        document.getElementById("currDist").innerHTML =
                 "Current distance traveled: " + currentDistance.toFixed(2) + " km";
        totalDistance += currentDistance;
        document.getElementById("totalDist").innerHTML =
                 "Total distance traveled: " + currentDistance.toFixed(2) + " km";
        updateStatus("Location successfully updated.");

}
      lastLat = latitude;
      lastLong = longitude;

}`

就这样。在不到 200 行的 HTML 和脚本中,我们创建了一个示例应用,该应用可以随时监控查看者的位置,并演示了几乎整个地理定位 API,包括错误处理。虽然这个例子在台式电脑上看起来没那么有趣,但在你最喜欢的支持地理定位的手机或设备上试试,看看你在一天中的移动性。

最终代码

完整的代码示例如清单 5-14 所示。

清单 5-14。完成距离追踪器代码

`

<head>   <meta charset="utf-8" >   <title>Geolocation</title>   <link rel="stylesheet" href="geo-html5.css" > </head>


    

Odometer Demo


    

Live Race Data!


  


    

      

        

Your Location


      

Geolocation is not supported in your browser.


        

Latitude:


        

Longitude:


        

Accuracy:


        

Timestamp:


        

Current distance traveled:


        

Total distance traveled:


      


  


    

Powered by HTML5, and your feet!


  

</body> `

实用的临时演员

有时有些技术不适合我们的常规例子,但仍然适用于许多类型的 HTML5 应用。我们在这里向你展示一些简短的、普通的、实用的额外内容。

我的状态如何?

您可能已经注意到,地理定位 API 的很大一部分与时间值有关。这不应该太令人惊讶。众所周知,确定位置的技术——手机三角定位、GPS、IP 查找等——即使能完成,也要花很长时间。幸运的是,API 为开发人员提供了足够的信息来为用户创建合理的状态栏。

如果开发人员在位置查找上设置了可选的timeout值,那么如果查找时间超过了timeout值,她就请求地理定位服务通知她一个错误。这样做的副作用是,当请求正在进行时,在用户界面中向用户显示状态消息是完全合理的。状态的开始从请求发出时开始,状态的结束应该对应于超时值,不管它是以成功还是失败结束。

在清单 5-15 中,我们将启动一个 JavaScript 间隔计时器,用一个新的进度指示器值定期更新状态显示。

清单 5-15。添加状态栏

`function updateStatus(message) {
        document.getElementById("status").innerHTML = message;
    }

function endRequest() {
      updateStatus("Done.");
    }

function updateLocation(position) {
      endRequest();
      // handle the position data
    }

function handleLocationError(error) {
      endRequest();

// handle any errors
    }

navigator.geolocation.getCurrentPosition(updateLocation,
                                             handleLocationError,                                              {timeout:10000});
                                             // 10 second timeout value

updateStatus(“Requesting Geolocation data…”);`

让我们稍微分析一下这个例子。和以前一样,我们有一个函数来更新页面上的状态值,如下面的例子所示。

function updateStatus(message) {   document.getElementById("status").innerHTML = message; }

我们这里的状态将是一个简单的文本显示,尽管这种方法同样适用于更引人注目的图形状态显示(见清单 5-16 )。

清单 5-16。显示状态

`navigator.geolocation.getCurrentPosition(updateLocation,
                                         handleLocationError,
                                         {timeout:10000});
                                         // 10 second timeout value

updateStatus(“Requesting location data…”);`

我们再次使用地理定位 API 来获取用户的当前位置,但是设置了 10 秒的超时。一旦过了十秒钟,由于超时选项,我们应该要么成功,要么失败。

我们立即更新状态文本显示,以表明位置请求正在进行中。然后,一旦请求完成或者过了十秒钟——无论哪一个先发生——就使用回调方法来重置状态文本,如清单 5-17 所示。

清单 5-17。重置状态文本

`    function endRequest() {
      updateStatus("Done.");
    }

function updateLocation(position) {
      endRequest();
      // handle the position data
    }`

一个简单的额外,但易于扩展。

这种技术适用于一次性位置查找,因为开发人员很容易确定位置查找请求何时开始。当然,开发人员一调用getCurrentPosition(),请求就开始了。然而,在通过watchPosition()重复查找位置的情况下,开发者不能控制每个单独的位置请求何时开始。

此外,直到用户准许地理定位服务访问位置数据,超时才开始。由于这个原因,实现精确的状态显示是不切实际的,因为在用户授予权限的瞬间页面不会得到通知。

在谷歌地图上显示给我看

对地理位置数据的一个非常常见的请求是在地图上显示用户的位置,例如流行的 Google Maps 服务。事实上,这是如此受欢迎,以至于谷歌自己在其用户界面中内置了对地理定位的支持。只需按下显示我的位置按钮(参见图 5-6);Google Maps 将使用地理定位 API(如果可用)来确定并在地图上显示您的位置。

Image

图 5-6。谷歌地图的显示我的位置按钮

但是,你自己也有可能做到这一点。尽管 Google Map API 超出了本书的范围,但它(并非巧合)被设计成可以获取十进制的纬度和经度位置。因此,您可以轻松地将位置查找的结果传递给 Google Map API,如清单 5-18 所示。你可以在开始谷歌地图应用,第二版(2010 年出版)中读到更多关于这个主题的内容。

清单 5-18。向谷歌地图 API 传递位置

`//Include the Google maps library

// Create a Google Map… see Google API for more detail
var map = new google.maps.Map(document.getElementById("map"));

function updateLocation(position) {
  //pass the position to the Google Map and center it
  map.setCenter(new google.maps.LatLng(
                               parseFloat(position.coords.latitude),
                               parseFloat(position.coords.longitude));
navigator.geolocation.getCurrentPosition(updateLocation,
                                         handleLocationError);`

总结

本章讨论了地理定位。您了解了地理位置信息(纬度、经度和其他属性)以及它们的来源。您还了解了伴随地理定位而来的隐私问题,并且看到了如何使用地理定位 API 来创建引人注目的位置感知 web 应用。

在下一章,我们将演示 HTML5 如何让你在标签页和窗口之间以及页面和不同域的服务器之间进行通信。

六、使用通信 API

在这一章中,我们将探索如何使用两个重要的实时跨源通信构件:跨文档消息传递XMLHttpRequest Level 2 ,我们将向您展示如何使用它们来创建引人注目的应用。这两个构建块都为 HTML5 应用添加了新的通信选项,并允许来自不同域的应用安全地相互通信。

首先,我们将讨论postMessage API 和 origin 安全概念 HTML5 通信的两个关键元素——然后我们将向您展示如何使用postMessage API 在 iframes、选项卡和窗口之间进行通信。

接下来,我们将讨论 XMLHttpRequest 级别 2——XMLHttpRequest 的改进版本。我们将向您展示 XMLHttpRequest 在哪些方面得到了改进。具体来说,我们将向您展示如何使用 XMLHttpRequest 进行跨源请求,以及如何使用新的进度事件。

跨文档消息传递

直到最近,由于安全考虑,在运行的浏览器中,框架、标签和窗口之间的通信完全受到限制。例如,虽然某些网站从浏览器内部共享信息可能很方便,但这也为恶意攻击打开了方便之门。如果浏览器被授予以编程方式访问加载到其他框架和标签中的内容的能力,网站将能够使用脚本从另一个网站的内容中窃取任何信息。明智的是,浏览器供应商限制了这种访问;试图检索或修改从另一个源加载的内容会引发安全异常并阻止该操作。

然而,在一些合理的情况下,不同网站的内容可以在浏览器内部进行交流。典型的例子是“mashup”,一个不同应用的组合,比如来自不同站点的地图、聊天和新闻,所有这些组合在一起形成一个新的元应用。在这些情况下,一组协调良好的应用将由浏览器内部的直接通信通道提供服务。

为了满足这种需求,浏览器供应商和标准机构同意引入一个新特性:跨文档消息传递。跨文档消息传递支持跨 iframes、选项卡和窗口的安全跨源通信。它将postMessage API 定义为发送消息的标准方式。如下例所示,用postMessage API 发送消息非常简单。

chatFrame.contentWindow.postMessage('Hello, world', 'http://www.example.com/');

要接收消息,只需在页面中添加一个事件处理程序。当消息到达时,您可以检查其来源,并决定是否对该消息进行处理。清单 6-1 显示了一个事件监听器,它将消息传递给一个 messageHandler 函数。

清单 6-1。消息事件的事件监听器

window.addEventListener(“message”, messageHandler, true); function messageHandler(e) {     switch(e.origin) {       case “friend.example.com”:       // process message       processMessage(e.data);       break;     default:       // message origin not recognized       // ignoring message   } }

消息事件是具有dataorigin属性的 DOM 事件。data属性是发送者传递的实际消息,而origin属性是发送者的来源。使用origin属性,接收方很容易忽略来自不可信来源的消息;可以简单地对照允许的来源列表来检查来源。

如图 6-1 中的所示,postMessage API 提供了一种在[chat.example.net](http://chat.example.net)托管的聊天窗口小部件 iframe 和包含[portal.example.com](http://portal.example.com)托管的聊天窗口小部件 iframe 的 HTML 页面(两个不同的来源)之间进行通信的方法。

Image

图 6-1。iframe 和主 HTML 页面之间的邮件通信

在本例中,聊天小部件包含在另一个源的 iframe 中,因此它不能直接访问父窗口。当聊天小部件接收到聊天消息时,它可以使用postMessage向主页面发送消息,这样页面就可以提醒聊天小部件的用户收到了新消息。类似地,页面可以向聊天小部件发送关于用户状态的消息。页面和小部件都可以通过将各自的来源添加到允许来源的白名单中来侦听来自彼此的消息。

图 6-2 显示了使用 postMessage API 的实际例子。这是一个名为 DZSlides 的 HTML5 幻灯片查看器应用,由 Firefox 工程师兼 HTML5 传道者 Paul Rouget ( [paulrouget.com/dzslides](http://paulrouget.com/dzslides))构建。在这个应用中,表示及其容器使用 postMessage API 进行通信。

Image

图 6-2。dz slides 应用中 postMessage API 的实际使用

在引入postMessage之前,iframes 之间的通信有时可以通过直接编写脚本来完成。在一个页面中运行的脚本会试图操作另一个文档。由于安全限制,这可能是不允许的。与直接编程访问不同,postMessage提供了 JavaScript 上下文之间的异步消息传递。如图图 6-3 所示,如果没有postMessage,跨源通信会导致安全错误,由浏览器强制执行以防止跨站脚本攻击。

Image

图 6-3。火狐和 Firebug 早期版本的跨站点脚本错误

postMessage API 可用于同源文档之间的通信,但是当通信可能被浏览器强制执行的同域策略禁止时,它特别有用。然而,也有理由使用postMessage在同源文档之间传递消息,因为它提供了一致的、易于使用的 API。每当 JavaScript 上下文之间有通信时,就会使用postMessage API,比如 HTML5 Web 工作器。

了解原产地安全

HTML5 通过引入来源的概念来澄清和细化域安全性。源是用于在网络上建模信任关系的地址的子集。Origins 由一个方案、一个主机和一个端口组成。例如,[www.example.com](https://www.example.com)处的页面与[www.example.com](http://www.example.com)处的页面具有不同的来源,因为方案不同(httpshttp)。原点值中不考虑路径,所以在[www.example.com/index.html](http://www.example.com/index.html)的页面与在[www.example.com/page2.html](http://www.example.com/page2.html)的页面具有相同的原点,因为只有路径不同。

HTML5 定义了起源的序列化。在字符串形式中,源可以在 API 和协议中引用。这对于使用 XMLHttpRequest 的跨源 HTTP 请求以及 WebSockets 来说是非常重要的。

跨来源通信通过来源识别发送者。这允许接收方忽略来自它不信任或不期望从其接收消息的来源的消息。此外,应用必须通过为消息事件添加事件侦听器来选择接收消息。因此,不存在消息干扰未受怀疑的应用的风险。

postMessage的安全规则确保消息不会被发送到来源不期望的页面。发送消息时,发送方指定接收方的来源。如果发送者调用 postMessage 的窗口没有那个特定的来源(例如,如果用户已经导航到另一个站点),浏览器将不会传输那个消息。

同样,在接收消息时,发送者的来源也包含在消息中。消息的来源是由浏览器提供的,不能被欺骗。这允许接收方决定处理哪些消息,忽略哪些消息。您可以保留一个白名单,只处理来自来源可信的文档的邮件。

小心外部输入

Frank 说:“处理跨来源消息的应用应该总是验证每条消息的来源。此外,应该谨慎对待消息数据。即使一条消息来自一个可信的来源,它也应该像其他任何外部输入一样被小心对待。下面两个例子展示了一种注入内容的方法,这种方法可能会带来麻烦,同时也是一种更安全的替代方法。

`// Dangerous: e.data is evaluated as markup!
element.innerHTML = e.data;

// Better
element.textContent = e.data;`

作为最佳实践,从不评估来自第三方的字符串。此外,避免对来自您自己的应用的字符串使用eval。相反,您可以在 window 中使用 JSON。JSON 或 json.org 解析器。JSON 是一种数据语言,旨在供 JavaScript 安全使用,而 json.org 解析器被设计成偏执型的。"

浏览器支持跨文档信息传递

所有主流浏览器,包括 Internet Explorer 8 和更高版本,都支持 postMessage API。在使用 HTML5 跨文档消息传递之前,最好先测试一下它是否受支持。本章后面的“检查浏览器支持”一节将向您展示如何以编程方式检查浏览器支持

使用 postMessage API

在这一节中,我们将更详细地探索 HTML5 postMessage API 的使用。

检查浏览器支持

在调用postMessage之前,最好检查一下浏览器是否支持它。以下示例显示了检查postMessage支持的一种方法:

if (typeof window.postMessage === “undefined”) {     // postMessage not supported in this browser }

发送消息

要发送消息,调用目标窗口对象上的postMessage,如下例所示:

window.postMessage(“Hello, world”, “portal.example.com”);

第一个参数包含要发送的数据,第二个参数包含预期的目标。要向 iframe 发送消息,可以在 iframe 的 contentWindow 上调用postMessage,如下例所示:

document.getElementsByTagName(“iframe”)[0].contentWindow.postMessage(“Hello, world”, “chat.example.net”);

监听消息事件

脚本通过监听窗口对象上的事件来接收消息,如清单 6-2 中的所示。在事件监听器函数中,接收应用可以决定接受或忽略消息。

清单 6-2。监听消息事件并将来源与白名单进行比较

`var originWhiteList = [“portal.example.com”, “games.example.com”, “www.example.com”];

function checkWhiteList(origin) {
    for (var i=0; i<originWhiteList.length; i++) {
        if (origin === originWhiteList[i]) {
          return true;
        }
    }
    return false;
}

function messageHandler(e) {
    if(checkWhiteList(e.origin)) {
        processMessage(e.data);
    } else {
        // ignore messages from unrecognized origins
    }
}
window.addEventListener(“message”, messageHandler, true);`

`images 注意html 5 定义的 MessageEvent 接口也是 HTML5 WebSockets 和 HTML5 Web 工作器 的一部分。HTML5 的通信特性有一致的接收消息的 API。其他通信 API,如 EventSource API 和 Web 工作器,也使用 MessageEvent 来传递消息。

使用 postMessage API 构建应用

假设您想要构建前面提到的带有跨来源聊天小部件的门户应用。您可以使用跨文档消息在门户页面和聊天小部件之间进行通信,如图图 6-4 所示。

Image

图 6-4。带有跨来源聊天工具 iframe 的门户页面

在这个例子中,我们展示了门户如何在 iframes 中嵌入来自第三方的小部件。我们的例子展示了来自 chat.example.net 的一个小部件。然后,门户页面和小部件使用postMessage进行通信。在这种情况下,iframe 表示一个聊天小部件,它希望通过闪烁标题文本来通知用户。这是在后台接收事件的应用中常见的 UI 技术。但是,因为小部件被隔离在 iframe 中,而 iframe 的来源不同于父页面,所以更改标题会违反安全性。相反,小部件使用postMessage请求父页面代表它执行通知。

示例门户还向 iframe 发送消息,通知小部件用户已经更改了他或她的状态。以这种方式使用postMessage允许这样的门户与组合应用中的小部件协调。当然,因为在发送消息时检查目标来源,在接收消息时检查事件来源,所以不存在数据意外泄露或被欺骗的可能性。

`images 注意在这个示例应用中,聊天小部件没有连接到实时聊天系统,通知是由应用的用户点击发送通知来驱动的。一个有效的聊天应用可以使用 Web 套接字,如第七章中所述。

为了便于说明,我们创建了几个简单的 HTML 页面:postMessagePortal.htmlpostMessageWidget.html.下面的步骤强调了构建门户页面和聊天小部件页面的重要部分。以下示例的示例代码位于code/communication文件夹中。

构建门户页面

首先,添加位于不同原点的聊天小部件 iframe:

<iframe id="widget" src="http://chat.example.net:9999/postMessageWidget.html"></iframe>

接下来,添加一个事件监听器 messageHandler 来监听来自聊天小部件的消息事件。如下面的示例代码所示,小部件将要求门户通知用户,这可以通过闪烁标题来完成。为了确保消息来自聊天小部件,消息的来源被验证;如果它不是来自于[chat.example.net:9999](http://chat.example.net:9999),门户页面就会忽略它。

`var trustedOrigin = "http://chat.example.net:9999";

function messageHandler(e) {
    if (e.origin == trustedOrigin) {
        notify(e.data);
    } else {
        // ignore messages from other origins
    }
}`

接下来,添加一个与聊天小部件通信的函数。它使用postMessage向门户页面中包含的小部件 iframe 发送状态更新。在实时聊天应用中,它可以用来传达用户的状态(在线、离开等)。

function sendString(s) {     document.getElementById("widget").contentWindow.postMessage(s, targetOrigin); }

构建聊天小部件页面

首先,添加一个事件监听器 messageHandler 来监听来自门户页面的消息事件。如下面的示例代码所示,聊天小部件监听传入的状态更改消息。为了确保消息来自门户页面,验证消息的来源;如果它不是来自[portal.example.com:9999](http://portal.example.com:9999),小工具就简单地忽略它。:

var trustedOrigin = "http://portal.example.com:9999"; function messageHandler(e) {     if (e.origin === trustedOrigin {         document.getElementById("status").textContent = e.data;     } else {         // ignore messages from other origins     }
}

接下来,添加一个与门户页面通信的函数。小部件将要求门户代表它通知用户,并在收到新的聊天消息时使用postMessage向门户页面发送消息,如下例所示:

function sendString(s) {     window.top.postMessage(s, trustedOrigin); }

最终代码

清单 6-3 显示了门户页面 postMessagePortal.html 的完整代码。

清单 6-3。【postMessagePortal.html 内容

`

<title>Portal [http://portal.example.com:9999]</title> <link rel="stylesheet" href="styles.css"> <style>     iframe {         height: 400px;         width: 800px;     } </style> <link rel="icon" href="http://apress.com/favicon.ico"> <script>

var defaultTitle = "Portal [http://portal.example.com:9999]";
var notificationTimer = null;

var trustedOrigin = "http://chat.example.net:9999";

function messageHandler(e) {
    if (e.origin == trustedOrigin) {
        notify(e.data);
    } else {
        // ignore messages from other origins
    }
}

function sendString(s) {
    document.getElementById("widget").contentWindow.postMessage(s, trustedOrigin);
}

function notify(message) {
    stopBlinking();
    blinkTitle(message, defaultTitle);
}`

`function stopBlinking() {
    if (notificationTimer !== null) {
        clearTimeout(notificationTimer);
    }
    document.title = defaultTitle;
}

function blinkTitle(m1, m2) {
    document.title = m1;
    notificationTimer = setTimeout(blinkTitle, 1000, m2, m1)
}

function sendStatus() {
var statusText = document.getElementById("statusText").value;
            sendString(statusText);
}

function loadDemo() {
    document.getElementById("sendButton").addEventListener("click", sendStatus, true);
    document.getElementById("stopButton").addEventListener("click", stopBlinking, true);
    sendStatus();
}
window.addEventListener("load", loadDemo, true);
window.addEventListener("message", messageHandler, true);

Cross-Origin Portal

Origin: http://portal.example.com:9999

Status

This uses postMessage to send a status update to the widget iframe contained in the portal page.

    

`

清单 6-4 显示了门户页面 postMessageWidget.html 的代码。

清单 6-4。【postMessageWidget.html 内容

`

<title>widget</title> <link rel="stylesheet" href="styles.css"> <script>

var trustedOrigin = "http://portal.example.com:9999";`

`function messageHandler(e) {
    if (e.origin === "http://portal.example.com:9999") {
        document.getElementById("status").textContent = e.data;
    } else {
        // ignore messages from other origins
    }
}

function sendString(s) {
    window.top.postMessage(s, trustedOrigin);
}

function loadDemo() {
    document.getElementById("actionButton").addEventListener("click",
        function() {
            var messageText = document.getElementById("messageText").value;
            sendString(messageText);
        }, true);

}
window.addEventListener("load", loadDemo, true);
window.addEventListener("message", messageHandler, true);

Widget iframe

Origin: http://chat.example.net:9999

Status set to: by containing portal.

         

This will ask the portal to notify the user. The portal does this by flashing the title. If the message comes from an origin other than http://chat.example.net:9999, the portal page will ignore it.

`
实际应用

要查看这个例子,有两个先决条件:页面必须由 web 服务器提供,页面必须由两个不同的域提供。如果您可以访问不同域上的多个 web 服务器(例如,两个 Apache HTTP 服务器),那么您可以在这些服务器上托管示例文件并运行演示。在本地机器上完成这项工作的另一种方法是使用 Python SimpleHTTPServer,如下面的步骤所示。

  1. Update the path to the Windows hosts file (C:\Windows\system32\drivers\etc\hosts) and the Linux version (/etc/hosts) by adding two entries pointing to your localhost (IP address 127.0.0.1), as shown in the following example: 127.0.0.1 chat.example.net 127.0.0.1 portal.example.com

    `images 注意修改主机文件后,您必须重启浏览器,以确保 DNS 条目生效。

  2. 安装 Python 2,它包括轻量级的 SimpleHTTPServer web 服务器。

  3. 导航到包含两个示例文件(postMessageParent.html and postMessageWidget.html)的目录。

  4. 启动 Python 如下:python -m SimpleHTTPServer 9999

  5. 打开浏览器并导航至[portal.example.com:9999/postMessagePortal.html](http://portal.example.com:9999/postMessagePortal.html)。你现在应该看到图 6-4 中的页面。

XMLHttpRequest 级别 2

XMLHttpRequest 是使 Ajax 成为可能的 API。有很多关于 XMLHttpRequest 和 Ajax 的书。你可以在 John Resig 的Pro JavaScript Techniques(a press,2006)中阅读更多关于 XMLHttpRequest 编程的内容。

XMLHttpRequest Level 2—XMLHttpRequest 的新版本—得到了显著增强。在本章中,我们将介绍 XMLHttpRequest Level 2 中引入的改进。这些改进集中在以下几个方面:

  • 跨源 XMLHttpRequests
  • 进度事件
  • 二进制数据

跨源 XMLHttpRequest

在过去,XMLHttpRequest 仅限于同源通信。XMLHttpRequest Level 2 允许使用跨来源资源共享(CORS)的跨来源 XMLHttpRequest,它使用了前面的跨文档消息传递部分中讨论的来源概念。

跨源 HTTP 请求有一个源头。这个头向服务器提供请求的来源。该标头受浏览器保护,不能从应用代码中更改。本质上,它是跨文档消息传递中使用的消息事件上的 origin 属性的网络等价物。origin 标头不同于旧的 referer [ 原文为 ]标头,因为 referer 是包含路径的完整 URL。因为路径可能包含敏感信息,所以试图保护用户隐私的浏览器有时不会发送 referer。但是,在必要的时候,浏览器总是会发送所需的Origin头。

使用跨源 XMLHttpRequest,您可以构建使用不同源上托管的服务的 web 应用。例如,如果您想要托管一个 web 应用,该应用使用来自一个来源的静态内容和来自另一个来源的 Ajax 服务,那么您可以使用跨来源 XMLHttpRequest 在两者之间进行通信。如果没有跨源 XMLHttpRequest,您将被限制在同源通信中。这将限制您的部署选项。例如,您可能必须在单个域上部署 web 应用,或者设置一个子域。

如图 6-5 所示,跨起源 XMLHttpRequest 允许你在客户端聚合来自不同起源的内容。此外,如果目标服务器允许,您可以使用用户的凭据访问受保护的内容,从而为用户提供对个性化数据的直接访问。另一方面,服务器端聚合迫使所有内容通过单一的服务器端基础设施,这可能会造成瓶颈。

Image

图 6-5。客户端和服务器端聚合的区别

CORS 规范规定,对于敏感操作(例如,具有凭证的请求,或者除 GET 或 POST 之外的请求),浏览器必须向服务器发送选项预检请求,以查看是否支持和允许该操作。这意味着成功的通信可能需要支持 CORS 的服务器。清单 6-5 和 6-6 显示了在[www.example.com](http://www.example.com)上托管的页面和[www.example.net](http://www.example.net)上托管的服务之间的跨源交换中涉及的 HTTP 头。

清单 6-5。请求报头示例

POST /main HTTP/1.1 Host: www.example.net
User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.3) Gecko/20090910 Ubuntu/9.04 (jaunty) Shiretoko/3.5.3 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 300 Connection: keep-alive Referer: http://www.example.com/ Origin: http://www.example.com Pragma: no-cache Cache-Control: no-cache Content-Length: 0

清单 6-6。示例响应标题

HTTP/1.1 201 Created Transfer-Encoding: chunked Server: Kaazing Gateway Date: Mon, 02 Nov 2009 06:55:08 GMT Content-Type: text/plain Access-Control-Allow-Origin: http://www.example.com Access-Control-Allow-Credentials: true

进展事件

XMLHttpRequest 中最重要的 API 改进之一是与渐进式响应相关的变化。在以前版本的 XMLHttpRequest 中,只有一个 readystatechange 事件。最重要的是,它在不同浏览器上的实现不一致。例如,readyState 3(进度)在 Internet Explorer 中从不触发。此外,readyState变更事件缺少一种交流上传进度的方式。实现一个上传进度条不是一个简单的任务,需要服务器端的参与。

XMLHttpRequest 级别 2 引入了具有有意义名称的进度事件。表 6-2 显示了新的进度事件名称。您可以通过为事件处理程序属性设置回调函数来侦听这些事件。例如,当 loadstart 事件激发时,将调用 onloadstart 属性的回调。

`images

`images

为了向后兼容,将保留旧的readyState属性和readystatechange事件。

“看似任意”的时代

在 XMLHttpRequest Level 2 规范对readystatechange事件的描述中(为了向后兼容而维护),由于历史原因,readyState属性被描述为在看似任意的时间发生变化。

浏览器支持 HTML5 XMLHttpRequest Level 2

在撰写本文时,许多浏览器已经支持 HTML5 XMLHttpRequest。由于支持的级别不同,在使用这些元素之前,最好先测试一下是否支持 HTML5 XMLHttpRequest。本章后面的“检查浏览器支持”一节将向您展示如何以编程方式检查浏览器支持。

使用 XMLHttpRequest API

在这一节中,我们将更详细地探索 XMLHttpRequest 的使用。为了便于说明,我们创建了一个简单的 HTML page—crossOriginUpload.html。以下示例的示例代码位于code/communication文件夹中。

检查浏览器支持

在尝试使用 XMLHttpRequest 第 2 级功能(比如跨源支持)之前,最好检查一下它是否受支持。你可以通过检查新的withCredentials属性在 XMLHttpRequest 对象上是否可用来做到这一点,如清单 6-7 中的所示。

清单 6-7。检查 XMLHttpRequest 中的跨源支持是否可用

var xhr = new XMLHttpRequest() if (typeof xhr.withCredentials === undefined) {     document.getElementById("support").innerHTML =         "Your browser <strong>does not</strong> support cross-origin XMLHttpRequest"; } else {     document.getElementById("support").innerHTML =         "Your browser <strong>does</strong>support cross-origin XMLHttpRequest"; }

跨产地请求

要创建跨源 XMLHttpRequest,必须首先创建一个新的 XMLHttpRequest 对象,如下例所示。:

var crossOriginRequest = new XMLHttpRequest()

接下来,通过在不同的起点上指定地址来进行跨起点 XMLHttpRequest,如下例所示。

crossOriginRequest.open("GET", "http://www.example.net/stockfeed", true);

确保你听到了错误。这个请求可能不成功的原因有很多。例如,网络故障、拒绝访问以及目标服务器上缺乏 CORS 支持。

为什么不是 JSONP?

Frank 说:“从另一个数据源获取数据的一种常用方法是 JSONP(带填充的 JSON)。JSONP 包括用 JSON 资源的 URL 创建一个脚本标记。URL 有一个查询参数,包含脚本加载时要调用的函数的名称。远程服务器通过调用命名函数来包装 JSON 数据。这有严重的安全隐患!当您使用 JSONP 时,您必须完全信任提供数据的服务。恶意脚本可能会接管您的应用。

使用 XMLHttpRequest (XHR)和 CORS,您接收的是数据而不是代码,您可以安全地解析这些数据。这比评估外部输入要安全得多。"

使用进度事件

XMLHttpRequest Level 2 提供了命名的进度事件,而不是用数字状态来表示请求和响应的不同阶段。您可以通过为事件处理程序属性设置回调函数来侦听这些事件。

清单 6-8 展示了回调函数是如何处理进度事件的。进度事件包含要传输的总数据量、已经传输的数据量以及一个布尔值,该值指示总数据量是否已知(在流式 HTTP 中可能不是这样)。XMLHttpRequest.upload 调度具有相同字段的事件。

清单 6-8。使用 onprogress 事件

`crossOriginRequest.onprogress = function(e) {
    var total = e.total;
    var loaded = e.loaded;

if (e.lengthComputable) {
        // do something with the progress information
    }
}
crossOriginRequest.upload.onprogress = function(e) {
    var total = e.total;     var loaded = e.loaded;

if (e.lengthComputable) {
        // do something with the progress information
    }
}`

二进制数据

支持新的二进制 API 如 Typed Array(这是 WebGL 和可编程音频所必需的)的浏览器可能能够用 XMLHttpRequest 发送二进制数据。XMLHttpRequest Level 2 规范支持使用 Blob 和 ArrayBuffer(也称为类型化数组)对象调用send()方法(参见清单 6-9 )。

清单 6-9。发送类型化的字节数组

var a = new Uint8Array([8,6,7,5,3,0,9]); var xhr = new XMLHttpRequest(); xhr.open("POST", "/data/", true) console.log(a) xhr.send(a.buffer);

这会产生一个带有二进制内容体的 HTTP POST 请求。内容长度为 7,正文包含字节 8,6,7,5,3,0,9。

XMLHttpRequest 级别 2 还公开二进制响应数据。将responseType属性设置为" text "、" document "、" arraybuffer "或" blob "控制由response属性返回的对象的类型。要查看 HTTP 响应主体包含的原始字节,请将responseType设置为“arraybuffer”或“blob”

在下一章,我们将看到如何使用 WebSocket 来发送和接收使用相同类型的二进制数据。

使用 XMLHttpRequest 构建应用

在这个例子中,我们将看看如何将比赛地理位置坐标上传到一个位于不同原点的 web 服务器上。我们使用新的进度事件来监控 HTTP 请求的状态,包括上传百分比。图 6-6 显示了实际应用。

Image

图 6-6。上传地理定位数据的网络应用

为了便于说明,我们已经创建了 HTML 文件crossOrignUpload.html.,以下步骤强调了构建跨原点上传页面的重要部分,如图图 6-5 所示。以下示例的示例代码位于code/communication文件夹中。

首先,创建一个新的XMLHttpRequest对象,如下例所示。

var xhr = new XMLHttpRequest();

接下来,检查浏览器是否支持跨源 XMLHttpRequest,如下例所示。

if (typeof xhr.withCredentials === undefined) {   document.getElementById("support").innerHTML =            "Your browser <strong>doesnot</strong> support cross-origin XMLHttpRequest"; } else {     document.getElementById("support").innerHTML =              "Your browser <strong>does</strong> support cross-origin XMLHttpRequest"; }

接下来,设置回调函数来处理进度事件,并计算上传和下载的比率。

`xhr.upload.onprogress = function(e) {
  var ratio = e.loaded / e.total;
  setProgress(ratio + "% uploaded");
}

xhr.onprogress = function(e) {
  var ratio = e.loaded / e.total;
  setProgress(ratio + "% downloaded");
}

xhr.onload = function(e) {
  setProgress("finished");
}`

xhr.onerror = function(e) {   setProgress("error"); }

最后,打开请求并发送包含编码的地理位置数据的字符串。这将是一个跨源请求,因为目标位置是一个与页面具有不同源的 URL。

`var targetLocation = "http://geodata.example.net:9999/upload";
xhr.open("POST", targetLocation, true);

geoDataString = dataElement.textContent;
xhr.send(geoDataString);`

最终代码

清单 6-10 显示了完整的应用代码——crossOriginUpload.html 文件的内容。

清单 6-10。【crossOriginUpload.html 内容

`

<title>Upload Geolocation Data</title> <link rel="stylesheet" href="styles.css"> <link rel="icon" href="http://apress.com/favicon.ico"> <script>

function loadDemo() {
    var dataElement = document.getElementById("geodata");
    dataElement.textContent = JSON.stringify(geoData).replace(",", ", ", "g");

var xhr = new XMLHttpRequest()
    if (typeof xhr.withCredentials === undefined) {
        document.getElementById("support").innerHTML =
            "Your browser does not support cross-origin XMLHttpRequest";
    } else {
        document.getElementById("support").innerHTML =
            "Your browser does support cross-origin XMLHttpRequest";
    }

var targetLocation = "http://geodata.example.net:9999/upload";

function setProgress(s) {
        document.getElementById("progress").innerHTML = s;
    }

document.getElementById("sendButton").addEventListener("click",
        function() {
            xhr.upload.onprogress = function(e) {
                var ratio = e.loaded / e.total;
                setProgress(ratio + "% uploaded");
            }`

`            xhr.onprogress = function(e) {
              var ratio = e.loaded / e.total;
              setProgress(ratio + "% downloaded");
            }

xhr.onload = function(e) {
                setProgress("finished");
            }

xhr.onerror = function(e) {
                setProgress("error");
            }

xhr.open("POST", targetLocation, true);

geoDataString = dataElement.textContent;
            xhr.send(geoDataString);
        }, true);

}
window.addEventListener("load", loadDemo, true);

XMLHttpRequest Level 2

Geolocation Data to upload:

    Status: ready

`
实际应用

要查看这个示例的运行情况,有两个先决条件:页面必须由不同的域提供,目标页面必须由理解 CORS 标头的 web 服务器提供。本章的示例代码中包含了一个符合 CORS 标准的 Python 脚本,它可以处理传入的跨源 XMLHttpRequests。您可以通过执行以下步骤在本地计算机上运行演示:

  1. Update your hosts file (C:\Windows\system32\drivers\etc\hosts on Windows or /etc/hosts on Unix/Linux) by adding two entries pointing to your localhost (IP address 127.0.0.1) as shown in the following example: 127.0.0.1 geodata.example.net 127.0.0.1 portal.example.com

    `images 注意修改主机文件后,您必须重启浏览器,以确保 DNS 条目生效。

  2. 安装 Python 2,它包括轻量级的SimpleHTTPServer web 服务器,如果您在前面的例子中没有这样做的话。

  3. 导航到包含示例文件(crossOrignUpload.html)和 Python CORS 服务器脚本(CORSServer.py)的目录。

  4. 在这个目录下启动 Python,如下:python CORSServer.py 9999

  5. 打开浏览器并导航至[portal.example.com:9999/crossOriginUpload.html](http://portal.example.com:9999/crossOriginUpload.html)。你现在应该看到如图 6-6 所示的页面。

实用的临时演员

有时有些技术不适合我们的常规例子,但仍然适用于许多类型的 HTML5 应用。我们在这里向你展示一些简短但常见的实用附加功能。

结构化数据

早期版本的postMessage只支持字符串。后来的版本允许其他类型的数据,包括 JavaScript 对象、画布图像数据和文件。随着规范的发展,对不同对象类型的支持将因浏览器而异。

在某些浏览器中,可以用postMessage发送的 JavaScript 对象的限制与 JSON 数据的限制相同。特别是,可能不允许有循环的数据结构。包含自身的列表就是一个例子。

框架破坏

框架破坏是一种确保你的内容不被加载到 iframe 中的技术。一个应用可以检测到它的窗口不是最外面的窗口(window.top),然后跳出它的包含框架,如下例所示。:

if (window !== window.top) {     window.top.location = location; }

支持 X-Frame-Options HTTP 头的浏览器还将防止对将该头设置为 DENY 或 SAMEORIGIN 的资源的恶意成帧。但是,您可能希望选择性地允许某些合作伙伴页面来框定您的内容。一个解决方案是使用postMessage在协作的 iframes 和包含页面之间握手,如清单 6-11 中的所示。

清单 6-11。在 iframe 中使用 postMessage 与可信伙伴握手页面

`var framebustTimer;
var timeout = 3000; // 3 second framebust timeout

if (window !== window.top) {
    framebustTimer = setTimeout(
        function() {
             window.top.location = location;
        }, timeout);
}

window.addEventListener(“message”, function(e) {
    switch(e.origin) {
        case trustedFramer:
            clearTimeout(framebustTimer);
            break;
    }
), true);`

总结

在本章中,您已经看到了如何使用 HTML5 跨文档消息传递和 XMLHttpRequest Level 2 来创建能够跨来源安全通信的引人注目的应用。

首先,我们讨论了postMessage和 origin 安全概念 HTML5 通信的两个关键元素——然后我们向您展示了如何使用postMessage API 在 iframes、选项卡和窗口之间进行通信。

接下来,我们讨论了 XMLHttpRequest 级别 2——XMLHttpRequest 的改进版本。我们向您展示了 XMLHttpRequest 在哪些方面得到了改进;最重要的是在 readystatechange 事件区域。然后,我们向您展示了如何使用 XMLHttpRequest 进行跨源请求,以及如何使用新的进度事件。

最后,我们用几个实际例子结束了这一章。在下一章中,我们将演示 HTML5 WebSockets 如何让您以难以置信的简单性和最小的开销将实时数据传输到应用。

七、使用 WebSocket API

在这一章中,我们将探索 HTML5 规范中最强大的通信功能可以做什么: WebSocket ,它定义了一个全双工通信通道,通过 web 上的单个套接字进行操作。WebSocket 不仅仅是传统 HTTP 通信的另一个增量增强;它代表了一个巨大的进步,特别是对于实时的、事件驱动的 web 应用。

WebSocket 对用于模拟浏览器中全双工连接的古老、复杂的“黑客”进行了改进,这促使谷歌的伊恩·希克森(HTML5 规范负责人)说:

“将数千字节的数据减少到 2 字节……并将延迟从 150 毫秒减少到 50 毫秒远不止是微不足道。事实上,仅这两个因素就足以让 WebSocket 引起谷歌的严重兴趣。”

[www.ietf.org/mail-archive/web/hybi/current/msg00784.html](http://www.ietf.org/mail-archive/web/hybi/current/msg00784.html)

我们将向您详细展示为什么 WebSocket 提供了如此巨大的改进,并且您将看到 WebSocket 如何—一举—使所有旧的 Comet 和 Ajax 轮询、长轮询和流解决方案变得过时。

web socket 概述

让我们通过将 HTTP 解决方案与使用 WebSocket 的全双工“实时”浏览器通信进行比较,来看看 WebSocket 如何减少不必要的网络流量和延迟。

实时和 HTTP

通常,当浏览器访问一个网页时,一个 HTTP 请求被发送到承载该网页的 web 服务器。web 服务器确认该请求并发回响应。在许多情况下,例如,对于股票价格、新闻报道、门票销售、交通模式、医疗设备读数等等,浏览器呈现页面时,响应可能已经过时。如果您想获得最新的实时信息,您可以不断地手动刷新该页面,但这显然不是一个很好的解决方案。

当前提供实时 web 应用的尝试主要围绕轮询和其他服务器端推送技术,其中最著名的是“Comet ”,它延迟 HTTP 响应的完成以向客户端传递消息。

通过轮询,浏览器定期发送 HTTP 请求,并立即收到响应。这项技术是浏览器传递实时信息的首次尝试。显然,如果知道消息传递的确切时间间隔,这是一个好的解决方案,因为您可以将客户端请求同步为仅在服务器上有信息时发生。然而,实时数据通常不可预测,这使得不必要的请求不可避免,因此,在低消息速率的情况下,许多连接被不必要地打开和关闭。

对于长轮询,浏览器向服务器发送一个请求,服务器在一段设定的时间内保持请求打开。如果在此期间收到通知,则包含该消息的响应将被发送到客户端。如果在设定的时间段内没有收到通知,服务器将发送响应以终止打开请求。但是,理解这一点很重要,当您的消息量很大时,长轮询与传统轮询相比不会提供任何实质性的性能改进。

使用流式传输,浏览器发送一个完整的请求,但是服务器发送并维护一个打开的响应,该响应不断更新并无限期地(或在一段设定的时间内)保持打开。然后,每当消息准备好发送时,响应就会更新,但是服务器永远不会发出完成响应的信号,从而保持连接打开以传递未来的消息。但是,由于流仍然封装在 HTTP 中,中间的防火墙和代理服务器可能会选择缓冲响应,从而增加了消息传递的延迟。因此,在检测到缓冲代理服务器的情况下,许多流解决方案会退回到长轮询。或者,可以使用 TLS (SSL)连接来保护响应不被缓冲,但是在这种情况下,每个连接的建立和断开会加重可用服务器资源的负担。

最终,所有这些提供实时数据的方法都涉及 HTTP 请求和响应头,它们包含大量额外的、不必要的头数据,并引入了延迟。最重要的是,全双工连接不仅仅需要从服务器到客户端的下行连接。为了在半双工 HTTP 上模拟全双工通信,今天的许多解决方案使用两个连接:一个用于下游,一个用于上游。这两个连接的维护和协调在资源消耗方面引入了大量开销,并增加了许多复杂性。简单地说,HTTP 不是为实时、全双工通信而设计的,正如您在图 7-1 中看到的,该图显示了构建一个 web 应用的复杂性,该应用使用半双工 HTTP 上的发布/订阅模型显示来自后端数据源的实时数据。

Image

图 7-1 。实时 HTTP 应用的复杂性

当您尝试横向扩展这些解决方案时,情况会变得更糟。模拟 HTTP 上的双向浏览器通信容易出错且复杂,并且所有这些复杂性都无法扩展。即使您的最终用户可能喜欢看起来像实时 web 应用的东西,这种“实时”体验也有很高的价格。这是额外的延迟、不必要的网络流量和 CPU 性能下降的代价。

了解 WebSocket

伊恩·希克森(HTML5 规范的主要作者)首先在 HTML5 规范的通信部分将 WebSocket 定义为“TCP 连接”。该规范发展并更改为 WebSocket,它现在是一个独立的规范(就像地理定位、Web 工作器 等等),以保持讨论的重点。

TCPConnection 和 WebSocket 都是指较低级别的网络接口的名称。TCP 是互联网的基本传输协议。WebSocket 是 web 应用的传输协议。它提供按顺序到达的双向数据流,很像 TCP。与 TCP 一样,更高级别的协议可以在 WebSocket 上运行。作为 Web 的一部分,WebSocket 连接到 URL,而不是连接到互联网主机和端口。

WEBSOCKET 和模型火车有什么共同点?

彼得说:“伊恩·希克森是模型火车的狂热爱好者;自 1984 年马克林首次推出数字控制器以来,他就一直在计划用电脑控制火车的方法,这远远早于网络的存在。

当时,Ian 将 TCPConnection 添加到 HTML5 规范中,他正在编写一个程序,以便从浏览器控制模型火车组,并且他正在使用 WebSocket 出现之前流行的“hanging GET”和 XHR 技术来实现浏览器到火车的通信。如果有一种方法可以在浏览器中进行套接字通信,那么列车控制器程序的构建就会容易得多——很像在“胖”客户端中发现的传统的异步客户端/服务器通信模型。因此,受可能的启发,(火车)轮子已经启动,网络插座火车已经离开车站。下一站:实时网络。"

web socket 握手

为了建立 WebSocket 连接,客户端和服务器在初始握手期间从 HTTP 协议升级到 WebSocket 协议,如图图 7-2 所示。请注意,此连接描述代表协议草案 17。

Image

图 7-2。web socket 升级握手

清单 7-1。web socket 升级握手

`From client to server:
GET /chat HTTP/1.1
Host: example.com
Connection: Upgrade
Sec-WebSocket-Protocol: sample
Upgrade: websocket
Sec-WebSocket-Key: 7cxQRnWs91xJW9T0QLSuVQ==
Origin: http://example.com

[8-byte security key]

From server to client:

HTTP/1.1 101 WebSocket Protocol Handshake Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 7cxQRnWs91xJW9T0QLSuVQ==
WebSocket-Protocol: sample`

一旦建立,WebSocket 消息就可以以全双工模式在客户机和服务器之间来回发送。这意味着基于文本的消息可以同时双向全双工发送。在网络上,每条消息以一个0x00字节开始,以一个0xFF字节结束,中间包含 UTF-8 数据。

web socket 接口

除了 WebSocket 协议的定义,该规范还定义了在 JavaScript 应用中使用的 WebSocket 接口。清单 7-2 显示了WebSocket界面。

清单 7-2。web socket 接口

`[Constructor(DOMString url, optional DOMString protocols),
 Constructor(DOMString url, optional DOMString[] protocols)]
interface WebSocket : EventTarget {
  readonly attribute DOMString url;

// ready state
  const unsigned short CONNECTING = 0;
  const unsigned short OPEN = 1;
  const unsigned short CLOSING = 2;
  const unsigned short CLOSED = 3;
  readonly attribute unsigned short readyState;
  readonly attribute unsigned long bufferedAmount;

// networking
  [TreatNonCallableAsNull] attribute Function? onopen;
  [TreatNonCallableAsNull] attribute Function? onerror;
  [TreatNonCallableAsNull] attribute Function? onclose;
  readonly attribute DOMString extensions;
  readonly attribute DOMString protocol;
  void close([Clamp] optional unsigned short code, optional DOMString reason);

// messaging
  [TreatNonCallableAsNull] attribute Function? onmessage;
           attribute DOMString binaryType;
  void send(DOMString data);
  void send(ArrayBuffer data);
  void send(Blob data);
};`

使用WebSocket接口很简单。要连接一个远程主机,只需创建一个新的WebSocket实例,为新对象提供一个 URL,表示您希望连接的端点。注意,ws://wss://前缀分别表示 WebSocket 和安全 WebSocket 连接。

在客户端和服务器之间的初始握手期间,通过相同的底层 TCP/IP 连接,通过从 HTTP 协议升级到 WebSocket 协议来建立 WebSocket 连接。一旦建立,WebSocket 数据帧就可以以全双工模式在客户机和服务器之间来回发送。连接本身通过由WebSocket接口定义的message事件和send方法公开。在您的代码中,使用异步事件侦听器来处理连接生命周期的每个阶段。

myWebSocket.onopen = function(evt) { alert("Connection open ..."); }; myWebSocket.onmessage = function(evt) { alert( "Received Message:  "  +  evt.data); }; myWebSocket.onclose = function(evt) { alert("Connection closed."); };

大幅减少不必要的网络流量和延迟

那么 WebSocket 能有多高效呢?让我们并排比较一下轮询应用和 WebSocket 应用。为了说明轮询,我们将研究一个 web 应用,其中一个网页使用传统的轮询模型从 web 服务器请求实时股票数据。它通过轮询 web 服务器上托管的 Java Servlet 来实现这一点。消息代理从一个虚构的股票价格提要接收数据,并不断更新价格。web 页面连接并订阅特定的股票频道(消息代理上的一个主题),并使用 XMLHttpRequest 每秒轮询一次更新。当接收到更新时,执行一些计算并显示股票数据,如图图 7-3 所示。

Image

图 7-3。【JavaScript 股票行情应用示例

这听起来很棒,但是深入了解一下就会发现这个应用存在一些严重的问题。例如,在带有 Firebug 的 Mozilla Firefox 中,您可以看到 GET 请求以一秒的间隔敲打服务器。查看 HTTP 头可以发现与每个请求相关的惊人的开销。清单 7-3 和 7-4 显示了单个请求和响应的 HTTP 头数据。

清单 7-3。 HTTP 请求头

GET /PollingStock//PollingStock HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.5) Gecko/20091102  Firefox/3.5.5 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 300 Connection: keep-alive Referer: http://www.example.com/PollingStock/ Cookie: showInheritedConstant=false; showInheritedProtectedConstant=false;  showInheritedProperty=false; showInheritedProtectedProperty=false;  showInheritedMethod=false; showInheritedProtectedMethod=false;  showInheritedEvent=false; showInheritedStyle=false; showInheritedEffect=false

清单 7-4。 HTTP 响应头

HTTP/1.x 200 OK X-Powered-By: Servlet/2.5 Server: Sun Java System Application Server 9.1_02 Content-Type: text/html;charset=UTF-8 Content-Length: 21 Date: Sat, 07 Nov 2009 00:32:46 GMT

只是为了好玩(哈!),我们可以把所有的人物都算进去。总的 HTTP 请求和响应头信息开销包含 871 个字节,这还不包括任何数据。当然,这只是一个例子,您可以拥有少于 871 字节的头数据,但也有头数据超过 2,000 字节的常见情况。在这个示例应用中,典型的股票主题消息的数据只有大约 20 个字符长。正如您所看到的,它实际上被过多的头信息淹没了,而这些头信息本来就不是必需的。

那么,当您将这个应用部署给大量用户时会发生什么呢?让我们来看看在三个不同的用例中与这个轮询应用相关的 HTTP 请求和响应头数据的网络开销。

  • 用例 A :每秒 1000 个客户端轮询:网络流量为(871×1000)= 871000 字节=每秒 6968000 比特(6.6 Mbps)
  • 用例 B :每秒 10000 个客户端轮询:网络流量为(871×10000)= 8710000 字节= 69680000 比特每秒(66 Mbps)
  • 用例 C :每秒 10 万个客户端轮询:网络流量为(871 × 10 万)= 8710 万字节= 69680 万比特每秒(665 Mbps)

这是大量不必要的网络开销。考虑一下,如果我们重新构建应用以使用 WebSocket,向 web 页面添加一个事件处理程序来异步侦听来自消息代理的股票更新消息(稍后会详细介绍)。这些消息中的每一条都是一个 WebSocket 帧,只有两个字节的开销(而不是 871)。看看这在我们的三个用例中是如何影响网络开销的。

  • 用例 A:1000 个客户端每秒接收 1 条消息:网络流量为(2×1000)= 2000 字节=每秒 16000 比特(0.015 Mbps)
  • 用例 B:10000 个客户端每秒接收 1 条消息:网络流量为(2×10000)= 20000 字节= 160000 比特每秒(0.153 Mbps)
  • 用例 C: 10 万个客户端每秒接收 1 条消息:网络流量为(2 × 10 万)= 20 万字节=每秒 160 万比特(1.526 Mbps)

正如你在图 7-4 中看到的,与轮询解决方案相比,WebSocket 大大减少了不必要的网络流量。

Image

图 7-4。轮询 WebSocket 流量之间不必要的网络开销比较

那么延迟的减少呢?看一下图 7-5 。在上半部分,您可以看到半双工轮询解决方案的延迟。对于这个例子,如果我们假设一条消息从服务器到浏览器需要 50 毫秒,那么轮询应用就会引入很多额外的延迟,因为当响应完成时,必须向服务器发送一个新的请求。这个新请求又需要 50 毫秒,在此期间,服务器无法向浏览器发送任何消息,从而导致额外的服务器内存消耗。

在图的下半部分,您可以看到 WebSocket 解决方案减少了延迟。一旦连接升级到 WebSocket,消息就可以在到达时从服务器流向浏览器。消息从服务器传输到浏览器仍然需要 50 毫秒,但是 WebSocket 连接仍然保持打开,因此不需要向服务器发送另一个请求。

Image

图 7-5。轮询和 WebSocket 应用之间的延迟比较

WebSocket 在实时 web 的可伸缩性方面向前迈进了一大步。正如您在本章中所看到的,WebSocket 可以提供 500:1 甚至 1000:1 的不必要 HTTP 报头流量减少率和 3:1 的延迟减少率,具体取决于 HTTP 报头的大小。

编写一个简单的 Echo WebSocket 服务器

在使用 WebSocket API 之前,您需要一个支持 WebSocket 的服务器。在这一节中,我们将看看如何编写一个简单的 web socket“echo”服务器。为了运行本章的例子,我们包含了一个用 Python 编写的简单的 WebSocket 服务器。以下示例的示例代码位于图书网站的 WebSocket 部分。

WEBSOCKET 服务器

已经有很多 WebSocket 服务器实现,甚至还有更多正在开发中。以下只是现有 WebSocket 服务器的一部分:

  • Kaazing WebSocket 网关—基于 Java 的 WebSocket 网关
  • mod _ pyweb socket—Apache HTTP 服务器的基于 Python 的扩展
  • Netty—一个包含 WebSocket 支持的 Java 网络框架
  • node . js—一个服务器端的 JavaScript 框架,上面写了多个 WebSocket 服务器

Kaazing 的 WebSocket 网关包括对没有 WebSocket 本机实现的浏览器的完整客户端 WebSocket 仿真支持,这允许您根据当前的 WebSocket API 进行编码,并让您的代码在所有浏览器中工作。

要在ws://localhost:8000/echo运行接受连接的 Python WebSocket echo 服务器,请打开命令提示符,导航到包含该文件的文件夹,并发出以下命令:

python websocket.py

我们还包含了一个广播服务器,它在ws://localhost:8080/broadcast接受连接。与 echo 服务器相反,发送到这个特定服务器实现的任何 WebSocket 消息都将被反弹回当前连接的每个人。这是向多个听众广播消息的一种非常简单的方式。要运行广播服务器,请打开命令提示符,导航到包含该文件的文件夹,并发出以下命令:

python broadcast.py

这两个脚本都利用了websocket.py中的示例 WebSocket 协议库。您可以为实现其他服务器端行为的其他路径添加处理程序。

`images 注意这只是一个 WebSocket 协议的服务器,它不能响应 HTTP 请求。握手解析器不完全符合 HTTP。但是,因为 WebSocket 连接以 HTTP 请求开始,并且依赖于 Upgrade 头,所以其他服务器可以在同一个端口上同时服务于 WebSocket 和 HTTP。

让我们看看当一个浏览器试图与这个服务器通信时会发生什么。当浏览器向 WebSocket URL 发出请求时,服务器发回完成 WebSocket 握手的头。WebSocket 握手响应必须包含一个HTTP/1.1 101状态代码和升级连接头。这通知浏览器,对于 TCP 会话的剩余部分,服务器正在从 HTTP 握手切换到 WebSocket 协议。

![`images](https://gitee.com/OpenDocCN/vkdoc-html-css-zh/raw/master/docs/pro-h5-prog/img/square.jpg) 注意如果你正在实现一个 WebSocket 服务器,你应该参考 IETF 在``tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol``的协议草案或者最新的规范。

`# write out response headers
self.send_bytes("HTTP/1.1 101 Switching Protocols\r\n")
self.send_bytes("Upgrade: WebSocket\r\n")
self.send_bytes("Connection: Upgrade\r\n")
self.send_bytes("Sec-WebSocket-Accept: %s\r\n" % self.hash_key(key))

if "Sec-WebSocket-Protocol" in headers:
    protocol = headers["Sec-WebSocket-Protocol"]
    self.send_bytes("Sec-WebSocket-Protocol: %s\r\n" % protocol)`

WebSocket 框架

握手之后,客户端和服务器可以随时发送消息。在这个服务器中,每个连接都由一个WebSocketConnection实例来表示。WebSocketConnectionsend函数,如图图 7-6 所示,根据 WebSocket 协议写出一条消息。数据有效载荷之前的字节标记了帧的长度和类型。文本框架是 UTF-8 编码的。在这个服务器中,每个 WebSocket 连接都是一个asyncore.dispatcher_with_send,它是一个异步套接字包装器,支持缓冲发送。

从浏览器发送到服务器的数据被屏蔽。屏蔽是 WebSocket 协议的一个不寻常的特性。有效负载数据的每个字节都与随机掩码进行异或运算,以确保 WebSocket 流量看起来不像其他协议。像 Sec-WebSocket-Key 散列一样,这是为了减轻对不兼容的网络基础设施的跨协议攻击的神秘形式。

Image

图 7-6。web socket 框架的组件

`images 注意Python 和其他语言还有很多其他的异步 I/O 框架。选择 Asyncore 是因为它包含在 Python 标准库中。还要注意的是,这个实现使用了协议草案 10。这是一个为测试和说明而设计的简单示例。

WebSocketConnection继承了asyncore.dispatcher_with_send并覆盖了send方法,以便构造文本和二进制消息。

def send(self, s):   if self.readystate == "open":     self.send_bytes("\x00")
    self.send_bytes(s.encode("UTF8"))     self.send_bytes("\xFF")

websocket.pyWebSocketConnections的处理程序遵循一个简化的调度程序接口。处理程序的dispatch()方法是用连接接收到的每个帧的有效负载来调用的。EchoHandler将每条消息发送回发送者。

`class EchoHandler(object):
    """
    The EchoHandler repeats each incoming string to the same WebSocket.
    """

def init(self, conn):
        self.conn = conn

def dispatch(self, data):
        self.conn.send("echo: " + data)`

基本广播服务器broadcast.py的工作方式大致相同,但在这种情况下,当广播处理器接收到一个帧时,它会在所有连接的 WebSockets 上发回该帧,如下例所示:

`class BroadcastHandler(object):
    """
    The BroadcastHandler repeats incoming strings to every connected
    WebSocket.
    """

def init(self, conn):
        self.conn = conn

def dispatch(self, data):
        for session in self.conn.server.sessions:
            session.send(data)`

broadcast.py中的处理程序提供了一个轻量级的消息广播器,它简单地发送和接收任何数据。这对于我们的例子来说已经足够了。请注意,这个广播服务不执行任何输入验证,而这在生产消息服务器中是需要的。生产 WebSocket 服务器至少应该验证传入数据的格式。

为了完整起见,清单 7-5 和清单 7-6 提供了websocket.pybroadcast.py的完整代码。请注意,这只是一个示例服务器实现;它不适合生产部署。

清单 7-5。【websocket.py 的完整代码

`#!/usr/bin/env python

import asyncore
import socket
import struct
import time
from hashlib import sha1
from base64 import encodestring`

`class WebSocketConnection(asyncore.dispatcher_with_send):

TEXT = 0x01
  BINARY = 0x02

def init(self, conn, server):
    asyncore.dispatcher_with_send.init(self, conn)

self.server = server
    self.server.sessions.append(self)
    self.readystate = "connecting"
    self.buffer = ""

def handle_read(self):
    data = self.recv(1024)
    self.buffer += data
    if self.readystate == "connecting":
      self.parse_connecting()
    elif self.readystate == "open":
      self.parse_frame()

def handle_close(self):
    self.server.sessions.remove(self)
    self.close()

def parse_connecting(self):
    """
    Parse a WebSocket handshake. This is not a full HTTP request parser!
    """
    header_end = self.buffer.find("\r\n\r\n")
    if header_end == -1:
      return
    else:
      header = self.buffer[:header_end]
      # remove header and four bytes of line endings from buffer
      self.buffer = self.buffer[header_end + 4:]
      header_lines = header.split("\r\n")
      headers = {}

# validate HTTP request and construct location
      method, path, protocol = header_lines[0].split(" ")
      if method != "GET" or protocol != "HTTP/1.1" or path[0] != "/":
        self.terminate()
        return

# parse headers
      for line in header_lines[1:]:
        key, value = line.split(": ")
        headers[key] = value`

`      headers["Location"] = "ws://" + headers["Host"] + path

self.readystate = "open"
      self.handler = self.server.handlers.get(path, None)(self)

self.send_server_handshake_10(headers)

def terminate(self):
    self.ready_state = "closed"
    self.close()

def send_server_handshake_10(self, headers):
    """
    Send the WebSocket Protocol draft HyBi-10 handshake response
    """
    key = headers["Sec-WebSocket-Key"]

# write out response headers
    self.send_bytes("HTTP/1.1 101 Switching Protocols\r\n")
    self.send_bytes("Upgrade: WebSocket\r\n")
    self.send_bytes("Connection: Upgrade\r\n")
    self.send_bytes("Sec-WebSocket-Accept: %s\r\n" % self.hash_key(key))

if "Sec-WebSocket-Protocol" in headers:
      protocol = headers["Sec-WebSocket-Protocol"]
      self.send_bytes("Sec-WebSocket-Protocol: %s\r\n" % protocol)

def hash_key(self, key):
    guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
    combined = key + guid
    hashed = sha1(combined).digest()
    return encodestring(hashed)

def parse_frame(self):
    """
    Parse a WebSocket frame. If there is not a complete frame in the
    buffer, return without modifying the buffer.
    """
    buf = self.buffer
    payload_start = 2

# try to pull first two bytes
    if len(buf) < 3:
      return
    b = ord(buf[0])
    fin = b & 0x80    # 1st bit
    # next 3 bits reserved
    opcode = b & 0x0f   # low 4 bits
    b2 = ord(buf[1])
    mask = b2 & 0x80  # high bit of the second byte
    length = b2 & 0x7f  # low 7 bits of the second byte`

`    # check that enough bytes remain
    if len(buf) < payload_start + 4:
      return
    elif length == 126:
      length, = struct.unpack(">H", buf[2:4])
      payload_start += 2
    elif length == 127:
      length, = struct.unpack(">I", buf[2:6])
      payload_start += 4

if mask:
      mask_bytes = [ord(b) for b in buf[payload_start:payload_start + 4]]
      payload_start += 4

# is there a complete frame in the buffer?
    if len(buf) < payload_start + length:
      return

# remove leading bytes, decode if necessary, dispatch
    payload = buf[payload_start:payload_start + length]
    self.buffer = buf[payload_start + length:]

# use xor and mask bytes to unmask data
    if mask:
      unmasked = [mask_bytes[i % 4] ^ ord(b)
              for b, i in zip(payload, range(len(payload)))]
      payload = "".join([chr(c) for c in unmasked])

if opcode == WebSocketConnection.TEXT:
      s = payload.decode("UTF8")
      self.handler.dispatch(s)
    if opcode == WebSocketConnection.BINARY:
      self.handler.dispatch(payload)
    return True

def send(self, s):
    """
    Encode and send a WebSocket message
    """

message = ""
    # always send an entire message as one frame (fin)
    b1 = 0x80

# in Python 2, strs are bytes and unicodes are strings
    if type(s) == unicode:
      b1 |= WebSocketConnection.TEXT
      payload = s.encode("UTF8")
    elif type(s) == str:
      b1 |= WebSocketConnection.BINARY
      payload = s`

`    message += chr(b1)

# never mask frames from the server to the client
    b2 = 0
    length = len(payload)
    if length < 126:
      b2 |= length
      message += chr(b2)              
    elif length < (2 ** 16) - 1:
      b2 |= 126
      message += chr(b2)
      l = struct.pack(">H", length)
      message += l
    else:
      l = struct.pack(">Q", length)
      b2 |= 127
      message += chr(b2)
      message += l

message += payload

if self.readystate == "open":
      self.send_bytes(message)

def send_bytes(self, bytes):
    try:
      asyncore.dispatcher_with_send.send(self, bytes)
    except:
      pass

class EchoHandler(object):
  """
  The EchoHandler repeats each incoming string to the same WebSocket.
  """

def init(self, conn):
    self.conn = conn

def dispatch(self, data):
    try:
      self.conn.send(data)
    except:
      pass

class WebSocketServer(asyncore.dispatcher):

def init(self, port=80, handlers=None):
    asyncore.dispatcher.init(self)
    self.handlers = handlers
    self.sessions = []     self.port = port
    self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
    self.set_reuse_addr()
    self.bind(("", port))
    self.listen(5)

def handle_accept(self):
    conn, addr = self.accept()
    session = WebSocketConnection(conn, self)

if name == "main":
  print "Starting WebSocket Server"
  WebSocketServer(port=8080, handlers={"/echo": EchoHandler})
  asyncore.loop()`

您可能已经注意到了 WebSocket 握手中一个不寻常的密钥计算。这是为了防止跨协议攻击。简而言之,这将阻止恶意 WebSocket 客户端代码欺骗到非 WebSocket 服务器的连接。散列一个 GUID 和一个随机值就足以确定响应服务器理解 WebSocket 协议。

清单 7-6。【broadcast.py 的完整代码

`#!/usr/bin/env python

import asyncore
from websocket import WebSocketServer

class BroadcastHandler(object):
    """
    The BroadcastHandler repeats incoming strings to every connected
    WebSocket.
    """

def init(self, conn):
        self.conn = conn

def dispatch(self, data):
        for session in self.conn.server.sessions:
            session.send(data)

if name == "main":
    print "Starting WebSocket broadcast server"
    WebSocketServer(port=8080, handlers={"/broadcast": BroadcastHandler})
    asyncore.loop()`

现在我们已经有了一个工作的 echo 服务器,我们需要编写客户端。web 浏览器实现 WebSocket 协议的连接部分。我们可以使用 JavaScript 中的 API 与我们的简单服务器进行通信。

使用 WebSocket API

在这一节中,我们将更详细地探索 WebSocket 的使用。

检查浏览器支持

在使用 WebSocket API 之前,您需要确保浏览器支持您将要做的事情。这样,您可以提供一些替代文本,提示应用的用户升级到更新的浏览器。清单 7-7 显示了一种测试浏览器支持的方法。

清单 7-7。检查浏览器支持

function loadDemo() {   **if (window.WebSocket) {**     document.getElementById("support").innerHTML = "HTML5 WebSocket is supported in your                                        browser.";   } else {      document.getElementById("support").innerHTML = "HTML5 WebSocket is not supported in                                         your browser.";   } }

在这个例子中,您在loadDemo函数中测试浏览器支持,这个函数可能在应用的页面加载时被调用。对window.WebSocket的调用将返回WebSocket对象(如果它存在的话),或者如果它不存在就触发失败案例。在这种情况下,通过用合适的消息更新页面上先前定义的support元素,页面被更新以反映是否有浏览器支持。

查看 WebSocket 在您的浏览器中是否受支持的另一种方法是使用浏览器的控制台(例如 Firebug 或 Chrome 开发工具)。图 7-7 展示了如何测试 WebSocket 在 Google Chrome 中是否被原生支持(如果不支持,window.WebSocket命令返回“未定义”))

Image

图 7-7。在谷歌 Chrome 开发者工具中测试 WebSocket 支持

基本 API 用法

以下示例的示例代码位于图书网站的 WebSocket 部分。该文件夹包含一个 websocket.html 文件和一个 broadcast.html 文件(以及一个 tracker.html 文件,在下一节中使用)以及前面显示的可以在 Python 中运行的 WebSocket 服务器代码。

创建 WebSocket 对象并连接到 WebSocket 服务器

使用 WebSocket 接口非常简单。要连接到一个端点,只需创建一个新的 WebSocket 实例,为新对象提供一个表示您希望连接的端点的 URL。您可以使用ws://wss://前缀分别表示 WebSocket 和 WebSocket 安全连接。

url = "ws://localhost:8080/echo"; w = new WebSocket(url);

当连接 WebSocket 时,您可以选择列出您的应用可以使用的协议。WebSocket 构造函数的第二个参数可以是一个字符串或字符串数组,其中包含应用理解并希望用来通信的“子协议”的名称。

w = new WebSocket(url, protocol);

您甚至可以列出几个协议:

w = new WebSocket(url, [“proto1”, “proto2”]);

假设,proto1 和 proto2 是定义良好的,甚至可能是注册和标准化的,客户机和服务器都能理解的协议名称。服务器将从列表中选择一个首选协议。当套接字打开时,它的协议属性将包含服务器选择的协议。

onopen = function(e) {   // determine which protocol the server selected   log(e.target.protocol) }

您可能使用的协议包括可扩展消息和存在协议(XMPP,或 Jabber)、高级消息队列协议(AMQP)、远程帧缓冲区(RFB,或 VNC)和面向流文本的消息协议(STOMP)。这些是许多客户端和服务器使用的真实协议。使用标准协议可以确保不同组织的 web 应用和服务器之间的互操作性。它也为公共 WebSocket 服务打开了大门。您可以使用已知的协议与服务器对话。理解相同协议的客户端应用可以连接并参与。

这个例子没有使用标准协议。我们不会引入外部依赖,也不会占用空间来实现完整的标准协议。例如,它直接使用 WebSocket API,就像您开始为新协议编写代码一样。

添加事件监听器

WebSocket 编程遵循异步编程模型;一旦你有一个打开的套接字,你只需要等待事件。您不必再主动轮询服务器。为此,您向WebSocket对象添加回调函数来监听事件。

一个WebSocket对象调度三个事件:打开、关闭和消息。open 事件在建立连接时触发,message 事件在收到消息时触发,close 事件在 WebSocket 连接关闭时触发。error 事件触发以响应意外失败。与大多数 JavaScript APIs 一样,在调度事件时会调用相应的回调函数(onopenonmessageoncloseonerror)。

w.onopen = function() {   log("open");   w.send("thank you for accepting this websocket request"); } w.onmessage = function(e) {   log(e.data); } w.onclose = function(e) {   log("closed"); } w.onerror = function(e) {   log(“error”); }

让我们再来看看这个消息处理程序。如果 WebSocket 协议消息被编码为文本,则消息事件的数据属性是一个字符串。对于二进制消息,数据可以是 Blob 或 ArrayBuffer,这取决于 WebSocket 的binaryType属性的值。

w.binaryType = "arraybuffer"; w.onmessage = function(e) {   // data can now be either a string or an ArrayBuffer   log(e.data); }

发送消息

当套接字打开时(即在调用onopen监听器之后和调用onclose监听器之前),您可以使用send函数发送消息。在发送一条或多条消息后,您也可以调用close来终止连接,或者您也可以保持连接打开。

document.getElementById("sendButton").onclick = function() {     w.send(document.getElementById("inputMessage").value); }

就这样。双向浏览器通信变得简单。为了完整起见,清单 7-8 显示了带有 WebSocket 代码的整个 HTML 页面。

在 WebSocket 的更高级的应用中,您可能希望在调用send()之前测量在传出缓冲区中备份了多少数据。bufferedAmount属性表示已经在 WebSocket 上发送但尚未写入网络的字节数。这对于限制应用发送数据的速率非常有用。

document.getElementById("sendButton").onclick = function() {   if (w.bufferedAmount < bufferThreshold) {     w.send(document.getElementById("inputMessage").value);   } }

除了字符串,WebSocket 还可以发送二进制数据。这对于实现二进制协议特别有用,例如通常位于 TCP 之上的标准互联网协议。WebSocket API 支持将 Blob 和 ArrayBuffer 实例作为二进制数据发送。

var a = new Uint8Array([8,6,7,5,3,0,9]); w.send(a.buffer);

清单 7-8。【websocket.html 代码】??

`

<title>WebSocket Test Page</title>


Running the WebSocket Page`

要测试包含 WebSocket 代码的websocket.html页面,请打开命令提示符,导航到包含 WebSocket 代码的文件夹,并发出以下命令来托管 HTML 文件:

python -m SimpleHTTPServer 9999

接下来,打开另一个命令提示符,导航到包含 WebSocket 代码的文件夹,发出以下命令来运行 Python WebSocket 服务器:

python websocket.py

最后打开一个原生支持 WebSocket 的浏览器,导航到[localhost:9999/websocket.html](http://localhost:9999/websocket.html)

图 7-8 显示了运行中的网页。

Image

图 7-8。websocket.html 在行动

示例代码文件夹还包含一个网页,该网页连接到在上一节中创建的广播服务。要查看该操作,请关闭运行 WebSocket 服务器的命令提示符,导航到包含 WebSocket 代码的文件夹,并发出以下命令来运行 python WebSocket 服务器。

python broadcast.py

打开两个本地支持 WebSocket 的独立浏览器,并(在每个浏览器中)导航到[localhost:9999/broadcast.html](http://localhost:9999/broadcast.html)

图 7-9 显示了在两个独立的网页上运行的广播 WebSocket 服务器。

Image

图 7-9。【broadcast.html 在两个浏览器中的表现】??

构建 WebSocket 应用

现在我们已经看到了 WebSocket 的基础知识,是时候解决一些更实质性的问题了。之前,我们使用 HTML5 地理定位 API 构建了一个应用,允许我们直接在网页中计算行进的距离。我们可以利用这些相同的地理定位技术,结合我们对 WebSocket 的新支持,创建一个简单的应用来保持多个参与者的连接:位置跟踪器。

`images 注意我们将使用上面描述的广播 WebSocket 服务器,所以如果你不熟悉它,你应该考虑花些时间学习它的基础知识。

在这个应用中,我们将通过确定我们的位置并将其广播给所有可用的侦听器来组合 WebSocket 和地理定位。加载这个应用并连接到同一个广播服务器的每个人都会定期使用 WebSocket 发送他们的地理位置。与此同时,应用将监听来自服务器的任何消息,并为它听到的每个人实时更新显示条目。在比赛场景中,这种应用可以让跑步者知道所有竞争对手的位置,并提示他们跑得更快(或更慢)。

这个微小的应用不包括除经纬度位置之外的任何个人信息。姓名、出生日期和最喜欢的冰淇淋口味都是严格保密的。

你被警告了!

Brian 说:“这个应用是关于分享你的个人信息的。当然,只有一个位置是共享的。然而,如果您(或您的用户)不理解首次访问地理位置 API 时出现的浏览器警告,那么这个应用应该是一个严峻的教训,告诉您将敏感数据传输到远程位置是多么容易。确保您的用户了解同意提交位置数据的后果。

当有疑问时,在你的应用中让用户知道如何使用他们的敏感数据。让选择退出成为最简单的行动方式。"

但是警告已经够多了…让我们深入研究代码。和往常一样,整个代码示例都在网上供您阅读。我们将在这里集中讨论最重要的部分。完成的应用将看起来像图 7-10 。尽管在理想情况下,将它叠加在地图上会得到增强。

Image

图 7-10。位置跟踪器应用

编码 HTML 文件

该应用的 HTML 标记将刻意保持简单,以便我们可以专注于手头的数据。有多简单?

`

HTML5 WebSocket / Geolocation Tracker

Geolocation:

HTML5 Geolocation is  not supported in your browser.

WebSocket:

WebSocket is not  supported in your browser.

`

</body>

简单到我们只包括一个标题和几个状态区域:一个状态区域用于地理位置更新,另一个用于记录任何 WebSocket 活动。当消息被实时接收时,位置数据的实际图像将被插入到页面中。

默认情况下,我们的状态消息表明查看者的浏览器不支持地理定位或 WebSocket。一旦我们检测到对这两种 HTML5 技术的支持,我们会用一些更友好的东西来更新状态。

` `

总结

在本章中,您已经看到了 WebSocket 如何提供一个简单而强大的机制来创建引人注目的实时应用。

首先,我们看了协议本身的性质,以及它如何与现有的 HTTP 流量进行互操作。我们比较了当前基于轮询的通信策略的网络开销需求和 WebSocket 的有限开销。

为了说明 WebSocket 的作用,我们探索了一个 WebSocket 服务器的简单实现,以展示在实践中实现这个协议是多么简单。类似地,我们研究了客户端 WebSocket API,注意到它提供的与 JavaScript 集成的便利性。

最后,我们浏览了一个更复杂的示例应用,它结合了地理定位和 WebSocket 的强大功能,展示了这两种技术如何很好地协同工作。现在我们已经看到了 HTML5 如何将 TCP 风格的网络编程引入浏览器,我们将把注意力转向收集更有趣的数据,而不仅仅是用户的当前位置。在下一章,我们来看看 HTML5 中对表单控件的增强。

八、使用表单 API

在这一章中,我们将探索一项由来已久的技术:HTML 表单所带来的所有新功能。自从表单第一次出现以来,它一直是网络爆炸的支柱。如果没有表单控件,web 商务交易、社交讨论和高效搜索根本就不可能实现。

可悲的是,HTML5 表单是规范和实现中变化最大的领域之一,尽管已经设计了很多年。有好消息也有坏消息。好消息是,这一领域的进步虽然是渐进的,但增长速度相当快。坏消息是,您需要仔细寻找可以在所有目标浏览器中工作的新表单控件的子集。表单规范详细说明了一大组 API,并且不难发现,符合 HTML5 的 web 浏览器的每个主要新版本都增加了对一个或多个表单控件和一些有用的验证功能的支持。

不管怎样,我们将利用这一章来帮助你在虚拟的控件海洋中导航,并找到哪些控件今天就可以使用,哪些控件即将发布。

html 5 表单概述

如果您已经熟悉 HTML 中的表单——如果您对专业 HTML 编程感兴趣,我们假设您已经熟悉——那么您会发现 HTML5 中的新功能非常适合坚实的基础。如果您还不熟悉表单的基本用法,我们推荐您阅读大量关于创建和处理表单值的书籍和教程。这个主题在这一点上得到了很好的阐述,您会很高兴地知道:

  • 表单仍然应该封装在一个设置了基本提交属性的<form>元素中。
  • 当用户或应用程序员提交页面时,表单仍然会将控件的值发送到服务器。
  • 所有熟悉的表单控件——文本字段、单选按钮、复选框等等——仍然存在,并像以前一样工作(尽管增加了一些新功能)。
  • 对于那些希望编写自己的修饰符和处理程序的人来说,表单控件仍然是完全脚本化的。

HTML 表单与 XForms

在过去几年里,早在 HTML5 取得很大进展之前,您可能就听说过 XForms。XForms 是一个以 XML 为中心的、强大的、有点复杂的标准,用于指定客户端表单行为,已经在它自己的 W3C 工作组中发展了近十年。XForms 利用 XML Schema 的全部功能来定义精确的验证和格式化规则。不幸的是,如果没有额外的插件,目前主流浏览器都不支持 XForms。

HTML5 表单不是 XForms。

功能形式

相反,HTML5 Forms 专注于发展现有的简单 HTML 表单,以包含更多类型的控件,并解决 web 开发人员今天面临的实际限制。有一点需要记住,特别是在比较不同浏览器的表单实现时。

`images 注意关于 HTML5 表单,要掌握的最重要的概念是,规范处理的是功能行为和语义,而不是外观或显示。

例如,虽然规范详细说明了诸如颜色和日期选择器、数字选择器和电子邮件地址输入等元素的功能 API,但是规范并没有说明浏览器应该如何将这些元素呈现给最终用户。从多个层面来看,这是一个很好的选择。它允许浏览器在提供用户交互的创新方式上竞争;它将样式和语义分开;并且它允许未来或专门的用户输入设备以对其操作自然的方式进行交互。但是,在目标浏览器平台支持应用中的所有表单控件之前,请确保为用户提供足够的上下文信息,以便他们知道如何与回退呈现进行交互。有了正确的提示和描述,用户使用您的应用将不会有任何问题,即使它在出现未知输入类型时退回到替代内容。

HTML5 表单包含了大量新的 API 和元素类型,现在对它们的支持无处不在。为了理解所有的新功能,我们将把它分成两类

  • 新输入类型
  • 新功能和属性

然而,在我们开始之前,让我们快速评估一下当今的浏览器是如何支持 HTML5 表单规范的。

浏览器支持 HTML5 表单

浏览器对 HTML5 表单的支持正在增长,但仍然有限。主要的浏览器供应商都支持许多表单控件,Opera 在早期实现中处于领先地位。但是,规格是稳定的。

检查浏览器支持在新表单的上下文中用处不大,因为它们被设计为在旧浏览器中优雅地降级。很大程度上,这意味着现在使用新的元素是安全的,因为旧的浏览器对于它们不理解的任何输入类型都会退回到简单的文本字段显示。然而,正如我们将在本章后面看到的,这提高了多层表单验证的重要性,因为仅仅依靠浏览器验证器来强制表单控件的数据类型是不够的,即使您假设完全支持现代浏览器。

现在我们已经了解了浏览器的前景,让我们来看看 HTML5 规范中添加的新表单控件。

一个输入目录

获得 HTML5 中所有新增和变更元素的目录的最佳地方之一是 W3C 站点本身维护的标记列表。W3C 在[dev.w3.org/html5/markup/](http://dev.w3.org/html5/markup/)保存一个目录页文件

这个页面表示 HTML 页面中所有当前和未来的元素。目录列表中注明了新的和更改的元素。然而,这个列表中的“新”仅仅意味着该元素是在 HTML4 规范之后添加的——并不意味着该元素已经在浏览器或最终规范中实现。有了这个警告,让我们看看 HTML5 带来的新表单元素,从今天实现的表单元素开始。表 8-1 列出了新的type属性。例如,许多 HTML 开发者会非常熟悉<input type="text"><input type="checkbox">。新的输入类型遵循与现有输入类型相似的模型。

`images

这些新的输入类型提供了什么?就编程 API 而言…不是很多。事实上,就telemailurlsearch的类型而言,没有属性将它们与最简单的输入类型text区分开来。

那么,通过指定一个输入是专用类型,您到底得到了什么呢?你有专门的输入控制。(可能会有限制。在许多桌面浏览器中提供 void。)

让我们用一个例子来说明。通过指定输入是类型email

<input type="email">

而不是使用传统的标准,即字段仅仅是文本类型

<input type="text">

你向浏览器提供一个提示,在适当的时候呈现不同的用户界面或输入。您还为浏览器提供了在提交之前进一步验证字段的能力,但是我们将在本章的后面讨论这个主题。

移动设备浏览器是支持这些新的表单输入类型最快的浏览器之一。在手机上,对于没有完整键盘的用户来说,每一次按键都是更大的负担。因此,移动设备浏览器通过基于声明的类型显示不同的输入界面来支持这些新的输入类型。在苹果 iPhone 中,输入类型为text的标准屏幕键盘显示如图 8-1 中的所示。

Image

图 8-1。输入文本的屏幕键盘显示

然而,当一个输入字段被标记为e-mail类型时,iPhone 会呈现一个为电子邮件输入定制的不同键盘布局,如图图 8-2 所示。

Image

图 8-2。电子邮件输入的屏幕键盘显示

请注意对键盘空格键区域的细微调整,以允许@符号和方便地访问句点。对类型URL和类型search的键盘布局进行了类似的调整。然而,在桌面版本的 Safari 浏览器中,以及在任何没有明确支持e-mailURLsearchtel类型的浏览器中,只会显示普通的文本输入栏。未来的浏览器,甚至是桌面版本,可能会向用户提供视觉提示或线索,以指示该字段是某个子类型。例如,Opera 会在一个字段旁边显示一个小信封图标,表示它需要一个电子邮件地址。然而,在今天的 web 应用中使用这些类型是安全的,因为任何浏览器要么针对该类型进行优化,要么干脆什么都不做。

另一种在浏览器中日益流行的特殊类型是<input type="range">。这种专门的输入控件旨在让用户从一系列数字中进行选择。例如,可以在表单中使用范围控件,从限制 18 岁以下未成年人访问的范围中选择年龄。通过创建一个范围输入并设置其特殊的minmax值,开发人员可以请求页面显示一个受约束的数字选择器,该选择器只能在指定的范围内操作。例如,在 Opera 浏览器中,控件:

<input type="range" min="18" max="120">

为受年龄限制的材料选择合适的值提供了一种便捷的方法。在 Opera 浏览器中,它显示如下:

`images

不幸的是,范围输入本身并不显示浏览器的数字表示。此外,如果没有,用户实际上不可能知道当前选择的值是什么。要解决这个问题,我们可以很容易地添加一个onchange处理程序,根据当前范围值的变化来更新显示字段,如清单 8-1 中的所示。

![`images](https://gitee.com/OpenDocCN/vkdoc-html-css-zh/raw/master/docs/pro-h5-prog/img/square.jpg) 注意为什么`range`元素默认不包含可视化显示?也许是这样,用户界面设计者可以定制显示器的确切位置和外观。使显示可选会增加一些工作量,但更具灵活性。

新的表单控件现在包括一个简单的输出元素,它就是为这种类型的操作而设计的。输出是一个表单元素,它只保存一个值。因此,我们可以用它来显示范围控件的值。

清单 8-1。 onchange处理程序更新一个输出

<label for="age">Age</label> <input id="age" type="range" min="18" max="120" value="18" onchange="ageDisplay.value=value"> <output id="ageDisplay">18</output>

这很好地显示了我们的范围输入,如下所示:

`images

Opera 和基于 WebKit 的浏览器——Safari 和 Chrome——现在增加了对 type range元素的支持。Firefox 支持是计划中的,但在撰写本文时还没有计划。当呈现一个range输入类型时,Firefox 将退回到一个简单的文本元素。

另一个获得广泛支持的新表单元素是 progress 元素。progress 元素完全按照您的预期工作;它以方便的可视化格式显示任务完成的百分比。

进步可以是确定的,也可以是不确定的。可以把不确定的进度想象成一个花费未知时间的任务,但是你要向用户保证已经取得了一些进展。要显示不确定的进度元素,只需包含一个没有属性的元素:

<progress></progress>

不确定的进度条通常显示一个移动的进度条,但没有总完成百分比的指示器。

`images

另一方面,一个确定的进度条以百分比的形式显示已完成的工作。要触发一个确定的进度条显示,在元素上设置valuemax属性。通过将您设置的value除以您设置的max来计算显示为已完成的条形的百分比。为了便于计算,它们可以是您选择的任何值。例如,要显示 30%的完成情况,我们可以创建一个进度元素,如:

<progress value=”30” max=”100”></progress>

通过设置这些值,用户可以快速看到长时间运行的操作或多步骤流程完成了多少。使用脚本来更改 value 属性,可以很容易地更新显示来指示朝着最终目标的进展。

`images

这里有龙

布莱恩说:“据说历史上‘这里有龙’这个短语被用来在地图上标示潜伏着未知危险的危险区域。下面的表单元素也是如此。尽管它们被详细说明,并且已经存在了很长时间,但是大多数都缺乏实际的执行。

因此,从现在到浏览器开发人员有机会参与设计、磨平粗糙的边缘,并以反馈和变化作出回应的这段时间,预计会有很大的变化。不要认为下面的组件是不可避免的,把它们看作 HTML5 表单发展方向的标志。如果你今天试图使用它们,风险由你自己承担……”

计划中但尚未得到广泛支持的其他表单元素包括表 8-2 中列出的元素。

尽管这些元素的一些早期实现开始出现在前沿浏览器中(例如,Opera 中的日期时间显示,如图 8-3 中所示),但我们在本章中不会关注它们,因为它们可能会经历重大的变化。敬请关注未来的改版!

Image

图 8-3。显示日期时间类型的输入

使用 HTML5 表单 API

现在我们已经花了一些时间来熟悉新的表单元素类型,让我们来看看新旧表单控件上都存在的属性和 API。其中许多都是为了减少创建强大的 web 应用用户界面所需的脚本数量。您可能会发现,新的属性为您提供了从未考虑过的增强用户界面的能力。或者,至少,您可以删除现有页面中的脚本块。

新的表单属性和功能

首先,我们将考虑新的属性、功能和一些以前在 HTML 早期版本中不存在的元素。像新的输入类型一样,现在使用这些属性通常是安全的,不管您的目标浏览器是否支持它们。这是因为如果浏览器不理解这些属性,那么今天市场上的任何浏览器都会安全地忽略这些属性。

占位符属性

属性为输入控件提供了一种简单的方式来提供描述性的替代提示文本,只有当用户还没有输入任何值时才会显示。这在许多现代用户界面框架中很常见,流行的 JavaScript 框架也提供了对这一特性的模拟。然而,现代浏览器内置了这一功能。

要使用这个属性,只需将它添加到一个带有文本表示的输入中。这包括基本的文本类型,以及诸如emailnumberurl等语义类型。

<label>Runner: <input name="name" placeholder="First and last name"></label>

在现代浏览器中,这会导致该字段显示一个模糊的占位符文本,当用户或应用将焦点放在该字段上时,或者当存在一个值时,该文本就会消失。

`images

当在不支持的浏览器中运行时,相同的属性将被忽略,导致显示默认的字段行为。

`images

同样,只要在字段中输入值,占位符文本就不会出现。

`images

自动完成属性

Internet Explorer 5.5 中引入的autocomplete属性终于被标准化了。万岁!(浏览器几乎从一开始就支持该属性,但是拥有一个指定的行为对每个人都有帮助。)

autocomplete属性告诉浏览器是否应该保存这个输入的值以备将来使用。例如:

<input type="text" name="creditcard" autocomplete="off">

应该使用autocomplete属性来保护敏感的用户数据不被不安全地存储在本地浏览器文件中。表 8-3 显示了不同的行为类型。

`images

自动对焦属性

属性让开发人员指定给定的表单元素应该在页面加载时立即获得输入焦点。每页只有一个属性应该指定autofocus属性。如果多个控件设置为自动聚焦,则行为未定义。

`images 注意如果您的内容被呈现到门户或共享内容页面中,每页仅一个自动对焦控件是难以实现的。如果你不能完全控制页面,不要依赖自动对焦。

要将焦点自动设置到一个控件上,比如一个搜索文本字段,只需在该元素上设置autofocus属性:

<input type="search" name="criteria" autofocus>

像其他布尔属性一样,真实情况下不需要指定值。

`images 注意如果用户不希望改变焦距,自动对焦会让他们很恼火。许多用户利用击键来导航,而将焦点切换到表单控件会破坏这种能力。只有当一个表单控件应该接受所有默认键时,才使用它。

拼写检查属性

可以在具有文本内容的输入控件以及 textarea 上设置拼写检查属性。当设置时,它向浏览器建议是否应该给出拼写反馈。这个元素的一个正常表示是在文本下画一条红色虚线,它不映射当前设置的字典中的任何条目。这提示用户仔细检查拼写或从浏览器本身获得建议。

注意,spellcheck属性需要一个值。不能只在元素上单独设置属性。

<textarea id=”myTextArea” spellcheck=”true”>

还要注意,大多数浏览器默认打开拼写检查,所以除非元素(或其父元素之一)关闭拼写检查,否则它将默认显示。

列表属性和数据列表元素

属性和元素结合起来让开发者为输入指定一个可能值的列表。要使用这种组合:

  1. 在文档中创建一个datalist元素,将它的id设置为一个惟一的值。数据列表可以位于文档中的任何位置。
  2. 根据需要用尽可能多的option元素填充datalist,以表示控件值的完整建议集。例如,代表电子邮件联系人的datalist应该包含所有联系人的电子邮件地址,作为单独的option子节点。<datalist id="contactList">     <option value="x@example.com" label="Racer X">     <option value="peter@example.com" label="Peter"> </datalist>
  3. 通过将list属性设置为相关datalistid值,将输入元素链接到datalist<input type="email" id="contacts" list="contactList">

在支持的浏览器上,这将生成如下所示的自定义列表控件:

`images

最小值和最大值属性

正如我们之前在<input type="range">的例子中看到的,minmax属性允许将数字输入限制为最小和最大值。可以根据需要提供这些属性中的一个、两个或两个都不提供,并且输入控件应该相应地调整以增加或减少可接受值的范围。例如,要创建一个表示从 0%到 100%的能力置信度的范围控件,可以使用以下代码:

<input id="confidence" name="level" type="range" min="0" max="100" value="0">

这将创建一个范围控件,其最小值为 0,最大值为 100,巧合的是,这两个值都是该控件的默认值。

步骤属性

此外,对于需要数值的输入类型,step属性指定了调整范围时值的增量或减量的粒度。例如,我们上面列出的置信水平范围控制可以用五的step属性来设置,如下所示:

<input id="confidence" name="level" type="range" min="0" max="100" step="5" value="0">

这将把可接受的值限制为从起始值开始的 5 个增量。换句话说,根据输入的浏览器表示,通过键入的输入或通过滑块控件,只允许 0、5、10、15、… 100。

默认的step值取决于它所应用的控制类型。对于range输入,默认步长为一步。为了配合step属性,HTML5 在 input 元素上引入了两个允许控制值的函数:stepUpstepDown

如您所料,这些函数分别递增或递减当前值。如您所料,该值增加或减少的量就是该步骤的值。因此,数字输入控件的值可以在没有用户直接输入的情况下进行调整。

valueas number 函数

新的valueAsNumber函数是一种将控件的值从文本转换成数字的简便方法……反之亦然!这是因为valueAsNumber既是 getter 函数也是 setter 函数。当作为 getter 调用时,valueAsNumber函数将输入字段的文本值转换成允许计算的数字类型。如果文本值没有完全转换成number类型,那么将返回NaN值(不是数字)。

valueAsNumber也可用于将输入值设置为数字类型。例如,我们的置信范围可以使用以下呼叫来设置:

document.getElementById("confidence").valueAsNumber(65);

确保数字满足minmaxstep的要求,否则将抛出错误。

所需属性

如果任何输入控件设置了required属性,那么在提交表单之前必须设置一个值。例如,要根据需要设置文本输入字段,只需添加如下所示的属性:

<input type="text" id="firstname" name="first" required>

如果这个字段没有设置值,无论是通过编程还是由用户设置,提交这个表单的能力将被阻止。属性是最简单的表单验证类型,但是验证的能力非常强大。现在让我们更详细地讨论表单验证。

通过验证检查表单

在我们深入细节之前,让我们回顾一下表单验证到底需要什么。表单验证的核心是一个检测无效控制数据并为最终用户标记这些错误的系统。换句话说,表单验证是一系列的检查和通知,让用户在将表单提交给服务器之前更正表单的控件。

但是什么是表单验证呢?

这是一种优化。

表单验证是一种优化,因为它本身不足以保证提交给服务器的表单是正确和有效的。这是一种优化,因为它旨在帮助 web 应用快速失败。换句话说,最好是使用浏览器的内置处理来通知用户页面中包含无效的表单控件。为什么要花费网络往返的费用,只是为了让服务器通知用户数据输入中有一个打字错误呢?如果浏览器拥有在错误离开客户端之前捕捉错误的所有知识和能力,我们应该利用这一点。

但是,浏览器表单检查不足以处理所有错误。

恶意还是误解?

Brian 说:“尽管 HTML5 规范在提高浏览器中检查表单的能力方面取得了很大进步,但它仍然不能取代服务器验证。可能永远不会。

显然,有许多错误情况需要服务器交互来验证,比如信用卡是否被授权进行购买,甚至是基本的身份验证。然而,即使是普通的验证也不能仅仅依赖于客户端。一些用户可能正在使用不支持表单验证功能的浏览器。一些人可能会完全关闭脚本,这最终会禁用除了最简单的基于属性的验证器之外的所有验证器。然而,其他用户可以利用各种工具,如 Greasemonkey 浏览器插件来修改页面内容,以适应他们的需要。呃,内容。这可能包括删除所有表单验证检查。最终,依赖客户端验证作为检查任何重要数据的唯一方法是不够的。如果它存在于客户端,就可以被操纵。

HTML5 表单验证让用户快速获得重要的反馈,但不要依赖它来获得绝对的正确性!"

也就是说,HTML5 引入了八种简便的方法来加强表单控件输入的正确性。让我们依次检查它们,从让我们访问它们状态的对象开始:ValidityState

可以从支持 HTML5 表单验证的浏览器中的任何表单控件访问ValidityState:

var valCheck = document.myForm.myInput.validity;

这个简单的命令获取一个名为myInput的表单元素的ValidityState对象的引用。该对象包含对八种可能的有效性状态的方便引用,以及一个全面的有效性汇总检查。您可以通过调用以下命令来获取该表单的整体状态:

valCheck.valid

这个调用将提供一个布尔值,通知我们这个特定的表单控件当前是否满足所有的有效性约束。把valid标志看作一个总结:如果所有八个约束都通过了,那么valid标志将为真。否则,如果任何有效性约束失败,valid属性将为假。

![`images](https://gitee.com/OpenDocCN/vkdoc-html-css-zh/raw/master/docs/pro-h5-prog/img/square.jpg) `ValidityState`物体是一个活的物体。一旦您获取了对它的引用,您就可以保持对它的控制,当发生变化时,它返回的有效性检查将根据需要进行更新。

如前所述,任何给定的表单元素都有八种可能的有效性约束。通过访问具有适当名称的字段,可以从ValidityState访问每一个。让我们看看它们是什么意思,如何在表单控件上执行它们,以及如何使用ValidityState来检查它们:

值缺失

目的:确保在这个表单控件上设置了一些值

用法:将表单控件上的required属性设置为 true

用法举例 : <input type="text" name="myText" **required>**

细节:如果在表单控件上设置了required属性,那么控件将处于无效状态,除非用户或编程调用为该字段设置一些值。例如,空白文本字段将无法通过要求的检查,但只要输入任何文本,就会通过检查。为空时,valueMissing将返回 true。

类型不匹配

目的:保证值的类型符合预期(数字、电子邮件、URL 等等)

用法:在表单控件上指定一个合适的type属性

用法举例 : <input type="email" name="myEmail">

细节:特殊的表单控件类型不只是针对定制的手机键盘!如果浏览器可以确定输入到表单控件中的值不符合该类型的规则(例如,没有@符号的电子邮件地址),浏览器可以将该控件标记为类型不匹配。另一个例子是不能解析为有效数字的数字字段。无论哪种情况,typeMismatch都将返回true

模式匹配

目的:在表单控件上强制执行任何模式规则集,该规则集详细说明了特定的有效格式

用法:用合适的模式设置表单控件上的pattern属性

用法举例 : <input type="number" name="creditcardnumber" pattern="[0-9]{16}" title="A credit card number is 16 digits with no spaces or dashes">

细节:pattern属性为开发人员提供了一种强大而灵活的方式,在表单控件的值上实施正则表达式模式。当在控件上设置模式时,只要值不符合模式的规则,patternMismatch就会返回 true。为了帮助用户和辅助技术,你应该在任何模式控制的字段上设置title来描述格式的规则。

工具长

目的:确保一个值不包含太多字符

用法:在表单控件上放置一个maxLength属性

用法举例 : <input type="text" name="limitedText" **maxLength="140"**>

细节:如果值长度超过了maxLength,这个幽默命名的约束将返回 true。虽然表单控件通常会尝试在用户输入时强制最大长度,但某些情况(包括编程设置)可能会导致该值超过最大值。

支流

目的:强制数值控制的最小值

用法:设置一个min属性的最小允许值

用法举例 : <input type="range" name="ageCheck" **min="18"**>

细节:在任何进行数值范围检查的表单控件中,数值都有可能被临时设置在允许的范围之下。在这些情况下,ValidityState将为rangeUnderflow字段返回 true。

范围溢出

目的:强制数值控制的最大值

用法:设置一个max属性的最大允许值

用法举例 : <input type="range" name="kidAgeCheck" **max="12"**>

细节:类似于它的对应物rangeUnderflow,如果一个表单控件的值大于max属性,这个有效性约束将返回true

stepMismatch

目的:保证一个值符合minmaxstep的组合

用途:设置步长属性,指定数值的粒度步长

用法举例 : <input type="range" name="confidenceLevel" min="0" max="100" **step="5"**>

细节:该约束增强了minmaxstep组合的健全性。具体来说,当前值必须是最小值加上步长的倍数。例如,从 0 到 100 的范围,步长为每 5 步,如果stepMismatch不返回 true,则不允许值为 17。

客户错误

目的:处理应用代码明确计算和设置的错误

用法:调用setCustomValidity(message)将表单控件置于customError状态

用法举例 : passwordConfirmationField.setCustomValidity("Password values do not match.");

细节:对于内置有效性检查不适用的情况,自定义有效性错误就足够了。每当一个字段不符合语义规则时,应用代码应该设置一个自定义的有效性消息。

自定义有效性的一个常见用例是当控件之间没有实现一致性时,例如,如果密码确认字段不匹配。(我们将在“实用的额外功能”部分深入研究这个具体的例子。)无论何时设置自定义有效性消息,控件都将无效,并将customError约束作为true返回。要清除错误,只需用空字符串值调用控件上的setCustomValidity("")

验证字段和功能

总之,这八个约束允许开发人员找出给定表单控件未通过验证检查的确切原因。或者,如果您不关心是哪个具体原因导致了失败,只需访问ValidityState上的布尔值valid;它是其他八个约束的集合。如果所有八个约束都返回false,那么valid字段将返回true。表单控件上还有一些其他有用的字段和函数,可以帮助您进行验证检查编程。

will validate 属性

属性仅仅表明是否在这个表单控件上检查验证。如果存在上述任何约束条件,例如required属性、pattern属性等。-----------------------------------------------------------------------------,那么,-----------------------------------------------------------------------《??》将会让您知道将会强制执行验证检查。

检查有效性功能

checkValidity函数允许您在没有任何显式用户输入的情况下检查表单的有效性。通常,每当用户或脚本代码提交表单时,都会检查表单的有效性。这个函数允许在任何时候进行验证。

![`images](https://gitee.com/OpenDocCN/vkdoc-html-css-zh/raw/master/docs/pro-h5-prog/img/square.jpg) 注意在表单控件上调用`checkValidity`并不仅仅是检查有效性,它会导致所有的结果事件和 UI 触发器发生,就好像表单已经被提交了一样。

验证消息属性

目前的浏览器版本还不支持这个属性,但是当你读到这篇文章的时候,可能已经支持了。validationMessage属性允许您以编程方式查询浏览器基于当前验证状态显示的本地化错误消息。例如,如果一个required字段没有值,浏览器可能会向用户显示一条错误消息“这个字段需要一个值。”一旦得到支持,这就是由validationMessage字段返回的文本字符串,它将根据控件的当前验证状态进行调整。

验证反馈

关于验证反馈……到目前为止,我们避免的一个话题是浏览器应该如何以及何时向用户提供关于验证错误的反馈。该规范没有规定如何更新用户界面来显示错误信息,现有的实现也有很大的不同。以歌剧为例。在 Opera 10.5 中,浏览器通过用弹出消息和闪烁的红色字段标记错误字段来指示发生了验证错误:

`images

相比之下,在撰写本文时,谷歌 Chrome 13 浏览器只导航到违规字段,并在发现错误时将焦点放在那里。什么是正确的行为?

两者都没有指定。但是,如果您想在验证错误发生时控制显示给用户的反馈,有一个合适的处理程序可以帮您做到这一点:invalid事件。

每当检查表单的有效性时——无论是由于表单被提交,还是由于直接调用了checkValidity函数——任何处于无效状态的表单都将被传递一个invalid事件。可以忽略、观察甚至取消该事件。要将一个事件处理程序添加到接收该通知的字段中,添加一些类似于清单 8-2 的代码。

清单 8-2。为无效事件添加事件处理程序

`// event handler for "invalid" events
function invalidHandler(evt) {
  var validity = evt.srcElement.validity;

// check the validity to see if a particular constraint failed
  if (validity.valueMissing) {
    // present a UI to the user indicating that the field is missing a value
  }

// perhaps check additional constraints here…

// If you do not want the browser to provide default validation feedback,
  // cancel the event as shown here
  evt.preventDefault();
}

// register an event listener for "invalid" events
myField.addEventListener("invalid", invalidHandler, false);`

让我们把这段代码分解一下。

首先,我们声明一个处理程序来接收invalid事件。我们在处理程序中做的第一件事是检查事件的来源。回想一下,invalid事件是在表单控件上触发的,带有一个验证错误。因此,事件的srcElement将是行为不端的表单控件。

从源开始,我们获取validity对象。使用这个ValidityState实例,我们可以检查它的单个约束字段,以确定到底哪里出错了。在这种情况下,因为我们知道我们的字段有一个required属性,我们首先检查是否违反了valueMissing约束。

如果检查成功,我们可以修改页面上的用户界面,通知用户需要为出错的字段输入一个值。也许可以显示一个警告或信息错误区域?这由你来决定。

一旦我们告诉用户错误是什么以及如何纠正它,我们需要决定是否希望浏览器本身显示其内置的反馈。默认情况下,浏览器会这样做。为了防止浏览器显示自己的错误信息,我们可以调用evt.preventDefault()来停止默认处理,完全由我们自己来处理。

再一次,这里的选择是你的。HTML5 表单 API 为您提供了实现自定义 API 或恢复默认浏览器行为的灵活性。

关闭验证

尽管验证 API 背后有强大的功能,但还是有……(咳咳)正当的理由让你想关闭对一个控件或整个表单的验证。最常见的原因是,您可能会选择提交表单的临时内容,以便以后保存或检索,即使这些内容还不是非常有效。

想象一下这样一种情况,一个用户正在输入一个复杂的订单输入表单,但是在这个过程的中途需要跑一趟腿。理想情况下,您可以为用户提供一个“保存”按钮,通过将表单的值提交给服务器来保存这些值。但是,如果表单只完成了一部分,验证规则可能会阻止提交内容。如果用户由于意外中断而不得不完成或放弃表单,她会非常不高兴。

为了处理这个问题,可以用属性noValidate编程设置表单本身,这将导致它放弃任何验证逻辑,只提交表单。当然,这个属性可以通过脚本或原始标记来设置。

关闭验证的一个更有用的方法是在控件上设置一个formNoValidate属性,比如表单提交按钮。以下面的提交按钮,设置为"保存"按钮为例:

<input type="submit" formnovalidate name="save" value="Save current progress"> <input type="submit" name="process" value="Process order">

这个代码片段将创建两个普通外观的提交按钮。第二个将像往常一样提交表单。然而,第一个按钮被标记了noValidate属性,导致在使用它时所有的验证都被绕过。这允许将数据提交给服务器,而无需检查正确性。当然,您的服务器需要设置为处理未经验证的数据,但是最佳实践表明这应该是始终如此。

使用 HTML5 表单构建应用

现在,让我们使用本章描述的工具创建一个简单的注册页面,展示 HTML5 表单的新特性。回到我们熟悉的 Happy Trails Running Club,我们将创建一个包含新表单元素和验证的比赛注册页面。

和往常一样,我们在这里展示的演示文件的源代码可以在 code/forms 文件夹中找到。因此,我们将减少对 CSS 和外围标记的关注,更多地关注页面本身的核心。话虽如此,让我们先来看看图 8-4 所示的完成页面,然后把它分成几个部分,逐一解决。

Image

图 8-4。带有比赛注册表单的示例页面

这个注册页面展示了我们在本章中探索过的许多元素和 API,包括验证。尽管实际显示在您的浏览器上可能会有所不同,但即使浏览器不支持某个特定功能,它也会正常降级。

继续编码!

页眉、导航和页脚在我们之前的例子中已经出现过了。该页面现在包含一个<form>元素。

        <form name="register">           <p><label for="runnername">Runner:</label>              <input id="runnername" name="runnername" type="text"                     placeholder="First and last name" required></p>           <p><label for="phone">Tel #:</label>              <input id="phone" name="phone" type="tel"                     placeholder="(xxx) xxx-xxx"></p>           <p><label for="emailaddress">E-mail:</label>              <input id="emailaddress" name="emailaddress" type="email"                     placeholder="For confirmation only"></p>           <p><label for="dob">DOB:</label>              <input id="dob" name="dob" type="date"                     placeholder="MM/DD/YYYY"></p>

在第一部分,我们看到了四个主要输入的标记:姓名、电话、电子邮件和生日。对于每个控件,我们都设置了一个带有描述性文本的<label>,并使用for属性将它绑定到实际控件。我们还设置了占位符文本,向用户显示内容类型的描述。

对于跑步者姓名文本字段,我们通过设置required属性使其成为必需值。如果没有输入任何内容,这将导致表单验证使用一个valueMissing约束。在电话输入上,我们已经声明它的类型是tel。您的浏览器可能会以不同的方式显示该字段,也可能不会提供优化的键盘。

类似地,电子邮件字段被标记为类型e-mail。任何特定的处理都取决于浏览器。如果一些浏览器检测到输入的值不是有效的电子邮件,它们会抛出一个typeMismatch约束。

最后,出生日期字段被声明为类型date。还没有多少浏览器支持这一点,但是当它们支持时,它们会在这个输入上自动呈现一个日期选择控件。

          <fieldset>             <legend>T-shirt Size: </legend>             <p><input id="small" type="radio" name="tshirt" value="small">                <label for="small">Small</label></p>             <p><input id="medium" type="radio" name="tshirt" value="medium">                <label for="medium">Medium</label></p>             <p><input id="large" type="radio" name="tshirt" value="large">                <label for="large">Large</label></p>             <p><label for="style">Shirt style:</label>                <input id="style" name="style" type="text" list="stylelist" title="Years of                           participation"></p>             <datalist id="stylelist">              <option value="White" label="1st Year">              <option value="Gray" label="2nd - 4th Year">              <option value="Navy" label="Veteran (5+ Years)">             </datalist>           </fieldset>

在下一节中,我们将设置用于 t 恤选择的控件。前几个控件是用于选择衬衫尺码的一组标准单选按钮。

下一节更有趣。这里,我们使用了list属性和它对应的<datalist>元素。在<datalist>,中,我们声明了一组应该显示在这个列表中的类型,它们具有不同的值和标签,代表基于退伍军人身份的可用 t 恤类型。虽然这个列表非常简单,但是同样的技术也可以用于很长的动态元素列表。

         <fieldset>             <legend>Expectations:</legend>             <p>             <label for="confidence">Confidence:</label>             <input id="confidence" name="level" type="range"                    onchange="confidenceDisplay.value=(value +'%')"                    min="0" max="100" step="5" value="0">             <output id="confidenceDisplay">0%</output></p>             <p><label for="notes">Notes:</label>                <textarea id="notes" name="notes" maxLength="140"></textarea></p>          </fieldset>

在控件的最后一部分,我们为用户创建了一个滑块来表达他或她完成比赛的信心。为此,我们使用类型为range的输入。因为我们的置信度是用百分比来衡量的,所以我们在输入上设置了一个minimummaximumstep值。这些强制约束在正常的百分比范围内。此外,我们将值的移动限制为 5%的步长增量,如果您的浏览器支持范围滑块界面控件,您将能够观察到这一点。尽管不可能通过简单的控件交互来触发它们,但对于rangeUnderflowrangeOverflowstepMismatch,该控件上可能存在验证约束。

因为默认情况下,范围控件不显示其值的文本表示,所以我们将为我们的应用添加一个

。将通过范围控件的onchange处理程序来操纵confidenceDisplay,但是我们将在一分钟后看到它的运行。

最后,我们添加一个<textarea>来包含注册人的任何额外注释。通过在 notes 控件上设置一个maxLength约束,我们允许它实现一个tooLong约束,如果一个很长的值被粘贴到字段中的话。

         <p><input type="submit" name="register" value="Register"></p>         </form>

我们用一个提交按钮来结束我们的控件部分,这个按钮将发送我们的表单注册。在这个默认示例中,注册实际上没有被发送到任何服务器。

我们仍然需要描述一些脚本:我们将如何覆盖浏览器内置的表单验证反馈,以及我们将如何监听事件。尽管您可能会发现浏览器对表单错误的默认处理是可以接受的,但了解您的选择总是有好处的。

``

这个脚本展示了我们如何覆盖验证错误的处理。我们从注册特殊事件类型invalid的事件监听器开始。为了捕获所有表单控件上的invalid事件,我们在表单上注册了处理程序,确保注册了事件捕获,以便事件到达我们的处理程序。

// register an event handler on the form to
// handle all invalid control notifications document.register.addEventListener("invalid", invalidHandler, true);

现在,只要我们的任何表单元素触发了验证约束,我们的invalidHandler就会被调用。为了提供比一些主流浏览器默认情况下更微妙的反馈,我们将违规表单字段的标签涂成红色。为此,首先我们通过遍历父节点来定位<label>

`// find the label for this form control
var label = evt.srcElement.parentElement.getElementsByTagName("label")[0];

// set the label's text color to red
label.style.color = 'red';`

将标签设置为可爱的红色后,我们希望阻止浏览器或任何其他处理程序重复处理我们的无效事件。利用 DOM 的强大功能,我们调用preventDefault()来停止任何浏览器对事件的默认处理,调用stopPropagation()来阻止其他处理程序访问。

`// stop the event from propagating higher
evt.stopPropagation();

// stop the browser's default handling of the validation error
evt.preventDefault();`

通过几个简单的步骤,我们提供了一个经过验证的表单,带有我们自己的特殊界面验证代码!

实用的临时演员

有时有些技术不适合我们的常规例子,但仍然适用于许多类型的 HTML5 应用。我们在这里向你展示一些简短但常见的实用附加功能。

密码是:验证!

为自定义验证器使用 HTML5 表单验证支持的一种简便方法是实现在密码更改期间验证密码的常用技术。标准技术是提供两个密码字段,这两个字段必须匹配才能成功提交表单。这里,我们提供了一种方法来利用setCustomValidation调用来确保在表单提交之前两个密码字段匹配。

回想一下,customError验证约束让您有机会在标准约束规则不适用时在表单控件上设置错误。具体来说,触发customError约束的一个很好的理由是当验证依赖于多个控件的并发状态时,比如这里的两个密码字段。

因为一旦获得对对象的引用,就假定对象是活动的,所以每当密码字段不匹配时,在对象上设置自定义错误,每当字段再次匹配时,立即清除错误是一个好主意。实现这一点的一个好方法是对密码字段使用 onchange 事件处理程序。

<form name="passwordChange">     <p><label for="password1">New Password:</label>     <input type="password" id="password1" onchange="checkPasswords()"></p>
`    


    

`

正如您在这里看到的,在一个有两个密码字段的简单表单上,我们可以注册一个函数,在每次其中一个密码的值发生变化时执行。

`function checkPasswords() {
  var pass1 = document.getElementById("password1");
  var pass2 = document.getElementById("password2");

if (pass1.value != pass2.value)
    pass1.setCustomValidity("Your passwords do not match. Please recheck that your
          new password is entered identically in the two fields.");
  else
    pass1.setCustomValidity("");
}`

这里有一种处理密码匹配的方法。只需获取两个密码字段的值,如果它们不匹配,就设置一个自定义错误。出于验证例程的考虑,只在两个密码字段中的一个上设置错误可能是可以接受的。如果它们匹配,将空字符串设置为自定义错误以清除它;这是删除自定义错误的指定方式。

一旦您在字段上设置了错误,您就可以使用本章前面描述的方法向用户显示反馈,并让她按照预期更改密码。

表单是有样式的

为了帮助开发人员区分具有特定验证特征的表单控件,CSS 的开发人员添加了一组伪类,用于根据表单控件的有效性状态来设置表单控件的样式。换句话说,如果您希望页面上的表单元素根据它们当前是否符合验证来自动更改样式,您可以在规则中设置这些样式伪类。这些函数与链接上的:visited:hover等长期存在的伪类非常相似。表 8-4 显示了为 CSS 选择器 4 级规范提出的新的伪类可以用来选择表单元素。

`images

`images

有了这些伪类,很容易在页面中用可视样式标记表单控件,这些样式会随着表单元素本身的调整而改变。例如,要用红色背景显示所有无效的表单元素,只需使用 CSS 规则:

:invalid {     background-color:red; }

这些伪类将在用户输入时自动调整。不需要代码!

总结

在这一章中,你已经看到了如何利用 HTML5 中的新元素、属性和 API 将旧的 HTML 表单变成新的东西。我们已经看到了高级输入类型的新控件,甚至更多。我们已经看到了如何将客户端验证直接集成到表单控件中,以防止不必要的服务器往返处理坏数据。总的来说,我们已经看到了减少创建全功能应用用户界面所需的脚本数量的方法。

在下一章中,我们将研究浏览器如何给你能力产生独立的执行环境来处理长时间运行的任务:HTML5 Web 工作器。

九、使用 HTML5 拖放

自最初的苹果麦金塔电脑问世以来,传统的拖放操作一直很受用户欢迎。但是今天的计算机和移动设备有更复杂的拖放行为。拖放用于文件管理、传输数据、绘制图表和许多其他操作,在这些操作中,用手势移动对象比用键盘命令更自然。在街上问开发人员拖放包含什么,你可能会得到无数不同的答案,取决于他们最喜欢的程序和当前的工作任务。问非技术用户关于拖拽的问题,他们可能会茫然地盯着你;这个特性现在已经在计算机中根深蒂固,以至于不再经常被点名。

然而,HTML 在其存在的许多年中并没有将拖放作为核心特性。虽然一些开发人员已经使用内置功能来处理低级鼠标事件,作为破解原始拖放功能的一种方式,但与桌面应用中已经存在了几十年的拖放功能相比,这些努力就相形见绌了。随着一组精心设计的拖放功能的出现,HTML 应用向与桌面应用的功能相匹配又迈进了一步。

网络拖放:故事到此为止

你可能已经在网上看到过拖放的例子,并且想知道这些是不是 HTML5 拖放的用法。答案?可能不会。

原因是 HTML 和 DOM 从 DOM 事件的早期就已经公开了低级的鼠标事件,这对于有创造力的开发人员来说已经足够制作一个基本的拖放功能了。当与 CSS 定位结合使用时,通过创建复杂的 JavaScript 库和对 DOM 事件的深入了解,可以近似一个拖放系统。

例如,通过处理以下 DOM 事件,如果您编写了一组逻辑步骤(和一些注意事项),就有可能在网页中移动项目:

  • mousedown:用户正在开始一些鼠标操作。(是拖还是只是点?)
  • mousemove:如果鼠标还没有抬起,移动操作开始。(是拖还是选?)
  • mouseover:鼠标已经移动到一个元素上。(是我想顺道拜访的人之一吗?)
  • 鼠标留下了一个元素,它将不再是一个可以放置的地方。(我需要画反馈吗?)
  • mouseup:鼠标已经释放,可能触发了拖放操作。(根据起点,是否应在此位置完成卸货?)

虽然使用低级事件来建模一个粗糙的拖放系统是可能的,但是它有一些明显的缺点。首先,处理鼠标事件所需的逻辑比您想象的要复杂,因为每个列出的事件都有许多必须考虑的边缘情况。尽管有些人在之前的名单中,但现实是他们中有足够多的人值得拥有自己的一章。在这些事件中,CSS 必须小心地更新,以向用户提供关于在任何特定位置拖放的可能性的反馈。

然而,一个更严重的缺点是,这种特定的拖放实现依赖于对系统的完全控制。如果你试图将你的应用内容和其他内容混合在同一个页面中,当不同的开发者开始利用事件来达到他们自己的目的时,事情很快就会失控。类似地,如果您试图从别人的代码中拖放内容,您可能会遇到麻烦,除非这两个代码库事先仔细协调。此外,即席拖放不与用户的桌面交互,也不跨窗口工作。

新的 HTML5 拖放 API 旨在解决这些限制,借鉴了其他用户界面框架中提供的拖放方式。

Image 注意即使实现得当,也要注意拖放在任何应用中的局限性。如果拖动行为被覆盖,使用拖动手势导航的移动设备可能无法正常工作。此外,拖放会干扰拖动选择。小心谨慎、适当地使用它。

html 5 拖放概述

如果您在 Java 或 Microsoft MFC 等编程技术中使用过拖放 API,那么您很幸运。新的 HTML5 拖放 API 严格按照这些环境的概念建模。开始很容易,但是掌握新的功能意味着您需要熟悉一组新的 DOM 事件,尽管这次是在更高的抽象层次上。

大局

学习新 API 最简单的方法是将它映射到您已经熟悉的概念上。如果你正在阅读一本关于 pro HTML5 编程的书,我们将大胆假设你在日常计算中对使用拖放很有经验。尽管如此,我们可以从一些主要概念的标准术语开始。

如图图 9-1 所示,当你(作为用户)开始一个拖放操作时,你是通过点击拖动指针开始的。开始拖动的项目或区域被称为拖动源。当您释放指针并完成操作时,您最终瞄准的区域或项目被称为拖放目标。当鼠标在页面上移动时,您可能会在实际释放鼠标之前遍历一系列拖放目标。

Image

图 9-1。拖动源和放下目标

目前为止,一切顺利。但是简单地按住鼠标并把它移动到应用的另一部分并不构成拖放。相反,是操作过程中的反馈促成了成功的交互。考虑你自己在过去的经历中对拖放的使用;最直观的是系统不断更新,让你知道如果你在这个时间点释放会发生什么:

  • 光标是否表明当前位置是有效的放置目标,或者它是否用“禁止”光标指示器暗示拒绝?
  • 光标是否向用户暗示操作将是移动、链接或复制,例如光标上的“加号”指示符?
  • 如果你现在释放鼠标,你所悬停的区域或目标是否会以任何方式改变它的外观,以表明它当前被选择为拖放?

为了在 HTML 拖放操作过程中向用户提供类似的反馈,浏览器将在单次拖动过程中发出一系列事件。这证明是非常方便的,因为在这些事件中,我们将完全有权力改变页面元素的 DOM 和样式,以给出用户期望的反馈类型。

除了拖放源和拖放目标之外,新 API 中还有一个关键概念需要学习:数据传输。该规范将数据传输描述为一组对象,用于公开拖放操作背后的拖动数据存储。然而,把数据传输看作是拖放的中央控制可能更容易。操作类型(例如,移动、复制或链接)、拖动过程中用作反馈的图像以及数据本身的检索都在这里进行管理。

关于数据本身,完成拖放的数据传输机制直接解决了前面描述的旧的专门拖放技术的一个限制。数据传输机制的工作方式类似于网络协议协商,而不是强制所有的拖放源和拖放目标知道彼此。在这种情况下,协商是通过多用途互联网邮件交换(MIME)类型执行的。

Image 注意 MIME 类型与用于在电子邮件中附加文件的类型相同。它们是在所有类型的网络流量中普遍使用的互联网标准,在 HTML5 中非常常见。简而言之,MIME 类型是标准化的文本字符串,用于对未知内容的类型进行分类,例如“text/plain”表示纯文本,“image/png”表示 png 图像。

使用 MIME 类型的目的是允许源和目标协商哪种格式最适合放置目标的需要。如图 9-2 所示,在拖动启动期间,dataTransfer 对象加载了代表所有合理类型或“风格”的数据,通过这些数据可以进行传输。然后,当 drop 完成时,drop 处理程序代码可以扫描可用的数据类型,并决定哪种 MIME 类型格式最适合它的需要。

例如,假设网页中的列表项代表一个人。有许多不同的方式来表示一个人的数据;有些是标准的,有些不是。当拖动开始于一个特定的人的列表项时,拖动开始处理程序可以声明这个人的数据有几种格式,如表 9-1 所示。

`images

当放下完成时,放下处理程序可以查询可用数据类型的列表。从提供的列表中,处理程序可以选择最合适的类型。文本列表放置目标可以选择获取文本/普通“味道”的数据来检索人员的姓名,而更高级的控件可以选择检索并显示人员的 PNG 图像作为放置的结果。而且,如果源和目标在非标准类型上协调一致,目标也可以检索下落时人的年龄。

Image

图 9-2 。数据“风味”的拖放谈判

正是这个协商过程允许拖放源和拖放目标分离。只要拖动源以多种 MIME 类型提供数据,拖放目标就可以选择最适合其操作的格式,即使这两种格式来自不同的开发人员。在本章的后面部分,我们将探索如何使用更不寻常的 MIME 类型,比如文件。

要记住的事件

既然我们已经探索了拖放 API 的关键概念,那么让我们把重点放在可以在整个过程中使用的事件上。正如您将看到的,这些事件在比以前用来模拟拖放系统的鼠标事件更高的层次上操作。但是,拖放事件扩展了 DOM 鼠标事件。因此,如果需要的话,您仍然可以访问低级别的鼠标信息,比如坐标

传播与预防

但是在我们关注拖放本身之前,让我们回顾一下自从浏览器对 DOM Level 3 事件进行标准化以来就存在的两个 DOM 事件函数:stopPropagation 和 preventDefault 函数。

考虑页面中的一个元素嵌套在另一个元素中的情况。我们将它们分别称为子元素和父元素。子元素占据了父元素的部分可视空间,但不是全部。尽管在我们的例子中我们只提到了两个元素,但实际上一个网页通常有许多嵌套层次。

当用户在孩子身上点击鼠标时,哪个元素应该实际接收事件:孩子,父母,还是两者?如果两者都有,按什么顺序?这个问题的答案是由万维网联盟(W3C)在 DOM events 规范中确定的。在称为“事件捕获”的过程中,事件从父节点开始,通过中介,向下传递到最具体的子节点一旦孩子访问了事件,事件通过一个称为“事件冒泡”的过程流回元素层次结构这两个流一起允许开发人员以最适合其页面架构的方式捕捉和处理事件。只有实际注册了处理程序的元素才会处理事件,这使系统保持轻量级。总体方法是来自多个浏览器厂商的不同行为的折衷,并且与其他本地开发框架一致,其中一些是捕获的,一些是冒泡的。

但是,任何时候处理程序都可以对事件调用 stopPropagation 函数,这将阻止它进一步向下遍历事件捕获链或向上遍历冒泡阶段。

Image 注意微软在[ie.microsoft.com/testdrive/HTML5/ComparingEventModels](http://ie.microsoft.com/testdrive/HTML5/ComparingEventModels)提供了一个很棒的事件模型互动演示

浏览器对于如何处理一些事件也有默认的实现。例如,当用户单击页面链接时,默认行为是将浏览器导航到该链接指定的目的地。开发人员可以通过拦截处理程序中的事件并对其调用 preventDefault 来防止这种情况。这允许代码重写某些内置事件的默认行为。这也是开发人员在事件处理程序中取消拖放操作的方式。

在我们的拖放 API 示例中,stopPropagationpreventDefault都很方便。

拖放事件流

当用户在 HTML5 浏览器中启动拖放操作时,一系列事件在开始时触发,并在整个操作过程中持续。我们将在这里依次检查它们。

拖启动

当用户开始在页面中的元素上拖动时,dragstart 事件在该元素上触发。换句话说,一旦鼠标按下,用户移动鼠标,就会启动dragstart。dragstart 事件非常重要,因为它是唯一一个可以使用setData调用在dataTransfer上设置数据的事件。这意味着在一个dragStart处理程序中,需要设置可能的数据类型,以便在拖放结束时可以查询它们,如前所述。

拦截!

Brian 说:“如果你想知道为什么数据类型只能在dragStart事件期间设置,实际上有一个很好的理由。

因为拖放被设计成跨窗口和跨各种来源的内容工作,如果drag事件监听器能够在拖动经过它们时插入或替换数据,这将是一个安全风险。想象一下,插入了事件监听器的恶意代码段查询并替换了经过的任何拖动的拖动数据。这将歪曲拖动源的意图,因此禁止在开始后进行任何数据替换。"

拖动

拖动事件可以被认为是拖动操作的连续事件。当用户在页面上移动鼠标光标时,在拖动上重复调用drag事件。在操作过程中,拖动事件将每秒触发几次。虽然拖动反馈的视觉效果可以在drag事件中修改,但是dataTransfer上的数据是禁止的。

拖曳器

当拖动进入页面上的新元素时,会在该元素上触发一个dragenter事件。此事件是根据元素是否可以接收放下来设置放下反馈的好时机。

休假

相反,每当用户将拖动移出先前调用dragenter的元素时,浏览器将触发一个dragleave事件。此时可以恢复拖放反馈,因为鼠标不再位于该目标上方。

疏浚

在拖动操作过程中,当鼠标移动到一个元素上时,会频繁地调用dragover事件。与在拖动源上调用的对应拖动事件不同,此事件在鼠标的当前目标上调用。

下降

当用户释放鼠标时,在当前鼠标目标上调用drop事件。基于dataTransfer对象的结果,这是处理拖放的代码应该执行的地方。

承载

链中的最后一个事件dragend在拖动源上触发,表示拖动完成。它特别适合清理拖动过程中使用的state,因为不管拖放是否完成都会调用它。

总之,有很多方法可以拦截拖放操作并采取行动。拖拽事件链总结在图 9-3 中。

Image

图 9-3。拖放事件流

拖参

既然您已经看到了在拖放操作中可以触发的不同事件,您可能想知道如何将 web 应用中的元素标记为可拖动的。那很简单!

除了少数元素(如文本控件)之外,页面中的元素在默认情况下是不可拖动的。然而,为了将特定元素标记为可拖动的,您需要做的就是添加一个属性:draggable。

<div id=”myDragSource” **draggable=”true”**>

只需添加该属性,就可以让浏览器触发上述事件。然后,您只需要添加事件处理程序来管理它们。

转移和控制

在进入我们的例子之前,让我们更详细地评估一下dataTransfer对象。每个拖放事件都可以进行数据传输,如清单 9-1 所示。

清单 9-1。正在检索数据传输对象

Function handleDrag(evt) {     var transfer = evt.dataTransfer;     // … }

如清单 9-1 中所述,dataTransfer用于在源和目标之间的协商过程中获取和设置实际的丢弃数据。这是使用下列函数和属性完成的:

  • setData(format, data):在 dragStart 过程中调用该函数,可以注册一个 MIME 类型格式的传输项目。
  • getData(format):该功能允许检索给定类型的注册数据项。
  • types:该属性返回所有当前注册格式的数组。
  • items:这个属性返回一个所有项目及其相关格式的列表。
  • files:该属性返回任何与放置相关的文件。这将在后面的章节中详细讨论。
  • clearData():不带参数调用此函数,清除所有注册数据。用格式参数调用它只会移除该特定的注册。

拖动操作期间,还有两个函数可用于改变反馈:

  • setDragImage(element, x, y):告诉浏览器使用现有的图像元素作为拖动图像,该图像将显示在光标旁边,向用户提示拖动操作的效果。如果提供了 x 和 y 坐标,那么这些坐标将被视为鼠标的放置点。
  • 通过用提供的页面元素调用这个函数,你告诉浏览器把这个元素绘制成一个拖动反馈图像。

最后一组属性允许开发者设置和/或查询所允许的拖动操作的类型:

  • effectAllowed:将该属性设置为 none、copy、copyLink、copyMove、Link、linkMove、Move 或 all 中的一个,告诉浏览器只允许用户执行此处列出的操作类型。例如,如果设置了复制,则只允许复制操作,而移动或链接操作将被阻止。
  • dropEffect:该属性可用于确定当前正在进行哪种类型的操作,或者设置为强制特定的操作类型。操作类型包括复制、链接和移动。或者,可以设置值 none 来防止在该时间点发生任何丢弃。

总之,这些操作提供了对拖放的精细控制。现在,让我们看看他们的行动。

使用拖放功能构建应用

使用我们已经学过的概念,我们将在 Happy Trails Running Club 的主题中构建一个简单的拖放页面。该页面允许俱乐部比赛组织者将俱乐部成员拖入两个列表之一:赛车手和志愿者。为了将他们划分到不同的组别,参赛者将按照他们的年龄进行分类。另一方面,志愿者只按他们的名字排序,因为当他们不参加比赛时,他们的年龄并不重要。

列表的排序是自动完成的。应用本身将显示反馈,指示成员在两个列表中的适当放置区域,如图 9-4 所示。

Image

图 9-4。显示分类成列表的参赛者的示例页面

本示例的所有代码都包含在 code/draganddrop 目录中的本书示例中。我们将浏览页面并解释它在实践中是如何工作的。

首先,让我们看看页面的标记。在顶部,我们已经声明了我们俱乐部成员的数据(参见清单 9-2 )。

清单 9-2。显示可拖动成员姓名和年龄的标记

`

Drag members to either the Racers or Volunteers list.

      
  • Brian Albers
  •   
  • Frank Salim
  •   
  • Jennifer Clark
  •   
  • John Kemble
  •   
  • Lorraine Gaunce
  •   
  • Mark Wang
  •   
  • Morgan Stephen
  •   
  • Peter Lubbers
  • ` `  
  • Vanessa Combs
  •   
  • Vivian Lopez
`

如您所见,每个成员列表元素都被标记为draggable。这告诉浏览器让拖动在它们中的每一个上开始。接下来您会注意到,给定成员的年龄被编码为一个数据属性。data -符号是在 HTML 元素上存储非标准属性的标准方式。

我们的下一部分包含目标列表(见清单 9-3 )。

清单 9-3。下拉列表目标的标记

`

Racers (by Age):
    Volunteers (by Name):
      `

      被标识为racersvolunteers的无序列表是我们的成员将被插入的最终目的地。围绕它们的fieldsets在功能上相当于城堡周围的护城河。当用户拖入fieldset时,我们将知道他们已经退出了包含的列表,我们将相应地更新我们的视觉反馈。

      说到反馈,我们的页面中有一些 CSS 样式值得注意(见清单 9-4 )。

      清单 9-4。拖放演示的样式

      `#members li {
          cursor: move;
      }

      .highlighted {
          background-color: yellow;
      }

      .validtarget {
          background-color: lightblue;
      }`

      首先,我们确保源列表中的每个成员都显示一个移动光标。这给用户一个提示,即项目是可拖动的。

      接下来,我们定义两个样式类:highlightedvalidtarget。这些用于在拖放过程中绘制列表的背景颜色。在整个拖动过程中,validtarget背景将显示在我们的目的地列表中,以提示它们是有效的拖放目标。当用户实际上在目标列表上移动一个成员时,它将改变为highlighted样式,表明用户实际上在一个放置目标上。

      为了跟踪页面上的状态,我们将声明几个变量(见清单 9-5 )。

      清单 9-5。清单项目申报

      `    // these arrays hold the names of the members who are
          // chosen to be racers and volunteers, respectively
          var racers = [];
          var volunteers = [];

      // these variables store references to the visible
          // elements for displaying who is a racer or volunteer
          var racersList;
          var volunteersList;`

      前两个变量将作为内部数组,用来记录哪些成员在参赛者和志愿者列表中。后两个变量只是用来方便地引用无序列表,这些列表包含了各个列表中成员的可视化显示。

      现在,让我们设置所有的页面项目来处理拖放(见清单 9-6 )。

      清单 9-6。事件处理程序注册

      `    function loadDemo() {

      racersList = document.getElementById("racers");
             volunteersList = document.getElementById("volunteers");

      // our target lists get handlers for drag enter, leave, and drop
             var lists = [racersList, volunteersList];
             [].forEach.call(lists, function(list) {
                 list.addEventListener("dragenter", handleDragEnter, false);
                 list.addEventListener("dragleave", handleDragLeave, false);
                 list.addEventListener("drop", handleDrop, false);
             });

      // each target list gets a particular dragover handler
             racersList.addEventListener("dragover", handleDragOverRacers, false);
             volunteersList.addEventListener("dragover", handleDragOverVolunteers, false);

      // the fieldsets around our lists serve as buffers for resetting
             // the style during drag over
             var fieldsets = document.querySelectorAll("#racersField, #volunteersField");
             [].forEach.call(fieldsets, function(fieldset) {
                 fieldset.addEventListener("dragover", handleDragOverOuter, false);
             });

      // each draggable member gets a handler for drag start and end
             var members = document.querySelectorAll("#members li");
             [].forEach.call(members, function(member) {
                 member.addEventListener("dragstart", handleDragStart, false);
                 member.addEventListener("dragend", handleDragEnd, false);        });

      }

      window.addEventListener("load", loadDemo, false);`

      当窗口最初加载时,我们调用一个loadDemo函数来设置所有的拖放事件处理程序。它们中的大多数不需要事件捕获,我们将相应地设置捕获参数。racersListvolunteersList都将收到dragenter, dragleavedrop事件的处理程序,因为这些事件是针对投放目标的。每个列表都将接收一个单独的拖拽事件监听器,因为这将允许我们基于用户当前拖拽的目标轻松地更新拖拽反馈。

      如前所述,我们还在目标列表周围的字段集上添加了dragover处理程序。我们为什么要这样做?当一个拖拽退出我们的目标列表时,让detect变得更容易。虽然我们很容易检测到用户在我们的列表上拖动了一个项目,但是确定用户何时将项目从我们的列表中拖出就不那么容易了。这是因为当一个项目被拖出我们的列表时,dragleave 事件都会被触发。实质上,当您从父元素拖动到它包含的一个子元素上时,拖动将退出父元素并进入子元素。虽然这提供了很多信息,但实际上很难知道拖动何时离开父元素的外部边界。因此,我们将使用一个通知,通知我们正在拖动列表周围的元素,通知我们已经退出列表。稍后将提供更多相关信息。

      往出口走

      Brian 说:“拖放规范的一个更反直觉的方面是事件的顺序。虽然您可能认为被拖动的项目会在进入另一个目标之前退出一个目标,但是您错了!

      在从元素 A 拖动到元素 B 的过程中,事件的触发顺序是在元素 A 上触发dragleave事件之前在元素 B 上触发dragenter事件。这保持了与 HTML 鼠标事件规范的一致性,但这是设计中比较奇怪的方面之一。可以肯定的是,未来还会有更多这样的怪癖。"

      我们的最后一组处理程序在我们的初始列表中注册每个draggable俱乐部成员的dragstartdragend监听器。我们将使用它们来初始化和清理任何拖动。您可能会注意到,我们没有为drag事件添加处理程序,该事件会在拖动源上定期触发。因为我们不会更新被拖动项目的外观,所以在我们的例子中没有必要。

      现在,我们将根据事件处理程序通常触发的顺序,依次检查实际的事件处理程序(见清单 9-7 )。

      清单 9-7。 dragstart 事件处理程序

      `    // called at the beginning of any drag
          function handleDragStart(evt) {

      // our drag only allows copy operations
              evt.effectAllowed = "copy";

      // the target of a drag start is one of our members
              // the data for a member is either their name or age
              evt.dataTransfer.setData("text/plain", evt.target.textContent);
              evt.dataTransfer.setData("text/html", evt.target.dataset.age);

      // highlight the potential drop targets
              racersList.className = "validtarget";
              volunteersList.className = "validtarget";

      return true;
          }`

      在用户开始操作的draggable项上调用dragstart的处理程序。它是一个有点特殊的处理程序,因为它设置了整个流程的功能。首先,我们设置了effectAllowed,它告诉浏览器从这个元素拖动时只允许复制——不允许移动或链接。

      接下来,我们预加载所有可能的数据类型,这些数据可能会在成功放下后被请求。自然,我们希望支持元素的文本版本,所以我们设置 MIME 类型text/plain来返回节点draggable中的文本(即俱乐部成员的名字)。

      对于我们的第二种数据风格,我们希望 drop 操作传输关于拖动源的另一种类型的数据;在我们的例子中,是俱乐部成员的年龄。不幸的是,由于缺陷,并不是所有的浏览器都支持用户定义的 MIME 类型,比如application/x-age,这是最适合这种任意风格的。相反,我们将重用另一种普遍支持的 MIME 格式——text/html——暂时代表一种时代风格。希望 WebKit 浏览器将很快解决这个限制。

      不要忘记dragstart处理器是唯一可以设置数据传输值的处理器。为了防止恶意代码在拖动过程中更改数据,在其他处理程序中尝试这样做将会失败。

      我们在 start 处理程序中的最后一个动作纯粹是为了演示。我们将改变潜在拖放目标列表的背景颜色,给用户一个可能的提示。我们的下一个处理程序将在被拖动的项目进入和离开页面元素时处理事件(见清单 9-8 )。

      清单 9-8。 dragenter 和 dragleave 事件处理程序

      `    // stop propagation and prevent default drag behavior
          // to show that our target lists are valid drop targets
          function handleDragEnter(evt) {
              evt.stopPropagation();
              evt.preventDefault();
              return false;
          }

      function handleDragLeave(evt) {         return false;
          }`

      我们的演示不使用dragleave事件,我们处理它纯粹是为了说明的目的。

      然而,dragenter事件可以通过在有效的放置目标上触发时调用preventDefault来处理和取消。这通知浏览器当前目标是有效的放下目标,因为默认行为是假设任何目标都不是有效的放下目标。

      接下来,我们将看看拖拽处理程序(见清单 9-9 )。回想一下,每当拖动鼠标悬停在相关元素上时,这些按钮就会定时触发。

      清单 9-9。外集装箱拖拽搬运机

      `    // for better drop feedback, we use an event for dragging
          // over the surrounding control as a flag to turn off
          // drop highlighting
          function handleDragOverOuter(evt) {

      // due to Mozilla firing drag over events to
              // parents from nested children, we check the id
              // before handling
              if (evt.target.id == "racersField")
                racersList.className = "validtarget";

      else if (evt.target.id == "volunteersField")
                volunteersList.className = "validtarget";

      evt.stopPropagation();
              return false;
          }`

      我们的三个dragover处理程序中的第一个将仅用于调整拖动反馈。回想一下,当一个拖拽体离开了一个目标时,很难检测到,比如我们想要的参赛者和志愿者名单。因此,我们在列表周围的字段集上使用拖动移动来表示拖动已经离开了列表附近。这允许我们相应地关闭列表上的拖放高亮显示。

      请注意,如果用户停留在字段集区域,我们列出的简单代码将重复更改 CSS className。出于优化的目的,最好只改变一次className,因为这可能会导致浏览器做更多不必要的工作。

      最后,我们停止将事件传播到页面中的任何其他处理程序。我们不希望任何其他处理程序覆盖我们的逻辑。在接下来的两个dragover处理程序中,我们采用不同的方法(见清单 9-10 )。

      清单 9-10。目标列表拖拽处理程序

      `    // if the user drags over our list, show
          // that it allows copy and highlight for better feedback
          function handleDragOverRacers(evt) {
              evt.dataTransfer.dropEffect = "copy";
              evt.stopPropagation();
              evt.preventDefault();

      racersList.className = "highlighted";
              return false;     }

      function handleDragOverVolunteers(evt) {
              evt.dataTransfer.dropEffect = "copy";
              evt.stopPropagation();
              evt.preventDefault();

      volunteersList.className = "highlighted";
              return false;
          }`

      这两个处理程序虽然有些冗长,但还是完整地列出来,以阐明我们的演示。第一个处理参赛者列表中的拖拽事件,第二个处理志愿者列表中的dragover事件。

      我们采取的第一个动作是设置dropEffect来表明在这个节点上只允许复制,不允许移动或链接。这是一个很好的实践,尽管我们最初的dragstart处理程序已经将拖放操作限制为只复制。

      接下来,我们阻止其他处理程序访问该事件并取消它。取消拖拽事件有一个重要的功能:它告诉浏览器默认操作——不是允许在这里拖拽——是无效的。本质上,我们是在告诉浏览器,它不应该不允许拖放;因此,下降是允许的。虽然这看起来似乎违背直觉,但回想一下,preventDefault是用来告诉浏览器不要为一个事件做正常的内置操作。例如,在点击一个链接时调用preventDefault会告诉浏览器不要导航到该链接的引用。规范设计者本可以为这个dragover创建一个新的事件或 API,但是他们选择保留已经在整个 HTML 中使用的 API 模式。

      每当用户拖动我们的列表时,我们还会通过highlighted CSS 类将背景颜色改为黄色来给用户视觉反馈。拖放的主要工作是在 drop 处理程序中完成的,我们接下来将在清单 9-11 中研究它。

      清单 9-11。目标列表的删除处理器

      `    // when the user drops on a target list, transfer the data
          function handleDrop(evt) {
              evt.preventDefault();
              evt.stopPropagation();

      var dropTarget = evt.target;

      // use the text flavor to get the name of the dragged item
              var text  = evt.dataTransfer.getData("text/plain");

      var group = volunteers;
              var list  = volunteersList;

      // if the drop target list was the racer list, grab an extra
              // flavor of data representing the member age and prepend it
              if ((dropTarget.id != "volunteers") &&
                  (dropTarget.parentNode.id != "volunteers")) {
                  text = evt.dataTransfer.getData("text/html") + ": " + text;
                  group = racers;
                  list  = racersList;         }

      // for simplicity, fully clear the old list and reset it
              if (group.indexOf(text) == -1) {
                  group.push(text);
                  group.sort();

      // remove all old children
                  while (list.hasChildNodes()) {
                      list.removeChild(list.lastChild);
                  }

      // push in all new children
                  [].forEach.call(group, function(person) {
                      var newChild = document.createElement("li");
                      newChild.textContent = person;
                      list.appendChild(newChild);
                  });
              }

      return false;
          }`

      同样,我们从防止默认的放下行为和防止控件传播到其他处理程序开始。默认的放置事件取决于放置元素的位置和类型。例如,拖放从另一个源拖入的图像会在浏览器窗口中显示该图像,默认情况下,将链接拖放到窗口中会导航到该图像。我们希望在演示中完全控制拖放行为,所以我们取消了任何默认行为。

      回想一下,我们的演示展示了如何从拖放的元素中检索在dragstart中设置的多种数据类型。在这里,我们可以看到检索是如何完成的。默认情况下,我们使用 text/plain MIME 格式获取代表俱乐部成员姓名的纯文本数据。如果用户进入志愿者列表,这就足够了。

      然而,如果用户将俱乐部成员放入赛车列表,我们需要额外的步骤来获取俱乐部成员的年龄,这是我们之前在dragstart期间使用文本/html 风格设置的。我们将它添加到俱乐部成员的名字前面,以在参赛者列表中显示年龄和姓名。

      我们的最后一个代码块是一个简单但未经优化的例程,用于清除目标列表中所有以前的成员,添加新成员(如果他还不存在),排序,并重新填充列表。最终结果是一个排序列表,包含旧成员和新删除的成员(如果他以前不存在)。

      不管用户是否完成了拖放,我们都需要一个 dragend 处理程序来清理(见清单 9-12 )。

      清单 9-12。用于清理的拖拉装卸机

      `    // make sure to clean up any drag operation
          function handleDragEnd(evt) {

      // restore the potential drop target styles
              racersList.className = null;
              volunteersList.className = null;
              return false;
          }`

      在拖动结束时会调用一个dragend处理程序,不管拖放是否真的发生。如果用户取消了拖动或完成了拖动,仍会调用dragend处理程序。这给了我们一个很好的地方来清理我们在过程开始时改变的任何状态。毫不奇怪,我们将列表的 CSS 类重置为默认的非样式状态。

      分享就是关爱

      Brian 说:“如果你想知道拖放功能是否值得所有的事件处理程序代码,不要忘记 API 的一个关键好处:跨窗口甚至跨浏览器共享拖拽。

      因为 HTML5 拖放的设计是为了反映桌面功能而构建的,所以它也支持跨应用共享就不足为奇了。您可以通过在多个浏览器窗口中加载我们的示例,并将成员从一个源列表拖到另一个窗口的参赛者和志愿者列表中来进行尝试。虽然我们简单的突出显示反馈不是为这种情况设计的,但是实际的拖放功能可以跨窗口工作,甚至跨浏览器工作,如果它们支持 API 的话。“我们的拖放示例很简单,但它展示了 API 的全部功能。

      进入空投区

      如果你认为处理所有的拖放事件很复杂,你并不孤单。该规范的作者设计了一种替代的、简化的机制来支持放下事件:dropzone 属性。

      dropzone 为开发人员提供了一种简洁的方式来注册元素是否愿意接受拖放,而无需编写冗长的事件处理程序。该属性由几个空格分隔的模式组成,当提供这些模式时,允许浏览器自动为您连接拖放行为(见表 9-2 )。

      `images

      借用我们的示例应用,racers 列表元素可以被指定为具有以下属性:

      <ul id="racers" dropzone=”copy s:text/plain s:text/html” ondrop=”handleDrop(event)”>

      这提供了一种快速的方式来告诉浏览器,支持纯文本或 HTML 数据格式的元素的复制操作可以从我们的列表中删除。

      在撰写本文时,dropzone还不被大多数主流浏览器厂商支持,但对它的支持很可能即将到来。

      处理文件的拖放

      如果您曾经想要一种更简单的方式将文件添加到您的 web 应用中,或者您想知道一些最新的网站如何允许您将文件直接拖动到页面中并上传它们,答案就是 HTML5 文件 API。尽管整个 W3C 文件 API 的大小和状态超出了本次讨论的范围,但是许多浏览器已经支持该标准的一个子集,它允许将文件拖入应用中。

      Image 注意W3C 文件 API 在[www.w3.org/TR/FileAPI](http://www.w3.org/TR/FileAPI)在线文档化。

      File API 包含异步读取网页中的文件、在跟踪进程的同时将文件上传到服务器以及将文件转换为页面元素的功能。然而,拖放等附属规范使用了文件 API 的一个子集,这也是我们在本章中关注的地方。

      回想一下,我们已经在本章中两次提到了文件拖放。首先,dataTransfer对象包含一个名为files的属性,如果合适的话,该属性将包含一个附加到拖动的文件列表。例如,如果用户将一个或一组文件从桌面拖到应用的网页中,浏览器将在dataTransfer.files对象有值的地方触发拖放事件。此外,支持前面提到的 dropzone 属性的浏览器通过使用f : MIME 类型前缀,允许特定 MIME 类型的文件有效地拖放到元素上。

      Image 注意目前 Safari 浏览器只支持对文件的拖放操作。在页面内启动的拖动将触发大多数拖放事件,但只有当拖动类型为文件时,才会发生拖放事件。

      通常,在大多数拖放事件中,您无法访问这些文件,因为出于安全原因,它们受到保护。尽管有些浏览器可能允许您在拖动事件期间访问文件列表,但没有浏览器允许您访问文件数据。此外,在拖动源元素处触发的dragstart, drag和 dragend 事件不会在文件拖放中触发,因为源是文件系统本身。

      文件列表中的文件项支持以下属性:

      • 名称:带扩展名的完整文件名
      • 类型:文件的 MIME 类型
      • size :文件的大小,以字节为单位
      • lastModifiedDate :最后一次修改文件内容的时间戳

      让我们看一个简单的文件拖放的例子,我们将展示任何被拖放到页面上的文件的特征,如图 9-5 所示。该代码包含在本书附带的 fileDrag.html 示例中。

      Image

      图 9-5。显示丢失文件特征的演示页面

      我们演示的 HTML 实际上非常简单(见清单 9-13 )。

      清单 9-13。文件拖放演示的标记

      `

      `

      页面中只有两个元素。放置文件的放置目标和状态显示区域。

      和上一个例子一样,我们将在页面加载期间注册拖放事件处理程序(参见清单 9-14 )。

      清单 9-14。文件拖放演示的加载和初始化代码

      `    var droptarget;

      // set the status text in our display
          function setStatus(text) {
              document.getElementById("status").innerHTML = text;
          }

      // ...

      function loadDemo() {

      droptarget = document.getElementById("droptarget");
              droptarget.className = "validtarget";

      droptarget.addEventListener("dragenter", handleDragEnter, false);
              droptarget.addEventListener("dragover", handleDragOver, false);
              droptarget.addEventListener("dragleave", handleDragLeave, false);
              droptarget.addEventListener("drop", handleDrop, false);

      setStatus("Drag files into this area.");
          }

      window.addEventListener("load", loadDemo, false);`

      这一次,放置目标接收所有的事件处理程序。只需要处理程序的子集,我们可以忽略在拖动源发生的事件。

      当用户拖动文件到我们的拖放目标时,我们将显示我们所知道的关于拖放候选对象的信息(见清单 9-15 )。

      清单 9-15。文件拖放进入处理程序

      `    // handle drag events in the drop target
          function handleDragEnter(evt) {

      // if the browser supports accessing the file
              // list during drag, we display the file count
              var files = evt.dataTransfer.files;

      if (files)
                  setStatus("There are " + evt.dataTransfer.files.length +
                      " files in this drag.");
              else
                  setStatus("There are unknown items in this drag.");

      droptarget.className = "highlighted";

      evt.stopPropagation();
              evt.preventDefault();`

              return false;     }

      虽然有些浏览器允许在拖动过程中访问dataTransfer文件,但我们会处理禁止访问该信息的情况。当计数已知时,我们将在状态中显示它。

      处理dragoverdragleave事件很简单(参见清单 9-16 )。

      清单 9-16。文件删除拖拽和拖拽离开处理程序

      `    // preventing the default dragover behavior
          // is necessary for successful drops
          function handleDragOver(evt) {
              evt.stopPropagation();
              evt.preventDefault();

      return false;
          }

      // reset the text and status when drags leave
          function handleDragLeave(evt) {
              setStatus("Drag files into this area.");

      droptarget.className = "validtarget";

      return false;
          }`

      和往常一样,我们必须取消dragover事件,以允许拖放由我们自己的代码处理,而不是浏览器的默认行为,通常是内联显示。对于一个dragleave,我们只设置状态文本和样式来表示鼠标离开时拖放不再有效。我们的大部分工作是在 drop handler 中完成的(见清单 9-17 )。

      清单 9-17。文件删除处理器

      `    // handle the drop of files
          function handleDrop(evt) {
              // cancel the event to prevent viewing the file
              evt.preventDefault();
              evt.stopPropagation();

      var filelist = evt.dataTransfer.files;

      var message = "There were " + filelist.length + " files dropped.";

      // show a detail list for each file in the drag
              message += "

        ";

        [].forEach.call(filelist, function(file) {
                    message += "

      1. ";
                    message += "" + file.name + " ";
                    message += "(" + file.type + ") : ";             message += "size: " + file.size + " bytes - ";
                    message += "modified: " + file.lastModifiedDate;
                    message += "
      2. ";
                });

        message += "

      ";

      setStatus(message);
              droptarget.className = "validtarget";

      return false;
          }`

      如前所述,有必要使用preventDefault取消事件,这样浏览器的默认丢弃代码就不会被触发。

      然后,因为我们在拖放处理程序中比在拖动过程中能访问更多的数据,所以我们可以检查附加到dataTransferfiles并发现被拖放文件的特征。在我们的例子中,我们将仅仅显示文件的属性,但是通过充分使用 HTML5 文件 API,您可以读入本地显示的内容,或者将它们上传到支持您的应用的服务器。

      实用的临时演员

      有时有些技术不适合我们的常规例子,但仍然适用于许多类型的 HTML5 应用。在这里,我们向您呈现一个简短、普通、实用的附加内容。

      定制拖动显示

      通常,浏览器将默认拖动操作的可视光标指示器。图像或链接会随着光标移动(有时为了实际查看会缩小尺寸),或者被拖动元素的重影图像会悬停在拖动位置。

      但是,如果您需要更改默认的拖动图像显示,API 为您提供了一个简单的 API 来实现这一点。只可能在 dragstart 处理程序中更改拖动图像——同样是出于安全考虑——但是您可以通过简单地将表示光标外观的元素传递给dataTransfer来轻松实现。

              var dragImage = document.getElementById("happyTrails");         evt.dataTransfer.setDragImage(dragImage, 5, 10);

      注意传递给setDragImage调用的偏移坐标。这些 x 和 y 坐标告诉浏览器将图像中的哪个像素用作鼠标光标下的点。例如,通过分别为 x 和 y 传入值 5 和 10,图像将被定位成光标距离左边 5 个像素和顶部 10 个像素,如图 9-6 中的所示。

      Image

      图 9-6。演示页面,拖动图像设置为快乐小径标志

      然而,拖动图像不必是图像。任何元素都可以设置为拖动图像;如果它不是一个图像,浏览器将创建一个可视的快照来作为光标显示。

      总结

      拖放 API 可能很难掌握。它涉及到许多事件的正确处理,如果你的拖放目标布局很复杂,其中的一些可能很难管理。但是,如果您正在寻找跨窗口或浏览器的拖动操作,甚至与桌面交互,您将需要学习 API 的微妙之处。从设计上来说,它结合了本机应用拖放功能,同时还能在必须保护数据免受第三方代码攻击的环境的安全限制内工作。

      有关使用拖放文件作为应用数据的更多信息,请务必查看 W3C 文件 API。在下一章中,我们将研究 Web 工作器 API,它将允许您在主页之外生成后台脚本,以加快执行速度并改善用户体验。

      十、使用 Web 工作器 API

      JavaScript 是单线程的。因此,长时间的计算(不一定是因为糟糕的代码)将阻塞 UI 线程,使其无法向文本框添加文本、单击按钮、使用 CSS 效果,并且在大多数浏览器中,在控制返回之前无法打开新的选项卡。为了解决这个问题,HTML5 Web 工作器 为 Web 应用提供了后台处理能力,并且通常在单独的线程上运行,以便使用 Web 工作器 的 JavaScript 应用可以利用多核 CPU。将长时间运行的任务分离到 Web 工作器 中也避免了可怕的慢脚本警告,如图 10-1 所示,当 JavaScript 循环持续几秒钟时就会出现这种警告。

      Image

      图 10-1。火狐浏览器中的慢速脚本警告

      尽管网络工作者很强大,但也有他们做不了的事情。例如,当一个脚本在 web Worker 内部执行时,它不能访问 Web 页面的window对象(window.document),这意味着 Web Worker 不能直接访问 Web 页面和 DOM API。尽管 Web 工作者不能阻止浏览器 UI,但他们仍然会消耗 CPU 周期,降低系统的响应速度。

      假设您想要创建一个 web 应用,它必须执行一些后台数字处理,但是您不希望这些任务干扰 web 页面本身的交互性。使用 Web Worker,您可以生成一个 Web Worker 来执行任务,并添加一个事件侦听器来侦听来自 Web Worker 的消息。

      web 工作者的另一个用例可能是一个应用,它侦听来自后端服务器的广播新闻消息,当从后端服务器接收到消息时,将消息发布到主 Web 页面。这个 Web Worker 可能使用 Web 套接字或服务器发送的事件与后端服务器通信。

      在这一章中,我们将探索你能对网络工作者做些什么。首先,我们将讨论 Web 工作者是如何工作的,以及在撰写本文时可用的浏览器支持级别。然后,我们将讨论如何使用 API 来创建新的 worker,以及如何在 worker 和产生它的上下文之间进行通信。最后,我们将向您展示如何使用 Web 工作器 构建应用。

      浏览器对网络工作者的支持

      大多数现代 web 浏览器都支持 Web 工作器。查看网站[caniuse.com](http://caniuse.com)(搜索 Web 工作器)获取最新的支持列表。虽然大多数其他 API 都有 polyfill(仿真)库,例如,对于 HTML5 Canvas,有像excanvas.jsflashcanvas.js这样的库提供 Canvas APIs 的仿真(在幕后使用 Flash),但是对于 Web 工作者来说,仿真没有太大意义。您可以调用您的辅助代码作为辅助,或者在您的页面中内嵌运行相同的代码,阻塞 UI 线程。基于工作人员的页面响应能力的提高可能足以让人们升级到更现代的浏览器(至少我们希望如此)。

      使用 Web 工作器 API

      在这一节中,我们将更详细地探索 Web 工作器 API 的使用。为了便于说明,我们创建了一个简单的浏览器页面:echo.html。使用 Web Worker 相当简单——创建一个 Web Worker 对象,并传入一个要执行的 JavaScript 文件。在页面中,您设置了一个事件监听器来监听传入的消息和 Web Worker 发布的错误,如果您想从页面与 Web Worker 通信,您可以调用postMessage来传入所需的数据。Web Worker JavaScript 文件中的代码也是如此——必须设置事件处理程序来处理传入的消息和错误,并且通过调用postMessage来处理与页面的通信。

      检查浏览器支持

      在调用 Web 工作器 API 函数之前,您需要确保浏览器支持您将要做的事情。这样,您可以提供一些替代文本,提示应用的用户使用更新的浏览器。清单 10-1 显示了你可以用来测试浏览器支持的代码。

      清单 10-1。检查浏览器支持

      function loadDemo() {   if (typeof(Worker) !== "undefined") {     document.getElementById("support").innerHTML =             "Excellent! Your browser supports Web 工作器";   } }

      在本例中,您在loadDemo函数中测试浏览器支持,该函数可能在页面加载时被调用。对typeof(Worker)的调用将返回窗口的全局Worker属性,如果浏览器不支持 Web 工作器 API,该属性将是未定义的。在本例中,通过用合适的消息更新页面上先前定义的支持元素,页面被更新以反映是否有浏览器支持,如图 10-2 顶部所示。

      Image

      图 10-2。显示是否支持 Web 工作器 的示例

      创建网络工作者

      Web 工作器 用 JavaScript 文件的 URL 初始化,该文件包含 worker 将要执行的代码。这段代码设置事件侦听器,并与产生它的脚本进行通信。JavaScript 文件的 URL 可以是与主页具有相同来源(相同的方案、主机和端口)的相对或绝对 URL:

      worker = new Worker("echoWorker.js");

      内联工人

      要启动一个 worker,你需要指向一个文件。您可能已经见过一些类型为javascript/worker的脚本元素示例,如下例所示:

       <script id="myWorker" type="javascript/worker">

      不要让这种想法欺骗您,以为您可以简单地设置脚本元素的类型,以作为 Web Worker 运行 JavaScript 代码。在这种情况下,类型信息用于通知浏览器及其 JavaScript 引擎而不是解析并运行脚本。事实上,类型也可以是除了text/javascript之外的任何东西。所示的脚本示例是内联 Web 工作器 的构建块——只有当您的浏览器也支持文件系统 API (Blob Builder 或文件编写器)时,才能使用该功能。在这种情况下,您可以通过编程找到脚本块(在前面的例子中,是带有myWorker id 的元素),并将 Web Worker JavaScript 文件写入磁盘。之后,您可以在代码中调用内联 Web Worker。

      共享工作者

      还有另一种类型的工作者,在撰写本文时还没有得到广泛的支持:共享 Web 工作者。共享 Web Worker 类似于普通的 Web Worker,但是它可以在同一来源的多个页面上共享。共享网络工作者引入了用于PostMessage通信的端口的概念。共享的 Web 工作器 对于相同来源的多个页面(或选项卡)之间的数据同步或者在几个选项卡之间共享长期资源(如 WebSocket)非常有用。

      启动共享 Web Worker 的语法如下:

      sharedWorker = new SharedWorker(sharedEchoWorker.js');

      加载并执行附加 JavaScript

      由几个 JavaScript 文件组成的应用可以包含在页面加载时同步加载 JavaScript 文件的<script>元素。但是,因为 Web 工作器 不能访问document对象,所以有一种替代机制可以从 Workers 中同步导入额外的 JavaScript 文件,即importScripts:

      importScripts("helper.js");

      导入 JavaScript 文件只是将 JavaScript 加载并执行到现有的 worker 中。对importScripts的同一个调用可以导入多个脚本。它们按指定的顺序执行:

      importScripts("helper.js", "anotherHelper.js");

      与网络工作者交流

      一旦产生了 Web Worker,就可以使用postMessage API 向 Web Worker 发送数据和从 Web Worker 接收数据。这与用于跨框架和跨窗口通信的postMessage API 相同。postMessage可用于发送大多数 JavaScript 对象,但不能用于函数或具有循环引用的对象。

      假设您想要构建一个简单的 Web worker 示例,该示例允许用户向 Worker 发送消息,Worker 反过来回显该消息。这个例子在现实生活中可能不太有用,但它足以解释构建更复杂的例子所需的概念。图 10-3 显示了这个示例网页和它的 Web Worker 的运行。这个简单页面的代码列在本节的末尾。

      Image

      图 10-3。一个使用网络工作者的简单网页

      为了与 Web Worker 建立适当的通信,必须将代码添加到主页(调用 Web Worker 的页面)以及 worker JavaScript 文件中。

      编码主页面

      为了从页面与 Web Worker 通信,您调用postMessage来传递所需的数据。要侦听 Web Worker 发送到页面的传入消息和错误,需要设置一个事件侦听器。

      要在主页和 Web Worker 之间建立通信,首先将对postMessage的调用添加到主页,如下所示:

      document.getElementById("helloButton").onclick = function() {     worker.postMessage("Here's a message for you"); }

      在前面的例子中,当用户点击 Post a Message 按钮时,一条消息被发送到 Web Worker。接下来,向页面添加一个事件侦听器,用于侦听来自 Web Worker 的消息:

      worker.addEventListener("message", messageHandler, true);
      function messageHandler(e) {     // process message from worker }

      编写 Web Worker JavaScript 文件

      您现在必须向 Web Worker JavaScript 文件添加类似的代码——必须设置事件处理程序来处理传入的消息和错误,并且通过调用postMessage来处理与页面的通信。

      要完成页面和 Web Worker 之间的通信,首先,添加对postMessage的调用;例如,在一个messageHandler函数中:

      function messageHandler(e) {     postMessage("worker says: " + e.data + " too"); }

      接下来,向 Web Worker JavaScript 文件添加一个事件侦听器,该文件处理来自主页的消息:

      addEventListener("message", messageHandler, true);

      在本例中,当收到消息时,立即调用messageHandler函数,以便消息可以被回显。

      注意,如果这是一个共享的 worker,您将使用稍微不同的语法(使用一个port):

      sharedWorker.port.addEventListener("message", messageHandler, true); sharedWorker.port.postMessage("Hello HTML5");  

      此外,工作人员可以监听一个connect事件来接收连接。您可以用它来计算活动连接的数量。

      处理错误

      Web Worker 脚本中未处理的错误会在 Web Worker 对象上引发错误事件。当您调试使用 Web 工作器 的脚本时,侦听这些错误事件尤其重要。下面显示了一个 Web Worker JavaScript 文件中的错误处理函数示例,该文件将错误记录到控制台:

      function errorHandler(e) {     console.log(e.message, e); }

      要处理这些错误,您必须向主页添加一个事件侦听器:

      worker.addEventListener("error", errorHandler, true);

      大多数浏览器还没有一个很好的方法来单步调试 Web Worker 代码,但谷歌 Chrome 在其 Chrome 开发工具中提供了 Web Worker 调试功能(在脚本选项卡中,查找 Worker inspectors),如图图 10-4 所示。

      Image

      图 10-4。Chrome 开发者工具中的 Web Worker 调试选项

      停止网络工作者

      网络工作者不会自己停下来;但是启动它们的页面可以阻止它们。如果页面关闭,Web 工作器 将被垃圾收集,所以请放心,不会有任何僵尸 Workers 在执行后台任务。但是,当不再需要 Web Worker 时,您可能希望回收资源——可能是当主页被通知 Web Worker 已完成其任务时。您可能还希望取消长时间运行的任务来响应用户操作。调用terminate停止 Web Worker。被终止的 Web Worker 将不再响应消息或执行任何额外的计算。您不能重新启动工作进程;相反,您可以使用相同的 URL 创建一个新的工作进程:

      worker.terminate();

      在 Web 工作器 中使用 Web 工作器

      Worker API 可以在 Web Worker 脚本中用来创建子工作器:

      var subWorker = new Worker("subWorker.js");

      大量工人

      Peter 说:“如果你生成一个工人,而递归地用相同的 JavaScript 源文件生成子工人,至少你会看到一些有趣的结果。”

      Image

      使用计时器

      虽然 Web 工作者不能访问window对象,但是他们可以使用完整的 JavaScript 计时 API,通常可以在全局窗口中找到:

      var t = setTimeout(postMessage, 2000, "delayed message");

      示例代码

      为了完整起见,清单 10-2 和清单 10-3 显示了简单页面和 Web Worker JavaScript 文件的代码。

      清单 10-2。调用网络工作者的简单 HTML 页面

      `

      <title>Simple Web 工作器 Example</title> <link rel="stylesheet" href="styles.css">

      Simple Web 工作器 Example

      Your browser does not support Web 工作器.


      `

      清单 10-3。简单的 Web Worker JavaScript 文件

      function messageHandler(e) {     postMessage("worker says: " + e.data + " too"); } addEventListener("message", messageHandler, true);

      用 Web 工作器 构建应用

      到目前为止,我们一直关注于使用不同的 Web Worker APIs。让我们通过构建一个应用来看看 Web 工作器 API 到底有多强大:一个带有图像模糊过滤器的网页,并行运行在多个 Web 工作器 上。图 10-5 显示了这个应用在你启动时的样子。

      Image

      图 10-5。基于网络工作者的网页,带有图像模糊过滤器

      这个应用将图像数据从画布发送到几个 Web 工作器(您可以指定数量)。然后,网络工作人员用简单的模糊滤镜处理图像。这可能需要几秒钟的时间,取决于图像的大小和可用的计算资源(即使是具有快速 CPU 的机器也可能有来自其他进程的负载,导致 JavaScript 执行需要更多的挂钟时间来完成)。图 10-6 显示了运行模糊过滤程序一段时间后的同一页面。

      Image

      图 10-6。运行一段时间后图像模糊的网页

      然而,因为繁重的工作发生在 Web 工作器 中,所以不存在缓慢脚本警告的危险,因此,不需要手动将任务划分为调度的片——如果您不能使用 Web 工作器,您将不得不考虑这一点。

      编写 blur.js 辅助脚本

      blur.js应用页面中,我们可以使用一个模糊过滤器的简单实现,它会一直循环直到完全处理完它的输入,如清单 10-4 所示。

      清单 10-4。文件 blur.js 中的一个 JavaScript 框模糊实现

      `function inRange(i, width, height) {
          return ((i>=0) && (i < widthheight4));
      }

      function averageNeighbors(imageData, width, height, i) {
          var v = imageData[i];

      // cardinal directions
          var north = inRange(i-width4, width, height) ? imageData[i-width4] : v;
          var south = inRange(i+width4, width, height) ? imageData[i+width4] : v;
          var west = inRange(i-4, width, height) ? imageData[i-4] : v;
          var east = inRange(i+4, width, height) ? imageData[i+4] : v;

      // diagonal neighbors
          var ne = inRange(i-width4+4, width, height) ? imageData[i-width4+4] : v;
          var nw = inRange(i-width4-4, width, height) ? imageData[i-width4-4] : v;
          var se = inRange(i+width4+4, width, height) ? imageData[i+width4+4] : v;
          var sw = inRange(i+width4-4, width, height) ? imageData[i+width4-4] : v;

      // average
          var newVal = Math.floor((north + south + east + west + se + sw + ne + nw + v)/9);

      if (isNaN(newVal)) {
              sendStatus("bad value " + i + " for height " + height);
              throw new Error("NaN");
          }
          return newVal;
      }

      function boxBlur(imageData, width, height) {
          var data = [];
          var val = 0;
          for (var i=0; i<widthheight4; i++) {
              val = averageNeighbors(imageData, width, height, i);
              data[i] = val;
          }

      return data;
      }`

      简而言之,这种算法通过平均附近的像素值来模糊图像。对于具有数百万像素的大型图像,这需要大量的时间。在 UI 线程中运行这样的循环是非常不可取的。即使没有出现慢速脚本警告,页面 UI 也不会响应,直到循环终止。由于这个原因,它为 Web 工作器 中的后台计算提供了一个很好的例子。

      编码 blur.html 申请页面

      清单 10-5 显示了调用 Web Worker 的 HTML 页面的代码。为了清楚起见,这个例子的 HTML 保持简单。这里的目的不是构建一个漂亮的界面,而是提供一个简单的框架,可以控制 Web 工作人员并演示他们的行为。在这个应用中,显示输入图像的 canvas 元素被注入到页面中。我们有按钮来开始模糊图像,停止模糊,重置图像,并指定要繁殖的工人数量。

      清单 10-5。【blur.html 页面代码

      `

      <title>Web 工作器</title> <link rel="stylesheet" href = "styles.css">

      Web 工作器

      Your browser does not support Web 工作器.




      `

      接下来,让我们将创建 workers 的代码添加到文件blur.html.中,我们实例化了一个worker对象,传入一个 JavaScript 文件的 URL。每个实例化的工作者将运行相同的代码,但是负责处理输入图像的不同部分:

      function initWorker(src) {     var worker = new Worker(src);     worker.addEventListener("message", messageHandler, true);     worker.addEventListener("error", errorHandler, true);     return worker; }

      让我们将错误处理代码添加到文件blur.html,如下所示。如果 worker 出现错误,页面将能够显示一条错误消息,而不是继续不知道。我们的例子应该不会遇到任何问题,但是监听错误事件通常是一种很好的实践,对于调试非常有价值。

      function errorHandler(e) {     log("error: " + e.message); }

      编写 blurWorker.js Web Worker 脚本

      接下来,我们将工人用来与页面通信的代码添加到文件blurWorker.js(参见清单 10-6 )。当 Web 工作者完成计算块时,他们可以使用postMessage通知页面他们已经取得了进展。我们将使用这些信息来更新主页上显示的图像。创建后,我们的网络工作人员等待包含图像数据和指令的消息开始模糊。这个消息是一个 JavaScript 对象,包含消息类型和用数字数组表示的图像数据。

      清单 10-6。发送和处理 blurWorker.js 文件中的图像数据

      `function sendStatus(statusText) {
          postMessage({"type" : "status",
                       "statusText" : statusText}
                      );
      }

      function messageHandler(e) {
          var messageType = e.data.type;
          switch (messageType) {
              case ("blur"):
                  sendStatus("Worker started blur on data in range: " +
                                  e.data.startX + "-" + (e.data.startX+e.data.width));
                  var imageData = e.data.imageData;
                  imageData = boxBlur(imageData, e.data.width, e.data.height, e.data.startX);

      postMessage({"type" : "progress",
                               "imageData" : imageData,
                               "width" : e.data.width,
                               "height" : e.data.height,
                               "startX" : e.data.startX
                              });
                  sendStatus("Finished blur on data in range: " +
                                  e.data.startX + "-" + (e.data.width+e.data.startX));
                  break;
              default:
                  sendStatus("Worker got message: " + e.data);
          }
      }
      addEventListener("message", messageHandler, true);`

      与网络工作者交流

      在文件blur.html中,我们可以通过向工人发送一些代表模糊任务的数据和参数来使用他们。这是通过使用postMessage发送一个 JavaScript 对象来完成的,该对象包含 RGBA 图像数据的数组、源图像的尺寸以及工人负责的像素范围。每个工作者基于其接收的消息处理图像的不同部分:

      `function sendBlurTask(worker, i, chunkWidth) {
              var chunkHeight = image.height;
              var chunkStartX = i * chunkWidth;
              var chunkStartY = 0;
              var data = ctx.getImageData(chunkStartX, chunkStartY,
                                          chunkWidth, chunkHeight).data;

      worker.postMessage({'type' : 'blur',
                                  'imageData' : data,
                                  'width' : chunkWidth,
                                  'height' : chunkHeight,
                                  'startX' : chunkStartX});
      }`

      画布图像数据

      Frank 说::postMessage被指定为允许imageData对象的高效序列化,以便与 canvas API 一起使用。一些包含 Worker 和postMessageAPI 的浏览器可能还不支持postMessage的扩展序列化能力。

      正因为如此,我们在本章中给出的图像处理例子发送imageData.data(它像 JavaScript 数组一样序列化)而不是发送imageData对象本身。当 Web 工作者计算他们的任务时,他们将状态和结果反馈给页面。清单 10-6 展示了模糊过滤器处理数据后,数据是如何从工作者发送到页面的。同样,该消息包含一个 JavaScript 对象,该对象具有图像数据和坐标字段,用于标记已处理部分的边界。"

      在 HTML 页面端,消息处理程序使用这些数据,并用新的像素值更新画布。经过处理的图像数据输入后,结果立即可见。我们现在有了一个示例应用,它可以处理图像,同时有可能利用多个 CPU 内核。此外,我们没有锁定用户界面,使其在 Web 工作人员活动时没有响应。图 10-7 显示了实际应用。

      Image

      图 10-7。模糊应用在行动中

      实际应用

      要查看这个示例的运行情况,页面blur.html必须由 web 服务器提供(例如,Apache 或 Python 的 SimpleHTTPServer)。下面的步骤展示了如何使用 Python SimpleHTTPServer 运行应用:

      1. 安装 Python。
      2. 导航到包含示例文件(blur.html)的目录。
      3. 启动 Python 如下:python -m SimpleHTTPServer 9999
      4. 打开浏览器并导航至[localhost:9999/blur.html](http://localhost:9999/blur.html)。你现在应该看到如图 10-7 所示的页面。
      5. 如果你让它运行一段时间,你会看到图像的不同象限慢慢模糊。同时模糊的象限数量取决于您启动的工作线程数量。

      示例代码

      为了完整起见,清单 10-7 、 10-8 和 10-9 包含了示例应用的完整代码。

      清单 10-7。【blur.html 文件内容

      `

      <title>Web 工作器</title> <link rel="stylesheet" href = "styles.css">

      Web 工作器

      Your browser does not support Web 工作器.




      `

      清单 10-8。文件内容 blurWorker.js

      `importScripts("blur.js");

      function sendStatus(statusText) {
          postMessage({"type" : "status",
                       "statusText" : statusText}
                      );
      }

      function messageHandler(e) {
          var messageType = e.data.type;
          switch (messageType) {
              case ("blur"):
                  sendStatus("Worker started blur on data in range: " +
                                  e.data.startX + "-" + (e.data.startX+e.data.width));
                  var imageData = e.data.imageData;
                  imageData = boxBlur(imageData, e.data.width, e.data.height, e.data.startX);

      postMessage({"type" : "progress",
                               "imageData" : imageData,
                               "width" : e.data.width,
                               "height" : e.data.height,
                               "startX" : e.data.startX
                              });
                  sendStatus("Finished blur on data in range: " +
                                  e.data.startX + "-" + (e.data.width+e.data.startX));
                  break;
              default:
                  sendStatus("Worker got message: " + e.data);
          }
      } addEventListener("message", messageHandler, true);`

      清单 10-9。blur . js 文件内容

      `function inRange(i, width, height) {
          return ((i>=0) && (i < widthheight4));
      }

      function averageNeighbors(imageData, width, height, i) {
          var v = imageData[i];

      // cardinal directions
          var north = inRange(i-width4, width, height) ? imageData[i-width4] : v;
          var south = inRange(i+width4, width, height) ? imageData[i+width4] : v;
          var west = inRange(i-4, width, height) ? imageData[i-4] : v;
          var east = inRange(i+4, width, height) ? imageData[i+4] : v;

      // diagonal neighbors
          var ne = inRange(i-width4+4, width, height) ? imageData[i-width4+4] : v;
          var nw = inRange(i-width4-4, width, height) ? imageData[i-width4-4] : v;
          var se = inRange(i+width4+4, width, height) ? imageData[i+width4+4] : v;
          var sw = inRange(i+width4-4, width, height) ? imageData[i+width4-4] : v;

      // average
          var newVal = Math.floor((north + south + east + west + se + sw + ne + nw + v)/9);

      if (isNaN(newVal)) {
              sendStatus("bad value " + i + " for height " + height);
              throw new Error("NaN");
          }
          return newVal;
      }

      function boxBlur(imageData, width, height) {
          var data = [];
          var val = 0;

      for (var i=0; i<widthheight4; i++) {
              val = averageNeighbors(imageData, width, height, i);
              data[i] = val;
          }

      return data;
      }`

      总结

      在本章中,您已经看到了如何使用 Web 工作器 来创建具有后台处理的 Web 应用。本章向您展示了 Web 工作器(以及内联和共享 Web 工作器)是如何工作的。我们讨论了如何使用 API 创建新的 worker,以及如何在 worker 和产生它的上下文之间进行通信。最后,我们向您展示了如何使用 Web 工作器 构建应用。在下一章,我们将演示 HTML5 让你保存数据的本地副本和减少应用中网络开销的更多方法。

      十一、使用 Web 存储 API

      在这一章中,我们将探索你可以用 HTML5 Web 存储做什么——有时被称为 DOM Storage——这是一个 API,它使得跨 Web 请求保留数据变得容易。在 web 存储 API 出现之前,远程 Web 服务器需要通过在客户机和服务器之间来回发送来存储任何持久的数据。随着 Web 存储 API 的出现,开发人员现在可以将数据直接存储在浏览器的客户端,以便跨请求重复访问,或者在您完全关闭浏览器后很长时间内检索,从而减少网络流量。

      我们将首先看看 Web 存储与 cookies 有何不同,然后探讨如何存储和检索数据。接下来,我们将看看localStoragesessionStorage之间的区别,存储接口提供的属性和功能,以及如何处理 Web 存储事件。我们最后看一下 Web SQL 数据库 API 和一些实用的附加内容。

      网络存储概述

      为了解释 Web 存储 API,最好回顾一下它的前身,名字有趣的 cookie。浏览器 cookie——以在程序之间传递小数据值的古老编程技术命名——是一种在服务器和浏览器之间来回发送文本值的内置方式。服务器可以使用它们放入这些 cookies 中的值来跨网页跟踪用户信息。每当用户访问一个域时,Cookie 值就会来回传输。例如,cookie 可以存储一个会话标识符,通过在浏览器 cookie 中存储一个与服务器自己的购物车数据库相匹配的唯一 ID,允许 web 服务器知道哪个购物车属于某个用户。然后,当用户从一个页面移动到另一个页面时,购物车可以一致地更新。cookies 的另一个用途是将本地值存储到应用中,以便这些值可以在后续的页面加载中使用。

      Cookie 值还可以用于用户不太喜欢的操作,比如跟踪用户为了定向广告访问了哪些页面。因此,一些用户要求浏览器包含允许他们随时或针对特定网站阻止或删除 cookies 的功能。

      不管你喜不喜欢,早在 20 世纪 90 年代中期网景浏览器的早期,浏览器就已经支持 cookies 了。Cookies 也是少数几个自网络早期以来就一直得到浏览器厂商支持的特性之一。Cookies 允许跨多个请求跟踪数据,只要数据在服务器和浏览器代码之间仔细协调。尽管 cookies 无处不在,但它也有一些众所周知的缺点:

      • Cookies 的大小极其有限。一般来说,在一个 cookie 中只能设置大约 4KB 的数据,这意味着对于像文档或邮件这样的大数据量来说,这是不可接受的。
      • 对于该 cookie 范围内的每个请求,cookie 在服务器和浏览器之间来回传输。这不仅意味着 cookie 数据在网络上是可见的,使它们在未加密时存在安全风险,而且每次加载 URL 时,作为 cookie 保存的任何数据都将消耗网络带宽。因此,相对较小的 cookies 更有意义。

      在许多情况下,不需要网络或远程服务器也能达到同样的效果。这就是 HTML5 Web 存储 API 的用武之地。通过使用这个简单的 API,开发人员可以将值存储在容易检索的 JavaScript 对象中,这些对象可以跨页面加载保持不变。通过使用sessionStoragelocalStorage,开发人员可以选择让这些值分别在单个窗口或选项卡中的页面加载中或浏览器重启中存在。存储的数据不通过网络传输,并且在返回访问页面时很容易访问。此外,使用高达几兆字节的 Web 存储 API 值可以持久保存更大的值。这使得 Web 存储适用于文档和文件数据,这些数据会很快突破 cookie 的大小限制。

      浏览器对网络存储的支持

      Web 存储是 HTML5 最广泛采用的特性之一。事实上,自 2009 年 Internet Explorer 8 发布以来,所有当前发布的浏览器版本都在一定程度上支持网络存储。在本文发表时,不支持存储的浏览器的市场份额正在缩减至个位数百分比。

      Web 存储是目前在 web 应用中使用的最安全的新 API 之一,因为它得到了广泛的支持。不过,和往常一样,在使用 Web 存储之前先测试它是否受支持是个好主意。下一节“检查浏览器支持”将向您展示如何以编程方式检查 Web 存储是否受支持。

      使用网络存储 API

      Web 存储 API 使用起来非常简单。我们将首先介绍值的基本存储和检索,然后继续讨论sessionStoragelocalStorage之间的差异。最后,我们将看看 API 更高级的方面,比如值改变时的事件通知。

      检查浏览器支持

      给定域的存储数据库直接从window对象访问。因此,确定用户的浏览器是否支持 Web 存储 API 就像检查是否存在window.sessionStoragewindow.localStorage一样简单。清单 11-1 显示了一个例程,它检查存储支持并显示一条关于浏览器对 Web 存储 API 支持的消息。除了使用这些代码,您还可以使用 JavaScript 实用程序库 Modernizr,它可以处理一些可能导致误报的情况。

      清单 11-1。检查网络存储支持

      `function checkStorageSupport() {

      //sessionStorage
        if (window.sessionStorage) {
          alert('This browser supports sessionStorage');
        } else {
          alert('This browser does NOT support sessionStorage');
        }

      //localStorage
        if (window.localStorage) {
          alert('This browser supports localStorage');
        } else {
          alert('This browser does NOT support localStorage');
        }
      }`

      图 11-1 显示了对存储支持的检查。

      Image

      图 11-1。检查 Opera 中的浏览器支持

      有些浏览器不支持从文件系统直接访问文件的sessionStorage。当你运行本章中的例子时,确保你从一个 web 服务器提供页面!例如,您可以在code/storage目录中启动 Python 的简单 HTTP 服务器,如下所示:

      python -m SimpleHTTPServer 9999

      之后,您可以在[localhost:9999/](http://localhost:9999)访问文件。比如[localhost:9999/browser-test.html](http://localhost:9999/browser-test.html)

      但是,您可以自由地使用任何服务器或 URL 位置来运行这些示例。

      `images 注意如果用户在浏览器设置为“私人”模式的情况下浏览,那么一旦浏览器关闭,localStorage 值实际上就不会存在。这是故意的,因为这种模式的用户明确选择不留下任何痕迹。尽管如此,如果存储值在以后的浏览会话中不可用,您的应用应该能够正常响应。

      设置和检索值

      首先,在您学习设置和检索页面中的简单值时,我们将重点关注会话存储功能。设置一个值可以很容易地在一条语句中完成,我们最初将使用手写符号来编写这条语句:

      sessionStorage.setItem(‘myFirstKey', ‘myFirstValue');

      此存储访问语句中有几个要点需要注意:

      • 我们可以省略对window的引用,因为存储对象在默认页面上下文中是可用的。
      • 我们正在调用的函数是setItem,它带有一个键字符串和一个值字符串。尽管有些浏览器可能支持传入非字符串值,但规范只允许字符串作为值。
      • 这个特定的调用将把字符串myFirstValue设置到会话存储器中,稍后可以通过键myFirstKey检索该字符串。

      为了检索该值,手写符号包括调用getItem函数。例如,如果我们用下面的语句来扩充前面的例子

      alert(sessionStorage.getItem(‘myFirstKey'));

      浏览器发出显示文本myFirstValue的 JavaScript 警告。如您所见,从 Web 存储 API 设置和检索值非常简单。

      然而,有一种更简单的方法来访问代码中的存储对象。您还可以使用 expando-properties 来设置存储中的值。使用这种方法,只需直接在sessionStorage对象上设置和检索对应于键值对的值,就可以完全避免setItemgetItem调用。使用这种方法,我们的值集调用可以重写如下:

      sessionStorage.myFirstKey = ‘myFirstValue';

      以至

      sessionStorage[‘myFirstKey'] = ‘myFirstValue';

      类似地,值检索调用可以重写为:

      alert(sessionStorage.myFirstKey);

      为了可读性,我们将在本章中交替使用这些格式。

      这是最基本的。现在,您已经掌握了在应用中使用会话存储所需的所有知识。然而,您可能想知道这个sessionStorage对象有什么特别之处。毕竟,JavaScript 允许您设置和获取几乎任何对象的属性。区别在于范围。您可能没有意识到的是,我们的示例 set 和 get 调用不需要出现在同一个 web 页面中。只要页面是从同一个来源提供的——方案、主机和端口的组合——那么就可以使用相同的键从其他页面检索设置在sessionStorage上的值。这也适用于同一页面的后续加载。作为一名开发人员,您可能已经习惯了这样的想法:每当页面被重新加载时,脚本中所做的更改就会消失。对于在 Web 存储 API 中设置的值来说,这不再适用;它们将跨页面加载继续存在。

      堵塞数据漏洞

      这些价值观会持续多久?对于设置在sessionStorage中的对象,只要浏览器窗口(或标签)没有关闭,它们就会持续存在。一旦用户关闭窗口——或者浏览器,就此而言——sessionStorage值就会被清除。将一个sessionStorage值看作是一个便签提醒是很有用的。放入sessionStorage的价值不会持续很久,你也不应该放任何真正有价值的东西进去,因为这些价值不能保证在你寻找的时候就在你身边。

      那么,为什么您会选择在 web 应用中使用会话存储区呢?会话存储非常适合通常用向导或对话框表示的短期流程。如果您需要存储几个页面中的数据,并且您不希望在用户下次访问您的应用时重新出现这些数据,那么可以随意将它们存储在会话存储区中。在过去,这些类型的值可能通过表单和 cookies 提交,并在每次页面加载时来回传输。使用存储消除了这种开销。

      API 还有另一个非常特殊的用途,它解决了困扰许多 web 应用的一个问题:取值范围。举个例子,一个让你购买机票的购物应用。在这样的应用中,可以使用 cookies 在浏览器和服务器之间来回发送理想出发日期和返回日期等偏好数据。这使得服务器能够在用户浏览应用、选择座位和用餐时记住之前的选择。

      然而,用户在购买旅游优惠时打开多个窗口,比较不同供应商相同出发时间的航班是很常见的。这在 cookie 系统中引起问题,因为如果用户在比较价格和可用性的同时在浏览器窗口之间来回切换,他们很可能在一个窗口中设置 cookie 值,该值将在下一次操作中意外地应用于从同一 URL 提供的另一个窗口。这有时被称为泄漏数据,是由于 cookies 是基于其存储位置共享的。图 11-2 显示了这是如何发生的。

      Image

      图 11-2。使用旅游网站比价时数据泄露

      另一方面,使用sessionStorage允许像出发日期这样的临时值跨访问应用的页面保存,但不会泄漏到用户也在浏览航班的其他窗口。因此,这些偏好将被隔离到预订相应航班的每个窗口。

      本地存储与会话存储

      有时,应用需要在单个选项卡或窗口的生命周期之外持续存在的值,或者需要在多个视图之间共享的值。在这些情况下,使用不同的 Web 存储实现更合适:localStorage。好消息是你已经知道如何使用localStoragesessionStoragelocalStorage之间唯一的编程区别是访问它们的名字——分别通过sessionStoragelocalStorage对象。主要的行为差异在于价值观持续的时间和分享的方式。表 11-1 显示了两种储存方式的区别。

      Image

      请记住,浏览器有时会重新定义标签页或窗口的生命周期。例如,当浏览器崩溃时,或者当用户关闭带有许多打开标签的显示时,一些浏览器将保存并恢复当前会话。在这些情况下,当浏览器重启或恢复时,浏览器可以选择保留sessionStorage。所以,实际上,sessionStorage可能比你想象的要长寿!

      其他 Web 存储 API 属性和功能

      Web 存储 API 是 HTML5 集合中最简单的 API 之一。我们已经研究了从会话和本地存储区域设置和检索数据的显式和隐式方法。让我们通过查看完整的可用属性和函数调用来完成对 API 的调查。

      可以从使用它们的文档的window对象中检索出sessionStoragelocalStorage对象。除了它们的名称和值的持续时间,它们在功能上是相同的。两者都实现了Storage接口,如清单 11-2 所示。

      清单 11-2。存储接口

      interface Storage {   readonly attribute unsigned long length;   getter DOMString key(in unsigned long index);   getter any getItem(in DOMString key);   setter creator void setItem(in DOMString key, in any data);   deleter void removeItem(in DOMString key);   void clear(); };

      让我们更详细地看看这里的属性和功能。

      • length属性指定存储对象中当前存储了多少个键值对。请记住,存储对象特定于它们的原点,因此这意味着存储对象的项目(和长度)只反映为当前原点存储的项目。

      • key(index)函数允许检索给定的密钥。通常,当您希望遍历特定存储对象中的所有键时,这是最有用的。键是从零开始的,这意味着第一个键位于索引(0)处,最后一个键位于索引(长度–1)处。一旦检索到一个键,就可以用它来获取相应的值。在给定存储对象的生命周期中,键将保留它们的索引,除非移除一个键或它的前一个键。

      • 正如您已经看到的,getItem(key)函数是一种基于给定键检索值的方法。另一种是将键作为数组索引引用到存储对象。在这两种情况下,如果存储中不存在该键,将返回值null

      • Similarly, setItem(key, value) function will put a value into storage under the specified key name, or replace an existing value if one already exists under that key name. Note that it is possible to receive an error when setting an item value; if the user has storage turned off for that site, or if the storage is already filled to its maximum amount, a QUOTA_EXCEEDED_ERR error will be thrown during the attempt. Make sure to handle such an error should your application depend on proper storage behavior. Image

        图 11-3。Chrome 出现配额超标错误

      • The removeItem(key) function does exactly as you might expect. If a value is currently in storage under the specified key, this call will remove it. If no item was stored under that key, no action is taken.

        `images 注意与一些集合和数据框架不同,删除一个项目不会因为调用删除它而返回旧值。确保您已经存储了与删除无关的任何所需副本。

      • 最后,clear()函数从存储列表中删除所有值。在空存储对象上调用此方法是安全的;因此,调用将什么也不做。

      磁盘空间配额

      Peter 说:“规范建议浏览器允许每个源有 5 兆字节。为了获得更多的空间,浏览器应该在达到配额时提示用户,并且可以为用户提供查看每个源使用了多少空间的方法。

      现实中,行为还是有点不一致。一些浏览器默默地允许更大的配额或提示增加空间,而另一些则简单地抛出如图 11-3 中所示的QUOTA_EXCEEDED_ERR错误,而另一些,如图 11-4 中所示的 Opera,实现了一种动态分配更多配额的好方法。本例中使用的测试文件testQuota.html位于code/storage目录中。"

      Image

      图 11-4。歌剧增加临时配额

      交流网络存储更新

      有时,事情变得有点复杂,存储需要被多个页面、浏览器选项卡或工作人员访问。每当存储值改变时,您的应用可能需要连续触发许多操作。对于这些情况,Web 存储 API 包括一个事件机制,允许将数据更新通知传递给感兴趣的侦听器。对于与存储操作起源相同的每个窗口,Web 存储事件都在 window 对象上触发,而不管侦听窗口本身是否正在执行任何存储操作。

      `images 注意 Web 存储事件可以用来在同源的窗口之间进行通信。这将在“实用附加功能”一节中进行更深入的探讨。

      要注册接收窗口源的存储事件,只需注册一个事件侦听器,例如:

      window.addEventListener("storage", displayStorageEvent, true);

      如您所见,名称storage用于表示对存储事件的兴趣。每当针对该来源的Storage事件——无论是sessionStorage还是localStorage——被引发时,任何注册的事件监听器都将接收存储事件作为指定的事件处理程序。存储事件本身的形式如清单 11-3 所示。

      清单 11-3。存储事件接口

      interface StorageEvent : Event {   readonly attribute DOMString key;   readonly attribute any oldValue;   readonly attribute any newValue;   readonly attribute DOMString url;   readonly attribute Storage storageArea; };

      StorageEvent对象将是传递给事件处理程序的第一个对象,它包含了理解存储变化本质所需的所有信息。

      • key属性包含存储中更新或删除的键值。
      • oldValue包含与更新前的密钥相对应的先前值,而newValue包含更改后的值。如果该值是新添加的,oldValue将为空,如果该值已被删除,newValue将为空。
      • url将指向storage事件发生的原点。
      • 最后,storageArea提供了对值被改变的sessionStoragelocalStorage的方便引用。这为处理程序提供了一种简单的方法来查询当前值的存储或根据其他存储更改进行更改。

      清单 11-4 显示了一个简单的事件处理程序,它会弹出一个警告对话框,显示在页面原点触发的任何存储事件的内容。

      清单 11-4。显示存储事件内容的事件处理程序

      `// display the contents of a storage event
      function displayStorageEvent(e) {
        var logged = "key:" + e.key + ", newValue:" + e.newValue + ", oldValue:" +
                     e.oldValue +", url:" + e.url + ", storageArea:" + e.storageArea;

      alert(logged);
      }

      // add a storage event listener for this origin
      window.addEventListener("storage", displayStorageEvent, true);`

      探索网络存储

      由于 Web 存储在功能上与 cookies 非常相似,所以最先进的浏览器以非常相似的方式对待它们也就不足为奇了。存储在localStoragesessionStorage中的值可以在最新的浏览器中像浏览 cookies 一样浏览,如图图 11-5 所示。

      Image

      图 11-5。谷歌浏览器资源面板中的存储值

      该接口还允许用户根据需要删除存储值,并在访问页面时轻松查看给定网站记录的值。毫不奇怪,Safari 浏览器对 cookies 和存储有类似的统一显示,因为它与 Chrome 基于相同的底层 WebKit 渲染引擎。图 11-6 显示了 Safari 资源面板。

      Image

      图 11-6。Safari 的资源面板中的存储值

      像其他浏览器一样,Opera 蜻蜓存储显示器不仅允许用户浏览和删除存储值,还允许用户创建存储值,如图图 11-7 所示。

      Image

      图 11-7。Opera 存储面板中的存储值

      随着各种浏览器厂商越来越广泛地实现 Web 存储,用户和开发人员可用的容量和工具都将迅速增加。

      使用网络存储构建应用

      现在,让我们将您在将存储集成到 web 应用中所学到的东西放在一起。随着应用变得越来越复杂,在没有服务器交互的情况下管理尽可能多的数据变得越来越重要。将数据保存在客户端本地,通过从本地机器而不是远程位置获取数据,减少了网络流量并提高了响应速度。

      开发人员面临的一个常见问题是,当用户在应用中从一个页面移动到另一个页面时,如何管理数据。传统上,web 应用通过在服务器上存储数据并在用户导航页面时来回移动数据来实现这一点。或者,应用可能试图将用户保持在单个页面中,并动态更新所有内容。然而,用户容易走神,当用户返回到应用页面时,将数据快速返回到显示中是增强用户体验的一个好方法。

      在我们的示例应用中,我们将展示当用户在网站上从一个页面移动到另一个页面时,如何在本地存储临时应用数据,并在每个页面上从存储中快速加载它。为了做到这一点,我们将建立在前几章的例子上。在第五章中,我们展示了收集用户当前位置是多么容易。然后,在第七章中,我们演示了如何获取位置数据并将其发送到远程服务器,以便任何数量的感兴趣的用户都可以查看。在这里,我们将更进一步:我们将侦听通过 WebSocket 传递的广播位置数据,并将其存储在本地存储中,以便当用户从一个页面移动到另一个页面时,可以立即获得这些数据。

      想象一下,我们的跑步俱乐部拥有来自其比赛参与者的实时位置信息,这些信息通过他们的移动设备广播并通过 WebSocket 服务器共享。当参赛者在比赛中上传新的位置信息时,web 应用可以很容易地实时显示每个参赛者的当前位置。智能网站会缓存这些比赛位置,以便在用户浏览网站页面时快速显示。这正是我们要建造的。

      为了实现这一点,我们需要引入一个演示网站,可以保存和恢复我们的赛车数据。我们已经创建了一个三页的跑步网站示例,并将其放在我们的在线资源文件夹code/storage中,但是您可以使用您选择的任何网站进行演示。这里的关键仅仅是你有多个用户可以轻松访问的网页。我们将在这些页面中插入一些动态内容来表示一个实时的排行榜,或者一个比赛参与者的列表以及他们离终点的当前距离。图 11-8 显示了组成比赛网站的三个页面。

      Image

      图 11-8。榜样竞赛网站

      我们的每个网页都将包含一个公共部分来显示排行榜数据。排行榜中的每个条目将显示我们的一名选手的姓名以及他或她目前离终点线的距离。当我们的任何页面被加载时,它将与一个比赛广播服务器建立 WebSocket 连接,并监听指示一个参赛者位置的消息。反过来,参赛者会将他们的当前位置发送到同一个广播服务器,从而使位置数据实时传输到页面。

      所有这些都已经在前面与地理定位和 WebSocket 相关的章节中介绍过了。事实上,这里的许多演示代码都与本书前面的例子共享。然而,在这个例子中有一个关键的区别:当数据到达页面时,我们将把它存储在会话存储区中,以便以后检索。然后,每当用户导航到新页面时,在建立新的 WebSocket 连接之前,将检索并显示存储的数据。这样,临时数据就可以在页面之间传输,而无需使用任何 cookies 或 web 服务器通信。

      为了保持我们的数据量较小,我们将通过网络以简单的格式发送我们的赛车位置信息,以便于阅读和解析。这种格式是一个String,它使用分号(;)作为分隔符来分隔数据块:名称、纬度和经度。例如,在纬度 37.20 和经度–121.53 的名为 Racer X 的赛车手将使用以下字符串进行标识:

      ;Racer X;37.20;-121.53

      `images 注意一种常见的技术是使用 JSON 格式在客户机和服务器之间发送对象表示。我们将在本章后面的“实用附加功能”一节中告诉你如何去做。

      现在,让我们深入研究代码本身。我们的每个页面都将包含相同的 JavaScript 代码,以连接到 WebSocket 服务器,处理和显示排行榜消息,并使用sessionStorage保存和恢复排行榜。因此,这段代码是包含在真实应用的 JavaScript 库中的主要候选代码。

      首先,我们将建立一些您以前见过的实用方法。为了计算任何一个特定的参赛者离终点线的距离,我们需要例程来计算两个地理位置之间的距离,如清单 11-5 中的所示。

      清单 11-5。距离计算例程

      `// functions for determining the distance between two
          // latitude and longitude positions
          function toRadians(num) {
            return num * Math.PI / 180;
          }

      function distance(latitude1, longitude1, latitude2, longitude2) {
            // R is the radius of the earth in kilometers
            var R = 6371;

      var deltaLatitude = toRadians((latitude2-latitude1));
            var deltaLongitude = toRadians((longitude2-longitude1));
            latitude1 = toRadians(latitude1), latitude2 = toRadians(latitude2);

      var a = Math.sin(deltaLatitude/2) *
                    Math.sin(deltaLatitude/2) +
                    Math.cos(latitude1) *               Math.cos(latitude2) *
                    Math.sin(deltaLongitude/2) *
                    Math.sin(deltaLongitude/2);

      var c = 2 * Math.atan2(Math.sqrt(a),
                                   Math.sqrt(1-a));
            var d = R * c;
            return d;
          }

      // latitude and longitude for the finish line in the Lake Tahoe race
          var finishLat = 39.17222;
          var finishLong = -120.13778;`

      在这组熟悉的函数中——之前在第五章中使用过——我们用一个distance函数计算两点之间的距离。这些细节并不特别重要,也不是赛道上距离的最准确表示,但它们对我们的例子来说已经足够了。

      在终点线,我们为比赛的终点线位置确定了纬度和经度。正如你将看到的,我们将这些坐标与即将到来的参赛者位置进行比较,以确定参赛者离终点线的距离,从而确定他们在比赛中的排名。

      现在,让我们来看一小段用于显示页面的 HTML 标记。

              <h2>Live T216 Leaderboard</h2>         <p id="leaderboardStatus">Leaderboard: Connecting...</p>         <div id="leaderboard"></div>

      尽管大部分页面 HTML 与我们的演示无关,但在这几行代码中,我们用 idleaderboardStatusleaderboard声明了一些命名元素。leaderboardStatus是我们显示 WebSocket 连接信息的地方。我们将在排行榜上插入div元素,以指示我们从 WebSocket 消息中接收的位置信息,使用清单 11-6 中所示的实用函数。

      清单 11-6。位置信息效用函数

      `    // display the name and distance in the page
          function displayRacerLocation(name, distance) {
              // locate the HTML element for this ID
              // if one doesn't exist, create it
              var incomingRow = document.getElementById(name);
              if (!incomingRow) {
                  incomingRow = document.createElement('div');
                  incomingRow.setAttribute('id', name);
                  incomingRow.userText = name;

      document.getElementById("leaderboard").appendChild(incomingRow);
              }

      incomingRow.innerHTML = incomingRow.userText + " is " +
                                    Math.round(distance*10000)/10000 + " km from the finish line";
          }`

      这个实用程序是一个简单的显示例程,它获取参赛者的名字和离终点线的距离。图 11-9 显示了index.html页面上的引导板部分。

      Image

      图 11-9。竞赛领导委员会

      该名称有两个用途:它不仅被放入该赛车手的状态消息中,还被用来引用存储该赛车手状态的唯一的div元素。如果我们的参赛者已经有了一个div,当我们使用标准的document.getElementById()程序查找时,我们会找到它。如果该参赛者的页面中不存在div,我们将创建一个并将其插入到leaderboard区域。无论哪种方式,我们都用离终点线的最新距离更新对应于该赛车手的div元素,这将立即在页面的显示中更新它。如果你已经阅读了第七章,你会从我们在那里创建的示例应用中熟悉这一点。

      我们的下一个函数是消息处理器,每当数据从 broadcasting race WebSocket 服务器返回时都会调用它,如清单 11-7 所示。

      清单 11-7。 WebSocket 消息处理功能

      `    // callback when new position data is retrieved from the websocket
          function dataReturned(locationData) {
              // break the data into ID, latitude, and longitude
              var allData = locationData.split(";");
              var incomingId   = allData[1];
              var incomingLat  = allData[2];
              var incomingLong = allData[3];

      // update the row text with the new values
              var currentDistance = distance(incomingLat, incomingLong, finishLat, finishLong);

      // store the incoming user name and distance in storage
              window.sessionStorage[incomingId] = currentDistance;

      // display the new user data in the page
              displayRacerLocation(incomingId, currentDistance);
          }`

      这个函数接受前面描述的格式的字符串,一个分号分隔的消息,包含一个参赛者的姓名、纬度和经度。我们的第一步是使用 JavaScript split()例程将它分割成组件,分别产生incomingIdincomingLatincomingLong

      接下来,它将赛车的纬度和经度,以及终点线的纬度和经度传递给我们之前定义的distance实用程序方法,将结果距离存储在currentDistance变量中。

      现在我们实际上已经有了一些值得存储的数据,我们可以看一下使用 Web 存储的调用。

              // store the incoming user name and distance in storage         window.sessionStorage[incomingId] = currentDistance;

      在这一行中,我们使用窗口上的sessionStorage对象将比赛者离终点线的当前距离存储为比赛者姓名和 ID 下的值。换句话说,我们将在会话存储中设置一个值,键是赛车手的名字,值是赛车手离终点的距离。您马上会看到,当用户在 web 站点上从一个页面导航到另一个页面时,将从存储器中检索这些数据。在函数的最后,我们调用我们先前定义的displayLocation()例程,以确保最近的位置更新在当前页面中可视地显示。

      现在,让我们看看存储示例中的最后一个函数——清单 11-8 中的加载例程,每当访问者访问网页时就会触发。

      清单 11-8。初始页面加载例程

      `    // when the page loads, make a socket connection to the race broadcast server
          function loadDemo() {
              // make sure the browser supports sessionStorage
              if (typeof(window.sessionStorage) === "undefined") {
                  document.getElementById("leaderboardStatus").innerHTML = "Your browser does
                           not support HTML5 Web Storage";
                  return;
              }
              var storage = window.sessionStorage;
              // for each key in the storage database, display a new racer
              // location in the page
              for (var i=0; i < storage.length; i++) {
                  var currRacer = storage.key(i);
                  displayRacerLocation(currRacer, storage[currRacer]);
              }

      // test to make sure that Web Sockets are supported
              if (window.WebSocket) {

      // the location where our broadcast WebSocket server is located
                  url = "ws://websockets.org:7999/broadcast";
                  socket = new WebSocket(url);
                  socket.onopen = function() {
                      document.getElementById("leaderboardStatus").innerHTML = "Leaderboard:

      Connected!";
                  }
                  socket.onmessage = function(e) {
                      dataReturned(e.data);
                  }
              }
          }`

      这是一个比其他函数更长的函数,并且有很多正在进行的函数。让我们一步一步来。首先,如清单 11-9 所示,我们做了一个基本的错误检查,通过检查查看页面的浏览器在窗口对象上是否支持sessionStorage。如果sessionStorage不可访问,我们简单地更新leaderboardStatus区域来指示,然后退出加载程序。在这个例子中,我们不会试图解决浏览器存储不足的问题。

      清单 11-9。检查浏览器支持

              // make sure the browser supports sessionStorage         if (typeof(window.sessionStorage) === "undefined") {             document.getElementById("leaderboardStatus").innerHTML = "Your browser does                      not support HTML5 Web Storage";             return;         }

      `images但是,我们的目标是展示存储如何优化用户和网络的体验。

      我们在页面加载上做的下一件事是使用存储来检索已经提供给我们网站的这个或其他页面的任何参赛者距离结果。回想一下,我们在每个站点页面上运行相同的脚本代码块,这样,当用户浏览不同的位置时,leader board 会跟随他们。因此,leader board 可能已经将其他页面的值存储到存储器中,这些值将在加载时直接在这里检索和显示,如清单 11-10 所示。只要用户不关闭窗口、选项卡或浏览器,先前保存的值将在导航期间跟随用户,从而清除会话存储。

      清单 11-10。显示存储的参赛者数据

      `        var storage = window.sessionStorage;

      // for each key in the storage database, display a new racer
              // location in the page
              for (var i=0; i < storage.length; i++) {
                  var currRacer = storage.key(i);
                  displayRacerLocation(currRacer, storage[currRacer]);
              }`

      这是代码的一个重要部分。这里,我们查询会话的长度——换句话说,存储包含的键的数量。然后,我们使用storage.key()获取每个键,并将其存储到currRacer变量中,稍后使用该变量引用带有storage[currRacer]的键的相应值。键和它的值一起表示一个参赛者和该参赛者的距离,它们存储在对上一页的访问中。

      一旦我们有了先前存储的参赛者姓名和距离,我们就使用displayRacerLocation()函数显示它们。这一切在页面加载时发生得非常快,导致页面立即用先前传输的值填充其引导板。

      `images 注意我们的示例应用依赖于作为唯一一个将值存储到会话存储区的应用。如果您的应用需要与其他数据共享存储对象,那么您将需要使用一种更细致的键策略,而不是简单地在根级别存储键。我们将在“实用附加功能”一节中探讨另一种储物策略。

      我们的最后一个加载行为是使用一个简单的 WebSocket 将页面连接到 racer 广播服务器,如清单 11-11 所示。

      清单 11-11。连接 WebSocket 广播服务

      `        // test to make sure that WebSocket is supported
              if (window.WebSocket) {

      // the location where our broadcast WebSocket server is located
                  // for the sake of example, we'll just show websockets.org
                  url = "ws://websockets.org:7999/broadcast";
                  socket = new WebSocket(url);
                  socket.onopen = function() {
                      document.getElementById("leaderboardStatus").innerHTML = "Leaderboard:
                               Connected!";
                  }
                  socket.onmessage = function(e) {
                      dataReturned(e.data);
                  }
              }`

      正如我们之前在 WebSocket 章节中所做的,我们首先通过检查window.WebSocket对象的存在来确保浏览器支持 WebSocket。一旦我们确认它存在,我们就连接到运行 WebSocket 服务器的 URL。该服务器广播前面列出的分号分隔格式的赛车位置消息,每当我们通过socket.onmessage回调接收到这些消息之一时,我们调用前面讨论过的dataReturned()函数来处理和显示它。我们还使用socket.onopen回调用一条简单的诊断消息来更新我们的leaderboardStatus区域,以表明套接字成功打开。

      我们的load套路到此结束。我们在脚本块中声明的最后一个代码块是注册函数,它请求在页面加载完成时调用loadDemo()函数:

          // add listeners on page load and unload     window.addEventListener("load", loadDemo, true);

      正如您以前多次看到的,这个事件监听器请求在窗口完成加载时调用我们的loadDemo()函数。

      但是,我们如何将赛车数据从赛道传输到广播 WebSocket 服务器并进入我们的页面呢?实际上,我们可以使用之前在 WebSocket 章节中声明的 tracker 示例,只需将其连接 URL 指向之前列出的广播服务器。然而,我们也创建了一个非常简单的 racer 广播源页面,如清单 11-12 所示,它有类似的用途。理论上,这个页面可以在参赛者的移动设备上运行。尽管它本身并不包含任何 Web 存储代码,但当在支持 WebSocket 和地理定位的浏览器中运行时,这是一种传输正确格式化的数据的便捷方式。文件racerBroadcast.html可以从本书提供的网站示例区域获得。

      清单 11-12。【racerBroadcast.html 文件内容

      `

      <head> <title>Racer Broadcast</title> <link rel="stylesheet" href="styles.css"> </head>

      Racer Broadcast

      Racer name:

      Geolocation:

      HTML5 Geolocation not![Image](https://gitee.com/OpenDocCN/vkdoc-html-css-zh/raw/master/docs/pro-h5-prog/img/U002.jpg)  started.

      WebSocket:

      HTML5 Web Sockets are![Image](https://gitee.com/OpenDocCN/vkdoc-html-css-zh/raw/master/docs/pro-h5-prog/img/U002.jpg) not supported in your browser.

      `

      我们不会花太多的篇幅详细讨论这个文件,因为它与第七章中的跟踪器示例几乎相同。主要区别在于该文件包含一个用于输入参赛者姓名的文本字段:

      Racer name: <input type="text" id="racerName" value="Racer X"/>

      现在,参赛者的姓名作为数据字符串的一部分发送到广播服务器:

      var toSend =    ";" + document.getElementById("racerName").value                     + ";" + latitude + ";" + longitude;

      要自己尝试一下,在支持 Web 存储、地理定位和 WebSocket 的浏览器中打开两个窗口,比如 Google Chrome。首先,加载跑步俱乐部的index.html页面。您将看到它使用 WebSocket 连接到比赛广播站点,然后等待任何参赛者数据通知。在第二个窗口中,打开racerBroadcast.html文件。在这个页面连接到 WebSocket 广播站点后,输入您的参赛者的名字,然后单击 Start 按钮。你会看到赛车手广播已经传送了你最喜欢的赛车手的位置,它应该会出现在你的另一个浏览器窗口的排行榜上。图 11-10 显示了这个样子。

      Image

      图 11-10。比试佩奇和 racerBroadcast.html 并排

      现在,使用页面左侧的注册和关于比赛链接导航到其他赛车俱乐部页面。因为所有这些页面都已被配置为加载我们的脚本,所以它们将立即加载并使用以前的参赛者数据填充排行榜,这些数据是在浏览其他页面时提交的。发送更多的参赛者状态通知(从广播页面),当你导航时,你也会看到它们通过俱乐部网站页面传播。

      现在我们已经完成了代码,让我们回顾一下我们构建了什么。我们已经创建了一个简单的功能块,适合包含在一个共享的 JavaScript 库中,它连接到一个 WebSocket 广播服务器并监听 racer 更新。当收到一个更新时,脚本显示页面中的位置并且使用sessionStorage存储它。当加载页面时,它检查任何先前存储的 racer 位置值,从而在用户导航站点时保持状态。我们从这种方法中获得了哪些好处?

      • 减少网络流量:比赛信息存储在浏览器本地。一旦它到达,它就停留在每次页面加载,而不是使用 cookies 或服务器请求再次获取它。
      • 立即显示值:浏览器页面本身可以缓存,而不是从网络加载,因为页面的动态部分——当前排行榜状态——是本地数据。这些数据可以快速显示,无需任何网络加载时间。
      • 临时存储:比赛结束后,比赛数据就没什么用了。因此,我们将它存储在会话存储区,这意味着当窗口或选项卡关闭时,它将被丢弃,不再占用任何空间。

      关于防弹的一句话

      Brian 说:“在这个例子中,我们只用了几行脚本代码就完成了很多工作。但是不要以为在一个真实的、可公开访问的网站上,一切都这么简单。我们采用了一些生产应用无法接受的捷径。

      例如,我们的消息格式不支持名称相似的参赛者,最好用代表每个参赛者的唯一标识符来代替。我们的距离计算是“直线的”,并不能真正反映越野比赛的进展。标准免责声明适用-更多的本地化,更多的错误检查,更多的关注细节将使您的网站为所有参与者服务。"

      我们在本例中演示的相同技术可以应用于任何数量的数据类型:聊天、电子邮件和体育比分是可以使用本地或会话存储进行缓存和逐页显示的其他示例,正如我们在这里展示的那样。如果您的应用定期在浏览器和服务器之间来回发送特定于用户的数据,请考虑使用 Web 存储来简化您的流程。

      浏览器数据库存储的未来

      键值存储 API 非常适合持久化数据,但是可以查询的索引存储呢?HTML5 应用最终也会访问索引数据库。数据库 API 的具体细节仍在酝酿中,有两个主要的提议。

      Web SQL 数据库

      其中一个提议,Web SQL 数据库,已经在 Safari、Chrome 和 Opera 中实现。表 11-2 显示了浏览器对 Web SQL 数据库的支持。

      Image

      Web SQL 数据库允许应用通过异步 JavaScript 接口访问 SQLite。虽然它不是通用 Web 平台的一部分,也不是 HTML5 应用最终推荐的数据库 API,但 SQL API 在针对特定平台(如 mobile Safari)时会很有用。无论如何,这个 API 在浏览器中展示了数据库的威力。就像其他存储 API 一样,浏览器可以限制每个源的可用存储量,并在用户数据被清除时清除数据。

      Web SQL 数据库的命运

      Frank 说:“尽管 Web SQL DB 已经存在于 Safari、Chrome 和 Opera 中,但它不会在 Firefox 中实现,并且在 WHATWG wiki 上被列为‘已停止’。该规范定义了一个 API 来执行以字符串形式给出的 SQL 语句,并遵从 SQL 方言的 SQLite。由于不希望标准要求特定的 SQL 实现,所以 Web SQL 数据库已经被更新的规范所超越,即索引数据库(以前称为 WebSimpleDB ),它更简单并且不依赖于特定的 SQL 数据库版本。索引数据库的浏览器实现目前正在进行中,我们将在下一节讨论它们。”

      因为 Web SQL 数据库已经在野外实现了,所以我们包括了一个基本的例子,但是省略了 API 的完整细节。这个例子演示了 Web SQL 数据库 API 的基本用法。它打开一个名为mydb的数据库,创建一个racers表(如果这个名称的表还不存在),并用一个预定义名称的列表填充这个表。图 11-11 显示了 Safari 的 Web Inspector 中带有 racers 表的数据库。

      Image

      图 11-11。Safari 浏览器中带有参赛者表格的数据库

      首先,我们按名称打开一个数据库。window.openDatabase()函数返回一个Database对象,通过它进行数据库交互。openDatabase()函数接受一个名称以及可选的版本和描述。对于开放的数据库,应用代码现在可以启动事务。使用transaction.executeSql()函数在事务上下文中执行 SQL 语句。这个简单的例子使用executeSql()创建一个表,将个赛车手的名字插入表中,然后查询数据库创建一个 HTML 表。图 11-12 显示了从表格中检索到的姓名列表的输出 HTML 文件。

      Image

      图 11-12。【sql.html 展示参赛者评选结果

      数据库操作可能需要一些时间才能完成。查询在后台运行,而不是在结果集可用之前阻止脚本执行。当结果可用时,作为第三个参数给定给executeSQL()的函数被回调,事务和结果集作为参数。

      清单 11-13 显示了文件sql.html的完整代码;所示的示例代码也位于code/storage文件夹中。

      清单 11-13。使用 Web SQL 数据库 API

      `

      <title>Web SQL Database</title> <script>

      // open a database by name
          var db = openDatabase('db', '1.0', 'my first database', 2 * 1024 * 1024);
          function log(id, name) {
              var row = document.createElement("tr");
              var idCell = document.createElement("td");
              var nameCell = document.createElement("td");
              idCell.textContent = id;
              nameCell.textContent = name;
              row.appendChild(idCell);
              row.appendChild(nameCell);

      document.getElementById("racers").appendChild(row);     }

      function doQuery() {
              db.transaction(function (tx) {
                      tx.executeSql('SELECT * from racers', [], function(tx, result) {
                          // log SQL result set
                          for (var i=0; i<result.rows.length; i++) {
                              var item = result.rows.item(i);
                              log(item.id, item.name);
                          }
                      });
                  });
          }

      function initDatabase() {
              var names = ["Peter Lubbers", "Brian Albers", "Frank Salim"];

      db.transaction(function (tx) {
                      tx.executeSql('CREATE TABLE IF NOT EXISTS racers (id integer primary keyImage
      autoincrement, name)');

      for (var i=0; i<names.length; i++) {
                          tx.executeSql('INSERT INTO racers (name) VALUES (?)', [names[i]]);
                      }

      doQuery();
                  });
          }

      initDatabase();

      Web SQL Database

               
      IdName
      `

      索引数据库 API

      第二个关于浏览器数据库存储的提议在 2010 年获得了关注。索引数据库 API 受到微软和 Mozilla 的支持,被视为 Web SQL 数据库的一个计数器。Web SQL 数据库希望将已建立的 SQL 语言引入浏览器,而索引数据库旨在引入低级索引存储功能,希望在索引核心之上构建更多开发人员友好的库。

      Web SQL API 支持使用查询语言对数据表发出 SQL 语句,而索引 DB API 直接对树状对象存储引擎发出同步或异步函数调用。与 Web SQL 不同,索引数据库不处理表和列。

      对索引数据库 API 的支持正在增加(见表 11-3 )。

      微软和 Mozilla 已经宣布他们将不支持 Web SQL 数据库,而是支持索引数据库。谷歌的 Chrome 也加入了支持,因此,索引数据库很可能是浏览器中标准化结构化存储的未来。他们的理由包括 SQL 不是真正的标准,以及 Web SQL 的唯一实现是 SQLite 项目。只有一个实现和一个松散的标准,他们不能在 HTML5 规范中支持 WebSQL。

      索引数据库 API 避开了查询字符串,支持将值直接存储在 JavaScript 对象中的低级 API。存储在数据库中的值可以通过键或使用索引来检索,并且可以以同步或异步方式访问 API。与 WebSQL 提案一样,索引数据库的范围是由源确定的,因此您只能访问在您自己的 web 页面中创建的存储。

      索引数据库存储的创建或修改是在事务的上下文中完成的,事务可以分为只读、读写或版本更改。虽然前两个可能是不言自明的,但是只要操作将修改数据库的结构,就会使用 VERSION_CHANGE 事务类型。

      从索引数据库中检索记录是通过游标对象完成的。游标对象以递增或递减的顺序遍历一系列记录。在任何时候,游标要么有值,要么没有值,因为它要么正在加载,要么已经到达迭代的末尾。

      索引数据库 API 的详细描述超出了本书的范围。如果您打算在内置 API 之上实现一个查询引擎,您应该参考官方规范,否则,您应该等待一个基于标准之上的建议引擎,以使用一个对开发人员更友好的数据库 API。在这一点上,没有第三方库获得突出地位或重要支持。

      为什么要用锤子…

      Brian 说:“…当你可以使用这些金属锭、熔炉和你选择的模具时?在 Mozilla 博客上,阿伦·阮冈纳赞认为他会欢迎像 Web SQL API 这样建立在索引数据库标准之上的 API。这种态度困扰了许多开发人员,因为人们普遍认为,为了使索引数据库可用,需要在标准之上构建第三方 JavaScript 库。对于大多数 web 开发人员来说,索引数据库本身太复杂了,无法以当前的形式使用它。

      这就引出了一个问题:如果开发人员最终需要第三方库来利用内置的存储 API,那么简单地用本机代码构建存储,而不是作为必须在运行时下载和解释的 JavaScript 库,难道不是明智的吗?时间会证明索引数据库是否适合大多数人的需求。"

      实用的临时演员

      有时,有些技术并不适合我们的常规例子,但仍然适用于许多类型的 HTML5 应用。我们在这里向你展示一些简短但常见的实用附加功能。

      JSON 对象存储

      尽管 Web 存储规范允许将任何类型的对象存储为键值对,但在当前的实现中,一些浏览器将值限制为文本字符串数据类型。但是,有一个实用的解决方法,因为现代版本的浏览器包含对 JavaScript 对象符号(JSON)的内置支持。

      JSON 是数据交换的标准,可以将对象表示为字符串,反之亦然。十多年来,JSON 一直被用来通过 HTTP 将对象从浏览器客户端传输到服务器。现在,我们可以用它来序列化 Web 存储中的复杂对象,以便持久化复杂的数据类型。考虑清单 11-14 中的脚本块。

      清单 11-14。 JSON 对象存储

      ``

      如您所见,该脚本包含事件侦听器,用于在浏览器窗口中注册加载和卸载事件的处理程序。在这种情况下,处理程序分别调用loadData()saveData()函数。

      loadData()函数中,向会话存储区查询存储键的值,并将该键传递给JSON.parse()函数。JSON.parse()例程将获取一个先前保存的对象的字符串表示,并将其重组为原始对象的副本。每次页面加载时都会调用这个例程。

      类似地,saveData()函数接受一个数据值,并对其调用JSON.stringify(),将其转换为对象的字符串表示。该字符串又被存储回存储器中。通过在unload浏览器事件上注册 saveData()函数,我们确保它在用户每次导航离开或关闭浏览器或窗口时被调用。

      这两个函数的实际结果是,我们希望在存储中跟踪的任何对象,无论它是否是复杂的对象类型,都可以在用户进出应用时存储和重新加载。这允许开发人员将我们已经展示的技术扩展到非文本数据。

      分享的窗口

      正如前面提到的,Web 存储事件能够在浏览相同来源的任何窗口中触发,这具有一些强大的含义。这意味着存储可以用来在窗口之间发送消息,即使它们并不都使用存储对象本身。这反过来意味着我们现在可以跨具有相同来源的窗口共享数据。

      让我们使用一些代码示例来看看这是如何工作的。为了监听跨窗口消息,一个简单的脚本只需要注册一个存储事件的处理程序。让我们假设在[www.example.com/storageLog.html](http://www.example.com/storageLog.html)运行的页面包含清单 11-15 中的所示的代码(本例中的示例文件storageLog.html也位于code/storage文件夹中)。

      清单 11-15。使用存储的跨窗口通信

      `// display records of new storage events
      function displayStorageEvent(e) {
        var incomingRow = document.createElement('div');
        document.getElementById("container").appendChild(incomingRow);

      var logged = "key:" + e.key + ", newValue:" + e.newValue + ", oldValue:" +
                      e.oldValue + ", url:" + e.url + ", storageArea:" + e.storageArea;
                      incomingRow.innerHTML = logged;
      }

      // add listeners on storage events
      window.addEventListener("storage", displayStorageEvent, true);`

      在为storage事件类型注册一个事件监听器之后,该窗口将接收任何页面中存储变化的通知。例如,如果正在浏览同一原点的浏览器窗口设置或更改了新的存储值,storageLog.html页面将收到通知。因此,要向接收窗口发送消息,发送窗口只需修改一个存储对象,其新旧值将作为通知的一部分发送。例如,如果使用localStorage.setItem()更新一个存储值,那么位于同一原点的storageLog.html页面中的displayStorageEvent()处理程序将接收一个事件。通过仔细协调事件名称和值,这两个页面现在可以进行通信,这在以前是很难实现的。图 11-13 显示了运行中的storageLog.html页面,简单地记录它接收到的存储事件。

      Image

      图 11-13。storageLog.html 页面日志存储事件概述

      总结

      在本章中,我们展示了如何使用 Web 存储作为浏览器 cookies 的替代方案,在窗口、标签和(用localStorage)甚至浏览器重启之间保存数据的本地副本。您已经看到,可以通过使用sessionStorage在窗口之间适当地隔离数据,并通过使用存储事件共享数据——甚至跨窗口共享。在我们的完整示例中,我们展示了一种实用的方法,可以在用户浏览网站时使用存储来逐页跟踪数据,这也可以很容易地应用于其他数据类型。我们甚至演示了在页面加载或卸载时如何存储非文本数据类型,以便在不同的访问中保存和恢复页面的状态。

      在下一章,我们将向您展示 HTML5 如何让您创建离线应用。

      十二、创建 HTML5 脱机 Web 应用

      在这一章中,我们将探索你可以用离线 HTML5 应用做什么。HTML5 应用不一定需要持续访问网络,加载缓存资源现在可以由开发人员更灵活地控制。

      html 5 离线 Web 应用概述

      使用应用缓存的第一个也是最明显的原因是离线支持。在普遍连接的时代,离线应用仍然是可取的。没有网络连接时,您会做什么?在您说间歇性连接的时代已经结束之前,请考虑以下几点:

      • 你乘坐的所有航班都有机上无线网络吗?
      • 你的移动互联网设备有完美的信号覆盖吗(你最后一次看到零信号是什么时候)?
      • 你做报告时能指望有互联网连接吗?

      随着越来越多的应用转移到 Web 上,假设所有用户 24/7 不间断连接是很诱人的,但互联网的现实是中断时有发生,而且在像航空旅行这样的情况下,可以预见一次会发生几个小时。

      间歇性连接一直是网络计算系统的致命弱点。如果您的应用依赖于与远程主机的通信,而这些主机是不可达的,那么您就不走运了。但是,当您连接到互联网时,web 应用总是最新的,因为每次使用时代码都是从远程位置加载的。

      如果您的应用只需要偶尔的通信,只要应用资源存储在本地,它们仍然是有用的。随着纯浏览器设备的出现,在没有持续连接的情况下继续运行的 web 应用只会变得更加重要。历史上,不需要持续连接的桌面应用比 web 应用更有优势。

      HTML5 公开了对应用缓存的控制,以便两全其美:用 web 技术构建的应用可以在浏览器中运行,在线时可以更新,但也可以离线使用。但是,必须显式地使用这个新的脱机应用特性,因为当前的 web 服务器没有为脱机应用提供任何默认的缓存行为。

      HTML5 离线应用缓存使得在没有网络连接的情况下运行应用成为可能。你不需要连接到互联网只是为了起草一封电子邮件。HTML5 引入了离线应用缓存,允许 Web 应用在没有网络连接的情况下运行。

      应用开发人员可以指定包含 HTML5 应用的特定附加资源(HTML、CSS、JavaScript 和图像),以使应用可供离线使用。这有许多使用案例,例如:

      • 阅读和撰写电子邮件
      • 编辑文档
      • 编辑和显示演示文稿
      • 创建待办事项列表

      使用离线存储可以避免加载应用所需的正常网络请求。如果缓存清单是最新的,浏览器知道它不需要检查其他资源是否也是最新的,并且大部分应用可以从本地应用缓存中快速加载。此外,从缓存中加载资源(而不是发出多个 HTTP 请求来查看资源是否已经更新)可以节省带宽,这对移动 web 应用尤其重要。目前,与桌面应用相比,web 应用的加载速度较慢。缓存可以弥补这一点。

      应用缓存为开发人员提供了对缓存的明确控制。缓存清单文件允许您将相关资源分组到一个逻辑应用中。这是一个强大的概念,可以赋予 web 应用一些桌面应用的特征。你可以用新的、创造性的方式使用这种额外的力量。

      缓存清单文件中标识的资源创建了所谓的应用缓存,这是浏览器持久存储资源的地方,通常在磁盘上。一些浏览器为用户提供了查看应用缓存中数据的方法。例如,Firefox 内部about:cache页面中的离线缓存设备部分向您展示了关于应用缓存的细节,以及查看缓存中单个文件的方法,如图图 12-1 所示。

      Image

      图 12-1。在 Firefox 中查看应用缓存条目

      类似地,内部页面chrome://appcache-internals/提供了关于存储在系统上的不同应用缓存内容的详细信息。它还提供了查看内容和完全移除这些缓存的方法,如图 12-2 所示。

      Image

      图 12-2。在 Chrome 中查看应用缓存条目

      浏览器支持 HTML5 离线网络应用

      有关当前浏览器支持(包括移动支持)的完整概述,请参考[caniuse.com](http://caniuse.com)并搜索离线 Web 应用或应用缓存。如果您必须支持旧的浏览器,那么在使用 API 之前,最好先看看是否支持应用缓存。本章后面的“检查浏览器支持”一节将向您展示如何以编程方式检查浏览器支持。

      使用 HTML5 应用缓存 API

      在这一节中,我们将探索如何使用离线 Web 应用 API 的细节。

      检查浏览器支持

      在尝试使用脱机 Web 应用 API 之前,最好检查一下浏览器支持。清单 12-1 展示了如何做到这一点。

      清单 12-1。检查浏览器对离线 Web 应用 API 的支持

      if(window.applicationCache) {   // this browser supports offline applications }

      创建简单的离线应用

      假设您想要创建一个包含 HTML 文档、样式表和 JavaScript 文件的单页应用。为了给你的 HTML5 应用添加离线支持,你需要在html元素中包含一个manifest属性,如清单 12-2 所示。

      清单 12-2。HTML 元素上的清单属性

      `

        .   .   . `

      在 HTML 文档旁边,提供一个带有扩展名*.appcache的清单文件,指定要缓存哪些资源。清单 12-3 显示了一个示例缓存清单文件的内容。

      清单 12-3。示例缓存清单文件的内容

      CACHE MANIFEST example.html example.js example.css example.gif

      离线

      为了让应用知道间歇性连接,HTML5 浏览器还公开了其他事件。您的应用可能有不同的在线和离线行为模式。对window.navigator对象的一些添加使得这变得更容易。首先,navigator.onLine是一个布尔属性,表示浏览器是否认为自己在线。当然,onLinetrue值并不能确定 web 应用必须与之通信的服务器可以从用户的机器上到达。另一方面,false值意味着浏览器甚至不会尝试通过网络连接。清单 12-4 显示了如何检查你的页面是在线还是离线。

      清单 12-4。检查在线状态

      `// When the page loads, set the status to online or offline
      function loadDemo() {
        if (navigator.onLine) {
          log("Online");
        } else {
          log("Offline");
        }
      }

      // Now add event listeners to notify a change in online status
      window.addEventListener("online", function(e) {
        log("Online"); }, true);

      window.addEventListener("offline", function(e) {
        log("Offline");
      }, true);`

      清单文件

      脱机应用由一个清单组成,该清单列出了浏览器将缓存以供脱机使用的一个或多个资源。清单文件具有 MIME 类型text/cache-manifest。Python 标准库中的SimpleHTTPServer模块将提供带有.manifest扩展名和头文件Content-type: text/cache-manifest的文件。要配置设置,打开文件PYTHON_HOME/Lib/mimetypes.py,并添加以下行:

      '.appcache'    : 'text/cache-manifest manifest',

      其他 web 服务器可能需要额外的配置。例如,对于 Apache HTTP Server,您可以通过添加以下行来更新 conf 文件夹中的mime.types文件:

      text/cache-manifest appcache

      如果您使用的是 Microsoft IIS,在您网站的主页中,双击 MIME 类型图标,然后在添加 MIME 类型对话框中添加 MIME 类型为text/cache-manifest.appcache扩展名。

      清单语法是以CACHE MANIFEST(作为第一行)开始的简单的行分隔文本。行可以以CRLFCRLF结尾——格式很灵活——但是文本必须是 UTF-8 编码的,这是大多数文本编辑器的典型输出。注释以哈希符号开始,并且必须在自己的行上;您不能将注释附加到文件中的其他非注释行。

      清单 12-5。包含所有可能部分的示例清单文件

      `CACHE MANIFEST

      files to cache

      about.html
      html5.css
      index.html
      happy-trails-rc.gif
      lake-tahoe.JPG

      do not cache signup page

      NETWORK
      signup.html

      FALLBACK
      signup.html     offline.html
      /app/ajax/      default.html`

      让我们看看不同的部分。

      如果没有指定CACHE:标题,列出的文件将被视为要缓存的文件(缓存是默认行为)。下面的简单清单指定必须缓存三个文件(index.htmlapplication.jsstyle.css):

      CACHE MANIFEST index.html application.js style.css

      类似地,下面的部分将做同样的事情(如果您愿意,可以在一个清单文件中多次使用相同的CACHENETWORKFALLBACK头):

      `CACHE MANIFEST

      Cache section

      CACHE:
      Index.html
      application.js
      style.css`

      通过在CACHE部分列出一个文件,您指示浏览器从应用缓存中提供文件,即使应用在线。没有必要指定应用的主 HTML 资源。最初指向清单文件的 HTML 文档被隐式包含在内(这称为主条目)。但是,如果您希望缓存多个 HTML 文档,或者希望多个 HTML 文档作为可缓存应用的可能入口点,那么它们都应该在缓存清单文件中明确列出。

      FALLBACK条目允许您给出替代路径来替换无法获取的资源。清单 12-5 中的清单会导致对/app/ajax/或以/app/ajax/开头的子路径的请求在/app/ajax/*不可达时退回到default.html

      NETWORK指定总是使用网络获取的资源。简单地从清单中省略这些文件的区别在于,主条目被缓存,而没有在清单文件中显式列出。为了确保应用从服务器请求文件,即使缓存的资源缓存在应用缓存中,您可以将该文件放在NETWORK:部分。

      应用缓存 API

      ApplicationCache API 是使用应用缓存的接口。一个新的window.applicationCache对象触发了几个与缓存状态相关的事件。该对象有一个数字属性window.applicationCache.status,表示缓存的状态。缓存可以有六种状态,如表 12-1 所示。

      Image

      今天,网络上的大多数页面都没有指定缓存清单,并且是未缓存的。Idle 是具有缓存清单的应用的典型状态。处于空闲状态的应用的所有资源都由浏览器存储,没有更新正在进行。如果曾经有一个有效的缓存,但是清单现在丢失了,则缓存进入过时状态。API 中有对应于这些状态的事件(和回调属性)。例如,当缓存在更新后进入空闲状态时,就会触发缓存事件。此时,应用可能会通知用户,他们可以断开网络连接,但仍然希望应用在脱机模式下可用。表 12-2 显示了一些常见事件及其相关的缓存状态。

      Image

      此外,当没有可用的更新或发生错误时,还有指示更新进度的事件:

      • onerror
      • onnoupdate
      • onprogress

      window.applicationCache有一个update()方法。调用update()请求浏览器更新缓存。这包括检查清单文件的新版本,并在必要时下载新资源。如果没有缓存或者缓存过时,将会引发错误。

      应用缓存正在运行

      尽管创建清单文件并在应用中使用它相对简单,但是在服务器上更新页面时发生的事情并不像您想象的那样直观。要记住的主要事情是,一旦浏览器成功地将应用的资源缓存在应用缓存中,它将总是首先从缓存中提供这些页面。之后,浏览器将只做一件事:检查服务器上的清单文件是否已被更改。

      为了更好地理解这个过程是如何工作的,让我们使用清单 12-5 中显示的清单文件来一步步完成一个示例场景。

      1. 当您第一次访问index.html页面时(在线时),比如说在[www.example.com](http://www.example.com),浏览器会加载页面及其子资源(CSS、JavaScript 和图像文件)。
      2. 在解析页面时,浏览器遇到 html 元素中的 manifest 属性,并继续加载在example.com站点的应用缓存的缓存(默认)和回退部分中列出的所有文件(浏览器允许大约 5 MB 的存储空间)。
      3. 从现在开始,当你导航到www.example.com时,浏览器将总是从应用缓存中加载站点,然后它将尝试检查清单文件是否已经更新(它只能在你在线时进行后者)。这意味着,如果你现在离线(自愿或不自愿),并将浏览器指向 http://www.example.com 的,浏览器将从应用缓存中加载该网站——是的,你仍然可以在离线模式下完整地使用该网站。
      4. 如果您尝试在脱机时访问缓存的资源,它将从应用缓存中加载。当您尝试访问网络资源(signup.html)时,将提供回退内容(offline.html)。只有当您重新联机时,网络文件才再次可用。
      5. 目前为止一切顺利。一切按预期运行。我们现在将试着带你穿过当你改变服务器上的内容时必须跨越的数字雷区。例如,当您更改服务器上的 about.html 页面,并通过在浏览器中重新加载该页面以在线模式访问该页面时,有理由期待更新后的页面出现。毕竟,你是在线的,可以直接访问服务器。然而,你只会看到和以前一样的旧页面,脸上可能带着困惑的表情。这是因为浏览器总是从应用缓存中加载页面,之后它只检查一件事:清单文件是否已经更新。因此,如果您希望下载更新的资源,您还必须对清单文件进行更改(不要只是“触摸”该文件,因为这不会被视为更改—它必须是逐字节的更改)。进行这种更改的一种常见方式是在文件顶部添加版本注释,如清单 12.5 所示。浏览器实际上并不理解版本注释,但这是一个很好的最佳实践。由于这个原因,也由于很容易忽略新的或删除的文件,建议您使用某种构建脚本来维护清单文件。html 5 Boilerplate 2.0(html5boilerplate.com)附带了一个构建文件,可以用来自动构建和版本化 appcache 文件,这是对已经很棒的资源的一个很好的补充。
      6. 当您对 about.html 页面和清单文件都进行了更改,并随后在线刷新浏览器中的页面时,您将再次失望地看到相同的旧页面。发生了什么事?尽管浏览器现在已经发现清单已经更新,并且将所有文件再次下载到新版本的缓存中,但是在执行服务器检查之前,页面已经从应用缓存中加载,并且浏览器不会自动在浏览器中为您重新加载页面。您可以将此过程与如何在后台下载新版本的软件程序(例如 Firefox 浏览器)进行比较,但需要重启程序才能生效。如果不能等待下一次页面刷新,可以通过编程方式为 onupdateready 事件添加一个事件侦听器,并提示用户刷新页面。一开始有点困惑,但仔细想想就明白了。

      使用应用缓存提升性能

      Peter 说:“应用缓存机制的一个很好的副作用是你可以用它来预取资源。常规浏览器缓存存储您访问过的页面,但存储的内容取决于客户端和服务器配置(浏览器设置和过期标题)。因此,至少可以说,依靠常规浏览器缓存返回特定页面是不稳定的——任何曾经试图依靠常规浏览器缓存在飞机上浏览网站页面的人可能都会同意这一点。

      然而,使用应用缓存,你不仅可以在访问页面时缓存它们,还可以缓存你还没有访问过的页面;它可以作为一种有效的预取机制。当需要使用这些预取的资源时,它将从本地磁盘上的应用缓存中加载,而不是从服务器上加载,从而大大加快加载速度。明智地使用(不要预取维基百科),您可以使用应用缓存来显著提高性能。需要记住的一件重要事情是,常规的浏览器缓存仍然有效,所以要注意误报,尤其是当您试图调试应用缓存行为时。"

      使用 HTML5 离线 Web 应用构建应用

      在这个示例应用中,我们将在跑步时跟踪跑步者的位置(断断续续或没有连接)。例如,Peter 去跑步,他将带着新的支持地理定位的手机和 HTML5 网络浏览器,但在他家周围的树林中并不总是有很好的信号。他想使用这个应用来跟踪和记录他的位置,即使他不能使用互联网。

      离线时,地理定位 API 应该可以在有硬件地理定位的设备上继续工作(比如 GPS ),但显然不能在使用 IP 地理定位的设备上工作。IP 地理定位需要网络连接来将客户端的 IP 地址映射到坐标。此外,脱机应用始终可以通过 API(如本地存储或索引数据库)访问本地机器上的持久存储。

      该应用的示例文件位于图书页面上的[www.apress.com](http://www.apress.com)和图书网站上的offline代码文件夹中,您可以通过导航到 code/offline 文件夹并发出以下命令来开始演示:

      Python –m SimpleHTTPServer 9999.

      在启动 web 服务器之前,确保您已经将 Python 配置为提供清单文件(带有*的文件)。appcache 扩展)与前面描述的正确 mime 类型。这是脱机 web 应用失败的最常见原因。如果它不像预期的那样工作,请在 Chrome 开发者工具中检查控制台,查看可能的描述性错误信息。

      这将在端口 9999 上启动 Python 的 HTTP 服务器模块(您可以在任何端口上启动它,但是您可能需要管理员权限来绑定到低于 1024 的端口。启动 HTTP 服务器后,您可以导航到[localhost:9999/tracker.html](http://localhost:9999/tracker.html)来查看运行中的应用。

      图 12-3 显示了当你第一次访问火狐网站时会发生什么:你被提示选择在你的电脑上存储数据(然而,注意,不是所有的浏览器都会在存储数据前提示你)。

      Image

      图 12-3。 Firefox 提示为网络应用存储数据

      在允许应用存储数据之后,应用缓存进程启动,浏览器开始下载应用缓存清单文件中引用的文件(这发生在页面加载之后,因此对页面的响应性影响最小。图 12-4 显示了 Chrome 开发者工具如何在资源窗格中提供关于localhost原点缓存内容的详细概述。它还在控制台中提供有关在处理页面和清单时触发的应用缓存事件的信息。

      Image

      图 12-4。Chrome 中的离线页面,详细介绍了 Chrome 开发者工具中的应用缓存

      要运行这个应用,您需要一个 web 服务器来服务这些静态资源。请记住,清单文件必须提供内容类型text/cache-manifest。如果您的浏览器支持应用缓存,但该文件提供了不正确的内容类型,您将收到缓存错误。一个简单的测试方法是查看 Chrome 开发者工具控制台中触发的事件,如图 12-4 所示;它会告诉您 appcache 文件是否使用了错误的 mime 类型。

      要使用完整的功能运行此应用,您需要一个可以接收地理位置数据的服务器。这个例子的服务器端补充可能会存储、分析和提供这些数据。它可能来自静态应用,也可能不来自静态应用。图 12-5 显示了在 Firefox 中以离线模式运行的示例应用。你可以在 Firefox 和 Opera 中使用文件Image离线工作来打开这个模式。其他浏览器没有这个便利功能,但是你可以断网。但是,请注意,断开网络连接不会中断与运行在 localhost 上的 Python 服务器的连接。

      Image

      图 12-5。离线模式下的应用

      为应用资源创建清单文件

      首先,在文本编辑器中,创建如下的tracker.appcache文件。该清单文件将列出属于该应用的文件:

      `CACHE MANIFEST

      JavaScript

      ./offline.js

      ./tracker.js

      ./log.js

      stylesheets

      ./html5.css

      images`

      为用户界面创建 HTML 结构和 CSS

      这是该示例的基本 UI 结构。tracker.htmlhtml5.css都将被缓存,因此应用将由应用缓存提供服务。

      `

      <html lang="en" manifest="tracker.appcache"> <head>     <title>HTML5 Offline Application</title>     <script src="log.js"></script>` `    <script src="offline.js"></script>     <script src="tracker.js"></script>     <link rel="stylesheet" href="html5.css"> </head> <body>     <header>       <h1>Offline Example</h1>     </header>


            

              
              

      Log


              

              

            

          

      `

      关于这个应用的离线能力,在这个 HTML 中有一些事情需要注意。第一个是 HTML 元素上的manifest属性。本书中的大多数 HTML 例子都省略了<html>元素,因为它在 HTML5 中是可选的。但是,脱机缓存的能力取决于在那里指定清单文件。

      第二个要注意的是按钮。这将使用户能够控制该应用的离线配置。

      创建离线 JavaScript

      对于这个例子,JavaScript 包含在多个带有<script>标签的.js文件中。这些脚本与 HTML 和 CSS 一起被缓存。

      <offline.js> /*  * log each of the events fired by window.applicationCache  */ window.applicationCache.onchecking = function(e) { `log("Checking for application update");
      }

      window.applicationCache.onnoupdate = function(e) {
          log("No application update found");
      }

      window.applicationCache.onupdateready = function(e) {
          log("Application update ready");
      }

      window.applicationCache.onobsolete = function(e) {
          log("Application obsolete");
      }

      window.applicationCache.ondownloading = function(e) {
          log("Downloading application update");
      }

      window.applicationCache.oncached = function(e) {
          log("Application cached");
      }

      window.applicationCache.onerror = function(e) {
          log("Application cache error");
      }

      window.addEventListener("online", function(e) {
          log("Online");
      }, true);

      window.addEventListener("offline", function(e) {
          log("Offline");
      }, true);

      /*
       * Convert applicationCache status codes into messages
       */
      showCacheStatus = function(n) {
          statusMessages = ["Uncached","Idle","Checking","Downloading","Update Ready","Obsolete"];
          return statusMessages[n];
      }

      install = function() {
          log("Checking for updates");
          try {
              window.applicationCache.update();
          } catch (e) {
              applicationCache.onerror();
          }
      }

      onload = function(e) {
          // Check for required browser features
          if (!window.applicationCache) {
              log("HTML5 Offline Applications are not supported in your browser.");
              return;
          }

      if (!navigator.geolocation) {
              log("HTML5 Geolocation is not supported in your browser.");
              return;
          }     if (!window.localStorage) {
              log("HTML5 Local Storage not supported in your browser.");
              return;
          }

      log("Initial cache status: " + showCacheStatus(window.applicationCache.status));
          document.getElementById("installButton").onclick = checkFor;
      }

      <log.js>
      log = function() {
          var p = document.createElement("p");
          var message = Array.prototype.join.call(arguments, " ");
          p.innerHTML = message;
          document.getElementById("info").appendChild(p);
      }`

      检查应用缓存支持

      除了离线应用缓存之外,这个示例还使用了地理位置和本地存储。我们确保浏览器在页面加载时支持所有这些功能。

      `onload = function(e) {
          // Check for required browser features
          if (!window.applicationCache) {
              log("HTML5 Offline Applications are not supported in your browser.");
              return;
          }

      if (!navigator.geolocation) {
              log("HTML5 Geolocation is not supported in your browser.");
              return;
          }

      if (!window.localStorage) {
              log("HTML5 Local Storage is not supported in your browser.");
              return;
          }

      if (!window.WebSocket) {
              log("HTML5 WebSocket is not supported in your browser.");
              return;
          }
          log("Initial cache status: " + showCacheStatus(window.applicationCache.status));
          document.getElementById("installButton").onclick = install;
      }`

      添加更新按钮处理程序

      接下来,添加更新应用缓存的更新处理程序,如下所示:

      install = function() {     log("Checking for updates");     try {         window.applicationCache.update();     } catch (e) {         applicationCache.onerror();     } }

      单击此按钮将明确启动缓存检查,这将导致在必要时下载所有缓存资源。当可用更新完全下载后,会在 UI 中记录一条消息。此时,用户知道应用已经成功安装,可以在脱机模式下运行。

      添加地理位置跟踪代码

      该代码基于第四章中的地理定位代码。它包含在tracker.js JavaScript 文件中。

      `/*
       * Track and report the current location
       */
      var handlePositionUpdate = function(e) {
          var latitude = e.coords.latitude;
          var longitude = e.coords.longitude;
          log("Position update:", latitude, longitude);
          if(navigator.onLine) {
              uploadLocations(latitude, longitude);
          }
          storeLocation(latitude, longitude);
      }

      var handlePositionError = function(e) {
          log("Position error");
      }

      var uploadLocations = function(latitude, longitude) {
          var request = new XMLHttpRequest();
          request.open("POST", "http://geodata.example.net:8000/geoupload", true);
          request.send(localStorage.locations);
      }

      var geolocationConfig = {"maximumAge":20000};

      navigator.geolocation.watchPosition(handlePositionUpdate,
                                          handlePositionError,
                                          geolocationConfig);`

      添加存储代码

      接下来,添加当应用处于离线模式时向localStorage写入更新的代码。

      var storeLocation = function(latitude, longitude) {     // load stored location list     var locations = JSON.parse(localStorage.locations || "[]");     // add location     locations.push({"latitude" : latitude, "longitude" : longitude});     // save new location list     localStorage.locations = JSON.stringify(locations); }

      该应用使用 HTML5 本地存储器存储坐标,如第九章所述。本地存储非常适合支持脱机的应用,因为它提供了一种在浏览器中本地保存数据的方法。这些数据将在今后的会议中提供。当网络连接恢复时,应用可以与远程服务器同步。

      在这里使用存储还有一个好处,就是允许从失败的上传请求中恢复。如果应用由于任何原因遇到网络错误,或者如果应用被关闭(由于用户操作、浏览器或操作系统崩溃或页面导航),数据将被存储以备将来传输。

      添加离线事件处理

      每次位置更新处理程序运行时,它都会检查连接状态。如果应用在线,它将存储并上传坐标。如果应用离线,它将只存储坐标。当应用重新联机时,它可以更新 UI 以显示联机状态,并上传联机时存储的任何数据。

      `window.addEventListener("online", function(e) {
          log("Online");
      }, true);

      window.addEventListener("offline", function(e) {
          log("Offline");
      }, true);`

      当应用不在运行时,连接状态可能会改变。例如,用户可能已经关闭了浏览器、刷新或导航到不同的站点。为了处理这些情况,我们的离线应用会在每次页面加载时检查它是否已经恢复在线。如果有,它将尝试与远程服务器同步。

      // Synchronize with the server if the browser is now online if(navigator.onLine) {     uploadLocations(); }

      总结

      在本章中,您已经看到了如何使用 HTML5 离线 Web 应用来创建引人注目的应用,这些应用甚至可以在没有互联网连接的情况下使用。通过在缓存清单文件中指定属于 web 应用的文件,然后从应用的主 HTML 页面引用这些文件,可以确保所有文件都被缓存。然后,通过为联机和脱机状态更改添加事件侦听器,您可以使您的站点根据 Internet 连接是否可用而有不同的行为。

      在最后一章,我们将讨论 HTML5 编程的未来。

      十三、HTML5 的未来

      正如您在本书中已经看到的,HTML5 提供了强大的编程特性。我们还讨论了 HTML5 开发背后的历史和 HTML5 新的无插件范例。在这一章中,我们将看看事情的发展方向。我们将讨论一些还没有完全成熟,但有着巨大潜力的特性。

      浏览器对 HTML5 的支持

      随着每个新的浏览器更新,HTML5 功能的采用正在加速。在我们写这本书的时候,我们提到的几个特性已经在浏览器中发布了。不可否认,浏览器中的 HTML5 开发正在获得巨大的发展势头。

      今天,许多开发人员仍然在努力开发与旧浏览器兼容的一致的 web 应用。Internet Explorer 6 代表了当今互联网上普遍使用的最苛刻的传统浏览器,但即使是 IE6 的寿命也是有限的,因为越来越难获得任何支持它的操作系统。假以时日,将会有接近零的用户使用 IE6 浏览网页。越来越多的 Internet Explorer 用户正在升级到最新版本。总会有一个最老的浏览器与之抗衡,但那就是久而久之;在撰写本文时,Internet Explorer 6 的市场份额不到 10%,并且还在下降。大多数升级的用户会直接选择现代的替代品。随着时间的推移,最小公分母将包括 HTML5 视频、画布、WebSocket 和任何其他您今天可能必须模仿的功能,以达到更广泛的受众。

      在本书中,我们介绍了在多种浏览器中基本稳定的特性。对 HTML 和 API 的其他扩展目前正处于开发的早期阶段。在这一章中,我们将看看一些即将推出的功能。一些还处于早期试验阶段,而另一些可能会看到最终的标准化和广泛的可用性,只需对其当前状态进行微小的改变。

      HTML 在发展

      在这一节中,我们将探索几个可能在不久的将来出现在浏览器中的令人兴奋的特性。你可能也不需要等到 2022 年才能看到这些。可能不会有一个形式化的 HTML6WHATWG 暗示未来的开发将简称为“HTML”开发将是渐进的,特定的特性和它们的规范将单独发展,而不是作为一个整合的努力。随着人们对浏览器的共识越来越多,浏览器将会采用新的特性,而即将到来的新特性甚至可能在 HTML5 定型之前就已经在浏览器中广泛使用了。负责推动 Web 向前发展的社区致力于发展平台,以满足用户和开发人员的需求。

      WebGL

      WebGL 是一个用于网络 3D 图形的 API。历史上,包括 Mozilla、Opera 和 Google 在内的几个浏览器供应商已经为 JavaScript 开发了独立的实验性 3D APIs。今天,WebGL 正沿着标准化和跨 HTML5 浏览器广泛可用的道路前进。浏览器供应商和 Khronos 集团正在进行标准化过程,Khronos 集团是 OpenGL 的负责机构,OpenGL 是 1992 年创建的跨平台 3D 绘图标准。OpenGL 目前处于规范版本 4.0,作为微软 Direct3D 的对手和竞争者,广泛用于游戏和计算机辅助设计应用。

      正如你在第二章中看到的,你通过调用元素上的getContext("2d")从一个canvas元素中得到一个 2D 绘图上下文。不出所料,这为其他类型的绘图环境打开了大门。WebGL 也使用了canvas元素,但是通过 3D 上下文。当前的实现使用实验性的厂商前缀(moz-webglwebkit-3d等)。)作为getContext()调用的参数。例如,在支持 WebGL 的 Firefox 版本中,您可以通过调用canvas元素上的getContext("moz-webgl")来获得 3D 上下文。对getContext()的这种调用所返回的对象的 API 不同于 2D 的 canvas 等价类,因为它提供的是 OpenGL 绑定,而不是绘图操作。WebGL 版本的 canvas 上下文管理纹理和顶点缓冲区,而不是调用画线和填充形状。

      三维 HTML

      像 HTML5 的其他部分一样,WebGL 将成为 web 平台不可或缺的一部分。因为 WebGL 呈现给一个canvas元素,所以它是文档的一部分。您可以定位和变换 3D canvas元素,就像您可以在页面上放置图像或 2D 画布一样。事实上,你可以用任何其他的canvas元素做任何你能做的事情,包括叠加文本和视频以及表演动画。与纯 3D 显示技术相比,结合其他文档元素和 3D 画布将使平视显示器(hud)以及混合 2D 和 3D 界面的开发更加简单。想象一下,拍摄一个 3D 场景,并使用 HTML 标记在其上覆盖一个简单的 web 用户界面。与许多 OpenGL 应用中的非本地菜单和控件非常不同,WebGL 软件将轻松地结合漂亮的 HTML5 表单元素。

      Web 的现有网络架构也将补充 WebGL。WebGL 应用将能够从 URL 获取纹理和模型等资源。多人游戏可以用 WebSocket 通信。例如,图 13-1 显示了一个这样的例子。谷歌最近使用 HTML5 WebSocket、Audio 和 WebGL 将经典的 3D 游戏 Quake II 移植到了网络上,并完成了多人游戏。游戏逻辑和图形用 JavaScript 实现,调用 WebGL 画布进行渲染。使用持久的 WebSocket 连接来实现与服务器的通信,以协调玩家的移动。

      Image

      图 13-1。雷神之锤 II

      3D 着色器

      WebGL 是 OpenGL ES 2 在 JavaScript 中的绑定,所以它使用了 OpenGL 中标准化的可编程图形管道,包括着色器。着色器允许将高度灵活的渲染效果应用于 3D 场景,从而增加显示的真实感。WebGL 着色器是用 GL 着色语言(GLSL)编写的。这又给 web 堆栈增加了一种单一用途的语言。带有 WebGL 的 HTML5 应用由用于结构的 HTML、用于样式的 CSS、用于逻辑的 JavaScript 和用于着色器的 GLSL 组成。开发人员可以将他们的 OpenGL 着色器知识转移到 web 环境中的类似 API。

      WebGL 很可能成为网络上 3D 图形的基础层。正如 JavaScript 库抽象了 DOM 并提供了强大的高级结构一样,在 WebGL 之上也有提供额外功能的库。目前正在为场景图、COLLADA 等 3D 文件格式以及游戏开发的完整引擎开发库。图 13-2 显示了 Shader Toy——一个由 Inigo Quilez 构建的 WebGL 着色器工作台,附带了其他九个 demoscene 艺术家的着色器。这张截图展示了 Rgba 的 Leizex。我们可以预计,在不久的将来,高级渲染库将会大规模涌现,为网络编程新手带来 3D 场景创作能力。

      Image

      图 13-2。着色器玩具是一个 WebGL 着色器工作台

      设备

      Web 应用需要访问多媒体硬件,如网络摄像头、麦克风或附加的存储设备。为此,已经有一个提议的device元素,让 web 应用能够从连接的硬件访问数据流。当然,这涉及到严重的隐私问题,所以不是每个脚本都能随意使用你的摄像头。当应用请求提升权限时,我们可能会看到一个提示用户权限的 UI 模式,就像在地理位置和存储 API 中看到的那样。网络摄像头的明显应用是视频会议,但计算机视觉在网络应用中还有许多其他惊人的可能性,包括增强现实和头部跟踪。

      音频数据 API

      可编程音频 API 将为<audio><canvas><img>做的事情。在canvas标签出现之前,网页上的图像对于脚本来说是不透明的。图像创建和操作必须在场外进行,即在服务器上进行。现在,有了基于canvas元素的来创建和操作视觉媒体的工具。类似地,音频数据 API 将支持 HTML5 应用中的音乐创作。这将有助于完善 web 应用可用的内容创建功能,并使我们更接近一个在 Web 上为 Web 创建媒体的自托管工具世界。想象一下,不用离开浏览器就可以在网上编辑音频。

      简单的声音回放可以用<audio>元素来完成。然而,任何即时操纵、分析或生成声音的应用都需要一个较低级别的 API。不访问音频数据,文本到语音、语音到语音的翻译、合成器和音乐可视化都是不可能的。

      我们可以期待标准音频 API 能够很好地处理来自数据元素的麦克风输入以及音频标签中包含的文件。有了<device>和一个音频数据 API,你也许可以开发一个 HTML5 应用,允许用户在一个页面中记录和编辑声音。音频剪辑将能够存储在本地浏览器存储器中,并与基于canvas的编辑工具结合使用。

      目前,Mozilla 在夜间版本中有一个实验性的实现。Mozilla 音频数据 API 可以作为标准跨浏览器音频编程能力的起点。

      触摸屏设备事件

      随着网络访问越来越多地从台式机和笔记本电脑转移到手机和平板电脑,HTML5 也在继续适应交互处理的变化。当苹果推出 iPhone 时,它也在浏览器中引入了一组特殊事件,可用于处理多点触摸输入和设备旋转。虽然这些事件还没有被标准化,但是它们正在被其他移动设备供应商所采用。今天学习它们将允许你为现在最流行的设备优化你的网络应用。

      方向

      在移动设备上处理的最简单的事件是方向事件。定向事件可以添加到文档正文中:

      <body onorientationchange="rotateDisplay();">

      在方向更改的事件处理程序中,您的代码可以引用window.orientation属性。该属性将给出表 13-1 中显示的旋转值之一,该值相对于页面初始加载时设备所处的方向。

      Image

      一旦知道了方向,您就可以选择相应地调整内容。

      手势

      移动设备支持的下一种事件是一种高级事件,称为手势。将手势事件视为代表大小或旋转的多点触摸变化。这通常在用户将两个或更多手指同时放在屏幕上并挤压或扭转时执行。扭曲表示旋转,而收缩或缩小分别表示缩小或放大。为了接收手势事件,您的代码需要注册表 13-2 中显示的一个处理程序。

      Image

      在手势期间,事件处理程序可以自由检查相应事件的旋转和缩放属性,并相应地更新显示。清单 13-1 展示了一个手势处理程序的使用示例。

      清单 13-1。手势处理器示例

      `function gestureChange(event) {
        // Retrieve the amount of change in scale caused by the user gesture
        // Consider a value of 1.0 to represent the original size, while smaller
        //  numbers represent a zoom in and larger numbers represent a zoom
        //  out, based on the ratio of the scale value
      var scale = event.scale;

      // Retrieve the amount of change in rotation caused by the user gesture
        // The rotation value is in degrees from 0 to 360, where positive values
        //   indicate a rotation clockwise and negative values indicate a counter-
        //   clockwise rotation
      var rotation = event.rotation;

      // Update the display based on the rotation.
      }

      // register our gesture change listener on a document node
      node.addEventListener("gesturechange", gestureChange, false);`

      手势事件特别适用于需要操作对象或显示的应用,如图表工具或导航工具。

      触动

      对于那些需要对设备事件进行低级控制的情况,触摸事件提供了您可能需要的尽可能多的信息。表 13-3 显示了不同的触摸事件。

      Image

      与其他移动设备事件不同,触摸事件需要表示同时存在多个数据点(许多潜在的手指)。因此,触摸处理的 API 稍微复杂一点,如清单 13-2 所示。

      清单 13-2。触摸 API

      `function touchMove(event) {
      // the touches list contains an entry for every finger currently touching the screen
      var touches = event.touches;

      // the changedTouches list contains only those finger touches modified at this
        // moment in time, either by being added, removed, or repositioned
      varchangedTouches = event.changedTouches;

      // targetTouches contains only those touches which are placed in the node
        // where this listener is registered
      vartargetTouches = event.targetTouches;

      // once you have the touches you'd like to track, you can reference
        // most attributes you would normally get from other event objects
      varfirstTouch = touches[0];
      varfirstTouchX = firstTouch.pageX;
      varfirstTouchY = firstTouch.pageY;
      }

      // register one of the touch listeners for our example
      node.addEventListener("touchmove", touchMove, false);`

      您可能会发现设备的本机事件处理会干扰您对触摸和手势事件的处理。在这些情况下,您应该进行以下呼叫:

      event.preventDefault();

      这将覆盖默认浏览器界面的行为,并自己处理事件。在移动事件标准化之前,建议您查阅应用所针对的设备的文档。

      对等网络

      我们也没有看到高级网络在 web 应用中的终结。对于 HTTP 和 WebSocket,都有一个客户端(浏览器或其他用户代理)和一个服务器(URL 的主机)。对等(P2P)网络允许客户端直接通信。这通常比通过服务器发送所有数据更有效。效率,当然,降低托管成本,提高应用性能。P2P 应该有助于更快的多人游戏和协作软件。

      P2P 与device元素结合的另一个直接应用是 HTML5 中的高效视频聊天。在点对点视频聊天中,对话双方可以直接互相发送数据,而不需要通过中央服务器。在 HTML5 之外,P2P 视频聊天在 Skype 等应用中非常流行。由于流式视频需要高带宽,如果没有点对点通信,这两种应用都不可能实现。

      浏览器供应商已经在试验 P2P 网络,例如 Opera 的 Unite 技术,它直接在浏览器中托管一个简化的 web 服务器。Opera Unite 允许用户创建并向他们的同伴公开服务,用于聊天、文件共享和文档协作。

      当然,网络的 P2P 网络需要一个考虑到安全性和网络中介的协议,以及一个供开发者编程的 API。

      终极方向

      到目前为止,我们一直致力于让开发人员能够构建强大的 HTML5 应用。一个不同的角度是考虑 HTML5 如何增强 web 应用的用户。许多 HTML5 特性允许你删除或减少脚本的复杂性,并执行以前需要插件的专长。例如,HTML5 video 允许您指定控件、自动播放、缓冲行为和占位符图像,而无需任何 JavaScript。使用 CSS3,您可以将动画和效果从脚本移动到样式。这种声明性代码使应用更符合用户风格,并最终将权力还给每天使用您的作品的人。

      您已经看到了所有现代浏览器中的开发工具如何公开有关 HTML5 特性的信息,如存储,以及至关重要的 JavaScript 调试、分析和命令行评估。HTML5 开发将趋向于简单性、声明性代码和浏览器或 web 应用本身的轻量级工具。

      谷歌对 HTML 的持续发展充满信心,它已经发布了谷歌 Chrome 操作系统,这是一个围绕浏览器和媒体播放器构建的精简操作系统。谷歌的操作系统旨在使用 HTML APIs 包含足够的功能,以提供引人注目的用户体验,其中应用使用标准化的 web 基础设施交付。同样,微软已经宣布 Windows 8 将不支持新 Metro 模式中的任何插件,包括该公司自己的 Silverlight 插件。

      总结

      在这本书里,你已经学会了如何使用强大的 HTML5 APIs。明智地使用它们!

      在这最后一章中,我们已经让你看到了一些即将到来的事情,如 3D 图形,新的设备元素,触摸事件和 P2P 网络。HTML5 的发展没有显示出放缓的迹象,将会非常令人兴奋。

      回想一分钟。对于那些已经在网上冲浪,或者甚至已经为之开发了十年或更长时间的人来说,想想 HTML 技术在过去几年里已经走了多远。十年前,“专业 HTML 编程”意味着学习使用 HTML 4 的新功能。当时最前沿的开发人员刚刚发现动态页面更新和,“Ajax”这个术语距离引入还有数年时间,即使 Ajax 描述的技术已经开始获得关注。浏览器中的许多专业编程都是为了处理框架和操作图像地图而编写的。

      今天,需要几页脚本的功能只需标记就能完成。现在,对于那些愿意下载众多免费 HTML5 浏览器之一、打开他们最喜欢的文本编辑器并尝试专业 HTML5 编程的人来说,多种新的通信和交互方法都是可用的。

      我们希望你喜欢这个 web 开发的探索,并且我们希望它激发了你的创造力。我们期待着十年后写下你使用 HTML5 创造的创新。

      posted @ 2024-08-13 14:31  绝不原创的飞龙  阅读(1)  评论(0编辑  收藏  举报