HTML5-和-VS2015-高级教程-全-
HTML5 和 VS2015 高级教程(全)
一、开始之前
在本书中,我将展示如何利用超文本标记语言(HTML5)中包含的非常酷的新特性。这将是非常实际的大量代码样本和工作网页。然而,在我们开始之前,我将搭建舞台,并为我们将要去的地方提供一些背景。一般所说的 HTML5 包含了很多技术,HTML 只是冰山一角。
在这一章中,我将简要回顾当前和历史上托管网站的操作环境。我还将描述可用的开发工具。虽然这本书特别关注 Visual Studio 2015,但也有一些免费的替代方法可以帮助您完成大多数练习。最后,我将快速盘点一下当前和未来浏览器对 HTML5 的支持。
审查网络环境
为了从 web 开发人员的角度更好地理解 HTML5,我将首先回顾一下我们所处的 web 环境。这将是一个基本的概述,对大多数读者来说相当熟悉。然而,我经常发现偶尔后退一步是有用的,可以获得更好的视角。
基本的 HTTP 页面
在网络的早期,这个模型非常简单。它包括一个负责提供网页的网络服务器和一个在客户端呈现网页的浏览器。在微软的堆栈中,Internet 信息服务(IIS)提供了服务器组件,而 Internet Explorer 是事实上的浏览器。当然,还有其他浏览器,比如网景。浏览器将通过传递超文本传输协议(HTTP) GET 请求中的地址(URL)向 web 服务器请求页面。服务器将通过提供一个 HTML 文档来响应,然后由浏览器呈现,如图 1-1 所示。

图 1-1。
A simple page-centric web model
如果网页包含一个带有输入字段的表单,浏览器将提供这些数据的输入。当提交页面时,这些数据通过 HTTP POST 请求发送到 web 服务器。web 应用将对这些数据做一些事情,然后返回一个更新的网页。然后,浏览器会在客户机上呈现整个页面。
在这里,我想重点介绍两个即使在当今的网络环境下仍然具有重大影响的关键方面:
- 该模型非常以页面为中心。
- web 开发包括服务器和客户端两个方面。
以页面为中心的网站
正如我提到的,网站主要集中在网页上。请求、返回并呈现页面。页面上的数据被发送到服务器并进行处理,然后返回并呈现更新后的页面。因为 web 服务器是无状态的,所以它不知道先前返回的页面。这就是为什么整个页面必须提交和返回。当前和未来的技术正在帮助摆脱这种范式,我将在本书中演示其中的许多技术。然而,以页面为中心的设计仍然很流行,而且可能会持续一段时间。
客户机-服务器模型
构建 web 应用时,需要考虑服务器和客户端组件。在服务器上,IIS 响应我提到的 HTTP 请求。对于静态内容,HTML 文件可以简单地存储在 IIS 中的一个虚拟文件夹中,不需要编程。对于动态内容,需要一个 web 应用来生成 HTML。进入 ASP.NET。
ASP.NET 允许你编写代码来动态创建 HTML。例如,页面可以查询数据库,并使用从数据库返回的数据填充网格。同样,HTTP POST 请求中呈现的数据可以写入数据库。此外,虽然 web 应用通常被认为是无状态的,但 ASP.NET 提供了几种在请求之间保存信息的技术。
在客户端,浏览器负责呈现内容。这些内容以 HTML 的形式提供,实质上是嵌入了格式化标签的文本。此外,级联样式表(CSS)可用于指示浏览器如何格式化内容。然而,对这些 HTML 标签和 CSS 结构的支持会因浏览器的不同而不同,这也是 web 开发的一些最大挑战。
改善网络体验
以页面为中心的方法是提高整体用户体验的主要障碍。刷新整个页面效率不是很高。为了解决这一问题,引入了两项关键改进:
- 客户端脚本
- 异步 JavaScript 和 XML (AJAX)
使用客户端脚本
现在所有的浏览器都提供了运行客户端脚本的能力,这些脚本主要是用 JavaScript 编写的,尽管其他的如 VBScript 也可以在一些浏览器中运行。在浏览器中运行脚本的能力是一个巨大的改进。例如,脚本可以根据用户输入隐藏或显示某个部分,或者修改内容的格式。因为这发生在客户端,所以没有必要往返服务器。这使得网站看起来响应速度更快。
Caution
JavaScript 可以在客户端禁用,您应该考虑并测试禁用脚本后您的页面将如何运行。
使用 AJAX
AJAX 是异步 JavaScript 和 XML 的缩写。虽然有点用词不当,因为它不一定是异步的、使用 JavaScript 或使用 XML,但该术语指的是支持客户端脚本在典型的页面刷新场景之外与 web 服务器通信的技术集合。简而言之,AJAX 使用 JavaScript 从 web 服务器请求数据。然后,它使用文档对象模型(DOM)更新页面内容。这允许根据需要更新网页的一部分,而不需要完全刷新。
AJAX 还可以用来独立于托管网页的 web 服务器调用 web 服务。您可以使用 AJAX 访问第三方提供的数据,比如股票报价或货币兑换。您还可以调用自己的 web 服务来执行实时更新或根据用户输入加载数据。例如,您可以提供产品搜索功能,并使用 AJAX 调用返回匹配产品的 web 服务。同样,这完全独立于标准的页面刷新范例。
图 1-2 展示了当今大多数网站使用的更健壮的模型。

图 1-2。
A more robust web environment
由于包含了客户端脚本和 AJAX 请求,您现在可以创建更具交互性和响应性的基于 web 的解决方案。当然,这需要更复杂的 web 应用和广泛的技术在服务器和客户机上使用。
回顾网络技术
让我们快速回顾一下在构建漂亮的交互式 web 应用时可能需要使用的各种技术。
- HTML:超文本标记语言是向浏览器传递内容的主要方式。除了显示的实际文本之外,HTML 还包含控制内容格式的嵌入标记。标签用于对齐节和表中的内容,修改文本属性,以及包含非文本内容,包括链接和图形。
- CSS:级联样式表被用作控制网页视觉方面的中心位置,比如字体、颜色、背景图像和边距。它们被称为级联,因为样式选项是在 DOM 的不同层次上定义的。您可以在一个样式表中定义站点级样式,然后根据需要提供其他样式表,以便为特定页面、部分或类进一步定义或覆盖这些样式。
- DOM:浏览器呈现的 HTML 类似于 XML 文档,文档对象模型定义了这个文档的结构。这用于以编程方式访问和修改文档内容。
- ECMAScript:客户端脚本由浏览器解释和执行。为了提高跨浏览器兼容性,ECMAScript 标准定义了脚本语言的语法和功能。JavaScript 是 ECMAScript 标准的一种方言。
Note
从历史上看,JavaScript 和 JScript 是同一种脚本语言的两种实现。微软将其实现命名为 JScript,以避免与 Sun 的商标问题,但它们本质上是相同的,并遵循不断发展的 ECMAScript 标准。随着 Internet Explorer 10 的推出,微软正在摆脱这种区分,将其脚本语言称为 JavaScript。为了保持趣味性,微软仍然提供了一种 JScript 语言,它提供了对。NET,与 JavaScript 有很大不同。在本书中,我将 JavaScript 称为符合 ECMAScript 的标准脚本语言。
探索 HTML5
那么,HTML5 在这个等式中处于什么位置呢?几乎无处不在!通常被归类为 HTML5 的实际上是一组与 web 浏览器标准化相关的广泛规范,其中许多与 HTML 无关。我将在这里简要总结一下,然后在本书的其余部分详细演示这些特性。以下是一些你应该记住的事情:
- 许多规范尚未最终确定。大部分核心规范已经完成,但一些高级功能仍有待更改。
- 浏览器对这些功能的支持会有所不同。浏览器厂商正在积极地在每个后续版本中加入新功能。
- 这些规范为每个浏览器供应商留下了决定如何实现每个特性的空间。例如,所有兼容的浏览器都将提供日期选择器控件来输入日期,但每个浏览器可能以不同的方式实现这一点。
HTML5 的总体趋势是在浏览器中提供更多的原生支持。正如你将在本书中看到的,浏览器提供了越来越多令人印象深刻的功能。这将使您能够用更少的工作构建更好的 web 应用。
审阅标记更改
正如您所料,HTML5 在标记元素中包含了一些重要的改进。有一个相当大的新标记元素列表,我将在第二章、 3 和 4 中演示其中的许多。
通用的<div>元素仍然受支持,但是也提供了新的、更加特定于上下文的元素。我将在第四章中解释和演示这一点。新的内容标签如下:
<article><aside><footer><header><hgroup><nav><section>
提供了几个新的输入类型元素,允许本地格式化和验证功能。这些将在第二章和第三章中描述。新类型如下:
colordatetime(以及datetime-local、date、time、month、week)emailnumberrangesearchtelurl
还有一些新元素使您能够使用浏览器实现的控件,如下所示:
<audio><figcaption><figure><meter><output><progress><video>
HTML5 还引入了一些其他元素,我将在后面详细描述。我将在第八章的中演示<audio>和<video>标签。新的<canvas>元素提供了一些重要的图形功能,我将在第十章中演示这一点。
理解级联样式表
像 HTML 一样,CSS 功能是由一组不断发展的规范定义的。当前发布的推荐标准是 CSS 2.1,下一个正在起草的版本称为 CSS3。然而,它被分解成 50 多个“模块”,每个模块都有单独的规范。在撰写本文时,这些模块中只有几个已经成为官方的 W3C 推荐标准(REC ),还有几个处于 W3C 候选推荐标准(CR)状态。
Tip
由于每个 CSS 模块的状态都在不断变化,有关每个模块当前状态的完整信息,请参见 www.w3.org/Style/CSS/current-work 中的文章。
因此,目前实际的 CSS3“规范”是一个移动的目标,浏览器对这些规范的支持也将有所不同。然而,已经有许多很酷的功能普遍可用,我将在第四章中演示其中的一些。
查看其他 HTML 功能
实际的脚本语法由我前面提到的 ECMAScript 规范定义。目前的版本是 5.1,发布于 2011 年 6 月。虽然它实际上不是 HTML5 规范的一部分,但符合 HTML5 的浏览器应该支持 ECMAScript 5.1 标准。然而,正如我所说的,这个规范描述了语言语法和一些内置函数,比如元素选择器。
除了语言规范之外,还有相当多的其他规范松散地包含在 HTML5 的保护伞下,它们定义了特定的客户端功能。我将在第五章中演示其中的许多,其余的将在后面的章节中介绍。新功能包括以下内容:
- 拖放:这提供了选择一个项目并将其放到网页上的另一个项目上的能力。我将在第十四章中演示这一点。
- Web 工作器:这允许您在单独的线程上执行脚本。这包括与工作人员通信的机制和在多个网页之间共享工作人员的能力。我会在第五章中解释这一点。
- Web 存储:这包括用于隔离连接到同一个站点的多个选项卡之间的会话数据的
sessionStorage,以及用于在会话关闭后在客户端上存储数据的localStorage。IndexedDB 是另一种客户端数据存储技术,我将在第十一章中演示。 - 地理定位:这不是官方规范的一部分,但在讨论 HTML5 特性时通常会包括在内。地理定位定义了一个 API,可以从 JavaScript 调用该 API 来确定当前的地理位置。浏览器如何实现这一点取决于可用的硬件。在支持 GPS 的设备上,它将使用 GPS 卫星。如果 GPS 支持不可用,它将在可能的情况下使用 Wi-Fi 来确定位置。移动设备可以使用蜂窝塔三角测量。如果所有这些都失败了,IP 地址至少可以提供一些位置的估计。显然,准确性会有很大的差异,API 会处理这一点。我将在第十二章中演示地理定位。
- Web sockets:这提供了网页(浏览器)和服务器之间的异步通信。一旦建立了连接,服务器就可以向客户端发送实时更新。这将在第十三章中演示。
选择开发工具
有几种开发环境可以用来创建利用 HTML5 特性的 ASP.NET 应用。我将在这里简要介绍它们,并在后续章节中更详细地介绍它们。需要知道的关键一点是,Visual Studio 有一些免费的替代品。
使用 Visual Studio 2015
Visual Studio 2015 是构建 ASP.NET 应用的首要开发环境。我不会在这里说太多,因为我将在本书中主要使用它来演示 HTML5 的实现。但是,如果购买 Visual Studio 的成本过高,有一些免费的替代方案仍然可以让您完成本书中的大部分练习。
Tip
对于大多数练习,您可以使用 Visual Studio 的早期版本。配置项目的一些细节会因旧版本而异,尤其是在第二章和第三章中。然而,本书中的大多数 HTML、CSS 和 JavaScript 示例也适用于任何版本的 Visual Studio。
使用微软的网络矩阵
微软的 WebMatrix 是一个轻量级集成开发环境(IDE ),专门用于构建网站。虽然不仅限于 ASP.NET 页面,但您可以构建成熟的 ASP.NET 应用。它包括 SQL Server Compact,这是基于文件的 SQL Server 版本。它还使用 IIS Express 来承载用于调试的本地网站。这与 Visual Studio 2012 中提供的托管环境相同,它取代了以前版本的 Visual Studio 中使用的 ASP.NET 开发服务器。
ASP 页面基于 ASP.NET MVC 并使用 Razor 视图引擎。因此,文件扩展名是.cshtml(如果使用 Visual Basic,则是.vbhtml)。然而,不支持带有.aspx标记文件和单独的.cs代码隐藏文件的经典 ASP 模型。您可以创建.aspx文件,但是添加代码隐藏文件并不实用。
您可以从以下网址下载并安装 WebMatrix 第 3 版: www.microsoft.com/web/webmatrix 。创建新站点时,如果使用 Starter Site 模板,它将创建一个熟悉的默认 ASP web 应用,如图 1-3 所示。

图 1-3。
The default ASP application Note
当选择 Starter 站点模板时,我在尝试下载模板时收到了一个 404 错误。我发现其他人也经历了这个错误。然而,它似乎是间歇性的,因为当我稍后再次尝试时,它工作得很好。
图 1-4 显示了 IDE。请注意。用于页面实现的扩展和 Razor 语法。

图 1-4。
The WebMatrix IDE
WebMatrix IDE 包括管理 SQL Server 数据库的能力。您可以创建新的数据库或连接到现有的 SQL Server 数据库。您可以创建和修改表格,以及查看和编辑数据。也可以运行 SQL 查询,如图 1-5 所示。

图 1-5。
WebMatrix database IDE
关于使用 WebMatrix 的更多信息,我建议从 www.microsoft.com/web/post/how-to-use-the-starter-site-template-for-aspnet-web-pages 的教程开始。
使用 Visual Studio 社区版
微软在 2014 年 11 月公布了 Visual Studio 的免费版本,名为 Community Edition。已经有了其他的免费版本,比如 Visual Studio Express for Web 但是,Community Edition 的重要之处在于它的外观和功能就像 Visual Studio Professional 的完整零售版一样。Visual Studio 速成版面向特定的技术(例如,用于 Web 或桌面)。此外,速成版没有与 Team Foundation Server 集成,并且不支持 Visual Studio 扩展。
社区版在功能上等同于专业版。它的限制主要基于谁可以使用它。一般来说,任何学术或非盈利的使用都是允许的。企业组织也可以使用它,但有一些限制。更多详情,请参见 www.visualstudio.com/en-us/products/visual-studio-community-vs 的文章。如果这些限制是一个问题,您应该考虑 Visual Studio 的一个 Express 版本,它也是免费的,但是功能有限。
可以在 https://www.visualstudio.com/en-us/downloads/visual-studio-2015-downloads-vs 下载 Visual Studio 社区版。
ASP.NET 5
ASP.NET 5 的最新版本与以前的版本有很大的不同。这篇文章, http://docs.asp.net/en/latest/conceptual-overview/aspnet.html ,很好的概述了 ASP.NET 5 中引入的变化。创建新项目时,Visual Studio 2015 为版本 5 和 4.6 提供了单独的模板,因为结构非常不同。图 1-6 显示了可用的模板。

图 1-6。
The ASP.NET project templates
本书中的大多数主题在两个版本中都同样适用。对于您将要构建的示例项目,区别仅在于项目是如何创建的,以及您需要添加和编辑哪些文件。
章节练习
本书中的练习将使用 4.6 和 5 以及 WebMatrix 应用。第二章和第三章将使用 ASP.NET 4.6,你将修改标准的 Web 表单和 MVC 应用。第四章将像第六章一样使用 WebMatrix,因为它使用已完成的第四章项目作为其起点。剩下的章节练习使用新的 ASP.NET 5 结构。第九章使用 SQL Server 和实体框架,但大部分章节只是基本的 HTML、CSS、JavaScript。如果您喜欢使用单一的项目类型,您可以调整初始步骤以满足您的需要。
项目结构
当你第一次创建一个 ASP.NET 5 项目时,你会发现文件夹结构发生了显著的变化。典型结构如图 1-7 所示。

图 1-7。
A sample project structure
wwwroot文件夹是放置静态网页文件的地方,比如 HTML、CSS、JavaScript 文件以及其他内容,包括图像、音频和视频文件。编译后的文件(如控制器、视图和 web 表单)放在其他文件夹中。您将主要处理wwwroot文件夹中的文件。
请注意,这里没有web.config文件。使用 ASP.NET 5,配置信息可以放在多个文件中,并且可以是各种格式,包括 JSON。ini 文件和环境变量。项目模板生成如图 1-7 所示的global.json和project.json等 JSON 文件。我将在第五章中进一步解释这一点。
解密浏览器对 HTML5 的支持
将应用迁移到 HTML5 的所有工作都是基于这样一个假设,即大多数浏览器都兼容 HTML5。这就要求浏览器供应商挺身而出,提供兼容 HTML5 的浏览器,并让公众普遍接受它们。这也包括移动设备,这是推动 HTML5 合规性的关键部分。普遍的共识是,每个人都在朝着这个方向快速前进。
正如我前面提到的,实际的 HTML5 规范仍在定义中。根据 HTML5 编辑伊恩·希克森的说法,在最终建议完成之前,最初的估计是 2022 年。然而,随着规范的大部分被最终确定,供应商们正在实现它们,所以在当前使用的浏览器中已经有了很多。作为 web 开发人员,我们应该把重点放在现在普遍可用的或预计很快可用的功能上,这些就是我将在本书中涉及的功能。
在 http://html5test.com 有一个非常好的网站,它提供了当前可用的和仍在开发中的浏览器的概要。每个浏览器都根据其支持的 HTML5 特性获得积分。除了允许您比较浏览器的总体分数之外,分数还按功能区域细分,因此您可以看到大多数浏览器对哪些区域的支持较好。
摘要
HTML5 涵盖了一系列广泛的技术,包括对 HTML 标记、级联样式表和客户端脚本的改进。此外,浏览器有一些重要的增强,使得提供一些优秀的 web 应用变得更加容易。虽然官方规范仍在发展中,浏览器供应商也在追赶,但已经有相当多的功能可用。此外,正如您将在接下来的几章中看到的,Visual Studio 和 ASP.NET 平台已经扩展到利用 HTML 特性集。
二、ASP.NET Web 窗体
在这一章中,我将演示 HTML5 定义的一些新的输入类型,并向你展示如何在 ASP.NET web 表单中使用它们。通常,当需要在表单上输入数据时,会使用TextBox控件。用户可以在TextBox中输入各种数据,包括字符串、数字、日期等等。为了确保数据有效,表单需要提供服务器端或客户端验证逻辑。HTML5 规范提供了几个新的输入类型,可以为您完成大部分工作,并实现更好的客户体验。
定义了以下输入类型(然而,并非所有的浏览器都支持它们):
selectcolordatetime(包括datetime-local、date、time、month、week)emailnumberrangetelurl
当您使用 ASP.NET 构建 web 表单时,发送到浏览器的实际 HTML 是由. NET 生成的。我将向您展示 ASP.NET 插入新输入类型的方式。此外,使用一些新的 HTML 元素需要一些额外的操作,所以我也将演示如何处理。
引入新的输入类型
我将从一个相当简单的例子开始,演示如何使用新的电子邮件控件结合占位符属性来快速提供客户端指令和验证。首先,您将使用 Visual Studio 模板创建一个标准的 ASP 项目,然后修改注册页面。然后,您将检查正在呈现的 HTML。
创建 ASP.NET 项目
在本章中,您将使用 Visual Studio 2015 中的标准 Web 窗体模板创建一个 ASP.NET 项目。启动 Visual Studio 2015。从起始页中,单击新建项目链接。在新建项目对话框中,选择 Web 类别,选择 ASP.NET Web 应用模板,输入章节 2 作为项目名称,选择合适的位置,如图 2-1 所示。如果选择了应用洞察,请将其关闭。

图 2-1。
Creating an ASP.NET Web Application project
Visual Studio 的早期版本提供了三种不同的创建 web 应用的方法。
- Web 表单最适合相当轻量级的网页。
- MVC 为构建更复杂的 web 应用提供了一个框架。
- Web API 主要用于创建 Web 服务。
虽然这三种技术中的一些概念是相似的,但是它们是在完全不同的堆栈上实现的。从开发人员的角度来看,一旦选择了一种方法,就不容易转换到另一种方法。此外,技能组合不容易转移到其他技术。在 MVC 6 中,微软将这三者合并成一个单一的实现。
如果您使用过 Visual Studio 的早期版本,您会注意到由此产生的一个细微差别。当选择项目类型时,您只需选择 ASP.NET 网络应用。选择使用哪种风格的应用被推迟到下一步,在那里选择模板。模板定义了在您构建新项目时为您创建的文件。与 Web 窗体应用相比,MVC 应用需要不同的文件和文件夹。
在下一个对话框中,如图 2-2 所示,选择 Web Forms 模板。请注意,其中一个可用的样式(Web Forms、MVC 或 Web API)会根据所选的模板自动选中。

图 2-2。
Choosing the Web Forms template
使用电子邮件控件
在第一个练习中,您将使用placeholder属性让用户知道该字段需要一个电子邮件地址。
EXERCISE 2-1. MODIFYING THE REGISTRATION PAGEIn the Chapter2 project, open the Register.aspx page, which you’ll find in the Account folder. There are several div elements in the fieldset node that include the input fields. The first one is for the Email field. Change this as follows by entering the attributes shown in bold:
<asp:TextBox runat="server" ID="Email" CssClass="form-control" TextMode="Email"
placeholder="use your email address" Width="200" />
<asp:RequiredFieldValidator runat="server" ControlToValidate="Email"
CssClass="text-danger" ErrorMessage="The email field is required." />
Try viewing this page with several different browsers. Notice that the email validation message looks different in each. In Firefox this will look like Figure 2-6, and in Opera it looks like Figure 2-7.

图 2-7。
The invalid e-mail message in Opera

图 2-6。
The invalid e-mail message in Firefox Close the browser and stop debugging. For this example, we used Google Chrome as the browser. If you want to use a different browser, you can select it from the drop-down list in the menu, as shown in Figure 2-5.

图 2-5。
Selecting the browser to use for debugging If you enter an invalid email address, you should see the error message shown in Figure 2-4 when you attempt to submit the page.

图 2-4。
The invalid email error message Start the application by pressing F5. Using the Chrome browser, the Register page will look like Figure 2-3. Notice the text in the Email field.

图 2-3。
The initial Register page Tip
该下拉列表自动包括当前安装的所有浏览器。您不必做任何事情来添加它们。如果安装新的浏览器,您需要重新启动 Visual Studio,它才会被包括在列表中。如果使用 Internet Explorer,浏览器将与调试器集成得更好。例如,当您关闭浏览器时,Visual Studio 将自动停止调试。然而,当测试 HTML5 支持时,除了 Internet Explorer 之外,您还需要使用其他浏览器。
使用页面检查器
使用 Internet Explorer 显示注册页面。选择工具下拉菜单,然后单击 F12 开发人员工具链接。这将允许您查看实际生成的 HTML。按 Ctrl+B 启用元素选择,然后单击电子邮件字段。这将高亮显示相关标记,如图 2-8 所示。

图 2-8。
The HTML generated for the email control Tip
大多数其他浏览器都有类似的特性,允许您检查表单内容,这通常通过它们的工具菜单来访问。
除了相当神秘的控件name和id,这是标准的 HTML5 语法。特别是,请注意以下属性;email type值和placeholder属性是 HTML5 中的新特性:
type="email"
placeholder="use your email address"
您在Register.aspx页面中输入的placeholder属性不是 ASP.NET 属性。它不是由处理的。NET,而是直接传递给生成的 HTML。
还要注意右边的窗格,它提供了几个用于查看 CSS 样式的选项卡。我们选择了 Attributes 选项卡,它显示了元素的所有属性值。其他选项卡显示应用的样式。停止调试器以关闭页面检查器。
探索其他输入类型
HTML5 引入了其他几种输入类型。为了看到他们的实际行动,您将添加一个反馈表单,其中包含一些相当做作的问题。这些将实现您可以使用的其他类型。
Tip
要获得每个输入元素的详细解释,请查看实际的 HTML5 规范。这个地址将把你带到输入元素部分: www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#the-input-element 。
实现反馈表
在下一个练习中,您将创建一个新表单并添加几个输入控件,每种类型一个。创建完表单后,我将讨论每个控件。
EXERCISE 2-2. ADDING A FEEDBACK FORMOpen the Chapter2 project in Visual Studio if not already open. In the Solution Explorer, right-click the Chapter2 project and click the Add and Webform links. Enter Feedback when prompted for the form name. This will create a new form with a single div, as shown in Listing 2-1.
清单 2-1。空白表单实现
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Feedback.aspx.cs"
Inherits="Chapter2.Feedback" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
</div>
</form>
</body>
</html>
Within the empty div, enter the code shown in Listing 2-2. This will add several fields that each demonstrate one of the new input types.
清单 2-2。添加反馈字段
<fieldset>
<legend>Feedback Form</legend>
<ol>
<li>
<asp:Label ID="lblURL" runat="server"
AssociatedControlID="URL">Default home page</asp:Label>
<asp:textbox runat="server" ID="URL" TextMode="Url"></asp:textbox>
</li>
<li>
<asp:Label ID="lblOptions" runat="server"
AssociatedControlID="Options">Default browser</asp:Label>
<asp:DropDownList ID="Options" runat="server">
<asp:ListItem Text="Internet Explorer" Value="1"></asp:ListItem>
<asp:ListItem Text="Google Chrome" Value="2" Selected></asp:ListItem>
<asp:ListItem Text="Firefox" Value="3"></asp:ListItem>
<asp:ListItem Text="Opera" Value="4"></asp:ListItem>
</asp:DropDownList>
</li>
<li>
<asp:Label ID="lblBirthday" runat="server"
AssociatedControlID="Birthday">Birthday</asp:Label>
<asp:TextBox runat="server" ID="Birthday" TextMode="Date"></asp:TextBox>
</li>
<li>
<asp:Label ID="lblMonth" runat="server"
AssociatedControlID="Month">Favorite Month</asp:Label>
<asp:TextBox runat="server" ID="Month" TextMode="Month"></asp:TextBox>
</li>
<li>
<asp:Label ID="lblWeek" runat="server"
AssociatedControlID="Week">Busiest Week</asp:Label>
<asp:TextBox runat="server" ID="Week" TextMode="Week"></asp:TextBox>
</li>
<li>
<asp:Label ID="lblStart" runat="server"
AssociatedControlID="DateTime">Start Date/Time</asp:Label>
<asp:TextBox runat="server" ID="DateTime"
TextMode="DateTimeLocal"></asp:TextBox>
</li>
<li>
<asp:Label ID="lblTime" runat="server"
AssociatedControlID="Time">Current Time</asp:Label>
<asp:TextBox runat="server" ID="Time" TextMode="Time" ></asp:TextBox>
</li>
<li>
<asp:Label ID="lblPhone" runat="server"
AssociatedControlID="Phone">Phone</asp:Label>
<asp:TextBox runat="server" ID="Phone" TextMode="Phone"></asp:TextBox>
</li>
<li>
<asp:Label ID="lblRange" runat="server"
AssociatedControlID="Range">Overall satisfaction</asp:Label>
<asp:TextBox runat="server" ID="Range" TextMode="Range"
Width="200" Height="30"></asp:TextBox>
</li>
<li>
<asp:Label ID="lblColor" runat="server"
AssociatedControlID="Color">Preferred color</asp:Label>
<asp:TextBox runat="server" ID="Color" TextMode="Color"></asp:TextBox>
</li>
<li>
<asp:Label ID="lblScore" runat="server"
AssociatedControlID="Score">Overall Rating</asp:Label>
<asp:TextBox ID="Score" runat="server" TextMode="Number"
MaxLength="1"></asp:TextBox>
</li>
<li>
<asp:Label ID="lblComments" runat="server"
AssociatedControlID="Multi">Comments</asp:Label>
<asp:TextBox runat="server" ID="Multi" TextMode="Multiline"
Rows="5" Columns="30"></asp:TextBox>
</li>
</ol>
<asp:Button ID="Submit" runat="server" CommandName="Submit" Text="Submit" />
</fieldset>
注意,我使用 Opera 浏览器来呈现反馈表单,因为在撰写本文时,它对新的输入类型提供了最好的支持。我将在这一章的后面详细解释。可以从 www.opera.com 下载歌剧。Chrome 对这些功能也有很好的支持。
Save the changes and press F5 to display the new page in the browser. Figure 2-9 shows the feedback form as rendered by the Opera browser.

图 2-9。
The initial feedback form
查看新的输入类型
现在让我们看看每一种新的输入类型,看看它们是如何在 Opera 中实现的。请记住,不同的浏览器可能会以不同的方式呈现控件。
统一资源定位器
第一个字段使用url输入类型,它需要一个有效的 web 地址。如果您输入了无效的地址,当页面被提交时,您将会看到如图 2-10 所示的验证错误

图 2-10。
The URL field Note
URL 中需要协议,如http://。例如,如果您输入 www.apress.com 并尝试提交表单,该地址将被视为无效。输入 http://www.apress.com改为。
选择列表
下一个字段提供了可用浏览器的下拉列表。在 ASP.NET,这被编码为一个包含许多元素的DropDownList。生成的 HTML 使用了一个包含option元素的select元素,如下所示:
<select name="Options" id="Options">
<option value="1">Internet Explorer</option>
<option selected="selected" value="2">Google Chrome</option>
<option value="3">Firefox</option>
<option value="4">Opera</option>
</select
注意,选中的项目用selected属性表示。这是一个布尔值,不需要值。Visual Studio 将显示一个警告,生成的标记将值设置为selected。浏览器将忽略该值,只寻找是否存在selected属性。
日期/时间字段
反馈表单包含以下日期/时间字段,展示了浏览器对各种日期类型字段的支持:
- 生日:(日期)单一日期(无时间部分)
- 最喜欢的月份:(Month)整月,包括一年
- 最忙的一周:(Week)整整一周,包括一年
- 开始日期/时间:(DateTime)包含时间部分的单个日期
- 当前时间:(Time)没有日期的时间
日期字段是文本框,您可以在其中键入所需的值,但具有内置的智能。例如,转到生日字段并键入 7,光标将自动转到日期的日部分。如果您键入 1,您将需要输入 0、1 或 2 来完成月份输入,或者只需按 Tab 移动到日期。
还有一个显示日期选择器控件的图标。该控件的不同格式(日期、月份和星期)分别如图 2-11 、 2-12 和 2-13 所示。这些控件本质上是相同的,只是月和周版本只允许您选择整个月或周。请注意,周格式还显示周数(从 1 到 52)。

图 2-13。
The date picker selecting an entire week

图 2-12。
The date picker selecting an entire month

图 2-11。
The date picker control
开始日期/时间和当前时间字段都包括一个时间控件,允许分别输入小时和分钟,如图 2-14 所示。您也可以使用向上/向下箭头来增加小时或分钟部分,这取决于当前的焦点。但是没有可以选择小时或分钟的下拉菜单。

图 2-14。
The time control Caution
在撰写本文时,ASP.NET 同时支持DateTime和DateTimeLocal文本模式。这些被翻译成 HTML 类型datetime和datetime-local。然而,datetime已经被弃用,取而代之的是datetime-local。所以,一定要在表单中使用DateTimeLocal文本模式。
电话
反馈表单包括一个使用新的tel输入类型的电话字段。在撰写本文时,没有一种桌面浏览器支持这种类型。我把它包括在练习中,希望当你读到这篇文章时,你已经有一个支持它的浏览器了。与所有不支持的类型一样,浏览器将其视为标准的TextBox控件。
范围
下一个控件使用类似于汽车中的燃油表,其中特定值不如相对值重要,例如新范围输入类型。这允许您在控件范围内滑动指示器,提供相对值,如四分之三满。我把这个定义为宽度 300,高度 30。
您可以在 HTML 中操作 range 控件的其他一些属性,但 ASP.NET 不支持这些属性。您仍然可以在。页面,它们会像placeholder属性一样被传递给生成的 HTML。然而,在本章的后面我将向你展示另一种配置范围控制的方法。
颜色
颜色控制包括一个显示选定颜色的小矩形。如果你点击这个,你可以从颜色选择器中选择一种颜色,如图 2-15 所示。

图 2-15。
The color-picker control
数字
“总体评分”字段使用数字输入类型。一些浏览器现在包括上下箭头,允许你增加和减少当前值。提交表单时,如果输入了非数字值,则会显示错误,如图 2-16 所示。

图 2-16。
A non-numeric value error
文本区域
最后一个字段使用文本区域输入类型。我指定使用 5 行 30 列。这只会影响字段在页面上的显示方式。文本存储为单个字符串。文本将被换行以适应页面上分配的大小,但它可以包含任意数量的行。
查看表单
图 2-17 显示了一个完整的表格。

图 2-17。
The completed feedback form
浏览器试图聪明地对适当字段的内容进行拼写检查。请注意,在注释字段中,拼写错误的单词带有下划线。您可以使用spellcheck属性明确地打开或关闭它。要禁用拼写检查,请在此处添加以粗体显示的代码:
<asp:TextBox runat="server" ID="Multi" TextMode="Multiline"
Rows="5" Columns="30"``spellcheck="false"
在每个字段中输入值后,单击 Submit 按钮,然后查看页面的源代码。每个字段现在都有一个value属性,其中包含提交页面时包含的值。这是服务器端代码用来存储和/或处理提交数据的内容。我提取了其中的一部分,如清单 2-3 所示。看看各种日期/时间字段值是如何格式化的。这些以粗体显示。另外,请注意,颜色是以所选 RGB 值的十六进制表示形式存储的。
Listing 2-3. The Source with Submitted Values
<li>
<label for="URL" id="lblURL">Default home page</label>
<input name="URL" type="url"``value="http://www.apress.com"
</li>
<li>
<label for="Options" id="lblOptions">Default browser</label>
<select name="Options" id="Options">
<option value="1">Internet Explorer</option>
<option value="2">Google Chrome</option>
<option value="3">Firefox</option>
<option selected="selected"``value="4"
</select>
</li>
<li>
<label for="Birthday" id="lblBirthday">Birthday</label>
<input name="Birthday" type="date"``value="1995-03-17"
</li>
<li>
<label for="Month" id="lblMonth">Favorite Month</label>
<input name="Month" type="month"``value="1999-07"
</li>
<li>
<label for="Week" id="lblWeek">Busiest Week</label>
<input name="Week" type="week"``value="2015-W14"
</li>
<li>
<label for="DateTime" id="lblStart">Start Date/Time</label>
<input name="DateTime" type="datetime-local"
value="2014-06-14T14:30" id="DateTime" />
</li>
<li>
<label for="Time" id="lblTime">Current Time</label>
<input name="Time" type="time"``value="09:52"
</li>
<li>
<label for="Phone" id="lblPhone">Phone</label>
<input name="Phone" type="tel"``value="800 555-1212"
</li>
<li>
<label for="Range" id="lblRange">Overall satisfaction</label>
<input name="Range" type="range"``value="76"
style="height:30px;width:200px;" />
</li>
<li>
<label for="Color" id="lblColor">Preferred color</label>
<input name="Color" type="color"``value="#ffbe7d"
</li>
<li>
<label for="Score" id="lblScore">Overall Rating</label>
<input name="Score" type="number"``value="8"
</li>
<li>
<label for="Multi" id="lblComments">Comments</label>
<textarea name="Multi" rows="5" cols="30" id="Multi">
This is a multi-line input box with 5 rows and 30 columns.
Notice my spellling mistake is underlined.
</textarea>
</li>.
使用 html 5 测试网站
我提到过我们在这个练习中使用 Opera。每个浏览器可以实现 HTML5 特征的不同子集。上一章提到的网站是一个非常有用的工具,可以帮助你找出哪种浏览器最适合某个特定的功能。
如果您转到“比较”选项卡,您可以选择多达五种不同的浏览器来查看每个功能的并排比较。例如,我选择了 Opera、Google Chrome、Firefox、IE 和 Safari 来查看它们的表单特性。结果显示在图 2-18 中。Opera 和 Chrome 在支持表单方面要先进得多。

图 2-18。
A side-by-side comparison of Opera, Chrome, Firefox, IE, and Safari
使用该网站的另一种方法是查看所有浏览器如何支持特定功能。在“功能”子选项卡(在“比较”选项卡中)中,您可以选择最多三个特定的功能来查看哪些浏览器支持它。我们选择了与range输入类型相关的三个特征,如图 2-19 所示。目前,Chrome 和 Opera 是唯一完全支持这些功能的桌面浏览器。

图 2-19。
Viewing each browser’s support of the range control Caution
这些比较和分析仅供演示之用。浏览器支持正以相当快的速度变化,当你读到这篇文章时,你可能会有不同的结果。但是,比较浏览器支持的方法仍然有效。
使用范围控制
范围控件支持允许您配置其行为的属性。例如,当滑块位于控件的两端时,您可以指定定义字段的value的min和max属性。您还可以指示step属性,该属性控制滑块在标尺上可以停止的位置。例如,如果min为 0,max为 100,step为 20,控制将只允许您以 20 的增量停止(例如,0、20、40、60、80 和 100)。
您可以用 HTML 编写这样的代码:
<!DOCTYPE html>
<input name="Range" type="range" id="Range"
min="0" max="200" step="20"
style="height:30px;width:200px;" />
即使 IntelliSense 不支持这些属性,您也可以在。页面,它们将被包含在最终的 HTML 中。另一种方法是在使用 JavaScript 加载页面时修改控件。
修改步骤属性
现在,您将编写一个简单的脚本来配置范围属性。
EXERCISE 2-3. MODIFYING THE RANGE CONTROLLoad the Chapter 2 project in Visual Studio and open the Feedback.aspx page. Inside the head tag, add the following script element shown in bold:
<head runat="server">
<title></title>
<script type="text/javascript">
function configureRange() {
var range = document.getElementById("Range");
range.min = 0;
range.max = 200;
range.step = 20;
}
</script>
</head>
This simple JavaScript function modifies the attributes of the range control. The document property represents the HTML document of the current page. The getElementById() function is a selector that returns the specified element, the range control in this case. (I will cover selectors in JavaScript in more detail in Chapter 5.) Now that the function has been implemented, you need to tell the page to execute it. To do that, add the following code in bold to the <body> tag:
<body``onload="configureRange()"
<form id="form1" runat="server">
This instructs the page to call the configureRange() function when the OnLoad event occurs. Save your changes and press F5 to load the page. The range control will look just like it did before, but when you move the slider, it will stop only at the preset values.
添加自定义刻度线
Opera 以前的版本会显示刻度线来帮助分级范围控制,但是现在的版本没有,Chrome 也没有。但是,您可以使用datalist标签自己添加这些内容。截至本文撰写之时,Firefox 还不支持这一特性。
EXERCISE 2-4. ADDING CUSTOM TICK MARKSAdd the following anywhere inside the fieldset tag. This defines the list of values where the tick marks should be placed.
<datalist id="ticks">
<option>0</option>
<option>20</option>
<option>40</option>
<option>60</option>
<option>80</option>
<option>100</option>
<option>120</option>
<option>140</option>
<option>160</option>
<option>180</option>
<option>200</option>
</datalist>
In the Range control, add the list attribute shown in bold. This specifies the datalist tag that defines where the tick marks should be.
<asp:Label ID="lblRange" runat="server"
AssociatedControlID="Range">Overall satisfaction</asp:Label>
<asp:TextBox runat="server" ID="Range" TextMode="Range"
Width="200" Height="30"``list="ticks"
Save your changes and press F5 to display the modified form. You should see tick marks at each step, as shown in Figure 2-20.

图 2-20。
The Range control with tick marks
显示范围值
当你在使用Range控件时,我将向你展示一个简单的技巧来显示它的值。您将在范围控件旁边添加一个TextBox控件,然后使用 JavaScript 在范围控件被修改时更新它的值。
Note
当用户移动范围控件时,Internet Explorer 11 会自动显示该控件的当前值。这是每个浏览器如何以不同方式实现功能的一个例子。
EXERCISE 2-5. DISPLAYING THE RANGE VALUEIn the Feedback.aspx page, add the following code in bold to the range item:
<li>
<asp:Label ID="lblRange" runat="server"
AssociatedControlID="Range">Overall satisfaction</asp:Label>
<asp:TextBox runat="server" ID="Range" TextMode="Range"
Width="200" Height="30" list="ticks"></asp:TextBox>
<asp:TextBox runat="server" ID="RangeValue" Width="50"></asp:TextBox>
</li>
Next, add the code in bold to the script section:
<script type="text/javascript">
function configureRange() {
var range = document.getElementById("Range");
range.min = 0;
range.max = 200;
range.step = 20;
updateRangeValue();
}
function updateRangeValue() {
document.getElementById("RangeValue").value
= document.getElementById("Range").value;
}
</script>
The updateRangeValue() function takes the current value of the Range control and stores it in the text box. Also, the configureRange() function that is called when the page is loaded calls updateRangeValue() to set its initial value. Now you’ll need to call the updateRangeValue() function whenever the range control is updated. To do that, add the code in bold to the Page_Load() event handler in the Feedback.aspx.cs code-behind file.
public partial class Feedback : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Range.Attributes.Add("onChange", "updateRangeValue()");
}
}
Save your changes and execute the page. As you move the slider, the selected value is displayed. Notice that it is updated in increments of 20 (if the step attribute is still set at 20).
摘要
在本章中,您使用 Visual Studio 提供的模板创建了一个基本的 ASP.NET web 窗体应用。在简单尝试了 email 控件之后,您创建了一个反馈页面,演示了许多其他输入类型。使用一些简单的 JavaScript,您配置了范围控件并提供了其值的实时显示。
同时,我还提供了一些关于开发环境的有用信息,包括以下内容:
- 配置浏览器进行测试
- 在 Internet Explorer 中检查页面元素
- 使用
HTML5Test.com网站研究浏览器支持
三、MVC Web 应用
在这一章中,你将使用 ASP.NET MVC 来创建一个反馈表单,展示几个新的输入类型。我将首先简要介绍。NET 平台,然后向您展示如何使用 MVC 构建一个基于 HTML5 的 web 页面。最终结果将类似于你在第二章中所做的事情,但是实现将会非常不同。正如您将看到的,该解决方案将非常依赖于扩展 MVC 框架以包含新的 HTML5 特性的能力。
模型-视图-控制器是一种架构模式,早在 20 世纪 70 年代就已经存在了。这种模式的主要好处是分离关注点,允许独立开发、测试和维护每一个关注点。模型提供了数据和业务逻辑。例如,如果应用显示产品目录,模型将提供产品细节。如果进行了更改,当控制器调用时,模型负责持久化数据。视图提供了用户体验,既格式化了数据的表示,又使用户能够与输入控件、按钮和链接进行交互。控制器处理用户请求,将其传递给模型并调用适当的视图。图 3-1 说明了这一过程。

图 3-1。
The MVC architectural pattern
介绍 ASP.NET MVC 6
ASP.NET MVC 是一个基于。NET 于 2009 年首次发布,实现了 MVC 模式。最初的版本使用相同的。传统 ASP.NETframework 中使用的 Web Forms 语法。2010 年,发布了一个名为 Razor 的新视图引擎,它以更自然、类似 HTML 的语法生成网页。此外,Razor 引擎允许代码包含在标记文件中,而不是代码隐藏文件。Visual Studio 2015 版本中包含的 MVC 版本 6 将许多实现堆栈与 Web 表单和 Web API 合并在一起。
和我在前一章讨论的传统 ASP.NET Web 表单一样,MVC6 不支持许多现成的新 HTML5 标签。然而,MVC 框架更具可扩展性,这使得添加 HTML5 支持相对容易。在这一章中,我将解释不同的技术来扩展 MVC 框架以包含新的 HTML5 特性。还有几个开源扩展可以安装,我也将简要演示其中的一个。
创建 ASP.NET MVC 项目
在本章中,你将使用 Visual Studio 2015 中的标准模板创建一个 ASP.NET MVC 项目。启动 Visual Studio 2015。在 Web 类别中,选择 ASP.NET Web 应用模板,输入第三章作为项目名称,选择合适的位置,如图 3-2 所示。单击“确定”按钮继续。

图 3-2。
Selecting the ASP.NET Web project
这与您在上一章中创建项目的方式相同。但是,在下一个对话框中,选择 MVC 模板,如图 3-3 所示。这将创建一个 web 应用,看起来就像《??》第二章中的项目,但是它是使用 MVC 风格实现的。

图 3-3。
Selecting the MVC template
创建项目后,您将在解决方案资源管理器中看到许多文件夹。注意,控制器、模型和视图都有单独的文件夹,如图 3-4 所示。该示例项目包括这些项目中每一项的几个示例。

图 3-4。
The initial Solution Explorer window
探索剃刀视角
为了快速演示 Razor 视图语法,您可以查看项目模板提供的现有视图。打开Register.cshtml文件,您可以在Views\Account文件夹中找到它。这实现了注册页面的视图。清单 3-1 显示了页面的主要部分。
Listing 3-1. The Initial Register.cshtml Implementation
<h4>Create a new account.</h4>
<hr />
@Html.ValidationSummary("", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.PasswordFor(m => m.Password, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Register" />
</div>
</div>
在 Razor 语法中,@表示后面的文本是代码而不是文字标记。代码将在运行时生成 HTML 内容。您会注意到大部分代码都使用了Html类。这是一个帮助器类,具有生成 HTML 标记的方法。例如,LabelFor()方法生成标记来插入一个Label控件。
对于表单中的每个字段,代码使用了Html助手类的LabelFor()和TextBoxFor()方法。(密码字段使用PasswordFor()方法。)这些方法中的每一个都采用一个 lambda 表达式(例如,m => m.Email)来指定相关模型中的数据元素。用于视图的模型由文件顶部的以下指令定义:
@model Chapter``3
如果你看一下AccountViewModels.cs文件,你会发现RegisterViewModel类的定义。这个类有三个公共属性。
EmailPasswordConfirmPassword
这些属性中的每一个都有一些元数据属性,比如用于生成正确的 HTML 的Required和DataType。我将在本章后面进一步解释这一点。
使用编辑器模板
TextBoxFor()方法将输出一个标准的TextBox控件。要使用新的 HTML5 输入类型,您需要修改这个实现。MVC 框架允许你使用EditorFor()方法来代替TextBoxFor()。这本身不会改变生成的标记,因为默认的EditorFor()实现仍然使用type="text"属性。我将向您展示如何创建一个编辑器模板来覆盖这个默认行为。
EXERCISE 3-1. ADDING AN EDITOR TEMPLATEOpen the Register.cshtml file, which you’ll find in the Views\Account folder. For the Email field, replace TextBoxFor with EditorFor. The code will look like this: <div class="form-group"> @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) <div class="col-md-10"> @Html. EditorFor (m => m.Email, new { @class = "form-control" }) </div> </div> In the Solution Explorer, right-click the Views\Shared folder and choose Add and New Folder. Enter EditorTemplates for the folder name.
注意在本章的后面,我将解释如何为每个属性选择合适的编辑器模板。编辑器模板必须在EditorTemplates文件夹中,MVC 框架才能使用它们。因为这个文件夹被添加到了Views\Shared文件夹中,所以模板对项目中的所有视图都是可用的。你可以在Views\Account文件夹中创建EditorTemplates文件夹。这将使它们对Account文件夹中的所有视图可用,但对其他文件夹(如Home文件夹)不可用。如果您希望Home模板不同于Account模板,这也允许您为每个文件夹创建一组单独的编辑器模板。如果在两个文件夹中有相同的名称,Home或Account文件夹中的名称将覆盖Shared版本。
Right-click the Views\Shared\EditorTemplates folder and choose Add and View links. In the Add View dialog box, enter EmailAddress as the view name and make sure all the check boxes are unselected, as shown in Figure 3-5. Click the Add button to create the template.

图 3-5。
Adding the EmailAddress template This will generate a view page named EmailAddress.cshtml. Delete the entire content and replace it with the following code. This uses the TextBox() method but specifies some additional attributes including type and placeholder. @Html.TextBox("", null, new { @class = "text-box single-line", type = "email", placeholder = "Enter an e-mail address" }) Save your changes and debug the application. By default, the debugger will try to display the page you have open. Open the Register.cshtml file before pressing F5, and that page will be opened in the browser. Go to the Registration page, and you should see the placeholder text displayed in the empty Email field, as shown in Figure 3-6.

图 3-6。
The blank register form If you look at the page’s source or the Page Inspector, the actual HTML will look similar to this: <input name="Email" class="text-box single-line" id="Email" type="email" placeholder="Enter an e-mail address" value="" data-val-required="The Email field is required." data-val-email="The Email field is not a valid e-mail address." data-val="true" > Close the browser and stop the debugger. Tip
和前一章一样,我将使用 Opera 浏览器进行大部分练习,因为它对新的输入类型有最好的支持。
注意生成的标记中的data-val标签。它们用于控制客户端验证逻辑。
ATTRIBUTE DRIVEN VALIDATION
ASP.NET MVC 中的数据验证从模型开始。如果您查看AccountViewModel.cs文件,您会看到附加到每个属性的元数据属性,比如Required。例如,Email属性如下所示:
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
TextBoxFor()助手函数使用元数据属性生成 HTML,就像您看到的电子邮件字段一样。具体来说,生成了data-val和data-val-required HTML 属性。该视图还包括以下 jQuery 库:
<script src="∼/Scripts/jquery.validate.js"></script>
<script src="∼/Scripts/jquery.validate.unobtrusive.js"></script>
这些 JavaScript 库使用 HTML 属性如data-val来执行客户端验证。更多信息,请参见 www.datahaunting.com/mvc/client-and-server-side-validation-using-dataannotation-in-mvc 一文。
添加反馈页面
现在,您将创建一个反馈表单,并使用它来演示如何实现新的 HTML5 功能。您将首先创建一个模型,然后基于该模型实现一个强类型视图。然后,您将添加一个控制器动作以及一个到新页面的链接。
Tip
向 web 应用添加页面通常涉及添加模型、添加视图以及创建或修改控制器。MVC 模式允许分别开发这些视图和模型,在大型项目中,通常会有不同的人负责视图和模型。您可以使用现有的模型。然而,在像这样的小项目中,你是唯一的开发者,你通常需要接触所有三个区域来添加一个页面。
创建反馈模型
模型定义了可以包含在页面中的数据元素。通过首先设计模型,您可以简化视图实现。
在解决方案资源管理器中,右键单击Models文件夹,选择 Add and Class,并输入 FeedbackModel.cs 作为类名。单击“确定”按钮创建该类。对于类的实现,输入清单 3-2 中所示的代码。
Listing 3-2. The FeedbackModel Class
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
namespace Chapter``3
{
public class FeedbackModel
{
[Display(Name = "Name", Prompt = "Enter your full name"),
Required]
public string Name { get; set; }
[Display(Name = "Average Score", Prompt = "Your average score"),
Range(1.0, 100.0),
Required]
public decimal Score { get; set; }
[Display(Name = "Birthday"),
DataType(DataType.Date)]
public DateTime? Birthday { get; set; }
[Display(Name = "Home page", Prompt = "Personal home page"),
DataType(DataType.Url),
Required]
public string Homepage { get; set; }
[Display(Name = "Email", Prompt = "Preferred e-mail address"),
DataType(DataType.EmailAddress),
Required]
public string Email { get; set; }
[Display(Name = "Phone number", Prompt = "Contact phone number"),
DataType(DataType.PhoneNumber),
Required]
public string Phone { get; set; }
[Display(Name = "Overall Satisfaction")]
public string Satisfaction { get; set; }
}
}
Note
视图文件使用 Razor 语法并有.cshtml(或。vbhtml)分机。然而,模型和控制器文件是标准的 C#(或 VB)类。
重新构建应用。这将使模型在定义视图时可用。
定义反馈视图
现在您将基于这个模型定义一个新的视图。最初,这将是一个具有单个字段的简单表单。然后,您将在主页上添加一个链接和一个控制器操作来处理这个问题。在本章的后面,您将向表单添加更多的字段。
EXERCISE 3-2. DESIGNING THE INITIAL FEEDBACK FORMIn the Solution Explorer, expand the Views folder. Right-click the Home folder and choose Add and View. Enter the name Feedback, select the Empty template, and select the FeedbackModel, as shown in Figure 3-7. Click the Add button to create the view.

图 3-7。
Creating the Feedback view The new view is generated with a single empty div inside the body tag. Enter the code shown in bold in Listing 3-3. This code includes an input control for the Email property using the EditorFor() method.
清单 3-3。定义初始形式
@model Chapter``3
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Feedback</title>
</head>
<body>
<div>
@using (Html.BeginForm((string)ViewBag.FormAction, "Home"))
{
<fieldset>
<legend>Feedback Form</legend>
<div>
@Html.EditorFor(m => m.Email)
</div>
<p>
<input type="submit" value="Submit" />
</p>
</fieldset>
}
</div>
</body>
</html>
Views are invoked by a controller, so you’ll need to add a controller action that will load this page. Open the HomeController.cs class, which you’ll find in the Controllers folder. Add the following method: public ActionResult Feedback() { return View(); } Finally, you’ll need a link that triggers this controller action. Open _Layout.cshtml in the View\Shared folder. Add the line shown in bold: <ul id="menu"> <li>@Html.ActionLink("Home", "Index", "Home")</li> <li>@Html.ActionLink("About", "About", "Home")</li> <li>@Html.ActionLink("Contact", "Contact", "Home")</li> <li>@Html.ActionLink("Feedback", "Feedback", "Home")</li> </ul> Save your changes and press F5 to debug. You should now have a Feedback link on the home page, as shown in Figure 3-8.

图 3-8。
The Feedback link on the home page Click this link to display the feedback form, which is shown in Figure 3-9.

图 3-9。
The initial feedback form Enter an invalid email address and click the Submit button. You should see the standard HTML5 validation error, as shown in Figure 3-10.

图 3-10。
The standard HTML5 validation error View the source of the feedback form, which should be similar to this: <form action="/Home/Feedback" method="post"> <fieldset> <legend>Feedback Form</legend> <div> <input class="text-box single-line" data-val="true" data-val-required="The Email field is required." id="Email" name="Email" placeholder="Enter an e-mail address" type="email" value="" /> </div> <p> <input type="submit" value="Submit" /> </p> </fieldset> </form>
填写反馈表
现在,您将把剩余的字段添加到反馈表单中。您还需要为其他数据类型提供编辑器模板。我将向您展示框架如何决定使用哪个模板。
添加其他字段
您将从添加在FeedbackModel.cs类中定义的其他字段开始。对于每一个,您将包含一个标签,并使用EditorFor()方法来生成输入字段。
EXERCISE 3-3. COMPLETING THE FEEDBACK FORMOpen the Feedback.cshtml file and add the code shown in bold in Listing 3-4.
清单 3-4。反馈视图实现
<div>
@Html.EditorFor(m => m.Email)
</div>
<div class="editor-label">
@Html.LabelFor(m => m.Name)
</div>
<div class="editor-field">
@Html.EditorFor(m => m.Name)
</div>
<div class="editor-label">
@Html.LabelFor(m => m.Birthday)
</div>
<div class="editor-field">
@Html.EditorFor(m => m.Birthday)
</div>
<div class="editor-label">
@Html.LabelFor(m => m.Homepage)
</div>
<div class="editor-field">
@Html.EditorFor(m => m.Homepage)
</div>
<div class="editor-label">
@Html.LabelFor(m => m.Phone)
</div>
<div class="editor-field">
@Html.EditorFor(m => m.Phone)
</div>
<div class="editor-label">
@Html.LabelFor(m => m.Score)
</div>
<div class="editor-field">
@Html.EditorFor(m => m.Score)
</div>
<div class="editor-label">
@Html.LabelFor(m => m.Satisfaction)
</div>
<div class="editor-field">
@Html.EditorFor(m => m.Satisfaction)
</div>
<p>
<input type="submit" value="Submit" />
</p>
Save your changes and press F5 to view the modified form. Click the Feedback link to display the page, which will look similar to Figure 3-11.

图 3-11。
The feedback form Notice that all the new fields, except Birthday, use the standard TextBox control and do not include a placeholder text. This is because there is no editor template defined for these data types. The Birthday property was defined in the model as a DateTime value, and the implementation of the Textbox control uses this placeholder for dates.
添加编辑器模板
你可能一直在问自己,框架如何知道使用哪个编辑器模板?框架会根据属性的数据类型尝试使用正确的模板。这不太可靠,因为电子邮件、URL 和电话号码都存储在一个string变量中。首选方法是使用元数据来定义它。
如果在模型类中包含了System.ComponentModel.DataAnnotations名称空间,那么就可以在模型中包含元数据。有两个元数据属性用于确定适当的模板。
DataTypeUIHint
使用DataType枚举来指定DataType属性。这包括一组相当大但固定的值,比如上下文类型EmailAddress、CreditCard、Currency、PostalCode和Url。如果您添加了一个DataType属性,将使用具有匹配名称的编辑器模板。当您实现FeedbackModel时,您包含了DataType属性。
UIHint属性是用一个字符串指定的,因此您可以使用任何想要的值。如果您希望一个属性以绿色字体显示,您可以在模型中指定UIHint("GreenFont")属性,然后提供一个GreenFont.cshtml模板。在确定要使用的合适模板时,UIHint优先于DataType属性。
Tip
我的GreenFont示例用来说明UIHint属性是如何工作的。您不应该使用它来设置样式属性,因为这是样式表的作用。当你实现一个range控件时,一个更合适的UIHint属性的应用将在本章后面演示。
Right-click the Views\Shared\EditorTemplates folder and choose Add and View. In the Add View dialog box, enter the name Date and unselect all of the check boxes. Replace the view implementation with the following code: @Html.TextBox("", null, new { @class = "text-box single-line", type = "date" }) In the same way, add another editor template named Url and use the following implementation: @Html.TextBox("", null, new { @class = "text-box single-line", type = "url", placeholder = "Enter a web address" }) Create a PhoneNumber template using the following code: @Html.TextBox("", null, new { @class = "text-box single-line", type = "tel", placeholder = "Enter a phone number" }) Create a Number template using the following code (you will be using this in a later exercise): @Html.TextBox("", null, new { @class = "text-box single-line", type = "number", placeholder = "Enter a number" }) Save your changes and press F5 to debug your application. The feedback form should now use the HTML5 controls, as shown in Figure 3-12.

图 3-12。
The form using HTML5 controls
生成自定义 HTML
您实现的编辑器模板都基于Html助手类的TextBox()方法。模板只是添加了一些额外的属性,比如type和placeholder。但是,您可以实现输出任何您想要的 HTML 内容的模板。为了演示这一点,我将向您展示如何构建自己的助手扩展,从头开始生成标记。你将用它来替换EmailAddress模板。
添加自定义助手类
您可以创建自己的助手类,并将其添加为现有Html助手类的属性。然后,您可以按如下方式访问您的自定义方法:
@Html.<CustomClass>.<CustomMethod>()
EXERCISE 3-4. CREATING A HELPER EXTENSIONIn the Solution Explorer, right-click the Chapter3 project and choose Add and Class links. Enter the name Html5.cs when prompted for the class name. Enter the source shown in Listing 3-5.
清单 3-5。初始 HTML5 助手类
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Globalization;
namespace System.Web.Mvc
{
public class Html5Helper
{
private readonly HtmlHelper htmlHelper;
public Html5Helper(HtmlHelper htmlHelper)
{
this.htmlHelper = htmlHelper;
}
private static CultureInfo Culture
{
get
{
return CultureInfo.CurrentCulture;
}
}
// Add custom methods here...
}
public static class HtmlHelperExtension
{
public static Html5Helper Html5(this HtmlHelper instance)
{
return new Html5Helper(instance);
}
}
}
这里有几件事需要指出。首先,注意名称空间被设置为System.Web.Mvc,而不是你的应用的名称空间Chapter 3 。您的自定义助手类被命名为Html5Helper,它的构造函数带有一个HtmlHelper参数。这是对标准助手类的引用,该类存储为私有类成员。您的定制方法将需要它来从框架中访问数据,如视图和模型信息。最后,这段代码还声明了一个静态的HtmlHelperExtension类,它提供了一个静态方法来返回您的自定义类。注意,方法名是Html5,所以您将从视图中访问您的定制类,如下所示:
@Html.Html5().<CustomMethod>()
拥有自己的自定义助手类的目的是为了能够实现自定义助手方法。所以,现在再加一个。第一种方法将生成一个电子邮件输入控件。然后您将在您的EmailAddress.cshtml模板中使用它。
Add the code shown in Listing 3-6 to your custom class where the // Add custom methods here placeholder is.
清单 3-6。EmailControl 实现
public IHtmlString EmailControl()
{
string id;
string name;
string placeHolder;
string value;
string valueAttribute;
ViewDataDictionary viewData = htmlHelper.ViewData;
ModelMetadata metaData = viewData.ModelMetadata;
// Build the HTML attributes
id = viewData.TemplateInfo.GetFullHtmlFieldId(string.Empty);
name = viewData.TemplateInfo.GetFullHtmlFieldName(string.Empty);
if (string.IsNullOrWhiteSpace(metaData.Watermark))
placeHolder = string.Empty;
else
placeHolder = "placeholder=\"" + metaData.Watermark + "\"";
value = viewData.TemplateInfo.FormattedModelValue.ToString();
if (string.IsNullOrWhiteSpace(value))
valueAttribute = string.Empty;
else
valueAttribute = "value=\"" + value + "\"";
// Determine the css class
string css = "text-box single-line";
ModelState state;
if (viewData.ModelState.TryGetValue(name, out state)
&& (state.Errors.Count > 0))
css += " " + HtmlHelper.ValidationInputCssClassName;
// Format the final HTML
string markup = string.Format(Culture,
"<input type=\"email\" id=\"{0}\" name=\"{1}\" {2} {3} " +
"class=\"{4}\"/>", id, name, placeHolder, valueAttribute, css);
return MvcHtmlString.Create(markup);
}
这个方法收集各种 HTML 属性,比如id、name、class和placeholder。该信息是从模型或模型元数据中提取的。在这个方法的最后,使用标准的string.Format()方法构建了markup字符串,该方法集合了各种属性。然后将它传递给静态的MvcHtmlString.Create()方法,以提供 MVC 框架所需的IHtmlString接口。
这个EmailAddress模板实现的主要区别是占位符属性是使用模型元数据设置的。前面的实现使用了硬编码的占位符“输入电子邮件地址”不幸的是,财产名称完全不一致。在模型中,这是使用Prompt属性(Prompt = "Preferred e-mail address")指定的。在ModelMetadata类中,这个值作为Watermark属性提供。当然,这作为一个placeholder属性包含在 HTML 文档中。
重新实现自定义电子邮件模板
现在您将使用一个更简单的模板替换EmailAddress模板,该模板使用您刚刚实现的新助手扩展。
EXERCISE 3-5. RE-IMPLEMENTING THE E-MAIL TEMPLATESave the changes and open the EmailAddress.cshtml template. Replace the entire implementation with the following: @Html.Html5().EmailControl() Save the changes and press F5 to debug. The placeholder text should now reflect the prompt specified in the model metadata, as demonstrated in Figure 3-13.

图 3-13。
The modified Email field View the source of this page, and the HTML markup for the Email field should look like this: <input type="email" id="Email" name="Email" placeholder="Preferred e-mail address" class="text-box single-line">
实现范围控制
正如你在前一章看到的,range控件支持一些标准TextBoxFor(甚至是EditorFor)实现中没有的附加属性。为了使用 MVC 框架实现这一点,您将实现一个定制的 helper 方法。然后,您将提供一个调用这个自定义方法的编辑器模板。最后,您将在模型元数据中添加一个UIHint属性,告诉框架使用新的模板。
实现自定义帮助器方法
第一步是创建一个定制的助手方法,它将为一个range控件生成适当的标记。这将类似于您刚刚实现的EmailControl()方法,除了它不包括placeholder属性。此外,min、max和step属性被传递给该方法。
将清单 3-7 中的代码添加到Html5.cs文件中(在Html5Helper类中)。
Listing 3-7. The RangeControl Implementation
public IHtmlString RangeControl(int min, int max, int step)
{
string id;
string name;
string value;
string valueAttribute;
ViewDataDictionary viewData = htmlHelper.ViewData;
// Build the HTML attributes
id = viewData.TemplateInfo.GetFullHtmlFieldId(string.Empty);
name = viewData.TemplateInfo.GetFullHtmlFieldName(string.Empty);
value = viewData.TemplateInfo.FormattedModelValue.ToString();
if (string.IsNullOrWhiteSpace(value))
valueAttribute = string.Empty;
else
valueAttribute = "value=\"" + value + "\"";
// Determine the css class
string css = "range";
ModelState state;
if (viewData.ModelState.TryGetValue(name, out state)
&& (state.Errors.Count > 0))
css += " " + HtmlHelper.ValidationInputCssClassName;
// Format the final HTML
string markup = string.Format(Culture,
"<input type=\"range\" id=\"{0}\" name=\"{1}\" " +
"min=\"{2}\" max=\"{3}\" step=\"{4}\" {5} class=\"{6}\"/>",
id, name, min.ToString(), max.ToString(), step.ToString(),
valueAttribute, css);
return MvcHtmlString.Create(markup);
}
添加范围模板
现在您需要为range控件创建一个编辑器模板,它将使用这个新的定制方法。
EXERCISE 3-6. ADDING A RANGE TEMPLATERight-click the Views\Shared\EditorTemplates folder and choose Add and View. In the Add View dialog box, enter the name Range and unselect all of the text boxes. Replace the default implementation with the following: @Html.Html5().RangeControl(0, 200, 20) Open the FeedbackModel.cs file and add the UIHint attribute to the Satisfaction property like this: [Display(Name = "Overall Satisfaction") , UIHint("Range") ] public string Satisfaction { get; set; } While you have the FeedbackModel.cs file open, add a UIHint attribute for the Score property as follows: [Display(Name = "Average Score", Prompt = "Your average score"), Range(1.0, 100.0), UIHint("Number"), Required] public decimal Score { get; set; } Save your changes and press F5 to debug. Go to the Feedback page; the page should look like Figure 3-14.

图 3-14。
The updated score and range control
使用开源扩展
到目前为止,您已经创建了两个基于定制助手方法的编辑器模板和四个基于TextBox()方法的简单模板。然而,除了这些,你可能还需要一些其他的模板。在你花时间去实现它们之前,你可能想知道是否有人已经为你做了这些。嗯,答案是肯定的。
有许多第三方库和工具可供您使用。Visual Studio 提供了一个名为 NuGet 的包管理器,可以很容易地找到、下载、安装和管理这些第三方包。我将向您展示如何使用 NuGet 来安装一个编辑器模板包,这样您就不必自己编写它们了。当然,现在你已经知道如何写你自己的了,如果其中的任何一个不像你希望的那样工作,你可以自由地这样做。
EXERCISE 3-7. INSTALLING EDITOR TEMPLATESWhen the third-party package is installed, it will prompt you before overwriting any existing templates. So before you begin, you should delete the existing editor templates. Delete all of the files in the EditorTemplates folder except for Range.cshtml (the third-party package does not include this template). In Visual Studio, with the Chapter3 project still open, choose Tools and NuGet Package Manager and Manage NuGet Packages for Solution. This will display the Manage NuGet Packages dialog box. If you select Installed in the Filter drop-down, it will list the packages currently installed. You might be surprised to find that quite a few have already been installed by the project template. The blue icon to the right of each package indicates whether there is an update available for it, as demonstrated in Figure 3-15.

图 3-15。
Listing the installed packages Change the Filter drop-down to All, and enter html5 editor templates in the search field. Select the package named Html5EditorTemplates, as shown in Figure 3-16. The pane on the right displays details of this package including author, description, and links for more information.

图 3-16。
Selecting the Html5EditorTemplates package Click the Install button. Once the install has completed, you should now see quite a few templates in the EditorTemplates folder. Open the EmailAddress.cshtml file. Listing 3-8 shows the third-party implementation for this template. While this is implemented differently from yours, it accomplishes basically the same thing, including getting the placeholder from the metadata.
清单 3-8。开源电子邮件模板
@{
var attributes = new Dictionary<string, object>();
attributes.Add("type", "email");
attributes.Add("class", "text-box single-line");
attributes.Add("placeholder", ViewData.ModelMetadata.Watermark);
//since this is a constraint, IsRequired and other constraints
//won't necessarily apply in the browser, but in case script
//turns off readonly we want the constraints passed
if (ViewData.ModelMetadata.IsReadOnly)
{
attributes.Add("readonly", "readonly");
}
if (ViewData.ModelMetadata.IsRequired)
{
attributes.Add("required", "required");
}
}
@Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue, attributes)
Press F5 to debug the Feedback page, which should look like Figure 3-17.

图 3-17。
The feedback page using third-party templates
添加文本 HTML
使用Html助手类,包括EditorFor()方法,是用 ASP.NET MVC 实现表单的推荐方法。这提供了与模型的紧密集成,包括模型元数据和关注点的分离(业务规则和用户体验)。但是,您总是可以在视图中嵌入实际的 HTML 标记。一个合适的用法是包含静态内容或没有连接到模型的控件,比如进度条。
我现在将演示三个例子,每个例子都使用直接 HTML 标记将一个新的 HTML5 控件插入到反馈表单中。
- 范围
- 进步
- 米
添加范围控件
您已经使用自定义编辑器模板包含了一个range控件。现在,您将通过简单地添加适当的 HTML 标记来插入另一个。出于好玩,您将通过将变换设置为旋转 90 度,使其成为垂直滑块。为此,将清单 3-9 中粗体显示的代码添加到Feedback.cshtml视图中。
Caution
如果高度大于宽度,Opera 以前的版本会垂直呈现一个range控件。当前版本(在撰写本文时)没有做到这一点。在如何实现这一点上,浏览器实现之间似乎没有什么共识。我发现使用transform属性是实现这一点的最一致的方式。我将在第四章的中更详细地解释转换。
Listing 3-9. Adding a range Control in HTML
<fieldset>
. . .
<div class="editor-label">
@Html.LabelFor(m => m.Satisfaction)
</div>
<div class="editor-field">
@Html.EditorFor(m => m.Satisfaction)
</div>
<div>
Custom range
<input type="range" id="CustomRange" name="CustomRange"
class="range"
style="width: 100px; height: 30px; transform: rotate(90deg)"
min="0" max="200" step="20" />
</div>
<p>
<input type="submit" value="Submit" />
</p>
</fieldset>
保存您的更改,然后按 F5 进行调试。该表格应如图 3-18 所示。

图 3-18。
Adding a vertical range control Note
该控件的值不是模型的一部分,不会与表单一起保存。如果控件仅用于帮助用户体验,并且不需要保持,这是合适的。例如,它可以控制视频或音频剪辑的音量。
添加进度条
接下来,您将通过在表单中插入一个progress标签来添加一个进度条。在提交按钮后添加以下粗体代码:
<p>
<input type="submit" value="Submit" />
</p>
<div>
<progress id="FormProgress" value="60" max="100">
<strong>Progress: 60%</strong>
</progress>
</div>:
</fieldset>
进度标签不支持min属性,只支持max属性。最小值被假定为零。value属性指定了当前的进度。按 F5 调试应用并导航到反馈表单。进度应如图 3-19 所示。

图 3-19。
The progress control in Opera
标签内的内容用于不支持标签的浏览器。例如,在 IE9 中,表单看起来如图 3-20 所示。

图 3-20。
The progress control in IE9
更新进度条
但是,一个静态的进度条不是很有趣;人们甚至会发现一个进度条,它永远不会变得令人沮丧。现在,您将添加一些 JavaScript 代码,以便在表单上的字段被输入时更新进度条。
首先,您将创建一个名为calculateProgress()的函数,它遍历所有输入字段,查看哪些字段有值。有六个字段,因此您将为每个字段赋值 17 (6 × 17 = 102)。将该值设置为任何大于 100 的值都会显示为 100%完成。这段代码使用了document.getElementsByClassName()选择器,它返回具有指定 class 属性的所有元素。在这种情况下,您需要具有text=box single-line类的元素。然后,该函数使用计算出的值更新进度条的值。
然后,每当输入字段发生变化时,您都需要调用这个函数。为此,您将创建一个名为bindEvents()的函数,并使用同一个getElementsByClassName()选择器。这一次,您将使用addEventListener()函数将calculateProgress()函数绑定到onChange事件。最后,您将调用onLoad事件处理程序中的bindEvents()函数。
将列表 3-10 中的粗体代码输入反馈表。
Listing 3-10. Adding JavaScript to Update the Progress Bar
<head>
<meta name="viewport" content="width=device-width" />
<title>Feedback</title>
<script type="text/javascript">
function calculateProgress() {
var value = 0;
var fieldList = document.getElementsByClassName("text-box single-line");
for (var i = 0; i < fieldList.length; i++) {
if (fieldList[i].value > "")
value += 17;
}
if (value > 100)
value = 100;
var progress = document.getElementById("FormProgress");
progress.value = value;
};
function bindEvents() {
var fieldList = document.getElementsByClassName("text-box single-line");
for (var i = 0; i < fieldList.length; i++) {
fieldList[i].addEventListener("change", calculateProgress, false);
}
}
</script>
</head>
<body``onload="bindEvents();"
Note
在计算进度时,这段代码忽略了用于Satisfaction字段的range控件以及总分数。这样做是因为这些控件总是有一个值,所以你不能告诉一个值是什么时候“输入”的
此外,将progress标签的初始值属性从 60 更改为 0,如下所示:
<progress id="FormProgress" value=``"0"
按 F5 调试应用。当您在输入字段中输入值时,请注意进度条会自动更新,如图 3-21 所示。

图 3-21。
The progress and range controls in Chrome Tip
正如我提到的,当浏览器不支持progress控件时,会显示progress标签中的文本。您可以用 JavaScript 动态更新它;但是,如果不支持,您也可以将其留空,不显示进度。
使用仪表控制
对于最后一个示例,您将添加一个仪表控件,它类似于进度条。血糖仪允许您在启用状态指示器颜色编码的范围内定义间隔。例如,考虑汽车上的油压表。“正常”范围显示在仪表上,低值或高值突出显示。我不需要知道油压是多少,甚至不需要知道油压应该是多少;我只想知道是否在正常范围内。
与range控件一样,meter控件支持min和max属性以及当前的value。它还提供了定义正常范围的low、high和optimum属性。以粗体输入以下代码:
<div>
<progress id="FormProgress" value="0" max="100">
<strong>Progress: 60%</strong>
</progress>
</div>
<div>
<meter id="Meter" value="50" min="20" max="120"
low="50" high="100" optimum="75">
<strong>Meter:</strong>
</meter>
</div>
</fieldset>
为了演示不同的值是如何显示的,您将添加一些 JavaScript 代码,每秒钟用一个随机值更新控件。为此,将以下粗体代码添加到bindEvents()函数中:
function bindEvents() {
var fieldList = document.getElementsByClassName("text-box single-line");
for (var i = 0; i < fieldList.length; i++) {
fieldList[i].addEventListener("change", calculateProgress, false);
}
setInterval(function () {
var meter = document.getElementById("Meter");
meter.value = meter.min + Math.random() * (meter.max - meter.min);
}, 1000);
}
这段代码使用了setInterval()函数,所以每隔 1000 毫秒就调用一次匿名函数。按 F5 启动应用。根据数值的不同,颜色会由绿色变为黄色,如图 3-22 所示。

图 3-22。
The meter control
摘要
如果你已经迷失在各种更新中,清单 3-11 展示了Feedback.cshtml视图的完整实现。
Listing 3-11. The Final Feedback.cshtml Implementation
@model Chapter``3
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Feedback</title>
<script type="text/JavaScript">
function calculateProgress() {
var value = 0;
var fieldList = document.getElementsByClassName("text-box single-line");
for (var i = 0; i < fieldList.length; i++) {
if (fieldList[i].value > "")
value += 17;
}
if (value > 100)
value = 100;
var progress = document.getElementById("FormProgress");
progress.value = value;
};
function bindEvents() {
var fieldList = document.getElementsByClassName("text-box single-line");
for (var i = 0; i < fieldList.length; i++) {
fieldList[i].addEventListener("change", calculateProgress, false);
}
setInterval(function () {
var meter = document.getElementById("Meter");
meter.value = meter.min + Math.random() * (meter.max - meter.min);
}, 1000);
}
</script>
</head>
<body onload="bindEvents();">
<div>
@using (Html.BeginForm((string)ViewBag.FormAction, "Home"))
{
<fieldset>
<legend>Feedback Form</legend>
<div>
@Html.EditorFor(m => m.Email)
</div>
<div class="editor-label">
@Html.LabelFor(m => m.Name)
</div>
<div class="editor-field">
@Html.EditorFor(m => m.Name)
</div>
<div class="editor-label">
@Html.LabelFor(m => m.Birthday)
</div>
<div class="editor-field">
@Html.EditorFor(m => m.Birthday)
</div>
<div class="editor-label">
@Html.LabelFor(m => m.Homepage)
</div>
<div class="editor-field">
@Html.EditorFor(m => m.Homepage)
</div>
<div class="editor-label">
@Html.LabelFor(m => m.Phone)
</div>
<div class="editor-field">
@Html.EditorFor(m => m.Phone)
</div>
<div class="editor-label">
@Html.LabelFor(m => m.Score)
</div>
<div class="editor-field">
@Html.EditorFor(m => m.Score)
</div>
<div class="editor-label">
@Html.LabelFor(m => m.Satisfaction)
</div>
<div class="editor-field">
@Html.EditorFor(m => m.Satisfaction)
</div>
<div>
Custom range
<input type="range" id="CustomRange" name="CustomRange"
class="range vertical"
style="width: 100px; height: 30px; transform: rotate(90deg)"
min="0" max="200" step="20" />
</div>
<p>
<input type="submit" value="Submit" />
</p>
<div>
<progress id="FormProgress" value="0" max="100">
<strong>Progress: 0%</strong>
</progress>
</div>
<div>
<meter id="Meter" value="50" min="20" max="120"
low="50" high="100" optimum="75">
<strong>Meter:</strong>
</meter>
</div>
</fieldset>
}
</div>
</body>
</html>
在这一章中,你在 ASP.NET MVC 项目中使用了一些新的 HTML5 输入类型。与传统的 Web 表单项目一样,您必须做一些额外的工作来使用它们,但是合并新的 HTML5 特性是相当容易的。特别是,MVC 框架被设计成可扩展的,这为构建 HTML5 应用提供了一个干净的平台。
MVC 模式提供了定义表单上使用的数据元素的模型。通过在模型中包含一些元数据属性,然后提供定制模板,您可以利用 HTML5 语义特定的控件。您可以下载并安装开源扩展,从而轻松构建符合 HTML5 的应用。然而,在这一章中,我向你展示了如何构建你自己的自定义助手扩展和编辑器模板。如果您发现自己处于一种独特的情况,需要一个特定的实现,那么您总是可以构建自己的实现。
使用 MVC Razor 视图引擎,您还可以包含文字 HTML 标记,这样您就可以最终控制用户体验。我还介绍了两个新的 HTML 控件,progress和meter,并演示了如何通过一些简单的 JavaScript 操作它们。
四、级联样式表
在第二章第一章和第三章第三章中,我向你展示了一些新的 HTML 元素以及如何在 ASP.NET 应用中使用它们。HTML5 的第二个主要方面包括样式表的改进。正如我在第一章中解释的,CSS3 推荐标准被分解成 50 多个模块,其中大部分仍在草案中(在撰写本文时)。然而,大多数浏览器已经提供了相当多的新功能。
在这一章中,我将演示许多更有用的特性。我将从解释创建样式表的基础开始。如果你有一些 CSS 的经验,这可能看起来像是复习,但其中一些是 CSS3 的新内容,尤其是选择器,它在 CSS3 中得到了显著的改进。然后,您将使用一些新的结构元素创建一个单独的 web 页面,比如nav、aside和footer。页面内容完成后,我将解释一些你可以用 CSS 做的有趣的事情。
查看样式语法
样式表由一组规则组成。每个规则由一个选择器和一个或多个声明组成,选择器指示规则适用于哪些元素。每个声明都包含一个属性值对。使用以下语法指定规则:
<selector> {<property:value>; <property:value>; ... }
例如,如果您希望所有段落标签使用绿色 12px 字体,则规则如下所示:
p {color:green; font-size:12px;}
与 HTML 一样,样式表中的空白会被忽略,因此这条规则也可以写成如下形式:
p
{
color:green;
font-size:12px;
}
我将在本章的其余部分使用这种格式,因为我认为它更容易阅读。
使用选择器
从文档中选择元素有很多不同的方法,CSS3 规范几乎是这个列表的两倍。我将概述可用的选择器。其中许多将在本章的后面演示。
元素选择器
我刚刚展示的第一个是元素选择器。要使用它,只需指定元素类型,如p、h1、input、ol、div等等。HTML5 引入了大量新标签,您可以在应用样式时加以利用。这些特定于上下文的元素,如article、footer和nav,更清楚地传达了它们的目的,因此更有可能将一致的格式应用于所有页面。这些新元素类型如下:
article:内容的独立部分,例如博客条目aside:内容通常放在页面的一边;通常用于相关信息details:用于根据用户输入隐藏或显示的可扩展内容figcaption:与figure一起使用,将标题与图像相关联figure:用于包装嵌入内容,如图像或图形footer:页面或章节页脚header:页面或章节标题hgroup:用于对h1、h2等表头元素进行分组nav:用于包含导航链接output:包含输出,例如用户操作的结果section:用于将内容组织成逻辑部分summary:通常与一个或多个details元素结合使用
使用组合子
如果要将相同的声明应用于多个元素类型,可以按如下方式对它们进行分组:
p, h1, h2
{
color:green;
font-size:12px;
}
逗号(,)字符用作逻辑或运算,例如,“类型为p或h1或h2的所有元素”。这只是选择器组合子的一个特例。您还可以组合选择器来指定某些元素层次结构。通过将元素与下列运算符之一组合,可以创建更复杂的选择器:
,(例如p, h1):选择所有p元素和所有h1元素。- space(例如,
header p):当第二个元素位于第一个元素内部时,选择第二个元素。例如,如果您想要一个header元素中的所有p元素,使用header p。header元素不一定是直接的父元素,只是在节点的父元素中的某个地方。 *(例如header*p):当第二个元素是第一个元素的孙元素或后续元素时,选择第二个元素。>(例如header>p):当第一个元素是直接父元素时,选择第二个元素。header>p选择器返回其父元素(直接)是header元素的所有p元素。+(例如header+p):当第一个元素是前面的兄弟元素时,选择第二个元素。∼(例如p∼header):在第一个元素之后选择第二个元素(不一定是立即)。
为了说明最后两个,如果您的文档如下所示,h1+p选择器不会返回任何元素,但是h2+p和h1∼p都将返回p元素:
<h1>Some header</h1>
<h2>Some sub-header</h2>
<p>Some text</p>
类别和 ID 选择器
类选择器允许您选择具有特定class属性的元素。因此,class 属性通常被称为 CSS 类。类选择器是通过在类名前面加一个点(.)像这样:
.featured
{
background-color:yellow;
}
这将为所有具有class="featured"属性的元素应用背景色。类选择器查找与选择器值匹配的整个单词。像class="the featured article"一样,一个元素在class属性中可以有多个单词,并且.featured选择器将返回它。
Caution
在 HTML 文档中,class属性是一个字符串,它可以有你想要给它的任何值。但是,为了能够在类选择器中使用它,它不能有任何空白或其他与 CSS 语法不兼容的字符。例如,您不能在类选择器中选择整个class="featured content"。如果你真的想要一个特色内容的类,使用featured_content或featuredContent。然而,你不能用一个类选择器只选择featured。相反,您需要使用一个属性选择器,我将在后面演示。
ID 选择器的工作方式类似于类选择器,只是它使用了id属性而不是class,并且在它前面加上了一个哈希符号(#,如下所示:
#Submit
{
color:blue;
}
ID 选择器根据元素的唯一 ID 来指定单个元素,因此,根据定义,该样式不会被重用。最好是基于元素或类来定义样式,这样相似的元素可以用相同的方式进行样式化。ID 选择器应该尽量少用,只在不需要重用样式的特殊情况下使用。
使用属性选择器
属性选择器为您提供了很大的灵活性,允许您根据元素的任何属性来选择元素。这些被指定为[attribute=value],如下所示:
[class="book"]
{
background-color:yellow;
}
这在功能上等同于使用.book类选择器;但是,属性选择器允许您仅使用属性值的一部分进行选择。为此,请在等号(=)前添加以下内容之一:
∼(例如[class∼="book"]):属性值必须包含选择器值指示的单词(例如class="somebooktitles")。这正是类选择器的工作方式。- |(例如,
[class|="book"]):属性值必须以匹配选择器值的单词开头(例如,class="booktitles") ^=(例如[class^="book"]):属性值必须以选择器值开头(例如class="books")$(例如[class$="book"]):属性值必须以选择器值结尾(例如class="checkbook")*(例如[class*="book"]):属性值必须包含选择器值(例如class="overbooked")
您可以指定不带值的属性,这将返回具有该属性的所有元素。一个很好的例子是[href]选择器,它将选择所有具有href属性的元素,不管其值如何。您还可以在属性选择器之前包含一个元素选择器,以进一步限制选定的元素。例如,img[src^="https"]将返回所有src属性以https开头的img元素。
伪类选择器
相当多的选择器基于元素的动态属性。例如,考虑一个超链接。如果链接引用的页面已经显示,链接通常以不同的颜色显示。这是通过使用 CSS 规则实现的,CSS 规则使用如下的visited属性:
a:visited
{
color: purple;
}
这将改变设置了visited标志的所有a元素的颜色。这些选择器中有几个已经存在一段时间了,但是 CSS3 定义了相当多的新选择器。以下是完整的列表:
:active:选择活动链接:checked:选择选中的元素(适用于复选框):disabled:选择当前禁用的元素(通常用于输入元素):empty:选择没有子元素的元素(不选择包含文本的元素):enabled:选择启用的元素(通常用于输入元素):first-child:选择其直接父元素的第一个子元素<tag>:first-of-type:选择其父元素中第一个指定类型的元素:focus:选择当前有焦点的元素:hover:选择鼠标当前悬停的元素:in-range:选择值在指定范围内的输入元素:invalid:选择没有有效值的输入元素:lang(value):选择具有以指定值开始的lang属性的元素:last-child:选择作为其父元素中最后一个子元素的元素:link:选择所有未访问的链接<tag>:last-of-type:选择其父元素中指定类型的最后一个元素:nth-child(n):选择其父元素中的第 n 个子元素:nth-last-child(n):选择其父元素中的第 n 个子元素,反向计数<tag>:nth-last-of-type(n):在父级中选择指定类型的第 n 个子级,反向计数<tag>:nth-of-type(n):选择其父级中指定类型的第 n 个子级:only-child:选择其父元素的唯一子元素<tag>:only-of-type:选择其父元素中指定类型的唯一兄弟元素:optional:选择不需要的输入元素(即没有required属性):read-only:选择具有readonly属性的输入元素:read-write:选择没有readonly属性的输入元素:required:选择具有required属性的输入元素:root:选择文档的根元素:target:选择具有目标属性的元素,其中目标是活动元素:valid:选择具有有效值的输入元素:visited:选择所有访问过的链接
nth-child(n)选择器计算父元素的所有子元素,而nth-of-type(n)只计算指定类型的子元素。这里的区别是微妙但重要的。对于only-child和only-of-type选择器也是如此。
Caution
有四个伪类可以与锚(a)元素一起使用(:link、:visited、:hover和:active)。如果使用多个,它们应该在样式规则中按此顺序出现。例如,如果使用了:link和:visited,则:hover必须在它们之后。同样,:active必须跟在:hover后面。
这些伪元素可用于返回选定元素的一部分:
:first-letter:选择每个选中元素的第一个字符:first-line:选择每个选中元素的第一行:selection:返回用户选择的元素部分
您可以将:before或:after限定符添加到选择器中,以便在文档中所选元素之前或之后插入内容。使用content:关键字来指定内容并包含任何所需的样式命令(样式仅适用于插入的内容)。比如要加上“重要!”在紧跟在header标签之后的每个p标签之前,使用以下规则:
header+p:before
{
content:"Important! ";
font-weight:bold;
color:red;
}
您还可以在选择器前面加上:not来返回所有未选中的元素。例如,:not(header+p)选择除了紧跟在header标签后面的p标签之外的所有元素。
理解工会
您还可以通过用逗号分隔复杂的选择器,将它们组合成逻辑 OR 关系。例如,我在本章前面展示的p, h1, h2选择器就是一个联合的例子。它将返回满足任何包含的选择器的所有元素。每个选择器可以是任何更复杂的类型。这也是一个有效的选择器:
header+p, .book, a:visited
它将返回所有元素,要么是紧跟在header元素之后的p元素,要么是带有图书class的元素,要么是已访问的a元素。
Tip
有关可用选择器的确切列表,请参见位于 www.w3schools.com/cssref/css_selectors.asp 的文章。
使用 CSS 属性
提供了所有这些选择器,以便您可以指定想要应用所需样式属性的适当元素。这才是 CSS 真正的肉。可用的 CSS 属性有数百个,我无法在此一一描述。在本章的剩余部分,我将演示许多更新的、更有用的特性。你会在 www.w3schools.com/cssref/default.asp 找到所有 CSS 属性的很好的参考。
使用供应商前缀
哦,生活在边缘的乐趣!与 HTML5 的其他领域一样,浏览器供应商将对 CSS 规范提供不同的支持。然而,在许多情况下,这些供应商在新属性成为官方推荐的一部分之前就实现了它们。事实上,CSS3 规范中包含的大部分内容已经可以从一个或多个浏览器中获得。
当浏览器供应商添加不属于 CSS3 建议的新功能时,该属性会被赋予一个特定于供应商的前缀,以指示这是一个非标准功能。如果这成为建议的一部分,前缀最终会被删除。为了利用一些较新的属性,您可能需要使用特定于供应商的属性,并且因为您希望您的页面在所有供应商上工作,所以您需要添加所有这些属性。例如,要指定边框半径,除了标准的border-radius属性,您可能还需要设置所有供应商特定的属性,如下所示:
header
{
-moz-border-radius: 25px;
-webkit-border-radius: 25px;
-ms-border-radius: 25px;
border-radius: 25px;
}
表 4-1 列出了最常见的前缀。还有其他的,但是这个表涵盖了绝大多数的浏览器。
表 4-1。
Vendor Prefixes
| 前缀 | 浏览器供应商 | | --- | --- | | `-moz-` | 火狐浏览器 | | `-webkit-` | Chrome、Safari、Opera | | `-ms-` | 微软公司出品的 web 浏览器 |您不能盲目地假设所有带有供应商前缀的属性都与标准属性同名,只是添加了前缀,尽管大多数情况下确实如此。这里有一篇很好的文章,列出了许多特定于供应商的属性: http://peter.sh/experiments/vendor-prefixed-css-property-overview 。遗憾的是,此页面已有一段时间没有更新,可能已经过时。如果您发现某个标准属性在特定的浏览器中不起作用,您可能需要做一些研究,看看他们的开发人员的网站上是否有前缀属性。例如,使用 Webkit 扩展的链接: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Webkit_Extensions 。
Caution
您应该始终将标准属性列在最后,这样它将覆盖特定于供应商的版本。有些浏览器会支持这两者,虽然大多数时候实现是相同的,但有时特定于供应商的版本表现不同。
了解盒子模型
文档中的每个元素都占用一定的空间,这取决于该元素的内容。此外,填充和边距等因素也会对此产生影响。填充是内容和元素边框之间的空间。边距是边框和相邻元素之间的空间。如图 4-1 所示。

图 4-1。
The box model
您可以使用margin声明来指定边距,并以像素或页面大小的百分比来指定值。您可以使用margin-top、margin-right、margin-bottom和margin-left声明单独指定上边距、右边距、下边距和左边距,或者使用margin声明指定所有四个值(按此顺序—上边距、右边距、下边距和左边距)。您也可以使用带有单个值的margin声明,这会将所有四个边距都设置为该值。如果您传递两个值,第一个将设置上边距和下边距,第二个将指定左边距和右边距。使用padding声明以同样的方式设置填充。
确定使用的空间时,记得包括边框宽度。例如,如果填充设置为 10px,边距设置为 5px,边框宽度设置为 3px,则使用的空间(除实际元素内容之外)将为(2 * 10) + (2 * 5) + (2 * 3) = 36px。
应用样式规则
样式是从各种来源以几种不同的方式指定的,顾名思义,它们是级联的,或者说是继承的。理解这是如何工作的很重要,特别是当有冲突的声明时。
包括样式规格
样式表有三个来源:
- 作者:这些是 web 开发人员创建的样式表,也是您在提到样式表时通常会想到的。
- 用户:用户也可以创建一个样式来控制网页的显示方式。
- 用户代理:用户代理(web 浏览器)将有一个默认的样式表。例如,如果您创建一个没有样式规则的文档,浏览器将使用默认字体系列和大小显示内容。这些实际上是在特定于浏览器的样式表中定义的。
对于作者样式,这是您可以控制的唯一来源,有三种方法可以在 HTML 文档中包含样式规则。
- 内联:样式是使用
style属性直接在元素中设置的,比如:<p style="color:red">This is red text</p>。当然,使用这种方法,不需要使用选择器,因为样式只适用于当前元素(以及所有子元素)。 - 内部:可以使用
style元素将样式规则包含在实际的 HTML 文档中。这通常放在head标签中,并应用于整个文档。以这种方式定义的样式将需要一个选择器来指示应该在哪些元素上使用该样式。这种方法有时被称为嵌入式样式。 - 外部:应用样式最常见的方式是将所有的样式规则放在一个扩展名为
.css的单独文件中。样式规则的格式就像内部样式一样。使用外部样式表的明显好处是同一组规则可以应用于多个页面。每个页面都引用了这个带有link元素的样式表,比如:<link rel="stylesheet" type="text/css" href="MyStyleSheet.css"
级联规则
当呈现页面时,浏览器必须处理来自所有这些来源的样式,以确定每个元素的适当样式。当存在冲突规则时,作者样式表优先于用户样式表,用户样式表优先于用户代理样式(浏览器默认)。可以使用我前面解释的三种方法(内联、内部和外部)来指定作者样式。在作者样式中,内联声明优先于内部声明和外部样式表。如果一个页面使用了一个内部的style元素,同时也使用了link元素来包含一个外部样式表,那么只要内部声明在link元素之后,它就会覆盖外部样式表中的冲突规则。
Caution
如果外部样式表在style标签后被引用,它将优先于内部样式。如果您既有外部样式表又有内部的style元素,那么您应该首先引用外部的样式表,这样优先级规则才能按预期工作。
此外,考虑到即使在单个样式表中也可能存在冲突声明。例如,样式表可能包括以下内容:
p
{
color: black;
}
header p
{
color: red;
}
一个header元素内的一个p元素被两个规则选中,那么使用哪个呢?在这种情况下,特殊性规则适用,它规定使用更具体的选择器,即header p选择器。在所有可用的选择器中,确定哪一个更具体并不像您想象的那样简单。ID 选择器被认为比类或属性选择器更具体,而类或属性选择器又比元素选择器更具体。如果只有元素选择器,那么元素最多的规则优先,所以包含两个元素的header p比只有p更具体。
最后,如果同一个选择器用不同的声明在同一个样式表中使用了两次会怎么样?假设p { color:black; }出现在样式表中,稍后p { color:green }出现。在这种情况下,最后出现的规则优先,因此您会看到绿色文本。
使用重要关键字
一种“王牌”是important关键字。如果在样式规则中使用,这将超越所有其他规则。您可以像这样添加important关键字:
p
{
color: red;
!important;
}
如果两个冲突的规则都有important关键字,那么优先级是基于我已经提到的规则确定的。然而,有一个显著的区别。通常,作者样式表中的规则会覆盖用户样式表中的规则。如果他们有important关键字,这是相反的;用户样式表将覆盖作者规则。乍一看,这似乎很奇怪,但它有一个重要的应用。这允许用户覆盖某些属性的作者样式。例如,有视觉障碍的人可能需要增加字体大小。标签important将确保这个样式不会被覆盖。
Caution
您可能会尝试使用important关键字来快速修复并覆盖级联样式规则。有了我刚刚描述的所有优先规则,您不应该需要这样做。我建议将此作为最后的手段。过度使用important关键字会使你的样式表难以维护。
创建网页
在这一章的剩余部分,我将向你展示如何构建一个单独的网页来展示 CSS 的许多新特性。为了多样化,我将使用 WebMatrix 应用而不是 Visual Studio 来创建单个网页。样式规则将使用内部的style元素,所以一切都可以放在一个文件中。少量的 JavaScript 也将包含在单个文件中。
我将在这个项目中使用 Chrome 浏览器,因为它支持我将演示的大多数 CSS 功能。在撰写本文时,其他浏览器还不支持这些特性中的一个或多个。当你读到这篇文章时,其他浏览器可能也支持这些。
Note
我在第一章中解释了如何安装 WebMatrix 应用。这是微软提供的免费下载。如果您愿意,也可以使用 Visual Studio 和 MVC 项目模板来实现网站。使用Index.cshtml文件遵循本章剩余部分的说明,你可以在Views\Home文件夹中找到这个文件,而不是Default.cshtml。也可以从 www.apress.com 下载源代码中包含的完整的 Visual Studio 项目。
规划页面布局
在创建一个新的网页之前,最好先勾画出基本的网页结构。这将帮助您可视化整体布局,并了解元素是如何嵌套在一起的。
你将在本章中开发的页面将在顶部使用header和nav元素,在底部使用footer元素。中间的主要区域将使用一个div元素,并有两个并排的区域,每个区域都有一系列的article标签。较大的区域将被另一个div元素包围,并提供组织成文章的主要内容。右边较小的区域将使用一个aside元素并包含一个section元素。这将包含一系列呈现相关信息的article元素。图 4-2 展示了页面布局。

图 4-2。
Planning the page layout Note
此图显示了每个元素之间的空间,以便于理解。在实际的 web 页面中,在大多数情况下,这个空间是通过将padding属性设置为 0 来移除的。
创建 Web 项目
规划好内容后,您就可以开始构建网页了。首先,您将使用 WebMatrix 创建一个项目。然后,您将进入基本的页面结构,并向每个元素添加内容。稍后,我将展示如何实现样式规则。
启动 WebMatrix 应用,点击新建图标,然后点击模板图库按钮,如图 4-3 所示。

图 4-3。
Launching the WebMatrix application Tip
为了便于将来参考,App Gallery 按钮将显示一个相当大的预构建 web 应用列表,您可以下载并使用它来构建您的 web 项目。这包括 WordPress、Joomla 和 Drupal 等软件包。
有几个模板可供选择。例如,起始站点模板将创建一个 ASP.NET MVC 项目。对于本章,您将使用空站点模板。选择此项,进入章节 4 作为站点名称,如图 4-4 所示。单击“确定”按钮创建项目。

图 4-4。
Selecting the Empty Project template
创建项目后,单击导航窗格中的“文件”按钮。为您创建的文件和文件夹应该如图 4-5 所示

图 4-5。
The initial files and folders
应该有一个名为Default.cshtml的网页。双击导航页面中的文件名将其打开。最初的内容如下所示:
@{
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>My Site's Title</title>
<link href="∼/favicon.ico" rel="shortcut icon" type="image/x-icon" />
</head>
<body>
</body>
</html>
定义页面结构
我发现在添加内容之前先输入结构元素会有所帮助。这将让你有机会清楚地看到结构,不被实际内容所干扰。打开Default.cshtml文件,输入清单 4-1 中所示的元素。
Listing 4-1. Entering the Page Structure
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>``Chapter 4
</head>
<body>
<header class="intro">
</header>
<nav>
</nav>
<div id="contentArea">
<div id="mainContent">
<section class="rounded">
<header>
</header>
</section>
<section>
<article class="featuredContent">
<a id="feature"></a>
<header>
</header>
<div>
</div>
</article>
<article class="otherContent">
<a id="other"></a>
<header>
</header>
<div>
</div>
</article>
<article class="otherContent">
<a id="another"></a>
<header>
</header>
<div>
</div>
</article>
</section>
</div>
<aside id="sidebar">
<section id="titles">
<article class="book">
<header>
</header>
</article>
<article class="book">
<header>
</header>
</article>
<article class="book">
<header>
</header>
</article>
<article class="book">
<header>
</header>
</article>
<article class="book">
<header>
</header>
</article>
</section>
</aside>
</div>
<footer>
</footer>
</body>
</html>
这只是一个基本的 HTML 结构,你可以从图 4-2 中的图表推断出来。article元素被赋予了class属性,因为您将使用它进行样式化。我将id属性赋给了一些顶级元素。我还为每个主要内容文章添加了一个锚元素(<a id="feature"></a>)。您将在nav元素中设置这些内容的导航链接。
添加内容
内容没什么特别的。它包含大量文本(主要是 Lorem ipsum)、一些图片和一些链接。
在导航窗格中,右键单击 Chapter 4 项目,然后单击新建文件夹链接。输入文件夹名称的图像。可下载的源代码中包含一个Images.zip文件。将图片从这个文件复制到项目中新的Images文件夹。
我建议下载内容,而不是手动输入。源代码中有一个Default_content.cshtml文件。用该文件中的代码替换您当前的实现。它只包含该页面的内容,没有定义任何样式。如果要手动输入内容,可以在附录 a 中找到。
Note
我想指出内容中的一个小细节。footer元素使用 HTML5 中添加的新的time元素。开始和结束标签之间的文本(2015 年 3 月 7 日)被显示,但是datetime属性包含一个机器可读的格式,可以被浏览器、搜索引擎或 JavaScript 使用。更多详情请看这篇文章: www.sitepoint.com/html5-time-element-guide 。
添加内容后,单击功能区中的 Run 按钮,查看页面目前的外观。应该类似于图 4-6 。

图 4-6。
The initial page with only default styles
实现样式规则
现在你到了有趣的部分,增加了风格。您可以使用大量的样式属性,我将展示一些对 CSS3 来说新的更有用的技术。其中许多样式已经使用了一段时间,但是在 CSS3 之前,它们的实现更加复杂,通常需要 JavaScript。在指定一些基本的样式规则后,我将向您展示如何使用更多的高级功能,包括以下内容:
- 圆角
- 渐变背景
- 桌子
- 多列
- 方框阴影
- 斑马条纹文本装饰
- 3D 转换
- CSS 动画
添加基本样式
在开始添加新的样式特性之前,您需要定义基本的样式格式。在Default.cshtml文件顶部的head元素中添加一个style元素。然后添加清单 4-2 中所示的规则。同样,如果您愿意,您可以下载Default_styled.cshtml文件并从那里复制代码。
Listing 4-2. Adding the Basic Styles
<style>
/* Basic tag settings */
body
{
margin: 0 auto;
width: 940px;
font: 13px/22px Helvetica, Arial, sans-serif;
background: #f0f0f0;
}
h2
{
font-size: 18px;
line-height: 5px;
padding: 2px 0;
}
h3
{
font-size: 12px;
line-height: 5px;
padding: 2px 0;
}
h1, h2, h3
{
text-align: left;
}
p
{
padding-bottom: 2px;
}
.book
{
padding: 5px;
}
/* Content sections */
.featuredContent
{
background-color: #ffffff;
border: 2px solid #6699cc;
padding: 15px 15px 15px 15px;
}
.otherContent
{
background-color: #c0c0c0;
border: 1px solid #999999;
padding: 15px 15px 15px 15px;
}
aside
{
background-color: #6699cc;
padding: 5px 5px 5px 5px;
}
footer
{
margin-top: 12px;
text-align:center;
background-color: #ddd;
}
footer p
{
padding-top: 10px;
}
/* Navigation Section */
nav
{
left: 0;
background-color: #003366;
}
nav ul
{
margin: 0;
list-style: none;
}
nav ul li
{
float: left;
}
nav ul li a
{
display: block;
margin-right: 20px;
width: 140px;
font-size: 14px;
line-height: 28px;
text-align: center;
padding-bottom: 2px;
text-decoration: none;
color: #cccccc;
}
nav ul li a:hover
{
color: #fff;
}
</style>
我不会说太多,因为这都是很标准的 CSS 东西。它主要使用元素选择器,偶尔使用类选择器。如果你现在预览你的网页,它看起来应该如图 4-7 所示。

图 4-7。
The web page with only basic styling Note
为了简化示例代码,我将仅使用 Chrome 供应商前缀-webkit-,并且仅在当前版本(43)不支持标准属性时使用。我可以这样做,因为我希望这个页面只能在 Chrome 浏览器中运行。通常,您不能做出这种假设,需要包括所有的供应商前缀。
使用圆角
添加圆角用 CSS3 很容易做到;只需定义border-radius属性。你的网页将为aside、nav和footer元素以及rounded类的元素使用圆角。
Note
在第七章中,我将向你展示如何在不支持圆角功能的旧浏览器中实现圆角。读完那一章后,你可能会对浏览器中支持这样的特性有更好的理解。
将清单 4-3 中所示的规则添加到style元素的末尾。
Listing 4-3. Using Rounded Borders
/* Rounded borders */
.rounded
{
border: 1px solid;
border-color:#999999;
border-radius:25px;
padding: 24px;
}
aside
{
border: 1px solid #999999;
border-radius:12px;
}
/* Make the radius half of the height */
nav
{
height: 30px;
border-radius:15px;
}
footer
{
height: 50px;
border-radius:25px;
}
对于nav和footer元素,由于它们是相当短的部分,您将设置半径为高度的一半。这会在两端形成一个半圆。顶部导航部分应该如图 4-8 所示。

图 4-8。
Using rounded borders
使用渐变
使用 CSS3,您可以通过使用linear-gradient函数设置background-image属性来轻松创建渐变。使用此功能,您可以指定开始和结束颜色以及应用渐变的角度。你将在主标题中使用渐变,它有intro类。
在style元素的末尾添加以下规则:
/* Gradients */
.intro
{
border: 1px solid #999999;
text-align: left;
padding-left: 15px;
margin-top: 6px;
border-radius: 25px;
background-image: linear-gradient(45deg, #ffffff, #6699cc);
}
这将应用 45 度角的渐变。这也创建了一个圆形的边界。页面顶部现在应该如图 4-9 所示。

图 4-9。
Using a gradient background
创建表格
在标记中使用表格进行格式化通常被认为是不好的做法。这种格式最好在样式表中完成。如果需要不同的格式,可以更新样式。您可能已经注意到,当前 web 页面的主要内容后面有一个aside元素,而不是两个并排的元素。现在您将使用 CSS 设置一个表来纠正这种情况。
将清单 4-4 中所示的规则添加到style元素的末尾。
Listing 4-4. Creating a Table
/* Setup a table for the content and sidebar */
#contentArea
{
display: table;
}
#mainContent
{
display: table-cell;
padding-right: 2px;
}
aside
{
display: table-cell;
width: 280px;
}
这些规则在顶级元素上设置了display属性。contentArea元素被设置为table,mainContent和aside元素被设置为table-cell。然后,这些元素被呈现为整个内容元素中的单元格。为了完成对齐,mainContent上的填充设置为 2px,aside元素的宽度设置为 280px。使用剩余空间自动计算mainContent的宽度。
页面布局现在应该如图 4-10 所示。

图 4-10。
The page layout with the sidebar on the right
添加列布局
CSS3 的另一个新特性是能够像在报纸或杂志上看到的那样将内容格式化成列。这是使用column-count属性完成的。您还应该指定定义列之间垂直间距的属性column-gap。
在style元素的末尾添加以下规则:
/* Setup multiple columns for the articles */
.otherContent
{
text-align:justify;
padding:6px;
-webkit-column-count: 2;
column-count: 2;
-webkit-column-gap: 20px;
column-gap: 20px;
}
文章现在应该被格式化为两列,如图 4-11 所示。

图 4-11。
Using two columns
添加方框阴影
图像可能看起来有点粗糙,添加阴影可以柔化外观,使页面在视觉上更具吸引力。使用box-shadow属性很容易添加阴影,该属性采用以下值:
- 水平位置:水平阴影的位置。如果是负数,阴影在左边。
- 垂直位置:垂直阴影的位置。如果为负,阴影在顶部。
- 模糊:阴影后模糊区域的大小。
- 扩散:阴影的宽度。
- 颜色:阴影的颜色。
- 插入:使图像看起来比周围区域低,使阴影位于图像上而不是图像外。
这些值在逗号分隔的列表中指定。它需要两到四个位置/大小值、一个可选的颜色属性和可选的inset关键字。只需要前两个,分别是水平和垂直位置。如果未指定,模糊和扩散值将默认为零。将以下规则添加到style元素的末尾:
/* Add the box shadow */
article img
{
margin: 10px 0;
box-shadow: 3px 3px 12px #222;
}
.book img
{
margin: 10px 0;
display: block;
box-shadow: 2px 2px 5px #444;
margin-left: auto;
margin-right: auto;
}
aside
{
box-shadow: 3px 3px 3px #aaaaaa;
}
.book img规则还包括margin-left和margin-right属性,它们都被设置为auto。这导致图像水平居中。图 4-12 和 4-13 显示了特色内容和侧边栏项目中图像的特写。请注意,第一个图像比侧边栏图像有更大的模糊区域。

图 4-13。
The shadow on the sidebar images

图 4-12。
The shadow of the phone in booth image the featured content section
使用斑马条纹
一种已经使用了很长时间的样式方法是在有项目列表时替换背景,这有时被称为斑马条纹。这可以追溯到旧的蓝条纸用于进入会计期刊。交替的背景使得区分每个项目变得更加容易。在 CSS3 之前,这是通过 JavaScript 来实现的,它会以编程的方式改变每个其他元素的背景。
CSS3 引入了nth-child选择器,它非常适合这个应用,因为它每隔 n 个元素返回一次。在将n设置为 2 的情况下使用它将返回所有其他元素。将以下代码添加到style元素的末尾:
/* Stripe the title list */
#titles article:nth-child(2n+1)
{
background: #c0c0c0;
border: 1px solid #6699cc;
border-radius: 10px;
}
#titles article:nth-child(2n+0)
{
background: #6699cc;
border: 1px solid #c0c0c0;
border-radius: 10px;
}
这个规则使用一个复杂的选择器#titles article:nth-child(2n+1),它首先选择# titles元素。这是一个包含书名的section元素。每个书名都在一个单独的article元素中。然后article:nth-child选择器返回#titles元素中的每第 n 个article元素。然而,2n+1 参数可能看起来有点奇怪。要获取所有其他元素,需要指定 2n 作为参数,这将返回奇数项(第一项、第三项、第五项等等)。通过使用 2n+1,列表被偏移 1,所以你将得到偶数项(第二、第四、第六等等)。因此,第一个规则格式化偶数项,第二个规则使用 2n+0 格式化奇数项。你可以简单地用2n代替2n+0,因为它们是等价的,但是我喜欢用2n+0来保持一致性。这两种样式规则的唯一区别是背景和边框颜色。图 4-14 为效果图。

图 4-14。
Applying the zebra striping to the sidebar
添加文本装饰
文本装饰允许您用各种效果来修饰文本。已经定义了三种类型的修饰:线条(如下划线和删除线)、强调标记和阴影。官方推荐定义了这个功能,尽管浏览器的实现有点粗略和不一致。我将首先解释该标准是如何定义的,然后向您展示使其工作所需的变通方法。
Note
文字装饰细节在 WC3 推荐中有很好的解释,可以在 www.w3.org/TR/css-text-decor-3 访问。
线条装饰
线条装饰由三个属性的组合定义:
text-decoration-line:指定该行应该在文本上方(overline)、文本下方(underline)还是穿过文本中间(line-through)text-decoration-style:定义线条的样式,如solid、dashed、dotted、double或wavytext-decoration-color:表示线条的颜色
该建议还允许使用text-decoration快捷方式,您可以在一个属性中指定所有三个属性。例如,您可以用它来定义红色波浪下划线:
text-decoration: underline wavy red;
如果样式和颜色从快捷方式中省略,这是向后兼容 CSS 级别 1 和 2。并且,在撰写本文时,这是大多数浏览器所支持的全部内容。因此,您可以添加下划线、上划线或删除线,但不能调整其样式或颜色。要进行尝试,请在style元素的末尾添加以下内容:
h2
{
text-decoration: underline overline line-through;
}
这将显示文本中的所有三行。保存这些更改并刷新浏览器窗口,标题文本应该如图 4-15 所示。

图 4-15。
Adding line decorations
Firefox 支持一些线条修饰风格,但是你需要使用他们的供应商前缀。为了演示如何做到这一点,用以下内容替换您刚刚添加的h2选择器:
h2
{
text-decoration: underline;
-moz-text-decoration-line: underline;
-moz-text-decoration-style: wavy;
-moz-text-decoration-color: red;
text-decoration-line: underline;
text-decoration-style: wavy;
text-decoration-color: red;
}
第一行使用大多数浏览器都支持的向后兼容属性。这将使用与文本相同的颜色定义实心下划线。接下来的三行使用 Firefox 供应商前缀定义了相同的红色波浪线,所有其他浏览器都将忽略这一点。最后三行使用 CSS3 标准定义红色波浪线。如果您使用大多数浏览器显示这个页面,您将会看到一条像前面一样的黑色实线。如果你使用 Firefox,页面会如图 4-16 所示。

图 4-16。
Displaying line decorations in Firefox
然而,随着时间的推移,随着浏览器采用 CSS3 标准,波浪下划线将取代实线。这是一个很好的例子,说明了如何设计您的页面来使用当前可用的功能,并利用新兴的功能。
强调标记
强调标记的使用类似于添加线条;他们添加符号或标记来强调指定的文本。在撰写本文时,没有一个桌面浏览器支持这个特性。使用三种属性的组合来定义强调标记。
text-emphasis-style:指定要使用的符号类型,如dot、triangle、double-circle或sesame。text-emphasis-color:表示用于强调标记的颜色。text-emphasis-position:定义标记相对于文本的位置;可能的值有over、under、left和right。可以包含over right等组合。
也允许使用快捷方式定义,因此您可以使用如下所示的单个属性来指定它:
text-emphasis: dot red;
文本阴影
文字阴影的定义类似于方框阴影,我之前解释过。与其他一些文本修饰功能不同,所有主流浏览器都支持文本阴影。
文本阴影由包含以下参数的单个属性定义:
- 水平偏移
- 垂直偏移
- 模糊半径
- 颜色
偏移值可以是负值。负垂直偏移会将阴影置于文本上方,负水平阴影会将阴影置于左侧。如果省略 color 参数,阴影将与文本颜色相同。
Caution
文本阴影的模糊半径应该很小。除非你有一个非常大的字体,否则使用大于 1px 或 2px 的值会使文本不可读。可以指定 0px,这样会导致阴影一点都不模糊。
要演示文本阴影,请在style元素的末尾添加以下内容:
h3:first-letter
{
text-shadow: 2px -5px 1px blue;
}
保存更改并刷新浏览器。标题文本应如图 4-17 所示。

图 4-17。
Adding a text shadow Tip
这个例子还演示了first-letter伪类。这将从每个h3元素中选择第一个字母。
使用 3D 变换
添加 3D 变换可以为您的网页增添一些活力。我将演示一个相当简单的应用,您可以在其中翻转三维电话亭图像。您还将添加一些 JavaScript 来制作旋转动画。
要格式化 3D 变换,您需要指定几个属性。首先,您将在包含图像的div上设置perspective属性。这将建立用于确定 3D 效果渲染方式的消失点。然后,您将在图像本身上设置preserve-3d属性,它告诉浏览器在旋转图像时保持 3D 透视图。为此,将以下内容添加到style部分的末尾:
/* Transforms */
.rotateContainer
{
-webkit-perspective: 360;
perspective: 360px;
}
.rotate
{
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
}
Note
标准的perspective属性需要单位(例如,360px),但是 WebMatrix 中的 IntelliSense 不识别这一点,并将这显示为 CSS 验证错误。这个可以忽略。还要注意,以供应商为前缀的属性-webkit-perspective不需要单位。
现在,您将添加一个 JavaScript 函数,该函数将对图像旋转进行动画处理。在head元素中输入粗体代码:
<head>
<meta charset="utf-8" />
<title>``Chapter 4
<script type="text/javascript">
var angle = 0;
var t;
function rotateImage(value) {
document.getElementById("phone").style.transform
= "rotateY(" + value + "deg)";
}
function toggleAnimation() {
if (angle == 0) {
t = setInterval(function () {
rotateImage(angle);
angle += 1;
if (angle >= 360)
angle = 0;
}, 100);
}
else {
clearInterval(t);
angle = 0;
rotateImage(angle);
}
}
</script>
<style>
angle变量存储图像的当前旋转角度,t变量是对间隔定时器的引用。rotateImage()函数只是为图像元素设置rotateY样式。这相当于将它添加到 CSS 中:
transform: rotate(20deg);
这是在 JavaScript 中完成的,因为每次调用它都会指定一个不同的角度。toggleAnimation()功能将启动或停止动画。为了开始动画,它调用了setInterval()函数,提供了一个每隔 100 毫秒调用一次的匿名函数。匿名函数在当前角度调用rotateImage(),然后递增angle。要取消动画,调用clearInterval()方法,然后图像被设置回初始旋转。
最后,添加以粗体显示的代码,当单击图像时,该代码调用toggleAnimation()函数:
<div id="rotateContainer">
<p>This is really cool...</p>
<img class="rotate" id="phone"
src="img/phonebooth.jpg"
alt="phonebooth"
onclick="toggleAnimation() "/>
<br />
保存这些更改并刷新浏览器窗口。要开始播放动画,请单击图像。您应该看到图像缓慢旋转,如图 4-18 所示。

图 4-18。
The rotated phonebooth—in 3D! Tip
在撰写本文时,Chrome、Opera 和 Safari 有一个有趣的 bug。在检查元素之前,该变换是一个简单的 2D 变换。右键单击image元素并选择检查元素。一旦变换变为 3D,您就可以关闭检查窗口。
添加动画
对于最后一个效果,我将向您展示如何在不使用任何 JavaScript 的情况下创建动画效果。你不能用 CSS 修改图像,因为那被认为是内容,而不是格式。但是,您可以更改背景图像,并利用它来实现动画效果。
aside元素的div定义如下:
<div id="moon"></div>
因为没有定义内容或大小,所以目前这对页面布局没有影响。现在,您将使用 CSS3 中的动画特性来迭代显示月相的各种图像。
在 CSS 中,动画是通过定义一组keyframes实现的。每个框架定义一个或多个 CSS 属性。在这个应用中,您将指定适当的背景图像,但是您也可以轻松地更改颜色、大小或任何其他 CSS 属性。对于每一帧,您还可以指定该帧应该出现的动画持续时间的百分比。您应该始终有一个 0%和 100%的帧,它们指定开始和结束属性。您可以在中间包含任意数量的步骤。在此示例中,有八个图像,因此,为了保持帧间距均匀,帧将以 0%、12%、25%、37%、50%、62%、75%、87%和 100%的比例过渡。
一旦您定义了keyframes,您就可以在您想要制作动画的元素上设置动画属性。您将通过设置animation-name属性来指定keyframes的名称。您还可以使用animation-duration属性设置动画的持续时间(以秒为单位)。将清单 4-5 中所示的代码添加到样式部分的末尾。
Listing 4-5. Defining the Animation Effect
/* Animate the moon phases */
@@-webkit-keyframes moonPhases
{
0% {background-image:url("img/moon1.png");}
12% {background-image:url("img/moon2.png");}
25% {background-image:url("img/moon3.png");}
37% {background-image:url("img/moon4.png");}
50% {background-image:url("img/moon5.png");}
62% {background-image:url("img/moon6.png");}
75% {background-image:url("img/moon7.png");}
87% {background-image:url("img/moon8.png");}
100% {background-image:url("img/moon1.png");}
}
@@keyframes moonPhases
{
0% {background-image:url("img/moon1.png");}
12% {background-image:url("img/moon2.png");}
25% {background-image:url("img/moon3.png");}
37% {background-image:url("img/moon4.png");}
50% {background-image:url("img/moon5.png");}
62% {background-image:url("img/moon6.png");}
75% {background-image:url("img/moon7.png");}
87% {background-image:url("img/moon8.png");}
100% {background-image:url("img/moon1.png");}
}
#moon
{
width:115px;
height:115px;
background-image: url("img/moon1.png");
background-repeat: no-repeat;
-webkit-animation-name:moonPhases;
-webkit-animation-duration:4s;
-webkit-animation-delay:3s;
-webkit-animation-iteration-count:10;
animation-name:moonPhases;
animation-duration:4s;
animation-delay:3s;
animation-iteration-count:10;
}
这段代码将总持续时间设置为 4 秒,因此图像应该每半秒转换一次。它还指定在开始前等待三秒钟,并重复播放动画十次。当你刷新网页时,大约三秒钟后,它会循环显示月相,如图 4-19 所示。

图 4-19。
Animating the moon’s phases Tip
如果您希望动画无限期地继续,请将animation-iteration-count属性设置为infinite。
这里还有另外两个不适用的动画属性,timing-function和direction。如果你正在使用一个简单的动画并且只定义开始和结束值,那么timing-function定义了过渡的速度。例如,如果您正在将一个元素移动到不同的位置,将此设置为linear将会以恒定的速率移动对象。然而,使用默认值ease,过渡将开始缓慢,然后加速,然后在接近结束时减速。还有其他选择,比如ease-in,开始时会很慢,然后在剩余的过渡阶段会加速。direction属性,如果设置为alternate,将在交替迭代中反转过渡。默认值normal,每次都会重放相同的过渡。
摘要
在这一章中,我介绍了很多关于 CSS 的信息,尤其是 CSS3 中的新特性。选择器非常强大,在应用样式时提供了很大的灵活性。在 CSS3 之前,大部分工作都必须通过大量的 JavaScript 函数来完成。我还向您展示了如何使用大量新的结构化 HTML5 元素来规划和构建一个示例 web 页面。附录 B 显示了完整的style元素。
使用 WebMatrix 应用,您创建了一个简单的 web 页面,定义了基本结构,然后填充了内容。使用一些新的 CSS3 特性,您添加了一些重要的样式特性,包括:
- 圆形边框
- 梯度
- 桌子
- 多列
- 阴影
- 斑马条纹
- 文本装饰
- 3D 转换
- 动画
在下一章,我将介绍 HTML5 中一些与脚本相关的新特性。
五、脚本增强
在这一章中,我将展示一些影响 web 开发脚本方面的各种改进。到目前为止,我已经介绍了标记更改和 CSS 增强。脚本是整个 HTML5 的第三个支柱,这个领域受到了极大的关注。本章将解释一些应用广泛的改进。
- 查询选择器
- 网络工作者
- 管理包和版本
包管理实际上不是 HTML5 的一部分,而是通过 Bower 和 Gulp 等开源工具完成的,这些工具已经集成到 Visual Studio 中。
使用查询选择器
在第四章中,我解释了可以用来创建强大样式规则的 CSS 选择器。CSS3 在这方面引入了显著的改进。有了健壮的属性选择器和一些新的伪类,比如你在第四章的中使用的nth-child,选择 DOM 元素就有了相当多的功能。但是更好的是:所有这些功能都可以从 JavaScript 中获得。
HTML5 规范包括两个新函数,querySelector()和querySelectorAll()。querySelector()函数返回单个元素,第一个匹配指定选择器的元素。querySelectorAll()函数返回匹配元素的数组。对于这两个函数,都要传入 CSS 选择器,就像在样式表中一样格式化。所以,一旦你学会了如何使用 CSS 选择器,你就可以把同样的经验应用到 JavaScript 上。
为了尝试这些功能,您将使用在第四章中创建的同一个网页。如果你想使用,第四章项目的最终版本,可以从源代码下载中获得。
使用查询选择器
querySelector()功能可以用来代替getElementById()功能。当然,它比这有用得多,因为您可以传入任何类型的 CSS 选择器。
打开Default.cshtml文件,修改rotateImage()函数,替换getElementById()函数如下:
function rotateImage(value){
document.``querySelector("#phone")
="rotateY(" + value + "deg)";
}
Caution
不要忘记在 ID 前面加上前缀#。因为querySelector()函数可以与任何类型的选择器一起使用,所以您需要散列符号来表示这是一个 ID 选择器。
使用 Firefox 运行网页,并验证 3D 旋转仍然有效。
使用 querySelectorAll
这是一个相当简单的例子,所以现在我将演示一个更复杂的选择器。您将添加一个 JavaScript 函数来改变nav元素中所有内部链接的颜色。可以说,您可以在样式表中这样做,但有时您也需要在代码中这样做。例如,您可能需要根据用户输入以编程方式更改样式。
向Default.cshtml页面中的script元素添加以下函数:
function adjustInternalLinks(){
var links = document.querySelectorAll("nav ul li a[href ^='#']");
for (var i=0; i < links.length; i++){
links[i].style.color = "green";
}
}
CSS 选择器是nav ul li a[href ^='#'],它返回所有具有以#字符开始的href属性的a元素。这被进一步过滤为仅具有nav、ul和li父子关系的元素。这将排除可能出现在其他部分的链接。
querySelectorAll()函数返回一个数组,所以这段代码遍历数组,使每个元素都变成绿色。现在你需要调用这个函数。将以下粗体显示的代码添加到body元素中:
<body``onload="adjustInternalLinks()"
这将在页面加载时调用该函数,但是您也可以根据一些适当的用户输入来调用它,以使样式动态化。保存更改并重新加载页面。你现在应该有绿色链接。注意到 www.apress.com 的链接不是绿色的,因为是外部链接,不是以#开头。
创建 Visual Studio 项目
对于本章的其余练习,您将使用 Visual Studio 项目。启动 Visual Studio 2015,点击新建项目。选择 ASP.NET Web 应用项目模板,输入章节 5 作为名称,如图 5-1 所示。

图 5-1。
Creating the Chapter5project
在第二个对话框中,选择 ASP.NET 5 网站模板。
雇佣网络工作者
随着越来越多的工作在客户端完成,让客户端应用多线程化变得更加重要。幸运的是,使用 Web 工作器 是实现这一目标的一种便捷方式。CPU 密集型或可能需要一些时间才能完成的功能可以在后台线程上执行,让主 UI 线程可以响应用户操作。
网络工作者使用一个相当简单的概念。您创建一个 worker 并传递给它一个定义其执行的 JavaScript 文件。然后,网页可以通过消息与工作人员通信。worker 实现onmessage事件处理程序来响应来自页面的传入消息,并使用postMessage()函数将数据发送回调用者。调用者还必须处理onmessage事件来接收来自工作者的消息。如图 5-2 所示。

图 5-2。
Communicating with a dedicated web worker Tip
对于您将在本章中创建的演示应用,呼叫者和工作人员之间的消息将是简单的文本消息。然而,它们可以是您想要的任何格式,包括 JSON 编码的数据。
Web 工作器 最大的限制之一是他们不能访问 DOM,所以你不能用他们来更新页面内容或样式。此外,他们不能访问窗口对象,这意味着,除其他事项外,你不能使用计时器。考虑到这些限制,您可能想知道何时使用 web worker。
网络工作者非常适合执行检索数据等任务。例如,如果您需要从外部源(如数据库、本地文件系统或 web)查找信息,您可以将查找参数传递给 worker,当查找完成时,数据可以作为 JSON 消息传递回来。这允许网页在检索数据时响应用户操作。
网络工作者有两种类型:专用型和共享型。专用工作器只能由单个页面使用,而共享工作器可以由多个 web 页面使用。专用工作人员和共享工作人员的工作方式基本相同,但沟通方式略有不同。您将从实现一个专用的 web worker 开始。
使用专门的工人
一个专门的网络工作者,顾名思义,就是专门为创建它的网页服务的。网页创建它,需要时使用它,不再需要时关闭它。一个网页可以根据需要创建任意多的工作人员。
为了演示一个专用的 web worker,您将构建一个简单的 web 页面,允许您创建一个 worker 并向它发送消息。它还将显示响应,以便您可以看到双向通信。worker 实现很简单,只是简单地回显发送给它的消息。
EXERCISE 5-1. USING A DEDICATED WEB WORKERIn the Chapter 5 project you created earlier, open the Index.cshtml file, which you’ll find in the Views\Home folder. Replace the default implementation of this view using the code shown in Listing 5-1. This will create a simple form with a text area for displaying messages and three buttons for communicating with the worker.
清单 5-1。索引视图实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Chapter``5
<link rel="stylesheet" type="text/css" href="∼/css/Sample.css" />
<script type="text/javascript" src="∼/controlWorker.js"></script>
</head>
<body>
<header>
<h1>Web 工作器 Demo</h1>
</header>
<div>
<textarea id="output"></textarea>
</div>
<form id="control" method="post" action="">
<input id="create" type="button" class="button" value="Create Worker"
onclick="createWorker()"> <br>
<input id="send" type="button" class="button" value="Send Message"
onclick="sendWorkerMessage()">
<input id="message" type="text" class="text" value="Hello, World!"><br>
<input id="kill" type="button" class="button" value="Close Worker"
onclick="closeWorker()">
</form>
</body>
</html>
Replace the default implementation with the code shown in Listing 5-2. From the Solution Explorer, right-click the wwwroot\css folder and choose Add and New Item. Select the Style Sheet item and enter Sample.css for the file name, as shown in Figure 5-3. Click the Add button to create the file.

图 5-3。
Adding the Sample.css style sheet
清单 5-2。Sample.css 样式表
h1
{
font-size:22px;
color:purple;
}
#output
{
width: 500px;
height: 250px;
background-color:#dfcaca;
}
.button
{
width:125px;
height:25px;
color:green;
}
.text
{
width:260px;
}
For the contents of this file, enter the following code. This is the implementation of the worker. It handles both the onconnect event (when the worker is first created) and the onmessage event (when a message is sent to the worker). The implementation simply echoes the message back to the caller. Close the browser and stop the debugger. In the Solution Explorer, right-click the wwwroot folder and choose Add and New Item. In the Add New Item dialog box, select the JavaScript File item. Enter worker.js for the file name, as shown in Figure 5-5.

图 5-5。
Adding the worker.js file Press F5 to debug the application; the form should look similar to Figure 5-4.

图 5-4。
The initial form design
/* This file implements the web worker */
// This event is fired when the web worker is started
onconnect = sendResponse("The worker has started");
// This event is fired when a message is received
onmessage = function (event) {
sendResponse("Msg received: " + event.data);
}
// Sends a message to the main thread
function sendResponse(message) {
postMessage(message);
}
Using the same instructions, add a controlWorker.js file in the wwwroot folder. Enter the code shown in Listing 5-3 for its implementation. I will explain this in more detail later in the chapter.
清单 5-3。controlWorker.js 实现
/* This file contains functions used to
communicate with the web worker */
var myWorker;
function createWorker() {
if (typeof(Worker) !== "undefined") {
var log = document.querySelector("#output");
log.value += "Starting worker process… ";
myWorker = new Worker("worker.js");
log.value += "Adding listener… ";
myWorker.onmessage = function(event){
log.value += event.data + "\n";
}
log.value += "Done!\n";
}
else {
alert("Your browser does not support Web 工作器");
}
}
function sendWorkerMessage(){
if (myWorker !== null) {
var log = document.querySelector("#output");
log.value += "Sending message… ";
var message = document.querySelector("#message");
myWorker.postMessage(message.value);
log.value += "Done!\n";
}
}
function closeWorker(){
if (myWorker !== null) {
var log = document.querySelector("#output");
log.value += "Closing worker… ";
myWorker.terminate;
myWorker = null;
log.value += "Done!\n";
}
}
Press F5 to debug the application. Click the Create Worker button and then click the Send button. Modify the message and try clicking the Send Message button again. Finally, click the Close Worker button. The text area should look like Figure 5-6.

图 5-6。
The message log
controlWorker.js文件包含三个函数,为表单上的三个按钮提供实现。它首先声明一个myWorker变量,该变量包含对专用 web worker 的引用,然后实现以下函数:
createWorker():该函数首先通过查看是否定义了Worker类来检查浏览器是否支持 Web 工作器。否则,将发出警报。然后它创建一个Worker类的实例,将它的引用保存在myWorker变量中。工人实现通过引用worker.js脚本文件传递给它的构造函数。然后它实现onmessage事件处理程序,将传入的消息添加到output字段。sendWorkerMessage():这只是调用工人的postMessage()方法,传入在message字段中指定的文本。注意它使用了我在本章前面解释过的querySelector()方法。closeWorker():调用 worker 的terminate()方法,将myWorker变量设置为null。该工作线程会立即关闭,无法执行任何清理操作。
提示与添加一个onmessage事件处理程序的方式相同,您也可以创建一个onerror事件处理程序来响应来自 worker 的错误。网络工作者可以通过使用throw功能来报告错误。
通过这个简单的实现,您可以看到创建一个 worker 并使用消息与之通信是多么容易。
创建共享工作者
共享 web worker 允许您创建一个 worker,然后从其他页面重用它。使用共享工作人员有几个好处。最明显的一点是,多个页面可以共享同一个线程,而不必为每个页面创建一个新的工作线程。另一个,我将在后面解释,是跨页面共享状态信息。
Caution
IE 11 支持专用 Web 工作器,但不支持共享 worker。
现在您将创建一个共享的 worker,它是在一个 JavaScript 文件中实现的。概念本质上是一样的,只是交流的方式有点不同。您将向 web 页面添加几个按钮,并实现一组新的 JavaScript 函数来与共享的工作人员进行通信。
EXERCISE 5-2. CREATING A SHARED WORKEROpen the Index.cshtml file and add the following script reference in the head element after the previous reference:
<script type="text/javascript" src="∼/controlSharedWorker.js"></script>
Add the code shown in bold in Listing 5-4 to the form element. This will add another set of buttons to control the shared worker.
清单 5-4。Index.cshtml 中的附加按钮
<form id="control" method="post" action="">
<input id="create" type="button" class="button" value="Create Worker"
onclick="createWorker()"> <br>
<input id="send" type="button" class="button" value="Send Message"
onclick="sendWorkerMessage()">
<input id="message" type="text" class="text" value="Hello, World!"><br>
<input id="kill" type="button" class="button" value="Close Worker"
onclick="closeWorker()">
<br><br>
<input id="createS" type="button" class="button" value="Create Shared"
onclick="createSharedWorker()"> <br>
<input id="sendS" type="button" class="button" value="Send Shared Msg"
onclick="sendSharedWorkerMessage()">
<input id="messageS" type="text" class="text" value="Hello, World!"><br>
<input id="killS" type="button" class="button" value="Close Shared"
onclick="closeSharedWorker()">
</form>
From the Solution Explorer, add another file to the wwwroot folder named sharedWorker.js and enter the code shown in Listing 5-5. This is the implementation of the shared worker.
清单 5-5。sharedWorker.js 实现
/* This file implements the shared web worker */
var clients = 0;
onconnect = function(event) {
var port = event.ports[0];
clients++;
/* Attach the event listener */
port.addEventListener("message", function(event){
sendResponse(event.target, "Msg received: " + event.data);
}, false);
port.start();
sendResponse(port, "You are client # " + clients + "\n");
}
function sendResponse(senderPort, message) {
senderPort.postMessage( message);
}
Add another file in the wwwroot folder named controlSharedWorker.js and enter the implementation shown in Listing 5-6. I will explain this code later.
清单 5-6。controlSharedWorker.js 实现
/* This file contains functions used to
communicate with the web worker */
var mySharedWorker;
function createSharedWorker() {
if (typeof(SharedWorker) !== "undefined") {
var log = document.querySelector("#output");
log.value += "Starting shared worker process… ";
mySharedWorker = new SharedWorker("sharedWorker.js");
log.value += "Adding listener… ";
mySharedWorker.port.addEventListener("message", function(event){
log.value += event.data + "\n";
}, false);
mySharedWorker.port.start();
log.value += "Done!\n";
}
else {
alert("Your browser does not support shared Web 工作器");
}
}
function sendSharedWorkerMessage(){
if (mySharedWorker !== null) {
var log = document.querySelector("#output");
log.value += "Sending message… ";
var message = document.querySelector("#messageS");
mySharedWorker.port.postMessage(message.value);
log.value += "Done!\n";
}
}
function closeSharedWorker(){
if (mySharedWorker !== null) {
var log = document.querySelector("#output");
log.value += "Closing worker… ";
mySharedWorker.port.terminate;
mySharedWorker = null;
log.value += "Done!\n";
}
}
Press F5 to debug the application. Create a shared worker and send a message to it. It should work just like the previous exercise. Leaving the browser tab running, create a new tab and enter the same URL as the first tab. This will open the same page in a second tab. Create a shared worker from the second tab. Then click the Send Shared Message button to test the connection. Notice the message says that you are the second client, as demonstrated in Figure 5-7.

图 5-7。
Opening a second copy of the page
消息在工作者和调用工作者的页面之间传递。多个页面可以调用一个共享的工作器,但是消息是不共享的;每条消息仍然在单个页面和工作人员之间。但是,员工内部的数据是共享的,可以从多个页面访问。
现在让我们看看共享工作器是如何实现的。就像专门的工作者一样,它必须处理onconnect和onmessage事件。但是,您不能将ommessage处理程序直接附加到 worker 上;相反,您必须访问一个端口并连接到该端口。onconnect事件接收一个event参数,你通过event.ports[0]访问端口。一旦有了port,就可以将事件处理程序附加到它上面。您使用端口的addEventHandler()方法。这需要两个参数。第一个是事件的名称,在本例中是message。第二个参数是引发事件时将调用的函数。
发送消息时,还必须使用port对象。这个port对象在传入消息的event.target属性中提供。这个事件处理程序和onconnect事件处理程序都使用传入port对象的sendResponse()函数。
controlSharedWorker.js文件中的功能几乎与它们的专用对应物完全相同。然而,它们也必须使用port对象。端口包含在事件中。
注意在sharedWorker.js文件中,clients变量被声明,然后在onconnect事件处理程序中递增。这用于跟踪有多少客户端已经连接到共享工作器。我添加这个只是为了演示这个变量对于连接到 worker 的所有客户机是如何全局的。事实上,没有每个端口的实例数据;所有数据都是全球性的。
另外,当消息进来时,event参数包括响应应该发送到的端口。工作人员不会“记住”每个客户端的端口。它只是做它被指示做的事情,并在指定的端口上返回一个响应。
客户端包管理
Visual Studio 2015 和 ASP.NET 5 在如何完成客户端打包方面引入了一个非常重要的转变。NuGet 仍然存在,但已经被归入服务器端包。在客户端,您现在可以使用 Bower、Grunt、Gulp 和节点包管理器(NPM)等工具。项目模板为您预先配置了大多数基本功能。你不需要处理这些来实现本书中的例子。然而,我想给你一个这些工具做什么和它们如何工作的概述。让我们从查看为您创建的项目文件开始。
配置:图标配置
如果您看一看解决方案资源管理器,您可能会问自己,“web.config文件在哪里?”如果您查看文件列表的末尾,您会看到一个config.json文件,在这里您可以找到配置设置,比如连接字符串。配置数据可以存储在 JSON、XML 或 INI 文件中,并且可以有许多配置文件。
看一下Startup.cs文件。它定义了一个IConfiguration成员和一个定义要加载的文件的构造函数。
public Startup(IHostingEnvironment env)
{
// Setup configuration sources.
var configuration = new Configuration()
.AddJsonFile("config.json")
.AddJsonFile($"config.{env.EnvironmentName}.json", optional: true);
if (env.IsEnvironment("Development"))
{
// This reads the configuration keys from the secret store.
// For more details on using the user secret store see
// http://go.microsoft.com/fwlink/?LinkID=532709
configuration.AddUserSecrets();
}
configuration.AddEnvironmentVariables();
Configuration = configuration;
}
public IConfiguration Configuration { get; set; }
因此,您可以决定如何组织配置数据以及使用何种文件格式。项目模板生成的初始代码使用AddJsonFile()方法从config.json文件加载数据。它还加载任何可能使用AddEnvironmentVariables()方法定义的环境变量。
这里有一篇解释新配置模型的好文章: http://blog.jsinh.in/asp-net-5-configuration-microsoft-framework-configurationmodel/#.VQ3TUvnF9Cg 。
静态文件:wwwroot
wwwroot文件夹是 ASP.NET 5 的新增功能,它提供了一个存放所有静态内容的地方,比如 CSS、JavaScript、图片和静态 HTML。这里的想法是明确区分通过服务器端代码生成的内容和简单地按原样提供给浏览器的内容。
该文件夹被称为 web 根目录。这大致相当于以前版本的 MVC 使用的Content和Scripts文件夹。这些文件夹在解决方案浏览器中与Models、Views和Controllers处于同一级别。将它们向上移动一个级别,合并成一个级别,并将其命名为 web root,这样就可以更清楚地知道应该包含什么。
包管理:Bower
虽然 NuGet 是一个受欢迎的朋友。作为. NET 开发人员,Bower 在管理客户端依赖性方面一直很受欢迎。因此,在 ASP.NET 5 中,您将使用 Bower 来配置您的应用所需的客户端软件包。(您将继续对服务器端包使用 NuGet。)客户端依赖关系在bower.json文件中列出;清单 5-7 显示了初始的、模板生成的文件。
Listing 5-7. The bower.json Configuration File
{
"name": "WebApplication",
"private": true,
"dependencies": {
"bootstrap": "3.0.0",
"jquery": "1.10.2",
"jquery-validation": "1.11.1",
"jquery-validation-unobtrusive": "3.2.2",
"hammer.js": "2.0.4",
"bootstrap-touch-carousel": "0.8.0"
},
"exportsOverride": {
"bootstrap": {
"js": "dist/js/*.*",
"css": "dist/css/*.*",
"fonts": "dist/fonts/*.*"
},
"bootstrap-touch-carousel": {
"js": "dist/js/*.*",
"css": "dist/css/*.*"
},
"jquery": {
"": "jquery.{js,min.js,min.map}"
},
"jquery-validation": {
"": "jquery.validate.js"
},
"jquery-validation-unobtrusive": {
"": "jquery.validate.unobtrusive.{js,min.js}"
},
"hammer": {
"": "hammer.{js,min.js}"
}
}
}
使用这个文件的好处是智能感知支持。例如,打开这个文件并转到依赖项部分。在定义了bootstrap-touch-carousel的最后一行,转到行尾,输入一个逗号,然后按回车键。然后输入一个报价并开始输入一个包名。请注意,在您键入时,可用包的列表会自动显示。输入“modernizr”:”,注意显示的是可用的版本号,如图 5-8 所示。

图 5-8。
Bower IntelliSense support r
还要注意版本语义。撰写本文时,当前的稳定版本是 2.8.3。这些数字分别指定主要版本、次要版本和修补程序编号。在版本前面加上一个克拉符号(^)表示主要版本必须匹配。例如,如果指定了².8.3,则只要主版本是 2,就会使用等于或高于 2.8.3 的任何版本。因此,将使用 2.8.5 或 2.9,而不是 3.1。波浪号(∾)表示主要版本和次要版本必须匹配。所以,∞2 . 8 . 3 将使用大于或等于 3 的 2.8 的任何路径级别;因此,将使用 2.8.5,但不使用 2.9。省略这两者表明应该使用最新版本,只要它至少是 2.8.3。
构建任务:吞咽
对于大多数 web 应用需要的所有客户端文件,获取、组织和准备它们可能是一项单调乏味的任务。您已经将 Bower 视为管理依赖性的一个很好的工具。Gulp 是另一个有用的工具,它允许您自动化构建任务。Gulp 是一个基于 JavaScript 的框架,使用 Node.js 和 NPM。
Gulp 的一个典型场景是告诉 Bower 检查并下载依赖项。事实上,清单 5-8 中显示的初始gulpfile.js就是这么做的。
Listing 5-8. The Initial gulpfile.js File
/// <binding Clean='clean' />
var gulp = require("gulp"),
rimraf = require("rimraf"),
fs = require("fs");
eval("var project = " + fs.readFileSync("./project.json"));
var paths = {
bower: "./bower_components/",
lib: "./" + project.webroot + "/lib/"
};
gulp.task("clean", function (cb) {
rimraf(paths.lib, cb);
});
gulp.task("copy", ["clean"], function () {
var bower = {
"bootstrap": "bootstrap/dist/**/*.{js,map,css,ttf,svg,woff,eot}",
"bootstrap-touch-carousel": "bootstrap-touch-carousel/dist/**/*.{js,css}",
"hammer.js": "hammer.js/hammer*.{js,map}",
"jquery": "jquery/jquery*.{js,map}",
"jquery-validation": "jquery-validation/jquery.validate.js",
"jquery-validation-unobtrusive": "jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"
}
for (var destinationDir in bower) {
gulp.src(paths.bower + bower[destinationDir])
.pipe(gulp.dest(paths.lib + destinationDir));
}
});
当然,您还可以完成许多其他任务,比如捆绑和缩小您的 JavaScript 或 CSS 文件。另一个例子是将较少的样式表预编译成 CSS 文件。
Note
除了 Gulp,Grunt 还用于客户端构建自动化。Grunt 可以执行与 Gulp 相同类型的任务,但以不同的方式完成。咕噜声和吞咽声可能会持续一段时间。如果你正在考虑使用这些工具中的一个,但不确定是哪一个,这里有一篇很好的文章,它描述了 Grunt 和 Gulp 之间的区别,并提供了一些关于你应该考虑使用哪一个的建议: https://medium.com/@preslavrachev/gulp-vs-grunt-why-one-why-the-other-f5d3b398edc4 。
摘要
在这一章中,您尝试了一些有用的技术,这些技术可能会在您的许多 web 项目中使用。
- 查询选择器在 JavaScript 代码中利用了同样强大的 CSS 选择器。
- Web 工作器 在单独的线程上执行 CPU 密集型或缓慢的操作,以提高整体响应能力。
我还介绍了一些可用于管理 web 应用的新的客户端工具。在第六章中,我将向您展示 HTML5 的改进如何用于创建移动友好的 web 应用。
六、移动网络应用
到目前为止,我们只看了桌面浏览器;然而,HTML5 真正伟大的方面之一是它在包括手机、平板电脑和电视在内的各种设备上的支持程度。据 Html5Test.com 报道,在撰写本文时,Chrome 和 Opera 平台分别以 523 分和 489 分领先。但亚马逊丝绸,火狐手机,安卓和黑莓以 468,456,452 和 449 分紧随其后,满分为 555 分。
在移动设备上,您将使用本地应用和 web 应用。原生应用是为特定的移动平台开发的,并安装在设备上,或者通过电话服务下载。原生应用通常可以提供最佳的用户体验,因为它们可以最大限度地利用设备的特定硬件和操作系统功能。然而,部分由于 HTML5 的流行,网络应用的需求也越来越大。它们的开发几乎和桌面浏览器一样容易。
使用模拟器
要查看您的网站在移动设备上如何工作,您可以使用许多电话模拟器应用。虽然这些功能可能不完全像实际的硬件,但它们提供了一个合理的近似值。我将向您展示如何安装和使用几个更常见的实用程序。
使用 Opera 移动模拟器
Opera 提供了一个免费的移动模拟器应用,你可以从 www.opera.com/developer/tools/mobile 下载。这个工具特别好的一点是,你可以从一个很长的列表中选择你想要模拟的设备。安装好这个应用后,启动它,你应该会看到如图 6-1 所示的启动窗口。

图 6-1。
The Opera emulator launch window
当您选择一个设备时,窗口会显示硬件详细信息,如屏幕分辨率。选择 LG Optimus One 设备,然后单击启动按钮。有了这个模拟器,你可以使用设备键盘或桌面键盘。从第四章的中输入你的站点的 URL,它应该看起来像图 6-2 。

图 6-2。
Emulating the LG Optimus One device
请注意,页面被缩放以适合屏幕,这使得它几乎不可读。你将在本章的后面处理这个问题。您可以尝试一些其他设备,如诺基亚 N800,如图 6-3 所示。正如您所料,较大的外形尺寸可以更好地处理页面。

图 6-3。
Emulating the Nokia N800
安装镀铬波纹
在 Chrome 和 Firefox 上模拟移动设备需要一种不同的方法,即使用桌面浏览器的附加组件。使用仿真器时,实际上是使用带有一些附加功能的桌面浏览器来模拟设备的外形。
启动 Chrome 桌面浏览器,点击应用图标,选择网络商店应用。在搜索框中,选择扩展选项并键入 ripple emulator。结果应该如图 6-4 所示。单击免费按钮安装扩展。

图 6-4。
The Ripple emulator in the web store
安装完插件后,使用 Chrome 浏览器并输入第四章网站的 URL。右上角有一个按钮,如图 6-5 所示,用于启动模拟器。

图 6-5。
Launching Ripple
点击该按钮,然后点击使能按钮,如图 6-6 所示。

图 6-6。
Enabling the Ripple emulator
这将使用模拟器模式显示当前页面。第一次为一个特定的 URL 启动 Ripple,你会看到如图 6-7 所示的提示。点击 BlackBerry 10 WebWorks 按钮,选择要模拟的平台。

图 6-7。
Selecting the desired platform
你在 BlackBerry 10 设备上的网页应该如图 6-8 所示。

图 6-8。
The web page on the BlackBerry 10 device
浏览器窗口的左上角和右上角有两个带箭头的小按钮。使用这些来显示/隐藏选项窗口。例如,如图 6-9 所示,左边的界面允许您更改设备方向或选择不同的平台进行仿真。它还提供了当前设备的一些技术细节,如屏幕分辨率。右边的按钮包括“设置”标签,您可以在暗主题和亮主题之间切换。

图 6-9。
Displaying the emulator options
模拟其他设备
要在 iPhone 上模拟你的网站,使用 Chrome 浏览器,进入这个网站: http://iphone-emulator.org 。当模拟器显示时,在设备上的搜索框中输入第四章网站的 URL。该站点将如图 6-10 所示。

图 6-10。
Emulating the web page on an iPhone
请注意页面顶部的按钮,这些按钮使您能够模拟其他设备,如 iPad 和 Android。
Tip
还有其他模拟器可用;我只讲了几个。如果您想查看其他选项,请尝试在 https://www.browserstack.com/list-of-browsers-and-platforms?product=live 浏览堆栈。您还可以在 www.asp.net/mobile/device-simulators 查看 ASP.net 上的资源。
处理外形尺寸
创建在移动设备上运行良好的 web 应用时,最大的挑战是处理各种形状因素。在较大的设备上,你会希望利用额外的空间,同时在较小的设备上也有一个合理的外观。到目前为止,在我展示的示例中,设备要么缩放页面以适应页面,要么裁剪页面。这两种方法都不是最佳的。
有三种技术可以帮助你改善你的网站在各种外形下的外观。
- 媒体查询:这允许您根据现有视口的属性应用不同的样式。我将用这一章的大部分时间来证明这一点。
- 使用 CSS flexbox 布局:这类似于使用 Windows Presentation Foundation(WPF)设计表单,允许浏览器根据窗口大小动态调整大小或移动元素。我将解释这是如何工作的,然后您将使用它来配置导航链接。
- 灵活的图像和视频:这只是指示浏览器拉伸或收缩图像,以适应可用的空间。
Tip
各种仿真器做的事情之一是根据设备特性限制窗口大小。您可以通过简单地调整浏览器窗口的大小来完成同样的事情。对于您的初始测试,您可以缩小窗口并查看布局如何响应。然后使用模拟器进行最后的测试。
了解媒体查询
CSS 2.1 引入了media关键字,允许你定义一个打印机友好的样式表。例如,您可以使用这样的内容:
<link rel="stylesheet" type="text/css" href="screen.css" media="screen" />
<link rel="stylesheet" type="text/css" href="print.css" media="print" />
然后,您可以为浏览器(屏幕)定义一个样式表,并为网页的打印版本定义一个不同的样式表。或者,您可以在单个样式表中嵌入特定于媒体的样式规则。例如,这将改变打印时的字体大小:
@media print
{
h1, h2, h3
{
font-size: 14px;
}
}
Tip
还支持其他媒体类型,包括aural、braille、handheld、projection、tty和tv。如您所见,媒体类型最初用于表示呈现页面的设备类型。此外,支持all类型,但是如果没有指定媒体类型,也是隐含的。带有all类型的样式适用于每个设备。
在 CSS3 中,这一点得到了显著的增强,允许您查询各种属性来确定合适的样式。例如,当窗口宽度为 600 像素或更小时,您可以应用如下样式:
@media (max-width:600px)
{
h1
{
font-size: 12px;
}
}
媒体查询中可以选择的功能如下:
widthheightdevice-widthdevice-heightorientationaspect-ratiodevice-aspect-ratiocolor(0 表示单色或用于指定颜色的位数)color-index(可供选择的颜色数量)monochrome(彩色为 0,灰度为位数)resolution(在 dpi 或 dpcm 中指定)scan(对于电视,指定扫描模式)grid(1 如果是网格设备如 TTY 显示,0 如果是位图)
其中大多数都支持min-和max-前缀,这意味着您不必使用大于号或小于号运算符。例如,如果您想要一个介于 500 像素和 700 像素之间的窗口样式,您可以指定如下:
@media screen and (min-width: 500px) and (max-width: 700px)
注意,在这个例子中,我还包括了screen媒体类型。在这种情况下,对于所有其他类型,如print,该样式将被忽略。
Tip
关于这些特性的完整定义,请参见 W3 规范的 www.w3.org/TR/css3-mediaqueries/#media1 。
使用媒体查询
您可以通过媒体查询做很多事情来动态设计您的网页。例如,您可以使用color和monochrome功能在单色设备上显示时应用更合适的样式。color功能返回支持的颜色数量,因此(min-color: 2)将选择所有颜色设备。您也可以使用(orientation: portrait)和(orientation: landscape)根据设备的方向排列元素。
在本演示中,您将关注窗口的宽度,但相同的基本概念也适用于其他功能。随着窗口宽度的缩小,样式将逐渐调整以适应窗口的大小,同时尽可能多地保留原始布局。
一个典型的方法是计划三种不同的风格:大型、中型和小型。大字体可能是网站最初设计的方式,就像你的第四章网站一样。有侧栏和多列内容。中等风格将保持相同的基本布局,但开始根据需要缩小区域。一个有用的技巧是使用相对大小,这样当窗口缩小时,每个元素也逐渐缩小。小型样式将用于手持设备,您通常会将布局保持为单列。由于页面现在会变得更长,页面上的书签链接变得更加重要。
修改第四章站点
为了演示这些技术,您将向您在第四章中构建的站点添加一些额外的样式规则。您将使用媒体查询来根据窗口的宽度有选择地应用这些样式。
Tip
章节 4 站点是使用 WebMatrix 应用创建的。但是,源代码下载提供了 WebMatrix 项目和 Visual Studio 项目。你会在章节 4 文件夹中找到这些。你可以用你喜欢的任何一个。该说明将告诉您如何修改Default.cshtml文件。如果您使用的是 Visual Studio,这将是Index.cshtml文件;这两个文件中的更改是相同的。我使用在第五章中修改过的版本,用 JavaScript 将内部链接设置为绿色。
打开章节 4 项目并运行应用。我们将继续使用 Chrome 浏览器,但大多数浏览器将支持本章演示的样式特性。尝试缩小浏览器窗口的宽度。请注意,页面根本没有缩放;浏览器简单地截取不适合窗口的内容。这是你有工作要做的第一个线索。网页应该是流畅的,并根据窗口大小进行调整。
Caution
为了使更改更容易理解,您只需将附加的样式规则附加到您的style标签的末尾。正如我在第四章中提到的,相同的选择器会覆盖文件中早先定义的样式。然而,使用相同的选择器是很重要的。您可以编写一个类似的选择器,它将返回相同的元素,但是如果它被认为不够具体,它不会覆盖前面的样式。例如,在本文档中,nav a将返回与nav ul li a相同的元素,但是后者被认为更具体,并且将优先于前者,即使它在文件中更靠前。
配置媒体布局
您的网页的当前布局基于相对较大的窗口,如桌面浏览器。设计网页时,还应该考虑适合小型设备的布局。我建议为小分辨率设备(如典型的移动设备)创建一个单独的设计。在本章中,您将使用媒体查询来实现小型、中型和大型配置。然而,中型布局往往是小型和大型之间的妥协。从大布局开始,然后设计小布局通常效果最好。
水平滚动不直观,应该尽可能避免。所以如果你的分辨率很窄,你应该垂直堆叠元素。例如,aside e元素需要放在页面的底部。你可以考虑去掉图片或者改变字体大小。
一旦你有了一个小的布局,你可以随着宽度的缩小而逐渐引入这些变化。我喜欢采取的方法是逐渐开始缩小浏览器窗口的宽度,看看有什么会中断。然后进行更正,以处理这一点,并尝试缩小一些。有了已经设计好的小布局,当你在这个迭代过程中进行调整时,你就会知道你要去哪里。
现在,您将定义中小型布局的样式,从中等开始。在中型设备上,您将使用相同的基本布局,但只是缩小一些元素。对于此站点,中等将被定义为宽度介于 600 像素和 940 像素之间。网页的大小是 940px,所以如果窗口比这个宽,就不需要调整。600 像素的最小尺寸有些随意。稍后我会解释我如何得出这个数字。
媒体布局需要一点调整。您将使用一个简单的技巧来定义具有相对大小的元素。这允许它们在调整窗口大小时自动收缩或伸展。打开Default.cshtml文件,将清单 6-1 中所示的规则添加到现有的style元素中。将此添加到所有现有规则之后。
Listing 6-1. Defining the Medium Layout
@@media screen and (max-width: 940px)
{
body
{
width: 100%;
}
aside
{
width: 30%;
}
nav ul li a
{
width: 100px;
}
}
Note
在 Razor 语法中,“与”符号(@)用来表示跟在它后面的是代码,而不是内容。要在媒体查询这样的上下文中包含一个&符号,您需要使用一个双&符号。
通过将body宽度设置为 100%,它将自动缩小以适应窗口。但是,它不会超过 940 像素,因为这种样式仅在宽度小于 940 像素时应用。aside元素设置为 30%。当前比率(280 像素/940 像素)约为 30%。当你继续缩小窗口时,nav元素中的链接最终会被剪切掉,所以这种样式也会减少它们的宽度,使它们靠得更近。
运行应用并尝试缩小窗口。你应该注意到一个很好的流体布局,它可以根据窗口大小进行调整,如图 6-11 所示。

图 6-11。
Displaying the medium layout
配置小型布局
然而,最终布局效果并不好,如图 6-12 所示。

图 6-12。
The medium layout when shrunk too much
这里有几个你需要解决的问题。
- 页眉文本换行并重叠。
- 文本列太窄;这个大小不足以支持三个内容列。
调整布局的主要变化是将aside元素移到页面底部,而不是其他内容旁边。当你调整窗口大小时,其他的变化是渐进的,但是这个变化会导致一个跳跃。主要内容将从窗口大小的 70%变为 100%。您需要确定应该触发更改的适当宽度。我选择 600 像素,但是您可以尝试其他值,看看页面如何工作。
将清单 6-2 中的代码输入到现有style元素的末尾。
Listing 6-2. Defining the Small Layout
@@media screen and (max-width: 600px)
{
/* Move the aside to the bottom of the page */
#contentArea, #MainContent, aside
{
display: block;
}
aside
{
width: 98%;
}
/* Use a single column for the article content */
.otherContent
{
-webkit-column-count: 1;
column-count: 1;
}
/* Fix the line spacing of the header */
h2, h3
{
line-height:normal;
}
/* Force the intro element to stretch to fit the content */
.intro
{
height: min-content;
}
/* Move the book images to the left */
.book img
{
float: left;
margin-right: 10px;
margin-bottom: 5px;
}
/* Make the book elements tall enough to fit the image */
.book
{
min-height: 120px;
}
}
Note
您之前为中等尺寸添加的样式也适用于小尺寸样式,因为两者都适用于小于 940px 的宽度。小样式将定义额外的规则,但是记住前面的样式也适用。
小布局规则进行以下调整:
aside元素被移动到底部。这是通过撤销您在第四章中输入的表格和单元格属性,然后将宽度更改为 98%来完成的。以前,#contentArea元素的display属性被设置为表,#mainContent和aside元素被设置为table-cell。通过将这三个都设置为block,虚拟表被移除。- 内容显示在两列中,这将减少到一列。
- 由于标题文本现在可以使用多行,请更改行高,使各行不重叠。
- 强制
intro部分垂直拉伸,以确保所有内容适合。 - 将图书图像向左移动,将相应的文本向右移动。
- 确保书籍元素足够大以适合图像。
显示包含这些更改的网页并调整窗口大小。如果窗口足够窄,链接会换行,如图 6-13 所示。

图 6-13。
The web page with a narrow window
使用 Flexbox
为中等大小的窗口创建样式时,您减小了链接的宽度,以便它们适合较小的窗口。您可以再次这样做,这样它们仍然合适,但最终您将需要一个更好的解决方案。如果您需要添加另一个或两个链接,或者其中一个链接的长度超过了您指定的固定宽度,该怎么办?为了解决这个问题,你将使用一个灵活的盒子,或 flexbox。flexbox 允许您定义根据可用空间自动排列的内容块。
使用 flexbox 时,您需要配置容器以及容器中包含的物件。这些有时被称为父节点及其子节点。例如,文档中的nav标签有以下内容。ul标签包含一系列的li标签。
<nav>
<ul>
<li><a href="#feature">Feature</a></li>
<li><a href="#other">Article</a></li>
<li><a href="#another">Archives</a></li>
<li><a href="http://www.apress.com
</ul>
</nav>
配置容器
在容器元素上,在本例中是ul,您将把display属性设置为flex,以指示应该使用 flexbox。然后您可以指定flow-direction,它可以是row或column。方向设置为row时,项目从左到右水平对齐;有了column,它们就垂直堆叠,从上到下。您还可以添加–reverse来颠倒顺序(从右到左或从下到上)。这些是允许的值:
row:水平,从左到右(这是默认值)row-reverse:水平,从右到左column:垂直,从上到下column-reverse:垂直,从下到上
然后您可以指定flex-wrap属性,该属性决定当项目不适合分配的空间时会发生什么。以下是可能的设置:
nowrap:内容显示在一行(或一列)中,并在必要时进行剪裁(这是默认值)。wrap:项目按相同方向换行到下一行或下一列。wrap-reverse:项目反方向绕到下一行(或列)。例如,如果flow-direction是row(从左到右),第二行将从右到左。
flex-direction和flex-wrap可以合并成一个包含方向和环绕选项的flex-flow属性。例如,flex-flow: row wrap将水平对齐项目,并允许换行到下一行。
Note
方向指定主轴,水平或垂直。有几个属性可用于配置对齐;有些影响沿主轴的对齐,有些适用于副轴。我将假设使用了row方向来讨论这些。这是最常见的,使用这些术语会更容易理解。CSS 属性和值是有意通用的,使用像开始/结束这样的词,而不是顶部/底部或左侧/右侧。如果您使用的是column方向,属性和值同样适用,但是描述它们的词语会有所不同(例如,顶部对齐而不是左侧对齐)。
以下属性可用于进一步配置项目的对齐方式:
justify-content:主轴,影响一行内项目的水平间距align-items:短轴,影响一行内的垂直对齐align-content:短轴,影响行与行之间的垂直间距
您可以指定项目沿主轴对齐的方式,这将决定如何分配任何额外的空间。justify-content属性支持以下选项:
flex-start:默认。项目靠左对齐(如果使用列方向,则靠上对齐)。所有多余的空间都在末尾。flex-end:项目右对齐(或下对齐)。center:物品放在中间,首尾分开多余的空间。space-between:第一个项目左对齐,最后一个项目右对齐,多余的空间分布在项目之间的空间。space-around:类似于space-between,除了在第一个项目之前和最后一个项目之后也增加了额外的空格。
align-items属性指定项目如何在短轴上对齐。例如,如果flow-direction是row,那么justify-content属性描述了水平间距是如何排列的。然而,align-items属性指定了行内的项目如何垂直对齐。例如,如果行中的项目具有不同的高度,您可以选择对齐顶部或底部。以下是可用的选项:
flex-start:默认。这会对齐顶部。flex-end:这将底部对齐。center:这使每个项目居中,在顶部和底部之间均匀分布额外的空间。stretch:将项目拉伸到行中最大项目的大小。baseline:使用基线对齐项目。
Tip
基线一词来自印刷业,在印刷业中,字符沿着基线对齐。这不一定是文本的底部,因为字体有衬线以及上标和下标。但是,基线为放置字符提供了直观的指导,因此线条看起来是直的。在 CSS 中,这个概念甚至更复杂,因为我们不仅仅是在处理文本。如果你想深入研究这个课题,这里有一篇很好的文章: www.smashingmagazine.com/2012/12/17/css-baseline-the-good-the-bad-and-the-ugly 。
如果有多行项目,align-content属性指定如何在这些行周围放置额外的垂直间距。这支持与justify-content相同的选项:flex-start、flex-end、center、space-between和space-around。它还支持stretch选项。
配置项目
有几个属性可以应用于子项,这些属性会影响子项的对齐方式。您可以为flex-grow和flex-shrink属性分配一个数值。这表示该项相对于其他项可以增大或缩小多少。例如,值为 2 的项目的增长是值为 1 的项目的两倍。此外,flex-basis属性用于指示项目初始大小的基础。这是一个数值,表示要使用的初始值。如果设置为默认值auto,这将是项目的实际宽度。
您也可以为order属性指定一个数值。默认情况下,项目按照它们在 HTML 内容中出现的顺序显示。然而,order属性,如果使用的话,将会覆盖它。
正如我之前解释的,沿着短轴的对齐是由align-items属性决定的。但是,这可以在一个或多个项目上被覆盖。例如,如果您将 al ign-items设置为flex-start,这将使它们沿顶部对齐。您可以通过设置单个项目的align-self属性来覆盖它。这与align-items属性采用相同的值。
Note
你可以在 www.w3.org/TR/css-flexbox-1 找到完整的规格。在撰写本文时,它处于工作草案状态。通过一些直观的例子可以更好地理解其中的一些概念。这里有一篇很好的文章演示了这些技巧: https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Flexible_boxes 。
调整链接
现在,您将修改您的示例页面,以使用 flexbox 修复链接对齐。将以下内容添加到style标签的末尾。这将配置ul元素以水平显示元素,并在必要时启用换行。链接也将是左对齐的。链接的宽度被设置为auto,因此它可以容纳长和短的元素。
nav ul
{
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
}
nav ul li a
{
width: auto;
}
如果链接需要包装,您还需要固定nav元素的大小,以便它可以垂直增长。height当前被设置为30px。将height属性更改为min-height,如下面的代码所示。这将设置初始高度为30px,但允许它增长以适应内容。
/* Make the radius half of the height */
nav
{
min- height: 30px;
border-radius:15px;
}
保存您的更改并刷新网页。尽可能缩小窗口。即使在这个尺寸下,页面布局看起来还是不错的,如图 6-14 所示。

图 6-14。
The web page with its smallest size
当结合媒体查询时,flexbox 工作得很好。一个例子是当窗口宽度较小时,将链接的方向配置为垂直。尝试在您的style元素末尾输入以下内容来演示这一点:
@@media screen and (max-width: 400px)
{
nav ul
{
flex-direction: column;
}
}
如果你将页面缩小到足够小,链接会垂直对齐,如图 6-15 所示。

图 6-15。
Using vertical links
使用灵活的图像
如果您有大的图像,您可能会发现它们被裁剪。为了防止这种情况,将max-width属性设置为 100%。这将导致调整图像大小以适合容器的宽度。这不是在媒体查询中完成的,这种格式将应用于所有分辨率。例如,您可以指定以下内容来配置电话亭图像:
#phone
{
max-width: 100%;
height: auto;
}
将height设置为auto将改变高度以保持现有的纵横比。您可以使用下面的样式规则对video元素做同样的事情:
.video embed, .video object, .video iframe
{
width: 100%;
height: auto;
}
在移动设备上查看页面
最后一个测试,使用 Chrome 显示站点,并启用 Ripple 模拟器,就像我之前演示的那样。选择 PhoneGap 平台。你的页面应该如图 6-16 所示。

图 6-16。
The web page as seen on the Ripple emulator
摘要
在本章中,我向您展示了如何安装和使用几个移动设备模拟器,其中包括:
- Opera 移动仿真器
- 铬波纹附加组件
- iPhone 模拟器
为了处理各种形状因素,媒体查询被用来根据窗口宽度有选择地应用样式。您实现了大、中、小布局,它们可以随着窗口大小的调整而整齐地缩放。此外,通过将宽度设置为 100%,您可以自动调整图像和视频的大小。最后,您使用 flexbox 来动态排列导航链接。
七、支持旧浏览器
现在你有了在第四章中创建的这个漂亮的、符合 HTML5 的网页。你想炫耀它,所以你给一个碰巧还在使用 IE 8 的同事发了一个链接,他们看到了类似图 7-1 的东西。

图 7-1。
The CSS demo as shown in IE 8
这个页面看起来很糟糕,一点也不像你所期待的。你肯定不会因此获得任何奖项。不要被吓到,你把链接发给你的老板,事情变得更糟。你的老板正在使用 IE 7,看到类似图 7-2 的东西。

图 7-2。
The CSS demo as shown in IE 7
侧边栏不再在边上,而是被钉在页面的底部。你的老板开始想知道你在业余时间都做了些什么。你刚从惨痛的教训中学到了两个重要的教训。
- 始终控制您的演示环境;在这种情况下,让他们在您的浏览器上看到该页面。
- 更重要的是,在几种不同的浏览器上测试你的网站。
在这一章中,我将向你展示一些相当简单的技术,让你的页面即使在老版本的浏览器上看起来也是最好的。您不必编写太多代码,因为有许多开源代码,您可以轻松地将它们添加到您的站点中。
做一些简单的改变
有几个非常简单的改变会让网页看起来更好。您将从这些开始,稍后我将向您展示一些更复杂的解决方案。
模拟旧浏览器
要使用一些旧版本的 Internet Explorer 测试您的网页,您将在模拟器模式下使用 IE 11。启动 Internet Explorer 后,按 F12 显示开发人员工具窗格。默认情况下,浏览器会使用“edge”模式,这是最新版本。要改变这一点,点击边缘下拉菜单,如图 7-3 所示。

图 7-3。
Modifying the emulation mode
对于本章,使用你在第四章 (Visual Studio 版本)中创建的同一个项目,你可以从 www.apress.com 下载。
使用 Modernizr
当支持旧的浏览器时,您应该做的第一件事是使用 Modernizr 开源 JavaScript 库。这个库执行两个基本功能。
- 检测当前浏览器的可用功能,并将此信息作为可查询属性提供。例如,在您的 JavaScript 中,您可以像这样放置条件逻辑:
if (!Modernizr.cssanimations) {
alert("Your browser does not support CSS animation");
}
- 提供垫片来实现缺失的功能。这包括 html5shim 库,它允许您使用新元素,如
header、footer、nav和aside,对内容进行样式化。
Tip
欲了解更多信息,请访问 Modernizr 网站 http://modernizr.com 。
因此,让我们将 Modernizr 库添加到您的页面中,看看会发生什么!在第五章中,我简要解释了作为客户端包管理器的 Bower 工具。现在,您将看到它的实际应用。因为这个项目是使用空模板创建的,所以您需要安装 Bower。从工具菜单转到 NuGet PackageManager。在搜索框中输入鲍尔,选择鲍尔,如图 7-4 所示。单击安装按钮开始安装。

图 7-4。
Adding Bower to the project
现在您还需要创建bower.json文件。从解决方案浏览器中,右键单击第七章项目,并选择 Add 和 New Item 链接。选择 Bower JSON 配置文件选项,如图 7-5 所示。文件名应该默认为bower.json。

图 7-5。
Adding the bower.json file.
在以下代码中添加粗体显示的行:
{
"name": "Chapter``7
"private": true,
"dependencies": {
"modernizr": "2.8.3"
}
Tip
正如我在第五章中解释的,当你编辑这个文件时,Visual Studio 提供了智能感知。从列表中选择modernizr并输入冒号后,将显示当前版本。截至本文撰写之时,它是 2.8.3。您应该使用 IntelliSense 显示的最新版本。
您可以安装 Grunt 或 Gulp,并设置一个任务来自动更新 Modernizr 文件,并将其复制到wwwroot文件夹中。为简单起见,您将手动执行此操作。在 Solution Explorer 中,展开Dependencies文件夹并右键单击 Bower 项。选择恢复包链接。这将强制下载最新版本。
在 Solution Explorer 中,右键单击wwwroot文件夹,选择 Add 和 New Item 链接,并输入名称 lib。然后右键单击Dependencies\Bower文件夹中的 modernizr 项,并选择在文件浏览器中打开链接。这将在此位置打开 Windows 资源管理器。你应该看到一个modernizr.js文件;将它复制到解决方案资源管理器中的wwwroot\lib文件夹。
现在安装了 Modernizr,您可以在页面中包含 Modernizer 库,方法是将它添加到您的Index.html文件的顶部,就在DOCTYPE标签之后:
<script type="text/javascript" src="lib/modernizr.js"></script>
使用 Internet Explorer 显示页面;然后转到开发者工具窗格,将浏览器模式更改为 IE 7,就像我前面解释的那样。你的页面应该如图 7-6 所示。

图 7-6。
The demo page with Modernizr as viewed in IE 7
请注意,现在显示了边框和背景颜色,这大大有助于使页面看起来像最初设计的那样。此外,导航链接是水平排列的。
Note
垫片是一种薄的物体,通常由木头制成,用于填充两个物体之间的间隙。在这种情况下,该术语指的是填补浏览器当前功能和完整 HTML5 规范之间空白的相对较小的一段代码。术语 shim 已经在软件开发界使用了很长时间。引入术语 polyfill 是为了指代与浏览器相关的填充。因此,在这种情况下,这两个术语是同义的。
添加更多聚合填充
现在你可能开始感觉好一点了。通过添加 Modernizr,页面看起来不错。但是,经过仔细检查,有一个相当长的功能列表不起作用,包括以下内容:
- 桌子
- 圆角
- 渐变背景填充
- 条纹物品
- 动画
- 3D 转换
- 多列
如果有足够的耐心和毅力(当然,还有时间),你可能会实现所有这些功能,这样你的页面在 IE 7 和最新版本的 Chrome 上都是一样的。但是,我不建议你这么做。本质上,你应该确保你的页面在最新的 HTML5 兼容浏览器上运行良好,并且在旧的浏览器上运行良好。它不需要在每个浏览器上都运行良好。请考虑以下几点:
- 大多数用户不会在多种浏览器上浏览你的网站,并比较每种浏览器的体验。您的页面不需要在每个浏览器中看起来都一样。
- 如果有人正在使用 IE 7,他们已经习惯了难看的网站。实现其中的几个聚合填充可能会让你的页面成为他们访问过的最好的站点之一。
- HTML5 应该让你作为 web 开发人员的工作更容易。然而,如果你试图让每一页都像旧浏览器上的原生 HTML5 一样工作,你将花费更多的时间,而不是更少。
对于您的页面使用的、通常使用的浏览器本身不支持的每个功能,您有以下选项:
- 失败:简单地显示一个错误,说明该浏览器不支持必要的功能,并提供一些建议使用的浏览器。例如,您在第五章中创建的示例站点的主要目的是演示如何使用 Web 工作器。如果网页是用不支持 Web 工作器 的浏览器浏览的,那么让网页正常工作就没有意义了。失败就好!
- Polyfill:实现替代解决方案以提供所需的功能。这可以是简单的解决方案,也可以是相当复杂的。例如,如果不支持渐变填充,您可以只使用纯色填充,或者您可以提供一个填充并使用 JavaScript 实现渐变。
- 忽略:只保留未实现的特性。例如,您可以忽略圆角;在旧的浏览器中,它们会是方形的角。
这里没有硬性规定;你需要根据具体情况来决定哪些功能对你来说是重要的,以及你愿意花多少时间让它们在老版本的浏览器上工作。在本章的其余部分,我将演示一些技术,使用公开可用的开源垫片来回填这些特性。然而,我不想给你留下这样的印象,你必须回填每一个特性。事实上,这个演示中的几个特性,包括多列支持、3D 转换和动画都将被忽略,因为它们相对来说比较难或者不那么重要。
Tip
有过多的垫片和多孔填料可供选择。如果你正在寻找一些特定的东西,这篇文章提供了一个很好的参考: https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills 。请记住,这些可能并不总是正常工作,所以测试他们,并保持什么工作。此外,组合各种垫片可以创建一些有趣的结果,因为一个垫片的副作用可以破坏另一个垫片。
显示表格
当你在几个浏览器中测试你的页面时,注意那些不能正常工作的特性,然后确定它们的优先级。在这种情况下,aside元素应该在主要内容的旁边,而不是在页面的末尾。我认为,这是最关键的问题,因此应该首先解决。
Tip
IE 8 首先支持表格。如果你把浏览器模式改成 IE 8,你会看到侧边栏就在主要内容旁边。所以,表格支持只是 IE 7 和更老版本的问题。你可能会考虑简单地忽略这个问题,并解释说你的网站在 IE 8 和更新的版本上工作得最好。要想知道有多少用户会受到影响,请查看最新浏览器在 www.w3schools.com/browsers/browsers_stats.asp 的统计数据。根据这些统计数据,这仅占正在使用的浏览器总数的 0.1%。这些统计数据代表总体使用情况;您可能有特定的目标受众,他们可能有不同的特征。
为了支持 IE 7 中的表格,您将使用行为 CSS 扩展,它允许您在样式表中嵌入 JavaScript。通过添加如下规则来调用扩展:
header
{
behavior: url(customBehavior.htc);
}
该实现在扩展名为.htc的 HTML 组件(HTC)文件中提供。有一些关于使用.htc文件的事情你应该知道。
- 通常,您可以在浏览器中打开 HTML 文件,而无需使用 IIS。例如,您可以简单地用 Internet Explorer(或任何浏览器)打开
Index.html文件,页面就会正常工作。但是,如果页面实际上不是由 web 服务器(如 IIS 或 Apache)提供的,则.htc文件会被忽略。 - 您可能需要在 web 服务器上定义 HTC 内容类型。默认情况下,IIS 和 IIS Express 都支持这一功能,但是您可能需要在 Apache 或其他 web 服务器上添加这一功能。
- 尽管通常在 CSS 文件中引用
.htc文件,但是在behavior属性中指定的 URL 必须相对于调用样式表的 HTML 文档的位置。如果你把.htc文件放在css文件夹中(和所有其他样式表一起),你需要用一个相对路径css/customBehavior.htc来引用它。
为了显示表格,您将使用可以从 http://tanalin.com/en/projects/display-table-htc 下载的开源 HTC。
EXERCISE 7-1. SUPPORTING TABLESIn the Solution Explorer, right-click the wwwroot folder and select the Add and New Folder links. Enter the name css. Download the latest .zip file from this site: http://tanalin.com/en/projects/display-table-htc . (The latest file as of this writing is display-table.htc_2011-11-25.zip.) This file contains an uncompressed and a minimized version. Copy the display-table.htc file to the css folder in the Solution Explorer. Open the Index.html and find the portion where the table is defined. Add the code shown in bold from Listing 7-1 to the existing style rules. This specifies a vendor-prefixed version of the display attribute and invokes the display-table.htc component. Listing 7-1. Defining a New Table
/* Setup a table for the content and sidebar */
#contentArea
{
display: table;
-dt-display: table;
behavior: url(css/display-table.htc);
}
#mainContent
{
display: table-cell;
-dt-display: table-cell;
padding-right: 2px;
behavior: url(css/display-table.htc);
}
aside
{
display: table-cell;
-dt-display: table-cell;
width: 280px;
behavior: url(css/display-table.htc);
}
Save your changes and view the Index.html page in Internet Explorer. Change the emulator mode to IE 7, and you should now see a table set up, as shown in Figure 7-7.

图 7-7。
The table support added in IE 7 Note
display-table.htc文件使用自己的、非标准的、特定于供应商的前缀。因此,您需要添加–dt-display属性。您也可以忽略因此而产生的警告。
Now there’s one more thing that needs to be fixed. You’ll notice that the aside element is missing some styles such as background-color and padding. This is a side effect of the CSS extension. To create the table in your page, this code created real table elements for you such as tr and td. So, once the JavaScript runs, the aside element is removed and replaced with rows and cells. Since there is no aside element anymore, you can’t use an element selector to style it. However, there is only one aside element in your source document, and it has the sidebar id attribute. Close the browser. Replace all aside selectors with #sidebar, including the one you just added. There are several places in the Index.html file that you’ll need to change. View the page again and change the emulator mode to IE 7. The sidebar should now have a background color, and there is also padding around the text.
添加圆角
如果浏览器不支持圆角,您可以通过 Dave Methvin 编写的 jQuery 插件轻松添加圆角。除了圆角,该插件还可以创建许多其他图案,这些图案显示在 http://jquery.malsup.com/corner 处。这是通过开源许可证提供的,因此您可以免费下载并在您的应用中使用它。
您将使用这个插件来实现nav和footer元素的圆角。但是,只有在圆角本身不受支持的情况下,才应该这样做。所以,第一个问题是,如何知道浏览器是否支持圆角?答案还是现代化。添加这样的语句将有条件地调用自定义方法:
if (!Modernizr.borderradius)
Tip
在本章的后面,我将向你展示另一种圆角技术。
EXERCISE 7-2. ADDING ROUNDED CORNERSGo to http://jquery.malsup.com/corner . Click the jquery.corner.js link near the top of the page. This will download the latest version. Save the file in your wwwroot\lib folder in the Solution Explorer.
注意,这个插件不能通过 Bower 获得,所以您需要以传统的方式下载并安装它。
This function is based on jQuery, so you’ll also need to reference that in your page. Open the bower.json file and add the lines shown in bold (don’t forget to add the comma at the end of the previous line).
{
"name": "Chapter``7
"private": true,
"dependencies": {
"modernizr": "2.8.3" ,
"jquery": "2.1.4",
"jquery-validation-unobtrusive": "3.2.2"
}
}
After saving the bower.json file, you should see a jquery item in the Dependencies\Bower folder. Like you did for Modernizr, right-click the jquery item and select the Open in File Explorer link. Go to the dist subfolder and copy the jquery.js file to the wwwroot\lib folder in the Solution Explorer. In the same way, copy the jquery.validation.unobtrusive.js file to the wwwroot\lib folder. Open the Index.html file and add these references near the top of the page, just after the Modernizr script:
<script type="text/javascript" src="lib/jquery.js"></script>
<script type="text/javascript" src="lib/jquery.corner.js"></script>
Now invoke this by adding this script element at the end of the Index.html file, after the footer element and just before the body closing tag.
<script type="text/javascript">
if (!Modernizr.borderradius) {
$("nav").corner("15px");
$("footer").corner("25px");
}
</script>
This code uses the jQuery selector to find the nav and footer elements and calls the corner() method specifying the radius. Save your changes and view the page using Internet Explorer. Switch the emulator mode to IE 7, and your page should look like Figure 7-8.

图 7-8。
The demo page with rounded corners
添加渐变
接下来,您将使用 PIE(progressive Internet Explorer)的另一个开源解决方案向 intro 部分添加渐变背景。这是作为 HTC 文件实现的,就像您之前添加的表支持一样。一旦下载了组件,只需使用样式表规则的 behavior 属性调用它。
EXERCISE 7-3. ADDING BACKGROUND GRADIENTSGo to the http://css3pie.com site and click the Download button. This will download a PIE-1.0.0.zip file (you may see a different version number; just download the latest version). There are several files inside this .zip file. Copy the PIE.htc file to your wwwroot\css folder. From Solution Explorer, right-click the wwwroot\css folder and click the Add and Existing Item links. Navigate to the css folder and select the PIE.htc file. Open the Index.html file and find where the rules for the .intro class are defined. Add the following lines shown in bold. This code will add another vendor-prefixed attribute (-pie-) and then invoke the PIE component using the behavior property.
/* Gradients */
.intro
{
border: 1px solid #999999;
text-align:left;
margin-top: 6px;
padding-left: 15px;
border-radius:25px;
background-image: linear-gradient(45deg, #ffffff, #6699cc);
-pie-background: linear-gradient(45deg, #ffffff, #6699cc);
behavior: url(css/PIE.htc);
}
Save your changes, view the page using Internet Explorer, and switch the emulator mode to IE 7. You should now have a linear gradient that looks just like the native gradient. You might have also noticed that the corners are rounded as well. The PIE.htc shim also supports rounded corners and took care of that for you. Note
PIE 旨在回填几个 CSS3 特性,本文列举了: http://css3pie.com/documentation/supported-css3-features 。它将尝试处理引用PIE.htc垫片的元素中包含的任何这些特性。但是,它不会对本机支持的功能做任何事情。
该页面现在应该如图 7-9 所示。

图 7-9。
The demo page with a gradient background
将书单分条
回想一下第四章中的,图书列表是使用:nth-child选择器设计的,所以交替的元素会有不同的背景。在不支持这一功能的旧浏览器中,您可以用传统的方式来实现,即在 JavaScript 中迭代列表,并改变交替元素的样式。
然而,关键在于确定:nth-child选择器是否可用,因为 Modernizr 不提供这个功能。在撰写本文时,Modernizr 正在开发包含这一功能的版本 3 beta。更多详情,请查看 http://v3.modernizr.com/download 的功能列表。
Note
这里提供的解决方案基于 Lea Verou 的一篇文章。然而,我不得不调整它以适应 IE。更多详情,请查看 http://lea.verou.me/2011/07/detecting-css-selectors-support-my-jsconf-eu-talk/ 的文章。
EXERCISE 7-4. STRIPING THE BOOK LISTOpen the Index.html file and add the following code to the script element at the top of the file:
function supportsSelector(selector) {
var el = document.createElement('div');
el.innerHTML = ['', '<style>', selector, '{}', '</style>'].join('');
try
{
el = document.body.appendChild(el);
var style = el.getElementsByTagName('style')[0],
ret = !!(style.sheet.rules || style.sheet.cssRules)[0];
}
catch(e){
ret = false;
}
document.body.removeChild(el);
return ret;
}
This code creates a new style element and adds the selector in question. It then checks to see whether it is actually there. If not, the selector is not supported. This is done in a try/catch block in case older browsers do not support either the style.sheet.rules or style.sheet.cssRules property. Now with your handy supportsSelector() function, you can implement the manual striping technique. Add the following code to the script element at the bottom of the file after the existing function you added for the rounded corners:
if (!supportsSelector(":nth-child(2n+0)")) {
var titles = document.getElementById("titles");
var articles = titles.getElementsByTagName("article");
for (var i = 0; i < articles.length; i++) {
var title = articles[i];
if (i % 2) {
title.style.background = "#6699cc";
title.style.border = "1px solid #c0c0c0";
}
else {
title.style.background = "#c0c0c0";
title.style.``bor
}
}
}
If the :nth-child selector is not supported, this code gets the #titles element using the getElementById() function. This is the section element that contains a series of article elements, one for each book. It then gets an array of child article elements using the getElementsByTagName() function. Note that this method is invoked on the titles object and not the document object. Once it has the array of elements, the code simply iterates the array, modifying the background and border properties. Save your changes and view the page using the IE 7 emulation mode. The page should look like Figure 7-10.

图 7-10。
The aside element with manual striping
隐藏不支持的元素
正如本章前面所述,对于每一个不支持的特性,你需要决定这是否是一个交易破坏者,页面是否需要失败,你是否想要填充这个特性,或者你是否想要在旧的浏览器上忽略它。从最初的不支持的更改列表中,还有三项您尚未实现:
- CSS 动画
- 3D 转换
- 多列
您可以通过使用 JavaScript 在计时器到期时改变背景图像来轻松实现动画。在我们有 CSS 动画之前,这是通常的做法。然而,在旧的浏览器中实现 3D 变换是行不通的。我认为这两个功能都很好,但是不值得这么麻烦,所以如果浏览器不支持这些功能,我们就把它们去掉。
最好模仿的一个特性是多列支持。对此有垫片可用,如 GitHub 提供的这种: https://github.com/gryzzly/CSS-Multi-column-Layout-Module-Polyfill/blob/master/index.html 。也许有足够的时间和耐心,你可以得到一些工作,但这是其中一个艰难的决定。值得努力吗?在某些特殊的情况下,可能是这样,但是一般来说,你不应该把 80%的时间花在一个只会影响百分之几的预期观众的功能上。
但是,您应该考虑的一件事是隐藏没有功能的元素。例如,月亮的静态图片不是很有趣,所以您将通过将其大小设置为 0 来隐藏该元素。
EXERCISE 7-5. HIDING ELEMENTSAdd the following code to the script element at the bottom of the Index.html file:
if (!Modernizr.cssanimations) {
document.getElementById("moon").style.width = "0px";
document.getElementById("moon").style.height = "0px";
}
Finally, after all this work, you should try the page in a browser that supports all these features to make sure it still looks great there. The final version in Chrome should look like Figure 7-12.

图 7-12。
The final demo page as shown in Chrome This code simply shrinks the moon div if CSS animations are not supported. View the page in Internet Explorer and switch the emulator mode to IE 7. The final web page should look like Figure 7-11.

图 7-11。
The final demo page as shown in IE7 Tip
源代码下载包含完整的Index.html文件。如果对具体应该如何或在哪里进行更改有任何疑问,请参考本指南。
摘要
在这一章中,我向你展示了一些让你的网页看起来很棒的技巧,即使是在不支持 HTML5 新特性的旧浏览器上。这些技术包括以下内容:
- 使用 Modernizr 进行特性检测和基本元素支持
- 显示表格
- 添加圆角
- 支持渐变背景图像
- 手动条带化列表
- 隐藏不支持的元素
对于每个不受支持的功能,您需要确定以下事项:
- 该特性是否对页面至关重要(如果是,页面应该失败)
- 该特征是否容易聚合填充
- 该功能是否可以忽略
这是一种平衡行为,因为您希望页面在所有浏览器中都很好看,但又不想花费过多的时间来支持每种可能的浏览器。
演示页面的最终实现达成了一个很好的妥协。该网站看起来很棒,功能正常。虽然省略了一些新的 HTML5 功能,但考虑到浏览器支持,总体来说它仍然是一个很好的网站,额外的工作也很少。
在下一章,我将向你展示如何使用 HTML5 中引入的新的audio和video e元素。
八、音频和视频
在这一章中,我将演示 HTML5 中引入的新的audio和video元素。这两个元素在它们的属性以及它们支持的方法和事件方面是相同的。我将用这一章的大部分时间来讨论和演示audio元素,但请记住,我向你展示的一切也适用于视频。本章末尾的一些练习将把这些相同的技术应用到video元素上,这样你就可以自己看到了。
我将演示如何使用浏览器提供的本地控件添加audio和video元素。这种方法使得在网站中嵌入音频和视频成为添加一些简单标记的小事。然而,如果你想写你自己的控件,本章也将演示如何去做,以及如何用 JavaScript 连接所有的事件。
因为每个浏览器支持不同的媒体格式,所以您可能需要对媒体文件的多个版本进行编码。然而,现在大多数主流浏览器都支持 MP3 和 MP4 格式,所以这不再是个问题。audio和video元素可以支持多个来源,因此每个浏览器都可以选择合适的版本来使用。
Note
video元素支持audio元素不支持的三个附加属性(width、height和poster)。我将在本章后面解释这些。
使用音频元素
我将从一个非常简单的练习开始,向网页添加一个audio元素。然后,您将支持多种格式,并在各种 bowsers 上尝试您的站点。
创建示例项目
在本章中,您将创建一个网站项目,您将使用它来尝试 HTML5 的audio和video元素。现在,您将创建一个空网站,然后在本章中逐步向其添加功能。
EXERCISE 8-1. ADDING AUDIO TO A PAGEStart Visual Studio 2015 and click New Project. Select the ASP.NET Web Application project template and enter Chapter 8 for the name. In the second dialog, select the ASP.NET 5 Empty project. In Solution Explorer, right-click the wwwroot folder and click the Add and New Folder links. Enter the name Media. You’ll need an MP3 file to use as a sample audio clip. The file I’m using is copyrighted, so I can’t include it with the source code. You should be able to find one on your computer or download one from the internet. You can also rip a CD through Windows Media Player and select MP3 as the format. Drag the MP3 file from Windows Explorer to the wwwroot\Media folder in Visual Studio. Now you’ll add the web page that you’ll be working on throughout this chapter. From Solution Explorer, right-click the wwwroot folder and click the Add and New Item links. In the Add New Item dialog, select HTML Page and enter the name Index.html, as shown in Figure 8-1.

图 8-1。
Adding the Index.html page Open the Index.html file. In the empty body that was created by the file template, create a div element. Inside that div, enter <audio src=, and you should see a link that you can use to select the source from a file in your project. Select the Media folder and then your MP3 file, as shown in Figure 8-2.

图 8-2。
Using a link to select the source Add the autoplay attribute and close the audio element. Add text inside the audio element like this: <body> <div> <audio src="Media/Linus and Lucy.mp3" autoplay> <p>HTML5 audio is not supported on your browser</p> </audio> </div> </body> Save your changes, make sure Internet Explorer is chosen as your default browser for debugging, and browse the Index.html page. Open the Startup.cs file and comment out the implementation of the Configure() method. Once the page has loaded, your audio clip should start playing. The page, however, will be blank. Press F12 to open the Developer Tools pane, if not already opened. Change the browser mode to IE 8. The music will stop, and you’ll see the “HTML5 audio is not supported on your browser” text displayed.
第一个练习演示了audio元素的基本用法。您只需输入src属性,它指定音频文件的 URL。当浏览器不支持audio元素时,使用audio元素内的内容。由于 IE 8 不支持audio元素,因此显示包含在p标签中的文本。您可以利用这一点简单地显示一条消息,就像您在这里所做的那样。但是,您可以使用它来提供一个下载文件的链接,或者使用一个插件来实现一个后备解决方案。
使用本机控件
就 UI 而言,基本上有三个选项:
- 无控件:音频会播放,但用户没有可用的控件。当使用
autoplay属性加载页面时,剪辑可以自动开始。您还可以使用 JavaScript 开始、暂停和停止音频剪辑。 - 本机控件:浏览器为用户提供播放、暂停和停止音频剪辑以及控制音量的控件。
- 定制控件:页面提供了通过 JavaScript 与
audio元素交互的定制控件。
要启用本地控件,只需像这样添加controls属性:
<audio src="∼/Media/Linus and Lucy.mp3" autoplay``controls
保存您的更改并浏览到您的页面,本地控件应该类似于图 8-3 所示。

图 8-3。
Displaying the native audio controls in Internet Explorer
在 Opera 和 Chrome 中,控件看起来像图 8-4 。

图 8-4。
The audio controls in Opera
在 Firefox 中,控件看起来如图 8-5 所示。

图 8-5。
The audio controls in Firefox
在 Safari 中,音频控制看起来像图 8-6 。

图 8-6。
The audio control in Safari Tip
Windows 上的 Safari 要求安装 QuickTime 以支持audio元素。你可以从这个网站下载: https://support.apple.com/kb/DL837?locale=en_US 。安装 QuickTime 后,您可能需要重新启动电脑,Safari 才能工作。
如您所见,每个浏览器中的控件都有不同的样式。使用原生控件,您几乎无法控制音频控件的显示方式。您可以通过设置style属性来改变宽度,这将拉伸进度条。超出正常高度只会在控件顶部增加空白,如图 8-7 所示。然而,在 IE 中,降低高度会缩小控件;在 Chrome 中,它会对其进行剪辑。

图 8-7。
Extending the size of the native controls
查看浏览器支持
虽然所有主流浏览器都支持audio元素,但它们并不都支持相同的音频格式。直到最近,浏览器通常要么支持 MP3,要么支持 Vorbis,你需要两者都提供。然而,现在大多数浏览器都支持 MP3 和 MP4 格式的视频。如果需要,HTML5 提供了一种提供多种格式的方法来支持旧的浏览器。
Tip
这里有一个方便的页面测试浏览器对audio和video元素的支持: http://hpr.dogphilosophy.net/test/ 。它还概述了对各种浏览器的支持。
audio元素允许您指定多个源,浏览器将遍历这些源,直到找到它支持的一个。不使用src属性,而是在audio元素中提供一个或多个source元素,如下所示:
<audio autoplay controls>
<source src="Media/Linus and Lucy.ogg" />
<source src="Media/Linus and Lucy.mp3" />
<p>HTML5 audio is not supported on your browser</p>
</audio>
浏览器将使用它支持的第一个源,因此如果这对您很重要,您应该首先列出首选文件。例如,Chrome 同时支持 MP3 和 Vorbis 格式。如果您希望使用 MP3 文件,您应该在。ogg文件。
虽然像这样只列出源代码是可行的,但是浏览器必须下载文件并打开它,看看它是否能够播放。下载了一个相当大的文件却发现它无法使用,这不是很有效率。您还应该包括type属性,它指定了资源的类型。然后,浏览器可以通过查看标记来确定该文件是否受支持。type属性像这样指定 MIME 格式:
<source src="Media/Linus and Lucy.ogg"``type="audio/ogg"
<source src="Media/Linus and Lucy.mp3"``type="audio/mp3"
您也可以在type属性中指定编解码器,如下所示:
<source src="Media/Linus and Lucy.ogg" type='audio/ogg; codecs="vorbis"' />
这将帮助浏览器更有效地选择一个兼容的媒体文件,我将在本章后面解释。请注意,编解码器值包含在双引号中,因此您需要在type属性值两边使用单引号。现在,您将修改您的网页,以便它也可以在其他浏览器上工作。
EXERCISE 8-2. ADDING MULTIPLE SOURCESCreate a Vorbis-encoded audio file of your sample audio clip that has the .ogg extension and copy this to the wwwroot\Media folder.
提示我使用了一个叫做 XMedia Recode 的工具,你可以在 http://www.xmedia-recode.de/download.html 下载。您可以使用此实用程序来格式化音频和视频文件。安装此应用后,运行它,单击功能区中的打开文件按钮,然后选择 MP3 文件。在“格式”选项卡上,选择 OGG 格式。请注意,文件扩展名选项自动设置为。ogg并且音频选项卡上的编解码器选项设置为 Vorbis。单击功能区中的“添加到队列”按钮。选择“队列”选项卡,查看已定义的转换该文件的作业。在窗口底部,您可以指定新文件的保存位置。点击浏览按钮,导航到Chapter 8 \Media文件夹。最后,单击 Encode 按钮启动作业。将显示一个对话框,显示作业的进度。
In Solution Explorer, right-click the Media folder and click the Add and Existing Item links. Navigate to the Chapter 8 \wwwroot\Media folder and select the .ogg file that you just encoded. In the Index.html file, replace the audio element with the following code (you’ll need to adjust the actual file name to match yours): <audio autoplay controls > <source src="Media/Linus and Lucy.ogg" type="audio/ogg" /> <source src="Media/Linus and Lucy.mp3" type="audio/mp3" /> <p>HTML5 audio is not supported on your browser</p> </audio> Save your changes and browse to your page. Open the page using several browsers and verify that the controls are displayed and the audio starts playing when the page is loaded.
构建您自己的控件
所有的 DOM 元素和事件都在 JavaScript 中可用,所以创建自己的控件来处理audio元素是一个相当简单的过程。然而,有几个方面你需要控制,所以这不是一个微不足道的练习。您需要解决三个方面的问题:
- 播放/暂停
- 显示进度和快进/快退
- 调节音量/静音
您需要响应来自自定义控件和audio元素的事件。在本练习中,您将从向页面添加所有必要的控件开始。然后,我将向您展示如何实现每个领域所需的事件处理程序。您将用来控制音频元素的输入元素如下:
- 播放/暂停按钮:标签将根据音频元素的状态在“播放”和“暂停”之间切换。
- 寻找:这是一个
range控件(在第二章和第三章中介绍),既可以显示进度,也可以让用户寻找特定的位置。 - Duration:这是一个
span元素,显示音频文件的当前位置和总时长。 - 静音按钮:标签将在“静音”和“取消静音”之间切换
- 音量:这是另一个用于指定音量级别的
range控件。
您将为其提供处理程序的audio事件包括:
onplay:音频开始时触发onpause:音频暂停时引发onended:音频完成时触发ontimeupdate:随着音频剪辑的播放周期性地触发ondurationchanged:当持续时间改变时引发,这发生在加载文件时onvolumnechanged:当音量改变或静音属性改变时触发
添加自定义控件
您将从添加自定义控件和定义所需的事件处理程序开始。然后我将解释您将用来实现它们的 JavaScript。
EXERCISE 8-3. ADDING CUSTOM CONTROLSOpen the Index.html file and remove the controls attribute. This will cause the audio element to be hidden. Add the following div after the audio element. This will include all the input elements that you’ll need to control the audio. <div id="audioControls"> <input type="button" value="Play" id="play" onclick="togglePlay()" /> <input type="range" id="audioSeek" onchange="seekAudio()" /> <span id="duration"></span> <input type="button" id="mute" value="Mute" onclick="toggleMute()" /> <input type="range" id="volume" min="0" max="1" step="any" onchange="setVolume()" /> </div> Modify the audio element to add the necessary event handlers by adding the code shown in bold. This also defines the id attribute that you’ll use to access the audio and source elements. <audio id="audio" autoplay onplay="updatePlayPause()" onpause="updatePlayPause()" onended="endAudio()" ontimeupdate="updateSeek()" ondurationchange="setupSeek()" onvolumechange="updateMute()" > <source src="Media/Linus and Lucy.ogg" type="audio/ogg" id="oggSource" /> <source src="Media/Linus and Lucy.mp3" type="audio/mp3" id="mp3Source" /> <p>HTML5 audio is not supported on your browser</p> </audio> In Visual Studio, change the debug browser to use Opera. (You can also use Chrome if you prefer.) Save your changes and browse to your page. The page should look like Figure 8-8.

图 8-8。
The custom audio controls Close the browser and stop the debugger.
实现事件处理程序
现在,您已经准备好实现事件处理程序了。我将围绕三个主要方面(播放、搜索和音量)对它们进行分组,并一次解释一个部分。
支持播放和暂停
将清单 8-1 中所示的代码添加到外层div之后,就在body元素结束之前。
Listing 8-1. The Initial script Element
<script type="text/javascript">
var audio = document.getElementById("audio");
function setupSeek() {
var seek = document.getElementById("audioSeek");
seek.min = 0;
seek.max = Math.round(audio.duration);
seek.value = 0;
var duration = document.getElementById("duration");
duration.innerHTML = "0/" + Math.round(audio.duration);
}
function togglePlay() {
if (audio.paused || audio.ended) {
audio.play();
}
else {
audio.pause();
}
}
function updatePlayPause() {
var play = document.getElementById("play");
if (audio.paused || audio.ended) {
play.value = "Play";
}
else {
play.value = "Pause";
}
}
function endAudio() {
document.getElementById("play").value = "Play";
document.getElementById("audioSeek").value = 0;
document.getElementById("duration").innerHTML = "0/" + Math.round(audio.duration);
}
</script>
这段代码首先声明引用audio元素的audio变量。因为大多数函数都使用它,所以获取一次并将其存储在所有函数都可以访问的变量中会更有效。
第一个方法setupSeek()被调用以响应ondurationchange事件。当页面第一次加载时,它不知道音频剪辑有多长,直到打开文件并加载元数据。一旦加载了元数据,就可以确定持续时间,并引发事件。duration属性以秒表示。setupSeek()函数使用duration属性来设置audioSeek范围控件的max属性。它也用于设置span元素的初始值。请注意,调用了Math.round()函数来将该值四舍五入为最接近的整数。
当用户点击播放按钮时,调用togglePlay()方法。如果audio元素的当前状态是暂停或结束,它调用play()函数。否则,它调用pause()方法。updatePlayPause()方法设置播放按钮的标签。如果音频当前正在播放,文本将更改为“暂停”,因为这将是单击按钮的结果。否则,文本被设置为“播放”
Tip
togglePlay()函数响应播放按钮被点击,updatePlayPause()函数响应audio元素被启动或暂停。当点击按钮时,togglePlay()方法将改变audio元素的状态。这种状态变化将引发一个onplay或onpause事件,这两个事件都由updatePlayPause()函数处理。这样做是因为可以通过点击播放按钮以外的方式来播放或暂停音频。例如,如果您保留了controls属性,那么您将同时拥有本地控件和自定义控件。响应onplay和onpause事件确保按钮标签总是正确的,不管audio元素是如何操作的。
最后,当音频播放完毕时,调用endAudio()函数。这执行一些同步,包括设置按钮标签和初始化range和span控件。
支持进步和寻求
接下来,将清单 8-2 中所示的函数添加到同一个script元素中。
Listing 8-2. Functions to Support the range Control
function seekAudio() {
var seek = document.getElementById("audioSeek");
audio.currentTime = seek.value;
}
function updateSeek() {
var seek = document.getElementById("audioSeek");
seek.value = Math.round(audio.currentTime);
var duration = document.getElementById("duration");
duration.innerHTML = Math.round(audio.currentTime) + "/" + Math.round(audio.duration);
}
就像播放按钮一样,有一个事件处理程序seekAudio()响应输入元素,还有一个单独的事件处理程序updateSeek()响应audio元素。当用户移动range控件上的滑块时,调用seekAudio()函数。它只是使用由range控件选择的值来设置currentTime属性。
当audio元素引发ontimeupdate事件时,调用updateSeek()函数。这将更新range控件,以反映文件中的当前位置。它还更新了span控件来显示实际位置(以秒为单位)。同样,currentTime属性被四舍五入为最接近的整数。
控制音量
最后一组功能用于支持音量控制和静音按钮。将清单 8-3 中所示的代码添加到您一直在使用的同一个script元素中。
Listing 8-3. Controlling the Volume
function toggleMute() {
audio.muted = !audio.muted;
}
function updateMute() {
var mute = document.getElementById("mute");
if (audio.muted) {
mute.value = "Unmute";
}
else {
mute.value = "Mute";
}
}
function setVolume() {
var volume = document.getElementById("volume");
audio.volume = volume.value;
}
顾名思义,toggleMute()函数切换audio元素的muted属性。当这被改变时,onvolumechange事件由audio元素引发。updateMute()函数响应该事件,并根据muted属性的当前值设置按钮标签。同样,这样做可以确保按钮标签是正确的。
最后,当用户移动第二个range控件上的滑块时,调用setVolume()函数。它将audio元素的volume属性设置为在range控件上选择的内容。
Note
volume属性的值介于 0 和 1 之间。你可以认为这是 0%和 100%。当您定义range控件时,min属性被设置为 0,max被设置为 1,因此比例是正确的。您可以简单地使用范围值设置volume属性。如果要显示volume属性的实际值,只需将其转换成百分比即可。
现在,您已经准备好尝试自定义控件了。保存您的更改并浏览到您的页面。该页面应类似于图 8-9 。

图 8-9。
The completed custom controls
您没有提供任何样式规则,因此控件使用默认样式显示。但是现在你可以访问单独的控件了,所以你可以按照你想要的方式来排列和样式化它们(关于 CSS 样式的使用,请参考第四章)。
更改音频源
在这个例子中,音频源是在标记中定义的。但是,您可以使用 JavaScript 轻松控制这一点。如果您像最初一样使用单个的src属性,那么您只需要修改这个属性来引用一个不同的文件。然而,如果你使用多个source元素,你需要更新所有这些元素,然后调用load()方法。
为了演示这一点,当剪辑完成时,您将把源更改为第二个音频剪辑,并播放该音频剪辑。
EXERCISE 8-4. CHANGING THE AUDIO SOURCEClose the browser and stop the debugger. Create another audio file in both .mp3 and .ogg formats and copy these to the wwwroot\Media folder. In Solution Explorer, right-click the wwwroot\Media folder and click the Add and Existing Item links. Navigate to the Chapter 8 \wwwroot\Media folder and select both the .mp3 and .ogg files that you just encoded. Open the Index.html file. At the top of the existing script element, declare the following variable. This will be used to keep track of how many songs were played. var songCount = 0; Add the following code shown in bold to the endAudio() function. If the audio clip that just finished is the first one, the code will change the src attribute to reference the second file. The file is then loaded and played. (Note you’ll need to change these file names to use the actual files that you included in your project.) function endAudio() { document.getElementById("play").value = "Play"; document.getElementById("audioSeek").value = 0; document.getElementById("duration").innerHTML = "0/" + Math.round(audio.duration); if (++songCount < 2) { document.getElementById("oggSource").src = "Media/Sample.ogg"; document.getElementById("mp3Source").src = "Media/Sample.mp3"; audio.load(); audio.play(); } } Save your changes and browse to your page. The initial song should start playing. To save some time, fast-forward the clip to almost the end of the file and then wait for it to finish. The second file should automatically start playing. You should notice the span control was updated to show the duration of the second file.
检测音频支持
您可以通过调用audio元素上的canPlayType()方法,传入type属性,以编程方式确定浏览器是否支持某个源。为了演示这一点,将以下代码添加到script块的开头,在audio变量声明之后:
var sources = audio.getElementsByTagName("source");
for (var i = 0; i < sources.length; i++) {
alert("[" + sources[i].type + "] - " + audio.canPlayType(sources[i].type));
}
这段代码遍历所有的source元素,并显示一个弹出窗口,指示该类型是否受支持。保存您的更改,并尝试在 Opera 中运行。您应该会看到如图 8-10 和 8-11 所示的结果。

图 8-11。
The canPlayType( ) results for audio/ogg

图 8-10。
The canPlayType( ) results for audio/mp3
你觉得怎么样?你问一个是或否的问题,然后得到一个“可能”或“也许”的回答。事实证明,canPlayType()函数既不返回“否”也不返回“是”。相反,它返回“可能”、“可能”或空字符串。空白字符串可以解释为“不”。我们来谈谈“可能”和“可能”
源文件是一个容器,除了实际数据之外,它还提供有关媒体的元数据。指定像 audio/ogg 这样的 MIME 类型仅仅表示容器的类型,而没有明确说明数据是如何编码的(使用什么编解码器)。如果浏览器支持该容器类型,canPlayType()返回“可能”没有证据表明它不受支持,但也不能确定它是否受支持。如果容器类型不受支持,如 audio/mp3,则返回一个空字符串(意味着它不受支持)。
Note
空字符串用于表示“不支持”,因为在 JavaScript 中,空字符串是假值,而“否”是真值。所以,你可以编码if (canPlayType(type)),只有“可能”和“大概”的结果会被选中。
无论如何,用canPlayType()方法,“可能”是你能得到的最好的结果;你从来没有得到一个“是”表 8-1 列出了可能的响应。
表 8-1。
canPlayType( ) Method Responses
| 多媒体数字信号编解码器 | 支持容器 | 不支持容器 | | --- | --- | --- | | 不明确的 | “也许” | 空白的 | | 支持 | “大概” | 空白的 | | 不支持 | 空白的 | 空白的 |了解视频格式
正如我在本章开始时所说的,video元素的工作方式和audio元素一样,所以到目前为止你所学到的一切也适用于视频。
查看浏览器支持
视频文件通常包含音频和视频,所以我前面提到的所有音频类型和编解码器仍然适用。此外,视频部分可以各种方式编码。幸运的是,该行业似乎正在缩小到三种主要形式:
- MP4 (*.mp4):使用 H.264 视频编码和 MP3 音频编码
- WebM (*。webm):使用 VP8 视频编码和 Vorbis 音频编码
- Ogg (*。ogv):使用 Theora 视频编码和 Vorbis 音频编码
表 8-2 列出了目前主流浏览器支持的格式。
表 8-2。
Video/Audio Codec Support
| 浏览器 | MP4 | WebM | Ogg | | --- | --- | --- | --- | | IE 11 | 是 | 不 | 不 | | 火狐浏览器 | 是 | 是 | 是 | | 铬 | 是 | 是 | 是 | | 旅行队 | 是 | 不 | 不 | | 歌剧 | 是 | 是 | 是 |如您所见,MP4 格式已经成为所有主流浏览器的标准。但是,与音频一样,您可以用多种格式对视频文件进行编码,并提供多个来源,以便浏览器可以选择它支持的来源。
下载示例视频
在本章的剩余部分,我将使用一个演示视频,你可以通过这个网站从微软下载: http://ie.microsoft.com/testdrive/graphics/videoformatsupport/default.html 。这是大巴克兔子电影的预告片。这个网站提供了这个视频剪辑的几个版本,所以你可以看到不同的浏览器是如何显示它们的。
使用 Internet Explorer 打开此网站。右键单击 H.264 基线配置文件视频,然后单击“视频另存为”链接。浏览到Chapter 8 \wwwroot\Media文件夹,将文件另存为BigBuckBunny.mp4。使用 Chrome 或 Opera 打开同一个站点,右键单击 WebM 版本。点击“视频另存为”链接,浏览到Chapter 8 \wwwroot\Media文件夹。将此另存为BigBuckBunny.webm。
使用视频元素
现在您已经有了一些视频文件,您将向您的应用添加一个video元素。您将像处理audio元素一样添加两个源。在第一个练习中,您将使用本地控件并在几个浏览器中测试应用。然后,您将添加像音频控件一样实现的自定义控件。
向演示页面添加视频
添加视频很简单,只需将媒体文件添加到项目中,然后在页面中添加一些简单的标记来播放视频。
EXERCISE 8-5. ADDING A VIDEO ELEMENTIn Solution Explorer, right-click the wwwroot\Media folder and click the Add and Existing Item links. Select both versions of the Big Buck Bunny video files. In the Index.html file, remove the autoplay attribute from the audio element. You will autoplay the video, and removing this will keep them from both playing at the same time. Add the following markup, just after the div that contains the audio controls: <video id="video" autoplay controls loop> <source src="Media/BigBuckBunny.webm" type="video/webm" /> <source src="Media/BigBuckBunny.mp4" type="video/mp4" /> <p>HTML5 video is not supported on your browser</p> </video> Save your changes and browse to your page. Try it in several browsers to make sure the video can be played in each.
对于video元素,您包括了autoplay、controls和loop属性。在处理audio元素时,您已经使用了autoplay和controls属性。loop属性将导致视频结束时从头开始。这也受到audio元素的支持。
Tip
您之前在音频源上使用的canPlayType()方法也可以用于视频文件。它的工作方式是一样的,要么返回一个空字符串表示该文件不受支持,要么返回一个“可能”或“很可能”的结果。
添加自定视频控制
现在您将为video元素添加自定义控件,就像您为audio元素所做的那样。
EXERCISE 8-6. ADDING CUSTOM VIDEO CONTROLSModify the markup in the Index.html file by replacing the video element with this: <video id="video" onplay="updatePlayPauseVideo()" onpause="updatePlayPauseVideo()" onended="endVideo()" ontimeupdate="updateSeekVideo()" ondurationchange="setupSeekVideo()" onvolumechange="updateMuteVideo()"> <source src="Media/BigBuckBunny.webm" type="video/webm" /> <source src="Media/BigBuckBunny.mp4" type="video/mp4" /> <p>HTML5 video is not supported on your browser</p> </video> Add a new div after the video element and enter the following markup for it: <div id="videoControls"> <input type="button" value="Play" id="playVideo" onclick="togglePlayVideo()" /> <input type="range" id="videoSeek" onchange="seekVideo()" /> <span id="durationVideo"></span> <input type="button" id="muteVideo" value="Mute" onclick="toggleMuteVideo()" /> <input type="range" id="volumeVideo" min="0" max="1" step="any" onchange="setVolumeVideo()" /> </div> Add a new script element using the code shown in Listing 8-4. This code is identical to the script used for the audio element except it uses the video element and the video controls.
清单 8-4。视频控件的 JavaScript 函数
<script type="text/javascript">
var video = document.getElementById("video");
function setupSeekVideo() {
var seek = document.getElementById("videoSeek");
seek.min = 0;
seek.max = Math.round(video.duration);
seek.value = 0;
var duration = document.getElementById("durationVideo");
duration.innerHTML = "0/" + Math.round(video.duration);
}
function togglePlayVideo() {
if (video.paused || video.ended) {
video.play();
}
else {
video.pause();
}
}
function updatePlayPauseVideo() {
var play = document.getElementById("playVideo");
if (video.paused || video.ended) {
play.value = "Play";
}
else {
play.value = "Pause";
}
}
function endVideo() {
document.getElementById("playVideo").value = "Play";
document.getElementById("videoSeek").value = 0;
document.getElementById("durationVideo").innerHTML = "0/"
+ Math.round(video.duration);
}
function seekVideo() {
var seek = document.getElementById("videoSeek");
video.currentTime = seek.value;
}
function updateSeekVideo() {
var seek = document.getElementById("videoSeek");
seek.value = Math.round(video.currentTime);
var duration = document.getElementById("durationVideo");
duration.innerHTML = Math.round(video.currentTime) + "/"
+ Math.round(video.duration);
}
function toggleMuteVideo() {
video.muted = !video.muted;
}
function updateMuteVideo() {
var mute = document.getElementById("muteVideo");
if (video.muted) {
mute.value = "Unmute";
}
else {
mute.value = "Mute";
}
}
function setVolumeVideo() {
var volume = document.getElementById("volumeVideo");
video.volume = volume.value;
}
</script>
This code is almost identical to the code you added earlier to support the custom audio controls. Save your changes. Select Opera as the debug browser and browse to your page. Try the video controls, which should look like Figure 8-12.

图 8-12。
The video element and controls
添加海报
属性由video元素支持(但不支持audio元素)。在视频开始之前,您可以使用poster属性来指定显示的图像。如果没有指定,浏览器通常会打开视频并显示第一帧。要添加海报,只需将图像包含在项目中,并在poster属性中引用它。
然而,有一件事需要小心。如果你定义了一个海报,video元素的初始大小将是海报图像的大小。如果这与视频不同,视频开始播放时,大小会发生变化。您应该确保图像大小相同,或者明确调整video元素的大小,这将拉伸(或收缩)海报图像以适合它。
EXERCISE 8-7. ADDING A POSTER IMAGEDownload an image file to use as the poster image. You can find images from this site: http://wiki.creativecommons.org/Case_Studies/Blender_Foundation . Save the picture in the Chapter 8 \wwwroot\Media folder and name it BBB_Poster.png. In Solution Explorer, right-click the wwwroot\Media folder and click the Add and Existing Item links. Browse to the Chapter 8 \wwwroot\Media folder and select the BBB_Poster.png image. Modify the markup of the video element by adding the poster, width, and height attributes shown in bold. <video id="video" poster="Media/BBB_Poster.png" width="852" height="480" Save your changes and browse to your page. You should now see the poster image until the video is started, as shown in Figure 8-13.

图 8-13。
The video element showing the poster image
摘要
在本章中,您创建了一个简单的 web 页面,展示了audio和video元素的特性。浏览器已经标准化了音频的 MP3 格式和视频的 MP4 格式。然而,通过提供多种格式的媒体文件,可以实现额外的支持,这样浏览器就可以使用它所支持的格式。这很容易做到,而且相当有效,因为浏览器通常可以从标记中确定下载哪个文件。
我向您展示了如何创建自己的控件来播放、暂停和查找音频和视频文件。通过连接一些简单的 JavaScript 事件处理程序,制作一个定制的媒体播放器变得非常简单。
在下一章,我将演示如何利用 HTML5 中的可缩放矢量图形(SVG)。
九、可缩放矢量图形
在这一章中,我将向你展示如何使用 Visual Studio、ASP.NET MVC 和 SQL Server 在 HTML5 web 应用中使用可缩放矢量图形(SVG)。使用 SVG 可以做很多非常酷的事情。我挑选了一个有趣的演示,可以很容易地应用于许多业务应用。但是首先,让我给你介绍一下什么是 SVG。
大多数人认为图形元素是某种形式的位图,由像素的行和列组成,每个像素都分配有特定的颜色。然而,相比之下,矢量图形将图像表示为公式的集合。例如,画一个圆心在点 x,y,半径为 r 的圆。更复杂的图像被定义为图形元素的集合,包括圆、线和路径。虽然渲染引擎将最终确定需要设置的特定像素,但图像定义是基于公式的。这一根本区别为使用矢量图形提供了两个显著的优势。
首先,顾名思义,矢量图形是可缩放的。如果您想要扩大图像的大小,渲染引擎只需根据新的大小重新计算公式,不会损失清晰度。如果你放大位图图像,你会很快看到颗粒感,图像变得模糊。
第二,图像中的每个元素都可以独立操作。例如,如果图像中有几个圆圈,您可以通过简单地改变该图像的颜色来突出显示其中一个。由于矢量图形是基于公式的,您可以轻松地调整公式来修改图像。特别有用的是,这些元素可以使用 CSS 样式化,使用我在第四章中展示的强大的选择器和格式化功能。
SVG 简介
首先,您将创建一个使用简单几何形状来绘制图片的页面。然后,您将使用 CSS 对这些形状应用样式。我将向您展示如何将这些标记元素保存在一个.svg图像文件中。该图像文件可以像其他图像文件一样使用,例如。jpg和。png文件。
创建示例项目
您首先需要创建一个 Visual Studio 项目。这将使用与您在前面章节中使用的不同的项目模板。对于本章中的一个练习,您需要连接到 SQL Server 数据库。您将使用网站模板,而不是手动连接,它会为您完成大部分工作。
EXERCISE 9-1. CREATING THE VISUAL STUDIO PROJECTStart Visual Studio 2015. In the Start Page, click the New Project link. In the New project dialog box, select the ASP.NET Web Application template. Enter the project name Chapter 9 and select a location for this project. In the next dialog box, select the ASP.NET 5 Web Site template and make sure the “Host in the cloud” check box is not selected. Click the OK button, and the project will be created (this may take a minute). Right-click the Views\Home folder and click the Add and New Item links. In the Add New Item dialog box, select the MVC View Page template, enter the name Snowman.cshtml, and click the Add button.
添加一些简单的形状
为了演示svg元素是如何工作的,您将添加一些简单的形状,如圆形、矩形和线条。正如我将在这里演示的,大多数图像可以表示为几何形状的集合。
EXERCISE 9-2. ADDING A SNOWMANReplace the initial contents of the Snowman.cshtml file with the following: <svg xmlns:svg=" http://www.w3.org/2000/svg " version="1.2" width="100px" height="230px" xmlns=" http://www.w3.org/2000/svg " xmlns:xlink=" http://www.w3.org/1999/xlink "> </svg>
注意width和height属性定义了元素的内在维度。在 IE 9 中,你可以省略这些,页面会根据实际使用的空间正确显示。对于其他浏览器,如果没有指定width和height,图像将被裁剪为某个默认大小。
Inside the svg element, add the following elements. These are just simple shapes, mostly circle elements with a rectangle (rect), line, and polygon. <circle class="body" cx="50" cy="171" r="40" /> <circle class="body" cx="50" cy="103" r="30" /> <circle class="body" cx="50" cy="50" r="25" /> <line class="hat" x1="30" y1="25" x2="70" y2="25" /> <rect class="hat" x="40" y="10" width="20" height="15" /> <circle class="button" cx="50" cy="82" r="4" /> <circle class="button" cx="50" cy="100" r="4" /> <circle class="button" cx="50" cy="118" r="4" /> <circle class="eye" cx="42" cy="42" r="4" /> <circle class="eye" cx="58" cy="42" r="4" /> <polygon class="nose" points="45,60 45,50 60,55" /> A circle is expressed as a center point, cx and cy, and a radius, r. A line is specified as a beginning point, x1 and y1, and an endpoint, x2 and y2. A rectangle (rect) element is described by the top-left corner location, x and y, a width, and a height. A polygon is defined by a set of points in the form of x1,y1 x2,y2 x3,y3. You can specify any number of points. It is rendered by drawing a line segment between each of these points and a line segment from the last point, back to the first point. Open the HomeController.cs file (in the Controllers folder) and add the following action. This will allow you to navigate to the new view. public IActionResult Snowman() { return View("∼/Views/Home/Snowman.cshtml"); } Save your changes and press F5 to view the application. To get to the new page, add /Home/Snowman to the URL. The page should look like Figure 9-1 (you will also see the ASP.NET default header and footer on your page).

图 9-1。
The initial SVG image without styling
添加样式
这些元素的默认样式是纯黑色填充,并且因为这些形状中的一些在彼此之上,所以有几个当前是不可见的。请注意,您为每个元素分配了一个class属性。现在您将使用class属性为这些元素应用样式。将清单 9-1 中所示的代码添加到svg元素中,就在您之前添加的元素之前。
Listing 9-1. Adding SVG Styles
<style type="text/css" >
.body
{
fill: white;
stroke: gray;
stroke-width: 1px;
}
.hat
{
fill: black;
stroke: black;
stroke-width: 3px;
}
.button
{
fill: black;
}
.eye
{
fill: black;
}
.nose
{
fill: orange;
}
</style>
保存这些更改并刷新浏览器以查看更新后的网页,该网页应如图 9-2 所示。

图 9-2。
The SVG images with styling applied
使用 SVG 图像文件
除了嵌入一个svg元素之外,您还可以用。svg 扩展。该文件可以像其他图形图像一样使用。我将向您展示如何创建一个独立的 SVG 图像,然后在页面上使用它。
创建 SVG 图像
我将首先向您展示如何创建一个独立的.svg文件,然后使用它作为背景图像。这也将展示 SVG 图像的可伸缩性。
EXERCISE 9-3. CREATING AN SVG IMAGEFrom the wwwroot\images folder, click the New and File links. In the Add New Item dialog box, select Text File, enter the name snowman.svg, and click the Add button. Enter the following markup instructions: <?xml version="1.0" standalone="no"?> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.2//EN" " http://www.w3.org/Graphics/SVG/1.2/DTD/svg12.dtd "> Copy and paste the entire svg element from the Snowman.cshtml file, including the style element to the new text file. Click the Save button. To test your image, from Solution Explorer, right-click the snowman.svg file and click the “Open with” link. Then select Internet Explorer in the Open With dialog box. This should launch a browser and display the snowman image.
使用 SVG 背景
现在您有了一个图像文件,可以像使用其他图像一样使用它。为了演示这一点,您将向页面添加一个div元素,并将snowman.svg文件用作背景图像。您还将调整div的大小,这样您就可以看到调整大小时图像的样子。
EXERCISE 9-4. ADDING A BACKGROUND IMAGEIn the Snowman.cshtml file, add the following code after the svg element: <div id="container"></div> This simply defines a div element. Now you’ll use CSS to configure it. Add the following style element before the svg element: <style type="text/css"> #container { height: 800px; width: 350px; background-image: url("../img/snowman.svg"); background-size: contain; } </style> Press F5 to debug your application. In addition to the small image, you should also see a larger version of your image, as shown in Figure 9-3. Notice that there is no loss of image quality when expanding the size of the image.

图 9-3。
The page with the snowman background
创建交互式地图
绘制雪人的图片可能很有趣,但是让我们继续讨论 SVG 的一些更实际的用途。您将创建一个美国地图,每个州用一个单独的 SVG path 元素表示,我将在后面解释。您将在 SQL Server 数据库中存储路径定义。我将向您展示如何使用模型类访问数据库,然后使用视图定义显示它。一旦显示了地图,我将向您展示一些使用静态和动态样式来设置地图样式的 CSS 技巧。最后,您将添加一些动画来为您的 web 页面增添一点趣味。
使用路径元素
path元素是所有 SVG 元素中最通用的。它是“移动到”、“直线到”和各种“曲线到”命令的集合。该形状是按照路径命令绘制的。每个命令从当前位置开始,或者移动到新位置,或者绘制一条线到下一个位置。这里有一个例子:
- 移动到 25,50。
- 画一条线到 50,50。
- 画一条线到 50,25。
- 画一个弧线到 25,50。
这表示如下:
<path d="M25,50 L50,50 L50,25 A25,25 0 0,0 25,50 z" />
“移动到”和“行到”命令非常简单。“弧到”命令,以及所有其他曲线命令,更复杂,因为你需要提供额外的控制点,描述如何绘制曲线。每个命令使用一个字母,如表 9-1 所示。
表 9-1。
The Available Path Commands
| 命令 | 缩写 | 描述 | | --- | --- | --- | | 移到 | M | 移动到指定位置 | | 行到 | L | 在指定位置绘制一条线 | | 水平线至 | H | 在指定的 x 坐标处画一条水平线 | | 垂直线至 | V | 在指定的 y 坐标上画一条垂直线 | | 弧形到 | A | 在指定位置绘制弧线 | | 弯曲到 | C | 绘制三次贝塞尔曲线 | | 速记曲线到 | S | 绘制简化的三次贝塞尔曲线 | | 二次曲线到 | Q | 绘制二次贝塞尔曲线 | | 速记二次曲线到 | T | 绘制简化的二次贝塞尔曲线 | | 关闭路径 | Z | 通过在起始位置画一条线来闭合图形 |对于这些命令中的每一个,当使用绝对坐标时,使用大写字母。您还可以指定相对坐标,并使用小写字母来表示这些值相对于当前位置。有关构造路径元素的更多信息,请参见文章 http://www.w3.org/TR/SVG/paths.html#PathData 。
正如您可能想象的那样,绘制一个像阿拉斯加州这样的复杂形状需要很多命令。您不会想要手动编辑它。幸运的是,有工具可以帮助构建路径定义。例如,在 http://code.google.com/p/svg-edit 有一个免费的网络工具。笑一笑,列表 9-2 显示了阿拉斯加的path元素。
Listing 9-2. The Path Element Definition for Alaska
<path d="M 158.07671,453.67502 L 157.75339,539.03215 L 159.36999,540.00211 L 162.44156,540.16377 L 163.8965,539.03215 L 166.48308,539.03215 L 166.64475,541.94205 L 173.59618,548.73182 L 174.08117,551.3184 L 177.47605,549.37846 L 178.1227,549.2168 L 178.44602,546.14524 L 179.90096,544.52863 L 181.0326,544.36697 L 182.97253,542.91201 L 186.04409,545.01361 L 186.69074,547.92352 L 188.63067,549.05514 L 189.7623,551.48006 L 193.64218,553.25833 L 197.03706,559.2398 L 199.78529,563.11966 L 202.04855,565.86791 L 203.50351,569.58611 L 208.515,571.36439 L 213.68817,573.46598 L 214.65813,577.83084 L 215.14311,580.9024 L 214.17315,584.29729 L 212.39487,586.56054 L 210.77826,585.75224 L 209.32331,582.68067 L 206.57507,581.22573 L 204.7968,580.09409 L 203.98849,580.9024 L 205.44344,583.65065 L 205.6051,587.36885 L 204.47347,587.85383 L 202.53354,585.9139 L 200.43195,584.62061 L 200.91693,586.23722 L 202.21021,588.0155 L 201.40191,588.8238 C 201.40191,588.8238 200.59361,588.50048 200.10863,587.85383 C 199.62363,587.20719 198.00703,584.45895 198.00703,584.45895 L 197.03706,582.19569 C 197.03706,582.19569 196.71374,583.48898 196.06709,583.16565 C 195.42044,582.84233 194.7738,581.71071 194.7738,581.71071 L 196.55207,579.77077 L 195.09712,578.31582 L 195.09712,573.30432 L 194.28882,573.30432 L 193.48052,576.6992 L 192.34888,577.1842 L 191.37892,573.46598 L 190.73227,569.74777 L 189.92396,569.26279 L 190.24729,574.92094 L 190.24729,576.05256 L 188.79233,574.75928 L 185.23579,568.77781 L 183.13419,568.29283 L 182.48755,564.57462 L 180.87094,561.66472 L 179.25432,560.53308 L 179.25432,558.26983 L 181.35592,556.97654 L 180.87094,556.65322 L 178.28436,557.29986 L 174.88947,554.87495 L 172.30289,551.96504 L 167.45306,549.37846 L 163.41152,546.79188 L 164.70482,543.55866 L 164.70482,541.94205 L 162.92654,543.55866 L 160.01664,544.69029 L 156.29843,543.55866 L 150.64028,541.13375 L 145.14381,541.13375 L 144.49717,541.61873 L 138.03072,537.73885 L 135.92912,537.41553 L 133.18088,531.59573 L 129.62433,531.91905 L 126.06778,533.374 L 126.55277,537.90052 L 127.68439,534.99062 L 128.65437,535.31394 L 127.19941,539.67879 L 130.43263,536.93055 L 131.07928,538.54716 L 127.19941,542.91201 L 125.90612,542.58869 L 125.42114,540.64875 L 124.12785,539.84045 L 122.83456,540.97208 L 120.08632,539.19381 L 117.01475,541.29541 L 115.23649,543.397 L 111.8416,545.4986 L 107.15342,545.33693 L 106.66844,543.23534 L 110.38664,542.58869 L 110.38664,541.29541 L 108.12338,540.64875 L 109.09336,538.22384 L 111.35661,534.34397 L 111.35661,532.5657 L 111.51827,531.75739 L 115.88313,529.49413 L 116.85309,530.78742 L 119.60134,530.78742 L 118.30805,528.20085 L 114.58983,527.87752 L 109.57834,530.62576 L 107.15342,534.02064 L 105.37515,536.60723 L 104.24352,538.87049 L 100.04033,540.32543 L 96.96876,542.91201 L 96.645439,544.52863 L 98.908696,545.4986 L 99.717009,547.60018 L 96.96876,550.83341 L 90.502321,555.03661 L 82.742574,559.2398 L 80.640977,560.37142 L 75.306159,561.50306 L 69.971333,563.76631 L 71.749608,565.0596 L 70.294654,566.51455 L 69.809672,567.64618 L 67.061434,566.67621 L 63.828214,566.83787 L 63.019902,569.10113 L 62.049939,569.10113 L 62.37326,566.67621 L 58.816709,567.96951 L 55.90681,568.93947 L 52.511924,567.64618 L 49.602023,569.58611 L 46.368799,569.58611 L 44.267202,570.87941 L 42.65059,571.68771 L 40.548995,571.36439 L 37.962415,570.23276 L 35.699158,570.87941 L 34.729191,571.84937 L 33.112578,570.71775 L 33.112578,568.77781 L 36.184142,567.48452 L 42.488929,568.13117 L 46.853782,566.51455 L 48.955378,564.41296 L 51.86528,563.76631 L 53.643553,562.958 L 56.391794,563.11966 L 58.008406,564.41296 L 58.978369,564.08964 L 61.241626,561.3414 L 64.313196,560.37142 L 67.708076,559.72478 L 69.00137,559.40146 L 69.648012,559.88644 L 70.456324,559.88644 L 71.749608,556.16823 L 75.791141,554.71329 L 77.731077,550.99508 L 79.994336,546.46856 L 81.610951,545.01361 L 81.934272,542.42703 L 80.317657,543.72032 L 76.922764,544.36697 L 76.276122,541.94205 L 74.982838,541.61873 L 74.012865,542.58869 L 73.851205,545.4986 L 72.39625,545.33693 L 70.941306,539.51713 L 69.648012,540.81041 L 68.516388,540.32543 L 68.193068,538.3855 L 64.151535,538.54716``L``62.049939,539.67879 L 59.463361,539.35547 L 60.918305,537.90052 L 61.403286,535.31394 L 60.756645,533.374 L 62.211599,532.40404 L 63.504883,532.24238 L 62.858241,530.4641 L 62.858241,526.09925 L 61.888278,525.12928 L 61.079966,526.58423 L 54.936843,526.58423 L 53.481892,525.29094 L 52.835247,521.41108 L 50.733651,517.85452 L 50.733651,516.88456 L 52.835247,516.07625 L 52.996908,513.97465 L 54.128536,512.84303 L 53.320231,512.35805 L 52.026941,512.84303 L 50.895313,510.09479 L 51.86528,505.08328 L 56.391794,501.85007 L 58.978369,500.23345 L 60.918305,496.51525 L 63.666554,495.22195 L 66.253132,496.35359 L 66.576453,498.77851 L 69.00137,498.45517 L 72.23459,496.03026 L 73.851205,496.67691 L 74.821167,497.32355 L 76.437782,497.32355 L 78.701041,496.03026 L 79.509354,491.6654 C 79.509354,491.6654 79.832675,488.75551 80.479317,488.27052 C 81.125959,487.78554 81.44928,487.30056 81.44928,487.30056 L 80.317657,485.36062 L 77.731077,486.16893 L 74.497847,486.97723 L 72.557911,486.49225 L 69.00137,484.71397 L 63.989875,484.55231 L 60.433324,480.83411 L 60.918305,476.95424 L 61.564957,474.52932 L 59.463361,472.75105 L 57.523423,469.03283 L 58.008406,468.22453 L 64.798177,467.73955 L 66.899773,467.73955 L 67.869736,468.70951 L 68.516388,468.70951 L 68.354728,467.0929 L 72.23459,466.44626 L 74.821167,466.76958 L 76.276122,467.90121 L 74.821167,470.00281 L 74.336186,471.45775 L 77.084435,473.07437 L 82.095932,474.85264 L 83.874208,473.88268 L 81.610951,469.51783 L 80.640977,466.2846 L 81.610951,465.47629 L 78.21606,463.53636 L 77.731077,462.40472 L 78.21606,460.78812 L 77.407756,456.90825 L 74.497847,452.22007 L 72.072929,448.01688 L 74.982838,446.07694 L 78.21606,446.07694 L 79.994336,446.72359 L 84.197528,446.56193 L 87.915733,443.00539 L 89.047366,439.93382 L 92.765578,437.5089 L 94.382182,438.47887 L 97.130421,437.83222 L 100.84863,435.73062 L 101.98027,435.56896 L 102.95023,436.37728 L 107.47674,436.21561 L 110.22498,433.14405 L 111.35661,433.14405 L 114.91316,435.56896 L 116.85309,437.67056 L 116.36811,438.80219 L 117.01475,439.93382 L 118.63137,438.31721 L 122.51124,438.64053 L 122.83456,442.35873 L 124.7745,443.81369 L 131.88759,444.46033 L 138.19238,448.66352 L 139.64732,447.69356 L 144.82049,450.28014 L 146.92208,449.6335 L 148.86202,448.82518 L 153.71185,450.76512 L 158.07671,453.67502 z M 42.973913,482.61238 L 45.075509,487.9472 L 44.913847,488.91717 L 42.003945,488.59384 L 40.225672,484.55231 L 38.447399,483.09737 L 36.02248,483.09737 L 35.86082,480.51078 L 37.639093,478.08586 L 38.770722,480.51078 L 40.225672,481.96573 L 42.973913,482.61238 z M 40.387333,516.07625 L 44.105542,516.88456 L 47.823749,517.85452 L 48.632056,518.8245 L 47.015444,522.5427 L 43.94388,522.38104 L 40.548995,518.8245 L 40.387333,516.07625 z M 19.694697,502.01173 L 20.826327,504.5983 L 21.957955,506.21492 L 20.826327,507.02322 L 18.72473,503.95166 L 18.72473,502.01173 L 19.694697,502.01173 z M 5.9534943,575.0826 L 9.3483796,572.81934 L 12.743265,571.84937 L 15.329845,572.17269 L 15.814828,573.7893 L 17.754763,574.27429 L 19.694697,572.33436 L 19.371375,570.71775 L 22.119616,570.0711 L 25.029518,572.65768 L 23.897889,574.43595 L 19.533037,575.56758 L 16.784795,575.0826 L 13.066588,573.95097 L 8.7017347,575.40592 L 7.0851227,575.72924 L 5.9534943,575.0826 z M 54.936843,570.55609 L 56.553455,572.49602 L 58.655048,570.87941 L 57.2001,569.58611 L 54.936843,570.55609 z M 57.846745,573.62764 L 58.978369,571.36439 L 61.079966,571.68771 L 60.271663,573.62764 L 57.846745,573.62764 z M 81.44928,571.68771 L 82.904234,573.46598 L 83.874208,572.33436 L 83.065895,570.39442 L 81.44928,571.68771 z M 90.17899,559.2398 L 91.310623,565.0596 L 94.220522,565.86791``L
Tip
该数据以及所有其他状态的数据都是从 http://en.wikipedia.org/wiki/File:Blank_US_Map.svg 下载的。去 http://commons.wikimedia.org ,在搜索条件中输入 svg map,可以找到很多类似的素材。
实现初始地图
您将从创建没有应用任何样式的初始地图开始。实际的路径元素将存储在 SQL 数据库中。您将创建数据库,添加一个State表,并存储路径定义。然后,您将使用实体框架创建一个模型来提供状态数据。最后,您将创建一个显示地图的新视图,然后提供一个访问它的链接。
创建数据库
路径元素可能很长并且是静态的(阿拉斯加的形状不可能很快改变),因此它们可以存储在数据库中并由检索。NET 来呈现页面。您使用的 MVC 项目模板已经为数据库连接进行了配置。您需要创建State表,并用适当的路径定义填充它。
EXERCISE 9-5. CREATING THE STATE TABLEThe database used by .NET is not actually created until the first time it is accessed. The easiest way to create the database is to register yourself. Press F5 to debug the application. Click the Register link in the header and enter a username and password. Once the registration is done, you can close the browser window, which will also stop the debugger. Start SQL Server Management Studio (SSMS). In the Connect to Server dialog box, enter the server name as (LocalDB)\MSSQLLocalDB and use Windows authentication, as shown in Figure 9-4. Click the Connect button to open the database.

图 9-4。
Connecting to SQL Server After connecting, you should see the database in Object Explorer, as shown in Figure 9-5.

图 9-5。
The database contents If you don’t have SQL Server Management Studio, you can access the database through Visual Studio. From the View menu, click the SQL Server Object Explorer link. You can then navigate to your database, as shown in Figure 9-6.

图 9-6。
Selecting the Chapter9 database In the download that is available at www.apress.com , you’ll find a States.sql file in the Chapter9 folder. Open this file in SSMS and click the Execute button. This will create the State table using the following script and then populate it with a record for each state: CREATE TABLE State( Id int identity NOT NULL, StateCode nchar(10) NOT NULL, StateName nvarchar(50) NOT NULL, Path ntext NULL, CONSTRAINT PK_State PRIMARY KEY CLUSTERED ( Id ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] To verify the data was loaded correctly, open another query window using the New Query button. After connecting, select the Chapter9 database and execute this query: select * from State You should see results similar to Figure 9-7.

图 9-7。
The contents of the State table Tip
如果您使用的是 Visual Studio 而不是 SSMS,可以在服务器资源管理器中右键单击数据库,然后单击“新建查询”链接。然后在查询窗口中选择章节 9 数据库。
创建模型
创建一个使用 SQL 表的模型是一项非常简单的任务。您将使用实体框架来创建一个模型类,该类提供指定表中的数据。
EXERCISE 9-6. CREATING AN ENTITY FRAMEWORK MODELFrom Solution Explorer, right-click the Models folder and click the Add and New Item links. In the Add New Item dialog box, select the Class template and enter State.cs for the name. Add the following using statements at the top of the file: using System.ComponentModel.DataAnnotations; using Microsoft.Data.Entity; Add the following properties to the State class. These will map to the columns in the State table that you just created. [Key] public int Id { get; set; } public string StateCode { get; set; } public string StateName { get; set; } public string Path { get; set; } Add the following class in this same State.cs file, after the State class definition. This class configures the Entity Framework so it can create a State class for each record in the State table. public class StateDbContext : ApplicationDbContext { public DbSet<State> States { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder options) { options.UseSqlServer(Startup.Configuration .Get("Data:DefaultConnection:ConnectionString")); base.OnConfiguring(options); } } Open the Startup.cs class and add the static attribute to the Configuration property, as shown here in bold: public static IConfiguration Configuration { get; set; } You now have a model that you can use to provide the state details for the map. Rebuild the project. This will make the model available for linking to a view.
创建地图视图
已经定义了一个模型,现在您将创建显示模型元素的视图。
EXERCISE 9-7. CREATING THE MAP VIEWRight-click the Views\Home folder and click the Add and New Items links. In the Add New Item dialog box, select the MVC View Page template, enter the name Map.cshtml, and click the Add button. Replace the entire contents with the following. This will set up the view to use the State model and enable the code to access its properties. @model IEnumerable< Chapter 9 .Models.State> @using Chapter 9 .Models Add the following code, which defines an svg element just like you did earlier. It then uses a foreach loop to create a path element for each State defined in the model. Notice that it is storing the StateCode column in the id attribute and the StateName column in the class attribute. <svg xmlns:svg=" http://www.w3.org/2000/svg " version="1.2" width="959" height="593" id="map"> @foreach (State s in Model) { <path id="@s.StateCode.Trim()" class="@s.StateName.Trim()" d="@s.Path" /> } </svg> Now you’ll need to implement a controller action that will display the map view. Open the HomeController.cs class and add the following namespace: using Chapter 9 .Models; Then add the following method to the HomeController class. This executes a query to extract all the records from the State table and provide it to the view. public ActionResult Map() { StateDbContext DC = new StateDbContext(); var states = from s in DC.States select s; return View(states); } Now you’ll add a link that will display the map page. Go to the _Layout.cshtml file in the Views\Shared folder and add the following line after the existing asp-action statements: <li><a asp-controller="Home" asp-action="Map">Map</a></li> Press F5 to build and run the application. Click the Map link. You should see a map of the United States, and all of the states are filled with the default color (black).
设置状态元素的样式
现在所有的机械工作都完成了,你可以享受一下设计path元素的乐趣。正如我前面用雪人图像演示的那样,每个元素都可以使用特殊的样式表进行样式化。您还可以使用 JavaScript 动态地设计它们的样式。我将向您展示如何使用纯色填充、渐变和背景图像来格式化每个元素。
使用基本填充颜色
Note
在这一章中,你将使用不同的颜色来设计不同的状态。在本书的印刷版本中,其中一些颜色在转换为灰度时可能显示不佳。您可能希望完成练习或下载项目,以查看应用样式的结果。
您将从添加一些简单的填充规则开始。使用一个简单的元素选择器,将stroke颜色设置为黑色,将fill颜色设置为卡其色。然后,为了增加一些变化并演示如何使用属性选择器,您将根据州代码更改填充颜色。id属性包含两个字母的州代码,class属性包含州名。使用id属性的第一个字母,你将如下设置填充颜色:
- 甲:红色
- n:黄色
- 男:绿色
- 姜敏赫
- 奥:紫色
- I:橙色
在foreach循环之前的svg元素内输入清单 9-3 中所示的style元素。
Listing 9-3. Adding Basic Fill Definitions
<style type="text/css" >
path
{
stroke: black;
fill: khaki;
}
path[id^="A"]
{
fill: red;
}
path[id^="N"]
{
fill: yellow;
}
path[id^="M"]
{
fill: green;
}
path[id^="C"]
{
fill: blue;
}
path[id^="O"]
{
fill: purple;
}
path[id^="I"]
{
fill: orange;
}
</style>
刷新你的浏览器,地图现在应该看起来如图 9-8 所示。

图 9-8。
The map with some basic styling
当您在地图上移动鼠标时,最好突出显示鼠标当前指向的州。将以下规则添加到现有规则之后的style元素中:
path:hover
{
opacity: .5;
}
使用渐变填充
您可以对 SVG 元素使用渐变填充,但是它们的实现方式不同于典型的 HTML 元素。您首先必须定义渐变,然后使用 URL 引用它。
在svg元素内部和style元素之前添加以下defs元素:
<defs>
<linearGradient id="blueGradient"
x1="0%" y1="0%"
x2="100%" y2="100%"
spreadMethod="pad">
<stop offset="0%" stop-color="#ffffff" stop-opacity="1"/>
<stop offset="50%" stop-color="#6699cc" stop-opacity="1"/>
<stop offset="100%" stop-color="#4466aa" stop-opacity="1"/>
</linearGradient>
</defs>
defs元素用于定义文档中稍后可以引用的内容。在被实际引用之前,它不会做任何事情。在这里,您定义了一个linearGradient元素,并赋予它id blueGradient。您将使用id属性来引用它。
这些属性与你在《??》第四章中使用的渐变不同,但基本上完成了同样的事情。x1、y1、x2和y2属性定义了一个指定渐变方向的向量。在这种情况下,它将从左上角开始,到右下角。这指定了三个颜色值,用于定义起点、中点和终点的渐变颜色。
现在在 style 元素的末尾添加下面的path规则。这将使用怀俄明州的新渐变。
path[id="WY"]
{
fill: url(#blueGradient);
}
刷新浏览器,你应该会看到怀俄明州的渐变填充,如图 9-9 所示。

图 9-9。
Using a gradient fill
使用背景图像
您也可以使用图像文件作为形状背景。您需要首先在defs元素中将它定义为pattern,然后像处理渐变一样引用它。在本练习中,您将使用德克萨斯州的州旗图像,并将其作为该州的背景。
EXERCISE 9-8. USING A BACKGROUND IMAGEIn the source code download for Chapter 9 there is a TX_Flag.jpg file; copy this to the wwwroot\images folder in Solution Explorer. Add the following code to the defs element to define the background image. This specifies that the pattern should use the TX_Flag.jpg image file and stretch it to 377 x 226 pixels. This will make it large enough to cover the path element without needing to repeat. <pattern id="TXflag" patternUnits="objectBoundingBox" width="1" height="1"> <image xlink:href="∼/img/TX_Flag.jpg" x="0" y="0" width="377" height="226" /> </pattern> Add the following path rule, which will use the new pattern for the state of Texas. path[id="TX"] { fill: url(#TXflag); } Save your changes and refresh the browser. You should see the background image, as shown in Figure 9-10.

图 9-10。
Using a background image
因为这是关于 SVG 的一章,所以我觉得使用位图图像有点滑稽。当图像被拉伸时,您可以看到图像质量下降。德克萨斯州的州旗是最容易用 SVG 绘制的州旗之一,但是我想演示位图图像可以在 SVG 定义中使用。但是为了记录起见,清单 9-4 显示了用 SVG 表示的标志(这是从我前面提到的同一个 Wikimedia Commons 站点下载的,并稍微进行了重新格式化)。
Listing 9-4. The Texas State Flag in SVG
<rect width="1080" height="720" fill="#fff"/>
<rect y="360" width="1080" height="360" fill="#bf0a30"/>
<rect width="360" height="720" fill="#002868"/>
<g transform="translate(180,360)" fill="#fff">
<g id="c">
<path id="t" d="M 0,-135 v 135 h 67.5"
transform="rotate(18 0,-135)"/>
<use xlink:href="#t" transform="scale(-1,1)"/>
</g>
<use xlink:href="#c" transform="rotate(72)"/>
<use xlink:href="#c" transform="rotate(144)"/>
<use xlink:href="#c" transform="rotate(216)"/>
<use xlink:href="#c" transform="rotate(288)"/>
</g>
请注意,组元素g用于定义一条路径。这是旋转五个不同的角度,创造一个五角星。
用 JavaScript 改变样式
这种应用的主要用途之一是根据一些外部数据动态地设计每个元素的样式。例如,您可能希望突出显示销售地点所在的州。或者,您可能希望根据某种类型的人口统计数据(如人口)来设置颜色。到目前为止,您只使用了静态样式,但是您可以使用 JavaScript 轻松地设置样式。
在这个例子中,您将首先使用 JavaScript 将所有path元素上的fill属性设置为卡其色。这将替换设置默认颜色的 CSS 属性。这段代码将为弗吉尼亚州设置path元素的填充颜色。在实际的应用中,您通常会根据外部数据来定义样式。
这个练习还将向您展示如何使用 JavaScript 来响应onmouseover和onmouseout事件。您将替换path:hover规则,并使用这些事件处理程序来完成。
EXERCISE 9-9. ADJUSTING STYLES USING JAVASCRIPTAdd the following script element in the map.cshtml file, just before the defs element: <script type="text/javascript"> function adjustStates() { var paths = document.getElementsByTagName("path"); for (var i = 0; i < paths.length; i++) { paths[i].setAttributeNS(null, "fill", "khaki"); } var path = document.getElementById("VA"); path.setAttributeNS(null, "fill", "teal"); } </script> In the svg element, add the onload attribute using the code shown in bold: <svg xmlns:svg=" http://www.w3.org/2000/svg " version="1.2" width="959" height="593" id="map" onload="adjustStates()" > In the style element, remove the default khaki fill like this: path { stroke: black; /* fill: khaki; */ } Refresh the browser, and Virginia should no longer use the default color, as shown in Figure 9-11.

图 9-11。
Virginia styled with JavaScript Now you’ll also use JavaScript to implement the hover style. You can use the event.target property to get the path element that triggered the event. You can then determine the state code by accessing its id attribute. Add the following methods to the existing script element: function hoverState(e) { var event = e || window.event; var state = event.target.getAttributeNS(null, "id"); var path = document.getElementById(state); path.setAttributeNS(null, "fill-opacity", "0.5"); } function unhoverState(e) { var event = e || window.event; var state = event.target.getAttributeNS(null, "id"); var path = document.getElementById(state); path.setAttributeNS(null, "fill-opacity", "1.0"); } Then bind the mouseover and mouseout event handlers by adding the code shown in bold to the adjustStates() function. This uses the addEventListener() method to bind hoverState() and unhoverState() event handlers to each path element. function adjustStates() { var paths = document.getElementsByTagName("path"); for (var i = 0; i < paths.length; i++) { paths[i].setAttributeNS(null, "fill", "khaki"); paths[i].addEventListener("mouseover", hoverState, true); paths[i].addEventListener("mouseout", unhoverState, true); } var path = document.getElementById("VA"); path.setAttributeNS(null, "fill", "teal"); }
注意在 Internet Explorer 中,event对象不会传递给事件处理程序。相反,它通过全局window.event属性变得可用。通过如下设置事件变量,可以对事件处理程序进行编码,使其适用于任何一种模型:var event = e || window.event。这将使用传入的对象,如果可用,如果不可用,将使用全局window.event对象。然而,要做到这一点,您必须使用addEventListener()方法注册事件处理程序。您不能简单地设置onmouseover属性。
Remove the path:hover style rule like this: /* path:hover { opacity: .5; } */ Save your changes and refresh the browser. As you move the mouse around, the states should highlight just like they did with the path:hover style.
添加动画
像这样的地图的典型应用将允许用户选择一个区域,并作为选择的结果发生一些事情。该页面将根据所选择的项目显示一些信息。为了演示这一点,您将在用户单击一个状态时添加一些动画。这个例子将使用 3D 转换和 Opera,所以我将使用–webkit-供应商前缀。
我在第四章给你看的 CSS 动画对 SVG 元素不起作用。相反,您将使用 JavaScript 实现动画。当一个状态被选中时,您将首先复制所选的元素。然后你将使用一个计时器来逐渐改变它的旋转角度。你需要做一个拷贝,这样当图像旋转时就不会在地图上留下一个洞。此外,新元素将位于所有其他元素之上,因此您不必担心它会被其他元素隐藏。
一旦元素的副本完成动画,您就可以将它从文档中移除。然后您将显示一个警告,显示所选择的path的州代码和州名。
EXERCISE 9-10. ADDING ANIMATIONBecause this uses a 3D transform, you’ll need to set some of the transform properties on the path elements. Add the following rule to the style element: path { -webkit-perspective: 200px; -webkit-transform-style: preserve-3d; } Then add the code shown in Listing 9-5 to the script element.
清单 9-5。添加支持动画的功能
// Setup some global variables
var timer;
var stateCode;
var stateName;
var animate;
var angle;
function selectState(e) {
var event = e || window.event;
// Get the state code and state name
stateCode = event.target.getAttributeNS(null, "id");
stateName = event.target.getAttributeNS(null, "class");
// Get the selected path element and then make a copy of it
var path = document.getElementById(stateCode);
animate = path.cloneNode(false);
// Set some display properties and add the copy to the document
animate.setAttributeNS(null, "fill-opacity", "1.0");
animate.setAttributeNS(null, "stroke-width", "3");
document.getElementById("map").appendChild(animate);
angle = 0;
// Setup a timer to run every 10 msec
timer = setInterval(function () { animateState(); }, 10);
}
function animateState() {
angle += 1;
// If we've rotated 360 degress, stop the timer, destroy the copy
// of the element, and show an alert
if (angle > 360) {
clearInterval(timer);
animate.setAttributeNS(null, "visibility", "hidden");
var old = document.getElementById("map").removeChild(animate);
alert(stateCode + " - " + stateName);
return;
}
// Change the image rotation
animate.style.webkitTransform = "rotateY(" + Math.round(angle) + "deg)";
}
selectState()函数从选中的path元素中获取状态代码和状态名。然后它获取path元素并使用它的cloneNode(方法来复制它。因为鼠标当前位于所选路径上,所以它的不透明度将设置为 50%。因此,这段代码将副本的不透明度更改为 100%。它还设置了描边宽度,以便为该元素提供更宽的边框。然后将副本添加到文档中,并启动计时器来播放动画。
每隔十毫秒,调用一次animateState()函数,增加角度并重新绘制图像。如果旋转达到了 360 度,这个方法取消计时器并删除path元素的副本。它还会发出显示州代码和州名称的警报。
Add another event handler by adding the code shown in bold to the adjustStates() function. This will call the selectState() method when the user clicks a path element. function adjustStates() { var paths = document.getElementsByTagName("path"); for (var i = 0; i < paths.length; i++) { paths[i].setAttributeNS(null, "fill", "khaki"); paths[i].addEventListener("mouseover", hoverState, true); paths[i].addEventListener("mouseout", unhoverState, true); paths[i].addEventListener("click", selectState, true); } var path = document.getElementById("VA"); path.setAttributeNS(null, "fill", "teal"); } Change the debug browser to Opera. Press F5 to start the application and go to the map page. Click a state and you should see it fly off the page, as shown in Figure 9-12.

图 9-12。
Animating the selected state The image will then fly back into place, and an alert will appear, as shown in Figure 9-13.

图 9-13。
The alert showing the name of the selected state
摘要
在这一章中,我用几个相当简单的应用介绍了 SVG。SVG 图像由多个元素组成,这些元素可以是简单的元素,如直线、圆形和矩形,也可以是更复杂的选项,如多边形和路径。SVG 的关键特性是每个单独的元素都可以静态和动态地独立样式化。这实现了更好的控制和交互。此外,由于图像基于表达式,因此可以在不影响图像质量的情况下缩放图像。
在本章的练习中,您执行了以下操作:
- 使用简单的几何图形设计图像
- 创建了一个独立的
.svg图像文件 - 将地图显示为
path元素的集合 - 在 SVG 元素上实现动画
您还使用 Entity Framework 实现了一个访问 SQL Server 数据库的模型类,然后设计了一个使用模型元素创建 SVG 图像的视图。
在下一章,我将向您展示如何使用canvas元素在 HTML5 中构建图形元素。
十、画布
在这一章中,我将展示如何使用 HTML5 中的canvas元素来创建一些有趣的图形。正如您将看到的,它与您在前一章探索的 SVG 非常不同。稍后我将更详细地讨论它们的区别,但是您将注意到的主要事情是 canvas 完全是用 JavaScript 实现的。标记中唯一的部分是一个简单的元素定义,如下所示:
<canvas id="myCanvas" width="400" height="400">
Canvas is not supported on this browser
</canvas>
相反,您将通过使用 JavaScript 调用各种绘制方法来定义内容。就像audio和video元素一样,当浏览器不支持 canvas 时,使用canvas元素中的标记。您可以使用它来提供适当的回退内容。
通过本章的练习,你将创建三个不同的 canvas 实现,它们共同展示了 canvas 的功能。您将创建以下内容:
- 有移动棋子的棋盘
- 一个简单的太阳系模型
- 演示各种形状组合方式的页面
当然,您可以发挥您的想象力,将这些原则应用到许多有趣且引人注目的图形应用中。
创造一个棋盘
在第一个应用中,您将绘制一个棋盘,它只是一系列颜色交替的方块。我将向你展示如何使用渐变来使棋盘更有趣一点。您将使用图像文件在适当的方格中绘制棋子。最后,您将应用一点动画来移动棋盘上的棋子。这将让你在进入更高级的话题之前,对基本的绘画技巧有一个很好的了解。
创建 Visual Studio 项目
首先,您将创建一个 Visual Studio 项目,使用您在前面章节中使用的相同的空模板。
EXERCISE 10-1. CREATING THE VISUAL STUDIO PROJECTStart Visual Studio 2015. In the Start Page, click the New Project link. In the New project dialog box, select the ASP.NET Web Application template. Enter the project name Chapter 10 and select a location for this project. In the next dialog box, select the ASP.NET 5 Empty template. Click the OK button and the project will be created. Open the Startup.cs file and comment out the implementation for the Configure() method like this: public void Configure(IApplicationBuilder app) { //app.Run(async (context) => //{ // await context.Response.WriteAsync("Hello World!"); //}); } In Solution Explorer, right-click the wwwroot folder and click the Add and New Item links. In the Add New Item dialog box, select the HTML Page template, enter the name Index.html, and click the Add button.
canvas元素被恰当地命名,因为它提供了一个可以用来绘图的区域。当您创建一个canvas元素时,您使用height和width属性来定义它的大小。您可以通过标记或 CSS 指定其他属性来指定margin、padding和border。这些属性影响元素在页面中的位置。但是,您不能修改元素中的任何内容。元素本身简单地定义了一个空白区域,你可以在上面创建你的杰作。
当在 HTML 中创建一个canvas元素时,通常会分配一个id属性,这样就可以在 JavaScript 中使用getElementById()方法访问它。你不必这样做;你可以使用getElementsByTagName()方法或者使用我在第五章中描述的新的查询选择器来访问它。
一旦有了canvas元素,就可以通过调用getContext()来获得它的绘图上下文。您必须指定要使用的上下文。上下文指定了一组 API 函数和绘图功能。唯一普遍可用的是2d,我们将在本章中专门使用它。
Note
另一个可能的上下文并不像您所期望的那样是3d;它是WebGL,或者在某些浏览器中是experimental-webgl。这还没有准备好迎接黄金时间,而且它与2d的背景非常不同。
绘制矩形
与 SVG 不同,您可以直接绘制的唯一形状是矩形。你可以使用路径绘制更复杂的形状,我稍后会解释。有三种方法可以用来绘制矩形。
clearRect():清除指定的矩形strokeRect():在指定的矩形周围绘制一个无填充的边框fillRect():绘制一个填充矩形
这些方法中的每一个都有四个参数。前两个定义矩形左上角的 x 和 y 坐标。最后两个参数分别指定宽度和高度。绘图上下文具有strokeStyle和fillStyle属性,控制如何绘制边框或填充。在绘制矩形之前设置这些。设置后,所有后续形状都将使用这些属性绘制,直到您更改这些属性。
Tip
就像 SVG 一样,在 canvas 中,canvas元素的左上角有 0,0 的 x 和 y 坐标。
为了演示如何绘制矩形,您将从绘制棋盘开始,棋盘包含八行,每行八个正方形。
EXERCISE 10-2. DRAWING A SIMPLE CHESSBOARDAdd a canvas to the index.html page by inserting the following markup in the blank body that was created by the template: <canvas id="board" width ="600" height ="600"> Not supported </canvas> Then add a script element after the canvas element but still inside the body element using the code shown in Listing 10-1.
清单 10-1。画一个简单的棋盘
<script id="chess board" type="text/javascript">
// Get the canvas context
var chessCanvas = document.getElementById("board");
var chessContext = chessCanvas.getContext("2d");
drawBoard();
// Draw the chess board
function drawBoard() {
chessContext.clearRect(0, 0, 600, 600);
chessContext.fillStyle = "red";
chessContext.strokeStyle = "red";
// Draw the alternating squares
for (var x = 0; x < 8; x++) {
for (var y = 0; y < 8; y++) {
if ((x + y) % 2) {
chessContext.fillRect(75 * x, 75 * y, 75, 75);
}
}
}
// Add a border around the entire board
chessContext.strokeRect(0, 0, 600, 600);
}
</script>
Save your changes and press F5 to start the application. The page should look like Figure 10-1.

图 10-1。
The initial chessboard
drawBoard()函数首先清除将要绘制的区域。然后,它使用嵌套的for循环来绘制方块。fillStyle和strokeStyle属性都设置为红色;默认情况下,它们都是黑色的。请注意,它只画红色方块。由于整个区域首先被清除,任何未被绘制的区域将是白色的。这段代码使用嵌套的for循环来遍历八行八列。红色方块是行和列之和为奇数的方块。对于偶数行(0、2、4 和 6),奇数列(1、3、5 和 7)将为红色。对于奇数行,偶数列将为红色。为了清理边缘方块,在整个棋盘周围画一个红色边框。
使用渐变
您也可以使用渐变来填充形状,而不是纯色。为此,您必须首先使用绘图上下文的createLinear Gradient()方法创建一个渐变对象。这个方法有四个参数,分别是渐变起点和终点的 x 和 y 坐标。这允许您指定渐变应该从上到下、从左到右还是从一个角到另一个角。渐变是在整个画布上计算的。不能为单个元素定义渐变。
然后,您必须定义色标。每个色标定义了沿渐变的一个位置和一种颜色。至少,您需要在 0 和 1 处设置色标,用于定义开始和结束颜色。如果您想要控制过渡,也可以在它们之间添加色标。例如,如果要在中间点定义颜色,请使用 0.5。
最后,您将使用这个渐变来指定fillStyle属性。要进行尝试,请添加以粗体显示的以下代码:
function drawBoard() {
chessContext.clearRect(0, 0, 600, 600);
var gradient = chessContext.createLinearGradient(0, 600, 600, 0);
gradient.addColorStop(0.0, "#D50005");
gradient.addColorStop(0.5, "#E27883");
gradient.addColorStop(1.0, "#FFDDDD");
chessContext.fillStyle = gradient;
chessContext.strokeStyle = "red";
保存您的更改,然后按 F5 启动应用。该页面现在应该如图 10-2 所示。请注意,颜色会在整个画布上过渡,而不是在每个方块上过渡。

图 10-2。
The board using a gradient fill
使用图像
现在,您已经准备好添加棋子,这些棋子将使用图像文件绘制。将图像添加到画布上真的很容易。您创建一个Image对象,将其src属性设置为图像文件的位置,然后调用绘图上下文的drawImage()方法,如下所示:
var myImage = new Image();
myImage.src = "img/sample.jpg";
context.drawImage(myImage, 0,0, 50, 100);
drawImage()方法的第一个参数指定将要绘制的图像。这可以是一个Image对象,正如我在这里展示的。或者,您也可以指定一个已经在页面上的img、video或canvas元素。接下来的两个参数指定图像左上角的 x 和 y 位置。第四个和第五个参数是可选的,它们分别指定图像的宽度和高度。如果不指定这些参数,将使用图像的固有大小来绘制图像。
drawImage()方法还允许您提供四个额外的参数。这些仅用于指定应该在画布上显示的图像部分。这些附加参数包括指定左上角的 x 坐标和 y 坐标,以及定义指定部分的宽度和高度。如果您只想绘制图像的一部分,请使用最后四个参数。如果省略这些,将显示整个图像。
在这个应用中,您将使用 12 幅不同的图像绘制 32 幅作品。此外,在本章的后面,您将添加代码来移动这些部分。为了方便起见,您将在应用中添加一些结构。您将定义一个存储棋子属性的类,例如要使用的图像及其在棋盘上的位置。然后,您将实现一个使用这些属性的细节的通用绘图函数。
EXERCISE 10-3. DRAWING CHESS PIECESIn Solution Explorer, right-click the wwwroot folder and click the Add and New Folder links. Enter images for the folder name. The images for the chess pieces are included in the source code download file. You’ll find these in the Chapter 10 \Images folder. Drag all 12 files to the wwwroot\images folder in Solution Explorer. Add the variable declarations shown in bold in Listing 10-2 to your script element. This will define a variable to reference an Image object for each of the 12 image files. It will also define an array that you will be using to store the 32 chess pieces.
清单 10-2。定义图像变量
<script type="text/javascript">
// Get the canvas context
var chessCanvas = document.getElementById("board");
var chessContext = chessCanvas.getContext("2d");
// Define the chess piece images
var imgPawn = new Image();
var imgRook = new Image();
var imgKnight = new Image();
var imgBishop = new Image();
var imgQueen = new Image();
var imgKing = new Image();
var imgPawnW = new Image();
var imgRookW = new Image();
var imgKnightW = new Image();
var imgBishopW = new Image();
var imgQueenW = new Image();
var imgKingW = new Image();
// Define an array to store 32 pieces
var pieces = new Array(32);
drawBoard();
Add the loadImages() function, shown in Listing 10-3, to your script element after the existing drawBoard() function.
清单 10-3。加载图像文件
function loadImages() {
imgPawn.src = "img/pawn.png";
imgRook.src = "img/rook.png";
imgKnight.src = "img/knight.png";
imgBishop.src = "img/bishop.png";
imgQueen.src = "img/queen.png";
imgKing.src = "img/king.png";
imgPawnW.src = "img/wpawn.png";
imgRookW.src = "img/wrook.png";
imgKnightW.src = "img/wknight.png";
imgBishopW.src = "img/wbishop.png";
imgQueenW.src = "img/wqueen.png";
imgKingW.src = "img/wking.png";
}
Now you’re ready to define the chess pieces. You’ll use a class definition that will store the attributes needed to draw the chess piece. The image property contains a reference to the appropriate Image object. The x and y properties specify the square that the piece is in, from 0 to 7, left to right and top to bottom. The height and width properties indicate the size of the image, which will vary depending on the type of piece. The killed property is used to indicate whether the piece has been captured. Captured images are not displayed. Add the following code to the end of the script element: // Define a class to store the piece properties function ChessPiece() { this.image = null; this.x = 0; this.y = 0; this.height = 0; this.width = 0; this.killed = false; } Add the code shown in Listing 10-4 to the end of your script element. This implements the drawPiece() function that draws a single chess piece based on the class properties. Finally, it provides a drawAllPieces() function that will draw each of the pieces defined in the array.
清单 10-4。画棋子
// Draw a chess piece
function drawPiece(p) {
if (!p.killed)
chessContext.drawImage(p.image,
(75 - p.width) / 2 + (75 * p.x),
73 - p.height + (75 * p.y),
p.width,
p.height);
}
// Draw all of the chess pieces
function drawAllPieces() {
for (var i = 0; i < 32; i++) {
if (pieces[i] != null) {
drawPiece(pieces[i]);
}
}
}
Now you need to create 32 instances of the ChessPiece class and specify all of the appropriate properties. Add the createPieces() function shown in Listing 10-5. This function creates the instances of the ChessPiece class, storing them in the pieces array, and sets the properties of each one.
提示由于这个过程相当冗长和乏味,所以我把这个函数作为一个单独的文件放在源代码下载中。如果您愿意,您可以在解决方案资源管理器的 Chapter 10 文件夹中找到createPieces.js文件,并将它拖到wwwroot文件夹中,而不是键入清单 10-5 中的函数。然后在head部分添加该参考:
<script type="text/javascript" src="createPieces.js"></script>
清单 10-5。实现 createPieces()函数
function createPieces() {
var piece;
// Black pawns
for (var i = 0; i < 8; i++) {
piece = new ChessPiece();
piece.image = imgPawn,
piece.x = i;
piece.y = 1;
piece.height = 50;
piece.width = 28;
pieces[i] = piece;
}
// Black rooks
piece = new ChessPiece();
piece.image = imgRook;
piece.x = 0;
piece.y = 0;
piece.height = 60;
piece.width = 36;
pieces[8] = piece;
piece = new ChessPiece();
piece.image = imgRook;
piece.x = 7;
piece.y = 0;
piece.height = 60;
piece.width = 36;
pieces[9] = piece;
// Black knights
piece = new ChessPiece();
piece.image = imgKnight;
piece.x = 1;
piece.y = 0;
piece.height = 60;
piece.width = 36;
pieces[10] = piece;
piece = new ChessPiece();
piece.image = imgKnight;
piece.x = 6;
piece.y = 0;
piece.height = 60;
piece.width = 36;
pieces[11] = piece;
// Black bishops
piece = new ChessPiece();
piece.image = imgBishop;
piece.x = 2;
piece.y = 0;
piece.height = 65;
piece.width = 30;
pieces[12] = piece;
piece = new ChessPiece();
piece.image = imgBishop;
piece.x = 5;
piece.y = 0;
piece.height = 65;
piece.width = 30;
pieces[13] = piece;
// Black queen
piece = new ChessPiece();
piece.image = imgQueen;
piece.x = 3;
piece.y = 0;
piece.height = 70;
piece.width = 32;
pieces[14] = piece;
// Black king
piece = new ChessPiece();
piece.image = imgKing;
piece.x = 4;
piece.y = 0;
piece.height = 70;
piece.width = 28;
pieces[15] = piece;
// White pawns
for (var i = 0; i < 8; i++) {
piece = new ChessPiece();
piece.image = imgPawnW,
piece.x = i;
piece.y = 6;
piece.height = 50;
piece.width = 28;
pieces[16 + i] = piece;
}
// White rooks
piece = new ChessPiece();
piece.image = imgRookW;
piece.x = 0;
piece.y = 7;
piece.height = 60;
piece.width = 36;
pieces[24] = piece;
piece = new ChessPiece();
piece.image = imgRookW;
piece.x = 7;
piece.y = 7;
piece.height = 60;
piece.width = 36;
pieces[25] = piece;
// White knights
piece = new ChessPiece();
piece.image = imgKnightW;
piece.x = 1;
piece.y = 7;
piece.height = 60;
piece.width = 36;
pieces[26] = piece;
piece = new ChessPiece();
piece.image = imgKnightW;
piece.x = 6;
piece.y = 7;
piece.height = 60;
piece.width = 36;
pieces[27] = piece;
// White bishops
piece = new ChessPiece();
piece.image = imgBishopW;
piece.x = 2;
piece.y = 7;
piece.height = 65;
piece.width = 30;
pieces[28] = piece;
piece = new ChessPiece();
piece.image = imgBishopW;
piece.x = 5;
piece.y = 7;
piece.height = 65;
piece.width = 30;
pieces[29] = piece;
// White queen
piece = new ChessPiece();
piece.image = imgQueenW;
piece.x = 3;
piece.y = 7;
piece.height = 70;
piece.width = 32;
pieces[30] = piece;
// White king
piece = new ChessPiece();
piece.image = imgKingW;
piece.x = 4;
piece.y = 7;
piece.height = 70;
piece.width = 28;
pieces[31] = piece;
}
Modify the drawBoard() function to also call drawAllPieces() after the board has been drawn. // Add a border around the entire board chessContext.strokeRect(0, 0, 600, 600); drawAllPieces(); } Finally, replace the call to drawBoard() function in the main script with the code shown in bold. This will call the loadImages() and createPieces() functions and wait 300ms before calling drawBoard(). // Define an array to store 32 pieces var pieces = new Array(32); loadImages(); createPieces(); setTimeout(drawBoard, 300); // Draw the chess board function drawBoard() { Save your changes and press F5 to start the application. You should now see the chess pieces, as shown in Figure 10-3.

图 10-3。
The chess board with the pieces displayed Note
当您创建一个Image对象并设置它的src属性时,指定的图像文件被异步下载。有可能在调用drawImage()函数之前文件还没有被加载。如果发生这种情况,图像不会显示。300 毫秒的延迟是解决这个问题的简单方法。您可以为每个Image对象实现onload事件处理程序,当图像被加载时会调用这个处理程序。这有点复杂,因为您需要等待所有 12 个图像被加载。
添加简单动画
为了演示使用 canvas 的简单动画,您将四处移动各个部分。绘制每个棋子的函数根据该棋子所在的正方形计算位置。要移动一个棋子,只需要更新x或y属性,然后重画即可。
当您在新位置重绘一个片段时,它在旧位置仍然可见。此外,如果你要通过在同一个方块中移动一个棋子来捕捉另一个棋子,你将会在同一个方块中得到两个棋子。您可以实现一些复杂的逻辑来清除方块,并在移动棋子之前重新绘制一个红色或白色的方块。然而,在这个演示中,您将简单地清除整个画布并重新绘制棋盘和所有的棋子。
为了实现自动化,您将创建一个makeNextMove()函数。这将调整一个棋子的x和y位置,然后重新绘制棋盘和所有的棋子。您将使用setInterval()函数反复调用它,这样棋子将会连续移动。
EXERCISE 10-4. ANIMATING THE CHESS PIECESAdd the following variables shown in bold near the beginning of the script element: // Define an array to store 32 pieces var pieces = new Array(32); var moveNumber = -1; var timer; loadImages(); Implement the makeNextMove() function shown in Listing 10-6. This code “moves” a piece by adjusting its x and y properties. It keeps track of the move number and uses this to adjust the appropriate piece. The seventh move captures a piece and sets its killed property. Since this ends the animation, the seventh move also uses the clearTimer() function so no more timer events will occur. After each move, the board and all the pieces are redrawn. After the seventh move, this function also uses the fillText() method, which is used to write text to the canvas.
清单 10-6。makeNextMove 实现
function makeNextMove() {
function inner() {
if (moveNumber === 1) {
pieces[20].y--;()
}
if (moveNumber === 2) {
pieces[4].y += 2;
}
if (moveNumber === 3) {
pieces[29].y = 4;
pieces[29].x = 2;
}
if (moveNumber === 4) {
pieces[6].y++;
}
if (moveNumber === 5) {
pieces[30].x = 5;
pieces[30].y = 5;
}
if (moveNumber === 6) {
pieces[7].y++;
}
if (moveNumber === 7) {
pieces[30].x = 5;
pieces[30].y = 1;
pieces[5].killed = true;
clearInterval(timer);
}
moveNumber++;
drawBoard();
drawAllPieces();
if (moveNumber > 7) {
chessContext.font = "30pt Arial";
chessContext.fillStyle = "black";
chessContext.fillText("Checkmate!", 200, 220);
}
}
return inner;
}
Add the following code to the end of the main script, just before the drawBoard() function definition. This will call the makeNextMove() function every two seconds. timer = setInterval(makeNextMove(), 2000); Save your changes and press F5 to start the application. After a series of moves, the page should look like Figure 10-4.

图 10-4。
The completed chess board() Caution
makeNextMove()函数使用了一个经常被误解的 JavaScript 特性,叫做 closure。这个函数定义了另一个名为inner()的函数,它执行实际的工作。然后返回inner()函数。当定时器到期时,window对象将调用makeNextMove()函数。然而,它使用的所有变量,比如棋子的数组,都不在范围之内。inner()函数将能够访问这些变量,因此这解决了范围问题。关于闭包的更多信息,请参见本文: http://stackoverflow.com/questions/111102/how-do-javascript-closures-work 。
模拟太阳系
在下一张画布上,你将绘制一个移动的太阳系模型。出于时间的考虑,您将只显示地球、太阳和月亮。这个实现将利用 canvas 的这两个重要特性:
- 小路
- 转换
使用路径
正如我前面提到的,canvas 支持的唯一简单形状是矩形,您在前面的示例中使用了它。对于所有其他形状,您必须定义一个路径。在 canvas 中定义路径的基本方法类似于 SVG。使用移动命令设置起点,然后使用直线和曲线命令的组合来绘制形状。
在 canvas 中,你总是从一个beginPath()命令开始。调用所需的绘图命令后,通过调用stroke()绘制形状轮廓或fill()填充形状来完成路径。在调用stroke()或fill()之前,形状不会真正绘制到画布上。如果在完成当前形状之前再次调用beginPath()(调用stroke()或fill()),画布将忽略之前未完成的命令。与矩形相同的strokeStyle和fillStyle属性也定义了路径的颜色。
实际的绘图命令如下:
moveTo()lineTo()arcTo()bezierCurveTo()quadraticCurveTo()
此外,这些函数可用于绘图:
closePath():从当前位置到起始位置执行一个lineTo()命令,以闭合形状。如果您使用fill()命令,如果您当前不在起始位置,则会自动调用closePath()功能。arc():在指定位置画一条弧;你不必先搬到那里。然而,这仍然被视为路径;你需要首先调用beginPath(),直到你调用stroke()或者fill()时,弧线才真正画出来。
画弧线
arc()命令是一个您可能会经常使用的命令,并且在本例中很重要。arc()命令采用以下参数:
arc(x, y, radius, start, end, counterclockwise)
前两个参数指定中心点的 x 和 y 坐标。第三个参数指定半径。第四个和第五个参数确定圆弧的起点和终点。它们被指定为与 x 轴的角度。0°角是圆的右边;90 度角是圆的底边。然而,角度是以弧度而不是度数来指定的。
除非你在画一个完整的圆,否则弧线的方向很重要。例如,如果你画一个从 0 度到 90 度的圆弧,那么这个圆弧将是圆的 1/4,从右侧到底部。然而,使用相同的端点,但以逆时针方向绘制,该弧将是圆的 3/4。最后一个参数,如果为 true,则表示应该以逆时针方向画弧线。该参数是可选的。如果不指定,它会顺时针方向画圆弧。
使用转换
起初,canvas 中的变换看起来有点混乱,但是一旦你理解了它们是如何工作的,它们会很有帮助。首先,变换对已经在画布上绘制的内容没有影响。相反,变换会修改将用于绘制后续形状的网格系统。在本章中,我将演示三种类型的转换。
- 翻译
- 轮流
- 缩放比例
正如我前面提到的,一个canvas元素使用一个网格系统,原点在画布的左上角。因此,100,50 处的一个点将是从该角向右 100 像素,向下 50 像素。转换只是调整网格系统。例如,以下命令将原点向右移动 100 像素,向下移动 50 像素:
context.translate (100, 50);
如图 10-5 所示。

图 10-5。
Translating the context origin
现在,当你移动到 10,20,因为这是相对于新的原点,实际位置(相对于画布),将是 110,70。你可能想知道为什么你会想这样做。嗯,假设你在画一面美国国旗,上面有 50 颗星星。五角星是一个相当复杂的图形,需要大量的绘图命令。一旦你画了第一颗星,你需要重复这个过程 49 次,每次使用不同的值。
通过简单地将上下文向右平移一点,您可以使用相同的值重复相同的命令。但是现在这颗星将会在不同的位置。当然,您可以通过创建一个接受x、y参数的drawStar()函数来完成同样的事情。然后调用这个 50 次,传入不同的值。然而,一旦你习惯了使用变换,你会发现这更容易,特别是与其他类型,如旋转。
旋转变换不会移动原点;相反,它将 x 轴和 y 轴旋转指定的量。正值用于顺时针旋转,负值用于逆时针旋转。图 10-6 展示了旋转变换是如何工作的。

图 10-6。
Rotating the drawing context’s grid Note
我将旋转角度指定为 30 °,因为这是大多数人所熟悉的。然而,rotate()命令需要弧度值。如果你的几何有点生疏,一整圈就是 360 或者 2π弧度。在 JavaScript 中,可以使用Math.PI属性来获取π (Pi)的值。例如,30 是一整圆的 1/12,所以你可以把它写成(Math.PI*2/12))。一般情况下,弧度计算为degrees * (Math.PI/180)。
您可以使用多重转换。例如,可以平移原点,然后旋转 x 轴或 y 轴。你也可以再旋转网格一些,然后再平移一次。每个变换总是相对于当前位置和方向。
保存上下文状态
绘图上下文的状态包括各种属性,如您已经使用过的fillStyle和strokeStyle。它还包括已应用的所有转换的累积。如果你开始使用多重转换,回到原始状态可能会很困难。幸运的是,绘图上下文提供了保存和恢复上下文状态的能力。
通过调用save()函数保存当前状态。保存状态会将当前状态推送到堆栈上。调用restore()函数将最近保存的状态弹出堆栈,并使其成为当前状态。如图 10-7 所示。

图 10-7。
Saving and restoring the drawing context state
在进行任何转换之前,尤其是复杂的转换之前,通常应该保存状态。当您绘制完需要转换的任何元素后,您可以将状态恢复到原来的样子。记住,通过设置fillStyle或执行转换来改变状态不会影响已经绘制的内容。
绘制太阳系
有了这些特征,让我们画一个简单的太阳系模型。
EXERCISE 10-5. MODELING THE SOLAR SYSTEMOpen the index.html file and add the canvas element shown in bold just after the existing canvas element. <div> <canvas id="board" width ="600" height ="600"> Not supported </canvas> <canvas id="solarSystem" width="450" height="400"> Not supported </canvas> </div> Add a new script element in the body element just after the existing script element using the code shown in Listing 10-7.
清单 10-7。初始太阳系实现
<script id="solar system" type="text/javascript">
var ss = document.getElementById('solarSystem')
var ssContext = ss.getContext('2d');
setInterval(animateSS, 100);
function animateSS() {
var ss = document.getElementById('solarSystem')
var ssContext = ss.getContext('2d');
// Clear the canvas and draw the background
ssContext.clearRect(0, 0, 450, 400);
ssContext.fillStyle = "#2F1D92";
ssContext.fillRect(0, 0, 450, 400);
ssContext.save();
// Draw the sun
ssContext.translate(220, 200);
ssContext.fillStyle = "yellow";
ssContext.beginPath();
ssContext.arc(0, 0, 15, 0, Math.PI * 2, true);
ssContext.fill();
// Draw the earth orbit
ssContext.strokeStyle = "black";
ssContext.beginPath();
ssContext.arc(0, 0, 150, 0, Math.PI * 2);
ssContext.stroke();
ssContext.restore()
}
</script>
提示在 Visual Studio 中,可以在编辑器中折叠 HTML 元素。因为您已经完成了前面的script元素,所以您可以折叠它,这样可以更容易地看到您将要处理的新元素。虽然您不需要设置script元素的id属性,但是如果您设置了,它会在元素折叠时显示出来,如图 10-8 所示。这将使管理包含多个script元素的页面变得更加容易。
This code gets the canvas element and then obtains the 2d drawing context, just like the previous example. It then uses the setInterval() function to call the animateSS() function every 100 milliseconds. The animateSS() function is what does the real work. It clears the entire area and then fills it with dark blue. The rest of the code relies on transformations, so it first saves the drawing context and then restores it when finished. This animateSS() function uses the translate() function to move the origin to the approximate midpoint of the canvas. The sun and the earth orbits are drawn using the arc() function. Notice the center point for both is 0, 0 since the context’s origin is now in the middle of the canvas. Also, notice the start angle is 0 and the end angle is specified as Math.PI 2. In radians, this is a full circle or 360°. The arc for the sun is filled in, and the orbit is not. Press F5 to start the application. So far, the drawing is not very interesting; it’s a sun with an orbit drawn around it, as shown in Figure 10-9.

图 10-9。
The initial solar system drawing Now you’ll draw the earth and animate it around the orbit. Normally the earth will revolve around the sun once every 365.24 days, but we’ll speed this up a bit and complete the trip in 60 seconds. To determine where to put the earth each time the canvas is redrawn, you must calculate the number of seconds. The amount of rotation per second is calculated as Math.PI * 2 / 60. Multiply this value by the number of seconds to determine the angle where the earth should be. Add the code from Listing 10-8 that is shown in bold. This code uses the rotate function to rotate the drawing context the appropriate angle. Since the arc for the earth orbit is 150px, this code then uses the translate function to move the context 150 pixels to the right so the earth can be drawn at the adjusted 0,0 coordinate. Notice that this is combining two separate transforms, one to rotate based the earth position in its orbit and one to translate the appropriate distance from the sun. The earth is then drawn using a filled arc with a center point of 0,0, the new origin of the context.

图 10-8。
Collapsing the script element
清单 10-8。绘制地球
// Draw the earth orbit
ssContext.strokeStyle = "black";
ssContext.beginPath();
ssContext.arc(0, 0, 150, 0, Math.PI * 2);
ssContext.stroke();
// Compute the current time in seconds (use the milliseconds
// to allow for fractional parts).
var now = new Date();
var seconds = ((now.getSeconds() * 1000) + now.getMilliseconds()) / 1000;
//---------------------------------------------
// Earth
//---------------------------------------------
// Rotate the context once every 60 seconds
var anglePerSecond = ((Math.PI * 2) / 60);
ssContext.rotate(anglePerSecond * seconds);
ssContext.translate(150, 0);
// Draw the earth
ssContext.fillStyle = "green";
ssContext.beginPath();
ssContext.arc(0, 0, 10, 0, Math.PI * 2, true);
ssContext.fill();
ssContext.restore()
Save your changes and press F5 to start the application. Now you should see the earth make its way around the sun, as shown in Figure 10-10.

图 10-10。
Adding the earth to the drawing Now you’ll show the moon revolving around the earth, which will demonstrate the real power of using transformations. The specific position of the moon is based on two moving objects. While it’s certainly possible to compute this using some complex formulas (scientists have been doing this for centuries) with transformations, you don’t have to. The drawing context was rotated the appropriate angle based on current time (number of seconds). It was then translated by the radius of the orbit, so the earth is now at the origin of the context. It doesn’t really matter where the earth is; you can simply draw the moon relative to the current origin. You will now draw the moon just like you drew the earth. Instead of the origin being at the sun and rotating the earth around the sun, the origin is on the earth, and you’ll rotate the moon around the earth. The moon will rotate around the earth approximately once each month; in other words, it will complete about 12 revolutions for each earth orbit. So, you’ll need to rotate 12 times faster. The anglePerSecond is now computed as 12 * (( Math.PI * 2 ) / 60). Add the code shown in bold in Listing 10-9.
清单 10-9。画月亮
// Draw the earth
ssContext.fillStyle = "green";
ssContext.beginPath();
ssContext.arc(0, -0, 10, 0, Math.PI * 2, true);
ssContext.fill();
//---------------------------------------------
// Moon
//---------------------------------------------
// Rotate the context 12 times for every earth revolution
anglePerSecond = 12 * ((Math.PI * 2) / 60);
ssContext.rotate(anglePerSecond * seconds);
ssContext.translate(0, 35);
// draw the moon
ssContext.fillStyle = "white";
ssContext.beginPath();
ssContext.arc(0, 0, 5, 0, Math.PI * 2, true);
ssContext.fill();
ssContext.restore()
注意,每个太阳年大约有 12.368 个太阴月。通过使用这个数字而不是前面代码中的 12,可以使模型更加精确。
Save your changes and press F5 to start the application. You should now see the moon rotating around the earth, as shown in Figure 10-11.

图 10-11。
Including the moon
应用缩放
在你完成这个模型之前,你要做一个小小的修改。地球的轨道实际上不是一个正圆。这种属性被称为偏心率。(如果你对轨道偏心率感到好奇,请查看 http://en.wikipedia.org/wiki/Orbital_eccentricity 的文章。)要在您的绘图中对此进行建模,您将拉伸轨道,使其宽度略大于高度。为此,您将使用缩放。
scale()函数执行第三种类型的转换。该函数采用两个参数来指定沿 x 轴和 y 轴的缩放比例。比例因子 1 是正常比例。小于 1 的系数将压缩图形,大于 1 的系数将拉伸图形。虽然地球轨道的不完美是非常轻微的,但您会在这里将其夸大,并对 x 轴使用 1.1 的比例因子。
在绘制地球轨道之前,添加以下以粗体显示的代码:
// Draw the earth orbit
ssContext.scale(1.1, 1);
ssContext.strokeStyle = "black";
按 F5 键启动应用,它看起来应该如图 10-12 所示。

图 10-12。
Adding scaling
你现在有一个稍微变形的轨道。通过简单地改变比例因子,所有不同的绘图元素被成比例地调整。此外,在恢复上下文后,缩放将恢复正常,以便正确绘制后续元素。
Note
图 10-12 中的直线展示了一次日食,月亮从地球和太阳之间穿过,在地球上投下了阴影。
裁剪画布
我想再介绍一个与路径相关的特性。之前我说过在你调用了beginPath()和想要的绘图函数之后,你可以调用stroke()或者fill()。您还可以调用一个函数:clip()。clip()函数将使用您刚刚定义的路径,并且不允许在该路径之外绘制任何东西。这不会影响已经绘制的内容,但是任何将来的形状都将被限制在由该路径定义的剪辑区域内。
为了演示这一点,您将返回到国际象棋棋盘示例,并使用圆弧定义一个裁剪路径。转到 board script元素,将粗体显示的代码添加到drawBoard()函数中。
var gradient = chessContext.createLinearGradient(0, 600, 600, 0);
gradient.addColorStop(0, "#D50005");
gradient.addColorStop(0.5, "#E27883");
gradient.addColorStop(1, "#FFDDDD");
// Clip the path
chessContext.beginPath();
chessContext.arc(300, 300, 300, 0, (Math.PI * 2), true);
chessContext.clip();
chessContext.fillStyle = gradient;
chessContext.strokeStyle = "red";
// Draw the alternating squares
这在棋盘上定义了一个圆圈,圆圈之外的任何东西都是不可见的。按 F5 键启动应用,它看起来应该如图 10-13 所示。

图 10-13。
The chess board with a clipping path Note
如果在画板后定义裁剪路径,将会画出整个画板,但会裁剪部分,因此裁剪区域外的任何部分都将被隐藏。
了解合成
到目前为止你画的所有形状中,最后画的那个覆盖或隐藏了之前的任何形状。这种行为被称为合成。默认行为称为源覆盖,是在画布上已经存在的任何形状上绘制当前形状,如您所见。合成术语使用 source 来指代正在绘制的形状,使用 destination 来指代之前绘制的结果。除了source-over之外,您还可以使用globalCompositeOperation属性配置 11 种其他行为。这些可以通过查看每一种的样本得到最好的解释。
在本练习中,您将重叠一个红色正方形和一个蓝色圆形。您将这样做 12 次,每次为globalCompositeOperation属性使用不同的值。为了正确地工作,您将创建 12 个canvas元素,在每个元素上绘制相同的元素。
EXERCISE 10-6. EXPLORING COMPOSITINGIn the main div element, comment out the board and solarSystem canvas elements and create 12 new canvas elements using the code shown in Listing 10-10.
清单 10-10。创建 12 个画布元素
<div>
<div>
<canvas id="composting1" width="120" height="120"></canvas>
<br />source-over
</div>
<div>
<canvas id="composting2" width="120" height="120"></canvas>
<br />destination-over
</div>
<div>
<canvas id="composting3" width="120" height="120"></canvas>
<br />source-in
</div>
<div>
<canvas id="composting4" width="120" height="120"></canvas>
<br />destination-in
</div>
<div>
<canvas id="composting5" width="120" height="120"></canvas>
<br />source-out
</div>
<div>
<canvas id="composting6" width="120" height="120"></canvas>
<br />destination-out
</div>
<div>
<canvas id="composting7" width="120" height="120"></canvas>
<br />source-atop
</div>
<div>
<canvas id="composting8" width="120" height="120"></canvas>
<br />destination-atop
</div>
<div>
<canvas id="composting9" width="120" height="120"></canvas>
<br />xor
</div>
<div>
<canvas id="composting10" width="120" height="120"></canvas>
<br />copy
</div>
<div>
<canvas id="composting11" width="120" height="120"></canvas>
<br />lighter
</div>
<div>
<canvas id="composting12" width="120" height="120"></canvas>
<br />darker
</div>
<!--<canvas id="board" width ="600" height ="600">
Not supported
</canvas>
<canvas id="solarSystem" width="450" height="400">
Not supported
</canvas> -->
</div>
Add the following style element in the head section. This will format the canvas elements into three columns so you can see all 12 examples on one screen. <style> body div { -webkit-column-count: 3; column-count: 3; } </style Comment out the chessboard and solar system script elements. (Since the canvas elements are no longer on the page, the scripts will fail.) Add the script element shown in Listing 10-11 to the body element, just after the existing script elements.
清单 10-11。绘制合成画布
<script id="compositing" type="text/javascript">
for (var i = 1; i <= 12; i++) {
var c = document.getElementById("composting" + i);
var cContext = c.getContext("2d");
cContext.fillStyle = "red";
cContext.fillRect(10, 20, 80, 80);
switch (i) {
case 1: cContext.globalCompositeOperation = "source-over"; break;
case 2: cContext.globalCompositeOperation = "destination-over"; break;
case 3: cContext.globalCompositeOperation = "source-in"; break;
case 4: cContext.globalCompositeOperation = "destination-in"; break;
case 5: cContext.globalCompositeOperation = "source-out"; break;
case 6: cContext.globalCompositeOperation = "destination-out"; break;
case 7: cContext.globalCompositeOperation = "source-atop"; break;
case 8: cContext.globalCompositeOperation = "destination-atop"; break;
case 9: cContext.globalCompositeOperation = "xor"; break;
case 10: cContext.globalCompositeOperation = "copy"; break;
case 11: cContext.globalCompositeOperation = "lighter"; break;
case 12: cContext.globalCompositeOperation = "darker"; break;
}
cContext.fillStyle = "blue";
cContext.beginPath();
cContext.arc(65, 75, 40, 0, (Math.PI * 2), true);
cContext.fill();
}
</script>
This code uses a for loop to process all 12 canvas elements. It gets the corresponding element and then obtains its drawing context. It adds a red square and then sets the globalCompositeOperation property. Finally, it adds a blue circle, which is offset slightly from the position of the square. Change the debugging browser to use Chrome because this supports all of the compositing options.
注意所有的浏览器都支持所有这些选项,除了darker。在撰写本文时,Chrome 是唯一正确支持darker的浏览器。
Save your changes and press F5 to start the application. The web page should look like Figure 10-14.

图 10-14。
Demonstrating the compositing options
合成选项如下:
source-over:这是默认操作。源元素(正在添加的元素)被绘制在目标元素(该位置已经存在的任何元素)的顶部。destination-over:这与source-over相反,源元素被添加到现有元素的下面。source-in:仅显示源对象中也在目标元素中的部分。请注意,没有显示任何目标元素;它被用作剪贴形状。destination-in:仅显示源元素中的目标对象部分。source-out:只显示源元素中不与目标元素重叠的部分。destination-out:仅显示目标元素中不与源元素重叠的部分。source-atop:源显示在目标元素的顶部,但整个形状被目标元素剪裁。destination-atop:源显示在目标元素的下方,但整个形状被源元素裁剪掉。xor:仅显示源元素和目的元素不重叠的部分。copy:名字容易让人误解。这将绘制源元素并清除所有其他元素。lighter:这将绘制源元素和目标元素,重叠区域以较浅的颜色显示。实际的颜色是通过将源元素和目标元素的颜色值相加来确定的。darker:这将绘制源元素和目标元素,重叠区域以较暗的颜色显示。实际颜色是通过减去源元素和目标元素的颜色值来确定的。
Tip
这些合成选项的一些名称可能不是很直观。我建议你把这个图放在手边,以便日后参考,例如,万一你不记得copy是做什么的。
摘要
在本章中,您使用了 canvas 元素来创建一些图形化的 web 页面。您使用矩形和路径在画布上绘制形状。你还在画布上加入了图像。canvas 真正强大的特性之一是应用转换的能力。适当地使用转换确实可以简化一些复杂的绘图应用。
Canvas 与 SVG 有着本质的不同。在 SVG 中,每个形状都是一个单独的 DOM 节点。这提供了两个 canvas 无法实现的重要特性:
- 将事件处理程序附加到各个形状。
- 可以操纵单个形状。一个很好的例子是定义了
:hover伪规则,当鼠标悬停在形状上时,它允许形状的属性被改变。
与 SVG 相反,canvas 是基于像素的,这意味着它依赖于分辨率。请注意,所有绘图命令都使用像素位置或大小。当你在一个canvas元素上绘制一个形状时,该画布的像素会被适当地调整,所记住的只是最终的像素内容。
Canvas 由于其原始像素操作而更有效率。另一方面,SVG 必须执行大量的渲染(和重新渲染)。但是,内容密度较低的较大图像(如地图)在 SVG 中的性能通常会更好。
十一、索引数据库
随着浏览器技术的发展,在客户端设备上提供越来越多的功能,在本地存储和操作数据的需求也增加了。为了满足这一需求,出现了两种相互竞争的技术。
- Web SQL:托管在浏览器中的 SQL 引擎
- Indexed DB:使用键和索引存储和检索对象的 API
Note
2010 年 11 月,W3C 工作组决定停止 Web SQL 的工作,它不再是 HTML5 规范的一部分。然而,一些浏览器仍然支持它,但是它不太可能作为跨平台的标准。
本章将演示如何使用索引数据库在客户端存储和使用数据。如果你习惯使用 SQL 数据库,我要警告你,这不是 SQL 数据库。一旦你掌握了它,它就非常强大和有用,但是当你学习本章时,你需要调整你的视角,把你的 SQL 经验放在一边。
为了探索索引数据库的功能,您将重写在第十章中使用 canvas 创建的棋盘应用。当我解释每一个练习的时候,我不会深入到画布的细节;然而,如果你需要更多信息,请参考第十章。新版本的应用将创建对象存储来定义每个棋子的位置,然后在棋子移动时操作这些数据。
引入索引数据库
在开始详细演示之前,我认为有几个要点可以帮助您更好地理解索引数据库是如何工作的。像其他数据库一样,数据放在持久数据存储中。在这种情况下,它在本地硬盘上。数据是永久的。
我将更详细地解释这些实体中的每一个,但是我将首先介绍它们和它们的关系。数据库由对象存储组成。每个对象存储都是对象的集合,每个对象都由唯一的键标识。一个对象存储可以有一个或多个索引。每个索引都提供了另一种标识对象存储键的方法。如图 11-1 所示。

图 11-1。
The database entities
对象存储是通过事务对象访问的。创建事务时,必须定义其范围。这表明它将引用哪个对象存储,以及它将向数据库读取还是写入数据。
使用对象存储
主存储单元称为对象存储。这个名字很恰当,因为它们只是由一个键引用的对象的集合。您可以将它看作一组名称-值对,值是一个具有一组属性的对象。可以使用内嵌键,其中一个对象特性用作键。例如,如果对象有一个带有唯一值的id属性,您可以将它用作内联键。如果您使用行外键,那么您将在向存储区添加对象时指定一个键。或者,您可以使用密钥生成器,其中对象存储将为您分配递增的密钥值。以下代码演示了这些替代方案:
// Using an inline key
var typeStore = db.createObjectStore("pieceType", { keyPath: "id" });
typeStore.add(pieceType);
// Using an out-of-line key
var sampleStore = db.createObjectStore("sample", { });
sampleStore.add(sample, 5);
// Using a key generator
var pieceStore = db.createObjectStore("piece", { autoIncrement: true });
pieceStore.add(piece);
顾名思义,您还可以在对象存储上创建索引;事实上,您可以创建任意多的索引。索引使您能够快速找到特定的对象或对象集合。索引是名称-值对的集合,其中值是对象存储的键。例如,如果您有一个客户对象存储,并且想要按姓氏进行搜索,那么您可以在对象的lastName属性上创建一个索引。数据库将自动为存储中的每个对象在索引中创建一个条目。该条目将包含该对象的姓氏和相应的键。下面的代码演示了如何使用索引:
// Create an index on the lastName property
customerStore.createIndex("lastName", "id", { unique: true });
// Get the index
var index = customerStore.index("lastName");
index.get(lastName).onsuccess = function(); // get the object
index.getKey(lastName).onsuccess = function(); // get the key
createIndex()函数的第二个参数,在本例中是id,指定了关键路径。这告诉数据库引擎如何从对象中提取密钥。对于内联键,这是用于定义唯一键的属性的名称。
索引数据库不支持对象存储之间的关系。例如,您不能强制外键关系。您当然可以使用外键,其中一个对象存储中的属性是另一个对象存储的键,我将在后面演示。但是,数据库不强制此约束。此外,您不能在对象存储之间执行连接。
定义数据库
当您打开一个数据库时,您需要实现三个事件处理程序。
onsuccess:数据库打开;用它做点什么。- 出现错误,可能是访问问题。
onupgradeneeded:需要创建或升级数据库。
打开数据库时,如果不存在,会自动创建;但是,会引发onupgradeneeded事件。您必须为此事件实现一个事件处理程序,它将创建对象存储并用任何默认数据填充它们。这是唯一允许你改变数据库结构的地方。需要记住的重要一点是,onupgradeneeded事件是在onsuccess事件之前触发的。
open()调用还指定了一个版本号。如果这不是当前版本,在这种情况下也会引发onupgradeneeded事件。您的事件处理程序需要改变结构以匹配调用者请求的版本。您可以像这样查询数据库的当前版本:
var request = dbEng.open("Sample", 2); // get version 2
request.onupgradeneeded = function (event) {
alert("Configuring database - current version is " + e.oldVersion +
", requested version is " + e.newVersion);
}
基于当前版本,代码可能需要执行不同的操作。
异步处理
索引数据库的一个关键方面是它的异步处理,可能需要一些时间来适应;几乎所有的数据库操作都是异步完成的。一般模式是调用一个方法来执行数据库操作,比如打开一个数据库或检索一组记录(对象)。这将返回一个请求对象。然后,您必须为该请求对象实现onsuccess和onerror事件处理程序。如果请求成功,则调用onsuccess处理程序,方法调用的结果通过event对象传递。
对于需要多次数据库调用的复杂处理,您需要小心嵌套事件处理程序,并考虑何时执行它们。例如,如果您需要发出三个数据库请求,您的代码可能如下所示:
var request = dbCall1()
request.onsuccess = function (e1) {
f1(e1.target.result);
dbCall2().onsuccess = function (e2) {
f3(e2.target.result, e1.target.result);
dbCall3().onsuccess = function (e3) {
f5(e3.target.result, e2.target.result, e1.target.result);
}
f4(e2.target.result);
}
f2(e1.target.result);
}
request.onerror = function(e) {
alert("The call failed")
}
这段代码依次调用dbCall1()、dbCall2()和dbCall3(),它们将被依次处理。换句话说,在dbCall1()完成之前,dbCall2()不会启动,并且只有在它成功的情况下。每个调用都提供了一个onsuccess事件处理程序,它会进行下一个调用。如果第一次调用失败,就会发出警报。可能出乎意料的是非数据库调用的执行顺序。数据库调用立即返回,稍后当操作完成时,调用事件处理程序。一旦对dbCall2()的调用完成,函数返回,并且f2()将被执行。稍后,dbCall2()完成,它的事件处理程序被调用,并且f3()被执行。
Tip
onerror事件是从层次结构中冒了出来的。例如,在请求对象上发生的错误,如果不处理,将在事务对象上引发。如果不在那里处理,它将在数据库对象上被引发。在许多情况下,您可以在数据库级别使用一个事件处理程序来处理所有的错误。
由于采用了嵌套方法,事件处理程序可以从以前的调用中访问事件对象。因此,您应该为事件参数使用唯一的名称。这将避免歧义。另外,注意使用闭包来访问这些事件对象。正如我提到的,f2()在f3()之前被调用,所以定义了e1参数的dbCall1()的事件处理程序已经完成,并且在执行dbCall2()的事件处理程序时不再在范围内。JavaScript 的闭包特性允许后续的事件处理程序访问这个对象。这很重要,因为如果您需要访问所有三个对象存储来完成一个操作,您将需要等到所有三个都完成,然后访问所有三个结果。
Tip
为了避免关闭,您可以从前两个数据库调用中提取您需要的属性,并将它们存储在局部变量中(在dbCall1()调用之前声明)。然后在f5()调用中,可以用这些变量代替e1和e2事件对象。这只是个人喜好的问题,因为两种方法都可以。
使用交易
所有的数据访问,无论是读还是写,都是在一个事务中完成的,所以您必须首先创建一个事务对象。创建事务时,指定其范围,该范围由它将访问的对象存储定义。您还可以指定模式(只读或读写)。然后,您可以从事务中获取一个对象存储,并像这样从存储中获取数据或将数据放入存储中:
var xact = db.transaction(["piece", "pieceType"], "readwrite");
var pieceStore = xact.objectStore("piece");
对于读写事务,在事务完成之前不会提交数据更改。有趣的问题是“交易何时完成?”当一个事务不再有未完成的请求时,它就完成了。记住,一切都是基于请求的。您发出一个请求,然后实现一个事件处理程序,在请求完成时做一些事情。如果该事件处理程序对该事务发出另一个请求,那么该事务将保持活动状态。这是嵌套事件处理程序的另一个重要原因。如果在没有发出另一个请求的情况下结束一个事件处理程序,事务将会完成,并且所有的更改都会被提交。如果在此之后尝试使用该事务,将会得到一个TRANSACTION_INACTIVE_ERR错误。
另一件要记住的事情是读写事务不能有重叠的作用域。如果您创建了一个读写事务,您可以创建第二个事务,只要它们不包含一些相同的对象存储。如果它们有重叠的范围,您必须等待第一个事务完成,然后才能创建第二个事务。但是,只读事务可以有重叠的范围。
创建应用
首先,您将使用 canvas 创建一个棋盘,并像上一章一样为棋子配置图像。
创建 Visual Studio 项目
首先,您将创建一个 Visual Studio 项目,使用与前几章中使用的相同的空模板。
EXERCISE 11-1. CREATING THE VISUAL STUDIO PROJECTStart Visual Studio 2015. From the Start Page, click the New Project link. In the New project dialog box, select the ASP.NET Web Application template. Enter the project name Chapter 11 and select a location for this project. In the next dialog box, select the ASP.NET 5 Empty template and click the OK button to create the project. Open the Startup.cs file and comment out the implementation for the Configure() method like this:
public void Configure(IApplicationBuilder app)
{
//app.Run(async (context) =>
//{
// await context.Response.WriteAsync("Hello World!");
//});
}
In Solution Explorer, right-click the wwwroot folder and click the Add and New Item links. In the Add New Item dialog box, select the HTML Page template, enter the name Index.html, and click the Add button. In the Index.html file, in the empty body element that was created, add the following markup:
<div>
<canvas id="board" width ="600" height ="600">
Not supported
</canvas>
</div>
In Solution Explorer, right-click the wwwroot folder and click the Add and New Folder links. Enter images for the folder name. The images for the chess pieces are included in the source code download file. These are the same images used in Chapter 10. You’ll find these in the Chapter 10 \Images folder. Drag all 12 files to the images folder in Solution Explorer.
创建画布
现在您将使用 JavaScript 设计canvas元素。最初的设计只是画一个空棋盘,稍后您将添加棋子。参考第十章的获取更多关于使用canvas元素的解释。使用清单 11-1 中的代码,在div元素之后和body元素内部添加一个script元素。
Listing 11-1. Designing the Initial Canvas
<script>
// Get the canvas context
var chessCanvas = document.getElementById("board");
var chessContext = chessCanvas.getContext("2d");
drawBoard();
function drawBoard() {
chessContext.clearRect(0, 0, 600, 600);
var gradient = chessContext.createLinearGradient(0, 600, 600, 0);
gradient.addColorStop(0, "#D50005");
gradient.addColorStop(0.5, "#E27883");
gradient.addColorStop(1, "#FFDDDD");
chessContext.fillStyle = gradient;
chessContext.strokeStyle = "red";
// Draw the alternating squares
for (var x = 0; x < 8; x++) {
for (var y = 0; y < 8; y++) {
if ((x + y) % 2) {
chessContext.fillRect(75 * x, 75 * y, 75, 75);
}
}
}
// Add a border around the entire board
chessContext.strokeRect(0, 0, 600, 600);
}
</script>
按 F5 键启动应用,看起来应该如图 11-2 所示。

图 11-2。
The initial (blank) chessboard
配置图像
您将使用图像文件来表示棋子。在将它们添加到画布之前,您需要为每一个创建一个Image对象,并指定其src属性。您还将把它们放入一个数组中,以便更容易地以编程方式选择所需的图像。将清单 11-2 中所示的代码添加到您刚刚创建的script元素的开头,在现有代码之前。
Listing 11-2. Adding the Image Objects
// Define the chess piece images
var imgPawn = new Image();
var imgRook = new Image();
var imgKnight = new Image();
var imgBishop = new Image();
var imgQueen = new Image();
var imgKing = new Image();
var imgPawnW = new Image();
var imgRookW = new Image();
var imgKnightW = new Image();
var imgBishopW = new Image();
var imgQueenW = new Image();
var imgKingW = new Image();
// Specify the source for each image
imgPawn.src = "img/pawn.png";
imgRook.src = "img/rook.png";
imgKnight.src = "img/knight.png";
imgBishop.src = "img/bishop.png";
imgQueen.src = "img/queen.png";
imgKing.src = "img/king.png";
imgPawnW.src = "img/wpawn.png";
imgRookW.src = "img/wrook.png";
imgKnightW.src = "img/wknight.png";
imgBishopW.src = "img/wbishop.png";
imgQueenW.src = "img/wqueen.png";
imgKingW.src = "img/wking.png";
// Define an array of Image objects
var images = [
imgPawn ,
imgRook ,
imgKnight ,
imgBishop ,
imgQueen ,
imgKing ,
imgPawnW ,
imgRookW ,
imgKnightW ,
imgBishopW ,
imgQueenW ,
imgKingW
];
创建数据库
现在,您已经准备好创建和使用本地索引 DB 数据库来配置和显示棋子。最初,数据将从静态数据加载,您只需显示起始位置。稍后,您将通过更新它们在对象存储中的位置来制作动画。
声明静态数据
您需要用一些数据填充数据库。对于这个应用,您只需将数据声明为静态数组,并将其复制到对象存储中。对于其他应用,这可以从服务器下载或从用户输入中输入。将清单 11-3 中所示的声明添加到script元素中,就在图像变量之后。
Listing 11-3. Declaring the Static Data
var pieceTypes = [
{ name: "pawn", height: "50", width: "28", blackImage: 0, whiteImage: 6 },
{ name: "rook", height: "60", width: "36", blackImage: 1, whiteImage: 7 },
{ name: "knight", height: "60", width: "36", blackImage: 2, whiteImage: 8 },
{ name: "bishop", height: "65", width: "30", blackImage: 3, whiteImage: 9 },
{ name: "queen", height: "70", width: "32", blackImage: 4, whiteImage: 10 },
{ name: "king", height: "70", width: "28", blackImage: 5, whiteImage: 11 }
];
var pieces = [
{ type: "pawn", color: "white", row: 6, column: 0, pos: "a2", killed: false },
{ type: "pawn", color: "white", row: 6, column: 1, pos: "b2", killed: false },
{ type: "pawn", color: "white", row: 6, column: 2, pos: "c2", killed: false },
{ type: "pawn", color: "white", row: 6, column: 3, pos: "d2", killed: false },
{ type: "pawn", color: "white", row: 6, column: 4, pos: "e2", killed: false },
{ type: "pawn", color: "white", row: 6, column: 5, pos: "f2", killed: false },
{ type: "pawn", color: "white", row: 6, column: 6, pos: "g2", killed: false },
{ type: "pawn", color: "white", row: 6, column: 7, pos: "h2", killed: false },
{ type: "rook", color: "white", row: 7, column: 0, pos: "a1", killed: false },
{ type: "rook", color: "white", row: 7, column: 7, pos: "h1", killed: false },
{ type: "knight", color: "white", row: 7, column: 1, pos: "b12", killed: false },
{ type: "knight", color: "white", row: 7, column: 6, pos: "g1", killed: false },
{ type: "bishop", color: "white", row: 7, column: 2, pos: "c1", killed: false },
{ type: "bishop", color: "white", row: 7, column: 5, pos: "f1", killed: false },
{ type: "queen", color: "white", row: 7, column: 3, pos: "d1", killed: false },
{ type: "king", color: "white", row: 7, column: 4, pos: "e1", killed: false },
{ type: "pawn", color: "black", row: 1, column: 0, pos: "a7", killed: false },
{ type: "pawn", color: "black", row: 1, column: 1, pos: "b7", killed: false },
{ type: "pawn", color: "black", row: 1, column: 2, pos: "c7", killed: false },
{ type: "pawn", color: "black", row: 1, column: 3, pos: "d7", killed: false },
{ type: "pawn", color: "black", row: 1, column: 4, pos: "e7", killed: false },
{ type: "pawn", color: "black", row: 1, column: 5, pos: "f7", killed: false },
{ type: "pawn", color: "black", row: 1, column: 6, pos: "g7", killed: false },
{ type: "pawn", color: "black", row: 1, column: 7, pos: "h7", killed: false },
{ type: "rook", color: "black", row: 0, column: 0, pos: "a8", killed: false },
{ type: "rook", color: "black", row: 0, column: 7, pos: "h8", killed: false },
{ type: "knight", color: "black", row: 0, column: 1, pos: "b8", killed: false },
{ type: "knight", color: "black", row: 0, column: 6, pos: "g8", killed: false },
{ type: "bishop", color: "black", row: 0, column: 2, pos: "c8", killed: false },
{ type: "bishop", color: "black", row: 0, column: 5, pos: "f8", killed: false },
{ type: "queen", color: "black", row: 0, column: 3, pos: "d8", killed: false },
{ type: "king", color: "black", row: 0, column: 4, pos: "e8", killed: false }
];
pieceTypes[]数组定义了显示每个部分所需的属性,比如高度和宽度。它还为黑白图像指定了images[]数组中的相应索引。pieces[]数组包含与前一章相同的细节,如行和列,并定义了 32 个棋子的起始位置。
打开数据库
将清单 11-4 中所示的代码添加到script对象中,就在调用drawBoard()之后(在实现drawBoard()函数之前)。
Listing 11-4. Opening the Database
var dbEng = window.indexedDB ||
window.webkitIndexedDB || // Chrome
window.mozIndexedDB || // Firefox
window.msIndexedDB; // IE
var db; // This is a handle to the database
if (!dbEng)
alert("IndexedDB is not supported on this browser");
else {
var request = dbEng.open("Chess", 1);
request.onsuccess = function (event) {
db = event.target.result;
}
request.onerror = function (event) {
alert("Please allow the browser to open the database");
}
request.onupgradeneeded = function (event) {
configureDatabase(event);
}
}
如果您不能访问indexedDB对象,那么浏览器不支持它。对于这个演示,您可以简单地使用alert()来通知用户并停止进一步的处理。
这段代码然后使用indexedDB对象打开 Chess 数据库,指定应该使用版本 1。如前所述,open()方法返回一个IDBOpenDBRequest对象。您将为此请求附加三个事件处理程序。
onsuccess:这个事件处理程序只是保存对数据库的引用。稍后您将在这里添加更多的逻辑。注意,数据库是从event.target.result属性中获得的,这就是所有结果的返回方式。onerror:浏览器无法打开数据库的主要原因是浏览器屏蔽了 IndexedDB 功能。出于安全原因,可以禁用此功能。在这种情况下,系统会提示用户允许访问。或者,您可以选择显示错误消息。onupgradeneeded:如果数据库不存在或者指定的版本不是当前版本,则会引发此问题。这调用了configureDatabase()函数,现在您将实现它。
定义数据库结构
添加清单 11-5 中所示的代码来实现configureDatabase()函数。
Listing 11-5. Defining the Database Structure
function configureDatabase(e) {
alert("Configuring database - current version is " + e.oldVersion +
", requested version is " + e.newVersion);
db = e.currentTarget.result;
// Remove all existing data stores
var storeList = db.objectStoreNames;
for (var i = 0; i < storeList.length; i++) {
db.deleteObjectStore(storeList[i]);
}
// Store the piece types
var typeStore = db.createObjectStore
("pieceType", { keyPath: "name" });
for (var i in pieceTypes){
typeStore.add(pieceTypes[i]);
}
// Create the piece data store (you'll add
// the data later)
var pieceStore = db.createObjectStore
("piece", { autoIncrement: true });
pieceStore.createIndex
("piecePosition", "pos", { unique: true });
}
Caution
如果数据库不存在或者不是当前版本,将调用configureDatabase()函数。对于版本变更,可以使用db.version属性获取当前版本,然后进行必要的调整。同样,传递给onupgradeneeded事件处理程序的event对象将具有e.oldVersion和e.newVersion属性。为了简化这个项目,您只需删除所有对象存储并从头开始重建数据库。这将清除所有现有数据。这对于本例来说没问题,但是在大多数情况下,您需要尽可能地保存数据。
数据库对象的objectStoreNames属性包含所有已创建的对象存储的名称列表。为了删除所有现有的对象存储,这个列表中的每个名称都被传递给deleteObjectStore()方法。
最初,您将使用createObjectStore()方法创建两个数据存储。
- 包含每种类型棋子的一个对象,如兵、车或王
piece:每块包含一个对象,16 黑 16 白
指定对象键
创建对象存储时,必须在调用createObjectStore()方法时指定存储的名称。您还可以指定一个或多个可选参数。仅支持两种。
keypath:这被指定为属性名的集合。如果使用单个属性,可以将其指定为字符串,而不是字符串的集合。这定义了将用作关键字的对象属性。如果未指定keypath,则必须使用密钥生成器或提供密钥来离线定义密钥,如本节稍后所述。autoIncrement:如果为真,这表示关键字是由对象存储顺序分配的。
存储中的每个对象都必须有一个唯一的键。有三种方法可以指定密钥。
- 使用
keypath参数指定一个或多个定义唯一键的属性。随着对象的添加,keypath用于根据对象的属性生成一个键。 - 使用密钥生成器。如果指定了
autoIncrement,对象存储将根据内部计数器分配一个键。 - 添加对象时提供键值。如果不指定密钥路径或使用密钥生成器,则在向存储区添加对象时必须提供密钥。
对于pieceType商店,您将使用一个keypath。name属性将指定类型,如pawn或knight。这将是每个对象的唯一值。这也是将用于检索对象的值,因此这是关键路径的完美候选。创建对象存储后,来自pieceTypes[]数组的数据被复制到pieceType存储。
Note
而在onupgradeneeded事件处理程序中,数据可以添加到对象存储中,而无需显式创建事务。为响应onupgradeneeded事件,创建了一个隐式事务。
创建索引
对于piece商店,片段数据中没有可用的自然键,所以您将使用一个键生成器。它将生成唯一的密钥,但这些密钥没有实际意义;它们只是用来满足唯一约束的合成键。最初当你在画板的时候,你会检索所有的对象,所以你不需要知道键是什么。
稍后,您需要取回一个棋子,以便移动它。您将根据棋子在棋盘上的位置找到所需的棋子。为了方便起见,您将基于pos属性向存储添加一个索引。因为没有两个片段可以占据相同的空间,所以可以使用pos属性作为唯一索引。通过将此指定为唯一索引,如果试图插入与现有对象位置相同的对象,将会出现错误。
Caution
因为pos属性是惟一的,所以您可能想用它作为键。然而,由于您将移动棋子,它们的位置将会改变,使用经常改变的键被认为是一种糟糕的设计模式。对于索引数据库,这尤其成问题,因为您实际上不能更改一个键;您必须删除当前对象,然后用新密钥添加它。
创建索引时,必须像这样指定一个keypath:
pieceStore.createIndex
("piecePosition", "pos", { unique: true });
在这种情况下,pos属性就是这个索引的keypath。keypath可能包含多个属性,在这种情况下,索引将基于所选属性的组合。当对象存储有索引时,当对象添加到存储时,索引会自动填充。
重置电路板
您已经创建了piece对象存储,但是还没有填充它。您将在一个单独的函数中完成这项工作。为了理解为什么,让我解释一下数据库的生命周期。第一次显示网页时,会打开数据库,因为它不存在,所以会创建一个新的数据库。这是因为引发了onupgradeneeded事件,并且您实现了这个事件处理程序来创建对象存储。当页面再次显示时(或者只是刷新),这一步将被跳过,因为数据库已经存在。
稍后,当您开始移动和删除这些片段时,您会希望在页面重新加载时将它们移回初始位置。你可以用这种方法来做那件事。现在,您将使用以下代码向script添加一个resetBoard()函数。这不会在创建数据库时调用,而是在加载页面时调用。
function resetBoard() {
var xact = db.transaction(["piece"], "readwrite");
var pieceStore = xact.objectStore("piece");
var request = pieceStore.clear();
request.onsuccess = function(event) {
for (var i in pieces) {
pieceStore.put(pieces[i]);
}
}}
这段代码使用读写模式创建了一个事务,并且只指定了piece对象存储,因为这是您唯一需要访问的对象存储。然后从事务中获得piece存储。clear()方法用于删除存储中的所有对象。最后,pieces[]数组中的所有对象都被复制到对象存储中。
现在将下面以粗体显示的代码添加到onsuccess事件处理程序中。这将在数据库打开后调用resetBoard()函数。()
Note
引发了onupgradeneeded事件,并且其事件处理程序必须在引发onsuccess事件之前完成。这确保了数据库在使用之前已经被正确配置。
var request = dbEng.open("Chess", 1);
request.onsuccess = function (event) {
db = event.target.result;
// Add the pieces to the board
resetBoard();
}
Tip
在resetBoard()函数中,您调用了put()方法(重复 32 次)。但是,您没有获得任何响应对象或实现任何事件处理程序。这段代码看起来是同步运行的。实际上,这些调用是异步处理的,在两种情况下都会返回一个响应对象,但是返回值被忽略了。您可以为这些请求实现onsuccess和onerror事件处理程序。在这种情况下,你作弊了一点。因为不需要像检索数据时那样需要结果值,所以不必处理onsuccess事件。因为这些调用是在一个事务中进行的,所以在更新完成之前,不同的事务对这些对象存储的后续使用将被阻止。
画碎片
到目前为止,您已经打开了数据库,并在必要时配置了对象存储。您还用初始位置填充了件商店。现在,您已经准备好绘制碎片了。为此,您将实现一个drawAllPieces()函数来遍历所有的片段,并实现一个drawPiece()函数来显示单个图像。这些功能将类似于你在第十章中创建的同名功能。但是,这些函数的数据将从新数据库中检索。
drawAllPieces()函数将使用游标来处理piece对象存储中的所有对象。对于每一块,这将提取必要的属性,并将它们传递给drawPiece()函数。然后,drawPiece()函数必须访问pieceType存储以获取类型属性,如height和width,并在适当的位置显示图像。
使用光标
当从对象存储中检索数据时,如果您想使用它的键检索单个记录,使用get()方法,我将在下面描述它。你也可以使用一个索引来选择一个或多个对象,我将在本章后面解释这一点。要获得所有的片段,您需要访问整个对象存储,这将使用游标来完成。
在创建事务并获得对象存储之后,您将调用它的openCursor()方法。这将返回一个IDBRequest对象,您需要为它提供一个onsuccess事件处理程序。当事件触发时,它只提供第一个对象。您可以通过调用continue()方法获得下一个对象。为了演示这一点,将清单 11-6 中所示的函数添加到script元素中。
Listing 11-6. Drawing the Pieces
function drawAllPieces() {
var xact = db.transaction(["piece", "pieceType"]);
var pieceStore = xact.objectStore("piece");
var cursor = pieceStore.openCursor();
cursor.onsuccess = function (event) {
var item = event.target.result;
if (item) {
if (!item.value.killed) {
drawPiece(item.value.type,
item.value.color,
item.value.row,
item.value.column,
xact);
}
item.continue();
}
}
}
这段代码创建了一个使用piece和pieceType对象存储的事务。未指定模式,默认值为readonly。然后它获取piece对象存储并调用它的openCursor()方法。onsuccess事件处理程序从event对象中获取第一个对象(使用event.target.result)。如果这个片段还没有被捕获,那么就调用drawPiece()函数来显示它,接下来您将实现这个函数。稍后我会解释killed属性。您传入它将需要的所有属性,比如type、color、row和column。您还将传入事务对象,以便drawPiece()函数可以使用相同的事务来访问pieceType存储。
调用continue()方法将导致再次引发相同的事件,这一次在event.target.result属性中提供下一个对象。如果没有更多的对象,result属性将是null。这就是你如何知道所有的对象都被处理了。
openCursor()方法提供了一些过滤返回对象的基本功能。如果没有提供参数,它将返回存储中的所有对象。您可以使用下列选项之一来指定关键点范围:
IDBKeyRange.only():指定单个值,只返回匹配的记录。IDBKeyRange.lowerBound():仅返回大于指定值的键值。默认情况下,这是包含性的,因此它也将返回具有完全匹配的键的对象,但是您可以将其更改为仅返回更大的值。IDBKeyRange.upperBound():与lowerBound()一样工作,只是它返回小于或等于指定值的值。我将在本章后面演示这一点。IDBKeyRange.bound():允许您指定下限和上限。您还可以指示这些值是否包含。这些值的默认值为 false,表示不包含。
您还可以向openCursor()函数传递第二个参数来指定记录返回的方向。这个支持的值在IDBCursorDirection枚举中定义。可能的值如下:
next:返回下一条按关键字升序排列的记录(默认值)prev:返回上一条记录nextunique:返回下一个具有不同键的记录;这将忽略重复的键prevunique:返回上一条记录,忽略重复的键
以下示例将返回键大于 3 且小于或等于 7 的对象,并按降序返回这些对象。创建键范围时的最后两个参数表示下限不包含,但上限包含。当打开光标时,第二个参数指定应该使用相反的方向。
var keyRange = IDBKeyRange.bound(3, 7, false, true);
store.openCursor(keyRange, IDBCursorDirection.prev);
检索单个对象
现在,您将实现drawPiece()函数,该函数将在棋盘上绘制一个棋子。它必须首先访问pieceType对象存储来获取图像细节。在这种情况下,您将使用键检索单个对象。pieceType对象存储的关键是type属性。将清单 11-7 中所示的函数添加到script元素中。
Listing 11-7. Drawing a Single Piece
function drawPiece(type, color, row, column, xact) {
var typeStore = xact.objectStore("pieceType");
var request = typeStore.get(type);
request.onsuccess = function (event) {
var img;
if (color === "black") {
img = images[event.target.result.blackImage];
}
else {
img = images[event.target.result.whiteImage];
}
chessContext.drawImage(img,
(75 - event.target.result.width) / 2 + (75 * column),
73 - event.target.result.height + (75 * row),
event.target.result.width,
event.target.result.height);
}
}
这段代码使用传入的同一个事务对象。它获取pieceType对象存储,然后调用它的get()方法。onsuccess事件处理程序获取必要的属性并调用 canvas drawImage()方法。关于在画布上绘制图像的更多信息,请参见第十章。
现在,通过添加粗体显示的代码,在open()调用的onsuccess事件处理程序中添加对drawAllPieces()的调用。
request.onsuccess = function (event) {
db = event.target.result;
// Add the pieces to the board
resetBoard();
// Draw the pieces in their initial positions
drawAllPieces();
}
测试应用
现在您已经准备好测试应用,它将显示初始的起始位置。按 F5 启动应用。您应该会看到一个警告,让您知道数据库正在配置中,如图 11-3 所示。

图 11-3。
The alert showing the database is being configured
当您再次运行此应用时,将不再需要此配置。棋盘应该如图 11-4 所示。

图 11-4。
The completed chessboard with static positions Tip
如果要从计算机中删除数据库,可以找到存储数据库的文件夹,并删除相应的子文件夹。在我的机器上,路径是C:\Users\Mark\AppData\Roaming\Mozilla\Firefox\Profiles\p1i1rsab.default\storage\default。在该文件夹中,每个数据库都有一个子文件夹。子文件夹名称包括协议(http)、域名和端口(如果适用)。对于我的申请,这是http+++localhost+25519。删除此文件夹并重新启动浏览器。该页面应该重新配置数据库,因为它必须创建一个新的数据库。
移动棋子
现在,您已经准备好通过移动棋子来激活棋盘。你将使用在第十章中使用的相同的固定招式。只需更新棋子的位置,然后重新绘制棋盘,就可以移动棋子。然而,有一个复杂的问题;如果一步棋抓住了一个棋子,你需要将它从棋盘上移走。现在,您只需从存储中删除对象,但是在本章的最后我会向您展示一个更好的方法。
定义移动
既然你现在对数据库如此熟悉,你也可以将这些动作存储在数据库中。移动是由开始和结束位置定义的。例如,“将 e2 处的棋子移动到 e3。”您将从 1 到 7 对这些移动进行编号,这样它们将按正确的顺序应用。您需要一个新的对象存储来保存移动细节。为此,您需要指定一个新版本,这将引发onupgradeneeded事件。然后您将添加逻辑函数configureDatabase()来创建新的商店。
EXERCISE 11-2. ADDING THE MOVES STOREAdd the following code to the script element just after the existing static data definitions for the pieceTypes and pieces arrays:
var moves = [
{ id: 1, start: "e2", end: "e3" },
{ id: 2, start: "e7", end: "e5" },
{ id: 3, start: "f1", end: "c4" },
{ id: 4, start: "h7", end: "h6" },
{ id: 5, start: "d1", end: "f3" },
{ id: 6, start: "g7", end: "g6" },
{ id: 7, start: "f3", end: "f7" }
];
Add the following code shown in bold to the end of the configureDatabase() function. This will create and populate the move store when the database is configured.
pieceStore.createIndex
("piecePosition", "pos", { unique: true });
// Store the moves
var moveStore = db.createObjectStore
("move", { keyPath: "id" });
for (var i in moves) {
moveStore.add(moves[i]);
}
On the open() call, change the version to 2 as shown in bold. This will cause the onupgradeneeded event to be raised the next time the page is loaded.
if (!dbEng)
alert("IndexedDB is not supported on this browser");
else {
var request = dbEng.open("Chess", 2 );
Caution
在这个例子中,configureDatabase()函数只是删除所有现有的数据存储,然后重新创建它们。您可以这样做,因为您不需要关心维护任何现有的数据;它是用固定值重新加载的。在许多情况下,您将无法做到这一点。相反,您需要在当前版本的基础上进行特定的更改。例如,如果请求版本 5,而当前版本是 2,您需要添加版本 3 的更改,然后是版本 4 的更改,最后是版本 5 的更改。在计划结构变化时,请记住这一点。
转换位置
piece商店中的对象具有row、column和pos属性。row和column属性遵循第十章中使用的相同约定,其中左上角的方块位于 0,0 处。这与 canvas 的工作方式一致,并简化了drawPiece()的实现。相比之下,pos属性使用在国际象棋中广泛使用的符号,当你从左向右移动时,列(文件)从 a 移动到 h。当你从棋盘底部移动到顶部时,行(等级)从 1 到 8。因此,a1 是左下角的正方形。
在开始移动棋子的繁重工作之前,您将创建一个函数,将pos属性转换成row和column属性。例如,当一个棋子移动到 e3 时,您需要将 e3 转换成相应的row和column坐标,这将是 5(行)和 4(列)。将清单 11-8 中所示的函数添加到现有script元素的末尾。
Listing 11-8. Implementing the computeRowColumn() Function
function computeRowColumn(oStart, end) {
oStart.pos = end;
switch (end.substring(0, 1)) {
case "a": oStart.column = 0; break;
case "b": oStart.column = 1; break;
case "c": oStart.column = 2; break;
case "d": oStart.column = 3; break;
case "e": oStart.column = 4; break;
case "f": oStart.column = 5; break;
case "g": oStart.column = 6; break;
case "h": oStart.column = 7; break;
}
oStart.row = 8 - parseInt(end.substr(1, 1));
}
oStart参数是在起始位置找到的来自piece存储的对象(在我们的例子中是 e2)。end参数是结束位置 e3,它被复制到pos属性,因为这将是片段的新位置。
然后,这段代码使用一个switch语句将 a–h 文件符号转换成 0–7 坐标。然后将其存储在column属性中。row属性的计算方法是从位置中取出最后一位数字,然后从 8 中减去它。
采取行动
就像你在第十章中所做的那样,你将使用一个计时器,每两秒钟进行一次下一步行动。你需要一个timer变量,这样你就可以在动画结束时清空计时器。你还需要跟踪当前的行动。在调用drawBoard()方法之前,将粗体显示的两个变量添加到 script 元素中。
// Get the canvas context
var chessCanvas = document.getElementById("board");
var chessContext = chessCanvas.getContext("2d");()
其中 move number = 1;
每隔几个小时:
drawBoard();
移动一个棋子将需要进行多达五次数据库调用:
Get the next object from the move store (this defines the start and end positions). Get the object at the start position. Get the object at the end position (there will be one only if the move is capturing a piece). Remove the object at the end position (this step will be needed only on some moves). Update the object at the start position (to move it to the end position).
这些调用都将使用同一个事务进行。正如我在本章开始时所演示的,您需要为每个调用嵌套onsuccess事件处理程序。将清单 11-9 中所示的makeNextMove()函数添加到script元素的末尾。
Listing 11-9. Implementing the makeNextMove() Function
function makeNextMove() {
var xact = db.transaction(["move", "piece"], "readwrite");
var moveStore = xact.objectStore("move");
moveStore.get(moveNumber).onsuccess = function (e1) {
var startPos = e1.target.result.start;
var endPos = e1.target.result.end;
var startKey = null;
var oStart = null;
var pieceStore = xact.objectStore("piece");
var index = pieceStore.index("piecePosition");
index.getKey(startPos).onsuccess = function (e2) {
startKey = e2.target.result;
index.get(startPos).onsuccess = function (e3) {
oStart = e3.target.result;
// If there is a piece at the ending location, we'll
// need to update it to prevent a duplicate pos index
removePiece(endPos, oStart, startKey, pieceStore);
}
}
}
}
该函数创建一个事务,该事务将访问move和piece存储。模式被设置为readwrite,因为piece存储中的对象将被修改。然后它获取move存储并调用它的get()方法来指定当前的移动,这是表的关键。这将返回一个对象,并且从onsuccess事件处理程序的结果中提取出start和end位置。
Tip
请注意,这段代码没有显式定义请求变量。相反,onsuccess事件处理程序直接附加到数据库调用上。在前面的示例中,我声明了一个请求变量,然后将事件处理程序附加到该变量上,以帮助您了解发生了什么。然而,将事件处理程序直接附加到方法上可以完成同样的事情,但是稍微简化了代码。
获取对象密钥
对于piece商店,您使用了一个密钥生成器,所以密钥不是对象的一部分。makeNextMove()函数中的代码将使用基于pos属性的索引来检索起始位置的对象(如果有一个片段,还会检索结束位置的对象)。要更新或删除一个对象,您将需要它的密钥。
在起始位置检索piece对象时,这段代码首先从事务中获取piece存储。然后,它从存储中获取piecePosition索引。要获得键值,您需要调用index.getKey()方法,该方法返回请求的起始位置的键。这存储在startKey变量中。
为了获得想要的对象,您将调用index.get()方法,传递要搜索的位置。这将在请求的起始位置返回对象,并将其存储在oStart变量中。
在这两种情况下,数据都在result属性中返回。同样,处理结果的事件处理程序是嵌套的。
获得必要的数据后,调用removePiece()方法,传入以下参数:
end:正在移动的棋子的结束位置oStart:表示正在移动的棋子的对象startID:对象oStart的键pieceStore:将用于执行更新的块存储
执行更新
现在您将实现removePiece()函数。这可能是名不副实,因为它只会在必要时移除一块。将以下代码添加到script元素的末尾,以实现removePiece()功能:
function removePiece(endPos, oStart, startKey, pieceStore) {
var index = pieceStore.index("piecePosition");
index.getKey(endPos).onsuccess = function (e4) {
var endKey = e4.target.result;
if (endKey) {
pieceStore.delete(endKey).onsuccess = function (e5) {
movePiece(oStart, startKey, endPos, pieceStore)
}
}
else
movePiece(oStart, startKey, endPos, pieceStore);
}
}
这段代码获取结束位置的密钥。如果那里有一块,它调用delete()方法移除它,然后调用delete()方法的onsuccess处理程序中的movePiece()函数。请注意,它不检索对象;执行删除只需要键。如果那里没有棋子,它就调用movePiece()函数。当调用movePiece()函数时,它需要的所有数据都被传递给它,包括对象、它的键、结束位置和它将使用的对象存储。
现在您将实现movePiece()函数,该函数将最终执行实际的更新。要更新一个对象,可以调用put()方法。与您之前使用的添加片段的add()方法不同,put()方法需要对象和键。如果没有具有指定键的对象,将添加该对象。将清单 11-10 中所示的movePiece()方法添加到script元素的末尾。
Listing 11-10. Implementing the movePiece() Function
function movePiece(oStart, startID, end, pieceStore) {
computeRowColumn(oStart, end);
var startUpdateReq = pieceStore.put(oStart, startID);
startUpdateReq.onsuccess = function (event) {
moveNumber++;
drawBoard();
drawAllPieces();
if (moveNumber > 7) {
clearInterval(timer);
chessContext.font = "30pt Arial";
chessContext.fillStyle = "black";
chessContext.fillText("Checkmate!", 200, 220);
}
}
}
这段代码首先使用您之前创建的computeRowColumn()函数计算row和column属性。然后它更新对象。在onsuccess事件处理程序中,它增加了moveNumber变量,并使用现有函数绘制棋盘和所有棋子。最后,如果这是最后一步棋,计时器被清零,“将死!”文本被绘制在画布上。
开始播放动画
最后一步是启动定时器,这将导致调用makeNextMove()函数。您将在open()调用的onsuccess事件处理程序中完成这项工作。添加以粗体显示的代码:
var request = dbEng.open("Chess", 2);
request.onsuccess = function (event) {
db = event.target.result;
// Add the pieces to the board
resetBoard();
// Draw the pieces in their initial positions
drawAllPieces();
//开始播放动画
计时器=设定间隔(makenextmove,2000 年);
}
保存您的更改,然后按 F5 启动应用。您应该看到一个警告,让您知道自从您更改数据库版本以来,数据库正在被配置。经过一系列移动后,您应该会看到如图 11-5 所示的完整棋盘。

图 11-5。
The completed chessboard
跟踪捕捉到的片段
当捕获一个片段时,您只需删除该对象,这是可行的,因为该片段不需要显示。但是,如果您的应用希望跟踪捕获的片段,您可能希望将对象保存在存储中。现在我将向您展示如何更改它来更新对象而不是删除它。我还向您展示了如何查询这个商店来列出已经捕获的片段。
第一步,改变removePiece()功能。不是删除结束位置的对象,而是更新它并设置killed属性。您还需要更改pos属性,因为这里有一个唯一的索引。因为没有显示该块,所以位置可以是任何位置。为了确保它是惟一的,您将为它的惟一 ID 加上前缀x。同样,通过在它们前面加上一个x,你将能够查询它们,我将在后面解释。
注释掉delete()调用,并添加以粗体显示的代码:
function removePiece(end, oStart, startID, pieceStore) {
var index = pieceStore.index("piecePosition");
index.getKey(end).onsuccess = function (e4) {
var endID = e4.target.result;
if (endID) {
// pieceStore.delete (endID).onsuccess = function (e5) {
// movePiece(oStart, startID, pieceStore)
//}
index.get(endPos)。onsuccess =函数(e5) {
oEnd = E5 . target . result;
位置代号=‘x '+端盖;
oEnd.killed = true
pieceStore.put(oEnd,endKey)。onsuccess =函数(e6) {
movePiece(oStart、startkey、endPos、Pieter)
}
}
}
else
movePiece(oStart, startID, end, pieceStore);
}
}
现在将以下代码添加到script元素的末尾,以实现displayCapturedPieces()函数:
function displayCapturedPieces() {
var xact = db.transaction(["piece"]);
var textOut = "";
var pieceStore = xact.objectStore("piece");
var index = pieceStore.index("piecePosition");
var keyRange = IDBKeyRange.lowerBound("x");
var cursor = index.openCursor(keyRange);
cursor.onsuccess = function (event) {
var item = event.target.result;
if (item) {
textOut += " - " + item.value.color + " " +
item.value.type + "\r\n";
item.continue();
}
else if (textOut.length > 0)
alert("The following pieces were captured:\r\n" + textOut);
}
}
这段代码仅使用piece存储创建了一个只读事务。然后,它获取商店及其piecePosition索引。它使用x的下限定义了一个键范围。这将只返回以 x 或更大开头的对象。由于棋盘上的棋子将有一个从 a 到 h 开始的位置,这些将被排除。然后,代码遍历光标,将片段细节连接成一个文本字符串。使用alert()功能显示结果。
Caution
请注意,键范围中的字符串比较区分大小写。如果您使用大写的 X,这将不会起作用,因为小写的 a 跟在大写的 X 后面。详见 www.w3.org/TR/IndexedDB/#key-construct 一文。
现在你需要在动画完成后调用这个函数。将下面一行代码添加到makeNextMove()函数的“Checkmate!”显示文本:
displayCapturedPieces();
保存您的更改,然后按 F5 启动应用。动画结束后,您应该会看到如图 11-6 所示的警告。

图 11-6。
Listing the captured pieces
摘要
在本章中,您学习了索引数据库的速成课程。通过一个相当简单的应用,您利用了这项新技术的大部分功能。最大的挑战可能是习惯异步处理。示例应用提供了许多通过onsuccess事件处理程序嵌套连续调用的例子。以下是一些需要记住的关键概念:
- 创建数据库,并在数据库打开时通过响应
onupgradeneeded事件处理程序来创建其结构。如有必要,使用版本强制升级。 - 存储中的对象必须有一个唯一的键,该键可以由内联键的键路径定义,也可以由外联键的键生成器定义。您也可以在添加对象时手动提供密钥,但这通常不是一个实用的解决方案。
- 所有数据访问(读和写)都必须通过事务对象来完成。创建事务时,必须指定范围,即它将使用的对象存储列表以及所需的访问类型。
- 您可以向对象存储添加一个或多个索引。每个索引将对象中的键路径映射到对象的键。
- 使用游标处理对象存储中的多个对象。可以通过指定关键点范围来过滤选定的对象。
- 将对象添加到对象存储中,并在以后更新它。
- 从对象存储中检索对象。
- 通过指定对象的键从对象存储中删除对象。
- 使用索引的
getKey()方法获取对象的键。 - 使用对象存储的
put()方法来添加或更新对象。put()方法需要对象和键。如果找不到指定的键,这将添加对象。
使用索引数据库可以做很多事情。因为数据在客户机上,所以可以避免往返于服务器。
十二、地理定位和制图
本章将展示两种技术,它们提供了强大的功能,使您能够轻松地创建一些有用的网站。地理定位提供了用于确定客户端位置的标准化 API。制图技术增加了在地图上显示该位置以及其他兴趣点的能力。总之,这些形成了一个有许多有用应用的平台。
在本章中,您将使用地理定位 API 来查找您的当前位置。根据可用硬件和环境的不同,定位的准确性会有很大的不同。然而,HTML5 定义了一个在所有设备上使用的标准 API,因此您可以提供与设备无关的解决方案。
仅仅知道你所在位置的经度和纬度并没有太大的帮助。为了使用这些数据,您将使用 Bing Maps API 在地图上显示该位置。然后,您可以绘制其他感兴趣的点,并查看它们与您当前位置的关系。
了解地理定位
虽然在技术上不是 HTML5 规范的一部分,但 WC3 已经定义了一个用于访问地理位置信息的标准 API,当前所有主流浏览器版本都支持该 API。但是,确定位置的技术会因设备功能和客户端环境的不同而有很大差异。
勘测地理定位技术
可以使用几种技术来确定当前位置,包括以下技术:
- 全球定位系统:全球定位系统与卫星通信,以极高的精度确定当前位置,特别是在农村地区。城市中的高层建筑会影响精度,但在大多数情况下,GPS 提供了良好的结果。最大的限制是这在室内效果不好。要使用 GPS,设备必须有特定的 GPS 硬件,但这在移动设备上越来越常见。
- Wi-Fi 定位:Wi-Fi 网络的覆盖范围相对较短,Skyhook Wireless 等系统维护着一个关于 Wi-Fi 网络及其位置的大型数据库。简单地连接到 Wi-Fi 网络就能很好地知道你在哪里。然而,通常情况下,您可能在多个网络的范围内,系统可以使用三角测量来更准确地确定位置。当然,这需要你有一个支持 Wi-Fi 的设备,在没有 Wi-Fi 网络的农村地区是不行的。
- 手机信号发射塔三角测量:这与 Wi-Fi 定位使用相同的原理,只是它使用手机信号发射塔。然而,这并不准确,因为手机信号发射塔的覆盖范围要大得多。由于所有的手机都将有能力与手机信号塔通信,这项技术有着广泛的应用。
- IP 地址块:每台连接到互联网的设备都有一个 IP 地址,通常由 ISP 提供。每个 ISP 都有一组可以使用的 IP 地址,这些地址通常是按地理位置分配的。因此,您连接到互联网的 IP 地址可以提供一个大致的位置,通常是一个大都市地区。但是,有几个因素会产生不正确的结果,例如 NAT 地址。
这些技术中的每一种都有不同的硬件要求,并提供不同级别的准确性。通过地理定位规范,您可以轻松地从浏览器请求当前位置,并让浏览器根据当前硬件和对外部资源(包括卫星、手机信号发射塔和 Wi-Fi 网络)的访问来确定提供当前位置的最佳方式。
使用地理定位数据
大多数人认为地理定位是一种提供路线指引的设备,但这只是这项技术的一个应用。当然,这需要精确的位置,只有通过 GPS 才能获得。然而,即使当前位置远不准确,您的网站仍然可以利用这些信息。例如,即使位置仅由 IP 地址决定,这通常也足以设置默认语言。您可能需要允许最终用户覆盖这一点,但是您的大多数观众将会看到以他们的母语显示的初始页面。
当检索当前位置时,地理定位服务也返回估计的精度。您的应用应该使用它来确定将要提供的特性。例如,假设您正在为美国邮政服务创建一个网页,显示最近的邮局在哪里。如果当前位置是高精度已知的,则网页可以显示地图并指示当前位置以及附近的邮局。此外,它可以为每个人提供估计的驾驶时间。
但是,如果知道的位置不太准确,该页面可以显示一张地图,显示该地区邮局的位置。据推测,用户将知道他们在哪里,并且可以使用该信息来确定最佳使用位置。但是,如果准确性很差,页面应该提示输入邮政编码,然后根据用户输入显示最近的邮局。因此,根据准确性,应用可以适当地降低功能。
使用地理定位 API
为了演示如何使用地理定位 API,您将创建一个简单的 web 页面,该页面调用 API 来确定您的当前位置。最初,这些数据将作为文本显示在网页上。稍后,您将在地图上显示该位置。
创建 Visual Studio 项目
首先,您将创建一个 Visual Studio 项目,使用与前几章中使用的相同的空模板。
EXERCISE 12-1. CREATING THE VISUAL STUDIO PROJECTStart Visual Studio 2015 and click the New Project link from the Start page. In the New project dialog box, select the ASP.NET Web Application template. Select a location for this project and enter the project name Chapter 12. In the next dialog box, select the ASP.NET 5 Empty template. Click the OK button and the project will be created. Open the Startup.cs file and comment out the implementation of the Configure() method as you did in previous projects. Right-click the new wwwroot folder and click the Add and New Item links. In the Add New Item dialog box, select the HTML page, enter the name Index.html, and click the Add button. In the Index.html file, add the following div element inside the empty body that was created:
<div style="width:800px; height:50px;">
<span id="lbl"> </span>
</div>
使用地理位置对象
地理定位 API 由geolocation对象提供,您可以通过navigator对象访问它,如下所示:
navigator .geolocation
如果返回虚假值,如null或undefined,则当前浏览器不支持地理定位。您可以使用如下代码来检查支持:
if (!navigator.geolocation) {
alert(“Geolocation is not supported”);
}
else
// do something with geolocation
要获得当前位置,使用getCurrentPosition()函数,它有三个参数:
- 当调用成功时执行的回调函数
- 发生错误时调用的错误回调函数
- 包含零个或多个选项的
PositionOptions集合
最后两个参数可以省略。支持以下选项:
maximumAge:浏览器可以缓存之前的位置并返回,而无需实际尝试确定位置。然而,maximumAge属性指定了在不重新查询当前位置的情况下,前一个位置可以被重用多长时间(以毫秒为单位)。timeout:timeout属性指定浏览器应该等待来自geolocation对象的响应多长时间。这也用毫秒表示。enableHighAccuracy:这只是给浏览器的一个提示。如果您不需要更高的精度来实现某个特定目的,将此设置为 false 可能会产生更快的响应或使用更少的功率,这是移动设备的一个考虑因素。
如果调用成功,位置将被传递给指定的回调函数。Position对象包括一个coords对象,该对象包含以下必需属性:
latitude(以度为单位)longitude(以度为单位)accuracy(单位为米)
此外,根据环境和可用硬件,可能会提供以下可选属性。如果不支持这些,它们将被设置为 null。(可选属性通常仅在使用 GPS 时可用。)
altitude(单位为米)altitudeAccuracy(单位为米)heading(单位为度;north= 0,west= 90,以此类推)speed(单位为米/秒,静止时为NaN)
这些属性可以通过回调函数获得,如下所示:
function successCallback(pos) {
var lat = pos.coords.latitude;
var long = pos.coords.longitude;
var accuracy = pos.coords.accuracy + " meters";
}
如果调用不成功,PositionError对象被传递给错误回调函数。这个对象包括一个code属性和一个message属性。误差code将具有三个可能值中的一个。
- 1:
PERMISSION_DENIED - 2:
POSITION_UNAVAILABLE - 3:
TIMEOUT
Caution
您的应用将获得位置并简单地显示它(并在以后绘制地图)。但是,您的脚本可以很容易地将这些信息传递回服务器,这是一个潜在的隐私问题。由于浏览器无法控制客户端如何处理这些信息,出于隐私原因,浏览器可能会阻止对geolocation对象的访问。在这种情况下,返回PERMISSION_DENIED错误代码。稍后我将演示这一点。
如果客户端正在移动,并且您想要持续监视当前位置,您可以使用setInterval()函数重复调用getCurrentLocation()函数。为了简化起见,geolocation对象包含了一个watchPosition()函数。它采用与getCurrentLocation()函数相同的三个参数(成功回调、错误回调和选项)。每当位置改变时,回调函数就被调用。watchPosition()函数返回一个定时器句柄。当您想要停止监控位置时,您可以将该句柄传递给clearWatch()函数,如下所示:
var handle = geolocation.watchPosition(callback);
...
geolocation.clearWatch(handle);
显示位置
现在,您将向您的应用添加代码,以获取当前位置并显示它。该网页有一个span元素,其id为lbl。你将得到geolocation对象并调用它的getCurrentLocation()函数。成功和错误回调函数都将在span元素中显示适当的结果。
EXERCISE 12-2. DISPLAYING THE LOCATIONAdd the script element shown in Listing 12-1 to the end of the body element.
清单 12-1。显示位置
<script type="text/javascript">
var lbl = document.getElementById("lbl");
if (navigator.geolocation) {
navigator.geolocation
.getCurrentPosition(showLocation,
errorHandler,
{
maximumAge: 100,
timeout: 6000,
enableHighAccuracy: true
});
}
else {
alert("Geolocation not suported");
}
function showLocation(pos) {
lbl.innerHTML =
"Your latitude: " + pos.coords.latitude +
" and longitude: " + pos.coords.longitude +
" (Accuracy of: " + pos.coords.accuracy + " meters)";
}
function errorHandler(e) {
if (e.code === 1) { // PERMISSION_DENIED
lbl.innerHTML = "Permission denied. - " + e.message;
} else if (e.code === 2) { //POSITION_UNAVAILABLE
lbl.innerHTML = "Make sure your network connection is active and " +
"try this again. - " + e.message;
} else if (e.code === 3) { //TIMEOUT
lbl.innerHTML = "A timeout ocurred; try again. - " + e.message;
}
}
</script>
Press F5 to start the application. The first time a site tries to access the geolocation object, you will get a prompt like the one shown in Figure 12-1.

图 12-1。
Prompting for geolocation access
注意,我在这个演示中使用的是 IE 11。如果您使用的是不同的浏览器,该提示可能会有所不同。
To test the error handler, expand the “Options for this site” drop-down and select the “Always deny and don’t tell me” option. The page should display an error message like the one shown in Figure 12-2.

图 12-2。
Displaying the access denied error Once you have set this option, IE will no longer prompt you anymore but will always deny the access. To clear this, select the Tools menu and then select Internet Options. In the middle of the Privacy tab there are options to control the geolocation access. Click the Clear Sites button shown in Figure 12-3.

图 12-3。
Clearing the site access choices Click the OK button to close this dialog box. Refresh the web page. It should now prompt you again. This time select the “Always allow” option. Your current location should be displayed as shown in Figure 12-4.

图 12-4。
Displaying the current location
我用的是普通的局域网连接的机器,没有手机或 GPS 支持,所以它使用 IP 地址来确定位置。因此,精度估计为 1.8 公里(刚刚超过 1 英里)。
Note
地理定位可以在当前所有的浏览器上运行。然而,如果你在 IE 8 等较旧的浏览器上尝试这个应用,你会看到不支持地理定位的警告。
使用制图平台
简单地显示纬度和经度并不有趣(或者没有帮助)。然而,显示你相对于其他兴趣点的位置更有用。将它们显示在带有道路和其他参考点的地图上,可以让这些信息真正发挥作用。幸运的是,测绘技术已经变得如此复杂和容易,这真的很容易做到。
Note
在本章的演示中,我将使用 Bing 地图。还有其他可用的地图平台。如果你感兴趣,请查看位于 http://en.wikipedia.org/wiki/Comparison_of_web_map_services 的文章,了解不同地图服务的概述。
正在创建阿炳地图帐户
要使用必应地图,你需要先设置一个账户,这个账户对开发者是免费的。创建帐户后,您将收到一个密钥,您需要在访问映射 API 时包含该密钥。我将带你完成建立帐户的过程。
EXERCISE 12-3. CREATING A BING MAPS ACCOUNTGo to the Bing Maps site at this address: www.microsoft.com/maps/create-a-bing-maps-key.aspx . You need to get a key that will allow you to access the mapping API. Go to the Basic Key tab. A free, basic key is fine for working through these exercises. Click the Get the Basic Key link near the bottom of the page. In the next page, you’ll need to log in with a Windows Live ID. If you don’t have one, click the Create button to create an account. Once you have signed in, you should see the “Create account” page shown in Figure 12-5.

图 12-5。
The “Create account” page Enter an account name. This is just for you to identify it if you have multiple accounts; Testing is fine. The e-mail address should default in from your Windows Live account. Make sure you select the check box agreeing to the terms of use. Click the Save button to create the account. From the My account menu, select the “Create or view keys” link. You probably won’t have any existing keys shown. Click the link to create a new key. In the “Create key” page, enter an application name such as HTML5 Test. For the URL, enter http://localhost and select Dev/Test for the application type, as shown in Figure 12-6. Enter the characters that are displayed at the bottom of the form and click the Submit button.

图 12-6。
Creating a key
注意 Bing 地图会监控您的密钥的使用情况。然而,由于您实际上并没有将它部署到面向公众的网站上,所以这实际上并不适用。如果您正在开发一个商业应用,您可以出于开发目的使用一个免费的密钥,但是您将需要为实时网站购买一个密钥。
After the key has been generated, you should see it displayed on the page. Save this because you will need it later.
添加地图
现在,您将向网页添加地图。您将首先向包含地图的页面添加一个div。您还需要添加对用于操作地图的 Ajax 脚本的引用。然后,您将显示地图,以您的当前位置为中心。
EXERCISE 12-4. ADDING A MAPAdd the code shown in bold to the body element, which will add the div that the map will be displayed in.
<body>
<div style="width:800px; height:50px;">
<span id="lbl"> </span>
</div>
<div id="map" style="width:800px; height:600px;">
</div>
Add the following reference inside the head element. This will enable your page to call the map API.
<script
type="text/javascript"
src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0
</script>
Add the following declaration at the top of the existing script element (inside the body element). This will store a reference to the map object.
var map = null;
Modify the showLocation() function, adding the code shown in bold in Listing 12-2. Enter your Bing Maps key where is says <use your key here>. The key should be enclosed in double quotes.
清单 12-2。修改后的 showLocation()函数
function showLocation(pos) {
lbl.innerHTML =
"Your latitude: " + pos.coords.latitude +
" and longitude: " + pos.coords.longitude +
" (Accuracy of: " + pos.coords.accuracy + " meters)";
// Save the current location
var lat = pos.coords.latitude;
var long = pos.coords.longitude;
// Create the map
map = new Microsoft.Maps.Map(document.getElementById("map"),
{ credentials:
"<use your key here>" });
// Center it on the current location
map.setView({ zoom: 18, center: new Microsoft.Maps.Location(lat, long) });
}
Press F5 to start the application. Depending on your location, your page should look like Figure 12-7. Notice the controls at the top-left corner of the page. You can use this to zoom in or out and pan in any direction. The Automatic mode will switch to the satellite view if the map is zoomed in sufficiently.

图 12-7。
Displaying the initial map
当调用setView()函数来指定中心位置时,这段代码也将缩放设置为 18。根据您的应用,您可能不想一开始就放大那么多。用 15 或 16 试试这段代码,看看看起来怎么样。当然,一旦显示地图,用户也可以调整缩放。
添加图钉
现在,您将在地图上显示一些图钉。要添加图钉,首先创建一个Pushpin对象,指定其位置。然后将其添加到地图的实体集合中。首先,您将在当前位置添加一个默认图钉。稍后,您将添加自定义图钉来指示兴趣点。
将下面以粗体显示的代码添加到showLocation( )函数的末尾:
// Center it on the current location
map.setView({ zoom: 18, center: new Microsoft.Maps.Location(lat, long) });
// Mark the current location
var pushpin = new Microsoft.Maps.Pushpin
(new Microsoft.Maps.Location(lat, long), null);
map.entities.push(pushpin);
lat和long变量包含用于使地图居中的相同值。按 F5 启动应用。你应该会看到一个图钉指示当前位置,如图 12-8 所示。

图 12-8。
Adding a pushpin in the current location
地图在网页中最常见的用途之一是显示附近的位置。例如,您可能有多个商店位置,并且您希望显示每个位置。或者,您可能在警察局工作,想要找出某些犯罪发生的地点。你可以有一个公共交通系统,并想显示所有的公共汽车或火车站在哪里。
这些场景基本上都是一样的;您有一组想要在地图上显示的位置。您可以根据需要添加任意多个位置。对于每一个,只需创建一个图钉对象并将其添加到实体集合中。如果你有不止一个位置,你应该让图钉看起来不同,这样用户就可以很容易区分它们。
在本演示中,您将指出附近哪里有洗手间。您将使用一个带有熟悉的洗手间图标的图像,而不是标准的图钉。通常情况下,您会查询服务器以获得基于客户端位置的位置列表。然而,为了简化这个练习,这些将被硬编码。
Caution
我在硬编码洗手间的位置,可能离你现在的位置很远。您可以提供离您最近的不同洗手间位置,或者简单地覆盖您的当前位置以匹配我的位置。这将与洗手间的位置一致。
EXERCISE 12-5. ADDING CUSTOM PUSHPINSIn Solution Explorer, right-click the wwwroot folder project and click the Add and New Folder links. Enter the name images. The source code download contains a restroom.gif image file. Drag this onto the wwwroot\images folder in Solution Explorer. Add the following declaration at the top of the existing script element. This defines the locations of the restrooms.
var restrooms = [
{ lat: 37.810079, long: -122.410806 },
{ lat: 37.809079, long: -122.410206 },
{ lat: 37.811279, long: -122.410446 }
];
Add the following code to the showLocation() function just before creating the map object. This will override your current location to be near where the restrooms are.
// Override these for testing purposes
lat = 37.811079;
long = -122.410546;
Add the following functions to the end of the script element. The markRestrooms() function iterates through this array, calling the markRestroom() function for each. The markRestroom() function adds a single pushpin. This first creates an options collection that defines the image file to use as well as the size of the image. This is passed in when creating the Pushpin object.
function markRestrooms() {
for (var i in restrooms) {
markRestroom(restrooms[i].lat, restrooms[i].long);
}
}
function markRestroom(lat, long) {
var pushpinOptions = { icon: '/img/restroom.gif', width: 35, height: 35 };
var pushpin = new Microsoft.Maps.Pushpin
(new Microsoft.Maps.Location(lat, long), pushpinOptions);
map.entities.push(pushpin); ()
}
Add this function call at the end of the showLocation() function to display the additional pushpins:
// Display the restroom locations
markRestrooms();
Press F5 to debug the application. You should now see pushpins where the restrooms are located, as shown in Figure 12-9.

图 12-9。
Adding the restroom pushpins()
注意,这纯粹是虚构的数据。如果你在读这本书的时候碰巧在旧金山的 39 号码头,不要用这张地图去找厕所。
使用映射 API 可以做更多的事情。例如,您可以显示到达所选兴趣点的方向。你甚至可以显示当前交通拥挤的地方。在 www.bingmapsportal.com/isdk/ajaxv7 查看互动 SDK。您可以尝试每个特性,相应的 JavaScript 代码会显示在地图下方。
摘要
在本章中,您将地理定位功能与 Bing 地图结合起来,创建了一个真正有用的网站。地理定位请求是异步处理的。获得geolocation对象后,调用它的getCurrentPosition()函数,并指定成功和错误回调函数。当位置被检索到时,Position对象被传递给回调函数。它包含纬度、经度和估计精度。如果客户端具有 GPS 功能,Position对象还将包括高度、速度和方向。
Bing Maps 等地图平台非常容易使用,并且可以集成到您的网页中。在这个应用中,您显示了地图并以当前位置为中心。您还添加了图钉来显示附近的洗手间在哪里。
十三、双向通信
正如我在第一章中解释的,web 应用从 web 服务器中提取数据。客户端通过向服务器发送请求来启动传输。然后在浏览器中呈现响应。所有的 web 应用设计都基于这种请求-响应范式。然而,websocket 技术打开了一个全新的世界,允许服务器发起与客户端应用的通信。
套接字技术并不新鲜。几十年来,套接字一直被用作应用之间点对点通信的有效协议。然而,令人兴奋的消息是,W3C 已经将 WebSocket API 规范作为 HTML5 保护伞的一部分。随着标准协议的定义和兼容浏览器的出现,可以预见会有越来越多的 web 应用利用这项技术。
在本章中,您将构建一个使用 WebSockets 实现多会话聊天应用的解决方案,该应用使代理能够同时与几个客户聊天。该解决方案将包括一个托管在控制台应用中的 WebSocket 服务器,您将使用 C# .NET 创建该服务器。为此,您需要理解 WebSocket 协议,因此我将首先解释这项技术,然后向您展示如何创建您自己的定制服务器。然后,您将利用 HTML5 中的本地 WebSocket 支持来实现代理和客户客户端应用。
Caution
在撰写本文时,W3C 规范仍处于草案阶段,可能会有变化。现在全心全意地投入其中可能还为时过早。然而,由于这是 HTML5 规范中非常有前途的部分,我想向您介绍 WebSockets。只是要意识到事情会变。你可以在 http://dev.w3.org/html5/websockets 查看 W3C 规范的当前状态。当前所有主流浏览器都支持 WebSockets。
了解 WebSockets
套接字为应用之间的双向传输提供了一种机制,包括对等通信。正如我将在本章中解释的,WebSockets 是套接字的一个具体实现,它使 web 应用能够与 web 服务器进行通信。图 13-1 展示了浏览器和服务器之间传递的消息。在建立连接的一系列握手消息之后,双方可以互相发送消息。

图 13-1。
The WebSockets messages Tip
虽然 WebSockets 不提供对等通信,但正如您将在本章后面看到的,可以实现服务器将消息从一个客户端路由到另一个客户端。客户端 A 可以向服务器发送消息,然后简单地转发给客户端 b。这模拟了对等消息传递,同时提供了对路由的控制。
完成握手
WebSockets 的工作需要大量的握手和协议操作。幸运的是,浏览器为您实现了客户端协议,这使得编写使用 WebSockets 的应用变得非常容易。握手消息使用 HTTP 协议。一旦建立了连接,就使用 WebSocket 协议发送后续消息。
Note
各种浏览器已经提出并实现了几种协议版本。目前的版本(在撰写本文时)是版本 13,所有主流浏览器都支持它。您可以在 https://tools.ietf.org/html/rfc6455 查看 13 版协议的规范。
当浏览器向服务器发送握手请求时,该过程开始。握手请求将由多行文本组成,如下所示:
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: localhost:8100
Origin: http://localhost:29781
Sec-WebSocket-Key: <request key>
Sec-WebSocket-Version: 13
该请求包括关于客户端地址及其支持的协议的信息。在这个例子中可以看到,13是为Sec-WebSocket-Version指定的。请求密钥由浏览器生成,每次发出连接请求时都会有所不同。
作为回报,服务器将返回如下响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <response key>
响应密钥由服务器生成,并基于指定的请求密钥。我将在本章的后面解释这个算法。
构建 WebSocket 框架
一旦握手完成并且协议已经协商好,就可以在客户机和服务器之间交换消息。这些消息使用 WebSocket 协议发送。消息总是单向的;不需要响应。当然,您可以发送响应,但这只是另一个反方向的单向消息。两个端点总是在监听消息。
消息以帧的形式发送。该帧由一系列字节组成,这些字节表示应该如何处理消息。消息的其余部分包含正在发送的实际数据,通常称为有效负载。图 13-2 为车架布局图。

图 13-2。
The WebSocket frame
帧的初始部分最多由 14 个字节组成。前两个字节将用于所有帧。第一位表示这是否是最后一帧。一条消息可以在多个帧中传输,该位应在最后一帧置 1,以表示消息已完成。接下来的三位留作将来使用。第一个字节的后半部分包含一个操作码,指定这是什么类型的消息。值 0x 3–0x 7 和 0xB–0xF 是为将来的控制帧保留的,但目前定义了以下值:
- 0x0 表示这是一个连续帧。
- 0x1 指定有效负载包含文本。
- 0x2 表示二进制有效载荷。
- 0x8 表示连接正在关闭。
- 0x9 指定消息是 ping。
- 0xA 指定消息是 pong。
第二个字节的第一位表示使用了屏蔽。稍后我将描述掩蔽。该字节的剩余部分指定有效载荷长度。对于小于 126 字节的有效负载,长度在此指定。但是,如果长度在 126 到 32,183 之间,则长度被设置为 126,实际长度在接下来的两个字节中提供。对于超过这个长度的消息,长度设置为 127,实际长度在接下来的八个字节中指定。因此,根据消息的长度,帧将包含 0 到 8 个额外的字节。
接下来的四个字节包含屏蔽键。如果不使用屏蔽,则忽略这一点。之后才是真正的有效载荷。
对于包含文本 Hello 的简单无掩码消息,帧将包含以下字节:
0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f
二进制的第一个字节 0x81 是10000001。第一位被设置为指示这是最后一帧,最后一位被设置为指示有效载荷包含文本。下一个字节指定了紧跟其后的五个字符的有效载荷长度。其余五个字节包含 H、e、l、l 和 o 字符。
揭开框架的面纱
出于安全原因,来自客户端的所有帧都应该被屏蔽。屏蔽是一种简单的编码方案,它使用不同于每一帧的屏蔽键。浏览器会为您处理这些问题;但是,服务器需要取消数据屏蔽。发送到客户端的帧不应被屏蔽。
屏蔽键在长度后面的四个字节中提供。屏蔽密钥由客户端随机生成。为了解除数据屏蔽,对于有效载荷中的每个字节,XOR 运算符应用于该字节和屏蔽密钥中的相应字节。第一个有效载荷字节与屏蔽键的第一个字节进行异或运算,第二个字节与屏蔽的第二个字节进行异或运算,依此类推。第五个字节与掩码的第一个字节进行异或运算。
这可以通过下面的 C#表达式来完成。这里假设payload[]包含一组屏蔽数据,而mask[]包含 4 字节屏蔽键。
for (int i = 0; i < length; i++)
{
payload[i] = (byte)(payload[i] ^ masks[i % 4]);
}
i % 4表达式获得适当的屏蔽字节,而^操作符执行 XOR 运算。处理完所有的字节后,payload[]数组将包含未屏蔽的数据。
WebSocket 服务器
要使用 WebSockets,您需要提供一个实现服务器端协议的应用。我将向您展示如何使用。NET 并承载在控制台应用中。正如您可能期望的那样,有必要进行一些位和字节操作(没有双关语)。
Note
有许多开源 WebSocket 服务器实现。这篇文章提供了几个链接,你可能想考虑将来参考: http://stackoverflow.com/questions/1530023/do-i-need-a-server-to-use-html5s-websockets 。这些都是在各种平台上实现的,包括 JavaScript 和 PHP。其中许多支持多种协议。
要创建 WebSocket 服务器,您将首先实现以下功能:
- 连接握手
- 监听消息
- 解码 WebSocket 帧
- 构建和发送消息帧
一旦基础设施完成,您就可以为您的应用提供所需的定制服务器特性。毕竟,WebSockets 的全部意义在于允许服务器与客户端进行通信。
设计代理聊天应用
在本章中,您将构建一个服务器和两个客户端应用,它们将允许代理同时与多个客户端聊天。代理将使用代理 web 应用登录并连接到服务器,让服务器知道代理已准备好接受聊天会话。代理应用将被设计为处理多达四个同时进行的聊天会话。然后,客户端可以使用客户端 web 应用连接到服务器。每个客户端被路由到一个可用的代理,并且聊天会话开始。从这一点来说,服务器只是在客户端和代理之间转发消息。图 13-3 描述了这种通信。

图 13-3。
The WebSocket communication
要作为代理进行连接,系统需要提供一些身份验证,以确保只有经过授权的用户才能响应客户端。为了模拟这种情况,代理应用将使用提供登录功能的标准 ASP.NET web 表单。然后,您将添加一个自定义聊天页面,该页面将允许代理同时响应四个聊天会话。然而,任何人都应该能够作为客户端连接,所以您将使用您在前面章节中使用的空模板。
两个应用都将使用正常的握手协议连接到 WebSocket 服务器。一旦连接,代理将向服务器发送包含他们姓名的消息。这将通知服务器一个新的代理已经上线。代理的连接将保存在集合中以供将来使用。
建立连接后,客户端应用还会向服务器发送一条消息,指定客户端的名称。然后,服务器将找到第一个有开放聊天会话的可用代理,并向该代理发送消息,提供客户端的名称。然后,代理页面将在页面上保存客户端的名称。同时,服务器将向客户端发送一条消息,让他们知道已经为他们分配了一个代理。
此时,客户端和代理都可以向服务器发送消息,该消息将被转发给另一个应用。因为代理应用可以有四个活动会话,所以服务器会在消息前面加上客户端号,这样代理应用就知道要更新哪个会话。
如果客户端断开连接,服务器将向代理发送一条消息,让他们知道这一点。然后,代理应用将清除相应的聊天会话。如果代理断开连接,与该代理有活动会话的所有客户端也会收到通知,并被指示尝试重新连接。
创建简单的应用
在本节中,您将构建一个处理基本消息协议的 WebSocket 服务器,并使用一个简单的 web 客户端对其进行测试。最初,服务器只是将消息回显给客户机。稍后您将添加聊天应用所需的功能。在本练习中,服务器将托管在一个控制台应用中。
创建 WebSocket 服务器
为了实现 WebSocket 服务器,您将创建一个WsServer类。该类创建一个套接字,用于侦听新的连接。当接收到一个连接时,它为该连接创建另一个套接字,并执行我前面描述的握手。如果握手成功,它将创建一个管理客户端连接的WsConnection类的实例。
WsConnection类使用新的套接字监听该连接上的传入消息。这个类调用ReadMessage()方法来处理传入的消息。这将处理所有可能需要的帧解码和去屏蔽。WsConnection类还提供了一个SendMessage()方法,该方法将向连接另一端的客户端发送消息。
如果提供了处理程序,WsConnection类提供了两个被引发的事件。当收到传入消息时,引发第一个事件。当连接关闭时,将引发第二个事件。WsServer类将为这些事件提供事件处理程序。
EXERCISE 13-1. CREATING A SIMPLE WEBSOCKET SERVERStart Visual Studio 2015 and create a new project named WsServer. Select the Console Application template from the Windows category. Change the solution name to Chapter 13 , as shown in Figure 13-4.

图 13-4。
Creating a console application project In Solution Explorer, right-click the WsServer project and click the Add and Class links. Enter WsServer.cs for the class name. Enter the code shown in Listing 13-1 for the initial implementation of this class.
清单 13-1。实现 WsServer 类
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Security.Cryptography;
namespace WsServer
{
public class WsServer
{
#region Members
// This socket listens for new connections
Socket _listener;
// Configurable port # that is passed in the constructor
int _port;
// List of connections
List<WsConnection> _unknown;
#endregion Members
public WsServer(int port)
{
_port = port;
// This is a list of active connections
_unknown = new List<WsConnection>();
}
public void StartSocketServer()
{
try
{
// Create a socket that will listen for messages
_listener = new Socket(AddressFamily.InterNetwork,
SocketType.Stream,
ProtocolType.IP);
// Create and bind the endpoint
IPEndPoint ip = new IPEndPoint(IPAddress.Loopback, _port);
_listener.Bind(ip);
// Listen for new connections - the OnConnect() method
// will be invoked to handle them
_listener.Listen(100);
_listener.BeginAccept(new AsyncCallback(OnConnect), null);
}
catch (Exception ex)
{
}
}
void MessageReceived(WsConnection sender, MessageReceivedEventArgs e)
{
string msg = e.Message;
sender.SendMessage("echo: " + msg);
}
void Disconnected(WsConnection sender, EventArgs e)
{
_unknown.Remove(sender);
}
}
}
调用StartSocketServer()方法让服务器开始工作。它创建一个Socket对象,并使用指定的端口对其进行配置。这个方法被硬编码为使用localhost地址。一旦配置了端点,就会调用Socket对象的BeginAccept()方法。当接收到新连接时,这将调用指定的回调方法(OnConnect)。OnConnect()函数被异步调用。稍后您将提供它的实现,该实现将调用EndAccept()函数来处理细节。MessageReceived()事件处理程序只是将输入消息写入控制台,然后将消息回显给客户机。管理这个连接的WsConnection对象被传递给事件处理程序。这段代码使用它的SendMessage()方法发送回一个回应。Disconnected()事件处理程序从其活动列表中删除该连接,并在控制台窗口中显示一条消息。
Add the PerformHandshake() method using the code shown in Listing 13-2.
清单 13-2。实现握手协议
private void PerformHandshake(Socket s)
{
using (NetworkStream stream = new NetworkStream(s))
using (StreamReader reader = new StreamReader(stream))
using (StreamWriter writer = new StreamWriter(stream))
{
string key = "";
// Read the input data using the stream reader, one line
// at a time until all lines have been processed. The only
// item that we need to get is the request key.
string input = "Empty";
while (!string.IsNullOrWhiteSpace(input))
{
input = reader.ReadLine();
if (input != null &&
input.Length > 18 &&
input.Substring(0, 18) == "Sec-WebSocket-Key:")
// Save the request key
key = input.Substring(19);
}
// This guid is used to generate the response key
const String keyGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
string webSocketAccept;
// The response key in generated by concatenating the request
// key and the special guid. The result is then encrypted.
string ret = key + keyGuid;
SHA1 sha = new SHA1CryptoServiceProvider();
byte[] sha1Hash = sha.ComputeHash(Encoding.UTF8.GetBytes(ret));
webSocketAccept = Convert.ToBase64String(sha1Hash);
// Send handshake response to the client using the
// stream writer
writer.WriteLine("HTTP/1.1 101 Switching Protocols");
writer.WriteLine("Upgrade: websocket");
writer.WriteLine("Connection: Upgrade");
writer.WriteLine("Sec-WebSocket-Accept: " + webSocketAccept);
writer.WriteLine("");
}
}
PerformHandshake()方法创建一个NetworkStream对象,将Socket对象传递给它的构造函数。这是为此连接创建的新套接字。它使用一个StreamReader对象读取输入的数据,然后使用一个StreamWriter将数据发送回来。通过创建这些嵌套的using语句,您不必担心如何处理它们。请记住,握手是使用 HTTP 协议完成的,因此读取和发送数据的行为不使用 WebSocket 帧。
StreamReader对象用于读取输入,一次一行。您不需要这些数据,因为在这个练习中,您假设正在请求正确的协议。然而,在更一般的情况下,您可能需要支持多种协议,因此您需要读取和解释正在发送的内容。然而,您将需要请求键,所以这是从适当的输入行提取的。
然后,响应密钥与一个特殊的 guid 值连接在一起。这在版本 13 规范( https://tools.ietf.org/html/rfc6455 )中有记载。然后使用 SHA1 算法对得到的字符串进行哈希运算。最后,StreamWriter对象用于发送响应,包括生成的密钥。
Add the OnConnect() event handler using the code in Listing 13-3.
清单 13-3。实现 OnConnect()事件处理程序
private void OnConnect(IAsyncResult asyn)
{
// create a new socket for the connection
Socket socket = _listener.EndAccept(asyn);
// Perform the necessary handshaking
PerformHandshake(socket);
// Create a WsConnection object for this connection
WsConnection client = new WsConnection(socket);
_unknown.Add(client);
// Wire-up the event handlers
client.MessageReceived += new MessageReceivedEventHandler(MessageReceived);
client.Disconnected += new WsDisconnectedEventHandler(Disconnected);
// Listen for more connections
_listener.BeginAccept(new AsyncCallback(OnConnect), null);
}
OnConnect()方法通过调用EndAccept()方法为这个连接获取一个新的Socket。它调用了PerformHandshake()方法并创建了一个WsConnection类,接下来您将实现这个类。然后它连接事件处理程序,这样当收到消息或连接关闭时,WsServer对象将得到通知。最后,再次调用BeginAccept()来监听更多的连接。
In Solution Explorer, right-click the WsServer project and click the Add and Class links. Enter WsConnection.cs for the class name. Enter the code shown in Listing 13-4 as the initial implementation for this class.
清单 13-4。实现 WsConnection 类
using System;
using System.Text;
using System.Net.Sockets;
namespace WsServer
{
// This class defines the data that is passed to the MessageReceived
// event handler
public class MessageReceivedEventArgs
{
public string Message { get; private set; }
public int DataLength { get; private set; }
public MessageReceivedEventArgs(string msg, int len)
{
DataLength = len;
Message = msg;
}
}
// Define the event handler delegates
public delegate void MessageReceivedEventHandler
(WsConnection sender, MessageReceivedEventArgs e);
public delegate void WsDisconnectedEventHandler
(WsConnection sender, EventArgs e);
public class WsConnection : IDisposable
{
#region Members
public Socket _mySocket;
protected byte[] _inputBuffer;
protected StringBuilder _inputString;
// Define the events that are available
public event MessageReceivedEventHandler MessageReceived;
public event WsDisconnectedEventHandler Disconnected;
#endregion Members
public WsConnection(Socket s)
{
_mySocket = s;
_inputBuffer = new byte[255];
_inputString = new StringBuilder();
// Begin listening - the ReadMessage() method will be
// invoked when a message is received.
_mySocket.BeginReceive(_inputBuffer,
0,
_inputBuffer.Length,
0,
ReadMessage,
null);
}
protected void OnMessageReceived(string msg)
{
// When a message is received, call the event handler if
// one has been specified
if (MessageReceived != null)
MessageReceived(this, new MessageReceivedEventArgs(msg, msg.Length));
}
public void Dispose()
{
_mySocket.Close();
}
}
}
WsConnection类有三个类成员。
_mySocket:为这个连接创建的Socket对象。这由WsServer类实例化并传递给构造函数。_inputBuffer:这是一个保存原始帧数据的字节数组。这由Socket对象填充。_inputString:这是一个StringBuilder对象,包含处理后的传入消息。
WsConnection类支持两个在重要事件发生时通知的事件。
MessageReceived:收到消息时引发WsDisconnected:插座断开时引发
MessageReceived事件使用一个MessageReceivedEventArgs类将收到的消息提供给事件处理程序。WsConnection类还实现了Dispose()方法,该方法简单地关闭了与这个连接相关联的套接字。
WsConnection类有两个主要方法,现在您将实现它们。这些方法实现了 WebSocket 帧协议。
Add the ReadMessage() method using the code shown in Listing 13-5.
ReadMessage()SendMessage()
清单 13-5。实现 ReadMessage()方法
protected void ReadMessage(IAsyncResult msg)
{
int sizeOfReceivedData = _mySocket.EndReceive(msg);
if (sizeOfReceivedData > 0)
{
// Get the data provided in the first 2 bytes
bool final = (_inputBuffer[0] & 0x80) > 0 ? true : false;
bool masked = (_inputBuffer[1] & 0x80) > 0 ? true : false;
int dataLength = _inputBuffer[1] & 0x7F;
int actualLength;
int dataIndex = 0;
byte[] length = new byte[8];
byte[] masks = new byte[4];
// Depending on the initial data length, get the actual length
// and the maskingkey from the appropriate bytes.
if (dataLength == 126)
{
dataIndex = 4;
Array.Copy(_inputBuffer, 2, length, 0, 2);
actualLength = BitConverter.ToInt16(length, 0);
if (masked)
Array.Copy(_inputBuffer, 4, masks, 0, 4);
}
else if (dataLength == 127)
{
dataIndex = 10;
Array.Copy(_inputBuffer, 2, length, 0, 8);
actualLength = (int)BitConverter.ToInt64(length, 0);
if (masked)
Array.Copy(_inputBuffer, 10, masks, 0, 4);
}
else
{
dataIndex = 2;
actualLength = dataLength;
if (masked)
Array.Copy(_inputBuffer, 2, masks, 0, 4);
}
// If a mask is supplied, skip another 4 bytes
if (masked)
dataIndex += 4;
// Get the actual data in the payload array
byte[] payload = new byte[actualLength];
Array.Copy(_inputBuffer, dataIndex, payload, 0, dataLength);
// Unmask the data, if necessary
if (masked)
{
for (int i = 0; i < actualLength; i++)
{
payload[i] = (byte)(payload[i] ^ masks[i % 4]);
}
}
// Copy the data into the input string and empty the buffer
_inputString.Append(Encoding.UTF8
.GetString(payload, 0, (int)actualLength));
Array.Clear(_inputBuffer, 0, _inputBuffer.Length);
// If this is the final frame, raise an event and clear the input
if (final)
{
// Do something with the data
OnMessageReceived(_inputString.ToString());
// Clear the input string
_inputString.Clear();
}
// Listen for more messages
try
{
_mySocket.BeginReceive(_inputBuffer,
0,
_inputBuffer.Length,
0,
ReadMessage,
null);
}
catch (Exception ex)
{
}
}
// If we were not able to read the message, assume that
// the socket is closed
else
{
}
}
ReadMessage()处理单个输入帧。它查看前两个字节,以确定指定长度的位置以及是否使用掩码。然后获取实际长度并提取掩码。最后,数据被揭露。该帧的原始数据由Socket对象放置在_inputBuffer字节数组中。处理后的数据存储在_inputString成员中。这两个都是班级成员。来自每一帧的处理数据被附加到_inputString成员中。当处理完最后一帧时,整个字符串被传递给OnMessageReceived()方法。这允许单个消息在多个帧中传输。OnMessageReceived()方法简单地调用事件处理程序,如果定义了的话。
Add the SendMessage() method using the code in Listing 13-6.
清单 13-6。实现 SendMessage()方法
public void SendMessage(string msg)
{
if (_mySocket.Connected)
{
// Create the output buffer
Int64 dataLength = msg.Length;
int dataStart = 0;
byte[] dataOut = new byte[dataLength + 10];
// Build the frame data - depending on the length, it can
// be passed one of three ways
dataOut[0] = 0x81;
// Store the length in the 2nd byte
if (dataLength < 256)
{
dataOut[1] = (byte)dataLength;
dataStart = 2;
}
// Store the length in the 3rd and 4th bytes
else if (dataLength < UInt16.MaxValue)
{
dataOut[1] = 0xFE;
dataOut[2] = (byte)(dataLength & 0x00FF);
dataOut[3] = (byte)(dataLength & 0xFF00);
dataStart = 4;
}
// Store the length in bytes 3 - 9
else
{
dataOut[1] = 0xFF;
for (int i = 0; i < 8; i++)
dataOut[i + 2] = (byte)((dataLength >> (i * 8)) & 0x000000FF);
dataStart = 10;
}
// Encode the data and store it in the output buffer
byte[] data = Encoding.UTF8.GetBytes(msg);
Array.Copy(data, 0, dataOut, dataStart, dataLength);
// Send the message
try
{
_mySocket.Send(dataOut,
(int)(dataLength + dataStart),
SocketFlags.None);
}
catch (Exception ex)
{
// If we get an error, assume the socket has been disconnected
if (Disconnected != null)
Disconnected(this, EventArgs.Empty);
}
}
}
SendMessage()方法构造帧头,然后附加正在发送的实际文本。然后它使用Socket对象的Send()方法将这个帧发送给客户端。
With these classes implemented, you can now implement the main Program class. Add the following code to the Main() method: // Create the WsServer, specifying the server's address WsServer server = new WsServer(8300); // Start the server server.StartSocketServer(); // Keep running until the Enter key is pressed string input = Console.ReadLine();
这段代码创建了WsServer类并调用了StartSocketServer()方法。它调用Console.ReadLine()方法,该方法将一直等到按下回车键。
创建 Web 应用
有了一个基本的服务器实现,现在就可以创建一个使用它的 web 应用了。您将使用在前面章节中使用的 ASP.NET 5 空模板创建一个项目。您稍后将修改此应用,将其用作客户端网页。
EXERCISE 13-2. CREATING A SIMPLE CLIENTIn Solution Explorer, right-click the Chapter13 solution and then click the Add and New Project links. In the Add New Project dialog box, select the ASP.NET Web Application template. Enter the project name Client. In the next dialog box, select the ASP.NET 5 Empty template. Click the OK button, and the project will be created. Open the Startup.cs file and comment out the implementation of the Configure() method as you have in previous projects. Right-click the new wwwroot folder and click the Add and New Item links. In the Add New Item dialog box, select the HTML Page, enter the name Index.html, and click the Add button. In the Index.html file, replace the body element with the following markup: <body onload="connect();"> <div> <pre id="output"></pre> <input type="text" id="input" value="" /> <input type="submit" id="sendMsg" value="Send Message" onclick="send();" /> </div> </body> This creates a pre element that will be used to display messages that are received as well as other debugging messages. This also defines a text box for entering the message text and a button to send it. The onload event will call the connect() function that you will implement next. Now you’re ready to implement the JavaScript that will communicate with your WebSocket server. The browser takes care of the protocol and frame manipulation, so the client side is pretty easy. Add the script element shown in Listing 13-7 to the head element.
清单 13-7。客户端 JavaScript
<script type="text/javascript">
var ws; // This is our socket
function connect() {
output("Connecting to host...");
try {
ws = new WebSocket("ws://localhost:8300/chat");
} catch (e) {
output(e);
}
ws.onopen = function () {
output("connected... ");
};
ws.onmessage = function (e) {
output(e.data);
};
ws.onclose = function () {
output("Connection closed");
};
};
function send() {
var input = document.getElementById("input");
try {
ws.send(input.value);
} catch (e) {
output(e);
}
}
function output(msg) {
var o = document.getElementById("output");
o.innerHTML = o.innerHTML + "<p>" + msg + "</p>";
};
</script>
body元素中的onload事件调用connect()函数。connect()函数创建一个WebSocket对象并连接onOpen、onMessage和onClose()事件处理程序。点击发送消息按钮时,调用send()功能。这将从文本框中获取消息,并调用 WebSocket 的send()函数。output()函数只是将指定的文本添加到pre元素中。
测试初始项目
现在您有了一个基本的服务器应用和一个简单的客户机。在添加定制特性之前,您现在将对此进行测试,以确保 WebSocket 工作正常。
EXERCISE 13-3. TESTING THE INITIAL APPLICATIONIn Solution Explorer, right-click the Chapter13 solution and click the Set StartUp Projects link. In the dialog box, select the “Multiple startup projects” radio button. For both the WsServer and Client projects, change Action to Start. Also, use the arrows to the right of the project list so the WsServer project is started first, as shown in Figure 13-5. Click the OK button to save these options.

图 13-5。
Setting the startup projects Press F5 to start both the console application that hosts the WebSocket server and the client web page. The web page will show the “Connecting to host...” text and then “connected....” Enter some text in the input box and click the Send Message button. You should see this text display in the console window; it will also be echoed on the client page, as shown in Figure 13-6.

图 13-6。
The initial client web page
增强 WebSocket 服务器
到目前为止,您开发的解决方案实现了 WebSocket 协议,并演示了数据如何在服务器和客户端之间传递。然而,使用仅仅回显消息的 WebSocket 服务器不是很有用。该服务器必须管理与客户端和代理的通信。当客户端向服务器发送消息时,它将被转发到适当的代理。当代理发送响应(返回到服务器)时,服务器必须将其路由回相应的客户端。
为此,您将在 WsServer 项目中再实现两个类。
WsAgentConnection管理与代理应用的通信。WsClientConnection管理与客户端应用的通信。
这两个类都将使用WsConnection类的一个实例来发送和接收消息。WsAgentConnection类将引用最多四个WsClientConnection类的实例,代表代理当前与之聊天的四个客户端。WsClientConnection还必须有一个对WsAgentConnection对象的引用,该对象代表支持该客户端的代理,如图 13-7 所示。

图 13-7。
The internal server classes
当服务器第一次接收连接时,它还不知道它是客户机还是代理。它将创建一个WsConnection对象来监听消息。这两个应用都将被编码为立即向服务器发送消息,以识别应用的类型(客户端或代理)以及代理或客户端的名称。当收到这个消息时,服务器将创建一个WsAgentConnection对象或一个WsClientConnection对象,并将其添加到代理或客户端列表中。
如果这是一个客户端,服务器将找到一个可用的代理,并在WsAgentConnection和WsClientConnection对象之间执行必要的链接。服务器还将向客户机发送一个响应,让它们知道将与它们一起工作的代理的名称。如果这是一个代理,则会创建一个WsAgentConnection并将其添加到代理列表中,以便它可以响应新的客户端。
当第一次创建WsConnection对象时,它的MessageReceived和Disconnected事件由WsServer对象处理。服务器将需要处理标识客户端或代理的传入消息。然而,一旦创建了专门的类(WsAgentConnection或WsClientConnection,这个类将需要处理这些事件。为此,WsServer对象必须移除事件处理程序,然后从新类中关联事件处理程序。专用类都将重新引发Disconnected事件,该事件将由WsServer对象处理。
添加 WsAgentConnection 类
在解决方案资源管理器中,右键单击 WsServer 项目,然后单击“添加”和“类”链接。输入 WsAgentConnection.cs 作为类名。输入清单 13-8 中所示的代码作为该类的实现。
Listing 13-8. Implementing the WsAgentConnection Class
using System;
using System.Collections.Generic;
namespace WsServer
{
public delegate void WsDisconnectedAgentEventHandler
(WsAgentConnection sender, EventArgs e);
public class WsAgentConnection : IDisposable
{
public WsConnection _connection;
public string _name;
public Dictionary<int, WsClientConnection> _clients;
public event WsDisconnectedAgentEventHandler AgentDisconnected;
public WsAgentConnection(WsConnection conn, string name)
{
_connection = conn;
_name = name;
// Initialize our client list
_clients = new Dictionary<int, WsClientConnection>();
for (int i=1; i <= 4; i++)
{
_clients.Add(i, null);
}
}
public void MessageReceived(WsConnection sender,
MessageReceivedEventArgs e)
{
if (e.Message.Length >= 1)
{
if (e.Message[0] == '\u0003')
{
if (AgentDisconnected != null)
AgentDisconnected(this, EventArgs.Empty);
}
else if (e.Message.Length > 1)
{
string s = e.Message.Substring(0, 1);
int i = 0;
if (int.TryParse(s, out i))
{
WsClientConnection client = _clients[i];
if (client != null)
{
client.SendMessage(e.Message.Substring(2));
}
}
}
}
}
public void SendMessage(string msg)
{
if (_connection != null)
_connection.SendMessage(msg);
}
public void Disconnected(WsConnection sender, EventArgs e)
{
if (AgentDisconnected != null)
AgentDisconnected(this, EventArgs.Empty);
}
public void Dispose()
{
if (_connection != null)
_connection.Dispose();
}
}
}
WsAgentConnection类使用一个Dictionary来存储客户端连接。密钥将是一个整数(1–4),这对代理应用很重要,因此它知道要更新哪个聊天窗口。构造函数创建了所有四个条目,将WsConnection引用设置为null.。空引用表示在这个窗口中没有客户端主动通信。构造函数也接收相关的WsConnection对象。这保存在_connection成员中,用于向代理应用发送消息。
当网页关闭其套接字时,会向服务器发送一个关闭帧,指示连接正在关闭。第一个字符表示为\u0003。在此之后可能会有附加数据来说明原因。如果找到这个字符,就会调用AgentDisconnected()事件处理程序来执行必要的清理。MessageReceived()方法检查第一个字符中的消息,如下所示:
if (e.Message[0] == '\u0003')
当代理向服务器发送消息时,会以一个数字(1–4)作为前缀,表示该消息应该转发到哪个客户端。MessageReceived()事件处理程序将其剥离,然后在Dictionary中找到相应的WsClientConnection对象。然后使用WsClientConnection对象的SendMessage()方法将消息的剩余部分转发给客户端。
SendMessage()方法简单地调用与这个代理相关联的WsConnection对象的SendMessage()。Disconnected()事件处理程序引发AgentDisconnected事件,该事件将由WsServer类处理。
添加 WsClientConnection 类
在解决方案资源管理器中,右键单击 WsServer 项目,然后单击“添加”和“类”链接。输入 WsClientConnection.cs 作为类名。输入清单 13-9 中所示的代码作为该类的实现。
Listing 13-9. Implementing the WsClientConnection Class
using System;
namespace WsServer
{
public delegate void WsDisconnectedClientEventHandler
(WsClientConnection sender, EventArgs e);
public class WsClientConnection : IDisposable
{
public WsConnection _connection;
public string _name;
public WsAgentConnection _agent;
public int _clientID;
public event WsDisconnectedClientEventHandler ClientDisconnected;
public WsClientConnection(WsConnection conn,
WsAgentConnection agent,
int id,
string name)
{
_connection = conn;
_agent = agent;
_clientID = id;
_name = name;
}
public void MessageReceived(WsConnection sender,
MessageReceivedEventArgs e)
{
if (_agent != null && e.Message.Length > 0)
{
if (e.Message[0] == '\u0003')
{
if (ClientDisconnected != null)
ClientDisconnected(this, EventArgs.Empty);
}
else
_agent.SendMessage(_clientID.ToString() + ": " + e.Message);
}
}
public void SendMessage(string msg)
{
if (_connection != null)
_connection.SendMessage(msg);
}
public void Disconnected(WsConnection sender, EventArgs e)
{
if (ClientDisconnected != null)
ClientDisconnected(this, EventArgs.Empty);
}
public void Dispose()
{
if (_connection != null)
_connection.Dispose();
}
}
}
WsClientConnection类类似于WsAgentConnection类。它有一个对WsAgentConnection对象的引用,而不是一个关联客户端的Dictionary。这表示客户端正在与之聊天的代理。构造函数提供底层的WsConnection对象,相关的WsAgentConection对象,以及客户端的 ID。这用于为转发给代理的消息添加前缀。MessageReceived()方法也使用\u0003字符来查看套接字是否正在关闭。
增强 WsServer 类
打开WsServer.cs文件,添加粗体显示的代码。这将创建存储代理和客户端对象的集合。
#region Members
// This socket listens for new connections
Socket _listener;
// Configurable port # that is passed in the constructor
int _port;
// List of connections
List<WsConnection> _unknown;
List<WsAgentConnection> _agents;
List<WsClientConnection> _clients;
#endregion Members
public WsServer(int port)
{
_port = port;
// This is a list of active connections
_unknown = new List<WsConnection>();
_agents = new List<WsAgentConnection>();
_clients = new List<WsClientConnection>();
}
在WsServer类中,用清单 13-10 中所示的代码替换MessageReceived()事件处理程序。
Listing 13-10. The Revised MessageReceived() Event Handler
void MessageReceived(WsConnection sender, MessageReceivedEventArgs e)
{
string msg = e.Message;
if (e.DataLength > 14 && (msg.Substring(0, 14) == "[Agent SignOn:"))
{
// This is an agent signing on
string name = msg.Substring(14, e.DataLength - 15);
WsAgentConnection agent = new WsAgentConnection(sender, name);
// Re-wire the event handlers
sender.Disconnected -= Disconnected;
sender.MessageReceived -= MessageReceived;
sender.Disconnected += agent.Disconnected;
sender.MessageReceived += agent.MessageReceived;
agent.AgentDisconnected +=
new WsDisconnectedAgentEventHandler(AgentDisconnected);
// Move this socket to the agent list
_unknown.Remove(sender);
_agents.Add(agent);
// Send a response
agent.SendMessage("Welcome, " + name);
}
else if (e.DataLength > 15 &&
(msg.Substring(0, 15) == "[Client SignOn:"))
{
// This is a client requesting assistance
string name = msg.Substring(15, e.DataLength - 16);
// Find an agent
WsAgentConnection agent = null;
int clientID = 0;
foreach (WsAgentConnection a in _agents)
{
foreach (KeyValuePair<int, WsClientConnection> d in a._clients)
{
if (d.Value == null)
{
agent = a;
clientID = d.Key;
break;
}
}
if (agent != null)
break;
}
if (agent != null)
{
WsClientConnection client =
new WsClientConnection(sender, agent, clientID, name);
// Re-wire the event handlers
sender.Disconnected -= Disconnected;
sender.MessageReceived -= MessageReceived;
sender.Disconnected += client.Disconnected;
sender.MessageReceived += client.MessageReceived;
client.ClientDisconnected +=
new WsDisconnectedClientEventHandler(ClientDisconnected);
// Add this to the agent list
_unknown.Remove(sender);
_clients.Add(client);
agent._clients[clientID] = client;
// Send a message to the agent
agent.SendMessage("[ClientName:" + clientID.ToString() +
name + "]");
// Send a response
client.SendMessage("Hello! My name is " + agent._name +
". How may I help you?");
}
else
{
// There are no agents available
sender.SendMessage("There are no agents currently available;" +
"please try again later");
sender.Dispose();
}
}
}
正如我前面所解释的,客户机和代理应用发送的第一条消息是一条包含它们名字的登录消息。这将被格式化成这样。任何其他消息都将被忽略。
- 代理= "[代理登录:
]" - 客户= "客户签名:
MessageReceived()事件处理程序检查传入的消息是否是其中之一。对于代理登录,从消息中提取名称。然后它创建一个WsAgentConnection类并设置它的_name属性。来自WsConnection对象的MessageReceived和Disconnected事件当前被映射到WsServer事件处理程序。这个映射被删除,取而代之的是这些事件被映射到WsAgentConnection对象的事件句柄。此外,WsAgentConnection类定义了一个AgentDisconnected事件,该事件被映射到一个新的事件处理程序,您稍后将实现该处理程序。从_unknown列表中移除WsConnection对象,并将WsAgentConnection对象添加到_agents列表中。最后,向代理应用发送一条欢迎消息。
当接收到客户端登录时,进行类似的处理来创建WsClientConnection对象并重新连接事件处理程序。此外,代码还会寻找一个可用的代理。这将遍历_agents列表,并为每个代理寻找一个引用为空WsClientConnection的Dictionary条目。当找到第一个空条目时,搜索停止。
Tip
您可能希望改进这种搜索,以平衡可用代理之间的负载。由于这是当前实现的,第一个代理将处理前四个客户端,而其他代理是空闲的。该搜索可以遍历代理,寻找具有最少活动客户端的代理,并将新客户端发送给它们。
如果找到了可用的代理,WsAgentConnection对象和Dictionary键被传递给WsClientConnection对象构造器。另外,WsClientConnection对象存储在可用的Dictionary条目中。向代理应用发送一条消息,让他们知道已经为他们分配了一个新的客户端。最后,向客户机应用发送一条消息,让客户机知道哪个代理将协助它们。如果没有可用的代理,则向客户端应用发送一条指示该情况的消息。
将清单 [13-11 中所示的事件处理程序添加到WsServer类中。
Listing 13-11. Adding the Additional Event Handlers
void ClientDisconnected(WsClientConnection sender, EventArgs e)
{
if (sender._agent != null)
{
sender._agent._clients[sender._clientID] = null;
sender._agent.SendMessage("[ClientClose:" +
sender._clientID.ToString() + "]");
}
_clients.Remove(sender);
sender.Dispose();
}
void AgentDisconnected(WsAgentConnection sender, EventArgs e)
{
foreach (KeyValuePair<int, WsClientConnection> d in sender._clients)
{
if (d.Value != null)
{
_clients.Remove(d.Value);
d.Value.SendMessage
("The agent has been disconnected; please reconnect");
}
}
_agents.Remove(sender);
sender.Dispose();
}
当客户端或代理应用关闭或断开连接时,将调用这些事件处理程序。除了从适当的列表中删除连接之外,还会向会话的另一端发送一条消息,以便执行适当的清理。如果代理断开连接,需要通知所有与之连接的客户端。
创建代理应用
现在,您将创建代理应用,代理将使用它来响应客户机请求。这将支持多达四个同时聊天会话。
创建代理项目
代理应用将从 Agent 网站模板开始。这提供了注册和登录站点的能力,模拟了代理通常使用的身份验证。
EXERCISE 13-4. CREATING THE AGENT PROJECTFrom Solution Explorer, right-click the Chapter13 solution and click the Add and New Project links. In the Add New Project dialog box, select the ASP.NET Web Application template and enter the name Agent. Click the OK button to continue. In the next dialog box, select the ASP.NET 5 Web Site template and click the OK button. This template provides a login form that the agents will use to authenticate. You will create a separate web page that supports the chat sessions. In Solution Explorer, right-click the Views\Home folder and click the Add and New Item links. In the Add New Item dialog box, select the MVC View Page template, enter the name Chat.cshtml, and click the Add button to create the view. In the Controllers folder, open the HomeController.cs file. Add the following method to the HomeController class: public IActionResult Chat() { ViewBag.Message = "Respond to chat"; return View(); } In the Views\Shared folder, open the _Layout.cshtml file. Inside the body element you’ll see three li tags containing an asp-action property. These tags create the navigation links on the home page. Add the following line to this section to create a new navigation link to the new chat page: <li><a asp-controller="Home" asp-action="Chat">Chat</a></li>
实现聊天网页
现在您将实现聊天页面。这将支持四个单独的聊天会话。您将首先为您需要的元素添加标记,然后应用一个style元素来使页面看起来更好。然后您将添加 JavaScript 来访问 WebSocket 并与您创建的 WebSocket 服务器通信。
EXERCISE 13-5. IMPLEMENTING THE CHAT PAGEOpen the Chat.cshtml file and replace the entire contents with the markup shown in Listing 13-12. Listing 13-12. Adding the Page Markup
<head>
</head>
<body onload="connect();">
<div class=”agent”>
<div>
<p id="agentName">@User.Identity.Name</p>
<pre id="output"></pre>
</div>
<div id="div1" class="client">
<p id="client1" class="clientName">unassigned</p>
<div id="chat1" class="chat">
</div>
<input type="text" id="input1" class="input" value="" />
<input type="submit" value="Send" onclick="send('1');" />
</div>
<div id="div2" class="client">
<p id="client2" class="clientName">unassigned</p>
<div id="chat2" class="chat">
</div>
<input type="text" id="input2" class="input" value="" />
<input type="submit" value="Send" onclick="send('2');" />
</div>
<div id="div3" class="client">
<p id="client3" class="clientName">unassigned</p>
<div id="chat3" class="chat">
</div>
<input type="text" id="input3" class="input" value="" />
<input type="submit" value="Send" onclick="send('3');" />
</div>
<div id="div4" class="client">
<p id="client4" class="clientName">unassigned</p>
<div id="chat4" class="chat">
</div>
<input type="text" id="input4" class="input" value="" />
<input type="submit" value="Send" onclick="send('4');" />
</div>
</div>
</body>
在body元素的顶部,有一个值为@User.Indentity.Name的p元素。这是 Razor 语法,将在此显示代理的名称。由于代理将登录,网页已经知道他们的名字。
该页面将为每个聊天窗口使用一个div元素。其中有一个空的div元素,包含来回发送的消息。在外部的div中有一个p元素,它将保存客户端的名称,该名称目前被设置为unassigned。还有一个用于输入消息的文本框和一个发送消息的按钮。
To improve the layout of the form, add the style element shown in Listing 13-13 inside the head element. Listing 13-13. Adding the style Element
<style>
body
{
background: #f0f0f0;
width: 900px;
}
.agent
{
display:block;
float:right;
}
.client
{
display: block;
float: left;
width: 400px;
height: 385px;
border: 2px solid #6699cc;
border-radius: 5px;
background-color: white;
}
.chat
{
height: 300px;
font-size: smaller;
line-height: 12px;
overflow-y: scroll;
}
.input
{
width:330px;
}
.clientName
{
height: 20px;
width: 380px;
text-align: center;
font-size: 15px;
font-weight: bold;
}
</style>
Now you’ll add the JavaScript code that will make all of this work. Add the script element shown in Listing 13-14 to the head element. Listing 13-14. The JavaScript Implementation
<script type="text/javascript">
var ws; // This is our socket
function connect() {
output("Connecting to host...");
try {
ws = new WebSocket("ws://localhost:8300/chat");
} catch (e) {
output(e);
}
ws.onopen = function () {
output("connected... ");
// Send the Agent sign-on message
var p = document.getElementById("agentName");
ws.send("[Agent SignOn:" + p.innerHTML + "]");
};
ws.onmessage = function (e) {
displayMsg(e.data);
};
ws.onclose = function () {
output("Connection closed");
};
};
// Send the input text to the server
function send(i) {
var input = document.getElementById("input" + i);
try {
ws.send(i + ":" + input.value);
var o = document.getElementById("chat" + i);
o.innerHTML = o.innerHTML + "<p><b>Me:</b>" + input.value + "</p>";
input.value = "";
} catch (e) {
output(e);
}
}
// Add text to the debug area
function output(msg) {
var o = document.getElementById("output");
o.innerHTML = o.innerHTML + "<p>" + msg + "</p>";
};
// Handle a received message
function displayMsg(msg) {
var i = msg.substring(0, 1);
var cmd = msg.substring(0, 12);
// For the initial message from the server, save the client's name
if (cmd === "ClientName:") {
displayClientName(msg.substring(12));
}
// If the client has disconnected, clear the chat window
else if (cmd === "[ClientClose") {
resetClient(msg.substring(13,14));
}
// Display the message in the debug area is not formatted properly
else if (i != "1" && i != "2" && i != "3" && i != "4") {
output(msg)
}
// Display the message in the chat window
else {
var o = document.getElementById("chat" + i);
o.innerHTML = o.innerHTML + "<p><b>Client:</b>" +
msg.substring(3, msg.length) + "</p>";
}
};
// Display the client's name in the chat window
function displayClientName(msg) {
var i = msg.substring(0, 1);
var o = document.getElementById("client" + i);
o.innerHTML = msg.substring(1, msg.length - 1);
}
// Clear the chat window so it can be reused for another client
function resetClient(i) {
// Clear the client's name
var o = document.getElementById("client" + i);
o.innerHTML = "unassigned";
// Remove the chat messages
var o2 = document.getElementById("chat" + i);
while (o2.hasChildNodes()) {
o2.removeChild(o2.firstChild);
}
}
</script>
就像您之前实现的 web 页面一样,onload事件调用了connect()函数,该函数连接了onopen、onmessage和onclose事件处理程序。在这种情况下,onopen事件处理程序向服务器发送代理登录消息。onmessage事件处理程序调用displayMsg()函数。这有特殊的逻辑来解释信息。当分配客户端时,服务器将发送客户端的名称,当客户端断开连接时,服务器将发送一条消息。这些特殊情况分别由displayClientName()和resetClient()函数处理。对于所有其他消息,第一个字符应为 1–4,表示这是哪个窗口。使用它,可以获得适当的div元素,并将消息添加到其中。该消息以 Client: text 为前缀。
单击 Send 按钮时调用的send()函数接受一个参数,该参数指示哪个窗口正在发送消息。它使用这个来获取适当的输入元素,并为消息添加前缀,这样服务器就知道这是给哪个客户端的。它还在div元素中显示以 Me: text 为前缀的文本。这样做是为了让聊天窗口包含传入和传出的消息。
resetClient()功能将客户端名称改回unassigned。它还遍历div,删除所有添加的p标签。
测试代理应用
在您完成开发之前,让我们测试一下表单,确保它看起来不错。在解决方案资源管理器中,右击代理项目,然后单击“调试”和“启动新实例”链接。这将启动浏览器并显示主页。点击页面顶部的注册链接,输入您的电子邮件地址和密码,如图 [13-8 所示。

图 13-8。
Registering the user Tip
下次打开该站点时,您将使用登录链接,而不是注册链接。当您登录时,如果您选择了“记住我”复选框,它会在应用启动时自动让您登录。
还要注意您在页面标题中添加的聊天链接。点击此链接打开聊天页面,该页面应如图 13-9 所示。

图 13-9。
The new chat page
因为服务器没有运行,所以无法建立连接,并显示“连接关闭”消息。关闭窗口并停止调试器。
实现客户端应用
您在本章前面创建了客户端项目来测试初始的服务器实现。现在您将修改Index.html文件作为聊天解决方案的客户端。
EXERCISE 13-6. CREATING THE CLIENT APPLICATIONOpen the Index.html file in the Client project. Replace the body element with the following markup. This adds a div element that contains a text box for the client’s name and a submit button. A second div implements the actual chat window. It includes a div that will display the chat messages, a text box for entering a new message, and a submit button that will send it. <body> <div> <p>Enter your name to begin chat</p> <input type="text" id="name" class="input" value="" /> <input type="submit" id="connect" value="Chat now..." onclick="connect();" /> <pre id="output"></pre> </div> <div id="div1" class="client"> <p id="client1" class="clientName"></p> <div id="chat1" class="chat"> </div> <input type="text" id="input" class="input" value="" /> <input type="submit" value="Send" class="send" onclick="send();" /> </div> </body> Add the style element shown in Listing 13-15 to the head element.
清单 13-15。定义 CSS 样式
<style>
body
{
background: #f0f0f0;
width: 450px;
}
.client
{
display: block;
float: left;
width: 400px;
height: 345px;
border: 2px solid #6699cc;
border-radius: 5px;
background-color: white;
}
.chat
{
height: 300px;
font-size: smaller;
line-height: 12px;
overflow-y: scroll;
}
.input
{
width:330px;
}
</style>
Replace the existing script element with the code shown in Listing 13-16.
清单 13-16。添加 JavaScript
<script type="text/javascript">
var ws; // This is our socket
function connect() {
output("Connecting to host...");
try {
ws = new WebSocket("ws://localhost:8300/chat");
} catch (e) {
output(e);
}
ws.onopen = function () {
output("connected... ");
var p = document.getElementById("name");
ws.send("[Client SignOn:" + p.value + "]");
};
ws.onmessage = function (e) {
displayMsg(e.data);
};
ws.onclose = function () {
output("Connection closed");
};
};
function send() {
var input = document.getElementById("input");
try {
ws.send(input.value);
var o = document.getElementById("chat1");
o.innerHTML = o.innerHTML + "<p><b>Me:</b>" + input.value + "</p>";
input.value = "";
} catch (e) {
output(e);
}
}
function output(msg) {
var o = document.getElementById("output");
o.innerHTML = o.innerHTML + "<p>" + msg + "</p>";
};
function displayMsg(msg) {
var o = document.getElementById("chat1");
o.innerHTML = o.innerHTML + "<p><b>Agent:</b>" + msg + "</p>";
};
</script>
这段代码类似于代理应用上的 JavaScript。connect()函数获取一个 WebSocket 并发送初始登录消息,然后连接onmessage和onclose事件处理程序。send()函数将输入的文本发送到服务器,并在页面上回显。displayMsg()函数是处理来自服务器的消息的事件处理器,这些消息显示在页面上。
添加日志记录
现在,您将向套接字服务器添加日志记录。这将有助于您更好地理解幕后发生的事情,并有助于调试您的解决方案。我将向您展示如何使用 Log4Net 轻松地将日志集成到您的服务器端应用中。
安装 Log4Net
您将使用 NuGet 来安装 Log4Net 包,并将其添加到WsServer项目中。从 Visual Studio 的“工具”菜单中,单击“NuGet 包管理器”和“管理解决方案的 NuGet 包”链接。在 NuGet 包管理器中,在搜索框中输入 log4net。然后选择 log4net 包,只选择WsServer项目,如图 13-10;单击安装按钮。

图 13-10。
Selecting the log4net package
将显示如图 13-11 所示的确认窗口。单击“确定”按钮继续。

图 13-11。
Confirming the installation
配置 Log4Net
Log4Net 是通过app.config文件配置的。这使您能够在不更改应用的情况下更改记录的内容。例如,您可以将其设置为仅记录错误消息。如果您随后需要调试某个问题,您可以更改日志记录级别,以便在日志中获得更多信息。
打开WsServer项目中的app.config文件。用清单 13-17 中所示的代码替换全部内容。
Listing 13-17. The app.config Settings
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net"
type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
</startup>
<log4net >
<root>
<level value="DEBUG"/>
<appender-ref ref="RollingFileAppender"/>
</root>
<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
<file value="WsServer.log" />
<appendToFile value="true" />
<rollingStyle value="Size" />
<maxSizeRollBackups value="5" />
<maximumFileSize value="10MB" />
<staticLogFileName value="true" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %level %logger - %message%newline" />
</layout>
</appender>
</log4net>
</configuration>
您需要定义一个配置节,Log4Net 条目将放在这个配置节中。这里显示的设置是创建名为WsServer.log的滚动日志文件的一些基本设置。日志记录级别设置为DEBUG,因此您的所有日志语句都将保存在日志中。
Note
使用 Log4Net 可以做很多事情。如果你有兴趣了解这个工具的更多信息,我建议你从蒂姆·科里的这篇文章开始: www.codeproject.com/Articles/140911/log-net-Tutorial 。
添加日志语句
添加日志记录的真正工作是将代码放在正确的位置,以保存有用的信息。您应该知道代码的哪些部分更有可能失败,以及哪些事件对捕获最有意义。避免在循环内部进行日志记录,因为这可能会生成大量没有实际帮助的日志条目。对于套接字服务器,您需要知道何时创建或关闭套接字,以及何时发送或接收消息。您还想知道错误何时发生。
您需要在程序集中的某个地方引用 Log4Net。每个组件只执行一次。打开Program.cs文件,添加粗体显示的行:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
[assembly: log4net.Config.XmlConfigurator(Watch = true)]
namespace WsServer
现在,您将在每个使用 Log4Net 的类中添加一个日志记录器。这将在类中创建一个静态成员,稍后您将使用它来生成一些日志条目。将以下粗体显示的代码添加到WsServer类中:
public class WsServer
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger
(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#region Members
在WsConnection类中添加相同的行。
您应该在几个地方添加日志记录语句。您使用的方法,如Debug、Info或Error,将决定记录级别。然后,您可以控制在app.config文件中记录哪个级别。我使用的是...Format()方法,比如InfoFormat(),它的工作原理类似于string.Format()方法。您也可以使用像Info()这样的基本方法,其中您必须传入一个已经格式化的字符串。
EXERCISE 13-7. ADD LOGGING TO THE WSSERVER CLASSIn the WsServer class, add the Error statement inside the catch block of the StartSocketServer() method. catch (Exception ex) { log.ErrorFormat("Listener failed, handle = {0}: {1}", _listener.Handle, ex.Message); } Add a Debug statement at the beginning of the MessageReceived() handler. void MessageReceived(WsConnection sender, MessageReceivedEventArgs e) { string msg = e.Message; log.Debug("Message received: " + msg); Add an Info statement just before sending a message to the agent. log.InfoFormat("Socket attached to agent {0}, handle = {1}", name, sender._mySocket.Handle.ToString()); // Send a response agent.SendMessage("Welcome, " + name); Add another just before attaching the client to the selected agent. if (agent != null) { WsClientConnection client = new WsClientConnection(sender, agent, clientID, name); log.InfoFormat("Client {0} assigned to agent {1}", name, agent._name); // Re-wire the event handlers Add an Info statement inside the Disconnected() handler. void Disconnected(WsConnection sender, EventArgs e) { log.InfoFormat("Unattached socket disconnected, handle = {0}", sender._mySocket.Handle.ToString()); _unknown.Remove(sender); } Add an Info statement inside the OnConnect() method. // Perform the necessary handshaking PerformHandshake(socket); log.InfoFormat("New socket created, handle = {0}", socket.Handle.ToString()); // Create a WsConnection object for this connection Add an Info statement inside the ClientDisconnected() handler. void ClientDisconnected(WsClientConnection sender, EventArgs e) { log.InfoFormat("Client {0}, socket disconnected, handle = {1}", sender._name, sender._connection._mySocket.Handle.ToString()); if (sender._agent != null) Add a similar statement inside the AgentDisconnected() handler. void AgentDisconnected(WsAgentConnection sender, EventArgs e) { log.InfoFormat("Agent {0} socket disconnected, handle = {1}", sender._name, sender._connection._mySocket.Handle.ToString()); foreach (KeyValuePair<int, WsClientConnection> d in sender._clients) EXERCISE 13-8. ADD LOGGING TO THE WSCONNECTION CLASSAdd a Debug statement inside the Dispose() method. public void Dispose() { log.DebugFormat("Socket closing, handle {0}", _mySocket.Handle); _mySocket.Close(); } At the end of the ReadMessage() function, add an Error statement inside the catch block. catch (Exception ex) { log.ErrorFormat("ReceiveMessage failed, handle {0}, {1}", _mySocket.Handle, ex.Message); } Add an Error statement inside the else block. // If we were not able to read the message, assume that // the socket is closed else { log.ErrorFormat("ReceiveMessage failed, handle {0}", _mySocket.Handle); } Finally, add an Error statement inside the catch block in the SendMessage() method. catch (Exception ex) { log.ErrorFormat("SendMessage failed, handle = {0}: {1}", _mySocket.Handle, ex.Message); // If we get an error, assume the socket has been disconnected if (Disconnected != null) Disconnected(this, EventArgs.Empty); }
测试解决方案
在本章的前面,您设置了在调试时启动的 WsServer 和 Client 项目。现在您还需要添加代理应用。然后,您将同时调试所有三个应用。
测试消息传递
首先测试基本的套接字通信,确保每条消息都显示在正确的窗口中。您需要启动一个额外的客户端页面来测试并发客户端。
EXERCISE 13-9. TESTING THE SOLUTIONRight-click the Chapter13 solution and click the Set StartUp Projects link. In the dialog box, change the action for the Agent application to Start. Also move this down to be the second project loaded. Click the OK button to save these changes. Press F5 to debug the applications. You should see the console app as well as two browser windows. Go to the agent page. If not already logged in, log in now. You should have to register only once. After that, you just log in. If you selected the “Remember me” check box, you should be logged in automatically. Click the Chat link, which should display the new chat page, and show that you are connected to the server. Go to the Client page, enter a name, and click the “Chat now” button. You should get a response in the chat window, as illustrated in Figure 13-12.

图 13-12。
The client web application Enter a message and send it. From the Agent page, enter a response and send it. You should see all of the messages displayed in the client’s chat window. Create another copy of the client page and enter a different name. Enter a message and send it. Go to the Agent window and you should see messages in two windows, as shown in Figure 13-13.

图 13-13。
The Agent page with multiple sessions
测试断开
对于最后一个测试,您将关闭一些网页,并验证其他页面是否正确响应。您还将尝试重新连接到以前的客户端。
在代理和两个客户端窗口仍然打开的情况下,关闭第一个客户端窗口。转到代理页面,您应该看到第一个窗口已经被清除并设置为 unassigned。但是,第二个客户端仍处于连接状态。
现在关闭代理窗口。转到客户端窗口,您应该会看到一条消息,表明代理已经断开连接,如图 13-14 所示。

图 13-14。
A client window with a disconnected agent
现在打开一个新的选项卡,通过键入 URL 打开代理页面,这将类似于http://localhost:7778/Home/Chat。您可能会有一个不同的端口号。返回客户端页面,点击“立即聊天”按钮。然后,您应该会重新连接到代理。关闭客户端和代理窗口,并停止调试器。
打开 Log4Net 创建的WsServer.log文件,您可以在WsServer\bin\Debug文件夹中找到它。它应该类似于清单 13-18 中所示的条目。
Listing 13-18. The WsServer.log File
INFO WsServer.WsServer - New socket created, handle = 1284
DEBUG WsServer.WsServer - Message received: [Agent SignOn:markc@.com]
INFO WsServer.WsServer - Socket attached to agent markc@.com, handle = 1284
INFO WsServer.WsServer - New socket created, handle = 1316
DEBUG WsServer.WsServer - Message received: [Client SignOn:John]
INFO WsServer.WsServer - Client John assigned to agent markc@thecreativepeople.com
INFO WsServer.WsServer - New socket created, handle = 1356
DEBUG WsServer.WsServer - Message received: [Client SignOn:Jane]
INFO WsServer.WsServer - Client Jane assigned to agent markc@thecreativepeople.com
INFO WsServer.WsServer - Client John, socket disconnected, handle = 1316
DEBUG WsServer.WsConnection - Socket closing, handle 1316
ERROR WsServer.WsConnection - ReceiveMessage failed, handle 1316,
Cannot access a disposed object.
INFO WsServer.WsServer - Agent markc@.com socket disconnected, handle = 1284
DEBUG WsServer.WsConnection - Socket closing, handle 1284
ERROR WsServer.WsConnection - ReceiveMessage failed, handle 1284,
Cannot access a disposed object.
INFO WsServer.WsServer - New socket created, handle = 732
DEBUG WsServer.WsServer - Message received: [Agent SignOn:markc@.com]
INFO WsServer.WsServer - Socket attached to agent markc@.com, handle = 732
INFO WsServer.WsServer - New socket created, handle = 1344
DEBUG WsServer.WsServer - Message received: [Client SignOn:Jane]
INFO WsServer.WsServer - Client Jane assigned to agent markc@.com
INFO WsServer.WsServer - Client Jane, socket disconnected, handle = 1356
DEBUG WsServer.WsConnection - Socket closing, handle 1356
ERROR WsServer.WsConnection - ReceiveMessage failed, handle 1356,
Cannot access a disposed object.
INFO WsServer.WsServer - Client Jane, socket disconnected, handle = 1344
DEBUG WsServer.WsConnection - Socket closing, handle 1344
ERROR WsServer.WsConnection - ReceiveMessage failed, handle 1344,
Cannot access a disposed object.
INFO WsServer.WsServer - Agent markc@.com socket disconnected, handle = 732
DEBUG WsServer.WsConnection - Socket closing, handle 732
ERROR WsServer.WsConnection - ReceiveMessage failed, handle 732,
Cannot access a disposed object.
这很好地总结了每个套接字是如何创建的,以及来回发送的消息。
摘要
在这篇关于 WebSockets 的相对简短的介绍中,您创建了一个简单的聊天系统,它允许一个代理同时与多个客户端聊天。虽然服务器实现相当复杂,但是客户端相当简单。在创建 WebSocket 对象并指定 WebSocket 服务器的位置之后,您只需连接事件处理程序,当连接建立、收到消息以及连接关闭时就会收到通知。
在这个演示中,您创建了一个独立的应用,但是在许多情况下,您只是将聊天功能添加到现有的 web 应用中。例如,您可能会询问用户是否需要他们正在查看的页面的帮助,如果他们需要,这个简单的代码将允许他们直接从该页面进行聊天。
服务器端需要一些协议处理。首先,服务器使用http接收握手消息。请求密钥被获取并用于生成响应密钥。然后,整个响应被发送回客户端。实际的消息是使用包含帧头的ws协议发送的。来自客户端的消息被屏蔽,这需要服务器中的逻辑来解除屏蔽。来自服务器的消息不会被屏蔽。
演示应用提供了聊天功能。这只是 WebSockets 的一种可能用途。它们也可以在服务器需要与客户端通信的任何时候使用。但是,请记住,客户端必须启动与服务器的连接。
十四、拖放
选择一个元素并将其拖动到另一个位置的能力是自然用户体验的一个很好的例子。我还记得早期的苹果电脑,你可以通过把文件拖到垃圾桶图标上来删除它。这个动作,以及成百上千个类似的动作,是桌面应用用户体验的重要组成部分。然而,Web 应用在这个领域已经远远落后了。有了 HTML5 中的拖放(DnD) API,你会发现 web 应用正在迅速赶上来。
在本章中,你将构建一个实现跳棋游戏的 web 应用,使用 DnD API 在棋盘上移动棋子。我将首先解释 DnD 应用的概念和结构。然后我将深入代码,演示各个方面。最后,我将介绍一些高级功能,包括在浏览器窗口之间拖动。
理解拖放
在开始构建应用之前,我想解释一下 DnD API 的基本概念。这将有助于您在开始编写代码时将它放在上下文中。我将首先解释引发的事件;重要的是要知道什么时候每一个都被提出来了,在哪个对象上。然后您将看到dataTransfer对象,您将使用它将信息从被拖动的对象传递到每个事件,并最终传递到放下动作。您还可以使用它来配置拖动操作的各个方面。最后,我将向您展示如何使对象可拖动。
处理事件
和它的桌面版一样,DnD 是一个基于事件的 API。当用户选择、移动和放下某项时,会引发事件,允许应用控制和响应这些操作。为了有效地使用这个 API,您需要知道这些事件何时被引发,以及它们是在哪个元素上被引发的。起初,这可能看起来令人困惑,但是一旦你正确地看待它,它就变得非常简单了。
在 DnD 操作中,涉及两个要素:
- 被拖动的元素,有时称为源
- 被拖放的元素,通常称为目标
你可以把它想象成一个正在射向目标的箭头,如图 14-1 所示。

图 14-1。
The source and target elements
在 DnD 操作中,两个元素都触发了事件,我已经指出了每个元素引发了哪些事件。在源元素上,dragstart、drag和dragend事件与 Windows 应用中的mousedown、mousemove和mouseup事件相当。当您单击一个元素并开始移动鼠标时,就会引发dragstart事件。紧随其后的是drag事件,并且drag事件也随着鼠标的每一次移动而被重复引发。最后,释放鼠标按钮时会引发dragend事件。
目标元素上的事件更有趣一些。当鼠标在页面上移动时,当它进入由一个元素定义的区域时,dragenter事件在该元素上被引发。随着鼠标继续移动,在目标元素上引发了dragover事件。如果鼠标移动到该元素之外,目标元素上将触发dragleave事件。据推测,鼠标现在位于不同的元素上,并且在该元素上引发了一个dragenter事件。然而,如果在目标元素上释放鼠标按钮,将会引发drop事件,而不是dragleave事件。
现在让我们浏览一个典型的场景,看看这些事件的顺序。如表 14-1 所示。
表 14-1。
Sequence of Events
| 元素 | 事件 | 笔记 | | --- | --- | --- | | 来源 | `dragstart` | 当鼠标被单击并开始移动时引发 | | 来源 | `drag` | 每次鼠标移动时引发 | | 目标 | `dragenter` | 当鼠标进入目标元素的空间时引发 | | 目标 | `dragover` | 当指针位于目标上方时,随着每次鼠标移动而引发 | | 来源 | `drag` | 当鼠标移动时继续上升 | | 目标 | `dragleave` | 当鼠标移过当前目标时引发 | | 目标 | `dragenter` | 当鼠标移动到新的目标元素时引发 | | 目标 | `drop` | 释放鼠标按钮时引发 | | 来源 | `dragend` | 结束拖放操作 |现在您已经理解了所使用的事件,您可以通过为每个事件提供适当的处理程序来实现 DnD 操作。
使用数据传输对象
还有一个你应该理解的 DnD 概念。简单地在页面上拖动一个元素并不那么有用;您真正需要的是与元素相关联的数据。在我前面给出的将文件拖到垃圾桶的例子中,看到图标被垃圾桶吞噬可能看起来很有趣,但最终目的是删除文件。在这种情况下,您将文件规范传递给回收站,以便它可以在文件系统中执行请求的操作。
存储数据
在 DnD API 中,dataTransfer对象用于存储与操作相关的数据。对象通常在dragstart事件处理程序中初始化。回想一下,这个事件是在 source 元素上引发的。事件处理程序可以从源元素中访问数据,并将其存储在dataTransfer对象中。然后将它提供给所有其他事件处理程序,以便它们可以在自己的特定处理中使用它。最终,drop事件处理程序使用它来对这些数据采取适当的行动。
dataTransfer对象是作为传递给每个事件处理程序的event对象的属性提供的。您使用setData()方法将数据存储在dataTransfer对象中。为了指示数据的类型,还需要提供适当的 MIME 类型。例如,要添加一些简单的文本,请像这样调用方法:
e.dataTransfer.setData("text", "Hello, World!");
要在后续事件(如drop事件)中访问该数据,请像这样使用getData()方法:
var msg = e.dataTransfer.getData("text");
检索数据时需要使用与存储数据时相同的 MIME 类型。
Caution
并非所有的浏览器都能识别所有的 MIME 类型。在这个例子中,您可能希望使用text/plain。这在 Firefox 和 Opera 中运行良好,但在 Chrome 或 IE 中不受支持。然而,如果你只使用text,这将在所有这些浏览器上工作。
使用拖放效果
对象的另一个目的是向用户提供当项目被放下时将发生的动作的反馈。这就是所谓的下降效应,有四种可能的值。
copy:选中的元素将被复制到目标位置。move:选中的元素将被移动到目标位置。link:将在目标位置创建一个指向所选项目的链接。none:不允许拖放操作。
当您开始拖动一个项目时,光标将会改变,以指示当项目被放到目标上时将会出现的拖放效果。这是标准的 Windows 用户界面,您可以在大多数应用上尝试。例如,使用 Visual Studio 中的文本编辑器,选择一些文本,然后开始拖动它。您应该会看到光标变为移动光标或“不允许”光标,这取决于您尝试将它移动到的位置。如果在移动之前按住 Ctrl 键,您应该会看到复制光标而不是移动光标。
在dragstart事件处理程序中,您可以根据选择的源元素指定允许的拖放效果。您可以通过简单地连接它们来指定多个允许的效果(例如,copyMove)或者像这样指定所有效果:
e.dataTransfer.effectAllowed = "all"; // "copy", "link", "move", "copyLink", "linkMove", "copyMove"
然后,在dragover事件中,您将指定如果源元素被放到那里将会发生的拖放效果。如果该拖放效果是允许的效果之一,光标将改变以指示该拖放效果。但是,如果不允许该效果,光标将使用“不允许”图标。如果这不是接受放置的有效位置,将放置效果设置为none,如下所示:
if (validLocation) {
e.dataTransfer.dropEffect = "move";
}
else {
e.dataTransfer.dropEffect = "none";
}
启用可拖动元素
所以,现在你知道可以通过在dragover事件中将拖放效果设置为none来禁用元素上的drop事件。但是如何控制哪些元素可以拖动开始呢?答案很简单:只需在元素的标记中设置draggable属性。例如,要创建一个可以拖动的div,输入如下标记:
<div id="myDiv" draggable="true">
<p>This div is draggable</p>
</div>
默认情况下,图像和链接是可拖动的。转到google.com并尝试拖动谷歌标志。当您移动光标时,您应该会看到该图像的一个稍微柔和的副本被拖动。
如果你把这个图片拖到 Firefox 浏览器窗口,Firefox 会导航到这个图片。您刚刚看到了拖放操作。因为使用拖放是一种非常自然的工作方式,浏览器试图尽可能地适应这种开箱即用的方式。例如,如果您从文本编辑器中将一些看起来像 URL 的文本拖到浏览器上,它会尝试导航到该地址。如果您将图像文件拖到浏览器上,它将导航到该文件或下载该文件。
有时,默认操作会导致自定义代码出现问题。我将在练习 14-3 中告诉你如何禁用它。
Note
有关 DnD API 的更多信息,请查看 W3C 规范,网址为 http://dev.w3.org/html5/spec/single-page.html#dnd 。
创建 Checkers 应用
为了演示 DnD API,您将创建一个 web 应用来显示一个典型的红白相间的棋盘。您将使用图像文件来表示跳棋,并在它们的初始起始位置显示它们。然后,您将创建允许您将棋子移动到不同方块的事件处理程序。最后,您将添加逻辑来禁用非法移动。
Tip
在本章中,当您向应用添加特性时,您将在这个项目中添加和修改代码。如果对应该在哪里进行更改有任何疑问,最终代码在附录 C 中列出,也可以从源代码下载中获得。
创建项目
您首先需要创建一个 Visual Studio 项目,该项目类似于您之前创建的项目。这将使用网站 ASP.NET 5 项目模板。
EXERCISE 14-1. CREATING THE VISUAL STUDIO PROJECTStart Visual Studio 2015. In the Start Page, click the New Project link. In the New project dialog box, select the ASP.NET Web Application template. Enter the project name Chapter 14 and select a location for this project. In the next dialog box, select the ASP.NET 5 Web Site template. Click the OK button to create the project. In Solution Explorer, right-click the Views\Home folder and click the Add and New Item links. In the Add New Item dialog box, select MVC View Page, enter the name Checkers.cshtml, and click the Add button. Open the HomeController.cs file in the Controllers folder. Add the following code at the end of the class. This will display the new Checkers view.
public IActionResult Checkers()
{
return View();
}
The source code download for this chapter includes an Images folder with five images. Copy all five images to the wwwroot\images folder in Solution Explorer.
画棋盘
为了绘制棋盘,您将为每个方块使用一个单独的div元素。您需要 8 行,每行 8 个div元素。幸运的是,使用几个嵌套的for循环和 Razor 语法很容易做到这一点。
Note
在第十章中,你用canvas元素画了一个棋盘。然而,这对于这个应用来说是行不通的,因为每个方块都需要单独的 DOM 元素。您可能会尝试使用 SVG 来创建棋盘,因为每个rect元素都是一个独立的 DOM 元素;然而,SVG 元素不支持 DnD API。
EXERCISE 14-2. DRAWING THE BOARDReplace the default implementation of the Checkers.cshtml with the code shown in Listing 14-1.
清单 14-1。最初的 Checkers.cshtml 实现
<head>
</head>
<body>
<div class="board">
@for (int y = 0; y < 8; y++)
{
for (int x = 0; x < 8; x++)
{
string id = x.ToString() + y.ToString();
string css;
if ((x + y) % 2 == 0)
{
css = "bwhite";
}
else
{
css = "bblack";
}
<text>
<div id="@id" class="@css" draggable="false">
</div>
</text>
}
}
</div>
</body>
This code uses two nested for loops to create the div elements. Inside the second for loop, the id variable is computed by concatenating the x and y variables. The css variable alternates between bwhite and bblack. For even-numbered rows, the even columns are black, and the odd columns are white. This reverses for odd-numbered rows. The draggable attribute is set to false because we don’t want squares being dragged, only pieces. Now you’ll need to add some style rules to set the size and color of each square. Add the style element shown in Listing 14-2 inside the head element.
清单 14-2。添加 CSS 样式
<style type="text/css" >
.board
{
width: 400px;
height: 400px;
margin-top: 20px;
}
.bblack
{
background-color: #b93030;
border-color: #b93030;
border-width: 1px;
border-style: solid;
width: 48px;
height: 48px;
float: left;
margin: 0px;
padding: 0px;
}
.bwhite
{
background-color: #f7f7f7;
border-color: #b93030;
border-width: 1px;
border-style: solid;
width: 48px;
height: 48px;
float: left;
margin: 0px;
padding: 0px;
}
</style>
Press F5 to preview this page, which should look like Figure 14-2. You didn’t provide a link to get to the Checkers page. To navigate to your page, add /Home/Checkers to the URL in the browser.

图 14-2。
The initial board Now you’ll add the checkers by including an img element inside the appropriate div elements. Add the code shown in bold in Listing 14-3.
清单 14-3。添加图像
<text>
<div id="@id" class="@css" draggable="false">
@if ((x + y) % 2 != 0``&&``y != 3``&&
{
string imgSrc;
string pid;
if (y < 3)
{
imgSrc = "../img/WhitePiece.png";
pid = "w" + id;
}
else
{
imgSrc = "../img/BlackPiece.png";
pid = "b" + id;
}
<text>
<img id="@pid" src="@imgSrc" draggable="true" class="piece" >
</text>
}
</div>
</text>
To determine the appropriate squares, the first rule is that checkers are only on the black (or red in this case) squares. So, the code uses the same (x + y) % 2 != 0 logic that was used to compute the css variable. Then, checkers are placed only on the top three and bottom three rows, so the code excludes rows 3 and 4. If the row is less than 3, this will add a white checker and use a black checker for the other rows. The code computes the id for the img element by prefixing the id of the square with either w or b. Notice that the draggable attribute is set to true. The class attribute for the img elements was set to piece. Now add the following rule to the existing style element, which will add padding so the checker will be centered in the square.
.piece
{
margin-left: 4px;
margin-top: 4px;
}
Press F5 to start the application, and you should now see the checkers, as demonstrated in Figure 14-3.

图 14-3。
The initial checker board with checkers
添加拖放支持
img元素是用draggable属性添加的,所以您应该能够选择并拖动它。但是,您会注意到没有一个方块接受拖放,光标显示“不允许”图标。如果您想尝试一些默认的浏览器功能,请尝试将图像拖动到地址栏。浏览器将导航到图像的 URL。现在,您将添加启用拖放的代码,以便您可以开始移动棋子。然后,您将细化该代码,以确保只允许合法的移动。
允许下降
您有可拖动的元素,而完成拖放操作所需要的只是一个接受拖放的元素。为此,您需要一个用于设置拖放效果的dragover事件的事件处理程序。默认情况下,effectAllowed属性被设置为 all,因此将拖放效果设置为 move、copy 或 link 都是有效的设置。要尝试这样做,在body元素的末尾添加一个script元素,并添加清单 14-4 中所示的代码。
Listing 14-4. The Initial Event Implementation
<script type="text/javascript">
// Get all the black squares
var squares = document.querySelectorAll('.bblack');
var i = 0;
while (i < squares.length) {
var s = squares[i++];
// Add the event listeners
s.addEventListener('dragover', dragOver, false);
}
// Handle the dragover event
function dragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = "move";
}
</script>
这段代码使用了我在第五章中描述的querySelectorAll()函数来获取所有的黑色方块。然后,它遍历返回的集合,并为dragover事件添加一个事件处理程序。dragover()函数调用preventDefault()函数取消浏览器的默认动作。然后它获取dataTransfer对象并将dropEffect属性设置为move。
按 F5 运行应用,并尝试拖动一个 checker。现在你应该在所有的黑色方块上看到一个移动光标,但是在白色方块上看到一个“不允许”的光标。试着把棋子放到一个空的黑色方块上。因为您还没有实现一个drop事件处理程序,浏览器将执行它的默认放下动作。根据浏览器的不同,这可能会导航到图像文件。
执行自定义拖放操作
默认动作不是您在这里要找的,所以您需要实现drop事件处理程序并提供您自己的逻辑。drop 事件处理程序是所有实际工作发生的地方。如果这是一个垃圾桶,文件就在这里被删除。对于这个应用,拖放操作将在目标位置创建一个新的img元素,并删除之前的图像。
要实现 drop,您还需要提供dragstart事件处理程序。在dragstart事件处理程序中,您将存储被拖动到dataTransfer对象中的img元素的id。这将由drop事件处理程序使用,因此它将知道删除哪个元素。
EXERCISE 14-3. IMPLEMENTING THE DROPAdd the following function to the existing script element, which will be used as the dragstart event handler. This code gets the id of the source element (remember the dragstart event is raised on the source element), which is the selected checker image. This id is stored in the dataTransfer object. This function also specifies that the allowed effects should be move since you’ll be moving this image.
function dragStart(e) {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text", e.target.id);
}
To provide the drop event handler, add the code shown in Listing 14-5.
清单 14-5。实现 Drop 事件处理程序
function drop(e) {
// Prevent the event from being raised on the parent element
if (e.stopPropagation) {
e.stopPropagation();
}
// Stop the browsers default action
if (e.preventDefault) {
e.preventDefault();
}
// Get the img element that is being dragged
var droppedID = e.dataTransfer.getData("text");
var droppedPiece = document.getElementById(droppedID);
// Create a new img on the target location
var newPiece = document.createElement("img");
newPiece.src = droppedPiece.src;
newPiece.id = droppedPiece.id.substr(0, 1) + e.target.id;
newPiece.draggable = true;
newPiece.classList.add("piece");
newPiece.addEventListener("dragstart", dragStart, false);
e.target.appendChild(newPiece);
// Remove the previous image
droppedPiece.parentNode.removeChild(droppedPiece);
}
This code first calls the stopPropagation() function to keep this event from bubbling up to the parent element. It also calls preventDefault() to cancel the browser’s default action. It then gets the id from the dataTransfer object and uses this to access the img element. This function then creates a new img element and sets all the necessary properties and adds the necessary event handlers. As I explained, the drop event is raised on the target element, which is the element being dropped on. The id for the new img element is computed using the id of the new location, which is obtained from the target property of the event object. The ID prefix (b or w) is copied from the existing img element. Finally, this code removes the existing img element. Now you’ll need to wire up the event handlers. To do that, add the following code shown in bold near the beginning of the script element:
var squares = document.querySelectorAll('.bblack');
var i = 0;
while (i < squares.length) {
var s = squares[i++];
// Add the event listeners
s.addEventListener('dragover', dragOver, false);
s.addEventListener('drop', drop, false);
}
i = 0;
var pieces = document.querySelectorAll('img');
while (i < pieces.length) {
var p = pieces[i++];
p.addEventListener('dragstart', dragStart, false);
}
The drop event handler is added to the squares since these are the target elements. The dragstart event must be added to the img elements. This code gets all of the img elements using the querySelectorAll() function. Now press F5 to start the application. You should be able to drag a checker to any red square.
提供视觉反馈
当拖动一个元素时,提供一些指示被选择对象的视觉反馈是一个好主意。通过在dragover事件处理程序中设置dropEffect属性,光标指示是否允许拖放。然而,你应该做得更多。源元素和目标元素都应该在视觉上突出,这样用户可以很容易地看到,如果他们释放鼠标按钮,这个部分将从这里移动到那里。
为此,您将向源和目标元素动态添加一个class属性。然后,您可以使用普通的 CSS 样式规则来设置它们的样式。对于源元素,您将使用dragstart和dragend事件来添加和移除class属性。同样,对于目标元素,您将使用dragenter和dragleave事件。
EXERCISE 14-4. ADDING VISUAL FEEDBACKYou already have a dragstart event handler; add the following code in bold to the dragStart() function. This will add the selected class to the element.
function dragStart(e) {
e.dataTransfer.effectAllowed = "all";
e.dataTransfer.setData("text/plain", e.target.id);
e.target.classList.add("selected");
}
Add the dragEnd() function using the following code that will simply remove the selected class when the drag operation has completed.
function dragEnd(e) {
e.target.classList.remove("selected");
}
Add the dragEnter() and dragLeave() functions using the following code. This adds the drop class to the element and then removes it.
function dragEnter(e) {
e.target.classList.add('drop');
}
function dragLeave(e) {
e.target.classList.remove("drop");
}
Since you’ve added three new event handlers, you’ll need to add code to add the event listeners. Add the code shown in bold to the existing script element.
var squares = document.querySelectorAll('.bblack');
var i = 0;
while (i < squares.length){
var s = squares[i++];
// Add the event listeners
s.addEventListener('dragover', dragOver, false);
s.addEventListener('drop', drop, false);
s.addEventListener('dragenter', dragEnter, false);
s.addEventListener('dragleave', dragLeave, false);
}
i = 0;
var pieces = document.querySelectorAll('img');
while (i < pieces.length){
var p = pieces[i++];
p.addEventListener('dragstart', dragStart, false);
p.addEventListener('dragend', dragEnd, false);
}
Now you’ll need to make a couple of changes to the drop event handler. You added the drop class to the target element in the dragenter event and then removed it in the dragleave event. However, if they drop the image, the dragleave event is not raised. You’ll also need to remove the drop class in the drop event as well. Also, when creating a new img element, you’ll need to wire up the dragend event handler. Add the code shown in bold.
// Create a new img on the target location
var newPiece = document.createElement("img");
newPiece.src = droppedPiece.src;
newPiece.id = droppedPiece.id.substr(0, 1) + e.target.id;
newPiece.draggable = true;
newPiece.classList.add("piece");
newPiece.addEventListener("dragstart", dragStart, false);
newPiece.addEventListener("dragend", dragEnd, false);
e.target.appendChild(newPiece);
// Remove the previous image
droppedPiece.parentNode.removeChild(droppedPiece);
// Remove the drop effect from the target element
e.target.classList.remove('drop');
Finally, you’ll need to define the CSS rules for the drop and selected values. I’ve chosen to set the opacity attribute, but you could just as easily add a border, change the background color, or implement any number of effects to achieve the desired purpose. Add the following rules to the existing style element:
.bblack.drop
{
opacity: 0.5;
}
.piece.selected
{
opacity: 0.5;
}
Press F5 to start the application. Try dragging an image to a red square, and you should see the expected visual feedback, as shown in Figure 14-4.

图 14-4。
Displaying the drag-and-drop visual feedback
执行游戏规则
你可能已经注意到你可以移动一个棋子到任何红色方块。当前的实现没有实现任何规则来确保进行合法的移动。现在,您将添加该逻辑。以下事件将需要这一点:
dragover:将dropEffect设置为none,用于非法移动dragenter:仅改变有效放置位置的样式- 只有在合法的情况下才执行移动
您将实现一个isValidMove()函数,该函数将评估尝试的移动,如果这是非法移动,则返回false i。然后,您将在前面列出的三个事件中调用这个函数。
验证移动
幸运的是,跳棋的规则相当简单。因为dragover事件处理程序没有被添加到白色方块中,所以在那里放置一块已经被禁用,这进一步简化了所需的工作。以下是您将实现的规则:
- 你不能移动到已经被占领的方块。
- 棋子只能向前移动。
- 棋子只能对角移动一格或两格(对角),如果跳过一个被占领的方格。
- 你只能跳一个不同颜色的棋子。
- 跳下的棋子必须从棋盘上移走。
Note
稍后,您将添加逻辑来处理将棋子提升为国王的过程。
EXERCISE 14-5. ENFORCING THE RULESImplement the isValidMove() function by adding the code shown in Listing 14-6 to the existing script element.
清单 14-6。实现 isValidMove()函数
function isValidMove(source, target, drop) {
// Get the piece prefix and location
var startPos = source.id.substr(1, 2);
var prefix = source.id.substr(0, 1);
// Get the drop location, strip off the prefix, if any
var endPos = target.id;
if (endPos.length > 2) {
endPos = endPos.substr(1, 2);
}
// You can't drop on the existing location
if (startPos === endPos) {
return false;
}
// You can't drop on occupied square
if (target.childElementCount != 0) {
return false;
}
// Compute the x and y coordinates
var xStart = parseInt(startPos.substr(0, 1));
var yStart = parseInt(startPos.substr(1, 1));
var xEnd = parseInt(endPos.substr(0, 1));
var yEnd = parseInt(endPos.substr(1, 1));
switch (prefix) {
// For white pieces...
case "w":
if (yEnd <= yStart)
return false; // Can't move backwards
break;
// For black pieces...
case "b":
if (yEnd >= yStart)
return false; // Can't move backwards
break;
}
// These rule apply to all pieces
if (yStart === yEnd || xStart === xEnd)
return false; // Move must be diagonal
if (Math.abs(yEnd - yStart) > 2 || Math.abs(xEnd - xStart) > 2)
return false; // Can't move more than two spaces
// If moving two spaces, find the square that is jumped
if (Math.abs(xEnd - xStart) === 2) {
var pos = ((xStart + xEnd) / 2).toString() +
((yStart + yEnd) / 2).toString();
var div = document.getElementById(pos);
if (div.childElementCount === 0)
return false; // Can't jump an empty square
var img = div.children[0];
if (img.id.substr(0, 1).toLowerCase() === prefix.toLowerCase())
return false; // Can't jump a piece of the same color
// If this function is called from the drop event
// Remove the jumped piece
if (drop) {
div.removeChild(img);
}
}
return true;
}
The parameters to the isValidMove() function include the source and target elements. Remember, the source is an img element, and its id attribute is a combination of the color (w or b) and the x and y coordinates. The target is a div element, and its id attribute is just the x and y coordinates. I’ve added lots of comments to this code, so it should be fairly self-explanatory, but I will point out a couple of the more interesting points.
- 要确定一个正方形是否被占用,您可以简单地检查
childElementCount属性。对于空方块,这将是 0。 - 对于白色棋子,向前移动意味着 y 坐标增加,但是对于黑色棋子,情况正好相反。为了处理这个问题,函数使用了一个
switch语句来为每一个应用不同的规则。 - 如果棋子移动了两格,那么这个函数需要检查被跳过的方格。它的位置通过平均起始和结束位置来确定。
- 如果方块被占据,那么代码会检查该块是否是相同的颜色。代码首先获取子元素,这将是方块上的
img。颜色由id属性的前缀决定。代码在比较前将前缀转换为小写。稍后我会解释的。 - 如果一个不同颜色的块被跳转,那么你将移除它,因为代码已经有了
img元素。然而,只有从drop事件中调用该方法时,才需要这样做,该事件由该函数的第三个参数指定。另外两个事件(dragOver和dragEnter)使用这个方法来验证移动,但并不实际进行移动,它们将为第三个参数传递 false。
Now you’ll need to change dragover event to validate the move before setting the dropEffect. Replace the existing implementation of the dragOver() function with the code shown in Listing 14-7. The new code gets the id of the img that is being dragged from the dataTransfer object and then uses the id to get the element. This is passed in to the isValidMove() function along with the target element, which is obtained from the event object (e.target). The dropEffect is set to move only if this is a valid move.
清单 14-7。修订后的 dragOver 事件处理程序
function dragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
// Get the img element that is being dragged
var dragID = e.dataTransfer.getData("text");
var dragPiece = document.getElementById(dragID);
// Work around - if we can't get the dataTransfer, don't
// disable the move yet, the drop event will catch this
if (dragPiece) {
if (e.target.tagName === "DIV" &&
isValidMove(dragPiece, e.target, false)) {
e.dataTransfer.dropEffect = "move";
}
else {
e.dataTransfer.dropEffect = "none";
}
}
}
注意在撰写本文时,Chrome、IE 和 Opera 都不允许你在dragEnter和dragOver事件中访问dataTransfer对象。然而,这在drop事件中确实有效。在dragOver事件中,如果源对象不可用,则允许移动。游戏还会运行,因为drop事件会忽略任何无效的招式,但是用户体验并不理想。dragEnter事件用于应用drop类进行样式设计,这也不能正常工作。在本章的剩余部分,我将使用 Firefox 来测试应用。
Replace the implementation of the dragEnter() function with the following code. This code is essentially the same as the dragOver() function, except it adds the drop class to the element instead of setting the dropEffect.
function dragEnter(e) {
// Get the img element that is being dragged
var dragID = e.dataTransfer.getData("text");
var dragPiece = document.getElementById(dragID);
if (dragPiece &&
e.target.tagName === "DIV" &&
isValidMove(dragPiece, e.target, false)) {
e.target.classList.add('drop');
}
}
For the drop() function, wrap the code that performs the drop inside an if statement that validates the move by adding the code shown in bold. This time, the code is passing true for the third parameter to the isValidMove() function.
if (droppedPiece &&
e.target.tagName === "DIV" &&
isValidMove(droppedPiece, e.target, true)) {
// Create a new img on the target location
var newPiece = document.createElement("img");
newPiece.src = droppedPiece.src;
newPiece.id = droppedPiece.id.substr(0, 1) + e.target.id;
newPiece.draggable = true;
newPiece.classList.add("piece");
newPiece.addEventListener("dragstart", dragStart, false);
newPiece.addEventListener("dragend", dragEnd, false);
e.target.appendChild(newPiece);
// Remove the previous image
droppedPiece.parentNode.removeChild(droppedPiece);
// Remove the drop effect from the target element
e.target.classList.remove('drop');
}
With these changes now in place, try running the application. You should be allowed to make only legal moves. If you jump a checker, it should be removed from the board.
晋升为国王
在跳棋中,当一个棋子一直移动到最后一行时,它就被提升为国王。国王的工作方式和普通棋子一样,只是它可以向后移动。现在,您将添加代码来检查某个部分是否需要升级。要升级一个棋子,您需要更改显示的图像以表明它是一个国王。您还将更改前缀,使其成为大写的 B 或 w。然后您可以允许国王使用不同的规则。
您将把所有这些逻辑放在一个名为kingMe()的函数中,并且在每次发生拖放时调用这个函数。如果棋子已经是一个国王,或者不在最后一行,函数就返回。否则,它会执行提升。
EXERCISE 14-6. ADDING PROMOTIONAdd the kingMe() function shown in Listing 14-8 to the existing script element.
清单 14-8。实现 kingMe()函数
function kingMe(piece) {
// If we're already a king, just return
if (piece.id.substr(0, 1) === "W" || piece.id.substr(0, 1) === "B")
return;
var newPiece;
// If this is a white piece on the 7th row
if (piece.id.substr(0, 1) === "w" && piece.id.substr(2, 1) === "7") {
newPiece = document.createElement("img");
newPiece.src = "../img/WhiteKing.png";
newPiece.id = "W" + piece.id.substr(1, 2);
}
// If this is a black piece on the 0th row
if (piece.id.substr(0, 1) === "b" && piece.id.substr(2, 1) === "0") {
var newPiece = document.createElement("img");
newPiece.src = "../img/BlackKing.png";
newPiece.id = "B" + piece.id.substr(1, 2);
}
// If a new piece was created, set its properties and events
if (newPiece) {
newPiece.draggable = true;
newPiece.classList.add("piece");
newPiece.addEventListener('dragstart', dragStart, false);
newPiece.addEventListener('dragend', dragEnd, false);
var parent = piece.parentNode;
parent.removeChild(piece);
parent.appendChild(newPiece);
}
}
The kingMe() function simply returns if the id prefix is either B or W, which indicates this is already a king. It then checks to see whether this is a white piece on row 7 or a black piece on row 0. If so, a new img element is created with the appropriate src and id properties. If a new img was created, the function then sets all of the properties and events, removes the existing img element from the div element, and adds the new one. Modify the drop() function to call the kingMe() function after a drop has been performed by adding the line shown in bold.
// Remove the previous image
droppedPiece.parentNode.removeChild(droppedPiece);
// Remove the drop effect from the target element
e.target.classList.remove('drop');
// See if the piece needs to be promoted
kingMe(newPiece);
提示当你实现了isValidMove()函数时,防止棋子向后移动的规则只适用于b和w前缀。由于国王有一个大写的B或W,这条规则不适用,因此国王可以向后移动。此外,当跳转一个棋子时,比较是在首先转换成小写字母之后进行的。这将允许白棋跳过黑棋或黑王。
Try moving the pieces around until you move one to the last row. You should see the image change to indicate this is now a king, as shown in Figure 14-5.

图 14-5。
The check board with a king Once you have a king, try moving it backward and also try jumping pieces with it.
依次移动
你可能已经注意到这个应用并没有强制每个玩家轮流玩。现在您将实现这个逻辑。每次移动后(drop事件被处理),您将为刚刚移动的所有颜色块设置draggable属性为false。这将使你无法移动相同颜色的棋子。然而,这条规则有一个例外,需要做一些额外的工作。如果你跳了一个棋子,那么同一个棋子只要是另一跳就可以再移动一次。
您将首先从实现通用规则开始。这将通过创建一个名为enableNextPlayer()的新函数来完成。这个函数将使用querySelectorAll()函数来获取所有的img元素。根据前缀id,属性将被设置为true或false。然后您将添加特殊的逻辑来处理跳转条件。
EXERCISE 14-7. TAKING TURNSAdd the enableNextPlayer() function to the existing script element using the code shown in Listing 14-9.
清单 14-9。实现 enableNextPlayer()函数
function enableNextPlayer(piece) {
// Get all of the pieces
var pieces = document.querySelectorAll('img');
i = 0;
while (i < pieces.length) {
var p = pieces[i++];
// If this is the same color that just moved, disable dragging
if (p.id.substr(0, 1).toUpperCase() ===
piece.id.substr(0, 1).toUpperCase()) {
p.draggable = false;
}
// Otherwise, enable dragging
else {
p.draggable = true;
}
}
}
At the end of the isValidMove() function, add the code shown in bold. This will call the enableNextPlay() function when a drop is being performed.
// Set the draggable attribute so the next player can take a turn
if (drop) {
enableNextPlayer(source);
}
return true;
}
注意通常将这个调用放在drop()函数中可能更有意义。然而,只有isValidMove()函数知道发生了跳转,您需要在这里添加覆盖逻辑。这需要在应用一般规则之后。
The drop() function creates a new img element and currently sets the draggable attribute to true. Now you’ll need to make this conditional based on the draggable attribute of the existing piece. Add the following code shown in bold to the drop() function:
// Create a new img on the target location
var newPiece = document.createElement("img");
newPiece.src = droppedPiece.src;
newPiece.id = droppedPiece.id.substr(0, 1) + e.target.id;
newPiece.draggable = droppedPiece.draggable;
newPiece.classList.add("piece");
newPiece.addEventListener("dragstart", dragStart, false);
newPiece.addEventListener("dragend", dragEnd, false);
e.target.appendChild(newPiece);
Now you’ll need change the dragStart event handler to ignore this event if the element is not draggable. Add the following code shown in bold to the dragStart() function:
function dragStart(e) {
if (e.target.draggable) {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", e.target.id);
e.target.classList.add("selected");
}
}
Now you’ll implement the special jump logic. If the piece just made a jump, you’ll set the draggable attribute back to true so it will be allowed to make another move. However, you’ll also add the jumpOnly class to the classList so you can enforce that the only move that it is allowed to make is another jump. Add the code shown in bold to the isValidMove() function. This will look for jumpOnly in the classList and set the jumpOnly flag accordingly.
var jumpOnly = false;
if (source.classList.contains("jumpOnly")) {
jumpOnly = true;
}
// Compute the x and y coordinates
var xStart = parseInt(startPos.substr(0, 1));
var yStart = parseInt(startPos.substr(1, 1));
Now add the code shown in bold to the isValidMove() function. The first part adds the rule to make sure a jump is being made if jumpOnly is true. The second part sets the jumped flag to indicate that this move is making a jump.
// These rule apply to all pieces
if (yStart === yEnd || xStart === xEnd)
return false; // Move must be diagonal
if (Math.abs(yEnd - yStart) > 2 || Math.abs(xEnd - xStart) > 2)
return false; // Can't move more than two spaces
if (Math.abs(xEnd - xStart) === 1``&&
return false; // Only jumps are allowed
var jumped = false;
// If moving two spaces, find the square that is jumped
if (Math.abs(xEnd - xStart) === 2) {
var pos = ((xStart + xEnd) / 2).toString() +
((yStart + yEnd) / 2).toString();
var div = document.getElementById(pos);
if (div.childElementCount === 0)
return false; // Can't jump an empty square
var img = div.children[0];
if (img.id.substr(0, 1).toLowerCase() === prefix.toLowerCase())
return false; // Can't jump a piece of the same color
// If this function is called from the drop event
// Remove the jumped piece
if (drop) {
div.removeChild(img);
jumped = true;
}
}
At the end of the isValidMove() function, add the code shown in bold. This will override the draggable attribute if a jump was made and add jumpOnly to the classList.
if (drop) {
enableNextPlayer(source);
// If we jumped a piece, we're allowed to go again
if (jumped) {
source.draggable = true;
source.classList.add("jumpOnly"); // But only for another jump
}
}
注意enableNextPlayer()功能禁用当前玩家的所有棋子,并启用其他玩家的。然后这段代码启用了刚刚跳转的部分。所以,两者都被启用;这个棋子可以再次跳跃,或者下一个玩家可以移动。两者都是有效的,所以我们需要允许它们都存在。
Modify the drop() function to also add jumpOnly to the classList when creating the new img element by adding the code shown in bold.
// Create a new img on the target location
var newPiece = document.createElement("img");
newPiece.src = droppedPiece.src;
newPiece.id = droppedPiece.id.substr(0, 1) + e.target.id;
newPiece.draggable = droppedPiece.draggable;
if (droppedPiece.draggable){
newPiece.classList.add("jumpOnly");
}
newPiece.classList.add("piece");
Now you’ll need to clear jumpOnly from the classList when the next move is completed. You’ll do that in the enableNextPlayer() function by adding the code shown in bold.
function enableNextPlayer(piece) {
// Get all of the pieces
var pieces = document.querySelectorAll('img');
i = 0;
while (i < pieces.length) {
var p = pieces[i++];
// If this is the same color that just moved, disable dragging
if (p.id.substr(0, 1).toUpperCase() ===
piece.id.substr(0, 1).toUpperCase()) {
p.draggable = false;
}
// Otherwise, enable dragging
else {
p.draggable = true;
}
p.classList.remove("jumpOnly");
}
}
Now test the application and make sure that each player must alternate turns. Also, verify that you can make successive jumps.
注意最初,白色和黑色棋子的draggable属性都被设置为true,因此任何一种颜色都可以先走一步。如果您想指定先使用哪种颜色,您可以更改创建初始img元素的 Razor 语法,将一种颜色的draggable属性设置为false。我做了一些研究,看看什么颜色应该先去,但发现混合的结果。有些地方指示黑色先走,有些地方说白色先走。然而,有些人说这只是一个游戏,有什么区别吗?我决定实现这个逻辑,所以两者都可以先走。
使用高级功能
在我结束这一章之前,有几件事我要简单地讨论一下。首先,我将向您展示如何使用自定义拖动图像。然后,我将演示在浏览器窗口中拖动元素。
更改拖动图像
当您拖动元素时,当您在页面上移动光标时,该元素的副本会跟随光标。这被称为拖动图像。但是,您可以指定使用不同的图像。这是通过dataTransfer对象的setDragImage()函数完成的。
wwwroot\images文件夹里有一张笑脸图片。将粗体显示的代码添加到dragStart()函数中,将其用作拖动图像。
function dragStart(e) {
if (e.target.draggable) {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text", e.target.id);
e.target.classList.add("selected");
var dragIcon = document.createElement("img");
dragIcon.src = "../img/smiley.jpg";
e.dataTransfer.setDragImage(dragIcon, 0, 0);
}
}
试试这个应用,当你移动棋子时,你应该会看到如图 14-6 所示的笑脸。

图 14-6。
Changing the drag image
在窗口间拖动
正如我在本章开始时提到的,在源元素和目标元素上引发了不同的事件。这些元素可能位于不同的浏览器窗口甚至不同的应用中。然而,这个过程的工作方式是一样的。
为了演示这一点,打开 Firefox 浏览器的第二个实例,并导航到 checkers 应用。您应该会看到两个浏览器窗口,每个窗口都显示了棋盘。在一个窗口中选择一个棋子,并将其拖到第二个窗口中的一个方块上。你会注意到,在第一个窗口中,你只能把它放到相对于它原来位置的方块上。当您放下它时,该片段会被移动到放下位置,但会从第二个窗口中移除,而不是从您最初选择的图像中移除。
跨窗口拖动的关键是dataTransfer对象。这在目标对象上的dragenter、dragover和dragleave事件中提供。阻力从哪里开始并不重要;这些信息放在dataTransfer对象中,并提供给任何支持这些事件的窗口。当 drop 事件接收到这个信息时,它在dataTransfer对象中指定的位置删除了img元素。因为 drop 事件是在第二个窗口中处理的,所以从第二个窗口中移除了img元素。
在源元素上引发了drag和dragend事件。无论在这些事件处理程序上写了什么逻辑,都会在第一个窗口中执行。请注意,所选的img元素在拖动过程中被静音,但在执行拖放时又恢复正常。这是因为在源元素上触发的dragend事件清除了选中的属性。
当您像这里一样控制操作的两端时,您可以决定需要传输什么数据,并实现两组事件处理程序。在许多情况下,您只能控制过程的一个方面。例如,用户可以将文件从 Windows 资源管理器拖到您的网页上。dragstart、drag和dragend事件(或其等效事件)在 Windows 资源管理器应用中引发,这是您无法控制的。然而,dragenter、dragover、dragleave和drop事件都是在您的网页上触发的。您可以根据拖放的元素和dataTransfer对象的内容来决定是否接受拖放。您还可以控制放置完成时发生的过程。
摘要
在这一章中,我解释了作为 DnD API 的一部分而引发的所有事件,以及它们是在哪些元素上引发的。源元素接收以下事件:
dragstart:选中元素并移动鼠标时drag:鼠标移动时连续调用dragend:释放鼠标按钮时
目标元素上会引发下列事件:
dragenter:鼠标第一次进入目标空间时dragover:鼠标在目标上移动时持续dragleave:当鼠标离开目标空间时drop:释放鼠标按钮时
dataTransfer对象用于传递关于源元素的信息。这在所有事件处理程序中都提供了。它尤其被drop事件处理程序用来执行必要的处理。这也支持跨应用拖动。
dragover事件处理程序设置dropEffect,它控制所使用的光标。将此项设置为None将导致使用“不允许”光标,表示信号源不能放在那里。
为了提供一些视觉反馈,dragstart和dragend事件处理程序应该修改源元素,以表明它已被选中并被拖动。同样,dragenter和dragleave事件句柄应该突出显示目标元素。这将为用户提供一种简单的方式来查看所选元素将被放在哪里。
您创建的示例应用实现了一些复杂的规则,用于确定哪些元素可以被拖放到哪里。
十五、第四章的示例内容
清单 A-1 指定了用于第四章中的练习的初始 HTML 内容。这可以从下载源的Default_content.cshtml文件中获得。我把它放在这里,以防你不下载代码就想看。
Listing A-1. Chapter 4 Sample Content
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Chapter``4
</head>
<body>
<header class="intro">
<h1>CSS Demo</h1>
<h2>Introducing the new HTML5 features</h2>
<h3>
Use the new CSS3 features to build some of the most visually
appealing web sites.
</h3>
</header>
<nav>
<ul>
<li><a href="#feature">Feature</a></li>
<li><a href="#other">Article</a></li>
<li><a href="#another">Archives</a></li>
<li><a href="http://www.apress.com
</ul>
</nav>
<div id="contentArea">
<div id="mainContent">
<section class="rounded">
<header>
<h2>Main content area</h2>
</header>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut.
</p>
</section>
<section>
<article class="featuredContent">
<a id="feature"></a>
<header>
<h3>Featured Article</h3>
</header>
<div class="rotateContainer">
<p>This is really cool...</p>
<img class="rotate" id="phone"
src="img/phonebooth.jpg"
alt="phonebooth"
onclick="toggleAnimation()"/>
<br />
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing
elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing
elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing
elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut.
</p>
</div>
</article>
<article class="otherContent">
<a id="other"></a>
<header>
<h3>Rounded Borders</h3>
</header>
<div>
<p>Details about rounded corners</p>
<p>
One of the most common features that you'll hear
about is the use of rounded corners and we'll cover
that here. Also, by configuring the div size and
radius properly you can also make circular divs
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing
elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut.
</p>
</div>
</article>
<article class="otherContent">
<a id="another"></a>
<header>
<h3>Another Interesting Article</h3>
</header>
<div>
<p>More things to say...</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Proin luctus tincidunt justo nec tempor.
Aliquam erat volutpat. Fusce facilisis ullamcorper
consequat. Vestibulum non sapien lectus. Nam mi
augue, posuere at tempus vel, dignissim vitae nulla.
Nullam at quam eu sapien mattis ultrices. Quisque
quis leo mi, at lobortis dolor. Nullam scelerisque
facilisis placerat. Fusce a augue erat, malesuada
euismod dui. Duis iaculis risus id felis volutpat
elementum. Fusce blandit iaculis quam a cursus.
Cras varius tincidunt cursus. Morbi justo eros,
adipiscing ac placerat sed, posuere et mi.
Suspendisse vulputate viverra aliquet. Duis non
enim a nibh consequat mollis ac tempor lorem.
Phasellus elit leo, semper eu luctus et, suscipit
at lacus. In hac habitasse platea dictumst. Duis
dignissim justo sit amet nulla pulvinar sodales.
</p>
</div>
</article>
</section>
</div>
<aside id="sidebar">
<h3>Other Titles</h3>
<div id="moon"></div>
<p>
Check out some of the other titles available from Apress.
</p>
<section id="titles">
<article class="book">
<header>
<a href="http://www.apress.com/9781430240747
target="_blank">
<img src="images\office365.png"
alt="Pro Office 365"/>
</a>
</header>
<p>
Pro Office 365 Development is a practical, hands-on
guide to building cloud-based solutions using the
Office 365 platform.
</p>
</article>
<article class="book">
<header>
<a href="http://www.apress.com/9781430235781
target="_blank">
<img src="images\access2010.png"
alt="Pro Access 2010"/>
</a>
</header>
<p>
Pro Access 2010 Development is a fundamental resource
for developing business applications that take
advantage of the features of Access 2010\. You'll learn
how to build database applications, create Web-based
databases, develop macros and VBA tools for Access
applications, integrate Access with SharePoint, and
much more.
</p>
</article>
<article class="book">
<header>
<a href="http://www.apress.com/9781430228295
target="_blank">
<img src="images\sharepoint_pm.png"
alt="Pro Project Management w/SharePoint 2010"/>
</a>
</header>
<p>
The intention of this book is to provide a working
case study that you can follow to create a complete
PMIS (project management information system) with
SharePoint Server's out-of-the-box functionality.
</p>
</article>
<article class="book">
<header>
<a href="http://www.apress.com/9781430229049
target="_blank">
<img src="images\office:workflow.png"
alt="Office 2010 Workflow"/>
</a>
</header>
<p>
Workflow is the glue that binds information worker
processes, users, and artifacts—without it,
information workers are just islands of data and
potential. Office 2010 Workflow walks you through
implementing workflow solutions.
</p>
</article>
<article class="book">
<header>
<a href="http://www.apress.com/9781430224853
target="_blank">
<img src="images\beginning_wf.png"
alt="Beginning WF"/>
</a>
</header>
<p>
Indexed by feature so you can find answers easily
and written in an accessible style, Beginning WF
shows how Microsoft's Workflow Foundation (WF)
technology can be used in a wide variety of
applications.
</p>
</article>
</section>
</aside>
</div>
<footer>
<p>
Last updated <time datetime="2015-03-07T20:32:22+05:00">
March 7th 2015</time>
by <a href="http://www.thecreativepeople.com
target="_blank">Mark J. Collins</a>
</p>
</footer>
</body>
</html>
十六、第四章的完整样式
清单 B-1 显示了来自第四章项目的已完成的style元素。我已经分块解释过了,但是如果你想一起看的话,我把它包括在这里。
Listing B-1. Chapter 4 Completed style Element
<style>
/* Basic tag settings */
body
{
margin: 0 auto;
width: 940px;
font: 13px/22px Helvetica, Arial, sans-serif;
background: #f0f0f0;
}
h2
{
font-size: 18px;
line-height: 5px;
padding: 2px 0;
}
h3
{
font-size: 12px;
line-height: 5px;
padding: 2px 0;
}
h1, h2, h3
{
text-align: left;
}
p
{
padding-bottom: 2px;
}
.book
{
padding: 5px;
}
/* Content sections */
.featuredContent
{
background-color: #ffffff;
border: 2px solid #6699cc;
padding: 15px 15px 15px 15px;
}
.otherContent
{
background-color: #c0c0c0;
border: 1px solid #999999;
padding: 15px 15px 15px 15px;
}
aside
{
background-color: #6699cc;
padding: 5px 5px 5px 5px;
}
footer
{
margin-top: 12px;
text-align: center;
background-color: #ddd;
}
footer p
{
padding-top: 10px;
}
/* Navigation Section */
nav
{
left: 0;
background-color: #003366;
}
nav ul
{
margin: 0;
list-style: none;
}
nav ul li
{
float: left;
}
nav ul li a
{
display: block;
margin-right: 20px;
width: 140px;
font-size: 14px;
line-height: 28px;
text-align: center;
padding-bottom: 2px;
text-decoration: none;
color: #cccccc;
}
nav ul li a:hover
{
color: #fff;
}
/* Rounded borders */
.rounded
{
border: 1px solid;
border-color: #999999;
border-radius: 25px;
padding: 24px;
}
aside
{
border: 1px solid #999999;
border-radius: 12px;
}
/* Make the radius half of the height */
nav
{
height: 30px;
border-radius: 15px;
}
footer
{
height: 50px;
border-radius: 25px;
}
/* Gradients */
.intro
{
border: 1px solid #999999;
text-align: left;
padding-left: 15px;
margin-top: 6px;
border-radius: 25px;
background-image: linear-gradient(45deg, #ffffff, #6699cc);
}
/* Setup a table for the content and sidebar */
#contentArea
{
display: table;
}
#mainContent
{
display: table-cell;
padding-right: 2px;
}
aside
{
display: table-cell;
width: 280px;
}
/* Setup multiple columns for the articles */
.otherContent
{
text-align: justify;
padding: 6px;
-webkit-column-count: 2;
column-count: 2;
-webkit-column-gap: 20px;
column-gap: 20px;
}
/* Add the box shadow */
article img
{
margin: 10px 0;
box-shadow: 3px 3px 12px #222;
}
.book img
{
margin: 10px 0;
display: block;
box-shadow: 2px 2px 5px #444;
margin-left: auto;
margin-right: auto;
}
aside
{
box-shadow: 3px 3px 3px #aaaaaa;
}
/* Stripe the title list */
#titles article:nth-child(2n+1)
{
background: #c0c0c0;
border: 1px solid #6699cc;
border-radius: 10px;
}
#titles article:nth-child(2n+0)
{
background: #6699cc;
border: 1px solid #c0c0c0;
border-radius: 10px;
}
/* Text decorations */
h2
{
text-decoration: underline;
-moz-text-decoration-line: underline;
-moz-text-decoration-style: wavy;
-moz-text-decoration-color: red;
text-decoration-line: underline;
text-decoration-style: wavy;
text-decoration-color: red;
}
h3:first-letter
{
text-shadow: 2px -5px 1px;
}
/* Transforms */
.rotateContainer
{
-webkit-perspective: 360;
perspective: 360px;
}
.rotate
{
-webkit-transform-style: preserve-3d;
transform-style: preserve-3d;
}
/* Animate the moon phases */
@@-webkit-keyframes moonPhases
{
0% { background-image: url("img/moon1.png"); }
12% { background-image: url("img/moon2.png"); }
25% { background-image: url("img/moon3.png"); }
37% { background-image: url("img/moon4.png"); }
50% { background-image: url("img/moon5.png"); }
62% { background-image: url("img/moon6.png"); }
75% { background-image: url("img/moon7.png"); }
87% { background-image: url("img/moon8.png"); }
100% { background-image: url("img/moon1.png"); }
}
@@keyframes moonPhases
{
0% { background-image: url("img/moon1.png"); }
12% { background-image: url("img/moon2.png"); }
25% { background-image: url("img/moon3.png"); }
37% { background-image: url("img/moon4.png"); }
50% { background-image: url("img/moon5.png"); }
62% { background-image: url("img/moon6.png"); }
75% { background-image: url("img/moon7.png"); }
87% { background-image: url("img/moon8.png"); }
100% { background-image: url("img/moon1.png"); }
}
#moon
{
width: 115px;
height: 115px;
background-image: url("img/moon1.png");
background-repeat: no-repeat;
-webkit-animation-name: moonPhases;
-webkit-animation-duration: 4s;
-webkit-animation-delay: 3s;
-webkit-animation-iteration-count: infinite;
animation-name: moonPhases;
animation-duration: 4s;
animation-delay: 3s;
animation-iteration-count: infinite;
}
</style>
十七、第十四章的最终代码
清单 C-1 指定了第十四章中项目的最终代码。这可以从下载源的Checkers.cshtml文件中获得。我把它放在这里,以防你不下载代码就想看。
Listing C-1. Chapter 14 Final Code
<head>
<style type="text/css" >
.board
{
width: 400px;
height: 400px;
margin-top: 20px;
}
.bblack
{
background-color: #b93030;
border-color: #b93030;
border-width: 1px;
border-style: solid;
width: 48px;
height: 48px;
float: left;
margin: 0px;
padding: 0px;
}
.bwhite
{
background-color: #f7f7f7;
border-color: #b93030;
border-width: 1px;
border-style: solid;
width: 48px;
height: 48px;
float: left;
margin: 0px;
padding: 0px;
}
.piece
{
margin-left: 4px;
margin-top: 4px;
}
.bblack.drop
{
opacity: 0.5;
}
.piece.selected
{
opacity: 0.5;
}
</style>
</head>
<body>
<div class="board">
@for (int y = 0; y < 8; y++)
{
for (int x = 0; x < 8; x++)
{
string id = x.ToString() + y.ToString();
string css;
if ((x + y) % 2 == 0)
{
css = "bwhite";
}
else
{
css = "bblack";
}
<text>
<div id="@id" class="@css" draggable="false">
@if ((x + y) % 2 != 0 && y != 3 && y != 4)
{
string imgSrc;
string pid;
if (y < 3)
{
imgSrc = "../img/WhitePiece.png";
pid = "w" + id;
}
else
{
imgSrc = "../img/BlackPiece.png";
pid = "b" + id;
}
<text>
<img id="@pid" src="@imgSrc" draggable="true" class="piece">
</text>
}
</div>
</text>
}
}
</div>
<script type="text/javascript">
// Get all the black squares
var squares = document.querySelectorAll('.bblack');
var i = 0;
while (i < squares.length) {
var s = squares[i++];
// Add the event listeners
s.addEventListener('dragover', dragOver, false);
s.addEventListener('drop', drop, false);
s.addEventListener('dragenter', dragEnter, false);
s.addEventListener('dragleave', dragLeave, false);
}
i = 0;
var pieces = document.querySelectorAll('img');
while (i < pieces.length) {
var p = pieces[i++];
p.addEventListener('dragstart', dragStart, false);
p.addEventListener('dragend', dragEnd, false);
}
// Handle the dragover event
function dragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
// Get the img element that is being dragged
var dragID = e.dataTransfer.getData("text");
var dragPiece = document.getElementById(dragID);
// Work around - if we can't get the dataTransfer, don't
// disable the move yet, the drop event will catch this
if (dragPiece) {
if (e.target.tagName === "DIV" &&
isValidMove(dragPiece, e.target, false)) {
e.dataTransfer.dropEffect = "move";
}
else {
e.dataTransfer.dropEffect = "none";
}
}
}
function dragStart(e) {
if (e.target.draggable) {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text", e.target.id);
e.target.classList.add("selected");
var dragIcon = document.createElement("img");
dragIcon.src = "../img/smiley.jpg";
e.dataTransfer.setDragImage(dragIcon, 0, 0);
}
}
function dragEnd(e) {
e.target.classList.remove("selected");
}
function drop(e) {
// Prevent the event from being raised on the parent element
if (e.stopPropagation) {
e.stopPropagation();
}
// Stop the browsers default action
if (e.preventDefault) {
e.preventDefault();
}
// Get the img element that is being dragged
var droppedID = e.dataTransfer.getData("text");
var droppedPiece = document.getElementById(droppedID);
if (droppedPiece &&
e.target.tagName === "DIV" &&
isValidMove(droppedPiece, e.target, true)) {
// Create a new img on the target location
var newPiece = document.createElement("img");
newPiece.src = droppedPiece.src;
newPiece.id = droppedPiece.id.substr(0, 1) + e.target.id;
newPiece.draggable = droppedPiece.draggable;
if (droppedPiece.draggable) {
newPiece.classList.add("jumpOnly");
}
newPiece.classList.add("piece");
newPiece.addEventListener("dragstart", dragStart, false);
newPiece.addEventListener("dragend", dragEnd, false);
e.target.appendChild(newPiece);
// Remove the previous image
droppedPiece.parentNode.removeChild(droppedPiece);
// Remove the drop effect from the target element
e.target.classList.remove('drop');
// See if the piece needs to be promoted
kingMe(newPiece);
}
}
function dragEnter(e) {
// Get the img element that is being dragged
var dragID = e.dataTransfer.getData("text");
var dragPiece = document.getElementById(dragID);
if (dragPiece &&
e.target.tagName === "DIV" &&
isValidMove(dragPiece, e.target, false)) {
e.target.classList.add('drop');
}
}
function dragLeave(e) {
e.target.classList.remove("drop");
}
function isValidMove(source, target, drop) {
// Get the piece prefix and location
var startPos = source.id.substr(1, 2);
var prefix = source.id.substr(0, 1);
// Get the drop location, strip off the prefix, if any
var endPos = target.id;
if (endPos.length > 2) {
endPos = endPos.substr(1, 2);
}
// You can't drop on the existing location
if (startPos === endPos) {
return false;
}
// You can't drop on occupied square
if (target.childElementCount != 0) {
return false;
}
var jumpOnly = false;
if (source.classList.contains("jumpOnly")) {
jumpOnly = true;
}
// Compute the x and y coordinates
var xStart = parseInt(startPos.substr(0, 1));
var yStart = parseInt(startPos.substr(1, 1));
var xEnd = parseInt(endPos.substr(0, 1));
var yEnd = parseInt(endPos.substr(1, 1));
switch (prefix) {
// For white pieces...
case "w":
if (yEnd <= yStart)
return false; // Can't move backwards
break;
// For black pieces...
case "b":
if (yEnd >= yStart)
return false; // Can't move backwards
break;
}
// These rule apply to all pieces
if (yStart === yEnd || xStart === xEnd)
return false; // Move must be diagonal
if (Math.abs(yEnd - yStart) > 2 || Math.abs(xEnd - xStart) > 2)
return false; // Can't move more than two spaces
if (Math.abs(xEnd - xStart) === 1 && jumpOnly)
return false; // Only jumps are allowed
var jumped = false;
// If moving two spaces, find the square that is jumped
if (Math.abs(xEnd - xStart) === 2) {
var pos = ((xStart + xEnd) / 2).toString() +
((yStart + yEnd) / 2).toString();
var div = document.getElementById(pos);
if (div.childElementCount === 0)
return false; // Can't jump an empty square
var img = div.children[0];
if (img.id.substr(0, 1).toLowerCase() === prefix.toLowerCase())
return false; // Can't jump a piece of the same color
// If this function is called from the drop event
// Remove the jumped piece
if (drop) {
div.removeChild(img);
jumped = true;
}
}
// Set the draggable attribute so the next player can take a turn
if (drop) {
enableNextPlayer(source);
// If we jumped a piece, we're allowed to go again
if (jumped) {
source.draggable = true;
source.classList.add("jumpOnly"); // But only for another jump
}
}
return true;
}
function kingMe(piece) {
// If we're already a king, just return
if (piece.id.substr(0, 1) === "W" || piece.id.substr(0, 1) === "B")
return;
var newPiece;
// If this is a white piece on the 7th row
if (piece.id.substr(0, 1) === "w" && piece.id.substr(2, 1) === "7") {
newPiece = document.createElement("img");
newPiece.src = "../img/WhiteKing.png";
newPiece.id = "W" + piece.id.substr(1, 2);
}
// If this is a black piece on the 0th row
if (piece.id.substr(0, 1) === "b" && piece.id.substr(2, 1) === "0") {
var newPiece = document.createElement("img");
newPiece.src = "../img/BlackKing.png";
newPiece.id = "B" + piece.id.substr(1, 2);
}
// If a new piece was created, set its properties and events
if (newPiece) {
newPiece.draggable = true;
newPiece.classList.add("piece");
newPiece.addEventListener('dragstart', dragStart, false);
newPiece.addEventListener('dragend', dragEnd, false);
var parent = piece.parentNode;
parent.removeChild(piece);
parent.appendChild(newPiece);
}
}
function enableNextPlayer(piece) {
// Get all of the pieces
var pieces = document.querySelectorAll('img');
i = 0;
while (i < pieces.length) {
var p = pieces[i++];
// If this is the same color that just moved, disable dragging
if (p.id.substr(0, 1).toUpperCase() ===
piece.id.substr(0, 1).toUpperCase()) {
p.draggable = false;
}
// Otherwise, enable dragging
else {
p.draggable = true;
}
p.classList.remove("jumpOnly");
}
}
</script>
</body>
第一部分:HTML5 是什么?
第二部分:使用新的 HTML5 特性
第三部分:深入挖掘
第四部分:高级功能
第五部分:附录


浙公网安备 33010602011771号