HTML5-程序员参考-全-
HTML5 程序员参考(全)
零、简介
万维网已经存在了将近 25 年。它最初是由蒂姆·伯纳斯·李和罗伯特·卡里奥提出的一个简单的建议,作为欧洲粒子物理研究所的科学家们轻松发表论文的一种方式,但它迅速发展成为一个吸引了全世界想象力的平台。
网络可能是从一个简单的文档发布平台开始的,但是很快就清楚了,它注定要变得更好。随着人们要求更多的交互性和更丰富的体验,最初的 HTML 标准的局限性很快变得明显。级联样式表和 JavaScript 等其他技术的出现有所帮助,但开发人员仍然花费大量资源来构建人们想要的体验。
HTML5 旨在帮助解决这些问题。HTML 标准的第五代,HTML5,被设计成既有丰富的特性又更容易使用。HTML 的早期版本关注于如何最好地标准化文档标记,这是为早期 Web 的混乱带来标准的好方法。然而,HTML5 专注于为构建丰富的交互提供一个平台。HTML5 的大部分也是专门为移动技术而设计的,而旧版本的 HTML 则不是。
这本书涵盖的内容
这本书旨在成为 HTML5 特性的最佳参考。它分为两个部分。
第一部分“深入研究 HTML5”有几章提供了对 HTML5 特性的详细检查,包括多个示例和截至发稿时的当前支持级别。
- 第一章“欢迎来到 HTML5”是一堂历史课,解释了万维网及其技术是如何发展的,以及 HTML5 是如何出现的。这将有望帮助你理解为什么 HTML5 与以前的 HTML 标准如此不同,并让你对 HTML5 的结构有更好的了解。
- 第二章“HTML5 元素”涵盖了 html 5 中新的语义标签。和以前的标准一样,HTML5 包含了一组新的标记,用于标记文档中的内容。在这里,您将了解如何使用新的音频和视频标签,以及许多其他 HTML5 功能。
- 第三章“HTML5 API”深入探讨 html 5 标准中规定的 JavaScript APIs。您将了解 HTML5 应用通信和保存数据的新方法。
- 第四章,“画布”,涵盖了 HTML5 最具创新性的特性之一:
canvas
元素。在这里,您将学习如何使用这个元素来绘制、修改图像和创建动画。 - 第五章“相关标准”涵盖了几个与 HTML5 相关的 JavaScript APIs(经常与 HTML5 一起使用),但实际上并不是 HTML5 标准的一部分。这些 API 也倾向于拥有强大的移动焦点。
- 第六章“实用 HTML5”涵盖了在生产项目中实际使用 HTML5 的内容。它涵盖了检测功能和应用垫片,并包括一个完整的 HTML5 移动游戏的设计和建立从地面向上。
第二部分“HTML5 参考”,包含第一部分中所有 html 5 特性的参考章节。每章旨在为每种功能提供一目了然的参考,包括功能的简要描述、使用方法(包括语法和示例)以及在哪里可以找到其标准。
- 第七章是 HTML5 元素的参考章节。
- 第八章是 HTML5 JavaScript APIs 的参考章节。
- 第九章是
canvas
元素的参考章节。
你需要知道什么
虽然整本书有很多详细的例子,但它是作为参考而不是作为教程来写的。我假设您对浏览器如何工作以及如何使用 JavaScript 有一定的了解,并且至少对 CSS 和 HTTP 中涉及的网络协议有基本的了解。你应该能够轻松地创建和编辑网页,编写自己的 CSS 和 JavaScript。
运行代码示例
整本书有大量的代码示例。可以从www.apress.com
下载样本,也可以手工输入。许多示例可以通过使用浏览器的文件菜单将文件加载到 web 浏览器中来运行。
但是,有些示例必须从实际的服务器上运行,要么是出于安全限制,要么是因为您希望在移动设备上查看它们。为了构建和测试书中的所有例子,我使用了 Aptana Studio,可以在http://www.aptana.com
免费获得。Aptana Studio 附带了一个内部调试服务器,您可以使用它来运行这些示例。如果您更喜欢独立的解决方案,我非常喜欢 XAMPP,它是 Apache web 服务器的独立安装,以及可选的组件,如 MySQL、PHP 和 Perl。当然,MacOS 和 Windows 都有自己的 web 服务器解决方案,您可以激活和使用,就像大多数标准的 Linux 发行版一样。
最后,请务必查看附录 A“JavaScript 技巧和技术”中的“注释注释”部分,了解示例格式的解释以及如何阅读注释。
一、欢迎来到 HTML5
在这一章中,我将深入探究 HTML 的历史以及 HTML5 是如何产生的。我将谈论 HTML 从一个简单的提案一直到其当前版本的演变,包括相关技术的回顾。我还将介绍 HTML5 是什么,它的范围,它与以前版本的不同之处,以及它如何与其他技术相结合。
HTML5 是什么?
自 1989 年以来,超文本标记语言(HTML)一直伴随着我们。HTML 之前的版本只为内容定义了标记:列表、段落、标题、表格等等。然而,HTML5 定义了更多。它有新的内容标签(如<audio>
和<video>
),但它也定义了复杂的交互,如拖放,新的网络接口,如服务器事件,甚至有新的异步功能,如 web workers。HTML5 之前的 HTML 规范也定义了 SGML 中的标签(稍后会详细介绍),但是 HTML5 规范只根据注释内容和预期行为来定义标签。因为 HTML5 是一套新的高级 web 技术的重要组成部分,很多时候你会在网络上或流行媒体上看到一些文章,错误地包含了 HTML5 中与 HTML 无关的技术。
那么 HTML5 到底是什么?为什么 HTML5 定义的不仅仅是标签?HTML5 是怎么来的?为什么 HTML5 标准在定义和范围上与以前的标准有如此大的不同?为了回答这些问题,我将首先快速回顾一下 HTML 最初是如何出现的。
HTML 的简史
HTML 的起源可以追溯到 1989 年。那时,网上分享信息最常见的方式是通过电子邮件、新闻组新闻组和公共 FTP 站点。电子邮件和新闻组使人们直接相互交流变得容易,FTP 站点为人们提供了访问文件集的途径。主要问题是,所有这些形式的信息共享都需要不同的软件,以及一定水平的技能才能真正在互联网上导航——尽管那时的互联网比现在小得多。
蒂姆·伯纳斯·李在 1989 年提出了一个更好的解决方案。当时他在欧洲核研究组织(其法语缩写 CERN,即欧洲核研究委员会)工作,他敏锐地意识到需要一种更好的方式来在线共享信息。尤其是,Berners-Lee 需要解决在线共享技术文档的问题。CERN 产生了大量的技术文档,从准备出版的核物理论文到内部政策文档,他们需要一个适用于所有这些不同用例的解决方案。
伯纳斯·李发现自己试图同时解决两个问题:
- 他需要一个解决方案,提供一种可视化格式化 CERN 科学家产生的信息的方法。这种信息可以采取文件的形式,如发表的论文以及在实验中观察到的数据。
- 他需要一个能够处理交叉引用并嵌入图形和其他媒体的解决方案。CERN 的许多文件包括图表和图形,并相互引用,或引用其他内部数据源,甚至外部文件和数据源。
幸运的是,伯纳斯-李已经有了一些解决这些问题的经验。早在 1980 年,当他还是 CERN 的一名承包商时,他已经建立了一个名为 ENQUIRE 的原型系统,提供了该组织所需的一些功能,但未能很好地扩展。然而,它使用了一个非常重要的关键概念:超文本。
输入超文本
超文本 是一种引用其他信息的文本,用户可以激活它来立即访问这些信息。这包括同一文档中包含的信息以及外部文档或其他数据源中的信息。这些链接被称为超链接??。在大多数现代计算机中,超文本显示在屏幕上,超链接通过用鼠标点击或(在触摸屏的情况下)用手指轻击来激活。术语 超媒体是超文本概念的延伸,不仅包括超链接,还包括图形、音频、视频和其他信息源。
超媒体的概念已经存在很长时间了。1945 年,美国工程师兼发明家万尼瓦尔·布什为《大西洋月刊》写了一篇名为《正如我们所想》的文章。作为论文的一部分,布什提出了一种“记忆扩展器”或“memex”,一种人们可以用来存储所有个人信息来源的设备:书籍、唱片、相册等等。memex 将通过使用一组书签向人们提供对他们所有信息的访问,并且可以根据需要进行扩展。
提示你可以在大西洋的网站
www.theatlantic.com/magazine/archive/1945/07/as-we-may-think/303881/
上阅读“我们可能认为的”。
1960 年,Ted Nelson 创立了 Project Xanadu ,试图建立一个能够存储多种版本文档的文字处理系统,让用户能够以非顺序的方式浏览这些文档。他将这些不连续的路径称为“拉链式列表”,并假设通过使用这些拉链式列表,可以在一个他称之为“串并”的过程中从其他文档的片段形成新文档 1963 年,尼尔森创造了术语超文本和超媒体,这两个术语首次发表在他的论文《复杂信息处理:复杂、变化和不确定的文件结构》(可在http://dl.acm.org/citation.cfm?id=806036
获得)。当时 Nelson 使用超文本来指代可编辑的文本,而不是基于文本的交叉引用,所以这个术语从 Nelson 第一次创造它开始就有了一些语义上的变化。
1962 年,美国工程师和发明家道格拉斯·恩格尔巴特开始研究他的“在线系统”或“NLS”NLS 是第一个包括当今可用的大多数现代计算机功能的系统:一个指点设备、窗口、用于呈现不同种类数据的独立程序、按相关性组织的信息、超媒体链接等等。1968 年 12 月,Englebart 在旧金山的秋季联合计算机会议上演示了 NLS。这次演示是开创性的,不仅因为它首次同时展示了所有这些正在使用的现代功能,还因为它使用了最先进的视频会议技术来展示 Englebart 使用的 NLS 用户界面。因为这个演示在范围上是如此具有突破性,所以它经常被称为“所有演示之母”
提示该演示可以在斯坦福大学的网站
http://web.stanford.edu/dept/SUL/library/extra4/sloan/MouseSite/1968Demo.html
上看到。
Berners-Lee 在超文本概念的基础上建立了 ENQUIRE。在 ENQUIRE 中,一个给定的文档由一个名为“卡”的信息页面表示,它实际上是一个超链接列表,定义了文档包括的内容、使用方法、描述以及作者。激活这些链接可以很容易地跟踪它们,使用户能够浏览整个网络的文件。
图 1-1 。ENQUIRE 的截图
在这方面,ENQUIRE 类似于图书馆卡片目录系统的在线版本,不幸的是需要大量的工作来保持更新。
ENQUIRE 也没有解决文档可视化格式的需求。然而,CERN 已经以文档标记语言的形式使用了一种可能的解决方案。
输入标记语言
文档标记语言是一种编程语言,它提供了一种对文档进行注释(或“标记”,就像编辑对正在审阅的文档进行标记)的方式,使得注释在语法上不同于主要内容文档。基于注释的目的,标记语言分为三大类:
- 表示标记语言用来描述文档应该如何呈现给用户。大多数现代文字处理器使用嵌入在文档中的二进制代码形式的表示标记。表示性标记通常是为特定的程序或显示方法设计的,因此并不意味着人类可读。
- 过程化的标记语言提供注释,说明文档的内容应该如何处理,通常是在印刷的布局和排版环境中。过程标记语言最常见的例子之一是 PostScript。
- 描述性的标记语言被用来用对文档内容的描述来注释文档。描述性标记没有给出内容应该如何处理或显示的任何指示;这是留给处理代理的。
文档标记语言已经存在了几十年。第一种广为人知的文档标记语言是由计算机科学家 William Tunnicliffe 在 1967 年提出的,但是 IBM 研究员 Charles Goldfarb 通常被称为现代标记语言的“父亲”,因为他在 1969 年发明了 IBM 通用标记语言(GML) 。戈德法布负责推动 IBM 将 GML 纳入其文档管理解决方案。GML 最终发展成为标准通用标记语言(SGML ,它在 1986 年成为 ISO 标准(ISO 8879:1986 信息处理-文本和办公系统-标准通用标记语言),由 Goldfarb 担任委员会主席。
SGML 不是一种你可以直接使用的语言;相反,它是一种“元语言”——一种用于定义其他语言的语言。在这种情况下,SGML 用于定义标记语言,然后这些语言可用于描述文档。具体来说,SGML 要求标记语言描述文档的结构和内容属性(相对于描述如何处理文档),并且严格定义标记语言,以便可以构建遵循相同规则的处理和查看软件。由 SGML 定义的语言被称为“SGML 应用”(不要与在计算机上运行并执行任务的应用相混淆)。常见的 SGML 应用包括 XML(可扩展标记语言)和 DocBook(为技术文档设计的标记语言)。
CERN 一直使用名为 SGMLguid) 的 SGML 应用来标记其文档,Berners-Lee 认识到 SGMLguid 与超文本的结合可能是他解决 CERN 文档管理问题所需的解决方案。
超文本标记语言诞生了
1989 年末,Berners-Lee 提出了一个以超文本和简单标记语言为基础的试验项目。Berners-Lee 设想超链接将成为将所有不同文档联系在一起的关键功能:
超文本是一种链接和访问各种信息的方式,就像一个由节点组成的网络,用户可以随意浏览。潜在地,超文本为许多大类的存储信息,如报告、笔记、数据库、计算机文档和在线系统帮助,提供了单一的用户界面。
摘自 1990 年 11 月 12 日的“万维网:超文本项目提案”
该提案概述了一个简单的客户机/服务器网络协议,用于新的文档“网络”,以及它们将如何一起工作,将信息从服务器传输到浏览客户机。伯纳斯-李将新协议称为“超文本传输协议”或 HTTP。该项目获得批准,伯纳斯-李和他的团队开始研究最终成为万维网的东西。
在为新的文档系统创建了客户机和服务器软件之后,Berners-Lee 发表了第一个文档,该文档定义了一组基本的标记,这些标记可用于标记将要包含在新的在线文档 web 中的文档。这份名为“HTML 标签”的文档定义了 18 个标签,它们可以用来标记文档的内容,新的 web 客户端可以解析并显示它们。几乎所有的标记都来自 SGMLguid,只有一个例外:anchor 标记。锚标签是对新系统如此重要的超文本链接功能的实现。
提示你可以在
www.w3.org/History/19921103-hypertext/hypertext/WWW/MarkUp/Tags.html
阅读 W3C 历史档案中的原始“HTML 标签”文档。
第一个文档是一个简单的标签列表,描述了如何使用它们来描述文档的内容。后来,随着“超文本标记语言(HTML)”(www.w3.org/MarkUp/draft-ietf-iiir-html-01.txt
)作为提交给 互联网工程任务组(IETF)的工作草案的出版,这些标签在 1993 年正式成为 SGML 应用。该草案到期,同年晚些时候由戴夫·拉格特撰写的题为“HTML+(超文本标记格式)”的竞争草案紧随其后。
开放和协作
蒂姆·伯纳斯·李致力于保持 HTML 定义过程的开放性和协作性,利用许多参与者的知识和经验。这些早期的合作不仅为通过公共合作设计的整个 web 技术生态系统的创建铺平了道路,也为维护项目的核心小组的创建铺平了道路。
浏览器大战
在致力于定义 HTML 的同时,蒂姆·伯纳斯·李也在开发第一个可以利用新的网络文档的软件。1991 年,Berners-Lee 为 NeXTStep 平台发布了第一款网络浏览器“WorldWideWeb”。其他程序员对开发他们自己的 web 浏览器有着浓厚的兴趣,所以 1993 年 Berners-Lee 向公众发布了一个名为 libwww
的可移植 C 库,这样任何人都可以开发 web 浏览器。(在此之前,该库是作为更大的万维网软件应用的一部分提供的。)
至此,在多个平台上已经有了几个实验性的 web 浏览器项目。其中一些是简单的基于文本的浏览器,可以在任何终端上使用,比如 Lynx 浏览器。其他的是当时图形桌面中使用的图形应用。
最受欢迎的图形应用之一是 Mosaic,由伊利诺伊大学的国家超级计算应用中心(NCSA) 开发。Marc Andreesen 和 Eric Bina 于 1992 年底开始了 Mosaic 的工作,并于 1993 年发布了第一个版本。
1994 年,安德里森离开 NCSA,成立了一家名为马赛克通信的公司,在那里他们用全新的代码构建了一款新的浏览器。新浏览器被称为网景导航器(最终马赛克通信更名为网景通信)。
实际的马赛克代码库本身是由一家名为 Spyglass,Inc .的公司从 NCSA 获得许可的。Spyglass 从未对代码做任何事情,1995 年微软从他们那里获得了代码许可,对其进行了修改,并将其重命名为 Internet Explorer。
网景公司和微软公司都开始扩展他们浏览器的功能,增加新的 HTML 标签和其他功能。网景于 1995 年在 Navigator 中加入了 JavaScript(代号“mocha”,最初发布时为“LiveScript”)。微软很快在 1996 年推出了他们自己版本的同一种语言,称为 JScript,以避免商标问题。
Netscape Navigator 和 Internet Explorer 对相同的功能有完全不同的实现,也有它们自己的专有功能。给定的 HTML 文档可能在 Navigator 中以一种方式呈现,而在 Internet Explorer 中呈现时看起来完全不同。即使简单的 HTML 标记在两种浏览器中也会产生明显不同的视觉效果,任何更高级的尝试都是不可能的。
这为所谓的浏览器大战埋下了伏笔。任何为 Web 制作内容的人都必须做出选择:选择支持单个浏览器,或者花费大量资源尝试同时支持两个浏览器(在许多情况下,这意味着制作同一内容的两个不同版本,每个浏览器一个版本)。常见的是只针对一种浏览器进行优化的网站,用图形表示选择,如图 1-2 所示。
图 1-2 。浏览器大战中的图片
通过将 Internet Explorer 作为 Windows 操作系统的标准部分,微软轻松赢得了第一轮浏览器大战。这给了 Internet Explorer 一个巨大的安装基础,人们没有理由为 Netscape 付费。到 1999 年,Internet Explorer 占据了万维网浏览器使用量的 96%。网景通信被美国在线收购,网景导航器(当时叫网景通信器)被封存。
浏览器大战:网景反击
AOL 开源了 Netscape Communicator 代码库,并将其委托给新成立的非营利组织 Mozilla Foundation 。作为一个开源项目,Mozilla 基金会继续在 Navigator 代码基础上进行构建,并获得了相当大的发展势头,为浏览器添加了新功能,包括电子邮件和 HTML 编辑功能。2002 年末,该套件的一个精简版被创建出来,最初被命名为 Phoenix,然后是 Firebird,后来(由于项目命名冲突)被命名为 Firefox。Firefox 继续成功挑战了 Internet Explorer 在浏览器市场的垄断地位,这被许多人称为第二轮浏览器大战。
救援标准
打击网络碎片化意味着让各方坐到谈判桌前,就每个人都可以建立的技术标准达成一致。标准为浏览器制造商和内容创作者提供了一个共同的基础:
- 通过将标准作为制造过程的一部分,浏览器制造商将为网络提供一个可预测的平台。
- 通过将标准作为编码实践的一部分,内容创建者可以确信他们的内容将在所有浏览器上一致地呈现。
1994 年 10 月,这正是蒂姆·伯纳斯·李所做的,这一举动唤起了他保持网络开放和协作的愿望。他离开了 CERN,成立了万维网联盟(W3C) ,,这是一个致力于网络技术的标准组织。该联盟由任何想要参与定义和维护网络技术标准的人组成:最终包括微软、苹果、脸书和谷歌的公司;像 NASA 和国家标准技术研究所这样的政府组织;像斯坦福大学和牛津大学这样的大学;像欧洲核子研究中心这样的研究机构;以及非营利组织,如 Mozilla 基金会和电子前沿基金会。
W3C 标准进程从发布标准的工作草案开始。然后,联合体成员可以对草案发表意见,草案可能会经历相当大的演变。一旦草稿固化,候选人推荐就会发布。从实施的角度审查候选推荐标准——实施和使用标准的难度有多大。一旦实施者有了发言权,草案就进入了建议状态。提议的建议将提交给 W3C 顾问委员会进行最终批准。一旦获得最终批准,该标准就获得了 W3C 官方推荐标准的地位。
标准不会在一夜之间解决浏览器战争。浏览器制造商实施这些标准花了一段时间。微软尤其支持“拥抱和扩展”的哲学,在这种哲学中,他们同意标准,但也继续增加他们自己的专有技术,试图使 Internet Explorer 成为一个更有吸引力的 web 开发平台。不过,最终,对跨所有浏览器的一致行为的需求取得了胜利,标准为胜利提供了蓝图。
HTML 的持续发展
HTML 标准最初由 IETF 维护,IETF 于 1995 年将 HTML 2.0 标准发布为 RFC 1866。
注“RFC”代表“征求意见”,这意味着发布了文档,并邀请利益相关者对其进行评论,作为持续审核流程的一部分。
W3C 在 1996 年接管了 HTML 标准。1997 年,W3C 发布了 HTML 3.2 标准。这个版本正式否决了几个供应商特定的功能,并进一步稳定了浏览器制造商和内容创作者的标准。在不到一年的时间里,W3C 发布了 HTML 4.0。这个版本的 HTML 将标准推向了纯语义标记的方向:许多可视化标签,比如那些创建粗体或斜体标签的标签,都被弃用,取而代之的是级联样式表(CSS ) 。W3C 在 1999 年发布了 HTML 4.1,它本质上是 HTML 4.0 ,做了一些小的编辑和修改。在 2000 年 HTML 4.1 因为一个 ISO 标准:ISO/IEC 15445:2000。
所有这些 HTML 版本都被定义为 SGML 应用。每个标签及其属性都是使用 SGML 规则定义的,如清单 1-1 中的所示。
清单 1-1 。HTML 4.1 中 UL 标签的 SGML 定义
<!ELEMENT UL - - (LI)+ -- unordered list -->
<!ATTLIST UL
%attrs; -- %coreattrs, %i18n, %events -->
<!ELEMENT OL - - (LI)+ -- ordered list -->
<!ATTLIST OL
%attrs; -- %coreattrs, %i18n, %events -->
随着标准的发展,内容创建者必须越来越严格地遵循这些标准,以保证跨浏览器的一致行为。
XHTML 的兴衰
2008 年,一个新的 SGML 应用被提出,它将提供一个更小、更易管理的 SGML 指令子集。被称为可扩展标记语言,或 XML,它也是用来定义数据标记语言的。HTML 4 标准很快被翻译成 XML,产生了 XHTML。XHTML 1.0 标准于 2000 年发布。
XHTML 旨在使 HTML 语言更加模块化和可扩展。XHTML 语法比普通 HTML 更严格,XHTML 标记中的错误将导致呈现代理发布错误并停止,而不是恢复到基本行为并继续。然而,由于缺乏对旧内容的向后兼容性和浏览器支持,XHTML 从未被广泛采用。
WHATWG 的形成和 HTML5 的创建
到 2004 年,W3C 开始致力于 XHTML 2.0。然而,联盟的一些成员认为基于 XML 的方向不是 web 技术应该遵循的正确方向。Mozilla 基金会和 Opera 软件在 2004 年 6 月向 W3C 提交了一份立场文件。本文将 web 应用作为一个整体来关注:如何构建它们,它们应该采用什么技术,与现有 web 浏览器的向后兼容性,等等。这篇论文包括了一个 Web 表单的规范草案,作为指导的例子。你可以在 W3C 网站上的www.w3.org/2004/04/webapps-cdf-ws/papers/opera.html
阅读这篇论文。这篇论文提出的问题比它回答的要多,但是总的来说,它指向了一个不同于 W3C 目前的基于 XML 的解决方案的方向。最终 W3C 投票否决了这份文件,选择继续使用 XML 解决方案。
许多利益相关者强烈地感受到以本文提出的整体方式来看待 web 应用,因此成立了一个小组来专注于 web 应用标准的创建。名为网络超文本应用技术工作组(WHATWG) ,的成员包括来自苹果公司、Mozilla 基金会和 Opera 软件公司的个人。最初,他们为 web 应用标准创建了一个草案,其中涵盖了该团队认为对于创建丰富的交互式 Web 应用非常重要的所有特性,包括:
- 新的语义标记标签用于常见的内容模式,如页脚、侧栏和引用。
- 新的状态管理 0074 和数据存储功能。
- 本机拖放交互。
- 新的网络功能,如服务器推送事件。
这个新标准最终与 Web Forms 标准(同样由 WHATWG 制定)合并,合并后的标准被重新命名为 HTML5 。这就是 HTML5 标准不是 SGML 应用的原因,也是它不仅仅包含标记的原因:它旨在为创建 web 应用提供更好的工具。
2007 年,W3C 的 HTML 小组采用了 WHATWG 的 HTML5 规范,并开始向前推进。这两个组织都继续维护他们自己版本的相同标准。根据双方协议,W3C 维护 HTML5 的规范标准。WHATWG 的标准被认为是一种“生活标准”,因此它永远不会完整,而且会不断发展。这样,W3C 的标准就像是 WHATWG 标准的快照。
W3C HTML5 标准
W3C 的 HTML5 标准在www.w3.org/TR/html5/Overview.html
可用。这是 W3C 的官方建议。
WHATWG 生活标准
WHATWG HTML 标准位于https://html.spec.whatwg.org/multipage/index.html
。
HTML5 特性
因为 HTML5 被设计成能够创建丰富的交互式 web 应用,所以它规定的不仅仅是标记标签——尽管它也涵盖了这些。
新标签
HTML5 标准为标记文档指定了许多新的标签。新的 sectioning 标记提供了指示常见设计模式(如页脚和导航组件)的方法,并为屏幕阅读器提供了改进的语义信息。新的分组标签提供了指示内容组(如图表)的方法。当然,HTML5 包括新的音频和视频标签,可以像嵌入图像一样轻松地将多媒体嵌入到 web 应用中。HTML5 还包括一整套新的交互元素,用于实现常见的设计模式,如对话框和渐进式披露。
由于包含了 Web 表单规范,HTML5 还包含了许多新的表单元素,包括数据列表(可过滤的下拉列表)、指示器和进度条以及滑块。HTML5 还指定了几个新的表单属性,允许与表单进行更丰富的交互。现在,通过简单的属性,您可以在表单域中指定占位符文本,或者指示在页面加载时哪个表单域应该具有焦点(处于活动状态)。
帆布
HTML5 指定了新的canvas
特性,一种以编程方式在网页上绘图的方式。canvas
还包括文本、图层混合和图像处理功能。
JavaScript API
HTML5 标准包括一组新的 JavaScript API,为 web 应用添加更多功能。客户端/服务器通信有了新的 API,包括服务器将事件推送到网页的能力,以及保护跨文档和域通信的新方法。还有在浏览器中本地存储数据、拖放交互和多线程的特性。
相关标准
有一系列与 HTML5 交互的相关标准,由 W3C 维护,但在技术上不是 HTML5 标准的成员。这些功能包括地理定位、设备定位和 WebGL。
摘要
在这一章中,我讲述了 HTML 的历史以及 HTML5 是如何产生的,包括:
- 底层技术的起源,
- 浏览器大战,还有
- 标准的诞生。
我还介绍了 HTML5 标准及其相关标准的总体构成。
历史够了!下一章将深入新的 HTML5 元素,包括audio
和video
元素。
二、HTML5 元素
虽然 HTML5 规范比以前的版本复杂得多,但像那些版本一样,它包括新元素的定义和旧元素的废弃。在这一章中,我将把重点放在 HTML5 规范的元素部分。
我将首先展示最佳实践是如何对 HTML 的发展做出贡献的。然后,我将介绍 HTML5 规范中包含的许多新标签:用于创建新部分、内容分组、语义标记、嵌入内容、新交互内容和表单的标签。我还将介绍 web 表单的新特性:新的表单属性、字段属性和输入类型。最后,我将介绍 HTML5 中已被否决的元素。
HTML 的功能、语义和演变
HTML5 代表了该语言进化路线的最新发展。在网络之初,这种演变主要是由浏览器制造商推动的,他们都想在网络上创建自己的专有空间,以区别于他们的竞争对手。不幸的是,这导致了现在被称为“浏览器战争”的网络分裂
第一个标准就是为了对抗这种分裂而诞生的。通过为所有的浏览器制造商提供一个共同的基础,他们使得开发者能够编写独立于平台的 HTML 代码。
更重要的是,随着标准的进一步发展,一组最佳实践也随之发展,以帮助开发人员利用符合标准的优势。这些实践中最重要的两个可能是功能和语义标记分离的概念。
功能 的分离决定了我们应该根据每个工具的长处来使用它们。它通常被总结为“表示与内容的分离”,但它比这更深入:使用 HTML 表示内容,使用 CSS 表示表示,使用 JavaScript 表示功能。
*将 HTML 从 CSS 和 JavaScript 中分离出来,允许这三种语言独立发展,也使得开发人员可以更容易地升级,甚至在以后完全改变技术,而不必完全重做所有三种语言的代码。
语义 标记 的核心思想是使用正确的标签来标记由内容决定的给定部分或数据段。在某种程度上,这是功能分离的更深层次的应用:为工作使用正确的标签。因此段落应该用<p>
标记,无序列表用<ul>
,列表项用<li>
等等。今天,这种最佳实践已经成为 web 开发人员的第二天性,但并不总是这样。常见的是,标签只用于默认样式提供的缩进或空白,这使得标记非常混乱。
这些最佳实践的问题是,除了在 Web 上构建简单的信息文档之外,它们几乎没有什么帮助。如果你想做复杂的布局,HTML 没有必要的语义。如果你想用 web 技术来构建功能,HTML 开始显示出它缺乏语义。
这种缺乏的一个主要症状是像<div>
和<span>
这样的无意义标签的泛滥。因为这些标签只表示节(<div>
表示块级节,<span>
表示内联节),所以它们可以安全地用于包含任何内容。这种无意义的标签导致了新词“div-itis”的产生,或者更常见的“divitis”
HTML5 的主要目的之一就是解决这些缺点。HTML5 为文档的不同部分指定了几个新的标签,新的语义标签,以及用于提高交互性和扩展表单功能的标签。
部分
维持价
好的
所有主流浏览器都支持至少后两个版本的 section 元素。
WHATWG 生活水平:http://www.whatwg.org/specs/web-apps/current-work/multipage/sections.html#sections
W3C 候选人推荐:http://www.w3.org/TR/html5/sections.html#sections
HTML5 包括一组新标签,旨在解决以前版本的 HTML 中缺乏结构化标签的问题。标记即使是中等复杂程度的文档也暴露了原始 HTML 标记集中的几个弱点,这些弱点导致将无意义的标记用于许多常见目的,如导航部分、文档页眉和文档页脚。
新标签如下:
- 一篇文章是一个页面中一组完整、独立的内容。从概念上讲,一篇文章可以被分发或重用。有效文章的示例包括大型杂志中的一篇杂志文章、一篇博客文章、用户界面中可重用的小部件或任何其他自包含的内容集。
- 旁白是表示侧边栏的一种方式:一组独立于周围内容并与之相切的内容。例子包括引用、侧边栏,甚至是大文档中的广告部分。
- 导航部分是指向其他文章或其他文档的主要导航链接。它通常不用于次要链接的集合,例如经常被归入页脚的链接(在这种特定情况下,
<footer>
标记被认为在语义上是足够的)。 <footer>
这个名副其实的标签代表了包含 section 元素的页脚(<body>
、<article>
等等)。).页脚通常包含有关包含节元素的信息,如版权信息、联系信息以及支持文档和站点地图的链接。<header>``<header>
标签将当前包含的 section 元素的一组介绍性标签(<body>
、<article>
等)组合在一起。).标题可以包含导航、搜索表单,甚至是文档的目录和内部链接。- 标签用于将主题相似的内容组合在一起,通常带有某种标题。
在引入这些标签之前,这些部分通常是用相关 CSS 类的<div>
标签标记的,如清单 2-1 中的所示。
清单 2-1 。带有无意义标签 0073 的旧标记
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
body {
margin: 0;
padding: 0;
}
.page {
background-color: #C3DBE8;
}
.header {
background-color: #DDDDDD;
}
.header li {
display: inline-block;
border: 1px solid black;
border-radius: 5px;
padding: 0 5px;
}
.footer {
background-color: #DDDDDD;
}
</style>
</head>
<body>
<div class="page">
<div class="header">
<h1>Lorem Ipsum Dolor Sit Amet</h1>
<div class="navigation">
<ul>
<li>one</li>
<li>two</li>
<li>three</li>
</ul>
</div>
</div>
<div class="section">
<h2>Section Header</h2
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Proin congue leo ut nut tincidunt, sed hendrerit justo
tincidunt. Mauris vel dui luctus, blandit felis sit amet,
mollis enim. Nam tristique cursus urna, id vestibulum
tellus condimentum vulputate. Aenean ut lectus adipiscing,
molestie nibh vitae, dictum mauris. Donec lacinia odio
sit amet odio luctus, non ultrices dui rutrum. Cras
volutpat tellus at dolor rutrum, non ornare nisi
consectetur. Pellentesque sit amet urna convallis, auctor
tortor pretium, dictum odio. Mauris aliquet odio vel
congue fringilla. Mauris pellentesque egestas lorem.</p>
</div>
<div class="aside">
<h2>Aside Header</h2>
<p>Vivamus hendrerit nisl nec imperdiet bibendum. Nullam
imperdiet turpis vitae tortor laoreet ultrices. Etiam
vel dignissim orci, a faucibus dui. Pellentesque
tincidunt neque sed sapien consequat dignissim.</p>
</div>
<div class="footer">
<div class="address">
Sisko’s Creole Kitchen, 127 Main Street,
New Orleans LA 70112
</div>
</div>
</div>
</body>
</html>
清单 2-1 将你的内容分成一个单独的“页面”,包含在一个应用了类"page"
的<div>
标签中。在这个页面中,你有一个带有导航的页眉、一个部分、一个旁白和一个页脚。您还对标记应用了一些基本样式,以更好地说明页眉和页脚部分,并使导航元素看起来更像按钮,而不是简单的无序列表。
这是您可能已经习惯看到的那种标记,除了它在很大程度上依赖于无意义的<div>
标签这一事实之外,它没有任何问题。然而,有了新的 HTML5 标签,你可以去掉所有的<div>
标签,用语义标签代替它们,就像你在清单 2-2 中做的那样。
清单 2-2 。带有 HTML5 语义标签的新热点标记
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
body {
margin: 0;
padding: 0;
}
.page, article {
background-color: #C3DBE8;
}
.header, header {
background-color: #DDDDDD;
}
.header li, header li {
display: inline-block;
border: 1px solid black;
border-radius: 5px;
padding: 0 5px;
}
.footer, footer {
background-color: #DDDDDD;
}
</style>
</head>
<body>
<article>
<header>
<h1>Lorem Ipsum Dolor Sit Amet</h1>
<nav>
<ul>
<li>one</li>
<li>two</li>
<li>three</li>
</ul>
</nav>>
</header>
<section>
<h2>Section Header</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Proin congue leo ut nut tincidunt, sed hendrerit justo
tincidunt. Mauris vel dui luctus, blandit felis sit amet,
mollis enim. Nam tristique cursus urna, id vestibulum
tellus condimentum vulputate. Aenean ut lectus adipiscing,
molestie nibh vitae, dictum mauris. Donec lacinia odio
sit amet odio luctus, non ultrices dui rutrum. Cras
volutpat tellus at dolor rutrum, non ornare nisi
consectetur. Pellentesque sit amet urna convallis, auctor
tortor pretium, dictum odio. Mauris aliquet odio vel
congue fringilla. Mauris pellentesque egestas lorem.</p>
</section>
<aside>
<h2>Aside Header</h2>
<p>Vivamus hendrerit nisl nec imperdiet bibendum. Nullam
imperdiet turpis vitae tortor laoreet ultrices. Etiam
vel dignissim orci, a faucibus dui. Pellentesque
tincidunt neque sed sapien consequat dignissim.</p>
</aside>
<footer>
<address>
Sisko’s Creole Kitchen, 127 Main Street,
New Orleans LA 70112
</address>
</footer>
</article>
</body>
</html>
您已经用相关的语义 HTML5 标签替换了所有无意义的 div。您还更新了样式表,因此新标签将与应用于您移除的<div>
标签的旧类共享相同的样式。
浏览器对这两个例子的渲染略有不同。不同浏览器之间的区别是不同的:Internet Explorer 10 的变化最小,唯一的区别是包含在<address>
标签中的文本自动以斜体显示。Chrome 和 Firefox 的差别更大,如图 2-1 中的所示。
图 2-1 。Chrome 渲染的清单 2-1 截图(左)和 Firefox 渲染的清单 2-2 截图(右)
正如你所看到的,在两种浏览器中,<header>
标签中的<h1>
标签的字体较小,而且它们都以斜体显示<address>
标签中的文本,就像 Internet Explorer 一样。如果您正在迁移到新的语义标签,请确保考虑到这些差异。
分组
维持价
好的
所有主流浏览器至少支持最近两个版本的<figure>
和<figcaption>
特性。Internet Explorer 本身不支持<main>
标签,但其他浏览器支持。
WHATWG 生活水平:http://www.whatwg.org/specs/web-apps/current-work/multipage/grouping-content.html#grouping-content
W3C 候选人推荐:http://www.w3.org/TR/html5/grouping-content.html#grouping-content
HTML5 为分组内容定义了一些新标签。这些标签与 HTML5 Section 标签的不同之处在于,它们将给定的一组数据定义为特定类型的数据,而 Section 标签为文档提供结构。新标签如下:
<figure>
该标签用于将一组独立于主文档流的内容组合在一起,但从文档流内部引用。图形的例子包括插图、屏幕截图和代码片段。<figcaption>
该标签用于为<figure>
标签提供标题。标题是可选的。- 在 W3C 和 WHATWG 规范中,
<main>
标签的定义是不同的。根据 W3C 的规定,<main>
标签应该用来将文档或应用中与主题或功能相关的主要内容组合在一起。根据 WHATWG 的说法,<main>
标签没有内在含义,而是代表其内容。W3C 的 bugbase:https://www.w3.org/Bugs/Public/show_bug.cgi?id=21553
中的 Bug 21553 详细解释了这种差异的原因。
新的分组标签使用起来很简单,如清单 2-3 所示。
清单 2-3 。使用新的 HTML5 分组标签
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
figure figcaption {
font-style: italic;
}
figure pre {
line-height: 1.6em;
font-size: 11px;
padding: 1em 0.5em 0.0em 0.9em;
border: 1px solid #bebab0;
border-left: 11px solid #ccc;
margin: 0.3 0 1.7em 0.3em;
overflow: auto;
max-height: 500px;
position: relative;
background: #faf8f0;
}
</style>
</head>
<body>
<main>
<article>
<h1>Main, Figure and Figcaption</h1>
<h2>Best Things Ever</h2>
<p>Vivamus hendrerit nisl nec imperdiet bibendum. Nullam
imperdiet turpis vitae tortor laoreet ultrices. Etiam
vel dignissim orci, a faucibus dui. Pellentesque
tincidunt neque sed sapien consequat dignissim.</p>
<figure>
<figcaption>Using Figure and Figcaption for Code Samples</figcaption>
<pre>
[sample code here]
</pre>
</figure>
<p>More content about Main, Figure and Figcaption...</p>
</article>
</main>
</body>
</html>
这个示例使用<main>
来表示示例文档的主要部分,使用<figure>
和<figcaption>
来定义代码示例区域。你还在代码区域及其标题上应用了一些简单的 CSS 样式,使其从文档的其余部分中更加突出,如图 2-2 所示。
图 2-2 。火狐渲染的清单 2-3 截图
在这个截图中,你可以看到浏览器对<figure>
标签应用了一些默认的边距,这在不同的浏览器中是相当一致的。
语义学
维持价
混合
任何浏览器都很少支持<bdi>
、<data>
、<ruby>
、<rt>
、<rp>
或<time>
。
对<mark>
的支持很好(至少可以追溯到主流浏览器的两个版本),对<wbr>
的支持非常好(可以追溯到主流浏览器的最早版本)。
WHATWG 生活水平:http://www.whatwg.org/specs/web-apps/current-work/multipage/text-level-semantics.html#text-level-semantics
W3C 候选人推荐:http://www.w3.org/TR/html5/text-level-semantics.html#text-level-semantics
HTML5 包括几个新的语义标签,旨在帮助澄清内容类型。
<bdi>
双向隔离元素用于隔离可能以不同于周围文本的方向呈现的文本的行内范围。<data>``<data>
标签用于将机器可读数据与其包含的内容相关联。它提供了用数据 002E 标注内容的语义方式- 此标签用于标记文档中的事件,如搜索结果。
<ruby>
、<rp>
和<rt>
这些标签用于拼音注释,用于显示东亚字符的发音。有关红宝石注释的详细信息,请参见http://www.w3.org/TR/ruby/
和http://en.wikipedia.org/wiki/Ruby_character
。<time>``<time>
标签与<data>
相似,它提供了一种将数据(在本例中,具体是日期/时间数据)与所包含的内容相关联的方法。<wbr>
断字机会标签用于指示文档流中的一个位置,在该位置浏览器可以启动一个换行符,尽管其内部规则可能不会这样做。它对双向排序没有影响,如果浏览器确实在标签处开始换行,则不使用连字符。
不幸的是,对这些标签的支持相当少。<data>
和<time>
标签,以及 Ruby 注释标签,即使在最现代的浏览器中也没有得到广泛的支持。
然而,<mark>
标签得到了很好的支持,并且和其他内联标签一样易于使用。清单 2-4 显示了一个非常简单的使用<mark>
标记来突出显示文档中的某些单词。
清单 2-4 。在文档中标记单词
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
mark {
background-color: #E3DA5D;
}
</style>
</head>
<body>
<article>
<h1>Using the <mark> tag</h1>
<p>Vivamus hendrerit nisl nec imperdiet <mark>bibendum</mark>. Nullam
imperdiet turpis vitae tortor laoreet ultrices. Etiam
vel dignissim orci, a faucibus dui. Pellentesque
tincidunt <mark>neque</mark> sed sapien consequat dignissim.</p>
</article>
</body>
</html>
本例在所有浏览器中呈现相同的效果(图 2-3 )。
图 2-3 。火狐渲染的清单 2-4 截图
<wbr>
标签可能是所有 HTML5 标签中支持最广泛的标签之一。这是一个在所有浏览器中都可用的非标准标签,它是由 HTML5 引入标准的。它用于在长词中提供断词建议,这在特定情况下很有用。清单 2-5 显示了一个简单的例子,在插入<wbr>
标签之前有很长的单词:
清单 2-5 。文档中的长单词
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
.larger {
font-size: 2em;
}
</style>
</head>
<body>
<article>
<h1>Using the <wbr> tag</h1>
<p>Here are some long words in a slightly larger font size to demonstrate
how useful the <wbr> tag can be.</p>
<p class="larger">Supercalifragilisticexpialidocious and antidisestablishmentarianism,
also pneumonoultramicroscopicsilicovolcanoconiosis.</p>
</article>
</body>
</html>
如图 2-4 所示,清单 2-5 在所有现代浏览器中呈现出你所期望的效果。
图 2-4 。Firefox 中清单 2-5 的渲染
你可以很容易地使用一些<wbr>
标签来帮助浏览器决定在哪里打断那些长单词,如清单 2-6 中的所示。
清单 2-6 。建议在大词中换行
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
.larger {
font-size: 2em;
}
</style>
</head>
<body>
<article>
<h1>Using the <wbr> tag</h1>
<p>Here are some long words in a slightly larger font size to demonstrate
how useful the <wbr> tag can be.</p>
<p class="larger">Supercali<wbr>fragilistic<wbr>expialidocious and
antidis<wbr>establishment<wbr>arianism.</p>
</article>
</body>
</html>
如果需要的话,浏览器可以根据我们的建议断词(图 2-5 )。
图 2-5 。在不同的浏览器宽度下渲染清单 2-6
如你所见,如果需要的话,浏览器现在可以使用我们的断词建议。如果您在屏幕空间非常宝贵的狭窄布局上工作,例如在移动设备上,这可能特别有用。
音频和视频内容
维持价
好的
所有现代浏览器都支持至少后两个版本的音频和视频元素,但请参阅以下有关格式支持的信息。
WHATWG 生活水平:http://www.whatwg.org/specs/web-apps/current-work/multipage/edits.html#embedded-content
W3C 候选人推荐:www.w3.org/TR/html5/embedded-content-0.html#embedded-content-0
以前版本的 HTML 最大的缺点之一是它们不能方便地在网页上包含多媒体内容。HTML5 有专门解决这个问题的新标签。有了这些新标签,在网页上包含多媒体内容就像包含静态图像一样简单。更好的是,所有现代浏览器都非常好地支持这些功能。
在 HTML5 之前,如果你想在网页中嵌入一个视频,你需要一个第三方插件,它能够播放你想要的内容,还能调节音量、快进或快退内容等等。通过 HTML5,浏览器制造商已经将这些功能内置到他们的软件中。这些功能包括控制回放的用户界面和播放各种音频和视频编码媒体格式的能力。
一个不幸的复杂情况是,音频和视频都可以用许多不同的格式编码,而这些格式中的许多都有专利障碍,使得浏览器制造商不愿意支持它们。因此,尽管所有现代浏览器都支持多媒体标签,但有些浏览器支持的格式与其他浏览器不同。有关何种浏览器支持何种格式的详细信息,请参见第七章。
另一个复杂因素来自与多媒体的互动。例如,用户经常想要跳过内容,根据需要前进或后退。支持这样的交互功能需要服务器能够对这些用户交互做出反应,并根据需要提供部分内容。简单的 web 服务器通常没有这种能力,尽管它们中的许多可以配置成这样。有关配置多媒体服务器的更多信息,请参见第七章。
嵌入的音频内容
有了 HTML5 <audio>
标签,你可以将音频内容嵌入到你的网页中,就像包含图像一样简单。像任何 HTML 标签一样,<audio>
标签有几个可以设置的属性:
autoplay
:这是一个布尔标志,当设置为(任何值,甚至false
)时,将使浏览器立即开始播放音频内容,而不需要停下来进行缓冲。controls
:如果设置了此属性,浏览器将显示音频播放器的默认用户界面控件(音量控件、进度条/滑动条等)。).loop
:如果设置了该属性,浏览器将循环播放指定文件。muted
:该属性指定默认静音播放。preload
:该属性用于向浏览器提供如何为指定内容提供最佳用户体验的提示。可以取三个值:none
、metadata
和auto
。none
值指定作者想要最小化音频内容的下载,可能因为内容是可选的,或者因为服务器资源是有限的。metadata
值指定作者推荐下载音频内容的元数据(持续时间、曲目列表、标签等。)和可能的前几帧内容。auto
值指定浏览器可以把用户的需求放在第一位,而不会给服务器带来风险。这意味着浏览器可以开始缓冲内容、下载所有元数据等等。请注意,这些值可以在页面加载后更改。例如,如果您有一个包含许多<audio>
标签的页面,每个标签的preload
都被设置为none
以防止淹没服务器,当用户选择他们想要听到的<audio>
标签时,您可以动态地将其preload
值更改为auto
以提供更好的用户体验。这使您能够平衡用户体验和可用资源。src
:这个属性指定内容的来源,就像一个<img>
标签一样。如果需要的话,可以省略这个属性,而在<audio>
标签中包含一个或多个<source>
标签。
<audio>
标签不是自动关闭的,因此需要一个关闭标签。请注意,由于较旧的浏览器不支持<audio>
标记,因此包含在其中的任何内容都将在这些浏览器中显示,从而提供了一种在较旧的浏览器中提供替代内容的向后兼容方式。
音频标签非常容易使用,如清单 2-7 所示。
清单 2-7 。在网页中嵌入音频内容
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<article>
<h1>Using the <audio> tag</h1>
<audio controls="controls" src="../media/windows-rolled-down.mp3">
</audio>
</article>
</body>
</html>
注意本节的例子使用音频文件。您应该根据需要替换您自己的文件。
清单 2-7 选择显示本地玩家的控件来演示它们默认的样子。每个浏览器的原生播放器看起来略有不同,功能也略有不同(图 2-6 )。
图 2-6 。清单 2-7 以 Chrome(左)和 Firefox(右)呈现
Chrome 的默认音频播放器有一个音量滑块,而 Firefox 的播放器有一个指示当前播放时间的工具提示。设计播放器的样式是不可能的,所以如果你想在所有浏览器上有一致的外观和感觉,你必须构建自己的播放器——这实际上很容易做到,因为 HTML5 还指定了一个 JavaScript API 来处理<audio>
标签。有关该 API 的详细信息,请参见第七章。
提示在 Chrome 中,音频和视频播放器被实现为使用新的 Shadow DOM 规范的 web 组件。使用 Shadow DOM API 可以直接访问播放器的组件并设置样式。例如,播放器的背景是一个阴影
<div>
,它可以用 CSS 选择器audio::-webkit-media-controls-panel
选择,并且它的外观(例如背景颜色)可以根据需要改变。同样,音量条是一个带有选择器audio::-webkit-media-controls-volume-slider
的<input type="range">
标签。不幸的是,在撰写本文时,Chrome 和 Opera 是仅有的两种支持影子 DOM 规范的浏览器。其他浏览器也可能使用 Shadow DOM 实现其播放器,当它们完全支持该规范时,它们的播放器也可能变得可访问,从而允许 web 开发人员控制播放器的外观,而不必从头开始构建自己的播放器。
嵌入式视频内容
HTML5 <video>
标签支持浏览器中的基本视频功能。它的功能类似于<audio>
标签,并且有一组类似的可以设置的属性:
autoplay
:这是一个布尔标志,当它被设置(设置为任何值,甚至是false
)时,将使浏览器立即开始播放视频内容,而不需要停下来进行缓冲。controls
:如果设置了该属性,浏览器将显示视频播放器的默认用户界面控件(音量控件、进度条/滑动条等)。).height
:该属性可用于指定视频播放器的高度,以像素为单位。loop
:如果设置了该属性,浏览器将循环播放指定文件。muted
:该属性指定默认静音播放。poster
:该属性可用于指定视频播放前海报显示的 URL。如果没有指定海报,那么一旦加载,播放器将默认显示视频的第一帧。preload
:该属性用于给浏览器提供一个提示,如何为指定的内容提供最佳的用户体验。可以取三个值:none
、metadata
和auto
。none
值指定作者希望最小化视频内容的下载,可能是因为内容是可选的,或者是因为服务器资源有限。metadata
值指定作者推荐下载视频内容的元数据(持续时间、曲目列表、标签等。)和可能的前几帧内容。auto
值指定浏览器可以把用户的需求放在第一位,而不会给服务器带来风险。这意味着浏览器可以开始缓冲内容、下载所有元数据等等。请注意,这些值可以在页面加载后更改。例如,如果您有一个包含许多<video>
标签的页面,每个标签都将preload
设置为none
以防止淹没服务器,当用户选择他们想要查看的<video>
标签时,您可以动态地将其preload
值更改为auto
以提供更好的用户体验。这使您能够平衡用户体验和可用资源。src
:这个属性指定内容的来源,就像一个<img>
标签一样。如果需要的话,可以省略这个属性,而在<video>
标签中包含一个或多个<source>
标签。width
:该属性可用于指定视频播放器的宽度,以像素为单位。
标签<video>
和标签<audio>
一样容易使用,如清单 2-8 所示。
清单 2-8 。在网页中嵌入视频内容
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<article>
<h1>Using the <video> tag</h1>
<video controls="controls" src="../media/podcast.m4v">
</video>
</article>
</body>
</html>
注意本节的例子使用的是视频文件。您应该根据需要替换您自己的文件。
和<audio>
标签一样,每个浏览器提供了稍微不同的视频播放器,如图 2-7 中的所示。
图 2-7 。清单 2-8 以 Chrome(左)和 Firefox(右)呈现
和以前一样,这两种浏览器的视频播放器界面略有不同。如果你想改变播放器的外观和它的控制,你必须自己构建。
指定多个来源
<audio>
和<video>
标签都有一个 src
属性,但是您可以放弃该属性,在<audio>
或<video>
标签中包含一个或多个<source>
标签。您甚至可以为同一个文件指定不同的编码,从而避开浏览器在编码支持方面的任何限制。浏览器将沿着<source>
标签列表向下,并播放它支持的第一个文件。
<source>
标签有两个属性:
src
:音频文件的 URL。type
:音频文件的 MIME 类型,带有可选的编解码器参数,根据 RFC 4281 指定。
举个例子,假设你有一个我们想要提供的视频。你有两种不同的格式:Ogg Vorbis 和 MP4。使用两个<source>
标签,如清单 2-9 中的所示。
清单 2-9 。为多媒体指定多个源
<video controls>
<source src="../media/video-1.mp4" type="video/mp4">
<source src="../media/video-1.ogv" type="video/ogg">
</video>
通过使用“类型”属性中的可选编解码器参数,您可以非常精确地了解音频和视频的编码。例如,如果你有一个 H.264 视频(profile 3)和低复杂度的 AAC 音频都包含在一个 MP4 容器中,你可以指定如清单 2-10 所示的编解码器:
清单 2-10 。为视频源指定音频和视频编解码器
<source src="../media/video-1.mp4" type="video/mp4, codecs=’ avc1.4D401E, mp4a.40.2’">
这对于为您的视频提供尽可能高质量的编码,同时允许大多数浏览器访问它,而不管编码支持限制,特别有用。
互动元素
维持价
不支持
除了实验版本,现代浏览器不支持交互元素。
WHATWG 生活水平:http://www.whatwg.org/specs/web-apps/current-work/multipage/interactive-elements.html#interactive-elements
W3C 候选人推荐:http://www.w3.org/TR/html5/interactive-elements.html
HTML5 包括一组新的交互式元素,旨在提供一些可以在网页和应用中使用的预构建用户界面元素。不幸的是,大多数浏览器还不支持这些特性,但是随着时间的推移,支持程度可能会提高。
对话
最令人兴奋的新特性之一是<dialog>
标签,它提供了轻松创建弹出对话框的能力。包含在<dialog>
标签中的任何内容都不会呈现在文档中,直到您调用其显示方法之一:
show
:调用这个方法将打开一个标准的弹出对话框。showModal
:调用这个方法将打开一个模态对话框,对话框后面的页面是灰色的。
此外,每个对话框在关闭时都会调度一个close
事件。
清单 2-11 是一个简单的例子,演示了如何使用<dialog>
标签。在撰写本文时,唯一支持<dialog>
标签的浏览器是 Chrome,即使这样,你也必须在chrome://flags
中激活实验性的网络平台功能。如果您启用了这些特性,这个例子将会运行得很好。
清单 2-11 。网络对话
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
li {
display: inline-block;
background-color: #A9DCF5;
border-radius: 4px;
padding: 2px 10px;
cursor: pointer;
}
</style>
</head>
<body>
<article>
<h1>Using the <dialog> tag</h1>
<ul>
<li id="open-dialog">Open Dialog</li>
<li id="open-modal">Open Modal</li>
</ul>
<dialog id="dialog">
<p>Hello World!</p>
<button id="close-dialog">Okay</button>
</dialog>
</article>
<script>
var myDialog = document.getElementById(’dialog’),
openDialog = document.getElementById(’open-dialog’),
openModal = document.getElementById(’open-modal’),
closeDialog = document.getElementById(’close-dialog’),
status = document.getElementById(’status’);
closeDialog.addEventListener(’click’, function(event) {
myDialog.close();
}, false);
openDialog.addEventListener(’click’, function(event) {
myDialog.show();
}, false);
openModal.addEventListener(’click’, function(event) {
myDialog.showModal();
}, false);
myDialog.addEventListener(’close’, function(event) {
alert(’A close event was dispatched.’);
}, false);
</script>
</body>
</html>
这将呈现一个非常简单的对话框,如图 2-8 所示。
图 2-8 。清单 2-11 产生的对话框
每当您关闭其中一个对话框时,close 事件将被触发,产生一个警告,内容为“close 事件已被调度”
这是对话框的默认外观。可以很容易地用 CSS 对它们进行样式化,使它们更有吸引力,当然它们可以包含任何内容,包括图像、表单域等等。您还可以为模式实例设置背景样式;它是一个伪元素,可以使用对话框选择器上的::backdrop
来访问。
例如,在清单 2-12 中,如果你在你的样式表中添加几个简单的 CSS 指令,你会有一个更吸引人的对话框。
清单 2-12 。网络对话框的 CSS 样式
dialog {
text-align: center;
padding: 1.5em;
margin: 1em auto;
border: 0;
border-radius: 8px;
box-shadow: 0 2px 10px #111;
}
dialog::backdrop {
background-color: rgba(187, 217, 242, 0.8);
}
这些样式将改变对话框和模态背景的外观(图 2-9 )。
图 2-9 。应用 CSS 样式的 Web 对话框(模式状态)
虽然目前只有 Chrome 支持<dialog>
标签,但是在https://github.com/GoogleChrome/dialog-polyfill
有一个 polyfill 可以提供其他浏览器中的大部分功能。
提示 Polyfill 是一个术语,指在浏览器中启用或复制不支持的功能的库。polyfill 的另一个术语是垫片。
逐步解密
一个常见的 UI 特性是渐进式披露:你提供一个简单的条目列表,当用户点击一个条目时,下面的空间就会展开,显示更多的信息。这些小部件根据使用的框架有不同的名称(例如,jQuery UI 将它们称为 accordions )。HTML5 使用<summary>
和<details>
标签包含了这个特性的定义。<details>
标签包含了所有需要的内容,包括一个<summary>
标签,它应该只包含内容的简短摘要。一个<details>
标签的默认呈现是只显示前面有一个小三角形的<summary>
标签的内容。然后,用户可以点击摘要上的任何地方,其余的内容就会显示出来。您可以通过赋予给定的<details>
标签 open 属性来指定该标签在打开状态下呈现。
目前,Chrome、Opera 和 Safari 是唯一支持<details>
和<summary>
标签的浏览器。火狐将会支持这些标签,你可以在https://bugzilla.mozilla.org/show_bug.cgi?id=591737
的 bug 591737 中查看它们的支持状态。Internet Explorer 中的支持状态未知。
标签使用起来很简单,如清单 2-13 所示。
清单 2-13 。逐步解密
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<article>
<h1>Using the <summary> and <details> tags</h1>
<details>
<summary>Item 1</summary>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus
accumsan orci nec justo rhoncus facilisis. Integer pellentesque
ipsum vitae semper lacinia. Quisque non nisl rutrum, porta est at,
ultrices neque. Aenean consequat, lacus vulputate vestibulum
faucibus, turpis magna mollis quam, a congue neque lorem at
justo.</p>
</details>
<details>
<summary>Item 2</summary>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus
accumsan orci nec justo rhoncus facilisis. Integer pellentesque
ipsum vitae semper lacinia. Quisque non nisl rutrum, porta est at,
ultrices neque. Aenean consequat, lacus vulputate vestibulum
faucibus, turpis magna mollis quam, a congue neque lorem at
justo.</p>
</details>
<details open>
<summary>Item 3--this one will be open by default</summary>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus
accumsan orci nec justo rhoncus facilisis. Integer pellentesque
ipsum vitae semper lacinia. Quisque non nisl rutrum, porta est at,
ultrices neque. Aenean consequat, lacus vulputate vestibulum
faucibus, turpis magna mollis quam, a congue neque lorem at
justo.</p>
</details>
<details>
<summary>Item 3</summary>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus
accumsan orci nec justo rhoncus facilisis. Integer pellentesque
ipsum vitae semper lacinia. Quisque non nisl rutrum, porta est at,
ultrices neque. Aenean consequat, lacus vulputate vestibulum
faucibus, turpis magna mollis quam, a congue neque lorem at
justo.</p>
</details>
</article>
</body>
</html>
在 Chrome 中,这个例子渲染得很好,如图 2-10 所示。
图 2-10 。清单 2-13 用 Chrome 渲染的
单击关闭的项目会显示其隐藏的内容,单击打开的项目会隐藏其内容。
形式
表单在 HTML5 中得到了显著的改进。该规范包括表单的新标签(如数据列表、进度表和日期选择器)以及现有表单标签的新属性。这些新功能旨在使表单对用户更具交互性,并且更易于构建和维护。
新表单元素
维持价
混合
最近两个版本的主流浏览器都很好地支持这些特性。然而,Internet Explorer 不支持<meter>
标签。
WHATWG 生活水平:http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#forms
W3C 候选人推荐:http://www.w3.org/TR/html5/forms.html#forms
HTML5 有一些新的表单元素,它们是专门为实现过去几年发展起来的通用用户界面模式而设计的。具体来说,这些新标签实现了自动完成功能和进度条。
数据列表
第一个新标记实现了一个常见的自动完成功能:当您开始在表单域中键入内容时,会出现一个下拉列表,其中包含与已经键入的内容相匹配的选项。随着您继续键入,列表会变得更加具体,并且您可以随时使用箭头键来选择其中一个选项。这种自动完成字段通常被称为数据列表(有时也称为组合框),HTML5 有一个新的<datalist>
标签来实现这个用户界面元素。
实际上,<datalist>
标签包含<option>
标签,数据列表中的每一项都有一个标签。<datalist>
元素本身不在页面中呈现,可以出现在文档结构中的任何地方。创建后,数据列表必须与输入字段相关联才能使用。给<datalist>
标签一个唯一的id
属性。要将其与一个<input>
元素相关联,将该元素的 list 属性设置为惟一的id
。这告诉浏览器使用<input>
元素呈现指定的数据列表,如清单 2-14 所示。
清单 2-14 。数据列表
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<!-- Note the datalist can be anywhere -->
<datalist id="browsers">
<option value="Chrome">
<option value="Firefox">
<option value="Internet Explorer">
<option value="Opera">
<option value="Safari">
</datalist>
<article>
<h1>Using the <datalist> tag</h1>
<input list="browsers" />
</article>
</body>
</html>
与其他 HTML5 用户界面元素一样,每个浏览器呈现的数据列表略有不同(图 2-11 )。
图 2-11 。清单 2-14 在 Chrome(左)和 Firefox(右)中呈现
可以看到,Chrome 在输入栏的右侧提供了下拉箭头提示,表示输入栏是数据列表,Firefox 在下拉菜单上有轻微的阴影。不同浏览器之间的列表功能保持不变。
米
新的<meter>
标签提供了一个简单的仪表栏或仪表视觉元素。此条旨在模拟已知范围内的测量值,或整体的分数值(例如,卷、磁盘使用情况等)。).它不应该用于显示进度(例如,在下载中);为此使用新的<progress>
标签。
<meter>
标签具有以下属性:
value
:要显示的当前值。如果指定,该值必须在最小值和最大值之间。如果没有设置值,或者值不正确,浏览器将默认为 0。如果已指定,但该值大于 max 属性,则该值将被设置为 max 属性的值。如果该值小于 min 属性,则该值将被设置为 min 属性的值。min
:范围的最小值。如果未指定,则默认为 0。max
:范围的最大值。必须大于 min 属性的值(如果指定)。默认为 1。
也可以在测量范围内指定子范围。可以有low
范围、high
范围和optimum
范围。low
范围从min
值到指定值,而high
范围从high
值到max
值。通过使用optimum
属性在范围内指定一个数字,可以将low
范围或high
范围指定为optimum
范围。
low
:低量程的最高值。当 value 属性在low
范围内时,默认情况下,条将呈现黄色。high
:high
范围的最小值,从该值到max
属性的值。当value
属性在high
范围内时,默认情况下,条会呈现黄色。optimum
:表示范围的最佳值。该值必须在范围的min
和max
值之间。如果使用了low
和high
范围,在其中一个范围内指定一个optimum
值将指示这些范围中的哪一个是优选的。当该值在首选范围内时,该条将呈现绿色。当它在另一个范围内时,它将呈现红色。
创建这些计量器很简单,只需在文档中添加一个<meter>
标签,如清单 2-15 所示。
清单 2-15 。米条
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<article>
<h1>Using the <meter> tag</h1>
<p>Simple meter from 1 to 100, value set to 25:<br>
<meter min="1" max="100" value="25"></meter>
</p>
<p>Simple meter from 1 to 100, low range from 1 to 25, high range from
75 to 100, value set to 90:<br>
<meter min="1" max="100" low="25" high="75" value="90"></meter>
</p>
<p>Simple meter from 1 to 100, low range from 1 to 25, high range from
75 to 100, value set to 10:<br>
<meter min="1" max="100" low="25" high="75" value="10"></meter>
</p>
<p>Simple meter from 1 to 100, low range from 1 to 25, high range from
75 to 100, optimum set to 10, value set to 10:<br>
<meter min="1" max="100" low="25" high="75" optimum="10" value="10"></meter>
</p>
<p>Simple meter from 1 to 100, low range from 1 to 25, high range from
75 to 100, optimum set to 10, value set to 10:<br>
<meter min="1" max="100" low="25" high="75" optimum="10" value="90"></meter>
</p>
</article>
</body>
</html>
这些仪表在不同的浏览器中呈现得相当一致(图 2-12 )。
图 2-12 。清单 2-15 在 Chrome(左)和 Firefox(右)中呈现
输出
新的<output>
标签提供了一种在表单中指定计算输出或其他用户操作的方式。它没有任何特殊的功能;相反,它提供了一个语义标签来标记这类内容。
清单 2-16 中显示了一个简单的例子。
清单 2-16 。表单中的计算输出
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<article>
<h1>Using the <output> tag</h1>
<form id="mainform" onsubmit="return false">
<label for="input-number">Temperature</label>
<input name="input-number" id="input-number" type="number" step="any"><br>
<input type="radio" name="convert-choice" id="radio-ftoc" checked value="ftoc">
<label for="radio-ftoc">Convert Fahrenheit to Celcius</label><br>
<input type="radio" name="convert-choice" id="radio-ctof" value="ctof">
<label for="radio-ctof">Convert Celcius to Fahrenheit</label><br>
Result:
<output name="output-target" for="input-number" id="output-target"></output>
</form>
</article>
<script>
var myForm = document.getElementById(’mainform’);
var converter = {
ctof: function(degreesC) {
return (((degreesC * 9) / 5) + 32);
},
ftoc: function(degreesF) {
return (((degreesF - 32) * 5) / 9);
}
};
myForm.addEventListener(’input’, function() {
var inputNumber = document.getElementById(’input-number’),
outputTarget = document.getElementById(’output-target’);
var sel = document.querySelector(’input[name=convert-choice]:checked’).value;
outputTarget.value = convertersel);
}, false);
</script>
</body>
</html>.
这是一个简单的例子,但是您使用了一些巧妙的技巧。
- 您创建了一个 converter 对象,它有两个方法,
ctof
(将摄氏温度转换为华氏温度)和ftoc
(将华氏温度转换为摄氏温度)。 - 您将单选按钮的值属性之一设置为
ctof
,将另一个设置为ftoc
。 - 您使用选择器
input[name=convert-choice]:checked
来获取选中的单选按钮,然后获取其值(ctof 或 ftoc)。 - 然后,只需使用查询结果,就可以直接访问 converter 对象的正确方法。
提示 JavaScript 也受一个标准——ECMA-262——控制,它明确定义了两种访问对象成员的方法:点符号或括号符号。所以
objectName.identifierName
在功能上等同于objectName[<identifierName string>]
,即使所讨论的对象不是数组。有关详细信息,请参见http://www.ecma-international.org/ecma-262/5.1/
的 ECMA-262 中的第 11.2.1 节“属性访问器”。
图 2-13 显示了用 Chrome 渲染的清单 2-16 。
图 2-13 。清单 2-16 用 Chrome 渲染的
进步
HTML5 定义了一个新的<progress>
标签,在文档中呈现为一个进度条。它用于指示任务的进度或完成情况,并向用户提供已经完成的工作量和剩余工作量的概念。它不应该用于可视化已知范围内的测量—为此,使用<meter>
标签。
<progress>
标签具有以下属性:
max
:活动的最大值。该值必须是有效的正浮点数。如果没有指定max
,最大值默认为 1。value
:进度的当前值。该值必须是介于 0 和max
(如果指定)或 1(如果未指定max
)之间的有效浮点数。如果没有指定value
,那么进度条被认为是不确定的,这意味着它正在建模的活动正在进行,但是没有给出需要多长时间才能完成的指示。
清单 2-17 提供了一个进度条的简单演示。
清单 2-17 。进度条
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<article>
<h1>Using the <progress> tag</h1>
<p>Downloading file1<br>
<progress max="100" value="10">10/100</progress> 10%</p>
<p>Downloading file2<br>
<progress max="100" value="50" orient="vertical">50/100</progress> 50%</p>
</article>
</body>
</html>
如图 2-14 所示,进度条在不同的浏览器中呈现不同的效果。
图 2-14 。清单 2-17 在 Chrome(左)、Internet Explorer(中)和 Windows 8 上的 Firefox(右)中呈现
在 MacOS 上渲染时,这些示例看起来也会有所不同。幸运的是,这些酒吧很容易设计。Firefox 和 Internet Explorer 可以直接访问元素的样式,而在 Chrome 中,你必须选择伪元素来更改它们。通过在你的 CSS 中添加一些简单的指令,如清单 2-18 所示,你可以让工具条在所有浏览器中看起来都一样。
清单 2-18 。进度条的 CSS 规则
progress {
color: #0063a6;
font-size: .6em;
line-height: 1.5em;
text-indent: .5em;
width: 15em;
height: 1.8em;
border: 1px solid #0063a6;
background-color: #fff;
}
::-webkit-progress-bar {
background-color: #fff;
}
::-webkit-progress-value {
background-color: #0063a6;
}
正如你在图 2-15 中看到的,这些条现在在不同的浏览器中呈现相同的效果。
图 2-15 。应用 CSS 规则的进度条
一个稍微实际一点的例子是定时器。使用<progress>
标签,你可以指示某个分配的时间——例如十秒——正在过去,如清单 2-19 中的所示。
清单 2-19 。十秒钟计时器
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
progress {
color: #0063a6;
font-size: .6em;
line-height: 1.5em;
text-indent: .5em;
width: 15em;
height: 1.8em;
border: 1px solid #0063a6;
background-color: #fff;
}
::-webkit-progress-bar {
background-color: #fff;
}
::-webkit-progress-value {
background-color: #0063a6;
}
</style>
</head>
<body>
<article>
<h1>Using the <progress> tag</h1>
<h2>Ten Second Timer</h2>
<p><progress max="10" value="0" id="myProgress">0</progress></p>
</article>
<script>
var progress = 0;
var myProgress = document.getElementById("myProgress");
var myTimer = setInterval(function() {
myProgress.value = ++progress;
if (progress > 10) {
clearInterval(myTimer);
}
}, 1000);
</script>
</body>
</html>
这个例子使用 DOM 方法setInterval()
每秒运行一个函数来更新进度条的值。当进度条已满时,它用clearInterval()
方法取消计时器。
新表单元素属性
HTML5 规范为表单元素提供了一些有用的新属性。同样,这些新属性是专门为解决以前版本的 HTML 表单中的缺点而设计的,并添加了通常需要的功能,到目前为止,这些功能都必须使用 JavaScript 来构建。
自动完成
所有的浏览器都提供了存储表单数据以备后用的功能。这对移动设备特别有帮助,因为它减少了打字。autocomplete
属性允许您指定哪些<input>
元素可以自动完成,哪些应该总是手动填充。autocomplete
属性可以取两个值:on
(允许自动完成;这是默认设置)或off
(不允许自动完成)。
清单 2-20 是一个简单的例子,有两个表单域,一个允许自动完成,另一个不允许。
清单 2-20 。控制表单中的自动完成
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<article>
<h1>Using the autocomplete attribute</h1>
<form id="test-form" action="#" method="post">
<p><label for="input-auto">This input allows autocomplete:</label><br>
<input autocomplete="on" id="input-auto" name="input-auto"></p>
<p><label for="input-noauto">This input does not allow autocomplete:</label><br>
<input autocomplete="off" id="input-noauto" name="input-noauto"></p>
<p><input type="submit"></p>
</form>
</article>
</body>
</html>
几乎在每个浏览器中,您都应该能够填写表单字段并单击提交按钮。然后,重新加载页面。双击第一个表单域,您应该会看到一个包含您之前输入的值的下拉框(图 2-16 )。
图 2-16 。清单 2-20 用 Chrome(左)和 Firefox(右)呈现
请注意,您必须在浏览器中启用自动填充功能。大多数浏览器在默认情况下会启用它,但出于安全考虑,许多人会将其关闭。如果用户在其浏览器中禁用了该功能,autocomplete 属性将不起作用。
浏览器使用许多线索来确定哪些表单字段应该用什么数据自动完成:字段的名称和 ID、<form>
标签的action
和method
属性,等等。这个过程是相当不标准的,并且进入了“魔术”的领域 2012 年,谷歌提议对autocomplete
属性进行扩展,以帮助标准化流程。在这个提议中,他们提出了一个具有从address-line1
到postal-code
再到url
的一系列值的autocompletetype
属性。你可以在http://wiki.whatwg.org/wiki/Autocompletetype
阅读他们的完整提案。那个提议从未被完全采纳,但其中的一些部分最终进入了新的自动填充规范,你可以在https://html.spec.whatwg.org/multipage/forms.html#autofill
查看。
自(动)调焦装置
autofocus
属性允许您指定页面加载时哪个表单字段应该有焦点。因为它是排他的,所以只能在给定页面上的一个表单字段上设置autofocus
,当页面加载完成时,焦点将转到该元素。您不能在类型为hidden
的表单元素上设置autofocus
。自动对焦可以设置为任何<input>
、<button>
或<textarea>
字段。
如果你在页面加载时给清单 2-20 中的第二个字段添加一个autofocus
属性,它将被聚焦,如清单 2-21 中的所示。
清单 2-21 。自动聚焦输入场
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<article>
<h1>Using the autofocus attribute</h1>
<form id="test-form" action="#" method="post">
<p><label for="input-auto">This input allows autocomplete:</label><br>
<input autocomplete="on" id="input-auto" name="input-auto"></p>
<p><label for="input-noauto">This input does not allow autocomplete:</label><br>
<input autocomplete="off" id="input-noauto" name="input-noauto" autofocus="autofocus"></p>
<p><input type="submit"></p>
</form>
</article>
</body>
</html>
当页面加载完成后,第二个输入字段将被选中并准备接收输入,如图图 2-17 所示。
图 2-17 。清单 2-21 用 Chrome 渲染的
当然,只要用户单击浏览器中的其他地方,该字段就会失去焦点,除非用户再次单击该字段,否则它不会返回。自动对焦只发生在页面加载时。
占位符
表单的另一个常见设计特征是在<input>
字段中的占位符文本。占位符文本有助于提供有关该字段用途的更多信息,当用户开始键入时,它会消失。HTML5 包括一个新的placeholder
属性,可以应用于<input>
和<textarea>
字段。为属性指定的值用作字段内的占位符文本。
清单 2-22 是一个简单的表单示例,用户可以在其中编写和发送电子邮件。
清单 2-22 。占位符文本
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<article>
<h1>Using the placeholder attribute</h1>
<p><label for="message-title">Title:</label><br>
<input placeholder="title of message" id="message-title"></p>
<p><label for="message-body">Body:</label><br>
<textarea placeholder="body of message" id="message-body"></textarea></p>
<p><input type="submit" value="Send Email"></p>
</article>
</body>
</html>
在现代浏览器中,占位符文本在表单字段内将显示为灰色文本(图 2-18 )。
图 2-18 。清单 2-22 用 Chrome 渲染的
一旦用户开始在字段中输入,占位符文本就会消失,如图 2-19 所示。
图 2-19 。清单 2-22 用户输入后的
注意,这个例子仍然包括<label>
标签。占位符文本是一个很好的设计概念,但是它不应该取代<label>
标签,后者是表单可访问性的重要组成部分。您通常不需要这两个标签——正如您在示例中看到的,这些标签有些多余。在这种情况下,你可以简单地用 CSS 隐藏<label>
标签,如清单 2-23 所示。
清单 2-23 。用 CSS 隐藏标签
label: {
display: none;
}
这个简单的 CSS 使得图 2-20 中的表单看起来更好。
图 2-20 。清单 2-23 用 Chrome 渲染的
新输入类型
HTML5 规范还包括了input
元素的type
属性的新值。您可能很熟悉使用这个属性来创建复选框和单选按钮:
<input type="checkbox">
<input type="radio">
HTML5 增加了几个新类型,为输入字段增加了新的用户界面功能,从颜色和日期选择器到搜索框。不幸的是,不同的浏览器对这些新输入类型的支持大不相同。
对于桌面浏览器,Chrome 的支持最好,Firefox 和 Internet Explorer 都远远落后。不幸的是,这限制了它们在桌面应用中的用途。
在移动浏览器上支持更好。大多数新的输入类型将使用设备的特殊键盘和输入工具。例如,当类型为tel
的输入字段在 Safari Mobile 中处于活动状态时,手机将显示电话键盘。这使得移动用户更容易输入电话号码。
尽管这些新类型现在没有得到广泛支持,但对它们的支持正在增长,特别是在移动设备上。考虑到使用特定键盘进行移动输入的好处,使用这些输入类型是一个好习惯,即使它们没有得到广泛支持。
新的输入类型如下:
color
:允许用户选择颜色。在 Chrome desktop 中,这将显示一个色卡,当单击它时,会显示主机操作系统的颜色选择器用户界面小部件。没有其他浏览器支持此元素。date
、datetime
、datetime-local
、month
、time
、week
:这些输入类型允许用户输入日期和时间。在 Chrome 桌面中,这些显示 Chrome 内置的日历和时间选择小部件。在移动设备上,这些显示日期和时间选择器(在 iOS 上,它们以微调器的形式出现)。email
:表示用于收集电子邮件地址的输入字段。在移动设备上,这种类型在激活时会显示一个互联网地址友好的键盘。在 iOS 上,这种键盘采用常规键盘的形式,带有@键和. com 键。number
:该输入类型指定用户将输入一个数字。在 Chrome 和 Firefox 桌面上,这种输入类型将显示一个简单的增加/减少小部件。在移动设备上,这种输入类型将显示字母数字键盘的数字页。range
:显示滑块小工具。这是桌面和移动设备上所有浏览器都广泛支持的唯一输入类型。search
:表示该字段为搜索字段。搜索字段和常规输入字段的主要区别在于,搜索字段包含“清除”功能,通常实现为输入字段边缘的×按钮。在 Chrome 和 Internet Explorer 桌面上,这显示了一个简单的搜索栏,右边有一个清除按钮。tel
:表示该字段将用于输入电话号码。在移动设备上,这种类型的输入字段将在激活时显示电话号码键盘。url
:表示该字段将用于输入一个 URL,很可能是一个网址。在移动设备上,这将显示一个互联网地址友好的键盘,而活跃。
如果你想测试这些输入字段,清单 2-24 有完整的输入字段,你可以把它们加载到任何浏览器中。
清单 2-24 。演示了新的输入类型
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<meta name="viewport" content="width=device-width, user-scalable=no">
<style>
input {
display: block;
margin-bottom: 20px;
}
</style>
</head>
<body>
<article>
<h1>New HTML5 Input Types</h1>
<form id="mainform" onsubmit="return false">
<label>type=color</label>
<input type="color">
<label>type=date</label>
<input type="date">
<label>type=datetime</label>
<input type="datetime">
<label>type=datetime-local</label>
<input type="datetime-local">
<label>type=email</label>
<input type="email">
<label>type=month</label>
<input type="month">
<label>type=number</label>
<input type="number">
<label>type=range</label>
<input type="range">
<label>type=search</label>
<input type="search">
<label>type=tel</label>
<input type="tel">
<label>type=time</label>
<input type="time">
<label>type=url</label>
<input type="url">
<label>type=week</label>
<input type="week">
</form>
</article>
</body>
</html>
Chrome 是这个例子中最有趣的浏览器。它为几种类型生成了一些非常有用的小部件,如图 2-21 所示。
图 2-21 。Chrome 桌面中呈现的颜色、日期和搜索输入类型
不推荐使用的元素和过时的参数
HTML5 已经正式否决了几个元素。其中一些已经被新的标签所取代,或者被 CSS 特性所取代,而另一些则不再需要。
<applet>
:用<embed>
或<object>
代替。<acronym>
:用<abbr>
代替。<frame>
、<frameset>
和<noframes>
:框架集在 HTML5 中已经完全被弃用。相反,可以考虑使用 iframes 或服务器端技术。<strike>
:使用<s>
,除非标记是为了编辑,在这种情况下使用<del>
。<basefont>
、<big>
、<blink>
、<center>
、<font>
、<marquee>
、<multicol>
、<nobr>
、<spacer>
、<tt>
:使用合适的元素或 CSS 来代替。对于<tt>
元素,使用<kbd>
(表示键盘输入)、<var>
(表示变量)、<code>
(表示计算机代码)或<samp>
(表示样本输出)。在<big>
元素的情况下,如果内容是标题,使用 header 标签,使用<strong>
元素表示强调或重要性,使用<mark>
元素突出显示引用。
此外,HTML5 已经废弃了现有元素的许多参数。同样,这些参数中的许多已经被 CSS 或其他特性所取代,而其他参数则是早期版本的 HTML 或 XHTML 的遗留物。
- 属性
background
、datasrc
、datafld
和dataformats
已从所有适用的标签中被弃用。在背景的情况下,使用 CSS 将背景应用于元素。 <a>
:charset
、coords
、methods
、name
、rev
(使用rel
的反义词)、shape
(使用area
的影像地图)、urn
。<body>
:alink
,bgcolor
,link
,marginbottom
,marginheight
,marginleft
,marginright
,margintop
,marginwidth
,text
,vlink
。<br>
:clear
。<caption>
:align
。<col>
:align
,char
,charoff
,valign
,width
。<div>
:align
。<dl>
:compact
。<hr>
:align
,color
,noshade
,size
,width
。- 所有表头标签:
align
。 <iframe>
:align
,allowtransparency
,frameborder
,hspace
,longdesc
,marginheight
,marginwidth
,scrolling
,vspace
。<img>
:align
,border
,datasrc
,hspace
,longdesc
,lowsrc
,name
,vspace
。<input>
:align
,hspace
,ismap
,usemap
,vspace
。<legend>
:align
。<link>
:charset
,methods
,rev
,target
,urn
。<menu>
:compact
。<object>
:align
、archive
、border
、classid
、code
、codebase
、codetype
(使用数据和类型属性以及<param>
元素)、declare
、hspace
、standby
、vspace
。<ol>
:compact
。<option>
:name
(用id
代替)。<p>
:align
。<param>
:type
和valuetype
(使用名称和值属性)。<pre>
:width
。<script>
:event
,for
,language
。<table>
:align
,bgcolor
,border
,bordercolor
,cellpadding
,cellspacing
,frame
,rules
,summary
,width
。<tbody>
:align
,char
,charoff
,valign
。<td>
:abbr
,align
,axis
,bgcolor
,char
,charoff
,height
,nowrap
,scope
,valign
,width
。<tfoot>
:align
,char
,charoff
,valign
。<th>
:align
,bgcolor
,char
,charoff
,height
,nowrap
,valign
,width
。<thead>
:align
,char
,charoff
,valign
。<tr>
:align
,bgcolor
,char
,charoff
,valign
。<ul>
:compact
和type
。
尽管浏览器仍然可以在 HTML5 文档中呈现这些标签并识别这些属性,但是您不应该使用它们。任何验证器也应该抛出错误。
摘要
在这一章中,我已经介绍了 HTML5 规范的元素部分的重点:
- 我讨论了 HTML 的发展如何受到语义的影响。
- 我简要介绍了 HTML5 为节、分组和语义引入的新标签。这些标签进一步扩展了语言处理复杂文档和布局的能力。
- 我探索了 HTML5 新的多媒体特性,并演示了
<audio>
和<video>
标签的基本用法。 - 然后,我向您展示了 HTML5 指定的新交互元素:对话框和渐进式披露。不幸的是,这些功能还没有得到很好的支持,但这应该会改变久而久之。
- 我回顾了 HTML5 引入的表单更改,其中一些在移动环境中特别有用。
- 最后,我查看了 HTML5 中不赞成使用的标签和属性。
在下一章,你将深入 HTML5 指定的 JavaScript APIs。*
三、HTML5 API
正如在第一章中提到的,HTML5 标准不同于以前的 HTML 标准,因为它不仅仅是标记语言的定义。由于该标准旨在成为创建 web 应用和网页的平台,因此它引入了许多旨在简化应用构建的新特性:浏览器与服务器通信的新方式、存储和检索数据的新方式、对常见用户交互(如拖放)的支持等等。
像新的audio
和video
标签一样,这些新的网络应用功能在过去已经使用大量的 JavaScript 程序甚至浏览器插件实现了。现在,有了 HTML5,浏览器制造商直接在他们的浏览器中实现它们。
所有这些新特性都可以在 JavaScript 程序中使用,所以浏览器制造商提供了使用脚本访问它们的接口。这些接口通常采用 JavaScript 对象和方法的形式。HTML5 标准也定义了这些接口,因此 JavaScript 对象和方法在所有实现该标准的浏览器中以相同的方式出现和运行(假设它们完全正确地实现了该标准)。
这些接口被称为应用编程接口 。如果你曾经在浏览器中编写过脚本,你可能已经熟悉了最常见的 API 之一:文档对象模型(DOM )。DOM 是一个 API,用于访问当前在浏览器中呈现的文档。任何获取浏览器中元素引用的方法(如getElementById
)都是 DOM API 的一部分,任何访问事件模型的方法(如addEventListener
)也是如此。该浏览器还发布了用于访问内部浏览器功能(如浏览历史)的Navigator
API。另一个常用的 API 是XMLHttpRequest
构造函数,它是浏览器网络通信系统的一个接口,允许您与服务器进行异步通信。
注意术语 API 有相当广泛的应用,从库和框架到网络服务(包括许多可以用浏览器异步访问的 web 服务),甚至是单个应用中对象之间的内部接口。
在这一章中,我将介绍 HTML5 规范中定义的新 API。我将采用一种实用的方法来研究 API,重点关注特性的支持程度以及可以用它来做什么,并提供大量的例子来作为您自己的 web 应用的起点。
服务器发送的事件
维持价
混合
Internet Explorer 不支持此 API。所有其他浏览器都完全支持。
WHATWG 生活水平:http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#server-sent-events
W3C 草案:http://dev.w3.org/html5/eventsource/
假设您正在构建一个简单的股票行情应用。您有一个服务器资源,它发布了获取股票值所需的 API,所以很容易就可以开始了。但是如何更新股票价值呢?服务器如何让您的客户端应用知道股票的价值已经改变?
这可能是服务器发送事件的典型用例:服务器需要通知客户端发生了一些事情。因为 HTTP 作为一种标准只定义了无状态通信,因此只有客户机可以向服务器发起请求,如果客户机不首先请求,服务器就无法向客户机发送消息。服务器发送的事件是 HTML5 解决这个问题的方式之一,在从服务器到客户端的单向通信的特定情况下。
在过去,人们将简单的轮询计时器写入他们的脚本,这实质上是询问服务器“有什么新东西吗?”上一个定时器,如清单 3-1 所示。
注意本节中的例子需要从服务器上运行,而不是直接从文件系统中加载。
清单 3-1 。轮询脚本示例
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<script>
// This is a mock API that simply returns the same JSON structure
// every time the URL is requested. This JSON structure has a single
// property, isChanged, which is set to false.
var strUri = ’./example3-1.json’;
// This is how often we’ll query the mock API, in milliseconds.
var timerLength = 1000;
/**
* Fetch an update from a web service at the specified URL. Initiates an
* XMLHttpRequest to the service and attaches an event handler for the success
* state.
* @param {string} strUrl The URL of the target web service.
*/
function fetchUpdates(strUrl) {
// Create and configure a new XMLHttpRequest object.
var myXhr = new XMLHttpRequest();
myXhr.open("GET", strUrl);
// Register an event handler for the readyStateChange event published by
// XMLHttpRequest.
myXhr.onreadystatechange = function() {
// If readyState is 4, request is complete.
if (myXhr.readyState === 4) {
handleUpdates(myXhr.responseText);
}
};
// Everything is configured. Send the request.
myXhr.send();
}
/**
* Handle an update from the mock API. Parses the JSON returned by the API and
* looks for changes.
* @param {string} jsonString A JSON-formatted string.
*/
function handleUpdates(jsonString) {
// Parse the JSON string.
var jsonResponse = JSON.parse(jsonString);
if (jsonResponse.isChanged) {
// Handle changes here, probably by checking the structure to determine
// what changed and then route the change to the approprate part of the app.
console.log(’change reported.’);
} else {
console.log(’no changes reported.’);
}
}
// Everything is all set up: we have a function for fetching an update and a
// function to handle the results of an update query. Now we just have to kick
// off everything. Using setInterval, we will call our fetchUpdates method every
// timerLength milliseconds. We can cancel the timer by calling the
// cancelInterval(pollTimer) method.
var pollTimer = setInterval(fetchUpdates.bind(this, strUri), timerLength);
</script>
</body>
</html>
这个例子在开始轮询过程之前做了大量的设置。首先定义模拟 API 的 URL,这只是您在文件系统上创建的一个文件。您还可以定义轮询模拟 API 的频率。然后创建一个获取更新的函数。这是您将使用计时器调用的方法,它向模拟服务发起XMLHttpRequest
(简称 XHR)。XHR 将在与服务器通信时发布readyStateChange
事件。通过查看 XHR 对象的readyState
属性,您可以知道查询处于什么状态:仍在与服务器对话,还是已经结束对话,或者即使发生了错误。因此,向 XHR 对象添加一个事件处理程序,每次发生readyStateChange
事件时都会调用它。在这种情况下,您使用内联函数来保持代码简单,尽管您可以在代码块之外定义它并通过名称引用它。事件处理程序检查readyState
属性,如果它处于正确的状态,它就调用handleResponse
函数。该函数接受从模拟 API 获取的 JSON 格式的字符串,并对其进行相应的处理。
这是一个非常简单的例子,但是它演示了使用定时器定期轮询 web 服务的基本原理。使用方法setInterval
和cancelInterval
很容易启动和停止定时器。如果您的应用需要多个计时器,您可以构建一个构造函数,用方便的方法来创建、启动、停止、暂停和释放它们。您可以围绕创建和管理计时器快速构建大量代码。
如果你仔细想想,简单的计时器并不是处理这个问题的好方法。如果出现暂时的网络问题,并且对fetchUpdates
的一个调用花费了超过一秒钟的时间,该怎么办?在这种情况下,对fetchUpdates
的另一个调用将在第一个调用返回之前执行,导致对服务器的两个活动和挂起的调用。根据网络条件,第二个呼叫可能会在第一个呼叫之前返回。这种情况被称为竞争条件,,因为两个未决调用实质上是在竞争看哪一个先完成。
幸运的是,这种竞争状况很容易修复:与其让计时器不顾外部限制发出请求,不如改变handleUpdates
方法,这样它做的最后一件事就是调度对fetchUpdates
的下一次调用。这样,你就不会有一个以上的调用发生,并且消除了竞争条件。脚本会如清单 3-2 中的所示改变(周围的 HTML 保持不变)。
清单 3-2 。从清单 3-1 的示例中删除竞争条件
// This is a mock API that simply returns the same JSON structure
// every time the URL is requested. This JSON structure has a single
// property, isChanged, which is set to false.
var strUri = ’./example3-1.json’;
// This is how often we’ll query the mock API, in milliseconds.
var timerLength = 1000;
// Reference to the currently active timer.
var pollTimeout;
/**
* Fetch an update from a web service at the specified URL. Initiates an
* XMLHttpRequest to the service and attaches an event handler for the success
* state.
* @param {string} strUrl The URL of the target web service.
*/
function fetchUpdates(strUrl) {
// Create and configure a new XMLHttpRequest object.
var myXhr = new XMLHttpRequest();
myXhr.open("GET", strUrl);
// Register an event handler for the readyStateChange event published by
// XMLHttpRequest.
myXhr.onreadystatechange = function() {
// If readyState is 4, request is complete.
if (myXhr.readyState === 4) {
handleUpdates(myXhr.responseText);
}
};
// Everything is configured. Send the request.
myXhr.send();
}
/**
* Handle an update from the mock API. Parses the JSON returned by the API and
* looks for changes, and then initiates the next query to the mock service.
* @param {string} jsonString A JSON-formatted string.
*/
function handleUpdates(jsonString) {
// Parse the JSON string.
var jsonResponse = JSON.parse(jsonString);
if (jsonResponse.isChanged) {
// Handle changes here, probably by checking the structure to determine
// what changed and then route the change to the approprate part of the app.
console.log(’change reported.’);
} else {
console.log(’no changes reported.’);
}
// Schedule the next polling call.
pollTimeout = setTimeout(fetchUpdates.bind(this, strUri), timerLength);
}
// Initiate polling process.
fetchUpdates(strUri);
与前一版本相比,该脚本的变化以粗体显示。这个版本的代码消除了竞争条件,因为在任何给定的时间只有一个对fetchUpdates
的调用是活动的。但是,您现在可能会以不可预知的速度轮询服务器。
围绕这些问题进行编程也是可能的,但是处理好所有的边缘情况可能是困难的,并且您将会得到大量的代码,而这些代码仅仅是为了智能地处理对 web 服务的轮询。
理想情况下,这种通信应该是浏览器的一个特性,这就是新的服务器发送事件特性的作用。服务器发送事件让浏览器处理连接到服务器和轮询事件的所有细节,并让您留下基于计时器的轮询脚本和它们带来的所有问题。服务器发送的事件为您提供了一种打开到服务器的通道的方式,然后将事件侦听器附加到该通道,以处理服务器将发布的各种事件类型。浏览器会为您处理所有其他事情。
要使用服务器发送的事件,您不仅需要一个可以打开 web 服务通道并处理该通道上发生的事件的客户端;您还需要一个能够正确处理传入频道订阅并发布正确格式化的事件的服务器。
客户端设置
服务器发送的事件规范在全局上下文中定义了一个新的构造函数EventSource
,您可以使用它来创建到服务器的新连接:
var serverConnection = new EventSource(targetUrl);
构造函数返回一个EventSource
对象,它是一个连接的接口,浏览器将维护这个连接到由targetUrl
指定的服务器资源。浏览器将为您处理所有的连接维护和轮询—您所要做的就是监听来自服务器的事件。
当服务器向连接发布事件时,服务器资源将发布从EventSource
对象分派的事件。像任何 DOM 事件一样,您可以使用addEventListener
方法将事件处理程序附加到EventSource
对象。
默认情况下,EventSource
接口将发布三种事件类型:
open
:首次打开连接,网络通信初始化时发布。用于初始化连接。最多触发一次,如果浏览器无法建立到指定服务的连接,则根本不会触发。message
:服务器发送新消息时发布。error
:如果连接中出现错误(例如连接超时),则发布。
当一个事件被调度时,EventSource
将调用为该事件类型注册的事件处理函数。这个函数将被调用,使用一个event
对象作为参数,这个event
对象将有一个data
属性,包含从服务器发送的数据。清单 3-3 展示了如何创建一个新的EventSource
对象并给它附加事件监听器。
清单 3-3 。存根 EventSource 事件处理程序和订阅
/**
* Handles message events published by the EventSource.
* @param {EventSourceEvent} event
*/
function handleMessage(event) {
// Handle message.
console.log(’A message was sent from the server: ’, event.data);
}
/**
* Handles error events published by the EventSource.
* @param {EventSourceEvent} event
*/
function handleError(event) {
// Handle an error.
console.error(’An error happened on the EventSource: ’, event.data);
}
/**
* Handles an open event published by the EventSource.
* @param {EventSourceEvent} event
*/
Function handleOpen(event) {
// Handle the open event.
console.log(’The connection is now open.’);
}
// Create a new connection to the server.
var serverConnection = new EventSource(targetUrl);
// Attach event handlers.
serverConnection.addEventListener(’message’, handleMessage);
serverConnection.addEventListener(’error’, handleError);
serverConnection.addEventListener(’open’, handleOpen);
现在,每当由strUrl
指定的资源发布事件时,就会调用handleMessage
事件处理程序。如果连接出现错误,浏览器将发布一个error
事件,并调用handleError
事件处理程序。请注意,您可以配置您的服务器来发布自定义事件类型,并且可以用完全相同的方式为它们添加事件处理程序(请参见下一节“从服务器发送事件”)。
要关闭到服务器的连接,调用EventSource
对象上的close
方法:
serverConnection.close();
这将通知浏览器停止轮询服务器并关闭连接。然后,您可以将 EventSource 对象设置为null
以将其从内存中删除。一旦连接被关闭,就无法重新打开。
从服务器发送事件
为了让服务器发送的事件工作,服务器上需要有一个资源,它知道如何处理来自浏览器的传入轮询请求,以及如何根据需要正确发布事件。服务器资源可以用任何语言编写——Java、PHP、JavaScript、C++等等。资源必须用text/event-stream
MIME 类型来响应轮询请求。响应由多行key: value
对组成,由一个双换行结束。有效密钥如下:
data
:指定一行发送给客户端的任意数据,客户端将接收它作为event
对象的数据属性。event
:指定与此服务器发送的事件相关联的任意事件类型。这将导致一个同名的事件从活动的EventSource
对象中被调度,从而允许从服务器中触发open
、message
和error
之外的任意事件。如果未指定事件类型,该事件将仅触发EventSource
上的message
事件。id
:指定与事件序列关联的任意 ID。在事件流上设置一个 ID 使浏览器能够跟踪最后触发的事件,如果连接断开,它将向服务器发送一个last-event-ID
HTTP 头。retry
:指定在浏览器为下一个事件重新查询服务器之前的毫秒数。默认设置为3000
(三秒)。这使得服务器资源能够抑制浏览器查询并防止自己被淹没。
例如,服务器发送的基本事件如下所示:
data: arbitrary line of text\n\n
这个事件将触发相关联的EventSource
对象上的message
事件,并调用message
事件处理程序(假设已经注册了一个)。消息事件处理程序接收的event
对象将有一个data
属性,它将包含服务器发送的文本(在前面的例子中,它将是“任意文本行”)。
您也可以发送多行事件,只需用一个双换行来终止事件:
data: arbitrary line of text\n
data: another arbitrary line of text\n\n
在这种情况下,event.data
属性将被设置为“任意文本行\ n 另一个任意文本行”。事件数据可以是任何文本:HTML、CSS、XML、JSON 等等。
多个事件类型也可以包含在一个服务器发送的事件中。回到股票报价器的原始示例,这里有一个显示两只不同股票的更新的事件:
event: update\n
data: {\n
data: "ticker":"GOOG",\n
data: "newval":"559.89",\n
data: "change":"+0.05"\n
data: }\n
event: update\n
data: {\n
data: "ticker":"GOOGL"\n
data: "newval":"571.65"\n
data: "change":"+1.09"\n
data: }\n\n
这个服务器发送的事件将触发对象EventSource
上的两个update
事件。每次调用update
事件处理程序时,都会调用一个包含该事件数据的event
对象。第一个事件的数据将是以下 JSON 格式的文本:
{
"ticker":"GOOG",
"newval":"559.89",
"change":"+0.05"
}
第二个事件的数据是以下 JSON 格式的文本:
{
"ticker":"GOOGL",
"newval":"571.65",
"change":"+1.09"
}
原点限制
服务器发送的事件受制于相同的源策略,因此来自一个源的页面不能订阅来自另一个源的事件流。特别是,事件流通常发布在与标准网页不同的 TCP 端口上,因此,发布在标准端口(如端口 80)上的网页不可能订阅发布在不同端口上的事件流,即使它来自同一服务器。只有从与事件流相同的源提供服务的客户端才能访问该事件流。
如果您想使用服务器发送的事件,您将需要一个足够灵活的 web 服务器来服务基于 HTML 的客户端(及其所有依赖的资源,如 CSS、JavaScript、图像等)。)并发布事件流。这使得 PHP、JSP 或 ColdFusion 等服务器集成脚本语言成为构建依赖于服务器发送事件的应用服务器的主要候选语言,因为您可以用集成脚本语言编写事件流,并使用相同的 web 服务器为客户端提供服务。还可以很容易地配置大多数 web 服务器,将对特殊 URL 的请求路由到不同的资源,使得从同一服务器发布常规 web 内容和事件流成为可能。这种实现的细节超出了本书的范围,但是这是对服务器发送事件的一个重要限制。
在下面的例子中,您将选择一个更简单的解决方案:构建一个服务器,它可以在充当事件流的同时为客户端 HTML 文件提供服务。由于 HTML 客户机文件和事件流都来自同一个来源,所以订阅不会有问题。
安全性
就像任何处理网络通信的技术一样,在使用服务器发送的事件构建应用时,注意安全性是一个好主意。不要在未加密的连接上发送敏感信息(如密码),因为服务器事件是以纯文本形式发送的。如果你需要发送敏感信息,你至少应该使用 HTTPS。
如前所述,服务器发送的事件受到同源策略的限制,因此脚本不能从不同于自己的网络资源订阅事件流。此外,EventSource 事件处理程序接收的事件对象将有一个 origin 属性,您可以检查该属性以验证服务器事件是否来自您期望的来源,如清单 3-4 所示。
清单 3-4 。从服务器发送的事件中检查事件来源
// The EventSource object.
var serverConnection;
/**
* Handle an event published on an EventSource object.
* @param {EventSourceEvent} event
*/
function messageHandler(event) {
if (event.origin !== ’https://www.myexample.com’) {
// Something bad has happened, stop listening for events and surface a warning to the user.
serverConnection.close();
alert(’Warning: Server event received from wrong network resource.’);
return;
}
// Handle event here.
}
// Initiate subscription to event stream and register event handler.
serverConnection = new EventSource(targetUrl);
serverConnection.addEventListener(’message’, messageHandler);
在这个代码片段中,您将检查浏览器所报告的事件的来源。然而,这不是一个万无一失的检查,因为它可能会被欺骗,但是它是您可以添加到您的应用中的又一层安全性。
一个示例应用
要构建一个功能性的示例应用,您需要一个服务器资源,它可以以预期的格式发送事件,还可以为订阅事件流的客户端提供服务。如上所述,您可以使用任何语言来构建这个服务器资源,但是为了与本书中的其他示例保持一致,请使用 JavaScript。您可能习惯于在浏览器中使用 JavaScript。您也可以在服务器上使用它,就像任何其他脚本引擎一样。作为独立脚本引擎的 JavaScript 最流行的实现是 Node.js 框架,它已经被移植到多个操作系统。Node.js 框架提供了一个快速的 JavaScript 解释器和一个 API 框架,用于访问文件系统、网络堆栈和服务器上的其他资源。
提示如果你从未使用过 Node.js,你可以在
http://nodejs.org
了解更多。
要运行这个示例,您需要一个安装了 Node.js 的服务器。您将构建一个简单的脚本,既充当事件流,又服务于事件客户端。正如你在清单 3-5 中看到的,用 Node.js 构建一个简单的 HTTP 服务器非常容易。
清单 3-5 。用 JavaScript 编写的简单事件流服务器
// Include the modules needed to build the server.
var http = require(’http’);
var sys = require(’sys’);
var fs = require(’fs’);
// Use the http.createServer method to create a server listening on port 8030.
// The server will call the handleRequest method each time a request is
// received.
http.createServer(handleRequest).listen(8030);
/**
* Handle an incoming request from the server.
* @param {Object} request The request headers.
* @param {Object} resource A reference to the server resource that received
* the request.
*/
function handleRequest(request, resource) {
// Incoming requests to our server will be to one of two URLs.
// If the request is for /example3-5-events we should send our SSE.
// If the request is for /example3-5.html, we should serve the example client.
if (request.url == ’/example3-5-events’) {
// Initialize an event stream.
sendSSE(request, resource);
} else if (request.url == ’/example3-6.html’){
// Send the client.
resource.writeHead(200, {’Content-Type’: ’text/html’});
resource.write(fs.readFileSync(’example3-6.html’));
resource.end();
}
}
/**
* Initializes an event stream and starts sending an event every 5 seconds.
* @param {Object} request
* @param {Object} resource
*/
function sendSSE(request, resource) {
// Initialize the event stream.
resource.writeHead(200, {
’Content-Type’: ’text/event-stream’,
’Cache-Control’: ’no-cache’,
’Connection’: ’keep-alive’
});
// Send an event every 5 seconds.
setInterval(function() {
// Randomly generate either 0 or 1.
var randNumber = Math.floor(Math.random() * 2);
// If the random number is 1, set isChanged to true. Otherwise, set it to
// false.
var isChanged = (randNumber === 1) ? true : false;
resource.write(’data: ’ + ’{"isChanged":’ + isChanged + ’}\n\n’);
}, 5000);
}
如果你请求example3-6.html
,它将服务于 HTML 客户端(你将在清单 3-6 中定义),如果你请求example3-5-events
,它将启动一个事件流,每五秒将一个事件推送到客户端。该事件将是一个简单的 JSON 格式的字符串,带有一个随机设置为true
或false
的isChanged
属性。要运行此服务器,请使用以下命令:
node example3-5server.js
这个服务器的 HTML 客户端只需要将EventSource
初始化到正确的 URL,如清单 3-6 中的所示。
清单 3-6 。服务器发送的事件客户端
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
#changeme {
width: 300px;
height: 300px;
border: 1px solid black;
overflow: auto;
}
</style>
</head>
<body>
<h1>Server-sent Events Demonstration</h1>
<div id="changeme"></div>
<script>
// The URL for our event stream. Note that we are not specifying a domain or
// port, so they will default to the same ones used by the host script.
var strUri = ’/example3-5-events’;
// Get a reference to the DOM element we want to update.
var changeMe = document.getElementById(’changeme’);
// Create a new server-side event connection and register an event handler for
// the ’message’ event.
var serverConnection = new EventSource(strUri);
serverConnection.addEventListener(’message’, handleSSE, false);
/**
* Handles a server-sent event by parsing the JSON in the data and handling
* any changes.
* @param {EventSourceEvent} event The event object from the event source.
*/
function handleSSE(event) {
// Parse the JSON string.
var jsonResponse = JSON.parse(event.data);
// Create a new element to append to the DOM.
var newEl = document.createElement(’div’);
if (jsonResponse.isChanged) {
newEl.innerHTML = ’Change reported.’;
} else {
newEl.innerHTML = ’No changes reported.’;
}
// Append the new element to the DOM.
changeMe.appendChild(newEl);
}
</script>
</body>
</html>
这个客户端为事件流的 URL 启动一个新的EventSource
,然后为它附加一个消息事件处理程序。每次服务器发布事件时,都会调用消息事件处理程序,解析事件数据,并将结果附加到 DOM 中。这个客户端比您之前的轮询示例简单得多,因为现在所有的细节都由浏览器处理。不需要初始化一个XMLHttpRequest
对象,不需要管理你自己的定时器——你所要做的就是初始化一个EventSource
对象并注册事件处理程序。
服务器发送的事件只允许从服务器到浏览器的单向通信。关于全双工通信,请参见 WebSocket 章节。
WebSockets
维持价
好的
所有现代浏览器都支持 WebSockets。
WHATWG 生活水平:https://html.spec.whatwg.org/multipage/comms.html#network
W3C 草案:http://www.w3.org/TR/websockets/
WebSockets 通过在客户机和服务器之间提供全双工通信建立在服务器发送的事件上:不仅服务器可以向客户机发送任意信息,客户机也可以将任意信息传输回服务器。此外,WebSockets 不受同源策略的约束,这使得来自一个源的脚本无法与来自不同源的页面进行交互。
浏览器中的 WebSocket JavaScript API 在全局上下文中表现为一个新的 WebSocket 构造函数,它返回一个 WebSocket 对象:
var mySocket = new WebSocket(url, protocols);
url
参数指定 WebSocket 兼容服务的有效 URL。WebSockets 有自己的通信协议,不同于我们一直看到的超文本传输协议(HTTP )。WebSocket 协议由ws://
(标准 WebSocket 连接)或wss://
(安全 WebSocket 连接)指定。在构建 WebSocket 时,任何指定不同协议(如http
或https
)的尝试都会导致错误。
可选的protocols
参数可以是单个协议字符串,也可以是指定由服务器实现的一个或多个子协议的协议字符串数组。(本文中的协议是一组关于数据如何在客户端和服务器之间传输的规则。)该参数不改变浏览器用来创建和维护与服务器的连接的整体网络协议;相反,它允许您为通过该连接发送和接收信息指定可接受的数据格式。这使得单个服务器能够实现向客户端传输数据和从客户端接收数据的多种方式。例如,您可以实现一个服务器,该服务器同时实现服务器发送的事件协议(使用为该 API 指定的键/值对)和以文本形式发送 JSON 格式的字符串的协议。然后,您的客户端可以指定它期望的协议。如果未指定协议,服务器将不得不选择默认协议。如果指定的协议在服务器上不可用,服务器将拒绝连接。
连接到服务器:在 WebSocket 握手中
web 浏览器使用握手过程创建与服务的双向连接。当您使用构造函数创建一个新的 WebSocket 时,浏览器将立即向 URL 中指定的主机发送一个简单的GET
查询。该查询将包含指定浏览器试图创建 WebSocket 连接(与发出简单的 HTTP 请求相反)所需的所有标头。例如,假设你在example.com
访问网站,他们正在宣传他们新的基于 WebSocket 的聊天服务。(聊天服务是 WebSockets 的一个常见用例,因为它需要发送和接收消息。)单击链接登录聊天服务,应用启动。
在后台,浏览器将尝试连接到聊天服务。假设服务也托管在位于 URL ws://www.example.com/chat
的同一个域example.com
上,JavaScript 套接字创建可能看起来像这样:
var mySocket = new WebSocket(’ws://www.example.com/chat’, [’chat’, ’json’]);
得到的请求和它的头看起来像这样:
GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: upgrade
Origin: http://www.example.com
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: chat, json
Sec-WebSocket-Version: 13
第一行是一个简单的GET
请求。请求的“chat”部分是可选的,但是允许单个服务器发布许多 WebSocket 服务。
头部包含与服务器建立 WebSocket 连接所需的信息。具体来说,Sec-WebSocket-Key
头包含一个惟一的标识符,客户机希望服务器在它的响应中以特定的方式使用这个标识符。Sec-WebSocket-Protocol
头包含构造函数中指定的子协议。在这种情况下,客户端指定它知道’chat’
和’json’
协议。
服务器将获取头中的信息,并制定一个响应,可能如下所示:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Accept
头的值取决于客户端发送的Sec-WebSocket-Key
头的值。客户机知道期望从服务器得到一个特定的值,如果指定了一个不同的值(或者根本没有指定),客户机就知道服务器不能处理 WebSocket 连接。Sec-WebSocket-Protocol
报头表示服务选择了’chat’
协议进行通信。
注意web socket 协议在 RFC 6455“web socket 协议”中有定义,你可以在
https://tools.ietf.org/html/rfc6455
阅读。该文档指定了整个协议,包括处理Sec-WebSocket-Key
报头以创建Sec-WebSocket-Accept
值的细节。
握手完成后,客户端和服务器将使用特殊的数据帧协议相互通信。该协议允许客户端和服务器轻松地相互发送任意信息。
从服务器接收信息
在客户端,与服务器的交互是事件驱动的:当与服务器发生通信时,特定的事件被发布在相关联的 WebSocket 连接对象上。
error
:当 WebSocket 无法连接到服务器,或者失去连接时,在 connection 对象上发布一个error
事件。open
:当 WebSocket 第一次成功连接指定的服务时,在 connection 对象上发布open
事件。此事件表示套接字已准备好发送和接收数据。close
:当 WebSocket 关闭时,无论是由于一个错误还是因为客户端故意关闭了连接,都会在连接对象上发布一个close
事件。message
:当服务器通过连接发送信息时,浏览器会在连接对象上发布消息事件。该事件将包含从服务器传输的数据。
清单 3-7 使用假想的聊天服务展示了这些事件的一些存根事件处理程序。
清单 3-7 。Web 套接字的存根事件处理程序
// Create a new WebSocket connection to the chat service.
var chatUrl = ’ws://www.fgjkjk4994sdjk.com/chat’;
var validProtocols = [’chat’, ’json’];
var chatSocket = new WebSocket(chatUrl, validProtocols);
/**
* Handles an error event on the chat socket object.
*/
function handleError() {
console.log(’An error occurred on the chat connection.’);
}
/**
* Handles a close event on the chat socket object.
* @param {CloseEvent} event The close event object.
*/
function handleClose(event) {
console.log(’The chat connection was closed because ’, event.reason);
}
/**
* Handles an open event on the chat socket object.
* @param {OpenEvent} event The open event object.
*/
function handleOpen(event) {
console.log(’The chat connection is open.’);
}
/**
* Handles a message event on the chat socket object.
* @param {MessageEvent} event The message event object.
*/
function handleMessage(event) {
console.log(’A message event has been sent.’);
// The event object contains the data that was transmitted from the server.
// That data is encoded either using the chat protocol or the json protocol,
// so we need to determine which protocol is being used.
if (chatSocket.protocol === validProtocols[0]) {
console.log(’The chat protocol is active.’);
console.log(’The data the server transmitted is: ’, event.data);
// etc...
} else {
console.log(’The json protocol is active.’);
console.log(’The data the server transmitted is: ’, event.data);
// etc...
}
// Register the event handlers on the chat socket.
chatSocket.addEventListener(’error’, handleError);
chatSocket.addEventListener(’close’, handleClose);
chatSocket.addEventListener(’open’, handleOpen);
chatSocket.addEventListener(’message’, handleMessage);
在本例中,您创建了一组简单的事件处理程序,每种事件类型一个,然后在 connection 对象上注册这些处理程序。如果你愿意,你可以运行这个例子。域fgjkjk4994sdjk.com
不存在,所以首先浏览器会在连接上发布一个error
事件,然后是一个close
事件。在控制台中,你会看到类似于图 3-1 的东西。
图 3-1 。在 Chrome 中运行清单 3-7 的结果
在关闭的事件处理程序handleClose
中,检查event
对象的reason
属性,查看是否指定了关闭的原因。该属性可能存在,也可能不存在,具体取决于发生的错误和为连接指定的子协议。
handleMessage
事件处理程序非常简单,但是演示了如何检查活动的子协议,以及如何访问服务器传输的数据。我们习惯于通过 HTTP 进行简单的基于文本的通信(就像服务器发送的事件一样),但是 WebSockets 也可以传输二进制数据。您可以通过 WebSocket 传输任意二进制数据;例如,您可以发送和接收图像。
在 JavaScript 中,使用二进制大型对象 (也称为blob)或数组缓冲区来表示二进制数据。这两种都是 JavaScript 中的有效数据类型:Blob
表示 blobs,ArrayBuffer
表示数组缓冲区。这两种类型的区别在于数据的使用方式。如果你正在处理一个永远不会改变的原始数据块(比如一幅图像),那么最好用一个Blob
来表示。如果您需要处理数据(查看数据的一部分,或者甚至修改它),最好使用ArrayBuffer
来表示。这两者都是 JavaScript 中的数据类型,所以很容易检查从服务器传输的信息是否是这种格式。下面是对handleMessage
事件处理程序的更新,演示了对Blobs
和ArrayBuffers
的检查:
/**
* Handles a message event on the chat socket object.
* @param {MessageEvent} event The message event object.
*/
function handleMessage(event) {
console.log(’A message event has been sent.’);
// The event object contains the data that was transmitted from the server.
// That data is encoded either using the chat protocol or the json protocol,
// so we need to determine which protocol is being used.
if (chatSocket.protocol === validProtocols[0]) {
console.log(’The chat protocol is active.’);
// Check the data type of the incoming data.
if (event.data instanceof Blob) {
console.log(’The data is a Blob.’);
}
if (event.data instanceof ArrayBuffer) {
console.log(’The data is an ArrayBuffer.’);
}
console.log(’The data the server transmitted is: ’, event.data);
// etc...
} else {
console.log(’The json protocol is active.’);
console.log(’The data the server transmitted is: ’, event.data);
// etc...
}
}
这里,您在chat
子协议部分添加了检查,以确定数据是 Blob 还是ArrayBuffer
。您也可以在json
子协议部分进行类似的检查;可以用 JSON 格式编码Blobs
和ArrayBuffers
(通常使用 Base64 编码)。
注意关于处理斑点的更多细节,见
https://developer.mozilla.org/en-US/docs/Web/API/Blob
,关于使用数组缓冲区的更多细节,见https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
。
向服务器发送信息
您也可以使用 WebSocket 将信息传输到服务器。每个 WebSocket 连接对象都有一个send
方法,您可以使用它将信息立即传输到服务器。您应该确保只在触发了open
事件之后发送信息,否则信息可能会丢失。因此,您经常会在 WebSocket 应用中看到一种模式,其中open
事件处理程序启动应用的初始化,以保证在连接可用之前不会发生任何事情。
send
方法只有一个参数:要发送的数据,如下所示:
var mySocket = new WebSocket(url);
mySocket.send(’hello world!’);
有效的数据类型有Strings
、Blobs
和ArrayBuffers
。
关闭连接
connection 对象有一个close
方法,可以用来在您完成连接时关闭与服务器的连接。调用这个方法将立即导致客户端向服务器发送一个close
请求,这又将关闭连接。然后,客户端将在连接对象上调度一个close
事件。无法重新打开已关闭的 WebSocket 连接对象。
一个示例 WebSocket 应用
与服务器发送的事件一样,您将需要一个能够处理 WebSocket 连接的服务器,以便构建一个正常运行的示例。从头开始构建这样的服务器是一项相当困难的任务,因为服务器必须知道如何将 HTTP 连接升级到 WebSocket 连接,以及如何根据 WebSocket 协议发送和接收数据。幸运的是,您可能不需要从头开始构建一个。有许多开源项目致力于创建可以在项目中使用的 WebSocket 服务器。对于示例 WebSocket 应用,您将使用 Node.js 构建一个简单的 WebSocket 服务器,这是一个用于服务器的 JavaScript 框架。Node.js 提供了一个快速的 JavaScript 解释器以及用于访问文件系统、网络堆栈和其他服务器技术的库。
您将使用 WebSocket-Node,而不是从头开始构建服务器,这是 Node.js 中 WebSocket 协议的开源实现。要安装该模块,请使用节点程序包管理器 npm:
npm install websocket
这将为您安装模块。如果没有,请查看项目主页上的安装说明。
一旦安装了 WebSocket-Node,您就可以使用它来为您的示例构建一个服务器。最简单的 WebSocket 例子是一个服务器,它简单地回显客户机发送的任何内容。清单 3-8 显示了使用 WebSocket-Node 库构建一个服务器是多么简单。
清单 3-8 。一个简单的 WebSocket 服务器
// Include the modules needed to build the server.
var WebSocketServer = require(’websocket’).server;
var http = require(’http’);
var currentConnection;
// Define the subprotocol name for the WebSocket connection.
var subProtocol = ’echo’;
/**
* Handles a request event on the WebSocket server.
* @param {Object} request
*/
function handleRequest(request) {
currentConnection = request.accept(subProtocol, request.origin);
currentConnection.on(’message’, handleMessage);
}
/**
* Handles a message event on a socket connection.
* @param {Object} message The message event object.
*/
function handleMessage(message) {
// Echo back whatever was received.
if (message.type === ’utf8’) {
currentConnection.sendUTF(message.utf8Data);
} else if (message.type === ’binary’) {
currentConnection.sendBytes(message.binaryData);
}
}
// Create a simple server that always returns 404 (not found) to any request.
// (We’re only going to use it to upgrade to the WebSocket protocol.)
var simpleServer = http.createServer(function(request, response) {
response.writeHead(404);
response.end();
});
simpleServer.listen(8080);
// Create a WebSocket server based on the simple server.
var socketServer = new WebSocketServer({
httpServer: simpleServer,
autoAcceptConnections: false
});
// Register the request event handler.
socketServer.on(’request’, handleRequest);
这个例子基于一个简单的 HTTP 服务器创建了一个 WebSocket 服务器。每当 socket 服务器收到连接请求时,就会在该对象上调度一个request
事件。在handleRequest
事件处理程序中,通过接受请求建立一个 WebSocket 连接,然后为message
事件注册一个事件处理程序。每当客户端向服务器发送消息时,就会在连接对象上调度一个message
事件。您的handleMessage
事件处理程序只是简单地回显接收到的任何数据。
将该脚本保存在名为example3-8-server.js
的文件中。要运行它,输入node example3-8-server.js
。
提示如果不想自己搭建服务器,有一个简单的 echo 服务器运行在
ws://echo.websocket.org
。使用它作为 URL,不要指定协议。请注意,如果您以这种方式运行该示例,它将证明 WebSocket 没有绑定到单一来源策略,因为原始页面将从本地服务器提供服务,但是 web socket 服务器在完全不同的域上。
接下来,您需要构建一个可以利用服务器的客户机。您的客户端应该尝试发送各种类型的数据,并显示从服务器返回的任何内容。清单 3-9 显示了一个连接到你的服务器并运行一系列测试的客户端。
清单 3-9 。一个 WebSocket 演示类
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>WebSockets Demonstration</h1>
<div id="display"></div>
<script>
function WebSocketDemo() {
/**
* The URL for the WebSocket server. There is a simple echo server running at
* ws://echo.websocket.org/ if you don’t have a local server running.
* @private {string}
*/
this.demoUrl_ = ’ws://localhost:8080/’;
/**
* The protocol used by the server. If using the server at echo.websocket.org
* set this to null, as it does not have a protocol.
* @private {string|Array<string>}
*/
this.subProtocol_ = ’echo’;
/**
* @private {WebSocket}
*/
this.demoSocket_ = null;
/**
* A reference to the DOM element to use for displaying messages.
* @private {HtmlElement}
*/
this.display = document.getElementById(’display’);
/**
* Displays a message on the page.
* @param {string} messageText A simple string of text.
* @param {*=} opt_messageData An optional set of data to append to the text
* string.
* @private
*/
this.displayMessage_ = function(messageText, opt_messageData) {
var messageData = opt_messageData ? opt_messageData : ’’;
var newParagraph = document.createElement(’p’);
newParagraph.innerText = messageText + messageData;
this.display.appendChild(newParagraph);
};
/**
* Handles an error event on the demo socket object.
* @private
*/
this.handleError_ = function() {
this.displayMessage_(’An error occurred on the demo connection.’);
};
/**
* Handles a close event on the demo socket object.
* @private
*/
this.handleClose_ = function() {
this.displayMessage_(’The demo connection was closed.’);
};
/**
* Handles an open event on the demo socket object.
* @private
*/
this.handleOpen_ = function() {
this.displayMessage_(’The demo connection is open.’);
// Now that the socket is open, we can send data.
this.sendDataAndClose_();
};
/**
* Handles a message event on the demo socket object.
* @param {MessageEvent} event The message event object.
* @private
*/
this.handleMessage_ = function(event) {
this.displayMessage_(’A message event has been received from the server.’);
// Check the data type of the incoming data.
if (event.data instanceof Blob) {
this.displayMessage_(’The data is a blob.’);
}
if (event.data instanceof ArrayBuffer) {
this.displayMessage_(’The data is an ArrayBuffer.’);
}
this.displayMessage_(’The data the server transmitted is: ’, event.data);
};
/**
* Initializes the demo by creating a new connection and registering event
* handlers.
* @private
*/
this.initDemo_ = function() {
// Open the socket.
this.demoSocket_ = new WebSocket(this.demoUrl_, this.subProtocol_);
// Register the event handlers on the demo socket.
this.demoSocket_.addEventListener(’error’, this.handleError_.bind(this));
this.demoSocket_.addEventListener(’open’, this.handleOpen_.bind(this));
this.demoSocket_.addEventListener(’message’,
this.handleMessage_.bind(this));
this.demoSocket_.addEventListener(’close’, this.handleClose_.bind(this));
};
/**
* Sends data to the server, then closes the socket.
* @private
*/
this.sendDataAndClose_ = function() {
// Send a text string.
this.demoSocket_.send(’Hello world!’);
// Send a JSON-formatted string.
var testObject = {
message: ’hello world’,
active: true
};
var testObjectString = JSON.stringify(testObject);
this.demoSocket_.send(testObjectString);
// Send a Blob.
var testBlob = new Blob([’some data’]);
this.demoSocket_.send(testBlob);
// Done! Demo over. Close the socket after waiting for a few seconds for
// all of the messages to be sent and received. You might need to adjust
// this depending on the speed of your connection.
setTimeout(function() {
this.demoSocket_.close();
}.bind(this), 5000);
};
/**
* Runs the demonstration.
*/
this.run = function() {
this.initDemo_();
};
}
// Create the demo and run it.
var myDemo = new WebSocketDemo();
myDemo.run();
</script>
</body>
</html>
在本例中,您已经将演示封装在一个类构造函数中。尽管您只运行了一次演示,但这是在构建这样的复杂连接时可以遵循的一个好模式,因为它有助于封装它们的功能。这也意味着,如果您愿意,可以很容易地一次实例化多个连接。
以前在全局范围内的所有函数或变量都被移到了这个类中。您还添加了一个新方法sendDataAndClose_
,它演示了发送各种类型的数据,然后在五秒钟的延迟后关闭连接。open
事件处理程序调用sendDataAndClose_
,所以除非连接就绪,否则不会发送任何数据。服务器发送的任何内容都将显示在页面上。
运行这个例子,它将产生一个类似于图 3-2 中截图的结果。
图 3-2 。运行的结果清单 3-9
您还可以通过为this.subProtocol_
指定无效的子协议值,在 WebSocket 连接中引发错误:
/**
* The protocol used by the server. If using the server at echo.websocket.org
* set this to null, as it does not have a protocol.
* @private {string|Array<string>}
*/
this.subProtocol_ = ’invalid protocol’;
WebSocket 服务器将不执行升级,套接字连接将失败。
跨文档信息/网络信息
维持价
好的
所有现代浏览器都支持这些功能。
WHATWG 生活水平:http://www.whatwg.org/specs/web-apps/current-work/multipage/web-messaging.html#crossDocumentMessages
W3C 草案:http://www.w3.org/TR/webmessaging/
当 web 浏览器制造商开始采用 JavaScript 时,人们很快意识到安全性将是一个重要的问题。早期,Netscape 引入了同源策略,它规定一个脚本只能访问与自己同源的 DOM 内容。如果没有这个策略,来自任何域的恶意脚本都可以在您的浏览器上运行,然后读取并修改浏览器可以访问的所有数据:呈现的页面、历史记录、cookies,甚至保存的密码。
同源策略没有明确的标准,但它主要基于 RFC 6454 “网络起源概念”(你可以在http://tools.ietf.org/html/rfc6454
阅读这个 RFC。)粗略地说,如果两个资源的协议(HTTP,HTTPS)、主机(例如,www.example.com
)和端口(默认为端口 80)都匹配,则这两个资源具有相同的来源。
注意 Internet Explorer 在其来源确定中不包括端口。相反,它使用资源所属的安全区域。
同源策略是 web 应用安全性的基石,它被浏览器严格执行。不幸的是,这给构建利用不同域甚至子域上的多种资源的 web 应用带来了困难(例如,www.example.com
将具有与services.example.com
不同的来源,尽管它们都具有相同的根域example.com
)。因此,web 开发人员创造了许多不同的变通办法,有些比其他的更为粗糙。
Web 消息传递,也称为跨文档消息传递,是 HTML5 提供在同源策略内工作的安全方法的方式之一,同时允许来自不同源的资源之间的安全通信。具体来说,该特性允许一个框架中的脚本使用可以随意触发的事件与另一个框架中的脚本进行通信。
规范在浏览器的window
对象上创建了新方法postMessage
。您使用这个方法向目标框架发送一条消息,这又导致在那个window
中触发一个message
事件。目标框架中的事件处理程序可以捕获事件并接收消息。
postMessage
方法有两个参数:
message
:您想要传输到目标框架的信息。origin
:您期望目标框架中资源具有的原点。如果目标框架中的资源没有指定的原点,则不会调度该事件。
目标框架在收到消息时会调度一个message
事件。得到的event
对象将有两个重要的属性:
Event.data
:该属性将包含从另一个窗口发送的消息。Event.source
:该属性包含发送窗口的原点。您应该始终仔细检查消息来源的来源,以防止意外捕获和处理来自意外(可能是恶意)来源的事件。
要创建一个例子,你需要两个页面,你应该称之为主页面(清单 3-10 )和目标页面(清单 3-11 )。主页将包含一个加载目标页面的 iframe。
清单 3-10 。跨域消息传递,主页面
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Cross-Domain Messaging</h1>
<iframe src="example3-8.html" id="targetFrame"></iframe>
<p><button id="clickme">Click to send a message to the iframe.</button></p>
<script>
// Message to send to the target window.
var strMessage = "This is a message sent from the main window.";
// Reference to the button.
var clickme = document.getElementById("clickme");
// Reference to the target frame.
var targetFrame = document.getElementById("targetFrame");
// Add a click event handler to the button.
clickme.addEventListener("click", function() {
// Send a message to the target frame.
targetFrame.contentWindow.postMessage(strMessage, "*");
});
/**
* Handle a cross domain message.
* @param {Event} event The event object from the cross domain message.
*/
function handleMessage(event) {
// Create a message and show it to the user using an alert.
var strAlert = "Message event in the main window!\nThe message was:\n";
strAlert += event.data;
alert(strAlert);
}
// Register the handleMessage event handler on the window.
window.addEventListener("message", handleMessage, false);
</script>
</body>
</html>
清单 3-11 。跨域消息传递,目标页面
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Target iframe</h1>
<script>
/**
* Handles message events dispatched to this window.
* @param {Event} event The event object from the cross domain message.
*/
function handleMessage(event) {
// Create a message and show it to the user using an alert.
var strAlert = "Message event in target iframe!\nThe message was:\n";
strAlert += event.data;
alert(strAlert);
// Post a message back to the parent frame.
window.top.postMessage("This is a message from the target iframe.", "*");
}
// Register the message event handler on the window.
window.addEventListener("message", handleMessage, false);
</script>
</body>
</html>
若要运行此示例,请将两个页面保存在同一目录中。将目标页面保存在名称“example3-8.html”下。当您将主页加载到浏览器中时,它会将目标页面加载到 iframe 中。通过单击文本“单击向 iframe 发送消息”来运行示例
加载时,两个文档都将message
事件处理程序绑定到它们的window
对象。当您单击按钮时,父文档使用postMessage
向 iframe 发送一条消息。这触发了 iframe 中的一个message
事件,该事件由handleMessage
事件处理器处理。这将向事件数据发出警报,然后向父文档发回一条消息。这又触发了父窗口中的message
事件,该事件调用那里的handleMessage
事件处理程序,并导致第二个警告发生。
在这个例子中,您没有利用这个特性的主要目的,即从不同来源的资源发送消息。如果你有不止一个来源,我鼓励你尝试这个例子。将页面上传到不同的源(确保相应地改变 iframe 的 URL ),看看结果是否如预期的那样工作。
注意,在这个例子中,您没有在对postMessage
的调用中指定目标原点,也没有在事件处理程序中检查原点。这本身就不安全,我强烈建议不要在产品代码中这样做。您在这里这样做只是因为这是一个示例,具体的域信息将根据您提供文件的方式而有所不同。我鼓励您修改这些脚本,以便它们正确地指定目标源,并根据您的特定环境检查源源。此外,尝试将它们设置为不同的值,以引起原点冲突,这样您就可以自己看到结果。
网络存储
维持价
优秀
所有现代浏览器都支持这些特性,并且在最近的三个版本中都有。
WHATWG 生活水平:http://www.whatwg.org/specs/web-apps/current-work/multipage/webstorage.html
W3C 草案:http://www.w3.org/TR/webstorage/
我已经提到过超文本传输协议是无状态的。这在一定程度上意味着服务器独立于其他请求处理来自客户端的每个请求。因此,没有内置的机制来跨网页加载或重新加载维护数据。例如,如果应用的第一页是一个登录表单,并且用户成功登录,那么当用户在站点的其余部分导航时,就没有机制来维护该会话。或者,如果您正在构建一个购物车应用,就没有办法将用户的选择从一个页面传递到另一个页面。
当然,这导致了糟糕的用户交互,所以在 1995 年——在网络历史的早期——网景公司的雇员 Lou Montulli 创建了一个规范,允许小块数据在浏览器和服务器之间使用特殊的 HTTP 请求进行通信。这些片段将存储在客户机中,但是服务器可以根据需要请求它们。Montulli 将这些小块数据称为“神奇的 Cookie”,这就是术语 HTTP Cookie 的由来。Cookies 支持 web 上的有状态通信,并迅速成为 web 应用的基石。然而,HTTP cookie 有点笨重。它们的大小非常有限(在大多数浏览器中为 4KB ),并且很难用 JavaScript 直接管理。
HTML5 引入了 Web 存储的概念,作为 HTTP Cookies 的替代。Web 存储允许在浏览器中存储更多的数据(在除 Internet Explorer 之外的所有浏览器中,每个源最多 5MB,Internet Explorer 允许每个源 10MB)。Web 存储也有一个非常简单的键/值 API,使得它很容易与 JavaScript 一起使用。与 HTTP Cookies 不同,Web 存储完全由浏览器控制,服务器不能直接访问内容。如果您想与服务器共享 Web 存储数据,您的脚本必须将数据传输到服务器。
Web 存储定义了两种不同形式的存储:会话存储和本地存储。顾名思义,会话存储只存储当前浏览器会话的数据。当用户关闭浏览器时,会话存储的内容将被清除。另一方面,本地存储是永久性的。一旦您将数据存储在本地存储中,即使用户关闭浏览器或重启计算机或设备,它也会一直保存在本地存储中,直到您将其删除。
像 HTTP Cookies 一样,Web 存储也受来源的限制。来自给定来源的脚本只能访问该来源的 Web 存储。不允许跨来源访问。与 HTTP Cookies 不同,您不能为 Web 存储数据设置到期日期或指定路径。
方法和语法
Web 存储在全局上下文中定义了两个新对象:sessionStorage
和localStorage
。他们都有相同的方法:
getItem(key)
:返回与指定键相关的数据。removeItem(key)
:删除与指定键相关的数据。setItem(key, data)
:用指定的键将数据存储在存储器中。clear()
:清除所有内容的存储。
使用网络存储非常简单,如清单 3-12 所示。
清单 3-12 。使用 Web 存储
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>localStorage Example</h1>
<script>
// Check to see if we’ve visited this page before.
var myValue = localStorage.getItem("test");
if (myValue == null) {
alert(’This is the first time you loaded this page! Now reload this page.’);
localStorage.setItem("test", "true");
} else {
alert(’You have loaded this page before!’);
}
</script>
</body>
</html>
这个例子首先测试你以前是否访问过这个页面。如果没有,它会在本地存储中存储一些数据。当您重新加载页面时,数据应该仍然存在。
Web 存储的一个主要限制是它只能存储原语(文本、数字和布尔值)。更复杂的数据如数组或对象不能存储在 Web 存储器中。但是,如果需要的数据可以被格式化为 JSON 字符串,那么它可以被序列化,然后被存储。检索之后,可以解析 JSON 字符串,并恢复数据结构以供使用。编写函数来处理这一点并不困难:
/**
* Serializes and stores an object in session storage under the specified key.
* @param {string} key The key to store the data under.
* @param {Object} value The object to serialize and store.
*/
function setSessionObject(key, value) {
sessionStorage.setItem(key, JSON.stringify(value));
}
/**
* Retrieves, deserializes, and returns the object stored in session
* storage under the specified key.
* @param {string} key The key that the object was stored under.
* @return {Object} The restored object.
* /
function getSessionObject(key) {
var value = sessionStorage.getItem(key);
return value && JSON.parse(value);
}
这里的setSessionObject
包装了sessionStorage.setItem
方法,而getSessionObject
方法包装了sessionStorage.getItem
方法。您也可以很容易地为localStorage
创建类似的函数。但是如果sessionStorage
和localStorage
都有getObject
和setObject
方法,而不必使用单独的函数,这不是很好吗?幸运的是,由于 JavaScript 及其继承模型的可扩展性,这很容易做到。
在不详细研究原型继承的情况下,这里有个秘密:sessionStorage
和localStorage
对象都继承自Storage
抽象对象。这意味着原型对象上对Storage
可用的任何方法对sessionStorage
和localStorage
都可用。所以你所要做的就是把你的新方法添加到Storage
:
/**
* Serializes and stores an object in web storage under the specified key.
* @param {string} key The key to store the data under.
* @param {Object} value The object to serialize and store.
*/
Storage.prototype.setObject = function(key, value) {
this.setItem(key, JSON.stringify(value));
};
/**
* Retrieves, deserializes, and returns the object stored in web storage under
* the specified key.
* @param {string} key The key that the object was stored under.
* @return {Object} The restored object.
*/
Storage.prototype.getObject = function(key) {
var value = this.getItem(key);
return value && JSON.parse(value);
};
同样,不要太在意语法,只要记住sessionStorage
和localStorage
都是Storage
的“孩子”,所以你对Storage
所做的任何改进也可以用于它的孩子。
清单 3-13 展示了新方法的使用。
清单 3-13 。使用自定义存储方法
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Web Storage Example</h1>
<script>
/**
* Serializes and stores an object in web storage under the specified key.
* @param {string} key The key to store the data under.
* @param {Object} value The object to serialize and store.
*/
Storage.prototype.setObject = function(key, value) {
this.setItem(key, JSON.stringify(value));
};
/**
* Retrieves, deserializes, and returns the object stored in web storage under
* the specified key.
* @param {string} key The key that the object was stored under.
* @return {Object} The restored object.
*/
Storage.prototype.getObject = function(key) {
var value = this.getItem(key);
return value && JSON.parse(value);
};
// Create a simple test object.
var myObject = {
test: true
};
// Create a simple test array.
var myArray = [1, ’two’, true];
// Check session storage for the stored data.
if (sessionStorage.getItem(’myObject’) == null) {
// First time here, so store the data.
sessionStorage.setObject(’myObject’, myObject);
sessionStorage.setObject(’myArray’, myArray);
alert(’Data stored. Reload the page to validate.’);
} else {
// We have been here before. Get values from storage and test them.
var newObject = sessionStorage.getObject(’myObject’);
var newArray = sessionStorage.getObject(’myArray’);
alert(myObject.test === newObject.test); // should alert true.
alert(myArray[1] === newArray[1]); // should alert true.
}
</script>
</body>
</html>
这个例子的第一步是用你的新方法扩展Storage
。接下来,创建一个对象和一个数组来测试新方法。当您加载页面时,它检查存储的值,如果不存在,它使用您的新方法存储对象和数组。如果有,它会用你的新方法得到它们,然后测试看值是否相同。
隐私和网络存储
在浏览器中存储大量永久数据的可能性引发了严重的隐私问题。尽管 Web 存储受单一来源策略的限制,但可用于设置第三方 HTTP Cookies 的相同技术也可用于设置第三方 Web 存储数据。虽然所有浏览器都为用户提供了对他们存储的 HTTP Cookies 的大量控制,但大多数浏览器还没有将这些功能扩展到 Web 存储。事实上,网络存储是用来创建所谓的 Evercookies 的方法之一(你可以在http://samy.pl/evercookie/
阅读关于 Evercookies 的内容)。
与 HTTP Cookies 一样,您不应该认为 Web 存储是安全的。不要在 Web 存储器中存储任何敏感信息,如密码。
大多数浏览器都实现了某种形式的隐私浏览。没有标准规定“隐私浏览”需要什么,但在大多数情况下,这意味着所有客户端存储仅限于当前会话。这包括网络存储。如果用户正在使用浏览器的隐私浏览功能,当用户关闭该标签时,使用 Web 存储器存储的任何数据都将被清除,即使您使用localStorage
存储了这些数据。因此,不能保证当用户下次返回你的应用时,你存储在localStorage
中的内容还会在那里,所以如果你的应用严重依赖于localStorage
,请记住这一点。
拖放
维持价
优秀
所有现代浏览器都支持这些特性,并且至少在最近的三个版本中都支持。
WHATWG 生活水平:http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html
W3C 草案:http://www.w3.org/TR/html5/editing.html#dnd
图形界面中最常见的交互之一是将元素从一个地方拖放到另一个地方。不幸的是,没有简单的方法来实现这种与 HTML 和 JavaScript 的基本交互。您可以这样做,但是这非常困难,需要大量的脚本,或者使用现有的库,比如 jQuery UI(参见http://jqueryui.com/draggable/
中的例子)。
现在,HTML5 规范为浏览器带来了原生的拖放交互。该流程由事件驱动,遵循以下简单步骤:
- 将一个或多个对象声明为可拖动的,并附加所需的事件处理程序。
- 将
drop
事件处理程序附加到目标元素。 - 当用户拖动项目并将它们放到目标上时,会触发各种事件并调用您的处理程序。
该规范包括几个新事件、一个用于 HTML 元素的draggable
属性和一个用于在事件间安全通信的dataTransfer
对象。
通常有两个原因可以解释为什么你想要在你的应用中构建一个拖放交互。最简单的(也可能是最常见的)是当拖放操作表示应用将要执行的另一个过程时。在这种情况下,被拖动的项目和它们被放置的目标本身并不重要——它们只是用户界面中的元素。根据拖放操作的结果,幕后还会发生其他事情。这方面的一个例子是购物车系统的界面。您将页面中的商品拖到购物车中,但是您正在拖动的东西和目标购物车只是任意的 HTML 元素,它们的样式已经被用户识别。在幕后,数据是基于拖放操作进行操作的:购物车的数据结构随着商品的添加或删除而改变,等等。
另一种情况是被拖动的项目和/或它们被放置的目标本身很重要。在这种情况下,被拖动的内容实际上很重要,因为它本身会被处理。这方面的一个例子是可视剪贴板,您可以在其中突出显示文档中的文本,然后将其拖动到剪贴板。文本实际上是通过拖放过程从 DOM 中的一个地方转移到另一个地方的。
HTML5 拖放规范很容易处理这两种情况,我将在我的示例中展示这两种情况。在深入研究这些之前,先详细了解一下这个过程。
draggable
属性
拖放规范的第一个组件是新的draggable
属性。这个属性被设置在 DOM 中任何你想拖动的 HTML 元素上。如果一个元素的draggable
属性设置为true
,如果用户在指针位于该元素上时按住鼠标按钮,然后移动指针,浏览器将从该元素启动一个拖放序列。
可以将draggable
属性设置为true
(表示该项可拖动)、false
(表示该项不能用于启动拖动序列)或auto
(表示应用浏览器的默认规则)。
例外情况是 DOM 中任何地方的选定文本,包括表单字段,如input
和textarea
字段。选定的文本总是可以启动拖动序列。
拖放事件
有几个新的拖放事件:
dragstart
:从被拖动的元素调度。dragenter
:当一个可拖动的项目被拖动到任何元素中时,从该元素调度。dragover
:从任何一个元素连续调度,只要一个可拖动的项目在它上面。请注意,无论可拖动项是否在移动,此事件都会连续触发。dragleave
:当一个可拖动的项目离开它的边界时,从一个元素调度。drag
:从整个拖动序列中被拖动的元素调度。像dragover
一样,不管指针是否被移动,这个事件都会被连续触发。dragend
:从鼠标释放时被拖动的元素调度。drop
:当用户通过释放鼠标按钮将可拖动的项目放到元素上时,从元素调度。
像其他 DOM 事件一样,您可以将拖放事件的事件处理程序添加到任何所需的元素中。但是请记住,拖放事件只会在拖动序列正在进行时触发。
拖放事件的一个重要特点是如何指定放置目标。HTML5 规范定义了一个dropzone
属性作为 draggable 属性的对应物。dropzone
属性应该指出哪些元素是有效的放置目标。dropzone
属性没有被广泛实现,所以你必须通过操作事件来指示有效的拖放目标。
一般来说,DOM 中的大多数元素不应该是有效的放置目标,所以dragover
事件的默认动作是取消放置。因此,为了指示一个有效的拖放目标,您必须通过调用事件处理程序中的event
对象上的preventDefault()
方法来取消dragover
事件的默认动作。
dataTransfer
对象
拖放拼图的最后一块是dataTransfer
对象。所有的拖放事件都可以用标准的事件处理程序来处理,这些事件处理程序将接收一个event
对象作为参数。拖放的event
对象的属性之一是dataTransfer
对象。该对象用于控制拖放助手的外观(在拖放操作中跟随光标的幻影视觉元素),指示拖放过程正在做什么,并轻松地将数据从dragstart
事件传输到drop
事件。
dataTransfer
对象有以下方法:
Event.dataTransfer.addElement(HtmlElement)
:指定拖动序列的源元素。这会影响到drag
和dragend
事件的触发位置。通常情况下,您可能不需要改变这一点。Event.dataTransfer.clearData(opt_DataType)
:清除与特定DataType
相关的数据(见该列表中的setData
)。如果未指定DataType
,所有数据将被清除。Event.dataTransfer.getData(DataType)
:获取与特定DataType
相关的数据(见setData
,下一步)。Event.dataTransfer.setData(DataType, data)
:将指定的data
与DataType
关联。有效的DataTypes
取决于浏览器。Internet Explorer 只支持text
和url
的DataTypes
。其他浏览器支持标准 MIME 类型,甚至任意类型。data
必须是一个简单的字符串,但也可以是 JSON 格式的序列化对象。Event.dataTransfer.setDragImage(HtmlElement, opt_offsetX, opt_offsetY)
:将拖动辅助图像设置为指定的 HTML 元素。默认情况下,辅助图像的左上角位于鼠标指针的下方,但是可以通过指定可选参数opt_offsetX
和opt_offsetY
进行偏移,以像素为单位。这种方法在 Internet Explorer 中不可用,而且显然永远也不会出现——参见http://connect.microsoft.com/IE/feedback/details/804304/implement-datatransfer-prototype-setdragimage-method
。
dataTransfer
对象还具有以下属性:
Event.dataTransfer.dropEffect
:拖放序列正在执行的拖放效果。有效值为copy
、move
、link
和none
。该值在dragenter
和dragover
事件中根据用户通过鼠标动作和修饰键的组合(例如,Ctrl-拖动、Shift-拖动、Option-拖动等)所请求的交互来自动初始化。).这些依赖于平台。只有由effectAllowed
(见下一页)指定的值才会真正启动拖放序列。Event.dataTransfer.effectAllowed
:该拖放序列允许哪些dropEffects
。有效值及其允许的效果如下:copy
:允许复制dropEffect
。move
:允许移动dropEffect
。link
:允许链接dropEffect
。copyLink
:允许复制和链接dropEffect
。copyMove
:允许复制和移动dropEffect
。linkMove
:允许链接和移动dropEffect
。all
:所有dropEffects
都是允许的。这是默认值。none
:不允许dropEffects
(该物品不能被丢弃)。
Event.dataTransfer.files
:包含数据传输中所有可用文件的列表。只有将文件从桌面拖到浏览器时,才会有值。Event.dataTransfer.types
:包含所有已经添加到dataTransfer
对象中的DataTypes
的列表,按照添加的顺序排列。
拖放 API 示例
清单 3-14 产生了可以想象的最简单的东西:一组可以拖放到单个目标上的可拖动的盒子。当箱子被放在目标上时,一个计数器会增加显示箱子被放的次数。
清单 3-14 。一个简单的拖放界面
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
.draggable {
margin: 5px;
width: 100px;
height: 100px;
background-color: #ccc;
border: 1px solid #000;
display: inline-block;
}
.target {
border: 10px solid #000;
width: 315px;
height: 100px;
margin-left: 5px;
margin-top: 50px;
}
.target.over {
border: 10px solid green;
}
</style>
</head>
<body>
<div class="draggable" draggable=’true’></div>
<div class="draggable" draggable=’true’></div>
<div class="draggable" draggable=’true’></div>
<div class="target"></div>
<script>
// Get a reference to the drop target.
var dropTarget = document.querySelector(’.target’);
// Add a dragenter event handler to the drop target.
dropTarget.addEventListener(’dragenter’, function(event) {
// Add the ’over’ CSS class to the drop target. This lets the user know that
// they have dragged something over a valid drop target.
this.classList.add(’over’);
}, false);
// Add a dragleave event handler to the drop target.
dropTarget.addEventListener(’dragleave’, function(event) {
// Remove the ’over’ CSS class.
this.classList.remove(’over’);
}, false);
// Add a dragover event handler to the drop target.
dropTarget.addEventListener(’dragover’, function(event) {
// Prevent the default event action.
event.preventDefault();
}, false);
// A counter that indicates how many times something has been dropped onto the
// drop target.
var counter = 1;
// Add a drop event handler to the drop target.
dropTarget.addEventListener(’drop’, function(event) {
// Update the counter and remove the ’over’ CSS class.
this.innerHTML = counter;
this.classList.remove(’over’);
counter++;
}, false);
</script>
</body>
</html>
在脚本中,您所做的只是在放置目标上注册了dragenter
、dragleave
、dragover
和drop
事件处理程序。在dragenter
上,你给元素添加一个 CSS 类,在dragleave
上,你移除 CSS 类。这给出了用户已经成功地将元素拖动到可以接收它的目标上的视觉反馈。然后阻止dragover
事件的默认动作,以阻止 drop 事件。在 drop 事件处理程序中,更新目标元素的innerHTML
并增加计数器。您还删除了视觉反馈 CSS 类,因为此时您将终止拖放序列,并且不会触发dragleave
事件。
这个例子在 Internet Explorer 和 Chrome 中运行良好。它在 Firefox 中根本不起作用。这是因为 Firefox 要求通过指定一些数据(任何数据)在dragstart
上初始化dataTransfer
对象。要更新你的脚本,你必须给每个可拖动的元素添加一个dragstart
事件处理程序,并在其中设置一些任意的数据,这样 Firefox 就会从这些元素中启动拖动序列。清单 3-15 对脚本进行了必要的修改(周围的 HTML 和 CSS 保持与清单 3-14 中的相同)。
清单 3-15 。拖放脚本更新了以在 Firefox 中工作
// Get a reference to all of the draggable objects.
var draggables = document.querySelectorAll(’.draggable’);
// On each draggable element intialize the dataTransfer object on dragstart so
// Firefox will initiate drag events with them.
for (var i = 0; i < draggables.length; i++) {
currEl = draggables[i];
currEl.addEventListener(’dragstart’, function(event) {
event.dataTransfer.setData(’text’, ’anything’);
}, false);
};
// Get a reference to the drop target.
var dropTarget = document.querySelector(’.target’);
// Add a dragenter event handler to the drop target.
dropTarget.addEventListener(’dragenter’, function(event) {
// Add the ’over’ CSS class to the drop target. This lets the user know that
// they have dragged something over a valid drop target.
this.classList.add(’over’);
}, false);
// Add a dragleave event handler to the drop target.
dropTarget.addEventListener(’dragleave’, function(event) {
// Remove the ’over’ CSS class.
this.classList.remove(’over’);
}, false);
// Add a dragover event handler to the drop target.
dropTarget.addEventListener(’dragover’, function(event) {
// Prevent the default event action.
event.preventDefault();
}, false);
// A counter that indicates how many times something has been dropped onto the
// drop target.
var counter = 1;
// Add a drop event handler to the drop target.
dropTarget.addEventListener(’drop’, function(event) {
// Update the counter and remove the ’over’ CSS class.
this.innerHTML = counter;
this.classList.remove(’over’);
counter++;
}, false);
您会注意到代码使用querySelectorAll
来获取对所有可拖动元素的引用。然后,它在一个for
循环中遍历所有这些元素,并对每个元素应用dragstart
事件监听器。(另一种方法是将 dragstart 事件处理程序委托给包含它的元素。)现在元素在 Firefox 中是可拖动的,这个例子在那个浏览器中的工作方式和在其他浏览器中一样。
实际上,您可能会为拖放序列初始化数据。我之前提到过一个可视剪贴板的例子,你可以在清单 3-16 中看到。
清单 3-16 。可视剪贴板
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
p {
margin-bottom: 0;
}
div#dropTarget {
width: 300px;
height: 300px;
border: 10px solid black;
}
div#dropTarget.over {
border: 10px solid green;
}
.draggable {
width:100px;
height: 100px;
background-color: #ccc;
}
</style>
</head>
<body>
<p>Type some text here, then highlight it and drag it to the clipboard below.</p>
<textarea id="dragSource"></textarea>
<p>Clipboard</p>
<div id="dropTarget"></div>
<script>
// Get references to our drag source and drop target.
var dragSource = document.getElementById(’dragSource’);
var dropTarget = document.getElementById(’dropTarget’);
// Add a dragstart event handler to the dragsource element.
dragSource.addEventListener(’dragstart’, function(event) {
// Initialize the dataTransfer object with the current text.
event.dataTransfer.setData(’text’, this.value);
}, false);
// Add a dragenter event handler to the target.
dropTarget.addEventListener(’dragenter’, function(event) {
// Add the ’over’ CSS class to the element.
this.classList.add(’over’);
}, false);
// Add a dragleave event handler to the target.
dropTarget.addEventListener(’dragleave’, function(event) {
// Remove the ’over’ CSS class from the element.
this.classList.remove(’over’);
}, false);
// Add a dragover event handler to the target.
dropTarget.addEventListener(’dragover’, function(event) {
// Prevent the default action of the dragover event.
event.preventDefault();
}, false);
// Finally, add a drop event handler to the target.
dropTarget.addEventListener(’drop’, function(event) {
// Append the text in the dataTransfer object to the clipboard.
this.innerHTML = event.dataTransfer.getData(’text’);
// Remove the ’over’ CSS class from the element.
this.classList.remove(’over’);
}, false);
</script>
</body>
</html>
在本例中,您已经创建了一个简单的textarea
,您可以在其中输入一些文本。然后,您可以突出显示该文本,并将其拖到剪贴板。在幕后,代码在dragstart
事件期间将文本设置为dataTransfer
对象上的数据,然后从drop
事件上的dataTransfer
对象获取文本。
这个例子在几乎所有的浏览器中都运行良好,除了 Firefox 再次出现了一个小问题。当你将文本拖放到剪贴板区域时,Firefox 会在drop
上触发一个默认动作,试图将页面的 URL 更新为被拖放的文本。为了防止这种情况,您需要在drop
事件处理程序中调用event.preventDefault()
。通过添加这一行,该示例将在所有浏览器中运行相同。
作为最后一个例子,考虑将可拖动项的移动限制到特定区域的需要。你不想让它离开一个包含元素。或者,您可能希望将运动限制在单个轴上。不幸的是,HTML5 拖放 API 没有为这个相当常见的用例提供现成的解决方案,但是您可以使用它提供的工具构建一个。
问题的核心是你没有办法用 JavaScript 来限制鼠标指针。这有道理;用 JavaScript 操纵鼠标指针动作会带来很大的安全风险。拖放序列的设置方式,指针走到哪里,辅助图像就跟到哪里,所以如果不能限制指针,就不能限制辅助图像的位置。
这意味着你要做的第一件事是使用dataTransfer.setDragImage()
删除默认的助手图像,这意味着这个例子不能在 Internet Explorer 中工作,因为它没有实现那个方法。但是这个例子确实可以在其他浏览器中工作,并且它是一个很有价值的例子来演示一些更复杂的与 API 的交互。
下一步是构建您自己的助手映像,可以根据需要对其进行操作。一旦做到这一点,它只是一个听事件的问题。
清单 3-17 提供了完整的例子。
清单 3-17 。将拖放限制在特定区域
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
.container {
width:200px;
height:500px;
border: 1px solid #000;
position: relative;
}
#dragTarget {
height: 20px;
background-color: #ccc;
}
.drag-helper {
opacity: 0.5;
position: absolute;
width: 200px;
height: 20px;
background-color: #ccc;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="container">
<div id="dragTarget" draggable="true">Drag me!</div>
</div>
<div id="helper" style="width: 1px;height: 1px;"></div>
<script>
// Get references to the drag target and container.
var dragTarget = document.getElementById(’dragTarget’);
var dragContainer = document.querySelector(’.container’);
// This variable will hold the new helper when we build it.
var dragHelper;
// Add a dragstart event handler to the drag target.
dragTarget.addEventListener(’dragstart’, function(event) {
// Initialize the dataTransfer object for Firefox.
event.dataTransfer.setData(’text’, ’Fix for Firefox’);
// Replace the default drag image with a small, transparent DIV.
var dragImage = document.getElementById(’helper’);
event.dataTransfer.setDragImage(dragImage, 0, 0);
if (dragHelper == null) {
// Create our own drag helper by cloning the target element. Note that when
// we clone the element we need to do some cleanup, like removing the clone’s
// id attribute (so we do not introduce duplicate ids even temporarily) and
// making not draggable.
dragHelper = this.cloneNode(true);
dragHelper.id = ’’;
dragHelper.classList.add(’drag-helper’);
dragHelper.draggable = false;
dragContainer.appendChild(dragHelper);
} else {
// We’ve already created a clone, so let’s just use it.
dragHelper.classList.remove(’hidden’);
}
}, false);
// Add a dragover event handler to the drag container.
dragContainer.addEventListener(’dragover’, function(event) {
// Prevent the default action of the event.
event.preventDefault();
// Move the helper to the desired location.
if (event.clientY < 485) {
dragHelper.style.top = event.clientY + ’px’;
}
}, false);
// Add a dragend event handler to the target.
dragTarget.addEventListener(’dragend’, function(event) {
// Dragging is done, so hide the clone.
dragHelper.classList.add(’hidden’);
}, false);
// Add a drop event to the drag container.
dragContainer.addEventListener(’drop’, function(event) {
event.preventDefault();
}, false);
</script>
</body>
</html>
您将在dragstart
事件处理程序中完成所有的设置。首先是对 Firefox 的修复,否则这个例子就不能在那个浏览器中运行。然后,通过获取对一个小的透明div
的引用并将其用作新的辅助图像来隐藏默认的辅助图像。接下来,克隆目标元素,将克隆的元素设置为新的助手,并将其添加到 DOM 中。
在您的dragover
事件处理程序中,您所要做的就是将辅助对象移动到期望的位置。请记住,由于dragover
事件在整个拖动过程中持续触发,您应该尽可能保持其事件处理程序的轻量级。这就是为什么您使用对拖动辅助对象的缓存引用,这样您就不必在每次事件触发时都查询它。当你使用这个例子时,你会注意到,因为你只监听包含元素中的dragover
事件,当你将指针移出该元素时,助手将停止移动。您可以将dragover
事件处理程序委托给body
元素,这将允许用户从页面上的任何地方移动元素。
您将自定义助手隐藏在您的dragend
事件处理程序中。这样做是为了无论用户在哪里释放鼠标按钮,助手都将被隐藏。这是dragend
事件的常见用法。
如上所述,这个例子在 Internet Explorer 中不起作用,所以尽管这个例子很有趣,但对于大多数情况来说并不实用。不幸的是,也没有办法填充setDragImage
方法。因此,如果这是您需要的交互类型,并且您必须支持 Internet Explorer,您可能需要寻找除 HTML5 原生拖放 API 之外的另一种解决方案。
Web Worker s
维持价
好的
所有现代浏览器都支持这些特性,并且至少在最近两个版本中都支持。
WHATWG 生活水平:http://www.whatwg.org/specs/web-apps/current-work/multipage/workers.html
W3C 草案:http://dev.w3.org/html5/workers/
对基于网络的应用最大的批评之一是速度:它们不够快,尤其是在移动设备上。尽管我是 web 技术(以及用这些技术构建应用)的粉丝,但速度确实是一个合理的问题。
web 应用运行缓慢的原因有很多,但其中一个主要问题是在 web 浏览器中运行的脚本一次只能做一件事。例如,考虑从服务器获取和解析(或者操作)一个大文件的常见例子。使用XMLHttpRequest
很容易建立一个异步请求,它将加载文件,然后在准备好的时候执行一个回调函数。在回调函数被调用之前,您的脚本可以执行其他任务。但是一旦回调函数开始执行,当它以您指定的方式处理数据时,一切都必须等待。你的脚本不能更新用户界面或响应用户交互或任何事情。这对用户来说是非常令人沮丧的。
web 浏览器的异步环境帮助我们创建了响应速度更快的 web 应用,但仍然不允许我们同时执行多项任务。HTML5 规范解决了 Web Workers 的这一限制,它使我们能够同时创建和操作多个独立的任务。
在引擎盖下,Web 工作者让我们能够访问现代浏览器及其主机系统的多线程功能。一个线程是执行一个特定任务所需的操作系统和程序资源的组合,并通过启动、暂停、停止和处理其完成来管理该任务的状态。在具有多个处理器内核的现代硬件上运行的现代操作系统可以同时处理多个线程。当您创建一个新的 Web Worker 时,浏览器会为该任务生成一个单独的线程,它与主浏览器线程同时执行。
多线程是一个强大的工具,像所有强大的工具一样,它也有一些危险。网络工作者有一些旨在减少这些危险的重要限制:
- Web Worker 运行在自己独立的 JavaScript 上下文中。它不能直接访问任何其他执行上下文中的任何内容,比如其他 Web Workers 或主 JavaScript 线程。
- Web Worker 上下文和主 JavaScript 线程之间的通信是通过一个类似于 Web 消息传递所使用的
postMessage
接口来完成的。这使您能够将数据传入和传出 Web Worker 上下文,但是因为所有上下文都是独立的,所以在上下文之间传递的任何数据都是复制的,而不是共享的。 - Web Worker 无法访问 DOM。Web 工作者可以使用的 DOM 方法只有
atob
、btoa
、clearInterval
、clearTimeout
、dump
、setInterval
和setTimeout
。 - Web Workers 受相同来源策略的约束,因此您不能从不同于原始脚本的来源加载 worker 脚本。
这些都是严格的限制(特别是缺少对 DOM 的访问权限),但是它们有助于使 Web Workers 成为您使用的更安全的工具。如果您曾经用其他语言构建过多线程应用,您可能对该功能中固有的所有危险都很熟悉:并发性、安全性等等。由于他们的限制,网络工作者大多没有这些危险。
Web Workers 的另一个重要特征是,您必须自己管理您的员工。您负责创建它们、启动它们、停止它们,以及在它们的任务完成后处置它们。因为 Web Workers 消耗主机系统资源,所以正确管理它们以避免影响整个系统的性能非常重要。
创建网络工作者
创建和管理 Web Workers 遵循三个基本步骤:
- 创建新的 Web Worker。
- 将一个
message
事件处理程序附加到新的 worker,假设您希望它传达结果。您还应该附加一个error
事件处理程序,以便您的脚本可以对 worker 执行期间发生的任何错误做出反应。 - 通过向 worker 实例发布消息来启动它。这将导致 worker 开始运行,并且还会在其中触发一个
message
事件,这样 worker 就可以处理您刚刚发布的消息。
第一步很简单。Web Workers 规范在全局上下文中创建新的Worker
构造函数。通过为它指定一个 JavaScript 程序来加载和执行,您创建了一个Worker
的新实例:
var myNewWorker = new Worker(’my-new-worker.js’);
文件’my-new-worker.js’
必须是有效的 JavaScript 文件,一旦创建了 worker,它就会被加载。
Worker 实例myNewWorker
将发布message
和error
事件,这样您就可以为这些事件附加事件处理程序。清单 3-18 显示了带有存根函数的基本模式。
清单 3-18 。Web Worker 的存根错误和消息事件处理程序
/**
* Handles an error event from web worker.
* @param {WorkerErrorEvent} event The error event object.
*/
function handleWorkerError(event) {
// Handle the error here.
console.warn(’Error in web worker: ’, event.message);
}
/**
* Handles a message event from a web worker.
* @param {WorkerMessageEvent} event The message event object.
*/
function handleWorkerMessage(event) {
// Handle the message here.
console.log(’Message from worker: ’, event.data);
}
// Create a new worker.
var myNewWorker = new Worker(’my-new-worker.js’);
// Register error and message event handlers.
myNewWorker.addEventListener(’error’, handleWorkerError);
myNewWorker.addEventListener(’message’, handleWorkerMessage);
在这个基本示例中,有一些简单的函数来处理error
和message
事件,这些事件只是在 JavaScript 控制台中显示结果。您使用Worker
实例myNewWorker
上的addEventListener
方法将它们注册为处理程序。请注意,您应该总是在启动 worker 之前注册事件处理程序。如果您启动 worker,然后注册事件处理程序,在完成注册的几毫秒内可能会发生一些事情,您可能会错过一条消息或一个错误。
要启动 worker,只需使用postMessage
方法向它发布一条消息:
myNewWorker.postMessage(’start’);
当您将消息发送到 worker 实例时,它将开始执行脚本,并且还将在 worker 的执行上下文中为开始消息触发一个message
事件。(注意,虽然Worker.postMessage
方法类似于用于 Web 消息传递的window.postMessage
方法,但是它没有后者的可选域参数。)
在网络工作者内部
在 Web Worker 内部,脚本的环境与主执行上下文略有不同。如前所述,Web Worker 不能访问 DOM,因此任何访问window
对象或其子对象(比如document
对象或 DOM 中的任何元素)的尝试都将失败。Web Workers 可以访问以下标准属性和方法:
-
DOM 方法
atob
、btoa
、clearInterval
、clearTimeout
、dump
、setInterval
和setTimeout
。 -
XMLHttpRequest
构造函数,因此 Web 工作者可以执行异步网络任务。 -
WebSocket
构造函数,因此 Web Workers 可以创建和管理 WebSockets(在撰写本文时,Firefox 没有为 Web Workers 启用WebSocket
;不过,这个功能正在实现中,你可以在https://bugzilla.mozilla.org/show_bug.cgi?id=504553
跟踪它的状态 -
Worker
构造函数,因此 Web Workers 可以产生自己的 Workers(被称为子 workers )。截至本文撰写之时,Chrome 和 Safari 还没有为 Web Workers 实现Worker
构造函数。在https://code.google.com/p/chromium/issues/detail?id=31666
的 Chrome 和https://bugs.webkit.org/show_bug.cgi?id=22723
的 Safari 的 WebKit 都有一个 bug。从版本 10 开始,Internet Explorer 支持子工作器。 -
EventSource
构造函数,因此 Web 工作者可以订阅服务器发送的事件流。这似乎是一个非标准特性,但是在撰写本文时,似乎在所有主流浏览器中都可以使用。 -
Navigator
属性的特殊子集,可通过navigator
对象获得:-
navigator.language
:返回浏览器当前使用的语言。 -
navigator.onLine
:返回一个布尔值,表示浏览器是否在线。 -
navigator.platform
:返回表示主机系统平台的字符串。 -
navigator.product
:返回当前浏览器名称的字符串。 -
navigator.userAgent
: Returns the user agent string for the browser.这些属性的实现因浏览器而异,因此最好将所需的
Navigator
信息从主线程传递给 Web Worker。
-
-
在
location
对象上可用的Location
属性的特殊子集:location.href
:Web Worker 正在执行的脚本的完整 URL。location.protocol
:Web Worker 正在执行的脚本的 URL 的协议方案,包括最后的“:”。location.host
:Web Worker 正在执行的脚本的 URL 的主机部分(主机名和端口)。location.hostname
:Web Worker 正在执行的脚本的 URL 的主机名部分。location.port
:Web Worker 正在执行的脚本的 URL 的端口部分。location.pathname
:首字母“/
”,后跟 Web Worker 正在执行的脚本的路径。location.search
:Web Worker 正在执行的脚本的 URL 的参数(如果有的话),后跟首字母“?
”。location.hash
:Web Worker 正在执行的脚本的 URL 的片段标识符(如果有的话),后面是首字母“#
”。
-
还有一个方法
location.toString()
简单的返回location.href
。
Web Worker 执行上下文还有一个新的可用方法:importScripts
。importScripts
方法采用逗号分隔的一个或多个 JavaScript 文件名列表,这些文件名将按顺序加载和执行。例如,这一行
importScripts(’script1.js’, ’script2.js’, ’subdirectory/script3.js’);
将按顺序加载并执行指定的三个脚本。相对 URL 被解析为相对于创建 Web Worker 实例时指定的脚本的 URL。importScripts
方法也受相同起源策略的约束,因此您不能从为 Web Worker 实例的父脚本提供服务的不同起源导入脚本。
importScripts
方法是一个阻塞方法,这意味着每个脚本都将按顺序加载和执行,并且在最后一个脚本完成加载和执行之前,工人不会继续下一行。如果其中一个脚本由于网络问题而无法加载,或者加载了但由于内部错误而无法运行,Web Worker 将停止执行并发布一个error
事件。
用importScripts
加载的脚本在与 Web Worker 相同的上下文中执行。他们不能访问 DOM,但是他们可以访问上面列出的所有标准属性和方法以及importScripts
方法,所以导入的脚本可以导入其他脚本。
当 Web Worker 启动时,它遵循以下步骤:
- 它从头到尾执行脚本,包括任何异步任务(比如
XMLHttpRequest
调用)。 - 如果其执行的一部分是注册一个
message
事件处理程序,那么它将进入一个等待传入消息的循环。它收到的第一条消息将是发布来启动工作进程的消息。工作线程将保持等待模式,直到您手动终止它,或者它自己终止。 - 如果没有注册
message
事件处理程序,工作线程将自动终止。
这是很重要的一点:如果您的 Web Worker 注册了一个message
事件处理程序,它将永远处于等待模式,除非您终止它。同样,因为 Web 工作线程消耗系统资源,所以您应该确保终止任何不需要的工作线程。您可以通过以下两种方式之一解雇员工:
- 可以在 Worker 实例:
myWebWorker.terminate();
上调用terminate
方法。 - Web Worker 可以通过调用它的
close
方法:self.close();
来终止自己。
这两种方法都会立即停止工作进程。
一个网络工作者的简单例子
清单 3-19 扩展了清单 3-18 中的存根示例。
注意本节中的例子需要从服务器上运行。
清单 3-19 。创建 Web Worker
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Web Workers</h1>
<div id="message-box"></div>
<script>
// Get a reference to the target element.
var messageBox = document.getElementById(’message-box’);
/**
* Handles an error event from web worker.
* @param {WorkerErrorEvent} event The error event object.
*/
function handleWorkerError(event) {
console.warn(’Error in web worker: ’, event.message);
}
/**
* Handles a message event from a web worker.
* @param {WorkerMessageEvent} event The message event object.
*/
function handleWorkerMessage(event) {
messageBox.innerHTML = ’Message received from worker: ’ + event.data;
}
// Create a new worker.
var myNewWorker = new Worker(’web-worker.js’);
// Register error and message event handlers on the worker.
myNewWorker.addEventListener(’error’, handleWorkerError);
myNewWorker.addEventListener(’message’, handleWorkerMessage);
// Start the worker.
myNewWorker.postMessage(’begin’);
</script>
</body>
</html>
和前面一样,创建两个事件处理程序,一个用于错误事件,一个用于消息事件。错误事件处理程序只是将错误记录到控制台,而消息事件处理程序将消息文本附加到 DOM。然后创建一个新的 Web Worker,在其上注册您的事件处理程序,最后,向它发布一条消息来启动它。
清单 3-20 显示了工人本身的代码。
清单 3-20 。一个简单的 Web Worker 脚本
/**
* Handles a message event from the main context.
* @param {WorkerMessageEvent} event The message event.
*/
function handleMessageEvent(event) {
// Do something with the message.
console.log(’Worker received message:’, event.data);
// Send the message back to the main context.
self.postMessage(’Your message was received.’);
}
// Register the message event handler.
self.addEventListener(’message’, handleMessageEvent);
这个 Web Worker 创建一个消息事件处理程序,将消息记录到控制台。然后,它将确认消息发送回父线程,并在执行上下文中注册事件处理程序。
当您运行这个示例时,它将来回传递消息,但不会展示 Web Workers 的真正威力,即它们与主线程同时执行。
常见使用案例
Web Worker 允许您以这样一种方式重新构造应用,即您有一个处理 UI 的主线程,而任何其他密集型或异步操作都由 Web Worker 线程处理。好的例子包括:
- 异步活动:因为 Web Workers 可以访问
XMLHttpRequest
构造函数以及 WebSockets 和theimportScripts
方法,所以它们可以用来加载和解析数据,或者(甚至更好)将大量数据发送回服务器。 - 计算密集型活动:任何需要大量计算的工作都是网络工作者的理想选择。密码学是一个很好的例子,游戏的物理引擎也是。
- 图像处理:如果你有大量来自 canvas 元素的数据需要处理,你可以利用 Web Workers 来处理这些数据。
- 分而治之:我已经提到过使用 Web Workers 来处理大量数据。如果您可以将所讨论的数据分成更小的部分,那么您可以给每个部分一个自己的 Web Worker 来处理,从而使事情进行得更快。
摘要
在这一章中,我介绍了几个 HTML5 JavaScript APIs。使用这些新的 API,您的应用可以:
- 与服务器进行更高效、更安全的通信。您的应用现在可以订阅服务器发送的事件流,甚至可以使用 WebSockets 建立与服务器的双向通信,而不仅仅是依赖于
XmlHttpRequest
。您还可以使用跨文档消息传递来实现脚本来源之间更安全的通信。 - 在客户端更高效地存储信息。使用新的 Web 存储特性,您的应用可以轻松地存储和检索信息,包括序列化的对象和数据结构。
- 使用新的拖放 API 高效地实现拖放交互。拖放项目是一种非常常见的用户交互隐喻,现在使用新的 API 更容易实现。
- 创建和管理线程。使用 Web Workers,您的应用现在可以是多线程的。
使用这些新的 API,您的应用可以更高效、更易于使用,也更易于创建和维护。
在下一章中,你将深入了解 HTML5 最激动人心的特性之一:canvas
元素。
四、画布
当 HTML5 首次公布时,最令人兴奋的功能可能是新的canvas
元素——页面上的一个区域,您可以使用绘图上下文 API 中的各种命令在其上绘制位图图形。这意味着第一次有了用 JavaScript 创建动态图形的官方方法。
元素最初由苹果公司在 2004 年创建,作为 WebKit 的专有附加物。它后来被其他浏览器制造商采用,然后被 W3C 作为 HTML5 的一部分。今天,canvas
在现代浏览器中享有广泛的支持。
维持价
优秀
至少在最近的三个版本中,所有的现代浏览器都支持canvas
元素和本章涵盖的所有特性。
WHATWG 生活水平:http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html
W3C 草案:http://www.w3.org/html/wg/drafts/2dcontext/html5_canvas_CR/
Canvas
绘图模式
如果你熟悉图形库,你可能听说过术语即时模式和保留模式来描述事物如何在屏幕上呈现。在即时模式呈现中,图形在 API 调用启动时呈现,绘图上下文不存储任何有关图形的内容。
在保留模式呈现中,对 API 的调用不会立即在屏幕上呈现。相反,API 的结果存储在由库维护的内部模型中,从而允许库在绘制所有内容时进行各种优化。
canvas
标签以即时模式呈现:只要调用 API,结果就会呈现在屏幕上,而canvas
不会存储任何关于刚刚绘制的内容的信息。如果你想重画同样的东西,你将不得不再次发出同样的命令。
Canvas
绘图上下文
元素可以像其他 HTML 元素一样通过 DOM 访问。然而,每个canvas
元素都公开了一个或多个绘图上下文,这些上下文可以用来以各种方式在canvas
上绘图。目前,标准中规定的并且浏览器支持的唯一上下文是二维(或 2d )上下文。
2d 上下文公开了一个令人印象深刻的 API,用于在canvas
元素上绘制直线、曲线、形状、文本等等。每个canvas
都有一个坐标系,原点(0,0)在左上角。2d 绘图上下文使用一个虚构的笔隐喻作为其基本的绘图功能,因此在canvas
上绘图的命令类似于“将笔移动到这些坐标,然后绘制这个东西”另外,画东西和填东西或者划东西是分开的概念,是由分开的命令来执行的。当您第一次绘制路径时,它不会显示在屏幕上,您必须应用填充或描边才能使它可见。这是为了提高效率,因为这样你可以画一个由许多部分组成的复杂路径,然后一次性描边或填充整个东西。
首先,画一条简单的线。语法非常简单,如清单 4-1 中的所示。
清单 4-1 。canvas
的基本绘图语法
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
width: 200px;
height: 200px;
}
</style>
</head>
<body>
<canvas id="myCanvas"></canvas>
<script>
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
myContext.moveTo(0, 0);
myContext.lineTo(200, 200);
myContext.strokeStyle = ’#000’;
myContext.stroke();
</script>
</body>
</html>
这个例子在页面上有一个基本的canvas
元素。它使用 CSS 给出了canvas
的尺寸和边框,这样你就可以看到它了。该脚本获取对canvas
元素的引用,然后使用该引用获取绘图上下文。然后它使用moveTo
方法将笔移动到canvas
的左上角,然后指示上下文在(200,200)处画一条线(作为路径)到右下角。最后,它将描边样式设置为黑色,并指示上下文描边路径。
图 4-1 显示的结果有些出乎意料。
图 4-1 。清单 4-1 的结果
你期望直线从 0,0 到 200,200。。。事实上它做到了。一个canvas
元素的默认大小是 200 像素高 400 像素宽。您使用 CSS 来指定canvas
的尺寸,这只是让canvas
调整其纵横比,而不是实际减少其默认宽度。这给我们带来了一个重要的细节:在一个canvas
中,坐标系不一定与屏幕像素相对应。
这是画布的一个常见错误,它的发生是因为我们都被训练使用 CSS 来改变 HTML 元素的外观。但是,对于canvas
元素,您需要使用它的width
和height
属性来指定它的尺寸。清单 4-2 将这些添加到标记中。
清单 4-2 。指定画布元素的宽度和高度
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200"></canvas>
<script>
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
myContext.moveTo(0, 0);
myContext.lineTo(200, 200);
myContext.strokeStyle = ’#000’;
myContext.stroke();
</script>
</body>
</html>
如您所见,这从 CSS 规则中移除了宽度和高度声明,而是使用width
和height
属性将尺寸直接应用于canvas
元素。然后它绘制并描边路径,结果如预期,如图图 4-2 所示。
图 4-2 。小小的胜利
如您所见,canvas
现在真正是 200 像素乘 200 像素,并且您的线条完全按照您的预期绘制。
canvas
标签不是自动结束的,所以结束标签是强制的。您可以在canvas
标签中包含替代内容,如果浏览器不支持canvas
元素,就会呈现替代内容。你可以很容易地扩展这个简单的例子,使其包含一些老浏览器的替代内容,如清单 4-3 所示。
清单 4-3 。不支持 Canvas 的浏览器的替代内容
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
myContext.moveTo(0, 0);
myContext.lineTo(200, 200);
myContext.strokeStyle = ’#000’;
myContext.stroke();
</script>
</body>
</html>
如你所见,我们都应该为小猫着想。
现在,您已经对 canvas 标签和绘图上下文有了一个基本的概念,接下来将深入研究可用的绘图命令。
基本绘图命令
Canvas
提供一组绘图命令,可用于构建复杂图形。大多数绘图命令用于构建路径。事实上,canvas
只包含一个形状原语的命令:矩形。你将不得不使用更简单的曲线组合来构建任何其他形状。
给定一张图纸Context
,基本曲线为:
Context.lineTo(x, y)
:从当前笔位置到指定坐标画一条线。Context.arc(x, y, radius, startAngle, endAngle, anticlockwise)
:以(x, y)
为圆心,以指定的半径画一个圆弧。startAngle
和endAngle
参数是以弧度表示的开始和结束角度,可选的anticlockwise
参数是一个布尔值,指示曲线是否应该逆时针绘制(默认为false
,因此默认情况下圆弧是顺时针绘制的)。Context.quadraticCurveTo(cp1x, cp1y, x, y)
:画一条二次曲线,从当前笔的位置开始,到坐标(x, y)
结束,控制点在(cp1x, cp1y)
。Context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
:从当前笔的位置开始,到坐标(x, y)
结束,用(cp1x, cp1y)
指定控制点 1,用(cp2x, cp2y)
指定控制点 2,绘制一条贝塞尔曲线。Context.rect(x, y, width, height)
:从(x, y)
开始画一个指定宽度和高度的矩形。
使用两个简单的命令来声明路径:
Context.beginPath()
:开始新的路径定义。路径闭合前的所有曲线都将包含在路径中。Context.closePath()
:结束路径定义,从当前笔位置到路径起点画一条直线,关闭路径。
路径本身是看不见的。你必须告诉canvas
要么抚摸它们,要么填满它们:
Context.strokeStyle
:该属性定义了调用 stroke 方法时,在当前路径上描边的样式。该属性可以接受任何有效的 CSS 颜色字符串(例如,’red’
、’#000’
或’rgb(30, 50, 100)’
)、渐变对象或图案对象。Context.stroke()
:用Context.strokeStyle
中指定的样式对当前路径进行描边。Context.fillStyle
:该属性定义了调用fill
方法时填充到当前路径中的样式。该属性可以接受 CSS 颜色字符串、渐变对象或图案对象。Context.fill()
:用Context.fillStyle
中指定的样式填充当前路径。Context.lineWidth
:该属性定义应用于路径的笔画粗细。默认为 1 个单位。Context.lineCap
:该属性定义线条如何封顶。有效值包括:butt
:线端被切成方形,并精确地终止于指定的端点。这是默认值。round
:线条末端是圆形的,稍微超过指定的端点。square
:在线条末端加一个宽度等于线条宽度、高度为线条宽度一半的方框,使线条末端呈方形。
Context.lineJoin
:该属性定义连接线如何连接在一起。有效值包括:bevel
:接头是斜的。- 接头是斜接的。
round
:关节呈圆形。
清单 4-4 给出了lineCap
属性的示例。
清单 4-4 。线帽
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
myContext.lineWidth = 20;
// Set up an array of valid ending types.
var arrEndings = [’round’, ’square’, ’butt’];
var i = 0, arrEndingsLength = arrEndings.length;
for (i = 0; i < arrEndingsLength; i++){
myContext.lineCap = arrEndings[i];
myContext.beginPath();
myContext.moveTo(50 + (i * 50), 35);
myContext.lineTo(50 + (i * 50), 170);
myContext.stroke();
}
</script>
</body>
</html>
本例使用canvas
绘制粗线,以更好地说明线帽。和往常一样,首先获取目标canvas
的绘图上下文,并为绘图设置lineWidth
。然后利用一个行结束值的数组,遍历数组为每个值画一条线,如图图 4-3 所示。
图 4-3 。帆布线帽
你可以看到圆形和方形的帽线比实际的线端多一点。有时,如果你的划水需要特别紧,这会产生奇怪的效果。如果是这种情况,只需减少一点路径的长度,以解决额外的行程。
清单 4-5 展示了lineJoin
属性的各种值。
清单 4-5 。线条连接
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
myContext.lineWidth = 20;
// Set up an array of valid ending types.
var arrJoins = [’round’, ’miter’, ’bevel’];
var i = 0, arrJoinsLength = arrJoins.length;
for (i = 0; i < arrJoinsLength; i++){
myContext.lineJoin = arrJoins[i];
myContext.beginPath();
myContext.moveTo(55, 60 + (i * 60));
myContext.lineTo(95, 20 + (i * 60));
myContext.lineTo(135, 60 + (i * 60));
myContext.stroke();
}
</script>
</body>
</html>
与前面的例子相似,清单 4-5 使用一个有效连接值的数组来提供这个演示的结构。它循环遍历数组,并绘制出每个数组的一个例子,如图图 4-4 所示。
图 4-4 。画布线条连接
你可以看到round
接头在接头的钝角侧提供了一个稍微圆的盖子,而miter
接头在钝角侧稍微成方形。
清单 4-6 显示了在弧线上使用笔画属性。
清单 4-6 。随机圆发生器
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a loop that will draw a random circle on the canvas.
var cycles = 10,
i = 0;
for (i = 0; i < cycles; i++) {
var randX = getRandomIntegerBetween(50, 150);
var randY = getRandomIntegerBetween(50, 150);
var randRadius = getRandomIntegerBetween(10, 100);
myContext.beginPath();
myContext.arc(randX, randY, randRadius, 0, 6.3);
randStroke();
}
/**
* Returns a random integer between the specified minimum and maximum values.
* @param {number} min The lower boundary for the random number.
* @param {number} max The upper boundary for the random number.
* @return {number}
*/
function getRandomIntegerBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Returns a random color formatted as an rgb string.
* @return {string}
*/
function getRandRGB() {
var randRed = getRandomIntegerBetween(0, 255);
var randGreen = getRandomIntegerBetween(0, 255);
var randBlue = getRandomIntegerBetween(0, 255);
return ’rgb(’ + randRed + ’, ’ + randGreen + ’, ’ + randBlue + ’)’;
}
/**
* Performs a randomized stroke on the current path.
*/
function randStroke() {
myContext.lineWidth = getRandomIntegerBetween(1, 10);
myContext.strokeStyle = getRandRGB();
myContext.stroke();
}
</script>
</body>
</html>
在本例中,您在canvas
上创建了十个随机的圆,每个圆位于一个随机的位置,具有随机的半径、线宽和描边颜色。getRandomIntegerBetween
功能可以轻松获得您需要的号码。你也有一个randStroke
功能,用随机的宽度和颜色描绘当前路径。结果如图 4-5 所示。
图 4-5 。清单 4-6 的结果
我之前提到过canvas
也可以画矩形。命令很简单:
Context.fillRect(x, y, width, height)
:在指定坐标绘制一个指定宽度和高度的矩形,用当前填充样式填充。Context.strokeRect(x, y, width, height)
:在指定坐标处,用当前笔画样式绘制一个指定宽度和高度的矩形。Context.clearRect(x, y, width, height)
:清除任何其他图形的指定矩形区域。
清单 4-7 展示了矩形的绘制。
清单 4-7 。在画布上绘制矩形
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Set a stroke style and stroke a rectangle.
myContext.strokeStyle = ’green’;
myContext.strokeRect(30, 30, 50, 100);
// Set a fill style and fill a rectangle.
myContext.fillStyle = ’rgba(200, 100, 75, 0.5)’;
myContext.fillRect(20, 20, 50, 50);
// Clear a rectangle.
myContext.clearRect(25, 25, 25, 25);
</script>
</body>
</html>
在这个例子中,你没有做任何花哨的事情,只是描边、填充和清除矩形。结果就像你预期的那样(图 4-6 )。
图 4-6 。长方形——耶!
渐变和图案
你已经看到了canvas
如何设置不同的笔画和填充样式,我提到过这些样式可以是任何有效的 CSS 颜色字符串(例如green
或rgba(100, 100, 100, 0.3)
)。此外,canvas
可以定义用于填充和描边路径的gradient
和pattern
对象。
梯度
Canvas
可以创建线性和径向渐变:
Context.createLinearGradient
(x, y, x1, y1)
:创建一个线性渐变,从坐标(x, y)
开始,到坐标(x1, y1)
结束。返回一个可用作描边或填充样式的Gradient
对象。Context.createRadialGradient
(x, y, r, x1, y1, r1)
:创建由两个圆组成的径向渐变,第一个以(x, y)
为中心,半径r
,另一个以(x1, y1)
为中心,半径r1
。返回一个可用作笔画或填充样式的Gradient
对象。Gradient.addColorStop
(position, color)
:给Gradient
增加一个色标。位置参数必须介于 0 和 1 之间;它定义了色标渐变中的相对位置。您可以向特定的Gradient
添加任意数量的色标。
清单 4-8 显示了一个简单的三停止渐变被用来描边矩形。
清单 4-8 。三站坡度
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a gradient object and add color stops.
var myGradient = myContext.createLinearGradient(0, 0, 200, 200);
myGradient.addColorStop(0, ’#000’);
myGradient.addColorStop(0.6, ’green’);
myGradient.addColorStop(1, ’blue’);
// Set the stroke styles and stroke some rectangles.
myContext.strokeStyle = myGradient;
myContext.lineWidth = 20;
myContext.strokeRect(10, 10, 110, 110);
myContext.strokeRect(80, 80, 110, 110);
</script>
</body>
</html>
这个例子创建了一个线性的gradient
对象并添加了三个色标,然后用它作为两个矩形的描边样式。结果如图图 4-7 所示。
图 4-7 。线性梯度
模式
Canvas
还支持图案作为填充或描边样式的概念:
Context.createPattern
(Image, repeat)
:创建可用作填充或描边样式的Pattern
对象。Image
参数必须是任何有效的Image
(详见下一节“图像”)。repeat
参数指定图案图像如何重复。必须是下列之一:repeat
:水平和垂直平铺图像。repeat-x
:仅水平重复图像。repeat-y
:仅垂直重复图像。no-repeat
:完全不重复图像。
清单 4-9 展示了使用一个简单的图像作为模式。
清单 4-9 。创建图案
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a new image element and fill it with a kitten.
var myImage = new Image();
myImage.src = ’http://www.placekitten.com/g/50/50’;
// We can’t do anything until the image has successfully loaded.
myImage.onload = function() {
// Create a pattern with the image and use it as the fill style.
var myPattern = myContext.createPattern(myImage, ’repeat’);
myContext.fillStyle = myPattern;
myContext.fillRect(5, 5, 150, 150);
};
</script>
</body>
</html>
此示例创建一个新的image
元素,并将其 URL 设置为一个占位符图像服务。在继续之前,您必须等待图像完成加载,因此您为它附加了一个onload
事件处理程序,在其中您创建了pattern
,并将其用作矩形的填充样式。
结果看起来和你预期的一样可爱,如图图 4-8 所示。
图 4-8 。一只小猫作为图案
图像
元素还可以加载和操作图像。一旦图像被加载到canvas
中,您也可以使用绘图命令在其上绘图。
canvas
元素可以将这些来源用于图像:
- 一个
img
元素, - 一个视频元素,以及
- 另一个
canvas
元素。
Canvas
有一种绘制图像的方法,但它可以接受许多不同的参数,因此具有多种功能:
Context.drawImage
(CanvasImageSource, x, y)
:在坐标(x, y)
处从CanvasImageSource
处画出图像。Context.drawImage(CanvasImageSource, x, y, width, height)
:在坐标(x, y)
绘制图像,将图像缩放到指定的width
和height
。Context.drawImage(CanvasImageSource, sliceX, sliceY, sliceWidth, sliceHeight, x, y, width, height)
:用sliceWidth
和sliceHeight
从(sliceX, sliceY)
开始的矩形指定的图像区域进行切片,然后在x
、y
处的canvas
上绘制该切片,将切片缩放到指定的width
和height
。
清单 4-10 展示了drawImage
的基本功能。
清单 4-10 。在画布上绘制图像
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a new image element and fill it with a kitten.
var myImage = new Image();
myImage.src = ’http://www.placekitten.com/g/150/150’;
// We can’t do anything until the image has successfully loaded.
myImage.onload = function() {
myContext.drawImage(myImage, 25, 25);
};
</script>
</body>
</html>
在这个例子中,你所做的就是为一个占位符图像创建一个新的img
元素。一旦图像加载完毕,在你的canvas
上绘制,如图图 4-9 所示。
图 4-9 。画在画布上的图像
清单 4-11 演示了在画布上缩放图像。
清单 4-11 。用画布缩放图像
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a new image element and fill it with a kitten.
var myImage = new Image();
myImage.src = ’http://www.placekitten.com/g/50/50’;
// We can’t do anything until the image has successfully loaded.
myImage.onload = function() {
myContext.drawImage(myImage, 25, 25, 50, 150);
};
</script>
</body>
</html>
这个例子给你一个 100 像素乘 100 像素的占位符,但是当你在canvas
上画它的时候,你把它缩放到 50 像素×150 像素,如图图 4-10 所示。
图 4-10 。在画布中缩放图像
最后,清单 4-12 显示了在canvas
上切片一个更大的图像并缩放切片。
清单 4-12 。对canvas
上的图像进行切片和缩放
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a new image element and fill it with a kitten.
var myImage = new Image();
myImage.src = ’http://www.placekitten.com/g/300/300’;
// We can’t do anything until the image has successfully loaded.
myImage.onload = function() {
myContext.drawImage(myImage, 25, 25, 150, 150, 0, 0, 150, 50);
};
</script>
</body>
</html>
这里你加载了一个 300px × 300px 的占位符图像,但是从(25, 25)
开始只截取了它的 75px × 75px 部分。然后你把这个切片渲染到canvas
中,缩放到 150 像素×50 像素。结果相当失真,如图图 4-11 所示。
图 4-11 。可怜的小猫
保存 Canvas
内容
一旦你在canvas
上画了一幅画,你可能想以某种方式保存它。这将包括获取图像数据,并将其传输到服务器,从服务器可以重建和显示图像数据。canvas
API 确实提供了保存渲染位图的方法:
Context.toDataUrl(opt_type, opt_quality)
:将渲染后的位图转换成数据 URI。数据 URIs 是一种将数据直接嵌入网页的方式,定义在 RFC 2397 中,您可以在http://tools.ietf.org/html/rfc2397
阅读。有效类型包括image/png
(默认)、image/jpeg
和(对于 Chrome 和基于 Chrome 的浏览器,image/webp
)。如果类型是image/jpeg
或image/webp
,可以提供一个可选的 0 到 1 之间的第二参数来表示质量。此方法返回编码为数据 URI 的渲染位图,然后您可以将它传输回服务器,甚至在同一页面的其他地方使用。
请注意,如果您已经将一个来自不同于宿主页面的图像加载到canvas
中,或者如果您已经将一个图像从您的硬盘加载到canvas
中,这个方法将抛出一个安全错误。这样做是为了防止通过粗心或恶意脚本泄漏信息。
正文
除了绘图和图像,canvas
元素还可以呈现文本。文本呈现的方法和属性如下:
Context.fillText
(textString, x, y, opt_maxWidth)
:用当前填充样式填充从(x, y)
开始的canvas
上的textString
。如果指定了可选的maxWidth
参数,并且呈现的文本将超过该宽度,浏览器将尝试以这样的方式来呈现文本,以使其适合指定的宽度(例如,如果可用,使用压缩的字体,使用较小的字体大小,等等)。).Context.measureText
(textString)
:测量使用当前样式渲染指定的textString
时的宽度。返回一个TextMetrics
对象,该对象的 width 属性包含值。Context.strokeText
(textString, x, y, opt_maxWidth)
:以当前笔画风格从(x, y)
开始在canvas
上划textString
。如果指定了可选的maxWidth
参数,并且呈现的文本将超过该宽度,浏览器将尝试以这样的方式来呈现文本,以使其适合指定的宽度(例如,如果可用,使用压缩的字体,使用较小的字体大小,等等)。).Context.font
:设置文本渲染的字体。允许任何有效的 CSS 字体字符串。Context.textAlign
:按指定对齐文本。有效值包括:left
:左对齐文本。right
:右对齐文本。center
:文本居中。start
:将文本从当前语言环境的起始端对齐(即,从左到右的语言靠左,从右到左的语言靠右)。这是默认值。end
:将文本在当前语言环境的结束端对齐。
Context.textBaseline
:为指定的文本设置基线。有效值包括:alphabetic
:对文本使用正常的字母基线。这是默认值。bottom
:基线是 em 方块的底部。hanging
:对文本使用悬挂基线。ideographic
:使用字符体的底部(假设它们突出于字母基线之下)。middle
:文本基线是 em 方块的中间。top
:文本基线是 em 方块的顶部。
清单 4-13 展示了在canvas
上绘制文本是多么容易。
清单 4-13 。在画布上呈现文本
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Draw some text!
myContext.font = ’35px sans-serif’;
myContext.strokeStyle = ’#000’;
myContext.strokeText(’Hello World’, 0, 40);
myContext.textAlign = ’center’;
myContext.fillStyle = ’rgba(200, 50, 25, 0.8)’;
myContext.fillText(’HTML5’, 100, 100);
</script>
</body>
</html>
这个例子对canvas
上的一些文本进行描边和填充。字体足够大,可以显示字母边缘的实际笔划,如图图 4-12 所示。
图 4-12 。呈现在画布上的文本
阴影
canvas
元素也可以根据其上绘制的元素投射阴影。这通常用于文本,但也适用于形状和路径。如果你已经熟悉 CSS 阴影,那么canvas
阴影的参数将会非常熟悉:
Context.shadowBlur
:虚化效果的大小。默认值为 0。Context.shadowColor
:阴影的颜色。可以是任何有效的 CSS 颜色字符串。默认为’rgba(0, 0, 0, 0)’
。Context.shadowOffsetX
:阴影的 x 轴偏移量。默认值为 0。Context.shadowOffsetY
:阴影的 y 轴偏移量。默认值为 0。
清单 4-14 演示了在一些文本上投射阴影。
清单 4-14 。投影
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Add some shadow!
myContext.shadowOffsetX = 2;
myContext.shadowOffsetY = 2;
myContext.shadowBlur = 2;
myContext.shadowColor = "rgba(0, 0, 0, 0.8)";
// Draw some text!
myContext.font = ’35px sans-serif’;
myContext.strokeStyle = ’#000’;
myContext.strokeText(’Hello World’, 0, 40);
myContext.textAlign = ’center’;
myContext.fillStyle = ’rgba(200, 50, 25, 0.8)’;
myContext.shadowOffsetX = 4;
myContext.shadowOffsetY = 4;
myContext.fillText(’HTML5’, 100, 100);
</script>
</body>
</html>
这个例子简单地给清单 4-13 中的代码添加了阴影。它增加了两个不同的阴影偏移量,一个很近,一个很远,如图图 4-13 所示。
图 4-13 。画布上呈现的阴影
保存绘图状态
canvas
API 提供了一种存储绘图上下文当前状态信息的方法。信息存储在一个堆栈中,您可以根据需要从堆栈中推入和拉出状态。可存储的绘图上下文属性如下:
globalAlpha
的当前值- 电流
strokeStyle
和fillStyle
lineCap
、lineJoin
、lineWidth
和miterLimit
中的当前线路设置shadowBlur
、shadowColor
、shadowOffsetX
和shadowOffsetY
中的当前阴影设置- 在
globalCompositeOperation
中设置的当前合成操作 - 当前剪辑路径
- 已应用于绘图上下文的任何转换
这些值共同构成了绘图状态。保存和恢复状态的方法很简单:
Context.save()
:拍摄当前绘图状态的快照,并将值保存在堆栈中。Context.restore()
:从堆栈中删除最近存储的绘图状态,并将其恢复到上下文中。
绘图状态保存在先进先出堆栈中。save 和 restore 方法是访问堆栈和其中存储的状态的仅有的两种方法。
清单 4-15 提供了一个有点做作的保存和恢复绘图状态的演示。
清单 4-15 。保存和恢复绘图状态
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="210">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create an array of colors to load into the stack.
var allTheColors = [’#ff0000’, ’#ff8800’, ’#ffff00’, ’#00ff00’, ’#0000ff’,
’#4b0082’, ’#8f00ff’];
// Load the colors and stroke style into the stack.
for (var i = 0; i < allTheColors.length; i++) {
myContext.strokeStyle = allTheColors[i];
myContext.lineWidth = 30;
myContext.save();
}
// Restore colors from the stack and draw.
for (var i = 0; i < 8; i++) {
myContext.restore();
myContext.beginPath();
myContext.moveTo(0, ((30 * i) + 15));
myContext.lineTo(200, ((30 * i) + 15));
myContext.stroke();
}
</script>
</body>
</html>
本示例以编程方式创建一组具有不同颜色和特定线宽的绘图状态。然后它一次恢复一个状态,并画一条线。
您会注意到每一行的 y 坐标都是基于循环索引的。每条线的描边宽度为 30 个单位:线上 15 个单位,线下 15 个单位。如果你只是从(0, 0)
到(200, 0)
画第一条线,然后描边,你不会看到笔画的前 15 个单元。将每一行下移 15 个单位可确保您将看到第一行和后续每一行的完整笔画宽度。
合成
到目前为止,在你所有的canvas
例子中,当你在canvas
上绘制多个项目时,它们只是一个在另一个之上。canvas
API 提供了在绘制时组合项目的能力,这使您能够进行一些相当复杂的操作。
每当你在canvas
上绘制一个新元素时,合成器会查看已经存在于canvas
上的内容。这个当前内容被称为目的地??。新内容被称为源。然后合成器根据当前活动的合成器参照目的地绘制源。
合成器是使用当前上下文的globalCompositeOperation
属性指定的。可用的合成器如下:
source-over
:在目标内容上绘制源内容。这是默认的合成器。source-atop
:源内容仅在与目标内容重叠的地方绘制。source-in
:仅在源内容和目标内容重叠的地方绘制源内容。其他一切都是透明的。source-out
:源内容仅在不与目标内容重叠的地方绘制。其他一切都是透明的。destination-over
:源内容绘制在目的内容的下面。destination-atop
:源内容仅保留在与目标内容重叠的地方。目标内容绘制在源内容的下方。其他一切都是透明的。destination-in
:源内容只保留在与目的内容重叠的地方。其他一切都是透明的。destination-out
:源内容只保存在不与目的内容重叠的地方。其他一切都是透明的。copy
:仅绘制目标内容。其他一切都是透明的。lighter
:当目标内容和源内容重叠时,通过将两个内容的值相加来确定颜色。xor
:目标内容正常呈现,除非它与源内容重叠,在这种情况下,两者都呈现为透明。
要指定合成器,只需将Context.
globalCompositeOperation
设置为所需的值:
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
myContext.globalCompositeOperation = ’lighter’;
清单 4-16 提供了一种查看不同合成器的方法。
清单 4-16 。画布合成器演示
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<br>
<select id="compositor">
<option value="source-over" selected>source-over</option>
<option value="destination-atop">destination-atop</option>
<option value="destination-in">destination-in</option>
<option value="destination-out">destination-out</option>
<option value="destination-over">destination-over</option>
<option value="source-atop">source-atop</option>
<option value="source-in">source-in</option>
<option value="source-out">source-out</option>
<option value="copy">copy</option>
<option value="lighter">lighter</option>
<option value="xor">xor</option>
</select>
<button id="toggle-triangle">Toggle Triangle</button>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Get references to the form elements.
var mySelector = document.getElementById(’compositor’);
var toggleTriangle = document.getElementById(’toggle-triangle’);
/**
* Draws the example shapes with the specified compositor.
*/
function drawExample() {
// First set the compositing to source-over so we can guarantee drawing the
// first shape.
myContext.globalCompositeOperation = ’source-over’;
myContext.clearRect(0, 0, 200, 200);
myContext.beginPath();
// Draw the circle first.
myContext.arc(60, 100, 40, 0, 7);
myContext.fillStyle = ’#ff0000’;
myContext.fill();
// Change the compositing to the chosen value.
myContext.globalCompositeOperation = mySelector.value;
// Draw a rectangle on top of the circle.
myContext.beginPath();
myContext.fillStyle = ’#0000ff’;
myContext.rect(60, 60, 80, 80);
myContext.fill();
}
/**
* Whether or not to show the triangle.
* @type {boolean}
*/
var showTriangle = false;
/**
* Shows or hides the triangle.
*/
function showHideTriangle() {
if (showTriangle) {
myContext.fillStyle = ’#00ff00’;
myContext.beginPath();
myContext.moveTo(40, 80);
myContext.lineTo(170, 100);
myContext.lineTo(40, 120);
myContext.lineTo(40, 80);
myContext.fill();
} else {
drawExample();
}
}
// Draw the example for the first time.
drawExample();
// Add a change event handler to the selector to redraw the example with the
// chosen compositor.
mySelector.addEventListener(’change’, function() {
showTriangle = false;
drawExample();
}, false);
// Add a click event handler to the toggle button to show or hide the triangle.
toggleTriangle.addEventListener(’click’, function() {
showTriangle = showTriangle ? false : true;
showHideTriangle();
}, false);
</script>
</body>
</html>
这个示例创建了一个简单的选择字段,其中包含所有可用的合成器可供选择。当您选择合成器时,形状将重新绘制。第一个形状(红色圆圈)将总是用source-over
绘制。第二个形状(蓝色方块)将使用新选择的合成器绘制。您可以打开和关闭绿色三角形,以查看它将如何与第一次合成的结果合成。
合成器适用于任何可以在canvas
上绘制的东西,甚至是图像,如清单 4-17 所示。
清单 4-17 。合成照片
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a new image element and fill it with a kitten.
var myImage = new Image();
myImage.src = ’http://www.placekitten.com/g/150/150’;
// We can’t do anything until the image has successfully loaded.
myImage.onload = function() {
// Create a simple gray linear gradient and set it to the fill style.
var myGradient = myContext.createLinearGradient(25, 25, 25, 175);
myGradient.addColorStop(0.1, ’#000’);
myGradient.addColorStop(1, ’rgba(200, 200, 200, 1)’);
myContext.fillStyle = myGradient;
// Draw a square that almost fills the region where the image will be rendered
// and fill it with the gradient.
myContext.beginPath();
myContext.rect(30, 30, 140, 140);
myContext.fill();
// Set the compositor to lighter.
myContext.globalCompositeOperation = ’lighter’;
// Draw the kitten.
myContext.drawImage(myImage, 25, 25);
};
</script>
</body>
</html>
本示例创建一个简单的线性渐变并将其用作正方形的填充样式,然后使用较亮的合成器在其上合成一只小猫的图像。结果示例如图 4-14 所示。
图 4-14 。将渐变与图像合成的结果
使用带有渐变、图案和图像的合成器,你可以用你的canvas
图创建一些非常复杂的效果。
剪辑
您可以将canvas
的绘图区域限制在您定义的任何闭合路径上。这被称为 ?? 削波。通过首先在canvas
上绘制一条路径,然后调用Context.clip()
方法来创建一个裁剪区域,这将把绘制限制在那个区域。您仍然可以描边和填充路径,也可以创建新的路径或其他绘图。可见性将被限制在剪辑区域。
有三种方法可以重置裁剪区域:
- 您可以定义一个包含整个
canvas
的路径,然后剪切到该路径。 - 可以用不同的剪辑区域恢复到以前的绘图状态。
- 您可以通过调整大小来重置整个
canvas
。
清单 4-18 演示了创建一个裁剪区域来限制绘图。
清单 4-18 。创建一个裁剪区域
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a square clipping area.
myContext.beginPath();
myContext.rect(50, 50, 50, 50);
myContext.clip();
// Draw a large circle in the canvas and fill it. Only the portion within
// the clipping area will be visible. myContext.beginPath();
myContext.arc(75, 75, 100, 0, 7);
myContext.fillStyle = ’red’;
myContext.fill();
</script>
</body>
</html>
这个简单的例子首先使用rect
方法创建一个正方形路径,然后将其设置为剪辑区域。然后它画了一个大圆并用红色填充,但是唯一可见的区域是在裁剪区域内,如图图 4-15 所示。
图 4-15 。剪裁的效果
转换
canvas
API 包含了一组改变图形在canvas
上呈现方式的方法:旋转它们,缩放它们,甚至任意改变,如反射或剪切。这些变化被称为变换??。设置转换后,将以指定的方式修改更多的图形。canvas
API 为一些常见的转换提供了一套简化方法:
Context.translate
(translateX, translateY)
:将canvas
的原点从当前位置移动到距离当前原点translateX
单位的新 x 位置和距离当前原点translateY
单位的新 y 位置。Context.rotate
(angle)
:以弧度为单位将canvas
绕原点旋转指定角度。Context.scale
(scaleX, scaleY)
:水平scaleX
和垂直scaleY
缩放canvas
单位。
此外,您可以使用变换方法指定任意变换矩阵:
-
Context.transform(scaleX, skewX, skewY, scaleY, translateX, translateY)
: Transform thecanvas
by applying a transformation matrix specified as:。
rotate
、translate
和scale
的简写方法都映射到转换矩阵,从而调用转换方法。比如Context.translate(translateX, translateY)
映射到Context.transform(1, 0, 0, 1, translateX, translateY)
,Context.scale(scaleX, scaleY)
映射到Context.transform(scaleX, 0, 0, scaleY, 0, 0)
。
注意如果你是线性代数爱好者,所有的
canvas
变换都是仿射变换。
关于canvas
转换,需要记住的重要一点是,它们会影响整个canvas
——一旦转换被实现,它会影响从该点开始绘制的所有内容。Canvas
当你应用两个不同的变换时,第二个变换的结果会基于第一个变换。如果您不仔细管理活动转换并根据需要重置它们,这可能会导致一些意想不到的结果。您可以通过以下三种方式之一重置转换:
- 指定一个称为“单位变换矩阵”的特殊变换,它对绘图没有影响。您可以使用变换方法:
Context.transform(1, 0, 0, 1, 0, 0)
指定该矩阵。 - 恢复先前保存的绘图状态,这会将转换设置为该状态的转换。
- 通过调整大小重置整个
canvas
。
在探索一些更复杂的转换之前,先看一些简单的例子。清单 4-19 演示了一个简单的translate
转换:
清单 4-19 。简单的平移变换
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
/**
* Draws a 100x100 square at (0, 0) in the specified color. Indicates the origin
* corner with a small black square.
* @param {string} color A valid CSS color string.
*/
function drawSquare(color) {
myContext.fillStyle = color;
myContext.beginPath();
myContext.rect(0, 0, 100, 100);
myContext.fill();
myContext.fillStyle = ’#000’;
myContext.beginPath();
myContext.rect(0, 0, 5, 5);
myContext.fill();
}
// Draw a square, fill it with red.
drawSquare(’rgba(255, 0, 0, 0.5)’);
// Translate the canvas.
myContext.translate(20, 40);
// Draw the same square again, fill it with blue.
drawSquare(’rgba(0, 0, 255, 0.5)’);
// Translate the canvas again.
myContext.translate(50, -20);
// Draw the same square again, fill it with green.
drawSquare(’rgba(0, 255, 0, 0.5)’);
</script>
</body>
</html>
这个例子(将构成接下来几个例子的基础)创建了一个在canvas
的原点画一个正方形的简单方法。该函数用指定的颜色填充正方形(或者您可以传入任何有效的fillStyle
)。为了帮助跟踪原点,该函数还在原点处的正方形的角上创建一个小的后凹口。
首先,它在原点画一个正方形,并把它涂成红色。然后它翻译canvas
,并绘制一个蓝色方块。最后,它再次翻译canvas
并绘制一个绿色方块。结果如图图 4-16 所示。
图 4-16 。清单 4-18 的结果
如你所见,平移导致canvas
的原点按照指定移动。
接下来,清单 4-20 在这个例子的基础上应用了旋转和变换:
清单 4-20 。在平移上叠加旋转
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
/**
* Draws a 100x100 square at (0, 0) in the specified color. Indicates the origin
* corner with a small black square.
* @param {string} color A valid CSS color string.
*/
function drawSquare(color) {
myContext.fillStyle = color;
myContext.beginPath();
myContext.rect(0, 0, 100, 100);
myContext.fill();
myContext.fillStyle = ’#000’;
myContext.beginPath();
myContext.rect(0, 0, 5, 5);
myContext.fill();
}
// Draw a square, fill it with red.
drawSquare(’rgba(255, 0, 0, 0.5)’);
// Translate the canvas.
myContext.translate(20, 40);
// Rotate the canvas 45 degrees (about 0.785 radians).
myContext.rotate(0.785);
// Draw the same square again, fill it with blue.
drawSquare(’rgba(0, 0, 255, 0.5)’);
// Translate the canvas again.
myContext.translate(50, -20);
// Rotate the canvas 45 degrees (about 0.785 radians).
myContext.rotate(0.785);
// Draw the same square again, fill it with green.
drawSquare(’rgba(0, 255, 0, 0.5)’);
</script>
</body>
</html>
它使用和以前一样的平移,但是在绘制新的方块之前也增加了一个旋转。结果如图 4-17 所示。
图 4-17 。旋转和平移
这里你可以看到同样的平移和旋转。你可以看到每个方块都绕着它的原点旋转。
最后,你可以看看清单 4-21 中的一些比例变换。
清单 4-21 。缩放和平移变换
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
/**
* Draws a 100x100 square at (0, 0) in the specified color. Indicates the origin
* corner with a small black square.
* @param {string} color A valid CSS color string.
*/
function drawSquare(color) {
myContext.fillStyle = color;
myContext.beginPath();
myContext.rect(0, 0, 100, 100);
myContext.fill();
myContext.fillStyle = ’#000’;
myContext.beginPath();
myContext.rect(0, 0, 5, 5);
myContext.fill();
}
// Draw a square, fill it with red.
drawSquare(’rgba(255, 0, 0, 0.5)’);
// Translate the canvas.
myContext.translate(20, 40);
// Scale the canvas.
myContext.scale(1, 1.5);
// Draw the same square again, fill it with blue.
drawSquare(’rgba(0, 0, 255, 0.5)’);
// Translate the canvas again.
myContext.translate(50, -20);
// Scale the canvas again.
myContext.scale(1.5, 1);
// Draw the same square again, fill it with green.
drawSquare(’rgba(0, 255, 0, 0.5)’);
</script>
</body>
</html>
同样,这个例子建立在清单 4-19 的之上,并使用相同的函数和翻译。这次在画第二个和第三个方块之前增加了一个缩放平移,如图图 4-18 所示。
图 4-18 。缩放和平移
如果仔细观察,您会发现蓝色正方形的原点标记根据您对其应用的缩放变换而略微拉长。如果你比较绿色方块和红色方块的原点标记,你会发现前者的大小是后者的两倍。
对于一个更实际的例子,考虑创建元素的动态反射。转换很容易,如清单 4-22 所示。
清单 4-22 。简单的文字反映
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Draw some text!
myContext.font = ’35px sans-serif’;
myContext.fillStyle = ’#000’;
myContext.fillText(’Hello World’, 10, 100);
// Set a reflection transform.
myContext.setTransform(1, 0, 0, -1, 0, 0);
// Set a slight scale transform.
myContext.scale(1, 1.2);
// Draw the text again with the transforms in place and a light gray fill style.
myContext.fillStyle = ’rgba(100, 100, 100, 0.4)’;
myContext.fillText(’Hello World’, 10, -85);
</script>
</body>
</html>
该示例绘制一些文本,然后对canvas
应用反射变换和缩放变换,然后用浅灰色重新绘制相同的文本。结果如图 4-19 中的所示。
图 4-19 。文本反射
您甚至可以使用渐变作为反射文本的填充样式,从而产生从上到下渐变的阴影
动画
API 没有为动画提供任何本地支持。它没有为其内容增加动画效果的方法,而且正如您所看到的,一旦内容被渲染,它也没有提供引用内容的方法。然而,canvas
提供的绘图工具是如此的低级和高效,以至于你可以用canvas
通过单独绘制每个动画帧来创建动画。
正如你将在第五章的的“动画计时”中看到的,大多数基于 JavaScript 的动画是在计时循环中完成的,用canvas
制作动画也没什么不同。事实上,为了简化动画示例,您将使用您在清单 5-5 中构建的 DrawCycle 构造函数。这将允许你创建一个绘制周期管理器,使用requestAnimationFrame
来最大化你的动画的效率。关于requestAnimationFrame
的详细信息,参见第五章中的“动画计时”。
要使用canvas
制作动画,您必须单独绘制动画的每一帧,清除帧之间的canvas
(如果需要,保存/恢复动画状态)。清单 4-23 说明了这个循环。
清单 4-23 。用画布制作动画
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="500" height="500">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script src="drawcycle.js"></script>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Set the stroke style.
myContext.strokeStyle = ’#000’;
// Create a new draw cycle object that we can use for our animation.
var myDrawCycle = new DrawCycle();
/**
* Draws a circle of specified radius at the specified coordinates.
* @param {number} x The x-coordinate of the center of the circle.
* @param {number} y The y-coordinate of the center of the circle.
* @param {number} rad The radius of the circle.
*/
function drawCircle(x, y, rad) {
myContext.beginPath();
myContext.moveTo(x + rad, y);
myContext.arc(x, y, rad, 0, 7);
myContext.stroke();
}
// Counter for the x-coordinate.
var x = 0;
/**
* Animates a circle from one corner of the canvas to another. Used as an
* animation function for the draw cycle object.
*/
function animateCircle() {
if (x < 500) {
myContext.clearRect(0, 0, 500, 500);
drawCircle(x, x, 10);
x++;
} else {
myDrawCycle.stopAnimation();
}
}
// Add the animation function to the draw cycle object.
myDrawCycle.addAnimation(animateCircle);
// Begin the animation.
myDrawCycle.startAnimation();
</script>
</body>
</html>
如上所述,在制作任何动画之前,您将加载您的绘制周期构造函数。有关绘制循环构造器如何工作的详细信息,请参见第五章。本示例创建一个新的绘制循环实例,并使用它来管理动画计时。
首先创建一个在指定位置画圆的函数。然后创建实际的动画函数,在每个循环中在新的位置画圆。然后,用绘制周期注册动画函数,并开始动画。这个例子简单地从canvas
的一个角到另一个角制作了一个圆的动画。
因为你必须在canvas
上分别绘制每一帧,而且因为动画帧的计时如此之快,你将很快碰到效率极限。要制作复杂的动画,您通常需要一个框架来帮助您管理效率,提供基本的动画功能,如运动、弹跳和摩擦的物理功能,并使创建和管理单个动画更加容易。
互动
由于canvas
是 DOM 中的一个元素,用户可以像其他 DOM 元素一样与它交互。一个canvas
元素将调度所有常见的 DOM 事件,如鼠标事件和触摸事件;您可以像附加任何其他元素一样附加事件处理程序。然而,canvas
不分派任何新的事件,也不提供访问任何内部绘制的东西的方法。
使用鼠标事件,创建一个允许用户在canvas
上绘图的应用是非常容易的,如清单 4-24 所示。
清单 4-24 。用鼠标在画布上绘图
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
cursor: crosshair;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="500" height="500">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
myContext.strokeStyle = ’#000’;
// Whether or not the mouse button is being pressed.
var isMouseDown = false;
// Add a mousedown event listener that will set the isMouseDown flag to true,
// and move the pen to the new starting location.
myCanvas.addEventListener(’mousedown’, function(event) {
myContext.moveTo(event.clientX, event.clientY);
isMouseDown = true;
}, false);
// Add a mouseup event handler that will set the isMouseDown flag to false.
myCanvas.addEventListener(’mouseup’, function(event) {
isMouseDown = false;
}, false);
// Add a mousemove event handler that will draw a line to the current mouse
// coordinates.
myCanvas.addEventListener(’mousemove’, function(event) {
if (isMouseDown) {
window.requestAnimationFrame(function() {
myContext.lineTo(event.clientX, event.clientY);
myContext.stroke();
});
}
}, false);
</script>
</body>
</html>
要在canvas
上绘图,您希望当用户按下鼠标按钮时开始绘图,当用户松开鼠标按钮时停止绘图。因此,您需要 mousedown 和 mouseup 事件处理程序来设置一个标志,指示鼠标按钮的状态。mousedown 事件处理程序还将笔移动到新的位置,这样您就不会意外地从最后一个停止点到新的起点绘制一条线。然后,您需要一个 mousemove 事件处理程序,该处理程序绘制一条指向当前鼠标指针坐标的线,假设用户按住鼠标按钮。为了保持效率,使用requestAnimationFrame
方法;关于这种方法如何工作的详细信息,请参见第五章的中的“动画计时”。最后,使用 CSS 将光标变为canvas
元素的十字准线。
当你使用这个例子时,你会注意到它没有画在光标的正中间。相反,它绘制在光标的右下角附近。mousemove 事件处理程序从 DOM 传递给它的事件对象接收坐标,这些坐标有一点偏差,因为光标本身的大小是非零的。为了说明这一点,您所要做的就是将坐标偏移几个像素——准确地说,是光标宽度和高度的一半。新的事件处理程序如下所示:
// Add a mousemove event handler that will draw a line to the current mouse
// coordinates, with a slight offset.
myCanvas.addEventListener(’mousemove’, function(event) {
if (isMouseDown) {
window.requestAnimationFrame(function() {
myContext.lineTo(event.clientX - 7, event.clientY - 7);
myContext.stroke();
});
}
}, false);
现在,该示例将直接绘制在十字线下。
摘要
本章深入探讨了 HTML5 canvas
元素。它涵盖了所有重要功能,包括:
- 绘制形状和线条
- 绘图文本
- 对图像使用
canvas
元素 - 剪裁和遮罩
- 转换
- 带有
canvas
元素的基本动画 - 处理用户与
canvas
元素的交互
HTML5 canvas
元素为直接在网页上绘图提供了一个相当低级但灵活的 API。它在桌面和移动浏览器中也享有广泛的支持,这使它成为移动应用的一个很好的候选。
在第五章中,你会看到一些与 HTML5 相关的 JavaScript APIs,但它们不是规范的直接组成部分。
五、相关标准
HTML5 标准涵盖了大量内容,但它并不是 W3C 开发的唯一新的 web 技术。有一系列技术也是对网络平台的增强,但不属于 HTML5 的范畴。在这一章中,我将介绍一些更令人兴奋的新技术,特别关注为移动设备设计的技术。
地理位置〔??〕
维持价
优秀
所有现代浏览器都支持这些特性,并且在最近的三个版本中都有。
W3C 推荐:http://www.w3.org/TR/geolocation-API/
地理定位是确定托管浏览器的设备的物理位置的能力,通常是根据纬度和经度。地理定位对于移动设备非常重要,它与地图应用、提醒、紧急应答器甚至游戏(如 Ingress 参见https://www.ingress.com/
。
设备可以使用多种技术来确定您的位置:
- GPS 卫星 : 几乎所有现代智能手机和其他移动设备都有能够与全球定位系统卫星通信的收发器。
- 蜂窝塔 : 使用三角测量算法,可以根据蜂窝设备与蜂窝塔的通信来确定其位置(大致)。
- Wi-Fi 地图 : Wi-Fi 接入点往往非常固定且范围有限,因此只需带着支持 Wi-Fi 的设备四处行驶,就可以创建 Wi-Fi 接入点的“地图”。使用这样的地图,人们可以根据给定设备的范围内有哪些 Wi-Fi 接入点来确定该设备的大致位置。
- 蓝牙映射 : 类似 Wi-Fi 映射;最适合近距离地理定位。
- IP 地址映射 : 对于非移动设备,可以根据其外部 IP 地址确定其位置。一些公司提供 IP 地址映射服务。
所有这些方法都不精确,并且有它们自己的局限性,但是当一起使用时,它们可以提供设备的精确位置。但是,不能保证它们将返回设备的实际位置,或者以有用的准确度返回。
不过,当它们配合良好时,就有可能相当精确地定位设备。这就是为什么当你的 Wi-Fi 关闭时,大多数移动设备会警告你地理定位的准确性会受到影响。例如,当你关闭 iPhone 上的 Wi-Fi 无线电时,iOS 会警告你,你的定位精度会降低,如图图 5-1 所示。
图 5-1 。iOS 定位精度
隐私考虑
显然,地理定位有严重的隐私问题。定位和跟踪设备——以及携带设备的人——是一项强大的功能。因此,所有浏览器都实现了警告系统,通知用户他们的位置将被跟踪。当你的应用第一次访问地理定位 API 时,浏览器会通知用户并给他们阻止定位的选项。这些警告旨在引起注意,但因浏览器而异(图 5-2 )
图 5-2 。来自各种浏览器的地理位置警告
在所有的浏览器中,你的脚本都会暂停并等待用户对对话框做出响应。如果用户选择允许地理定位,脚本将继续。如果用户决定阻止地理定位,API 将抛出一个错误。
当您构建支持地理定位的应用时,考虑用户的隐私和安全需求非常重要:
- 您应该只在需要时请求位置数据。这对于隐私/安全和移动设备电池寿命都很重要,因为地理位置查询会激活移动设备中的多个无线电,因此会非常消耗电池。
- 您应该只请求满足您特定目的所需的足够的地理位置信息。
- 您应该仅将该信息用于特定目的,并且一旦达到目的,您应该从内存中清除地理位置数据。
- 您应该小心您的应用如何共享和传输地理位置数据。地理位置数据在任何网络上的任何传输都应该是安全的,以防止未经授权的访问。
- 如果您的应用需要将地理位置数据发送到服务器进行进一步处理,那么您应该更加小心服务器软件处理和存储数据的方式,牢记物理安全和法律后果。
这些似乎是显而易见的准则,事实上它们是处理任何敏感信息的基本准则。但是当你忙于编码时,很容易忽略这些简单的想法,所以一定要从一开始就把它们包含在你的工作中。
你也应该对你的用户透明,你的应用收集和处理地理位置数据。你应该告诉他们:
- 你收集什么数据;
- 你为什么收集它;
- 您是否共享或传输数据,以及您采取了哪些安全措施来保护该通信;和
- 您是否存储数据,以及您采取什么安全措施来保护存储。如果您确实存储了这些信息,您应该告诉他们您如何保护这些信息,以及用户如何从您的存储中删除他们的信息。
如果可能的话,您还应该为用户提供一种选择退出应用地理定位功能的方式。当然,有时这是不实际的,但是为用户提供一种控制这种特性的方法将对建立信任有很大帮助。
地理定位 API
地理定位 API 指定了一个新的navigator.geolocation
对象。这个对象有三个新方法来访问浏览器和托管设备 的地理定位功能。由于解析设备的位置可能需要未知的时间量(脚本将第一次暂停,并在继续之前等待用户响应许可对话框,然后必须查询各种定位方法,每个方法可能需要未知的时间量),这些方法是异步的,并提供了注册成功和错误回调函数的方法。
提示你可以使用承诺(在移动浏览器中得到很好的支持)来帮助简化异步动作的代码。参见附录 A 中关于承诺的部分。
navigator.geolocation.getCurrentPosition
(successCallback, errorCallback, PositionOptions)
:成功返回位置时调用successCallback
,出错时调用errorCallback
。当调用successCallback
时,它将接收一个Position
对象作为参数,当调用errorCallback
时,它将接收一个PositionError
对象作为参数。navigator.geolocation.watchPosition
(successCallback, errorCallback, PositionOptions)
:立即返回一个PositionWatch
标识,然后每次设备位置变化时调用successCallback
函数。如果尝试解析位置失败,则调用errorCallback
。当调用successCallback
时,它将接收一个Position
对象作为参数,当调用errorCallback
时,它将接收一个PositionError
对象作为参数。navigator.geolocation.clearWatch
(PositionWatch)
:停止由PositionWatch
值指定的watchPosition
呼叫。
此外,API 定义了三个新的对象模板:PositionOptions
对象、Position
对象和PositionError
对象。PositionOptions
对象为getCurrentPosition
和watchPosition
方法提供了一个接口来微调查询和结果,如下所示。
PositionOptions = {
// Specifies whether the query should return the most accurate location possible
boolean enableHighAccuracy,
// The number of milliseconds to wait for the device to return a location
number timeout,
// The number of milliseconds a cached value can be used.
number maximumAge
}
Position
对象定义了在成功解析主机设备的位置时将由getCurrentPosition
和watchPosition
方法返回的响应,如下所示。
Position = {
object coords : {
// The latitude in decimal degrees.
number latitude,
// The longitude in decimal degrees.
number longitude,
// The altitude in meters above nominal sea level.
number altitude,
// The accuracy of the latitude and longitude values, in meters.
number accuracy,
// The accuracy of the altitude value, in meters.
number altitudeAccuracy,
// The current heading of the device in degrees clockwise from true north.
number heading,
// The current ground speed, in meters per second.
number speed,
},
// The time when the location query was successfully created.
date timestamp
}
注意,根据浏览器对地理定位标准的实现和主机设备的能力,用于altitude
、accuracy
、altitudeAccuracy
、heading
和speed
的值可以作为null
返回。
PositionError
对象定义了如果用户拒绝允许地理定位,或者如果设备无法解析其位置时将返回的响应,如下所示。
PositionError = {
// The numeric code of the error (see table below).
number code,
// A human-readable error message.
string message
}
PositionError.code
的有效代码为整数,如表 5-1 所示。
表 5-1 。有效PositionError
代码
|
密码
|
常数
|
描述
|
| --- | --- | --- |
| Zero | UNKNOWN_ERROR
| 由于未知错误,设备无法解析其位置。 |
| one | PERMISSION_DENIED
| 应用没有使用地理定位服务的权限,通常是由于用户拒绝权限。 |
| Two | POSITION_UNAVAILABLE
| 设备无法解析其位置,因为服务不可用。(通常在各种所需的无线电被停用时返回,例如当移动设备处于“飞行模式”时) |
| three | TIMEOUT
| 设备无法在PositionOptions.timeout
指定的超时限制内解析其位置。 |
使用这个 API 最简单的例子是做一个简单的位置查询 并显示所有返回的值,如清单 5-1 所示。
清单 5-1 。地理定位 API 的基本查询
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Geolocation Example</h1>
<div id="locationValues">
</div>
<div id="error">
</div>
<script>
/**
* The success callback function for getCurrentPosition.
* @param {Position} position The position object returned by the geolocation
* services.
*/
function successCallback(position) {
console.log(’success’)
// Get a reference to the div we’re going to be manipulating.
var locationValues = document.getElementById(’locationValues’);
// Create a new unordered list that we can append new items to as we enumerate
// the coords object.
var myUl = document.createElement(’ul’);
// Enumerate the properties on the position.coords object, and create a list
// item for each one. Append the list item to our unordered list.
for (var geoValue in position.coords) {
var newItem = document.createElement(’li’);
newItem.innerHTML = geoValue + ’ : ’ + position.coords[geoValue];
myUl.appendChild(newItem);
}
// Add the timestamp.
newItem = document.createElement(’li’);
newItem.innerHTML = ’timestamp : ’ + position.timestamp;
myUl.appendChild(newItem);
// Enumeration complete. Append myUl to the DOM.
locationValues.appendChild(myUl);
}
/**
* The error callback function for getCurrentPosition.
* @param {PositionError} error The position error object returned by the
* geolocation services.
*/
function errorCallback(error) {
var myError = document.getElementById(’error’);
var myParagraph = document.createElement(’p’);
myParagraph.innerHTML = ’Error code ’ + error.code + ’\n’ + error.message;
myError.appendChild(myParagraph);
}
// Call the geolocation services.
navigator.geolocation.getCurrentPosition(successCallback, errorCallback);
</script>
</body>
</html>
首先,这个例子创建了一个成功的回调函数,它枚举了Position
对象的属性。这样做的时候,它会将它们添加到一个无序列表中,这个列表会附加到 DOM 中,这样您就可以看到它了。错误回调的行为与此类似,只是它不生成列表,而是简单地更新一个段落的内容。
第一次运行这个示例时,您的浏览器应该提示您获得访问地理位置 API 的权限。第一次通过时,拒绝权限,这样您就可以看到错误情况是什么样子了。图 5-3 显示了在 Chrome 中生成的页面。
图 5-3 。Chrome 中列表 5-1 的错误情况
您可以看到错误处理程序是用错误代码 1 调用的。错误消息的实际文本因浏览器而异(例如,Internet Explorer 11 使用错误消息“此站点无权使用地理定位 API。”)但是错误码是一样的。
地理定位规范没有定义必须呈现给用户的权限模型,这就是为什么每个浏览器都有不同的做法。说明书只是说,
未经用户明确许可,用户代理不得向网站发送位置信息。用户代理必须通过用户界面获得许可,除非他们与用户有预先安排的信任关系,如下所述。用户界面必须包括文档 URI 的主机组件。通过用户界面获得的并且在当前浏览会话之后(即,在浏览上下文被导航到另一个 URL 的时间之后)被保留的那些权限必须是可撤销的,并且用户代理必须尊重被撤销的权限。
一些用户代理将具有预先安排的信任关系,不需要这样的用户界面。例如,当网站执行地理定位请求时,网络浏览器将呈现用户界面,而当使用位置信息来执行 E911 功能时,VOIP 电话可能不呈现任何用户界面。
因此,用户如何授予或拒绝地理位置许可、该决定被记住多长时间以及用户如何在以后改变主意,都取决于浏览器制造商来决定和实现。
例如,在 Internet Explorer 中,用户会看到一个弹出窗口,允许他们选择一些有趣的选项,如图 5-4 所示。
图 5-4 。Internet Explorer 中的地理位置许可选项 11
如果用户选择“允许一次”或“总是允许”,脚本将继续运行,浏览器将尝试解析客户端的位置。“允许一次”选项可能应该读作“允许此浏览会话”,因为该权限一直有效,直到用户关闭并重新启动浏览器。此时,重新访问页面会重新提示用户。选项“Always allow”如你所料地起作用:一旦用户选择了它,他们将不再被提示许可。“总是拒绝且不要告诉我”选项在此时以及用户每次访问该页面时拒绝权限。他们永远不会被重新提示权限,他们可以撤销此决定的唯一方法是打开 Windows 的 Internet 选项对话框,选择“隐私”选项卡,然后单击“位置”部分中的“清除站点”按钮,这将清除授予或拒绝所有站点的所有永久权限。
Firefox 呈现给用户的是完全不同的交互,如图图 5-5 所示。
图 5-5 。火狐 29 中的地理位置许可选项
如果用户选择“共享位置” ,脚本将继续运行,浏览器将尝试解析客户端的位置。但是,与 Internet Explorer 不同的是,此权限不适用于当前的浏览器会话,而仅适用于当前对网站的访问。重新加载页面将立即再次提示用户权限。用户不必重启浏览器。“总是共享位置”选项授予共享位置的永久权限,而“从不共享位置”则作为页面权限的永久拒绝。选择“不是现在”或单击弹出窗口右上角的×图标,或单击弹出窗口之外的任何地方,将关闭弹出窗口,而不授予或拒绝权限,并将使您的应用挂起。可以通过点击 URL 旁边的“目标”图标来重新打开弹出窗口,但这并不一定是显而易见的。这种行为是有意的;参见相关的 Bugzilla bug,https://bugzilla.mozilla.org/show_bug.cgi?id=675533
,获取解释。
只有在 iOS 上的 Safari Mobile 中,权限弹出窗口才是真正的模态弹出窗口,需要用户做出响应,除非他们做出选择,否则不能被取消。在所有其他情况下,用户可以忽略(在 Firefox 中完全忽略)弹出窗口,让您的脚本等待执行回调。更糟糕的是,处于这种未定义状态的时间不计入您可能用PositionOption.timeout
指定的任何超时——只有在用户授予权限并且浏览器开始尝试解析位置后,计时器才开始运行。
为了解决这个问题,您需要实现一个全局超时计时器,该计时器在脚本访问地理位置 API 时开始运行。如果用户授予(或拒绝)权限,我们的常规回调应该发生,这个全局计时器应该被取消。如果用户不授予(或拒绝)权限,全局计时器应该执行一个回调来做一些事情—例如,将浏览器重定向到一个错误页面,向用户解释他们需要做什么才能继续。或者,如果您的应用不需要 GPS,全局计时器回调应该取消成功和错误回调,您的应用可以继续。
很容易将这样的全局计时器添加到清单 5-1 的中,如清单 5-2 中的所示。
清单 5-2 。注册全局超时
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Geolocation Example</h1>
<div id="locationValues">
</div>
<div id="error">
</div>
<script>
// Create the variable that will hold the timer reference.
var globalTimeout = null;
/**
* The success callback function for getCurrentPosition.
* @param {Position} position The position object returned by the geolocation
* services.
*/
function successCallback(position) {
// Check the state of the global timeout. If it is null, the application has
// timed out and we should not continue. If it isn’t null, the timeout timer
// is still running, so we should cancel it and continue.
if (globalTimeout == null) {
return;
} else {
clearTimeout(globalTimeout);
}
// Get a reference to the div we’re going to be manipulating.
var locationValues = document.getElementById(’locationValues’);
// Create a new unordered list that we can append new items to as we enumerate
// the coords object.
var myUl = document.createElement(’ul’);
// Enumerate the properties on the position.coords object, and create a list
// item for each one. Append the list item to our unordered list.
for (var geoValue in position.coords) {
var newItem = document.createElement(’li’);
newItem.innerHTML = geoValue + ’ : ’ + position.coords[geoValue];
myUl.appendChild(newItem);
}
// Add the timestamp.
newItem = document.createElement(’li’);
newItem.innerHTML = ’timestamp : ’ + position.timestamp;
myUl.appendChild(newItem);
// Enumeration complete. Append myUl to the DOM.
locationValues.appendChild(myUl);
}
/**
* The error callback function for getCurrentPosition.
* @param {PositionError} error The position error object returned by the
* geolocation services.
*/
function errorCallback(error) {
// Check the state of the global timeout. If it is null, the application has
// timed out and we should not continue. If it isn’t null, the timeout timer
// is still running, so we should cancel it and continue.
if (globalTimeout == null) {
return;
} else {
clearTimeout(globalTimeout);
}
var myError = document.getElementById(’error’);
var myParagraph = document.createElement(’p’);
myParagraph.innerHTML = ’Error code ’ + error.code + ’\n’ + error.message;
myError.appendChild(myParagraph);
}
/**
* The callback to execute if the whole process times out, specifically in the
* situation where a user ignores the permissions pop-ups long enough.
*/
function globalTimeoutCallback() {
alert(’Error: GPS permission not given, exiting application.’);
globalTimeout = null;
}
// Call the geolocation services.
navigator.geolocation.getCurrentPosition(successCallback, errorCallback);
// Start the timer for the global timeout call.
globalTimeout = setTimeout(globalTimeoutCallback.bind(this), 5000);
</script>
</body>
</html>
这个例子做的第一件事是定义一个globalTimeout
变量,它将保存计时器的标识符,当它启动地理定位请求时,计时器将启动。接下来,注意在successCallback
和errorCallback
函数中,它检查了globalTimeout
变量的状态。如果变量是null
,全局超时已经过期,代码不应该继续执行那些函数。如果不是null
,定时器仍然是活动的,所以代码应该取消它并继续。
接下来,它提供了一个globalTimeoutCallback
函数 ,简单地向用户发出一条消息。在实际的应用中,您可能希望在这里做一些更有用的事情——例如,将用户重定向到另一个页面。代码还将globalTimeout
变量设置为null
,这样,如果任何一个回调以某种方式被执行,它们将不会继续通过初始的全局超时检查。
最后,它在调用地理位置 API 后立即设置运行 的计时器。计时器设定为五秒钟。当您加载此页面时,您将看到以下内容之一:
- 如果您已经永久拒绝了对该页面的地理位置许可,那么
errorCallback
将会执行并且全局计时器将会被取消。将不显示任何权限弹出窗口。 - 如果您已经永久地允许了对页面的地理位置许可,那么
successCallback
将会执行,全局计时器将会被取消。将不显示任何权限弹出窗口。 - 如果您尚未永久授予或拒绝权限,将显示权限弹出窗口。您可以选择在全局超时计时器过期之前授予或拒绝权限,在这种情况下,将执行适当的回调,全局计时器将被取消。或者您可以什么都不做,等待全局计时器超时。发生这种情况时,将会出现警告消息。
在任何情况下,都不能以编程方式强制用户选择权限。他们必须通过浏览器提供的对话框进行权限选择。
从用户交互的角度来看,这有点令人遗憾,因为这意味着你的应用会让浏览器显示一个你无法控制的通知。一些用户可能会发现这令人担忧,并选择拒绝许可,甚至完全关闭浏览器,永远不返回您的应用。如果你已经向你的用户公开了你的应用是如何收集和存储地理位置信息的,他们会为这种互动做好准备,并且更愿意给予许可,因为他们知道你的应用将如何处理这些数据。
动画定时
维持价
好的
所有现代的浏览器都支持这些特性,并且在最近的两个版本中都有。
W3C 候选推荐:http://www.w3.org/TR/animation-timing/
动画计时标准旨在帮助您构建基于 JavaScript 的可视化动画。如果您曾经尝试过使用 JavaScript 手工制作动画,您可能对绘制周期的简单模式很熟悉:
- 创建一个 draw 函数,负责增量地“绘制”动画项目:定位元素、更改元素属性、在一个
canvas
元素上绘制等等。每次调用这个函数时,它都会产生一个完整的动画“帧”,就像您正在手工绘制动画帧,然后在电影中放映一样。 - 每隔几毫秒调用一次 draw 函数。
JavaScript 绘制周期通常使用计时器实现,计时器每隔几毫秒调用一次绘制函数。在清单 5-3 中可以看到一个例子。
清单 5-3 。基于定时器的绘制周期的 JavaScript 实现
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
#target-element {
width: 100px;
height: 100px;
background-color: #ccc;
position: absolute;
top: 100px;
left: 0px;
}
</style>
</head>
<body>
<h1>Simple Animation Example</h1>
<div id="target-element"></div>
<script>
// Get a reference to the element we want to move.
var targetEl = document.getElementById(’target-element’);
// Create a variable to keep track of its position.
var currentPosition = 0;
/**
* Draws the animation by updating the position on the target element and incrementing
* the position variable by 1.
*/
function draw() {
if (currentPosition > 500) {
// Stop the animation, otherwise it would run indefinitely.
clearInterval(animInterval);
} else {
// Update the element’s position.
targetEl.style.left = currentPosition++ + ’px’;
}
}
// Initiate the animation timer.
var animInterval = setInterval(draw, 17);
</script>
</body>
</html>
这个例子使用一个 JavaScript 定时器来更新页面上一个 div 的位置。更新之间的间隔是 17 毫秒。这不是一个任意的数字。大多数显示器的刷新频率是 60Hz,因此大多数浏览器都试图将屏幕重绘频率限制在 60Hz 以内。每秒 60 个周期大约是 17 毫秒。再快一点,你就会丢失“帧”
根据运行本示例所使用的浏览器和系统,这个动画可能看起来很流畅,也可能有点不流畅。那是因为这是一个动画的蛮力方法,没有考虑到浏览器如何重绘页面。它只是命令屏幕更新,浏览器必须尽最大努力。此外,不能保证动画更新之间的时间是 17 毫秒。setInterval
方法只是将更新添加到浏览器的 UI 队列中,如果浏览器忙于做其他事情(比如调整窗口大小,或者可能在后台获取和呈现其他内容),这很容易陷入困境,从而延迟屏幕呈现。
总的来说,这种方法扩展性不好。随着动画数量和复杂性的增加,以及它们所在的页面的复杂性和交互能力的增加,这些基于定时器的动画队列变得越来越低效。
动画计时规范通过提供一个新的计时器:requestAnimationFrame
解决了基于 JavaScript 的计时器的问题。从语法上来说,这个方法的使用类似于现有的 JavaScript 定时器方法setInterval
和setTimeout
。然而,在幕后,这种新方法与浏览器的屏幕管理算法紧密相连。因此,requestAnimationFrame
有一些重要的好处:
- 用
requestAnimationFrame
排队的动画被浏览器优化成一个回流/重画周期。 - 使用
requestAnimationFrame
排队的动画可以很好地播放来自其他来源的动画,比如 CSS 过渡。 - 浏览器将停止不可见的浏览器标签中的动画。这在移动设备上非常重要,因为密集的动画会快速消耗电池电量。
该规范在全局上下文中创建了两个新方法:
requestAnimationFrame(callback)
:请求将功能callback
作为下一个动画周期的一部分执行。回调将接收一个时间戳作为参数。像setTimeout
和setInterval
,requestAnimationFrame
返回一个可以用来停止循环的标识符。cancelAnimationFrame(identifier)
:取消标识符标识的动画帧请求。
将清单 5-3 更新为使用requestAnimationFrame
很容易,如清单 5-4 所示。
清单 5-4 。清单 5-3 使用requestAnimationFrame
重写
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
#target-element {
width: 100px;
height: 100px;
background-color: #ccc;
position: absolute;
top: 100px;
left: 0px;
}
</style>
</head>
<body>
<h1>Simple requestAnimationFrame Example</h1>
<div id="target-element"></div>
<script>
var targetEl = document.getElementById(’target-element’);
var currentPosition = 0;
/**
* Updates the position on the target element, the increments the position
* counter by 1.
*/
function animateElement() {
// Stop the animation, otherwise it would run indefinitely.
if (currentPosition <= 500) {
requestAnimationFrame(animateElement);
}
// Update the element’s position.
targetEl.style.left = currentPosition++ + ’px’;
}
// Initiate the animation timer.
animateElement();
</script>
</body>
</html>
这个例子更新了animateElement
函数来使用requestAnimationFrame
。每次调用该方法时,它都会更新元素的位置并递增位置计数器。它还安排自己通过requestAnimationFrame
再次呼叫。一旦元素到达 500 像素的位置,动画就会停止。
使用动画计时构建绘制周期管理器也很容易。绘制周期管理器将允许你注册动画函数(如清单 5-4 中的animateElement
函数),并开始、停止和暂停绘制周期。清单 5-5 展示了一个简单的绘制周期管理器。
清单 5-5 。拉伸循环管理器
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
.animatable {
width: 100px;
height: 100px;
background-color: #ccc;
position: absolute;
top: 110px;
left: 0px;
}
#elementTwo {
top: 220px;
}
</style>
</head>
<body>
<h1>Simple Animation Framework Example</h1>
<div class="animatable" id="elementOne"></div>
<div class="animatable" id="elementTwo"></div>
<button id="startAnimation">Start Animation</button>
<button id="togglePause">Toggle Pause</button>
<button id="stopAnimation">Stop Animation</button>
<button id="registerOne">Register Animation One</button>
<button id="unregisterOne">Unregister Animation One</button>
<button id="registerTwo">Register Animation Two</button>
<button id="unregisterTwo">Unregister Animation Two</button>
<script>
// Get references to the elements we will be animating, and create position
// tracking variables for them.
var elementOne = document.getElementById(’elementOne’);
var elOnePosition = 0;
var elementTwo = document.getElementById(’elementTwo’);
var elTwoPosition = 0;
/**
* Animates Element One by incrementally updating its left position. Animation
* stops at 500px.
*/
function animateElementOne() {
if (elOnePosition <= 500) {
elementOne.style.left = elOnePosition++ + ’px’;
} else {
// Done animating, so remove this animation from the draw cycle manager.
myCycle.removeAnimation(animateElementOne);
// Reset the counter so we can animate again. The function can be
// re-registered and will work as before.
elOnePosition = 0;
}
}
/**
* Animates Element Two by incrementally updating its left position. Animation
* stops at 500px.
*/
function animateElementTwo() {
if (elTwoPosition <= 500) {
elementTwo.style.left = elTwoPosition++ + ’px’;
} else {
// Done animating, so remove this animation from the draw cycle manager.
myCycle.removeAnimation(animateElementTwo);
// Reset the counter so we can animate again. The function can be
// re-registered and will work as before.
elTwoPosition = 0;
}
}
/**
* Creates a draw cycle object that will repetitively draw animation functions.
* @constructor
* @returns {Object} A new draw cycle object.
*/
var DrawCycle = function() {
var newCycle = {
/**
* The identifier for the current animation frame loop.
* @type {Number}
*/
animationPointer: null,
/**
* @type {Boolean}
*/
isPaused: false,
/**
* The array of animation callbacks.
* @type {!Array.<Function>}
*/
arrCallbacks: [],
/**
* Starts the animation cycle.
*/
startAnimation: function() {
// Like other JavaScript timers, requestAnimationFrame sets the execution
// context of its callbacks to the global execution context (the window
// object). We need the execution context to be ’this’, the newCycle
// object we’re creating. By using the bind method (which exists on
// Function.prototype) we are able to override the default execution
// context with the one we need.
this.animationPointer = window.requestAnimationFrame(this.draw.bind(this));
},
/**
* Stops the animation cycle.
*/
stopAnimation: function() {
window.cancelAnimationFrame(this.animationPointer);
},
/**
* Pauses the invocation of the animation functions each draw cycle. If set
* to true, the animation functions will not be invoked. If set to false,
* the functions will be invoked.
* @type {Boolean}
*/
pauseAnimation: function(boolPause) {
this.isPaused = boolPause;
},
/**
* Adds an animation function to the draw cycle.
* @param {Function}
*/
addAnimation: function(callback) {
if (this.arrCallbacks.indexOf(callback) == -1) {
this.arrCallbacks.push(callback);
}
},
/**
* Removes an animation function from the draw cycle.
* @param {Function}
*/
removeAnimation: function(callback) {
var targetIndex = this.arrCallbacks.indexOf(callback);
if (targetIndex > -1) {
this.arrCallbacks.splice(targetIndex, 1);
}
},
/**
* Draws any registered animation functions (assuming they are not paused)
* and then kicks off another animation cycle.
* You should not need to call this method directly.
* @private
*/
draw: function() {
if (!this.isPaused) {
var i = 0, arrCallbacksLength = this.arrCallbacks.length;
for (i = 0; i < arrCallbacksLength; i++) {
this.arrCallbacks[i]();
}
}
this.startAnimation();
}
};
return newCycle;
};
// Create a new draw cycle object.
var myCycle = new DrawCycle();
// Register a callback for the Start Animation button that starts the animation
// cycle.
var startAnimation = document.getElementById(’startAnimation’);
startAnimation.addEventListener(’click’, function() {
myCycle.startAnimation();
}, false);
// Register a callback for the Pause Animation button that pauses/unpauses the
// animation cycle.
var pauseAnimation = document.getElementById(’togglePause’);
pauseAnimation.addEventListener(’click’, function() {
myCycle.pauseAnimation(!myCycle.isPaused);
}, false);
// Register a callback for the Stop Animation button that stops the animation
// cycle.
var stopAnimation = document.getElementById(’stopAnimation’);
stopAnimation.addEventListener(’click’, function() {
myCycle.stopAnimation();
}, false);
// Register a callback for the Register Animation One button that adds the
// animation function for element one to the draw cycle object.
var registerOne = document.getElementById(’registerOne’);
registerOne.addEventListener(’click’, function() {
myCycle.addAnimation(animateElementOne);
}, false);
// Register a callback for the Unregister Animation One button that removes the
// animation function for element one from the draw cycle object.
var unregisterOne = document.getElementById(’unregisterOne’);
unregisterOne.addEventListener(’click’, function() {
myCycle.removeAnimation(animateElementOne);
}, false);
// Register a callback for the Register Animation Two button that adds the
// animation function for element two to the draw cycle object.
var registerTwo = document.getElementById(’registerTwo’);
registerTwo.addEventListener(’click’, function() {
myCycle.addAnimation(animateElementTwo);
}, false);
// Register a callback for the Unregister Animation Two button that removes the
// animation function for element two from the draw cycle object.
var unregisterTwo = document.getElementById(’unregisterTwo’);
unregisterTwo.addEventListener(’click’, function() {
myCycle.removeAnimation(animateElementTwo);
}, false);
</script>
</body>
</html>
此示例创建一个构造函数,为您提供一个新的绘制周期对象,该对象为处理动画提供了一个简化的 API。主要的 API 方法有:
addAnimation(animationFunction)
:注册绘图循环的动画功能。每次绘制周期运行时,animationFunction
将被调用。removeAnimation(animationFunction):
取消绘制循环的动画功能。startAnimation()
:开始动画绘制循环。当被调用时,这个方法将使用对象的draw
方法作为回调来调用requestAnimationFrame
,从而启动一个单独的循环。该方法存储循环的标识符,以便以后需要时可以取消它。stopAnimation()
:停止动画绘制循环。当被调用时,这个方法用由startAnimation
存储的标识符调用cancelAnimationFrame
。pauseAnimation(boolPause)
:暂停或不暂停调用已注册的动画功能。绘制周期仍在运行,但没有调用任何动画功能。
使用这个动画 API 很简单:
- 使用构造函数创建绘制循环的新实例。
- 注册一个或多个动画回调,您希望在每个绘制周期调用它们。
- 开始动画循环。
当你调用startAnimation
时,它从浏览器请求一个动画帧,用draw
方法作为回调。浏览器在适当的时候调用draw
方法。draw
方法调用所有注册的动画函数(假设动画没有暂停),从而完成一个循环。然后它调用startAnimation
开始新的周期。
您可以根据需要动态添加新的动画功能;它们将在下一个绘制周期自动调用。您也可以根据需要移除动画功能。每个动画方法还会在完成时将其自身从绘制循环中移除,并重置其计数器。您可以在此时重新注册动画功能,动画将再次出现。注意,即使没有注册动画函数,绘制周期也会继续运行,所以当你移除最后一个动画函数时,你也应该确保调用stopAnimation
方法 。
选择器
维持价
优秀
所有现代浏览器都支持这些特性,并且在最近的四个版本中都有。
W3C 候选人推荐:http://www.w3.org/TR/selectors/
新的选择器标准提供了访问 DOM 中元素的新方法。以前,访问 DOM 中元素的主要方式是使用getElementById
方法、使用遍历或者两者的组合。有了新的选择器标准,您可以基于元素的 CSS 选择器直接访问元素。
选择器标准从 jQuery 等流行的 JavaScript 框架中得到启示,这些框架大量使用了选择器。如果您熟悉 jQuery、Prototype、Dojo 或任何其他使用选择器的 JavaScript 库,您会发现新的选择器 API 非常熟悉。
选择器标准定义了元素抽象类的两个新方法:
querySelector(cssSelectorList)
:返回对第一个元素的直接引用,该元素匹配指定的逗号分隔的cssSelectorList
中的所有 CSS 选择器。如果没有匹配,返回null
。querySelectorAll(cssSelectorList)
:返回一个NodeList
对象,该对象包含所有在逗号分隔的cssSelectorList
中指定的 CSS 选择器的匹配。如果没有匹配的元素,返回一个没有成员的NodeList
。
注意
NodeList
对象看起来很像数组,因为它们有可以通过数字索引访问的成员元素,以及一个反映成员数量的length
属性。然而,NodeList
对象直接从Object
原型继承,而不是从Array
原型继承,所以它们没有任何您可能会想到的数组方法(例如Array.forEach
)。
使用新的选择器 API,您可以轻松地获得对 DOM 元素的直接引用,而不需要大量遍历,也不需要在标记中添加只用于 JavaScript 选择器的 id。这可以帮助您保持标记和 JavaScript 代码的整洁。此外,您会发现自己经常在 JavaScript 和 CSS 中使用相同的选择器,因为您需要设计样式的元素通常也是脚本需要访问的元素。
我在书中的例子中一直使用选择器 API。下面是一些其他的例子,可以帮助说明这个 API 有多么强大:
- 属性选择器:
[attribute=value]
允许您根据 DOM 元素的指定属性来定位它们。这在选择分配了数据属性的元素时特别有用。您也可以使用模式匹配:[att^=’val’]
选择属性以字母“val”开头的元素[att$=’lue’]
选择其att
属性以字母“lue”结尾的元素[att*=’val’]
选择其att
属性包含字母“val”的元素
- 元素状态伪类允许您根据状态伪类定位 DOM 元素。特别有用的是
:enabled
(选择启用的表单域)、:disabled
(选择禁用的表单域)和:checked
(选择选中的复选框和单选按钮)。 - 否定伪类 :
not(selector)
以不匹配指定选择器的 DOM 元素为目标。 - 结构化伪类允许您根据 DOM 元素在 DOM 结构中的位置来定位它们。特别有用的是:
:nth-child(n)
选择其父元素的第 n 个子元素:nth-last-child(n)
选择其父元素的第 n 个子元素,从最后一个子元素开始向后计数:nth-of-type(n)
选择其类型的第 n 个兄弟元素:nth-last-of-type(n)
选择其类型的第 n 个兄弟元素,从最后一个兄弟元素开始向后计数:last-child
选择父元素的最后一个子元素:first-of-type
和:last-of-type
选择其类型的第一个或最后一个同级元素:only-child
选择其父元素的唯一子元素
因为querySelector
和querySelectorAll
方法是元素方法,所以可以在任何元素上使用它们。这将匹配选择器的搜索限制在该元素的后代,如清单 5-6 中的所示。
清单 5-6 。将选择器查询限制到包含元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<p class="selectme">This has the selectme class, but will not be clickable.</p>
<div class="noselect">
<p class="selectme">This has the selectme class, but will not be clickable.</p>
</div>
<div class="selectable">
<p class="selectme">This has the selectme class, and will be clickable.</p>
</div>
<script>
// Get a reference to the containing element we want to search.
var selectable = document.querySelector(’.selectable’);
// Get a reference to the paragraph.
var targetPar = selectable.querySelector(’.selectme’);
// Give the target paragraph an event handler for the click event.
targetPar.addEventListener(’click’, function() {
alert(’I was clicked!’);
});
</script>
</body>
</html>
这个简单的例子将所需选择器的搜索限制在指定的div
元素。这相当于使用选择器".selectable .selectme"
。这种技术对于选择事件目标的后代特别有用。
设备方向
维持价
穷
该 API 仅在具有必要硬件的设备上有用,这些设备通常是移动设备。移动浏览器对这个 API 的支持相当不错,除了 Internet Explorer Mobile,它根本没有实现这个 API。Internet Explorer 11 支持 API,Chrome 和 Firefox 也支持,但 Safari 不支持。
W3C 工作草案:http://www.w3.org/TR/orientation-event/
大多数移动和手持设备都包含灵敏的陀螺仪,使设备能够知道自己在空间中的方向。设备定向 API 为主机设备提供了一个标准 API,以便与基于浏览器的应用共享这些信息。
注意这个特殊的标准已经经历了频繁的变化,以响应行业反馈。因此,大多数浏览器制造商都没有全面实施。我在新闻发布时展示了该标准的当前版本,因为我认为这是一个值得报道的重要特性,尽管它还处于草案状态。
该标准指定了一组在窗口对象上触发的新事件,以及结果事件对象上的新数据属性。通过为这些事件注册事件侦听器,您可以访问这些数据属性。
compassneedscalibration
事件
根据标准,compassneedscalibration
事件在window
对象上触发,“当用户代理确定用于获取方位数据的罗盘需要校准时”然而,没有说明用户代理应该做什么来校准自己,或者如何与开发人员或最终用户沟通。出于这个原因,这个事件目前在 Firefox 中被禁用(参见https://bugzilla.mozilla.org/show_bug.cgi?id=738121
)。其他移动用户代理可能会触发这个事件,尽管我从未见过它发生。
像任何其他事件一样,您只需在window
对象上为它注册一个事件处理程序,如下面的代码片段所示:
window.addEventListener(’compassneedscalibration’, function(event) {
alert(’Your compass needs calibration. Wave your device in a figure-8 motion.’);
}, false);
deviceorientation
事件
根据该标准,deviceorientation
事件在window
对象上触发“每当发生显著的方向变化”,但是将“显著变化”的定义留给浏览器制造商。实际上,这个事件似乎会定期在window
对象上触发,甚至对于完全静止在桌子上的设备也是如此。
deviceorientation
事件的事件对象是一个DeviceOrientationEvent
对象,它具有以下属性:
DeviceOrientationEvent.alpha
:旋转的阿尔法角度。DeviceOrientationEvent.beta
:旋转的β角。DeviceOrientationEvent.gamma
:旋转的伽玛角度。
如果你熟悉欧拉角,那么阿尔法角、贝塔角和伽马角就是 Z-X'-Y "类型的泰特-布赖恩角。为了形象化这些角度,想象一个设备平放在桌子上,如图图 5-6 所示。
图 5-6 。平放在桌子上的装置
围绕 z 轴旋转将使 x 轴和 y 轴平移旋转量,如图图 5-7 所示。
图 5-7 。绕 z 轴旋转
由此产生的角度被称为阿尔法角度。
围绕 x 轴旋转将使 z 轴和 y 轴平移旋转量,如图图 5-8 所示。
图 5-8 。绕 x 轴旋转
由此产生的角度被称为β角。
最后,绕 y 轴旋转会使 x 轴和 z 轴平移旋转量,如图图 5-9 所示。
图 5-9 。绕 y 轴旋转
由此产生的角度被称为伽玛角度。
如何使用这些角度的明确示例是根据 gamma 和 beta 角度在屏幕上移动 DOM 元素。因为角度从正到负变化,您可以简单地将角度的舍入值添加到相关纵坐标的当前值:对于 x 纵坐标,您使用伽马角度,对于 y 纵坐标,您使用β角度。设备倾斜得越多,角度越大,坐标上的增量越大,元素移动得越快,如清单 5-7 所示。
清单 5-7 。在屏幕上移动球
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>The HTML5 Programmer’s Reference</title>
<style>
#container {
position: absolute;
top: 220px;
left: 50px;
width: 204px;
height: 204px;
border: 1px solid red;
}
#ball {
width: 10px;
height: 10px;
position: absolute;
top: 0px;
left: 0px;
background-color: red;
border-radius: 50%;
}
</style>
</head>
<body>
<h1>Device Orientation Demonstration</h1>
<ul>
<li>Alpha: <span id="alpha"></span></li>
<li>Beta: <span id="beta"></span></li>
<li>Gamma <span id="gamma"></span></li>
<li>y-pos <span id="ypos"></span></li>
<li>x-pos <span id="xpos"></span></li>
</ul>
<div id="container">
<div id="ball"></div>
</div>
<script>
// Get references to the various DOM elements we will be manipulating.
var alpha = document.getElementById(’alpha’);
var beta = document.getElementById(’beta’);
var gamma = document.getElementById(’gamma’);
var ypos = document.getElementById(’ypos’);
var xpos = document.getElementById(’xpos’);
var ball = document.getElementById(’ball’);
// Initialize x and y coordinates.
var yposit = 0;
var xposit = 0;
/**
* Handles a deviceorientation event on the window object.
* @param {DeviceOrientationEvent} event A standard device orientation event.
*/
function handleDeviceOrientation(event) {
// Update the DOM with the raw event data.
alpha.innerHTML = event.alpha;
beta.innerHTML = event.beta;
gamma.innerHTML = event.gamma;
// Use the raw data to get x and y coordinates for the ball.
xposit = getCoord(event.gamma, xposit);
xpos.innerHTML = xposit;
yposit = getCoord(event.beta, yposit);
ypos.innerHTML = yposit;
ball.style.top = yposit + ’px’;
ball.style.left = xposit + ’px’;
}
/**
* Increments a coordinate based on an angle from the device orientation event.
* @param {number} angle The orientation angle.
* @param {number} coord The coordinate to increment.
*/
function getCoord(angle, coord) {
// First, get a delta value from the angle.
var delta = Math.round(angle);
var tempVal = coord + delta;
// Limit the incremented value to between 0 and 194.
if (tempVal > 0) {
coord = Math.min(194, tempVal);
} else {
coord = 0;
}
return coord;
}
// Register the event handler.
window.addEventListener(’deviceorientation’, handleDeviceOrientation, false);
</script>
</body>
</html>
此示例在屏幕上显示原始事件数据,并使用该原始事件数据来确定元素在屏幕上的坐标。在这种情况下,它会限制元素的位置,使其保持在其包含元素的内部。
devicemotion
事件
devicemotion
事件定期在window
对象上触发,并产生一个DeviceMotionEvent
类型的事件。DeviceMotionEvent
有四个属性:acceleration
(其值代表设备沿 x、y 和 z 轴的加速度,单位为米/秒平方)、accelerationIncludingGravity
(包括地球重力影响的加速度值,如果有的话)、rotationRate
(α、β和γ角度的旋转速率,单位为度/秒)和interval
(该信息从硬件刷新的频率,单位为毫秒)。总的来说,DeviceMotionEvent
的模式如下所示:
object DeviceMotionEvent = {
object acceleration: {
number x,
number y,
number z
},
object accelerationIncludingGravity: {
number x,
number y,
number z
},
object rotationRate: {
number alpha,
number beta,
number gamma
}
number interval
}
你可以很容易地显示这些值,如清单 5-8 所示。
清单 5-8 。显示devicemotion
事件的值
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Device Motion Demonstration</h1>
<ul>
<li>acceleration:
<ul>
<li id="accX">x: <span class="current"></span>,<br>
max: <span class="max"></span></li>
<li id="accY">y: <span class="current"></span>,<br>
max: <span class="max"></span></li>
<li id="accZ">z: <span class="current"></span>,<br>
max: <span class="max"></span></li>
</ul>
</li>
<li>accelerationIncludingGravity:
<ul>
<li id="aigX">x: <span class="current"></span>,<br>
max: <span class="max"></span></li>
<li id="aigY">y: <span class="current"></span>,<br>
max: <span class="max"></span></li>
<li id="aigZ">z: <span class="current"></span>,<br>
max: <span class="max"></span></li>
</ul>
</li>
<li>rotationRate:
<ul>
<li id="rrAlpha">alpha: <span class="current"></span>,<br>
max: <span class="max"></span></li>
<li id="rrBeta">beta: <span class="current"></span>,<br>
max: <span class="max"></span></li>
<li id="rrGamma">gamma: <span class="current"></span>,<br>
max: <span class="max"></span></li>
</ul>
</li>
</ul>
<script>
// Create a data structure to store the references to the various DOM elements
// we will be manipulating, as well as associated maximum values. The structure
// also includes an interface method for processing incoming data and mapping
// it to the correct DOM elements.
var motionValues = {
acceleration : {
x : {
domCurr : document.querySelector(’#accX .current’),
domMax : document.querySelector(’#accX .max’),
maxVal : 0
},
y : {
domCurr : document.querySelector(’#accY .current’),
domMax : document.querySelector(’#accY .max’),
maxVal : 0
},
z : {
selector : ’#accZ’,
domCurr : document.querySelector(’#accZ .current’),
domMax : document.querySelector(’#accZ .max’),
maxVal : 0
}
},
accelerationIncludingGravity : {
x : {
domCurr : document.querySelector(’#aigX .current’),
domMax : document.querySelector(’#aigX .max’),
maxVal : 0
},
y : {
domCurr : document.querySelector(’#aigY .current’),
domMax : document.querySelector(’#aigY .max’),
maxVal : 0
},
z : {
selector : ’#accZ’,
domCurr : document.querySelector(’#aigZ .current’),
domMax : document.querySelector(’#aigZ .max’),
maxVal : 0
}
},
rotationRate : {
alpha : {
domCurr : document.querySelector(’#rrAlpha .current’),
domMax : document.querySelector(’#rrAlpha .max’),
maxVal : 0
},
beta : {
domCurr : document.querySelector(’#rrBeta .current’),
domMax : document.querySelector(’#rrBeta .max’),
maxVal : 0
},
gamma : {
selector : ’#accZ’,
domCurr : document.querySelector(’#rrGamma .current’),
domMax : document.querySelector(’#rrGamma .max’),
maxVal : 0
}
},
/**
* Processes an acceleration value object of a specific type. The values are
* enumerated and mapped to their associated DOM elements for display.
* @param {string} valueType The type of the value object, one of
* ’acceleration’, ’accelerationIncludingGravity’, or ’rotationRate’.
* @param {object} valueObject The object containing the acceleration data.
*/
processValues : function(valueType, valueObject) {
// First, get a reference to the subproperty of the motionValues object we
// will be manipulating.
var mvRef = this[valueType];
// Enumerate the valueObject and process each property.
for (property in valueObject) {
// Convenience references to the current values we’re working with.
var currMVRef = mvRef[property];
var currVal = valueObject[property];
// Update the DOM to display the current value.
currMVRef.domCurr.innerHTML = currVal;
// If the current value is larger than the last stored maximum value,
// update the stored max value to match and display it in the DOM.
if (currVal > currMVRef.maxVal) {
currMVRef.maxVal = currVal;
currMVRef.domMax.innerHTML = currVal;
}
}
}
};
/**
* Handles a devicemotion event on the window object.
* @param {DeviceMotionEvent} event A standard device motion event object.
*/
function handleDeviceMotion(event) {
motionValues.processValues(’acceleration’, event.acceleration);
motionValues.processValues(’accelerationIncludingGravity’,
event.accelerationIncludingGravity);
motionValues.processValues(’rotationRate’, event.rotationRate);
}
// Register the event handler.
window.addEventListener(’devicemotion’, handleDeviceMotion, false);
</script>
</body>
</html>
因为有许多值要显示,而且由于有了DeviceMotionEvent
模式,许多数据都是专门构造的,清单 5-8 通过创建一个具有相似模式的对象来开始这个例子。对于每个单独的属性,它存储一个显示其当前值的元素的 DOM 引用,一个显示所达到的最大值的元素的 DOM 引用,以及最大值本身。它还包括一个简单的接口方法,将DeviceMotionEvent
子属性映射到对象中相关联的子属性,并更新 DOM 以反映新信息。
为了使用这个例子,你需要移动你的设备。这些值是加速度的,加速度是速度的变化率(而速度是位置的变化率)。为了看到可观的价值,你需要相当快地移动你的设备。沿着不同的运动轴摇动你的设备就足够了。小心握紧你的手机,以免不小心把它扔了。将为您记录各个轴上的最大加速度值,以便您在移动设备后可以看到它们。您也可以旋转设备来查看旋转速度。
web GL(web GL)
维持价
好的
所有现代桌面浏览器至少在最近两个版本中都支持这些特性,只有 Internet Explorer 例外,它从版本 11 开始才支持这些特性。移动支持很差,因为 iOS 的移动 Safari 目前不支持 WebGL,尽管苹果已经承诺在 iOS 版本 8 中提供全面支持。
规格:http://www.khronos.org/webgl/
Web Graphics Library (WebGL) 是一个 API,用于在 HTML canvas
元素中绘制复杂的 2d 和 3d 图形。WebGL API 在给定的canvas
元素上呈现为绘图上下文,就像你在第四章中探索的标准绘图上下文一样。就像标准的canvas
绘图上下文一样,WebGL 绘图上下文可以通过一个扩展的 API 在 JavaScript 中访问。许多 WebGL 任务,如图像处理,被委托给主机系统的图形处理单元,而不是由系统的主 CPU 处理,因此提供了显著的速度提升。
与本书中涉及的大多数其他标准不同,WebGL 标准不是由 W3C 或 WHATWG 维护的。该标准由非营利技术联盟 Khronos Group 维护。这种语言本身基于 OpenGL 语言,是 2009 年 Mozilla 在 3d 渲染方面的实验的产物。WebGL 目前的稳定版本是 1.0.2。WebGL 2 的工作始于 2013 年。
初始化 WebGL 绘图上下文非常类似于在canvas
元素中初始化标准绘图上下文,如清单 5-9 所示。
清单 5-9 。初始化 WebGL 绘图上下文
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
var myCanvas = document.getElementById(’myCanvas’);
var myGlContext = myCanvas.getContext(’webgl’);
</script>
</body>
</html>
这个例子使用了getContext
方法,就像你在第四章中所做的一样。不同之处在于,它没有为 2d 绘图上下文提供参数’2d’
,而是提供了’webgl’
参数来指定 WebGL 绘图上下文。你可以很容易地将它扩展成一个函数,它甚至提供了一个根据需要初始化上下文的地方,如清单 5-10 所示。
清单 5-10 。WebGL 初始化函数
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
/**
* Returns a WebGL drawing context on a specified canvas element. If opt_setup
* is provided and set to true, this method also performs some basic
* initialization on the context.
* @param {!Element} targetCanvas The reference to the desired canvas element.
* @param {boolean} opt_setup Whether or not to perform additional setup on the
* context.
* @return {Object} The WebGL drawing context, or null if WebGL is not supported
* or was otherwise unavailable.
*/
function initWebGLOnCanvas(targetCanvas, opt_setup) {
// If opt_setup was not specified, set it to false. In JavaScript, null and
// undefined are == to each other and nothing else, so:
if (opt_setup == null) {
opt_setup = false;
}
// Try and get the context.
var glContext = targetCanvas.getContext(’webgl’);
if (glContext == null) {
// Try falling back to an experimental version, works on some older browsers.
glContext = targetCanvas.getContext(’experimental-webgl’);
if (glContext == null) {
// We were unable to get a WebGL context. Provide a warning diagnostic
// message on the console, in case anyone is looking.
console.warn(’WebGL is not supported in this browser.’);
}
}
// If there is a context and setup was requested, do the setup.
if ((opt_setup === true) && (glContext != null)) {
// Set the clear color to black (rgba).
glContext.clearColor(0.0, 0.0, 0.0, 1.0);
// Initialize the depth function so that objects that are closer in
// perspective hide things that are further away.
glContext.depthFunc(glContext.EQUAL);
// Enable depth testing.
glContext.enable(glContext.DEPTH_TEST);
// Clear both the color and the depth buffer.
glContext.clear(glContext.COLOR_BUFFER_BIT|glContext.DEPTH_BUFFER_BIT);
}
return glContext;
}
var myCanvas = document.getElementById(’myCanvas’);
var myGLContext = initWebGLOnCanvas(myCanvas, true);
</script>
</body>
</html>
这个示例扩展了初始化函数,以检测获取 WebGL 上下文是否有问题,并回退到较旧浏览器上存在的较旧语法。如果根本无法获取上下文,就会向控制台输出一个警告。运行此示例将在浏览器中产生一个黑色方块。
在撰写本文时,Firefox 正在 WebGL 初始化过程中将大量 Windows、MacOS、Linux 和 Android 图形驱动程序列入黑名单。如果你有这些驱动,Firefox 默认情况下不会初始化一个 WebGL 绘图上下文。如果您在 Firefox 中运行这个示例,并且在控制台中看到警告消息,那么您的设置很可能被列入黑名单。有关如何覆盖该块的详细信息和说明,请参见https://wiki.mozilla.org/Blocklisting/Blocked_Graphics_Drivers
。另一种选择是使用具有不太脆弱的 WebGL 实现的浏览器(Chrome 的 WebGL 实现相当稳固)。
注意直到最近,iOS 上的 Safari Mobile 还不支持 WebGL。Safari 8.1 引入了完整的 WebGL 支持。
WebGL 是一种广泛的语言,完全涵盖它以及您可以用它做的一切超出了本书的范围。如果你想了解更多,请查看 Brian Danchilla 的《HTML5 的 WebGL 入门》。
挽救(saving 的简写)
维持价
优秀
所有现代浏览器都支持 SVG,并且至少支持最近的三个版本。
W3C 推荐:http://www.w3.org/TR/SVG11/
可缩放矢量图形(SVG) 是一种用于创建光栅图形、矢量图形和文本的图形格式。使用 SVG 可以轻松地对图形对象(在 SVG 中定义的和从外部文件导入的,如常规图像文件)进行分组和操作。
大多数图形格式(如可移植网络图形[PNG]格式)由二进制数据组成。SVG 图形是使用 XML 标记定义的,因此可以使用简单的文本编辑器轻松创建,就像网页一样。因为 SVG 图形是在 XML 标记中定义的,所以内容可以很容易地被扫描和索引。这使得 SVG 比其他图形格式更容易访问。
如上所述,SVG 标记可以像canvas
元素一样生成光栅图形。它还可以生成矢量图形,矢量图形是由包括点、线和曲线的数学函数定义的图形。光栅图形和矢量图形的主要区别在于矢量图形比光栅图形的缩放性更好。因此,SVG 定义的矢量图形是移动应用的最佳选择,因为它们在任何分辨率和大小下都保持清晰。
与 WebGL 一样,SVG 是一个大型标准,全面介绍它超出了本文的范围。
摘要
在这一章中,我探索了一些 JavaScript APIs,它们不是 HTML5 标准的一部分,但经常与 HTML5 特性结合使用。他们中的许多人也有令人兴奋的移动用途。
- 地理定位 API 让您的 JavaScript 应用能够访问移动设备的地理定位功能。您可以使用这个 API 来编写激动人心的新的位置感知移动应用。我还介绍了使用地理定位时重要的隐私考虑事项。
- 动画计时通过提供与浏览器窗口绘制直接相关的新计时器,提供了制作平滑动画的工具。
- 选择器 API 提供了一种使用 CSS 选择器轻松访问 DOM 元素的方法。
- 设备定位 API 让您的 JavaScript 应用能够访问移动设备的定位特性。您可以使用这个 API 来创建响应宿主移动设备移动的应用。
- 最后,我简要介绍了两项激动人心的新技术,WebGL 和 SVG。
使用这些具有 HTML5 特性的 API 将使您能够在各种设备上构建激动人心的动态应用。
在第六章中,你将深入 HTML5 的实际开发,包括从头开始构建一个完整的 HTML5 手机游戏。
六、实用 HTML5
现在我已经介绍了 HTML5 及其相关技术,是时候用它们来构建一些东西了。这一章将集中讨论如何使用 HTML5。它将涵盖浏览器兼容性问题,包括功能检测、polyfills 和 shims,以及设计用于 HTML5 的库。最后,你将从头到尾完成一个完整的 HTML5 项目。
我将从定义项目的需求开始,找出如何最好地实现它,然后按方法分解实现方法。
浏览器支持
使用 HTML5 的最大障碍是浏览器支持。如果你正在做一个必须支持很多旧浏览器的项目,你将很快遇到主流浏览器不支持的问题。这对于桌面浏览器来说尤其是个问题;Internet Explorer 直到版本 9 才支持 HTML5 的基本语义标签。移动浏览器很好地实现了 HTML5 特性,因为它们往往来自更新的代码库。然而,即使是移动浏览器也有支持问题。例如,许多运行早于版本 4 的 Android 操作系统的手机将拥有不支持几个现代功能的浏览器。
您的应用如何处理浏览器支持是一个重要的决定。您可以决定只支持每个浏览器的最新版本。这将保证您的应用可以访问最广泛的 HTML5 特性,但可能会将那些停留在旧浏览器或操作系统上的用户拒之门外。
更常见的是,要求您的应用支持至少几个修订版的浏览器版本。这意味着您的用户将试图在可能不支持您需要的 HTML5 特性的浏览器中使用您的应用。在这种情况下,您必须选择您的应用应该如何运行,但是最初的选择将基于检测该特性是否受支持。使用脚本来确定给定的特性是否受支持被称为“特性检测”,您可以轻松地测试大多数 HTML5 特性。
特征检测速成班
特性检测是 HTML 开发者的一个重要工具,它允许你根据当前浏览器的能力定制你的应用。根据相关功能的实现方式,有多种功能检测技术,例如,作为现有对象的属性或方法,或者作为新元素类型。
检测 JavaScript 属性和方法
许多新的 HTML5 JavaScript APIs 被实现为现有对象的新属性或方法,如window
、document
或navigator
。如果你试图在不支持这些特性的浏览器中访问这些特性,JavaScript 引擎会产生一个错误,你的脚本会嘎然而止,如清单 6-1 所示。
清单 6-1 。调用不存在的方法
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Feature Detection</h1>
<script>
// Attempt to invoke a fake method foo() on the window object
window.foo()
</script>
</body>
</html>
当您运行这个示例时,它将产生一个错误,如果您打开浏览器的 JavaScript 控制台,就可以看到这个错误。如果这是在一个更大的脚本中,它将使整个脚本停止,这是一个非常灾难性的结果。
当您试图访问一个不存在的属性时,结果会稍微微妙一些。简单地读取一个不存在的属性将返回undefined
的值,但实际上不会使脚本崩溃,如清单 6-2 所示。
清单 6-2 。访问不存在的属性
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Feature Detection</h1>
<script>
// Attempt to access a fake property bar on the window object
alert(window.bar);
</script>
</body>
</html>
如果您运行这个脚本,它将完美地工作,并且警告弹出窗口将包含文本“undefined”该脚本不会抛出错误,并将继续运行。然而,在 JavaScript 中,undefined
是一个特定的值和它自己的数据类型,所以如果你试图进一步操作它(就像你访问一个真实的属性一样),结果可能会令人惊讶,如清单 6-3 所示。
清单 6-3 。undefined
到底是什么?
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Feature Detection</h1>
<script>
// Compare undefined with basic data types
alert(window.bar + 5);
alert(window.bar + ’ is its own data type’);
alert(window.bar == true);
alert(window.bar == false);
if (window.bar) {
alert(’undefined is equal to true’);
}
if (!window.bar) {
alert(’undefined is equal to false’);
}
if (window.bar == null) {
alert(’undefined and null are equal’);
}
if (!(window.bar === null)) {
alert(’undefined and null are not strictly equal’);
}
</script>
</body>
</html>
当您运行这个脚本时,您会发现undefined
不太适合处理数字;即使是简单的加法运算也会产生值NaN
(表示“不是一个数字”)。虽然undefined
不存在,但它有一个字符串值“未定义”并且当undefined
不等于true
或false
时,出于流量控制的目的,它被评估为false
。最后,您可以使用类型转换或“弱”相等操作符(==
)和严格相等操作符(===
)来测试undefined
和null
如何彼此相等。弱相等运算符会自动解决操作数之间的类型差异,而严格相等运算符则不会。在undefined
和null
的情况下,当使用弱运算符时,这两个值彼此相等,但是由于它们具有不同的基本数据类型,因此它们无法通过严格的相等测试。
提示
undefined
vs. null
:重要的是要记住,虽然这些行为可能违反直觉,但它们实际上是由 ECMAScript 标准很好地定义的,并且是该语言的实际特征。请记住,undefined
作为一个值是指任何没有被赋值的属性,而null
是指故意缺少值。
为了充分解释这些行为,我必须深入讨论 JavaScript 数据类型以及该语言如何为弱等式运算符==
解决数据类型差异,这超出了本章的范围。不管怎样,清单 6-3 展示了一种检测 JavaScript 对象上属性存在的方法,并给出了可预测的结果。如清单 6-4 所示,该方法也适用于检测方法,并且不会抛出错误。
清单 6-4 。检测 JavaScript 对象上的属性和方法
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Feature Detection</h1>
<script>
// To check for a method foo() on the window object, check to see if it is
// defined
if (window.foo) {
alert(’Method foo() is available’);
} else {
alert(’Method foo() is not available’);
}
// To check for a property bar on the window object use the same test.
if (window.bar) {
alert(’Property bar is available’);
} else {
alert(’Property bar is not available’);
}
</script>
</body>
</html>
当您运行清单 6-4 时,它将显示window.foo()
和window.bar
都不可用,并且脚本不会抛出任何错误。使用这种方法很容易检测出真正的 HTML5 特性,如清单 6-5 所示。
清单 6-5 。检测 HTML5 JavaScript APIs
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Feature Detection</h1>
<script>
if (window.postMessage) {
alert(’The postMessage feature is available on this browser!’);
} else {
alert(’The postMessage feature is not available on this browser’);
}
if (window.localStorage) {
alert(’The localStorage feature is available on this browser!’);
} else {
alert(’The localStorage feature is not available on this browser’);
}
</script>
</body>
</html>
当您运行此示例时,您将发现postMessage
和localStorage
功能在您的浏览器上是否可用。
同样的方法也用于检测新的 HTML5 事件接口,比如新的设备运动和方向事件。不是直接检查事件处理程序(如ondevicemotion
)的存在,而是检查事件接口是否存在(如清单 6-6 中的window.DeviceMotionEvent
)。
清单 6-6 。检测对事件接口的支持
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Feature Detection</h1>
<script>
if (window.DeviceMotionEvent) {
alert(’This browser supports the device motion API!’);
} else {
alert(’This browser does not support the device motion API.’);
}
</script>
</body>
</html>
检测对新 HTML5 元素的支持
有两种主要方法来检测对新元素的支持:
- 创建元素的实例,然后测试预期属性和方法的结果。如果浏览器不知道如何实现元素,那么期望的属性将是
undefined
。这个测试对于像canvas
和video
这样的元素很有用,它们实现了自己独特的属性和方法。 - 创建元素的实例,然后测试它实现的接口。如果浏览器不知道如何实现元素,它将实现
HTMLUnknownElement
接口(详见下文)。该测试对于不实现唯一属性和方法的元素(如结构元素)很有用。
清单 6-7 展示了第一种方法。
清单 6-7 。检测对画布元素的支持
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Feature Detection</h1>
<script>
// Test for canvas support.
var testCanvas = document.createElement(’canvas’);
if (testCanvas.getContext) {
alert(’This browser supports the canvas element!’);
} else {
alert(’This browser does not support the canvas element.’);
}
// We are done with the test element, so delete it.
testCanvas = null;
</script>
</body>
</html>
这个例子创建了一个canvas
元素,然后测试是否存在getContext
方法。如果浏览器知道如何正确实现canvas
元素,方法就会出现,否则就是undefined
。
提示创建用于测试的元素而不将它们附加到 DOM 是一件相当安全的事情。这些元素存在于内存中(因此会占用物理内存空间),但不是 DOM 的一部分,不会影响文档的其余部分。因为它们占用内存,所以当不再需要它们时,最好通过将它们的引用设置为 null 来删除它们。
检测预期的属性和方法仅适用于实现基本元素属性和方法之外的唯一属性或方法的元素。像article
或aside
这样没有实现唯一属性或方法的元素呢?答案就在 HTML 标准定义的界面层次结构中。
HTML 标准定义了一个名为HTMLElement
的基本接口,它具有一组所有 HTML 元素共有的属性和方法:title
、lang
、focus
、blur
等等。该标准还定义了一组继承自它的子接口,如HTMLDivElement
、HTMLTitleElement
等。大多数受支持的元素都继承自这些子接口,因此共享HTMLElement
接口的基本属性和方法。该标准还为不支持的元素定义了一个名为HTMLUnknownElement
的子接口。您可以使用document.createElement
创建任意元素;如果元素不受支持,它将从HTMLUnknownElement
接口继承。
确定一个特定元素实现哪个接口很简单,只需检查元素的toString
方法。当你在一个元素上调用那个方法时,它将输出它实现的接口的名称,如清单 6-8 所示。
清单 6-8 。确定 HTML 元素实现的接口
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Feature Detection</h1>
<script>
// Create a div.
var myDiv = document.createElement(’div’);
alert(myDiv.toString());
// Create a fake element.
var myFake = document.createElement(’itsafake’);
alert(myFake.toString());
// Delete elements now that they are no longer needed.
myDiv = myFake = null;
</script>
</body>
</html>
这个例子创建了两个元素,一个div
和一个假元素,然后调用每个元素的toString
方法。如您所见,itsafake
元素实现了HTMLUnknownElement
接口。这给了你一个测试不受支持元素的简单方法,如清单 6-9 所示。
清单 6-9 。测试支持的元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Feature Detection</h1>
<script>
// Test for support for the article element.
var myArticle = document.createElement(’article’);
if (myArticle.toString().indexOf(’HTMLUnknownElement’) > -1) {
alert (’This browser does not support the article element.’);
} else {
alert(’This browser supports the article element.’);
}
// Create a fake element and test for support.
var myFake = document.createElement(’itsafake’);
if (myFake.toString().indexOf(’HTMLUnknownElement’) > -1) {
alert(’This browser does not support the itsafake element’);
} else {
alert(’This browser supports the itsafake element’);
}
myArticle = myFake = null;
</script>
</body>
</html>
在这个例子中,通过检查每个元素的toString
方法返回的值中是否存在子串’HTMLUnknownElement’
,来测试对article
元素和itsafake
元素的支持。
检测对新元素属性的支持
HTML5 还定义了一组全新的属性,可以应用于元素,比如placeholder
或draggable
。检测对这些属性的支持很简单:只需创建一个元素并设置所需的属性,然后测试该属性是否保持其值。设置值时,一定要设置为合适的类型;一些属性(比如autocomplete
和placeholder
)将期望字符串作为值,而其他属性(比如autofocus
和draggable
)将需要布尔值。如果在测试中设置了不正确的类型,就会产生假阴性。清单 6-10 展示了使用这种技术来测试对input
元素上placeholder
属性的支持。
清单 6-10 。测试属性支持
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Feature Detection</h1>
<script>
// Test for support for the new placeholder attribute on input elements.
var testPlaceholderText = ’Test placeholder text’;
var testInput = document.createElement(’input’);
testInput.setAttribute(’placeholder’, testPlaceholderText);
if (testInput.placeholder === testPlaceholderText) {
alert (’This browser supports the placeholder attribute on input elements’);
} else {
alert (’This browser does not support the placeholder attribute on input elements’);
}
// To prove the method works, test for an attribute we know doesn’t exist.
testInput.setAttribute(’fakeattr’, testPlaceholderText);
if (testInput.fakeattr === testPlaceholderText) {
alert (’This browser supports the fakeattr attribute on input elements’);
} else {
alert (’This browser does not support the fakeattr attribute on input elements’);
}
// We are done with the test element, so delete it.
testInput = null;
</script>
</body>
</html>
这个示例还演示了在不支持属性的情况下它会失败(在这种情况下,您只需虚构一个属性并对其进行测试)。
显然,这种技术要求目标对象实现一个setAttribute
方法。因此,它不能用于检测window
或navigator
元素上的特征,因为它们没有setAttribute
方法。
构建功能检测脚本
现在您已经知道如何测试各种 HTML5 特性,您可以构建一个测试所有特性的脚本。首先创建一个构造函数,当调用该函数时,它将运行特性检测测试并返回一个包含所有结果的对象。每个结果将是对象上的一个命名属性,根据对该特性的支持设置为true
或false
。该对象还将有三个方便的方法:
getTests
:这个方法将返回一个按字母顺序排列的所有被测试特性的数组。getTestResults
:该方法将返回一个数组,其中包含所有被测试特性的所有结果。单个结果将由一个对象组成,该对象的feature
属性被设置为该特性的名称,而isSupported
属性将被设置为true
或false
,这取决于该特性是否受支持。getFailedTestResults
:该方法将返回一个数组,其中包含所有测试失败的特性的所有结果,这些特性在当前浏览器中不受支持。
清单 6-11 给出了检测脚本的完整清单。
清单 6-11 。HTML5 功能检测脚本
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
table {
font-family: verdana,arial,sans-serif;
color: #333;
border-width: 1px;
border-color: #666;
border-collapse: collapse;
}
th {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666;
background-color: #dedede;
}
td {
border-width: 1px;
padding: 8px;
border-style: solid;
border-color: #666;
background-color: #fff;
}
</style>
</head>
<body>
<h1>Feature Detection</h1>
<table id="supported">
<tr><th>Feature</th><th>Support</th></tr>
</table>
<script>
/**
* Detects support for various HTML5 features.
* @constructor
* @returns {Object} An object with properties for each feature, each
* set to true or false depending on support.
*/
function DetectHTML5Support() {
var returnVal = {};
// Test for HTML5 APIs on the window object.
var apisToTest = [’EventSource’, ’postMessage’, ’sessionStorage’,
’localStorage’, ’Worker’, ’requestAnimationFrame’,
’cancelAnimationFrame’, ’DeviceMotionEvent’, ’DeviceOrientationEvent’];
for (i = 0; i < apisToTest.length; i++) {
var currApi = apisToTest[i];
returnVal[currApi] = (window[currApi] != undefined);
}
// Test for HTML5 APIs on the navigator object.
var apisToTest = [’geolocation’];
for (i = 0; i < apisToTest.length; i++) {
var currApi = apisToTest[i];
returnVal[currApi] = (navigator[currApi] != undefined);
}
// Test for HTML5 APIs on the document object.
var apisToTest = [’querySelector’, ’querySelectorAll’];
for (i = 0; i < apisToTest.length; i++) {
var currApi = apisToTest[i];
returnVal[currApi] = (document[currApi] != undefined);
}
// Test for suport for the new HTML5 elements.
var unsupported = ’HTMLUnknownElement’;
var elementsToTest = [’article’, ’aside’, ’nav’, ’footer’, ’header’,
’section’, ’figure’, ’figcaption’, ’main’, ’bdi’, ’data’, ’mark’,
’ruby’, ’rp’, ’rt’, ’time’, ’wbr’, ’dialog’, ’details’, ’summary’,
’datalist’, ’meter’, ’output’, ’progress’, ’audio’, ’canvas’, ’video’];
for (i = 0; i < elementsToTest.length; i++) {
var currItem = elementsToTest[i];
var testEl = document.createElement(currItem);
returnVal[currItem] = (testEl.toString().indexOf(unsupported) == -1);
testEl = null;
}
// Test for support for new input properties that are booleans.
var propsToTest = [’autofocus’, ’draggable’];
var inputEl = document.createElement(’input’);
// For variety we’ll use Array.forEach to run these tests instead of an
// explicit for loop.
propsToTest.forEach(function(currProp) {
var testValue = true;
inputEl.setAttribute(currProp, testValue);
returnVal[currProp] = (inputEl[currProp] === testValue);
}, this);
// Test for support for new input properties that are strings.
propsToTest = [’autocomplete’, ’placeholder’];
propsToTest.forEach(function(currProp) {
var testValue = ’testval’;
inputEl.setAttribute(currProp, testValue);
returnVal[currProp] = (inputEl[currProp] === testValue);
}, this);
inputEl = null;
/**
* Returns a sorted array of all features that were tested for.
* @returns {Array.<string>}
*/
returnVal.getTests = function() {
// Get all of the properties and methods we’ve added to returnVal and
// sort them.
var allPropsAndMethods = Object.keys(this).sort();
// This list will contain all the properties and methods, but we only want
// properties, so filter out the methods.
var allTests = [];
allPropsAndMethods.forEach(function(currItem) {
if (typeof this[currItem] != ’function’) {
allTests.push(currItem);
}
}, this);
return allTests;
};
/**
* Returns an array consisting of all test results. Each result is an object
* with the feature property set to the name of the test and the isSupported
* property set to true or false, depending on the support for that feature.
* @returns {Array.<Object>}
*/
returnVal.getTestResults = function() {
var tests = this.getTests();
var allResults = [];
tests.forEach(function(currTest) {
var currResult = {
feature: currTest,
isSupported: this[currTest]
};
allResults.push(currResult);
}, this);
return allResults;
};
/**
* Returns an array of test results for all failed tests. Each result is an
* object as described in getResults.
* @returns {Array.<Object>}
*/
returnVal.getFailedTestResults = function() {
var tests = this.getTests();
var failures = [];
tests.forEach(function(currTest) {
if (!this[currTest]) {
var currResult = {
feature: currTest,
isSupported: this[currTest]
};
failures.push(currResult);
}
}, this);
return failures;
};
// Return the object with all the results.
return returnVal;
}
// Test for supported features.
var supportedFeatures = new DetectHTML5Support();
// Fill the table with support information.
var supportTable = document.getElementById(’supported’);
var allResults = supportedFeatures.getFailedTestResults();
allResults.forEach(function (currTest) {
var newRow = document.createElement(’tr’);
var featureCell = document.createElement(’td’);
var supportCell = document.createElement(’td’);
featureCell.innerHTML = currTest.feature;
supportCell.innerHTML = currTest.isSupported;
newRow.appendChild(featureCell);
newRow.appendChild(supportCell);
supportTable.appendChild(newRow);
});
</script>
</body>
</html>
该脚本将相似的测试组合在一起,以便根据您的需要更容易地添加或删除测试。在每种情况下,您都将一组要测试的内容定义为简单字符串的数组,这些字符串是要测试的特性的名称:API 的名称、元素的名称或属性的名称。然后每个部分遍历数组,应用适当的测试并记录结果。注意,在这些测试中,你利用了这样一个事实:在 JavaScript 中,你可以通过点符号(Object.property
)或括号符号(Object[’property’]
)来访问属性,如第二章中所解释的。
该脚本通过调用构造函数运行测试并获得新的结果对象来演示检测过程,然后使用getFailedTestResults
方法获取不支持的特性列表并构建一个表来显示它们。(您可以很容易地修改它,使用getTestResults
方法来查看所有结果。)如果你在不同的浏览器中运行这个,你会看到不支持的变化,特别是如果你可以使用旧版本的浏览器。。。或者 Internet Explorer,如图图 6-1 所示。
图 6-1 。Chrome、Firefox 和 Internet Explorer 中特征检测脚本的结果
如您所见,即使在现代浏览器中,对某些功能的支持仍然缺失。尤其可悲的是,Firefox 和 Internet Explorer 都不支持dialog
、summary
或details
元素;火狐不支持autocomplete
属性;而且 Internet Explorer 不支持服务器发送的事件。
使用损坏或缺失的 HTML5 实现
这就把我们带到了下一个问题:既然您可以检测出 HTML5 支持哪些特性,那么您该如何处理这些信息呢?您希望使用服务器发送的事件,但 Internet Explorer 不支持它们。你想使用自动完成,但是 Firefox 不知道怎么做。或者很大一部分用户停留在较旧的系统上,因此您需要支持广泛的遗留浏览器。
坏消息是,对于实现中断或缺失的问题,没有“一刀切”的解决方案。好消息是许多 HTML5 特性可以用 JavaScript 模仿。以这种方式再现缺失特征的脚本被称为填补。
以网络存储功能为例(参见第三章中的“网络存储”部分)。旧的浏览器将没有可用的localStorage
或sessionStorage
方法,但是您仍然可以使用 HTTP Cookies 在客户机上存储信息。通过使用 HTML Cookies 作为存储机制,您可以在旧浏览器中实现localStorage
和sessionStorage
。像这样的解决方案可以让你在任何浏览器上使用网络存储。
不幸的是,并不是所有的东西都可以用垫片完全复制。需要访问底层硬件的特性,比如需要访问主机设备的加速度计和陀螺仪的设备定位 API(参见第五章中的“设备定位”部分),无法用 JavaScript 再现。
回到本节开始时提出的问题,很明显,如果您知道不支持什么,您可以根据需要加载 shim 脚本来重现这些特性。为此,您必须按需动态加载 JavaScript 文件。这项技术相当简单:只需使用document.createElement
创建一个脚本元素,然后将 source 属性设置为所需脚本的 URL。当脚本元素被附加到 DOM 后,浏览器将加载并执行脚本。清单 6-12 展示了使用这种技术和特征检测脚本。
清单 6-12 。基于特征支持动态加载垫片
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<script src="../js-lib/detect-support.js"></script>
<script>
/**
* Dynamically load a script.
* @param {string} srcUrl The URL of the script file to load.
*/
function loadScript(srcUrl) {
var newScript = document.createElement(script);
newScript.src = srcUrl;
document.querySelector(’head’).appendChild(newScript);
}
// Test for supported features.
var supportedFeatures = new DetectHTML5Support();
if (!supportedFeatures.localStorage) {
// The Web Storage is not supported, so load a shim.
loadScript(’../js-lib/webstorage-shim.js’);
}
</script>
</body>
</html>
此示例将功能检测脚本保存为一个单独的文件,并单独加载它。然后它创建了一个简单的函数,可以根据需要动态加载脚本。最后,它会检测对 Web 存储特性的支持,如果不支持,它会加载一个假想的填充脚本来再现 Web 存储方法。
不幸的是,这个简单的技术没有考虑到两个重要的问题。首先,该技术不允许在脚本加载时出现错误。如果没有找到脚本文件怎么办?如果应用是移动应用,用户掉线了怎么办?你的脚本需要考虑这些情况。幸运的是,当脚本元素遇到错误时,它会发布一个error
事件,您可以为其注册一个事件处理程序。
这种技术没有考虑的第二个问题是填充程序需要时间来加载。这可能只需要几秒钟,但是在加载完脚本并且方法可用之前,您不会想继续运行脚本。否则,您可能会在填充该特性之前访问它,这可能会导致您的应用出现严重错误。与错误条件一样,当脚本元素加载时,它会发布一个您可以监听的事件。不幸的是,事件类型因浏览器而异。对于 Chrome、Firefox、Opera 和 Safari,该事件是一个load
事件,你可以为它注册一个事件处理程序。
然而,对于 Internet Explorer ,该事件是一个readystatechange
事件。当readystatechange
事件触发时,脚本元素的readyState
属性的值发生变化,新值指示加载脚本的阶段:
uninitiated
:这是默认状态;脚本元素不执行任何操作。loading
:脚本已经开始下载到浏览器,但是还没有完成。loaded
:脚本已经完全下载到浏览器。interactive
:脚本已经完全下载,但还不能使用。complete
:脚本已经可以使用了。
更复杂的是,Internet Explorer 并不总是为加载过程的每个阶段分派readystatechange
事件。您应该对loaded
和complete
状态最感兴趣,而 Internet Explorer 可能只发布其中的一个或两个,所以您的readystatechange
事件处理程序将需要检查这两个状态,如果其中一个发生,处理程序将需要完成它的工作,然后注销自己,这样如果浏览器触发另一个readystatechange
事件,它就不会被再次调用。
清单 6-13 展示了一个新的loadScript
方法,它提供了一种在加载过程中注册成功和错误回调的方法。
清单 6-13 。在继续之前,等待加载一个垫片
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<script src="../js-lib/detect-support.js"></script>
<script>
/**
* Dynamically loads a script and invokes an optional callback.
* @param {string} srcUrl The URL of the script file to load.
* @param {function=} onLoadCallback An optional function to call when the
* script is loaded.
* @param {function=} onErrorCallback An optional function to call if the script
* fails to load.
*/
function loadScript(srcUrl, onLoadCallback, onErrorCallback) {
// Create a script tag.
var newScript = document.createElement(’script’);
// Apply the load callback, if one was provided.
if (onLoadCallback) {
if (newScript.readyState) {
// Internet explorer.
newScript.onreadystatechange = function() {
if (newScript.readyState == ’loaded’ ||
newScript.readyState == ’complete’) {
newScript.onreadystatechange = null;
onLoadCallback.call();
}
};
} else {
// Every other browser in the universe.
newScript.onload = onLoadCallback;
}
}
// Apply the error callback, if one was provided.
if (onErrorCallback) {
newScript.onerror = onErrorCallback;
}
newScript.src = srcUrl;
document.querySelector(’head’).appendChild(newScript);
}
// Test for supported features.
var supportedFeatures = new DetectHTML5Support();
if (!supportedFeatures.localStorage) {
// The Web Storage is not supported, so load a shim.
loadScript(’../js-lib/webstorage-shim.js’,
initApplication,
handleScriptLoadError);
} else {
// Web Storage was supported, so continue with the application.
initApplication();
}
/**
* Handles an error during a script load.
*/
function handleScriptLoadError() {
console.log(’Script failed to load.’);
// Etc.
}
/**
* Hypothetical function for initializing the application.
*/
function initApplication() {
console.log(’Application continues...’);
// Etc.
}
</script>
</body>
</html>
新的loadScript
函数现在接受onLoadCallback
和onErrorCallback
参数。脚本加载完成后调用onLoadCallback
函数,脚本加载失败时调用onErrorCallback
函数。这两个参数都是可选的,但是您很可能需要它们。该脚本像以前一样检查 Web 存储支持,如果存在,就继续执行。如果没有,它加载垫片,然后在加载完成时继续。
如果您只需要一个 HTML5 特性,这是很好的,但是您的项目可能需要验证对多个特性的支持。为了使这变得更容易,您可以创建一个简单的注册表,其中包含您需要的所有特性的名称,以及如果不支持它们,可以加载的垫片的路径:
Object featureRegistryEntry {
string ’featureName’,
string ’shim’
}
Array featureRegistry[featureRegistryEntry]
然后,该注册表成为代码中管理所有所需功能的单一位置,随着应用的变化和增长,添加或移除功能变得更加容易。
然而,这意味着您将检查多个特征,并且可能加载多个垫片。每个填充程序可能需要不同的加载时间,在所有填充程序都完成加载之前,您不会希望脚本继续运行。更复杂的是,其中一个脚本可能由于某种原因无法加载。为了跟踪正在加载的内容以及成功和失败的内容,您需要构建一个加载队列。这个队列可以是一个简单的数据结构,它有一个简单的成功条件布尔值(默认情况下设置为true
,但是一旦脚本加载失败,您就会将其设置为false
)和一个数组,该数组由当前加载的每个脚本的条目组成:
Object loadQueue {
boolean ’noErrorsOccurred’,
Array.<boolean> ’queue’
}
每次脚本开始加载时,它都会向loadQueue.queue
添加一个条目。实际的条目本身并不重要,因为我们只关心所有脚本何时完成加载,而不是特定脚本何时完成或者它们完成的顺序。在这种情况下,进入队列的条目将是一个简单的true
值。当脚本加载完成时,您将从队列中删除一个条目。如果此时队列是空的,那么您知道所有的脚本都完成了。
当单个脚本元素加载过程完成时,它将调用成功回调或失败回调。在加载失败的情况下,将loadQueue.noErrorsOccurred
值设置为false
。这样,当队列为空时,您将知道调用哪个最终回调。
提示这个简单的加载队列数据结构可以作为一个正式的类重新编写,使用添加和删除项目以及设置错误状态的方法,类似于你在第三章的“动画计时”一节中对
DrawCycle
类所做的。
清单 6-14 展示了这些技术。
清单 6-14 。使用队列加载多个垫片并跟踪过程
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<script src="../js-lib/detect-support.js"></script>
<script>
// Create a registry of HTML features that we need and shims to apply if they
// are not present. The registry will be an array of objects; each object will
// consist of a feature name and a path to a shim to apply if that feature is
// not supported.
var featureRegistry = [
{
’featureName’ : ’localStorage’,
’shim’ : ’../js-lib/webstorage-shim.js’
},
{
’featureName’ : ’requestAnimationFrame’,
’shim’ : ’../js-lib/animationframe-shim.js’
}
];
/**
* Dynamically loads a script and invokes an optional callback.
* @param {string} srcUrl The URL of the script file to load.
* @param {function=} onLoadCallback An optional function to call when the
* script is loaded.
* @param {function=} onErrorCallback An optional function to call if the script
* fails to load.
*/
function loadScript(srcUrl, onLoadCallback, onErrorCallback) {
// Create a script tag.
var newScript = document.createElement(’script’);
// Apply the load callback, if one was provided.
if (onLoadCallback) {
if (newScript.readyState) {
// Internet explorer.
newScript.onreadystatechange = function() {
if (newScript.readyState == ’loaded’ ||
newScript.readyState == ’complete’) {
newScript.onreadystatechange = null;
onLoadCallback.call();
}
};
} else {
// Every other browser in the universe.
newScript.onload = onLoadCallback;
}
}
// Apply the error callback, if one was provided.
if (onErrorCallback) {
newScript.onerror = onErrorCallback;
}
newScript.src = srcUrl;
document.querySelector(’head’).appendChild(newScript);
}
/**
* Verifies all features in the registry and applies shims as needed.
* @param {function=} onLoadCallback An optional callback function. If no shims
* were loaded this callback will be invoked immediately, otherwise it will
* be invoked after all shims have successfully loaded.
* @param {function=} onErrorCallback An optional callback function which will
* be invoked if even one of the shim scripts fails to load.
*/
function verifyAllFeatures(onLoadCallback, onErrorCallback) {
// Create loading queue. This queue consists of an error condition boolean
// and a simple array of entries.
window.loadQueue = {
’noErrorsOccurred’ : true,
’queue’ : []
};
// Flag for all feature support.
var allFeaturesSupported = true;
featureRegistry.forEach(function(currFeature) {
if (!supportedFeatures[currFeature.featureName]) {
// A feature is not supported.
allFeaturesSupported = false;
// Add an entry to the loading queue.
window.loadQueue.queue.push(true);
/**
* Callback function that is invoked when the shim script is loaded.
* Removes an entry from the loading queue and if the queue is then empty
* invokes one of the callbacks. If the queue is in an error condition,
* the error callback is invoked. Otherwise, the load callback is invoked.
*/
var handleThisLoad = function() {
// Remove entry from the loading queue.
window.loadQueue.pop();
// If the queue is empty, all scripts are loaded and we can invoke the
// callback.
if (window.loadQueue.queue.length === 0) {
// Check for error condition.
if (window.loadQueue.noErrorsOccurred) {
// Everything loaded, so call the load callback, if one was
// provided.
if (onLoadCallback) {
onLoadCallback.call();
}
} else {
// At least one of the scripts failed to load, so call the error
// callback, if one was provided.
if (onErrorCallback) {
onErrorCallback.call();
}
}
}
};
/**
* Callback function that is invoked when the shim script fails to load.
* Places the load queue in an error state and removes an entry. If the
* queue is then empty it invokes the final error callback.
*/
var handleThisError = function() {
// Immediately put the load queue into an error condition.
window.loadQueue.noErrorsOccurred = false;
// Remove entry from the loading queue.
window.loadQueue.pop();
// If the queue is empty, we need to invoke the error callback, if one
// was provided.
if (window.loadQueue.queue.length === 0 && onErrorCallback) {
onErrorCallback.call();
}
};
// Call the loadScript function with our custom handlers.
loadScript(currFeature.shim, handleThisLoad, handleThisError);
}
}, this);
if (allFeaturesSupported && onLoadCallback) {
onLoadCallback.call();
}
}
// Test for supported features.
var supportedFeatures = new DetectHTML5Support();
verifyAllFeatures(initApplication, handleScriptLoadError);
/**
* Handles an error during a script load.
*/
function handleScriptLoadError() {
console.log(’A script failed to load.’);
// Etc.
}
/**
* Hypothetical function for initializing the application.
*/
function initApplication() {
console.log(’Application continues...’);
// Etc.
}
</script>
</body>
</html>
在这个例子中,您首先创建要检查的测试和要加载的垫片的注册表。您还添加了一个执行以下操作的verifyAllFeatures
函数:
- 它检查每个功能的支持。
- 对于每个不支持的功能,它执行以下操作:
- 它向加载队列添加一个条目。
- 它为这个脚本创建一个加载回调函数,从加载队列中删除一个条目,如果队列为空,则调用最后一个回调函数。如果队列处于错误状态,则调用的最后一个回调是错误回调,否则是加载回调。
- 它为这个脚本创建一个错误回调函数,该函数立即将队列设置为错误状态。然后,它从队列中删除一个条目,如果队列是空的,它调用最后一个错误回调。
- 最后,它用新创建的回调函数调用
loadScript
函数。
- 如果所有特性都被支持,它调用
initApplication
函数继续应用。
运行时,该脚本将验证注册表中指定的所有功能,并根据结果调用适当的回调。
这种技术有一个主要缺点:对于每个所需的填充程序,它将生成一个单独的脚本标记,从而生成 HTTP 请求。如果你有几个垫片加载,这本身就可以导致你的应用明显的延迟。如果任何一个垫片是资源密集型的,那将会进一步降低它的速度。让这个问题变得复杂的是,你通常只需要为老版本的浏览器添加垫片,这些浏览器通常只运行在老版本的硬件上,有老版本的操作系统,所以它们已经受到了资源的限制。这对于系统资源非常有限的移动应用来说尤其成问题。
如果您需要垫片,您对此无能为力,您应该加载它们以便您的应用可以工作。帮助缓解这个问题的一个方法是确保在用户界面中提供正在加载的反馈,这样用户就知道应用并没有冻结。另一种减少加载垫片费用的方法是将其分散开来。到目前为止,在所有这些例子中,你已经一次测试了所有的特性。然而,使用像这样的动态技术的一个好处是,您可以在需要的时候测试特性。毕竟,如果用户从未进入应用中需要某个特定特性的部分,就没有必要为该特性加载填充程序。如果填充程序是一个大文件或者是资源密集型的,这一点尤其重要。
你可以很容易地添加一个新函数来检查注册表中的单个特性,如清单 6-15 所示。
清单 6-15 。用于检查单个特征的功能
/**
* Checks a single feature and applies a shim if needed.
* @param {string} featureName The name of the feature to check.
* @param {function=} onLoadCallback An optional function to call when the shim
* is loaded.
* @param {function=} onErrorCallback An optional function to call if the shim
* fails to load.
* @return {boolean} True if the feature was supported natively, or false if
* the feature was not supported and a shim was applied.
*/
function verifyFeature(featureName, onLoadCallback, onErrorCallback) {
var returnVal = true;
featureRegistry.forEach(function(currFeature) {
if ((currFeature.featureName === featureName) &&
!supportedFeatures[currFeature.featureName]) {
loadScript(currFeature.shim, onLoadCallback, onErrorCallback);
returnVal = false;
}
});
return returnVal;
}
// Test for supported features.
var supportedFeatures = new DetectHTML5Support();
// Verify the Animation Timing feature.
if (verifyAllFeatures(’requestAnimationFrame’, initApplication,
handleScriptLoadError)) {
initApplication();
}
/**
* Handles an error during a script load.
*/
function handleScriptLoadError() {
console.log(’A script failed to load.’);
// Etc.
}
/**
* Hypothetical function for initializing the application.
*/
function initApplication() {
console.log(’Application continues...’);
// Etc.
}
该功能通过featureRegistry
运行,直到找到与所需特征相对应的条目。然后,它检查支撑,并在需要时应用垫片。对于这个函数,如果不需要填充程序,它将返回true
而不是调用回调函数本身。这为回调的调用方式提供了更大的灵活性,允许您根据特性支持调用不同的函数。
浏览器支持、功能检测和垫片的在线资源
既然您可以检测特征并根据需要加载垫片,那么您需要加载垫片。为 HTML5 特性构建垫片的范围从简单的(结构元素垫片)到中等的(在 HTTP Cookies 上实现 Web 存储特性)到极其复杂的(实现 Web 套接字)。好消息是已经为大多数 HTML5 特性编写了垫片。
我能用吗
位于www.caniuse.com
的“我能使用数据库 吗”可能是研究浏览器支持和填补 HTML5 特性以及 CSS3 和高级 JavaScript 特性的最重要的资源。该网站有最新的浏览器支持表,表明对相关功能的支持程度,包括浏览器支持该功能的时间。还包括全球支持百分比、可视化支持数据的各种方式、相关规范的链接、文章、垫片和出色的定制编码测试套件。
实现现代化
位于www.modernizr.com
的 Modernizr 是一套功能检测脚本,用于检测对 HTML5 和 CSS3 功能的支持。Modernizr 还使用 YepNope ( www.yepnopejs.com
)实现垫片的动态加载;但是,他们建议动态地将服务器上所需的垫片合并到一个文件中,从而节省 HTTP 响应(就应用效率而言,这是非常昂贵的)。Modernizr 在他们的 wiki 上也有一个专门为 HTML5、CSS5 和 JavaScript 函数填充的页面,位于https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills
。
HTML5 岩石
位于www.html5rocks.com
的 HTML5 Rocks 网站 ,是关于 HTML5、CSS3 和 JavaScript 特性的文章的重要资源。文章包括教程、最佳实践、垫片等。
websim
位于http://afarkas.github.io/webshim/demos
的 Webshim 库 是一个多填充库,可以在旧浏览器上实现几个 HTML5 特性。
示例项目:MobiDex ,一款移动灵巧的益智游戏
灵巧拼图是历史上最古老的拼图之一。灵巧拼图最简单的版本是一个放在手掌中的小板子,上面有一个滚珠轴承或大理石。拼图的目标是引导球从一个位置到另一个位置,而不让它通过板上的洞落下。其他灵巧拼图包括迷宫和多球游戏,目标是让球停在特定的地方,要么得分,要么完成一幅画。
使用 HTML5 技术,特别是设备运动和动画计时 API,您可以轻松地在移动设备上实现灵巧拼图的版本。你的灵巧拼图,称为 MobiDex(移动灵巧),将由一个正方形游戏场组成。在游戏区域内,你将绘制一个“球”,当用户倾斜他们的设备时,这个球将被激活。您还将在游戏场地中绘制一组目标以及一组障碍。玩家将有有限的机会收集所有的目标,同时避免障碍。每当玩家遇到障碍,就会失去一次机会。如果他们能在用完“生命”之前收集到所有的目标,他们就赢得了游戏。
像这样的游戏表面上听起来很简单,但仔细想想,它其实非常复杂。像这样的项目中最大的困难之一是成功地捕获所有的需求,这样你就对你面前的任务有了一些概念。因为这个项目是一个游戏,所以从用户的角度来定义项目是有意义的。从用户的角度定义以用户为中心的项目的一个很好的工具是一种叫做“用户故事”的技术用户故事 是一个简单的陈述,它从用户的角度描述了一个单一的特性。用户故事类似于用例,但是更小更紧凑,通常定义一个单一的特性,而不是一个工作流。用户故事的典型模式是这样的:
作为一个<类型的用户>,我想要<的功能>,以便<想要的目标>。
该模式不是一成不变的,可以修改以适用于给定的项目。例如,在你的项目中只有一种类型的用户(玩家),所以真的没有必要在每个用户故事中重复。偶尔指定的功能是它自己的目标,所以不需要期望的目标子句。
提示用户故事是敏捷软件开发中经常使用的技术。
游戏的一组用户故事如下:
- 作为一名玩家,我希望游戏有一个明确的竞技场,所以我知道边界是什么。
- 我希望游戏有一个“球”来响应我的设备的倾斜运动,这样我就可以玩游戏了。
- 我希望“球”永远不要离开比赛场地。
- 我希望游戏在场上有一组目标让我捡起来,这样我就能赢得游戏。
- 当我用“球”接触目标时,我希望目标从比赛场地消失,这样我就知道还剩下多少个目标。
- 我希望游戏在场上有一组障碍,这样游戏才具有挑战性。
- 我希望目标和障碍物不要在一个上面,这样我就可以成功地收集所有的目标而不需要碰到障碍物。
- 我想有有限的机会收集所有的目标,同时避免障碍,使游戏更具挑战性。
- 每次我的“球”碰到障碍物,我都会失去一次机会。
- 我希望游戏能清楚地显示我何时撞上了障碍物。
- 我希望能够看到我还有多少机会赢得这场比赛。
- 我希望游戏能告诉我是赢了还是输了。
- 我希望游戏结束后能够重启,这样我就可以继续玩了。
这些用户故事定义了一组要构建的特性:目标、障碍、球、剩余机会数量的指示器、玩家期望的交互等等。
您将需要以下功能:
- 能够显示运动场、球、障碍物、目标等的 UI。
- 一种生成目标和障碍物随机集合的方法。目标和障碍物之间的唯一区别是颜色,所以你应该能够编写一组代码来生成两者。
- 在屏幕上移动球的方法。这将不得不使用设备定向 API,类似于我们在第五章的清单 5-7 中所做的。但是,在您的例子中,我们不希望
deviceorientation
事件驱动屏幕的重绘;我们想使用动画计时 API 中的requestAnimationFrame
。 - 一种确定球与目标和障碍物之间碰撞的方法。有几种方法可以做到这一点;最简单的方法是跟踪每个目标和障碍物的坐标,并随着游戏的进行将它们与球的坐标进行比较。
- 指示坐标的统一方式——您可以为此创建一个简单的类,然后根据需要实例化它。
- 一种初始化和重置游戏的方式,以便可以重播。
比赛场地 UI
首先,构建运动场用户界面。你要做的第一个决定是用来实现游戏 UI 的技术。你可以使用画布,但你并不真的需要。你的游戏有一个相当简单的界面;你所需要做的就是展示一个游戏场和一些物品在上面。您可以使用 HTML 和 CSS 轻松实现您需要的内容。这也有速度相当快的优点,这很好,因为这个游戏的目标是移动设备。
首先在屏幕上画一个简单的运动场。在该区域内,您将布置球、障碍物、目标和游戏结束信息。在字段上方,您将显示剩余生命的数量。您还将在运动场上方添加一个标题。基本标记如清单 6-16 中的所示。
清单 6-16 。MobiDex 游戏区的标记
<h1>MobiDex</h1>
<div id="remaining-balls"></div>
<div id="container-field">
<div id="game-field"></div>
<div id="ball"></div>
<div id="gameover" class="hidden"></div>
</div>
如你所见,有一个生命容器,一个球,游戏结束信息(可用于输赢),以及一个绘制目标和障碍的区域。
为了提高效率,保持简单的样式。从一个 200 像素宽 200 像素高的字段开始。在字段周围绘制一个简单的 1 像素边框(因此,根据 CSS box 模型,字段的尺寸实际上需要为 202 像素宽,202 像素高)。该字段将被绝对定位在屏幕上,这允许您相对于它的坐标原点绝对定位它内部的元素。
“球”以及目标和障碍物将是 div 元素。为了统一,它们都是 10 像素宽乘 10 像素高。球是蓝色的,目标是绿色的,障碍是红色的。给他们 50%的border-radius
,让他们看起来都很圆。
每当玩家遇到障碍时,就会损失一个球。他们剩余的球数将显示在比赛场地上方的分区中。
其中一个用户故事详细说明了这样一个要求,即游戏必须让玩家清楚地知道他们何时遇到了障碍。当然,你会从剩余球的显示中移除一个球,但是玩家的眼睛会盯着比赛场地,可能不会注意到这一点。让玩家知道他们何时撞上了障碍物的一个简单方法是在碰撞过程中改变游戏场地的背景颜色。这很简单;它只需要向容器中添加一个 CSS 类。
最后,您将在 message 上为游戏提供一些样式,并为其提供不同的背景颜色:一种代表赢,一种代表输。
清单 6-17 显示了整个游戏的 CSS 结果。
清单 6-17 。用于 MobiDex 游戏场的 CSS
body {
font-family: arial, helvetica, sans-serif;
}
h1 {
text-align: center;
}
#remaining-balls {
height: 10px;
left: 50px;
position: absolute;
top: 109px;
}
#container-field {
border: 1px solid red;
height: 202px;
left: 50px;
position: absolute;
top: 120px;
width: 202px;
}
#game-field {
height: 202px;
left: 0px;
position: absolute;
top: 0px;
width: 202px;
}
#ball,
.life {
background-color: blue;
height: 10px;
left: 0px;
position: absolute;
top: 0px;
width: 10px;
}
#gameover {
border-radius: 5px;
font-size: 2em;
font-weight: bold;
margin-left: 10px;
margin-right: 10px;
margin-top: 80px;
padding: 5px 0;
position: relative;
text-align: center;
}
.obstacle,
.target {
height: 10px;
position: absolute;
width: 10px;
}
.obstacle {
background-color: red;
}
.target {
background-color: green;
}
#ball,
.life,
.obstacle,
.target {
border-radius: 50%;
}
.hidden {
display: none;
}
.winner {
background-color: rgb(116, 216, 94);
}
.loser {
background-color: rgb(255, 85, 85);
}
.collision {
background-color: rgba(215, 44, 44, 0.4);
}
当你渲染游戏场地时,包括样本目标和障碍物,结果看起来会像图 6-2 。
图 6-2 。一个 MobiDex 游戏的示例渲染图
生成障碍和目标
可能最容易的开始是生成障碍和目标。从高层次来说,您需要为每个障碍物和目标随机生成一组坐标。这并不难做到,但如果你只是产生随机数,很有可能你的一些障碍和目标会非常接近,如果不是正好在另一个上面的话。这违反了你的一个用户故事,它规定目标和障碍不应该在彼此之上。因此,你需要一种方法来检测新生成的障碍物或目标何时与现有障碍物或目标发生碰撞。这与您需要确定球是否与障碍物或目标碰撞的功能相同,因此这个代码路径可以实现两个目的。
既然你在处理坐标,创建一个简单的Coordinate
类,你可以在整个过程中使用,如清单 6-18 所示。
清单 6-18 。一个简单的Coordinate
类
/**
* Coordinate class.
* @param {number} xOrd The x ordinate of the coordinate.
* @param {number} yOrd The y ordinate of the coordinate.
* @constructor
*/
function Coordinate(xOrd, yOrd) {
this.x = xOrd;
this.y = yOrd;
}
这个简单的类记录了 x 和 y 值(分别对应于 CSS 属性left
和top
)。你可以只保存一个对元素本身的引用,并在需要的时候获取左侧和顶部的 CSS 属性,但是这些属性实际上是带有单位的字符串(例如,"5px"
),所以你需要将它们重新转换为数字,以便根据冲突检测的需要相互比较。因为这些数字是我们自己生成的,所以您可以存储它们并在需要时随时使用,不需要解析。
您需要一种方法来生成上下边界之间的随机数。最初你可能会认为 0 到 200 之间的任何值都是可行的,因为比赛场地是 200 × 200。然而,这并没有考虑元素的宽度和高度;如果你将一个 10 × 10 的格放在(200, 200)
处,它将会在比赛场地之外。为了避免不小心碰到比赛场地外的目标或障碍物,请将随机数限制在 10 到 190 之间的整数。清单 6-19 显示了一个实用函数。
清单 6-19 。用于在两个边界之间生成随机整数的效用函数
/**
* Returns a random integer between the specified minimum and maximum values.
* @param {number} min The lower boundary for the random number.
* @param {number} max The upper boundary for the random number.
* @return {number}
*/
getRandomIntegerBetween_ = function(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
要生成新的Coordinate
,执行如下操作:
var target = new Coordinate(getRandomIntegerBetween(10, 190), getRandomIntegerBetween(10, 190));
您可以将Coordinates
存储在数组中,一个用于目标,一个用于障碍物。这样你就可以比较新的Coordinates
来确定它们是否太接近现有的。
比较Coordinates
为了检测冲突,你需要一个Coordinate
来检查,以及一个Coordinates
数组来检查它。如果Coordinate
太靠近数组中的任何一个坐标,你可以返回一个碰撞。
但是,什么决定了“太近”?有几种方法可以确定这一点,但对于你的简单游戏来说,你可以检查目标Coordinate
的给定纵坐标(x 或 y)是否在你正在检查的Coordinate
的定义范围内。该范围由灵敏度值定义。所以给定一个目标Coordinate
(x.t,y.t)和一个原始Coordinate
(x.o,y.o)和一个灵敏度 s:
(x.o - s) < x.t < (x.o + s)
(y.o - s) < y.t < (y.o + s)
如果两个不等式都成立,目标Coordinate
与原始Coordinate
足够接近,以至于它们的相关元素在视觉上发生碰撞。当然,这只是一个近似值,但是对于如此小的元素来说,应该是可行的。
清单 6-20 显示了一个执行这个检查的函数。
清单 6-20 。检查目标坐标和现有坐标数组之间的冲突
/**
* Check to see if the specified coordinates collide with an existing set of
* coordinates.
* @param {Coordinate} coordinate The coordinate to check.
* @param {number} sensitivity The sensitivity for a collision. If coordinates
* are within sensitivity distance of a target coordinate, a collision
* will be registered.
* @param {Array.<Coordinate>} arrTargetCoords An array of target coordinates
* to check against.
* @return {number} The index of the member of the target coordinates array
* that is being hit, or -1 if no collision is detected.
*/
checkCollision_ = function(coordinate, sensitivity, arrTargetCoords) {
// Loop through each target coordinate and compare the provided values.
for (var i = 0; i < arrTargetCoords.length; i++) {
var currObstacle = arrTargetCoords[i];
var xcoll = false;
var ycoll = false;
// If the provided x coordinate is within range of the obstacle coordinate,
// then there is an x collision.
if (((currObstacle.x - sensitivity) < coordinate.x) &&
(coordinate.x < (currObstacle.x + sensitivity))) {
xcoll = true;
}
// If the provided y coordinate is within range of the obstacle coordinate,
// Then there is a y collision.
if (((currObstacle.y - sensitivity) < coordinate.y) &&
(coordinate.y < (currObstacle.y + sensitivity))) {
ycoll = true;
}
// If there is both an x and a y collision, then return true.
if (xcoll && ycoll) {
return i;
}
}
return -1;
};
该函数采用一个Coordinate
、一个灵敏度值和一个数组Coordinates
进行检查。然后,它遍历数组中的每个Coordinate
,并确定是否发生了冲突。
既然有了检查冲突的方法,就可以构建一个函数来生成一个数组Coordinates
。你的游戏将需要两个Coordinates
阵列,一个用于目标,一个用于障碍。在为任一数组生成新的Coordinates
时,在检查冲突时,您需要检查两个数组。
此时,您应该开始考虑如何封装这些函数以及将要生成的数据。创建一个简单的类构造器很容易,它包含你到目前为止已经构建的函数,并添加目标和障碍的数组,如清单 6-21 所示。
清单 6-21 。MobiDex
类的开端
/**
* Creates a new game. Assumes that the required DOM elements are present.
* @constructor
*/
function MobiDex() {
/**
* Array of obstacle coordinates.
* @type {Array.<Coordinate>}
* @private
*/
this.arrObstacles_ = [];
/**
* Array of target coordinates.
* @type {Array.<Coordinate>}
* @private
*/
this.arrTargets_ = [];
/**
* The number of obstacles to draw on the game field.
* @type {number}
* @private
*/
this.numberOfObstacles_ = 10;
/**
* The number of targets to draw on the game field.
* @type {number}
* @private
*/
this.numberOfTargets_ = 10;
/**
* Checks to see if the specified coordinates collide with an existing set of
* coordinates.
* @param {Coordinate} coordinate The coordinate to check.
* @param {number} sensitivity The sensitivity for a collision. If coordinates
* are within sensitivity distance of a target coordinate, a collision
* will be registered.
* @param {Array.<Coordinate>} arrTargetCoords An array of target coordinates
* to check against.
* @return {number} The index of the member of the target coordinates array
* that is being hit, or -1 if no collision is detected.
* @private
*/
this.checkCollision_ = function(coordinate, sensitivity, arrTargetCoords) {
// Loop through each target coordinate and compare the provided values.
for (var i = 0; i < arrTargetCoords.length; i++) {
var currObstacle = arrTargetCoords[i];
var xcoll = false;
var ycoll = false;
// If the provided x coordinate is within range of the obstacle coordinate,
// then there is an x collision.
if (((currObstacle.x - sensitivity) < coordinate.x) &&
(coordinate.x < (currObstacle.x + sensitivity))) {
xcoll = true;
}
// If the provided y coordinate is within range of the obstacle coordinate,
// Then there is a y collision.
if (((currObstacle.y - sensitivity) < coordinate.y) &&
(coordinate.y < (currObstacle.y + sensitivity))) {
ycoll = true;
}
// If there is both an x and a y collision, then return true.
if (xcoll && ycoll) {
return i;
}
}
return -1;
};
/**
* Generates a set of random coordinates and adds them to the provided array.
* Tries to avoid duplicating too closely any previously-generated
* coordinates.
* @param {number} numberOfCoords The number of coordinates to generate.
* @param {Array} targetArray The array to fill with the new coordinates.
* @private
*/
this.generateCoords_ = function(numberOfCoords, targetArray) {
for (var i = 0; i < numberOfCoords; i++) {
var newCoord = new Coordinate(this.getRandomIntegerBetween_(10, 190),
this.getRandomIntegerBetween_(10, 190));
while (this.checkCollision_(newCoord, 15,
this.arrObstacles_.concat(this.arrTargets_)) > -1) {
newCoord.x = this.getRandomIntegerBetween_(10, 190);
newCoord.y = this.getRandomIntegerBetween_(10, 190);
}
targetArray.push(newCoord);
}
};
/**
* Returns a random integer between the specified minimum and maximum values.
* @param {number} min The lower boundary for the random number.
* @param {number} max The upper boundary for the random number.
* @return {number}
* @private
*/
this.getRandomIntegerBetween_ = function(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
};
/**
* Coordinate class.
* @param {number} xOrd The x ordinate of the coordinate.
* @param {number} yOrd The y ordinate of the coordinate.
* @param {Element} element A reference to the DOM element for these
* coordinates.
* @constructor
*/
function Coordinate(xOrd, yOrd, element) {
this.x = xOrd;
this.y = yOrd;
this.element = element;
}
新的MobiDex
类有你的getRandomIntegerBetween
和checkCollision
方法,以及障碍物和目标的数组。它也有应该产生的障碍和目标的数量的常数。
该类还有一个新方法:generateCoords
。这个方法有两个参数:一个数字(要生成的Coordinates
的数字)和一个目标数组,用它生成的Coordinates
填充。该方法使用checkCollisions
方法自动检查现有目标和障碍物阵列的碰撞。如果检测到碰撞,则生成一个新的Coordinate
并检查碰撞。该过程继续,直到新的Coordinate
不与任何现有的Coordinates
碰撞。
这足以生成目标和障碍物,并在界面中绘制它们。为此,你将添加一个新方法到类中,drawGameField
,如清单 6-22 中的所示。
清单 6-22 。drawGameField
方法和相关属性
/**
* Reference to the ’gamefield’ DOM element.
* @type {Element}
* @private
*/
this.gameField_ = document.getElementById(’game-field’);
/**
* Initializes the obstacles and targets and draws the UI.
* @private
*/
this.drawGameField_ = function() {
// Clear the game field.
this.gameField_.innerHTML = ’’;
// Fill up the obstacle and target arrays with random coordinates.
this.generateCoords_(this.numberOfObstacles_, this.arrObstacles_);
this.generateCoords_(this.numberOfTargets_, this.arrTargets_);
// Create a div that can be used as a template for cloning.
var templateDiv = document.createElement(’div’);
// Add the obstacles to the playing field.
this.arrObstacles_.forEach(function(currCoord) {
var newObstacle = templateDiv.cloneNode();
newObstacle.classList.add(’obstacle’);
newObstacle.style.left = currCoord.x + ’px’;
newObstacle.style.top = currCoord.y + ’px’;
this.gameField_.appendChild(newObstacle);
}, this);
// Add the targets to the playing field.
this.arrTargets_.forEach(function(currCoord) {
var newTarget = templateDiv.cloneNode();
newTarget.classList.add(’target’);
newTarget.style.left = currCoord.x + ’px’;
newTarget.style.top = currCoord.y + ’px’;
this.gameField_.appendChild(newTarget);
}, this);
};
这里,您已经向该类添加了两个新项目。第一个是对游戏字段 DOM 元素的引用,因为您将在整个游戏中使用它。(因为这个类会变得相当大,所以这个例子,以及以后的例子,将只显示你添加到类中的内容,而不是每次都重复所有的内容。在本章的最后,我将提供一个完整的、有组织的学习清单;您也可以下载示例。)
提示你的 DOM 结构相当简单。每次需要时获取元素引用不一定是性能问题,但是引用是不会改变的,所以您还不如获取引用并存储它以备后用。您将在整个课程中使用这一技巧。
第二种是drawGameField
方法,清空游戏场,生成目标和障碍Coordinates
的数组,然后在游戏场上绘制。您已经使用了创建模板元素并为每个新的Coordinate
克隆它的技术。
提示如果您在一次又一次地创建同一种元素,那么创建这样的模板元素并克隆它通常比重新创建每个元素更快。各种测试和结果见
http://jsperf.com/clonenode-vs-createelement-performance/32
。
在drawGameField
方法中还需要做一件事,那就是抽取剩余球的初始数量。为此,你将创建一个通用函数,你可以在任何时候调用它来更新用户界面的这一部分,因为当与障碍物发生碰撞时你会需要它(见清单 6-23 )。
清单 6-23 。updateRemainingBalls
法
/**
* Reference to the ’remaining-balls’ DOM element.
* @type {Element}
* @private
*/
this.remainingBalls_ = document.getElementById(’remaining-balls’);
/**
* The number of balls remaining.
* @type {number}
* @private
*/
this.balls_ = 3;
/**
* Updates the number of remaining balls displayed.
* @private
*/
this.updateRemainingBalls_ = function() {
// Clear the current lives.
this.remainingBalls_.innerHTML = ’’;
// Create a template that we can clone and use multiple times.
var lifeTemplate = document.createElement(’div’);
lifeTemplate.classList.add(’life’);
// Add an element for each life.
for (var i = 0; i < this.balls_; i++) {
var currLife = lifeTemplate.cloneNode();
currLife.style.left = (i * 15) + ’px’;
this.remainingBalls_.appendChild(currLife);
}
};
这里您添加了两个新的常量:对remaining-balls
DOM 元素的引用(再次缓存以备将来使用)和每场比赛开始时的球总数。您可以在drawGameField
方法的末尾添加对此方法的调用。
deviceorientation
事件处理程序
您将需要使用设备运动 API 的deviceorientation
事件来实际检测移动设备方向的变化,并使用它来计算将球移动到哪里。deviceorientation
事件连续触发,因此为了提高效率,它的事件处理程序应该尽可能精简。例如,您不想在事件处理程序中进行任何 DOM 操作(这应该在绘制周期中完成,我将在下一节中讨论)。
事件处理程序应该做两件事:
- 它应该确定球的新坐标,并将该信息存储在类中,并且
- 它应该检查新坐标上的冲突,并将该信息存储在类中。
然后,绘制循环实际上可以更新球的位置,并对已经发生的任何碰撞做出反应。这意味着事件处理程序只做一些算术并存储结果,这是非常有效的。
我将从第五章的例子 5-7 中借用一点代码,根据deviceorientation
事件公布的欧拉角计算一个新的Coordinate
。清单 6-24 显示了新方法及其相关属性。
清单 6-24 。deviceorientation
事件处理器
/**
* The current coordinate of the ’ball’.
* @type {Coordinate}
* @private
*/
this.currCoordinate_ = new Coordinate(0, 0);
/**
* The index of the obstacle that the ball is currently colliding with.
* @type {number}
* @private
*/
this.currentObstacleIndex_ = -1;
/**
* The index of the target that the ball is currently colliding with.
* @type {number}
* @private
*/
this.currentTargetIndex_ = -1;
/**
* Gets an ordinate based on a Euler Angle.
* @param {number} angle The orientation angle that is inducing the change.
* @param {number} ord The previous value of the ordinate.
* @return {number} The new value of the ordinate.
* @private
*/
this.getOrd_ = function(angle, ord) {
var delta = Math.round(angle - (angle * 0.3));
var tempVal = ord + delta;
if (tempVal > 0) {
ord = Math.min(192, tempVal);
} else {
ord = 0;
}
return ord;
};
/**
* Handles a deviceorientation event from the window.
* @param {DeviceOrientationEvent} event The device orientation event object.
* @private
*/
this.handleDeviceOrientation_ = function(event) {
// Get the x and y positions and update the current coordiate with them.
this.currCoordinate_.x = this.getOrd_(event.gamma, this.currCoordinate_.x);
this.currCoordinate_.y = this.getOrd_(event.beta, this.currCoordinate_.y);
// Check for collisions.
this.currentObstacleIndex_ = this.checkCollision_(this.currCoordinate_, 10,
this.arrObstacles_);
this.currentTargetIndex_ = this.checkCollision_(this.currCoordinate_, 10,
this.arrTargets_);
};
这段代码添加了一些新的常量:球的当前位置Coordinate
,您将使用事件处理程序来操作它;以及当前正在碰撞的障碍物和目标的索引。您正在使用checkCollision
方法来确定碰撞,就像您在生成障碍物和目标时所做的一样。
注意这里的
getOrd
方法中的 delta 计算已经针对 Safari Mobile 发布的欧拉角进行了优化。您可能需要为您的特定浏览器/平台组合调整计算。
抽奖周期
使用你在第五章中构建的DrawCycle
类,通过requestAnimationFrame
来简单管理绘制周期。
绘制循环方法需要做三件事:
- 通过
deviceorientation
事件处理程序将球定位在currCoordinate
属性中存储的Coordinate
处。 - 检查是否与障碍物发生碰撞,如果发生碰撞:
- 将碰撞类添加到容器中,
- 从用户的剩余号码中移除一个球并更新显示,以及
- 如果没有更多的球,以失败结束游戏。
- 检查是否与目标发生碰撞,如果发生了碰撞,
- 隐藏目标元素,并
- 检查是否所有元素都已收集完毕——如果是,以胜利结束游戏。
处理障碍物碰撞有点复杂,因为在绘制周期的多次迭代中,您可能会与同一个障碍物发生碰撞(例如,想象玩家非常小心,并且非常缓慢地移动球,因此它与障碍物碰撞了大约一秒钟)。无论球与障碍物碰撞了多长时间,与障碍物的碰撞只能从用户的总球数中移除一个球。为了防止类似的问题,当碰撞第一次发生时,存储当前处于碰撞中的障碍物的引用。然后在随后的绘制循环中,忽略与同一元素的进一步碰撞。当碰撞结束时,您将清除引用。
当与目标发生碰撞时,您需要隐藏游戏场上相关的目标元素。最简单的方法是在用drawGameField
方法生成元素时,在元素的Coordinate
中存储一个 DOM 引用。这将需要对该方法以及Coordinate
类进行修改,如清单 6-25 中的所示。
清单 6-25 。绘制周期、关联的类属性以及对类方法的更新
/**
* Reference to the ’gameover’ DOM element.
* @type {Element}
* @private
*/
this.domGameOver_ = document.getElementById(’gameover’);
/**
* Reference to the ’gamefield’ DOM element.
* @type {Element}
* @private
*/
this.gameField_ = document.getElementById(’game-field’);
/**
* Reference to the ball DOM element.
* @type {Element}
* @private
*/
this.ball_ = document.getElementById(’ball’);
/**
* The number of targets that have been collected.
* @type {number}
* @private
*/
this.collectedTargets_ = 0;
/**
* Reference to the current obstacle during a collision event. Stored between
* draw cycles to prevent firing multiple collisions.
* @type {Coordinate}
* @private
*/
this.currObstacle_ = new Coordinate(0, 0);
/**
* Initializes the obstacles and targets and draws the UI.
* @private
*/
this.drawGameField_ = function() {
// Clear the game field.
this.gameField_.innerHTML = ’’;
// Fill up the obstacle and target arrays with random coordinates.
this.generateCoords_(this.numberOfObstacles_, this.arrObstacles_);
this.generateCoords_(this.numberOfTargets_, this.arrTargets_);
// Create a div that can be used as a template for cloning.
var templateDiv = document.createElement(’div’);
// Add the obstacles to the playing field.
this.arrObstacles_.forEach(function(currCoord) {
var newObstacle = templateDiv.cloneNode();
newObstacle.classList.add(’obstacle’);
newObstacle.style.left = currCoord.x + ’px’;
newObstacle.style.top = currCoord.y + ’px’;
this.gameField_.appendChild(newObstacle);
}, this);
// Add the targets to the playing field.
this.arrTargets_.forEach(function(currCoord, index) {
var newTarget = templateDiv.cloneNode();
newTarget.classList.add(’target’);
newTarget.style.left = currCoord.x + ’px’;
newTarget.style.top = currCoord.y + ’px’;
this.gameField_.appendChild(newTarget);
// Store a reference to the new element in the array, we will need it
// later.
currCoord.element = newTarget;
this.arrTargets_.splice(index, 1, currCoord);
}, this);
// Update the lives displayed.
this.updateRemainingBalls_();
};
/**
* Draws the screen for the game: positions the ’ball’ and updates the number
* of lives as necessary. Registered in the draw cycle.
* @private
*/
this.drawScreen_ = function() {
// Move the "ball."
this.ball_.style.top = this.currCoordinate_.y + ’px’;
this.ball_.style.left = this.currCoordinate_.x + ’px’;
// Check for obstacle collisions.
if (this.currentObstacleIndex_ > -1) {
// Yes, there is a collision active. Check to see if it is a new
// collision.
var obstacle = this.arrObstacles_[this.currentObstacleIndex_];
if ((this.currObstacle_.x != obstacle.x) &&
(this.currObstacle_.y != obstacle.y)) {
// It is a new collision.
// Add the collision class to the game field.
this.gameField_.classList.add(’collision’);
// Store the current obstacle for the next check.
this.currObstacle_ = obstacle;
// A collision with an obstacle costs a life.
this.balls_--;
this.updateRemainingBalls_();
// If we’re out of lives, the game is over.
if (this.balls_ <= 0) {
this.gameOver_(false);
}
}
} else {
// There is no collision active.
// Remove the collision class from the game field.
this.gameField_.classList.remove(’collision’);
// Clear the current obstacle cache.
this.currObstacle_ = new Coordinate(0, 0);
}
// Check for target collisions.
if (this.currentTargetIndex_ > -1) {
// A target has been hit! Get the reference to the DOM element.
var hitEl = this.arrTargets_[this.currentTargetIndex_].element;
// If the element is not hidden, we need to hide it.
if (!hitEl.classList.contains(’hidden’)) {
hitEl.classList.add(’hidden’);
// Increment the collected targets counter.
this.collectedTargets_++;
// If that was the last target, the game is won!
if (this.collectedTargets_ >= this. numberOfTargets_) {
this.gameOver_(true);
}
}
}
};
/**
* Ends the game.
* @param {boolean} isWon Whether the game was won or lost.
* @private
*/
this.gameOver_ = function(isWon) {
if (isWon) {
this.domGameOver_.classList.remove(’loser’);
this.domGameOver_.classList.add(’winner’);
this.domGameOver_.innerHTML = ’Winner!’;
} else {
this.domGameOver_.classList.remove(’winner’);
this.domGameOver_.classList.add(’loser’);
this.domGameOver_.innerHTML = ’Try Again!’;
}
this.domGameOver_.classList.remove(’hidden’);
};
};
/**
* Coordinate class.
* @param {number} xOrd The x ordinate of the coordinate.
* @param {number} yOrd The y ordinate of the coordinate.
* @param {Element=} element A reference to the DOM element for these
* coordinates.
* @constructor
*/
function Coordinate(xOrd, yOrd, element) {
this.x = xOrd;
this.y = yOrd;
this.element = element;
}
对drawGameField
方法的更改被加粗,以便于查看(方法的其余部分与之前相同)。您还更新了Coordinate
类,以包含一个可选的元素属性,如果元素是目标,您可以使用该属性来存储对这些坐标处的元素的引用。
drawScreen
方法的行为如前所述,如果用户赢了或输了游戏,就调用gameOver
方法。gameOver
方法在 DOM 元素上显示游戏,并更新其内容和样式以反映输赢。
初始化游戏
MobiDex
类缺少了一些东西:
- 您需要注册
deviceorientation
事件处理程序。 - 您需要实例化一个
DrawCycle
对象并启动动画。 - 您需要在类上发布一个公共方法,可以调用该方法来启动游戏。
- 你需要有一种方法来重置游戏,以便它可以再次运行。
您可以使用相同的公共方法来启动和重新启动游戏,因为这两个代码路径几乎是相同的。主要区别在于,前两个动作(创建DrawCycle
对象和注册事件处理程序)应该只做一次,即游戏第一次启动时。因此,您必须将它们分解成一个单独的方法,并且只调用该方法一次。
要重置游戏,您需要将几个职业属性恢复为默认值。要玩这个游戏,你需要画出游戏场地,然后开始抽签循环,如清单 6-26 所示。
清单 6-26 。游戏初始化
/**
* Whether or not the game has been initialized.
* @type {boolean}
* @private
*/
this.isInitialized_ = false;
/**
* The draw cycle object for the game.
* @type {DrawCycle}
* @private
*/
this.drawCycle_ = new DrawCycle();
/**
* Start the game. Initializes data structures, draws the UI, and starts the
* animation cycle.
*/
this.startGame = function() {
// Reset the game variables.
this.reset_();
// Hide the game over message.
this.domGameOver_.classList.add(’hidden’);
// Draw a random game field.
this.drawGameField_();
if (!this.isInitialized_) {
this.init_();
}
// Start the draw cycle.
this.drawCycle_.startAnimation();
};
/**
* Resets game variables to their base state.
* @private
*/
this.reset_ = function() {
this.balls_ = 3;
this.arrObstacles_ = [];
this.arrTargets_ = [];
this.collectedTargets_ = 0;
this.currCoordinate_ = new Coordinate(0, 0);
this.currentObstacleIndex_ = -1;
this.currentTargetIndex_ = -1;
};
/**
* Initialize the game for the first time.
* @private
*/
this.init_ = function() {
// Register the device orientation event handler on the window object.
window.addEventListener(’deviceorientation’,
this.handleDeviceOrientation_.bind(this),
false);
// Add the draw method to the draw cycle.
this.drawCycle_.addAnimation(this.drawScreen_.bind(this));
this.isInitialized_ = true;
};
现在这个类上有一个公共方法,startGame
,当你想开始一个新游戏时,可以调用它,不管是第一个还是后续的游戏。如果需要,这个方法初始化游戏,更新默认值,绘制游戏区域,并开始动画。
假设您已经在文件mobidex.js
中保存了MobiDex
和Coordinate
类,现在您可以将它们加载到您的 HTML 文档中,如清单 6-27 中的所示。
清单 6-27 。完成的游戏
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>The HTML5 Programmer’s Reference</title>
<style>
// [...]
</style>
<script src="../js-lib/drawcycle.js"></script>
<script src="../js-lib/mobidex.js"></script>
</head>
<body>
<h1>MobiDex</h1>
<div id="remaining-balls"></div>
<div id="container-field">
<div id="game-field"></div>
<div id="ball"></div>
<div id="gameover" class="hidden"></div>
</div>
<script>
// Create a new instance of the game.
var myGame = new MobiDex();
// Attach an event handler to the game over message so that the user can restart
// the game.
document.getElementById(’gameover’).addEventListener(’click’, function() {
myGame.startGame();
}, false);
// Start the game.
myGame.startGame();
</script>
</body>
</html>
再次为了节省空间,你省略了 CSS,它没有改变。它不仅创建了一个新的 MobiDex 实例并启动了游戏,它还满足了您的最后一个要求:用户可以点击元素上的游戏并启动一个新游戏。
附加练习
这只是 MobiDex 游戏的开始。以下是您可以进行的一些修改:
- 添加评分:对每个目标奖励一分。在随后的回合中继续得分。第一次失败以最终得分结束游戏。将最终分数保存在本地存储器中。
- 添加一个计时器:给游戏添加一个全局计时器,在屏幕上倒计时。玩家必须在计时器结束前完成尽可能多的回合。在本地存储中保存最多的回合数。
- 添加自定义:添加滑块自定义增量计算,使球移动得更快或更慢。添加滑块来控制障碍物和/或目标的数量。将自定义保存在本地存储中。
完整的列表
清单 6-28 提供了完整的MobiDex
和Coordinate
类。
清单 6-28 。MobiDex 和 Coordinate 类的完整列表
/**
* Creates a new game. Assumes that the required DOM elements are present.
* @constructor
*/
function MobiDex() {
/**
* Whether or not the game has been initialized.
* @type {boolean}
* @private
*/
this.isInitialized_ = false;
/**
* Reference to the ’gameover’ DOM element.
* @type {Element}
* @private
*/
this.domGameOver_ = document.getElementById(’gameover’);
/**
* Reference to the ’gamefield’ DOM element.
* @type {Element}
* @private
*/
this.gameField_ = document.getElementById(’game-field’);
/**
* Reference to the ball DOM element.
* @type {Element}
* @private
*/
this.ball_ = document.getElementById(’ball’);
/**
* Reference to the ’remaining-balls’ DOM element.
* @type {Element}
* @private
*/
this.remainingBalls_ = document.getElementById(’remaining-balls’);
/**
* The current coordinate of the ’ball’.
* @type {Coordinate}
* @private
*/
this.currCoordinate_ = new Coordinate(0, 0);
/**
* The index of the obstacle that the ball is currently colliding with.
* @type {number}
* @private
*/
this.currentObstacleIndex_ = -1;
/**
* The index of the target that the ball is currently colliding with.
* @type {number}
* @private
*/
this.currentTargetIndex_ = -1;
/**
* Reference to the current obstacle during a collision event. Stored between
* draw cycles to prevent firing multiple collisions.
* @type {Coordinate}
* @private
*/
this.currObstacle_ = new Coordinate(0, 0);
/**
* Array of obstacle coordinates.
* @type {Array.<Coordinate>}
* @private
*/
this.arrObstacles_ = [];
/**
* Array of target coordinates.
* @type {Array.<Coordinate>}
* @private
*/
this.arrTargets_ = [];
/**
* The number of targets that have been collected.
* @type {number}
* @private
*/
this.collectedTargets_ = 0;
/**
* The number of ’lives’ remaining.
* @type {number}
* @private
*/
this.balls_ = 3;
/**
* The draw cycle object for the game.
* @type {DrawCycle}
* @private
*/
this.drawCycle_ = new DrawCycle();
/**
* The number of obstacles to draw on the game field.
* @type {number}
* @private
*/
this.numberOfObstacles_ = 10;
/**
* The number of targets to draw on the game field.
* @type {number}
* @private
*/
this.numberOfTargets_ = 10;
/**
* Start the game. Initializes data structures, draws the UI, and starts the
* animation cycle.
*/
this.startGame = function() {
// Reset the game variables.
this.reset_();
// Hide the game over message.
this.domGameOver_.classList.add(’hidden’);
// Draw a random game field.
this.drawGameField_();
if (!this.isInitialized_) {
this.init_();
}
// Start the draw cycle.
this.drawCycle_.startAnimation();
};
/**
* Resets game variables to their base state.
* @private
*/
this.reset_ = function() {
this.balls_ = 3;
this.arrObstacles_ = [];
this.arrTargets_ = [];
this.collectedTargets_ = 0;
this.currCoordinate_ = new Coordinate(0, 0);
this.currentObstacleIndex_ = -1;
this.currentTargetIndex_ = -1;
};
/**
* Initialize the game for the first time.
* @private
*/
this.init_ = function() {
// Register the device orientation event handler on the window object.
window.addEventListener(’deviceorientation’,
this.handleDeviceOrientation_.bind(this),
false);
// Add the draw method to the draw cycle.
this.drawCycle_.addAnimation(this.drawScreen_.bind(this));
this.isInitialized_ = true;
};
/**
* Initializes the obstacles and targets and draws the UI.
* @private
*/
this.drawGameField_ = function() {
// Clear the game field.
this.gameField_.innerHTML = ’’;
// Fill up the obstacle and target arrays with random coordinates.
this.generateCoords_(this.numberOfObstacles_, this.arrObstacles_);
this.generateCoords_(this.numberOfTargets_, this.arrTargets_);
// Create a div that can be used as a template for cloning.
var templateDiv = document.createElement(’div’);
// Add the obstacles to the playing field.
this.arrObstacles_.forEach(function(currCoord) {
var newObstacle = templateDiv.cloneNode();
newObstacle.classList.add(’obstacle’);
newObstacle.style.left = currCoord.x + ’px’;
newObstacle.style.top = currCoord.y + ’px’;
this.gameField_.appendChild(newObstacle);
}, this);
// Add the targets to the playing field.
this.arrTargets_.forEach(function(currCoord, index) {
var newTarget = templateDiv.cloneNode();
newTarget.classList.add(’target’);
newTarget.style.left = currCoord.x + ’px’;
newTarget.style.top = currCoord.y + ’px’;
this.gameField_.appendChild(newTarget);
// Store a reference to the new element in the array, we will need it
// later.
currCoord.element = newTarget;
this.arrTargets_.splice(index, 1, currCoord);
}, this);
// Update the lives displayed.
this.updateRemainingBalls_();
};
/**
* Handles a deviceorientation event from the window.
* @param {DeviceOrientationEvent} event The device orientation event object.
* @private
*/
this.handleDeviceOrientation_ = function(event) {
// Get the x and y positions and update the current coordiate with them.
this.currCoordinate_.x = this.getOrd_(event.gamma, this.currCoordinate_.x);
this.currCoordinate_.y = this.getOrd_(event.beta, this.currCoordinate_.y);
// Check for collisions.
this.currentObstacleIndex_ = this.checkCollision_(this.currCoordinate_, 10,
this.arrObstacles_);
this.currentTargetIndex_ = this.checkCollision_(this.currCoordinate_, 10,
this.arrTargets_);
};
/**
* Draws the screen for the game: positions the ’ball’ and updates the number
* of lives as necessary. Registered in the draw cycle.
* @private
*/
this.drawScreen_ = function() {
// Move the "ball."
this.ball_.style.top = this.currCoordinate_.y + ’px’;
this.ball_.style.left = this.currCoordinate_.x + ’px’;
// Check for obstacle collisisons.
if (this.currentObstacleIndex_ > -1) {
// Yes, there is a collision active. Check to see if it is a new
// collision.
var obstacle = this.arrObstacles_[this.currentObstacleIndex_];
if ((this.currObstacle_.x != obstacle.x) &&
(this.currObstacle_.y != obstacle.y)) {
// It is a new collision.
// Add the collision class to the game field.
this.gameField_.classList.add(’collision’);
// Store the current obstacle for the next check.
this.currObstacle_ = obstacle;
// A collision with an obstacle costs a life.
this.balls_--;
this.updateRemainingBalls_();
// If we’re out of lives, the game is over.
if (this.balls_ <= 0) {
this.gameOver_(false);
}
}
} else {
// There is no collision active.
// Remove the collision class from the game field.
this.gameField_.classList.remove(’collision’);
// Clear the current obstacle stored in the this.
this.currObstacle_ = new Coordinate(0, 0);
}
// Check for target collisions.
if (this.currentTargetIndex_ > -1) {
// A target has been hit! Get the reference to the DOM element.
var hitEl = this.arrTargets_[this.currentTargetIndex_].element;
// If the element is not hidden, we need to hide it.
if (!hitEl.classList.contains(’hidden’)) {
hitEl.classList.add(’hidden’);
// Increment the collected targets counter.
this.collectedTargets_++;
if (this.collectedTargets_ >= this.arrTargets_.length) {
this.gameOver_(true);
}
}
}
};
/**
* Updates the number of remaining balls displayed.
* @private
*/
this.updateRemainingBalls_ = function() {
// Clear the current lives.
this.remainingBalls_.innerHTML = ’’;
// Create a template that we can clone and use multiple times.
var lifeTemplate = document.createElement(’div’);
lifeTemplate.classList.add(’life’);
// Add an element for each life.
for (var i = 0; i < this.balls_; i++) {
var currLife = lifeTemplate.cloneNode();
currLife.style.left = (i * 15) + ’px’;
this.remainingBalls_.appendChild(currLife);
}
};
/**
* Check to see if the specified coordinates collide with an existing set of
* coordinates.
* @param {Coordinate} coordinate The coordinate to check.
* @param {number} sensitivity The sensitivity for a collision. If coordinates
* are within sensitivity distance of a target coordinate, a collision
* will be registered.
* @param {Array.<Coordinate>} arrTargetCoords An array of target coordinates
* to check against.
* @return {number} The index of the member of the target coordinates array
* that is being hit, or -1 if no collision is detected.
* @private
*/
this.checkCollision_ = function(coordinate, sensitivity, arrTargetCoords) {
// Loop through each target coordinate and compare the provided values.
for (var i = 0; i < arrTargetCoords.length; i++) {
var currObstacle = arrTargetCoords[i];
var xcoll = false;
var ycoll = false;
// If the provided x coordinate is within range of the obstacle coordinate,
// then there is an x collision.
if (((currObstacle.x - sensitivity) < coordinate.x) &&
(coordinate.x < (currObstacle.x + sensitivity))) {
xcoll = true;
}
// If the provided y coordinate is within range of the obstacle coordinate,
// Then there is a y collision.
if (((currObstacle.y - sensitivity) < coordinate.y) &&
(coordinate.y < (currObstacle.y + sensitivity))) {
ycoll = true;
}
// If there is both an x and a y collision, then return true.
if (xcoll && ycoll) {
return i;
}
}
return -1;
};
/**
* Gets an ordinate based on a Euler Angle.
* @param {number} angle The orientation angle that is inducing the change.
* @param {number} ord The previous value of the ordinate.
* @return {number} The new value of the ordinate.
* @private
*/
this.getOrd_ = function(angle, ord) {
var delta = Math.round(angle - (angle * 0.3));
var tempVal = ord + delta;
if (tempVal > 0) {
ord = Math.min(192, tempVal);
} else {
ord = 0;
}
return ord;
};
/**
* Generates a set of random coordinates and adds them to the provided array.
* Tries to avoid duplicating too closely any previously-generated
* coordinates.
* @param {number} numberOfCoords The number of coordinates to generate.
* @param {Array} targetArray The array to fill with the new coordinates.
* @private
*/
this.generateCoords_ = function(numberOfCoords, targetArray) {
for (var i = 0; i < numberOfCoords; i++) {
var newCoord = new Coordinate(this.getRandomIntegerBetween_(10, 190),
this.getRandomIntegerBetween_(10, 190));
while (this.checkCollision_(newCoord, 15,
this.arrObstacles_.concat(this.arrTargets_)) > -1) {
newCoord.x = this.getRandomIntegerBetween_(10, 190);
newCoord.y = this.getRandomIntegerBetween_(10, 190);
}
targetArray.push(newCoord);
}
};
/**
* Returns a random integer between the specified minimum and maximum values.
* @param {number} min The lower boundary for the random number.
* @param {number} max The upper boundary for the random number.
* @return {number}
* @private
*/
this.getRandomIntegerBetween_ = function(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
/**
* Ends the game.
* @param {boolean} isWinner Whether the game was won or lost.
* @private
*/
this.gameOver_ = function(isWon) {
this.drawCycle_.stopAnimation();
if (isWon) {
this.domGameOver_.classList.remove(’loser’);
this.domGameOver_.classList.add(’winner’);
this.domGameOver_.innerHTML = ’Winner!’;
} else {
this.domGameOver_.classList.remove(’winner’);
this.domGameOver_.classList.add(’loser’);
this.domGameOver_.innerHTML = ’Try Again!’;
}
this.domGameOver_.classList.remove(’hidden’);
};
};
/**
* Coordinate class.
* @param {number} xOrd The x ordinate of the coordinate.
* @param {number} yOrd The y ordinate of the coordinate.
* @param {Element} element A reference to the DOM element for these
* coordinates.
* @constructor
*/
function Coordinate(xOrd, yOrd, element) {
this.x = xOrd;
this.y = yOrd;
this.element = element;
}
摘要
在这一章中,我已经讨论了在实际项目中使用 HTML5。我讲述了:
- 特征检测,
- 动态响应不同级别的 HTML5 支持,以及
- 研究 HTML5 支持和定位垫片的在线资源。
你还从零开始构建了一个完整的 HTML5 手机游戏,从用户故事开始,到工作代码结束。
本书的讨论章节到此结束。接下来的章节都是 HTML5 特性的参考章节,从 HTML5 元素参考开始。
七、HTML5 元素参考
本章为所有新的 HTML5 元素提供了详细的参考。元素在语义上被分组,所以所有提供分段语义的元素都在一起,所有提供分组语义的元素都在一起,等等。每个元素条目将包含以下内容:
- 元素及其功能的简要描述。
- 包含元素语法和简短示例的用法部分。
- 属性部分列出了可以在元素上设置的所有属性。
- 包含该元素所有相关标准的表格。
所有这些相同的元素在第二章中有更详细的介绍。您可以在那里找到深入的讨论,以及更多的例子和浏览器支持。
部分
HTML5 定义了几个新元素,用于标记较大文档中的内容部分。这些部分通常是独立的或不同的内容组。一些部分在单个文档中是可重复的。区段元素有助于提供文档的整体结构。
article
元素
article
元素用于表示较大文档中自包含的独立内容的一部分:一页博客文章中的一篇博客文章、报纸页面中的一个故事或较大页面上的一则广告。
用法〔??〕
元素用于表示内容的块部分,并在文档流中呈现为块元素(类似于div
或标题元素)。结束标记是必需的。
语法
<article>...</article>
清单 7-1 。文章元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<h1>First Article Title</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
</article>
<article>
<h1>Second Article Title</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
</article>
</body>
</html>
属性
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
translate
表 7-1。article
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/sections.html#the-article-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-article-element
|
aside
元素
aside
元素表示与其包含的内容无关或松散相关的内容:侧边栏、注释或评论。省略aside
元素中的内容不应该影响包含内容的含义。
用法〔??〕
元素用于表示内容的块部分,并在文档流中呈现为块元素(类似于div
或标题元素)。结束标记是必需的。
语法
<aside>...</aside>
清单 7-2 。旁白元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<h1>Article Title</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
<aside>
<h2>Aside Title</h2>
<p>Lorem ipsum dolor sit amet, consectetur nisi id gravida.</p>
<ul>
<li>Item</li>
<li>Item</li>
<li>Item</li>
</ul>
</aside>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
</article>
</body>
</html>
属性
这个元素可以有任何“全局”属性,这是所有 HTML 元素的标准。这些属性包括:
accesskey
class
contenteditable
contextmenu
dataset
dir
draggable
hidden
id
lang
spellcheck
style
tabindex
title
表 7-2。aside
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/sections.html#the-aside-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-aside-element
|
footer
元素
footer
元素 用于表示出现在包含部分末尾的内容。页脚通常提供有关其包含节的信息。每个部分最多应该有一个footer
。
用法〔??〕
元素用于表示内容的块部分,并在文档流中呈现为块元素(类似于div
或标题元素)。结束标记是必需的。footer
元素不能包含header
、footer
或main
元素。
语法
<footer>...</footer>
清单 7-3 。页脚元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<h1>Article Title</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
<footer>
<p>Sitemap:</p>
<ul>
<li>link</li>
<li>link</li>
<li>link</li>
</ul>
<p>Copyright notice.</p>
</footer>
</article>
</body>
</html>
属性
这个元素可以有任何“全局”属性,这是所有 HTML 元素的标准。这些属性包括:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-3。footer
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/sections.html#the-footer-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-footer-element
|
header
元素
header
元素用于表示一节开始处的一组介绍性内容。每个部分最多应该有一个标题。
用法〔??〕
元素用于表示内容的块部分,并在文档流中呈现为块元素(类似于div
或标题元素)。结束标记是必需的。header
元素不能包含header
、footer
或main
元素。
语法
<header>...</header>
清单 7-4 。标题元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<header>
<h1>Article Title</h1>
<ul>
<li>navlink 1</li>
<li>navlink 2</li>
<li>navlink 3</li>
</ul>
</header>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
</article>
</body>
</html>
属性
这个元素可以有任何“全局”属性,这是所有 HTML 元素的标准。这些属性包括:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-4。header
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/sections.html#the-header-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-header-element
|
nav
元素
nav
元素用于表示导航部分,该导航部分具有到其他页面或当前文档中的内容或部分的主要导航链接。
用法〔??〕
元素用于表示内容的块部分,并在文档流中呈现为块元素(类似于div
或标题元素)。结束标记是必需的。
语法
<nav>...</nav>
清单 7-5 。nav
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<header>
<h1>Article Title</h1>
<nav>
<ul>
<li>navlink 1</li>
<li>navlink 2</li>
<li>navlink 3</li>
</ul>
</nav>
</header>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
</article>
</body>
</html>
性能
这个元素可以有任何“全局”属性,这是所有 HTML 元素的标准。这些属性包括:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-5。nav
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/sections.html#the-nav-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-nav-element
|
section
元素
section
元素用于表示一组通用的内容,通常覆盖一个特定的主题。通常,部分的主题由一个子 header 元素表示。
用法〔??〕
usage
元素用于表示内容的块部分,并在文档流中呈现为块元素(类似于div
或标题元素)。结束标记是必需的。
语法
<section>...</section>
清单 7-6 。section
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<section>
<h1>Introduction</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
</section>
<section>
<h1>First Section</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
</section>
<section>
<h1>Second Section</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
</section>
</article>
</body>
</html>
属性
这个元素可以有任何“全局”属性,这是所有 HTML 元素的标准。这些属性包括:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-6。section
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/sections.html#the-section-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-section-element
|
分组
HTML5 标准定义了一组新的元素来按类型将数据分组,以区别于用于提供整体文档结构的 section 元素。
figure
和figcaption
元素
figure
元素用于将一组独立的数据组合在一起,该组数据作为一个单独的集合从文档的主要内容中引用。典型的用途是用于插图、图表、代码示例等。元素可选地用于为一个图形元素提供标题。
用法〔??〕
这些元素用于表示内容的块部分,并在文档流中呈现为块元素(类似于div
或标题元素)。两个元素都需要结束标记。元素是可选的,但是必须是一个figure
元素的子元素。
语法
<figure>
<figcaption>...</figcaption>
...
</figure>
清单 7-7 。figure
和figcaption
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<h1>Title</h1>
<figure>
<figcaption>Caption: Source Information</figcaption>
<img src="diagrams/diagram1-1.png" alt="Schematic diagram" />
</figure>
</article>
</body>
</html>
属性
这个元素可以有任何“全局”属性,这是所有 HTML 元素的标准。这些属性包括:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-7。figure and figcaption
要素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/grouping-content.html#the-figure-element``www.w3.org/TR/html5/grouping-content.html#the-figcaption-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-figure-element``www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-figcaption-element
|
main
元素
main
元素用作另一个元素的主要内容的容器。主要元素本身并不提供结构或者有助于文档的大纲。它只提供一个分组容器。
用法〔??〕
元素用于表示内容的块部分,并在文档流中呈现为块元素(类似于div
或标题元素)。结束标记是必需的。
语法
<main>...</main>
清单 7-8 。main
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<h1>Article Title</h1>
<main>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
</main>
<aside>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
</aside>
</article>
</body>
</html>
属性
这个元素可以有任何“全局”属性,这是所有 HTML 元素的标准。这些属性包括:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-8。main
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/grouping-content.html#the-main-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-main-element
|
语义学
HTML5 标准指定了几个新元素,旨在为定义数据部分的语义目的提供更多功能。因为它们旨在提供语义细节而不是结构,所以这些标记被呈现为内联元素。
不幸的是,在当前的浏览器实现中很少支持这些标签。
bdi
元素
bdi
元素 ( bdi
是“双向隔离”的缩写)用于隔离可能与周围文本格式化方向不同的文本部分——例如,当在其他英文页面中直接包含阿拉伯文本时。
用法〔??〕
元素用于表示包含在其他文本中的一部分文本,因此呈现为内联元素。
语法
<bdi>...</bdi>
清单 7-9 。bdi
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<h1>Article Title</h1>
<p>Lorem ipsum dolor sit amet, <bdi>consectetur adipiscing elit</bdi>. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
</article>
</body>
</html>
属性
这个元素可以有任何“全局”属性,这是所有 HTML 元素的标准。这些属性包括:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-9。bdi
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/text-level-semantics.html#the-bdi-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-bdi-element
|
data
元素
data
元素用于表示嵌入在文档中的机器可读数据。通常,数据将使用类型或数据属性嵌入到元素中。
用法〔??〕
这个元素用于将机器可读的数据嵌入到文档中。它通常不被呈现。不需要结束标记。
语法
<data value="someval">
清单 7-10 。data
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<h1>Article Title</h1>
<ul>
<li><data value="ser-123-456">Serial Number 1</li>
<li><data value="ser-123-856">Serial Number 2</li>
<li><data value="ser-123-204">Serial Number 3</li>
</ul>
</article>
</body>
</html>
属性
这个元素可以有任何“全局”属性,这是所有 HTML 元素的标准。这些属性包括:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-10。data
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/text-level-semantics.html#the-data-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-data-element
|
mark
元素
mark
元素用于表示一组数据中的出现次数。事件本身是上下文特定的。
用法〔??〕
该元素用于表示较大内容中的数据部分,因此呈现为内联元素。结束标记是必需的。
语法
<mark>...</mark>
清单 7-11 。mark
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<h1>Article Title</h1>
<p>Lorem ipsum dolor sit amet, <mark>consectetur</mark> adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
<p>Lorem ipsum dolor sit amet, <mark>consectetur</mark> adipiscing elit. Vestibulum
tempus in nisi id gravida. Nullam vitae velit tincidunt, vulputate
arcu nec, ullamcorper velit. In in nulla tellus.</p>
</article>
</body>
</html>
属性
这个元素可以有任何“全局”属性,这是所有 HTML 元素的标准。这些属性包括:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-11。mark
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/text-level-semantics.html#the-mark-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-mark-element
|
ruby
、rp
和rt
元素
ruby
、rp
和rt
元素用于创建 Ruby 注释,这是主文本旁边显示的短文本。通常,拼音注释用于指示东亚语言的发音。有关 Ruby 注释的详细信息,请参见www.w3.org/TR/ruby/
和http://en.wikipedia.org/wiki/Ruby_character
。
用法〔??〕
一个ruby
元素通常由一组被ruby
标签包围的内容组成,带有一个或多个rp
或rt
注释。
语法
<ruby>base<rt>annotation</ruby>
<ruby><rb>base<rt>annotation</ruby>
清单 7-12 。ruby
、rt
和rp
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<h1>Article Title</h1>
<ruby>B<rt>a<rt>a</ruby><ruby>A<rt>a<rt>a</ruby>
<ruby>S<rt>a<rt>a</ruby><ruby>E<rt>a<rt>a</ruby>
<ruby>BASE<rt>annotation 1<rt>annotation 2</ruby>
</article>
</body>
</html>
属性
这些元素可以有任何“全局”属性,这是所有 HTML 元素的标准。这些属性包括:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-12。ruby, rt,
和rp
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/text-level-semantics.html#the-ruby-element``www.w3.org/TR/html5/text-level-semantics.html#the-rt-element``www.w3.org/TR/html5/text-level-semantics.html#the-rp-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-ruby-element``www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-rt-element``www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-rp-element
|
time
元素
类似于data
元素,time
元素用于表示嵌入在文档中的机器可读的日期/时间数据。
用法〔??〕
这个元素用于将机器可读的数据嵌入到文档中。需要一个结束标记。
语法
<time>...</time>
清单 7-13 。time
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<h1>Article Title</h1>
<time>2011-11-12T14:54</time>
<time>2011-11-12T14:54:39</time>
<time>2011-11-12T14:54:39.929</time>
<time>2011-11-12 14:54</time>
<time>2011-11-12 14:54:39</time>
<time>2011-11-12 14:54:39.929</time>
</article>
</body>
</html>
属性
这个元素可以有任何“全局”属性,这是所有 HTML 元素的标准。这些属性包括:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-13。time
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/text-level-semantics.html#the-time-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-time-element
|
wbr
元素
断字机会标签用于指示文档流中的一个位置,在该位置浏览器可以启动一个换行符,尽管其内部规则可能不会这样做。它对双向排序没有影响,如果浏览器确实在标签处开始换行,则不使用连字符。
用法〔??〕
此元素用于指示文本中的分词机会,因此仅在需要分词时才呈现。因此,它应该包含在其他块元素(如段落)中。不需要结束标记。
语法
<wbr>
清单 7-14 。wbr
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<h1>Article Title</h1>
<p>Supercali<wbr>fragilistic<wbr>expialidocious and
antidis<wbr>establishment<wbr>arianism.</p>
</article>
</body>
</html>
属性
这个元素可以有任何“全局”属性,这是所有 HTML 元素的标准。这些属性包括:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-14。wbr
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/text-level-semantics.html#the-wbr-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-wbr-element
|
音频和视频内容
audio
元素
元素用于在网页中嵌入声音内容(通常是音频文件)。
过去,在文档中嵌入音频内容通常需要使用插件(最常见的是 Flash)。这具有相当普遍的好处,因为只要目标浏览器安装了插件,它就能够播放内容。围绕用户界面控制、处理不同文件格式以及动态流等特殊功能的所有复杂性都由插件软件来处理。
当实现嵌入声音内容的能力时,web 浏览器制造商必须自己处理这些问题。因此,音频播放器的用户界面控件的外观和功能因浏览器而异。
由于专利阻碍,每个浏览器还支持不同的文件格式,并且一些浏览器根据本地安装的软件在不同的操作系统上支持不同的文件格式。对音频文件格式、专利问题和操作系统支持的深入讨论超出了本书的范围,但是你可以在网上找到大量的信息。具体来说:
- Mozilla 开发者网络有一个很好的页面,讨论了各种音频格式及其在主流浏览器中的支持。
http://hpr.dogphilosophy.net/test/index.php
是一个页面,您可以访问它来测试您的浏览器对各种音频格式的支持。该页面还提供了一些关于各种格式及其在主流浏览器中的支持状态的有用信息。www.jwplayer.com/html5/formats/
JW 播放器是基于 HTML5 技术的专有音频/视频播放器(播放器的核心是开源)。该公司对 HTML5 音频和视频支持的状态有着明显的兴趣,他们维护着自己关于该主题的统计数据。
网络上还有其他可用的资源,但其中许多似乎已经过时(或者无法核实它们最后更新的时间)。
用法〔??〕
元素用于在文档中嵌入声音内容。可以使用src
属性或者使用包含在audio
元素中的source
元素来指定内容。有关使用source
元素的详细信息,请参见下一节。
该元素还可以包含零个或多个track
元素,为音频内容指定基于时间的数据(如字幕)。详情见track
元件一节。
此外,如果浏览器不支持音频元素,该元素可以选择性地包含将被呈现的其他元素。
<audio>
标签不是自动关闭的,因此开始和结束标签都是必需的。
语法
<audio></audio>
清单 7-15 。audio
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<p>Basic</p>
<audio controls="true" src="../media/winamp-llama.mp3">
<p>Your browser does not support the HTML5 audio tag.</p>
</audio>
<p>Using source Elements</p>
<audio controls="true">
<source src="testfile.mp3" type="audio/mpeg">
<source src="testfile.ogg" type="audio/ogg">
<p>Your browser does not support the HTML5 audio tag.</p>
</audio>
</body>
</html>
属性
音频元素支持以下属性:
autoplay
:这是一个布尔标志,当它被设置(设置为任何值,甚至是false
)时,将使浏览器立即开始播放音频内容,而不需要停下来进行缓冲。controls
:如果设置了该属性,浏览器将显示音频播放器的默认用户界面控件(音量控件、进度条/滑动条等)。).loop
:如果设置了该属性,浏览器将循环播放指定文件。muted
:该属性指定默认静音播放。preload
:该属性用于提示浏览器如何为指定内容提供最佳的用户体验。它可以有三个值:none
指定作者想要最小化音频内容的下载,可能因为内容是可选的,或者因为服务器资源是有限的。metadata
指定作者推荐下载音频内容的元数据(持续时间、曲目列表、标签等。)和可能的前几帧内容。auto
指定浏览器可以把用户的需求放在第一位,而不会给服务器带来风险。这意味着浏览器可以开始缓冲内容、下载所有元数据等等。
src
:这个属性指定内容的来源,就像一个img
元素一样。如果需要,可以省略这个属性,而在audio
元素中包含一个或多个source
元素。
此外,audio
元素支持以下全局属性:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-15。audio
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/embedded-content-0.html#the-audio-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-video-element
|
source
元素
source
元素用于为其父元素指定一个内容源。因此,源元素 s 必须总是包含在音频或视频元素 s 中。它本身不代表任何东西。
用法〔??〕
source
元素用于为audio
或video
元素指定一个内容源。允许多个source
元素。如果一个audio
或video
元素中包含多个source
元素,浏览器将按顺序检查每个元素,并获取和播放第一个指定以其支持的方式编码的内容的元素。这为各种浏览器中对音频和视频格式的分散支持提供了一种变通方法:只需以所需的格式对所需的内容进行编码,并根据需要使用尽可能多的源元素来指定不同编码的位置。
<source>
标签不需要结束标签,也不会在文档中呈现。所有的source
元素必须在任何track
元素之前。
语法
<source src="testfile.mp3">
清单 7-16 。source
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<audio controls="true">
<source src="testfile.mp3" type="audio/mpeg">
<source src="testfile.ogg" type="audio/ogg">
<p>Your browser does not support the HTML5 audio tag.</p>
</audio>
<video controls="true">
<source src='video.mp4' type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'>
<source src='video.mp4' type='video/mp4; codecs="avc1.58A01E, mp4a.40.2"'>
<source src='video.3gp' type='video/3gpp; codecs="mp4v.20.8, samr"'>
<p>Your browser does not support the HTML5 video tag.</p>
</video>
</body>
</html>
属性
一个source
元素有两个属性:
src
:src
属性用于提供适合包含元素的媒体资源的地址。该属性是必需的。type
:type
属性用于指定媒体资源的 MIME 类型。浏览器使用该类型属性来确定它是否可以播放媒体资源。如果它不能播放媒体资源,浏览器将不会尝试获取它,并将移动到下一个source
元素(如果有的话)。type
属性可以包含可选的codec
参数,该参数指定用于创建指定媒体的编解码器。codec
参数的语法由 RFC6381 控制,“桶媒体类型的codecs
和profiles
参数”
表 7-16。source
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 互联网工程任务组 | 提议的标准 | http://tools.ietf.org/html/rfc6381
|
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 语言参考 | www.w3.org/TR/html-markup/source.html
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content.html#the-source-element
|
track
元素
track
元素用于为音频或视频元素指定基于时间的数据,例如隐藏式字幕或字幕。像source
元素一样,track
元素本身不定义任何内容,并且必须是音频或视频元素的子元素。由track
元素指定的基于时间的数据可以以浏览器支持的任何方式格式化;最常见的格式是新的 WebVTT 格式。
WebVTT 格式的数据
Web 视频文本轨道格式 (WebVTT ) 标准为文本文件(UTF-8 编码)指定了特定的模式或格式,将任意数据与时间点相关联。通常数据是标题信息,但也可以是任何格式的数据,包括 XML、HTML 甚至 JSON。
有效的 WebVTT 文件由WEBVTT
声明、声明旁边的可选描述以及零个或多个提示或注释组成。因此文件
WEBVTT
是有效的 WebVTT 文件。
一个有效的提示由一个可选的提示标识符组成,下一行是提示计时(也可能包括提示设置),下一行是提示的内容。一个简单的例子如清单 7-17 所示。
清单 7-17 。李尔王的示例 WebVTT 隐藏字幕文件
WEBVTT
1 - Act 1, Scene 1
00:00:1.000 --> 00:00:1.500
Scene: King Lear's Palace
Enter Kent, Gloucester, and Edmund.
00:00:1.500 --> 00:00:2.000 position:10% size:50%
<v Kent> I thought the king had more affected the Duke of
Albany than Cornwall.
00:00:2.100 --> 00:00:3.500 position:10% size:50%
<v Gloucester> It did always seem so to us: but now, in the
division of the kingdom, it appears not which of
the dukes he values most; for equalities are so
weighed, that curiosity in neither can make choice
of either's moiety.
在这个例子中,有三个独立的提示,每个都有一个时间戳范围。随着视频的播放,每个提示会在适当的时间显示。WebVTT 标准包括格式化结果字幕的能力;在该示例中,对话框提示框被限制为视频视口宽度的 50%,并且被定位在远离视口左侧总视口宽度的 10%。
WebVTT 标准非常广泛,我不打算在这里全面介绍。详情见http://dev.w3.org/html5/webvtt/
的 W3C 标准。在 HTML5 Doctor 网站上还有一个很棒的教程,可以在http://html5doctor.com/video-subtitling-and-webvtt/
找到。
用法〔??〕
track
元素用于为包含的音频或视频元素指定包含基于时间的数据的文件。同一个音频或视频元素允许有多个track
元素。
track
标签是自结束的,不需要结束标签,并且track
标签必须跟在任何源标签之后。
语法
<track src="karaoki.vtt">
清单 7-18 。track
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<audio controls="true">
<source src="testfile.mp3" type="audio/mpeg">
<source src="testfile.ogg" type="audio/ogg">
<track src="karaoki.vtt" kind="captions" label="Karaoki Cues">
<p>Your browser does not support the HTML5 audio tag.</p>
</audio>
<video controls="true">
<source src='thetwotowers.mp4' type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"'>
<source src='thetwotowers.mp4' type='video/mp4; codecs="avc1.58A01E, mp4a.40.2"'>
<source src='thetwotowers.3gp' type='video/3gpp; codecs="mp4v.20.8, samr"'>
<track src="closed-captioning.vtt" kind="captions" label="Closed Captioning">
<track src="peter_fran_philippa.vtt" kind="subtitles" src="en" label="Director and
Writer Scene Notes">
<p>Your browser does not support the HTML5 video tag.</p>
</video>
</body>
</html>
性能
track
元素具有以下属性:
default
:表示这是内容的默认轨道,除非被用户覆盖,否则应该显示该轨道。kind
:表示文件中包含何种数据。有效值包括:captions
:表示数据是内容的转录或翻译(如隐藏式字幕)。chapters
:表示数据是导航内容时使用的一组章节标题或其他小节信息。descriptions
:数据是对视频或音频内容的描述,适合盲人(视频的情况下)或聋人(音频的情况下)。metadata
:数据是脚本使用的,不会直接显示给用户。subtitles
:字幕是父内容的附加内容,如场景信息、额外的叙述背景等。如果指定了这个kind
,那么还必须为内容指定srclang
属性。
label
:当用户正在浏览可用曲目时,可以呈现的曲目的用户可读标签。src
:内容的 URI。srclang
:轨道数据的语言,在 BCP 47 语言标签中(见http://tools.ietf.org/html/bcp47
处的 BCP 47 标准)。如果kind
设置为字幕,则必须指定该属性。
表 7-17。track
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| Internet 工程任务组(Internet Engineering Task Force) | 最佳当前实践 | http://tools.ietf.org/html/bcp47
|
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 编辑草稿 | http://dev.w3.org/html5/webvtt/
|
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 语言参考 | www.w3.org/html/wg/drafts/html/master/embedded-content.html#the-track-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content.html#the-track-element
|
video
元素
元素用于在网页中嵌入视频内容(通常是视频文件)。
过去,在文档中嵌入视频内容通常需要使用插件(最常见的是 Flash)。这具有相当普遍的好处,因为只要目标浏览器安装了插件,它就能够播放内容。围绕用户界面控制、处理不同文件格式以及动态流等特殊功能的所有复杂性都由插件软件来处理。
当实现嵌入视频内容的能力时,网络浏览器制造商必须自己处理这些问题。因此,视频播放器的用户界面控件的外观和功能因浏览器而异。
由于专利阻碍,每个浏览器还支持不同的文件格式,并且一些浏览器根据本地安装的软件在不同的操作系统上支持不同的文件格式。对视频文件格式、专利问题和操作系统支持的深入讨论超出了本书的范围,但是你可以在网上找到大量的信息。具体来说:
- Mozilla 开发者网络有一个很好的页面,讨论了各种视频格式及其在主流浏览器中的支持。
- Zencoder 是一家基于云的转码服务提供商。因为他们的主要业务是视频转码,所以他们非常了解各种级别的支持需要什么样的格式。
www.jwplayer.com/html5/formats/
JW 播放器是基于 HTML5 技术的专有音频/视频播放器(播放器的核心是开源)。该公司对 HTML5 音频和视频支持的状态有着明显的兴趣,他们维护着自己关于该主题的统计数据。
网络上还有其他可用的资源,但其中许多似乎已经过时(或者无法核实它们最后更新的时间)。
用法〔??〕
元素用于在文档中嵌入视频内容。可以使用src
属性或者使用包含在video
元素中的source
元素来指定内容。有关使用源元素的详细信息,请参见本章中的source
元素一节。
该元素还可以包含零个或多个track
元素,为视频内容指定基于时间的数据(如字幕)。有关使用轨道元件的详细信息,请参见本章中的track
元件一节。
此外,如果浏览器不支持音频元素,该元素可以选择性地包含将被呈现的其他元素。
<video>
标签不是自动关闭的,因此开始和结束标签都是必需的。
语法
<video></video>
清单 7-19 。video
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<p>Basic</p>
<video controls="true" src="../media/lotr_thetwotowers.mp4">
<p>Your browser does not support the HTML5 video tag.</p>
</video>
<p>Using source Elements</p>
<video controls="true">
<source src="../media/video-1.mp4" type="video/mp4">
<source src="../media/video-1.ogv" type="video/ogg">
<p>Your browser does not support the HTML5 video tag.</p>
</video>
</body>
</html>
属性
视频元素支持以下属性:
autoplay
:这是一个布尔标志,当设置为(任何值,甚至是false
)时,浏览器会立即开始播放视频内容,而不会停下来进行缓冲。controls
:如果设置了该属性,浏览器将显示视频播放器的默认用户界面控件(音量控件、进度条/滑动条等)。).height
:该属性可用于指定视频播放器的高度,以像素为单位。loop
:如果设置了该属性,浏览器将循环播放指定文件。muted
:该属性指定默认静音播放。poster
:该属性可用于指定视频播放前要显示的海报的 URL。如果没有指定海报,那么一旦加载,播放器将默认显示视频的第一帧。preload
:该属性用于向浏览器提供如何为指定内容提供最佳用户体验的提示。它可以采用以下值:none
:指定作者希望尽量减少视频内容的下载,可能是因为内容是可选的,也可能是因为服务器资源有限。metadata
:指定作者推荐下载视频内容的元数据(时长、曲目列表、标签等。)和可能的前几帧内容。auto
:指定浏览器可以把用户的需求放在第一位,而不会给服务器带来风险。这意味着浏览器可以开始缓冲内容、下载所有元数据等。
src
:该属性指定内容的来源。如果需要的话,可以省略这个属性,而在<video>
标签中包含一个或多个<source>
标签。width
:该属性可用于指定视频播放器的宽度,以像素为单位。
此外,video
元素支持以下全局属性:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-18。video
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/html5/embedded-content-0.html#the-video-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#the-video-element
|
互动元素
details
和summary
元素
details
和summary
元素用于提供渐进公开的基本解决方案。通过点击一个summary
元素的内容,相关联的details
元素内的内容被显示(或隐藏)。这种特殊的用户界面小部件通常被称为“expando ”,是基于 web 的应用和本地应用的通用 UI 组件。
目前在 Internet Explorer、IE Mobile(参见http://status.modern.ie/detailssummary
)、Firefox 或 Firefox for Android(参见https://bugzilla.mozilla.org/show_bug.cgi?id=591737
)中都不支持这些元素。然而,IE 团队正在考虑实现,Firefox 团队正在积极开发该功能。除此之外,Chrome、Chrome for Android、Android 浏览器、Safari 和 Safari Mobile 都很好地支持该功能。
用法〔??〕
标签应该一起使用。details
标签是父标签,第一个元素是summary
标签。当用户点击summary
标签的内容时,出现在summary
标签后的内容(但仍在details
标签内)将被显示或隐藏。
这两个标记都呈现为块元素。这些标记不是自结束标记,因此两个元素都需要结束标记。
语法
<details>
<summary>...</summary>
...
</details>
清单 7-20 。details
和summary
元素
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<h1>Using the <summary> and <details> tags</h1>
<details>
<summary>Item 1</summary>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus
accumsan orci nec justo rhoncus facilisis. Integer pellentesque
ipsum vitae semper lacinia. Quisque non nisl rutrum, porta est at,
ultrices neque. Aenean consequat, lacus vulputate vestibulum
faucibus, turpis magna mollis quam, a congue neque lorem at
justo.</p>
</details>
<details>
<summary>Item 2</summary>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus
accumsan orci nec justo rhoncus facilisis. Integer pellentesque
ipsum vitae semper lacinia. Quisque non nisl rutrum, porta est at,
ultrices neque. Aenean consequat, lacus vulputate vestibulum
faucibus, turpis magna mollis quam, a congue neque lorem at
justo.</p>
</details>
<details open>
<summary>Item 3--this one will be open by default</summary>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus
accumsan orci nec justo rhoncus facilisis. Integer pellentesque
ipsum vitae semper lacinia. Quisque non nisl rutrum, porta est at,
ultrices neque. Aenean consequat, lacus vulputate vestibulum
faucibus, turpis magna mollis quam, a congue neque lorem at
justo.</p>
</details>
<details>
<summary>Item 3</summary>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus
accumsan orci nec justo rhoncus facilisis. Integer pellentesque
ipsum vitae semper lacinia. Quisque non nisl rutrum, porta est at,
ultrices neque. Aenean consequat, lacus vulputate vestibulum
faucibus, turpis magna mollis quam, a congue neque lorem at
justo.</p>
</details>
</article>
</body>
</html>
属性
details 元素有一个open
属性,当这个属性存在时(即使设置为 false ),它将导致相关内容默认可见。否则,这两个元素都支持以下标准全局属性:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-19。details
和summary
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/html/wg/drafts/html/master/interactive-elements.html#the-details-element``www.w3.org/html/wg/drafts/html/master/interactive-elements.html#the-summary-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#the-details-element``www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#the-summary-element
|
表单元素
datalist
元素
datalist
元素提供了一种将数据列表与标准输入元素相关联的方法。当用户开始在输入字段中键入时,列表出现在下方,并且随着用户继续键入,选择范围变窄。用户可以随时使用箭头键从列表中选择一个项目。
这种类型的输入字段通常被称为“组合框”,是 web 和本机应用中常见的用户界面元素。
请注意,目前对该功能的支持非常有限。Internet Explorer 将其列为“已发货”,但在实现中存在重大缺陷(参见http://playground.onereason.eu/2013/04/ie10s-lousy-support-for-datalists/
中的讨论和示例)。Safari 目前不支持桌面或移动设备上的功能。
用法〔??〕
元素为一个输入字段提供了一个可过滤的默认条目列表。当用户在字段中键入内容时,列表会出现并缩小到与输入的字符相匹配的选项。用户可以随时从列表中选择一个项目,或者不断键入以输入自定义选项。
datalist
元素将一组option
元素作为子元素,很像表单select
元素。在支持该元素的浏览器中,datalist
元素不会呈现在文档中,可以出现在标记中的任何位置。要将给定的datalist
与特定的输入字段相关联,设置输入的列表属性以匹配期望的datalist
元素的id
属性。
语法
<datalist id="example">
<option value="val1">
<option value="val2">
...
</datalist>
<input list="example" />
清单 7-21 。datalist
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<!-- Note the datalist can be anywhere -->
<datalist id="browsers">
<option value="Chrome">
<option value="Firefox">
<option value="Internet Explorer">
<option value="Opera">
<option value="Safari">
</datalist>
<article>
<h1>Using the <datalist> tag</h1>
<input list="browsers" />
</article>
</body>
</html>
属性
datalist
元素支持以下属性:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-20。datalist
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/html/wg/drafts/html/master/forms.html#the-datalist-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#the-datalist-element
|
meter
元素
meter
元素在页面上以填充栏的形式提供了一个可视的指示器或计量器。该条旨在模拟具有已知范围或已知范围的一部分(例如,磁盘使用或音量响度)的测量。它不应该用来显示进步;为此,使用progress
元素。
用法〔??〕
meter
元素提供了一种度量建模的方法,因此具有允许您定义当前值以及最小和最大值甚至范围的属性。根据这些设置,栏的外观会有所不同。
<meter>
标签不是自动关闭的,需要关闭标签。如果浏览器不支持<meter>
标签,<meter>
标签可以包含其他内容。
语法
<meter></meter>
清单 7-22 。meter
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<h1>Using the <meter> tag</h1>
<p>Simple meter from 1 to 100, value set to 25:<br>
<meter min="1" max="100" value="25"></meter>
</p>
<p>Simple meter from 1 to 100, low range from 1 to 25, high range from
75 to 100, value set to 90:<br>
<meter min="1" max="100" low="25" high="75" value="90"></meter>
</p>
<p>Simple meter from 1 to 100, low range from 1 to 25, high range from
75 to 100, value set to 10:<br>
<meter min="1" max="100" low="25" high="75" value="10"></meter>
</p>
<p>Simple meter from 1 to 100, low range from 1 to 25, high range from
75 to 100, optimum set to 10, value set to 10:<br>
<meter min="1" max="100" low="25" high="75" optimum="10" value="10"></meter>
</p>
<p>Simple meter from 1 to 100, low range from 1 to 25, high range from
75 to 100, optimum set to 10, value set to 10:<br>
<meter min="1" max="100" low="25" high="75" optimum="10" value="90"></meter>
</p>
</article>
</body>
</html>
属性
meter
元素支持以下属性:
value
:要显示的当前值。如果指定,该值必须在min
和max
值范围内。如果没有设置value
,或者设置不正确,浏览器将默认为 0。如果指定但值大于max
属性,该值将被设置为max
属性的值。如果该值小于 min 属性,则该值将被设置为min
属性的值。min
:范围的最小值。如果未指定,则默认为 0。max
:范围的最大值。必须大于min
属性的值(如果指定)。默认为 1。low
:低量程的最高值。当值属性在低范围内时,默认情况下,该条将呈现黄色。high
:上限范围的最小值,范围从该值到max
属性的值。当值属性在高范围内时,默认情况下,该条将呈现黄色。optimum
:表示范围的最佳值。该值必须在范围的min
和max
值之间。如果使用了low
和high
范围,在其中一个范围内指定一个optimum
值将指示这些范围中的哪一个是优选的。当该值在首选范围内时,该条将呈现绿色。当它在另一个范围内时,它将呈现红色。
此外,仪表元素支持以下全局属性:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-21。meter
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/html/wg/drafts/html/master/forms.html#the-meter-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#the-meter-element
|
output
元素
output
元素提供了一种方式来指示作为表单的一部分完成的计算的输出(例如,利息计算)。元素没有特殊的功能,只是一种从语义上指示计算输出的方式。
用法〔??〕
默认情况下,元素呈现为内联元素。它不是自结束的,结束标记是必需的。
语法
<output>...</output>
清单 7-23 。output
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<input name="operand1" id="operand1" /> +
<input name="operand2" id="operand2" /> =
<output></output><br>
<button>Add</button>
<script>
// Get references to the elements we will need.
var myOutput = document.querySelector('output');
var in1 = document.getElementById('operand1')
var in2 = document.getElementById('operand2')
var myButton = document.querySelector('button');
// Add a click event handler to the button that adds the contents of the two
// fields. We'll use parseFloat to cast the value to a number; experiment by
// entering various values including numbers, characters, and combinations of
// characters and numbers. Especially try combinations that start with numbers.
myButton.addEventListener('click', function() {
myOutput.innerText = parseFloat(in1.value) + parseFloat(in2.value);
}, false);
</script>
</body>
</html>
性能
output
元素支持以下属性:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-22。output
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/html/wg/drafts/html/master/forms.html#the-output-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#the-output-element
|
progress
元素
元素用于在页面上提供一个进度指示器。它用于指示任务的进度或完成情况,并向用户提供已经完成的工作量和剩余工作量的概念。它不应用于可视化已知范围内的测量;为此,请使用meter
元素。
用法〔??〕
progress
元素提供了一种对正在进行的过程的完成进行建模的方法,因此它具有允许您定义当前值和最大值的属性。根据这些设置,栏的外观会有所不同。
<progress>
标签不是自动关闭的,需要关闭标签。如果浏览器不支持<progress>
标签,<progress>
标签可以包含其他内容。
语法
<progress></progress>
清单 7-24 。progress
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<article>
<h1>Using the <progress> tag</h1>
<p>Downloading file1<br>
<progress max="100" value="10">10/100</progress> 10%</p>
</article>
</body>
属性
progress
元素支持以下属性:
max
:活动的最大值。该值必须是有效的正浮点数。如果没有指定max
,最大值默认为 1。value
:进度的当前值。该值必须是介于 0 和max
(如果指定)或 1(如果未指定max
)之间的有效浮点数。如果没有指定value
,那么进度条被认为是不确定的,这意味着它正在建模的活动正在进行,但是没有给出需要多长时间才能完成的指示。
此外,progress
元素支持标准的全局属性:
accesskey
class
classlist
contenteditable
contextmenu
dataset
dir
draggable
dropzone
hidden
id
lang
spellcheck
style
tabindex
title
表 7-23。进度要素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/html/wg/drafts/html/master/forms.html#the-progress-element
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#the-progress-element
|
八、HTML5 API 参考
本章为所有新的 HTML5 JavaScript APIs 提供了详细的参考。有关这些 API 的详细讨论,包括示例和发布时的支持级别,请参见第三章。
服务器发送的事件
服务器发送的事件 API 允许 HTML5 客户端订阅服务器发布的事件服务。然后,服务器可以将事件传输到 HTML5 客户端。
服务器发送事件的 API 是全局 JavaScript 范围内的一个EventSource
构造函数。EventSource
对象实现了EventTarget
接口,类似于 DOM 元素(因此事件可以在其上发布,事件处理程序可以在其上注册)。当一个新的EventSource
被实例化时,一个事件服务的 URL 被指定。这将指示浏览器建立到指定 URL 的连接,并开始定期轮询新事件。当接收到一个事件时,EventSource
对象将发布一个包含被传输数据的事件。
服务器可以通过使用text/event-stream
MIME 类型提供对轮询查询的标准 HTTP 响应,向服务发布事件(如果不使用该 MIME 类型,与服务相关联的EventSource
对象将发布一个错误事件)。
EventSource
构造函数的 JavaScript API 是:
constructor EventSource(DOMString url)
interface EventSource implements EventTarget: {
readonly DOMString url;
readonly unsigned short readyState;
EventHandler onopen;
EventHandler onmessage;
EventHandler onerror;
void close();
}
语法
var myEventSource = new EventSource('http://www.example.com:8030/event-stream/');
EventSource
构造函数接受一个有效 URL 的单个参数,表示一个事件服务。生成的接口具有以下属性:
url
:服务的 URL。readyState
:接口当前的就绪状态,以整数表示;- 0:正在连接到服务。
- 1:连接到服务并主动侦听事件。
- 2: Closed(在调用了
close
方法之后,或者在连接中发生了致命错误)。
onopen
:open
事件接口。onmessage
:message
事件接口。onerror
:error
事件接口。close()
:close
法。调用此方法将关闭与服务的连接。
清单 8-1 展示了一个使用EventSource
构造函数的基本例子(注意,你需要从服务器上运行这个例子,而不是直接把它加载到浏览器中)。
清单 8-1 。使用EventSource
构造函数
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<h1>Server-sent Events Reference</h1>
<script>
/**
* Handles message events published by the EventSource.
* @param {EventSourceEvent} event
*/
function handleMessage(event) {
// Handle message.
console.log('A message was sent from the server: ', event.data);
}
/**
* Handles error events published by the EventSource.
* @param {EventSourceEvent} event
*/
function handleError(event) {
// Handle an error.
console.error('An error happened on the EventSource: ', event.data);
}
/**
* Handles an open event published by the EventSource.
* @param {EventSourceEvent} event
*/
function handleOpen(event) {
// Handle the open event.
console.log('The connection is now open.');
}
// Create a new connection to the server.
var targetUrl = 'http://www.service.com/my-event-service';
var myEventSource = new EventSource(targetUrl);
// Attach event handlers. Here we are using the addEventListener method.
// You could also directly attach the event handlers using the event interfaces,
// e.g. myEventSource.onmessage = handleMessage.
myEventSource.addEventListener('message', handleMessage);
myEventSource.addEventListener('error', handleError);
myEventSource.addEventListener('open', handleOpen);
</script>
</body>
</html>
从服务器发送的事件采用简单 HTTP 响应的形式,以文本/事件流 MIME 类型发送。事件由多行key
: value
对组成,并以双换行结束。有效密钥如下:
data
:指定要发送给客户端的一行任意数据,客户端将接收它作为event
对象的数据属性。用双换行('\n\n'
)终止data
值表示特定事件的结束。在一个事件中允许多个data
值;只需用单行换行(\n
’)结束每一个,用双行换行结束最后一个。event
:指定与此服务器发送的事件相关联的任意事件类型。这将导致从关联的EventTarget
对象调度同名的事件,从而允许从服务器触发open
、message
和error
之外的任意事件。如果未指定事件类型,该事件将仅触发EventTarget
上的message
事件。id
:指定与事件序列关联的任意 ID。在事件流上设置一个 ID 使浏览器能够跟踪最后触发的事件,如果连接断开,它将向服务器发送一个last-event-ID
HTTP 头。retry
:指定浏览器为下一个事件重新查询服务器之前的毫秒数。默认情况下,该值设置为 3000(三秒)。这使得服务器资源能够抑制浏览器查询并防止自己被淹没。
任何文本都可以作为服务器发送的事件进行传输:HTML、CSS、XML、JSON 等等。一个响应可以包含多个事件,一个给定的事件可以包含多个数据属性。例如:
event: watch\n
data: {\n
data: "type":"flash flood",\n
data: "counties":"['Jefferson', 'Arapahoe', 'Douglas', 'Broomfield']",\n
data: "from":"12:30 pm June 12, 2015",\n
data: "to":"7:00 am June 13, 2015",\n
data: "details":"The National Weather Service has issued a flash flood watch."\n
data: }\n
event: warning\n
data: {\n
data: "type":"severe thunderstorm",\n
data: "counties":"['Jefferson']",\n
data: "from":"12:30 pm June 12, 2015",\n
data: "to":"1:00 pm June 12, 2015",\n
data: "details":"The National Weather Service has issued a severe thunderstorm warning."\n
data: }\n\n
这个服务器发送的事件将触发相关联的EventTarget
对象上的watch
事件和warning
事件。watch
事件的数据将是 JSON 格式的文本:
{
"type":"flash flood",
"counties":"['Jefferson', 'Arapahoe', 'Douglas', 'Broomfield']",
"from":"12:30 pm June 12, 2015",
"to":"7:00 am June 13, 2015",
"details":"The National Weather Service has issued a flash flood watch."\n
}
warning
事件的数据将是 JSON 格式的文本:
{
"type":" severe thunderstorm ",
"counties":"['Jefferson']",
"from":"12:30 pm June 12, 2015",
"to":"1:00 pm June 12, 2015",
"details":" The National Weather Service has issued a severe thunderstorm warning."\n
}
表 8-1。服务器发送事件的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 起草 | http://dev.w3.org/html5/eventsource/
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#server-sent-events
|
WebSockets
WebSockets API 提供了一种通过维护网络连接在客户端和服务器之间进行全双工通信的方式。
WebSockets 的 API 是全局 JavaScript 范围内的一个WebSocket
构造函数。WebSocket
对象实现了EventSource
接口,类似于 DOM 元素(这意味着可以在其上调度事件,并在其上注册事件处理程序)。构造函数需要一个 URL(其协议必须是ws://
或wss://
),还可以带一个可选的协议参数。协议是一个字符串或字符串数组,每个字符串代表一个协议的名称。
当一个新的WebSocket
被实例化时,客户端立即向服务器发送一个标准的 HTTP 1.1 GET
请求,然后服务器将连接从 HTTP 升级到 WebSocket 网络协议。然后,连接就可以发送和接收数据了。
Strings
、Blobs
、ArrayBuffers
都可以通过插座传输。来自服务器的通信在EventTarget
接口上调度事件。与服务器的通信是通过WebSocket
对象的send
方法完成的。
API 的定义是:
constructor WebSocket(DOMString url, optional (DOMString or DOMString[]) protocols)
interface WebSocket implements EventTarget {
readonly DOMString url;
readonly unsigned short readyState;
readonly unsigned long bufferedAmount;
EventHandler onopen;
EventHandler onerror;
EventHandler onclose;
readonly DOMString extensions;
readonly DOMString protocol;
void close(optional unsigned short code, optional USVString reason);
EventHandler onmessage;
BinaryType binaryType;
void send(USVString|Blob|ArrayBuffer data);
};
语法
// Create a web socket without specifying protocols.
var myWebSocket = new WebSocket('ws://www.example.com/');
// Create a web socket and specify one or more protocols.
var myChatWebSocket = new WebSocket('ws://www.example.com/', chat');
var myWebSocket = new WebSocket('ws://www.example.com/', ['chat', 'json']);
接口的属性是:
url
:服务的 URL,在构造WebSocket
对象时设置。readyState
:表示连接的通信状态的整数值;- 0:客户端仍在连接到服务的过程中。
- 1:连接已经打开,可以使用了。
- 2:连接正在关闭。
- 3:连接关闭,不再活动。
bufferedAmount
:等待发送回服务器但尚未发送的字节数。onopen
:开放事件接口。onerror
:错误事件界面。onclose
:关闭事件界面。onmessage
:消息事件接口。extensions
:服务器使用的任何文件扩展名的名称(如 zip)protocol
:正在使用的协议名称。binaryType
:正在传输什么类型的数据(如'blob'
或'arraybuffer'
)。send
:方法send
,用于将数据传回服务器。接受一个参数,即要发送的数据。close
:关闭连接的close
方法。可以采用两个可选参数,这两个参数通常由使用的协议定义:code
:可选数字,代表关闭代码。reason
:包含关闭连接原因的字符串。
清单 8-2 展示了一个 WebSocket 的简单实现,包括存根事件处理程序(注意,你需要从服务器上运行这个例子)。
清单 8-2 。使用WebSocket
构造函数
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<h1>Web Sockets Reference</h1>
<script>
// Create a new web socket connection.
var socketUrl = 'ws://www.fgjkjk4994sdjk.com/';
var validProtocols = ['chat', 'json'];
var myWebSocket = new WebSocket(socketUrl, validProtocols);
/**
* Handles an error event on the web socket object.
*/
function handleError() {
console.log('An error occurred on the web socket.');
}
/**
* Handles a close event on the web socket object.
* @param {CloseEvent} event The close event object.
*/
function handleClose(event) {
console.log('The web socket connection was closed because ', event.reason);
}
/**
* Handles an open event on the web socket object.
* @param {OpenEvent} event The open event object.
*/
function handleOpen(event) {
console.log('The web socket connection is open.');
}
/**
* Handles a message event on the web socket object.
* @param {MessageEvent} event The message event object.
*/
function handleMessage(event) {
console.log('A message event has been sent.');
// The event object contains the data that was transmitted from the server.
// That data is encoded either using the chat protocol or the json protocol,
// so we need to deterine which protocol is being used.
if (myWebSocket.protocol === validProtocols[0]) {
console.log('The chat protocol is active.');
console.log('The data the server transmitted is: ', event.data);
// etc...
} else {
console.log('The json protocol is active.');
console.log('The data the server transmitted is: ', event.data);
// etc...
}
}
// Register the event handlers on the web socket.
myWebSocket.addEventListener('error', handleError);
myWebSocket.addEventListener('close', handleClose);
myWebSocket.addEventListener('open', handleOpen);
myWebSocket.addEventListener('message', handleMessage);
</script>
</body>
</html>
提示从头开始构建 WebSocket 服务器是一项复杂的任务。但是,您可以在项目中使用几个开源的 web socket 服务器。如果您想从头开始构建一个,请参阅 WebSocket 协议 RFC 中的第 4、5 和 6 节。
表 8-2。WebSockets 标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/websockets/
|
| WHATWG | 生活标准 | https://html.spec.whatwg.org/multipage/comms.html#network
|
| 请求评论 | 完成 | https://tools.ietf.org/html/rfc6455
|
跨文档信息/网络信息
浏览器将允许您在 iframes 中打开来自不同来源的文档,但是如果来自一个来源的脚本试图与来自另一个来源的内容交互,浏览器将抛出一个错误。跨文档消息传递 API(也称为 Web 消息传递)为一个框架中来自一个来源的脚本与另一个框架中来自另一个来源的脚本之间的通信定义了一种安全的方式。这允许来自多个来源的脚本更安全地相互交互。
跨文档消息传递规范在window
对象上定义了一个新方法和一个新事件。新方法是postMessage
,它有三个参数:
message
:您想要从当前上下文传输到目标上下文的消息。使用结构化克隆算法对消息进行序列化,除非您指定应该使用transfer
参数来传输对象。origin
:您期望目标上下文中的资源具有的来源。如果目标上下文中的资源没有指定的来源,该方法将不起作用。transfer
:作为消息一部分的对象数组,这些对象的所有权应该转移到新的上下文中。转移所有权意味着对象将被绑定到目标上下文的源。转移所有权仅限于ArrayBuffer
和MessagePort
对象。
注意结构化克隆算法被定义为 HTML5 规范的一部分。可以在
www.w3.org/TR/html5/infrastructure.html#safe-passing-of-structured-data
看。基本上,这个算法允许你从一个上下文传输任何东西到另一个上下文。例外情况是函数、DOM 元素和Error
对象,如果您试图传输它们,它们将抛出一个DATA_CLONE_ERROR
。
新事件是message
事件,当使用postMessage
方法传输消息时,该事件在window
对象上被调度。产生的事件对象将有两个重要的属性:
data
:该属性将包含从其他上下文发送的消息。source
:该属性将包含发送上下文的来源。您应该始终仔细检查消息来源的来源,以防止意外捕获和处理来自意外(可能是恶意)来源的事件。
语法
var targetIframe = document.getElementById('my-iframe');
targetIframe.contentWindow.postMessage('hello world', 'apress.com');
window.addEventListener('message', function(event) {
if (event.source === 'apress.com') {
console.log('A message was received: ', event.data);
}
});
为了演示如何使用 API,您需要两个来自不同来源的页面:一个宿主页面和一个目标页面。主页面将包含一个 iframe 来加载目标页面。宿主页面将向目标页面分派事件,目标页面将侦听消息事件并警告其内容。清单 8-3 是主页面。
清单 8-3 。主页
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<h1>Cross-Domain Messaging</h1>
<iframe id="target-iframe" src="target-page.html"></iframe>
<p><button id="clickme">Click to send a message to the iframe.</button></p>
<script>
// Create some objects to transfer.
var testBlob = new Blob(['some data']);
var testBuffer = new ArrayBuffer(100);
var testBuffer2 = new ArrayBuffer(8);
// To transfer multiple objects, we need to wrap them in a single carrier. The
// names of the properties don't matter, they're just serving as a place to
// store references to the buffer objects.
var transferObject = {
buffer1: testBuffer,
buffer2: testBuffer2
};
var targetFrame = document.getElementById('target-iframe');
// Reference to the button.
var clickme = document.getElementById("clickme");
// Add a click event handler to the button.
clickme.addEventListener("click", function() {
// Send a simple text string to the target frame.
targetFrame.contentWindow.postMessage('hello world', '*');
// Send a Blob to the target frame.
targetFrame.contentWindow.postMessage(testBlob, '*');
// Transfer multiple array buffers to the target frame.
targetFrame.contentWindow.postMessage(transferObject, '*',
[transferObject.buffer1, transferObject.buffer2]);
});
</script>
</body>
</html>
注意,iframe 元素的src
被设置为从相同的上下文中加载目标页面。如果您可以访问不同的域(或者甚至是运行在不同端口上的同一个域中的另一个 web 服务器),您可以从那里提供目标页面,从而充分证明 API 允许跨源发送消息。
清单 8-4 中的包含了目标页面。
清单 8-4 。目标页面
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<h1>Target iframe</h1>
<script>
/**
* Handles a message event on the window object.
* @param {MessageEvent} event A message event object.
*/
function handleMessage(event) {
// Create a string for alerting.
var strAlert = "Target iframe:\n";
if (event.data.buffer1) {
// The two buffers have been transferred.
strAlert += event.data.buffer1 + '\n';
strAlert += event.data.buffer2 + '\n';
} else {
// Just alert the data.
strAlert += event.data;
}
alert(strAlert);
}
// Register the event handler.
window.addEventListener("message", handleMessage, false);
</script>
</body>
</html>
若要运行该示例,请单击按钮。宿主页面将向目标页面发送三条消息,从而产生三个警报。
表 8-3。跨文档消息传递的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/webmessaging/
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/web-messaging.html#crossDocumentMessages
|
网络存储
新的 Web 存储 API 指定了一种在客户端存储信息的新方法。在 Web 存储出现之前,在客户端存储信息的标准方式是使用 HTTP cookies,这种方式既麻烦又不方便。网络存储提供了一种更易于使用的存储功能。
Web 存储在全局上下文中定义了两个新的接口对象:sessionStorage
和localStorage
。sessionStorage
界面用于存储单个浏览会话的数据。当用户关闭浏览器时,数据将被自动删除。localStorage
接口用于跨会话存储数据。即使用户关闭浏览器,存储在 localStorage 中的数据也将持续存在。
注意所有的浏览器都实现了某种形式的“隐私浏览”使用此功能时,当用户结束会话时,
localStorage
数据被删除。此外,许多浏览器现在都有在浏览器关闭时自动清除localStorage
的功能,即使是常规会话。您的应用不应该假设存储在localStorage
中的任何数据总是可用的,并且应该在预期的数据不存在时做出适当的响应。
API 还定义了storage
事件,该事件将在与发生存储更改的文档相同的所有文档的window
对象上调度,但不包括发生更改的窗口文档。对于其他 DOM 事件,如果您加载了一个页面并且调度了一个事件,则该事件将在当前页面上被调度。storage
事件不会在当前页面调度。如果在选项卡中打开了同一页面的多个版本,则该事件将在除当前活动窗口对象之外的每个窗口对象上调度。
注意目前,Internet Explorer 会调度所有文档中的存储事件,而不仅仅是非活动文档。有一个针对当前被推迟的
https://connect.microsoft.com/IE/feedback/details/774798/localstorage-event-fired-in-source-window
行为的 bug。
API 定义是:
interface Storage {
readonly unsigned long length;
DOMString? key(unsigned long index);
getter DOMString? getItem(DOMString key);
setter creator void setItem(DOMString key, DOMString value);
deleter void removeItem(DOMString key);
void clear();
};
interface WindowSessionStorage {
readonly attribute Storage sessionStorage;
};
interface WindowLocalStorage {
readonly attribute Storage localStorage;
};
localStorage
和sessionStorage
都实现了Storage
接口,因此具有相同的方法:
getItem(key)
:返回与指定键相关的数据。removeItem(key)
:删除与指定键相关的数据。setItem(key, data)
:用指定的键将数据存储在存储器中。clear()
:清除所有内容的存储。
每当使用这些方法中的任何一种更改了localStorage
时,就会在与当前文档相同的任何文档的window
对象上调度一个storage
事件。关联的event
对象是一个StorageEvent
对象,它具有以下属性:
target
:target
属性是对在其上调度事件的 DOM 元素的引用。在这种情况下,那就是window
的对象。type
:type
属性设置为storage
。key
:key
属性包含其相关数据已经改变的键。oldValue
:oldValue
属性包含数据的前一个值。newValue
:newValue
属性包含数据的新值。url
:url
属性包含托管文档的 URL。storageArea
:storageArea
属性将是对实际的localStorage
对象的引用。
清单 8-5 演示了网络存储的使用。
清单 8-5 。使用 Web 存储
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<h1>Web Storage Example</h1>
<script>
/**
* Handles a storage event.
* @param {StorageEvent} event The storage event object.
*/
function handleStorageEvent(event) {
var alertMsg = 'Storage event!\n';
alertMsg += 'key: ' + event.key + '\n';
alertMsg += 'oldValue: ' + event.oldValue + '\n';
alertMsg += 'newValue: ' + event.newValue + '\n';
alert(alertMsg);
}
// Register the event handler on the window object.
window.addEventListener('storage', handleStorageEvent, false);
// Check to see if we've visited this page before.
var myValue = localStorage.getItem('myKey');
if (myValue == null) {
alert('This is the first time you loaded this page! Now reload this page.');
localStorage.setItem('myKey', 'true');
} else {
alert('You have loaded this page before!');
localStorage.removeItem('myKey');
}
</script>
</body>
</html>
当你第一次加载这个例子时,它会告诉你这是你第一次加载这个页面。当您重新加载时,它会检测存储的信息,然后删除它,从而重置测试。如果在两个选项卡中打开示例,您将看到由非活动选项卡上调度的存储事件产生的警报。
表 8-4。网络存储标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/webstorage/
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/webstorage.html
|
拖放
新的 HTML5 拖放规范提供了一个本地 API 来处理浏览器中的拖放交互。API 是事件驱动的,使用它包括以下步骤:
- 将一个或多个对象声明为
draggable
,并附加所需的事件处理程序。 - 将
drop
事件处理程序附加到目标元素。 - 当用户拖动项目并将它们放到目标上时,会调度各种事件。
指定可拖动的元素:draggable
属性
draggable
属性是 DOM 元素的一个新属性,它指示元素作为拖动目标的可用性。该属性可以设置为三个值:
true
:表示元素是可拖动的。false
:表示元素不可拖动。auto
:应用浏览器的默认规则。对于大多数元素,默认规则是false
。(例外是选定的文本,它总是可以启动拖动交互。)
处理交互:拖放事件
API 指定了几个新事件,这些事件发生在拖动元素或它被拖动到的元素上:
dragstart
:从被拖动的元素调度。dragenter
:当一个可拖动的项目被拖动到任何元素中时,从该元素调度。dragover
:从任何一个元素连续调度,只要一个可拖动的项目在它上面。请注意,无论可拖动项是否在移动,此事件都会连续触发。dragleave
:当一个可拖动的项目离开它的边界时,从一个元素调度。drag
:从整个拖动序列中被拖动的元素调度。像dragover
一样,不管指针是否移动,这个事件都会被连续触发。dragend
:从鼠标释放时被拖动的元素调度。drop
:当用户通过释放鼠标按钮将可拖动的项目放到元素上时,从元素调度。
指定投放目标
API 指定了一个dropzone
属性,该属性应该指示元素可以是放置目标。然而,dropzone
属性没有得到广泛的支持,所以指定给定元素是有效目标的唯一方法是通过事件处理程序。
一般来说,DOM 中的大多数元素不应该是有效的放置目标,所以dragover
事件的默认动作是取消放置。因此,为了指示一个有效的拖放目标,您必须通过调用事件处理程序中的event
对象上的preventDefault()
方法来取消dragover
事件的默认动作。
dataTransfer
物体
所有的拖放事件都可以用标准的事件处理程序来处理,这些事件处理程序将接收一个event
对象作为参数。拖放的event
对象的属性之一是dataTransfer
对象。该对象用于控制拖放助手的外观(在拖放操作中跟随光标的幻影可视元素),指示拖放过程正在做什么,并轻松地将数据从dragstart
事件传输到drop
事件。
dataTransfer
对象有以下方法:
Event.dataTransfer.addElement(HtmlElement)
:指定拖动序列的源元素。这会影响到drag
和dragend
事件的触发位置。这是在拖动交互开始时自动设置的,所以您可能不需要更改它。Event.dataTransfer.clearData(opt_DataType)
:清除与特定DataType
相关的数据(见该列表中的setData
)。如果未指定DataType
,所有数据将被清除。Event.dataTransfer.getData(DataType)
:获取与特定DataType
相关的数据(见setData
,下一步)。Event.dataTransfer.setData(DataType, data)
:将指定的data
与DataType
关联。有效的DataTypes
取决于浏览器。Internet Explorer 只支持text
和url
的DataTypes
。其他浏览器支持标准 MIME 类型,甚至任意类型。data
必须是一个简单的字符串,但也可以是 JSON 格式的序列化对象。请注意,Firefox 要求在dragstart
事件期间用数据初始化dataTransfer
对象,以便正确触发拖放事件。Event.dataTransfer.setDragImage(HtmlElement, opt_offsetX, opt_offsetY)
:将拖动辅助图像设置为指定的 HTML 元素。默认情况下,辅助图像的左上角位于鼠标指针的下方,但是可以通过指定可选参数opt_offsetX
和opt_offsetY
进行偏移,以像素为单位。此方法在 Internet Explorer 中不可用,而且显然永远不会可用;参见http://connect.microsoft.com/IE/feedback/details/804304/implement-datatransfer-prototype-setdragimage-method
。
dataTransfer
对象还具有以下属性:
Event.dataTransfer.dropEffect
:拖放序列正在执行的拖放效果。有效值为copy
、move
、link
和none
。该值在dragenter
和dragover
事件中根据用户通过鼠标动作和修饰键的组合(例如,Ctrl-拖动、Shift-拖动、Option-拖动等)所请求的交互来自动初始化。).这些依赖于平台。只有由effectAllowed
(见下一页)指定的值才会真正启动拖放序列。Event.dataTransfer.effectAllowed
:指定该拖放序列允许哪些dropEffects
。有效值及其允许的效果如下:copy
:允许复制dropEffect
。move
:允许移动dropEffect
。link
:允许链接dropEffect
。copyLink
:允许复制和链接dropEffect
。copyMove
:允许复制和移动dropEffect
。linkMove
:允许链接和移动dropEffect
。all
:所有dropEffects
都是允许的。这是默认值。none
:不允许dropEffects
(该物品不能被丢弃)。
Event.dataTransfer.
files
:包含数据传输中所有可用文件的列表。只有将文件从桌面拖到浏览器时,才会有值。Event.dataTransfer.types
:包含所有已经添加到dataTransfer
对象中的DataTypes
的列表,按照添加的顺序排列。
清单 8-6 展示了拖放 API。
清单 8-6 。拖放 API 在工作
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
<style type="text/css">
#drag-target,
#drop-target {
float: left;
padding: 10px;
margin: 10px;
box-sizing: border-box;
}
#drag-target {
background-color: #008000;
width:75px;
height:75px;
}
#drop-target {
background-color: #0000FF;
width:150px;
height:150px;
}
.drag-over {
border: 5px solid #FF0000;
}
</style>
</head>
<body>
<h1>Drag and Drop Example</h1>
<div id="drop-target">Target</div>
<div id="drag-target" draggable="true">Drag me!</div>
<script>
/**
* Handles a dragStart event.
* @param {DragEvent} event The event object.
*/
function handleDragStart(event) {
// Set the data in the dataTransfer object to the id of the element being
// dragged.
event.dataTransfer.setData("Text", event.target.getAttribute('id'));
}
/**
* Handles a dragenter event.
* @param {DragEvent} event The event object.
*/
function handleDragEnter(event) {
// Apply a class to the element.
event.target.classList.add('drag-over');
}
/**
* Handles a dragleave event.
* @param {DragEvent} event The event object.
*/
function handleDragLeave(event) {
// Remove the class from the element.
event.target.classList.remove('drag-over');
}
/**
* Handles a dragover event.
* @param {DragEvent} event The event object.
*/
function handleDragOver(event) {
// Indicates this element is a valid drop target.
event.preventDefault();
}
/**
* Handles a drop event.
* @param {DragEvent} event The event object.
*/
function handleDrop(event) {
// Get a reference to the dragging element and append it to the drop target.
var src = event.dataTransfer.getData("Text");
event.target.appendChild(document.getElementById(src));
event.preventDefault();
}
// Register event handlers.
var dragTarget = document.getElementById('drag-target');
dragTarget.addEventListener('dragstart', handleDragStart);
var dropTarget = document.getElementById('drop-target');
dropTarget.addEventListener('dragenter', handleDragEnter);
dropTarget.addEventListener('dragleave', handleDragLeave);
dropTarget.addEventListener('dragover', handleDragOver);
dropTarget.addEventListener('drop', handleDrop);
</script>
</body>
</html>
当您运行这个示例时,您将能够拖动拖动目标(标记为“拖动我!”)放入拖放目标(标记为“目标”)。它在dragstart
事件期间将dataTransfer
对象中的数据设置为拖动目标的 ID,然后在drop
事件期间检索它,并使用它来获取对元素的引用,并将它移动到 DOM 中。这是拖放 API 的一个非常常见的用例。
表 8-5。拖放的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 建议 | www.w3.org/TR/html5/editing.html#dnd
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html
|
网络工作者
Web Workers API 使您能够通过创建(或“生成”)子进程来处理某些任务,从而创建线程化的 JavaScript 应用。每个工作器都运行自己的 JavaScript 上下文,并执行您为其设置的任何任务。Web worker 也可以产生其他 Web worker。
Web Worker 上下文和主 JavaScript 线程之间的通信是通过一个类似于 Web 消息传递所使用的postMessage
接口来完成的。这使您能够将数据传入和传出 Web Worker 上下文,但是因为所有上下文都是独立的,所以在上下文之间传递的任何数据都会被复制,除非您专门传输它。(有关使用postMessage
发送和传输数据的更多详细信息,请参见前面的“跨文档消息传递/Web 消息传递”部分)。
当您创建一个新的 Web Worker 时,您为它指定一个 JavaScript 文件来加载和运行。要启动它,向它发送一条消息(任何消息都可以)。工作线程可以将消息发送回父上下文或它可以访问的任何其他 Web 工作线程。
Web 工作者有一些重要的限制,旨在帮助避免编写多线程应用时固有的常见缺陷:
- Web Worker 运行在自己独立的 JavaScript 上下文中。它不能直接访问任何其他执行上下文中的任何内容,比如其他 Web Workers 或主 JavaScript 线程。
- Web Worker 上下文和主 JavaScript 线程之间的通信是通过一个类似于 Web 消息传递所使用的
postMessage
接口来完成的。这使您能够将数据传入和传出 Web Worker 上下文,但是因为所有上下文都是独立的,所以在上下文之间传递的任何数据都是复制的,而不是共享的。 - Web Worker 无法访问 DOM。Web 工作者可以使用的 DOM 方法只有
atob
、btoa
、clearInterval
、clearTimeout
、dump
、setInterval
和setTimeout
。 - Web Workers 受相同来源策略的约束,因此您不能从不同于原始脚本的来源加载 worker 脚本。
Web Workers API 在全局 JavaScript 作用域中采用了新的WebWorker
构造函数的形式:
constructor WebWorker(DOMstring url)
interface WebWorker implements EventTarget {
readonly WorkerLocation location;
void terminate();
OnErrorEventHandler onerror;
EventHandler onlanguagechange;
EventHandler onoffline;
EventHandler ononline;
EventHandler onmessage;
};
语法
var myWorker = new WebWorker('worker-script.js');
构造函数返回一个WebWorker
对象,它实现了EventTarget
接口。这些属性是:
location
:location
属性类似于document.location
对象,但是包含特定于 Web Worker 的信息(详见下文)。terminate()
:terminate
方法将为工作线程结束线程。一旦工人被解雇,就不可能重新开始。onerror
:当一个error
事件在 worker 上调度时,调用onerror
事件处理程序。onlanguagechange
:当用户在浏览器中更改他们的首选语言时,在 worker 上调度onlanguagechange
处理程序。onoffline
:当一个offline
事件在 worker 上调度时,调用onoffline
事件处理程序。当浏览器失去网络连接并且navigator.onLine
的值更改为false
时,就会出现这种情况。ononline
:当一个online
事件在 worker 上调度时,调用ononline
事件处理程序。当浏览器恢复网络连接时,会出现这种情况。onmessage
:当一个message
事件在 worker 上调度时,调用onmessage
事件处理程序。
Web Worker 内部的执行上下文与全局执行上下文明显不同。Web 工作人员无权访问 DOM,但他们可以访问以下属性和方法:
-
DOM 方法
atob
、btoa
、clearInterval
、clearTimeout
、dump
、setInterval
和setTimeout
。 -
XMLHttpRequest
构造函数,因此 Web 工作者可以执行异步网络任务。 -
WebSocket
构造函数,因此 Web 工作者可以创建和管理 Web 套接字(在撰写本文时,Firefox 没有为 Web 工作者启用WebSocket
;不过,这个功能正在实现中,你可以在https://bugzilla.mozilla.org/show_bug.cgi?id=504553
跟踪它的状态 -
Worker
构造函数,因此 Web workers 可以产生自己的 Workers(称为“子 Workers”)。截至本文撰写之时,Chrome 和 Safari 还没有为 Web Workers 实现Worker
构造函数。在https://code.google.com/p/chromium/issues/detail?id=31666
的 Chrome 和https://bugs.webkit.org/show_bug.cgi?id=22723
的 Safari 的 WebKit 都有一个 bug。从版本 10 开始,Internet Explorer 支持子工作器。 -
EventSource
构造函数,因此 Web 工作者可以订阅服务器发送的事件流。这似乎是一个非标准特性,但是在撰写本文时,似乎在所有主流浏览器中都可以使用。 -
Navigator
属性的特殊子集,可通过navigator
对象获得:-
navigator.language
:返回浏览器当前使用的语言。 -
navigator.onLine
:返回一个布尔值,表示浏览器是否在线。 -
navigator.platform
:返回表示主机系统平台的字符串。 -
navigator.product
:返回当前浏览器名称的字符串。 -
navigator.userAgent
: Returns the user agent string for the browser.这些属性的实现因浏览器而异,因此最好将所需的
Navigator
信息从主线程传递给 Web Worker。
-
-
在
location
对象上可用的Location
属性的特殊子集:location.href
:Web Worker 正在执行的脚本的完整 URL。location.protocol
:Web Worker 正在执行的脚本的 URL 的协议方案,包括最后的“:”。location.host
:Web Worker 正在执行的脚本的 URL 的主机部分(主机名和端口)。location.hostname
:Web Worker 正在执行的脚本的 URL 的主机名部分。location.port
:Web Worker 正在执行的脚本的 URL 的端口部分。location.pathname
:首字母“/”,后跟 Web Worker 正在执行的脚本的路径。location.search
:初始'?'后跟 Web Worker 正在执行的脚本的 URL 的参数(如果有的话)。location.hash
:Web Worker 正在执行的脚本的 URL 的片段标识符(如果有的话)后面的首字母' # '。
除此之外,Web 工作者还有一个特殊的方法只有他们可以使用:importScripts
。该方法接受 JavaScript 文件的单个 URL 或以逗号分隔的 URL 列表,并按顺序加载和执行。importScripts
方法是一个阻塞方法,并且受同源策略的约束。
语法
importScripts('test.js');
importScripts('polymer.js', 'custom-element.js', 'jquery.js');
当 Web Worker 启动时,它遵循以下步骤:
- 它从头到尾执行脚本,包括任何异步任务(比如
XMLHttpRequest
调用)。 - 如果其执行的一部分是注册一个
message
事件处理程序,那么它将进入一个wait
循环来接收消息。它收到的第一条消息将是发布来启动工作进程的消息。工作线程将保持等待模式,直到您手动终止它,或者它自己终止。 - 如果没有注册
message
事件处理程序,工作线程将自动终止。
为了演示一个 Web worker,您需要两个文件:一个创建和运行 Worker 的宿主页面,以及一个供 Worker 执行的独立 JavaScript 脚本。清单 8-7 显示了一个基本的主机页面。
清单 8-7 。创建并使用 Web Worker
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<h1>Web Workers</h1>
<div id="message-box"></div>
<script>
/**
* Handles an error event from web worker.
* @param {WorkerErrorEvent} event The error event object.
*/
function handleWorkerError(event) {
console.warn('Error in web worker: ', event.message);
}
/**
* Handles a message event from a web worker.
* @param {WorkerMessageEvent} event The message event object.
*/
function handleWorkerMessage(event) {
displayMessage('Message received from worker: ' + event.data);
}
/**
* Displays a message in the message box.
* @param {string} message The message to display.
*/
function displayMessage(message) {
// Get a reference to the target element.
var messageBox = document.getElementById('message-box');
// Create a new paragraph and set its content to the message.
var newParagraph = document.createElement('p');
newParagraph.innerHTML = message;
// Append the new paragraph to the target element.
messageBox.appendChild(newParagraph);
}
// Create a new worker.
var myNewWorker = new Worker('example8-8.js');
// Register error and message event handlers on the worker.
myNewWorker.addEventListener('error', handleWorkerError);
myNewWorker.addEventListener('message', handleWorkerMessage);
// Start the worker.
myNewWorker.postMessage('begin');
</script>
</body>
</html>
对于网络工作者来说,清单 8-8 是一个非常基本的独立脚本。
清单 8-8 。一个简单的 Web Worker 脚本
/**
* Handles a message event from the main context.
* @param {WorkerMessageEvent} event The message event.
*/
function handleMessageEvent(event) {
// Do something with the message.
console.log('Worker received message:', event.data);
// Send the message back to the main context.
self.postMessage('Your message was received.');
}
// Register the message event handler.
self.addEventListener('message', handleMessageEvent);
// Dispatch 10 events to the host document.
var counter = 0;
var timer = setInterval(function() {
counter++;
self.postMessage('Message #' + counter);
if (counter == 10) {
// Stop the timer.
clearInterval(timer);
// Throw an error.
throw new Error();
}
}, 1000);
注意本例中的文件需要在常规服务器上提供,代码才能运行。如果您只是简单地从文件系统加载主机文件到浏览器中,浏览器将抛出一个跨原点冲突错误。
表 8-6。网络工作者的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 建议 | http://dev.w3.org/html5/workers/
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/workers.html
|
九、画布参考
本章将为canvas
元素和 2D 绘图上下文 API 提供详细参考。有关这些功能的详细讨论和更多示例,请参见第四章。
canvas
元素
HTML5 canvas
元素通过提供一个空白的“画布”让你在网页上绘制位图。canvas
元素本身是一个块级 DOM 元素。要使用canvas
元素进行绘图,您必须从元素中获取一个绘图上下文引用。该上下文公开了一个用于绘图的扩展 API,您可以在脚本中使用它。
canvas
元素本身的 API 定义是:
Interface HTMLCanvasElement implements HTMLElement {
unsigned long width;
unsigned long height;
renderingContext? getContext(DOMString contextId);
DOMString toDataURL(optional DOMString type);
}
语法
<canvas id="myCanvas" width="100" height="100"></canvas>
<script>
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
</script>
属性是
width
:元素的布局宽度。height
:元素的布局高度。getContext()
:返回请求的渲染上下文。所有的浏览器都支持'2D'
上下文,并且许多支持'webgl'
(在旧的浏览器中,'experimental-webgl'
)上下文。toDataURL()
:返回画布位图的数据 URI 表示。可选的 type 参数用于指定编码数据的格式。有效选项为"image/jpeg"
、"image/gif"
或"image/png"
。如果没有指定类型参数,默认为"image/png"
。编码图像的分辨率为 96dpi。
提示数据 URI 方案是一种将数据直接编码成文档的方式。您可以在数据 URI 中编码任何内容,但它最常用于编码图像。当图像被编码为数据 URI 时,您就可以在使用常规 URL 的地方使用该数据 URI(例如,在图像标签的
src
属性中)。数据 URIs 在 RFC 2397 中的tools.ietf.org/html/rfc2397
处定义。
您需要使用标签本身的
width和
height`属性来指定画布元素的宽度和高度,而不是使用 CSS。如果使用 CSS,绘图上下文的纵横比将会不正确(除非您指定 canvas 元素的默认大小,即 200 像素高,400 像素宽)。
清单 9-1 展示了一个canvas
元素的基本实现。
清单 9-1 。一个canvas
元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200"></canvas>
<script>
// Get a DOM reference to the canvas element.
var myCanvas = document.getElementById('myCanvas');
// Get a reference to the 2d drawing context from the canvas element.
var myContext = myCanvas.getContext('2d');
</script>
</body>
</html>
表 9-1。canvas
元素的标准
|
规格
|
状态
|
统一资源定位器
|
| --- | --- | --- |
| 万维网路联盟(World Wide Web Consortiumˌ简称 W3C) | 候选人推荐 | www.w3.org/TR/2dcontext/
|
| WHATWG | 生活标准 | www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html
|
绘图环境
一旦创建了一个canvas
元素并从中检索到绘图上下文,就可以使用绘图上下文上的 API 开始绘图。2d 绘图上下文是最常用的绘图上下文,其 API 将在本章中介绍。API 提供的命令很简单,但提供了创建复杂图形所需的所有工具。
2d 绘图上下文使用笔的隐喻来进行绘图,这意味着大多数绘制某物的命令采取“从笔的当前位置,绘制该项”或“从笔的当前位置绘制该项”的形式 2d 环境也采用了路径的概念。一个路径是你刚刚绘制的项目的一个不可见的表示,无论它是一条线,一个圆,还是由多个项目组成的复杂的绘图。路径可以是描边的(表示绘制路径时就像用钢笔描边一样)或填充的(表示路径包含的所有区域都被填充)。可以用纯色、渐变或从图像生成的图案来描边和填充路径。路径可以是开放的(起点和终点不同)或封闭的(起点和终点相同)。它们不必是连续的;您可以拥有一条包含几个不相连的“片段”的路径。2d 绘图上下文一次仅支持一个活动路径。
定义路径
2d 绘图上下文提供了一些用于定义路径的简单命令。
beginPath
法
此命令指定您正在定义一个新路径。先前的路径将从绘图上下文中清除。
语法
Context.beginPath();
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.beginPath();
closePath
法
该命令关闭当前路径。如果当前路径的起点和终点不相同(换句话说,如果当前路径还没有闭合),该命令将通过沿两点之间的直线延伸来闭合路径。
语法
Context.closePath();
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.closePath();
moveTo
法
此命令将笔移动到指定的坐标。这提供了一种创建不连续路径的方法。
语法
Context.moveTo(x, y);
表 9-2。moveTo
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| x | 数字 | 新位置的 x 坐标。 |
| y | 数字 | 新位置的 y 坐标。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.moveTo(10, 10);
清单 9-2 展示了在绘图环境中创建和管理路径。
清单 9-2 。管理路径
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn't support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
// Set the stroke style.
myContext.strokeStyle = '#000000';
myContext.lineWidth = 5;
// Create a closed path.
myContext.beginPath();
// Start the path at (30, 10).
myContext.moveTo(30, 10);
// Draw a line from the current pen location at (30, 10) to (50, 50).
myContext.lineTo(50, 50);
// Draw a line from current pen location at (50, 50) to (10, 50);
myContext.lineTo(10, 50);
// The pen is now currently at (10, 50). Closing the path will draw a straight
// line from (10, 50) back to the beginning point of the path at (30, 10).
myContext.closePath();
// We can't see the path without stroking it.
myContext.stroke();
// Create a new path.
myContext.beginPath();
// Start the path at (60, 10).
myContext.moveTo(60, 10);
// Draw a line from the current pen location at (60, 10) to (100, 10).
myContext.lineTo(100, 10);
// Move the pen from its current location at (100, 10) to (100, 50).
myContext.moveTo(100, 50);
// Draw a line from the current pen location at (100, 50) to (60, 50).
myContext.lineTo(60, 50);
// Give the new shape a different color.
myContext.strokeStyle = '#ff0000';
myContext.stroke();
// Creating a new path will clear the current path from memory without closing
// the previous one. We can demonstrate this by changing the stroke style and
// calling stroke again. The previous shape should remain red.
myContext.beginPath();
myContext.strokeStyle = '#00ff00';
myContext.stroke();
</script>
</body>
</html>
基本绘图命令
2d 绘图上下文提供了一组绘制曲线的方法:直线、圆弧等。
lineTo
法
此命令绘制从当前笔位置到指定坐标的路径。
语法
Context.lineTo(x, y);
表 9-3。lineTo
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| x | 数字 | 所需端点的 x 坐标。 |
| y | 数字 | 所需端点的 y 坐标。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.lineTo(10, 10);
arc
法
沿着以坐标(x, y)
为圆心,半径radius
的圆,从startAngle
到endAngle
画一个圆弧。
语法
Context.arc(x, y, radius, startAngle, endAngle, opt_isAnticlockwise);
表 9-4。arc
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| x | 数字 | 所需中心的 x 坐标。 |
| y | 数字 | 所需中心的 y 坐标。 |
| 半径 | 数字 | 弧的半径,以像素为单位。 |
| 半径 | 数字 | 以弧度表示的起始角度。 |
| 结束角度 | 数字 | 以弧度表示的结束角度。 |
| opt _ isatical lock wise-选项 isanticlockwise | 布尔代数学体系的 | 如果true
,圆弧将逆时针画出。可选;如果未提供,默认为false
。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
// Draw an arc starting from 0 to 3 radians.
myContext.arc(50, 50, 10, 0, 3);
quadraticCurveTo
法
该命令绘制一条二次曲线,从当前笔位置开始,到坐标(x, y)
结束,控制点在(cp1x, cp1y)
。
语法
Context.quadraticCurveTo(cplx, cply, x, y);
表 9-5。quadraticCurveTo
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| cplex(葡语共同体) | 数字 | 控制点的 x 坐标。 |
| cply | 数字 | 控制点的 y 坐标。 |
| x | 数字 | 曲线终点的 x 坐标。 |
| y | 数字 | 曲线终点的 y 坐标。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.quadraticCurveTo(50, 50, 10, 10);
bezierCurveTo
法
绘制一条贝塞尔曲线,从当前笔位置开始,到坐标(x, y)
结束,控制点 1 由(cp1x, cp1y)
指定,控制点 2 由(cp2x, cp2y)
指定。
语法
Context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
表 9-6。bezierCurveTo
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| 缔约方会议第一届会议 | 数字 | 控制点 1 的 x 坐标。 |
| cp 1 和 | 数字 | 控制点 1 的 y 坐标。 |
| cp2x(缔约方会议第二届会议) | 数字 | 控制点 2 的 x 坐标。 |
| cp 2 和 | 数字 | 控制点 2 的 y 坐标。 |
| x | 数字 | 终点的 x 坐标。 |
| y | 数字 | 终点的 y 坐标。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.bezierCurveTo(50, 50, 10, 10);
rect
法
该命令从坐标(x, y)
开始绘制一个矩形,指定了width
和height
。
语法
Context.rect(x, y, width, height);
表 9-7。rect
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| x | 数字 | 左上角的 x 坐标。 |
| y | 数字 | 左上角的 y 坐标。 |
| 宽度 | 数字 | 矩形的宽度,以像素为单位。 |
| 高度 | 数字 | 矩形的高度,以像素为单位。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.rect(50, 50, 10, 10);
描边和填充路径
如前所述,基本的绘图命令在画布上创建不可见的路径。要使路径可见,必须使用描边或填充命令。2d 绘图上下文还提供了一组用于定义笔画和填充样式的属性。
strokeStyle
属性
该属性指定了在对stroke
方法的后续调用中应该应用的样式。该属性可以接受任何有效的 CSS 颜色字符串(例如,'red'
、'#ff0000'
、'rgb(255, 0, 0)'
等)。),一个Gradient
对象,或者一个Pattern
对象。(参见下文,了解如何定义Gradient
和Pattern
对象。)
语法
Context.strokeStyle = StrokeStyleValue;
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.strokeStyle = '#FF0000';
stroke
法
此命令用当前设置的描边样式描边当前路径。
语法
Context.stroke();
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.strokeStyle = '#FF0000';
myContext.strokePath();
fillStyle
属性
该属性指定了在对fill
方法的后续调用中应该应用的样式。该属性可以接受任何有效的 CSS 颜色字符串(例如,'red'
、'#ff0000'
、'rgb(255, 0, 0)'
等)。),一个Gradient
对象,或者一个Pattern
对象。(参见下文,了解如何定义Gradient
和Pattern
对象。)
语法
Context.fillStyle = FillStyleValue;
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.fillStyle = '#FF0000';
fill
法
该命令用当前设置的填充样式填充当前路径。
语法
Context.fill();
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.fillStyle = '#FF0000';
myContext.fillPath();
lineWidth
属性
此属性以应用于路径的描边为单位定义粗细。如果没有设置,该属性默认为 1。
语法
Context.lineWidth = Number;
表 9-8。lineCap
属性的值
|
价值
|
说明
|
| --- | --- |
| 屁股 | 线端被切成方形,并精确地终止于指定的端点。这是默认值。 |
| 圆形物 | 线条端点是圆形的,并稍微超出指定的端点。 |
| 平方 | 通过在线条末端添加一个宽度等于线条宽度、高度为线条宽度一半的方框,线条末端被方形化。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.lineWidth = 2;
lineCap
属性
此属性定义如何封闭线条。有效值为'butt'
、'round'
或'square'
。
语法
Context.lineCap = LineCapValue;
表 9-9。lineJoin
属性的值
|
价值
|
说明
|
| --- | --- |
| 斜角规 | 接缝是斜的。 |
| 斜接 | 接缝是斜接的。 |
| 圆形物 | 关节是圆形的。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.lineCap = 'round';
lineJoin
属性
此属性定义连接线如何连接在一起。有效值为'bevel'
、'miter'
或'round'
。
语法
Context.lineJoin = LineCapValue;
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.lineJoin = 'round';
清单 9-3 提供了一个使用基本绘图命令和填充和描边命令来创建函数的例子,这些函数可以很容易地画出圆。
清单 9-3 。圆圈耶
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn't support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
/**
* Draws a circle of the specified dimensions at the target coordinates and
* fills it with the current fill style.
* @param {number} x The x coordinate of the center of the circle.
* @param {number} y The y coordinate of the center of the circle.
* @param {number} radius The radius of the circle.
*/
function fillCircle(x, y, radius) {
myContext.beginPath();
myContext.arc(x, y, radius, 0, 6.3);
myContext.fill();
myContext.closePath();
}
/**
* Draws a circle of the specified dimensions at the target coordinates and
* strokes it with the current stroke style.
* @param {number} x The x coordinate of the center of the circle.
* @param {number} y The y coordinate of the center of the circle.
* @param {number} radius The radius of the circle.
*/
function strokeCircle(x, y, radius) {
myContext.beginPath();
myContext.arc(x, y, radius, 0, 6.3);
myContext.stroke();
myContext.closePath();
}
// Set a fill style and draw a filled circle.
myContext.fillStyle = 'rgb(0, 0, 0)';
fillCircle(65, 65, 50);
// Set a stroke style and draw a stroked circle.
myContext.strokeStyle = 'rgb(0, 0, 0)';
myContext.lineWidth = 2;
strokeCircle(135, 135, 50);
</script>
</body>
</html>
绘制矩形
除了基本路径之外,2d 绘图上下文还有一些用于绘制简单矩形的功能。您可以用基本的路径命令来绘制这些,但是这些方便的方法使它更容易。
fillRect
法
该命令在指定坐标处绘制一个具有指定宽度和高度的矩形,并用当前填充样式填充。
语法
Context. fillRect(x, y, width, height);
表 9-10。fillRect
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| x | 数字 | 左上角的 x 坐标。 |
| y | 数字 | 左上角的 y 坐标。 |
| 宽度 | 数字 | 矩形的宽度,以像素为单位。 |
| 高度 | 数字 | 矩形的高度,以像素为单位。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.fillStyle = '#000000';
myContext.fillRect(50, 50, 10, 10);
strokeRect
法
此命令在指定的坐标处绘制一个矩形,该矩形具有指定的宽度和高度,并使用当前的描边样式描边。
语法
Context. strokeRect(x, y, width, height);
表 9-11。strokeRect
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| x | 数字 | 左上角的 x 坐标。 |
| y | 数字 | 左上角的 y 坐标。 |
| 宽度 | 数字 | 矩形的宽度,以像素为单位。 |
| 高度 | 数字 | 矩形的高度,以像素为单位。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.strokeStyle = '#000000';
myContext.strokeRect(50, 50, 10, 10);
clearRect
法
该命令清除任何其他图形的指定矩形区域。
语法
Context. clearRect(x, y, width, height);
表 9-12。clearRect
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| x | 数字 | 左上角的 x 坐标。 |
| y | 数字 | 左上角的 y 坐标。 |
| 宽度 | 数字 | 矩形的宽度,以像素为单位。 |
| 高度 | 数字 | 矩形的高度,以像素为单位。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.clearRect(50, 50, 10, 10);
清单 9-4 演示了矩形的绘制。
清单 9-4 。随机矩形
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn't support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
// Create a loop that will draw a random rectangle on the canvas.
var cycles = 10,
i = 0;
for (i = 0; i < cycles; i++) {
var randX = getRandomIntegerBetween(0, 150);
var randY = getRandomIntegerBetween(0, 150);
var randWidth = getRandomIntegerBetween(10, 100);
var randHeight = getRandomIntegerBetween(10, 100);
myContext.beginPath();
myContext.strokeRect(randX, randY, randWidth, randHeight);
randStroke();
myContext.closePath();
}
/**
* Returns a random integer between the specified minimum and maximum values.
* @param {number} min The lower boundary for the random number.
* @param {number} max The upper boundary for the random number.
* @return {number}
*/
function getRandomIntegerBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Returns a random color formatted as an rgb string.
* @return {string}
*/
function getRandRGB() {
var randRed = getRandomIntegerBetween(0, 255);
var randGreen = getRandomIntegerBetween(0, 255);
var randBlue = getRandomIntegerBetween(0, 255);
return 'rgb(' + randRed + ', ' + randGreen + ', ' + randBlue + ')';
}
/**
* Performs a randomized stroke on the current path.
*/
function randStroke() {
myContext.lineWidth = getRandomIntegerBetween(1, 10);
myContext.strokeStyle = getRandRGB();
myContext.stroke();
}
</script>
</body>
</html>
渐变和图案
Canvas has great support for gradients and patterns. Both patterns and gradients are represented by objects returned from construction functions. These objects can then be used as the values for fill or stroke styles.
createLinearGradient
法
该方法创建一个从坐标(startX, startY)
开始到坐标(endX, endY)
结束的线性渐变。返回一个可用作描边或填充样式的Gradient
对象。
语法
Context.createLinearGradient(startX, startY, endX, endY);
表 9-13。createLinearGradient
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| 启动 X | 数字 | 渐变起点的 x 坐标。 |
| 起始 Y | 数字 | 渐变起点的 y 坐标。 |
| endX(结束 x) | 数字 | 渐变终点的 x 坐标。 |
| 周国贤 | 数字 | 渐变终点的 y 坐标。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
var myGradient = myContext.createLinearGradient(50, 50, 10, 10);
createRadialGradient
法
该方法创建一个由两个圆组成的径向渐变,第一个以(x, y)
为中心,半径为r
,另一个以(x1, y1)
为中心,半径为r1
。返回一个可用作描边或填充样式的Gradient
对象。
语法
Context.createRadialGradient(x, y, r, x1, y1, r1);
表 9-14。createRadialGradient
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| x | 数字 | 第一个圆的圆心的 x 坐标。 |
| y | 数字 | 第一个圆的圆心的 y 坐标。 |
| r | 数字 | 第一个圆的半径。 |
| x1 | 数字 | 第二个圆的圆心的 x 坐标。 |
| y1 | 数字 | 第二个圆的圆心的 y 坐标。 |
| r1 | 数字 | 第二个圆的半径。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
var myGradient = myContext.createRadialGradient(50, 50, 50, 50, 50, 100);
addColorStop
法
该命令为Gradient
添加一个色标。position 参数必须介于 0 和 1 之间,并定义色标渐变内的相对位置。颜色可以是任何有效的 CSS 颜色值。您可以为特定的Gradient
添加任意数量的色标。
语法
Gradient.addColorStop(position, color);
表 9-15。lineTo
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| 位置 | 数字 | 色标的位置。 |
| 颜色 | CssColorValue | 色标的颜色。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
var myGradient = myContext.createRadialGradient(50, 50, 50, 50, 50, 100);
myGradient.addColorStop(0, '#FF0000');
myGradient.addColorStop(1, '#000000');
createPattern
法
该命令创建一个可用作填充或描边样式的Pattern
对象。Image
参数必须是任何有效的Image
(详见下一节“图像”)。repeat
参数指定图案图像如何重复,必须是'repeat'
、'repeat-x'
、'repeat-y'
或'no-repeat'
之一。
语法
Gradient.createPattern(position, color);
表 9-16。createPattern
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| 图像 | 图像 | 用于创建图案的图像。 |
| 重复 | 线 | 如何重复图像来创建图案。 |
表 9-17。重复参数的有效值
|
价值
|
说明
|
| --- | --- |
| 重复 | 水平和垂直平铺图像。 |
| 重复-x | 仅水平重复图像。 |
| 重复-y | 仅垂直重复图像。 |
| 不重复 | 完全不要重复图像。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
var myImage = document.getElementById('myImage');
var myPattern = myContext.createPattern(myImage, 'repeat');
清单 9-5 演示了使用径向渐变填充随机圆。
清单 9-5 。生成并使用径向渐变填充形状
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn't support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
/**
* Draws a circle of the specified dimensions at the target coordinates and
* fills it with the current fill style.
* @param {number} x The x coordinate of the center of the circle.
* @param {number} y The y coordinate of the center of the circle.
* @param {number} radius The radius of the circle.
*/
function fillCircle(x, y, radius) {
myContext.beginPath();
myContext.arc(x, y, radius, 0, 6.3);
myContext.fill();
myContext.closePath();
}
/**
* Returns a random integer between the specified minimum and maximum values.
* @param {number} min The lower boundary for the random number.
* @param {number} max The upper boundary for the random number.
* @return {number}
*/
function getRandomIntegerBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Returns a random color formatted as an rgb string.
* @return {string}
*/
function getRandRGB() {
var randRed = getRandomIntegerBetween(0, 255);
var randGreen = getRandomIntegerBetween(0, 255);
var randBlue = getRandomIntegerBetween(0, 255);
return 'rgb(' + randRed + ', ' + randGreen + ', ' + randBlue + ')';
}
// Create a loop that will draw a random circle on the canvas.
var cycles = 10,
i = 0;
for (i = 0; i < cycles; i++) {
// Get a random set of coordinates for the new circle.
var randX = getRandomIntegerBetween(50, 150);
var randY = getRandomIntegerBetween(50, 150);
// Get a random radius.
var randRadius = getRandomIntegerBetween(10, 50);
// Create a gradient object based on the coordinates we just generated.
var randGrad = myContext.createRadialGradient(randX, randY, 0, randX, randY,
randRadius);
// Create some random colors and add them as color stops to the gradient.
var randColor1 = getRandRGB();
var randColor2 = getRandRGB();
randGrad.addColorStop(0, randColor1);
randGrad.addColorStop(1, randColor2);
// Set the fill style and draw the circle.
myContext.fillStyle = randGrad;
fillCircle(randX, randY, randRadius);
}
</script>
</body>
</html>
形象
2d 绘图上下文还可以加载和操作图像。有效的图像源是一个img
元素、video
元素或另一个canvas
元素。图像源不必作为 DOM 的一部分呈现,因此您可以根据需要动态创建标签和加载内容,而不必将它们附加到 DOM。一旦图像被加载到canvas
中,您也可以使用绘图命令在其上绘图。
Canvas
有一个绘制图像的方法drawImage
,但是它可以接受许多不同的参数,因此具有多种功能。
画一幅图像
当您为 drawImage 提供一个图像源、一个 x 坐标和一个 y 坐标时,它将在坐标处绘制图像。
语法
Context.drawImage(image, x, y);
表 9-18。简单绘制图像时drawImage
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| 图像 | CanvasImageSource | 有效的画布图像源。 |
| x | 数字 | 图像的 x 坐标。 |
| y | 数字 | 图像的 y 坐标。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
var myImage = document.getElementById('myImage');
myContext.drawImage(myImage, 10, 10);
缩放图像
当您给drawImage
提供一个图像源、一个 x 坐标、一个 y 坐标、一个宽度和一个高度时,它将在这些坐标处绘制图像,并将图像缩放到指定的width
和height
。
语法
Context.drawImage(image, x, y, width, height);
表 9-19。缩放图像时drawImage
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| 图像 | CanvasImageSource | 有效的画布图像源。 |
| x | 数字 | 图像的 x 坐标。 |
| y | 数字 | 图像的 y 坐标。 |
| 宽度 | 数字 | 图像的所需宽度,以像素为单位。 |
| 高度 | 数字 | 图像的所需高度,以像素为单位。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
var myImage = document.getElementById('myImage');
myContext.drawImage(myImage, 10, 10, 50, 50);
绘制图像的切片
您也可以选择图像上的特定区域(“切片”),并在画布上绘制该区域。
语法
Context.drawImage(image, sliceX, sliceY, sliceWidth, sliceHeight, x, y);
表 9-20。绘制图像切片时,drawImage
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| 图像 | CanvasImageSource | 有效的画布图像源。 |
| sliceX | 数字 | 切片左上角图像上的 x 坐标。 |
| 微笑的 | 数字 | 切片左上角图像的 y 坐标。 |
| 切片宽度 | 数字 | 所需的切片宽度,以像素为单位。 |
| 切片高度 | 数字 | 切片的所需高度,以像素为单位。 |
| x | 数字 | 绘制图像切片的 x 坐标。 |
| y | 数字 | 绘制图像切片的 y 坐标。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
var myImage = document.getElementById('myImage');
myContext.drawImage(myImage, 10, 10, 50, 50, 0, 0);
清单 9-6 展示了如何将一幅图像加载到我们的基本画布模板中。
清单 9-6 。将图像加载到canvas
元素中
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn't support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
// Create a new image element and fill it with a kitten.
var myImage = new Image();
myImage.src = 'http://lorempixel.com/g/200/200/cats';
// We can't do anything until the image has successfully loaded.
myImage.onload = function() {
myContext.drawImage(myImage, 0, 0);
};
</script>
</body>
</html>
这里,您只是将一个随机的占位符图像加载到画布中的位置(0, 0)
。清单 9-7 展示了一个更复杂的图像操作。
清单 9-7 。使用画布操纵图像
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn't support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
// Create a new image element and fill it with a kitten.
var myImage = new Image();
myImage.src = 'http://lorempixel.com/g/300/300/cats';
// We can't do anything until the image has successfully loaded.
myImage.onload = function() {
myContext.drawImage(myImage, 25, 25, 150, 150, 0, 0, 150, 50);
};
</script>
</body>
</html>
这里你加载了一个 300 × 300 的占位符图像,但是从(25, 25)
开始只截取了 75 × 75 的部分。然后你把这个切片渲染到canvas
中,缩放到 150 × 50。
文本
2d 绘图上下文也可用于渲染文本。
fillText
法
此方法使用当前填充样式从指定坐标开始填充画布上的指定文本。如果指定了可选的maxWidth
参数,并且呈现的文本将超过该宽度,浏览器将尝试以这种方式呈现文本,使其适合指定的宽度(如果可用,使用压缩字体,使用较小的字体大小,等等)。).
语法
Context.fillText(textString, x, y, opt_maxWidth);
表 9-21。缩放图像时fillText
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| 文字字串 | 线 | 文本字符串。 |
| x | 数字 | 应该呈现文本的 x 坐标。 |
| y | 数字 | 应该呈现文本的 y 坐标。 |
| opt_maxWidth | 数字 | 最大宽度,以像素为单位。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.fillText('Hello world!', 10, 10, 200);
measureText
法
此方法测量使用当前样式呈现指定文本时将产生的宽度。返回一个具有包含该值的width
属性的TextMetrics
对象。这为您提供了一种测试文本是否适合给定区域的方法,而无需实际呈现它。
语法
Context.measureText(textString);
表 9-22。缩放图像时measureText
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| 文字字串 | 线 | 文本字符串。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
var textMetric = myContext.measureText('Hello world!');
var calculatedWidth = textMetric.width;
strokeText
法
此方法使用当前笔画样式从指定坐标开始对画布上的指定文本进行笔画。如果指定了可选的maxWidth
参数,并且呈现的文本将超过该宽度,浏览器将尝试以这种方式呈现文本,使其适合指定的宽度(如果可用,使用压缩字体,使用较小的字体大小,等等)。).
语法
Context.strokeText(textString, x, y, opt_maxWidth);
表 9-23。缩放图像时strokeText
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| 文字字串 | 线 | 文本字符串。 |
| x | 数字 | 应该呈现文本的 x 坐标。 |
| y | 数字 | 应该呈现文本的 y 坐标。 |
| opt_maxWidth | 数字 | 最大宽度,以像素为单位。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.strokeText('Hello world!', 10, 10, 200);
font
属性
此属性定义文本呈现的字体。任何有效的 CSS 字体字符串都是允许的,但是请注意,用户必须在他们的系统上安装指定的字体。
语法
Context.font = CssFontString;
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.font = 'arial, helvetica, sans-serif';
textAlign
属性
此属性定义文本呈现时的对齐方式。有效值为'left'
、'right'
、'center'
、'start'
和'end'
。
语法
Context.textAlign = AlignValue;
表 9-24。textAlign
属性的值
|
价值
|
说明
|
| --- | --- |
| 左边的 | 将文本左对齐。 |
| 正确 | 将文本右对齐。 |
| 中心 | 将文本居中。 |
| 开始 | 在当前区域设置的起始端对齐文本(即,对于从左到右的语言,靠左对齐;对于从右到左的语言,靠右对齐)。这是默认值。 |
| 目标 | 将文本在当前区域设置的末端对齐。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.textAlign = 'center';
textBaseline
属性
该属性定义文本呈现时的基线。有效值为'alphabetic'
、'bottom'
、'hanging'
、'ideographic'
、'middle'
和'top'
。
语法
Context.textAlign = AlignValue;
表 9-25。textBaseline
属性的值
|
价值
|
说明
|
| --- | --- |
| 字母的 | 对文本使用正常的字母基线。这是默认值。 |
| 底部 | 基线是 em 正方形的底部。 |
| 绞刑 | 文本使用悬挂基线。 |
| 表意的 | 使用字符体的底部(假设它们突出于字母基线之下)。 |
| 中间 | 文本基线位于 em 正方形的中间。 |
| 顶端 | 文本基线是 em 正方形的顶部。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.textAlign = 'center';
清单 9-8 展示了如何使用文本命令渲染文本。
清单 9-8 。在画布中渲染文本
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn't support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.font = '35px sans-serif';
myContext.strokeStyle = '#000';
myContext.lineWidth = 2;
myContext.textAlign = 'center';
myContext.strokeText('Hello World', 100, 100);
</script>
</body>
</html>
阴影
canvas
元素也可以根据其上绘制的元素来投射阴影。这通常用于文本,但也适用于形状和路径。如果你已经熟悉 CSS 阴影,画布阴影的参数将会非常熟悉。
shadowBlur
房产
该属性定义模糊效果的大小。有效值为 0(无模糊,这是默认值)或任何正整数。
语法
Context.shadowBlur = ShadowBlurValue;
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.shadowBlur = 5;
shadowColor
房产
这个属性定义了阴影的颜色。任何 CSS 颜色字符串都是有效值。默认为'rgba(0, 0, 0, 0)'
。
语法
Context.shadowColor = CssColorValue;
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.shadowColor = '#00FF00';
shadowOffsetX
房产
此属性定义阴影的 x 偏移。有效值可以是任何正整数或负整数,也可以是 0(默认值)。
语法
Context.shadowOffsetX = OffsetValue;
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.shadowOffsetX = 5;
shadowOffsetY
房产
此属性定义阴影的 y 偏移。有效值可以是任何正整数或负整数,也可以是 0(默认值)。
语法
Context.shadowOffsetY = OffsetValue;
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.shadowOffsetY = 5;
清单 9-9 展示了在文本上创建一个阴影。
清单 9-9 。在画布中投射阴影
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn't support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
// Define a shadow.
myContext.shadowBlur = 2;
myContext.shadowColor = 'rgba(0, 100, 0, 0.5)';
myContext.shadowOffsetX = 5;
myContext.shadowOffsetY = 5;
myContext.font = '35px sans-serif';
myContext.strokeStyle = '#000';
myContext.lineWidth = 2;
myContext.textAlign = 'center';
myContext.strokeText('Hello World', 100, 100);
</script>
</body>
</html>
作文
每当您在画布上绘制新元素时,合成器都会查看画布上已经存在的内容。这个当前内容被称为目的地。新内容被称为源。然后合成器根据当前活动的合成器参照目的地绘制源。
globalCompositeOperation
属性
此属性指定哪个合成器当前处于活动状态。
语法
Context.globalCompositeOperation = CompositorValue;
表 9-26。globalCompositeOperation
属性的值
|
价值
|
说明
|
| --- | --- |
| 源-结束 | 在目标内容上绘制源内容。这是默认的合成器。 |
| 源-顶部 | 源内容仅在与目标内容重叠的地方绘制。 |
| 源入 | 仅在源内容和目标内容重叠的地方绘制源内容。其他一切都是透明的。 |
| 源出 | 源内容仅在不与目标内容重叠的地方绘制。其他一切都是透明的。 |
| 目的地完毕 | 源内容绘制在目标内容的下方。 |
| 目的地-顶部 | 源内容只保存在与目标内容重叠的地方。目标内容绘制在源内容的下方。其他一切都是透明的。 |
| 目的地 | 源内容只保存在与目标内容重叠的地方。其他一切都是透明的。 |
| 目的地-出 | 源内容只保存在不与目标内容重叠的地方。其他一切都是透明的。 |
| 复制 | 仅绘制目标内容。其他一切都是透明的。 |
| 驳船 | 当目标内容和源内容重叠时,颜色是通过将两个内容的值相加来确定的。 |
| 异或运算 | 目标内容正常呈现,除非它与源内容重叠,在这种情况下,两者都呈现为透明。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.globalCompositeOperation = 'source-atop';
第四章中的清单 4-16 提供了所有这些属性的动态示例。
剪辑
您可以将画布的绘制区域限制为您定义的任何闭合路径。这被称为削波。
clip
法
此命令允许您基于当前路径创建一个剪辑区域。只有包含在剪辑区域内的内容才会显示。要重置剪辑区域,您可以执行以下三项操作之一:
- 您可以定义一个包含整个画布的路径,然后剪辑到该路径。
- 可以用不同的剪辑区域恢复到以前的绘图状态。这是最常见的解决方案。您可以在剪裁前保存绘图状态,然后在完成后恢复它。有关如何保存状态的详细信息,请参见后面的“保存和恢复画布状态”一节。
- 您可以通过调整大小来重置整个画布。
语法
Context.clip();
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.clip();
清单 9-10 演示了创建一个裁剪区域,并用它来裁剪一个正方形的角。
清单 9-10 。创建剪辑区域
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn't support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
// Create a circular clipping area.
myContext.beginPath();
myContext.arc(100, 100, 50, 0, 7);
myContext.clip();
// Draw a square in the canvas and fill it. Only the portion within the clipping
// area will be visible, so the corners will be cut off.
myContext.beginPath();
myContext.rect(60, 60, 80, 80);
myContext.fillStyle = 'black';
myContext.fill();
</script>
</body>
</html>
转换
2d 绘图上下文支持各种类型的转换。一旦设置了变换,它将应用于从该点开始渲染的所有内容。
translate
法
该方法将画布的原点从当前位置移动到坐标指定的新位置。
语法
Context.translate(translateX, translateY);
表 9-27。缩放图像时translate
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| 平移(translate) | 数字 | 原点的新 x 坐标。 |
| 平移(translate) | 数字 | 原点的新 y 坐标。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.translate(10, 50);
rotate
法
此方法将画布围绕原点顺时针旋转指定的角度(以弧度为单位)。
语法
Context.rotate(angle);
表 9-28。缩放图像时translate
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| 角 | 数字 | 旋转角度,以弧度为单位。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.rotate(2);
scale
法
该方法通过水平方向的scaleX
和垂直方向的scaleY
来缩放画布单元。
语法
Context.scale(scaleX, scaleY);
表 9-29。缩放图像时scale
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| 横向缩放 | 数字 | x 轴的缩放量。 |
| 数值 | 数字 | 缩放 y 轴的数量。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
myContext.translate(10, 50);
transform
法
此方法允许您指定通用转换矩阵:
。
rotate
、translate
和scale
简写方法都映射到转换矩阵,因此调用transform
方法。例如,Context.translate(translateX, translateY)
映射到Context.transform(1, 0, 0, 1, translateX, translateY)
,而Context.scale(scaleX, scaleY)
映射到Context.transform(scaleX, 0, 0, scaleY, 0, 0)
。
语法
Context.transform(scaleX, skewX, skewY, scaleY, translateX, translateY);
表 9-30。bezierCurveTo
方法的参数
|
参数
|
类型
|
说明
|
| --- | --- | --- |
| 横向缩放 | 数字 | x 轴的缩放量。 |
| skewX | 数字 | x 轴的倾斜量。 |
| 歪斜 | 数字 | y 轴的倾斜量。 |
| 数值 | 数字 | 缩放 y 轴的数量。 |
| 平移(translate) | 数字 | 新原点的 x 坐标。 |
| 平移(translate) | 数字 | 新原点的 y 坐标。 |
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
// Reset all transformations.
myContext.transform(0, 0, 0, 0, 0, 0);
清单 9-11 演示了如何使用scale
和translate
转换。
清单 9-11 。使用scale
和translate
变换
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn't support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
/**
* Draws a 100x100 square at (0, 0) in the specified color. Indicates the origin
* corner with a small black square.
* @param {string} color A valid CSS color string.
*/
function drawSquare(color) {
myContext.fillStyle = color;
myContext.beginPath();
myContext.rect(0, 0, 100, 100);
myContext.fill();
myContext.fillStyle = '#000';
myContext.beginPath();
myContext.rect(0, 0, 5, 5);
myContext.fill();
}
// Draw a square, fill it with red.
drawSquare('rgba(255, 0, 0, 0.5)');
// Translate the canvas.
myContext.translate(20, 40);
// Scale the canvas.
myContext.scale(1, 1.5);
// Draw the same square again, fill it with blue.
drawSquare('rgba(0, 0, 255, 0.5)');
// Translate the canvas again.
myContext.translate(50, -20);
// Scale the canvas again.
myContext.scale(1.5, 1);
// Draw the same square again, fill it with green.
drawSquare('rgba(0, 255, 0, 0.5)');
</script>
</body>
</html>
保存和恢复画布状态
2d 绘图环境包括一个基本状态管理系统。给定状态由上下文中的以下属性组成:
globalAlpha
的当前值- 电流
strokeStyle
和fillStyle
lineCap, lineJoin, lineWidth
和miterLimit
中的当前线路设置shadowBlur, shadowColor, shadowOffsetX
和shadowOffsetY
中的当前阴影设置- 在
globalCompositeOperation
中设置的当前合成操作 - 当前剪辑路径
- 已应用于绘图上下文的任何转换
状态保存在后进先出堆栈中,因此您保存的最后一个状态将是第一个可供检索的状态。没有办法在堆栈中跳来跳去。
save
法
该命令将当前上下文保存到堆栈中。
语法
Context.save();
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
// Saves an initial "blank" canvas state before anything has been drawn or set.
myContext.save();
restore
法
该命令从堆栈中删除最近存储的状态,并将其恢复到上下文中。
语法
Context.restore();
例如
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
// Saves an initial "blank" canvas state before anything has been drawn or set.
myContext.restore();
清单 9-12 展示了保存和恢复状态。
清单 9-12 。保存和恢复工程图上下文状态
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="210">Did You Know: Every time
you use a browser that doesn't support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById('myCanvas');
var myContext = myCanvas.getContext('2d');
// Create an array of colors to load into the stack.
var allTheColors = ['#ff0000', '#ff8800', '#ffff00', '#00ff00', '#0000ff',
'#4b0082', '#8f00ff'];
// Load the colors and stroke style into the stack.
for (var i = 0; i < allTheColors.length; i++) {
myContext.strokeStyle = allTheColors[i];
myContext.lineWidth = 30;
myContext.save();
}
// Restore colors from the stack and draw.
for (var i = 0; i < 8; i++) {
myContext.restore();
myContext.beginPath();
myContext.moveTo(0, ((30 * i) + 15));
myContext.lineTo(200, ((30 * i) + 15));
myContext.stroke();
}
</script>
</body>
</html>
十、附录 A:JavaScript 技巧和技术
JavaScript 是事实上的 Web 编程语言,也是您用来与本书中涉及的 HTML5 特性进行交互的主要工具。在这一章中,我将介绍一些为你的应用组织 JavaScript 的技巧,以及一些可以用来简化你的脚本的更强大的技术。这简短的一章并不意味着是完整的 JavaScript 参考——为此,你可以参考其他出版社的书籍,比如 JavaScript 程序员参考。
代码格式
总的来说,我避免关于括号和缩进等代码格式样式的圣战。通常个别的选择并不重要,因为更重要的是整个代码的一致性。您应该始终使用相同的括号样式、注释样式、缩进选择等等,因为这将有助于保持代码的可读性。即使你是唯一一个会看你的代码的人,它仍然很重要。
然而,对于 JavaScript,这些选择很重要。例如,在 JavaScript 中,括号的位置在某些情况下很重要。考虑以下代码片段:
function test1tbs() {
return {
objectLiteral: ’value’,
isExpected: true
};
}
这个例子遵循所谓的一个真正的大括号风格(有时缩写为 1TBS),其中函数定义的左括号(和对象文字)和它的声明在同一行。这是我在整本书的例子中使用的风格。然而,同样的例子也可以用奥尔曼式的括号来表示:
function testAllman()
{
return
{
objectLiteral: ’value’,
isExpected: false
};
}
这两个函数会有非常不同的结果。test1tbs
函数将返回内联定义为 return 语句一部分的对象文字,而包含testAllman
函数的代码甚至不会运行(JavaScript 引擎将抛出一个错误)。在其他语言中,这两个函数是相同的。
此外,管理 JavaScript 行为的 ECMAScript 标准还定义了如何解释丢失的分号的规则。这被称为自动分号插入(ASI ) ,是该语言的一个特性,但它偶尔会导致令人惊讶的结果。这就是为什么 JavaScript 代码中事实上的括号风格是 1TBS 风格,而不是更广泛的奥尔曼风格。
JavaScript 奖励冗长
当您阅读了本书中的示例后,您可能已经注意到代码风格相当冗长。有大量的注释,变量名和函数名往往很长并且是描述性的,等等。这部分是因为代码样本被设计为易于阅读和理解,但总体来说,这种冗长程度并不比我每天编写的代码高多少。
JavaScript 有几个方便的特性,比如 ASI、类型强制(当您比较两个不同类型的变量时会用到)等等。有时这些特性会导致令人惊讶的结果。详细代码通过提醒您变量和函数应该做什么,以及提供逻辑流和预期行为的文档,有助于避免这些意外。这也使得调试更容易,如果你和其他人合作,这将有助于他们更快地学习你的代码。
注释〔??〕
此外,在本书的例子中,我一直使用特定的注释格式来注释代码。如果您熟悉 JSDoc 或 JavaDoc,这些注释看起来会很熟悉,因为格式是从 JSDoc 派生的。如果您不熟悉 JSDoc,它是 JavaScript 代码中注释的标准,不仅提供了解释代码的好方法,还允许您使用解析工具从注释中生成实际的文档。在示例中,我没有使用 JSDoc 的全部功能——我只是专注于提供类型注释。如果您熟悉为 Closure JavaScript 编译器注释代码,这些注释将会非常熟悉。
提示要了解更多关于为闭包编译器注释代码以及在项目中使用闭包编译器的信息,请参见
https://developers.google.com/closure/compiler/docs/js-for-compiler
。你可以在www.usejsdoc.org/
了解更多关于 JSDoc 和自动文档生成器的知识。
对于每个函数,注释指定了以下内容:
- 对该函数应该做什么的描述。
- 函数的每个参数的预期数据类型(如果有)。
- 返回值的数据类型(如果有)。
- 函数是否意味着是私有的(只适用于类的成员)。
例如,考虑第四章中的函数:
/**
* Returns a random integer between the specified minimum and maximum values.
* @param {number} min The lower boundary for the random number.
* @param {number} max The upper boundary for the random number.
* @return {number}
*/
function getRandomIntegerBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
您可能想知道在动态类型语言(如 JavaScript)中定义参数的预期类型有什么价值。显然,您可以传入任何想要的值,JavaScript 引擎会尽可能地强制这些值,这可能会导致函数返回意外的结果。通过在函数定义中定义预期的数据类型,您不仅可以指出函数需要什么以避免意外的结果,还可以使您在忘记细节后更容易使用该函数。此外,如果你和一个团队合作,你会让他们更容易使用这个功能。在这本书的特殊情况下,我的意图是类型注释将有助于使例子更容易理解。
JavaScript 不直接支持公共或私有属性或方法的概念。在属性或方法上指定@private
标签有助于以更传统的方式定义类结构,并且可以使 JavaScript 代码更容易被习惯于具有更严格封装特性的语言的人接受。我还发现记住我打算将哪些接口作为公共或私有接口是有帮助的,因为如果我发现自己需要更改这些决定,这通常表明底层的类结构需要修改。
在本书中,我使用了这些标签:
@private
:表示属性或方法被认为是其上下文私有的。通常情况下,这不会以任何方式强制执行,只会有助于澄清意图。@constructor
:表示该函数是一个 JavaScript 构造函数,当与new
关键字结合使用时,将实例化并返回指定的对象类型。@param
:表示函数或方法的参数。一个@param
定义将包括一个括号中的类型定义,在整个函数中使用的参数名,以及一个可选的参数描述。可选参数用可选运算符表示(详见下文)。@return
:表示函数返回值。一个@return
标签将在括号中包含一个类型定义,指示返回值的数据类型。@type
:表示定义时变量或属性的类型。
类型定义 是注释的重要组成部分。所有类型定义都用花括号括起来。因为 JavaScript 是动态类型化的,所以类型注释可以指定多种数据类型,每种数据类型用竖线分隔。例如:
{boolean}
指定布尔类型,而
{boolean|number}
指定类型可以是布尔值或数字。
复合类型使用尖括号指定。例如:
{Array<boolean>}
指定该类型是布尔值数组,而
{Object<string, number>}
指定对象的键是字符串,关联的值是数字。
类型定义中还使用了一些运算符:
- 可空的:
?
操作符表示类型可以是指定的数据类型或空值。因此{?Object}
相当于{Object|null}
。我没有在本书的例子中使用可空操作符;相反,我假设所有类型在默认情况下都可以为空,除非使用不可为空的操作符指定了其他类型。 - 不可为空的:
!
操作符表示类型不能为空。例如,{!Array<!string>}
指定类型必须是字符串数组。不允许空数组或其他类型的数组。 - 可选:在
@param
类型定义中使用的=
运算符表示该参数是可选的。我通过在所有可选参数的名称前添加前缀opt_
来扩展这一点。例如,@param {boolean=} opt_isActive
指定参数opt_isActive
是可选的,但是如果它存在,它必须是一个布尔值(或 null)。
使用对象作为事件处理程序
我最喜欢的 DOM 鲜为人知的特性之一是EventListener
接口。我们都知道如何使用Element.addEventListener
方法将事件监听器附加到 DOM 元素,该方法有三个参数:
eventType
:表示事件类型的字符串handler
:事件发生时执行的功能bubble
:是否在气泡阶段执行该功能
鲜为人知的是,DOM 指定你可以使用任何对象作为处理程序,只要它实现了EventListener
接口。根据 DOM 级标准:
EventListener 接口是处理事件的主要方法。用户实现 EventListener 接口,并使用 AddEventListener 方法在 EventTarget 上注册他们的侦听器。用户在使用完侦听器后,还应该从 EventTarget 中删除他们的 EventListener。
一个EventListener
接口被定义为任何对象上的一个名为handleEvent
的方法:
interface EventListener {
void handleEvent(in Event evt);
};
这意味着任何实现了handleEvent
方法的对象都可以被用作事件处理器,如清单 A-1 中的所示。
清单 。使用对象作为事件处理程序
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<p id="targetElement">Click me!</p>
<script>
var targetElement = document.getElementById(’targetElement’);
var eventObject = {
handleEvent: function(event) {
console.log(event.type);
}
};
targetElement.addEventListener(’click’, eventObject, true);
</script>
</body>
</html>
在这个例子中,您已经创建了一个简单的eventObject
,它以一个叫做handleEvent
的方法的形式实现了EventListener
接口。然后将它绑定到目标元素的 click 事件,当您单击“click me”文本时,您将看到“click”出现在控制台中。
这种技术对于将事件处理程序封装在对象和类中很有用,而不是将它们作为单独的函数。您甚至可以创建一个具有多个事件处理程序的事件处理程序对象,并根据需要使用 EventListener 接口来委托活动。例如,回想一下第三章中的 WebSockets 示例(清单 3-7 ),它有单独的函数用于处理error
、close
、open
和message
事件,都绑定到 WebSocket 接口。您可以轻松地将所有这些事件处理程序创建为单个对象上的方法,如清单 A-2 中的所示。
清单 A-2 。重写清单 3-7 以使用通用 EventListener 接口
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Web Sockets Demonstration</h1>
<script>
// Create a new web socket connection to the chat service.
var chatUrl = ’ws://www.fgjkjk4994sdjk.com/chat’;
var validProtocols = [’chat’, ’json’];
var chatSocket = new WebSocket(chatUrl, validProtocols);
/**
* Creates an error handling class that implements the EventListener interface.
* @constructor
* @returns {Object}
*/
function CreateWebSocketEventObject() {
/**
* Handles an error event on the chat socket object.
* @private
*/
this.handleError_ = function() {
console.log(’An error occurred on the chat connection.’);
};
/**
* Handles a close event on the chat socket object.
* @param {CloseEvent} event The close event object.
* @private
*/
this.handleClose_ = function(event) {
console.log(’The chat connection was closed because ’, event.reason);
};
/**
* Handles an open event on the chat socket object.
* @param {OpenEvent} event The open event object.
* @private
*/
this.handleOpen_ = function(event) {
console.log(’The chat connection is open.’);
};
/**
* Handles a message event on the chat socket object.
* @param {MessageEvent} event The message event object.
* @private
*/
this.handleMessage_ = function(event) {
console.log(’A message event has been sent.’);
// The event object contains the data that was transmitted from the server.
// That data is encoded either using the chat protocol or the json protocol,
// so we need to deterine which protocol is being used.
if (chatSocket.protocol === validProtocols[0]) {
console.log(’The chat protocol is active.’);
console.log(’The data the server transmitted is: ’, event.data);
// etc...
} else {
console.log(’The json protocol is active.’);
console.log(’The data the server transmitted is: ’, event.data);
// etc...
}
};
/**
* Implements the EventListener interface for the object and invokes the
* correct handler based on the event type.
* @param {SocketEvent} event
*/
this.handleEvent = function(event) {
switch (event.type) {
case ’error’:
this.handleError_();
break;
case ’close’:
this.handleClose_(event);
break;
case ’open’:
this.handleOpen_(event);
break;
case ’message’:
this.handleMessage_(event);
break;
default:
console.warn(’Unknown event of type ’, event.type);
}
};
}
// Create a new event object using the constructor.
var eventHandlerObject = new CreateWebSocketEventObject();
// Bind the event object to the chat socket.
chatSocket.addEventListener(’error’, eventHandlerObject);
chatSocket.addEventListener(’close’, eventHandlerObject);
chatSocket.addEventListener(’open’, eventHandlerObject);
chatSocket.addEventListener(’message’, eventHandlerObject);
</script>
</body>
</html>
在这个版本的示例中,您构建了一个构造函数,它返回一个实现了EventListener
接口的对象。在该接口方法中,它检查传入事件的类型属性,并调用正确的处理程序方法。这为您提供了更好的事件处理程序封装,并提供了轻松打开多个 Web 套接字并使用同一个构造函数为所有这些套接字构建事件处理程序的可能性。
承诺
异步活动在 JavaScript 应用中很常见,处理它们的标准方式是使用回调函数。举个例子,考虑你在第六章中做的动态脚本加载。清单 A-13 有一个函数,它动态加载一个指定的脚本,并根据结果执行成功或错误回调函数:
/**
* Dynamically loads a script and invokes an optional callback.
* @param {string} srcUrl The URL of the script file to load.
* @param {function=} opt_onLoadCallback An optional function to call when the
* script is loaded.
* @param {function=} opt_onErrorCallback An optional function to call if the
* script fails to load.
*/
function loadScript(srcUrl, opt_onLoadCallback, opt_onErrorCallback) {
// Create a script tag.
var newScript = document.createElement(’script’);
// Apply the load callback, if one was provided.
if (opt_onLoadCallback) {
if (newScript.readyState) {
// Internet explorer.
newScript.onreadystatechange = function() {
if (newScript.readyState == ’loaded’ ||
newScript.readyState == ’complete’) {
newScript.onreadystatechange = null;
opt_onLoadCallback.call();
}
};
} else {
// Every other browser in the universe.
newScript.onload = opt_onLoadCallback;
}
}
// Apply the error callback, if one was provided.
if (opt_onErrorCallback) {
newScript.onerror = opt_onErrorCallback;
}
newScript.src = srcUrl;
document.querySelector(’head’).appendChild(newScript);
}
这个函数有三个参数:它需要加载的脚本的 URL,以及成功和错误回调函数。
使用回调的问题是它们会导致复杂的代码。如果有嵌套的回调函数,例如,如果成功回调函数还执行另一个异步任务,那么回调函数可能会变得难以管理,代码也难以阅读。
承诺提供了一种不同的方式来处理 JavaScript 代码中的异步操作。Promise 是一个表示异步操作结果的对象。实际结果(成功或失败)不需要在承诺产生时就知道;相反,异步动作将像任何其他同步动作一样返回一个承诺对象。这允许您简化异步代码,减少甚至消除对嵌套回调的需要。
许诺对象处于四种状态之一:
- 已完成:承诺表示的异步操作已经完成并且成功。
- 拒绝:承诺表示的异步操作已经完成,但导致了错误。
- 待定:这是创建承诺时的初始状态。处于待定状态的承诺既不履行也不拒绝。
- 已解决:承诺不再待定,并且已经履行或拒绝。
一旦承诺进入履行或拒绝状态,它就不能改变,所以履行的承诺永远不会被拒绝,反之亦然。
使用承诺构造函数创建承诺:
var myPromise = new Promise(executor)
executor
是带有两个参数的函数:resolve
和reject
。当您创建新的承诺时,这些resolve
和reject
参数将成为您稍后将指定的函数的占位符。通常,它们是异步操作成功或失败时要调用的函数。
Promise 对象公开了一个 API,用于访问异步操作的状态以及它可能返回的任何内容。
Promise.then(resolve, reject)
:then
方法使您能够指定当承诺完成时将被调用的resolve
和reject
函数。Promise.catch(reject)
:catch
方法允许您指定当承诺被拒绝时调用的reject
函数。
为了演示如何创建一个基本承诺,然后分配 resolve 和 reject 处理程序,清单 A-3 展示了重新编写的清单 A-13,以使用一个承诺。
清单 A-3 。使用承诺来表示动态加载脚本
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<script src="../js-lib/detect-support.js"></script>
<script>
/**
* Dynamically loads a script and invokes an optional callback.
* @param {string} srcUrl The URL of the script file to load.
* @return {Promise<null>}
*/
function loadScript(srcUrl) {
var myPromise = new Promise(function(resolve, reject) {
var newScript = document.createElement(’script’);
if (newScript.readyState) {
// Internet explorer.
newScript.onreadystatechange = function() {
if (newScript.readyState == ’loaded’ ||
newScript.readyState == ’complete’) {
newScript.onreadystatechange = null;
resolve();
}
};
} else {
// Every other browser in the universe.
newScript.onload = resolve;
}
newScript.onError = reject;
newScript.src = srcUrl;
document.querySelector(’head’).appendChild(newScript);
});
return myPromise;
}
// Test for supported features.
var supportedFeatures = new DetectHTML5Support();
if (!supportedFeatures.localStorage) {
// The Web Storage is not supported, so load a shim. The loadScript function
// now returns a Promise.
loadScript(’../js-lib/webstorage-shim.js’).then(function() {
initApplication();
}, function() {
console.log(’Script failed to load.’);
});
} else {
// Web Storage was supported, so continue with the application.
initApplication();
}
/**
* Hypothetical function for initializing the application.
*/
function initApplication() {
console.log(’Application continues...’);
// Etc.
}
</script>
</body>
</html>
您会注意到,loadScript
函数现在使用占位符来构造并返回一个承诺,这些占位符用于稍后将指定的resolve
和reject
函数。当函数被调用时,代码使用Promise.then
方法应用resolve
和reject
函数。
连锁承诺
承诺为处理涉及多个异步操作的情况提供了很大的灵活性。例如,如果您从Promise.then
方法返回一个承诺,您可以将承诺链接在一起。为了说明这一点,您可以使用loadScript
函数一个接一个地加载三个不同的脚本。由于loadScript
函数返回一个承诺,您可以简单地将对Promise.then
的调用链接在一起。要做到这一点,你需要以一个永远成功的空头承诺开始这个链条:
var promiseChain = Promise.resolve();
promiseChain.then(function() {
return loadScript(’script1.js’);
}).then(function() {
return loadScript(’script2.js’);
}).then(function() {
return loadScript(’script3.js’);
}).catch(function() {
console.log(’An error occurred when loading the scripts.’);
});
如果你使用第六章的的“处理破损或缺失的 HTML5 实现”一节中提到的特性注册模式,你可以进一步简化为一个简单的for
循环。清单 A-4 展示了使用这种技术重写清单 A-14。
清单 。链接承诺按顺序加载多个垫片
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<script src="../js-lib/detect-support.js"></script>
<script>
// Create a registry of HTML features that we need and shims to apply if they
// are not present. The registry will be an array of objects; each object will
// consist of a feature name and a path to a shim to apply if that feature is
// not supported.
var featureRegistry = [
{
’featureName’ : ’sessionStorage’,
’shim’ : ’../js-lib/webstorage-shim.js’
},
{
’featureName’ : ’requestAnimationFrame’,
’shim’ : ’../js-lib/animationframe-shim.js’
}
];
/**
* Dynamically loads a script and invokes an optional callback.
* @param {string} srcUrl The URL of the script file to load.
* @return {Promise<null>}
*/
function loadScript(srcUrl) {
var myPromise = new Promise(function(resolve, reject) {
var newScript = document.createElement(’script’);
if (newScript.readyState) {
// Internet explorer.
newScript.onreadystatechange = function() {
if (newScript.readyState == ’loaded’ ||
newScript.readyState == ’complete’) {
newScript.onreadystatechange = null;
resolve();
}
};
} else {
// Every other browser in the universe.
newScript.onload = resolve;
}
newScript.onError = reject;
newScript.src = srcUrl;
document.querySelector(’head’).appendChild(newScript);
});
return myPromise;
}
// Test for supported features.
var supportedFeatures = new DetectHTML5Support();
// Go through the registry and for each item load a shim if it isn’t supported.
var promiseChain = Promise.resolve();
featureRegistry.forEach(function(currFeature) {
if (!supportedFeatures[currFeature.featureName]) {
promiseChain = promiseChain.then(function() {
return loadScript(currFeature.shim);
});
}
});
promiseChain.then(function() {
initApplication();
}, function() {
console.log(’A shim failed to load.’);
});
/**
* Hypothetical function for initializing the application.
*/
function initApplication() {
console.log(’Application continues...’);
// Etc.
}
</script>
</body>
</html>
关于这个例子,你可能会注意到的第一件事是,它比第六章中的原始例子要紧凑得多。更简单的代码是使用承诺的好处之一。
在本例中,首先创建一个已履行的承诺,作为承诺链中的第一个环节。然后遍历特性注册表并测试每个特性。不支持的功能加载了垫片,它们的承诺添加到了链条上。这时在链条上使用Promise.then
方法。如果所有需要的特性都得到支持,那么这个链将只包含最初实现的承诺,因此将立即调用resolve
处理程序。如果有不支持的特性,那么这个链将由多个承诺组成,每个承诺将依次执行。
从承诺中返回值
到目前为止,在我的例子中,异步操作没有返回任何值。他们只是成功了或者失败了。许多异步动作将返回一个值,您需要在您的resolve
和reject
方法中访问该值。为此,您可以为您的resolve
和reject
方法指定参数。例如,想象这样一种情况,您使用返回承诺的fetchData
方法从服务器获取数据:
function fetchData() {
var myPromise = new Promise(function(resolve, reject) {
var client = new XMLHttpRequest();
client.open(’POST’, ’http://www.fakeservice.com/myservice’);
client.send();
client.onload = function () {
if (this.status == 200) {
// Successfully fetched information from the service. Resolve the
// promise with the information.
resolve(this.response);
} else {
// Did not successfully fetch information from the service. Reject the
// promise with the error message.
reject(this.statusText);
}
};
client.onerror = function () {
reject(this.statusText);
};
});
return myPromise;
}
fetchData().then(function(serviceResponse) {
console.log(’The service returned ’, serviceResponse);
}, function(errorMessage) {
console.error(errorMessage);
});
在这个示例函数中,您创建了一个新的XMLHttpRequest
对象来从服务器异步获取数据,但是返回一个包装了响应值的承诺。然后,您可以在Promise.then
回调中访问响应值。
浏览器支持承诺
承诺是 JavaScript 的一个相对较新的特性。截至本文撰写时,除了 Internet Explorer 之外的所有浏览器都支持承诺,Internet Explorer Edge 发布时将提供全面支持。与此同时,在https://github.com/jakearchibald/es6-promise
有一个很好的承诺填补空白。
进一步阅读
这只是对承诺的简单介绍。你可以用它们做更多的事情。本书中的许多例子可以使用 Promises 重写,从而产生更简单的代码。
要了解更多关于承诺的信息,请查看以下资源:
https://promisesaplus.com/
处的承诺/A+规格- 在
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
的 Mozilla 开发者网络参考 - HTML5Rocks 的承诺教程,
www.html5rocks.com/en/tutorials/es6/promises/
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 我与微信审核的“相爱相杀”看个人小程序副业