HTML5-企业级应用开发-全-
HTML5 企业级应用开发(全)
原文:
zh.annas-archive.org/md5/026a6828da3599b97fcd9c93981c8b10译者:飞龙
前言
HTML5,除了是互联网时代的最新热门词汇外,正迅速成为网络的通用语言。事实上,HTML5 是第一个拥有自己标志的 HTML 版本(www.w3.org/html/logo)。要理解其重要性,首先需要了解一点历史。

时间简史(客户端-服务器版)
在过去的几十年里,企业级应用开发一直在终端和主机、客户端和服务器之间摇摆不定。在 20 世纪 80 年代,由于“哑终端”或“瘦客户端”的作用,业务逻辑主要被推到服务器端,这些终端除了作为用户和服务器之间的中间人外,几乎不做任何事情。从 20 世纪 90 年代开始,随着“胖客户端”的出现,逻辑开始向客户端倾斜,承担处理负担。随着 1991 年万维网的引入,一种新的瘦客户端出现了。摆锤再次摆动。或者,它真的摆动了?
客户端与服务器之间的转变主要是由成本和功耗驱动的。早期,投资主要用于构建强大且昂贵的服务器。随着个人电脑在内存和处理能力方面的增强,以及成本的降低,现在可以构建更易于分布的应用程序,允许离线功能,最重要的是,需要更强大(且成本更低)的服务器基础设施。然而,维护、升级和部署“胖客户端”带来了新的负担。
基于 Web 的应用程序最终出现,以解决成本、功耗和维护性的问题。最初,它们看起来与它们的“瘦客户端”前辈非常相似:仅仅是用户和服务器之间的中间人,但没有部署开销。然而,随着 Java 和 JavaScript 等技术的引入,“瘦客户端”开始增加一些重量。不久,随着小程序、脚本和插件的普及,处理负担开始向客户端转移,随之而来的是维护性问题再次出现。问题不再是管理应用程序的分布,而是转向管理小程序、脚本和插件的分布。
随着“富客户端”的引入,情况出现了分化。业务逻辑分层。关注点的分离成为常态。让服务器处理服务器相关的事务。让客户端处理客户端相关的事务。然而,客户端需要一些时间才能处理世界对它的需求。
时间简史(网络浏览器版)
当蒂姆·伯纳斯-李(Tim Berners-Lee)在 1990 年向他的 CERN 同事介绍他的万维网浏览器时,只能隐约看到它将来会成为什么。马克·安德森(Marc Andreessen)将在 1993 年引入 Mosaic 和图形浏览,而 Netscape 将在 1994 年迅速跟进。之后,微软在 1995 年推出了 Internet Explorer。很快,第一场浏览器大战就会到来和结束,Internet Explorer 成为赢家,Netscape 的残余部分聚集在 Mozilla 和 Firefox 周围,Firefox 于 2002 年发布。在 21 世纪初,苹果发布了 Safari,谷歌发布了 Chrome。
然而,网络浏览器很少彼此保持*衡。尽管存在网络标准和名为 W3C 的管理机构,但浏览器制造商仍然按照自己的曲调演奏。最初,HTML 的实现差异很大,这一趋势一直持续到 HTML 4.0、XHTML、CSS 和 JavaScript。实现和行为上的差异使得网络开发变得困难且耗时。
为了克服网络浏览器的局限性和不一致性,基于插件的技术,如 Adobe Flash,开始蓬勃发展,继续在基于浏览器的技术,如 HTML、CSS 和 JavaScript 停止的地方继续前进。多年来,许多网站主要由——有时甚至完全是——像 Flash 这样的技术组成;这些插件甚至提供了更好的性能。随着摆钟摆回到更肥的客户端,富互联网应用(RIAs)的概念盛行。
注意
请参阅谷歌的“万维网演变”(The Evolution of the Web),其中包含关于网络浏览器及其实现现代浏览器功能的交互式图形,网址为evolutionofweb.appspot.com。
插件技术的引入成为了万维网承诺的误导。虽然内容连接性是 HTML 原始原则的一个标志,但内容是通过embed、object和applet等标签表示的,其中应用程序模块嵌入到网页上构成了黑盒,隐藏了语义网中的内容。
尽管如此,网络浏览器仍在不断发展。jQuery 等 JavaScript 框架出现,以抽象浏览器差异并提供更丰富的交互性。CSS 技术出现,以克服浏览器之间的局限性和不一致性。新浏览器出现,对网络标准的支持更好。
然而,还有一些东西缺失。尽管应用程序正在使用基于浏览器的技术进行开发,但许多应用程序功能被排除在浏览器之外。添加视频/音频播放、离线能力和甚至浏览器历史管理的一致机制都缺失。Flash 仍然被视为填补网络缺失部分的工具。
最后,在 2009 年,当 XHTML 2.0 被放弃,转而采用与早期 HTML 版本更兼容的技术时,浏览器开发围绕着 HTML5 聚集。HTML5 旨在解决浏览器中的两个主要领域:需要一种更一致、语义化的标记语言,以及需要更多原生支持的浏览器功能。当它在 2004 年推出时,预示了一系列 API,使浏览器成为真正的应用开发*台,从而更加语义化。
注意
HTML5 特性
-
媒体 API:这嵌入、播放和暂停多媒体内容
-
文本轨道 API:这读取多媒体内容的文本轨道属性
-
拖放 API:通过设置一个属性,原生地使元素可拖放
-
离线应用缓存:这为离线使用保存数据
-
历史 API:这增加了对后退按钮的控制
-
Canvas API:这在 2D 和 3D 中实际上在网络上绘制
-
跨文档消息传递:这克服了跨站脚本限制
-
微数据:这为搜索引擎提供更多语义内容以便查找
-
MIME 类型和协议处理程序注册:这通过处理程序扩展了应用程序以支持额外的协议
-
Web Workers:这产生与用户交互无关的线程
-
Web 存储:这将在客户端存储数据
-
Web Sockets:这可以在服务器和客户端之间发送双向消息
随着现代浏览器对 HTML5 支持的不断增强,对基于插件技术的依赖开始让位于基于浏览器的实现。有了允许更好地控制体验的 API,客户端终于开始实现其承诺。特别是移动网络浏览器成为了这一变革的催化剂。由于 Adobe Flash 不支持 iPhone 和 iPad 等设备,并且 Safari 浏览器对 HTML5 特性的实现日益增长,HTML5 正迅速成为移动网络应用开发的标准。然而,如果这一趋势要持续下去,浏览器制造商和应用开发者必须遵守 HTML5 的开发标准,这又把我们带回了那个标志。为了纠正过去的错误,HTML5 必须在实现上达成共识。为了培养这种共识,正在兴起一股运动,旨在在网页浏览器和应用程序中强制执行标准,并加快实施步伐,因为采用率正在逼*。HTML5 标志象征着这一努力。
注意
网络超文本应用技术工作组(WHATWG)于 2004 年成立,发展了 HTML,并构想 HTML5 为 HTML 标准演变的下一步。当时,W3C 正在制定 XHTML 2.0 标准;然而,在 2009 年,W3C 决定停止这一努力,并加入 WHATWG,共同致力于开发 HTML5。
2011 年 1 月,它宣布 HTML5 标准将被称为“HTML”,并且从此规范将是一个活文档。
2012 年 12 月,国际网络标准机构万维网联盟(W3C)宣布 HTML5 功能完整。尽管它还不是标准,但它最终为浏览器制造商提供了一个稳定的开发目标,以便开发 HTML5 功能。
语义是关键
HTML5 试图以前所未有的方式对网络上的信息进行编码。在 HTML 的先前版本中,内容是根据其应如何显示来结构化的,而不是其固有的意义。div标签经常被使用,但div标签真正意味着什么?为了解决这个问题,应用程序开发者根据网络展示的标准和最佳实践使用id属性将内容拆分。
例如,应用程序开发者使用以下标签:
<div id="header">
<div id="footer">
显然的问题是,用于id属性的值不一定遵循标准。一个应用程序开发者可能使用id="header",而另一个可能使用id="head"。为了根据语义标准化结构,HTML5 引入了一套新的标签,消除了这个过程的不确定性。
HTML5 引入了一套新的顶级标签,可以分为以下几类:内容、语言、布局和格式。
内容标签
HTML5 引入的内容标签定义了如何将新类型的内容嵌入到网页中。声音、视频和图形等内容以与文本和图像多年来相同的方式呈现。
-
audio: 这个标签用于嵌入声音内容。在 HTML5 之前,一些浏览器对音频的支持不一致,或者需要使用 Adobe Flash 开发的特殊播放器来播放声音。HTML5 消除了这种依赖,使音频播放器成为网页浏览器本身的一致功能。 -
video: 这个标签用于嵌入视频内容。与音频一样,视频内容的支持并不一致,或者需要特殊的播放器来播放。支持video标签的网页浏览器具有内置的视频播放器。 -
canvas: 这个标签允许通过 JavaScript 绘制基本的 2D 图形。然而,3D 图形并不始终得到支持,并且是可选的。
语言标签
随着国际化越来越受到重视,本地化对网络开发者来说一直是一个特殊的挑战。HTML5 引入了一套新的标签,以帮助使内容更容易被更广泛的受众访问。
-
bdi: 这个标签定义了文本的方向性。这通常用于支持从右到左阅读的语言。 -
ruby: 与rt和rp标签结合使用,ruby标签定义了东亚书法的 Ruby 注释。
布局标签
HTML5 提供了一套一流的标签,不仅有助于布局页面,还允许将页面完全拆分。使用 HTML5,网络开发者能够以更标准的方式共享内容部分:
-
header: 这个标签定义了页面、部分或文章的标题。 -
footer: 这个标签定义了页面、部分或文章的页脚。 -
nav: 这个标签定义了网站的菜单结构。这些是用于在语义上分割网站的导航链接。 -
section: 这个标签定义了页面的部分。article和aside标签在某种程度上是专门的区域标签。 -
aside: 这个标签定义了页面的侧边栏内容。通常,网页会通过将辅助内容推到一边来分割。 -
article: 这个标签定义了页面的主要内容。虽然section、aside、header和footer等标签定义了页面的辅助内容,但article标签标识了被认为是焦点的内容部分。通常,这部分内容是 URI 唯一的。
格式化标签
HTML5 引入了一套新的特殊标签,这些标签定义了如何识别和适当地格式化内容区域。
-
figure: 这个标签标识了非连续内容,这些内容被分层到文本主体中。例如,它可以用来包裹由文本主体引用的图表、图表和图形。 -
details: 这个标签定义了可以切换为可见或隐藏的内容。它旨在根据用户操作(如与帮助相关的内容)显示和隐藏内容。网页开发者已经构建了各种解决方案来做到这一点,并且随着 HTML5 的推出,网络浏览器负责处理。 -
hgroup: 这个标签将已知的h1-h6标签包裹成一个结构化的整体。当标题相关时,hgroup显示它们是相关的。例如,对于具有标题和副标题的文章,标题会被包裹在h1标签中,而副标题会被包裹在h2标签中。围绕它们的hgroup标签表示h2标签与h1标签相关联,而不是文档大纲的一部分。 -
wbr: 这个标签定义了一个单词断开的机会。通常,文本行是通过空格来分割的。wbr标签允许网页开发者指定在连续的非空格字符集中,在没有足够空间显示所有内容的情况下,可以在哪里引入换行。 -
progress: 这个标签表示任务的进度,可以与 JavaScript 结合使用,向用户显示进度条。 -
time: 这个标签是一个微格式化标签,允许指定某物是日期或时间。 -
meter: 这个标签是一个格式化标签,用于定义具有已知范围的标量测量。 -
mark: 这个标签表示对用户相关的文本。通常,这会用于突出显示段落中的特定单词。
表单升级
HTML5 中的表单获得了一整套全新的功能,以允许更好地验证内容并提高使用便捷性。
以下标签是 HTML5 中新增的:
-
datalist: 这个标签与select标签的工作方式类似,增加了能够预先输入以选择列表中项的功能。 -
keygen: 这个标签用于生成用于表单的密钥对。这通常用于客户端认证。 -
output: 此标签表示计算结果。它与form标签相关联,用于向用户显示简单的计算,尤其是在与新的表单输入类型一起使用时。此外,input标签获得了一组新的类型。以下输入类型是 HTML5 中新增的:-
color: 此类型显示颜色选择器,用于提交该颜色的十六进制代码。 -
date: 此类型显示日期选择器,用于提交日期。 -
datetime: 此类型显示日期和时间选择器,用于提交包括时区的日期和时间。 -
datetime-local: 此类型显示日期和时间选择器,用于提交不包含时区的日期和时间。 -
email: 此类型显示用于输入电子邮件地址的字段。 -
month: 此类型显示月年选择器,用于提交月份和年份。 -
number: 此类型显示一个用于输入数值的字段。 -
range: 此类型限制用户在指定范围内选择数字。通常,这将显示为滑块。 -
search: 此类型显示搜索字段。 -
tel: 此类型显示一个字段,限制用户输入有效的电话号码。 -
time: 此类型显示时间选择器。 -
url: 此类型显示一个字段,限制用户输入有效的 URL。 -
week: 此类型显示用于在一年中选择星期的控件。
-
输入微数据
HTML5 增加了为内容定义自定义语义的能力。类似于 HTML 之前版本中的微格式,其中一组预定义的属性允许你赋予内容的语义意义,微数据允许你创建自己的语义语言来赋予你的内容。虽然微格式依赖于通用的属性,如class和rel,但微数据引入了itemscope、itemtype和itemprop来描述内容。itemscope和itemtype属性允许你定义自定义类型并指示其定义位置。itemprop属性指向定义中的特定属性:
<div itemscope itemtype="http://mysite.com/Movie">
<h1 itemprop="title">Your Favorite Movie</h1>
<p itemprop="summary" >
A summary of your favorite movie.
</p>
</div>
一堂解剖课
现在我们已经了解了 HTML5 中构建页面的许多新工具,让我们深入了解 HTML5 页面看起来是什么样子。
关于 DOCTYPE 的一些话
HTML 文档中的DOCTYPE声明始终是浏览器了解文档遵循的标准的信号。如果浏览器知道文档使用的标准,它可以更有效地处理和渲染该文档。基于 SGML 的标记语言需要这样做。
为了简化DOCTYPE标签,HTML5 只有一个类型:
<!DOCTYPE html>
与之前的 HTML 版本不同,之前的版本需要引用所遵循的特定 DTD,HTML5 不是基于 SGML 的,因此不需要 DTD 引用。
lang 属性
HTML5 引入了一个简化的lang属性来指定页面的语言。在 XHTML 中,需要一个xmlns属性,但 HTML5 不需要这个属性。
元标签同样重要
HTML5 添加了一个名为 charset 的新元标签。这个标签指定了文档的特定字符编码。否则,它使用所有来自 HTML 4.01 的元标签。
HTML5 包括对 viewport 元标签的支持。这个元标签定义了网页应该如何查看,包括如宽度和高度等参数。它还允许您定义缩放设置,如初始缩放、最小和最大缩放。它甚至允许您针对特定的密度 DPI 进行定位,如果您想根据不同的屏幕分辨率更改页面外观的话。
整合一切
一个基本的 HTML5 页面将如下所示:
<!doctype html>
<html lang="en">
<head>
<title>My First HTML5 Page</title>
<meta charset="utf-8">
<meta name="description" content="My first HTML5 page.">
<meta name="author" content="Me">
</head>
<body>
</body>
</html>
我们当然会在继续的过程中添加更多内容。
应用程序
在本书的大部分内容中,我们将构建一个移动网络应用,以展示 HTML5 的许多功能。该应用名为 MovieNow,将成为查找、评论和预订附*电影的一站式商店。本书中我们将开发的功能如下:
-
使用地理位置查找附*的电影
-
向用户显示电影数据
-
使用
video标签观看预告片 -
使用
canvas标签显示评论 -
使用拖放 API 选择电影
-
与外部 API 集成
-
通过 Web Workers 显示附*的推文
本书涵盖的内容
在接下来的章节中,我们将将 HTML5 的各种功能构建到我们的 MoveNow 企业应用中。
第一章, HTML5 入门套件:兼容性,讨论了 HTML5 功能在多个网络浏览器和设备上的支持,以及绕过这些浏览器缺陷的方法。支持多个浏览器的主要驱动因素是确保在多个设备上访问企业网络应用的同时保持一致的用户体验。
第二章, HTML5 入门套件:实用工具,提供了开始 HTML5 企业应用开发的指南,包括可用的工具、它们的安装和使用。将讨论选择工具的商业驱动因素的综合评估。
第三章, 应用:结构和语义,将指导您设置 MovieNow 企业应用的模板。我们将设置整体页面结构,深入讨论语义标签,并讨论微数据。
第四章, 应用:通过地理位置获取电影,通过介绍地理位置开始 MovieNow 企业应用。我们将向您介绍地理位置 API 以及如何使用它来实现有用的功能。
第五章, 应用:通过 CSS3 显示电影数据,涵盖了 CSS3 的布局和功能,包括一些有趣的 CSS3 效果。我们还将介绍在 Web 应用和设备上定义标准样式(包括媒体查询和 CSS3 的兼容性考虑)的最佳实践。
第六章, 应用:通过 HTML5 视频的预告片,介绍了视频和音频标签及其在 HTML5 企业应用中的使用。我们将讨论如何通过 JavaScript 操作多媒体内容的播放,以及与不支持 HTML5 视频和音频标签的浏览器的向后兼容性。
第七章, 应用:通过 Canvas 显示评分,介绍了 HTML5 canvas 以及如何使用绘图 API 在您的企业应用中显示图形。我们将使用绘图 API 为我们的 MovieNow 应用创建评分图表。
第八章, 应用:通过拖放选择 UI,涵盖了拖放 API。我们将在 MovieNow 企业应用中实现拖放功能,展示事件委托和发布-订阅模式。
第九章, 应用:通过 Twitter 传播信息,通过集成 Twitter API 讨论 HTML5 中的表单和表单验证。我们将在 MovieNow 应用内部实现推文的发布。
第十章, 应用:通过 Web Workers 消费推文,展示了 Web Workers 和外部 API 的力量,将社交元素引入企业应用。我们将把附*的推文集成到 MovieNow 应用中。
第十一章, 完成:调试你的应用,讨论了调试 HTML5 企业应用的方法。我们将讨论浏览器控制台和 HTTP 代理。
第十二章, 完成:测试你的应用,涵盖了测试 HTML5 企业应用的工具。我们将涵盖功能测试套件和自动化工具。
第十三章, 完成:性能,讨论了性能,这是任何 HTML5 企业应用的关键话题。我们将讨论策略和工具,并指导如何分析您的 HTML5 应用。
你需要为这本书准备的内容
由于本书介绍了已经熟悉网络基础(包括 HTML、CSS 和 JavaScript)的开发者,并介绍了 HTML5 和 CSS3 的优势,因此你需要具备网络应用开发的相关知识。
本书面向的对象
本书主要面向有一定网络应用开发经验的开发者,他们希望将自己的知识扩展到 HTML5 和 CSS3 的最新发展。完成本书后,读者将对 HTML5 提供的用于开发企业应用的工具集有深入的理解。
术语
在本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是这些样式的示例及其含义的解释。
文本中的代码字如下所示:“将html5shiv.js从dist文件夹复制到您的 JavaScript 文件夹”。
代码块设置如下:
<div class="geolocation-available">
Congrats! Your browser supports Geolocation!
</div>
<div class="no-geolocation">
Your browser doesn't support Geolocation :(
</div>
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“点击生成按钮”。
注意
警告或重要提示以如下框的形式出现。
小贴士
小技巧和窍门看起来像这样。
读者反馈
我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中受益的标题非常重要。
要向我们发送一般反馈,只需发送电子邮件到 <feedback@packtpub.com>,并在邮件主题中提及书名。
如果您需要一本书并且希望我们出版,请通过www.packtpub.com上的建议书名表单或发送电子邮件到 <suggest@packtpub.com> 告诉我们。
如果您在某个主题上有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从您在www.packtpub.com的账户下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
下载本书的彩色图像
我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出中的变化。您可以从www.packtpub.com/sites/default/files/downloads/5689_graphics.pdf下载此文件。
下载示例代码
您可以从www.packtpub.com您购买的所有 Packt 书籍的账户中下载示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
错误清单
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问www.packtpub.com/support,选择您的书籍,点击错误提交表单链接,并输入您的错误详细信息来报告它们。一旦您的错误得到验证,您的提交将被接受,错误将被上传到我们的网站,或添加到该标题的现有错误清单中,在“错误清单”部分下。您可以通过从www.packtpub.com/support选择您的标题来查看任何现有错误。
盗版
互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过链接mailto:copyright@packtpub.com与我们联系,并提供疑似盗版材料的链接。
我们感谢您在保护我们的作者以及为我们提供有价值内容的能力方面的帮助。
询问
如果您在本书的任何方面遇到问题,可以通过链接mailto:questions@packtpub.com与我们联系,我们将尽力解决。
第一章. HTML5 入门套件:兼容性
在序言中,我们介绍了 HTML5 文档的一般结构,但在开始开发之前,我们首先必须谈谈解决跨浏览器和*台兼容性问题这一耗时的问题。在本章中,我们将介绍浏览器的工作原理以及支持多个浏览器和设备的 HTML5 策略。到本章结束时,你将能够遵循一个初步的行动计划,以持续支持你的企业应用的功能、界面和用户体验。
我们将涵盖以下主题:
-
兼容性的真正含义
-
浏览器
-
操作系统*台
-
显示分辨率
-
兼容性的重要性
-
补丁差异 - 兼容库
兼容性的真正含义
在一个理想的世界里,HTML、CSS 和 JavaScript 应该在所有浏览器和*台上以相同的方式被解释。虽然万维网联盟(W3C)为这些技术制定了标准,但浏览器制造商以自己的方式实现了它们。这意味着虽然你可以使用 W3C 标准来开发企业应用,但不同浏览器和*台之间可能存在不一致性。
兼容性并不意味着用户在每一个客户端都应该有相同的体验,但它需要保持一定的连贯性。例如,如果我们有一个应用程序中的用户列表,根据*台的不同,有不同的输入方式是好的做法。我们可以在任何桌面客户端使用滚动条来导航列表。对于触摸设备,手势可能更受欢迎。
另一方面,我们需要注意*台限制,因为有时在每台设备或客户端上支持相同的功能在技术上是不可能的。一个说明这一点的特定实例是 iOS 设备中的音频和视频音量管理(直到版本 5.1.1)。在 iOS 的 Safari 中使用 JavaScript 无法控制音量。在这种情况下,隐藏 iOS 设备的音量控制更可取。
为了更好地理解兼容性问题,了解万维网相对于渲染最终产品的客户端能力、操作系统或*台以及屏幕分辨率的演变是很重要的。
浏览器
自从万维网发布以来,浏览器市场一直存在竞争,争夺市场份额的主导地位。到 2001 年,随着网景公司停止成为主要竞争者,Internet Explorer 控制了超过 90%的浏览器市场,但在 2004 年 11 月 Mozilla Firefox 1.0 版本和 2008 年 9 月 Google Chrome 发布后,它开始看到新一代的竞争。

然而,截至 2012 年 6 月,Google Chrome 已成为市场份额仅为 32.76%的最常用浏览器。它现在与 Mozilla Firefox、Internet Explorer、Safari 和 Opera(包括移动版本)共享一个越来越拥挤的空间。此外,每个浏览器都有自己的版本列表,在知道新版本总是即将到来之后,在某些情况下,我们需要决定我们的应用程序需要支持哪个版本。
让我们稍微了解一下浏览器和版本多样性的复杂性。每个浏览器都有两个主要的软件组件:渲染引擎和 JavaScript 引擎。
渲染引擎
也称为布局引擎或网页浏览器引擎,渲染引擎负责解析标记内容(HTML)和样式信息(CSS、XSL 等),并生成格式化内容的视觉表示,包括引用的媒体文件(图像、音频、视频、字体等)。了解许多渲染引擎很重要,因为它可以帮助你识别某些行为,并根据它们的渲染引擎推断出哪些浏览器将以某种方式表现。
虽然 Chrome 和 Safari 使用 WebKit(由苹果、KDE、诺基亚、谷歌和其他公司开发),Firefox 使用 Gecko(由 Netscape/Mozilla 基金会开发),Internet Explorer 使用 Trident(由微软拥有),而 Opera 使用 Presto。
使用 CSS,可以通过前缀来识别一些专有的渲染引擎功能(称为 CSS 扩展)。WebKit 特有的功能以-webkit-开头,Gecko 特有的功能以-moz-开头。Opera 包括-o-前缀,而 Internet Explorer 8 及以上版本识别-ms-。
Trident 采用不同的方法。它识别以*或–为前缀的常见 CSS 属性以覆盖之前的定义(例如,*color:#ccc;和_color:#ccc;除了 Trident 以外的其他渲染引擎都不会识别)。
JavaScript 引擎
JavaScript 引擎是浏览器中解释和执行 JavaScript 代码的软件组件。虽然渲染引擎负责使用 CSS 样式呈现 HTML 内容的视觉表现,但 JavaScript 引擎将解释和执行 JavaScript 代码。
Chrome 使用 V8 引擎,Firefox 现在使用 Jägermonkey,Internet Explorer 9 使用 Chakra,Safari 使用 Nitro,而 Opera 在 2010 年用 Carakan 替换了 SunSpider。
注意
许多人认为的当前浏览器战争的排名很大程度上是由 JavaScript 引擎的速度驱动的。虽然其他引擎依赖于解释和编译,但 V8 没有中间解析器,它使用自己的运行时环境生成汇编器。换句话说,它有一个虚拟机。这使得 Chrome 成为最快的浏览器之一。
作为一般规则(有一些例外),最好是先加载 HTML 和 CSS,然后才是 JavaScript。这可以通过在关闭<body>标签之前包含导入 JavaScript 的<script>标签来实现。这样做的原因是渲染 HTML 和 CSS 比解释和执行 JavaScript 要快。因此,网页看起来会更快地加载。
当无法在主体中包含<script>标签时,<script>标签上有两个属性可以用来向浏览器指示脚本何时下载。这些是async,它在 HTML5 中引入,以及defer。defer属性确实做了它声称的事情;它将脚本执行推迟到页面渲染完毕。这样,DOM 就为你的脚本准备好了。async属性指示浏览器异步下载脚本,而不阻塞渲染引擎,并在准备好时执行它。两者都在DOMContentLoaded事件之前执行。关键区别在于defer按顺序执行每个脚本,而async则在脚本准备好时执行。通常,为了支持不支持async属性的老旧浏览器,这些属性会一起使用,这样不支持异步的浏览器可以回退到defer。
虽然浏览器之间存在许多差异,但重要的是要意识到,在同一个浏览器类别中存在多个版本,它们的 HTML5 和 CSS3 支持差异很大。这对于 Internet Explorer 来说尤其如此。对 HTML5 和 CSS3 的正确支持直到 Internet Explorer 9 才出现。
注意
微软已经开始了一项活动,旨在全球范围内废弃和减少 Internet Explorer 6 的市场份额。有关更多详情,请参阅www.ie6countdown.com。Aten 设计集团将这一举措推向了另一个层次,通过纪念/www.ie6funeral.com上的 IE6 葬礼来纪念这一事件。
大多数 HTML5 和 CSS3 功能在以下浏览器和版本中得到支持:
-
Internet Explorer 9 及以上版本
-
Firefox 7 及以上版本
-
Chrome 14 及以上版本
-
Safari 5 及以上版本
-
Safari Mobile 3.2 及以上版本
-
Opera 11 及以上版本
-
Opera Mobile 5 及以上版本
即使如此,仍然有一些功能不受支持,并且实现之间存在不一致。一个有趣的案例研究揭示了浏览器之间标准缺失的情况,那就是 HTML5 中的视频使用。为了使用 HTML5 的本地视频功能,视频文件必须使用特定的编解码器进行压缩。存在三种主要的编解码器:Ogg Theora(免费),H.264(对最终消费者免费,但编码和解码产品涉及版税),和WebM(免费)。由于 Firefox 倾向于使用开源技术,它最初只支持 Ogg 和 WebM。Chrome 目前支持所有三种编解码器,但出于与 Firefox 对 H.264 支持相同的原因,将在后续版本中移除(尽管可能在移动设备上继续支持)。Safari、Safari Mobile 和 Internet Explorer 9 及以上版本默认只支持 H.264,但您可以安装插件以支持 Ogg 和 WebM(Safari Mobile 除外)。
注意
以下网站提供了有关不同浏览器对 HTML5 功能支持以及行为差异的详细信息:
操作系统*台
任何运行在操作系统(OS)上的网络应用程序都会受到影响。最明显的区别是浏览器用户界面元素的外观,包括滚动条、提交按钮、复选框等。这是一个重要的考虑因素,因为这些元素的大小和形状在不同的操作系统中可能会有所不同——即使使用相同的浏览器。实际上,一些操作系统限制了某些功能,例如 <input type="file"> 在 iOS6 之前并未在 iOS 上得到支持。

操作系统*台统计显示,Windows 是目前网络使用最广泛的操作系统。然而,考虑到*板设备和智能手机技术的不断改进,移动*台在未来几年可能会更加突出。
显示分辨率
随着市场上设备的增多,屏幕分辨率在规划网络应用程序时正迅速成为一个重要的考虑因素。Android 拥有多种尺寸和密度的设备。根据屏幕分辨率的用法,新硬件技术的进步使得现代屏幕上的像素数量得以增加:

尽管现在大多数桌面系统都具有高于 1024 x 768 的分辨率,但移动技术的兴起创造了一个悖论,即低分辨率显示屏正在重新夺回失去的地盘。企业应用程序提供的服务体验不需要,实际上也不应该是所有设备的相同。在 1920 x 1200 分辨率的桌面屏幕上查看页面可能与 960 x 540 分辨率的手机屏幕大相径庭,这不仅因为分辨率,还因为设备的尺寸和可读性(文本的可读性和理解程度)。有时检测分辨率以适应用户体验是很重要的。毫无疑问,新的技术如响应式网页设计正在被采纳来解决这些问题。
注意
就像游戏场上的变量已经足够多一样,苹果公司在 2010 年 6 月推出了搭载 Retina 显示屏的 iPhone 4,其原生分辨率为 960 x 640。这项技术基于更高的像素密度,超出了人类眼睛在典型观看距离下看到屏幕上图像像素化的能力。尽管在 iPhone 4 上的网页图像中并不那么明显,但 2012 年推出的配备 Retina 显示屏的新 iPad 和新的 MacBook Pro 系列对网络应用提出了新的要求。
首先,有一些网络开发技术通过使用 CSS 检测设备分辨率来确定客户端是否为移动设备。随着新 iPad 的出现,2048 x 1536 px 的分辨率变得不可能或至少不是直观的。新 iPad 的分辨率高于市场上大多数台式机和笔记本电脑。其次,为了避免在新 iPad 或新 MacBook Pro 上查看的任何应用程序中出现像素化效果,有必要为这些苹果设备包含更高分辨率的图像,并为与所有其他设备的向后兼容性包含正常分辨率的图像。
兼容性的重要性
在这一点上,如果只需要开发用于组织内部使用的企业应用程序,并且可以强制指定特定的浏览器,那么关心兼容性的重要性似乎是自然的。这种态度有两个危险的原因。首先,企业正在迅速向移动交付转变,控制*台变得越来越难以维持。其次,以这种方式限制组织会阻碍其更新应用程序支持能力,因为它将企业应用程序与桌面支持的选择过于紧密地耦合。如果公司想要升级到较新的操作系统或默认网络浏览器,通过要求支持特定版本的浏览器来限制它可能会产生不希望的结果。
补丁差异 - 兼容库
通常情况下,我们希望支持尽可能多的浏览器,因此我们需要一种方法来实现向后兼容性,通过实现浏览器上不可用的功能,通知用户该功能不可用,或者根据浏览器的功能修改用户体验。为此,有许多 JavaScript 库可以帮助我们。
提示
对于这一章,样式和脚本将直接包含在 HTML 文件中,以简化理解,尽管将样式和脚本放在单独的文件中是一个好习惯。
HTML5 Shiv
如前所述,Internet Explorer 从版本 9 开始支持 HTML5 标签。HTML5 Shiv允许在早期版本中提供支持。也称为HTML5 Shim,它是一个开源 JavaScript 库,可以在 IE 9 之前的 Internet Explorer 版本中为 HTML5 元素提供样式。它是通过使用document.createElement("element")来告诉浏览器这些标签存在来实现的。
假设我们在测试 Internet Explorer 8,并且我们有以下代码:
<!DOCTYPE HTML>
<html>
<head>
<style>
header{
color:#ff0000;
font-size:40px;
}
</style>
</head>
<body>
<header>Hello HTML5!</header>
</body>
</html>
提示
下载示例代码
您可以从您在www.PacktPub.com的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.PacktPub.com/support并注册,以便将文件直接通过电子邮件发送给您。
在 Internet Explorer 8 中,文本将不会显示样式。可以使用 HTML5 Shiv 来实现这一功能。
提示
在使用库之前深入了解它们是一个好习惯。我们鼓励您查看以下位置的 HTML5 Shiv 代码:github.com/aFarkas/html5shiv/blob/master/src/html5shiv.js。
要安装这个库,你需要执行以下步骤:
-
解压文件。
-
将
html5shiv.js从dist文件夹复制到你的 JavaScript 文件夹(在我们的例子中是js)。 -
在
head标签内插入以下代码:<!--[if lt IE 9]> <script src="img/html5shiv.js"></script> <![endif]-->因此,我们的代码应该如下所示:
<!DOCTYPE HTML> <html> <head> <!--[if lt IE 9]> <script src="img/html5shiv.js"></script> <![endif]--> <style> header{ color:#ff0000; font-size:40px; } </style> </head> <body> <header>Hello HTML5!</header> </body> </html>
提示
注意,<!--[if lt IE 9]>条件注释仅在浏览器版本低于版本 9 的 Internet Explorer 中包含 JavaScript 库。
如果你在这个代码中运行 Internet Explorer 8,它将以红色显示更大的字体Hello HTML5!这是一个例外,我们需要在<head>内部加载 JavaScript 库,因为我们需要在开始样式化之前给浏览器提供识别 HTML5 元素的能力。

Modernizr
由于即使是最新版本的网页浏览器也无法支持所有的 HTML5 和 CSS3 功能,因此在不支持某个功能时显示通知或更改页面行为是有用的。一种旧的策略是查看 navigator.userAgent 属性以检测浏览器的用户代理,并根据相关案例编写代码。随着用户代理的变化,保持同步并修改应用程序变得困难。用户代理检测的另一种方法是 特性检测,其中应用程序检测特定功能是否受支持,并相应地做出反应。以下是一个特性检测的示例:
function supports_video() {
return !!document.createElement('video').canPlayType;
}
Modernizr 是一个开源的 JavaScript 库,它允许根据每个浏览器的功能支持不同级别的体验,使用简单的特性检测模型。此外,Modernizr 使用 HTML5 Shiv,增加了在版本 9 之前的 Internet Explorer 上样式化 HTML5 元素的能力。
回到我们之前的例子:
<!DOCTYPE HTML>
<html>
<head>
<style>
header{
color:#ff0000;
font-size:40px;
}
</style>
</head>
<body>
<header>Hello HTML5!</header>
</body>
</html>
假设我们想要使用 HTML5 实现地理位置功能,以获取用户的地理位置,并检测它是否可用。为此,我们开始使用 Modernizr:
-
选择您想要验证的功能和 html5shiv。在这种情况下,我们将选择 Misc. 下的 Geolocation API 功能,以及 Extra 下的 html5shiv v3.4 和 Add CSS Classes。
-
点击 GENERATE! 按钮。
-
复制生成的源代码。
-
创建一个新的 JavaScript 文件(命名为
modernizr.js),粘贴源代码,并将其保存在您的 JavaScript 文件夹中。 -
在
<head>中导入 Modernizr 库<script src="img/modernizr.js" type="text/javascript"></script>。此时代码应如下所示:<!DOCTYPE HTML> <html> <head> <script src="img/modernizr.js" type="text/javascript"></script> <style> header{ color:#ff0000; font-size:40px; } </style> </head> <body> <header>Hello HTML5!</header> </body> </html>从这里我们有两个可能的解决方案;使用 JavaScript 或使用 CSS 来检测回退。
为了显示消息或显示不同的样式,我们可以按照以下步骤使用 CSS:
-
将一个名为
no-js的类添加到<html>标签中。如果 JavaScript 不受支持,这将作为默认选项。如果 JavaScript 受支持,Modernizr 将用名为js的类替换no-js,并在不支持的情况下为所有功能添加以no-为前缀的类。例如,如果您的浏览器支持地理位置,html标签将类似于以下行代码:<html class="js geolocation">否则,它将看起来像以下代码:
<html class="js no-geolocation"> -
在
<body>标签中,添加两个div标签,分别包含当地理位置支持时的消息和当不支持时的消息:<div class="geolocation-available"> Congrats! Your browser supports Geolocation! </div> <div class="no-geolocation"> Your browser doesn't support Geolocation :( </div> -
添加 CSS 样式以显示和隐藏消息。默认情况下,隐藏两个消息,并使用检测类在
<html>标签中创建的类来相应地隐藏或显示类:div.geolocation-available, div.no-geolocation{ display: none; } .no-geolocation div.no-geolocation, .geolocation div.geolocation-available { display: block; }最后,完整的代码应如下所示:
<!DOCTYPE HTML> <html class="no-js"> <head> <script src="img/modernizr.js" type="text/javascript"></script> <style> header{ color:#ff0000; font-size:40px; } div.geolocation-available, div.no-geolocation{ display: none; } .no-geolocation div.no-geolocation, .geolocation div.geolocation-available { display: block; } </style> </head> <body> <header>Hello HTML5!</header> <div class="geolocation-available"> Congrats! Your browser supports Geolocation! </div> <div class="no-geolocation"> Your browser doesn't support Geolocation :( </div> </body> </html>
如果我们想要实现 JavaScript 回退,我们需要使用 Modernizr 创建一个条件语句。在我们的情况下,由于 Modernizr 是一个具有方法的 JavaScript 对象,我们可以使用Modernizr.geolocation来测试地理定位是否受支持。条件语句应该是这样的:
if (Modernizr.geolocation){
alert("Congrats! Your browser supports Geolocation!");
}else{
alert("Your browser doesn't support Geolocation :(");
}
完整的代码应该如下所示:
<!DOCTYPE HTML>
<html>
<head>
<script src="img/modernizr.js" type="text/javascript"></script>
<style>
header{
color:#ff0000;
font-size:40px;
}
</style>
</head>
<body>
<header>Hello HTML5!</header>
<script type="text/javascript">
if (Modernizr.geolocation){
alert("Congrats! Your browser supports Geolocation!");
}
else{
alert("Your browser doesn't support Geolocation :(");
}
</script>
</body>
</html>
尽管名为 Modernizr,但它实际上并没有为浏览器添加缺失的功能,除了 HTML5 标签的样式支持。在你需要为旧浏览器中的功能创建回退时,Modernizr 是一个不错的选择。然而,如果只需要使用 HTML5 和 CSS3 进行简单的样式设计,HTML5 Shiv 就足够了。
Explorer Canvas
早期版本的 Internet Explorer(版本 9 之前)不支持 HTML5 Canvas,它允许基于 2D 命令的绘图,但Explorer Canvas使这个功能成为可能。
要使用 Explorer Canvas,你可以执行以下步骤:
-
下载 Explorer Canvas 的最新版本。
-
将
excanvas.compiled.js复制到你的 JavaScript 文件夹中。 -
在
<head>中导入库,验证 Internet Explorer 的版本:<!--[if lt IE 9]> <script type="text/javascript" src="img/excanvas.compiled.js"></script> <![endif]--> -
现在,你可以在旧版本的 Internet Explorer 中使用 HTML5 Canvas API。
由于它是一个 JavaScript 库,这意味着它需要在页面加载时被解释和执行,因此它的性能将比现代浏览器低得多。它也不支持几个功能,并且相当有缺陷。
HTML5 模板文件
设置一个项目,包括 Modernizr 和基本配置的一个非常简单直接的方法是使用一个启动包,例如HTML5 模板文件。HTML5 模板文件是一个包含 HTML、CSS 和 JavaScript 文件的集合,包括 Modernizr、jQuery 和 CSS Reset2(一组 CSS 规则,以创建一个共同基线的方式覆盖了不同浏览器中的默认和不一致的渲染)。即使兼容性是兴趣最远的话题,这个模板也可以用作初始化 CSS 并将必要的 JavaScript 库放置到位,从而使兼容性成为一个非问题。
你可以通过选择以下选项之一从html5boilerplate.com/下载 HTML5 模板文件:
-
模板文件: 文件集合未最小化和注释
-
精简模板: 文件集合已最小化且未注释
-
自定义模板: 你可以配置哪些文件将进入你的基础项目
虽然最后一种情况可能足够,但有时需要包含更多的自定义。幸运的是,有一个名为Initializr的工具,它可以从我们的 HTML5 模板文件项目中删除不需要的文件。此外,你还可以使用 Initializr 提供的模板根据窗口大小/屏幕分辨率修改视觉表现。
要下载 Initializr,请访问www.initializr.com/并选择:
-
经典 H5BP:这是一个基本项目
-
响应式:这是一个基于屏幕分辨率的响应式项目
-
Bootstrap:这是一个使用 Twitter 的 Bootstrap 模板(
twitter.github.com/bootstrap/)的响应式项目,使用 Less([verekia.com/less-css/](http://verekia.com/less-css/)),这是一种动态样式表语言,它使用 JavaScript 编译器在编译时生成 CSS
在此之后,你可以选择或修改包含的文件。
在开始应用开发之前
在接下来的章节中构建 MovieNow 应用时,我们将从头开始,这样我们可以一步一步地看到这个过程。但请记住,你可以使用模板来构建你的企业应用。唯一的注意事项是,你总是需要了解项目内部的内容;有时未知的 JavaScript 或 CSS 文件可能会引起严重的性能问题。
虽然所有这些可能听起来像是一场噩梦,但你只需要遵循一个简单的策略就可以开始兼容性的神奇之旅:
-
遵循 W3C 的 HTML、CSS 和 JavaScript 标准(http://www.w3schools.com)。
-
选择 JavaScript 库或 CSS 以支持旧浏览器的向后兼容性。一般来说,不包含 JavaScript 的兼容性解决方案比包含 JavaScript 的方案更好,但有时仅使用 CSS 是无法实现的。
-
为允许任何用户代理的访问性定义一个行动方案。有几个值得注意的策略:优雅降级或渐进增强。优雅降级意味着你首先为现代浏览器开发,然后为功能较弱的设备添加处理程序。另一方面,渐进增强意味着从基本功能开始,构建到浏览器功能的最低公共分母,然后为功能更强的浏览器添加增强。
-
为了多个原因,支持移动设备的不同用户体验(UX)是一个好的实践:在手机等移动设备上,键盘可能会很麻烦,在触摸屏设备上更是如此;在较小的分辨率上查看相同的布局可能会迫使用户不断放大和缩小,或者使得点击某些按钮或链接变得困难,有时在技术上也不可能实现某些功能。例如,在 iOS 设备(iPhone、iPad 等)上使用 JavaScript 进行视频自动播放或音量控制是不可能的。
-
制定针对多个浏览器的测试计划。有一些服务允许你在多个浏览器和*台上测试你的企业应用。当然,使用快照的服务可能不是最优的,因为它们不测试 JavaScript 执行。始终在测试机器上安装你系统将支持的浏览器总是好的,而且有一些工具允许你动态更改浏览器版本。
-
使用官方浏览器文档以及社区论坛来了解浏览器制造商的最新动态。
摘要
在本章中,我们了解了不同浏览器之间的差异以及它们不一致的行为方式。我们讨论了兼容性的重要性以及你可以使用的策略来*衡竞争。一般来说,网络开发者必须尝试覆盖大多数情况以确保兼容性,但与此同时,理解项目和目标受众非常重要,我们首先需要将我们的解决方案适应他们,然后才是全球场景。
作为最后的思考,我们需要站在用户的角度考虑。用户最不想看到的是一条要求下载另一个浏览器来使用应用程序的消息。记住,作为开发者,我们的目标不仅仅是将一整套需求实现,还要创造引人入胜的用户体验,将应用程序定义为一个促进最终目标的媒介,而不是一个将用户与最终目标隔离开来的障碍。
在开始建造房屋之前,你需要了解你需要哪些工具以及如何使用它们。在下一章中,我们将探讨如何设置你的机器以及我们可以用来构建我们的 HTML5 企业应用的可用的工具,包括对这些工具选择中涉及到的商业决策的全面评估。
第二章:HTML5 入门套件:实用工具
构建 HTML5 企业级应用需要合适的工具来完成这项工作。幸运的是,市场上有很多工具可以支持网络应用开发的各个方面。本章介绍了众多对网络开发有用的工具,包括编辑器、集成开发环境(IDEs)、网络服务器、网络浏览器、浏览器工具,甚至还有 HTTP 代理。
我们将涵盖以下主题:
-
选择编辑器和 IDE
-
选择网络服务器
-
预装堆栈
-
网络浏览器和插件
-
HTTP 代理
选择编辑器和 IDE
编辑器和集成开发环境的健壮性差异很大,它们通常是为了满足最终用户的各种需求而构建的。它们可以从非常简单的文本编辑器,如 Windows 的记事本(Notepad)和 Mac 的 TextEdit,到复杂的 IDEs,如 Eclipse。更有趣的是,还有各种新的基于网络的编辑器和 IDEs,允许您从几乎任何机器上开发,并与其他人在网络项目上进行最小化设置的合作。
选择哪个将主要基于您的需求。如果您的需求简单,您的工具可能也很简单。如果您的需求复杂,您的工具将更复杂。尽管您几乎可以使用任何东西来编写代码,但我们将在此介绍专门针对 HTML5 开发的工具。
以下是对您在开发 HTML5 企业级应用时可用选项的简要讨论。当然,这并不是一个详尽的列表,因为随着时间的推移,软件更新将极大地改变这一领域。
Adobe Dreamweaver CS6
这适用于 Windows XP SP3 及以上版本和 Mac OS X 10.6.8 及以上版本。Adobe 的旗舰网站开发产品在 Adobe Creative Suite 6 中进行了重大升级。Adobe 在 CS5 的 11.0.3 更新中引入了该产品,内置了 HTML5 和 CSS3 支持,包括 HTML5 模板、WebKit 引擎的更新以及编码提示以简化 HTML5 开发:

在 Dreamweaver 的 HTML5 功能中,您可以使用多屏幕预览功能同时查看不同尺寸的屏幕上的网页。这包括音频和视频内容的动态渲染。
在创建新文档时,请确保在“新建文档”对话框中选中HTML5作为文档类型,如下截图所示:

关于 Adobe Dreamweaver 的更多信息,包括如何购买,可以在www.adobe.com/products/dreamweaver.html找到。
Aptana Studio 3
该软件适用于 Windows XP 及以上版本、Mac OS X 10.5 及以上版本、Linux Ubuntu 9 及以上版本和 Linux Fedora 12 及以上版本。Aptana Studio 3 是 Eclipse 引擎的衍生产品 (www.eclipse.org/),它是一个功能强大、商业友好的开源集成开发环境。它支持多种语言,包括 Java、Ruby 和 PHP,以及 HTML、CSS 和 JavaScript。它还提供了一系列插件,用于源代码管理、部署和其他许多自定义功能。

HTML5 兼容性支持是 Aptana Studio 3 的一个主要特性。在其代码辅助功能中,它显示每个元素支持哪些浏览器以及支持级别。此外,它将 HTML5 Boilerplate 作为网页项目模板内置,因此您可以立即使用所有构建跨浏览器兼容的 HTML5 企业应用程序所需的工具开始工作。

关于 Aptana Studio 3 的更多信息,包括下载地址,可以在 www.aptana.com/products/studio3 找到。
BlueGriffon 1.5.2
该软件适用于 Windows XP 及以上版本、Mac OS X 10.5 及以上版本、Linux Ubuntu 11.10 及以上版本和 Linux Fedora 16 及以上版本。BlueGriffon 是一个基于 Gecko 渲染引擎(Mozilla Firefox 使用的渲染引擎)的免费 WYSIWYG 编辑器。它包括用于开发 HTML5 页面的工具,如 DOM 探索器和直接嵌入音频和视频文件的支持。它还通过其样式编辑器抽象出许多 CSS3 特效。

在创建新文档时,只需下拉新文档工具栏(位于新文档图标旁边)并点击 更多选项…。确保已选择 HTML 5,语言已设置,然后点击 确定。

关于 BlueGriffon 的更多信息可以在 bluegriffon.org/ 找到。
Maqetta
Maqetta 是 Dojo 基金会的一个开源项目,旨在通过 WYSIWYG 用户界面构建一个基于 HTML5 的编辑器,面向视觉设计师。目前作为托管产品提供(尽管这可能在将来发生变化),它处于技术预览状态,希望不久后发布 1.0 版本。以下截图显示了 Maqetta 为使用 iPhone 设备并在 Google Chrome 上运行的移动应用程序提供的界面。

除了视觉编辑外,Maqetta 还提供了审查和评论、开发线框以及以易于开发者理解设计意图的方式捕捉交互状态的功能。
关于 Maqetta 的更多信息可以在 maqetta.org/ 找到。
eXo
eXo 提供了一个免费、基于云的 IDE,它提供了协作和部署功能。除了 HTML5 和 JavaScript,它还支持 Java、Ruby 和 Python 应用程序的开发。它还集成了 *台即服务 (PaaS) 提供商,如 CloudBees 和 Heroku。它基于 eXo Platform 3.5,这是一个企业级 Java 门户和用户体验*台,提供内容和管理以及社交活动流。

在 www.cloud-ide.com/ 可以找到有关 eXo 的更多信息。
Cloud9
Cloud9 是另一个基于云的 IDE,除了 HTML5 和 JavaScript,它还支持多种语言。它因其与 GitHub 和 Bit Bucket 认证集成以及实时协作而受到特别关注。它还允许 SSH 和 FTP 访问,并具有离线工作的能力。Cloud9 一直在将自己定位为 Node.js 开发的主要 IDE。

尽管 Cloud9 提供了有限的免费订阅,但它还提供了一种高级订阅,该订阅提供额外的私有工作空间和完整的 shell 访问。
在 c9.io/ 可以找到有关 Cloud9 的更多信息。
选择网络服务器
如果您正在管理自己的网络集群或在本地机器上进行开发,了解可用的网络服务器软件包会有所帮助。在您的机器上安装和运行网络服务器,以更好地了解您的 HTML5 企业应用程序在没有上传和与远程主机同步开销的情况下将如何运行,这尤其有帮助。以下是对市场上一些最知名网络服务器的简要介绍。
Apache
更广泛使用的 HTTP 服务器之一是 Apache。它是一个始于 1996 年的开源项目,可以在 Unix 和 Windows 操作系统上安装。
在 httpd.apache.org/docs/2.4/install.html 可以找到有关安装 Apache 网络服务器的信息。
Apache Tomcat
Tomcat 是一个开源网络服务器,提供 servlet 容器,允许您运行 Java 代码。它主要用于 Windows 和 Unix,但可以通过下载适当的 Unix 软件包安装在 Mac 上。
要在 Windows 或 Unix 上安装 Tomcat,您可以遵循此网站上的说明:
tomcat.apache.org/tomcat-6.0-doc/setup.html
对于 Mac 用户,您可以参考此链接:
www.malisphoto.com/tips/tomcatonosx.html
Jetty
Jetty 是由 Eclipse 基金会托管的一个 HTTP 服务器,它被集成到 Eclipse IDE 中。它以其小巧的体积而闻名。
要使用 Jetty,您只需要使用 Eclipse IDE(Aptana Studio 3,因为它源自 Eclipse,也包含 Jetty)。否则,您可以在wiki.eclipse.org/Jetty/找到有关下载和安装的信息。
Tornado
Tornado 是一个相对较新的开源 Web 服务器,基于 FriendFeed(一个实时聚合器,整合了多个社交网络的更新)所使用的服务器。它特别以其快速和非阻塞而闻名,因此推荐用于开发 Web 服务。
关于 Tornado 的信息可以在www.tornadoweb.org/找到。
nginx
发音为“engine x”的 nginx 最初运行了一些俄罗斯网站。现在它为 Netflix 和 Hulu 等企业供电。由 Igor Sysoev 于 2002 年开发,它依赖于异步架构,这使得它能够快速扩展而不影响系统资源。
关于下载和安装 nginx 的信息可以在nginx.org/en/download.html找到。
LightTPD
LighTPD 发音为“lighty”,是一个开源的 Web 服务器,设计和优化用于高性能环境。它被 YouTube、Wikipedia 和 Meebo 等网站使用。它是由 Jan Kneschke 开发的,作为一个概念验证,用于在同一个服务器上并行处理 10,000 个连接(被称为 c10k 问题)。
关于下载和安装过程的信息可以在www.lighttpd.net/找到。
Node.js
虽然技术上不是一个 Web 服务器本身,但 Node.js 是一个可以编写简单 Web 服务器的*台。它以 Google Chrome 为其核心的 V8 JavaScript 运行时为基础,可以轻松地使用 Node.js 开发企业级 Web 应用程序,因为 HTTP 协议已经集成到*台中。它使用与 Tornado 和 nginx 中相同的非阻塞原则构建,因此比其他 Web 服务器更容易扩展,在某些场景下对系统资源的影响也较小。
关于 Node.js 的更多信息可以在nodejs.org/找到。
预包装堆栈
为了使环境设置更加简单,有一些易于安装的预包装服务器堆栈,它们内置了 Web 服务器、数据库和脚本*台(通常是 PHP)。您只需安装包,就可以拥有一个完整的沙盒环境,随时可以使用。一些流行的解决方案包括为 Mac OS 构建的 MAMP(www.mamp.info/),为 Windows 构建的 WAMP(www.wampserver.com/en/),以及适用于 Mac OS、Windows、Linux 和 Solaris 的 XAMPP(www.apachefriends.org/en/xampp.html)。
网络浏览器和插件
现在我们有了编写和运行我们的 HTML5 企业应用程序的工具,讨论可用于查看它们的网络浏览器是必要的。然而,我们将更进一步,并介绍一些可用的开发附加组件。由于工具的丰富性,无论是内置在网络浏览器中还是可插拔的,了解您随时可用的工具对于确保您的 HTML5 企业应用程序是最优的非常重要。以下是最常见现代网络浏览器的介绍。
Mozilla Firefox
直到最*几年,Mozilla Firefox 一直是迄今为止最友好的开发者网络浏览器;多亏了它,许多其他网络浏览器都包括了针对开发者的最强大的工具集。实际上,许多网络开发者首先为 Firefox 开发,然后才适应其他浏览器。它内置了强大的网络开发者工具。然而,除此之外,您还可以通过附加组件来增强它,这些附加组件为您提供了开发企业应用程序的更强大的工具集。
Firebug (getfirebug.com/) 允许您检查渲染的 HTML 标记,调整 CSS,并自动在页面上看到更新,监控和配置网络活动,以及调试 JavaScript。在下面的屏幕截图中,您可以看到 Firebug HTML 代码检查器:

它不仅是一个扩展,还是一个可以自身扩展的附加组件。它拥有包括 FireCookie、FireUnit、FireQuery 和 PageSpeed 在内的众多扩展,这些扩展增加了更多功能。您可以在getfirebug.com/wiki/index.php/Firebug_Extensions找到 Firebug 扩展的完整列表。
另一个有用的附加组件是 FireStorage 和 HTML5toggle。FireStorage 允许您查看和管理 Firefox 的 HTML5 本地存储,而 HTML5toggle 允许您在测试回退时打开和关闭 HTML5 支持。

注意
如果您想在最终发布之前测试新功能,Mozilla 提供了下载 Firefox 预发布和发布版本的渠道,请访问www.mozilla.org/en-US/firefox/channel/。
您可以找到三个渠道:
-
Firefox: 这是最终发布版本,经过测试和验证
-
Firefox Beta: 这提供了一个相当稳定的较新功能环境
-
Firefox Aurora: 这个渠道是一个带有新功能的实验性发布版本,但不够稳定
Google Chrome
截至 2012 年 6 月,Google Chrome 已经成为世界上使用最广泛的网络浏览器,这是有充分理由的。除了拥有干净简单的用户界面外,它支持一系列扩展和附加组件,内置了多项开发者工具,并且拥有自己的任务管理器,允许您查看和管理您的内存和 CPU 使用情况。这对于调试企业应用程序和开发最佳性能非常有用。

它内置了开发者工具。只需在网页上右键单击,然后点击检查元素。同样,您也可以点击查看菜单,然后选择开发者,再选择开发者工具。这将打开浏览器窗口底部的部分,其中包含许多工具,包括 DOM 和 CSS 编辑器、HTML5 本地存储视图、JavaScript 分析器和性能审计器,如下面的截图所示:

注意
如果您想在最终发布之前尝试新功能,您可以通过以下链接下载 Chrome 的不同发布渠道版本:
www.chromium.org/getting-involved/dev-channel
在这里,您将找到四个不同的频道:
-
稳定频道:这是已经测试过的最终版本。
-
Beta频道:此频道每周更新一次,每六周进行主要更新。
-
Dev频道:此频道每周更新一两次,已测试但可能存在错误。
-
Canary构建:此频道每日更新,尚未测试或使用,可能存在错误,甚至可能无法运行。此版本可以与其他任何频道并行运行。
Safari
苹果公司的 Safari 浏览器既可扩展,又内置了一些开发者工具。然而,开发者工具默认是隐藏的。要启用它们,请转到高级首选项,并勾选在菜单栏中显示开发菜单,如下面的截图所示:

与使用相同 WebKit 引擎的 Google Chrome(Safari)和 Firefox 类似,您可以在网页上的任何位置右键单击并点击检查元素来查看网页检查器。您还可以点击开发菜单并点击显示网页检查器。Safari 的网页检查器提供了几乎与 Chrome 开发者工具相同的工具,包括 DOM 编辑器(在此称为片段编辑器)、分析器和本地存储视图。

此外,Safari 可以通过扩展来增强,尽管其选择不如 Firefox 丰富。Safari 扩展可以在extensions.apple.com找到。
Internet Explorer
在企业应用开发开发者工具的背景下,我们在此仅涵盖 Internet Explorer 9。Internet Explorer 9 内置了开发者工具来检查 DOM 并进行基本编辑。它还包括验证标记的工具、分析器和 HTTP 嗅探器。要查看开发者工具,请点击工具然后点击F12 开发者工具。开发者工具应出现在窗口底部,如下面的截图所示:

开发者工具还包括两个额外功能:浏览器模式和文档模式。浏览器模式功能允许您选择 Internet Explorer 的版本,而文档模式允许您选择在渲染页面时使用哪种渲染引擎模式。这里的区别在于浏览器模式设置默认的文档模式以及网络浏览器如何向服务器标识自己,而文档模式可以通过设置 X-UA-Compatible meta 标签来更改;例如,要模拟 Internet Explorer 8 的行为,我们可以使用以下标签:
<meta http-equiv="X-UA-Compatible" content="IE=8">
Opera
Opera 作为一个研究项目始于 1994 年,于 1996 年首次发布,版本号为 2.0。从那时起,它在多年中稳步增长,尽管它没有像 Google Chrome 那样经历爆炸性增长。尽管如此,它在乌克兰和白俄罗斯等国家被广泛使用,并被认为是市场上的主要网络浏览器之一。
Opera 使用自己的渲染引擎,称为 Vega,以及自己的 JavaScript 引擎,称为 Carakan。它还自带开发者工具,称为 Opera Dragonfly。Dragonfly 包含 DOM 和 CSS 检查器以及 JavaScript 控制台和性能分析器。要查看 Dragonfly,请右键单击网页上的任何位置,然后单击检查元素,或者单击查看,然后导航到开发者工具 | Opera Dragonfly;我们应该会看到一个类似于以下截图中的代码检查器:

HTTP 代理
网页代理可以用来捕获从您的网络浏览器到服务器的 HTTP 流量,以低级别地查看与服务器通信的方式以及返回的数据。尽管 HTTP 代理的使用将在我们讨论调试时更深入地介绍,但熟悉这些工具是有用的。
Charles
Charles 是一个广泛使用的网页代理,拥有 Windows、Mac OS 和 Linux 版本。它包括许多有用的功能,如带宽限制,您可以通过模拟较低带宽的连接来查看您的应用程序在有限连接性下的性能。它还可以用作反向代理,其中它充当中间人并重定向它捕获的流量。这对于调试不支持使用网页代理的设备(如苹果 iPad)上的网页应用程序非常有用。
更多关于 Charles 的信息可以在 www.charlesproxy.com/ 找到。
Fiddler
Fiddler 是一个专门为 Windows 系统构建的网页代理,需要 .NET Framework 版本 2.0 或更高版本。它包含多个插件,允许您扩展其功能,包括语法高亮和流量差异,后者可以比较两个流量配置文件。
更多关于 Fiddler 的信息可以在 www.fiddler2.com/ 找到。
摘要
在本章中,我们介绍了任何企业级应用网络开发所需的各类工具,包括编辑器、集成开发环境(IDE)、网络服务器、网络浏览器和 HTTP 代理。
虽然市面上有许多工具可以帮助您开始 HTML5 企业级应用开发,但了解它们的使用方法非常重要。此外,了解您的需求并找到符合这些需求的工具也同样重要。正如他们所说,“责怪工具的工匠是拙劣的”。
在下一章中,我们将一头扎进我们的 MovieNow 应用,从结构和语义开始。我们将讨论页面的整体布局、所需的语义标签,并介绍如响应式网页设计等技术,这些技术将决定我们的应用在不同设备上的外观。
第三章. 应用:结构和语义
现在我们已经介绍了一些有用的工具和关键思想,我们可以开始我们的企业应用案例研究:MovieNow。
本章将涵盖 HTML 页面结构的主要方面,应用 HTML5 语义标签的正确使用。此外,我们还将介绍微数据的使用以及搜索引擎优化(SEO)的最佳实践。最后,我们将介绍响应式网页设计(RWD)的概念,作为一种支持移动开发的技巧,讨论其中的优缺点以及替代方案。到本章结束时,我们将拥有一个具有基本样式的 HTML 页面,但其布局可以很容易地被任何具有基本 HTML 理解能力的网络开发者阅读。
本章涵盖的主要内容包括:
-
理解页面结构
-
元数据
-
微数据
-
网站图标和图标
-
CSS3 重置
-
固定底部
-
通用样式
-
响应式网页设计和自适应网页设计
理解页面结构
由于我们在前言中已经向您介绍了语义标签和页面元素,我们现在将把那些知识付诸实践,并深入探讨每个标签的意义和使用,同时遵循我们 HTML5 企业应用构建的自然顺序。
网络应用的一个常见布局如下:

任何 HTML 文件的核心结构包括一个DOCTYPE声明,一个html根节点,以及body和head标签:
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
</body>
</html>
从这一点开始,我们可以以任何我们想要的方式定义布局。通常,但并非总是,一个页面将包含一个指定公司或产品标志和一些文本的页眉部分,一个包含版权信息和一些链接到更多信息的页脚部分(如条款和条件),一个包含链接到每个部分的导航区域,以及内容区域。在 HTML5 之前,我们通常会使用class属性或通用 HTML 标签(如div和span标签)的id属性来定义部分。然而,HTML5 通过提供预定义标签来简化了这一点,用于这些标准部分。现在,我们可以使用<header>来包含主要导航和/或初始内容,使用<footer>来包含版权信息和替代导航内容,使用<nav>来定义导航区域,以及使用<section>来定义其他内容容器。这使我们能够从站点到站点标准化我们的内容。
以下是我们可以使用 HTML5 标签定义页面语义的一种方法:
<!DOCTYPE html>
<html lang="en">
<head>
<title>MovieNow</title>
</head>
<body>
<header>MovieNow</header>
<nav></nav>
<section><section>
<footer>
Copyright © 2012 MovieNow. All rights reserved.
</footer>
</body>
</html>
提示
我们可以在一个 HTML 页面中拥有任意数量的header和footer标签,只要它们位于不同的容器中。这听起来可能不太直观,但当你将每个容器视为一个独立于兄弟内容的相关内容逻辑组时,它就有意义了。
作为一般规则,当我们有多个header和footer标签时,它们应该包含在body、article和section标签中。尽管在语义标签的位置上没有技术限制,但我们应保持一种便于网络开发人员和搜索引擎爬虫阅读的结构。
为了添加主要内容,我们可以在我们的section标签中放置一个article标签。在其中,我们可以放置一个h1标签作为主要标题,以及一个p标签作为每个段落。生成的 HTML 看起来如下:
<section>
<article>
<h1>Home Title</h1>
<p>Home content</p>
</article>
</section>
导航列表
无序列表通常是表示网站导航的接受方式,因为它们具有一致的语义结构。因此,在我们的主要nav标签中,我们可以使用一个嵌入了列表元素(li)的无序列表ul:
<ul>
<li><a href="index.html">Home</a></li>
</ul>
二级内容
为了最终确定我们的主结构,我们需要一个侧边栏,该侧边栏将显示前五部票房电影。由于这个部分将作为主要内容的辅助内容,我们将使用aside标签。在aside标签内部,我们将放置一个带有h2标签的标题。我们使用h2而不是h1,因为这代表了页面内容整体大纲的下一级。为了表示电影列表,数字顺序很重要,这意味着最好的结构是使用有序列表。
结果应该看起来像以下代码:
<aside>
<h2>Top 5 Box Office</h2>
<ol>
<li
<h3>Dark Knight Rises</h3>
<p>Action</p>
</li>
<li>
<h3>Avengers</h3>
<p>Action</p>
</li>
<li>
<h3>Ice Age: Continental Drift</h3>
<p>Animation</p>
</li>
<li>
<h3>The Amazing Spider-Man</h3>
<p>Action</p>
</li>
<li>
<h3>Dark Shadows</h3>
<p>Comedy</p>
</li>
</ol>
</aside>
目前不必担心内容。现在,我们将使用示例数据来演示页面结构。在后面的章节中,我们将用来自网络服务的数据填充这个部分。
元数据
到目前为止,我们一直在使用 HTML5 语义标签构建 MovieNow 应用程序的主结构;然而,关于语义标签在 SEO 中效果的一个常见误解是,使用语义标签并不一定意味着更高的搜索引擎排名。尽管如此,它们简化了网络爬虫对内容的分析,这些爬虫会将流量带到你的应用程序,以进行与语义内容相关的特定搜索。本质上,它们使你的应用程序更像一本开放的书。
作为理论上的例子,如果内容被包含在一个具有语义意义的article标签中,那么网络爬虫将更容易确定特定页面中最重要内容,而不是被包含在一个没有语义意义的div标签中。
为了提供搜索引擎数据,将页面内容与将不可避免地带来人们访问你的网站或应用程序的搜索查询相连接,元标签是一个完美的解决方案。元标签存储有关网页的信息——称为元数据——这些信息对最终用户来说不一定可见(除非你揭示了页面源代码)。我们可以指定尽可能多的元标签。搜索引擎的网络爬虫通常会查看这些元标签以获取有关页面内容的更多信息,这些信息无法通过显示内容本身确定。
Meta 标签包含在head标签中,其内容类型由属性name定义,内容由属性content定义。以下是一些最常见的元标签:
<head>
<title>MovieNow</title>
<!-- Charset Encoding -->
<meta charset="utf-8" />
<!-- Description of MovieNow -->
<meta name="description" content="Your movie theater finder" />
<!-- Author or Authors of MovieNow -->
<meta name="author" content="Me" />
<!-- Keywords (recognized by certain search engines -->
<meta name="keywords" content="movie, hollywood" />
<!-- Copyright information -->
<meta name="copyright" content="Copyright © 2012 MovieNow. All rights reserved." />
</head>
通常,搜索引擎结果将显示来自title标签的主要链接文本和出现在meta标签下方的描述,该标签具有name="description"属性。例如,当我们搜索“电影”这个词时,我们将在搜索结果中找到fandango.com:

如果我们通过查看源代码来检查fandango.com上的代码,我们可以在head标签中看到以下内容:
<meta http-equiv="X-UA-Compatible" content="IE=Edge;chrome=1" > <meta name="viewport" content="width=980">
<title>Movie Tickets & Movie Times - Fandango.com</title>
<meta name="robots" content="noydir,noodp" id="MetaRobot" />
<meta name="description" content="Buy movie tickets in advance, find movie times, watch trailers, read movie reviews, and more at Fandango." />
您甚至可以定义自己的元标签。搜索引擎可能会忽略它们,因为它们可能不知道它们,但它们可能对提供特定数据给其他开发者或您可能想要编写的其他应用程序非常有用。
注意
根据 http://googlewebmastercentral.blogspot.com/2009/09/google-does-not-use-keywords-meta-tag.html,谷歌不将keywords元标签考虑在内;相反,它使用description和其他标签,结合一系列特定的谷歌搜索引擎元标签。SEO 专家建议将关键词放在title标签、URL 和H1标签中。
Meta 标签还通过允许网页开发者向网页浏览器告知网页的某些特性,提供额外的功能。
要防止页面自动翻译成客户端语言,您可以指定以下代码:
<meta name="google" content="notranslate" />
要指导谷歌网络爬虫(也称为 Googlebots)的行为,指定以下代码:
<meta name="googlebot" content="..., ..." />
要告诉搜索引擎爬虫是否检查页面的内容,指定以下代码:
<meta name="robots" content="..., ..." />
使用robots元标签,您可以包含以下列表中的任何或所有内容,用逗号分隔:
-
noindex: 这将完全防止页面被索引
-
nofollow: 这可以防止搜索引擎跟踪页面内的链接
-
noarchive: 这可以防止搜索引擎显示页面的缓存链接
例如,以下元标签建议搜索引擎不要索引页面,并跟踪页面上的链接以进行进一步索引:
<meta name="robots" content="noindex, nofollow" />
小贴士
根据每个搜索引擎的实现,元标签上声明的建议可能会被忽略。
如果您的企业应用在 Twitter 上被提及,您可以为 Twitter 添加新的元标签,这些标签将在有人通过 Twitter 分享(发布)指向您页面的链接时被 Twitter 解释以在 Twitter 流中显示。
要在通过推文引用您的页面时包含标题、描述和缩略图图像,您可以添加以下代码:
<meta name="twitter:card" value="summary" />
对于与网站关联的 Twitter 账户,您可以添加以下代码:
<meta name="twitter:site" value="@username" />
这些元标签为 Twitter 定义了某些数据,Twitter 可以使用这些数据来更详细地表达它提到的网页。有关元标签的更多信息,请参阅以下页面:
Facebook 也有它定义的元标签,用于表达关于网页资源的链接的元数据。有关 Facebook 元标签的更多信息,请参阅以下页面:
developers.facebook.com/docs/opengraphprotocol/
微数据
我们可以在页面级别定义元数据,但页面上的特定元素上的元数据怎么办?微数据为我们提供了答案。微数据是用于向 HTML 标签添加更多信息的 HTML 规范。
小贴士
一篇有趣的阅读材料,可以了解谷歌如何管理元数据和微数据,可以在support.google.com/webmasters/bin/answer.py?hl=en&answer=99170找到。
我们之前定义了 HTML5 语法用于电影列表。现在我们可以指定定义电影的每个标签的含义。首先,我们需要使用itemscope属性来识别项目或容器:
<li itemscope>
<h3>Dark Knight Rises</h3>
<p>Action</p>
</li>
现在我们可以使用itemprop属性和定义内容类型的单词来指定内容类型,在这个例子中是name和genre:
<li itemscope>
<h3 itemprop="name">Dark Knight Rises</h3>
<p itemprop="genre">Action</p>
</li>
在这里,我们需要依赖标准的重要性;虽然您可以按您喜欢的任何方式定义微数据,但目标是创建一种统一的方式来定义数据,以便任何网络爬虫或读取器实现都可以读取它。
假设我们决定以易于分析的方式定义 MovieNow 的微数据。我们需要与其他应用程序共享一个共同的模式。一个可能的解决方案是schema.org:
Schema.org 提供了一系列的方案,即 html 标签,网站管理员可以使用这些标签来标记他们的页面,以便主要搜索引擎识别。包括 Bing、Google、Yahoo!和 Yandex 在内的搜索引擎依赖于这种标记来改善搜索结果的显示,使人们更容易找到正确的网页。
使用此网站,我们只需要搜索所需的数据类型。搜索movie,我们得到一个包含电影模式的页面:schema.org/Movie。

如您所见,列表中包括name和genre属性,因此我们只需要使用itemtype属性将模式添加到我们的容器标签中:
<li itemscope itemtype="http://schema.org/Movie">
<h3 itemprop="name">Dark Knight Rises</h3>
<p itemprop="genre">Action</p>
</li>
任何使用此模式的系统都将识别我们的项目为电影以及相应的名称和类型。
小贴士
Google 提供了一个在线工具来测试网站的微数据。这个工具可以在www.google.com/webmasters/tools/richsnippets找到。
Favicon 和图标
现在我们来谈谈描述性属性 link 的用途。随着我们开发应用程序,我们需要图标来代表我们的产品。这些图标不仅可以在我们的 HTML 内部显示,还可以在 iOS 浏览器标签、书签列表以及某些 Android 设备的首页图标中显示。
小贴士
所需的所有图像都位于我们示例文件中的 img 文件夹中。
favicon,或称收藏夹图标,是浏览器用来识别网站或网络应用程序的图像。通常,favicon 的大小为 16 x 16 像素,格式为 .png、.gif(包括动画 GIF)或 .ico – 最后一个是支持最广泛的文件格式。
小贴士
ico 文件格式是由微软 Windows 引入的,用于包含一个或多个不同大小和颜色深度的图像,这样它们可以根据应用程序的要求进行适当的缩放。其他非微软浏览器后来也采用了这种格式以保持兼容性。
要创建 favicon,我们可以使用市场上的任何图形编辑程序,例如 Adobe Photoshop 或 Fireworks。其他可能的解决方案包括网络工具,如favicon.cc (www.favicon.cc/)。Favicon.cc 允许你上传一个图像并使用像素工具进行编辑;这将在下面的屏幕截图中展示:

虽然这是一个很好的工具,但它也有一些缺点,包括缺少图层和撤销/重做功能。
小贴士
当可能时,尽量将 favicon 导出为 ico 格式,除非你想要使用动画 GIF。请注意,ico 是一种文件格式本身,因此你需要使用图像编辑器并将其导出为 ico 格式。仅仅更改扩展名为 ico 是不起作用的。
要让应用程序知道你的 favicon,请在 head 标签内的 link 标签中指定名称、位置和/或格式,如下所示:
<head>
<title>MovieNow</title>
<!-- Charset Encoding -->
<meta charset="utf-8" />
<!-- Description of MovieNow -->
<meta name="description" content="Your movie theater finder" />
<!-- Author or Authors of MovieNow -->
<meta name="author" content="Me" />
<!-- Keywords (recognized by certain search engines -->
<meta name="keywords" content="movie, hollywood" />
<!-- Copyright information -->
<meta name="copyright" content="Copyright © 2012 MovieNow. All rights reserved." />
<!-- favico -->
<link rel="shortcut icon" href="img/favicon.ico" type="image/x-icon" />
</head>
注意到 rel 属性的使用,用于识别图像与网页之间的关系,以及 href 属性的使用,用于指示图像的位置,以及 type 属性用于指定图像的 MIME 类型。结果你将在浏览器标签、地址栏和收藏夹/书签列表中看到一个图像。在 Mac 上的 Firefox 浏览器中,你将看到如下类似的屏幕截图:

注意
默认情况下,如果没有带有 rel="shortcut icon" 的 link 标签,网络浏览器将会在服务器根目录中寻找一个名为 favicon.ico 的文件作为你的 favicon。
自从版本 1.1.3 以来,iOS 设备允许您添加一个主屏幕图标作为移动网站或应用的快捷方式。为了为我们的企业应用添加图标,我们需要考虑这样一个事实:根据设备的不同,图标的大小有多种。对于在 Retina 显示技术之前的 iPhone/iPod,图标应该是 57 x 57 像素,而具有 Retina 显示技术的 iPhone/iPod 应该有 114 x 114 像素的图标。对于在 Retina 显示技术之前的 iPad,图标应该是 72 x 72 像素,而具有 Retina 显示技术的 iPad 应该是 144 x 144 像素。
这里我们可以看到常规显示(左)和 Retina 显示(右)之间的区别:

如果在 head 中没有指定图标,iOS 设备将尝试查找图标。否则,图标将是应用屏幕截图的一部分。
例如,如果您有一个没有 Retina 显示的 iPod,它将尝试在根目录中查找一个具有以下文件名的图标,按照以下列表向下查找:
-
apple-touch-icon-57x57-precomposed.png. -
apple-touch-icon-57x57.png. -
apple-touch-icon-precomposed.png. -
apple-touch-icon.png. -
通过截取屏幕截图并使用其一部分来生成图标。
Retina 显示设备可以使用与非 Retina 显示设备相同大小的图像,但质量会差得多,在某些情况下,您可能会注意到一些像素化。
在我们的企业应用中,我们将使用 head 标签内的 link 标签为每种情况指定图标。在 link 中声明 iOS 图标的做法是使用 rel 属性中的 apple-touch-icon,href 属性中的图标路径,最后在 sizes 属性中指定大小:
<link rel="apple-touch-icon" href="img/touch-icon-iphone-rd.png" sizes="114x114" />
考虑到所有设备,我们应该有一个类似以下代码的样式:
<head>
<title>MovieNow</title>
<!-- Charset Encoding -->
<meta charset="utf-8" />
<!-- Description of MovieNow -->
<meta name="description" content="Your movie theater finder" />
<!-- Author or Authors of MovieNow -->
<meta name="author" content="Me" />
<!-- Keywords (recognized by certain search engines -->
<meta name="keywords" content="movie, hollywood" />
<!-- Copyright information -->
<meta name="copyright" content="Copyright © 2012 MovieNow. All rights reserved." />
<!-- favico -->
<link rel="shortcut icon" href="img/favicon.ico" type="image/x-icon" />
<!-- Apple iOS icons -->
<!—- iPhone 57x57 -->
<link rel="apple-touch-icon" href="img/touch-icon-iphone.png" />
<!—- iPad 72x72 -->
<link rel="apple-touch-icon" href="img/touch-icon-ipad.png" sizes="72x72" />
<!—- iPhone Retina Display 114x114 -->
<link rel="apple-touch-icon" href="img/touch-icon-iphone-rd.png" sizes="114x114" />
<!—- iPad Retina Display 144x144 -->
<link rel="apple-touch-icon" href="img/touch-icon-ipad-rd.png" sizes="144x144" />
</head>
默认情况下,iOS 会为图标添加圆角和反射光泽效果,但我们可以使用 apple-touch-icon-precomposed 而不是 apple-touch-icon 作为 rel 值来移除反射光泽。

上一张图片显示了我们的原始图像、默认的反射光泽效果图标以及没有反射光泽效果的图标之间的区别。在我们的示例文件中,我们使用了一个非反射版本,因为我们想更详细地展示原始图像。然而,这通常只是一个设计细节。
CSS3 重置
现在我们已经有了整体页面结构,我们准备开始为我们的企业应用添加一些样式。在用 CSS 进行样式设计之前,将默认样式重置为在所有浏览器中具有相同的初始条件是一个好习惯。如果您已经知道如何声明 CSS 重置样式,您可以跳过这一部分,继续到 响应式网页设计和适应性网页设计 部分。
CSS 重置定义了一组初始样式,用于移除或标准化浏览器默认值的一些属性,例如边距、填充等。CSS 重置有几种版本;最常见的是Yahoo 用户界面(YUI)CSS 重置(developer.yahoo.com/yui/reset/)、HTML5 Doctor 重置(html5doctor.com/html-5-reset-stylesheet/)、Nicolas Gallagher 的 normalize.css(necolas.github.com/normalize.css/)和 Eric Mayer 的重置(meyerweb.com/eric/thoughts/2011/01/03/reset-revisited/)。
我们将借鉴 Eric Mayer 的重置和 YUI 的重置来构建我们自己的。首先,我们需要创建一个 CSS 文件。将其命名为styles.css,并将其保存在应用程序根目录下的css文件夹中。
为了让 HTML 文件识别并应用,我们必须使用带有属性rel="stylesheet"、type="text/css"和href指向我们的 CSS 文件的link标签来导入文件:
<link rel="stylesheet" href="css/styles.css" type="text/css" />
我们还将导入modernizr,为旧浏览器添加 HTML5 标签支持并检测浏览器功能。head标签应如下所示:
<head>
<title>MovieNow</title>
<!-- Charset Encoding -->
<meta charset="utf-8" />
<!-- Description of MovieNow -->
<meta name="description" content="Your movie theater finder" />
<!-- Author or Authors of MovieNow -->
<meta name="author" content="Me" />
<!-- Keywords (recognized by certain search engines -->
<meta name="keywords" content="movie, hollywood" />
<!-- Copyright information -->
<meta name="copyright" content="Copyright © 2012 MovieNow. All rights reserved." />
<!-- favico -->
<link rel="shortcut icon" href="img/favicon.ico" type="image/x-icon" />
<!-- Apple iOS icons -->
<!—- iPhone 57x57 -->
<link rel=" apple-touch-icon-precomposed" href="img/touch-icon-iphone.png" />
<!—- iPad 72x72 -->
<link rel=" apple-touch-icon-precomposed" href="img/touch-icon-ipad.png" sizes="72x72" />
<!—- iPhone Retina Display 114x114 -->
<link rel=" apple-touch-icon-precomposed" href="img/touch-icon-iphone-rd.png" sizes="114x114" />
<!—- iPad Retina Display 144x144 -->
<link rel=" apple-touch-icon-precomposed" href="img/touch-icon-ipad-rd.png" sizes="144x144" />
<!-- Cascade Style Sheet import -->
<link rel="stylesheet" href="css/styles.css" type="text/css" />
<script src="img/modernizr.js" type="text/javascript"></script>
</head>
在styles.css中,我们将开始重置空间和字体样式。基本的间距涉及margin和padding属性,正如您在图中看到的:

对于这些属性(以及border属性),我们可以以几种方式设置样式。
单个边
您可以将上、右、下、左的margin设置为0,如下所示:
margin-top:0;
margin-right:0;
margin-bottom:0;
margin-left:0;
由于我们使用的是0作为值,所以我们不需要指定单位(%或 px)。您可以将相同的做法应用于填充和边框。
缩写
一种最佳实践是将margin属性声明在一个块中。这被称为缩写。margin和padding的缩写语法从top属性开始,然后以顺时针方向跟随其他属性:
margin:top right bottom left;
我们可以指定仅两个值:
margin:value-1 value-2;
这与以下相同:
margin:value-1 value-2 value-1 value-2;
或者只使用一个值:
margin:value-1;
这等同于:
margin:value-1 value-1 value-1 value-1;
我们还需要重置outline和border,所以把所有这些都放在一起,我们应该有以下内容:
margin:0;
padding:0;
border:0;
outline:0;
小贴士
边框的缩写可以包括颜色和样式。例如,border:5px solid blue;。
此外,我们还需要确保在浏览器中保持文本处理的标准化。一个避免在 Internet Explorer 中过度调整文本大小的修复方法是font-size:100%;。为了强制从父元素继承字体,我们可以使用缩写font:inherit。然而,为了避免与 Internet Explorer 6 和 7 的问题,我们必须使用 CSS 属性font-weight、font-style和font-family。
要使用元素与其父元素的基线设置垂直对齐,我们声明vertical-align:baseline。
到目前为止,我们的重置如下所示:
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video{
margin:0;
padding:0;
border:0;
outline:0;
font-size:100%;
font-weight: inherit;
font-style: inherit;
font-family: inherit;
vertical-align:baseline;
}
默认情况下,表格单元格之间有分隔。为了避免这种情况,我们可以使用 border-collapse:collapse 和 border-spacing:0 重置 table 样式。

重置样式应如下所示:
table{
border-collapse:collapse;
border-spacing:0;
}
我们需要清除 font 样式和权重,因为在某些浏览器中,一些标签会应用特殊的样式,例如粗体和斜体:
address,caption,cite,code,dfn,em,strong,th,var{
font-style:normal;
font-weight:normal;
}
为了从有序(ol)和无序列表(ul)中移除标记,我们可以将 list-style 属性设置为 none:
ol, ul{
list-style:none;
}
为了将主 HTML5 标签设置为块级框并避免浏览器之间的不一致:
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section{
display:block;
}
要从标签中移除引号,对于长引用(blockquote)和短引用(q):
blockquote, q{
quotes:none;
}
为了确保引号在所有浏览器中都能真正消失:
blockquote:before, blockquote:after,
q:before, q:after {
content:'';
content:none;
}
记住,outline 在元素获得焦点时使用,因此我们需要使用 :focus 选择器重新定义或取消它,使用 0。在这种情况下,我们重新定义了一条 1 像素的虚线灰色线:
:focus{
outline:1px dotted #666;
}
看起来这是一个简单的重置需要很多代码,但其中大部分是浏览器实现不一致的结果。重置提供了一个公*的竞技场,我们可以在此基础上构建我们的应用程序。
粘性页脚
CSS 粘性页脚布局允许即使在没有足够内容将其推下时,也能将页脚保持在页面底部。如果内容超过了页面高度,我们的页脚将移动到滚动末尾。
到目前为止,我们的企业应用程序应如下截图所示:

我们希望页脚固定在页面底部。为了实现这一点,有几种实现方式。我们将遵循最常见的一种实现方式,它包括两个容器和一个为我们的 footer 预留空间的 .push 元素:

首先,我们将向当前结构添加一些标签:一个带有 wrapper 类的 section 标签,以将所有标签与页脚分开,一个带有 main 类的 div 标签,以包含同一级别的页面标签,最后是一个带有 push 类的 div 标签,在包装部分内部创建空间,允许 footer 在 wrapper 类之上:
<body class="no-js">
<section class="wrapper">
<div class="main">
<header>MovieNow</header>
<nav>
<ul>
<li><a href="index.html">Home</a></li>
</ul>
</nav>
<aside>
<h2>Top 5 Box Office</h2>
<ol>
<li itemscope itemtype="http://schema.org/Movie">
<h3 itemprop="name">Dark Knight Rises</h3>
<p itemprop="genre">Action</p>
</li>
<li itemscope itemtype="http://schema.org/Movie">
<h3 itemprop="name">Avengers</h3>
<p itemprop="genre">Action</p>
</li>
<li itemscope itemtype="http://schema.org/Movie">
<h3 itemprop="name">Ice Age: Continental Drift</h3>
<p itemprop="genre">Animation</p>
</li>
<li itemscope itemtype="http://schema.org/Movie">
<h3 itemprop="name">The Amazing Spider-Man</h3>
<p itemprop="genre">Action</p>
</li>
<li itemscope itemtype="http://schema.org/Movie">
<h3 itemprop="name">Dark Shadows</h3>
<p itemprop="genre">Comedy</p>
</li>
</ol>
</aside>
<section>
<article>
<h1>Home Title</h1>
<p>Home content</p>
</article>
</section>
<div>
<div class="push"></div>
</section>
<footer>Copyright © 2012 MovieNow. All rights reserved.</footer>
</body>
现在我们来设置我们的结构样式。
我们需要扩展 html、body 和 .wrapper 的高度。
html, body, .wrapper {
height: 100%;
}
然后,我们需要为了兼容性原因添加 height:auto 和 min-height,以及 overflow:hidden 以在内容内部(可以浮动)增长时扩展 .wrapper 容器:
body > .wrapper{
height:auto;
min-height:100%;
overflow:hidden;
}
我们可以将 overflow 应用于 .main 以扩展它:
.main{
overflow:hidden;
}
小贴士
应谨慎使用 overflow:hidden 技术来包围内容。它的最大缺点是隐藏了绝对定位且位于框外的绝对定位内容。一个替代方案是 Clearfix。在这种情况下,我们就不需要在 .main 和 body > .wrapper 中使用 overflow:hidden:
body > .wrapper:after, .main:after {
content:".";
display:block;
height:0;
clear:both;
visibility:hidden;
}
然后,我们可以将相同的高度分配给 footer 和 .push。两者将对齐,并且如果需要添加浮动元素,我们可以使用 clear:
footer,.push{
height:2.0em;
clear:both
}
作为最后一步,我们为 footer 标签分配与 height 相同的负 margin 值,并出于兼容性原因设置 position:relative:
footer{
position:relative;
margin-top:-2.0em;
}
通用样式
使用我们的粘性页脚,我们可以继续进行一些基本样式设置。我们可以设置整体字体家族:
html,*{
font-family:Helvetica,Arial, sans-serif;
}
注意
关于通用选择器 * 的性能影响存在争议。虽然一些作者反对使用它,但其他人说 CSS 选择器在网页性能方面无关紧要。
为了将我们的前五名票房电影定位到右侧并设置其 width 属性:
aside{
float:right;
width:200px;
}
让我们为我们的主要导航菜单添加背景颜色:
nav{
background-color:#666;
}
提示
注意,我们正在使用十六进制颜色的简写版本。
作为装饰,我们可以为 header 添加背景图像。默认情况下,它将*铺图像,除非我们指定 no-repeat 到我们的 background 属性。然后我们可以设置 color 和 height:
header{
color:#fff;
height:122px;
background:#1A1A1A url(../img/logo_back.png);
}
我们可以定义我们的 footer 的颜色,以及 font、font-size、line-height(以垂直居中文本)和 text-align 以水*居中文本:
footer{
background-color:#000;
color:#fff;
font-size:.6em;
line-height:2em;
text-align:center;
}
设置我们的 wrapper 背景颜色:
.wrapper{
background-color:#fff;
}
我们可以为我们的内容定义一个固定的 width,并使用 auto 为侧边距以居中容器标签:
.wrapper,footer{
width:960px;
margin:0 auto;
}
我们在 header 标签内添加一个 div 标签来包含应用程序名称和标志:
<header><div>MovieNow</div></header>
我们使用 text-indent 属性定义 width、height 和我们的标志图像,以隐藏 div 内的文字内容:
header div{
width:320px;
height:122px;
background:url(../img/logo.png);
text-indent:-9999px;
}
使用 -9999px 在 text-indent 中将我们的文本移出可见区域。
提示
为了提高可访问性和 SEO 考虑,保持应用程序名称为文本是一种良好的实践。其他技术可以在css-tricks.com/css-image-replacement/中查看。
设置每侧列的颜色:
html,body{
background-color:#ccc;
}
定义包装器填充:
.wrapper section,nav{
padding:5px 35px;
}
我们随后移除链接下划线,并将导航栏内所有链接的颜色设置为白色:
nav a{
color:#fff;
text-decoration:none;
}
鼠标悬停时添加下划线:
nav a:hover{
text-decoration:underline;
}
改变标题的字体大小:
article h1{
font-size:1.5em;
margin:10px 0 5px;
}
为我们的前五名列表添加颜色:
aside{
padding:30px 0 10px 0;
margin:0 10px;
background-color:#E4E4E4;
}
注意
内容属性允许您定义和增加一个变量:
-
counter-reset:variable;将variable重置为 1 -
counter-increment:variable;将 1 增加到variable -
content:counter(variable);显示标签内variable的值
我们可以添加 padding 和一个 counter 变量来设置我们前五部电影上的数字:
aside ol{
padding:0 0 0 36px;
counter-reset:counter;
}
我们可以使用我们的计数器 content:counter(counter),使用 counter-reset:counter; 重置它,并使用 counter-increment:counter 增加它。然后我们可以添加一些间距并设置字体颜色:
aside ol li:before {
counter-increment:counter;
content:counter(counter) ". ";
margin-right:5px;
color: #333;
position:absolute;
top:0;
left:-16px;
}
提示
计数器属性在 Internet Explorer 7 中不受支持。Internet Explorer 8 仅在指定 !DOCTYPE 时支持它们。
您可以为 IE7 及之前版本包括条件 CSS 导入分配默认的有序列表管理:
<!--[if lte IE 7]>
<link rel="stylesheet" href="css/ie7.css" type="text/css" />
<![endif]-->
其中 ie7.css 包含:
ol{
list-style:decimal;
}
添加更多间距,设置字体大小,并添加装饰性的虚线边框:
aside h2{
padding:0 20px 10px;
margin:0 0 20px;
border-bottom:1px dashed #fff;
font-size:1.3em;
}
为电影类型设置不同的颜色:
aside li p{
color:#999;
}
最后,为电影标题添加 color:
aside li{
color:#666;
font-size:.8em;
line-height:1.2em;
margin:0 0 8px 0;
position:relative;
}
我们的应用程序现在结构化,看起来如下截图所示:

响应式网页设计和适应性网页设计
我们的应用正在成形,但是面对如此多的设备和屏幕分辨率,我们如何支持所有这些设备呢?
响应式网页设计是对这个问题的相当现代的回答。响应式网页设计是通过应用流体网格和媒体查询来适应观看环境而产生的。mediaqueri.es/ 是一个关于如何在现实世界案例中应用响应式网页设计的说明性指南。
使用响应式网页设计,我们可以解决与环境多样性相关的大量问题。
例如,考虑以下我们可以解决可能出现的问题的方法:
-
控制在移动设备上访问网站时网站的大小
-
为视网膜显示屏设备提供高分辨率图像
-
根据使用的设备改变用户体验
在响应式网页设计中,媒体查询检测屏幕分辨率等条件,基于这些条件我们可以应用不同的样式。
媒体查询由指定媒体类型(screen、print 等)和一系列特性(max-width、min-width、min-device-pixel-ratio 等)组成。
例如:
@media screen and (min-device-pixel-ratio: 2) and (max-device-width: 480px) {}
小贴士
对于语法的更详细解释,您可以访问 www.w3.org/TR/css3-mediaqueries/。
以下是三种使用媒体查询的方法:
使用媒体查询导入 CSS 文件
可以使用媒体查询和 head 标签中的 link 标签来指定要导入的 CSS 文件。在以下示例中,当检测到视网膜显示屏并且设备屏幕宽度小于或等于 480 px 时,我们加载 iphone4.css:
<link rel="stylesheet" type="text/css" href="iphone4.css" media=" screen and (-webkit-min-device-pixel-ratio: 2) and (max-device-width: 480px)" />
使用媒体查询从我们的主 CSS 中导入其他 CSS
我们可以使用 @import 在其他 CSS 文件中导入 CSS 文件。在这里,当检测到视网膜显示屏并且设备屏幕 width 小于或等于 480 px 时,我们可以加载 iphone4.css:
@import url(iphone4.css) screen and (-webkit-min-device-pixel-ratio: 2) and (max-device-width: 480px);
在我们的主 CSS 中将媒体查询作为条件使用
这是使用最广泛的技术,它由 CSS 中的媒体查询作为条件组成。我们将使用这项技术为我们的应用程序定义一些媒体查询。
首先,我们为 width 在 738 px 和 1024 px 之间的设备定义特殊样式。这适用于今天市场上许多*板电脑。在这里,我们将移除空格并使用设备的完整宽度,将 width 设置为 wrapper 和 footer 的 100%:
/** TABLETS **/
@media only screen and (min-width: 738px) and (max-width: 1024px){
.wrapper,footer{
width:100%;
}
}
定义小于 737 px 宽度的设备案例:
/** PHONES AND SMALL TABLETS **/
@media only screen and (max-width: 737px){
aside{
display:none
}
.wrapper,footer{
width:100%;
}
.wrapper section,nav{
padding:5px 15px;
}
header div{
background-position:-50px 0px
}
}
我们可以为像素比超过 2 的设备添加一个特殊案例,这是苹果视网膜显示屏设备的情况。在样式表中,我们使用我们标志的高清版本:
/** RETINA DISPLAY IMAGES **/
@media only screen and (-webkit-min-device-pixel-ratio:2),
only screen and (min-device-pixel-ratio: 2){
header div{
background:url(../img/logo2x.png);
-webkit-background-size: 320px 122px;
}
}
小贴士
虽然这种技术不会在需要 Retina 显示图像的 Safari 移动设备上下载我们的标志图像两次,但我们需要考虑其他情况。最好为每种情况定义一个媒体查询,而不是依赖于级联覆盖,这样我们可以避免加载相同的资产多次。您可以在以下链接中看到其他技术:
timkadlec.com/2012/04/media-query-asset-downloading-results/
我们可以通过定义一些额外的样式来为 iPhone 和 iPad Retina 显示添加情况:
/** IPAD RETINA DISPLAY SPECIFIC **/
@media only screen and (-webkit-min-device-pixel-ratio:2) and (min-device-width: 768px) and (max-device-width: 1024px),
only screen and (min-device-pixel-ratio: 2) and (min-device-width: 768px) and (max-device-width: 1024px){
.wrapper,footer{
width:100%;
}
}
/** IPHONE RETINA DISPLAY SPECIFIC **/
@media only screen and (-webkit-min-device-pixel-ratio:2) and (max-device-width: 480px),
only screen and (min-device-pixel-ratio: 2) and (max-device-width: 480px){
}
在 iOS 设备中,缩放值从 100%以上的值开始。因此,我们需要在我们的head标签中添加一行来设置初始缩放为 100%:
<meta name="viewport" content="width=device-width, initial-scale=1" />
小贴士
很遗憾,在切换横屏和竖屏方向时,存在一个缩放错误。多亏了 Scott Jehl,我们可以通过包含一个位于github.com/scottjehl/iOS-Orientationchange-Fix的 JavaScript 库来解决此问题。
如果我们在移动设备上查看我们的应用程序,我们可以看到以下截图:

我们现在可以在多个设备和分辨率上拥有不同的视图:

使用媒体查询的这种方式的副作用之一是,如果您调整浏览器窗口的大小,您将看到不同的视图。另一种方法是使用 JavaScript 检测设备,并根据每种情况导入不同的 CSS 和 JavaScript 文件。这可能很麻烦。然而,您必须确定地考虑所有变体。
响应式网页设计是一种良好的自适应网页设计方法,其中应用程序的行为由设备的性能决定。
摘要
我们涵盖了应用程序的主要结构和基本样式,以及元数据和微数据。我们介绍了图标的使用和常见的 CSS 技术,如固定页脚。最后,我们通过实际应用,涵盖了响应式和自适应网页设计概念,这些应用可以适用于任何企业应用程序。
在下一章中,我们将介绍 HTML5 地理位置功能、AJAX 调用和 API 使用的应用。
第四章:应用:通过地理位置获取电影
HTML5 引入了一种内置的能力,可以确定用户的位置。地理位置 API 定义了一个规范,用于使用 JavaScript 访问用于企业应用程序的位置数据。了解用户的位置对于显示与用户位置相关的新闻和服务非常有用。
我们 MovieNow 应用程序的第一个主要功能是根据地理位置数据找到用户附*的电影列表。我们将介绍地理位置 API 的工作原理,并介绍此功能的实现。由于这是我们第一个功能,我们还将介绍如何使用异步 JavaScript 和 XML(AJAX)进行请求。
我们将介绍以下主题:
-
如何工作
-
API
-
简单请求
-
附*的电影
如何工作
W3C 地理位置 API 规范仅定义了一个接口,通过该接口我们可以获取数据。地理位置数据从何而来以及如何到达则更多是实现的细节。在大多数移动设备上,GPS 通常内置,并通过卫星数据、WiFi 和 GSM/CDMA 基站位置的组合来收集。在桌面设备上,可以使用基于 IP 地址的 Wi-Fi 和地理位置。最后,Google 提供了一个由其 StreetView 数据驱动的地理位置服务。不用说,底层的实现细节不必让我们担心,但了解魔法是如何真正发生的总是好的。
支持以下浏览器:
-
Firefox 3.5+
-
Chrome 5.0+
-
Safari 5.0+
-
Opera 10.60+
-
Internet Explorer 9.0+
支持以下移动设备:
-
Android 2.0+
-
iPhone 3.0+
-
Opera Mobile 10.1+
-
Blackberry OS 6
API
地理位置 API 相当简单,只提供了两种方法:getCurrentPosition() 和 watchPosition()。这些方法在 navigator.geolocation 命名空间下可用,非常相似,但以不同的方式提供设备位置的数据。getCurrentPosition 是一个一次性调用以获取地理位置数据,而 watchPosition 返回地理位置数据,并在设备位置改变时继续重新调用其回调,直到调用 clearWatch 方法。
两种方法都接受相同的三个参数:一个 successCallback 函数,一个 errorCallback 函数,以及一个包含以下属性的 PositionOptions 函数:
-
boolean enableHighAccuracy:这表示应检索最准确的数据,这可能会导致响应时间较慢。 -
long timeout:这表示在请求应该超时之前的最长毫秒数。 -
long maximumAge:这表示应返回不超过指定毫秒数的缓存内容。如果设置为0,则始终返回新的位置数据。
两种方法也向 successCallback 函数返回一个 Position 对象,该对象包含以下属性:
-
coords.latitude:这表示十进制度数形式的纬度 -
coords.longitude: 这表示经度,以十进制度为单位 -
coords.altitude: 这表示相对于参考椭球的高度,以米为单位 -
coords.accuracy: 这表示纬度和经度的精度,以米为单位 -
coords.altitudeAccuracy: 这表示海拔的精度,以米为单位 -
coords.heading: 这表示设备相对于真北方向顺时针的行进方向,以度为单位 -
coords.speed: 这表示当前地面速度,以米/秒为单位 -
timestamp: 这表示获取位置时的日期和时间
最后,当调用errorCallback参数时,它将接收一个PositionError对象,该对象包括以下属性:
-
code: 这表示错误类型。这可以是以下值之一:PERMISSION_DENIED(1)、POSITION_UNAVAILABLE(2)和TIMEOUT(3)。 -
message: 这显示了错误的详细信息。
一个简单的请求
现在我们已经了解了地理位置 API 的机制,让我们来讨论一个实际请求。看看下面的代码片段:
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(successCallback);
}
这是我们可以做的最基本调用。首先,由于地理位置信息不是所有设备都支持,我们必须小心避免由于不支持而导致的意外错误,这需要检查是否支持,这就是if语句的作用所在。其次,我们调用getCurrentPosition方法,传入一个successCallback函数。successCallback可以是我们在位置返回时想要调用的任何函数。注意缺少的errorCallback函数和选项参数。这些参数是严格可选的,尽管实现它们以应对意外错误条件是一种良好的实践。
你附*的电影
要开始将地理位置信息添加到我们的 MovieNow 企业应用程序中,我们首先需要对我们的页面进行一些调整,该页面我们在第三章中设置,即应用:结构和语义。在article标签中,我们将添加一个button标签和一个div标签:
<article>
<h1>Home Title</h1>
<p>Home content</p>
<button id="find-movies">Find Movies</button>
<div id="movies-near-me">
</div>
</article>
button标签将用于调用获取电影数据的行为,而div标签则是数据将显示的位置。如果一切顺利,你的屏幕应该显示一个标签为查找电影的按钮,如下面的截图所示:

接下来,你可能还记得在index.html底部包含的一些 JavaScript 引用。让我们再添加三个 JavaScript 引用。看看下面的代码片段:
<footer>Copyright © 2012 MovieNow. All rights reserved.</footer>
<script src="img/ios-orientationchange-fix.js"></script>
<script src="img/jquery-1.8.0.min.js"></script>
<script src="img/jquery.xdomainajax.js"></script>
<script src="img/movienow.js"></script>
<script src="img/movienow.geolocation.js"></script>
</body>
你可能已经注意到了jquery.xdomainajax.js的包含。这是 jQuery 库的一个扩展,允许进行跨域 AJAX GET 请求。回到 Netscape Navigator 2.0,浏览器已经实现了同源策略,这是一种安全预防措施,限制了来自一个站点的页面无法访问另一个站点的页面属性和方法。在当时这是有意义的,但随着日益流动的万维网,许多站点的内容可以“混合”成一个统一体验,这些边界已经不可避免地被绕过了。有许多解决方案,包括带有填充的 JavaScript 对象表示法(JSONP),它允许通过传递回调参数进行跨域 AJAX 请求,因此被调用的服务可以将结果 JSON 对象包装在作为回调传递的函数中。
小贴士
可以在github.com/padolsey/jQuery-Plugins/tree/master/cross-domain-ajax/找到跨域 Ajax 库。这个库的所有功劳都归功于 James Padolsey。
接下来,我们将把跨域 Ajax 库添加到js文件夹中,然后在js文件夹中创建两个新文件:movienow.js和movienow.geolocation.js。在movienow.js中,我们将建立我们的根命名空间movienow。这将位于global或window作用域,这意味着它可以在任何地方访问。这就是我们可以根据需要向我们的企业应用程序添加核心功能的地方。一开始,我们这里需要的只有以下这一行,它设置了根命名空间:
var movienow = {};
在movienow.geolocation.js中,我们将添加我们的地理位置特定功能。我们这样做的原因是为了确保我们在企业应用程序开发中遵循模块化方法。模块化迫使我们把功能分解成离散的、高度内聚的、松散耦合的部分。模块化允许我们改变企业应用程序的某些部分,而不会影响整体。这就像可拆卸电池的手机和电池焊接在手机里的手机之间的区别。如果电池坏了,模块化意味着更换损坏的部分与更换整个设备之间的区别。
自调用
我们将首先获取对我们已建立的命名空间的引用。这是一种良好的防御性做法,以防你的核心命名空间 JavaScript 文件发生任何问题。
var movienow = movienow || {};
小贴士
注意,拥有这个声明并不是必须的,以包含我们的命名空间初始定义时所需的movienow.js文件。
接下来,我们将建立我们的地理位置命名空间:
movienow.geolocation = (function(){})();
注意第二个括号。这种结构被称为立即调用的函数表达式(IIFE)。这是一种方便的简写方式,可以以模块化的方式注册和立即调用 JavaScript 代码。所有关于地理位置的属性和方法都将被封装在 movienow.geolocation 命名空间中,这使得在全局命名空间中的占用更小,代码更干净、更模块化。
那就变成了这个
在我们新建立的命名空间声明中,我们将做几件事情。首先,我们需要捕获对对象的引用。我们将通过添加以下行来实现:
var that = this;
这可能看起来像一条有趣的话,但它的重要性将变得明显。JavaScript 中的 this 关键字是一个方便的函数,用于引用执行函数的所有者或函数是方法的对象。没有它,我们就需要在命名空间内的所有属性和方法前加上命名空间本身,当你想要更改命名空间时,这会变得很复杂。
以下说明了 this 关键字的价值:
var myNamespace = {
firstFunction: function() {
document.write('firstFunction invoked.');
myNamespace.secondFunction();
},
secondFunction: function() {
document.write('secondFunction invoked.');
}
};
myNamespace.firstFunction();
注意使用 myNamespace 来引用对象内的其他方法。我们可以用 this 来替换它,以便以更无差别的引用方式引用对象内的其他成员:
var myNamespace = {
firstFunction: function() {
document.write('firstFunction invoked.');
this.secondFunction();
},
secondFunction: function() {
document.write('secondFunction invoked.');
}
};
myNamespace.firstFunction();
不幸的是,当上下文改变时,this 也会改变。当我们在一个函数内部添加一个函数时,上下文将是外部函数的上下文:
var myNamespace = {
firstFunction: function() {
document.write('firstFunction invoked.');
var innerFunction = (function() {
this.secondFunction();
})();
},
secondFunction: function() {
document.write('secondFunction invoked.');
}
};
myNamespace.firstFunction();
在这里我们添加了 innerFunction,它调用了 secondFunction(注意立即调用的函数表达式)。然而,secondFunction 从未被调用。这是因为 this 的上下文已经改变为 firstFunction 的上下文。为了保持对 myNamespace 上下文的引用,我们只需声明一个变量并保留它:
var myNamespace = {
firstFunction: function() {
document.write('firstFunction invoked.');
var that = this;
var innerFunction = (function() {
that.secondFunction();
})();
},
secondFunction: function() {
document.write('secondFunction invoked.');
}
};
myNamespace.firstFunction();
而这就是 that 变成 this 的地方。
获取位置
到目前为止,我们放在页面上的 查找电影 按钮是非功能的。点击它,什么也不会发生。我们将为该按钮添加一个事件处理程序,以便当你点击它时会发生一些事情。在 movienow.geolocation 对象内部添加以下内容:
jQuery(document).ready(function(){
jQuery('#find-movies').click(function(){
alert('Button clicked!');
});
});
movienow.geolocation.js 文件现在应该看起来像以下代码:
var movienow = movienow || {};
movienow.geolocation = (function(){
var that = this;
jQuery(document).ready(function(){
jQuery('#find-movies').click(function(){alert('Button clicked!');});
});
})();
现在点击 查找电影。你应该会得到以下提示框:

这可能看起来很好,但我们的目标要高得多。我们想要获取一些位置数据。我们通过添加几个方法来实现:getLocation 和 locationCallback:
this.getLocation = function(){
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(this.locationCallback);
}
};
this.locationCallback = function(loc){
jQuery('#movies-near-me').html('Lat:' + loc.coords.latitude + ', Long: ' + loc.coords.longitude);
};
第一个函数当然是调用前面讨论过的 getCurrentPosition 方法的地方。第二个函数是 successCallback。我们现在可以移除 查找电影 按钮的事件处理程序中的提示框,并替换为以下内容:
that.getLocation();
movienow.geolocation.js 文件现在应该看起来像以下代码:
var movienow = movienow || {};
movienow.geolocation = (function(){
var that = this;
jQuery(document).ready(function(){
jQuery('#find-movies').click(function(){that.getLocation();});
});
this.getLocation = function(){
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(this.locationCallback);
}
};
this.locationCallback = function(loc){
jQuery('#movies-near-me').html('Lat:' + loc.coords.latitude + ', Long: ' + loc.coords.longitude);
};
})();
现在当你点击 查找电影 按钮时,将通过地理位置 API 发出位置数据请求。
网络浏览器通常会提示您允许跟踪您的物理位置。以下截图显示了 Safari、Chrome 和 Firefox 的示例。

这只会发生一次。当您点击允许时,浏览器将为指定的域名保存此设置。

您现在应该能在页面上看到纬度和经度。恭喜!您的企业应用程序现在知道您的位置了。
获取邮政编码
现在我们有了地理坐标,下一步是将它们映射到邮政编码。一旦我们有了邮政编码,我们就可以获取电影列表。为了获取邮政编码,我们需要向网络服务发送一个 AJAX 请求,发送纬度和经度,然后接收邮政编码。有许多网络服务提供这些数据。对于我们的 MovieNow 企业应用程序,我们将使用来自 geonames.org 的服务。
注意
GeoNames 地理数据库覆盖所有国家,包含超过八百万个可免费下载的地名。它采用 Creative Commons Attribution 3.0 许可。
Geonames.org 提供了一个方便的名为 findNearbyPostalCodesJSON 的网络服务,用于获取邮政编码数据。此服务接受以下参数:
-
lat: 这指定了十进制度数形式的纬度 -
lng: 这指定了十进制度数形式的经度 -
radius: 这指定了半径(千米) -
maxRows: 这指定了要返回的最大行数 -
style: 这指定了响应的详细程度(SHORT,MEDIUM,LONG,FULL) -
country: 这指定了要查找的国家 -
localCountry: 当此参数设置为true时,只返回国家内的代码 -
username: 这是您访问数据的账户
以下是一个示例服务调用:
api.geonames.org/findNearbyPostalCodesJSON?lat=45&lng=-66.7&username=demo
它返回以下 JSON 输出:
{
"postalCodes": [
{
"distance": "10.13582",
"adminCode1": "NB",
"postalCode": "E5H",
"countryCode": "CA",
"lng": -66.769962,
"placeName": "Pennfield",
"lat": 45.076588,
"adminName1": "New Brunswick"
}
]
}
您可以将此 URL 复制/粘贴到网络浏览器中,亲自查看。
小贴士
网络服务受到限制,意味着对于给定的用户名,每天只能服务一定数量的请求。这就是为什么您在继续之前应该在 geonames.org 上注册自己的账户。一旦这样做,就用您的用户名替换 demo。
现在我们有了将坐标映射到邮政编码的能力,我们需要发送一个 AJAX 请求来调用并检索数据。我们将使用 jQuery 来帮助我们进行请求。
AJAX 不仅仅是一种清洁产品
代表异步 JavaScript 和 XML(Asynchronous JavaScript and XML),AJAX 是一种技术,通过使用 XMLHttpRequest 对象调用服务器以获取额外内容、保存状态、轮询资源等。这是一种在不刷新页面的情况下扩展页面功能的有用方式。
jQuery 库(jquery.com)使得在跨浏览器兼容的方式下进行 AJAX 请求变得相当简单直接。看看以下代码:
jQuery.ajax({
url: 'http://some-domain.com/some-web-service',
data: 'q=something'
success: function(payload){
alert(payload);
},
error: function(error){
alert(error.responseText);
}
});
您只需设置 URL 和参数。您可以定义一个成功事件处理程序和一个错误事件处理程序。当 AJAX 请求成功完成并传递有效负载作为参数时,将调用成功处理程序。当 AJAX 请求返回除 200 状态码之外的内容时,将调用错误处理程序。
将以下代码片段添加到您的 movienow.geolocation 对象中:
this.reverseGeocode = function(loc){
jQuery.ajax({
url: 'http://api.geonames.org/findNearbyPostalCodesJSON',
data: 'lat=' + loc.coords.latitude + '&lng=' + loc.coords.longitude + '&username=demo', //Swap in with your geonames.org username
success: function(payload){
var data = that.objectifyJSON(payload);
var postalCodes = [];
for (var i=0; i<data.postalCodes.length; ++i) {
postalCodes.push(data.postalCodes[i].postalCode);
}
jQuery('#movies-near-me').html(postalCodes.join(','));
}
});
};
this.objectifyJSON = function(json) {
if (typeof(json) == "object") {
return json;
}
else {
return jQuery.parseJSON(json);
}
};
小贴士
我们正在使用 alert 弹出窗口显示错误,但对于最终应用程序,我们应该定义一个 CSS 样式的 DOM 来显示通知和错误。
将 locationCallback 的内容替换为以下内容:
that.reverseGeocode(loc);
当调用 successCallback 函数时,我们将获取 Positon 对象并将其传递给我们的 reverseGeocode 方法,该方法通过向 geonames.org 网络服务发出 AJAX 请求来检索设备的邮政编码。在 AJAX 请求的成功处理程序中,我们从 JSON 对象中提取邮政编码并将它们放入一个数组中。然后我们在页面上显示这个数组。注意 objectifyJSON 方法。我们这样做是因为一些浏览器会自动将有效负载数据打包成一个对象,而其他浏览器则将其视为一个字符串。
movienow.geolocation.js 文件现在应该看起来像以下代码:
var movienow = movienow || {};
movienow.geolocation = (function(){
var that = this;
jQuery(document).ready(function(){
jQuery('#find-movies').click(function(){that.getLocation();});
});
this.getLocation = function(){
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(this.locationCallback);
}
};
this.locationCallback = function(loc){
that.reverseGeocode(loc);
};
this.reverseGeocode = function(loc){
jQuery.ajax({
url: 'http://api.geonames.org/findNearbyPostalCodesJSON',
data: 'lat=' + loc.coords.latitude + '&lng=' + loc.coords.longitude + '&username=demo', //Swap in with your geonames.org username
success: function(payload){
var data = that.objectifyJSON(payload);
var postalCodes = [];
for (var i=0; i<data.postalCodes.length; ++i) {
postalCodes.push(data.postalCodes[i].postalCode);
}
jQuery('#movies-near-me').html(postalCodes.join(','));
},
error: function(error){
alert(error.responseText);
}
});
};
this.objectifyJSON = function(json) {
if (typeof(json) == "object") {
return json;
}
else {
return jQuery.parseJSON(json);
}
};
})();
当您点击 查找电影 时,您应该看到以下内容,如截图所示:

从邮政编码到放映时间
现在我们有了邮政编码,我们可以将这些映射到电影放映时间。不幸的是,没有免费的网络服务可以从那里获取这类数据。然而,并非全无希望。Moviefone.com 提供基于邮政编码的馈送。但是有一个问题,由于跨域限制,我们无法轻松地通过 JavaScript 获取馈送数据。跨域 Ajax 库仅适用于返回 JSON 的服务。为了解决这个问题,我们可以创建一个代理。
创建一个名为 movielistings.php 的文件。将以下内容添加到您新创建的文件中:
<?php
$zips = $_GET['zip'];
$zips = explode(',', $zips);
$listings = array();
for ($i=0; $i<count($zips); $i++) {
$listings[$i] = file_get_contents('http://gateway.moviefone.com/movies/pox/closesttheaters.xml?zip=' . $zips[$i]);
$listings[$i] = simplexml_load_string($listings[$i]);
}
echo json_encode($listings);
?>
这是一个简单的 PHP 文件,它根据查询字符串中传递的邮政编码字符串向 Moviefone.com 的最*影院馈送发出请求,并将输出转换为 JSON。要运行此文件,您需要确保您的机器上已安装 PHP。否则,我们可以轻松地使用 JSP、ASP.NET 或 Node.js 等编写类似的内容。
一旦我们有了我们的电影列表代理服务,我们可以在 movienow.geolocation 中添加以下内容:
this.getShowtimes = function(postalCodes) {
jQuery.ajax({
url: 'movielistings.php',
data: 'zip=' + postalCodes.join(','),
success: function(payload){
var data = that.objectifyJSON(payload);
that.displayShowtimes(that.constructMoviesArray(data));
},
error: function(error){
alert(error.responseText);
}
});
};
this.constructMoviesArray = function(data) {
var key, movie, theater = null;
var movies = {};
movies.items = {};
movies.length = 0;
for (var j=0; j<data.length; ++j) {
if (data[j].movie) {
theater = data[j].theater;
for (var i=0; i<data[j].movie.length; ++i) {
movie = data[j].movie[i];
key = movie.movieId + '|'+ theater.theaterId;
if (!movies.items[key]) {
movie.theater = theater;
movies.items[key] = movie;
movies.length++;
}
}
}
}
return movies;
};
this.displayShowtimes = function(movies) {
var movie = null;
var html = '';
for (var item in movies.items) {
movie = movies.items[item];
html += '<p><strong>' + movie.title + '</strong><br />' + movie.showtime.join(',') + '</p>';
}
jQuery('#movies-near-me').html(html);
};
完成后,将 reverseGeocode 方法中填充 #movies-near-me 的那一行替换为以下代码:
that.getShowtimes(postalCodes);
因此,我们增加了三个新方法:getShowtimes、constructMoviesArray 和 displayShowtimes。getShowtimes 方法向电影列表代理发送 AJAX 请求,获取返回的 JSON 数据,并调用 constructMoviesArray 提取相关数据并删除重复项,然后调用 displayShowtimes 显示数据。
最终的 movienow.geolocation.js 文件现在应该看起来像以下代码:
var movienow = movienow || {};
movienow.geolocation = (function(){
var that = this;
jQuery(document).ready(function(){
jQuery('#find-movies').click(function(){that.getLocation();});
});
this.getLocation = function(){
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(this.locationCallback);
}
};
this.locationCallback = function(loc){
that.reverseGeocode(loc);
};
this.reverseGeocode = function(loc){
jQuery.ajax({
url: 'http://api.geonames.org/findNearbyPostalCodesJSON',
data: 'lat=' + loc.coords.latitude + '&lng=' + loc.coords.longitude + '&username=demo',
success: function(payload){
var data = that.objectifyJSON(payload);
var postalCodes = [];
for (var i=0; i<data.postalCodes.length; ++i) {
postalCodes.push(data.postalCodes[i].postalCode);
}
that.getShowtimes(postalCodes);
},
error: function(error){
alert(error.responseText);
}
});
};
this.objectifyJSON = function(json) {
if (typeof(json) == "object") {
return json;
}
else {
return jQuery.parseJSON(json);
}
};
this.getShowtimes = function(postalCodes) {
jQuery.ajax({
url: 'movielistings.php',
data: 'zip=' + postalCodes.join(','),
success: function(payload){
var data = that.objectifyJSON(payload);
that.displayShowtimes(that.constructMoviesArray(data));
}
});
};
this.constructMoviesArray = function(data) {
var key, movie, theater = null;
var movies = {};
movies.items = {};
movies.length = 0;
for (var j=0; j<data.length; ++j) {
if (data[j].movie) {
theater = data[j].theater;
for (var i=0; i<data[j].movie.length; ++i) {
movie = data[j].movie[i];
key = movie.movieId + '|'+ theater.theaterId;
if (!movies.items[key]) {
movie.theater = theater;
movies.items[key] = movie;
movies.length++;
}
}
}
}
return movies;
};
this.displayShowtimes = function(movies) {
var movie = null;
var html = '';
for (var item in movies.items) {
movie = movies.items[item];
html += '<p><strong>' + movie.title + '</strong><br />' + movie.showtime.join(',') + '</p>';
}
jQuery('#movies-near-me').html(html);
};
})();
当你点击 查找电影 按钮时,你应该看到以下截图:

当然,我们还有更多数据要展示,但将在后面的章节中介绍。
摘要
在本章中,我们介绍了地理位置 API 的工作原理以及如何使用它。我们在企业应用中添加了一个按钮,并将其连接到地理位置 API 以发送请求。我们使用返回的 Position 对象的坐标向一个网络服务发送 AJAX 请求以获取这些坐标的邮政编码。使用邮政编码,我们向一个源请求电影放映时间数据,并在页面上显示这些数据。
在下一章中,我们将介绍在本章中为我们自己提供的大量数据。我们将更深入地介绍 CSS,并讨论 CSS3 中的新特性。我们甚至将构建一些巧妙的 CSS3 特效,使我们的企业应用看起来更有趣、更有吸引力。
第五章。应用:通过 CSS3 显示电影数据
我们已经在第三章中添加了一些使用 CSS 的企业应用样式,应用:结构和语义,但我们还没有介绍使 CSS3 成为变革者的属性。在本章中,我们将遍历一些有用的 CSS3 属性和针对我们应用的实用实现,解释它们在任何 Web 应用中的范围。
每个示例都将展示对市场上最受欢迎的 Web 浏览器的支持(以及当需要时的回退)。
本章涵盖的主要主题如下:
-
回到浏览器的 babel 塔
-
CSS 魔法:为 MovieNow 添加更多样式(圆角、颜色、渐变、阴影、文本阴影)
-
电影和样式(过渡和动画)
-
在过渡和动画之间选择
-
使用媒体查询
-
应用 CSS3 选择器
回到浏览器的 babel 塔
每当你开始使用一个新的 CSS 属性时,检查支持该属性的浏览器列表是必要的。如果它被支持,你需要验证如何实现它,以及是否需要前缀或特殊形式,例如在 Internet Explorer 中的 filter。
以下是最常见的 CSS 属性前缀:
-
-moz-Firefox -
-webkit-Safari、Safari iOS 和 Chrome -
-o-Opera -
-ms-Internet Explorer
当你没有任何属性的支持时,解决方案包括使用图像和移除一些视觉元素(遵循优雅降级并尽量避免移除功能)。
小贴士
我们可以使用像 Lea Verou 的 -prefix-free (leaverou.github.com/prefixfree/) 这样的 JavaScript 库来避免使用多个供应商前缀,但这可能会影响我们应用的性能。作为一般规则,CSS 几乎总是比 JavaScript(执行时间)更快,所以在性能方面,在样式表中多几行代码是值得的。
正如我们在第三章中看到的,应用:结构和语义,添加条件 CSS 导入是可能的。这项技术仅适用于 Internet Explorer,你可以使用以下语法来比较版本:
-
lt(小于) -
lte(小于或等于) -
gt(大于) -
gte(大于或等于)
例如,如果你想为 Internet Explorer 7 及之前版本添加特定的 CSS 文件,可以使用以下声明:
<!--[if lte IE 7]>
<link rel="stylesheet" href="css/ie7.css" type="text/css" />
<![endif]-->
将针对 Internet Explorer 的特定黑客和回退放在单独的样式表中是一种良好的实践,以实现清晰的编码并避免在其他浏览器中增加额外的加载时间。
小贴士
要添加对常见的 CSS3 功能,如 border-radius 和 box-shadow 的支持,你可以包含 CSS3 Pie (css3pie.com),这是一个 JavaScript 库,它为 Internet Explorer 6 到 9 添加了这些功能的支持。
CSS3 魔法 – 为 MovieNow 添加更多样式
让我们继续我们的电影应用程序开发。作为一个一般规则,您应该提前规划,换句话说,在开始与样式纠缠之前,应该有一个视觉设计。遵循此规则(并且最好有一个样式指南)的好处是,您的应用程序将反映统一的视觉身份。让我们开始为我们的企业应用程序中已知的一些元素添加样式。
我们移除了我们的查找电影按钮,以便稍后进行自动调用。
添加圆角
如果您必须使用 CSS1 和 CSS2 创建圆角,您应该知道可能的圆角解决方案有多么复杂。通常,它们涉及图像或影响企业应用程序性能的重量级 JavaScript 处理。
在 CSS3 中,我们有border-radius属性,允许我们指定元素的四个边框的圆角形状。
这个属性的语法如下:
border-radius:top-left-radius top-right-radius bottom-right-radius top-left-radius;
您可以为每个角落指定%、em和px作为单位。
假设我们需要为我们的Top 5 Box Office部分的底部添加圆角。我们可以使用border-radius属性(以及每个浏览器的带前缀的等效属性),对于top left和right使用0,对于bottom left和right使用8px。
aside{
float:right;
width:200px;
padding:30px 0 10px 0;
margin:0 10px;
background-color:#E4E4E4;
/** TOP 5 ROUNDED BORDER **/
border-radius:0 0 8px 8px;
-moz-border-radius:0 0 8px 8px;
-webkit-border-radius:0 0 8px 8px;
-o-border-radius:0 0 8px 8px;
}
应用此方法,我们可以看到原始 Box Office(左侧)和border-radius效果(右侧)之间的区别。

小贴士
注意,如果我们使用简写border-radius:0 8px,它只为右上角和左下角添加圆角。
这个属性是border-top-left-radius、border-top-right-radius、border-bottom-left-radius和border-bottom-right-radius属性的简写。
很遗憾,在 Internet Explorer 的情况下,该属性仅从 IE9 开始支持。
小贴士
作为替代方案,您可以使用 CSS3 Pie (css3pie.com) 或 Curved Corner (code.google.com/p/curved-corner/)) 为旧版本的 Internet Explorer 提供支持。
设置颜色
在我们的样式表中描述颜色的方法有多种;最常见的是十六进制#rrggbb,其中第一对表示红色的数值,第二对表示绿色,最后一对表示蓝色。此外,我们可以使用简写表示法#rgb,它将我们的值转换为#rrggbb,例如,如果我们使用#123,它将被识别为#112233。
让我们来看看描述颜色的其他方法:
红色、绿色和蓝色
您可以使用语法rgb(R,G,B)定义颜色,其中 R、G 和 B 表示红色、绿色和蓝色的强度,可以是:
-
一个从 0(无颜色)到 255(最大强度)的整数
-
从 0.0%(无颜色)到 100.0%(最大强度)的浮点数
您必须在声明中使用相同的单位。它在所有现代浏览器中都受支持。这里有一个使用红色字体的title类:
.title{
color:rgb(255,0,0);
}
红色、绿色、蓝色和透明度
一个扩展的 rgb 规范,在末尾添加一个用于 alpha 透明度的值,其值从 0.0(不可见)到 1.0(完全可见)。它被所有现代浏览器和从版本 9 开始的 Internet Explorer 支持。我们可以在类 title 中定义带有 50% alpha 透明度的红色字体:
.title{
color:rgba(255,0,0, .5);
}
色调、饱和度和亮度
HSL 是颜色的圆柱坐标表示。vHue 是角度的浮点表示;这个值定义了将应用饱和度和亮度的颜色,其值范围从 0 到 360。饱和度是一个百分比,从 0(白色)到 100%(全色)定义了颜色的鲜艳度。最后,亮度定义了光的量,从 0%(无光,全黑)到 100%(全色)。语法是 hsl(H,S,L)。它被所有现代浏览器和从版本 9 开始的 Internet Explorer 支持。如果我们想在类 title 中应用红色字体,我们可以这样做:
.title{
color:hsl(0,100%,100%);
}
色调、饱和度、亮度和透明度
这是一个扩展的 hsl 规范,它以与 rgba 对 rgb 的方式相同的方式在末尾添加一个用于 alpha 透明度的值。它被所有现代浏览器和从版本 9 开始的 Internet Explorer 支持。我们可以如下定义类 title 中带有 50% alpha 透明度的红色字体:
.title{
color:hsla(0,100%,100%, .5);
}
你可以使用条件 CSS 导入为旧版本的 Internet Explorer 应用透明度和 alpha 过滤器以获得相同的效果:
.title{
color:#f00;
opacity: 0.5;
filter: alpha(opacity=50);
}
添加渐变
市场上的新应用采用了简洁的设计,这并非因为技术限制,而是为了简化。尽管有时有必要添加一些样式来模拟深度,但渐变使这个过程变得容易得多。
CSS3 引入了 linear-gradient 和 radial-gradient 到 background 值。你可以将 渐变 应用到 background 或 background-image 属性。
这种可能的语法如下:
background-image:linear-gradient(angle, color position, color position);
你可以添加任意多的 颜色位置 对。尽管可以使用十六进制颜色,但在这个例子中我们将使用 rgb。
首先,我们在导航栏中添加一个从 top 到 bottom 的渐变。它从浅灰色开始,以浅灰色结束,所以我们只需要两个点:0% 和 100%。初始颜色将是 rgb(102,102,102),最终颜色将是 rgb(70,70,70)。将此添加到带有相关前缀的 nav 中,我们有:
nav{
background-color:#666;
background-image:linear-gradient(top, rgb(102,102,102) 0%, rgb(70,70,70) 100%);
background-image:-moz-linear-gradient(top, rgb(102,102,102) 0%, rgb(70,70,70) 100%);
background-image:-webkit-linear-gradient(top, rgb(102,102,102) 0%, rgb(70,70,70) 100%);
background-image:-o-linear-gradient(top, rgb(102,102,102) 0%, rgb(70,70,70) 100%);
background-image:-ms-linear-gradient(top, rgb(102,102,102) 0%, rgb(70,70,70) 100%);
}
结果我们可以看到与左侧原始图像相比的正确图像:

提示
如果你不想处理每个供应商的前缀,一个选择是使用生成器,例如 Colorzilla 渐变生成器(www.colorzilla.com/gradient-editor/)。你只需要使用可视化工具定义你的渐变,然后将生成的代码复制到你的 CSS 中。
为了说明我们可以添加多个点,让我们给我们的 Top 5 Box Office 区域应用一个更复杂的效果。在这种情况下,我们从 bottom 到 top 应用效果:
aside{
float:right;
width:200px;
padding:30px 0 10px 0;
margin:0 10px;
background-color:#E4E4E4;
/** TOP 5 ROUNDED BORDER **/
border-radius:0 0 8px 8px;
-moz-border-radius:0 0 8px 8px;
-webkit-border-radius:0 0 8px 8px;
-o-border-radius:0 0 8px 8px;
/** BOX OFFICE GRADIENT **/
background:linear-gradient(bottom, rgb(200,200,200) 35%, rgb(210,210,210) 68%, rgb(220,220,220) 98%, rgb(80,80,80) 100%);
background:-o-linear-gradient(bottom, rgb(200,200,200) 35%, rgb(210,210,210) 68%, rgb(220,220,220) 98%, rgb(80,80,80) 100%);
background:-moz-linear-gradient(bottom, rgb(200,200,200) 35%, rgb(210,210,210) 68%, rgb(220,220,220) 98%, rgb(80,80,80) 100%);
background:-webkit-linear-gradient(bottom, rgb(200,200,200) 35%, rgb(210,210,210) 68%, rgb(220,220,220) 98%, rgb(80,80,80) 100%);
background:-ms-linear-gradient(bottom, rgb(200,200,200) 35%, rgb(210,210,210) 68%, rgb(220,220,220) 98%, rgb(80,80,80) 100%);
}
如同我们之前的例子,我们使用百分比来定义位置。在这种情况下,我们使用 35%、68% 和 98%。
最后,我们可以比较原始区域(左侧)与最终区域(右侧),如下面的截图所示:

我们可以将相同的原理应用到我们的页眉上:
header{
color:#fff;
height:122px;
/** HEADER GRADIENT **/
background-color:#1A1A1A;
background-image:url(../img/logo_back.png), linear-gradient(right, rgb(26,26,26) 35%, rgb(40,40,40) 68%, rgb(61,61,61) 85%);
background-image:url(../img/logo_back.png), -moz-linear-gradient(right, rgb(26,26,26) 35%, rgb(40,40,40) 68%, rgb(61,61,61) 85%);
background-image:url(../img/logo_back.png), -webkit-linear-gradient(right, rgb(26,26,26) 35%, rgb(40,40,40) 68%, rgb(61,61,61) 85%);
background-image:url(../img/logo_back.png), -o-linear-gradient(right, rgb(26,26,26) 35%, rgb(40,40,40) 68%, rgb(61,61,61) 85%);
background-image:url(../img/logo_back.png), -ms-linear-gradient(right, rgb(26,26,26) 35%, rgb(40,40,40) 68%, rgb(61,61,61) 85%);
}
与原始的(顶部)相比,我们得到了一个更有趣的标题(底部):

小贴士
为 Internet Explorer 中的渐变指定的 -ms- 前缀已被微软弃用。请参阅以下链接:msdn.microsoft.com/en-us/library/windows/apps/hh453527.aspx。
总是可以通过使用*铺图像和 background-image 属性来回退渐变。
添加框阴影
我们可以使用阴影来模拟深度,产生嵌入和突出显示的视觉效果。box-shadow 属性允许我们根据元素的边框创建阴影。
box-shadow 的语法如下:
box-shadow:horizontal-shadow vertical-shadow blur spread color inset;
只需要 horizontal-shadow 和 vertical-shadow。inset 指定阴影是否应用于元素内部。
让我们在 nav 上添加一个底部的阴影。我们可以将 horizontal-shadow 指定为 0,将 vertical-shadow 指定为 1px 以显示元素下的阴影,将 3px 用于模糊,颜色为 #999:
nav{
background-color:#666;
background-image:linear-gradient(top, rgb(102,102,102) 0%, rgb(70,70,70) 100%);
background-image:-moz-linear-gradient(top, rgb(102,102,102) 0%, rgb(70,70,70) 100%);
background-image:-webkit-linear-gradient(top, rgb(102,102,102) 0%, rgb(70,70,70) 100%);
background-image:-o-linear-gradient(top, rgb(102,102,102) 0%, rgb(70,70,70) 100%);
background-image:-ms-linear-gradient(top, rgb(102,102,102) 0%, rgb(70,70,70) 100%);
/** NAVIGATION SHADOW **/
box-shadow: 0 1px 3px #999;
-moz-box-shadow: 0 1px 3px #999;
-webkit-box-shadow: 0 1px 3px #999;
-o-box-shadow: 0 1px 3px #999;
}
我们可以将没有阴影的 nav 菜单(左侧)和有阴影的菜单(右侧)进行比较:

为了演示 inset,我们可以在我们的 Top 5 Box Office 区域添加一个内部阴影。在这里,我们为 vertical-shadow 应用负定位 -1px 以在底部显示阴影的一部分,1px 用于模糊,1px 用于扩散(因为我们想修改阴影的大小),颜色为 #aaa,最后使用 inset 以获得内部阴影:
aside{
float:right;
width:200px;
padding:30px 0 10px 0;
margin:0 10px;
background-color:#E4E4E4;
/** TOP 5 ROUNDED BORDER **/
border-radius:0 0 8px 8px;
-moz-border-radius:0 0 8px 8px;
-webkit-border-radius:0 0 8px 8px;
-o-border-radius:0 0 8px 8px;
/** BOX OFFICE GRADIENT **/
background:linear-gradient(bottom, rgb(200,200,200) 35%, rgb(210,210,210) 68%, rgb(220,220,220) 98%, rgb(80,80,80) 100%);
background:-o-linear-gradient(bottom, rgb(200,200,200) 35%, rgb(210,210,210) 68%, rgb(220,220,220) 98%, rgb(80,80,80) 100%);
background:-moz-linear-gradient(bottom, rgb(200,200,200) 35%, rgb(210,210,210) 68%, rgb(220,220,220) 98%, rgb(80,80,80) 100%);
background:-webkit-linear-gradient(bottom, rgb(200,200,200) 35%, rgb(210,210,210) 68%, rgb(220,220,220) 98%, rgb(80,80,80) 100%);
background:-ms-linear-gradient(bottom, rgb(200,200,200) 35%, rgb(210,210,210) 68%, rgb(220,220,220) 98%, rgb(80,80,80) 100%);
/** BOX OFFICE INNER SHADOW **/
box-shadow:0 -1px 1px 1px #aaa inset;
-moz-box-shadow:0 -1px 1px 1px #aaa inset;
-webkit-box-shadow:0 -1px 1px 1px #aaa inset;
-o-box-shadow:0 -1px 1px 1px #aaa inset;
}
因此,我们的 Top 5 Box Office 区域看起来比以前更深:

我们可以将此应用到我们的包装器上,以便在左右边框上产生阴影:
.wrapper{
background-color:#fff;
/** PAGE SIDES SHADOWS **/
box-shadow: 1px 0 2px 1px #aaa;
-moz-box-shadow: 0 -1px 1px 1px #aaa;
-webkit-box-shadow: 0 -1px 1px 1px #aaa;
-o-box-shadow: 0 -1px 1px 1px #aaa;
}
虽然很难注意到,这些小细节的总和有助于反映企业应用的视觉丰富性:

box-shadow 被所有现代浏览器支持,除了 Internet Explorer,它只从 IE9 开始支持。
小贴士
不幸的是,创建假灵活的阴影很困难或成本很高,因为有时在旧浏览器中不使用阴影是更好的选择,遵循优雅降级的原理。
添加文字阴影
要添加文字阴影,我们不能使用 box-shadow,因为它应用于一个方形容器。如果我们想给任何文字添加阴影,我们应该使用 text-shadow 属性。
text-shadow 的语法如下:
text-shadow: horizontal-shadow vertical-shadow blur color;
text-shadow 不被 Internet Explorer 支持,但在此情况下可以使用 filter:dropshadow 代替。唯一的缺点(除了兼容性之外)是无法指定模糊效果。
filter:dropshadow 的语法如下:
filter: dropshadow(color=color, offx=horizontal-shadow, offy= vertical-shadow);
甚至阴影效果也被视为突出文本的一种方式。我们可以更改 color 属性并伪造 inset 元素。我们在 Top 5 Box Office 标题中使用浅灰色和水*垂直方向上 1px 的位移,没有模糊效果:
aside h2{
padding:0 20px 10px;
margin:0 0 0;
font-size:1.3em;
text-shadow: 1px 1px 0px #f2f2f2;
filter: dropshadow(color=#f2f2f2, offx=1, offy=1);
}
你可以检查 inset 元素(右侧):

我们将在我们的导航栏中使用更传统的方法,包括 hover 时的 blur:
nav a{
color:#ccc;
text-decoration:none;
}
nav a:hover{
color:#fff;
text-shadow: 2px 2px 1px #222;
filter: dropshadow(color=#222222, offx=2, offy=2);
}

一些伪造 3D 的技巧
通过使用 CSS 3 之前的版本可以模拟一些深度效果。例如,我们可以通过在浅色上放置深色边框来模拟深度:
aside ol{
padding:20px 0 0 36px;
margin:0;
counter-reset:counter;
border-top:1px dashed #f8f8f8;
}
aside h2{
padding:0 20px 10px;
margin:0 0 0;
font-size:1.3em;
text-shadow: 1px 1px 0px #f2f2f2;
filter: dropshadow(color=#f2f2f2, offx=1, offy=1);
border-bottom:1px dashed #bbb;
}
我们可以通过将此应用到我们的 Top 5 Box Office 区域来实现:

在某些情况下,效果可能不明显,但作为其他效果的一部分有助于提供深度:
header{
…
border-bottom:1px solid #222;
}
nav{
…
border-top:1px solid #777;
font-size:.9em;
}
将此应用到导航菜单的顶部,我们得到以下结果:

将所有效果应用到我们的初始布局中:

我们看到如下内容:

电影和风格
假设我们需要创建一个包含简单信息的电影列表,其中每个元素在点击时显示更多详细信息。由于时间紧迫,客户决定采用简单实现,因此需求交给了网页设计师,结果我们得到了一个初始设计:

在与我们的网页设计师和最终客户的会议中,我们决定在点击电影海报时显示电影简介。基于此,让我们构建我们结构的第一种方法。
对于一个顺序无关紧要的列表,我们可以使用无序列表 ul。我们首先添加电影海报来轻松识别每个元素(使用 img),然后使用 main-info(默认显示)和 description(点击时显示)部分定义两个区块。在 main-info 中,我们添加标题为 h3,类型和评分作为 p 并带有 details 类,电影院使用标签 p 和 theater 类,以及演员作为 p 并带有 actors 类。在详情中,我们添加标题为 h3 和描述为 p。现在,我们的结构看起来像以下代码:
<div id="movies-near-me">
<ul>
<li itemtype="http://schema.org/Movie" itemscope="" class="">
<img width="120" alt="The Amazing Spider-Man" src="img/30101_p_m">
<section class="main-info">
<h3 itemprop="name">The Amazing Spider-Man</h3>
<p itemprop="genre" class="details genre">Action,Drama,Fantasy,Science Fiction</p>
<p class="details">PG-13</p>
<p class="theater">Regal E-Walk 13 247 West 42nd St</p>
<p class="actors">Andrew Garfield,Emma Stone,Rhys Ifans,Denis Leary,Campbell Scott</p>
</section>
<section class="description">
<h3 itemprop="name">The Amazing Spider-Man</h3>
<p>Typical teenager Peter Parker (Andrew Garfield) embraces his incredible destiny after uncovering one of his father's most carefully guarded secrets as Columbia Pictures reboots the Spider-Man franchis...</p>
</section>
</li>
…
</ul>
</div>
首先,我们在 movienow.geolocation.js 的末尾添加 AJAX 调用:
this.getLocation();
然后,我们使用我们的结构修改 AJAX 回调的结构:
this.displayShowtimes = function(movies) {
var movie = null;
var html = '<ul>';
for (var item in movies.items) {
movie = movies.items[item];
var movieDesc=(movie.synopsis.length>200)?movie.synopsis.substr(0,200)+"...": movie.synopsis;
var movieHTML='<li itemscope itemtype="http://schema.org/Movie">';
movieHTML+='<img src="img/'+movie.poster+'" alt="'+movie.title+'" width="120" />';
movieHTML+='<section class="main-info">';
movieHTML+='<h3 itemprop="name">'+movie.title+'</h3>';
movieHTML+='<p class="details genre" itemprop="genre">'+Array(movie.genre).join(', ')+'</p>';
movieHTML+='<p class="details">'+movie.mpaaRating+'</p>';
movieHTML+='<p class="theater">'+movie.theater.title+" "+movie.theater.address+'</p>';
movieHTML+='<p class="actors">'+Array(movie.selectedStar).join(', ')+'</p>';
movieHTML+='</section>';
movieHTML+='<section class="description">';
movieHTML+='<h3 itemprop="name">'+movie.title+'</h3>';
movieHTML+='</section>';
movieHTML+='</li>';
html+=movieHTML;
}
html+= '</ul>';
$('#movies-near-me').html(html);
$("#movies-near-me li").click(function(){$(this).toggleClass("open")});
};
我们正在通过连接字符串来创建我们的 DOM 结构,但如果你想要一个更优雅的解决方案,你可以使用客户端模板库,如 jQuery tmpl (api.jquery.com/category/plugins/templates/)、Mustache (mustache.github.com/)、Underscore (documentcloud.github.com/underscore/)) 或 Pure (beebole.com/pure/)。模板库允许你将 DOM 结构从数据中分离出来。其中一些,如 Underscore,包括逻辑。
注意
注意,我们使用substr限制了movie.synopsys的大小。
由于我们想在白色中添加一些高亮,因此我们应该将wrapper和main-info的background-color结构更改为与原始设计中的浅灰色相同,这样我们就可以使用:
.wrapper, #movies-near-me li section.main-info{
background-color:#f1f1f1;
}
我们的前 5 个盒子向右浮动,因此我们可以为我们的电影容器添加一些margin以允许更灵活的设计。我们将更改wrapper结构的原始宽度:
#movies-near-me{
margin-right:200px;
}
为我们的列表添加样式
我们想在稍后应用一些动画,因此我们将添加position:relative来移动内部绝对定位的元素,使用li作为我们的参考点。我们添加overflow:hidden以处理任何超出li区域的元素。我们使用带有浅色和深色颜色的borders top和bottom来增加深度感。最后,我们添加深灰色作为background-color(原始设计中没有,但将被main-info和img覆盖),并将鼠标cursor属性设置为pointer以指示该元素是可点击的:
#movies-near-me li{
position:relative;
overflow:hidden;
border-top:1px solid #fff;
border-bottom:1px solid #ccc;
background-color: #202125;
cursor:pointer;
}
让我们浮动img以在旁边显示main-info而不是在下面。哦,还要一些margin来在img和描述文本之间留出空间(目前将隐藏):
#movies-near-me li img{
float:left;
margin-right:10px;
}
我们将如下定义标题的大小、粗细和间距:
#movies-near-me li h3{
font-size:1.2em;
font-weight:bold;
padding:10px 0 3px 14px;
}
我们将为p标签内的每个信息添加填充:
#movies-near-me li p{
padding:5px 14px;
}
为一些细节添加不同的文本颜色和大小:
#movies-near-me li .details{
color:#333;
font-size:.9em;
}
我们将使用以下声明定义电影院的不同的文本颜色和大小以及斜体样式:
#movies-near-me li .theater{
color:#555;
font-style:italic;
font-size:.9em;
}
为演员应用新的样式:
#movies-near-me li .actors{
color:#666;
font-size:.8em;
}
我们定义一个静态高度,与每个电影海报图像相同:
#movies-near-me li,#movies-near-me li section.main-info,#movies-near-me li section.description{
height:178px;
}
我们对main-info应用了绝对定位(以便稍后进行动画)。我们添加与我们的电影海报图像的width属性相等的margin以及我们文本内部的填充:
#movies-near-me li section.main-info{
position:absolute;
top:0;
left:0;
right:0;
margin-left:120px;
padding: 5px 0;
}
最后,我们将为我们的隐藏描述添加一些样式,包括一个模拟深度的inset box-shadow属性:
#movies-near-me li section.description{
color:#f1f1f1;
font-size:.9em;
line-height:1.4em;
box-shadow:1px -8px 3px 4px #000 inset;
-moz-box-shadow:1px -8px 3px 4px #000 inset;
-webkit-box-shadow:1px -8px 3px 4px #000 inset;
-o-box-shadow:1px -8px 3px 4px #000 inset;
}
到目前为止,我们的设计看起来与我们的网页设计师提供的图像相同,但我们仍然看不到电影详情。在我们满足这一要求之前,让我们谈谈转换和动画。
转换
通常,我们根据交互更改 HTML 元素的类。例如,悬停时的链接样式,点击时显示和隐藏用于选项卡的文本块等。在 CSS3 之前,如果我们想对这些更改进行动画处理,唯一的方法是使用 JavaScript。有了 CSS3,一个简单的方法是使用transition。有一个初始类和一个在交互上触发的伪类,我们可以添加一个具有在类和伪类之间改变属性的transition元素来动画化它们。
简写transition的语法如下:
transition: transition-property transition-duration transition-timing-function transition-delay
transition-timing-function指定转换发生速度。此属性的值可以是:linear、ease、ease-in、ease-out、ease-in-out和cubic-bezier(n,n,n,n)。
如果我们想在初始状态(0s)之外的其他时间点开始动画,则使用transition-delay。
我们可以同时使用多个转换:
transition: property1 duration1 easing1 start-point1,..., propertyN durationN easingN start-pointN
提示
转换由交互触发,并且只有两种状态:初始状态和最终状态。
动画
如果我们要实现涉及多个状态复杂动作,无法使用转换。为此,我们使用动画。此外,您不需要触发交互来启动动画(但我们应将其保密,以避免在这个时代出现新的动画 GIF 热潮)。
动画依赖于@keyframes。类似于动画工具(包括 Adobe Flash)中的对应物,关键帧允许您定义状态及其中的属性值。
例如,我们可以使用:
@keyframes animation-name
{
from {width:0}
to {width:50px}
}
或者使用百分比和多个属性的更复杂结构:
@keyframes animation-name
{
0%{width:0;height:0}
20%{width:5px;height:2px}
60%{width:7px;height:10px}
100%{width:50px;height:12px}
}
我们可以指定我们想要的步骤数量。animation-name稍后用于调用我们的关键帧。
用于动画的语法如下:
animation: animation-name animation-duration animation-timing-function animation-delay animation-iteration-count animation-direction;
大多数属性的含义与转换相同。animation-iteration-count指定动画将重复的次数(或如果它永不停止,则为infinite),animation-direction允许动画正常运行(normal),或交替前后(alternate)。
此外,我们还有animation-play-state属性,它不在简写模式中。此属性允许我们停止(paused)并重新开始(running)我们的动画。
在转换和动画之间进行选择
在我们的案例中,我们只有两种状态,一种是显示电影的一般细节,另一种是显示电影描述的伪类状态。这应该在点击时触发,因此最简单的解决方案是使用转换。
提示
虽然在每种情况下都可以使用animation,但对于与常见交互相关的简单需求,最好依赖于transition。
在我们的案例中,我们想要动画化main-info的left和right属性。初始状态为两者都是0:
#movies-near-me li section.main-info{
top:0;
left:0%;
right:0%;
margin-left:120px;
padding: 5px 0;
}
#movies-near-me li.open section.main-info{
left:100%;
right:-100%;
}
最终状态将是left:100%(li的右侧)和right:-100%(从li的右侧向右100%)。我们为li创建一个具有类open的伪状态:
要在点击时更改类,我们为每个li使用 jQuery 在movienow.js中添加一个toggleClass调用。toggleClass添加和删除open类:
$("#movies-near-me li").click(function(){$(this).toggleClass("open")});
如果你点击每个元素,你会注意到显示和隐藏每个描述的变化。
要添加我们的transition,我们指定left和right属性,以及每个的.3秒持续时间。使用多个浏览器前缀,我们得到以下代码:
#movies-near-me li section.main-info{
top:0;
left:0%;
right:0%;
margin-left:120px;
padding: 5px 0;
transition: right .3s, left .3s;
-moz-transition: right .3s, left .3s;
-webkit-transition: right .3s, left .3s;
-o-transition: right .3s, left .3s;
}
再次测试,我们应该看到一个从一种状态到另一种状态的流畅运动。

如果我们点击Brave,我们会看到一个动画,然后是电影简介,部分如图下截图所示。

让我们在标题中添加一个动画来测试animation属性。我们的标题显示一个电影卷轴装饰。如果我们想滚动卷轴,我们需要定义一些关键帧。在这种情况下,我们只指定两种状态:from和to。由于我们的设计,我们将卷轴水*从 0 移动到-19px(白色矩形之间的空间,以创建相同的初始和结束状态以供循环使用)。我们将使用相应的浏览器前缀,并将我们的keyframe命名为movierolling:
@keyframes movierolling{
from {background-position: 0 0;}
to {background-position: -19px 0;}
}
/* Firefox */
@-moz-keyframes movierolling{
from {background-position: 0 0;}
to {background-position: -19px 0;}
}
/* Safari and Chrome */
@-webkit-keyframes movierolling{
from {background-position: 0 0;}
to {background-position: -19px 0;}
}
/* Opera */
@-o-keyframes movierolling{
from {background-position: 0 0;}
to {background-position: -19px 0;}
}
我们在标题中添加movierolling作为animation,指定.5 segs animation:movierolling .5s,一个无限循环animation-iteration-count:infinite,以及线性缓动以创建流畅的循环animation-timing-function:linear。因此,我们得到以下代码:
header{
color:#fff;
height:122px;
/** HEADER GRADIENT **/
background-color:#1A1A1A;
background-image:url(../img/logo_back.png), linear-gradient(right, rgb(26,26,26) 35%, rgb(40,40,40) 68%, rgb(61,61,61) 85%);
background-image:url(../img/logo_back.png), -moz-linear-gradient(right, rgb(26,26,26) 35%, rgb(40,40,40) 68%, rgb(61,61,61) 85%);
background-image:url(../img/logo_back.png), -webkit-linear-gradient(right, rgb(26,26,26) 35%, rgb(40,40,40) 68%, rgb(61,61,61) 85%);
background-image:url(../img/logo_back.png), -o-linear-gradient(right, rgb(26,26,26) 35%, rgb(40,40,40) 68%, rgb(61,61,61) 85%);
background-image:url(../img/logo_back.png), -ms-linear-gradient(right, rgb(26,26,26) 35%, rgb(40,40,40) 68%, rgb(61,61,61) 85%);
border-bottom:1px solid #222;
animation:movierolling .5s;
-moz-animation:movierolling .5s;
-webkit-animation:movierolling .5s;
-o-animation:movierolling .5s;
animation-iteration-count:infinite;
-moz-animation-iteration-count:infinite;
-webkit-animation-iteration-count:infinite;
-o-animation-iteration-count:infinite;
animation-timing-function:linear;
-moz-animation-timing-function:linear;
-webkit-animation-timing-function:linear;
-o-animation-timing-function:linear;
}
轮播开始了!
让我们先注释掉这个动画代码,然后回到我们的应用中。
使用媒体查询
我们添加的用于可视化简介的过渡效果很好,但在移动设备上我们没有足够的空间显示每部电影的完整简介。一个可能的解决方案是隐藏移动设备的电影海报图像,这应该至少给我们额外的 120 px。
正如我们在前面的章节中看到的,我们可以使用媒体查询为不同的屏幕尺寸指定不同的行为。我们可以为 737 px 以下的设备添加一个案例:
@media only screen and (max-width: 737px){ … }
让我们应用一个与main-info相同时间的过渡,但在这个情况下只为margin-left:
#movies-near-me li img{
transition: margin-left .3s;
-moz-transition: margin-left .3s;
-webkit-transition: margin-left .3s;
-o-transition: margin-left .3s;
}
最终位置应该有一个负的margin值,以便将我们的图像移动到li区域之外:
#movies-near-me li.open img{
margin-left:-120px;
}
此外,让我们隐藏演员和类型,以便在main-info内部有更多空间:
#movies-near-me li .actors, #movies-near-me li .genre{
display:none;
}
现在,我们可以看到小设备上的不同交互,允许我们看到完整的描述:

如果我们点击The Bourne Legacy,我们会看到一个动画,然后是电影简介。

应用 CSS3 选择器
我们一直在使用通用选择器,但 CSS3 引入了一个新的集合,为样式化打开了新的可能性。
大多数这些选择器在 Internet Explorer 8 或更早版本中不受支持;您可以使用以下链接中提到的兼容性表格来验证支持:www.quirksmode.org/css/contents.html#CSS3。您始终可以用 CSS 中的类声明替换这些选择器,并在您的 HTML 中使用代码中的条件添加这些类。
我们不会将此代码作为我们项目的一部分,但您可以使用3.- selectors文件夹中的styles.css(代码已注释)来测试它,以可视化结果。
-
:first-of-type:这个选择器用于选择具有选择器类型的第一个元素。假设我们想为电影列表中的第一个元素应用不同的background-color。我们可以选择li的第一个出现,然后是表示哪个元素必须更改其背景颜色的选择器:#movies-near-me li:first-of-type section.main-info{ background-color:#ccc; } -
:last-of-type:这个选择器与前面的选择器类似,但它选择最后一个元素。应用之前的相同案例:#movies-near-me li:last-of-type section.main-info{ background-color:#ccc; } -
:only-of-type:这个选择器只选择指定类型的唯一元素。使用我们电影的详细信息,如果我们应用以下:#movies-near-me li section.main-info h3:only-of-type{ background-color:#ccc; }我们可以为
h3添加background-color属性,因为它只被其父元素包含,但如果使用以下方法,则没有任何内容被选中,因为其中包含多个p元素:#movies-near-me li section.main-info p:only-of-type{ background-color:#ccc; } -
:only-child:这个选择器用于选择其父元素只包含它们的元素。例如,使用这个选择器我们可以改变article的background-color,因为它是其父元素中唯一包含的元素。article:only-child{ background-color:#ccc; }但如果我们选择
section,则不会选择任何内容,因为其父元素中有多个子元素。section:only-child{ background-color:#ccc; } -
:nth-child(n):这个选择器允许我们通过位置指定要选择的元素。如果我们想选择列表中的第三个元素:#movies-near-me li:nth-child(3) section.main-info{ background-color:#ccc; } -
:nth-last-child(n):这个选择器与前面的选择器使用相同的原理,但计数从最后一个元素开始:#movies-near-me li:nth-last-child(2) section.main-info{ background-color:#ccc; } -
:nth-of-type(n):这个选择器与之前的选择器使用相同的原理,但它只会计数相同类型的元素。例如,如果我们应用p:nth-of-type(2)来选择第二个元素,它将忽略任何与p的差异。选择第二个p元素,我们有以下代码片段:#movies-near-me li section.main-info p:nth-of-type(2){ background-color:#ccc; } -
:nth-last-of-type(n):这个选择器做的是相同的,但它从最后一个元素开始计数:#movies-near-me li section.main-info p:nth-last-of-type(2){ background-color:#ccc; } -
:last-child:这个选择器用于选择是其父元素的最后一个子元素的元素。选择最后一个电影,我们有以下代码片段:#movies-near-me li:last-child section.main-info{ background-color:#ccc; } -
:root:这个选择器允许我们选择html根标签。让我们改变html的background-color值,但首先我们需要重置已经定义的html和body标签的background-color属性:html,body{ background:none; }将
background-color添加到root(html)::root{ background-color:#666; } -
:empty:这个选择器用于选择没有子元素或文本的元素。让我们在我们的应用中以红色显示没有内容的div元素:div:empty{ background-color:#ff0000; }您应该看到标志区域和
div.push以红色显示。 -
:target:这个选择器用于选择具有与活动anchor相等的id值的元素。为了测试这个,我们可以定义一个带有锚点和id属性的链接来标记该链接为活动链接:<nav> <ul> <li><a href="#home" id="home">Home</a></li> </ul> </nav>我们可以定义样式来标记黄色文本:
:target{ color:#FFFF00; }如果你点击链接,你会看到颜色变化。
-
:not(selector): 这将选择所有不满足选择器条件的元素。例如,如果我们想选择所有没有theater类的p元素:p:not(.theater){ background-color:#ccc; } -
:enabled: 这将选择没有禁用属性的输入字段。如果我们有<input type="button" value="enable" />,我们可以使用以下代码定义一个橙色边框:input:enabled{ border:1px solid #E38217; } -
:disabled: 这将选择具有disabled属性的输入字段。和之前一样,我们可以有:<input type="button" value="disable" disabled="disabled" /> input:disabled{ border:1px solid #E38217; } -
:checked: 这将选择type为checkbox且被checked的input。如果我们有以下代码,我们可以看到当它被选中时元素会改变样式:<label><input type="checkbox" />Checked</label> Applying style: input:checked{ width:20px; height:20px; } -
element1~element2: 这将选择由element1precede 的element2。如果我们想选择由h3precede 的p元素,我们可以应用以下方法:h3~p{ background-color:#ccc; } -
[attribute^=value]: 这将选择其"attribute"以特定"value"开头的元素。例如,让我们隐藏所有alt属性以Dark开头的图像:img[alt^="Dark"]{ display:none; } -
[attribute$=value]: 这将选择其"attribute"以特定"value"结尾的元素。例如,让我们隐藏所有alt属性以s结尾的图像:img[alt$="s"]{ display:none; } -
[attribute*=value]: 这将选择其"attribute"包含"value"的元素。例如,让我们隐藏所有alt属性包含ar的图像:img[alt*="ar"]{ display:none; }
摘要
新的 CSS3 特性并不是对网络开发的全新引入;它们是对执行过程的简化。在 CSS3 之前,可以使用渐变、阴影、圆角甚至动画,但实现成本高昂且可扩展性复杂。有了所有这些可能性,我们不应忘记依赖于图像和复杂 JavaScript 的旧技术,因为尽管我们都希望有一个基于新一代浏览器的更简单未来,但我们必须面对旧一代浏览器的问题。
我们已经展示了如何将最常用的 CSS3 属性应用到我们的企业应用中,以及如何管理与样式相关的跨浏览器兼容性问题。此外,我们介绍了 CSS3 动画和过渡,因此现在我们能够为我们的项目选择正确的解决方案。最后,我们可以将媒体查询和选择器应用到样式表中,以实现更复杂和优雅的解决方案。
下一章将介绍 HTML5 视频和音频管理、JavaScript 对媒体播放的控制,以及提供向后兼容性的基本策略。
第六章:应用:通过 HTML5 视频的预告片
HTML5 引入的最有趣的功能之一是能够在不使用额外插件的情况下播放多媒体。尽管这似乎是任何涉及媒体管理的企业应用的正确解决方案,但仍有许多因素需要考虑。本章涵盖了 HTML5 的video和audio标签,它们用于播放媒体,以及与当前技术状态相关的注意事项。
作为例子,我们将构建一个用于预告片的视频播放器和用于播客的音频播放器。
本章包括:
-
HTML5 视频介绍
-
实现视频播放器
-
HTML5 音频介绍
-
实现音频播放器
-
如何停止担忧并爱上 Flash
HTML5 视频介绍
多年来,浏览器一直依赖于外部插件如 Real Player、Quicktime 和 Flash 进行视频播放。Flash 的市场渗透率高达 99%,成为了媒体播放的事实标准;然而,在过去的几年里,移动设备已经用原生应用和 HTML5 解决方案取代了这一解决方案。
HTML5 视频迅速成为嵌入视频的标准和优雅方式。虽然一切似乎都指向 HTML5 的视频解决方案,但关于应支持哪些视频格式的缺乏共识阻碍了其使用。
理想情况下,所有浏览器至少应支持一种格式,但每个公司都有自己的看法。虽然微软和苹果支持 MP4 H.264(因为它们是该格式的专利持有者),但谷歌和 Mozilla 支持 Ogg Theora 和 VP8 WebM 作为免费解决方案。以下表格显示了每个视频格式的浏览器支持情况:
| 浏览器 | 操作系统 | Ogg Theora | MP4 H.264 | VP8 WebM |
|---|---|---|---|---|
| Internet Explorer | Windows | 手动安装 | 9.0 | 手动安装 |
| Windows Phone | 否 | 否 | ||
| Mozilla Firefox | Windows | 3.5 | 手动安装 | 4.0 |
| Unix | 否 | |||
| 其他 | ||||
| Google Chrome | 所有支持 | 3.0 | 3.0(计划移除) | 6.0 |
| Safari | iOS | 否 | 3.1 | 否 |
| MacOS | 手动安装 | 手动安装 | ||
| Windows | 手动安装 | |||
| Opera | 所有支持 | 10.50 | 否 | 10.60 |
注意
一种名为高效视频编码(HEVC)或 H.265 的新压缩标准可能在 2013 年进入商业产品。它的效率几乎是当前 H.264 标准的两倍。
幸运的是,video标签支持使用多个源,允许浏览器选择支持的视频格式,但这意味着每个视频至少需要编码两次。对于您的企业来说,这意味着编码和存储的额外成本。
小贴士
一些实现依赖于视频文件扩展名。例如,即使使用 MP4 H.264 格式,您也无法在 iOS 设备上播放扩展名为.f4v的视频。
大多数网络浏览器支持渐进式下载而不是流式传输。虽然 Flash 有自己的专有协议来流式传输(尽管已经发布了用于公共使用的规范的不完整版本),称为 实时消息协议 RTMP,但只有 Safari、Safari iOS 和一些 Android 浏览器支持苹果公司实现的新流式传输协议:HTTP Live Streaming (HLS)。
使用渐进式下载,从浏览器缓存中复制视频文件相当容易,这对于媒体盗版者来说是一个便利的条件。此外,如果你使用流式传输,并且内容分发网络支持自适应比特率流,你可以根据用户的比特率提供不同的视频质量,但使用渐进式下载则不可能做到这一点。
实现视频播放器
MovieNow 用户会喜欢有一个方式来可视化他们最喜欢的电影的预告片。为此,我们将创建一个具有基本功能(播放、暂停、快进、音量控制、全屏)的播放器。
我们将以 Sintel 的预告片为例,Sintel 是使用名为 Blender 的免费 3D 动画工具制作的动画电影。这个视频预告片托管在 www.w3.org/ 网站上,以三种主要视频格式:MP4 (mp4)、WebM (webm) 和 Ogg Theora (ogv)。
首先,让我们创建一个名为 trailer.html 的文件,并使用我们的主页面结构。
在 article 标签内,我们使用 video 标签,这允许我们使用 poster 属性指定初始图像,并使用 controls 属性显示默认控件。
<video poster="img/trailer-poster.png" controls>
你可以直接为 video 标签指定 src 属性,但为了支持多种视频格式文件,我们将在 video 标签内使用 source 标签来声明我们的文件。source 标签的 src 属性允许我们定义视频路径,而 type 属性(用于指定格式)。
<source type="video/mp4" src="img/path"></source>
在这个例子中,我们将使用:
-
media.w3.org/2010/05/sintel/trailer.mp4用于 Chrome(尽管它仍然受到支持)、Internet Explorer、Safari 和 Safari iOS -
media.w3.org/2010/05/sintel/trailer.webm用于 Firefox、Chrome 和 Opera -
media.w3.org/2010/05/sintel/trailer.ogv用于 Firefox、Chrome、Opera 和其他浏览器
在这种情况下,可能只需要使用两种格式,但我们将使用三种格式作为示例。最后我们得到:
<video poster="img/trailer-poster.png" controls>
<source type="video/mp4" src="img/trailer.mp4"></source>
<source type="video/webm" src="img/trailer.webm"></source>
<source type="video/ogg" src="img/trailer.ogv"></source>
<p>Video not supported.</p>
</video>
注意,如果没有支持的视频,它会显示 <p>Video not supported.</p>。这可以是任何你想要的 HTML 内容。
小贴士
如果你的内容分发网络支持 HLS,你可以使用分段编码为 H.264 的视频,并使用 .m3u8 播单作为索引文件。为此,你可以使用 Apple Stream Segmenter 这样的工具。
由于每个浏览器都有自己的实现,我们的播放器在 Firefox、Chrome、Safari 等浏览器中看起来都不同。我们的播放器在不同*台上渲染效果不同。
在 Firefox 中,我们的播放器渲染效果如下所示:

在 Chrome 中,我们的播放器渲染效果如下所示:

Chrome
在 iPhone 上,我们的播放器将如下所示:

当需要时,在播放器中反映企业视觉风格或添加自定义功能。隐藏默认控制器并构建自己的控制器是可能的。
自定义控件
对于 MovieNow,我们将创建播放/暂停、搜索、音量和全屏控件。我们的设计如下所示:

为了简化创建进度条、搜索条以及音量条的任务,我们使用了 jQuery UI。jQuery UI 是一个 JavaScript 库,它实现了最常见的用户界面元素和交互,如滑块、手风琴、标签等。在我们的案例中,我们创建了一个带有 UI Darkness 主题的自定义下载(jqueryui.com/download)。
保存我们的 jQuery UI JavaScript 文件到js文件夹和样式表到css文件夹后,我们像往常一样在body标签的末尾之前导入它们:
<script src="img/jquery-ui-1.8.23.custom.min.js"></script>
并且在head标签中的css:
<link rel="stylesheet" href=" css/ui-darkness/jquery-ui-1.8.23.custom.css" type="text/css" />
为了支持触摸设备上的 jQuery UI 交互,我们导入 Touch Punch JavaScript 库([touchpunch.furf.com/](http://touchpunch.furf.com/)):
<script src="img/jquery.ui.touch-punch.min.js"></script>
现在我们已经放置了所有需要的库,我们可以从video标签中移除controls属性来隐藏默认控件。
<video poster="img/trailer-poster.png" class="media">
在此基础上,让我们定义一个用于我们控件的 HTML 结构:
<div class="media-container">
<div>
<div class="media-area">
<video poster="img/trailer-poster.png" class="media">
<source type="video/mp4" src="img/trailer.mp4"></source>
<source type="video/webm" src="img/trailer.webm"></source>
<source type="video/ogg" src="img/trailer.ogv"></source>
<p>Video not supported.</p>
</video>
</div>
<div class="controls">
<div class="play-button"></div>
<div class="seek"></div>
<div class="fullscreen-button"></div>
<div class="volume-container">
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
<div class="volume-button"></div>
</div>
<div class="timer">00:00</div>
</div>
</div>
</div>
我们有三个主要类:
-
media-container– 包裹我们所有的播放器 -
media-area– 包裹视频标签 -
controls– 是包含我们控件的底部栏
在控件内部我们有:
-
play-button– 是播放器的播放/暂停按钮 -
seek– 进度/搜索条 -
fullscreen-button– 是全屏功能按钮 -
volume-container– 是volume-button的容器 -
volume-slider– 用于设置音量 -
timer– 是显示分钟和秒(mm:ss)的时间指示器小贴士
我们使用类,尽管使用 ID 的 jQuery 选择器运行更快,因为我们希望允许在同一个页面中必要时使用多个播放器。
样式化
首先,我们在style.css中添加一些额外的样式。我们定义了一个黑色背景并移除了所有带有media类的元素的轮廓:
.media{
width:100%;
background-color:#000;
outline:none;
}
我们为Top 5 Box Office部分添加了边距:
.media-container{
margin-right:200px;
}
我们为小型设备移除了那个边距,因为我们隐藏了Top 5 Box Office部分:
@media only screen and (max-width: 737px){
…
.media-container{
margin-right:0;
}
}
这些样式与布局相关,而不是直接与我们的播放器相关。为了为我们的视频播放器创建样式,让我们创建一个名为mediaplayer.css的样式表,并在trailer.html的head标签中导入它。
<link rel="stylesheet" href="css/mediaplayer.css" type="text/css" />
按钮 和 图像精灵
我们使用 controls 类定义控件区域,设置黑色背景,设置高度为 35px(与行高相同以垂直居中文本),并将 position 设置为 relative(这样如果我们在定位内设置绝对元素,它们将相对于 controls)。
.controls{
background-color:#000;
height:35px;
line-height:35px;
position:relative;
}
我们有一个包含所有播放器控件 player-control.png 的图像精灵。你可以在 img 文件夹中找到它。

使用精灵的基础是遮罩可见元素以显示它,并隐藏其余部分。在这种情况下,假设我们只想显示我们的暂停按钮。我们的按钮大小为 35 x 35 像素,背景图像为 player-control.png;图像中唯一可见的部分是在我们的按钮区域内,因此我们可以使用背景定位来显示不同的图标,如这里所示:

定义我们已有的播放、音量和全屏按钮:
.play-button,.volume-button,.fullscreen-button{
width:35px;
height:35px;
cursor:pointer;
background-image:url(../img/player-control.png);
}
如我们之前看到的,我们在 y 轴上移动 -35px 来显示我们的暂停图标:
.play-button.playing{
background-position:0 -35px;
}
我们将同样的原则应用于全屏按钮:
.fullscreen-button{
background-position:0 -105px;
}
在音量按钮的情况下,我们将在其下方设置一个用于设置音量的滑块,因此我们设置背景颜色来隐藏下面的元素,并将 position 设置为 absolute,z-index 为 1000 以覆盖滑块:
.volume-button{
background-position:0 -70px;
position:absolute;
left:0;
background-color:#000;
z-index:1000;
}
样式化搜索和音量条
搜索和音量条可以按照以下方式样式化:
-
让我们在
timer中定义字体样式:.timer{ color:#fff; font-size:.8em; padding:1px 8px 0; } -
到目前为止,我们通过浮动将
play-button定位到左侧,将计时器、音量和全屏定位到右侧:.play-button,.seek{ float:left; } .timer,.volume-container,.fullscreen-button{ float:right; } -
我们可以使用
media类将video标签浮动到左侧,以避免某些浏览器中的额外间距:.media{ float:left; } -
使用
overflow:hidden将media-area包裹在media旁边:.media-area{ overflow:hidden; background-color:#000; } -
使用绝对定位为
seek条,我们可以使用left和right属性动态扩展搜索区域:.seek{ top:12px; left:48px; right:133px; height:10px; position:absolute; }
小贴士
对于这个例子,我们创建了一个灵活的播放器来展示一些与样式相关的技术,但为你的播放器定义静态尺寸是一个好的实践。此外,使用标准分辨率会更好。使用标准分辨率可以提高客户端媒体播放的性能。
功能检测
一些 video 标签的功能不是通过 HTML5 JavaScript API 在所有浏览器中都可用。例如,iOS 设备不允许使用 JavaScript 进行音量控制;只能使用默认控件或硬件控件。使用 JavaScript 操作全屏控件仅在 WebKit 浏览器中可行。
我们可以定义一些类来隐藏在全屏或音量功能不可用时显示的按钮。首先,我们隐藏我们的按钮:
.no-fullscreen .fullscreen-button{
display:none;
}
.no-volume .volume-container{
display:none;
}
然后,我们改变 seek 条的 right 间距:
.no-volume .seek,.no-fullscreen .seek{
right:88px;
}
.no-fullscreen.no-volume .seek{
right:53px;
}
样式化滑块
由于我们使用 jQuery UI 来实现搜索和音量滑块,我们想要覆盖一些样式。jQuery UI 滑块使用以下样式:
-
ui-slider-handle: 我们用来拖动和搜索的圆形。 -
ui-state-active: 在我们拖动时添加到ui-slider-handle的类。 -
ui-slider-range: 定义活动区域的条形。在我们的例子中,它是一个蓝色条形。
让我们一步一步地看看这个过程。
-
我们希望
ui-slider-handle即使在活动状态下也有相同的颜色,因此我们移除了 jQuery UI 使用的背景图像:.ui-state-active{ background-image:none; } -
添加光标指针并移除轮廓:
.ui-slider .ui-slider-handle{ cursor:pointer; outline:none; } -
改变
ui-slider-handle的大小、圆角,并将其稍微向上移动(仅针对我们的seek滑块):.seek .ui-slider-handle { width:16px; height:16px; top: -4px; -moz-border-radius:10px; -ms-border-radius:10px; -webkit-border-radius:10px; border-radius:10px; } -
修改进度条的圆角并添加一些内部阴影:
.ui-slider-range { -moz-border-radius:15px; -ms-border-radius:15px; -webkit-border-radius:15px; border-radius:15px; box-shadow:inset 0 -3px 3px #39a2ce; } -
将寻道条进度颜色改为蓝色渐变:
.seek .ui-slider-range { background: #4cbae8; background-image:-moz-linear-gradient(top, #4cbae8, #39a2ce); background-image:-webkit-gradient(linear,left top,left bottom,color-stop(0, #4cbae8),color-stop(1, #39a2ce)); } -
将音量进度颜色改为纯蓝色:
.volume-slider .ui-slider-range { background:#4cbae8; } -
使用
margin居中音量滑块并设置width和height:.volume-slider{ margin:12px auto; width:6px; height:76px; } -
设置音量手柄的尺寸和定位:
.volume-slider .ui-slider-handle { width:12px; height:12px; left: -4px; } -
要显示和隐藏音量滑块,我们将
volume-container的定位设置为relative:.volume-container{ width:35px; height:35px; position:relative; } -
将滑块定位设置为
absolute。我们将z-index设置为900(在音量按钮下方),overflow设置为hidden,并为所有属性添加 CSS 过渡:.volume-slider-container{ -moz-transition:all 0.1s ease-in-out; -ms-transition:all 0.1s ease-in-out; -o-transition:all 0.1s ease-in-out; -webkit-transition:all 0.1s ease-in-out; transition:all 0.1s ease-in-out; position:absolute; bottom:1px; left:0; height:34px; width:35px; background-color:#000; z-index:900; overflow:hidden; } -
我们可以然后在悬停时调整
volume-container的大小,以及与之一起的volume-slider-container:.volume-container:hover .volume-slider-container{ height:135px; }
现在既然我们的播放器看起来一样,让我们使用 JavaScript 添加所有需要的交互。
使用 JavaScript 添加交互
要编写我们的 JavaScript 代码,我们在 js 文件夹中创建一个 movienow.mediaplayer.js 文件并在我们的 body 结束前包含它:
<script src="img/movienow.mediaplayer.js"></script>
初始设置
我们以与地理位置相同的方式开始我们的 JavaScript,通过将 mediaplayer 添加到我们的命名空间并定义 that 变量:
var movienow = movienow || {};
movienow.mediaplayer = (function(){
var that = this;
/** OUR CODE GOES HERE **/
})();
初始化视频控制器
当 ready 文档被触发时,我们向按钮添加点击事件监听器,检测全屏功能,如果不可用则添加 no-fullscreen 类;如果可用,初始化用于寻道和音量控制的 jQuery UI 滑块。注意,我们使用不同的函数管理 Mozilla、WebKit 和标准全屏功能。如果音量不可用,我们添加 no-volume 类,并最终绑定时间更新和播放结束的事件。
$(document).ready(function(){
$(".media-container .play-button").click(that.play);
var mediaElements=$(".media-container .media");
if (mediaElements[0].fullscreenEnabled) {$(".media-container .fullscreen-button").click(that.fullScreen);
}else if(mediaElements[0].mozRequestFullScreen){
$(".media-container .fullscreen-button").click(that.mozFullScreen);
}else if(mediaElements[0].webkitRequestFullScreen){
$(".media-container .fullscreen-button").click(that.webkitFullScreen);
}else{
$(".media-container").addClass("no-fullscreen");
}
$(".media-container .seek").each(function() {
var duration=that.getPlayer($(this))[0].duration;
duration = duration?duration:0;
$(this).slider({
value: 0,
step: 0.01,
orientation: "horizontal",
range: "min",
max: duration,
start: function(event,ui){
var mediaArea=that.getPlayer($(event.target));
mediaArea.addClass("seeking");
mediaArea[0].pause();
},
slide:function(event,ui){
sliderTime(event,ui);
},
stop:function(event,ui){
var mediaArea=that.getPlayer($(event.target));
controls=that.controls(mediaArea);
sliderTime(event,ui);
if(controls.find(".play-button").hasClass("playing")){
mediaArea[0].play();
}
mediaArea.removeClass("seeking");
}
});
if(navigator.userAgent.match(/(iPhone|iPod|iPad)/i)){
$(".media-container").addClass("no-volume");
}else{
that.controls($(this)).find(".volume-slider").slider({
value: 1,
step: 0.05,
orientation: "vertical",
range: "min",
max: 1,
animate: true,
slide:function(event,ui){
var mediaArea=that.getPlayer($(event.target));
mediaArea[0].volume=ui.value;
}
});
}
});
mediaElements.bind("timeupdate", that.timeUpdate);
mediaElements.bind('ended', that.endReproduction);
});
设置寻道滑块
要设置寻道滑块,我们将初始值 value 设置为 0,将 step 设置为 0.01 以在拖动时实现流畅的电影移动,将 orientation 设置为 horizontal,将 range 设置为 min 以考虑最小值和当前手柄位置值之间的范围:
$(this).slider({
value: 0,
step: 0.01,
orientation: "horizontal",
range: "min",
max: duration,
start: function(event,ui){
var mediaArea=that.getPlayer($(event.target));
mediaArea.addClass("seeking");
mediaArea[0].pause();
},
slide:function(event,ui){
sliderTime(event,ui);
},
stop:function(event,ui){
var mediaArea=that.getPlayer($(event.target));
controls=that.controls(mediaArea);
sliderTime(event,ui);
if(controls.find(".play-button").hasClass("playing")){
mediaArea[0].play();
}
mediaArea.removeClass("seeking");
}
});
有三个事件被管理:
-
start在滑块手柄被按下时触发。注意,我们使用getPlayer函数获取video(该方法将在稍后声明)。我们可以暂停播放并添加seeking类来指示我们仍在拖动。 -
slide在我们拖动时触发。我们调用slideTime函数来设置进度条位置和时间文本。 -
stop在鼠标抬起时触发。我们使用controls函数获取video标签和控制,调用sliderTime并使用play-button playing类恢复播放器的前一个状态(播放或暂停)。最后,我们移除seeking以指示我们停止拖动。
初始化音量滑块
如果有音量可用,我们初始化音量滑块:
that.controls($(this)).find(".volume-slider").slider({
value: 1,
step: 0.05,
orientation: "vertical",
range: "min",
max: 1,
animate: true,
slide:function(event,ui){
var mediaArea=that.getPlayer($(event.target));
mediaArea[0].volume=ui.value;
}
});
注意,当前滑块的值包含在 ui.value 变量中,要将其设置在我们的播放器中,我们使用如下所示的 volume 属性:
mediaArea[0].volume=ui.value;
获取 DOM 对象的函数
我们定义了两个函数来执行主播放器(对于 video 标签或如果是的话 audio 标签的 media 类)和控制(controls 类)的 jQuery 选择器:
this.getPlayer= function(domObject){
return $(domObject.parentsUntil(".media-container").find(".media"));
};
this.controls= function(domObject){
return $(domObject.parentsUntil(".media-container").find(".controls"));
};
播放和暂停
对于 play-button,我们切换 playing 类并将我们的播放器设置为播放状态(player[0].play())或暂停状态(player[0].pause())。
this.play = function(event){
var button=$(event.target);
var player=that.getPlayer(button);
if(button.hasClass("playing")) {
player[0].pause();
button.removeClass("playing");
} else {
player[0].play();
button.addClass("playing");
}
};
全屏
每个浏览器以不同的方式管理全屏功能。要进入全屏模式,我们使用 element.requestFullscreen() 以及其等价函数 element.mozRequestFullScreen()(Firefox),element.webkitEnterFullScreen()(Safari 和 Chrome)。要退出全屏模式,我们使用 document.cancelFullScreen(),document.mozCancelFullScreen()(Firefox),以及 document.webkitCancelFullScreen()(Safari 和 Chrome)。最后,为了验证浏览器是否处于全屏模式,我们使用 document.fullScreen,document.mozfullScreen(Firefox),以及 this.webkitFullScreen(Safari 和 Chrome)。
即使在用户体验方面,浏览器也有所不同;Chrome 和 Safari 在全屏时显示它们自己的视频控制器,而 Firefox 默认不显示任何控制器。在 Internet Explorer 中不可用全屏功能。我们的实现验证了模式并在全屏模式和正常模式之间切换。
使用标准调用,我们有:
this.fullScreen = function(event){
var button=$(event.target);
var player=that.getPlayer(button);
if(document.fullScreen){
document.exitFullScreen();
} else {
player[0].requestFullScreen();
}
};
使用 Firefox 前缀:
this.mozFullScreen = function(event){
var button=$(event.target);
var player=that.getPlayer(button);
if(document.mozfullScreen){
document.mozCancelFullScreen();
} else {
player[0].mozRequestFullScreen();
}
};
最后,对于 Safari 和 Chrome,我们有:
this.webkitFullScreen = function(event){
var button=$(event.target);
var player=that.getPlayer(button);
if(document.webkitIsFullScreen){
document.webkitCancelFullScreen();
} else {
player[0].webkitEnterFullScreen();
}
};
注意,退出全屏模式的触发事件没有发生,因为浏览器使用 Esc 键来管理该功能,但根据每个浏览器对 HTML5 全屏规范的未来实现,我们可以在全屏模式下显示我们的控制器并利用这一点。
格式化时间
我们定义了 timeFormat 函数来获取播放器时间(以秒为单位)并以 mm:ss 格式返回:
this.timeFormat=function(seconds){
var m=Math.floor(seconds/60)<10?"0"+Math.floor(seconds/60):Math.floor(seconds/60);
var s=Math.floor(seconds-(m*60))<10?"0"+Math.floor(seconds-(m*60)):Math.floor(seconds-(m*60));
return m+":"+s;
};
控制时间
每次我们使用寻道滑块时,我们使用 currentTime 属性设置媒体播放器时间,这会触发 timeupdate 事件并调用 timeUpdate 函数。
this.sliderTime = function(event, ui) {
var mediaArea=that.getPlayer($(event.target));
var controls=that.controls(mediaArea);
mediaArea[0].currentTime=ui.value;
};
timeUpdate 设置时间为 mm:ss,如果播放器不在 seeking 状态(由 mediaArea 中的 seeking 类定义),它也会更新进度/寻道条。此函数在 timeupdate 事件触发时被调用:
this.timeUpdate = function(event) {
var mediaArea=$(event.target);
var controls=that.controls(mediaArea);
var currentTime=mediaArea[0].currentTime;
var duration=mediaArea[0].duration;
var timer=$(controls.find(".timer"));
if(currentTime>=0)timer.html(that.timeFormat(currentTime));
if(!mediaArea.hasClass("seeking")){
var seekSlider=$(controls.find(".seek"));
if(seekSlider.slider("option","max")==0){
var newDuration=mediaArea[0].duration;
newDuration=newDuration?newDuration:0;
seekSlider.slider("option","max", newDuration);
}
seekSlider.slider("value", currentTime);
}
};
直到永远
当播放结束,endReproduction 被调用,我们从 play-button 中移除 playing 类以指示我们已经完成了播放:
this.endReproduction = function(event) {
var mediaArea=$(event.target);
$(that.controls(mediaArea)).find(".play-button").removeClass("playing");
};
最终的脚本应类似于以下代码片段:
var movienow = movienow || {};
movienow.mediaplayer = (function(){
var that = this;
$(document).ready(function(){
/*** play/pause button click event listener ***/ $(".media-container .play-button").click(that.play);var mediaElements=$(".media-container .media");
if(mediaElements[0].fullscreenEnabled) {/*** fullscreen button click event listener ***/
$(".media-container .fullscreen-button").click(that.fullScreen);
}else if(mediaElements[0].mozRequestFullScreen){/*** fullscreen button click event listener mozilla ***/
$(".media-container .fullscreen-button").click(that.mozFullScreen);
}else if(mediaElements[0].webkitRequestFullScreen){/*** fullscreen button click event listener webkit ***/
$(".media-container .fullscreen-button").click(that.webkitFullScreen);
}else{
/*** we add class no-fullscreen to hide fullscreen button when it is not available ***/$(".media-container").addClass("no-fullscreen");
}
/*** Loop to add jquery ui sliders to progress/seek bar and volume ***/
$(".media-container .seek").each(function() {
/*** Duration of the media ***/
var duration=that.getPlayer($(this))[0].duration;
duration = duration?duration:0;
$(this).slider({
value: 0,
step: 0.01,
orientation: "horizontal",
range: "min",
max: duration,
/*** Start seek ***/
start: function(event,ui){
var mediaArea=that.getPlayer($(event.target));
/*** Class seeking to know status of the media player ***/
mediaArea.addClass("seeking");
mediaArea[0].pause();
},
/*** During seek ***/
slide:function(event,ui){
sliderTime(event,ui);
},
/*** Stop seek ***/
stop:function(event,ui){
var mediaArea=that.getPlayer($(event.target));
var controls=that.controls(mediaArea);
sliderTime(event,ui);
/*** We restore the status (paying or not) to the one before start seeking ***/
if(controls.find(".play-button").hasClass("playing")){
mediaArea[0].play();
}
mediaArea.removeClass("seeking");
}
});
/*** Volume controllers ***/
if(navigator.userAgent.match(/(iPhone|iPod|iPad)/i)){
/*** ios devices only allow to change volume using the device hardware, so we hide volume controllers ***/
$(".media-container").addClass("no-volume");
}else{
/*** volume slider controller ***/
that.controls($(this)).find(".volume-slider").slider({
value: 1,
step: 0.05,
orientation: "vertical",
range: "min",
max: 1,
animate: true,
slide:function(event,ui){
var mediaArea=that.getPlayer($(event.target));
mediaArea[0].volume=ui.value;
}
});
}
});
/*** event triggered when time change on media player ***/
mediaElements.bind("timeupdate", that.timeUpdate);
/*** event triggered when reproduction end on media player ***/
mediaElements.bind('ended', that.endReproduction);
});
/*** get player using jQuery selectors ***/
this.getPlayer= function(domObject){
return $(domObject.parentsUntil(".media-container").find(".media"));
};
/*** get control area using jQuery selectors ***/
this.controls= function(domObject){
return $(domObject.parentsUntil(".media-container").find(".controls"));
};
/*** play or pause and chenge play button icon ***/
this.play = function(event){
var button=$(event.target);
var player=that.getPlayer(button);
if(button.hasClass("playing")) {
player[0].pause();
button.removeClass("playing");
}else{
player[0].play();
button.addClass("playing");
}
};
/*** set on and off fullscreen mode ***/
this.fullScreen = function(event){
var button=$(event.target);
var player=that.getPlayer(button);
if($(document).context.fullScreenElement){
$(document).context.exitFullscreen();
}else{
player[0].requestFullscreen();
}
};
this.mozFullScreen = function(event){
var button=$(event.target);
var player=that.getPlayer(button);
if($(document).context.mozFullScreenElement){
$(document).context.mozCancelFullScreen();
}else{
player[0].mozRequestFullScreen();
}
};
this.webkitFullScreen = function(event){
var button=$(event.target);
var player=that.getPlayer(button);
if($(document).context.webkitIsFullScreen){
$(document).context.webkitCancelFullScreen();
}else{
player[0].webkitEnterFullScreen();
}
};
/*** set time format to mm:ss ***/
this.timeFormat=function(seconds){
var m=Math.floor(seconds/60)<10?"0"+Math.floor(seconds/60):Math.floor(seconds/60);
var s=Math.floor(seconds-(m*60))<10?"0"+Math.floor(seconds-(m*60)):Math.floor(seconds-(m*60));
return m+":"+s;
};
/*** use by seek slider, change slider position and time on controllers ***/
this.sliderTime = function(event, ui) {
var mediaArea=that.getPlayer($(event.target));
var controls=that.controls(mediaArea);
mediaArea[0].currentTime=ui.value;
};
/*** use by timeupdate event, change slider position and time on controllers ***/
this.timeUpdate = function(event) {
var mediaArea=$(event.target);
var controls=that.controls(mediaArea);
var currentTime=mediaArea[0].currentTime;
var duration=mediaArea[0].duration;
var timer=$(controls.find(".timer"));
if(currentTime>=0)timer.html(that.timeFormat(currentTime));
if(!mediaArea.hasClass("seeking")){
var seekSlider=$(controls.find(".seek"));
/*** some players (like safari) don't have duration when a player is initialized, this verify duration and assigned again to max property on slider ***/
if(seekSlider.slider("option","max")==0){
var newDuration=mediaArea[0].duration;
newDuration=newDuration?newDuration:0;
seekSlider.slider("option","max", newDuration);
}
seekSlider.slider("value", currentTime);
}
};
/*** change play button when reproduction ends ***/
this.endReproduction = function(event) {
var mediaArea=$(event.target);
$(that.controls(mediaArea)).find(".play-button").removeClass("playing");
};
})();
因此,我们有一个适用于多个*台的视频播放器:

可能的改进
到目前为止,我们有一个功能齐全的播放器,但未来我们可以添加更多改进。一个有趣的功能是添加缓冲通知。为了实现这一点,你需要监听loadstart事件以识别视频加载的开始,waiting和stalled(根据浏览器的不同:www.longtailvideo.com/html5/buffering/)以检测由于缓冲而导致的播放停止,最后是canplay和canplaythrough以识别缓冲结束。
在loadstart、waiting和stalled时应该显示缓冲通知,而在canplay和canplaythrough时应该隐藏该通知。
仍然不完美
HTML5 视频规范仍在进行中。由于浏览器和*台之间存在多个实现决策,需要不同的编码,因此存在主要的不一致性。尽管如此,它是一个不使用插件的视频支持标准方式。
介绍 HTML5 音频
HTML5 音频规范——就像 HTML5 视频一样——仍在开发中,并且没有所有浏览器都支持的音频格式。造成这种情况的原因与阻碍 HTML5 视频标准化支持的原因相同,如下表所示:
| 浏览器 | Ogg Vorbis | WAV PCM | MP3 | AAC |
|---|---|---|---|---|
| Internet Explorer | 不支持 | 不支持 | 9 | 9 |
| Mozilla Firefox | 3.5 | 3.5 | 无 | 无 |
| Google Chrome | 6 | 6 | 6 | 6 |
| Safari | 手动安装 | 5 | 5 | 5 |
| Opera | 10.6 | 10.6 | 无 | 无 |
实现音频播放器
MovieNow 需要一个音频播客播放器。为此,我们将使用 HTML5 的audio标签。
audio标签的行为或多或少与video标签相同:
<audio>
<source src="img/horse.ogg" type="audio/ogg" />
<source src="img/horse.mp3" type="audio/mp3" />
<p>Audio not supported.</p>
</audio>
小贴士
就像video标签一样,audio标签允许你直接在其内部指定src属性。
为了测试,我们将使用来自www.w3schools.com/的声音效果音频:
-
www.w3schools.com/html5/horse.ogg适用于 Firefox、Google Chrome 和 Opera -
www.w3schools.com/html5/horse.mp3适用于 Internet Explorer、Google Chrome、Safari 和 Safari iOS
我们将创建一个podcast.html文件,并导入与trailer.html相同的库。
自定义控制器
我们的多媒体播放器足够通用,可以使用与video标签相同的 HTML 结构来播放音频。我们只需要将video标签替换为audio标签,将media类分配给audio标签,并移除全屏按钮:
<div class="media-container no-fullscreen">
<div>
<div class="media-area">
<audio class="media">
<source src="img/horse.ogg" type="audio/ogg" />
<source src="img/horse.mp3" type="audio/mp3" />
<p>Audio not supported.</p>
</audio>
</div>
<div class="controls">
<div class="play-button"></div>
<div class="seek"></div>
<div class="volume-container">
<div class="volume-slider-container">
<div class="volume-slider"></div>
</div>
<div class="volume-button"></div>
</div>
<div class="timer">00:00</div>
</div>
</div>
</div>
样式
最后一个调整与audio标签有关。一些浏览器默认定义了height属性,因此我们将它重置为0:
audio.media{
height:0;
}
我是如何学会停止担忧并爱上 Flash 的
令人不安的事实是,HTML5 的视频和媒体功能是一项新技术,浏览器之战使得将这些解决方案作为媒体播放的标准变得更加困难。虽然 Flash 需要安装插件,但它是一种可靠的技术,可以在多个浏览器中重现媒体并流式传输。
随着移动设备上 Flash 支持的减少和视频及音频规范的改进,肯定会有一个没有 Flash 媒体的未来,但就目前而言,Flash 在最坏的情况下,是跨浏览器兼容性的后备解决方案。
像 YouTube 这样的大型媒体交付产品仍然依赖于 Flash 作为主要技术。你可以选择将 HTML5 作为你的主要技术,如果video和audio标签不受支持,则回退到 Flash,反之亦然,但选择应基于你的应用需求。
摘要
HTML5 的video和audio标签是支持企业应用中媒体内容的简单而优雅的方式,但在需要将它们作为解决方案使用时,应考虑到不同浏览器之间的实现差异。目前,最佳方案是同时使用这两种解决方案,并定义一个主要解决方案和一个后备方案。
下一章将重点介绍 HTML5 的另一个令人兴奋的特性:canvas。我们将使用canvas标签作为可视化与电影评论相关的分析的工具。
第七章. 应用程序:通过 Canvas 显示评分
到目前为止,我们已经看到了使用 CSS 和图像在我们的企业应用程序中布局和绘制元素的方法。如果我们需要根据动态数据创建复杂可视化或动画,使用DOM对象会变得复杂,其操作也会变慢。因此,HTML5 规范中引入了canvas标签。canvas标签定义了一个矩形区域,我们可以使用其 JavaScript API 在其中绘制任何内容。本章介绍了canvas标签用于数据可视化和简单动画。
在本章中,我们将涵盖:
-
绘制图表
-
准备我们的代码
-
一切都取决于上下文(2D 和 3D 上下文)
绘制图表
我们当前的 MovieNow 实现使用了movielistings.php网络服务提供的数据子集。一些未使用的数据包括来自 MetaCritic、EditorBoost 和一般用户评分(分别对应avgMetaCriticRating、editorBoost和avgUserRating)。MovieNow 用户会喜欢以条形图的形式看到这些信息。为此,我们将使用canvas。
小贴士
虽然可以使用DOM对象渲染此信息,但这可能较慢且更具限制性。
准备我们的代码
我们需要添加一个新的交互来通过点击显示评分图表。让我们移除我们当前的点击交互:
$("#movies-near-me li").click(function(){
$(this).toggleClass("open")
});
新交互将包括两个按钮:一个用于显示电影描述,另一个用于显示评分图表。在img文件夹中,您将找到一个options.png图像精灵。它包含信息和图表的图标。

使用details-button和charting-button类,让我们在styles.css中添加一些样式。每个按钮将是 45 px x 45 px,使用绝对定位将其放置在右下角:
.details-button,.charting-button{
width:45px;
height:45px;
cursor:pointer;
position:absolute;
bottom:10px;
right:0;
background:none;
border:none;
background-image:url(../img/options.png)
}
将详细信息按钮放置在图表按钮的左侧:
.details-button{
right:45px;
}
设置图表按钮的正确图像:
.charting-button{
background-position:-45px 0;
}
在movienow.geolocation.js的displayShowtimes函数中,我们需要更改 HTML 结构以添加我们的新按钮:
<input type="button" class="charting-button" />
<input type="button" class="details-button" />
我们还将放置我们的图表容器及其 canvas。在此阶段,我们将使用 HTML5 自定义数据属性data-feed来存储每个 canvas 中的评分信息:
<section class="charting">
<h3 itemprop="name">movie.title</h3>
<p><canvas data-feed= "MetaCritic:movie.avgMetaCriticRating,EditorBoost:movie.editorBoost,User Rating:movie.avgUserRating"></canvas></p>
</section>
注意
HTML5 自定义数据属性允许在 HTML 元素上嵌入元数据。您的属性名必须以data-前缀开头,后跟至少一个字符字符串;在我们的情况下,我们使用单词feed,因此我们的属性名是data-feed。它不允许使用大写字母,并且值是一个字符串。
将它们组合起来,我们得到:
for (var item in movies.items) {
movie = movies.items[item];
var movieDesc=(movie.synopsis &&movie.synopsis.length>200)?movie.synopsis.substr(0,200)+"...": movie.synopsis;
var movieHTML='<li itemscope itemtype="http://schema.org/Movie">';
movieHTML+='<img src="img/'+movie.poster+'" alt="'+movie.title+'" width="120" />';
movieHTML+='<section class="main-info">';
movieHTML+='<input type="button" class="charting-button" />';
movieHTML+='<input type="button" class="details-button" />';
movieHTML+='<h3 itemprop="name">'+movie.title+'</h3>';
movieHTML+='<p class="details genre" itemprop="genre">'+Array(movie.genre).join(', ')+'</p>';
movieHTML+='<p class="details">'+movie.mpaaRating+'</p>';
movieHTML+='<p class="theater">'+movie.theater.title+" "+movie.theater.address+'</p>';
movieHTML+='<p class="actors">'+Array(movie.selectedStar).join(', ')+'</p>';
movieHTML+='</section>';
movieHTML+='<section class="description">';
movieHTML+='<h3 itemprop="name">'+movie.title+'</h3>';
movieHTML+='<p>'+movieDesc+'</p>';
movieHTML+='</section>';
movieHTML+='<section class="charting">';
movieHTML+='<h3 itemprop="name">'+movie.title+'</h3>';
movieHTML+='<p><canvas data-feed= "MetaCritic:'+movie.avgMetaCriticRating+",EditorBoost:"+movie.editorBoost+",User Rating:"+movie.avgUserRating+'"></canvas></p>';
movieHTML+='</section>';
movieHTML+='</li>';
html+=movieHTML;
}
在styles.css中图表元素之前添加一些间距:
.charting canvas{
margin-top:10px;
}
要隐藏和显示charting和description区域,我们将使用desc类。如果desc类应用于li标签,我们将隐藏charting并显示description。否则,我们将隐藏description并显示charting:
#movies-near-me li.desc section.description, #movies-near-me li section.charting{
display:block;
}
#movies-near-me li section.description, #movies-near-me li.desc section.charting{
display:none;
}
回到 movienow.geolocation.js,我们定义显示图表(showCharts)和显示细节(showDetails)的方法。对于 showCharts,我们将使用 jQuery 的链式调用功能。$(event.target) 如果按钮被点击,那么我们使用 parent() 向上移动两级;从当前对象(li)中移除 desc 类,添加 open 类,并找到第一个 canvas 元素:
$(event.target)
.parent()
.parent()
.removeClass("desc")
.addClass("open")
.find("canvas")[0];
小贴士
jQuery 允许方法调用的连接,将每个方法应用于前一个方法的结果。这提高了性能,但有时会牺牲可读性。
我们将创建一个名为 charts 的函数来绘制每个画布,这个函数将 canvas 对象作为参数。我们的最终方法应该如下所示:
this.showCharts = function(event) {
that.charts(
$(event.target)
.parent()
.parent()
.removeClass("desc")
.addClass("open")
.find("canvas")[0]
);
};
应用相同的思路来展示细节:
this.showDetails = function(event) {
$(event.target)
.parent()
.parent()
.addClass("desc")
.addClass("open");
};
添加点击事件处理程序以打开和关闭:
$("#movies-near-me li .details-button").click(that.showDetails);
$("#movies-near-me li .description, #movies-near-me li .charting").click(function(){
$(this)
.parent()
.removeClass("open")
});
$("#movies-near-me li .charting-button").click(that.showCharts);
现在我们创建 movienow.charts.js 并添加 charts 方法:
var movienow = movienow || {};
movienow.charts = (function(){
var that = this;
this.charts = function(canvas){
that.drawBarChart(canvas);
};
this.drawBarChart = function(canvas) {
}
})();
注意,charts 方法调用了 drawBarChart 方法。我们使用这个结构来更改后续的绘图方法。
记得在 index.html 中包含 movienow.charts.js:
<script src="img/ios-orientationchange-fix.js"></script>
<script src="img/jquery-1.8.0.min.js"></script>
<script src="img/jquery.xdomainajax.js"></script>
<script src="img/movienow.charts.js"></script>
<script src="img/movienow.geolocation.js"></script>
<script src="img/movienow.js"></script>
一切都取决于上下文
Canvas 提供了在二维或三维空间中绘图的 API,在支持的情况下。Canvas 2D 的支持范围比 Canvas 3D 更广;后者通常在任何移动浏览器上都不被支持。
你可以通过获取画布上下文来定义要使用的 API。假设 chart 是我们的 canvas 对象。如果你想绘制二维图形,你可以使用:
var context=chart.getContext("2D");
然后,你可以使用 2D API 来绘制,例如,一个红色的正方形,定义其颜色:
context.fillStyle="#FF0000";
按以下方式绘制形状:
context.fillRect(0,0,20,20);
对于 3D API,使用要复杂得多。首先,它仍然没有得到完全支持,因为一些浏览器识别 webgl:
var context=chart.getContext("webgl");
当其他人使用 experimental-webgl:
var context=chart.getContext("experimental-webgl");
这是因为 webgl 规范仍在开发中。使用 WebGL 需要了解计算机图形和诸如相机、灯光、纹理、材质、映射等概念。
2D 上下文
我们将创建一个水*条形图来显示 MetaCritic、EditorBoost 和用户评分。想法是使用绿色、黄色和红色来表示评分的高低。
在以下图像中,我们可以看到我们的图表设计:

根据我们已建立的 movienow.charts.js 结构,让我们编写 drawBarChart 方法。由于我们不希望在每次调用此方法时都重新绘制我们的画布,我们将使用 painted 类作为标志来决定画布是否已经绘制。
然后,我们可以将 canvas 保存为 jQuery 对象:
var myCanvas=$(canvas);
然后,我们可以验证它是否有 painted 类:
if(!myCanvas.hasClass("painted")){
//DRAW HERE
myCanvas.addClass("painted");
}
在我们的条件语句中,我们将包含评分信息的 data-feed 分割,以便遍历它,为每个评分类别构建一个条形图:
var values=myCanvas.attr("data-feed").split(",");
然后我们获取 2D 上下文:
var context=canvas.getContext("2d");
Canvas 2D 绘图 API 概览
让我们回顾一下 Canvas 在二维上下文中最有用的方法:
样式
Canvas 2D API 中设置样式的使用方法如下所述:
-
context.strokeStyle(value): 这接收一个包含 CSS 颜色的字符串,用于描边;如果没有传递参数,则返回当前描边样式。 -
context.fillStyle(value): 它接收一个包含用于填充形状的 CSS 颜色的字符串;如果没有传递参数,则返回当前填充样式。提示
strokeStyle和fillStyle可以接收CanvasGradient或CanvasPattern作为参数,允许您绘制渐变和图案。有关如何创建渐变和图案的更多信息,您可以查看 canvas 2D API 规范:dev.w3.org/2006/canvas-api/canvas-2d-api.html。 -
context.lineWidth(value): 它使用像素定义线的宽度。它只接受正值;如果没有传递值,则充当获取器并返回当前线宽。 -
context.lineCap(value): 它设置线的端点(或帽)的样式。可能的值是butt、round和square。如果没有传递值,则返回当前的线帽样式。 -
context.lineJoin(value): 它设置线的连接样式。可能的值是bevel、round和miter。如果没有传递值,则返回当前的线连接样式。
字体样式
在 canvas 2D API 中设置字体样式的使用方法如下:
-
context.font(value): 它使用 CSS 语法字符串定义字体样式。如果没有传递值,则返回当前的字体样式。 -
context.fillText``(text,x,y[, maxWidth]): 绘制文本。它接收一个包含要绘制信息的text字符串,以像素为单位指定x和y坐标,以及定义容器宽度最大像素值的maxWidth,后者是可选的。 -
context.strokeText(text,x,y[, maxWidth]): 它的行为类似于fillText,但只绘制文本的描边。
绘制简单形状
在 Canvas 2D API 中绘制简单形状使用的方法如下:
-
context.clearRect(x, y, w, h): 它定义一个坐标为x和y、宽度为w和高度为h的矩形,并清除该区域内定义的所有像素。值以像素为单位。 -
context.fillRect(x, y, w, h): 使用预定义的fillStyle绘制坐标为x和y、宽度为w和高度为h的矩形。 -
context.strokeRect``(x, y, w, h): 使用预定义的strokeStyle绘制坐标为x和y、宽度为w和高度为h的矩形描边。
绘制复杂形状
要创建更复杂的形状,Canvas 2D API 允许您定义路径和子路径。路径是一系列子路径的集合,而子路径是由线条或曲线连接的点列表。
当前项目不需要复杂的形状,但了解允许您绘制曲线和线条的方法,使其能够绘制任何图形是很好的。
我们需要意识到我们的上下文始终包含一个当前路径,并且不可能有多个。
-
co``ntext.beginPath(): 它重置当前路径 -
context.closePath(): 它关闭当前路径并创建一个新的路径,其第一个点使用与最后一个子路径点相同的坐标 -
context.moveTo(x, y): 它使用坐标x和y创建一个新的子路径 -
context.lineTo(x, y): 它在当前点和坐标为x和y的新点之间创建一条线,并将最新点添加到当前子路径中 -
context.quadraticCurveTo(cpx, cpy, x, y): 它使用一个控制点创建一个二次曲线,该控制点在 x 轴上的坐标为cpx,在 y 轴上的坐标为cpy,在当前点和坐标为x和y的新点之间 -
context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y): 它使用两个控制点 cp1(cp1x,cp1y) 和 cp2 (cp2x,cp2y) 在当前点和坐标为x和y的新点之间创建一个贝塞尔曲线 -
context.arcTo(x1, y2, x2, y2, radius): 它通过一个半径由参数radius定义的弧将当前点与坐标为x1和y1的新点连接起来,然后通过一个弧与之前的一个点连接,坐标为x2和y2 -
context.rect(x, y, w, h): 它将一个坐标为x和y、宽度为w和高度为h的矩形添加到子路径列表中 -
context.fill(): 它将当前填充样式应用于填充子路径 -
context.stroke(): 它将当前描边样式应用于创建子路径的描边线 -
context.isPointInPath(x, y): 如果由坐标x和y定义的点在当前路径中,则返回true,否则返回false
有其他方法对于更复杂的绘制和动画可能很有用;您可以在以下位置查看画布规范:dev.w3.org/2006/canvas-api/canvas-2d-api.html。
绘制图表
首先,让我们为该上下文定义文本样式以及一个用于计算当前条索引的变量(以防不是所有电影都有相同数量的评分类别):
context.font = "bold 14px sans-serif";
var index=0;
遍历评分类别并获取当前值:
for(var i=0; i<values.length; i++){
var info=values[i].split(":");
var val=info[1];
}
我们首先绘制一个灰色条(#292929),它定义了宽度为 290 像素和高度为 26 像素的宽度。使用 fillRect(x,y,width,height) 语法,注意我们使用 36 来在条之间提供 10 像素的间隔:
var pos=index*36;
context.fillStyle="#292929";
context.fillRect(0,pos,290,26);
注意
画布原点位于画布 DOM 对象的左上角。正值位于原点下方并向右。
然后,我们可以绘制我们的颜色条。为此,我们定义一个 getChartColor 方法,该方法根据评分值返回不同的颜色,对于较高的评分使用绿色,对于中等评分使用黄色,对于最低评分使用红色:
this.getChartColor = function(val){
var result="";
if(val<40){
result="#FF0066";
}else{
if(val<80){
result="#FFCC33";
}else{
result="#66CC33";
}
}
return result;
};
使用我们的 getChartColor 方法以及当前的评分值 val,我们可以设置形状:
context.fillStyle=that.getChartColor(val);
context.fillRect(0,pos,val*2.9,26);
要绘制评分类别标题,我们更改填充样式为半透明的白色,然后使用 fillText 语法 fillText(text, x,y):
context.fillStyle = "rgba(255, 255, 255, .9)";
context.fillText(info[0]+" "+val+"%", 10, pos+18);
index++;
小贴士
注意,我们可以使用十六进制、RGB 或 RGBA 颜色来定义 fillStyle。
最后,我们编写一个验证,如果没有数据,则显示一条消息,说明无可用数据:
if(index==0){
context.fillStyle = "#FFFFFF";
context.fillText("No Data Available", 40, 50);
}
将所有内容整合:
this.drawBarChart = function(canvas) {
var myCanvas=$(canvas);
if(!myCanvas.hasClass("painted")){
var values=myCanvas.attr("data-feed").split(",");
var context=canvas.getContext("2d");
context.font = "bold 14px sans-serif";
var index=0;
for(var i=0; i<values.length; i++){
var info=values[i].split(":");
var val=info[1];
if(val>0){
var pos=index*36;
context.fillStyle="#292929";
context.fillRect(0,pos,290,26);
context.fillStyle=that.getChartColor(val);
context.fillRect(0,pos,val*2.9,26);
context.fillStyle = "rgba(255, 255, 255, .9)";
context.fillText(info[0]+" "+val+"%", 10, pos+18);
index++;
}
}
if(index==0){
context.fillStyle = "#FFFFFF";
context.fillText("No Data Available", 40, 50);
}
myCanvas.addClass("painted");
}
};
在这个例子中,我们不需要清除我们的画布区域,因为我们第一次绘制后没有进行动画或更改信息。然而,如果我们确实需要重新绘制,我们可以使用:
context.clearRect(0, 0, canvas.width, canvas.height);
注意
Canvas 2D 上下文提供了一种过程式绘图方法——创建位图图像。如果我们需要使用矢量图形而不是位图,可以使用可缩放矢量图形(SVG),它使用 XML 提供了一种声明式方法。要了解更多关于 SVG 的信息,你可以在这里研究 www.w3.org/Graphics/SVG/。
Canvas 2D API 自 9.0 版本以来被所有现代浏览器支持,包括 Internet Explorer。正如我们在第二章中所示,HTML 入门套件:实用工具,仍然可以使用 ExplorerCanvas code.google.com/p/explorercanvas/downloads/list来支持之前的 Internet Explorer 版本。
3D 上下文 – WebGL 和实验性 WebGL
现在我们已经看到了我们图表解决方案的二维实现,让我们尝试创建一个三维版本,并添加一些动画使事物更加有趣。
注意
当我们考虑用户体验时,一个普遍的规则是“少即是多”。这意味着保持简单可以使我们的应用程序更易于使用。在这种情况下,我们将添加不必要的动画,以便学习如何进行操作。
在我们的例子中,我们将使用 WebGL 绘制条形图,并使用 DOM 对象绘制评分类别标题。
最终实现应该看起来像以下这样:

进入三维世界
WebGL 规范基于嵌入式系统 OpenGL 2.0(或 OpenGL ES)规范。除非你熟悉 OpenGL 并且需要执行低级操作,否则建议你使用抽象其使用的库。其优点包括代码可读性更高、开发时间更短、更好的可扩展性。
在我们的例子中,我们选择了Three.js,这是一个 JavaScript 库,它通过使用其他三维库中常用的隐喻简化了 WebGL 的使用。
小贴士
当你必须决定使用哪个库时,请考虑项目目标,并考虑该库未来的改进和限制。
Three.js
Three.js是一个 JavaScript 库,它抽象了 3D 操作,允许我们使用场景、相机、对象等简单的隐喻。您可以从github.com/mrdoob/three.js/下载Three.js,并在mrdoob.github.com/three.js/docs/50/阅读其文档。
让我们回顾一些基本概念:
场景
场景是我们可以插入对象的虚拟环境。每个对象都必须在一个scene中才能可视化。
您可以使用scene = new THREE.Scene()创建场景,并使用scene.add(object)向其中添加对象。
相机
相机指示要可视化的场景部分。将其视为一部电影;如果我们想记录特定的地方,我们需要将相机指向它。Three.js提供了一个抽象的Camera类用于相机,两个基本相机和两个额外的相机实现。
OrthographicCamera 使用构造函数参数定义的立方体定义正交投影
OrthographicCamera(left, right, top, bottom, near, far)
-
left– 使用表示位置的浮点数定义左*面 -
right– 使用表示位置的浮点数定义右*面 -
top– 使用表示位置的浮点数定义顶*面 -
bottom– 使用表示位置的浮点数定义底*面 -
near– 使用表示位置的浮点数定义最*的*面或**面 -
far– 使用表示位置的浮点数定义最远的*面或远*面
PerspectiveCamera 使用视场、宽高比、*点和远点值定义透视投影:
PerspectiveCamera(fov, aspect, near, far)
-
fov– 使用浮点数定义表示视场的角度 -
aspect– 定义由浮点数指定的相机宽高比 -
near– 在OrthographicCamera中,使用浮点数定义**面 -
far– 在OrthographicCamera中,使用浮点数定义远*面
以下图中展示了透视相机的视觉表示:

材质
材质定义了一组描述对象外观的属性。
纹理
纹理使用图像(或程序性图案)定义对象的外观。
网格
网格是可添加到场景的对象列表的一部分。您可以将几何体和材质分配给网格。
几何体
几何体是可分配给网格的对象的表示。在我们的例子中,我们将使用CubeGeometry来定义我们的条形。
首先,我们将下载并将three.js包含在我们的index.html文件中:
<script src="img/ios-orientationchange-fix.js"></script>
<script src="img/jquery-1.8.0.min.js"></script>
<script src="img/jquery.xdomainajax.js"></script>
<script src="img/three.js"></script>
<script src="img/movienow.charts.js"></script>
<script src="img/movienow.geolocation.js"></script>
<script src="img/movienow.js"></script>
我们然后向我们的charts类添加一个新参数来指定我们想要使用的渲染:
this.charts = function(canvas, type){
switch(type){
case "3DChart":
that.draw3DChart(canvas);
break;
case "barChart":
default:
that.drawBarChart(canvas);
break;
}
};
我们在showCharts方法中将默认定义为3DChart:
this.showCharts = function(event) {
that.charts($(event.target).parent().parent().removeClass("desc").addClass("open").find("canvas")[0], "3DChart");
};
现在我们来编写我们的 3D 绘图方法draw3DChart。像我们之前的二维绘图方法一样,这个方法将我们的canvas作为参数:
this.draw3DChart = function(canvas) {
}
与之前一样,我们验证 Canvas 是否有painted类以避免再次初始化它。然后我们可以验证 WebGL 支持,如果不支持,我们渲染 2D 图表:
this.draw3DChart = function(canvas) {
var myCanvas=$(canvas);
var myCanvasParent=myCanvas.parent();
if(!myCanvas.hasClass("painted")){
var webGlSupport=false;
try {
/*** VERIFICATION OF WEBGL SUPPORT ***/
webGlSupport = !!window.WebGLRenderingContext && !!document.createElement('canvas').getContext('experimental-webgl');
}catch(e){}
if (webGlSupport){
//DRAW 3D HERE
}else{
/** IF NOT WEBGL SUPPORT RENDERS CHART IN 2D ***/
that.drawBarChart(canvas);
}
}
};
在webGLSupport内部,我们获取我们的评分数据、canvas尺寸和存储动画时间的变量:
var data=myCanvas.attr("data-feed");
var values=data.split(",");
var w = myCanvas.width();
var h = myCanvas.height();
var lastTime = 0;
现在我们使用 WebGL 定义一个three.js渲染器,它包含我们的新画布,并设置其尺寸:
var renderer = new THREE.WebGLRenderer();
renderer.setSize(w, h);
我们将data属性和painted类分配给渲染器 DOM 元素(canvas),并用这个替换我们旧的canvas。
var newCanvas=$(renderer.domElement);
newCanvas.attr("data-feed",data);
myCanvas.addClass("painted");
myCanvas.replaceWith(newCanvas);
我们使用视场(FOV)45 度、基于画布尺寸的宽高比、*点为 1 和远点为 1000 来实现我们的相机,并将相机定位在 z 轴上的 700 处。
var camera = new THREE.PerspectiveCamera(45, w/h, 1, 1000);
camera.position.z = 700;
然后我们定义我们的场景,一个用于存储稍后要渲染的网格的bars数组,一个类似于我们 2D 示例中的index,以及一个用于存储将显示标题的 DOM 对象的labels字符串:
var scene = new THREE.Scene();
var bars=[];
var index=0;
var labels="<div class='chart-labels'>";
按照我们之前的示例迭代评分类别。我们使用getChartColor为我们当前的值获取一个颜色,并将#替换为0x,因为使用了颜色表示法:
var mainColor=that.getChartColor(val).replace("#", "0x");
由于每个条形将有六个面,我们将有六种不同的材质。每一种都将有自己的颜色,因此我们为colors和materials定义数组。我们还在labels字符串中填充我们的标题信息:
var colors = [mainColor, mainColor, mainColor, mainColor, mainColor, mainColor];
var materials = [];
labels+="<div>"+info[0]+"</div>";
for (var n = 0; n < 6; n++) {
materials.push([
new THREE.MeshLambertMaterial({
color: colors[n],
opacity:0.6,
transparent: true,
shading: THREE.FlatShading,
vertexColors: THREE.VertexColors
}),
new THREE.MeshBasicMaterial({
color: colors[n],
shading: THREE.FlatShading,
wireframe: true,
transparent: true
})
]);
}
注意,我们可以分配多个材质。在这种情况下,我们使用以下方式为我们的实体定义透明填充:
THREE.MeshLambertMaterial({
color: colors[n],
opacity:0.6,
transparent:
true,
shading: THREE.FlatShading,
vertexColors: THREE.VertexColors
})
以下绘制了边缘:
new THREE.MeshBasicMaterial({
color: colors[n],
shading: THREE.FlatShading,
wireframe: true,
transparent: true
})
每个bar被定义为具有几何形状的网格。使用以下CubeGeometry语法:
CubeGeometry(width, height, depth, segmentsWidth, segmentsHeight, segmentsDepth)
这将创建一个新的CubeGeometry对象:
var bar = new THREE.Mesh(new THREE.CubeGeometry(myWidth, 90, 90, 1, 1, 1, materials), new THREE.MeshFaceMaterial());
动画我们的几何形状
我们将动画条形的增长,所以我们将它们在 x 轴上缩放。
bar.scale.x=.01;
网格的参考点位于其中心,因此定位网格并设置重绘以管理透明几何形状,我们有:
bar.position.y=200-(index*140);
bar.position.x=-500+(myWidth/2)*bar.scale.x;
bar.overdraw = true;
我们将条形添加到我们的场景和我们的数组中,并设置最终宽度,这是动画结束时的宽度:
scene.add(bar);
bars.push({object:bar, width:myWidth});
index++;
因此,我们的迭代应该看起来如下:
for(var i=0; i<values.length; i++){
var info=values[i].split(":");
var val=info[1];
if(val>0){
var mainColor=that.getChartColor(val).replace("#", "0x");
var colors = [mainColor, mainColor, mainColor, mainColor, mainColor, mainColor];
var materials = [];
labels+="<div>"+info[0]+"</div>";
for (var n = 0; n < 6; n++) {
materials.push([
new THREE.MeshLambertMaterial({
color: colors[n],
opacity:0.6,
transparent: true,
shading: THREE.FlatShading,
vertexColors: THREE.VertexColors
}),
new THREE.MeshBasicMaterial({
color: colors[n],
shading: THREE.FlatShading,
wireframe: true,
transparent: true
})
]);
}
var myWidth=val*8;
var bar = new THREE.Mesh(new THREE.CubeGeometry(myWidth, 90, 90, 1, 1, 1, materials), new THREE.MeshFaceMaterial());
bar.scale.x=.01;
bar.position.y=200-(index*140);
bar.position.x=-500+(myWidth/2)*bar.scale.x;
bar.overdraw = true;
scene.add(bar);
bars.push({object:bar, width:myWidth});
index++;
}
}
到目前为止,我们还没有渲染任何东西。我们可以通过设置labels字符串并附加它来纠正这一点:
labels+"</div>";
myCanvasParent.append(labels);
我们设置一个three结构,我们将用它来进行渲染,然后调用我们的渲染方法animate3DChart:
var three = {
renderer: renderer,
camera: camera,
scene: scene,
bars: bars
};
that.animate3DChart(lastTime, three);
我们的draw3DChart方法如下所示:
this.draw3DChart = function(canvas) {
var myCanvas=$(canvas);
var myCanvasParent=myCanvas.parent();
if(!myCanvas.hasClass("painted")){
var webGlSupport=false;
try {
/*** VERIFICATION OF WEBGL SUPPORT ***/
webGlSupport = !!window.WebGLRenderingContext && !!document.createElement('canvas').getContext('experimental-webgl');
}catch(e){}
if (webGlSupport){
var data=myCanvas.attr("data-feed");
var values=data.split(",");
var w = myCanvas.width();
var h = myCanvas.height();
var lastTime = 0;
var renderer = new THREE.WebGLRenderer();
renderer.setSize(w, h);
var newCanvas=$(renderer.domElement);
newCanvas.attr("data-feed",data);
myCanvas.addClass("painted");
/*** REPLACES ORIGINAL CANVAS WITH THREE.JS CANVAS ***/
myCanvas.replaceWith(newCanvas);
/*** CAMERA DEFINITION ***/
var camera = new THREE.PerspectiveCamera(45, w/h, 1, 1000);
camera.position.z = 700;
/*** SCENE DEFINITION ***/
var scene = new THREE.Scene();
var bars=[];
var index=0;
var labels="<div class='chart-labels'>";
for(var i=0; i<values.length; i++){
var info=values[i].split(":");
var val=info[1];
if(val>0){
var mainColor=that.getChartColor(val).replace("#", "0x");
var colors = [mainColor, mainColor, mainColor, mainColor, mainColor, mainColor];
var materials = [];
labels+="<div>"+info[0]+"</div>";
for (var n = 0; n < 6; n++) {
materials.push([
new THREE.MeshLambertMaterial({
color: colors[n],
opacity:0.6,
transparent: true,
shading: THREE.FlatShading,
vertexColors: THREE.VertexColors
}),
new THREE.MeshBasicMaterial({
color: colors[n],
shading: THREE.FlatShading,
wireframe: true,
transparent: true
})
]);
}
var myWidth=val*8;
var bar = new THREE.Mesh(new THREE.CubeGeometry(myWidth, 90, 90, 1, 1, 1, materials), new THREE.MeshFaceMaterial());
bar.scale.x=.01;
bar.position.y=200-(index*140);
bar.position.x=-500+(myWidth/2)*bar.scale.x;
bar.overdraw = true;
scene.add(bar);
bars.push({object:bar, width:myWidth});
index++;
}
}
labels+"</div>";
myCanvasParent.append(labels);
/*** SAVE INFORMATION REQUIRED TO RENDER SCENE ***/
var three = {
renderer: renderer,
camera: camera,
scene: scene,
bars: bars
};
that.animate3DChart(lastTime, three);
}else{
/** IF NOT WEBGL SUPPORT RENDERS CHART IN 2D ***/
that.drawBarChart(canvas);
}
}
};
结束
我们创建一个window.requestAnimFrame方法来抽象我们的动画timeout定义。注意,我们使用1000/60。这表示每秒 60 帧(FPS):
window.requestAnimFrame = (function(callback){
return window.requestAnimationFrame ||window.webkitRequestAnimationFrame ||window.mozRequestAnimationFrame ||window.oRequestAnimationFrame ||window.msRequestAnimationFrame ||function(callback){
/* Using 60FPS */
window.setTimeout(callback, 1000 / 60);
};
})();
对于animate3DChart方法,我们简单地定义一个变量来停止我们的动画(isReady),并缩放和定位每个bar,直到我们达到 100%的缩放(在这种情况下为1):
this.animate3DChart = function(lastTime, three){
var isReady=false;
for(var i=0; i<three.bars.length; i++){
if(three.bars[i].object.scale.x<1){
three.bars[i].object.scale.x+=.03;
three.bars[i].object.position.x=-500+(three.bars[i].width/2)*three.bars[i].object.scale.x;
}
isReady=(three.bars[i].object.scale.x>=1);
}
lastTime = time;
/*** SCENE RENDER USING THREE.JS ***/
three.renderer.render(three.scene, three.camera);
if(!isReady){
requestAnimFrame(function(){
that.animate3DChart(lastTime, three);
});
}
}
如果我们想让每个条形无限旋转,我们可以定义一些值来控制动画:
var angularSpeed = 1.2;
var date = new Date();
var time = date.getTime();
var timeDiff = time - lastTime;
var angleChange = angularSpeed * timeDiff * 2 * Math.PI / 1000;
我们可以将isReady=(three.bars[i].object.scale.x>=1)替换为three.bars[i].object.rotation.x += angleChange。
要修改 x 轴上的旋转,我们可以添加以下内容:
this.animate3DChart = function(lastTime, three){
var angularSpeed = 1.2;
var date = new Date();
var time = date.getTime();
var timeDiff = time - lastTime;
var angleChange = angularSpeed * timeDiff * 2 * Math.PI / 1000;
var isReady=false;
for(var i=0; i<three.bars.length; i++){
if(three.bars[i].object.scale.x<1){
three.bars[i].object.scale.x+=.03;
three.bars[i].object.position.x=-500+(three.bars[i].width/2)*three.bars[i].object.scale.x;
}
//isReady=(three.bars[i].object.scale.x>=1);
three.bars[i].object.rotation.x += angleChange;
}
lastTime = time;
/*** SCENE RENDER USING THREE.JS ***/
three.renderer.render(three.scene, three.camera);
if(!isReady){
requestAnimFrame(function(){
that.animate3DChart(lastTime, three);
});
}
}
WebGL 画布 API 并非所有浏览器都完全支持。您可以使用 Firefox 4.0+、Chrome、Opera 和从版本 5.1 开始(在 OSX 或更高版本上,但不是 iOS 设备上的 Safari 或 Windows 上的 Safari)的 3D API。
小贴士
WebGL 在 Safari 中默认禁用。要在 Safari 中启用 WebGL,请点击Safari菜单并选择偏好设置,然后点击高级选项卡。在底部,勾选在菜单栏中显示开发菜单复选框。打开开发菜单,然后选择启用 WebGL。
对于 Internet Explorer,您可以通过安装 Chrome Frame 插件www.google.com/chromeframe来启用 WebGL 支持。Google Chrome Frame 用 Google Chrome 的 WebKit 布局引擎和 V8 JavaScript 引擎的版本替换了 Internet Explorer 的渲染机制。
摘要
尽管画布规范仍在开发中,我们仍然可以在我们的企业应用中应用其 API,以应对无数的使用场景。图表、科学可视化、图表和动画向导只是冰山一角。作为开发者,我们应该始终充分考虑回退或替代方案,以防某些功能不受支持,以确保适当的跨*台兼容性。
下一章将介绍使用 HTML5 的拖放功能和事件委托。
第八章。应用:通过拖放选择 UI
尽管拖放功能自 1999 年微软在 Internet Explorer 5.0 中实现以来就已经存在,但 HTML5 以一种更标准的方式将其推向前台。该规范定义了一组 API、事件处理程序和标记,用于将拖放功能(DnD)添加到您的企业应用程序中。为了演示这一点,我们将在 MovieNow 企业应用程序中实现将电影放映时间拖放到预演区域的功能,以指示用户感兴趣观看的电影。
本章涵盖的主要主题是:
-
添加放映时间
-
样式化放映时间
-
真是糟糕
-
放下它
添加放映时间
我们暂时从第四章应用:通过地理位置获取电影中移除了放映时间,以腾出空间放置电影数据、剧情简介、预告片和评分。现在我们将重新添加放映时间。为此,我们将通过在movienow.geolocation.js中的displayShowtimes方法中插入一个div标签来包含放映时间,并遍历我们之前构建的movie对象中包含的放映时间数组。注意我们如何包含一个data-movie属性,其中包含影院 ID、电影 ID 和放映时间。我们这样做是为了保存一些关于放映时间的数据,以便在以后我们需要知道放映时间属于哪部电影和哪个影院时使用。
我们将把以下代码片段插入到displayShowtimes方法中:
movieHTML+='<div class="showtimes">';
if (typeof movie.showtime == 'string') movie.showtime = Array(movie.showtime);
for(var i=0; i<movie.showtime.length; i++) {
if (movie.showtime[i]) movieHTML+='<div class="showtime" draggable="true" title="'+movie.title+' @ '+movie.theater.title+' ('+movie.theater.address+')" data-movie= "'+movie.theater.id+':'+movie.id+':'+movie.showtime[i]+'">'+that.formatTime(movie.showtime[i])+'</div> ';
}
movieHTML+='</div>';
完整的方法应如下所示:
this.displayShowtimes = function(movies) {
var movie = null;
var html = '<ul>';
for (var item in movies.items) {
movie = movies.items[item];
var movieDesc='';
if (movie.synopsis) movieDesc=(movie.synopsis.length>200)?movie.synopsis.substr(0,200)+"...": movie.synopsis;
var movieHTML='<li itemscope itemtype="http://schema.org/Movie">';
movieHTML+='<img src="img/'+movie.poster+'" alt="'+movie.title+'" width="120" />';
movieHTML+='<section class="main-info">';
movieHTML+='<input type="button" class="charting-button" />';
movieHTML+='<input type="button" class="details-button" />';
movieHTML+='<h3 itemprop="name">'+movie.title+'</h3>';
movieHTML+='<p class="details genre" itemprop="genre">'+Array(movie.genre).join(', ')+'</p>';
movieHTML+='<p class="details">'+movie.mpaaRating+'</p>';
movieHTML+='<p class="theater">'+movie.theater.title+" "+movie.theater.address+'</p>';
movieHTML+='<p class="actors">'+Array(movie.selectedStar).join(', ')+'</p>';
movieHTML+='<div class="showtimes">';
if (typeof movie.showtime == 'string') movie.showtime = Array(movie.showtime);
for(var i=0; i<movie.showtime.length; i++) {
if (movie.showtime[i]) movieHTML+='<div class="showtime" draggable="true" title="'+movie.title+' @ '+movie.theater.title+' ('+movie.theater.address+')" data-movie="'+movie.theater.id+':'+movie.id+':'+movie.showtime[i]+'">'+movie.showtime[i]+'</div> ';
}
movieHTML+='</div>';
movieHTML+='</section>';
movieHTML+='<section class="description">';
movieHTML+='<h3 itemprop="name">'+movie.title+'</h3>';
movieHTML+='<p>'+movieDesc+'</p>';
movieHTML+='</section>';
movieHTML+='<section class="charting">';
movieHTML+='<h3 itemprop="name">'+movie.title+'</h3>';
movieHTML+='<p><canvas data-feed= "MetaCritic:'+movie.avgMetaCriticRating+",EditorBoost:"+movie.editorBoost+",User Rating:"+movie.avgUserRating+'"></canvas></p>';
movieHTML+='</section>';
movieHTML+='</li>';
html+=movieHTML;
}
html+= '</ul>';
$('#movies-near-me').html(html);
$("#movies-near-me li .details-button").click(that.showDetails);
$("#movies-near-me li .description, #movies-near-me li .charting").click(function(){$(this).parent().removeClass("open")});
$("#movies-near-me li .charting-button").click(that.showCharts);
init();
};
如果您在网页浏览器中预览此更改,您应该看到以下类似的内容:

样式化放映时间
当然,原始的放映时间数据看起来并不像我们传统上看时间的方式。我们需要格式化时间,以确保我们的用户正确理解数据。
为了实现这一点,我们将修改以下displayShowtimes中的以下行:
if (movie.showtime[i]) movieHTML+='<div class="showtime" title="'+movie.title+' @ '+movie.theater.title+' ('+movie.theater.address+')" data-movie= "'+movie.theater.id+':'+movie.id+':'+movie.showtime[i]+'">'+movie.showtime[i]+'</div> ';
我们将使用即将编写的formatTime方法包装放映时间的显示。更改之前的行,使其看起来类似于以下行:
if (movie.showtime[i]) movieHTML+='<div class="showtime" title="'+movie.title+' @ '+movie.theater.title+' ('+movie.theater.address+')" data-movie= "'+movie.theater.id+':'+movie.id+':'+movie.showtime[i]+'">'+that.formatTime(movie.showtime[i])+'</div> ';
然后,我们可以添加以下方法来格式化时间。此方法接收传递给它的字符串,获取小时的第一两个字符,分钟的相邻两个字符,然后解释并修改小时数据,将其从 24 小时制改为 12 小时制。
this.formatTime = function(time) {
var hh = time.substr(0,2);
var mm = time.substr(2,2);
var period = 'AM';
hh = parseInt(hh, 10);
if (hh >= 12) period = 'PM';
if (hh > 12) hh -= 12;
return hh+':'+mm+period;
};
此更改的预览应类似于以下截图:

为了使放映时间更加美观,我们在styles.css中添加以下样式:
.showtimes {
float:left;
margin-left:10px;
}
.showtimes .showtime {
float:left;
padding:3px;
margin:0 2px;
border:1px solid #666;
-moz-border-radius:5px;
border-radius:5px;
cursor:move;
}
我们将放映时间浮动到左侧,以便它们水*排列,并在它们周围添加一个边框,以便与其他内容区分开来。最后,我们将光标设置为移动,这样当您将鼠标悬停在它们上时,鼠标指针会变为操作系统中的移动图标,以指示放映时间是一个可移动的对象。
刷新预览应显示如下内容:

真是件麻烦事
在 HTML5 中使某个元素可拖动的第一步是将 draggable 属性附加到该元素。这会向浏览器发出信号,创建一个元素幽灵图像,当用户触发鼠标按下事件时,该图像将出现并跟随鼠标指针,从而实现“拖动”元素,并在鼠标按钮释放时消失。
在 displayShowtimes 中更改此行,其中我们显示放映时间:
if (movie.showtime[i]) movieHTML+='<div class="showtime" title="'+movie.title+' @ '+movie.theater.title+' ('+movie.theater.address+')" data-movie= "'+movie.theater.id+':'+movie.id+':'+movie.showtime[i]+'">'+that.formatTime(movie.showtime[i])+'</div> ';
现在应包括 draggable="true" 属性:
if (movie.showtime[i]) movieHTML+='<div class="showtime" draggable="true" title="'+movie.title+' @ '+movie.theater.title+' ('+movie.theater.address+')" data-movie= "'+movie.theater.id+':'+movie.id+':'+movie.showtime[i]+'">'+that.formatTime(movie.showtime[i])+'</div> ';
接下来,将以下 CSS 样式添加到 styles.css。
[draggable=true] {
-moz-user-select:none;
-khtml-user-select:none;
-webkit-user-select:none;
user-select:none;
-khtml-user-drag:element;
-webkit-user-drag:element;
}
注意
前缀 -khtml 是用于旧版 Safari 的。
由于浏览器在用户点击并拖动时的默认行为是选择文本高亮,我们需要覆盖此行为。之前给出的样式是针对不同浏览器的简写,以防止这种行为。
注意
对于 Internet Explorer,我们需要一个 JavaScript 解决方案来覆盖拖动的默认选择行为,因为没有等效功能。我们将在实现拖放行为时介绍这一点。
最后,我们需要一些 JavaScript 来处理拖动时触发的事件。为了开始,我们需要创建一个新的 JavaScript 文件。我们将称之为 movienow.draganddrop.js 并将其放置在 js 文件夹中。我们还需要在 index.html 中添加对这个新文件的引用。在关闭 body 标签之前添加以下内容:
<script src="img/movienow.draganddrop.js"></script>
index.html 中的脚本标签应类似于以下代码片段:
<script src="img/ios-orientationchange-fix.js"></script>
<script src="img/jquery-1.8.0.min.js"></script>
<script src="img/jquery.xdomainajax.js"></script>
<script src="img/three.js"></script>
<script src="img/movienow.draganddrop.js"></script>
<script src="img/movienow.charts.js"></script>
<script src="img/movienow.geolocation.js"></script>
<script src="img/movienow.js"></script>
</body>
使用 JavaScript 处理拖动
在 movienow.draganddrop.js 中,我们将首先创建一个简单的对象:
var movienow = movienow || {};
movienow.draganddrop = (function(){
var that = this;
})();
在该对象中,我们将添加一个 init 方法,当放映时间加载到页面时执行。看看以下代码:
this.init = function() {
var dragItems = $('[draggable=true]');
for (var i=0; i<dragItems.length; i++) {
$(dragItems[i])[0].addEventListener('dragstart', function(event){
return false;
});
$(dragItems[i])[0].addEventListener('dragend', function(event) {
return false;
});
}
}
init 方法使用 jQuery 查找所有可拖动元素,即具有 draggable="true" 属性和值的元素。然后它遍历可拖动元素的集合,并为 dragstart 和 dragend 事件添加事件监听器。当可拖动元素被拖动时,会触发 dragstart 事件。所有事件监听器依次被调用。在这种情况下,我们只是什么也不做并返回 false,但稍后我们将做一些更有趣的事情。
注意
拖动事件
dragstart – 当可拖动元素开始被拖动时触发
drag – 当鼠标移动且可拖动元素正在被拖动时触发
dragend – 当可拖动元素被放下(当用户释放鼠标按钮时)时触发
dragenter – 当目标元素中拖入一个拖动元素时触发
dragover – 当鼠标在拖动元素内部移动时,会触发目标元素
dragleave – 当目标元素中拖出拖动元素时触发
drop – 当目标元素中释放拖动元素时触发
最后,当电影数据加载时,我们需要调用 init 方法。在 movienow.geolocation.js 中的 displayShowtimes 方法,我们需要将以下行作为方法的最后一行结束:
init();
在我们继续之前,我们需要添加以下内容以适应 Internet Explorer。由于 Internet Explorer 没有使用 CSS 覆盖拖动默认选择行为的方法,我们需要使用 JavaScript 实现。在这种情况下,我们处理 selectstart 事件,并在它被触发时向浏览器指示我们正在拖放:
$(dragItems).bind('selectstart', function() {
this.dragDrop(); return false;
});
我们的 init 方法现在应该看起来像以下这样:
this.init = function() {
var dragItems = $('[draggable=true]');
for (var i=0; i<dragItems.length; i++) {
$(dragItems[i])[0].addEventListener('dragstart', function(event){
return false;
});
$(dragItems[i])[0].addEventListener('dragend', function(event) {
return false;
});
}
$(dragItems).bind('selectstart', function() {
this.dragDrop(); return false;
});
}
放下它
既然我们可以拖动东西,让我们看看如何放下它们,当它们被放下时做一些有用的事情。首先,我们需要一个放置元素的地方。对于放映时间,我们将在页面右侧创建一个区域来放置元素。一旦元素被放置在那里,它们将显示在 Top 5 Box Office 部分之上。
让我们在 index.html 中的 aside 标签内添加几个 div 标签。我们将它们命名为 dropzone 和 dropstage。将以下行添加到 aside 标签的开头:
<div id="dropzone">Drop Here</div>
<div id="dropstage">
<h2>Selected Times</h2>
</div>
aside 标签的开头应该看起来像以下代码:
<aside>
<div id="dropzone">Drop Here</div>
<div id="dropstage">
<h2>Selected Times</h2>
</div>
<h2>Top 5 Box Office</h2>
现在我们有了一个放置元素的地方,以及一个放置元素的地方,一旦它们被放下,让我们专注于 dropzone。这是我们放置元素的区域。将以下样式添加到 styles.css:
#dropzone {
border:1px solid #ffb73d;
width:198px;
height:auto;
min-height:100%;
text-align:center;
z-index:2;
position:absolute;
margin-top:-28px;
padding-top:200px;
background: rgb(215, 215, 0);
background: rgba(255, 215, 0, 0.5);
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#80FFD700, endColorstr=#80FFD700);
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#80FFD700, endColorstr=#80FFD700)";
}
小贴士
注意,filter:progid 和 progid 是针对 Internet Explorer 的特定功能。
这应该添加一个覆盖右侧的黄色框。你的预览应该看起来像以下截图:

切换放置区域
让我们重新审视 movienow.draganddrop.js。记住,我们为拖动开始和拖动结束添加了事件监听器。让我们使用这些来在拖动时隐藏和显示放置区域。
将以下内容添加到 dragstart 事件监听器:
$('#dropzone').show();
将以下内容添加到 dragend 事件监听器:
$('#dropzone').hide();
事件监听器应该看起来像以下代码片段:
$(dragItems[i])[0].addEventListener('dragstart', function(event){
$('#dropzone').show();
return false;
});
$(dragItems[i])[0].addEventListener('dragend', function(event) {
$('#dropzone').hide();
return false;
});
默认隐藏放置区域,向 #dropzone 样式添加以下行:
display:none;
到目前为止,当你预览时,放置区域应该只在拖动放映时间时出现。
转移一些数据
我们开始理解拖放机制。然而,为了使我们的拖动功能更有趣,我们需要将一些数据附加到可拖动的元素上,这样一旦放下,我们就有一些有趣的东西可以展示。为了简单起见,让我们转移元素本身。为此,我们设置 event 对象的 dataTransfer 属性。event 对象允许我们跟踪页面上的事件并管理它们的数据。它是所有事件监听器的参数。
将以下行添加到 dragstart 事件监听器:
event.dataTransfer.setData('Text', this.outerHTML);
dragstart 事件监听器应该看起来像以下代码:
$(dragItems[i])[0].addEventListener('dragstart', function(event){
$('#dropzone').show();
event.dataTransfer.setData('Text', this.outerHTML);
return false;
});
我们还需要在拖放区域添加一些事件监听器,以便当元素被放置在其上时,我们可以让它做一些有趣的事情。在这种情况下,我们将在拖放阶段显示它。
将以下内容添加到movienow.draganddrop.js中的init方法:
$('#dropzone')[0].addEventListener('drop', function(event) {0event.stopPropagation();
if (event.preventDefault) event.preventDefault();
$('#dropstage').append(event.dataTransfer.getData('Text')).show();
return false;
});
$('#dropzone')[0].addEventListener('dragover', function(event) {
if (event.preventDefault) event.preventDefault();
return false;
});
$('#dropzone')[0].addEventListener('dragenter', function(event) {
if (event.preventDefault) event.preventDefault();
return false;
});
drop事件是核心。这是处理元素被拖放时发生的事情的地方。请注意,我们从dragstart中获取存储的数据并将其附加到dropstage的div标签上。我们还必须在drop、dragover和dragenter事件上停止传播、防止默认行为并返回false,以防止浏览器浏览到该元素。
显示结果
现在我们已经完成了拖动的框架,我们希望显示电影数据以及放映时间和风格,以更好地展示。我们首先通过向event对象添加更多数据来实现这一点。
将以下行添加到拖动开始事件监听器中,以便我们可以捕获电影标题、剧院标题以及时间数据作为单独的数据点:
event.dataTransfer.setData('Title', $(this)[0].title);
event.dataTransfer.setData('Time', $(this).html());
事件监听器应类似于以下代码片段:
$(dragItems[i])[0].addEventListener('dragstart', function(event){
$('#dropzone').show();
event.dataTransfer.setData('Text', this.outerHTML);
event.dataTransfer.setData('Title', $(this)[0].title);
event.dataTransfer.setData('Time', $(this).html());
return false;
});
在拖放区域的drop事件监听器中,修改以下行:
$('#dropstage').append(event.dataTransfer.getData('Text')).show();
我们需要构建一些 HTML 以插入到 DOM 对象中。将Title和Time数据点取出来并相应地显示:
var html='<div class="selected-time">';
html+='<div class="title">'+event.dataTransfer.getData('Title')+'</div>';
html+='<div class="time">'+event.dataTransfer.getData('Time')+'</div>';
html+='</div>';
$('#dropstage').append(html).show();
整个drop事件监听器应类似于以下代码片段:
$('#dropzone')[0].addEventListener('drop', function(event) {
if (event.stopPropagation) event.stopPropagation();
if (event.preventDefault) event.preventDefault();
var html='<div class="selected-time">';
html+='<div class="title">'+event.dataTransfer.getData('Title')+'</div>';
html+='<div class="time">'+event.dataTransfer.getData('Time')+'</div>';
html+='</div>';
$('#dropstage').append(html).show();
return false;
});
最后,我们需要为拖放阶段设置样式,以便当放映时间被拖放到它上面时,它们看起来是可展示的。将以下内容添加到styles.css中,以适当地显示选定时间的数据和拖放阶段:
.selected-time {
float:left;
margin-bottom:10px;
}
.selected-time .title {
font-size:.8em;
padding:0 10px;
}
.selected-time .time {
font-size:.9em;
clear:both;
float:right;
padding-right:10px;
}
#dropstage {
display:none;
float:left;
width:100%;
padding-bottom: 5px;
margin-bottom: 5px;
}
#dropstage h2 {
border:0;
}
注意到#dropstage上的display被设置为none。drop事件监听器实际上显示了这一部分,以便它仅在存在拖放项时显示。我们还需要调整aside h2的样式,以便Top 5 Box Office文本不会换行。为此,只需添加以下代码:
clear:both;
现在拖放阶段已经设置了样式,当我们将其拖放到拖放区域时,应该能够适当地显示我们的放映时间。试一试。

摘要
在本章中,我们介绍了如何通过标记它们来设置可拖动项。我们介绍了拖放和与之相关的事件的机制。此外,我们还介绍了如何将这些内容组合起来,为您的 HTML5 企业应用程序实现一些有趣的功能。
在下一章中,我们将讨论 HTML5 表单。我们将使用它们来提交关于特定放映时间的推文到 Twitter。
第九章:应用:通过 Twitter 传播信息
本章涵盖了我们的第二个第三方 API 使用示例——以 Twitter API 为例,并介绍 HTML5 表单验证功能。Twitter 是一个允许用户发布和查看最多 140 个字符的消息的社会网络。这个社交网络提供了一个公共 API,允许开发者执行各种操作。作为练习,我们将向 MovieNow 添加 Twitter OAuth 身份验证和消息发布功能,并介绍 HTML5 表单验证。
对于本章,需要一些后端技术的基本技能。您可以在以下位置查看 PHP 的基础知识:php.net/manual/en/tutorial.php。
虽然我们使用 PHP,但可以选择其他解决方案及其相应的 Twitter 库(dev.twitter.com/docs/twitter-libraries)。有 Java、.NET、Ruby 等库。
通过本章,我们将涵盖以下内容:
-
注册我们的应用程序
-
如何在 MovieNow 上发推文
-
身份验证
-
发布推文
-
新输入字段类型
注册我们的应用程序
要使用 Twitter API,我们需要一个 Twitter 账户来注册我们的应用程序。如果您没有 Twitter 账户,您可以在www.twitter.com免费注册一个。
注册并登录后,您可以访问 Twitter 开发者页面dev.twitter.com/。

点击创建应用链接,并输入您的应用的名称、描述和网站值。回调 URL是用户授权使用其账户后应用将被重定向到的地址。在这种情况下,您可以重定向到您的首页。

在此之后,您只需接受条款和条件,输入验证码,然后点击创建您的 Twitter 应用程序。
现在您已经创建了 Twitter 应用程序,您可以在以下页面查看其详细信息。最重要的参数是消费密钥和消费密钥,它们用于验证您的应用。
小贴士
您不应该向客户端暴露消费密钥和消费密钥。所有敏感数据都应该加密。

默认情况下,Twitter 的访问级别为只读。我们可以转到设置选项卡,在应用类型部分中将读取和写入设置为读和写,并在应用图标部分上传我们的应用图标。

我们已准备好开始使用 Twitter API。
如何在 MovieNow 上发推文?
要使用 MovieNow 发推文,我们需要两个功能:Twitter OAuth 身份验证和状态更新(推文)。
我们的示例工作流程很简单:用户使用 MovieNow 右上角的登录按钮进行身份验证,然后我们显示用户名和头像,当用户拖动电影以选择它(或在 iPhone 和其他禁用拖动的设备上点击)时,我们显示一个带有电影详情的推文表单,可以通过点击推文按钮来发布。
为了简化我们的示例,我们将使用一个包装 Twitter API 并提供异步调用的 Twitter-async PHP 库:github.com/jmathai/twitter-async。
小贴士
Jaisen Mathai 的 Twitter-async 文档可以在www.jaisenmathai.com/articles/twitter-async-documentation.html找到。
身份验证
下载 Twitter-async 库并将其放入我们在应用程序目录根目录中创建的lib文件夹中。我们应该有:
-
EpiCurl.php– 使用 PHPmulti_curl函数抽象并行处理 -
EpiOauth.php– 包含 OAuth 身份验证的基本方法 -
EpiTwitter.php– 扩展EpiOauth并抽象 Twitter API 身份验证和请求
此外,我们可以创建一个名为secret.php的 PHP 文件来存储我们的消费密钥和消费密钥(您可以在 Twitter 应用程序账户页面上找到它们)。
<?php
$consumer_key = "<PLACE YOUR CONSUMER KEY HERE>";
$consumer_secret = "<PLACE YOUR CONSUMER SECRET KEY HERE>";
?>
我们需要将index.html页面扩展名更改为index.php以添加 PHP 代码。之后,我们调用 PHP 的session_start方法来启动一个新的会话(如果存在一个,则恢复它)。然后,我们导入我们的库。最后,我们可以使用我们的消费者密钥($consumer_key)和消费者密钥($consume_secret)实例化EpiTwitter类。
<?php
session_start();
include 'lib/EpiCurl.php';
include 'lib/EpiOAuth.php';
include 'lib/EpiTwitter.php';
include 'lib/secret.php';
$twitterObj = new EpiTwitter($consumer_key, $consumer_secret);
?>
$twitterObj包含启动新的 Twitter OAuth 会话或使用现有会话获取用户信息所需的信息。为了显示 Twitter 登录或登录信息,我们将在header中有两种可能的案例:
如果用户未登录和/或应用程序未授权:
<header>
<div class="logo">
</div>
<div class="twitter-info">
<a href='url'><div class='twitter-signin'></div></a>
</div>
</header>
如果用户已登录:
<header>
<div class="logo">
</div>
<div class="twitter-info">
<a href='http://www.twitter.com/username' target='_blank' class='twitter-data'><img src='avatar' width='30' height='30'/><span>username</span></a>
</div>
</header>
小贴士
注意,我们在header中添加了类徽标以区分包含 MovieNow 徽标的divs。
用户未登录和/或应用程序未授权
当我们的用户未登录时,我们想在右上角显示 Twitter 登录页面和/或 MovieNow 的认证页面链接。

要做到这一点,我们从$twitterObj获取授权 URL 并创建一个带有该 URL 的链接:
$url = $twitterObj->getAuthorizationUrl();
echo "<a href='$url'><div class='twitter-signin'></div></a>";
如果用户访问此链接并且他们已经登录到 Twitter,他们将直接转到 MovieNow 的授权屏幕:

如果用户未登录到 Twitter,他们首先会看到登录屏幕。

登录后,他们将看到 Twitter MovieNow 授权屏幕。当登录/授权过程结束时,浏览器将重定向到您的 回调 URL。
小贴士
请记住,您可以通过访问 dev.twitter.com/ 并在 设置 选项卡中更改它来随时更改您的 回调 URL。
回调 URL 将接收 oauth_verifier 和 oauth_token 作为参数。我们需要使用 oauth_token 来设置用户会话信息。在会话中,我们将存储 $_SESSION['oauth_token'] 和 $_SESSION['oauth_token_secret']。如果它们已设置,则表示用户已经登录。如果没有,我们需要使用 $_GET['oauth_token'] 来设置我们的会话信息。
首先,我们验证:
if(isset($_GET['oauth_token']) || (isset($_SESSION['oauth_token']) && isset($_SESSION['oauth_token_secret']))){
if(!isset($_SESSION['oauth_token']) || !isset($_SESSION['oauth_token_secret'])){
//SET SESSION HERE
}
}
然后,为了设置我们的会话,我们使用 $twitterObj 传递 $_GET['oauth_token']:
$twitterObj->setToken($_GET['oauth_token']);
然后,我们访问令牌信息以设置我们的会话:
$token = $twitterObj->getAccessToken();
$_SESSION['oauth_token']=$token->oauth_token;
$_SESSION['oauth_token_secret']=$token->oauth_token_secret;
最后,我们使用从 getAccessToken 方法获得的 oauth_token 和 oauth_token_secret 来设置我们的令牌:
$twitterObj->setToken($token->oauth_token, $token->oauth_token_secret);
用户已登录
如果用户已登录,他们希望看到一些信息来告知他们已登录。因此,我们将在页眉的右上角显示他们的 Twitter 头像和用户名。

小贴士
在任何企业应用程序中,在相同的空间中显示登录和用户已登录信息是一种良好的做法。
现在,我们只需要获取用户信息,因此我们可以使用 get_accountVerify_credentials 来获取用户名和头像位置:
$twitterInfo= $twitterObj->get_accountVerify_credentials();
$username = $twitterInfo->screen_name;
$avatar = $twitterInfo->profile_image_url;
建立指向 Twitter 用户账户的链接并显示头像:
echo "<a href='http://www.twitter.com/$username' target='_blank' class='twitter-data'><img src='$avatar' width='30' height='30'/><span> $username</span></a>";
将所有这些整合在一起,我们有:
<header><div class="logo">
</div>
<div class="twitter-info">
<?php
if(isset($_GET['oauth_token']) || (isset($_SESSION['oauth_token']) && isset($_SESSION['oauth_token_secret']))){if(!isset($_SESSION['oauth_token']) || !isset($_SESSION['oauth_token_secret'])){$twitterObj->setToken($_GET['oauth_token']);
$token = $twitterObj->getAccessToken();
$_SESSION['oauth_token']=$token->oauth_token;
$_SESSION['oauth_token_secret']=$token->oauth_token_secret;
$twitterObj->setToken($token->oauth_token, $token->oauth_token_secret);}else{
$twitterObj->setToken($_SESSION['oauth_token'],$_SESSION['oauth_token_secret']);}
$twitterInfo= $twitterObj->get_accountVerify_credentials();
$username = $twitterInfo->screen_name;
$avatar = $twitterInfo->profile_image_url;
echo "<a href='http://www.twitter.com/$username' target='_blank' class='twitter-data'><img src='$avatar' width='30' height='30'/><span> $username</span></a>";}else{
$url = $twitterObj->getAuthorizationUrl();
echo "<a href='$url'><div class='twitter-signin'></div></a>";
}
?>
</div>
</header>
添加一些样式
现在我们已经完成了登录交互,我们可以在 styles.css 中添加样式。
首先,我们更改 header 的 div 样式为 logo,以便在 header 内部有一个具有不同样式的 div 标签。请记住,在 Retina 显示情况下也要更改此设置(@media only screen and (-webkit-min-device-pixel-ratio:2),only screen and (min-device-pixel-ratio: 2))。
twitter-info 类是我们用于存储 Twitter 信息的容器,无论用户是否已登录。我们设置 position 为 absolute 以在屏幕较小的设备上隐藏用户名并仅显示用户头像。
header .twitter-info{
position:absolute;
top:20px;
right:0;
}
我们可以删除链接的轮廓:
header .twitter-info a{
outline:none;
}
小贴士
您可以使用 a{outline:none;} 删除链接的所有轮廓。
添加登录图像和尺寸:
header .twitter-signin{
width:100px;
height:36px;
background:url(../img/twitter-signin.png);
}
定位用户头像:
header a.twitter-data img{
position:absolute;
left:-38px;
}
设置用户名的文本样式信息:
header a.twitter-data{
line-height:30px;
color:#fff;
text-decoration:none;
font-size:.8em;
padding-right:10px;
}
在 @media only screen and (max-width: 737px) 媒体查询内部,我们需要隐藏用户名并显示 Twitter 头像,如果用户未登录。
隐藏 twitter-info 块的一部分:
header .twitter-info{
right:-67px;
}
移动头像:
header a.twitter-data img{
left:-88px;
}
隐藏用户名:
header a.twitter-data span{
display:none;
}
现在,我们的 Twitter 登录过程在屏幕较小的设备上应该看起来像以下截图:

在您点击顶部的 Twitter 按钮后,您将被重定向到 Twitter 认证页面(如果您未登录到 Twitter,它将首先重定向您到登录页面)。

如果你授权应用程序,则下一个屏幕将显示你的 Twitter 头像在页面的顶部部分。
发布推文
我们的用户现在已认证。下一步是允许发布关于电影的推文。在这种情况下,让我们使用 AJAX 调用一个接收要推文的消息作为 tweet 参数的服务。
服务
让我们创建一个名为 tweet.php 的文件,并将其存储在我们的应用程序的根目录中。导入 Twitter-async 库和 secret.php 配置文件:
include 'lib/EpiCurl.php';
include 'lib/EpiOAuth.php';
include 'lib/EpiTwitter.php';
include 'lib/secret.php';
我们将返回一个 JSON 对象,指示推文是否成功发布。让我们定义一个变量 $result 并将其默认状态设置为 ok。
$result = array('status' => 'ok');
我们可以使用我们的应用程序信息和验证会话来实例化 EpiTwitter。如果不存在会话,将状态设置为 error:
$twitterObj = new EpiTwitter($consumer_key, $consumer_secret);
if(isset($_SESSION['oauth_token']) && isset($_SESSION['oauth_token_secret'])){
//TWEET
}else{
$result["status"]="error";
}
如果会话存在,设置 $twitterObj 令牌并验证凭证:
$twitterObj->setToken($_SESSION['oauth_token'],$_SESSION['oauth_token_secret']);
$twitterInfo= $twitterObj->get_accountVerify_credentials();
如果我们有 tweet 参数,我们将转义我们的消息并调用 post_statusesUpdate API,将推文放在 status,一个索引数组中。然后我们可以将响应保存到 $temp 变量中。
$msg = stripcslashes($_REQUEST['tweet']);
$update_status = $twitterObj->post_statusesUpdate(array('status' => $msg));
$temp = $update_status->response;
最后,我们验证 $temp 是否包含错误,并将 $result 作为 JSON 返回:
if($temp["error"])$result["status"]="error";
echo json_encode($result);
将所有这些整合在一起,我们应该有:
<?php
include 'lib/EpiCurl.php';
include 'lib/EpiOAuth.php';
include 'lib/EpiTwitter.php';
include 'lib/secret.php';
$result = array('status' => 'ok');
$twitterObj = new EpiTwitter($consumer_key, $consumer_secret);
if(isset($_SESSION['oauth_token']) && isset($_SESSION['oauth_token_secret'])){
$twitterObj->setToken($_SESSION['oauth_token'],$_SESSION['oauth_token_secret']);
$twitterInfo= $twitterObj->get_accountVerify_credentials();
if($_REQUEST['tweet']){
$msg = stripcslashes($_REQUEST['tweet']);
$update_status = $twitterObj->post_statusesUpdate(array('status' => $msg));
$temp = $update_status->response;
}
}else{
$result["status"]="error";
}
if($temp["error"])$result["status"]="error";
echo json_encode($result);
?>
应用 HTML
我们将显示我们的推文表单作为一个带有幕布的模态窗口,以遮挡应用的其他部分。我们将首先在 index.php 的末尾添加推文表单的代码。接下来,我们将设置幕布以覆盖我们的页面(我们将将其样式设置为黑色并带有透明度):
<div class="modal-background-color"></div>
对于我们的模态区域,我们将创建一个部分:
<section class="modal-background"></section>
提示
对于 modal-background-color,我们不使用 section,因为它没有任何语义信息。
我们将定义推文窗口为一个 div:
<div class="tweet"></div>
然后,我们可以添加一个带有标题和关闭按钮的栏:
<div class="tweet">
<div class="tweet-bar">
<h2>Tweet</h2>
<div id="close-tweet"></div>
</div>
</div>
最后,我们有我们的推文表单本身。注意,我们将 textarea 的 maxlength 属性设置为 140,因为推文不能超过 140 个字符。
提示
textarea 元素中的 maxlength 属性在 Internet Explorer 9 或更早版本中不受支持。这是因为它不是 HTML 4.01 规范中 textarea 的标准,但它后来被添加到 HTML5 中。
<form id="twitter">
<textarea name="tweet" rows="5" id="tweet" title="Tweet Required!" maxlength="140" required></textarea>
<div class="char-counter"><input type='submit' value='tweet' name='submit' id='tweet-submit' />
<div id="count">140</div>
</div>
</form>
将所有这些整合在一起,我们有:
<div class="modal-background-color"></div>
<section class="modal-background">
<div class="tweet">
<div class="tweet-bar">
<h2>Tweet</h2>
<div id="close-tweet"></div>
</div>
<form id="twitter">
<textarea name="tweet" rows="5" id="tweet" title="Tweet Required!" maxlength="140" required></textarea>
<div class="char-counter"><input type='submit' value='tweet' name='submit' id='tweet-submit' />
<div id="count">140</div>
</div>
</form>
</div>
</section>
注意,我们使用 required 属性来表示此字段不能为空。required 属性是一个用于表单验证的 HTML5 属性。我们还使用 maxlength 来限制允许的字符数。
<textarea name="tweet" rows="5" id="tweet" title="Tweet Required!" maxlength="140" required></textarea>
添加更多样式
我们已经有了我们的推文窗口的 HTML 结构,但我们还没有添加样式。我们的最终设计应该看起来像以下截图:

为了实现这一点,我们将在 styles.css 中添加一些样式。
-
我们将
modal-background-color设置为使用fixed定位和z-index: 5000来覆盖我们的页面。这个背景应该是黑色,透明度为 80%。.modal-background-color{ bottom: 0; left: 0; right: 0; top: 0; background-color:#000; opacity: 0.8; display:none; -moz-opacity: 0.8; -khtml-opacity: 0.8; filter: alpha(opacity=80); -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)"; position: fixed; z-index: 5000; }提示
虽然正在使用
-ms-filter来支持较老版本的 Internet Explorer,但这是一种微软的专有解决方案,并不遵循标准。 -
我们希望
modal-background位于modal-background-color之上,因此我们使用z-index: 5100。默认情况下,这两个区域都应通过display:none隐藏,如下面的代码片段所示:.modal-background{ bottom: 0; left: 0; right: 0; top: 0; display:none; position: fixed; z-index: 5100; } -
我们的
tweet区域为白色,带有圆角,并使用margin:110px auto进行居中:.tweet{ background-color:#fff; border-radius:4px 4px; -moz-border-radius:4px 4px; -webkit-border-radius:4px 4px; -o-border-radius:4px 4px; width:470px; margin:110px auto; } -
然后将 tweet 标题向左浮动并添加一些填充:
.tweet h2{ float:left; padding:8px 15px 6px; } -
对于我们的文本区域
#tweet,我们移除轮廓,添加一些填充和边距,并设置圆角边框样式和字体样式:#tweet{ outline:none; padding:5px 5px; resize:none; width:430px; margin:15px 15px 8px; border-radius:4px 4px 0 0; -moz-border-radius:4px 4px 0 0; -webkit-border-radius:4px 4px 0 0; -o-border-radius:4px 4px 0 0; border:1px solid #d3d3d3; font-size:1em; line-height:1.4em; } -
默认情况下,我们的提交按钮
#tweet-submit是灰色:#tweet-submit{ padding:6px 12px; background-color:#f6f6f6; font-size:.8em; border-radius:4px 4px; -moz-border-radius:4px 4px; -webkit-border-radius:4px 4px; -o-border-radius:4px 4px; border:1px solid #d3d3d3; cursor:pointer; color:#888; font-weight:bold; } -
添加一个
active类来设置活动时的样式,用蓝色渐变、蓝色边框和白色字体来着色我们的按钮:#tweet-submit.active{ background:linear-gradient(top, #33BCEF, #019AD2); background:-o-linear-gradient(top, #33BCEF, #019AD2); background:-moz-linear-gradient(top, #33BCEF, #019AD2); background:-webkit-linear-gradient(top, #33BCEF, #019AD2); background:-ms-linear-gradient(top, #33BCEF, #019AD2); border-color:#057ed0; color:#fff; } -
在鼠标悬停时,使其稍微暗一些:
#tweet-submit.active:hover{ background:linear-gradient(top, #2DADDC, #0271BF); background:-o-linear-gradient(top, #2DADDC, #0271BF); background:-moz-linear-gradient(top, #2DADDC, #0271BF); background:-webkit-linear-gradient(top, #2DADDC, #0271BF); background:-ms-linear-gradient(top, #2DADDC, #0271BF); } -
将字符计数器和提交按钮向右浮动并移除轮廓:
#tweet-submit,#count{ float:right; outline:none; } -
设置字符计数器的字体样式:
#count{ line-height:30px; padding-right:14px; color:#999; font-size:.9em; } -
将
close.png精灵图像添加到#close-tweet按钮,并设置其样式:#close-tweet{ cursor:pointer; border-left:1px solid #ccc; width:30px; height:30px; float:right; background-image:url(../img/close.png); } -
在鼠标悬停时移动图片:
#close-tweet:hover{ background-color:#E5E5E5; background-position:0 -30px; } -
为包含标题和关闭按钮的栏添加不同的样式。注意我们收圆了左上角和右下角:
.tweet-bar{ border-radius:4px 4px 0 0; -moz-border-radius:4px 4px 0 0; -webkit-border-radius:4px 4px 0 0; -o-border-radius:4px 4px 0 0; background-color:#ececec; color:#666; font-size:.9em; border-bottom:1px solid #ccc; overflow:hidden; } -
最后,为包含 tweet 提交按钮和字符计数器的区域设置
height:.char-counter{ height:40px; padding:0 14px; } -
对于移动设备,使用
@media only screen and (max-width: 737px)媒体查询为文本区域和 tweet 窗口添加不同的width:.tweet{ width:300px; } #tweet{ width:258px; }
在屏幕较小的设备上,我们的 tweet 窗口应类似于以下截图:

添加 JavaScript 交互
现在我们需要添加相应的 JavaScript 来连接这些点。让我们在js文件夹中创建一个新的文件movienow.tweet.js。在index.php中,我们在movienow.draganddrop.js之前导入该文件:
<script src="img/ios-orientationchange-fix.js"></script>
<script src="img/jquery-1.8.0.min.js"></script>
<script src="img/jquery.xdomainajax.js"></script>
<script src="img/three.js"></script>
<script src="img/movienow.tweet.js"></script><script src="img/movienow.draganddrop.js"></script>
<script src="img/movienow.charts.js"></script><script src="img/movienow.geolocation.js"></script>
<script src="img/movienow.js"></script>
提示
如您所注意到的,我们使用了一堆 JavaScript 库,实现最小化技术以减少我们的 JavaScript 和 CSS 文件的大小和请求数量是一个好习惯。
定义主要代码结构并添加一个变量twitterReady来验证 tweet 窗口是否已初始化:
var movienow = movienow || {};
movienow.tweet = (function(){
var that = this;
var twitterReady = false;
})();
隐藏和显示功能将由showTweetArea和hideTweetArea函数处理。对于showTweetArea,我们验证twitterReady是否为true;如果不是,我们调用initTweet函数来设置 Twitter 窗口事件并将true赋给twitterReady。
我们可以将message(带有默认文本)分配给文本区域并显示 tweet 窗口。如果用户未登录,我们显示一个警告。
this.showTweetArea = function(title, time) {if(!twitterReady){
that.initTweet();twitterReady=true;
}
if($(".twitter-data").length>0){
var message = "I'm going to "+title+" "+time;
$("#tweet").val(message);
that.updateCount();
$(".modal-background").css("display", "block");
$(".modal-background-color").css("display", "block");
$("html,body").css("overflow","auto");}
else{
alert("Please login in twitter to tweet this");}
};
提示
在我们的企业应用中显示通知和错误的一个更优雅的方法是使用 HTML 定义自定义模态窗口,并用 CSS 进行样式化。此外,使用模板引擎(如位于mustache.github.com的 Mustache)来实现此解决方案也是可能的。
注意我们设置了$("html,body").css("overflow","auto")来隐藏浏览器滚动条。
hideTweetArea方法隐藏推文窗口并恢复浏览器滚动条:
this.hideTweetArea = function() {
$(".modal-background").css("display", "none");
$(".modal-background-color").css("display", "none");
$("html,body").css("overflow","hidden");
}
要更新字符计数器,我们添加一个updateCount方法。我们提取#tweet文本区域消息并验证其长度是否为 140 个字符。只有当可以推文时,我们才将active类添加到我们的提交按钮上。
this.updateCount = function(){
var info=$("#tweet").val();
var count= 140 - info.length;
$('#count').html(count);
if(count===140||count<0){
$('#tweet-submit').removeClass("active");
}else{
$('#tweet-submit').addClass("active");
}
};
实现此功能的本地替代方法是使用输入字段中的disabled属性,而不是添加和删除active类。
我们的初始化方法initTweet添加了必要的事件:
this.initTweet = function() {
$("#tweet").keyup(that.updateCount);
$("#twitter").submit(that.tweet);
$("#close-tweet").click(that.hideTweetArea);
};
最后,我们创建了一个方法来调用我们的 PHP 服务:
this.tweet = function(event) {
if($("#twitter")[0].checkValidity()){
$.ajax({
url: 'tweet.php',
data: $("#twitter").serialize(),
success: function(info){
var data = that.objectifyJSON(info);
if(data.status!="ok"){
alert("Ops! There was an error sending your tweet, please try again.");
}else{
that.hideTweetArea();
}
}
});
}
return false;
};
checkValidity方法是一个 HTML5 JavaScript 函数,它允许我们验证字段是否有效。在我们的例子中,由于文本区域是必填的,验证就是它不能为空。
由于我们正在使用 AJAX,我们在最后添加return false以避免提交时页面刷新。
小贴士
通常,我们需要从表单中提取所有输入并构造一个数据参数用于我们的 AJAX 调用。jQuery 中有一个有用的函数叫做serialize,它可以为我们构造这个参数。
把所有这些都放在一起,我们应该有:
var movienow = movienow || {};
movienow.tweet = (function(){
var that = this;
var twitterReady = false;
this.initTweet = function() {
$("#tweet").keyup(that.updateCount);
$("#twitter").submit(that.tweet);
$("#close-tweet").click(that.hideTweetArea);
};
this.tweet = function(event) {
if($("#twitter")[0].checkValidity()){
$.ajax({
url: 'tweet.php',
data: $("#twitter").serialize(),
success: function(info){
var data = that.objectifyJSON(info);
if(data.status!="ok"){
alert("Ops! There was an error sending your tweet, please try again.");
}else{
that.hideTweetArea();
}
}
});
}
return false;
};
this.updateCount = function(){
var info=$("#tweet").val();
var count= 140 - info.length;
$('#count').html(count);
if(count==140||count<0){
$('#tweet-submit').removeClass("active");
}else{
$('#tweet-submit').addClass("active");
}
};
this.showTweetArea = function(title, time) {
if(!twitterReady){
that.initTweet();
twitterReady=true;
}
if($(".twitter-data").length>0){
var message = "I'm going to "+title+" "+time;
$("#tweet").val(message);
that.updateCount();
$(".modal-background").css("display", "block");
$(".modal-background-color").css("display", "block");
$("html,body").css("overflow","auto");
}else{
alert("Please login in twitter to tweet this");
}
};
this.hideTweetArea = function() {
$(".modal-background").css("display", "none");
$(".modal-background-color").css("display", "none");
$("html,body").css("overflow","hidden");
}
})();
小贴士
checkValidity在所有浏览器中都不是可用的。当它不可用时,建议编写自己的checkValidity函数以支持跨浏览器。你可以在perplexed.co.uk/5201_making_html5_form_backwards_compatible.htm页面上查看这种解决方案的一种方法。
剩下的就是在movienow.dragnaddrop.js中添加一个调用。在我们的拖放事件监听器内部,我们添加:
that.showTweetArea(event.dataTransfer.getData('Title'),event.dataTransfer.getData('Time'));
因此,我们现在有:
$('#dropzone')[0].addEventListener('drop', function(event) {
if (event.stopPropagation) event.stopPropagation();
if (event.preventDefault) event.preventDefault();
var html='<div class="selected-time">';
html+='<div class="title">'+event.dataTransfer.getData('Title')+'</div>';
html+='<div class="time">'+event.dataTransfer.getData('Time')+'</div>';
html+='</div>';
$('#dropstage').append(html).show();
that.showTweetArea(event.dataTransfer.getData('Title'),event.dataTransfer.getData('Time'));
return false;
});
对于移动和非拖放启用设备,我们添加:
var iOS = !!navigator.userAgent.match('iPhone OS') || !!navigator.userAgent.match('iPad');
if(('draggable' in document.createElement('span'))&&!iOS){
//PREVIOUS CODE
}else{
$(".showtime").bind('click', function(event){
var info=$(this)[0].title;
var time=$(this).html();
that.showTweetArea(info,time);
});
}
我们的推文系统已经准备好了。如果我们选择一部电影(根据设备是拖动还是点击),我们可以通过点击推文按钮来推文。

跨浏览器表单验证支持
就像其他 HTML5 特性一样,表单验证在各个浏览器之间并不一致。在我们的例子中,如果我们选择一部电影,删除推文窗口中的文本区域内容,然后尝试提交,每个浏览器的行为都会不同。
在 Firefox 中,我们看到的表单验证是一个红色边框和一条消息。

在 Chrome 中,表单验证使用标题来显示推文必填!消息,但不显示红色边框。

只有 Safari 阻止提交操作。
即使表单验证变得一致,每个浏览器的用户界面元素也完全不同(你会在 Firefox 中注意到一个黑色工具提示,在 Chrome 中则是一个带有图标的白色工具提示)。目前,可以通过从头开始使用 JavaScript 或使用 jQuery 插件(docs.jquery.com/Plugins/Validation)来进行验证。
要禁用 HTML5 表单验证,请将novalidate属性添加到表单中。
小贴士
即使数据在客户端进行了验证,也必须在服务器端进行验证。请记住,对于某些用户来说,更改 JavaScript 代码相当容易。
新的输入字段类型
HTML5 为表单引入了新的输入类型。这些类型允许更好的控制和验证,但遗憾的是,它们并不在所有现代浏览器中得到完全支持。它们如下所示:
-
color用于选择颜色 -
date允许选择日期 -
datetime允许选择日期和时间 -
datetime-local允许选择日期和时间,但不包括时区 -
email用于应包含电子邮件地址的输入字段 -
month允许选择月和年 -
number用于具有数值的输入 -
range以滑块的形式呈现,允许在数字范围内选择值 -
search用于搜索字段 -
tel用于输入电话号码 -
time允许选择时间 -
url用于应包含有效 URL 的输入 -
week允许选择周和年
摘要
随着众多社交网络和服务通过 API 提供数据,我们可以通过补充更多数据和功能来丰富我们的企业管理应用。此外,您可以使用 OAuth 身份验证为用户提供替代身份验证方法。最后,HTML5 表单验证和新的输入类型似乎还不够成熟,不能作为企业管理应用的解决方案;相反,应该实施回退 JavaScript 解决方案,以在浏览器之间提供相同的体验。我们希望浏览器开发者尽快采用 HTML5 规范的这一部分,因为它将缩短开发周期,提高表单数据的可靠性,并为用户提供更好的体验。
在下一章中,我们将介绍 Web Workers 以及它们通过添加运行后台进程和多线程应用的能力,为企业管理应用带来的强大功能。
第十章:应用:通过 Web Workers 消费推文
到目前为止,JavaScript 一直是单线程的。对于运行缓慢或耗时的过程,页面上的所有内容都可能因为等待某个任务完成而突然停止。到目前为止,你可以使用 AJAX 甚至setTimeout来委托任务;然而,这些解决方案都不允许真正的并行执行,处理状态变得相当混乱。
为了弥补这一不足,HTML5 规范引入了 Web Workers。Web Workers 允许你创建非用户导向的后台线程,它们可以同时运行。它们通常用于计算密集型任务。然而,对于我们的 MovieNow 企业应用,我们将使用 Web Workers 来查找剧院附*的推文并显示它们。尽管不一定计算密集,但 Web Workers 在更新页面上的多个元素状态时非常有用,而不会打断整体的用户体验(请注意,通常只有一个 UI 渲染线程)。
在本章中,我们将涵盖:
-
获取数据
-
捕获地理编码
-
网页工作线程的结构
-
使用 Web Workers 获取附*的推文
-
更新事件监听器
-
推文的样式
获取数据
为了开始,让我们创建一个查询 Twitter REST API 并返回指定地理编码附*推文的端点。在应用程序的根目录下创建一个名为nearbytweets.php的 PHP 文件。打开它并粘贴以下代码:
<?php
$latitude = $_GET['latitude'];
$longitude = $_GET['longitude'];
if (strpos($latitude, '.') == false) {
$latitude = substr($latitude, 0, -4) . '.' . substr($latitude, -4);
}
if (strpos($longitude, '.') == false) {
$longitude = substr($longitude, 0, -4) . '.' . substr($longitude, -4);
}
$tweets = file_get_contents('http://search.twitter.com/search.json?include_entities=true&result_type=mixed&geocode=' . $latitude . ',' . $longitude . ',0.25mi');
echo $tweets;
?>
这是一个简单的页面,它接受两个参数:纬度和经度。然后它查询 Twitter REST API 1.1,如dev.twitter.com/docs/api/1.1中定义的。它返回包含来自指定纬度和经度 0.25 英里范围内的推文的 JSON 数据。
捕获地理编码
现在我们有了获取数据的地方,我们需要捕获每个剧院的纬度和经度以发送到我们的新端点。在movienow.geolocation.js中,我们将进行一些小的修改。在displayShowtimes方法中,我们需要调整剧院名称的显示位置。具体来说,我们需要输入纬度和经度并将它们添加到一个新的data属性中。这允许我们稍后使用这些数据。
更改以下行:
movieHTML+='<p class="theater">'+movie.theater.title+" "+movie.theater.address+'</p>';
更改为:
movieHTML+='<p class="theater" data-location= "'+movie.theater.latitude+','+movie.theater.longitude+'">'+movie.theater.title+" "+movie.theater.address+'</p>';
接下来,我们将在js文件夹中创建一个名为movienow.nearbytweets.js的新 JavaScript 文件。在index.php中,我们将添加对新 JavaScript 文件的引用:
<script src="img/ios-orientationchange-fix.js"></script>
<script src="img/jquery-1.8.0.min.js"></script>
<script src="img/jquery.xdomainajax.js"></script>
<script src="img/three.js"></script>
<script src="img/movienow.nearbytweets.js"></script>
<script src="img/movienow.tweet.js"></script>
<script src="img/movienow.draganddrop.js"></script>
<script src="img/movienow.charts.js"></script>
<script src="img/movienow.geolocation.js"></script>
<script src="img/movienow.js"></script>
在movienow.nearbytweets.js中,我们将从一些模板代码开始。向movienow中设置nearbytweets命名空间添加以下代码:
var movienow = movienow || {};
movienow.nearbytweets = (function(){
var that = this;
})();
网页工作线程的结构
要真正理解 Web Workers,可以想象一个在家工作的业务,其中家庭被发送包含促销材料和信封的包裹。每个家庭都需要将促销材料装入信封,密封信封,并将包裹作为包裹送回原始业务。在家工作的家庭对业务的内部情况一无所知。他们只知道他们被给予了一个包裹,他们必须对包裹进行处理,并且他们必须将包裹送回。
Web Workers 在一个隔离的线程中运行,它们对调用它们的页面状态一无所知。它们只是接收一条消息,对这条消息进行处理,然后返回一条消息。调用程序指定了一个事件监听器,当 Web Worker 返回消息时,该监听器会做出响应。
Web Workers 有两种类型。它们是:
-
专用 Web Workers:有时它们也被称为 Web Workers。它们只能通过创建它们的脚本访问,尽管可以使用消息端口与其他上下文进行通信。
-
共享 Web Workers:它们有名称并且共享全局作用域,因此任何在同一源运行的脚本都可以获取此类工作者的引用。
在这种情况下,我们将使用专用 Web Workers。Web Workers 通常在单独的 JavaScript 文件中定义。要创建一个 Web Worker,你只需实例化它:
var worker = new Worker('mywebworker.js');
一旦创建,你可以通过使用 postMessage 方法与 Web Worker 进行通信:
worker.postMessage('Hi worker!');
要从 Web Worker 接收通信,只需定义一个基于 onmessage 事件触发的事件监听器:
worker.addEventListener('message', function(e) {
console.log(e.data);
}, false);
Web Worker 可以定义为以下内容:
self.addEventListener('message', function(e) {
self.postMessage(e.data);
}, false);
它基本上定义了一个用于接收消息的事件监听器。随着消息的到来,它执行附加到消息事件的函数,并通过使用 postMessage 方法返回它,这与它的调用方式非常相似。客户端的事件监听器被调用,然后大家继续各自的道路。
如果 Web Worker 中发生错误,可以通过监听 Web Worker 上的错误事件来处理异常:
worker.addEventListener('error', function(e) {
console.log(e.message);
}, false);
需要注意的是,Web Worker 处于一个沙盒中。它根本不访问页面的状态。相反,它接收到的任何内容都将始终是发送内容的副本。事件引用的 JavaScript 库不可用。实际上,DOM、window、document 和 parent 对象都不可用,因此在 Web Worker 中无法进行任何 DOM 操作或读取,也无法使用 window 对象。你是一个完全独立的家庭。
然而,你可以使用 navigator 对象,利用 XMLHttpRequest 对象,并启动其他 Web Workers。
小贴士
嵌套工作者必须托管在与父文档相同的源中。此外,嵌套工作者的 URI 是相对于父工作者的位置解析的,而不是拥有文档的位置。
你还可以使用 importScripts 方法导入脚本,以及使用 setTimeout 和 setInterval。
使用 Web Workers 获取附*的推文
在 movienow.nearbytweets.js 中,我们将定义几个方法。首先,让我们定义一个获取推文的入口点方法:
this.getTweets = function() {};
添加此方法后,我们可以在 movienow.geolocation.js 的 displayShowtimes 方法的最后调用它:
$("#movies-near-me li .charting-button").click(that.showCharts);
init();
getTweets();
};
到目前为止,一切顺利,但我们还没有做任何事情。让我们在 movienow.nearbytweets.js 中添加一个新的方法,名为 getTweetsByTheater:
this.getTweetsByTheater = function(theater) {};
新的 getTweetsByTheater 方法将接受一个 "theater" 元素并获取其推文。在这种情况下,我们所说的 "theater" 元素是指 movienow.geolocation.js 中定义的 theater 类的 div 标签。然后我们将通过简单的 jQuery 调用从 getTweets 方法中调用它。如下增强 getTweets:
this.getTweets = function() {
$('.theater').each(function() {
that.getTweetsByTheater(this);
});
};
现在让我们进入脚本的核心部分。我们将实例化我们的 Web Worker。让我们首先添加 Web Worker 机制的框架。将以下内容添加到 getTweetsByTheater 方法中:
var worker = new Worker('js/movienow.worker.js');
worker.addEventListener('message', function(e) {
}, false);
worker.postMessage();
为了完成框架,我们将在 js 文件夹中添加一个新的 JavaScript 文件,名为 movienow.worker.js。将以下代码片段添加到其中:
self.addEventListener('message', function(e) {
}, false);
现在我们已经设置了 Web Worker 的初始框架,让我们从传递给 getTweetsByTheater 的剧院对象中提取地理编码,并将其传递给 Web Worker。我们将使用之前在 movienow.geolocation.js 中添加的 data-location 属性并解析出纬度和经度。用以下代码替换 worker.postMessage() 调用的框架:
var geocode = $(theater).attr('data-location');
var latitude = geocode.split(',')[0];
var longitude = geocode.split(',')[1];
worker.postMessage({
'latitude': latitude,
'longitude': longitude
});
现在我们将经纬度传递给 Web Worker,让我们更新它以调用本章开头实现的该服务。将以下内容添加到 movienow.worker.js 的事件监听器主体中:
var url = '../nearbytweets.php';
var data = 'latitude='+e.data.latitude+'&longitude='+e.data.longitude;
var xhr = new XMLHttpRequest();
xhr.open('GET', url + '?' + data, true);
xhr.send(null);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status ==0) {
self.postMessage(xhr.responseText);
} else {
throw xhr.status + xhr.responseText;
}
}
}
注意,我们正在使用 XMLHttpRequest。通常,我们会使用 jQuery 的 ajax 方法来执行此类调用。然而,由于 jQuery 依赖于 DOM,并且——如您所回忆的那样——在这个上下文中 DOM 不可用,所以我们不能使用它。相反,我们必须直接调用该对象并发出请求。一旦发出 AJAX 请求并触发 onreadystatechange 事件,负载将通过 self.postMessage() 返回给客户端。
完整的 movienow.worker.js 代码应类似于以下代码片段:
self.addEventListener('message', function(e) {
var url = '../nearbytweets.php';
var data = 'latitude='+e.data.latitude+'&longitude='+e.data.longitude;
var xhr = new XMLHttpRequest();
xhr.open('GET', url + '?' + data, true);
xhr.send(null);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200 || xhr.status ==0) {
self.postMessage(xhr.responseText);
} else {
throw xhr.status + xhr.responseText;
}
}
}
}, false);
小贴士
可以在不创建单独的 worker 文件的情况下实现 Web Workers。您可以在以下位置查看有关实现此技术的教程:www.html5rocks.com/en/tutorials/workers/basics/#toc-inlineworkers
更新事件监听器
返回到 movienow.nearbytweets.js,我们可以更新事件监听器以接收 Web Worker 返回的 JSON 对象。如果您查看我们将添加到事件监听器主体的以下代码,您将看到我们获取返回的数据并遍历结果集以捕获每条推文的文本:
var data = objectifyJSON(e.data);
var nearbyTweets = '';
for (var i=0; i<data.results.length; i++) {
nearbyTweets += '<li>'+data.results[i].text+'</li>';
}
var tweetCounter = (data.results.length==1) ? data.results.length+" tweet" : data.results.length+" tweets";
$(theater)
.append(' <span class="tweet-count">('+ tweetCounter +')</span>')
.parents('li').append('<section class="nearby-tweets"><h3>Nearby Tweets</h3><ul>'+nearbyTweets+'</ul></section>')
.find('.tweet-count').click(that.showNearbyTweets)
.parents('li').find('.nearby-tweets').click(that.hideNearbyTweets);
注意
记住我们在之前的章节中在 movienow.geolocation.js 内定义了 objectifyJSON 函数。如果传入的参数是对象,它返回相同的输入;如果它是一个字符串,它解析内容并返回一个对象。
在这里我们做了两件事。首先,我们在剧院名称旁边添加了一个推文计数(注意我们验证推文数量以添加单数 tweet 或复数 tweets 标签)。其次,我们添加了一个包含推文无序列表的 section 元素。目标是点击推文计数并显示推文。此外,我们还需要向 nearbytweets 对象添加两个更多的方法来显示和隐藏推文:
this.showNearbyTweets = function(event) {
$(event.target).parents('li').addClass('nearby-tweets').addClass('open');
};
this.hideNearbyTweets = function(event) {
$(this).parents('li').removeClass('open');
};
完整的 movienow.nearbytweets.js 代码应如下所示:
movienow.nearbytweets = (function(){
var that = this;
this.getTweets = function() {
$('.theater').each(function() {
that.getTweetsByTheater(this);
});
};
this.showNearbyTweets = function(event) {
$(event.target).parents('li').addClass('nearby-tweets').addClass('open');
};
this.hideNearbyTweets = function(event) {
$(this).parents('li').removeClass('open');
};
this.getTweetsByTheater = function(theater) {
var worker = new Worker('js/movienow.worker.js');
worker.addEventListener('message', function(e) {
var data = objectifyJSON(e.data);
var nearbyTweets = '';
for (var i=0; i<data.results.length; i++) {
nearbyTweets += '<li>'+data.results[i].text+'</li>';
}
var tweetCounter = (data.results.length==1) ? data.results.length+" tweet" : data.results.length+" tweets";
$(theater)
.append(' <span class="tweet-count">('+ tweetCounter +')</span>')
.parents('li').append('<section class="nearby-tweets"><h3>Nearby Tweets</h3><ul>'+nearbyTweets+'</ul></section>')
.find('.tweet-count').click(that.showNearbyTweets)
.parents('li').find('.nearby-tweets').click(that.hideNearbyTweets);
}, false);
var geocode = $(theater).attr('data-location');
var latitude = geocode.split(',')[0];
var longitude = geocode.split(',')[1];
worker.postMessage({
'latitude': latitude,
'longitude': longitude
});
};
})();
在我们继续之前,我们需要对 movienow.geolocation.js 进行一次修改。因为我们正在模仿信息和评分按钮的行为,我们需要确保我们与附*的推文部分一起隐藏和显示这些内容。
通过更改以下行来更改 showCharts 方法:
that.charts($(event.target).parent().parent().removeClass("desc").addClass("open").find("canvas")[0], "3DChart");
我们将其改为:
that.charts($(event.target).parent().parent().removeClass("desc").addClass("open").removeClass('nearby-tweets').find("canvas")[0], "3DChart");
通过更改以下行来更改 showDetails 方法:
$(event.target).parent().parent().addClass("desc").addClass("open");
改为:
$(event.target).parent().parent().addClass("desc").addClass("open").removeClass('nearby-tweets');
为推文添加样式
现在已经为检索和加载附*的推文设置了机制,我们需要添加一些样式来使一切看起来完整。首先,我们需要为新附*的推文部分添加与描述和图表部分相同的处理。我们将通过修改 styles.css 中的以下行来实现这一点。
寻找以下行:
#movies-near-me li,#movies-near-me li section.main-info,#movies-near-me li section.description,#movies-near-me li section.charting{
通过添加 nearby-tweets 选择器来更改:
#movies-near-me li,#movies-near-me li section.main-info,#movies-near-me li section.description,#movies-near-me li section.charting,#movies-near-me li section.nearby-tweets{
同样,寻找以下行:
#movies-near-me li section.description,#movies-near-me li section.charting{
通过添加 nearby-tweets 选择器来更改:
#movies-near-me li section.description,#movies-near-me li section.charting,#movies-near-me li section.nearby-tweets{
现在将以下代码添加到 styles.css:
.tweet-count {
cursor:pointer;
color:#0000cd;
}
#movies-near-me li section.nearby-tweets,#movies-near-me li.nearby-tweets section.description,#movies-near-me li.nearby-tweets section.charting {
display:none;
}
#movies-near-me li.nearby-tweets section.nearby-tweets {
display:block;
}
#movies-near-me li section.nearby-tweets {
overflow:scroll;
padding-left:10px;
}
#movies-near-me li section.nearby-tweets h3 {
padding: 10px 0 3px 0;
}
#movies-near-me li section.nearby-tweets li {
height:auto;
border:0;
margin-bottom:10px;
}
在样式就绪后,我们可以通过预览我们的更改来测试我们的行为。当数据加载时,您应该看到以下内容:

注意剧院名称旁边的 (18 tweets)。这是 Web Worker 的成果。如果您点击它,您应该看到以下内容:

如果一切顺利,附*的推文部分应该会打开,显示在剧院位置附*发布的最新推文。
摘要
我们探讨了 Web Worker 的结构,如何设置它,以及它如何在不影响用户体验的情况下解决问题。虽然这是一个虚构的例子,但我们逐步展示了如何使用 Web Workers 根据剧院的地理编码获取附*的推文。
Web Workers 的实际应用案例包括:
-
应该不会阻塞一般企业应用流程的处理器密集型计算
-
自动纠错和语法高亮
-
将图片发布到消息队列
-
使用并发 AJAX 请求并行处理数据
在下一章中,我们将探讨调试应用程序。我们将介绍我们可用的工具,以便深入了解并找出我们的企业应用程序中正在发生的事情。我们还将介绍一些使用代理来嗅探问题的强大技术。
第十一章. 完成工作:调试您的应用
调试过程在开发过程中可能需要相当多的时间。可能会有不可预见的行为、边缘情况,甚至需要找到并解决的错误。因此,尽可能优化是很重要的,而最重要的第一步是选择正确的工具。任何开发过程都必须涉及测试和调试;即使您的应用程序按预期完美运行,您也应该执行测试用例并分析性能,以确保在您使其发展和引入新功能时其完整性。本章涵盖了在企业应用程序中进行调试和分析性能的一系列工具。
本章将涵盖以下内容:
-
要寻找什么
-
使用哪些工具
-
玩转 HTML 和 CSS
-
使用 JavaScript 逐步进行
-
移动调试
-
Web 调试代理
要寻找什么
客户端调试过程集中在浏览器解释的元素上。除非我们使用外部插件(如 Adobe Flash Player),否则我们需要调试:
-
HTML 以查找不正确的样式或测试标签结构的更改
-
CSS 以验证样式的正确性或测试对其的更改
-
使用 JavaScript 验证代码执行,查找可能的错误,或测试代码中的更改
此外,我们还应该测试资源的加载时间(media、html、css和js文件)以及 JavaScript 的执行时间(性能分析)。
使用哪些工具
大多数现代浏览器现在都提供了调试 Web 应用程序的工具。由于跨浏览器兼容性很重要,我们需要了解它们是如何工作的。一般来说,每个调试器都提供以下功能:
-
实时检查和修改 HTML,并在页面上选择视觉元素,显示其 HTML 代码的等效项(HTML | 元素)
-
实时检查和修改 CSS(HTML | 元素或CSS | 资源 | 样式)
-
实时检查和修改(或声明)JavaScript,并创建断点以停止代码执行并逐步检查(控制台 | 脚本 | 源代码)
-
分析每个资源的加载时间(网络 | Net | 时间线 | 工具)
让我们来看看一些最受欢迎的浏览器调试工具:

Firefox 15.0.1 Firebug(插件)
小贴士
Firebug 插件默认未安装在 Firefox 上。您需要访问getfirebug.com/,并在 Firefox 中点击安装 Firebug。

Chrome 22.0.1229.79 开发者工具
对于我们的目的,因为它是最稳健的工具之一,我们将介绍 Firefox 的 Firebug 插件。许多相同的基本概念(HTML 检查、脚本调试、控制台的使用)存在于其他浏览器提供的开发者工具中。
注意
对于 Internet Explorer 来说,有许多调试工具可用。除了 Internet Explorer 8 和 9 中包含的开发者工具外,还有 DebugBar (www.debugbar.com/) 和 dynaTrace (www.compuware.com/application-performance-management/ajax-performance-testing.html)。
操纵 HTML 和 CSS
在 Firebug 中,您可以使用HTML选项卡来探索您的 HTML DOM 对象:

将鼠标悬停在每个标签上,可以看到其对应的渲染元素被突出显示。如果您决定在渲染页面上导航,可以点击检查按钮。

检查按钮允许您选择渲染页面的区域以查看相应的 HTML 代码。

点击编辑,可以在浏览器本身内修改您的 HTML 代码。当然,您并不是在修改页面本身,而是在修改浏览器的本地副本。渲染的页面会自动根据修改而改变。这在尝试修改 HTML 结构并在代码中实现之前获得即时反馈非常有用。
右侧的面板允许我们查看所选 DOM 对象的样式,包括对文件和行号的引用(在右侧以蓝色突出显示)。样式选项卡按优先级分组样式;您可以通过双击在此选项卡中修改或创建属性或修改选择器。

计算样式列出了计算样式,它是浏览器、用户和作者应用样式的合并,其中相对值被计算。例如,如果一个div标签的样式是width:50%,并且它被设置为width:760px的div包围,那么计算样式将是width:380px。

布局显示基于 CSS 盒模型应用的填充、边框和边距的图形表示。

DOM显示所选 DOM 的对象属性。这里有很多信息,包括作为“可浏览”树的document对象和查看 JavaScript 全局命名空间的能力,以查看哪些对象已被加载。

小贴士
您可以使用CSS选项卡来修改每个css文件中的样式。
步步为营学习 JavaScript
脚本选项卡允许您检查所有使用的 JavaScript 文件。

在选择您要检查的文件后,您可以执行 JavaScript 代码的逐步执行。找到您想要停止的行,并点击行号以创建断点。一个表示断点的图形将出现。
小贴士
在脚本标签中查看时,压缩后的 JavaScript 文件通常显示为单行。在页面的调试版本中包含“未压缩”文件是有用的。

现在刷新浏览器,你将看到执行如何在该行停止。

右侧的面板将显示当前作用域中可用的变量。你可以使用它来更改变量值,输入自己的变量,或检查表达式。

在右上角,你可以找到继续代码执行的控制按钮:

播放符号继续代码执行,指向下的黄色小箭头继续执行到下一行,如果调用了一个函数,则会进入该函数,指向下的黄色大箭头继续执行到当前执行上下文中的下一行,跳过被调用的函数,最后一个黄色箭头则从函数中退出。当然,你可以始终将鼠标悬停在每个按钮上以获取这些信息,因为通常每个按钮都有一个工具提示。这允许你控制如何遍历你的代码。你可以在想要了解特定代码块如何评估的地方设置断点来停止执行。然后你可以逐行执行代码,执行你不需要关心的方法调用,或者完全退出到调用方法。在每一步,你都可以看到设置了哪些变量,当前作用域中有什么可用,真正地了解你的代码在做什么。
JavaScript 控制台
控制台标签记录 JavaScript 错误、警告和 Ajax 调用。你可以在 JavaScript 代码中使用以下方式强制代码写入控制台:
console.log("CONTENT TO WRITE");
这特别有用,可以输出消息以了解代码的执行情况。你可以写入调试语句来显示特定变量的值,或者只是指示特定的代码块正在执行。你还可以打印对象并在 Firebug 中检查它们。
控制台对象包括其他有用的调试方法,如info()、warn()和error(),以提供更增强的调试反馈。

小贴士
在将代码部署到生产环境时,删除或注释掉所有console调用是一个好的实践,因为一些浏览器不支持它,这会导致执行中断并完全停止。一个解决方案是创建一个脚本,删除这些行,并将其包含在构建过程中。通常,这样的脚本还会打包和压缩你的代码。
控制台的一个有趣特性是它允许你在实时环境中编写和执行代码。你可以在底部找到一个提示,允许你输入代码。
假设你想要打印具有movies-near-me ID 的对象中的所有图像,你可以在控制台中编写以下代码:
$("#movies-near-me img").each(function(){console.log($(this).attr("src"))})
您应该看到以下结果:

分析加载时间
网络选项卡编译了所有网络调用,包括状态、域名、文件大小、IP 和加载时间。您可以使用此部分来验证性能问题所在。

JavaScript 性能分析
控制台选项卡提供了一个分析我们 JavaScript 代码性能的工具。要执行分析,请单击分析按钮,然后执行您的 JavaScript 操作(或刷新页面以分析主要 JavaScript 执行),然后再次单击分析按钮。

如果函数被注册为匿名,无需担心。文件字段给出了确切的代码行。
移动调试
在移动网络应用程序中调试 HTML、CSS 和 JavaScript 可能会很痛苦,尤其是如果我们使用的是触摸设备。远程调试提供了一种使用您的桌面或笔记本电脑在移动设备上测试您的企业网络应用程序的方法。
Chrome 支持通过 USB 为 Android 设备进行远程调试:developers.google.com/chrome/mobile/docs/debugging。
Firefox 15 也为 Android 引入了远程调试:hacks.mozilla.org/2012/08/remote-debugging-on-firefox-for-android/
在版本 6 之前,iOS 设备内部有一个简单的调试界面。

Safari 6.0.1(仅限 Mac)和 iOS 6 支持远程调试。要在任何 iOS 6 中开始远程调试:
-
在设备(iPhone、iPad 或 iPod)中,转到设置 | Safari | 高级并启用Web 检查器选项。
-
在 Mac 计算机上打开 Safari。
-
使用您的 iOS 设备上的 Safari 打开要调试的 Web 应用程序。
-
使用 USB 线缆将设备连接到您的 Mac 计算机。
-
如果您在 Safari(Mac)中没有开发菜单项,请打开Safari | 偏好设置 | 高级并勾选在菜单栏中显示开发菜单。
-
转到开发 | 设备名称 然后选择 Web 应用程序。
-
您现在应该看到一个网页检查器。
-
还有一个名为 JConsole 的在线工具,允许您远程控制并调试其他窗口和设备中的浏览器。它通过为您提供要包含在您正在调试的应用程序中的脚本引用来实现。然后它提供了一个控制台,您可以在此发送
console.log消息和其他调试信息。更多信息可以在jsconsole.com/remote-debugging.html找到。
Web 调试代理
代理是一种充当客户端和服务器之间中间人的硬件设备。将软件代理作为调试网络应用程序的方法是一种广泛使用的做法,其主要目的涉及检查请求数据并查看服务器响应。
在 Windows *台上,我们推荐使用 Fiddler。您可以在www.fiddler2.com/fiddler2/找到它。如果您使用的是 Mac,可以使用 Charles,您可以在www.charlesproxy.com/找到它。两者都是 Web 代理,用于记录服务器和客户端之间的通信。
简单地打开应用程序,开启捕获,并加载您的 Web 应用程序。在这里,您可以看到 Charles 正在捕获 MovieNow 请求:

使用代理模拟响应也是可能的。例如,在 Charles 中,您可以前往工具,然后选择映射本地...来使用本地文件或服务作为您应用程序的响应。您可以在同一菜单中使用重写...来修改响应的一部分并将其发送回您的应用程序。这对于测试边缘情况是一个非常有价值的功能。

如果您想使用 Charles 与 iOS 设备一起进行远程调试,请按照以下步骤操作:
-
使用终端中的
ipconfig getifaddr en1.命令获取您的 Mac IP 地址。 -
在代理菜单中激活启用 Mac OS X 代理选项。
-
将您的 iOS 设备连接到运行 Charles 的 Mac 电脑使用的同一网络。
-
在您的 iOS 设备上,前往设置 | Wi-Fi,点击您连接的蓝色箭头,然后在HTTP 代理部分输入您的 Mac IP 地址和 Charles 使用的端口号(默认为 8888)。
-
在您的 iOS 设备上的 Safari 中导航。
-
Charles 应该会显示一个警告,通知您已尝试建立新的连接。点击允许。
-
现在,您应该能够记录您的流量。
摘要
在本章中,我们介绍了有用的调试工具,包括移动和 Web 代理,以及如何在浏览器中操作 HTML、CSS 和 JavaScript 以调试我们的应用程序。作为开发者,了解所有可用的调试代码选项对我们来说是一种良好的实践。当调试仅在一个浏览器中发生的错误时,我们必须特别注意该浏览器的技术限制和功能,因为这些限制通常是原因。例如,复杂的 JavaScript 代码在像 Internet Explorer 7 这样的浏览器中可能会很慢。使用该浏览器提供的工具可以让我们深入了解该浏览器的内部结构,从而了解如何防止未来的错误。
在下一章中,我们将介绍企业级应用程序项目的测试工具和框架,以及自动化功能测试的优势。
第十二章. 完成工作:测试您的应用
尽管测试的主题可能涵盖整本书,实际上也确实有许多关于这个主题的书籍,但我们将提供一个测试 HTML5 企业应用的框架,以及一系列有说服力的主题概要,这些主题将作为进一步研究的起点。不同的测试工具都带有它们自己的特定习语;我们将涵盖这些习语背后的概念。
本章将涵盖以下内容:
-
单元测试
-
功能测试
-
浏览器测试
-
持续集成
测试类型
测试可以在许多不同的层面上进行。从代码级别到集成,甚至测试企业应用程序面向用户的实现中的单个功能,都有许多工具和技术可以测试您的应用程序。特别是,我们将涵盖以下内容:
-
单元测试
-
功能测试
-
浏览器测试
小贴士
黑盒与白盒测试
测试通常在黑盒测试与白盒测试的背景下讨论。这是一个有用的隐喻,有助于理解不同层次的测试。在黑盒测试中,您将您的应用程序视为一个黑盒,对其内部一无所知——通常是从系统用户的角度来看。您只需执行应用程序的功能,并测试预期的结果是否与实际结果相符。白盒测试与黑盒测试的不同之处在于,您事先了解应用程序的内部结构,因此可以直接定位故障并测试特定条件。在这种情况下,您只需向系统的特定部分输入数据,并测试预期的输出是否与实际输出相符。
单元测试
测试的第一级是在代码级别。当您在测试特定的和单个代码单元是否达到其声明的目标时,您正在进行单元测试。单元测试通常与测试驱动开发一起讨论,这是一种先编写单元测试,然后编写通过这些测试所需的最少代码的实践。对您的代码有一套单元测试并采用测试驱动流程——如果做得正确——可以使您的代码保持专注,并有助于确保企业应用程序的稳定性。
通常,单元测试是在代码库中的单独文件夹中设置的。每个测试用例由以下部分组成:
-
设置构建测试条件,以便在测试代码或模块
-
测试中的代码或模块的实例化和调用
-
验证返回的结果
设置您的单元测试
您通常从设置测试数据开始。例如,如果您正在测试需要认证账户的代码片段,您可能需要考虑创建一组企业应用的测试用户。建议您的测试数据与测试相关联,这样您的测试就不会依赖于系统处于特定状态。
调用您的目标
一旦你设置了测试数据和测试代码需要运行的条件,你就可以调用它了。这可以简单到调用一个方法。
模拟是单元测试时需要理解的一个重要概念。考虑一个业务逻辑模块的单元测试集,该模块依赖于某些外部应用程序编程接口(API)。现在想象一下,如果 API 崩溃了,测试将失败。虽然得到一个指示,表明你依赖的 API 存在问题是很不错的,但因为这个原因而失败的单元测试是误导性的,因为单元测试的目标是测试业务逻辑,而不是你依赖的外部资源。这就是模拟对象出现的地方。模拟对象是复制资源接口的存根。它们被设置为在正常条件下总是返回外部资源会返回的相同数据。这样,你就可以将测试隔离到正在测试的代码单元。
模拟使用了一种称为依赖注入或控制反转的模式。当然,你正在测试的代码可能依赖于外部资源。然而,你将如何用模拟资源替换它?易于单元测试的代码允许你在调用它时传入或“注入”这些依赖。
依赖注入是一种设计模式,其中依赖于外部资源的代码将这种依赖传递给它,从而将你的代码与这种依赖解耦。以下代码片段难以测试,因为依赖被封装在正在测试的函数中。我们陷入了僵局。
var doSomething = function() {
var api = getApi();
//A bunch of code
api.call();
}
var testOfDoSomething = function() {
var mockApi = getMockApi();
//What do I do now???
}
以下新的代码片段使用依赖注入通过实例化依赖并将其传递给正在测试的函数来绕过问题:
var doSomething = function(api) {
//A bunch of code
api.call();
}
var testOfDoSomething = function() {
var mockApi = getMockApi();
doSomething(mockApi);
}
注意
通常,这不仅是单元测试的良好实践,而且对于保持代码干净和易于管理也是好的。一旦实例化依赖,并在需要的地方注入它,如果需要,就更容易更改该依赖。有许多模拟框架可用,包括 JavaScript 的 JsMockito (jsmockito.org/) 和 PHP 的 Mockery (github.com/padraic/mockery)。
验证结果
一旦你调用了正在测试的代码,你需要捕获结果并验证它们。验证以断言的形式出现。每个单元测试框架都附带自己的断言方法集,但概念是相同的:取一个结果并测试它是否符合预期。你可以断言两个事物是否相等。你可以断言两个事物是否不相等。你可以断言结果是否是有效的数字或字符串。你可以断言一个值是否大于另一个值。一般想法是,你正在测试实际数据是否符合你的假设。断言通常会上升到框架的报告模块,并以通过或失败的测试列表的形式体现。
框架和工具
在过去几年中,出现了许多辅助 JavaScript 单元测试的工具。以下是对一些显著的框架和工具的简要概述,这些工具用于对 JavaScript 代码进行单元测试。
JsTestDriver
JsTestDriver 是一个由 Google 构建的用于单元测试的框架。它有一个在机器上的多浏览器上运行的服务器,并允许你在 Eclipse IDE 中执行测试用例。

这张截图显示了 JsTestDriver 的结果。当运行时,它执行所有配置为运行的测试,并显示结果。
有关 JsTestDriver 的更多信息,请参阅 code.google.com/p/js-test-driver/。
QUnit
QUnit 是由 jQuery 闻名的人物 John Resig 创建的 JavaScript 单元测试框架。要使用它,你需要创建一个测试 harness 网页,并将 QUnit 库作为脚本引用包含在内。甚至还有一个托管版本的库。一旦包含,你只需要调用测试方法,传入一个函数和一组断言。然后它将生成一份漂亮的报告。

虽然 QUnit 没有依赖项,可以测试标准 JavaScript 代码,但它围绕 jQuery 展开。有关 QUnit 的更多信息,请参阅 qunitjs.com/。
Sinon.JS
通常与 QUnit 配合使用,Sinon.JS 引入了监视的概念,其中它记录函数调用、传入的参数、返回值,甚至 this 对象的值。你还可以创建假对象,如假服务器和假定时器,以确保你的代码在隔离环境中进行测试,并且测试尽可能快地运行。这在需要执行假 AJAX 请求时尤其有用。
有关 Sinon.JS 的更多信息,请参阅 sinonjs.org/。
Jasmine
Jasmine 是一个基于行为驱动开发概念进行测试的框架。它与测试驱动开发非常相似,通过引入领域驱动设计原则来扩展它,并试图将单元测试框架回归到面向用户的行为和业务价值。Jasmine 以及其他基于行为驱动的框架使用尽可能多的英语来构建测试用例——称为规范——以便在生成报告时,其阅读起来比传统的单元测试报告更自然。
有关 Jasmine 的更多信息,请参阅 pivotal.github.com/jasmine/。
功能测试
Selenium 已经成为网站功能测试的代名词。它的浏览器自动化功能允许你在你喜欢的网络浏览器中记录测试用例,并在多个浏览器上运行它们。当你拥有这些工具时,你可以自动化你的浏览器测试,将它们与你的构建和持续集成服务器集成,并在需要时同时运行以获得更快的测试结果。
Selenium 包含 Selenium IDE,这是一个用于记录和运行 Selenium 脚本的实用工具。作为一个 Firefox 插件构建,它允许你在 Firefox 中加载和点击网页来创建 Selenium 测试用例。你可以轻松地记录你在浏览器中的操作并回放它们。然后你可以添加测试来确定实际行为是否与预期行为匹配。这对于快速创建 Web 应用的简单测试用例非常有用。有关安装信息,请参阅 seleniumhq.org/docs/02_selenium_ide.html。
以下截图显示了 Selenium IDE。点击右侧的红色圆形图形以开始记录,然后在浏览器窗口中浏览到 google.com 并搜索 "html5"。点击红色圆形图形以停止记录。然后你可以添加断言来测试页面的某些属性是否与预期匹配。在这种情况下,我们正在断言搜索结果中第一个链接的文本是 HTML5 的维基百科页面。当我们运行测试时,我们看到它通过了(当然,如果 Google 上 "html5" 的搜索结果发生变化,那么这个特定的测试将失败)。

Selenium 包含 WebDriver,这是一个 API,允许你本地或远程原生地驱动浏览器。结合其自动化功能,WebDriver 可以在多台远程机器上的浏览器上运行测试,以实现更大的规模。
对于我们的 MovieNow 应用程序,我们将通过以下组件设置功能测试:
-
Selenium 独立服务器
-
来自 Facebook 的 php-webdriver 连接器
-
PHPUnit
Selenium 独立服务器
Selenium 独立服务器将请求路由到 HTML5 应用程序。为了运行测试,它需要启动。它可以部署在任何地方,但默认情况下,它可以通过 http://localhost:4444/wd/hub 访问。你可以在 code.google.com/p/selenium/downloads/list 下载独立服务器的最新版本,或者你可以启动 test/lib 文件夹中包含的示例代码中的版本。要启动服务器,请在命令行中执行以下行(你需要在你的机器上安装 Java):
java -jar lib/selenium-server-standalone-#.jar
在这里,# 表示版本号。
你应该会看到类似以下的内容:

在这一点上,它正在监听连接。当你运行测试时,你将在这里看到日志消息。请保持此窗口打开。
来自 Facebook 的 php-webdriver 连接器
php-webdriver 连接器作为 WebDriver 在 PHP 中的库。它允许您使用所有主要网络浏览器的驱动程序以及 HtmlUnit 来创建和检查网络请求。因此,它允许您针对任何网络浏览器创建测试用例。您可以在 github.com/facebook/php-webdriver 下载它。我们已将文件包含在 webdriver 文件夹中。
PHPUnit
PHPUnit 是一个单元测试框架,它为我们提供了运行测试所需的构造。它具有构建和验证测试用例所需的管道。任何单元测试框架都可以与 Selenium 一起工作;我们选择了 PHPUnit,因为它轻量级且与 PHP 工作良好。您可以通过多种方式下载和安装 PHPUnit(有关安装的更多信息,您可以访问 www.phpunit.de/manual/current/en/installation.html)。我们已将 phpunit.phar 文件包含在 test/lib 文件夹中,以便您方便使用。您可以通过在命令行中执行以下操作来简单地运行它:
php lib/phpunit.phar <your test suite>.php
首先,我们将向 test 文件夹添加一些 PHP 文件。第一个文件是 webtest.php。创建此文件并添加以下代码:
<?php
require_once "webdriver/__init__.php";
class WebTest extends PHPUnit_Framework_TestCase {
protected $_session;
protected $_web_driver;
public function __construct() {
parent::__construct();
$_web_driver = new WebDriver();
$this->_session = $_web_driver->session('firefox');
}
public function __destruct() {
$this->_session->close();
unset($this->_session);
}
}
?>
WebTest 类通过 php-webdriver 连接器将 WebDriver 集成到 PHPUnit 中。这将成为我们所有测试用例的基类。如您所见,它以以下内容开始:
require_once "webdriver/__init__.php";
这是对 php-webdriver 文件夹中的 __init__.php 的引用。这引入了 WebDriver 所需的所有类。在构造函数中,WebTest 初始化所有测试用例中使用的驱动程序和会话对象。在析构函数中,它清理其连接。
现在我们已经设置好了一切,我们可以创建我们的第一个功能测试。将名为 generictest.php 的文件添加到 test 文件夹中。我们将导入 WebTest 并扩展该类,如下所示:
<?php
require_once "webtest.php";
class GenericTest extends WebTest {
}
?>
在 GenericTest 类内部,添加以下测试用例:
public function testForData() {
$this->_session->open('http://localhost/html5-book/Chapter%2010/');
sleep(5); //Wait for AJAX data to load
$result = $this->_session->element("id", "movies-near-me")->text();
//May need to change settings to always allow sharing of location
$this->assertGreaterThan(0, strlen($result));
}
我们将打开到我们应用程序的连接(您可以将 URL 更改为您运行 HTML5 应用程序的地方),等待 5 秒以加载初始 AJAX,然后测试 movies-near-me div 是否已填充数据。
要运行此测试,请转到命令行并执行以下行:
chmod +x lib/phpunit.phar
php lib/phpunit.phar generictest.php
您应该会看到以下内容:

这表示测试已通过。恭喜!现在让我们看看它失败的情况。添加以下测试用例:
public function testForTitle() {
$this->_session->open('http://localhost/html5-book/Chapter%2010/');
$result = $this->_session->title();
$this->assertEquals('Some Title', $result);
}
重新运行 PHPUnit,您应该会看到类似以下的内容:

如您所见,它预期的是 'Some Title',但实际上找到的是 'MovieNow'。现在我们已经让您开始了,我们将让您创建自己的测试。有关使用 PHPUnit 可以做出的不同断言的指南,请参阅 www.phpunit.de/manual/3.7/en/index.html。
更多关于 Selenium 的信息可以在 seleniumhq.org/ 找到。
浏览器测试
在前面章节中关于浏览器兼容性的讨论中,对 HTML5 企业应用程序的测试必须涉及在不同网络浏览器上实际查看应用程序。幸运的是,许多网络浏览器都提供了适用于多个*台的版本。Google Chrome、Mozilla Firefox 和 Opera 都有适用于 Windows、Mac OSX 以及 Ubuntu 等 Linux 变种的版本,可以轻松安装。Safari 有适用于 Windows 和 Mac OSX 的版本,并且有方法通过一些调整在 Linux 上安装。
然而,Internet Explorer 只能在 Windows 上运行。一种绕过这种限制的方法是安装虚拟化软件。虚拟化允许您在宿主操作系统内虚拟运行整个操作系统。它允许您在 Mac OSX 上运行 Windows 应用程序或在 Windows 上运行 Linux 应用程序。包括 VirtualBox、VMWare Fusion、Parallels 和 Virtual PC 在内的许多虚拟化软件包都很有名。
小贴士
尽管 Virtual PC 只运行在 Windows 上,但微软确实提供了一套预包装的虚拟硬盘,包括用于测试目的的特定版本的 Internet Explorer。有关详细信息,请参阅以下 URL:www.microsoft.com/en-us/download/details.aspx?id=11575。
另一种常见的测试兼容性的方法是使用基于网络的浏览器虚拟化。有许多服务,如 BrowserStack (www.browserstack.com/)、CrossBrowserTesting (crossbrowsertesting.com/) 和 Sauce Labs (saucelabs.com/),提供了一种服务,您可以通过输入一个 URL,在多种网络浏览器和*台上(包括移动设备)虚拟地查看其渲染效果。其中许多服务甚至可以通过代理工作,让您查看、测试和调试在本地机器上运行的网络应用程序。
持续集成
在任何测试解决方案中,创建和部署您的构建以及以自动化方式运行测试都是非常重要的。像 Hudson、Jenkins、CruiseControl 和 TeamCity 这样的持续集成解决方案可以帮助您实现这一点。它们合并来自多个开发者的代码,并运行一系列自动化功能,从部署模块到运行测试。它们可以按计划运行,也可以通过事件触发,例如通过 post-commit 钩子将代码提交到代码库。
摘要
在本章中,我们涵盖了多种测试类型,包括单元测试、功能测试和浏览器测试。对于每种测试类型,都有许多工具可以帮助你确保你的企业应用程序以稳定的方式运行,其中大部分我们已经介绍,只有少数例外。因为你的应用程序代码的每一分钟更改都有可能使其不稳定,我们必须假设每一次更改都可能导致不稳定。为了确保你的企业应用程序保持稳定并且缺陷最小,拥有一个包含从单元到功能的丰富测试套件的测试策略,并结合运行这些测试的持续集成服务器是至关重要的。当然,必须权衡编写和执行测试所需的时间与编写生产代码所需的时间,但长期维护成本的节省可以使这种投资变得值得。
在下一章中,我们将介绍确保你的企业应用程序以最佳性能运行的技巧,包括对性能分析技术的讨论。
第十三章. 完成工作:性能
我们将结束讨论性能,这是有充分理由的。虽然在你开发企业应用时考虑性能很重要,但你可能会最终优化那些后来没有表现出任何性能问题的东西。这通常被称为过早优化,最终可能会浪费很多时间。尽管理解开发过程中每个决策的性能影响是一个好习惯,但网络性能优化不应被视为最终目标;相反,它是一个持续的调整,以改善并达到我们企业应用可接受的速度。我们的真正目标是构建我们的应用,并确保它正确运行,然后,如果必要,提高响应时间。
在本章中,我们将涵盖:
-
网络性能优化(WPO)
-
遵循标准
-
优化图像
-
优化 CSS
-
JavaScript 性能考虑
-
其他页面性能考虑
-
性能分析
网络性能优化(WPO)
由于 HTML5 企业应用有许多不同的组成部分,考虑你正在优化的部分很重要。总的来说,你的 HTML5 企业应用将包括 HTML、图像以及 CSS 和 JavaScript 代码,并且有方法可以优化这三者。
遵循标准
HTML 被设计成一种宽容的语言;也就是说,语法错误不是通过使页面崩溃和引起无尽的调试噩梦来处理的,而是以一种更优雅的方式处理。渲染引擎试图确定标记的意图,并据此布局页面。本质上,它会跌跌撞撞,但最终还能保持*衡。当然,没有障碍的赛跑比有障碍的赛跑要快。此外,不同的网络浏览器将以不同的方式从这些错误中恢复,导致当你的 HTML5 企业应用在不同浏览器中查看时,结果不一致。这就是为什么向浏览器提供干净、符合标准标记的重要性。
小贴士
尽管遵循标准是我们开始优化的良好基础,但在某些情况下,这可能会导致更冗长的代码(增加解析时间)。此外,HTML5 与之前的版本不同,它还不是一项完成的标准,应被视为一种指导而非一套规则。
幸运的是,有许多工具可以帮助你验证你的标记。负责制定 Web 标准的机构 World Wide Web Consortium(W3C)有自己的验证工具,可以在 validator.w3.org/ 找到。还有像 HTML Lint (lint.brihten.com/html/) 和 HTML Tidy (infohound.net/tidy/) 这样的工具可以帮助你清理你的标记。验证你的标记是一个好习惯,以确保你的企业应用程序运行快速且一致。
优化图像
如今,大多数网站都嵌入图像,而这些图像往往是性能问题的主要来源。由于带宽有限和图像文件的大尺寸,你的企业应用程序可能实际上运行得相当快,但会迫使用户等待大图像传输到他们的浏览器。在将图像用于 Web 应用程序之前,优化你的图像是至关重要的。
对于网络优化,有两个考虑因素:大小和类型。关于大小,虽然可以在 img 标签中设置图像的宽度和高度维度,但将单个大图像用于需要不同尺寸的 Web 应用程序的不同目的是一种常见的错误。例如,在显示缩略图时,仅使用 img 标签属性缩小较大图像是一种不良做法。相反,你应该为不同的目的创建图像的不同变体或版本。
小贴士
在 img 标签上指定 width 和 height 属性让浏览器知道在图像实际下载之前为图像分配多少空间,从而避免布局变化和 UI 中的不希望出现的“跳跃”。请注意,这在很大程度上与将内容层和表示层分离的最佳实践相矛盾。
关于图像类型,在网络上确实有三种常用的图像格式:GIF、JPEG 和 PNG。这些格式基于不同的压缩算法,用于非常不同的目的。GIF 图像针对低色阶图像进行了优化。它们支持 256 种颜色,是无损和交错式的,这意味着它们是分层渲染的,而不是一次性渲染(当你下载和渲染它们时,从模糊到清晰)。它们非常适合基于低色阶的标志和网站图形。JPEG 图像非常适合高分辨率照片,因为它们支持高达 1600 万色的颜色调色板。PNG 图像可以支持 256、24 位或 32 位色阶的图像格式,具有可选的透明度,这是一种非常灵活且高度压缩的无损格式,比 GIF 具有更好的透明度和压缩性能。PNG 算法是为了作为 GIF 压缩格式的开放替代品而创建的,其原始创造者 Unisys 在 1995 年宣布将执行其对该算法的专利权。
小贴士
Internet Explorer 6 及之前的版本不支持基于 HTML 标准的 PNG 透明度;相反,必须使用专有过滤器。例如,filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='image.png' ,sizingMethod='crop');。
优化 CSS
优化 CSS 的最佳方式是减小文件大小。你可以做很多事情来实现这一点。你可以通过将样式放在一行中来减少空白。
实现以下代码行:
body {background-color:#fff;color:#000;font-size:1.0em;font-family:Arial;}
而不是这样:
body
{
background-color:#ffffff;
color:#000000;
font-size:1.0em;
font-family:Arial;
}
你可以为许多规则使用缩写。
实现以下代码行:
p {margin:10px 20px;}
而不是这样:
p
{
margin-top: 10px;
margin-right: 20px;
margin-bottom: 10px;
margin-left: 20px;
}
最好尽可能地将相似样式分组在一起,并在必要时合并重复的样式。
实现以下代码行:
p, ul {color:#efefef;}
而不是这样:
p {color:#efefef;}
ul {color:#efefef;}
小贴士
如果你决定不遵循这些建议以保持可读性,或者即使遵循了这些建议,也始终可以对你的代码进行压缩(正如我们在以下章节中解释的)。
另一个选择是使用动态样式表语言,如Less (lesscss.org)。Less 扩展了基本的 CSS 功能,允许我们使用更复杂和优雅的结构,这些结构在编译过程中将被转换为标准的 CSS。
使用常规 CSS,你需要重复使用常见的属性,如颜色:
p{color:#efefef}
div.box{border:1px solid #efefef}
使用 Less,你可以定义一个变量@active-color,然后在你的样式中使用它,这样你只需要更改这些变量的值就可以更改多个属性的颜色的值:
@active-color:#efefef;
p{color:@active-color }
div.box{border:1px solid @active-color}
你甚至可以创建嵌套结构,如下所示:
@active-color:#efefef;
.content{
div.box{border:1px solid @active-color}
p{color:@active-color }
}
编译后,这将转换为以下内容:
.content div.box{border:1px solid @active-color}
.content p{color:@active-color }
JavaScript 性能考虑
虽然编写 JavaScript 可能会充满危险,因为它很容易引入运行缓慢的代码,但记住一些简单的指南可以帮助你的企业级应用避免运行得太慢。

首先,遍历 DOM 可能很昂贵。你想要最小化调用document.getElementById和,更糟糕的是,document.getElementsByTagName的次数。将 DOM 元素的引用捕获到变量中并使用该变量可以节省对 DOM 的昂贵调用。
所以,而不是实现以下代码:
document.getElementsById("elementId").setAttribute("data-position", 1);
document.getElementsById("elementId").setAttribute("data-position", 1);
或者使用 jQuery 实现其等效功能:
$("#elementId").attr("data-position", 1);
$("#elementId").attr("data-position", 1);
你可以实现以下代码:
var element= document.getElementsById("elementId");
element.setAttribute("data-position", 1);
element.setAttribute("data-position", 1);
或者你可以使用 jQuery 实现其等效功能:
var element= $("#elementId");
element.attr("data-position", 1);
element.attr("data-position", 1);
其次,避免使用with()和for-in这样的结构。
第三,记住数组可能会很慢。遍历数组——特别是深层数组——可能会很昂贵。如果你多次从数组中提取相同的元素,最好先将其捕获到变量中。也就是说,而不是以下代码:
var array=[1,2,3,4,5];
console.log(array[3]+2);
console.log(array[3]*3);
或者这样:
var array=[1,2,3,4,5];
var elementSelected=array[3];
console.log(elementSelected+2);
console.log(elementSelected*3);
第四,数组并不像 DOM 集合那样慢。遍历document.getElementsByTagName('p')比捕获结果到数组并遍历它要慢得多。
最后,改变 DOM 元素的类比改变样式要便宜。最好定义多个 CSS 类并在它们之间切换,而不是直接改变元素的风格。
因此,而不是以下代码:
domObject.style.display="none";
或者它的 jQuery 版本:
$(domObject).css(display, "none");
你可以实现以下代码:
domObject.setAttribute("class", "hideClass");
或者这样:
$(domObject.attr("class","hideClass");
你也可以使用一个名为 jsPerf 的工具来基准测试你的 JavaScript (jsperf.com/)。它提供了一种创建 JavaScript 代码片段测试用例的方法,以便你可以基准测试它们的性能。如果你想知道哪个更快,document.getElementsByTagName 还是 document.getElementsByClassName,这个工具将允许你在浏览器上测试你的理论。此外,它还允许你分享你的测试用例,这样其他人就可以在不同的浏览器上测试,为你提供跨各种浏览器和*台的统计数据。
额外的页面性能考虑因素
企业应用程序可以由许多文件组成,包括 HTML、CSS、JavaScript 和图像。尽管为了可维护性,将 CSS 和 JavaScript 文件分离出来是合适的。在部署你的代码时,合并和压缩你的文件会导致更好的性能。压缩是一种代码压缩技术,其中移除了所有不必要的字符,同时保留行为。有许多工具可以为你完成这项工作,包括以下这些:
-
YUI 压缩工具 (
developer.yahoo.com/yui/compressor/)
在减少文件大小、最小化请求以及总体上尽可能少使用带宽的方面,CSS Sprites 的使用已经变得很普遍,其中应用程序的所有静态图形元素都被组合成单个图像,其中的一部分使用 CSS 显示。这样,只需要下载一次一个图像,而不是多个。
服务器端考虑因素
企业应用程序通常包含许多静态资源,这些资源在每次页面请求时都会从服务器下载。由于这会在服务器上产生大量的不必要流量,一种减轻负担的方法是使用内容分发网络或 CDN。CDN 允许你在优化的服务器网络上镜像你的页面,以便快速交付静态资源。你可以在 Akamai、Edgecast 或 Cloudflare 等 CDN 上放置你的静态资源,以及使用流行的库的 CDN 托管版本。Google 在其 CDN (developers.google.com/speed/libraries/) 上托管了多个公共库,如 jQuery,同样 cdnjs (cdnjs.com/) 也是如此。
许多网络服务器,如 Apache,可以被指示在发送给浏览器之前压缩它们发送的内容。如果您能够做到,在您的响应中添加一个设置为 gzip 的 Content-Encoding 标头可以减少数据传输量高达 70%。
在您的企业应用程序中考虑缓存是很重要的。如果相同的请求被反复提出,并且响应始终相同或很少更改,则在后续请求时缓存响应并返回。浏览器通过 Cache-Control 标头和 Expires 标头固有地支持这一点。虽然 Expires 告诉浏览器在页面缓存中保留内容的时间,但 Cache-Control 提供了一组规则,用于何时保留缓存以及何时使缓存无效。一些有用的参数包括以下内容:
-
max-age: 这表示在内容应该刷新之前的最长时间 -
public: 这表示即使内容需要认证,该内容也是可缓存的 -
private: 这表示内容可以按用户基础进行缓存 -
no-cache: 这表示内容可以被缓存,但在每次请求时都应该刷新 -
no-store: 这表示不应将内容保存在缓存中 -
must-revalidate: 这表示浏览器在提供缓存版本之前必须先与服务器进行验证
缓存不仅有助于提高响应时间,还能减轻服务器负载并减少网络流量。
Yahoo! 提供了一系列有用的指南,以确保页面性能。虽然许多这些主题已经有所涉及,但鼓励您亲自查看developer.yahoo.com/performance/上的指南。
性能分析
即使遵循所有最佳性能规则,您可能仍然会遇到加载缓慢的应用程序。当这种情况发生时,您需要擅长评估加载时间和分析您的应用程序。幸运的是,有许多工具可以帮助定位瓶颈;这些工具将在接下来的章节中解释。
加载时间
所有主要浏览器都包含一个网络标签,可以图形化显示浏览器与其接触的服务器之间的所有请求和响应。Firefox 将 Net 标签作为 Firebug 插件的一部分,如下面的屏幕截图所示:

它显示一组条形图,表示随时间变化的每个请求的加载时间。在这里,您可以看到网页逐部分加载,包括它首先加载 HTML,然后请求辅助资产:图像文件、CSS 文件、JavaScript 文件,甚至后续的 AJAX 请求。正如您在下面的屏幕截图中所见,当您悬停在每根条形图上时,您将看到有关该请求的统计数据,包括 DNS 查询、连接时间、发送请求的时间、等待时间和接收响应的时间等。

当尝试调试性能问题时,这尤其有帮助,因为它会非常清楚地告诉你缓慢的原因是由于连接问题还是页面加载问题,例如 HTML 是如何构建和解析的。了解这些更详细的细节有助于大大揭示并消除瓶颈。
除了基于浏览器的工具来评估加载时间外,还有外部服务允许你评估加载时间。特别是,Pingdom (tools.pingdom.com/fpt/) 提供了一个你可以用来定期测试你的 Web 应用程序加载时间的服务。它提供了一个类似的图表来确定你的瓶颈在哪里。其他替代方案包括以下:
-
Google Speed (
developers.google.com/speed/) -
YSlow (
developer.yahoo.com/yslow/)
性能分析器
性能分析器是评估性能的另一种方式。通常,它们按执行时间顺序列出 JavaScript 调用,这有助于确定运行缓慢的函数。一些浏览器,如 Google Chrome 和 Safari,包括 CSS 选择器性能分析器,它列出了运行最慢的 CSS 选择器。
Firefox 在其 Firebug 插件中集成了性能分析器。点击控制台标签,然后点击分析。

在 Google Chrome 的开发者工具中,你会找到一个配置文件标签页。
Safari 提供了一个工具标签页。
在 Internet Explorer 9 的开发者工具中同样提供了性能分析器。点击性能分析器然后点击开始分析按钮开始使用。
性能分析器通常会列出运行最慢的函数,它们被调用的次数以及执行所需的时间。它们通常将执行时间分为两个类别。有些人称之为独占时间,而其他人称之为自时间或拥有时间。这是在函数内部执行的时间,不包括从该函数内部调用的其他函数的执行时间。另一个类别被称为包含时间、总时间或只是时间。这是包括从该函数内部调用的函数在内的函数执行时间。
摘要
我们已经讨论了在多个不同层面上确保企业应用程序性能的方法。有确保浏览器能理解的干净 HTML 的方法,也有确保最优 CSS 和 JavaScript 的方法。有减少页面占用空间、减少请求数量和减轻浏览器负担的方法。虽然这些方法各自都很重要,但真正的性能提升并不总是显而易见的,直到你将它们全部结合起来。
我们已经构建了我们的示例应用,MovieNow,包括构建任何企业应用的重要步骤;从使用语义标签定义有意义的结构,到使用 CSS3 进行样式和动画设计,展示了新的和令人兴奋的功能,如 2D 画布和 WebGL、地理位置、视频、音频、拖放和 Web Workers。我们实践了使用真实世界的 API,如 Twitter,并回顾了一系列工具和库,以促进开发、测试和性能改进的过程。
我们鼓励你继续阅读有关新网络技术的文章。正如我们所说,现在有许多开发者正在创建不仅新的企业应用,还有新的库、技术、工具和思维范式,这些都可以重新定义我们所知道的互联网。
最后,我们希望您像我们一样享受这段旅程,并且这本书的初始指南能引导您走向许多成功的 HTML5 项目。


浙公网安备 33010602011771号