四、使用音频和视频
在这一章中,我们将探索你能用两个重要的 HTML5 元素做什么——音频和视频并且我们将向你展示它们如何被用来创建引人注目的应用。音频和视频元素为 HTML5 应用添加了新的媒体选项,允许您在没有插件的情况下使用音频和视频,同时提供一个通用的、集成的、可脚本化的 API。**
首先,我们将讨论音频和视频容器文件和编解码器,以及为什么我们最终得到了今天支持的编解码器。我们将继续描述缺乏通用编解码器支持——这是使用媒体元素的最大缺点——并且我们将讨论我们如何希望这在未来不会成为如此大的问题。我们还将向您展示一种切换到最适合浏览器显示的内容类型的机制。
接下来,我们将向您展示如何使用 API 以编程方式使用音频和视频控件,最后我们将探索音频和视频在您的应用中的使用。
音频和视频概述
在下面的章节中,我们将讨论一些与音频和视频相关的关键概念:容器和编解码器。
视频容器
音频或视频文件实际上只是一个容器文件,类似于包含许多文件的 ZIP 存档文件。图 4-1 显示了一个视频文件(一个视频容器)是如何包含音频轨道、视频轨道和附加元数据的。音频和视频轨道在运行时被组合以播放视频。元数据包含有关视频的信息,如封面、标题和副标题、字幕信息等。
***图 4-1。*视频容器概述
一些流行的视频容器格式包括:
- 音频视频交错(。avi)
- Flash 视频(。flv)
- MPEG 4 (.mp4)
- 型芯型腔(. mkv)
- ogg(ogv)
音频和视频编解码器
音频和视频编码器/解码器 ( 编解码器)是用于编码和解码特定音频或视频流的算法,以便可以播放它们。原始媒体文件非常庞大,因此如果不进行编码,视频或音频剪辑将包含大量数据,这些数据可能太大,无法在合理的时间内通过互联网传输。如果没有解码器,接收方将无法从编码形式中重建原始媒体源。编解码器能够理解特定的容器格式,并对其包含的音频和视频轨道进行解码。
以下是一些示例音频编解码器:
- 加气混凝土
- MPEG-3
- 还有沃比斯
视频编解码器示例如下:
- H.264
- VP8
- Ogg Theora
编解码器大战和暂时休战
一些编解码器受专利保护,而另一些则免费提供。例如,Vorbis 音频编解码器和 Theora 视频编解码器是免费提供的,而 MPEG-4 和 H.264 编解码器的使用需要支付许可费。
最初,HTML5 规范要求支持某些编解码器。然而,一些供应商不希望包括 Ogg Theora,因为它不是他们现有的硬件和软件堆栈的一部分。例如,苹果的 iPhone 包括 h264 视频的硬件加速解码,但没有 Theora。另一方面,免费系统不能在不损害下游分发的情况下包含专有的付费编解码器。最重要的是,某些专有编解码器提供的性能是浏览器采用免费编解码器的一个因素。这种情况导致了僵局;似乎没有一个单一的编解码器,所有的浏览器供应商都愿意实现。
目前,编解码器要求已从规范中删除。然而,这一决定可能会在未来重新审议。现在,了解当前的浏览器支持,并了解您可能需要为不同的环境重新编码您的媒体。(您可能已经开始这样做了。)
我们确实希望对不同编解码器的支持会随着时间的推移而增加和融合,使常见媒体类型的选择变得容易和普遍。一种编解码器也有可能发展成为 Web 的事实上的标准编解码器。此外,媒体标签具有一种内置机制,可以切换到最适合浏览器显示的内容类型,从而简化对不同环境的支持。
韦伯来了
Frank 说:“谷歌在 2010 年 5 月推出了 WebM 视频格式。WebM 是一种新的音频和视频格式,旨在清除网络上模糊的媒体格式。WebM 文件的扩展名为.webm
,在基于 Matroska 的容器中包含 VP8 视频和 Ogg Vorbis 音频。Google 在涵盖源代码和专利权的许可许可下发布了 WebM 规范和软件。作为一种对实施者和发布者都免费的高质量格式,WebM 代表了编解码器领域的重大发展。”
音频和视频限制
音频和视频规范中有一些不支持的内容:
- 流媒体音频和视频。即 HTML5 视频目前没有码率切换的标准;当前的实现仅支持完整的媒体文件。然而,一旦支持流媒体格式,该规范的某些方面将在将来支持流媒体。
- 媒体受到 HTTP 跨源资源共享的限制。参见第六章了解更多关于跨产地资源共享的信息(CORS)。
- 全屏视频是不可脚本化的,因为让可脚本化的元素接管全屏会被认为是违反安全的。然而,浏览器可以让用户通过附加控件选择全屏观看视频。
支持音频和视频的浏览器
由于支离破碎的编解码器支持,仅仅知道哪些浏览器支持新的audio
和video
元素是不够的;您还需要知道支持哪些编解码器。表 4-1 显示了在撰写本文时哪些浏览器支持哪些编解码器。
还要注意的是,谷歌宣布将放弃对 MP4 格式的支持,但这还没有发生。此外,还有一个插件可以用来在 Internet Explorer 9 中播放 WebM。首先测试是否支持音频和视频总是一个好主意。本章后面的“检查浏览器支持”一节将向您展示如何以编程方式检查浏览器支持。
使用音频和视频 API
在本节中,我们将探讨音频和视频在您的应用中的使用。与以前的视频嵌入技术(通常使用 Flash、QuickTime 或 Windows Media 插件嵌入视频)相比,使用新的媒体标签有两个主要优势,旨在使用户和开发人员的生活更加轻松:
- 作为原生浏览器环境的一部分,新的音频和视频标签消除了部署障碍。虽然有些插件的安装率很高,但在受控的企业环境中却经常被屏蔽。一些用户选择禁用这些插件,因为…招摇…广告显示那些插件也能够,这也删除了他们的能力,用于媒体播放。插件也是安全问题的独立攻击媒介。插件通常很难将它们的显示与浏览器的其他内容整合在一起,导致某些网站设计的剪辑或透明度问题。因为插件使用一个独立的呈现模型,这个模型不同于基本网页的呈现模型,如果弹出菜单或其他可视元素需要跨越页面中的插件边界,开发人员会遇到困难。
- 媒体元素向文档展示了一个通用的、集成的、可脚本化的 API。作为一名开发人员,您对新媒体元素的使用允许以非常简单的方式编写内容的控制和回放。在这一章的后面,我们将会看到许多这样的例子。
当然,使用媒体标签有一个主要的缺点:缺乏通用的编解码器支持,这一点在本章前面已经讨论过了。然而,我们预计对编解码器的支持将会增加,并随着时间的推移而融合,使常见媒体类型的选择变得容易和普遍。此外,媒体标签有一个内置机制,可以切换到最适合浏览器显示的内容类型,您很快就会看到这一点。
检查浏览器支持
检查对video
和audio
标签支持的最简单方法是用脚本动态创建一个或两个标签,并检查函数的存在:
var hasVideo = !!(document.createElement('video').canPlayType);
这个简单的代码行将动态创建一个video
元素,并检查canPlayType()
函数是否存在。通过使用!!
操作符,结果被转换成一个布尔值,该值表示是否可以创建一个视频对象。
但是,如果不支持视频或音频,您可以选择使用一个启用脚本,该脚本将媒体脚本标记引入旧浏览器,允许相同的脚本能力,但使用 Flash 等技术进行回放。
或者,您可以选择在您的audio
或video
标签之间包含替代内容,替代内容将代替不支持的标签显示。如果浏览器不支持 HTML5 标签,这种替代内容可以用于 Flash 插件来显示相同的视频。如果你仅仅希望在不支持的浏览器上显示文本消息,在video
或audio
元素中添加内容是很容易的,如清单 4-1 所示。
***清单 4-1。*简单视频元素
<video src="video.webm" controls> Your browser does not support HTML5 video. </video>
然而,如果您选择使用一种替代方法来为不支持 HTML5 媒体的浏览器呈现视频,您可以使用相同的元素内容部分来提供对显示相同媒体的外部插件的引用,如清单 4-2 所示。
***清单 4-2。*带闪光后退的视频元素
<video src="video.webm" controls> <object data="videoplayer.swf" type="application/x-shockwave-flash"> <param name="movie" value="video.swf"/> </object> Your browser does not support HTML5 video. </video>
通过在video
元素中嵌入一个显示 Flash 视频的object
元素,如果 HTML5 视频可用,它将是首选,Flash 视频将用作后备。不幸的是,这需要提供多个版本的视频,直到 HTML5 支持无处不在。
无障碍
让每个人都能访问你的 web 应用不仅仅是正确的事情;这是一笔好生意,在某些情况下,这是法律!应该为视力或听力有限的用户提供满足他们需求的替代内容。请记住,位于视频和音频元素之间的替代内容只有在浏览器不支持这些元素时才会显示,因此不适用于浏览器可能支持 HTML5 媒体但用户可能不支持的可访问显示。
视频可访问性的新兴标准是 Web 视频文本轨道(WebVTT),以前称为 Web 子片段文本(WebSRT)格式。在撰写本文时,它才刚刚开始出现在一些早期版本的浏览器中。WebVTT 使用一个简单的文本文件(*.vtt
),在第一行以单词WEBVTT
开始。vtt
文件必须以 mime 类型text/vtt
提供。清单 4-3 显示了一个示例vtt
文件的内容。
清单 4-3。 WebVTT 文件
`WEBVTT
1
00:00:01,000 --> 00:00:03,000
What do you think about HTML5 Video and WebVTT?..
2
00:00:04,000 --> 00:00:08,000
I think it’s great. I can’t wait for all the browsers to support it!`
要在video
元素中使用vtt
文件,添加指向vtt
文件的track
元素,如下例所示:
<video src="video.webm" controls> <track label="English" kind="subtitles" srclang="en" src="subtitles_en.vtt" default> Your browser does not support HTML5 video. </video>
您可以添加多个轨道元素。清单 4-4 展示了如何使用指向vtt
文件的轨道元素来支持英语和荷兰语字幕。
***清单 4-4。*在视频元素中使用 WebVTT 轨道
<video src="video.ogg" controls> <track label="English" kind="subtitles" srclang="en" src="subtitles_en.vtt"> <track label="Dutch" kind="subtitles" srclang="nl" src="subtitles_nl.vtt"> Your browser does not support HTML5 video. </video>
WebVTT 标准支持的不仅仅是字幕。它还允许标题和提示设置(关于如何呈现文本的说明)。完整的 WebVTT 语法超出了本书的范围。更多细节见[www.whatwg.org/specs/web-apps/current-work/webvtt.html](http://www.whatwg.org/specs/web-apps/current-work/webvtt.html)
的 WHATWG 规范。
了解媒体元素
由于一个明智的设计决策,HTML5 中的audio
和video
元素之间有很多共性。音频和视频都支持许多相同的操作——播放、暂停、静音/取消静音、加载等——因此,通用行为被分离到规范的媒体元素部分。让我们通过观察它们的共同点来开始研究媒体元素。
基础知识:声明你的媒体元素
为了举例,我们将使用一个audio
标签来尝试 HTML5 媒体的常见行为。本节中的示例将会是非常媒体化的(惊喜!),它们包含在本书附带的支持文件的code/av
文件夹中。
举一个最简单的例子(示例文件audio.html
),让我们创建一个页面,显示一个舒缓、令人满意、非常公开的领域音频剪辑的音频播放器:约翰·塞巴斯蒂安·巴赫的“空气”(如清单 4-5 所示)。
***清单 4-5。*带有音频元素的 HTML 页面
`
这个片段假设 HTML 文档和音频文件(在本例中为johann_sebastian_bach_air.ogg
)来自同一个目录。如图图 4-2 所示,在支持audio
标签的浏览器中查看,会显示一个简单的控制和播放栏,代表要播放的音频。当用户单击播放按钮时,音轨会按预期开始播放。
***图 4-2。*简单的音频控制
controls
属性告诉浏览器显示用于在媒体剪辑中开始、停止和查找的常见用户控件,以及音量控件。省略controls
属性会隐藏它们,并且让用户无法开始播放剪辑。
标签之间的内容是浏览器在不支持媒体标签时将显示的文本表示。如果您和您的用户运行的是旧版本的浏览器,他们将会看到这种情况。它还提供了包含媒体的替代呈现器的机会,例如 Flash player 插件或媒体文件的直接链接。
使用源
最后,我们来看最重要的属性:src
。在最简单的设置中,单个src
属性指向包含媒体剪辑的文件。但是,如果有问题的浏览器不支持该容器或编解码器(在这种情况下,Ogg 和 Vorbis)呢?然后,另一个声明显示在清单 4-6 中;它包括多个来源,浏览器可以从中选择(参见示例文件audio_multisource.html
)。
***清单 4-6。*具有多个源元素的音频元素
<audio controls> <source src="johann_sebastian_bach_air.ogg"> <source src="johann_sebastian_bach_air.mp3"> An audio clip from Johann Sebastian Bach. </audio>
在这种情况下,我们在audio
标签上包含了两个新的source
元素,而不是src
属性。这允许浏览器选择最适合其回放能力的源,并将最适合的源用作实际的媒体剪辑。源是按顺序处理的,因此可以播放多个列出的源类型的浏览器将使用它遇到的第一个。
注意将用户体验最好或服务器负载最低的媒体源文件放在任何
source
列表的最前面。
在支持的浏览器中运行此剪辑可能不会改变您看到的内容。但是如果浏览器支持 MP3 格式而不支持 Ogg Vorbis 格式,那么现在将支持媒体播放。这种声明模型的优点在于,当您编写代码与媒体文件交互时,实际使用的是哪个容器或编解码器并不重要。浏览器为您提供了一个统一的界面来操作媒体,无论哪个源匹配回放。
但是,还有另一种方法可以提示浏览器使用哪种媒体源。回想一下,媒体容器可以支持许多不同的编解码器类型,您将会理解,浏览器可能会被误导,根据所声明的源文件的扩展名,它支持或不支持哪些类型。如果您指定的类型属性与您的源不匹配,浏览器可能会拒绝播放媒体。只有在您确实知道的情况下,包含类型才是明智的。否则,最好省略这个属性,让浏览器检测编码,如清单 4-7 (在示例文件audio_type.html
中)所示。还要注意,WebM 格式只允许一个音频编解码器和一个视频编解码器。这意味着.webm
扩展名或 video/webm 内容类型会告诉您有关该文件的所有信息。如果一个浏览器可以播放。webm,它应该可以播放任何有效的.webm
文件。
***清单 4-7。*在音频元素中包含类型和编解码器信息
<audio controls> <source src="johann_sebastian_bach_air.ogg" type="audio/ogg; codecs=vorbis"> <source src="johann_sebastian_bach_air.mp3" type="audio/mpeg"> An audio clip from Johann Sebastian Bach. </audio>
如您所见,type
属性可以声明容器和编解码器类型。这里的值分别代表 Ogg Vorbis 和 MP3。完整的列表由 RFC 4281 管理,RFC 4281 是由互联网工程任务组(IETF)维护的文档,但是一些常见的组合在表 4-2 中列出。
取得控制权
您已经看到,默认的回放控件可以通过使用video
或audio
标签中的controls
属性来显示。正如您所料,当显示媒体时,省略该属性将不会显示控件,但是对于音频文件,它也不会显示任何内容,因为音频元素的唯一可视表示是它的控件。(没有控件的视频仍会显示视频内容。)省略controls
属性不应该显示任何影响页面正常呈现的内容。让媒体播放的一种方法是在标签中设置另一个属性:autoplay
(参见清单 4-8 和示例文件audio_no_control.html
)。
***清单 4-8。*使用自动播放属性
<audio **autoplay**> <source src="johann_sebastian_bach_air.ogg" type="audio/ogg; codecs=vorbis"> <source src="johann_sebastian_bach_air.mp3" type="audio/mpeg"> An audio clip from Johann Sebastian Bach. </audio>
通过包含autoplay
属性,媒体文件将在加载后立即播放,无需任何用户交互。(注意,并非所有地方都支持自动播放。比如在 iOS 上是禁用的。)然而,大多数用户会觉得这非常令人讨厌,所以谨慎使用autoplay
。播放音频而不进行提示可能是为了营造一种氛围效果,或者更糟的是,向用户强加一个广告。但它也会干扰用户机器上的其他音频播放,并且对依赖音频屏幕阅读器来浏览网页内容的用户非常不利。还要注意的是,有些设备,比如 iPad,会阻止自动播放,甚至是自动播放媒体文件(例如,由页面加载事件触发)。
如果内置控件不适合用户界面的布局,或者如果您需要使用默认控件中没有公开的计算或行为来控制媒体元素,那么还有许多内置 JavaScript 函数和属性来帮助您。表 4-3 列出了一些最常见的功能。
canPlayType(type)
方法有一个不明显的用例:通过将任意视频剪辑的 MIME 类型传递给动态创建的video
元素,您可以使用一个简单的脚本来确定当前浏览器是否支持该类型。例如,以下代码提供了一种快速的方法来确定当前浏览器是否支持播放 MIME 类型为fooType
的视频,而不在浏览器窗口中显示任何可见内容:
var supportsFooVideo = !!(document.createElement('video').canPlayType(‘fooType’));
请注意,该函数返回非常非二进制的“null”、“maybe”或“possible”,其中“possible”是可能的最佳场景。
表 4-4 显示了媒体元素的一些只读属性。
表 4-5 显示了媒体元素上的一些属性,这些属性允许脚本修改它们并直接影响回放。因此,它们的行为类似于函数。
在各种功能和属性之间,开发人员可以创建任何媒体回放用户界面,并使用它来控制浏览器支持的任何音频或视频剪辑。
使用音频
如果你理解了audio
和video
媒体元素的共享属性,你基本上已经看到了audio
标签所能提供的一切。因此,让我们来看一个简单的例子,它展示了控件脚本的运行。
音频激活
如果您的用户界面需要为用户播放音频剪辑,但您不希望播放时间线或控件影响显示,您可以创建一个不可见的audio
元素,该元素的controls
属性未设置或设置为false
,并呈现您自己的音频播放控件。考虑一下清单 4-9 中的简单代码,它也可以在样本代码文件audioCue.html
中找到。
***清单 4-9。*添加自己的播放按钮控制音频
`
Play
if (music.paused) {
music.play();
toggle.innerHTML = “Pause”;
}
else {
music.pause();
toggle.innerHTML =“Play”;
}
}
我们再次使用audio
元素来演奏我们最喜欢的巴赫曲子。然而,在这个例子中,我们隐藏了用户控件,并且没有将剪辑设置为加载时自动播放。相反,我们创建了一个切换按钮来控制脚本的音频回放:
<button id="toggle" onclick="toggleSound()">Play</button>
我们的简单按钮被初始化来通知用户点击它将开始回放。并且每按一次按钮,就会触发toggleSound()
功能。在toggleSound()
函数中,我们首先访问 DOM 中的audio
和button
元素:
if (music.paused) { music.play(); toggle.innerHTML = "Pause"; }
通过访问audio
元素上的paused
属性,我们可以查看用户是否已经暂停了回放。如果没有开始播放,该属性默认为true
,所以在第一次点击时将满足该条件。在这种情况下,我们调用剪辑上的play()
函数,并更改按钮的文本,以指示下一次单击将暂停剪辑:
else { music.pause(); toggle.innerHTML ="Play"; }
相反,如果音乐剪辑没有暂停(如果正在播放),我们将主动pause()
它,并更改按钮文本以指示下次点击将重新开始播放。似乎很简单,不是吗?这就是 HTML5 中媒体元素的作用:在曾经存在无数插件的地方创建简单的跨媒体类型的显示和控制。简单是它自己的奖励。
处理视频
简单就够了。让我们试试更复杂的。HTML5 video
元素非常类似于audio
元素,但是加入了一些额外的属性。表 4-6 显示了其中的一些属性。
video
元素还有一个不适用于audio
元素的关键特性:它可以提供给 HTML5 Canvas 的许多功能(参见第二章)。
创建视频时间轴浏览器
在这个更复杂的例子中,我们将展示一个video
元素如何在动态画布中抓取并显示它的帧。为了演示这一功能,我们将构建一个简单的视频时间轴查看器。当视频播放时,来自显示器的周期性图像帧将被绘制到附近的画布上。当用户点击画布中显示的任何一帧时,我们将视频回放跳转到那个精确的时刻。只需几行代码,我们就可以创建一个时间轴浏览器,用户可以使用它在一个冗长的视频中跳转。
我们的视频剪辑样本是 20 世纪中期电影院诱人的特许广告,所以让我们都去大厅犒劳一下自己(见图 4-3 )。
***图 4-3。*视频时间轴应用
添加视频和画布元素
我们从显示视频剪辑的简单声明开始:
<video id="movies" autoplay oncanplay="startVideo()" onended="stopTimeline()" autobuffer="true" width="400px" height="300px"> <source src="Intermission-Walk-in.ogv"> <source src="Intermission-Walk-in_512kb.mp4"> </video>
由于音频示例中的大多数标记对您来说都很熟悉,所以让我们来关注一下它们的区别。很明显,<audio>
元素已经被<video>
取代,<source>
元素指向了浏览器将要选择的 Ogg 和 MPEG 电影。
在这种情况下,视频被声明为具有autoplay
,这样页面一加载它就开始播放。注册了两个额外的事件处理函数。当视频被加载并准备开始播放时,oncanplay
功能将触发并启动我们的例程。同样,当视频结束时,onended
回调将允许我们停止创建视频帧。
接下来,我们将添加一个名为timeline
的画布,我们将在其中定期绘制视频帧。
<canvas id="timeline" width="400px" height="300px">
添加变量
在演示的下一部分中,我们通过声明一些值来开始我们的脚本,这些值将使我们能够轻松地调整演示并使代码更具可读性:
// # of milliseconds between timeline frame updates var updateInterval = 5000;
`// size of the timeline frames
var frameWidth = 100;
var frameHeight = 75;
// number of timeline frames
var frameRows = 4;
var frameColumns = 4;
var frameGrid = frameRows * frameColumns;`
updateInterval
控制我们捕获视频帧的频率——在本例中,每五秒钟。frameWidth
和frameHeight
设置小时间轴视频帧在画布中显示时的大小。类似地,frameRows
、frameColumns
和frameGrid
决定了我们将在时间轴中显示多少帧:
`// current frame
var frameCount = 0;
// to cancel the timer at end of play
var intervalId;
var videoStarted = false;`
为了跟踪我们正在观看的视频帧,所有演示功能都可以访问一个frameCount
。(为了我们的演示,一帧是我们每五秒钟拍摄的视频样本之一。)这个intervalId
是用来停止我们将用来抓取帧的计时器。最后,我们添加了一个videoStarted
标志来确保每个演示只创建一个计时器。
添加 updateFrame 函数
我们演示的核心功能——视频与画布相遇的地方——是我们抓取一个视频帧并将其绘制到画布上的地方:
`// paint a representation of the video frame into our canvas
function updateFrame() {
var video = document.getElementById(“movies”);
var timeline = document.getElementById(“timeline”);
var ctx = timeline.getContext(“2d”);
// calculate out the current position based on frame
// count, then draw the image there using the video
// as a source
var framePosition = frameCount % frameGrid;
var frameX = (framePosition % frameColumns) * frameWidth;
var frameY = (Math.floor(framePosition / frameRows)) * frameHeight;
ctx.drawImage(video, 0, 0, 400, 300, frameX, frameY, frameWidth, frameHeight);
frameCount++;
}`
正如你在第二章中看到的,对于任何画布,首先要做的是从中获取二维绘图上下文:
var ctx = timeline.getContext("2d");
因为我们想用从左到右、从上到下的帧填充我们的画布网格,所以我们需要根据我们捕获的帧的数量,准确地计算出哪个网格槽将用于我们的帧。根据每个框架的宽度和高度,我们可以确定开始绘图的精确 X 和 Y 坐标:
var framePosition = frameCount % frameGrid; var frameX = (framePosition % frameColumns) * frameWidth; var frameY = (Math.floor(framePosition / frameRows)) * frameHeight;
最后,我们到达在画布上绘制图像的按键调用。我们之前在 canvas 演示中已经看到了位置和缩放参数,但是这里我们没有将图像传递给drawImage
例程,而是传递视频对象本身:
ctx.drawImage(video, 0, 0, 400, 300, frameX, frameY, frameWidth, frameHeight);
画布绘制例程可以将视频源作为图像或模式,这为您提供了一种修改视频并在另一个位置重新显示视频的便捷方式。
注意当画布使用视频作为输入源时,它只绘制当前显示的视频帧。画布显示不会随着视频播放而动态更新。相反,如果您希望画布内容更新,您必须在视频播放时重新绘制图像。
增加启动视频功能
最后,我们更新frameCount
以反映我们已经为我们的时间线拍摄了新的快照。现在,我们只需要一个例程来定期更新我们的时间轴帧:
`function startVideo() {
// only set up the timer the first time the
// video is started
if (videoStarted)
return;
videoStarted = true;
// calculate an initial frame, then create
// additional frames on a regular timer
updateFrame();
intervalId = setInterval(updateFrame, updateInterval);`
回想一下,一旦视频加载足够开始播放,就会触发startVideo()
功能。首先,我们确保每次页面加载只处理一次视频开始,以防视频重新开始:
// only set up the timer the first time the // video is started
` if (videoStarted)
return;
videoStarted = true;`
当视频开始时,我们将捕捉我们的第一帧。然后,我们将启动一个间隔计时器——一个在指定的更新间隔持续重复的计时器——它将定期调用我们的updateFrame()
函数。最终结果是每五秒钟捕获一个新帧:
// calculate an initial frame, then create // additional frames on a regular timer updateFrame(); intervalId = setInterval(updateFrame, updateInterval);
处理用户输入
现在,我们需要做的就是处理单个时间轴帧的用户点击:
`// set up a handler to seek the video when a frame
// is clicked
var timeline = document.getElementById(“timeline”);
timeline.onclick = function(evt) {
var offX = evt.layerX - timeline.offsetLeft;
var offY = evt.layerY - timeline.offsetTop;
// calculate which frame in the grid was clicked
// from a zero-based index
var clickedFrame = Math.floor(offY / frameHeight) * frameRows;
clickedFrame += Math.floor(offX / frameWidth);
// find the actual frame since the video started
var seekedFrame = (((Math.floor(frameCount / frameGrid)) *
frameGrid) + clickedFrame);
// if the user clicked ahead of the current frame
// then assume it was the last round of frames
if (clickedFrame > (frameCount % 16))
seekedFrame -= frameGrid;
// can’t seek before the video
if (seekedFrame < 0)
return;`
事情变得有点复杂了。我们检索时间轴画布,并在其上设置一个点击处理函数。处理程序将使用事件来确定用户单击了哪个 X 和 Y 坐标:
var timeline = document.getElementById("timeline"); timeline.onclick = function(evt) { var offX = evt.layerX - timeline.offsetLeft; var offY = evt.layerY - timeline.offsetTop;
然后,我们使用框架尺寸来计算用户点击了 16 个框架中的哪一个:
// calculate which frame in the grid was clicked // from a zero-based index var clickedFrame = Math.floor(offY / frameHeight) * frameRows; clickedFrame += Math.floor(offX / frameWidth);
单击的帧应该只是最近的视频帧之一,因此确定对应于该网格索引的最近的帧:
// find the actual frame since the video started var seekedFrame = (((Math.floor(frameCount / frameGrid)) * frameGrid) + clickedFrame);
如果用户在当前帧之前单击,则跳回一个完整的网格帧周期以找到实际时间:
// if the user clicked ahead of the current frame // then assume it was the last round of frames if (clickedFrame > (frameCount % 16)) seekedFrame -= frameGrid;
最后,我们必须防止用户点击视频剪辑开始前的帧:
// can't seek before the video if (seekedFrame < 0) return;
现在我们知道了用户想要寻找的时间点,我们可以使用该知识来更改当前的回放时间。尽管这是关键的演示函数,但例程本身非常简单:
` // seek the video to that frame (in seconds)
var video = document.getElementById(“movies”);
video.currentTime = seekedFrame * updateInterval / 1000;
// then set the frame count to our destination
frameCount = seekedFrame;`
通过在我们的视频元素上设置currentTime
属性,我们使视频搜索到指定的时间,并将我们当前的帧计数重置为新选择的帧。
注意与许多处理毫秒的 JavaScript 计时器不同,视频的
currentTime
是以秒为单位指定的。
添加 stopTimeline 功能
对于我们的视频时间轴演示来说,剩下的就是当视频结束播放时停止捕捉帧。虽然不是必需的,但如果我们不执行这一步,演示将继续捕获已完成的演示的帧,过一会儿整个时间轴将会消失:
// stop gathering the timeline frames function stopTimeline() { clearInterval(intervalId); }
当我们的另一个视频处理程序——onended
——被视频播放完成触发时,将调用stopTimeline
处理程序。
我们的视频时间轴的功能可能还不足以让高级用户满意,但它只用了很少的代码就完成了。现在,继续表演。
最终代码
清单 4-10 显示了视频时间线页面的完整代码。
***清单 4-10。*完整的视频时间轴代码
`
// # of milliseconds between timeline frame updates
var updateInterval = 5000;
// size of the timeline frames
var frameWidth = 100;
var frameHeight = 75;
// number of timeline frames
var frameRows = 4;
var frameColumns = 4;
var frameGrid = frameRows * frameColumns;
// current frame
var frameCount = 0;
// to cancel the timer at end of play
var intervalId;
var videoStarted = false;
function startVideo() {
// only set up the timer the first time the
// video is started
if (videoStarted)
return;
videoStarted = true;
// calculate an initial frame, then create
// additional frames on a regular timer
updateFrame();
intervalId = setInterval(updateFrame, updateInterval);
// set up a handler to seek the video when a frame
// is clicked
var timeline = document.getElementById(“timeline”);
timeline.onclick = function(evt) {
var offX = evt.layerX - timeline.offsetLeft;
var offY = evt.layerY - timeline.offsetTop;
// calculate which frame in the grid was clicked
// from a zero-based index
var clickedFrame = Math.floor(offY / frameHeight) * frameRows;
clickedFrame += Math.floor(offX / frameWidth);
// find the actual frame since the video started
var seekedFrame = (((Math.floor(frameCount / frameGrid)) *
frameGrid) + clickedFrame);
// if the user clicked ahead of the current frame
// then assume it was the last round of frames
if (clickedFrame > (frameCount % 16))
seekedFrame -= frameGrid;
// can’t seek before the video
if (seekedFrame < 0)
return;
// seek the video to that frame (in seconds)
var video = document.getElementById(“movies”);
video.currentTime = seekedFrame * updateInterval / 1000;
// then set the frame count to our destination
frameCount = seekedFrame;
}
}
// paint a representation of the video frame into our canvas
function updateFrame() {
var video = document.getElementById(“movies”);
var timeline = document.getElementById(“timeline”);
var ctx = timeline.getContext(“2d”);
// calculate out the current position based on frame
// count, then draw the image there using the video
// as a source
var framePosition = frameCount % frameGrid;
var frameX = (framePosition % frameColumns) * frameWidth;
var frameY = (Math.floor(framePosition / frameRows)) * frameHeight;
ctx.drawImage(video, 0, 0, 400, 300, frameX, frameY, frameWidth, frameHeight);
frameCount++;
}
// stop gathering the timeline frames
function stopTimeline() {
clearInterval(intervalId);
}
实用的临时演员
有时有些技术不适合我们的常规例子,但仍然适用于许多类型的 HTML5 应用。我们在这里向你展示一些简短但常见的实用附加功能。
页面中的背景噪音
许多网站试图通过默认为任何访问者播放音频来娱乐观众。虽然我们不容忍这种做法,但是音频支持使得实现这一点非常容易,如清单 4-11 所示。
***清单 4-11。*使用循环和自动播放属性
`
</audio
You’re hooked on Bach!
`正如你所看到的,播放一个循环的背景声音就像声明一个带有autoplay
和loop
属性集的audio
标签一样简单(参见图 4-4 )。
***图 4-4。*使用自动播放功能在页面加载时播放音乐
在<眨眼>中失去观众
布莱恩对说:“权力越大,责任越大,仅仅因为你能,并不意味着你就应该。如果你想要一个例子,只要记住<blink>
标签!”
不要让简单的音频和视频播放诱惑你在不合适的地方使用它。如果您有令人信服的理由来启用带有autoplay
的媒体—可能是用户期望内容在加载时启动的媒体浏览器—请确保提供禁用该功能的明确方法。没有什么比讨厌的内容更能让用户迅速离开你的网站,因为这些内容是他们不容易关掉的。"
鼠标悬停视频播放
对视频剪辑有效使用简单脚本的另一种方法是根据鼠标在视频上的移动触发play
和pause
例程。这在需要显示许多视频剪辑并让用户选择播放的站点中可能很有用。当用户将鼠标移动到视频剪辑上时,视频剪辑库可以显示简短的预览剪辑,当用户单击时,可以显示完整的视频。使用类似于清单 4-12 的代码样本很容易达到这种效果(参见示例文件mouseoverVideo.html
)。
***清单 4-12。*鼠标检测到一个视频元素上
`
` `
通过简单地设置一些额外的属性,当用户指向视频时可以触发预览回放,如图 4-5 所示。
***图 4-5。*鼠标悬停视频播放
总结
在这一章中,我们探索了你可以用两个重要的 HTML5 元素audio
和video
做什么。我们已经向您展示了如何使用它们来创建引人注目的 web 应用。audio
和video
元素为 HTML5 应用添加了新的媒体选项,允许您在没有插件的情况下使用音频和视频,同时提供一个通用的、集成的、可脚本化的 API。
首先,我们讨论了音频和视频容器文件和编解码器,以及为什么我们最终选择了目前支持的编解码器。然后,我们向您展示了一种切换到最适合浏览器显示的内容类型的机制,并向您展示了如何使用 WebVTT 访问视频。
接下来,我们向您展示了如何使用 API 以编程方式使用控件音频和视频,最后我们看了如何在您的应用中使用 HTML5 音频和视频。
在下一章,我们将展示如何用最少的代码使用地理定位来定制应用的输出以适应用户的位置。
五、使用地理定位 API
假设您想要创建一个 web 应用,在应用用户步行(或跑步)即可到达的商店中提供跑鞋折扣和特价。使用地理定位 API,您可以请求用户共享他们的位置,如果他们同意,您可以向他们提供如何去附近的商店以折扣价购买一双新鞋的说明。
使用地理定位的另一个例子是一个追踪你跑了(或走了)多远的应用。你可以想象在开始跑步时打开手机浏览器中的应用。当你在移动时,应用会跟踪你跑了多远。跑步的坐标甚至可以覆盖在地图上,甚至可能带有高程剖面图。如果你在和其他对手赛跑,这个应用甚至可以显示对手的位置。
其他地理定位应用的想法可能是逐圈 GPS 风格的导航,社交网络应用,让你可以看到你的朋友在哪里,这样你就可以选择你想去的咖啡店,以及许多不寻常的应用。
在这一章中,我们将探索使用地理定位可以做些什么,这是一个令人兴奋的 API,它允许用户与 web 应用共享他们的位置,以便他们可以享受位置感知服务。首先,我们来看看地理位置信息的来源——纬度、经度和其他属性——以及它们来自哪里(GPS、Wi-Fi、蜂窝三角测量等等)。然后,我们将讨论使用地理位置数据的隐私问题,以及浏览器如何处理这些数据。
之后,我们将深入讨论地理定位 API 中两种不同的位置请求函数(方法):一次性位置请求和重复位置更新,我们将向您展示如何以及何时使用它们。接下来,我们将向您展示如何使用相同的 API 构建一个实用的地理定位应用,最后我们将讨论一些额外的用例及技巧。
关于位置信息
使用地理定位 API 相当简单。您请求一个位置,如果用户同意,浏览器将返回位置信息。位置由运行支持地理定位的浏览器的底层设备(例如,膝上型电脑或移动电话)提供给浏览器。位置信息作为一组纬度和经度坐标以及附加元数据提供。有了这些位置信息,您就可以构建一个引人注目的位置感知应用。
经纬度坐标
位置信息主要由一对纬度和经度坐标组成,如下例所示,显示了美丽的太浩城的坐标,该城位于太浩湖(美国最美丽的山湖)的岸边:
Latitude: 39.17222, Longitude: -120.13778
在前面的示例中,纬度(表示赤道以北或以南距离的数值为 39.17222)和经度(表示英格兰格林威治以东或以西距离的数值)为-120.13778。
纬度和经度坐标可以用不同的方式表示:
- 十进制格式(例如,39.17222)
- 度分秒(DMS)格式(例如,39° 10′20′)
注意当你使用地理定位 API 时,坐标总是以十进制格式返回。
除了纬度和经度坐标,地理定位总是提供位置坐标的精度。根据运行浏览器的设备,可能还会提供其他元数据。这些包括高度、高度精度、航向和速度。如果此附加元数据不可用,它将作为空值返回。
位置信息从哪里来?
地理定位 API 没有指定设备必须使用哪种底层技术来定位应用的用户。相反,它只是公开了一个用于检索位置信息的 API。然而,暴露出来的是精确定位的程度。不能保证设备的实际位置会返回准确的位置。
位置,位置
彼得说:“这是一个有趣的例子。在家里,我使用无线网络。我在 Firefox 中打开了本章中显示的地理定位示例应用,它计算出我在萨克拉门托(距离我的实际物理位置大约 75 英里)。错了,但不要太惊讶,因为我的互联网服务提供商位于萨克拉门托市中心。
然后,我让我的儿子 Sean 和 Rocky 在他们的 iPhones 上浏览相同的页面(使用相同的 Wi-Fi 网络)。在 Safari 中,它们看起来像是位于加利福尼亚州的马里斯维尔——一个距离萨克拉门托 30 英里的小镇。真不敢相信"
设备可以使用以下任何来源:
- 国际电脑互联网地址
- 坐标三角测量
- 全球定位系统
- 带有来自 RFID、Wi-Fi 和蓝牙的 MAC 地址的 Wi-Fi
- GSM 或 CDMA 手机 id
- 用户定义的
许多设备使用一个或多个信号源的组合来确保更高的精度。每种方法都有自己的优点和缺点,这将在下一节中解释。
IP 地址地理位置数据
在过去,基于 IP 地址的地理定位是获得可能位置的唯一方法,但返回的位置往往被证明是不可靠的。基于 IP 地址的地理定位的工作原理是自动查找用户的 IP 地址,然后检索注册人的物理地址。因此,如果您的 ISP 为您提供 IP 地址,您的位置通常会被解析为服务提供商的物理地址,而该地址可能在数英里之外。表 5-1 显示了基于 IP 地址的地理定位数据的优缺点。
许多网站基于 IP 地址位置做广告。当你到另一个国家旅行,突然看到当地服务的广告(基于你所访问的国家或地区的 IP 地址)时,你可以看到这一点。
GPS 地理定位数据
只要能看到天空,GPS 就能提供非常精确的定位结果。通过从围绕地球飞行的多个 GPS 卫星获取信号来获取 GPS 定位。然而,修复需要一段时间,这对于必须快速启动的应用来说不是特别好。
因为获取 GPS 定位可能需要很长时间,所以您可能希望异步查询用户的位置。要向应用的用户显示正在获取修复,您可以添加一个状态栏。表 5-2 显示了基于 GPS 的地理定位数据的优缺点。
无线地理定位数据
基于 Wi-Fi 的地理定位信息是通过根据用户与许多已知 Wi-Fi 接入点的距离对位置进行三角测量来获取的,这些接入点大多位于城市地区。与 GPS 不同,Wi-Fi 在室内和市区都非常准确。表 5-3 显示了基于 Wi-Fi 的地理定位数据的利与弊。
手机地理定位数据
基于手机的地理定位信息是通过基于用户与多个手机信号塔的距离对位置进行三角测量而获得的。这种方法提供了相当精确的一般定位结果。这种方法通常与基于 Wi-Fi 和 GPS 的地理定位信息结合使用。表 5-4 显示了基于手机的地理定位数据的利与弊。
用户定义的地理位置数据
您也可以允许用户自己定义他们的位置,而不是通过编程来确定用户的位置。一个应用可能允许用户输入他们的地址、邮政编码或其他一些细节;然后,您的应用可以使用这些信息来提供位置感知服务。表 5-5 显示了用户定义的地理位置数据的优缺点。
支持地理定位的浏览器
地理定位是第一批被完全接受和实现的 HTML5 特性之一,现在它在所有主流浏览器中都可用。有关当前浏览器支持的完整概述,包括移动支持,请参阅[
caniuse.com](http://caniuse.com)
并搜索地理位置。
如果您必须支持较旧的浏览器,那么在使用 API 之前,最好先看看是否支持地理定位。本章后面的“检查浏览器支持”一节将向您展示如何以编程方式检查浏览器支持。
隐私
地理位置规范要求提供一种机制来保护用户的隐私。此外,除非应用的用户明确许可,否则位置信息不应公开。
这很有意义,并且解决了用户经常提出的关于地理定位应用的“老大哥”问题。然而,正如您从 HTML 5 地理定位应用的一些可能的用例中所看到的,用户通常会有分享这些信息的动机。例如,用户可能会同意分享他们的位置,如果这可以让他们知道一双跑鞋有罕见的 50%折扣,而这双跑鞋就在离他们碰巧喝咖啡的地方几个街区远的商店里。让我们仔细看看图 5-1 所示的浏览器和设备隐私架构。
***图 5-1。*地理定位浏览器和设备隐私架构
图表中显示了以下步骤:
- 用户在浏览器中导航到位置感知应用。
- 应用网页通过进行地理定位功能调用从浏览器加载并请求坐标。浏览器拦截这一请求并请求用户许可。让我们假设,在这种情况下,许可被授予。
- 浏览器从运行它的设备上检索坐标信息。例如,IP 地址、Wi-Fi 和可能的 GPS 坐标的组合。这是浏览器的内部功能。
- 浏览器将这些坐标发送给可信的外部位置服务,后者返回位置坐标,现在可以将这些坐标发送回地理定位应用的主机。
重要应用是否而非直接访问设备;它只能查询浏览器来代表它访问设备。
触发隐私保护机制
当你访问一个使用地理定位 API 的网页时,隐私保护机制就会发挥作用。图 5-2 显示了这在 Firefox 中的样子。
***图 5-2。*当使用地理定位 API 时,Firefox 中的通知栏被触发。
当地理定位代码被执行时,该机制被触发。简单地添加不在任何地方调用的地理位置代码(例如,在一个onload
方法中)不会做任何事情。然而,如果地理位置代码被执行,例如,在对navigator.geolocation.getCurrentPosition
的调用中(稍后更详细地解释),用户被提示与应用共享他们的位置。图 5-3 显示了在 iPhone 上运行 Safari 时会发生什么。
***图 5-3。*使用地理定位 API 时,Safari 中会触发通知对话框。
除了提供必要的机制来请求共享你的位置,一些实现(例如 Firefox)还允许你在下次登录时记住授予该站点的权限。这类似于你在浏览器中记住某些网站的密码。
注意如果你已经允许在 Firefox 中始终向某个网站提供你的位置,但后来又改变了主意,你可以很容易地撤销这个许可,方法是返回该网站,从工具菜单中选择页面信息。然后在权限选项卡上更改共享位置的设置。
处理位置信息
位置数据是敏感信息,因此当您收到它时,必须小心处理、存储和重新传输数据。除非用户授予存储数据的权限,否则您应该始终在需要数据的任务完成后处置数据。
因此,如果您重新传输位置数据,建议您首先加密数据。关于地理位置数据的收集,您的应用应该突出显示以下内容:
- 你正在收集位置数据
- 您收集位置数据的原因
- 位置数据保留多长时间
- 您如何保护数据
- 位置数据如何共享以及与谁共享(如果共享)
- 用户如何检查和更新他们的位置数据
使用地理定位 API
在这一节中,我们将更详细地探索地理定位 API 的使用。为了便于说明,我们创建了一个简单的浏览器页面— geolocation.html
。记住,你可以从本书的页面apress.com
或配套网站[
prohtml5.com](http://prohtml5.com)
上下载所有代码。
检查浏览器支持
在调用地理定位 API 函数之前,您需要确保浏览器支持您将要做的事情。这样,您可以提供一些替代文本,提示您的应用的用户放弃他们的恐龙般的浏览器或安装一个插件,如 Gears,它增强了现有的浏览器功能。清单 5-1 显示了一种测试浏览器支持的方法。
***清单 5-1。*检查浏览器支持
`function loadDemo() {
if(navigator.geolocation) {
document.getElementById(“support”).innerHTML = “Geolocation supported.”;
} else {
document.getElementById(“support”).innerHTML = “Geolocation is not supported in
your browser.”;
}
}`
在这个例子中,您在loadDemo
函数中测试浏览器支持,这个函数可能在应用的页面加载时被调用。对navigator.geolocation
(也可以使用 Modernizr)的调用将返回地理位置对象(如果它存在的话),或者如果它不存在就触发失败案例。在这种情况下,通过用合适的消息更新页面上先前定义的support
元素,页面被更新以反映是否有浏览器支持。
位置请求
有两种类型的职位请求:
- 一次性位置请求
- 重复位置更新
一次性位置请求
在许多应用中,只检索一次用户的位置,或者只根据请求检索,这是可以接受的。例如,如果有人正在寻找最近的电影院,放映今天的热门电影,可以使用清单 5-2 中最简单的地理定位 API。
***清单 5-2。*一次性位置请求
void getCurrentPosition(in PositionCallback successCallback, in optional PositionErrorCallback errorCallback, in optional PositionOptions options);
让我们更详细地看看这个核心函数调用。
首先,这是一个在navigator.geolocation
对象上可用的函数,所以您需要已经在脚本中检索了这个对象。如前所述,如果您的浏览器不支持地理定位,请确保您有一个好的后备处理程序。
该函数有一个必需的参数和两个可选的参数。
successCallback
函数参数告诉浏览器,当位置数据可用时,您希望调用哪个函数。这一点很重要,因为提取位置数据等操作可能需要很长时间才能完成。没有用户希望在检索位置时浏览器被锁定,也没有开发人员希望他的程序无限期暂停——特别是因为获取位置数据通常要等待用户的许可。successCallback 是您接收实际位置信息并对其进行操作的地方。- 然而,与大多数编程场景一样,最好为失败情况做好计划。对于位置信息的请求很有可能因为超出您控制的原因而无法完成,对于这些情况,您将希望提供一个
errorCallback
函数,该函数可以向用户提供一个解释,或者尝试再试一次。虽然是可选的,但建议您提供一个。 - 最后,可以向地理定位服务提供一个
options
对象来微调它收集数据的方式。这是一个可选参数,我们将在后面进行研究。
假设您在我们的页面上创建了一个名为updateLocation()
的 JavaScript 函数,在这个函数中,您用新的位置数据更新页面的内容。类似地,您已经创建了一个handleLocationError()
函数来处理错误情况。接下来我们将研究这些函数的细节,但这意味着您访问用户位置的核心请求将如下所示:
navigator.geolocation.getCurrentPosition(updateLocation, handleLocationError);
update location()函数
那么,在我们的updateLocation()
通话中会发生什么呢?其实挺简单的。一旦浏览器访问到位置信息,它将调用带有单个参数的updateLocation()
:一个位置对象。位置将包含坐标(作为属性coords
)和收集位置数据时的时间戳。虽然您可能需要也可能不需要时间戳,但是coords
属性包含位置的关键值。
坐标上总是有多个属性,但是它们是否有有意义的值取决于浏览器和用户设备的硬件。以下是前三个属性:
latitude
longitude
accuracy
这些属性保证有值,并且是不言自明的。latitude
和longitude
将包含以十进制度数指定的用户位置的地理定位服务的最佳确定值。accuracy
将包含一个以米为单位的值,该值指定纬度和经度值与实际位置的接近程度,置信度为 95%。因此,它可用于显示位置周围的邻近半径,为人们提供关于精确度的视觉线索。由于地理定位实现的性质,近似将是常见和粗略的。在您有把握地呈现返回值之前,请确保检查它们的准确性。推荐用户去一家“附近”的鞋店,而这家鞋店实际上有几个小时的路程,这可能会产生意想不到的后果。
坐标的其他属性不能保证得到支持,但是如果它们不可用,它们将返回一个null
值(例如,如果您在台式计算机上,您不太可能访问这些信息):
altitude
—用户所在位置的高度,单位为米altitudeAccuracy
—再次以米为单位,如果没有提供高度,则为null
heading
—相对于正北的行进方向,单位为度speed
——地面速度,单位为米/秒
除非您确定您的用户拥有能够访问此类信息的设备,否则建议您不要依赖它们作为您的应用的关键。虽然全球定位设备可能提供这种级别的细节,但简单的网络三角测量无法提供。
现在让我们来看看我们的updateLocation()
函数的代码实现,它用坐标执行一些琐碎的更新(见清单 5-3 )。
***清单 5-3。*使用 updateLocation()函数的例子
`function updateLocation(position) {
var latitude = position.coords.latitude;
var longitude = position.coords.longitude;
var accuracy = position.coords.accuracy;
var timestamp = position.timestamp;
document.getElementById(“latitude”).innerHTML = latitude;
document.getElementById(“longitude”).innerHTML = longitude;
document.getElementById(“accuracy”).innerHTML = accuracy
document.getElementById(“timestamp”).innerHTML = timestamp;
}`
在这个例子中,updateLocation()
回调用于更新页面不同元素中的文本;我们将longitude
属性的值放在经度元素中,将latitude
属性放在纬度元素中,并将精确度和时间戳放在它们对应的字段中。
handleLocationError()函数
处理错误对于地理定位应用非常重要,因为有许多移动部件,因此位置计算服务有许多出错的可能性。幸运的是,API 为您需要处理的所有情况定义了错误代码,并将它们设置在作为code
属性传递给错误处理程序的错误对象上。让我们依次看看它们:
PERMISSION_DENIED
(错误代码 1)—用户选择不让浏览器访问位置信息。POSITION_UNAVAILABLE
(错误代码 2)—尝试了用于确定用户位置的技术,但失败了。TIMEOUT
(错误代码 3)—超时值被设置为一个选项,确定位置的尝试超过了该限制。
在这些情况下,您可能希望让用户知道有什么地方出错了。在请求不可用或超时的情况下,您可能希望重试获取值。
清单 5-4 展示了一个错误处理程序的例子。
***清单 5-4。*使用错误处理器
function handleLocationError(error) { switch(error.code){ case 0: updateStatus("There was an error while retrieving your location: " + error.message); break; case 1: updateStatus("The user prevented this page from retrieving a location."); break; case 2: updateStatus("The browser was unable to determine your location: " + error.message); break; case 3: updateStatus("The browser timed out before retrieving the location."); break; } }
错误代码是从提供的error
对象的code
属性中访问的,而message
属性将提供对错误的更详细描述。在所有情况下,我们调用自己的例程用必要的信息更新页面的状态。
可选地理定位请求属性
处理了正常情况和错误情况后,您应该将注意力转向可以传递给地理定位服务的三个可选属性,以便微调它收集数据的方式。请注意,这三个属性可以使用速记对象符号来传递,这使得将它们添加到地理位置请求调用变得很简单。
-
enableHighAccuracy
—This is a hint to the browser that, if available, you would like the Geolocation service to use a higher accuracy-detection mode. This defaults to false, but when turned on, it may not cause any difference, or it may cause the machine to take more time or power to determine location. Use with caution.注奇怪的是,高精度设定只有一个拨动开关:
true
或false
。创建 API 不是为了允许将精度设置为各种值或数值范围。也许这将在规范的未来版本中得到解决。 -
timeout
—此可选值以毫秒为单位,告知浏览器允许计算当前位置的最大时间。如果计算没有在这段时间内完成,则调用错误处理程序。该值默认为无穷大,即无限制。 -
maximumAge
—This value indicates how old a location value can be before the browser must attempt to recalculate. Again, it is a value in milliseconds. This value defaults to zero, meaning that the browser must attempt to recalculate a value immediately.注意你可能想知道
timeout
和maximumAge
选项之间的区别。虽然名字相似,但它们确实有不同的用途。timeout
值是指计算位置值所需的持续时间,而maximumAge
是指位置计算的频率。如果任何一次计算花费的时间超过timeout
值,就会触发错误。但是,如果浏览器没有比maximumAge
更早的最新位置值,它必须重新获取另一个值。特殊值在这里适用:将maximumAge
设置为“0”要求值总是被重新提取,而将其设置为Infinity
意味着它永远不会被重新提取。
地理位置 API 不允许您告诉浏览器重新计算位置的频率。这完全取决于浏览器的实现。我们所能做的就是告诉浏览器maximumAge
是它返回的值的什么。实际频率是我们无法控制的细节。
让我们使用简写符号更新我们的位置请求,以包含一个可选参数,如以下示例所示:
navigator.geolocation.getCurrentPosition(updateLocation,handleLocationError, {timeout:10000});
这个新的调用确保任何耗时超过 10 秒(10,000 毫秒)的定位请求都会触发一个错误,在这种情况下,将使用TIMEOUT
错误代码调用handleLocationError
函数。我们可以将我们到目前为止讨论过的地理定位调用组合起来,并在一个页面上显示相关数据,如图 5-4 所示。
***图 5-4。*移动设备上显示的地理位置数据
重复位置更新
有时你不得不反复提出职位要求。幸运的是,地理定位 API 的设计者使得从一次性请求用户位置的应用切换到定期请求位置的应用变得很容易。事实上,这很大程度上就像切换请求调用一样简单,如以下示例所示:
- 一次性更新:
navigator.geolocation.**getCurrentPosition**(updateLocation, handleLocationError);
- 重复更新:
navigator.geolocation.**watchPosition**(updateLocation, handleLocationError);
这个简单的改变将导致地理定位服务随着用户位置的改变而重复调用您的updateLocation
处理程序,而不是一次。它的作用就好像你的程序正在监视位置,并且会让你知道位置的变化。
你为什么想这么做?
考虑这样一个网页,当浏览者在城市中四处走动时,它会给出一个一个转弯的方向。或者是一个不断更新的页面,在您驾车行驶在高速公路上时向您显示最近的加油站。或者甚至是一个记录并发送你的位置的网页,这样你就可以追溯你的脚步。一旦位置更新在发生变化时流入您的应用,所有这些服务都变得易于构建。
关闭更新也很简单。如果您的应用不再需要接收关于用户位置的定期更新,您只需要调用clearWatch()
函数,如下例所示:
navigator.geolocation.clearWatch(watchId);
此功能将通知地理定位服务您不再希望接收用户位置的更新。但是watchID
是什么,它是从哪里来的?它实际上是来自watchPosition()
调用的返回值。它标识了唯一的监视器请求,以便我们稍后取消它。因此,如果你的应用需要停止接收位置更新,你可以写一些代码,如清单 5-5 所示。
***清单 5-5。*使用观察位置
`var watchId = navigator.geolocation.watchPosition(updateLocation,
handleLocationError);
// do something fun with the location updates!
// OK, now we are ready to stop receiving location updates
navigator.geolocation.clearWatch(watchId);`
构建地理定位应用
到目前为止,我们主要关注单次定位请求。让我们通过使用它的 multirequest 特性来构建一个小而有用的应用:一个带有距离跟踪器的网页,来看看地理定位 API 到底有多强大。
如果你曾经想要一个快速的方法来确定你在一定时间内走了多远,你通常会使用一个专用的设备,如 GPS 导航系统或计步器。使用地理定位服务的强大功能,您可以创建一个网页来跟踪您从最初加载页面的位置移动了多远。尽管在台式电脑上用处不大,但这个页面对于今天数百万带有地理定位支持的网络电话来说是理想的。只需将您的智能手机浏览器指向该示例页面,授予该页面访问您的位置的权限,每隔几秒钟它就会更新您刚刚行驶的距离,并将其添加到累计里程中(参见图 5-5 )。
***图 5-5。*我们的地理定位应用实例
这个示例通过使用我们在上一节中讨论的watchPosition()
功能来工作。每次有新的位置发送给我们,我们都会将其与最后一个已知位置进行比较,并计算行进的距离。这是通过一个众所周知的计算方法来实现的,即哈弗辛公式,它允许我们计算球体上两个经度和纬度位置之间的距离。清单 5-6 展示了哈弗辛公式告诉我们的东西。
***清单 5-6。*哈弗辛公式
如果你希望了解哈弗辛公式是如何工作的,你会非常失望。相反,我们将向您展示该公式的 JavaScript 实现,它允许任何人使用它来计算两个位置之间的距离(参见清单 5-7 )。
***清单 5-7。*一个 JavaScript 哈弗辛实现
` Number.prototype.toRadians = function() {
return this * Math.PI / 180;
}
function distance(latitude1, longitude1, latitude2, longitude2) {
// R is the radius of the earth in kilometers
var R = 6371;
var deltaLatitude = (latitude2-latitude1).toRadians();
var deltaLongitude = (longitude2-longitude1).toRadians();
latitude1 = latitude1.toRadians(), latitude2 = latitude2.toRadians();
var a = Math.sin(deltaLatitude/2) *
Math.sin(deltaLatitude/2) +
Math.cos(latitude1) *
Math.cos(latitude2) *
Math.sin(deltaLongitude/2) *
Math.sin(deltaLongitude/2);
var c = 2 * Math.atan2(Math.sqrt(a),
Math.sqrt(1-a));
var d = R * c;
return d;
}`
如果你想知道这个公式为什么或如何工作,请查阅青少年的数学教科书。出于我们的目的,我们编写了一个从角度到弧度的转换,并提供了一个distance()
函数来计算两个纬度和经度位置值之间的距离。
如果我们检查用户的位置,并以频繁和有规律的时间间隔计算行进的距离,它会给出一个随着时间推移行进的距离的合理近似值。这假设用户在每个时间间隔都在直线运动,但是为了我们的例子,我们将做这样的假设。
编写 HTML 显示
让我们从 HTML 显示开始。在这个练习中,我们保持它非常简单,因为真正感兴趣的是驱动数据的脚本。我们会显示一个包含相关地理位置数据的页面。此外,我们将在适当的位置放置一些状态文本指示器,以便用户可以看到行进距离的摘要(参见清单 5-8 )。
***清单 5-8。*距离跟踪器 HTML 页面的代码
`
Odometer Demo
Live Race Data!
Your Location
Geolocation is not supported in your browser.
Latitude:
Longitude:
Accuracy:
Timestamp:
Current distance traveled:
Total distance traveled:
.
.
.
这些值目前都是默认的,一旦数据开始流入应用,就会填充这些值。
处理地理位置数据
我们的第一个 JavaScript 代码部分应该看起来很熟悉。我们已经设置了一个处理程序——loadDemo()
——它将在页面完成加载后立即执行。该脚本将检测浏览器中是否支持地理定位,并使用状态更新功能来更改页面顶部的状态消息,以指示找到的内容。然后它会请求监视用户的位置,如清单 5-9 中的所示。
***清单 5-9。*增加 loadDemo()和状态更新功能
` var totalDistance = 0.0;
var lastLat;
var lastLong;
function updateErrorStatus(message) {
document.getElementById(“status”).style.background = “papayaWhip”;
document.getElementById(“status”).innerHTML = "Error: " + message;
}
function updateStatus(message) {
document.getElementById(“status”).style.background = “paleGreen”;
document.getElementById(“status”).innerHTML = message;
}
function loadDemo() {
if(navigator.geolocation) {
document.getElementById(“status”).innerHTML = “HTML5 Geolocation is supported in your browser.”;
navigator.geolocation.watchPosition(updateLocation, handleLocationError,
{timeout:20000});
}
}`
请注意,我们在我们的位置监视上设置了一个maximumAge
选项:{maximumAge:20000}
。这将告诉位置服务,我们不想要任何超过 20 秒(或 20,000 毫秒)的缓存位置值。设置这个选项将使我们的页面定期更新,但是您可以随意调整这个数字,尝试更大或更小的缓存大小。
对于错误处理,我们将使用我们之前确定的相同例程,因为它对于我们的距离跟踪器来说足够通用。在其中,我们将检查收到的任何错误的错误代码,并相应地更新页面上的状态消息,如清单 5-10 所示。
***清单 5-10。*添加错误处理代码
function handleLocationError(error) { switch(error.code) { case 0: updateErrorStatus("There was an error while retrieving your location. Additional details: " + error.message); break; case 1: updateErrorStatus("The user opted not to share his or her location."); break; case 2: updateErrorStatus("The browser was unable to determine your location. Additional details: " + error.message); break; case 3: updateErrorStatus("The browser timed out before retrieving the location."); break; } }
我们的大部分工作将在我们的updateLocation()
函数中完成。这里我们将使用最近的值更新页面,并计算行进的距离,如清单 5-11 所示。
***清单 5-11。*添加 updateLocation()函数
` function updateLocation(position) {
var latitude = position.coords.latitude;
var longitude = position.coords.longitude;
var accuracy = position.coords.accuracy;
var timestamp = position.timestamp;
document.getElementById(“latitude”).innerHTML = "Latitude: " + latitude;
document.getElementById(“longitude”).innerHTML = "Longitude: " + longitude;
document.getElementById(“accuracy”).innerHTML = “Accuracy: " + accuracy + " meters”;
document.getElementById(“timestamp”).innerHTML = "Timestamp: " + timestamp;`
如您所料,当我们收到一组更新的位置坐标时,我们要做的第一件事就是记录所有信息。我们收集纬度、经度、精确度和时间戳,然后用新数据更新表值。
您可能不会选择在自己的应用中显示时间戳。这里使用的时间戳主要是对计算机有用的形式,对最终用户没有意义。你可以随意用一个更方便用户的时间指示器来代替它,或者干脆把它去掉。
精度值是以米为单位给我们的,乍一看似乎没有必要。但是,任何数据都取决于它的准确性。即使您没有向用户提供精度值,您也应该在自己的代码中考虑它们。显示不准确的值可能会让用户对他或她的位置产生误解。因此,我们将丢弃任何不合理的低精度位置更新,如清单 5-12 所示。
***清单 5-12。*忽略不准确的精度更新
// sanity test... don't calculate distance if accuracy // value too large if (accuracy >= 30000) { updateStatus("Need more accurate values to calculate distance."); return; }
最简单的旅行方式
Brian 说:“保持位置准确性至关重要。作为一名开发人员,您将无法访问浏览器用来计算位置的方法,但是您可以访问精确度属性。用它!
一个慵懒的下午,我坐在后院的吊床上,通过一个支持地理定位的手机浏览器监控自己的位置。我惊讶地发现,仅仅过了几分钟,据报道我倾斜的身体以不同的速度行进了半公里的距离。尽管这听起来令人兴奋,但它提醒我们,数据只有在来源允许的情况下才是准确的。"
最后,我们将计算行进的距离,假设我们之前已经接收了至少一个准确的位置值。我们将更新旅行距离的总和并显示给用户,我们将存储当前值以备将来比较。为了让我们的界面不那么混乱,对计算值进行舍入或截断是个好主意,如清单 5-13 所示。
***清单 5-13。*添加距离计算代码
` // calculate distance
if ((lastLat != null) && (lastLong != null)) {
var currentDistance = distance(latitude, longitude, lastLat, lastLong);
document.getElementById(“currDist”).innerHTML =
“Current distance traveled: " + currentDistance.toFixed(2) + " km”;
totalDistance += currentDistance;
document.getElementById(“totalDist”).innerHTML =
“Total distance traveled: " + currentDistance.toFixed(2) + " km”;
updateStatus(“Location successfully updated.”);
}
lastLat = latitude;
lastLong = longitude;
}`
就这样。在不到 200 行的 HTML 和脚本中,我们创建了一个示例应用,该应用可以随时监控查看者的位置,并演示了几乎整个地理定位 API,包括错误处理。虽然这个例子在台式电脑上看起来没那么有趣,但在你最喜欢的支持地理定位的手机或设备上试试,看看你在一天中的移动性。
最终代码
完整的代码示例如清单 5-14 所示。
***清单 5-14。*完成距离追踪器代码
`
Odometer Demo
Live Race Data!
Your Location
Geolocation is not supported in your browser.
Latitude:
Longitude:
Accuracy:
Timestamp:
Current distance traveled:
Total distance traveled:
Number.prototype.toRadians = function() {
return this * Math.PI / 180;
}
function distance(latitude1, longitude1, latitude2, longitude2) {
// R is the radius of the earth in kilometers
var R = 6371;
var deltaLatitude = (latitude2-latitude1).toRadians();
var deltaLongitude = (longitude2-longitude1).toRadians();
latitude1 = latitude1.toRadians(), latitude2 = latitude2.toRadians();
var a = Math.sin(deltaLatitude/2) *
Math.sin(deltaLatitude/2) +
Math.cos(latitude1) *
Math.cos(latitude2) *
Math.sin(deltaLongitude/2) *
Math.sin(deltaLongitude/2);
var c = 2 * Math.atan2(Math.sqrt(a),
Math.sqrt(1-a));
var d = R * c;
return d;
}
function updateErrorStatus(message) {
document.getElementById(“status”).style.background = “papayaWhip”;
document.getElementById(“status”).innerHTML = "Error: " + message;
}
function updateStatus(message) {
document.getElementById(“status”).style.background = “paleGreen”;
document.getElementById(“status”).innerHTML = message;
}
function loadDemo() {
if(navigator.geolocation) {
document.getElementById(“status”).innerHTML = “HTML5 Geolocation is supported in your
browser.”;
navigator.geolocation.watchPosition(updateLocation, handleLocationError,
{timeout:10000});
}
}
function updateLocation(position) {
var latitude = position.coords.latitude;
var longitude = position.coords.longitude;
var accuracy = position.coords.accuracy;
var timestamp = position.timestamp;
document.getElementById(“latitude”).innerHTML = "Latitude: " + latitude;
document.getElementById(“longitude”).innerHTML = "Longitude: " + longitude;
document.getElementById(“accuracy”).innerHTML = “Accuracy: " + accuracy + " meters”;
document.getElementById(“timestamp”).innerHTML = "Timestamp: " + timestamp;
// sanity test… don’t calculate distance if accuracy
// value too large
if (accuracy >= 30000) {
updateStatus(“Need more accurate values to calculate distance.”);
return;
}
// calculate distance
if ((lastLat != null) && (lastLong != null)) {
var currentDistance = distance(latitude, longitude, lastLat, lastLong);
document.getElementById(“currDist”).innerHTML =
“Current distance traveled: " + currentDistance.toFixed(2) + " km”;
totalDistance += currentDistance;
document.getElementById(“totalDist”).innerHTML =
“Total distance traveled: " + currentDistance.toFixed(2) + " km”;
updateStatus(“Location successfully updated.”);
}
lastLat = latitude;
lastLong = longitude;
}
function handleLocationError(error) {
switch(error.code)
{
case 0:
updateErrorStatus("There was an error while retrieving your location. Additional
details: " + error.message);
break;
case 1:
updateErrorStatus(“The user opted not to share his or her location.”);
break;
case 2:
updateErrorStatus("The browser was unable to determine your location. Additional
details: " + error.message);
break;
case 3:
updateErrorStatus(“The browser timed out before retrieving the location.”);
break;
}
}
实用的临时演员
有时有些技术不适合我们的常规例子,但仍然适用于许多类型的 HTML5 应用。我们在这里向你展示一些简短的、普通的、实用的额外内容。
我的状态如何?
您可能已经注意到,地理定位 API 的很大一部分与时间值有关。这不应该太令人惊讶。众所周知,确定位置的技术——手机三角定位、GPS、IP 查找等——即使能完成,也要花很长时间。幸运的是,API 为开发人员提供了足够的信息来为用户创建合理的状态栏。
如果开发人员在位置查找上设置了可选的timeout
值,那么如果查找时间超过了timeout
值,她就请求地理定位服务通知她一个错误。这样做的副作用是,当请求正在进行时,在用户界面中向用户显示状态消息是完全合理的。状态的开始从请求发出时开始,状态的结束应该对应于超时值,不管它是以成功还是失败结束。
在清单 5-15 中,我们将启动一个 JavaScript 间隔计时器,用一个新的进度指示器值定期更新状态显示。
***清单 5-15。*添加状态栏
`function updateStatus(message) {
document.getElementById(“status”).innerHTML = message;
}
function endRequest() {
updateStatus(“Done.”);
}
function updateLocation(position) {
endRequest();
// handle the position data
}
function handleLocationError(error) {
endRequest();
// handle any errors
}
navigator.geolocation.getCurrentPosition(updateLocation,
handleLocationError,
{timeout:10000});
// 10 second timeout value
updateStatus(“Requesting Geolocation data…”);`
让我们稍微分析一下这个例子。和以前一样,我们有一个函数来更新页面上的状态值,如下面的例子所示。
function updateStatus(message) { document.getElementById("status").innerHTML = message; }
我们这里的状态将是一个简单的文本显示,尽管这种方法同样适用于更引人注目的图形状态显示(见清单 5-16 )。
***清单 5-16。*显示状态
`navigator.geolocation.getCurrentPosition(updateLocation,
handleLocationError,
{timeout:10000});
// 10 second timeout value
updateStatus(“Requesting location data…”);`
我们再次使用地理定位 API 来获取用户的当前位置,但是设置了 10 秒的超时。一旦过了十秒钟,由于超时选项,我们应该要么成功,要么失败。
我们立即更新状态文本显示,以表明位置请求正在进行中。然后,一旦请求完成或者过了十秒钟——无论哪一个先发生——就使用回调方法来重置状态文本,如清单 5-17 所示。
***清单 5-17。*重置状态文本
` function endRequest() {
updateStatus(“Done.”);
}
function updateLocation(position) {
endRequest();
// handle the position data
}`
一个简单的额外,但易于扩展。
这种技术适用于一次性位置查找,因为开发人员很容易确定位置查找请求何时开始。当然,开发人员一调用getCurrentPosition()
,请求就开始了。然而,在通过watchPosition()
重复查找位置的情况下,开发者不能控制每个单独的位置请求何时开始。
此外,直到用户准许地理定位服务访问位置数据,超时才开始。由于这个原因,实现精确的状态显示是不切实际的,因为在用户授予权限的瞬间页面不会得到通知。
在谷歌地图上显示给我看
对地理位置数据的一个非常常见的请求是在地图上显示用户的位置,例如流行的 Google Maps 服务。事实上,这是如此受欢迎,以至于谷歌自己在其用户界面中内置了对地理定位的支持。只需按下显示我的位置按钮(参见图 5-6);Google Maps 将使用地理定位 API(如果可用)来确定并在地图上显示您的位置。
***图 5-6。*谷歌地图的显示我的位置按钮
但是,你自己也有可能做到这一点。尽管 Google Map API 超出了本书的范围,但它(并非巧合)被设计成可以获取十进制的纬度和经度位置。因此,您可以轻松地将位置查找的结果传递给 Google Map API,如清单 5-18 所示。你可以在开始谷歌地图应用,第二版(2010 年出版)中读到更多关于这个主题的内容。
***清单 5-18。*向谷歌地图 API 传递位置
`//Include the Google maps library
// Create a Google Map… see Google API for more detail
var map = new google.maps.Map(document.getElementById(“map”));
function updateLocation(position) {
//pass the position to the Google Map and center it
map.setCenter(new google.maps.LatLng(
parseFloat(position.coords.latitude),
parseFloat(position.coords.longitude));
navigator.geolocation.getCurrentPosition(updateLocation,
handleLocationError);`
总结
本章讨论了地理定位。您了解了地理位置信息(纬度、经度和其他属性)以及它们的来源。您还了解了伴随地理定位而来的隐私问题,并且看到了如何使用地理定位 API 来创建引人注目的位置感知 web 应用。
在下一章,我们将演示 HTML5 如何让你在标签页和窗口之间以及页面和不同域的服务器之间进行通信。
六、使用通信 API
在这一章中,我们将探索如何使用两个重要的实时跨源通信构件:跨文档消息传递和 XMLHttpRequest Level 2 ,我们将向您展示如何使用它们来创建引人注目的应用。这两个构建块都为 HTML5 应用添加了新的通信选项,并允许来自不同域的应用安全地相互通信。
首先,我们将讨论postMessage
API 和 origin 安全概念 HTML5 通信的两个关键元素——然后我们将向您展示如何使用postMessage
API 在 iframes、选项卡和窗口之间进行通信。
接下来,我们将讨论 XMLHttpRequest 级别 2——XMLHttpRequest 的改进版本。我们将向您展示 XMLHttpRequest 在哪些方面得到了改进。具体来说,我们将向您展示如何使用 XMLHttpRequest 进行跨源请求,以及如何使用新的进度事件。
跨文档消息传递
直到最近,由于安全考虑,在运行的浏览器中,框架、标签和窗口之间的通信完全受到限制。例如,虽然某些网站从浏览器内部共享信息可能很方便,但这也为恶意攻击打开了方便之门。如果浏览器被授予以编程方式访问加载到其他框架和标签中的内容的能力,网站将能够使用脚本从另一个网站的内容中窃取任何信息。明智的是,浏览器供应商限制了这种访问;试图检索或修改从另一个源加载的内容会引发安全异常并阻止该操作。
然而,在一些合理的情况下,不同网站的内容可以在浏览器内部进行交流。典型的例子是“mashup”,一个不同应用的组合,比如来自不同站点的地图、聊天和新闻,所有这些组合在一起形成一个新的元应用。在这些情况下,一组协调良好的应用将由浏览器内部的直接通信通道提供服务。
为了满足这种需求,浏览器供应商和标准机构同意引入一个新特性:跨文档消息传递。跨文档消息传递支持跨 iframes、选项卡和窗口的安全跨源通信。它将postMessage
API 定义为发送消息的标准方式。如下例所示,用postMessage
API 发送消息非常简单。
chatFrame.contentWindow.postMessage('Hello, world', 'http://www.example.com/');
要接收消息,只需在页面中添加一个事件处理程序。当消息到达时,您可以检查其来源,并决定是否对该消息进行处理。清单 6-1 显示了一个事件监听器,它将消息传递给一个 messageHandler 函数。
***清单 6-1。*消息事件的事件监听器
window.addEventListener(“message”, messageHandler, true); function messageHandler(e) { switch(e.origin) { case “friend.example.com”: // process message processMessage(e.data); break; default: // message origin not recognized // ignoring message } }
消息事件是具有data
和origin
属性的 DOM 事件。data
属性是发送者传递的实际消息,而origin
属性是发送者的来源。使用origin
属性,接收方很容易忽略来自不可信来源的消息;可以简单地对照允许的来源列表来检查来源。
如图 6-1 中的所示,postMessage
API 提供了一种在[
chat.example.net](http://chat.example.net)
托管的聊天窗口小部件 iframe 和包含[
portal.example.com](http://portal.example.com)
托管的聊天窗口小部件 iframe 的 HTML 页面(两个不同的来源)之间进行通信的方法。
***图 6-1。*iframe 和主 HTML 页面之间的邮件通信
在本例中,聊天小部件包含在另一个源的 iframe 中,因此它不能直接访问父窗口。当聊天小部件接收到聊天消息时,它可以使用postMessage
向主页面发送消息,这样页面就可以提醒聊天小部件的用户收到了新消息。类似地,页面可以向聊天小部件发送关于用户状态的消息。页面和小部件都可以通过将各自的来源添加到允许来源的白名单中来侦听来自彼此的消息。
图 6-2 显示了使用 postMessage API 的实际例子。这是一个名为 DZSlides 的 HTML5 幻灯片查看器应用,由 Firefox 工程师兼 HTML5 传道者 Paul Rouget ( [
paulrouget.com/dzslides](http://paulrouget.com/dzslides)
)构建。在这个应用中,表示及其容器使用 postMessage API 进行通信。
***图 6-2。*dz slides 应用中 postMessage API 的实际使用
在引入postMessage
之前,iframes 之间的通信有时可以通过直接编写脚本来完成。在一个页面中运行的脚本会试图操作另一个文档。由于安全限制,这可能是不允许的。与直接编程访问不同,postMessage
提供了 JavaScript 上下文之间的异步消息传递。如图图 6-3 所示,如果没有postMessage
,跨源通信会导致安全错误,由浏览器强制执行以防止跨站脚本攻击。
***图 6-3。*火狐和 Firebug 早期版本的跨站点脚本错误
postMessage
API 可用于同源文档之间的通信,但是当通信可能被浏览器强制执行的同域策略禁止时,它特别有用。然而,也有理由使用postMessage
在同源文档之间传递消息,因为它提供了一致的、易于使用的 API。每当 JavaScript 上下文之间有通信时,就会使用postMessage
API,比如 HTML5 Web 工作器。
了解原产地安全
HTML5 通过引入来源的概念来澄清和细化域安全性。源是用于在网络上建模信任关系的地址的子集。Origins 由一个方案、一个主机和一个端口组成。例如,[
www.example.com](https://www.example.com)
处的页面与[
www.example.com](http://www.example.com)
处的页面具有不同的来源,因为方案不同(https
与http
)。原点值中不考虑路径,所以在[
www.example.com/index.html](http://www.example.com/index.html)
的页面与在[
www.example.com/page2.html](http://www.example.com/page2.html)
的页面具有相同的原点,因为只有路径不同。
HTML5 定义了起源的序列化。在字符串形式中,源可以在 API 和协议中引用。这对于使用 XMLHttpRequest 的跨源 HTTP 请求以及 WebSockets 来说是非常重要的。
跨来源通信通过来源识别发送者。这允许接收方忽略来自它不信任或不期望从其接收消息的来源的消息。此外,应用必须通过为消息事件添加事件侦听器来选择接收消息。因此,不存在消息干扰未受怀疑的应用的风险。
postMessage
的安全规则确保消息不会被发送到来源不期望的页面。发送消息时,发送方指定接收方的来源。如果发送者调用 postMessage 的窗口没有那个特定的来源(例如,如果用户已经导航到另一个站点),浏览器将不会传输那个消息。
同样,在接收消息时,发送者的来源也包含在消息中。消息的来源是由浏览器提供的,不能被欺骗。这允许接收方决定处理哪些消息,忽略哪些消息。您可以保留一个白名单,只处理来自来源可信的文档的邮件。
小心外部输入
Frank 说:“处理跨来源消息的应用应该总是验证每条消息的来源。此外,应该谨慎对待消息数据。即使一条消息来自一个可信的来源,它也应该像其他任何外部输入一样被小心对待。下面两个例子展示了一种注入内容的方法,这种方法可能会带来麻烦,同时也是一种更安全的替代方法。
`// Dangerous: e.data is evaluated as markup!
element.innerHTML = e.data;
// Better
element.textContent = e.data;`
作为最佳实践,从不评估来自第三方的字符串。此外,避免对来自您自己的应用的字符串使用eval
。相反,您可以在 window 中使用 JSON。JSON 或 json.org 解析器。JSON 是一种数据语言,旨在供 JavaScript 安全使用,而 json.org 解析器被设计成偏执型的。"
浏览器支持跨文档信息传递
所有主流浏览器,包括 Internet Explorer 8 和更高版本,都支持 postMessage API。在使用 HTML5 跨文档消息传递之前,最好先测试一下它是否受支持。本章后面的“检查浏览器支持”一节将向您展示如何以编程方式检查浏览器支持
使用 postMessage API
在这一节中,我们将更详细地探索 HTML5 postMessage
API 的使用。
检查浏览器支持
在调用postMessage
之前,最好检查一下浏览器是否支持它。以下示例显示了检查postMessage
支持的一种方法:
if (typeof window.postMessage === “undefined”) { // postMessage not supported in this browser }
发送消息
要发送消息,调用目标窗口对象上的postMessage
,如下例所示:
window.postMessage(“Hello, world”, “portal.example.com”);
第一个参数包含要发送的数据,第二个参数包含预期的目标。要向 iframe 发送消息,可以在 iframe 的 contentWindow 上调用postMessage
,如下例所示:
document.getElementsByTagName(“iframe”)[0].contentWindow.postMessage(“Hello, world”, “chat.example.net”);
监听消息事件
脚本通过监听窗口对象上的事件来接收消息,如清单 6-2 中的所示。在事件监听器函数中,接收应用可以决定接受或忽略消息。
***清单 6-2。*监听消息事件并将来源与白名单进行比较
`var originWhiteList = [“portal.example.com”, “games.example.com”, “www.example.com”];
function checkWhiteList(origin) {
for (var i=0; i<originWhiteList.length; i++) {
if (origin === originWhiteList[i]) {
return true;
}
}
return false;
}
function messageHandler(e) {
if(checkWhiteList(e.origin)) {
processMessage(e.data);
} else {
// ignore messages from unrecognized origins
}
}
window.addEventListener(“message”, messageHandler, true);`
注意html 5 定义的 MessageEvent 接口也是 HTML5 WebSockets 和 HTML5 Web 工作器 的一部分。HTML5 的通信特性有一致的接收消息的 API。其他通信 API,如 EventSource API 和 Web 工作器,也使用 MessageEvent 来传递消息。
使用 postMessage API 构建应用
假设您想要构建前面提到的带有跨来源聊天小部件的门户应用。您可以使用跨文档消息在门户页面和聊天小部件之间进行通信,如图图 6-4 所示。
***图 6-4。*带有跨来源聊天工具 iframe 的门户页面
在这个例子中,我们展示了门户如何在 iframes 中嵌入来自第三方的小部件。我们的例子展示了来自 chat.example.net 的一个小部件。然后,门户页面和小部件使用postMessage
进行通信。在这种情况下,iframe 表示一个聊天小部件,它希望通过闪烁标题文本来通知用户。这是在后台接收事件的应用中常见的 UI 技术。但是,因为小部件被隔离在 iframe 中,而 iframe 的来源不同于父页面,所以更改标题会违反安全性。相反,小部件使用postMessage
请求父页面代表它执行通知。
示例门户还向 iframe 发送消息,通知小部件用户已经更改了他或她的状态。以这种方式使用postMessage
允许这样的门户与组合应用中的小部件协调。当然,因为在发送消息时检查目标来源,在接收消息时检查事件来源,所以不存在数据意外泄露或被欺骗的可能性。
注意在这个示例应用中,聊天小部件没有连接到实时聊天系统,通知是由应用的用户点击发送通知来驱动的。一个有效的聊天应用可以使用 Web 套接字,如第七章中所述。
为了便于说明,我们创建了几个简单的 HTML 页面:postMessagePortal.html
和postMessageWidget.html.
下面的步骤强调了构建门户页面和聊天小部件页面的重要部分。以下示例的示例代码位于code/communication
文件夹中。
构建门户页面
首先,添加位于不同原点的聊天小部件 iframe:
<iframe id="widget" src="http://chat.example.net:9999/postMessageWidget.html"></iframe>
接下来,添加一个事件监听器 messageHandler 来监听来自聊天小部件的消息事件。如下面的示例代码所示,小部件将要求门户通知用户,这可以通过闪烁标题来完成。为了确保消息来自聊天小部件,消息的来源被验证;如果它不是来自于[
chat.example.net:9999](http://chat.example.net:9999)
,门户页面就会忽略它。
`var trustedOrigin = “http://chat.example.net:9999”;
function messageHandler(e) {
if (e.origin == trustedOrigin) {
notify(e.data);
} else {
// ignore messages from other origins
}
}`
接下来,添加一个与聊天小部件通信的函数。它使用postMessage
向门户页面中包含的小部件 iframe 发送状态更新。在实时聊天应用中,它可以用来传达用户的状态(在线、离开等)。
function sendString(s) { document.getElementById("widget").contentWindow.postMessage(s, targetOrigin); }
构建聊天小部件页面
首先,添加一个事件监听器 messageHandler 来监听来自门户页面的消息事件。如下面的示例代码所示,聊天小部件监听传入的状态更改消息。为了确保消息来自门户页面,验证消息的来源;如果它不是来自[
portal.example.com:9999](http://portal.example.com:9999)
,小工具就简单地忽略它。:
var trustedOrigin = "http://portal.example.com:9999"; function messageHandler(e) { if (e.origin === trustedOrigin { document.getElementById("status").textContent = e.data; } else { // ignore messages from other origins }
}
接下来,添加一个与门户页面通信的函数。小部件将要求门户代表它通知用户,并在收到新的聊天消息时使用postMessage
向门户页面发送消息,如下例所示:
function sendString(s) { window.top.postMessage(s, trustedOrigin); }
最终代码
清单 6-3 显示了门户页面 postMessagePortal.html 的完整代码。
清单 6-3。【postMessagePortal.html 内容
`
var defaultTitle = “Portal [http://portal.example.com:9999]”;
var notificationTimer = null;
var trustedOrigin = “http://chat.example.net:9999”;
function messageHandler(e) {
if (e.origin == trustedOrigin) {
notify(e.data);
} else {
// ignore messages from other origins
}
}
function sendString(s) {
document.getElementById(“widget”).contentWindow.postMessage(s, trustedOrigin);
}
function notify(message) {
stopBlinking();
blinkTitle(message, defaultTitle);
}`
`function stopBlinking() {
if (notificationTimer !== null) {
clearTimeout(notificationTimer);
}
document.title = defaultTitle;
}
function blinkTitle(m1, m2) {
document.title = m1;
notificationTimer = setTimeout(blinkTitle, 1000, m2, m1)
}
function sendStatus() {
var statusText = document.getElementById(“statusText”).value;
sendString(statusText);
}
function loadDemo() {
document.getElementById(“sendButton”).addEventListener(“click”, sendStatus, true);
document.getElementById(“stopButton”).addEventListener(“click”, stopBlinking, true);
sendStatus();
}
window.addEventListener(“load”, loadDemo, true);
window.addEventListener(“message”, messageHandler, true);
Cross-Origin Portal
Origin: http://portal.example.com:9999
Status Change StatusThis uses postMessage to send a status update to the widget iframe contained in the portal page.
Stop Blinking Title
`清单 6-4 显示了门户页面 postMessageWidget.html 的代码。
清单 6-4。【postMessageWidget.html 内容
`
var trustedOrigin = “http://portal.example.com:9999”;`
`function messageHandler(e) {
if (e.origin === “http://portal.example.com:9999”) {
document.getElementById(“status”).textContent = e.data;
} else {
// ignore messages from other origins
}
}
function sendString(s) {
window.top.postMessage(s, trustedOrigin);
}
function loadDemo() {
document.getElementById(“actionButton”).addEventListener(“click”,
function() {
var messageText = document.getElementById(“messageText”).value;
sendString(messageText);
}, true);
}
window.addEventListener(“load”, loadDemo, true);
window.addEventListener(“message”, messageHandler, true);
Widget iframe
Origin: http://chat.example.net:9999
Status set to: by containing portal.
This will ask the portal to notify the user. The portal does this by flashing the title. If the message comes from an origin other than http://chat.example.net:9999, the portal page will ignore it.
`实际应用
要查看这个例子,有两个先决条件:页面必须由 web 服务器提供,页面必须由两个不同的域提供。如果您可以访问不同域上的多个 web 服务器(例如,两个 Apache HTTP 服务器),那么您可以在这些服务器上托管示例文件并运行演示。在本地机器上完成这项工作的另一种方法是使用 Python SimpleHTTPServer
,如下面的步骤所示。
-
Update the path to the Windows hosts file (
C:\Windows\system32\drivers\etc\hosts
) and the Linux version (/etc/hosts
) by adding two entries pointing to your localhost (IP address 127.0.0.1), as shown in the following example:127.0.0.1 chat.example.net 127.0.0.1 portal.example.com
注意修改主机文件后,您必须重启浏览器,以确保 DNS 条目生效。
-
安装 Python 2,它包括轻量级的 SimpleHTTPServer web 服务器。
-
导航到包含两个示例文件
(postMessageParent.html and postMessageWidget.html)
的目录。 -
启动 Python 如下:
python -m SimpleHTTPServer 9999
-
打开浏览器并导航至
[
portal.example.com:9999/postMessagePortal.html](http://portal.example.com:9999/postMessagePortal.html)
。你现在应该看到图 6-4 中的页面。
XMLHttpRequest 级别 2
XMLHttpRequest 是使 Ajax 成为可能的 API。有很多关于 XMLHttpRequest 和 Ajax 的书。你可以在 John Resig 的Pro JavaScript Techniques(a press,2006)中阅读更多关于 XMLHttpRequest 编程的内容。
XMLHttpRequest Level 2—XMLHttpRequest 的新版本—得到了显著增强。在本章中,我们将介绍 XMLHttpRequest Level 2 中引入的改进。这些改进集中在以下几个方面:
- 跨源 XMLHttpRequests
- 进度事件
- 二进制数据
跨源 XMLHttpRequest
在过去,XMLHttpRequest 仅限于同源通信。XMLHttpRequest Level 2 允许使用跨来源资源共享(CORS)的跨来源 XMLHttpRequest,它使用了前面的跨文档消息传递部分中讨论的来源概念。
跨源 HTTP 请求有一个源头。这个头向服务器提供请求的来源。该标头受浏览器保护,不能从应用代码中更改。本质上,它是跨文档消息传递中使用的消息事件上的 origin 属性的网络等价物。origin 标头不同于旧的 referer [ 原文为 ]标头,因为 referer 是包含路径的完整 URL。因为路径可能包含敏感信息,所以试图保护用户隐私的浏览器有时不会发送 referer。但是,在必要的时候,浏览器总是会发送所需的Origin
头。
使用跨源 XMLHttpRequest,您可以构建使用不同源上托管的服务的 web 应用。例如,如果您想要托管一个 web 应用,该应用使用来自一个来源的静态内容和来自另一个来源的 Ajax 服务,那么您可以使用跨来源 XMLHttpRequest 在两者之间进行通信。如果没有跨源 XMLHttpRequest,您将被限制在同源通信中。这将限制您的部署选项。例如,您可能必须在单个域上部署 web 应用,或者设置一个子域。
如图 6-5 所示,跨起源 XMLHttpRequest 允许你在客户端聚合来自不同起源的内容。此外,如果目标服务器允许,您可以使用用户的凭据访问受保护的内容,从而为用户提供对个性化数据的直接访问。另一方面,服务器端聚合迫使所有内容通过单一的服务器端基础设施,这可能会造成瓶颈。
***图 6-5。*客户端和服务器端聚合的区别
CORS 规范规定,对于敏感操作(例如,具有凭证的请求,或者除 GET 或 POST 之外的请求),浏览器必须向服务器发送选项预检请求,以查看是否支持和允许该操作。这意味着成功的通信可能需要支持 CORS 的服务器。清单 6-5 和 6-6 显示了在[www.example.com](http://www.example.com)
上托管的页面和[www.example.net](http://www.example.net)
上托管的服务之间的跨源交换中涉及的 HTTP 头。
***清单 6-5。*请求报头示例
POST /main HTTP/1.1 Host: www.example.net
User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.1.3) Gecko/20090910 Ubuntu/9.04 (jaunty) Shiretoko/3.5.3 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 300 Connection: keep-alive Referer: http://www.example.com/ Origin: http://www.example.com Pragma: no-cache Cache-Control: no-cache Content-Length: 0
***清单 6-6。*示例响应标题
HTTP/1.1 201 Created Transfer-Encoding: chunked Server: Kaazing Gateway Date: Mon, 02 Nov 2009 06:55:08 GMT Content-Type: text/plain Access-Control-Allow-Origin: http://www.example.com Access-Control-Allow-Credentials: true
进展事件
XMLHttpRequest 中最重要的 API 改进之一是与渐进式响应相关的变化。在以前版本的 XMLHttpRequest 中,只有一个 readystatechange 事件。最重要的是,它在不同浏览器上的实现不一致。例如,readyState
3(进度)在 Internet Explorer 中从不触发。此外,readyState
变更事件缺少一种交流上传进度的方式。实现一个上传进度条不是一个简单的任务,需要服务器端的参与。
XMLHttpRequest 级别 2 引入了具有有意义名称的进度事件。表 6-2 显示了新的进度事件名称。您可以通过为事件处理程序属性设置回调函数来侦听这些事件。例如,当 loadstart 事件激发时,将调用 onloadstart 属性的回调。
为了向后兼容,将保留旧的readyState
属性和readystatechange
事件。
“看似任意”的时代
在 XMLHttpRequest Level 2 规范对readystatechange
事件的描述中(为了向后兼容而维护),由于历史原因,readyState
属性被描述为在看似任意的时间发生变化。
浏览器支持 HTML5 XMLHttpRequest Level 2
在撰写本文时,许多浏览器已经支持 HTML5 XMLHttpRequest。由于支持的级别不同,在使用这些元素之前,最好先测试一下是否支持 HTML5 XMLHttpRequest。本章后面的“检查浏览器支持”一节将向您展示如何以编程方式检查浏览器支持。
使用 XMLHttpRequest API
在这一节中,我们将更详细地探索 XMLHttpRequest 的使用。为了便于说明,我们创建了一个简单的 HTML page—crossOriginUpload.html。以下示例的示例代码位于code/communication
文件夹中。
检查浏览器支持
在尝试使用 XMLHttpRequest 第 2 级功能(比如跨源支持)之前,最好检查一下它是否受支持。你可以通过检查新的withCredentials
属性在 XMLHttpRequest 对象上是否可用来做到这一点,如清单 6-7 中的所示。
***清单 6-7。*检查 XMLHttpRequest 中的跨源支持是否可用
var xhr = new XMLHttpRequest() if (typeof xhr.withCredentials === undefined) { document.getElementById("support").innerHTML = "Your browser <strong>does not</strong> support cross-origin XMLHttpRequest"; } else { document.getElementById("support").innerHTML = "Your browser <strong>does</strong>support cross-origin XMLHttpRequest"; }
跨产地请求
要创建跨源 XMLHttpRequest,必须首先创建一个新的 XMLHttpRequest 对象,如下例所示。:
var crossOriginRequest = new XMLHttpRequest()
接下来,通过在不同的起点上指定地址来进行跨起点 XMLHttpRequest,如下例所示。
crossOriginRequest.open("GET", "http://www.example.net/stockfeed", true);
确保你听到了错误。这个请求可能不成功的原因有很多。例如,网络故障、拒绝访问以及目标服务器上缺乏 CORS 支持。
为什么不是 JSONP?
Frank 说:“从另一个数据源获取数据的一种常用方法是 JSONP(带填充的 JSON)。JSONP 包括用 JSON 资源的 URL 创建一个脚本标记。URL 有一个查询参数,包含脚本加载时要调用的函数的名称。远程服务器通过调用命名函数来包装 JSON 数据。这有严重的安全隐患!当您使用 JSONP 时,您必须完全信任提供数据的服务。恶意脚本可能会接管您的应用。
使用 XMLHttpRequest (XHR)和 CORS,您接收的是数据而不是代码,您可以安全地解析这些数据。这比评估外部输入要安全得多。"
使用进度事件
XMLHttpRequest Level 2 提供了命名的进度事件,而不是用数字状态来表示请求和响应的不同阶段。您可以通过为事件处理程序属性设置回调函数来侦听这些事件。
清单 6-8 展示了回调函数是如何处理进度事件的。进度事件包含要传输的总数据量、已经传输的数据量以及一个布尔值,该值指示总数据量是否已知(在流式 HTTP 中可能不是这样)。XMLHttpRequest.upload 调度具有相同字段的事件。
***清单 6-8。*使用 onprogress 事件
`crossOriginRequest.onprogress = function(e) {
var total = e.total;
var loaded = e.loaded;
if (e.lengthComputable) {
// do something with the progress information
}
}
crossOriginRequest.upload.onprogress = function(e) {
var total = e.total;
var loaded = e.loaded;
if (e.lengthComputable) {
// do something with the progress information
}
}`
二进制数据
支持新的二进制 API 如 Typed Array(这是 WebGL 和可编程音频所必需的)的浏览器可能能够用 XMLHttpRequest 发送二进制数据。XMLHttpRequest Level 2 规范支持使用 Blob 和 ArrayBuffer(也称为类型化数组)对象调用send()
方法(参见清单 6-9 )。
***清单 6-9。*发送类型化的字节数组
var a = new Uint8Array([8,6,7,5,3,0,9]); var xhr = new XMLHttpRequest(); xhr.open("POST", "/data/", true) console.log(a) xhr.send(a.buffer);
这会产生一个带有二进制内容体的 HTTP POST 请求。内容长度为 7,正文包含字节 8,6,7,5,3,0,9。
XMLHttpRequest 级别 2 还公开二进制响应数据。将responseType
属性设置为" text “、” document “、” arraybuffer “或” blob "控制由response
属性返回的对象的类型。要查看 HTTP 响应主体包含的原始字节,请将responseType
设置为“arraybuffer”或“blob”
在下一章,我们将看到如何使用 WebSocket 来发送和接收使用相同类型的二进制数据。
使用 XMLHttpRequest 构建应用
在这个例子中,我们将看看如何将比赛地理位置坐标上传到一个位于不同原点的 web 服务器上。我们使用新的进度事件来监控 HTTP 请求的状态,包括上传百分比。图 6-6 显示了实际应用。
***图 6-6。*上传地理定位数据的网络应用
为了便于说明,我们已经创建了 HTML 文件crossOrignUpload.html.
,以下步骤强调了构建跨原点上传页面的重要部分,如图图 6-5 所示。以下示例的示例代码位于code/communication
文件夹中。
首先,创建一个新的XMLHttpRequest
对象,如下例所示。
var xhr = new XMLHttpRequest();
接下来,检查浏览器是否支持跨源 XMLHttpRequest,如下例所示。
if (typeof xhr.withCredentials === undefined) { document.getElementById("support").innerHTML = "Your browser <strong>doesnot</strong> support cross-origin XMLHttpRequest"; } else { document.getElementById("support").innerHTML = "Your browser <strong>does</strong> support cross-origin XMLHttpRequest"; }
接下来,设置回调函数来处理进度事件,并计算上传和下载的比率。
`xhr.upload.onprogress = function(e) {
var ratio = e.loaded / e.total;
setProgress(ratio + “% uploaded”);
}
xhr.onprogress = function(e) {
var ratio = e.loaded / e.total;
setProgress(ratio + “% downloaded”);
}
xhr.onload = function(e) {
setProgress(“finished”);
}`
xhr.onerror = function(e) { setProgress("error"); }
最后,打开请求并发送包含编码的地理位置数据的字符串。这将是一个跨源请求,因为目标位置是一个与页面具有不同源的 URL。
`var targetLocation = “http://geodata.example.net:9999/upload”;
xhr.open(“POST”, targetLocation, true);
geoDataString = dataElement.textContent;
xhr.send(geoDataString);`
最终代码
清单 6-10 显示了完整的应用代码——crossOriginUpload.html 文件的内容。
清单 6-10。【crossOriginUpload.html 内容
`
function loadDemo() {
var dataElement = document.getElementById(“geodata”);
dataElement.textContent = JSON.stringify(geoData).replace(“,”, ", ", “g”);
var xhr = new XMLHttpRequest()
if (typeof xhr.withCredentials === undefined) {
document.getElementById(“support”).innerHTML =
“Your browser does not support cross-origin XMLHttpRequest”;
} else {
document.getElementById(“support”).innerHTML =
“Your browser does support cross-origin XMLHttpRequest”;
}
var targetLocation = “http://geodata.example.net:9999/upload”;
function setProgress(s) {
document.getElementById(“progress”).innerHTML = s;
}
document.getElementById(“sendButton”).addEventListener(“click”,
function() {
xhr.upload.onprogress = function(e) {
var ratio = e.loaded / e.total;
setProgress(ratio + “% uploaded”);
}`
` xhr.onprogress = function(e) {
var ratio = e.loaded / e.total;
setProgress(ratio + “% downloaded”);
}
xhr.onload = function(e) {
setProgress(“finished”);
}
xhr.onerror = function(e) {
setProgress(“error”);
}
xhr.open(“POST”, targetLocation, true);
geoDataString = dataElement.textContent;
xhr.send(geoDataString);
}, true);
}
window.addEventListener(“load”, loadDemo, true);
XMLHttpRequest Level 2
Geolocation Data to upload:
Upload
Status: ready
`实际应用
要查看这个示例的运行情况,有两个先决条件:页面必须由不同的域提供,目标页面必须由理解 CORS 标头的 web 服务器提供。本章的示例代码中包含了一个符合 CORS 标准的 Python 脚本,它可以处理传入的跨源 XMLHttpRequests。您可以通过执行以下步骤在本地计算机上运行演示:
-
Update your hosts file (
C:\Windows\system32\drivers\etc\hosts
on Windows or/etc/hosts on Unix/Linux)
by adding two entries pointing to your localhost (IP address127.0.0.1
) as shown in the following example:127.0.0.1 geodata.example.net 127.0.0.1 portal.example.com
注意修改主机文件后,您必须重启浏览器,以确保 DNS 条目生效。
-
安装 Python 2,它包括轻量级的
SimpleHTTPServer
web 服务器,如果您在前面的例子中没有这样做的话。 -
导航到包含示例文件(
crossOrignUpload.html
)和 Python CORS 服务器脚本(CORSServer.py
)的目录。 -
在这个目录下启动 Python,如下:
python CORSServer.py 9999
-
打开浏览器并导航至
[
portal.example.com:9999/crossOriginUpload.html](http://portal.example.com:9999/crossOriginUpload.html)
。你现在应该看到如图 6-6 所示的页面。
实用的临时演员
有时有些技术不适合我们的常规例子,但仍然适用于许多类型的 HTML5 应用。我们在这里向你展示一些简短但常见的实用附加功能。
结构化数据
早期版本的postMessage
只支持字符串。后来的版本允许其他类型的数据,包括 JavaScript 对象、画布图像数据和文件。随着规范的发展,对不同对象类型的支持将因浏览器而异。
在某些浏览器中,可以用postMessage
发送的 JavaScript 对象的限制与 JSON 数据的限制相同。特别是,可能不允许有循环的数据结构。包含自身的列表就是一个例子。
框架破坏
框架破坏是一种确保你的内容不被加载到 iframe 中的技术。一个应用可以检测到它的窗口不是最外面的窗口(window.top
),然后跳出它的包含框架,如下例所示。:
if (window !== window.top) { window.top.location = location; }
支持 X-Frame-Options HTTP 头的浏览器还将防止对将该头设置为 DENY 或 SAMEORIGIN 的资源的恶意成帧。但是,您可能希望选择性地允许某些合作伙伴页面来框定您的内容。一个解决方案是使用postMessage
在协作的 iframes 和包含页面之间握手,如清单 6-11 中的所示。
***清单 6-11。*在 iframe 中使用 postMessage 与可信伙伴握手页面
`var framebustTimer;
var timeout = 3000; // 3 second framebust timeout
if (window !== window.top) {
framebustTimer = setTimeout(
function() {
window.top.location = location;
}, timeout);
}
window.addEventListener(“message”, function(e) {
switch(e.origin) {
case trustedFramer:
clearTimeout(framebustTimer);
break;
}
), true);`
总结
在本章中,您已经看到了如何使用 HTML5 跨文档消息传递和 XMLHttpRequest Level 2 来创建能够跨来源安全通信的引人注目的应用。
首先,我们讨论了postMessage
和 origin 安全概念 HTML5 通信的两个关键元素——然后我们向您展示了如何使用postMessage
API 在 iframes、选项卡和窗口之间进行通信。
接下来,我们讨论了 XMLHttpRequest 级别 2——XMLHttpRequest 的改进版本。我们向您展示了 XMLHttpRequest 在哪些方面得到了改进;最重要的是在 readystatechange 事件区域。然后,我们向您展示了如何使用 XMLHttpRequest 进行跨源请求,以及如何使用新的进度事件。
最后,我们用几个实际例子结束了这一章。在下一章中,我们将演示 HTML5 WebSockets 如何让您以难以置信的简单性和最小的开销将实时数据传输到应用。