原文:HTML5 Programming for ASP.NET Developers
协议:CC BY-NC-SA 4.0
十、将 Web 工作器用于网页中的多线程处理
JavaScript 的创造者发明了这种语言作为开发交互式网页的辅助工具。HTML 标记本身本质上是纯静态的,缺乏任何编程能力。JavaScript 的引入弥补了这种可编程性的不足。考虑到当时的需求,JavaScript 被创建为一种简单、轻量级、易于使用的语言。
在早期,JavaScript 主要用于为静态网页添加交互性和精美的图形效果。然而,多年来情况发生了巨大变化。现代 web 应用广泛依赖 JavaScript 来完成各种任务。现代 web 应用的网页不再仅仅使用 JavaScript 来制作精美的悬停效果或动画。它非常依赖 JavaScript 进行业务验证、对服务器进行 Ajax 调用以及特定于业务领域的处理。
使用 JavaScript 的网页在前台执行脚本。这意味着只要脚本运行,用户与页面的交互就会被阻止。换句话说,用户交互和脚本运行在一个线程上。当脚本执行密集且耗时的操作时,这种方法可能会很麻烦。为了克服这个限制,HTML5 和 JavaScript 提供了 Web 工作器 ,允许您在后台运行 JavaScript 处理。本章让你对网络工作者有一个详细的了解。具体来说,您将了解以下内容:
- What is web worker and how it works?
- 网络工作者的风味
- Restrictions on webworkers
- Using web worker to Develop Multithreaded Web Pages
- Communication with server from worker thread
网页多线程概述
如果你曾经用 C#或 Visual Basic 开发过桌面应用,你很可能知道位于System.Threading
名称空间中的Thread
类。Thread
类代表一个线程。一根螺纹是加工的最小单位。当您运行任何应用时,应用代码都在一个线程上运行。线程由操作系统处理,大多数现代编程语言都提供了封装它们的类。一个单线程应用在单线程中运行所有代码(包括用户界面和业务处理)。一个多线程应用在多个线程中运行应用代码。
多线程应用具有以下优势:
- It can significantly improve the user response ability of the application. This advantage is very useful when you are developing web applications. With multithreading, you can keep the user interface responsive when the script runs in the background, instead of your JavaScript code blocking user interaction.
- It can improve the overall performance of your application. However, this advantage depends on several other factors, such as the number of CPUs and whether the code runs locally or remotely. Although the detailed discussion of how multithreading affects the performance of applications is beyond the scope of this book, it can be said that multithreading can improve the performance of applications running on multi-processor machines instead of single-processor machines. This is because multiprocessor machines can run different threads on different processors at the same time, thus improving performance.
现在您已经对单线程和多线程应用有了基本的了解,让我们将这些概念应用到网页中。考虑清单 10-1 中显示的标记。
***清单 10-1。*在单线程中运行 UI 和 JavaScript 的网页
`
…
这个标记由一个按钮和一个单击按钮时执行的 JavaScript 块组成。ready()
函数使用 Modernizr 来检查浏览器是否支持 Web 工作器。这是通过检查 Modernizr 对象的webworkers
属性来完成的。如果浏览器不支持 Web 工作器,则会向用户显示一条错误消息。
按钮的click
事件处理程序首先显示一个警告框来指示处理的开始。do
- while
条件是这样写的,它简单地循环 10 秒。通过计算当前日期时间和循环操作开始时的日期时间之间的差值来实现循环。do
- while
循环旨在模拟冗长的处理过程。在现实世界的应用中,您有实际的业务处理,而不是循环。一旦退出循环,另一个警告框会标记处理结束。
如果您在浏览器中运行此网页并单击按钮,您显然会看到开始和结束警告框;但更重要的是,您会发现当循环运行时,网页的 UI 变得没有响应。在此期间,您的浏览器停止响应键盘输入和鼠标操作,如单击和滚动。这些操作可能会在do
- while
循环结束时排队播放,但只要循环在运行,UI 就没有响应。这个瓶颈的原因是 UI 和 JavaScript 运行在同一个线程上。因此,在任何给定时间,要么脚本可以运行,要么 UI 可以运行。
如果在自己的线程中运行 JavaScript 代码,就可以避免 UI 瓶颈。考虑图 10-1 。
***图 10-1。*网页中的单线程和多线程执行
请看图中所示的单线程执行模型。在这种情况下,同一个线程运行 JavaScript 代码并处理用户交互。现在看看多线程执行模型。这里,UI 由一个专用线程(UI 线程)处理,JavaScript 在它自己的线程中执行。运行 JavaScript 的线程通常被称为工作线程,因为它的主要工作是处理。通常,工作线程不能访问 UI 级元素。然而,它可以从 UI 线程接收输入,并可以将处理结果返回给 UI 线程。在多线程模型中,UI 线程也被称为前台线程,工作线程被称为后台线程。在 HTML5 术语中,工作线程被称为 web 工作线程,由Worker
对象表示。
图 10-1 只显示了一个工作线程,但是你可以生成任意多的线程。但是请记住,每个额外的线程都有开销,最终可能会降低应用的性能。在许多 web 应用中,仅仅一个工作线程就足以满足后台处理需求。
网络工作者的类型
HTML5 Web 工作器 有两种风格:专用的和共享的。专门的网络工作者依附于创造他们的网页。它们随着相关网页的出现而消失。它们的作用域是创建它们的网页。换句话说,专用 Web 工作器 只能由单个网页使用。一个专用的 web worker 由Worker
对象表示。
共享 Web 工作器 是由一个 web 页面创建的,但是一旦创建,它们就可以由同一个 web 应用中的多个 web 页面共享(也就是说,具有相同的来源)。创建 web worker 需要资源。如果您发现自己在 web 应用的许多页面中执行相同的脚本,您可能希望使用共享 Web 工作器,而不是在每个 web 页面中创建一个新线程。然而,与专用的 Web 工作器 相比,它们的编码更复杂,最好只在需要跨多个应用页面共享一个线程的情况下使用。一个共享的 web worker 由SharedWorker
对象表示。
网络工作者可以访问什么,不可以访问什么
尽管 web 工作者通过在单独的线程中运行 JavaScript 处理来解决 UI 阻塞的问题,但他们也需要仔细设计 JavaScript 代码。正如前面提到的,Web 工作器 是工作线程,不能有任何 UI 级的访问。这意味着您不能访问您打算使用 Web 工作器 运行的 JavaScript 代码中的任何 DOM 元素。这意味着以下几点:
- Web 工作器 cannot access HTML elements from the web page.
- Web 工作器 cannot access global variables and JavaScript functions from Web pages.
- Web worker can’t use
alert()
confirm()
and other functions that need users’ attention.- You can’t access
window
,document
,parent
and other objects in web-worker code.- You cannot use jQuery library in web worker code.
你可能想知道为什么 web 工作者不能访问 HTML DOM。这种限制背后的主要原因是这样做不是线程安全的。例如,假设两个 Web 工作器 正在后台运行。如果两者都被允许访问 HTML DOM,那么一个 web worker 可能会意外地覆盖另一个 web worker 所做的更改。在多线程之间没有任何同步的情况下,这样的执行必然会产生不希望的结果。这就是 HTML5 Web 工作器 不允许单个工作线程访问 DOM 元素的原因。当您将数据从网页传递到 web 工作线程时,数据的副本将被发送到工作线程。每个工作线程都维护自己的本地数据。
“禁止 DOM 访问”规则的一个副作用是,您不能在 Web 工作器 中使用 jQuery 库。jQuery 库与 HTML DOM 绑定在一起,允许它将违反前面的规则。这可能有点痛苦,因为即使是像$.ajax()
这样的方法也不能在 Web 工作器 中使用。幸运的是,您可以使用XMLHttpRequest
对象来发出 Ajax 请求。
您可以在 web worker 代码中访问 navigator 和 location 对象。您也可以在 Web 工作器 中使用setTimeout()
、setInterval()
、clearTimeout()
和clearInterval()
方法。
?? 注第十一章中的XMLHttpRequest
对象已涵盖。出于本章的目的,知道XMLHttpRequest
是一个允许您向服务器端资源发出请求(GET/POST)的对象就足够了。不使用$.ajax()
方法,你可以使用XMLHttpRequest
对象来调用 web 方法、MVC 动作方法和通用处理程序。
使用网络工作者
既然你对什么是网络工作者以及他们能做什么有了一个基本的了解,让我们转换来自清单 10-1 的网页,这样它就不会阻塞 UI。这实际上要求在工作线程中运行处理逻辑(do
- while
循环),以释放 UI 线程来处理用户交互。清单 10-2 展示了如何做到这一点。
***清单 10-2。*创建网络工作者
<script type="text/javascript"> var worker; $(document).ready(function () { $("#btnStart").click(function () { worker = new Worker("scripts/processing.js"); worker.addEventListener("message", ReceiveMessageFromWorker, false); worker.postMessage("Hello Worker!"); }); </script>
这段代码在 web 表单中创建了一个全局变量worker
。这个全局变量用于存储您在按钮的click
事件处理程序中创建的 web worker 对象。ready()
函数连接btnStart
的click
事件处理程序。在click
事件处理程序中,代码创建了一个新的Worker
对象。您希望在工作线程中执行的 JavaScript 代码放在一个单独的脚本文件中。包含要在Worker
中执行的 JavaScript 代码的脚本文件(scripts/processing.js
)的路径被传递给构造函数。
虽然不是强制性的,但通常您会对从工作线程接收处理结果感兴趣,以便了解结果。Worker
对象的message
事件让您可以监听工作线程发送的消息。addEventListener()
方法将message
事件连接到ReceiveMessageFromWorker()
函数。ReceiveMessageFromWorker()
是一个开发人员定义的函数,稍后将会讨论。然后代码调用Worker
对象的postMessage()
方法。postMessage()
作为启动工作线程的触发器。您可以将输入数据作为参数传递给它。尽管代码将一个字符串传递给工作线程,但是数据也可以是其他形式,比如 JSON 对象。如果不想传递任何数据,可以简单地传递一个空字符串。
ReceiveMessageFromWorker()
显示工作线程返回的处理结果,如下所示:
function ReceiveMessageFromWorker(evt) { alert(evt.data); }
evt.data
属性返回工作线程发送的处理结果。实际的处理(do
- while
循环)现在驻留在Processing.js
脚本文件中(参见清单 10-3 )。
***清单 10-3。*在工作线程中运行的代码
`addEventListener(“message”, ReceiveMessageFromPage, false);
function ReceiveMessageFromPage(evt) {
var date = new Date();
var currentDate = null;
do {
currentDate = new Date();
}
while (currentDate - date < 10000);
postMessage("Page said : " + evt.data + “\r\n Worker Replied : Hello Page!” );
}`
之前,您连接了 web 页面的message
事件处理程序来接收工作线程发送的消息。同样,您需要为Worker
连接message
事件处理程序,以接收 web 表单发送的消息。清单 10-3 中的addEventHandler()
调用完成了这项工作。当 web 表单调用Worker
对象上的postMessage()
方法时,引发工作线程的message
事件。在这种情况下,Worker
的message
事件由ReceiveMessageFromPage()
函数处理。
ReceiveMessageFromPage()
包含与之前相同的do
- while
循环。但是,请注意,没有alert()
调用,因为工作线程不能进行用户交互。一旦do
- while
循环完成,工作线程调用postMessage()
将处理结果发送回 web 表单。注意 web 表单发送的数据(“Hello Worker!”)是使用evt.data
属性在ReceiveMessageFromPage()
中检索的。
图 10-2 显示了一个网络表单的运行示例。
图 10-2。 Processing.js
运行中的一个工作线程
web 表单传递字符串“Hello Worker!”到工作线程。工作线程通过在evt.data
值前面加上“Page said:”和后缀“Worker reply:Hello Page!”来形成一个字符串。然后,该字符串被返回到 web 表单。web 表单中编写的ReceiveMessageFromWorker()
函数在一个alert()
框中显示返回值。使用 web worker 对象,当您单击“开始工作”按钮时,UI 不会被阻止。您可以自由地与页面交互,因为处理是在后台进行的。当处理完成时,会通知您并显示处理结果。
为了使 web 表单和工作线程之间的交互更加清晰,图 10-3 以图示的形式显示了该应用的工作。
***图 10-3。*网络工作者执行的图示
如图所示,执行顺序如下:
- The user clicks the start worker button.
postMessage()
is a form of address, and “Hello worker!” Sent to the worker thread.- The worker’s
message
event is raised, and theReceiveMessageFromPage()
function handles it.postMessage()
Called, and “Hello page!” Is returned to the page.- The
message
event of the page is triggered and handled by theReceiveMessageFromWorker()
function.- A warning box is displayed to the user.
导入外部脚本文件
在前面的示例中,整个处理逻辑包含在一个文件中。有时,您的处理逻辑可能驻留在不同的脚本文件中。在一个普通的网页中,一个或多个<script>
标签指向文件并使用它们的功能。然而,同样的技巧对 web worker 不起作用,因为 web worker 不能访问任何 DOM 元素,包括<script>
。
Web 工作器 有一个替代方案:importScripts()
函数。您在打算在工作线程上运行的脚本文件中使用了importScripts()
。importScripts()
函数获取您希望引用的脚本文件的逗号分隔列表,并以同步方式将它们导入当前文件。调用importScripts()
时,文件按您指定的顺序执行。以下代码显示了importScripts()
的一个示例用法:
importScripts("Helper1.js"); importScripts("Helper1.js","Helper2.js","Helper3.js");
对importScripts()
的第一次调用导入了Helper1.js
文件。第二个调用导入三个文件:Helper1.js
、Helper2.js
和Helper3.js
。第二个调用中的脚本按照指定的顺序执行。
处理错误
通常,web 工作人员会涉及到冗长的处理过程,有时他们可能会遇到意想不到的错误。因为 web 工作者不能访问 DOM,所以他们不能通过显示对话框或警告来报告错误。工作线程中任何未处理的错误都会引发错误事件。web 页面可以将一个事件处理程序连接到 web worker 对象的error
事件,并在出现任何错误时得到通知。清单 10-4 展示了这是如何做到的。
***清单 10-4。*处理错误
`$(“#btnStart”).click(function () {
worker = new Worker(“scripts/Processing.js”);
worker.addEventListener(“message”, ReceiveMessageFromWorker, false);
** worker.addEventListener(“error”, HandleError, false);**
worker.postMessage(“Hello Worker!”);
});
function HandleError(evt) {
var msg=“There was an error in the worker thread!\r\n”;
msg += "Message : " + evt.message + “\r\n”;
msg += "Source : " + evt.filename + “\r\n”;
msg += "Line No. : " + evt.lineno;
alert(msg);
}`
注意粗体显示的代码行。addEventListener()
方法将一个事件处理函数HandleError()
附加到error
事件上。HandleError()
函数接收一个ErrorEvent
对象。ErrorEvent
— message
、filename
和lineno
的三个属性给出了错误消息、发生错误的文件名以及发生错误的行号。图 10-4 显示了HandleError()
事件处理函数的运行示例。
**图 10-4。**通过处理error
事件显示错误详情
警告框显示错误消息“未捕获的意外错误!!!"。它还告诉用户错误的来源是Processing.js
,第 6 行有问题。
终止网络工作者
当您在一个 web worker 上触发一个冗长的操作时,您从 worker 线程那里听到反馈的唯一方式是当它使用postMessage()
方法向您发送处理结果时,或者当在 worker 线程的执行过程中发生错误时。如果在工作线程上执行的操作是长时间运行的,您可能希望允许用户在操作完成之前取消该操作。对象的terminate()
方法允许你这样做。下面这段代码展示了如何使用terminate()
:
$("#btnStop").click(function () { worker.terminate(); });
这段代码处理一个按钮(btnStop
)的click
事件,并通过调用terminate()
取消在工作线程上运行的操作。一旦 web worker 被终止,您就不能重用或重新启动它。重启操作的唯一方法是创建一个新的Worker
对象。
在开发过程中监控网络工作者
作为一名 ASP.NET 开发人员,您使用Worker
对象与 web 工作人员进行交互。你不需要知道浏览器如何创建和管理线程的内部细节。但是,在开发和测试阶段,您可能希望看到使用 Web 工作器 对浏览器创建的线程数量的影响。如果是这样,您可以使用 Windows 资源监视器来查看浏览器的内部线程处理。您可以从“任务管理器”对话框的“性能”选项卡中访问资源监视器。
图 10-5 显示了当你之前开发的 web 表单加载到 Chrome 中时,资源监视器中Chrome.exe
的条目。
***图 10-5。*资源监视器显示线程利用率
如你所见,在点击开始工作按钮之前,Chrome.exe
的两个实例的线程数分别是 29(主进程)和 6(单个选项卡)。当单击开始工作按钮时,线程利用率分别变为 29 和 7。正如您可能猜到的,由于 web worker 的创建,线程数从 6 增加到 7。然而,当 web worker 完成时,计数不会回到 6:虽然操作已经完成,但是 web worker 仍然是活动的,并且将在页面关闭时被回收。如果您单击调用terminate()
方法的 Stop Work 按钮,线程计数会回到 6,因为您显式地终止了线程。如果您多次单击“开始工作”按钮,而没有先终止前一个工作线程,线程计数会不断增加,这表明浏览器没有立即回收前一个线程。
注意之前的线程数(29 和 6)取自 web 表单的一次样本运行。这些值可能因浏览器而异,甚至在同一浏览器上多次运行时也会有所不同。重点是提供关于浏览器如何创建和销毁工作线程的见解。
使用共享网络工作者
创建和管理工作线程是一项资源密集型操作。正如您在上一节中观察到的,专门的 Web 工作器 是基于每个页面创建的。如果您的 web 应用的许多页面需要执行相同的脚本,那么使用专用的 Web 工作器 在线程利用和内存方面可能会很昂贵。例如,如果您在三个浏览器选项卡中打开三个网页,并且每个都创建一个Worker
对象,那么就会创建三个工作线程,自然资源消耗会很高。在这种情况下,您可以使用由SharedWorker
对象表示的共享 Web 工作器。
顾名思义,共享 Web 工作器 跨 web 应用的多个页面共享一个工作线程。例如,假设您有三个网页页面 1、页面 2 和页面 3。他们都希望执行同一个脚本文件SharedProcessing.js
。让我们进一步假设在浏览器中加载了 Page1,并且为运行SharedProcessing.js
创建了一个SharedWorker
的实例。稍后,页面 2 被加载到浏览器中,并试图创建一个SharedWorker
实例来运行SharedProcessing.js
。但是,因为 Page1 已经创建了一个用于处理SharedProcessing.js
的SharedWorker
,Page2 没有创建新的工作线程;相反,它使用之前创建的相同的SharedWorker
来完成工作。如果在浏览器中加载了第 3 页,则遵循相同的过程。只有当所有使用共享 web worker 的连接都关闭时,共享 web worker 才会被回收。
虽然使用共享的 Web 工作器 节省了系统资源,但缺点是它们比专用的 Web 工作器 编码稍微复杂一些。为了理解共享 Web 工作器 是如何编程的,让我们修改前面的例子,使用SharedWorker
对象。
清单 10-5 展示了如何在 web 表单中创建一个SharedWorker
对象。
***清单 10-5。*创建一个SharedWorker
对象
<script type="text/javascript"> var worker; $(document).ready(function () { $("#btnStart").click(function () { worker = new SharedWorker("scripts/SharedProcessing.js"); worker.port.addEventListener("message", ReceiveMessageFromWorker, false); worker.port.start(); worker.port.postMessage("Hello Shared Worker!"); }); });
... </script>
这段代码创建了一个名为worker
的全局变量来保存SharedWorker
引用。ready()
函数连接开始工作按钮的click
事件处理程序。在click
事件处理程序中,创建了一个SharedWorker
的实例。这个共享 web worker 应该运行的 JavaScript 文件路径(SharedProcessing.js
)在构造函数中传递。
在使用Worker
对象的情况下,会在该对象上引发message
事件。然而,在SharedWorker
的例子中,message
事件是在SharedWorker
实例的port
对象上引发的。这就是为什么在port
对象上调用addEventListener()
方法的原因。addEventListener()
将事件处理函数ReceiveMessageFromWorker()
连接到message
事件。接下来,您需要调用port
对象的start()
方法来启动那个端口。最后,在port
对象上调用postMessage()
,并向共享工作线程发送一个字符串消息。充当message
事件处理程序的ReceiveMessageFromWorker()
函数如下:
function ReceiveMessageFromWorker(evt) { alert(evt.data); }
这个函数只是在一个警告框中显示由共享工作线程发回的数据。
这就完成了 web 窗体级代码。现在让我们看看SharedProcessing.js
文件中的内容(清单 10-6 )。
***清单 10-6。*运行在共享 Web Worker 中的代码
`var port;
addEventListener(“connect”, ReceiveMessageFromPage, false);
function ReceiveMessageFromPage(evt) {
port = evt.ports[0];
port.addEventListener(“message”, SendMessageToPage, false);
port.start();
}
function SendMessageToPage(evt) {
var date = new Date();
var currentDate = null;
do {
currentDate = new Date();
}
while (currentDate - date < 10000);
port.postMessage("Page said : " + evt.data +
“\r\n Shared Worker Replied : Hello Page!”);
}`
这段代码首先创建一个名为port
的全局变量来保存对一个port
对象的引用。每当一个新的SharedWorker
对象获得对共享工作线程的引用时,就会引发connect
事件。要从存在SharedWorker
对象的 web 表单接收任何消息,您需要处理这个事件。addEventListener()
方法将一个事件处理函数ReceiveMessageFromPage()
附加到connect
事件。
ReceiveMessageFromPage()
事件处理函数使用evt.ports[0]
获取传入通信的port
对象。虽然看起来ports
数组可能有不止一个 port
对象,但是在当前共享 Web 工作器 的实现中,ports
数组中总是只有一个端口;因此,您在数组索引 0 处访问了port
对象。
addEventListener()
方法为port
对象的message
事件连接了一个事件处理函数SendMessageToPage()
。port
对象的start()
方法启动通信通道。
注意虽然在这个例子中没有用到,port
对象也有一个close()
方法。此方法关闭一个端口并停止该端口上的任何进一步通信。
SendMessageToPage()
事件处理函数包含与之前相同的处理逻辑(do
- while
循环)。为了将处理结果发送回 web 表单,在port
对象上调用postMessage()
。使用evt.data
属性检索 web 表单发送的字符串消息,并向其追加额外的字符串数据。
如果您运行 web 表单并点击开始工作按钮,您应该会看到如图图 10-6 所示的警告框。
***图 10-6。*行动中的共享网络工作者
图 10-6 显示共享 web worker 正在正确执行脚本。如果您希望看到使用共享 Web 工作器 对线程利用率的影响,打开资源监视器,像以前一样查看Chrome.exe
的条目(图 10-7 )。
图 10-7 显示了当 web 表单在两个 Chrome 标签中打开时的资源监视器。对于Chrome.exe
,总共有四个活动条目。有 29 个线程的是主浏览器窗口。每个打开的标签需要 6 个线程。因此,这两个 6 个线程的条目分别用于加载 web 表单的两个选项卡。第一次单击“开始工作”按钮时,会创建另一组 6 个线程;它们用于运行共享的 Web 工作器。如果多次单击 Start Work,线程数不会进一步增加,因为先前创建的共享工作线程会被后续请求重用。
注意在共享 web worker 中运行的脚本的后续执行获得全局变量的先前状态。此外,如果一个共享 web worker 已经被一个页面使用,其他对访问同一个共享 web worker 感兴趣的页面需要等待,直到当前处理完成。
***图 10-7。*使用资源监视器观察共享工作线程
与服务器通信
Web 工作者可能需要向服务器端资源发出请求才能正常工作。例如,在 web worker 中运行的脚本可能需要检索驻留在 SQL Server 数据库中的数据。为了满足这个需求,您可能会立即想到使用 jQuery $.ajax()
方法。毕竟,在本书中,您一直在使用 jQuery 对服务器进行 Ajax 调用。但是,您不能将 jQuery 用于 Web 工作器,因为 jQuery 需要 DOM 访问。如前所述,web 工作者不能访问任何 DOM 元素。因此,如果您希望对服务器进行 Ajax 调用,您需要使用XMLHttpRequest
对象。
清单 10-7 使用XMLHttpRequest
来访问服务器端资源。
***清单 10-7。*使用XMLHttpRequest
对象进行 Ajax 调用
function GetOrders(customerId) { var xhr = new XMLHttpRequest(); xhr.open("POST", "/Home/GetOrders"); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.onreadystatechange = function () { if (xhr.readyState == 4) { postMessage(xhr.responseText); } } var param = '{ "customerid": "' + customerId + '"}'; xhr.send(param); }
这段代码显示了一个 JavaScript 函数——GetOrders()
——用于从 SQL Server 中检索特定客户的订单数据。GetOrders()
函数接受一个要检索其订单的客户 ID。
在内部,该函数创建了一个新的XMLHttpRequest
实例。XMLHttpRequest
对象的open()
方法通过远程请求打开一个通信通道。open()
方法的第一个参数是请求的类型(GET/POST ),第二个参数是远程资源的位置。在这个例子中,一个 POST 请求被发送给一个名为GetOrders
的 MVC 动作方法。XMLHttpRequest
对象的setRequestHeader()
方法将请求的 MIME 内容类型设置为application/json
。
当请求的状态改变时,XMLHttpRequest
对象引发onreadystatechange
事件。onreadystatechange
事件处理函数检查XMLHttpRequest
对象的readyState
属性。readyState
对象指示请求的当前状态,可以有从0
到4
的值。值4
表示请求已完成。如果请求完成,您可以使用postMessage()
方法将远程资源返回的数据发送回 web 页面。XMLHttpRequest
对象的responseText
属性返回远程资源的响应。在这种情况下,假设 MVC 动作方法返回 JSON 格式的数据,因此responseText
被直接发送到 web 页面。
要发起请求,您需要调用XMLHttpRequest
对象的send()
方法。send()
接受请求参数,如果有的话。因为您希望以 JSON 格式发送请求数据,所以您将客户 ID 转换为 JSON 对象,然后将其作为参数传递给send()
。
使用需要服务器端数据的网络工作者
在最后一个例子中,您开发了一个 ASP.NET MVC 应用,它使用 Web 工作器 从服务器获取数据并在视图中显示出来。应用的主视图如图 10-8 所示。
***图 10-8。*订单历史应用
订单历史应用允许用户指定客户 ID 和日期范围。然后,它从 Northwind 数据库的Orders
和Order Details
表中获取所有现有的订单数据,并将它们返回给视图。Orders
和Order Details
表的实体框架数据模型如图图 10-9 所示。
***图 10-9。*订单表实体框架数据模型
虽然Order
和Order_Detail
类有几个属性。出于这个例子的目的,您只使用了其中的一些:OrderID
、CustomerID
、OrderDate
、Quantity
和UnitPrice
。JavaScript 代码中会计算订单的总金额。
订单历史视图
该视图在 HTML 表格中显示订单信息。从 SQL Server 数据库获取Order
记录的任务是使用 web worker 完成的。视图中的 HTML 标记如清单 10-8 所示。
***清单 10-8。*订单历史视图的 HTML 标记
<form id="form1" runat="server"> <h3>Order History</h3> <div>Customer ID :</div> <input id="customerID" type="text" />
`
** **
End Date :
** **
订单历史视图由一个指定客户 ID 的文本框和两个日期字段组成。注意粗体标记:接受日期范围的<input>
字段属于类型date
。这样,能够显示弹出日期选择器的浏览器可以相应地呈现字段。单击 Get Orders 按钮触发 web worker 操作。返回的订单详细信息显示在orderTable
HTML 表中。
创建网络工作者
输入客户 ID 和选择Order
记录的日期范围后,用户单击 Get Orders 按钮。负责创建 web worker 和执行所需脚本的 jQuery 代码包含在该按钮的click
事件处理程序中;参见清单 10-9 。
***清单 10-9。*创建一个 Web Worker 来获取订单记录
var worker; $(document).ready(function () { … $("#getOrders").click(function () { $(this).attr("disabled", "disabled"); $(this).val("Wait..."); worker = new Worker("../../scripts/Processing.js"); worker.addEventListener("message", ReceiveDataFromWorker, false); worker.addEventListener("error", HandleError, false); var settings = { "CustomerID": $("#customerID").val(), "StartDate": $("#startDate").val(), "EndDate": $("#endDate").val() }; worker.postMessage(settings); }); });
代码首先声明一个全局变量(worker
)来存储对 web worker 的引用。ready()
函数连接了获取订单按钮的click
事件处理程序。click
事件处理函数通过使用 jQuery attr()
方法向按钮添加一个disabled
属性来禁用按钮。这样做是为了防止用户多次单击按钮,从而创建多个工作线程。为了提示用户处理正在进行,使用val()
方法将按钮的值更改为“Wait…”。
然后实例化一个新的Worker
对象。从服务器获取数据的 JavaScript 代码驻留在Processing.js
中,脚本文件的路径在创建Worker
对象时指定。然后使用addEventListener()
方法附加message
和error
事件的事件处理函数。简单讨论一下各自的事件处理函数ReceiveDataFromWorker()
和HandleError()
。
视图需要向工作线程传递三条信息:要获取其订单详细信息的客户 ID、开始日期和结束日期。postMessage()
方法只能将一个参数传递给工作线程。因此,这些数据被分组到一个 JSON 对象中,该对象有三个属性:CustomerID
、StartDate
和EndDate
。
为了开始处理,调用了Worker
对象的postMessage()
方法,并将 JSON settings
对象作为参数传递给它。
消息和错误事件的事件处理程序
ReceiveDataFromWorker()
事件处理函数接收工作线程返回的订单数据。该功能如清单 10-10 中的所示。
***清单 10-10。*接收并显示订单明细
function ReceiveDataFromWorker(evt) { var data = evt.data; $("#orderTable").empty(); $("#orderTable").append("<tr><th>Order ID</th><th>Customer ID</th> <th>Order Date</th><th>Order Amount</th></tr>"); for (var i = 0; i < data.length; i++) { $("#orderTable").append("<tr>" + "<td>" + data[i].OrderID + "</td>" + "<td>" + data[i].CustomerID + "</td>" + "<td>" + ToJSDate(data[i].OrderDate) + "</td>" + "<td>" + data[i].OrderAmount + "</td>" + "</tr>"); } $("#getOrders").removeAttr("disabled"); $("#getOrders").val("Get Orders"); }
从工作线程接收订单数据作为数组。数组的每个元素代表一个订单,并具有属性:OrderID
、CustomerID
、OrderDate
和OrderAmount
。属性让你可以访问这个数组。
清空orderTable
HTML 表以删除任何先前添加的行。一个for
循环遍历 JSON 集合。随着每次迭代,一行被添加到表中,并且显示OrderID
、CustomerID
、OrderDate
和OrderAmount
属性值。
注意,与OrderID
和CustomerID
不同,OrderDate
并不直接显示在表格中。OrderDate
值首先被传递给一个助手函数—ToJSDate()
——ToJSDate()
的返回值显示在表格中。OrderDate
列是数据库中的日期时间列,但是 JSON 格式没有日期数据类型。因此,当日期从服务器序列化到客户机时,它们以一种特殊的格式发送。例如,如果数据库中的OrderDate
是8/12/1996
(mm/dd/yyyy),那么 JSON 等价为/Date(839788200000)/
。Date()
中的数字是该日期与 1970 年 1 月 1 日午夜之间的毫秒数。ToJSDate()
助手函数将这个看起来神秘的值转换成可读的形式。ToJSDate()
如清单 10-11 所示。
***清单 10-11。*将 JSON 日期值转换成 JavaScript 日期
function ToJSDate(value) { var pattern = /Date\(([^)]+)\)/; var results = pattern.exec(value); var dt = new Date(parseFloat(results[1])); return (dt.getMonth() + 1) + "/" + dt.getDate() + "/" + dt.getFullYear(); }
ToJSDate()
使用正则表达式和 JavaScript exec()
方法。exec()
在正则表达式上被调用并接受一个参数。它根据正则表达式测试指定的值,并返回匹配的文本。然后构造一个新的 JavaScript Date
对象,并将 mm/dd/yyyy 格式的日期字符串返回给调用者。
注意 JSON 没有特定的格式来表示日期和时间。当您在客户机和服务器之间序列化和反序列化日期时间数据时,这会带来困难。最近的趋势是在网络上使用 ISO 日期和时间格式(例如,Json.NET 和 Web API 使用这种格式)。在 ISO 格式中,日期和时间表示如下:YYYY-MM-DDThh:mm:ssTZD(例如,2010-08-20T19:20:10+01:00)。
在订单细节显示在 HTML 表中之后,通过删除之前添加的disabled
属性,按钮再次被激活。按钮值再次设置为获取订单。
error
事件的事件处理程序只是在一个警告框中显示一条错误消息,如下所示:
function HandleError(evt) { var msg="There was an error in the worker thread!\r\n\r\n"; msg += "Message : " + evt.message + "\r\n"; msg += "Source : " + evt.filename + "\r\n"; msg += "Line No. : " + evt.lineno; alert(msg); }
HandleError()
函数使用ErrorEvent
对象的三个属性:message
、filename
和lineno
显示错误消息、源文件名和错误发生的行。
Web Worker 中运行的代码
现在您已经完成了视图的编码,让我们转到由 web worker 执行的Processing.js
文件。来自Processing.js
的 JavaScript 使用XMLHttpRequest
对象从服务器检索订单数据。清单 10-12 显示了Processing.js
的内容。
***清单 10-12。*使用XMLHttpRequest
从服务器检索数据
`addEventListener(“message”, ReceiveMessageFromPage, false);
function ReceiveMessageFromPage(evt) {
GetOrders(evt.data);
}
function GetOrders(settings) {
var xhr = new XMLHttpRequest();
xhr.open(“POST”, “/Home/GetOrders”);
xhr.setRequestHeader(‘Content-Type’, ‘application/json’);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
var data = JSON.parse(xhr.responseText);
var orderAmount = 0;
var finalData = new Array();
var currOrderId = data[0].OrderID;
for (var i = 0; i < data.length; i++) {
if (data[i].OrderID != currOrderId || i == (data.length - 1)) {
if (i == (data.length - 1)) {
orderAmount += (data[i].Quantity * data[i].UnitPrice);
}
finalData.push({ CustomerID: data[i].CustomerID,
OrderID: currOrderId,
OrderAmount: orderAmount,
OrderDate: data[i].OrderDate });
currOrderId = data[i].OrderID;
orderAmount = 0;
}
orderAmount += (data[i].Quantity * data[i].UnitPrice);
}
postMessage(finalData);
}
}
var param = JSON.stringify(settings);
xhr.send(param);
}`
这段代码为 web worker 的message
事件附加了一个事件处理程序。事件处理函数ReceiveMessageFromPage()
调用另一个函数GetOrders()
。回想一下,视图将包含客户 ID 和日期范围的 JSON 对象传递给工作线程。这个 JSON 对象是使用evt.data
属性获得的,并被传递给GetOrders()
函数。
GetOrders()
创建一个XMLHttpRequest
的新实例,并用控制器动作方法(Home/GetOrders
)打开一个 POST 请求。因为作为请求的一部分发送的数据是 JSON 格式的,所以Content-Type
头被设置为application/json
。
onreadystatechange
事件处理函数检查XMLHttpRequest
对象的readyState
属性。如果readyState
是4
(意味着请求完成),它处理返回的数据。使用JSON.parse()
方法将responseText
属性的值转换成 JSON 格式。一个for
循环遍历订单数据,并计算每个订单的总金额。它通过将Quantity
和UnitPrice
值相乘来实现。
根据处理结果,用四个属性构造一个 JSON 对象:CustomerID
、OrderID
、OrderDate
和OrderAmount
。这个订单数据使用push()
方法存储在一个数组finalData
中。最后,在 web worker 上调用postMessage()
方法,将finalData
数组发送到页面。在发起请求之前,使用JSON.stringify()
方法将 JSON 设置转换成对应的字符串。send()
方法发出请求,并将设置传递给GetOrders()
动作方法。
GetOrders()操作方法
GetOrders()
动作方法驻留在 HomeControllerand 中,返回特定客户 ID 和日期范围的订单数据。GetOrders()
如清单 10-13 所示。
清单 10-13。 GetOrders()
动作方法
[HttpPost] public JsonResult GetOrders(OrderSettings settings) {
` NorthwindEntities1 db = new NorthwindEntities1();
var data = from item in db.Orders
join item2 in db.Order_Details on item.OrderID
equals item2.OrderID
where
item.CustomerID == settings.CustomerID
&&
item.OrderDate >= settings.StartDate
&&
item.OrderDate <= settings.EndDate
orderby item.OrderID ascending
select new { item.CustomerID, item.OrderID,item.OrderDate,
item2.UnitPrice, item2.Quantity };
return Json(data.ToArray());
}`
GetOrders()
接受一个类型为OrderSettings
的参数。OrderSettings
类的属性与客户端发送的 JSON 对象的属性相匹配。这样,ASP.NET MVC 自动将传入的 JSON 对象映射到OrderSettings
类。OrderSettings
看起来是这样的:
public class OrderSettings { public string CustomerID { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } }
然后,LINQ 到实体查询选择所有与CustomerID
匹配且在指定日期范围内的Order
和Order Details
记录。该查询返回一个自定义类型,其中包含来自Orders
表的CustomerID
、OrderID
和OrderDate
属性,以及来自Order Details
表的Quantity
和UnitPrice
属性。GetOrders()
的返回类型为JsonResult
。Json()
方法将查询结果转换成等效的 JSON 格式。
就这样!您现在可以运行订单历史视图并测试其功能。
注意在测试订单历史应用时,如果数据库在本地机器上运行,您可能会发现工作线程完成得太快,而且没有锁定 UI。为了测试和确认 UI 没有被阻塞,您可以故意在工作线程中引入一些延迟。作为添加引入延迟的虚拟do
- while
循环的替代方法,您可以使用setTimeout()
函数调用postMessage()
方法,就像这样:var t = setTimeout(postMessage, 10000, xhr.responseText);
。
总结
传统的 JavaScript 代码运行在与 UI 相同的线程中。由于这种单线程的特性,当 JavaScript 代码被执行时,用户界面被阻塞。HTML5 Web 工作器 为 JavaScript 添加了多线程功能。使用 Web 工作器,您可以在后台执行脚本文件。这样,在脚本运行时,用户界面不会被阻塞。当一个脚本很长且运行时间很长时,Web 工作器 会是一个很大的帮助。 Web 工作者分为两种类型:专用的和共享的,分别由Worker
和SharedWorker
对象表示。网络工作者没有访问 HTML DOM 的权限。使用 Web 工作器,您可以开发响应用户需求的网页,并改善整体用户体验。
在本章中,您看到了受益于新的多线程特性的 web 应用。下一章将介绍更多允许客户机-服务器通信以及跨域通信的特性。
十一、使用通信 API 和 WebSocket
到目前为止,在本书中,您已经了解了不需要服务器端通信的 HTML5 特性。尽管您使用了 jQuery $.ajax()
等技术在客户机和服务器之间发送和接收数据,但这样做并不是正在讨论的 HTML5 特性的一个组成部分。但是,在本章中,您将了解一些 HTML5 特性,这些特性是专门为促进客户端浏览器和服务器之间的通信而设计的。使用这些功能,您可以在来自同一 web 应用或不同应用的网页之间传递数据。此外,这些技术中的一些提供单向(客户端到服务器)通信,而另一些提供双向(客户端到服务器和服务器到客户端)通信。具体来说,您将了解以下内容:
- Cross-document messaging and cross-source resource sharing (CORS)
- Use the postMessage API to send data from different web applications to documents
- Make
GET
andPOST
requests with the newXMLHttpRequest
level 2 function.- Notify clients with events sent by the server
- Use Web sockets to perform two-way communication.
了解跨领域沟通
web 应用通常需要执行以下两种通信之一:
- A web page may want to communicate with another web page from the same web application.
- A web page may want to communicate with another web page belonging to another web application.
执行第一种类型的通信相对简单,因为浏览器对这种通信没有施加任何限制。此外,基于 JavaScript 的库(如 jQuery)很容易用于执行这种类型的通信。然而,当您希望从不同的 web 应用与网页进行通信时,事情就变得棘手了。
实现第二种通信的主要问题是,出于安全考虑,所有浏览器都禁止所谓的跨域通信(??)。如果允许跨域通信,恶意网页可能会利用这一特性对您的 web 应用造成安全威胁。
如果参与通信的双方的来源不同,则称通信为跨域。一个源由一个方案、一个主机和一个端口组成。比如考虑原点[
www.domain1.com](http://www.domain1.com)
。在这个 URL 中,方案是http
,主机是[www.domain1.com](http://www.domain1.com)
,没有指定显式端口。如果两个 URL 没有相同的来源,它们就不能互相通信。所以,[
www.domain1.com/Page1.aspx](http://www.domain1.com/Page1.aspx)
和[
www.domain2.com/Page2.aspx](http://www.domain2.com/Page2.aspx)
不能交流,因为它们的宿主不一样。同一行上,[
www.domain1.com](https://www.domain1.com)
和[
www.domain1.com](http://www.domain1.com)
因为方案不同(http
和https
),所以被认为是不同的原点。但是,[
www.domain1.com/Page1.aspx](http://www.domain1.com/Page1.aspx)
和[
www.domain1.com/Page2.aspx](http://www.domain1.com/Page2.aspx)
属于同源。
仅仅因为跨域通信会带来安全威胁,并不意味着它在任何情况下都不合适。假设你正在建立一个网站网络。每个网站都是独立的,为用户提供不同的内容。然而,网站网络的成员可能想要与网络中的其他网站共享一些特征(例如,成员聊天)。这是一个跨领域交流的真实案例。幸运的是,HTML5 理解当今开发人员在实现跨域通信时所面临的问题,并为此提供了两种方法:
- Cross-document messaging [CORS]
让我们更详细地看看这些。
跨文档消息传递
跨域消息是指属于不同来源的两个或多个网页之间的通信,其中一个网页嵌入或打开另一个网页。考虑这样一种情况,WebSite1 的网页声明了一个<iframe>
,并在这个<iframe>
中嵌入了 WebSite2 的网页。当网站 1 的网页使用window.open()
方法打开网站 2 的网页时,也会出现类似的情况。在这两个例子中,两个网页属于不同的来源。如果他们希望彼此交流,在 HTML5 之前没有简单的标准方法。
HTML5 借助 postMessage API 促进了跨文档消息传递。这是一种标准的方法,支持跨<iframe>
元素、选项卡和窗口的安全跨源通信。要使用 postMessage API 启用跨文档消息传递,您不需要在服务器端做任何配置。postMessage API 是安全的,不会造成安全威胁,因为您需要在 web 应用中显式地接收消息。从另一个来源的页面接收请求的网站必须明确地提供特定的页面来接受跨文档请求。因此,即使有人向您发送恶意脚本或数据,也不会有任何损害,除非您明确允许并接收这些数据。当您开发一个使用 postMessage API 的示例应用时,您会更好地理解这一点。
注意就语法而言,本章讨论的 postMessage API 与 Web 工作器 有着惊人的相似之处。这是因为他们使用相同的 HTML5 消息系统。然而,Web 工作器 的用途与 postMessage API 完全不同。
跨产地资源共享(CORS)
在跨文档消息传递期间,一个文档需要一个句柄到另一个文档。这个句柄通常是一个<iframe>
或窗口对象的形式。然而,在许多实际情况下,您只是想提出一个跨域的GET
或POST
请求。跨文档消息传递不允许您这样做。在这种情况下,你需要的是跨产地资源共享(CORS)。
与 postMessage API 不同,要启用 CORS,您只需在 web 服务器端进行少量配置。所有跨域请求都有一个源头。这个头由浏览器添加,并向 web 服务器提供请求源。应用代码不能篡改头。要接受来自不同来源的请求,应该将 web 服务器配置为具有 Access-Control-Allow-Origin HTTP 头。您可以使用 IIS 管理器或web.config
添加该标题。图 11-1 显示了 IIS 管理器对话框,您可以在其中添加 Access-Control-Allow-Origin 头。
***图 11-1。*使用 IIS 管理器添加 Access-Control-Allow-Origin 报头
Access-Control-Allow-Origin 头的值可以是*
(允许所有域)或特定域的列表。您可以使用web.config
文件达到同样的效果,如清单 11-1 所示。
***清单 11-1。*使用web.config
添加访问控制允许起源报头
<system.webServer> <httpProtocol> <customHeaders> <add name="Access-Control-Allow-Origin" value="*" /> </customHeaders> </httpProtocol> </system.webServer>
web.config
的<customHeaders>
部分允许您添加自定义标题。在这种情况下,添加了 Access-Control-Allow-Origin 头,其值被设置为*
,表示所有域都被允许。
为了实现 CORS 通信,HTML5 增强了XMLHttpRequest
对象(这些改进统称为 Level 2 ),以便可以进行跨域 HTTP 请求。
现在您已经对跨文档消息传递和 CORS 有了基本的了解,让我们来看看这些技术的代码级细节。
注意从另一个来源获取数据而不使用任何特定技术(如跨文档消息传递或 CORS)的常见方法是 JSONP: JSON with Padding。JSONP 的工作原理是,即使浏览器不允许您进行跨域请求,它们也允许您使用指向远程资源的<script>
标签。在这种技术中,脚本 URL 还在查询字符串中指定回调函数的名称。远程资源不是返回 JSON 数据,而是返回对这个回调函数的函数调用,并将 JSON 数据作为参数。这种技术的缺点是您必须信任远程服务器,这会带来安全威胁。
使用 postMessage API
为了使用 postMessage API,您需要一个目标文档窗口的句柄。目标文档接收主网页发送的数据。可选地,目标文档可以向主网页返回一个值,以指示正在执行的操作的结果。要获得此句柄,您可以使用两种常用技术中的任意一种:
- Use the
<iframe>
element in the main webpage and load the target webpage in<iframe>
. Then, you can use thecontentWindow
attribute of the<iframe>
DOM element to get the reference of the target window.- Use the
window.open()
method in the main webpage. This method returns a reference to the target window object.
使用带有< iframe 的 postMessage】
使用带有<iframe>
的 postMessage API 包括使用<iframe>
元素将目标 web 页面嵌入到主页面中,然后使用postMessage()
方法将数据发送到目标页面。清单 11-2 显示了带有<iframe>
元素的主 web 表单。
***清单 11-2。*使用<iframe>
元素嵌入目标 Web 表单
`
Data Received from Target Web Form :
Target Page in IFRAME
`
这个清单显示了主 web 表单(Iframe.aspx
)的标记,它使用一个<iframe>
元素嵌入了目标 web 表单(Target.aspx
)。注意,为了测试起见,Iframe.aspx
和Target.aspx
是在两个独立的 web 应用中创建的。虽然这不会使它们在真正意义上跨域,但它们仍然是跨源的,因为 Visual Studio 开发 web 服务器为不同的 web 应用分配了不同的端口号。
主 web 表单包含一个文本框,用于接受要发送到目标 web 表单的数据。Send 按钮触发一些 jQuery 代码,这些代码实际上将数据发送到目标 web 表单。<iframe>
元素的src
属性指向[
localhost:1052/Target.aspx](http://localhost:1052/Target.aspx)
。<div>
元素用于输出目标 web 表单返回的数据。
目标 web 表单标记很简单,由一个<div>
元素组成,该元素输出从主 web 表单接收的数据。Target.aspx
的标记如下所示:
`
`
图 11-2 显示了运行时的主 web 表单和嵌入的目标 web 表单。
***图 11-2。*目标网页表单加载到<iframe>
图 11-2 显示了主网页表单的运行示例。主 web 表单发送字符串数据“Hello World!”添加到目标 web 窗体。目标 web 表单输出数据并发回成功消息。主表单的原点以及目标 web 表单的原点也与它们各自的数据一起显示。
主 web 表单包含处理发送按钮的click
事件的 jQuery 代码。该代码如清单 11-3 所示。
***清单 11-3。*向目标 Web 表单发送数据
var targetOrigin = "http://localhost:1052"; $(document).ready(function () { if (!Modernizr.postmessage) { alert("This browser doesn't support the HTML5 postMessage API!"); return; } var targetWindow = $("#target").get(0).contentWindow; window.addEventListener("message", ReceiveMessage, false); $("#btnSend").click(function () { targetWindow.postMessage($("#txtData").val(), targetOrigin); }); });
这段代码使用 Modernizr 库来检测浏览器是否支持 postMessage API。这是使用Modernizr
对象的postmessage
属性完成的。如前所述,为了将数据发送到目标 web 表单,您需要一个目标窗口的句柄。要将window
对象附加到<iframe>
元素,可以使用<iframe>
DOM 元素的contentWindow
属性。
如果您对接收目标 web 表单发送的返回值感兴趣,您需要为message
事件连接一个事件处理程序。addEventListener()
方法可以将一个事件处理函数——在本例中是ReceiveMessage
——连接到主窗口的message
事件。
发送按钮的click
事件处理程序调用目标window
对象上的postMessage()
方法。postMessage()
方法的第一个参数是您希望传递给目标 web 表单的数据。第二个参数是目标 web 表单的原点。
ReceiveMessage
()事件处理函数如清单 11-4 所示。
**清单 11-4。**处理主 Web 窗体中的message
事件
function ReceiveMessage(evt) { if (evt.origin != targetOrigin) return; $("#divReceived").append(evt.origin + " : " + evt.data + "<br/>"); }
这段代码首先检查正在接收的消息的来源是否是允许的。它使用事件参数的origin
属性来实现。这样,其他网站(其服务被您的网站消费的网站)的开发者可以将的某些来源列入白名单。如果来源不属于批准的集合,您可以在进一步处理时忽略它。
为了访问目标 web 表单发送的数据,代码使用事件参数的data
属性。使用 jQuery append()
方法将原点和数据附加到<div>
元素。
目标 web 表单包含接收主 web 表单发送的数据的 jQuery 代码。该代码如清单 11-5 所示。
***清单 11-5。*接收主网页表单发送的数据
var targetOrigin = "http://localhost:1050"; $(document).ready(function () { ...
` window.addEventListener(“message”, ReceiveMessage, false);
});
function ReceiveMessage(evt) {
if (evt.origin != targetOrigin)
return;
$(“#divReceived”).append(evt.origin + " : " + evt.data + “
”);
evt.source.postMessage(“Data received successfully!”, evt.origin);
}`
前面提到过,postMessage API 提供了一种安全的方式来进行跨文档消息传递。这里你就能明白为什么了。主页的作者不能直接访问目标网页:一切都通过目标网页的代码来完成。
清单 11-5 中的代码将一个事件处理程序ReceiveMessage()
连接到目标窗口的message
事件。然后,ReceiveMessage()
事件处理函数对origin
属性执行检查,如果数据是从预期的来源接收的,则将数据追加到<div>
元素中。使用事件参数的source
属性将成功消息发送回主 web 表单。source
属性引用主 web 表单的window
对象。
注意除非你开发的是面向公众消费的通用服务,否则在使用 postMessage API 的应用中包含白名单机制是个好主意。如果没有这样的白名单,任何来源都可以向您的网页发送数据,并且它将由消息事件处理程序进行处理。如果你的白名单很小,你可以把它存储在一个数组中;否则,XML 文件或数据库表是更好的选择。
对窗口对象使用 postMessage
将 postMessage API 用于window
对象类似于将其用于<iframe>
元素。唯一的区别是,您需要使用由window.open()
方法返回的window
引用,而不是contentWindow
属性。清单 11-6 清楚地说明了这一点。
清单 11-6*。*用postMessage()
搭配window.open()
用
`$(document).ready(function () {
…
var targetWindow = window.open(targetOrigin + “/Target.aspx”);
window.addEventListener(“message”, ReceiveMessage, false);
KaTeX parse error: Expected 'EOF', got '#' at position 3: ("#̲btnSend").click…(“#txtData”).val(), targetOrigin);
});
});`
这段代码驻留在主 web 表单中。注意,这一次,您没有使用<iframe>
来加载目标 web 表单,而是使用了window.open()
方法。open()
方法接受目标 web 表单的 URL,并返回对目标 web 表单的window
对象的引用。一旦获得了对目标窗口的引用,它的使用方式与前面讨论的<iframe>
示例完全相同。
使用 XMLHttpRequest 发出请求
一种非常常见和流行的与服务器通信的方式是XMLHttpRequest
对象。XMLHttpRequest
对象允许您以编程方式向 web 服务器发出 HTTP 请求,例如GET
、POST
、PUT
和DELETE
。XMLHttpRequest
流行的主要原因是主流浏览器支持它。最初是由 Internet Explorer 引入的,但很快就被其他浏览器所吸收。今天,XMLHttpRequest
是大多数基于 Ajax 的通信的基础。事实上,您在本书中一直使用的 jQuery $.ajax()
方法在内部使用了XMLHttpRequest
的功能。使用XMLHttpRequest
,您可以向 web 服务器发出同步和异步请求,尽管异步模式更常用。
在 HTML5 之前就已存在,但随着 HTML5 的出现,它得到了改进。一些主要改进包括以下内容:
- You can use
XMLHttpRequest
object and CORS specification to make cross-origin request.- You can use progress events to track the progress of data download and upload operations.
XMLHttpRequest
It now supports sending binary data.
总之,这些对XMLHttpRequest
对象的改进被称为级别 2 。在与 Web 工作器 一起工作时,您曾短暂地使用过XMLHttpRequest
。接下来的章节将更详细地剖析XMLHttpRequest
对象。
XMLHttpRequest 的属性
在使用XMLHttpRequest
对象之前,让我们快速看一下它的属性、方法和事件。表 11-1 列出了XMLHttpRequest
的属性。其中一些属性是在发出请求之前设置的,而其他属性是在请求完成之后访问的。
在典型的用法中,在发起请求之前设置responseType
和timeout
。然后跟踪请求的readyState
,当它变成4
(完成)时,访问responseText
、responseXML
、status
和statusText
属性。
XMLHttpRequest 的方法
XMLHttpRequest
对象提供了允许您发起请求、随请求发送数据以及在请求完成前终止请求的方法。表 11-2 列出了这些方法,供您快速参考。
在一个典型的用法中,你先调用open()
方法,再调用setRequestHeader()
,然后调用send()
来发出请求。大多数情况下,在open()
方法中使用GET
或POST
动词。然而,open()
也可以用于其他动词。例如,在使用 ASP.NET Web API 时,除了GET
和POST
之外,还需要通过PUT
和DELETE
。
send()
可用于根据内容类型发送数据。常见的数据格式是文本和 JSON。您还可以使用FormData
对象将数据捆绑成键值对,并发送给服务器。
XMLHttpRequest 事件
XMLHttpRequest
对象的事件允许您跟踪请求并确定其状态。表 11-3 列出了这些事件。
如果您在 HTML5 之前使用了XMLHttpRequest
,请记住开发人员主要依靠readystatechange
事件进行任何类型的跟踪,包括请求完成和错误处理。然而,XMLHttpRequest
级别 2 提供了特定于任务的事件,您可以使用这些事件来使您的代码更加整洁。
注意,progress
事件是为XMLHttpRequest
对象本身以及XMLHttpRequest
实例的上传对象触发的。前一个事件跟踪下载数据的进度,而后一个事件跟踪上传数据的进度。通过处理progress
事件,您可以向用户显示操作的进度,比如在进度条中。
使用 XMLHttpRequest 发出请求
现在您已经知道了XMLHttpRequest
对象的属性、方法和事件,让我们开发一个应用来演示如何使用它们。在本节中,您将开发一个基于 ASP.NET Web Forms 的应用,如图 11-3 所示。
***图 11-3。*使用XMLHttpRequest
开发的客户列表应用
这个 web 表单在一个 HTML 表中显示了 Northwind 数据库的Customers
表中的所有记录。顶部的行允许用户插入新客户。可以使用更新或删除按钮分别修改或删除现有客户。通过 ASP.NET Web API 执行SELECT
、INSERT
、UPDATE
、DELETE
(CRUD)操作。Web API 控制器类公开的方法是使用XMLHttpRequest
对象调用的。web 表单标记很简单,如清单 11-7 所示。
***清单 11-7。*客户列表 Web 表单的标记
`
Customer List
web 表单由一个表格组成:tblCustomers
。根据数据库中客户记录的数量,以编程方式添加表中的行。应用所需的实体框架数据模型如图图 11-4 所示。
**图 11-4。**实体框架Customers
表的数据模型类
Customer
数据模型类有几个属性,但是在应用中只使用了四个:CustomerID
、CompanyName
、ContactName
和Country
。
开发 Web API 控制器
为了在Customers
表上执行 CRUD 操作,您开发了一个 Web API 控制器(CustomerController
)。Web API 控制器是一个从ApiController
基类继承的类,包含以下方法的实现:
Get()
: indicates an httpGET
request, which requests the data item ofSELECT
and returns it to the caller.Get(id)
: indicates an HTTPGET
request, andSELECT
indicates a data item returned to the caller based on the specified ID.Post()
: It means an HTTPPOST
request to put a data itemINSERT
into the database.Put(id)
: indicates an httpPUT
request for data items matching the specified ID in theUPDATE
database.Delete(id)
: indicates an httpDELETE
request, which is used for data items in theDELETE
database.
注意,虽然这个例子需要 CRUD 功能,但是 Web API 和XMLHttpRequest
不一定专门用于这种场景。清单 11-8 展示了CustomerController
的Get()
和Post()
方法。
清单 11-8。CustomerController
类的 Get()
和Post()
方法
`public class CustomerController : ApiController
{
public IEnumerable Get()
{
NorthwindEntities db = new NorthwindEntities();
var data = from item in db.Customers
orderby item.CustomerID
select item;
return data;
}
public void Post(Customer obj)
{
NorthwindEntities db = new NorthwindEntities();
db.Customers.AddObject(obj);
db.SaveChanges();
}
…
}`
Get()
方法返回一个Customer
项的IEnumerable
。在里面,Get()
选择所有的Customer
项,并根据CustomerID
列排序后返回给调用者。
Post()
方法接受一个Customer
对象作为参数。这个参数来自您稍后编写的 jQuery 代码。Post()
使用AddObject()
方法将提供的Customer
对象添加到Customers
表中。要将数据持久存储回数据库,您需要调用SaveChanges()
。
为了从客户端成功调用 Web API,您还需要在Global.asax
文件中添加以下路由信息:
protected void Application_Start(object sender, EventArgs e) { RouteTable.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = System.Web.Http.RouteParameter.Optional } ); }
MapHttpRoute()
方法将传入请求映射到 Web API 控制器类。例如,一个指向Customer
Web API 控制器的示例 URL 是[
localhost:1050/api/Customer](http://localhost:1050/api/Customer)
。
使用 XMLHttpRequest 调用 Web API
现在让我们编写客户端 jQuery 代码,使用XMLHttpRequest
对象调用Customer
Web API 控制器。jQuery 代码主要由四个函数组成:GetCustomers()
、InsertCustomer()
、UpdateCustomer()
和DeleteCustomer()
。这些函数通过调用 Web API 方法来执行各自的操作。
web 表单加载时,需要显示Customer
数据;因此,在 jQuery ready()
函数中调用了GetCustomers()
函数。清单 11-9 展示了这是如何做到的。为了可读性,省略了一些代码。
***清单 11-9。*创建一个XMLHttpRequest
对象并显示数据
`$(document).ready(function () {
GetCustomers();
});
function GetCustomers() {
KaTeX parse error: Expected 'EOF', got '#' at position 3: ("#̲tblCustomers").…(“#tblCustomers”).append(“…”);
var emptyRow = “”;
emptyRow += “”;
…
emptyRow += “”;
emptyRow += “”;
$(“#tblCustomers”).append(emptyRow);
var xhr = new XMLHttpRequest();
xhr.open(“GET”, “api/Customer”);
xhr.setRequestHeader(‘Accept’, ‘application/json’);
xhr.setRequestHeader(‘Content-Type’, ‘application/json’);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
var data = JSON.parse(xhr.responseText);
for (var i = 0; i < data.length; i++) {
var row = “”;
row += “”;
…
row += “”;
row += “”;
row += “”;
KaTeX parse error: Expected 'EOF', got '#' at position 3: ("#̲tblCustomers").…(“#tblCustomers input[value=‘Insert’]”).click(InsertCustomer);
KaTeX parse error: Expected 'EOF', got '#' at position 3: ("#̲tblCustomers in…(“#tblCustomers input[value=‘Delete’]”).click(DeleteCustomer);
}
}
xhr.send();
}`
这段代码声明了一个全局XMLHttpRequest
对象,这样所有其他方法都可以使用它。在 jQuery ready()
函数内部,调用了GetCustomers()
函数。GetCustomers()
首先添加表格标题和一个空行,用于接受新的Customer
细节。
然后创建一个新的XMLHttpRequest
对象。XMLHttpRequest
对象的open()
方法指定请求方法为GET
,URL 为api/Customer
。请注意,您不需要在 URL 中指定 Web API 方法名。基于 HTTP 动词(在本例中为GET
,适当的 Web API 方法被自动调用。
您设置了两个请求头:Accept
和Content-Type
。Accept
头主要用于 Web API,控制返回数据的格式(文本、JSON 等)。因为要访问 JSON 格式的Customer
数据项,所以Accept
头被设置为application/json
。
onreadystatechange
事件处理函数检查XMLHttpRequest
对象的readyState
属性。如果readyState
是4
(完成),使用JSON.parse()
方法将responseText
解析成一个 JSON 对象。回想一下Get()
Web API 方法返回了一个Customer
对象的IEnumerable
。因此,您使用一个for
循环来遍历数据对象。每次迭代都会添加一个新的表格行。每个新表行都由填充了现有客户信息的文本框组成(例如,data[i].CustomerID
)。
接下来,为插入、更新和删除按钮添加click
事件处理程序。注意如何使用属性选择器将<input>
元素的值与插入、更新和删除进行比较。相应的InsertCustomer
、UpdateCustomer
和DeleteCustomer
函数被连接为事件处理程序。
一旦连接了readystatechange
事件处理函数,就使用XMLHttpRequest
对象的send()
方法将请求发送到服务器。
就XMLHttpRequest
的用法而言,InsertCustomer()
、UpdateCustomer()
和DeleteCustomer()
功能类似。InsertCustomer()
如清单 11-10 所示。其他函数遵循类似的模式。
***清单 11-10。*插入新的Customer
`function InsertCustomer(evt) {
var customerID = $(this).closest(‘tr’).children().eq(0).children().eq(0).val();
var companyName = $(this).closest(‘tr’).children().eq(1).children().eq(0).val();
var contactName = $(this).closest(‘tr’).children().eq(2).children().eq(0).val();
var country = $(this).closest(‘tr’).children().eq(3).children().eq(0).val();
var obj = { “CustomerID”: customerID, “CompanyName”: companyName,
“ContactName”: contactName, “Country”: country };
var xhr = new XMLHttpRequest();
xhr.open(“POST”, “api/Customer”);
xhr.setRequestHeader(‘Content-Type’, ‘application/json’);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
alert(“Customer Inserted!”);
GetCustomers();
}
}
var param = JSON.stringify(obj);
xhr.send(param);
}`
InsertCustomer()
充当插入按钮的事件处理程序。该函数做的第一件事是获取新输入的Customer
细节,例如CustomerID
、CompanyName
、ContactName
和Country
。观察 jQuery 选择器如何从<input>
元素中检索值。在InsertCustomer()
函数中,关键字this
指的是插入按钮。jQuery closest()
方法返回最近的周围<tr>
元素。这样,您就到达了包含文本框的表格行。对表行调用的children()
方法返回它包含的所有<td>
元素。eq()
方法基于索引返回指定的子元素,允许您到达单个<td>
元素。另一组children()
和eq()
方法让您可以访问表格单元格中的<input>
元素。
文本框中输入的值存储在本地变量中:customerID
、companyName
、contactName
和country
。使用这些值形成一个JSON
对象。注意,JSON
对象必须具有与Customer
数据模型属性名称相同的键名,这样 ASP.NET 就可以将JSON
对象映射到数据模型类。
然后创建一个新的XMLHttpRequest
对象,并使用其open()
方法打开一个POST
请求。因为您正在向服务器发送 JSON 数据,所以使用setRequestHeader()
方法将Content-Type
头设置为application/json
。如果readyState
为4
(完成),则readystatechange
事件处理程序向用户显示一条成功消息。再次调用GetCustomers()
用新添加的记录刷新客户列表。
在您连接了readystatechange
事件处理程序之后,使用send()
将请求发送到服务器。send()
方法将 JSON Customer
对象的字符串表示作为其参数。这个JSON
对象由Customer
Web API 控制器的Post()
方法接收。
现在,您可以运行客户列表应用,并在其上测试 CRUD 操作。您可能想知道如何使用XMLHttpRequest
对象进行跨来源请求。幸运的是,您不需要对XMLHttpRequest
进行任何代码级别的更改就可以进行跨来源请求。您需要做的就是确保运行另一个域的 IIS 允许 CORS 请求。如前所述,您可以使用 IIS 管理器或web.config
来配置 CORS 报头。
使用 XMLHttpRequest 上传文件
在第九章中,你学习了文件 API。在那一章中,您还学习了如何使用 jQuery $.ajax()
方法从客户机上选择文件并上传到 web 服务器上。您可以使用XMLHttpRequest
对象完成同样的任务。使用XMLHttpRequest
的一个好处是它允许你通过progress
事件跟踪上传操作。通过处理progress
事件,您可以向用户显示某种进度指示器(比如进度条)。图 11-5 显示了一个使用上传progress
事件的 web 表单。
***图 11-5。*使用进度条显示文件上传的进度。
这个 web 表单应该看起来很熟悉:你在第九章中开发了类似的东西。web 表单允许您将文件从 Windows 资源管理器或桌面拖放到暂存架图像上。然后,您可以使用上传按钮将文件上传到服务器。可以使用取消按钮取消上传操作。文件上传操作的进度显示在进度条中。
web 表单的标记很简单。唯一不熟悉的元素是 HTML5 <progress>
元素,它在 web 表单的底部呈现一个进度条。<progress>
元素如下所示:
<progress id="uploadProgress" value="1" max="100"></progress>
这个<progress>
元素使用了两个属性:value
和max
。value
属性表示操作的进度。max
属性控制value
属性的比例。例如,如果您希望以百分比(%)的形式显示上传进度,您需要将max
属性设置为100
,这样value
属性就可以取最大值100
。注意,<progress>
元素本身不会自动递增。您需要以编程方式设置value
属性,以便进度条显示正确的进度。
当您单击 Upload 按钮时,会调用一个 JavaScript 函数UploadFiles()
,启动上传操作。UploadFiles()
如清单 11-11 所示。
**清单 11-11。**使用XMLHttpRequest
对象上传文件
`var xhr = new XMLHttpRequest();
function UploadFiles() {
var data = new FormData();
for (var i = 0; i < files.length; i++) {
data.append(files[i].name, files[i]);
}
xhr.upload.addEventListener(“progress”, OnProgress, false);
xhr.addEventListener(“load”, OnComplete, false);
xhr.addEventListener(“error”, OnError, false);
xhr.addEventListener(“abort”, OnAbort, false);
xhr.open(“POST”, “UploadHandler.ashx”);
xhr.send(data);
}`
这段代码声明了一个全局XMLHttpRequest
对象(xhr
)。UploadFiles()
函数首先创建一个新的FormData
对象。然后,它遍历用户选择的所有文件。文件集合包含使用拖放操作选择的File
对象。使用FormData
对象的append()
方法将所有选择的文件追加到FormData
对象。
接下来,addEventListener()
方法连接四个事件的事件处理程序:progress
、load
、error
和abort
。注意,因为您对跟踪数据上传操作的进度感兴趣,所以处理的是upload
对象的progress
事件,而不是XMLHttpRequest
对象的progress
事件。事件处理函数OnProgress()
、OnComplete()
、OnError()
和OnAbort()
将在稍后讨论。
然后通过将POST
指定为请求类型并将UploadHandler.ashx
指定为 URL 来调用XMLHttpRequest
对象的open()
方法。UploadHandler.ashx
是一个 ASP.NET 通用处理程序,将上传的文件保存在服务器上。最后,调用XMLHttpRequest
对象的send()
方法,并将FormData
对象作为参数传递给它。
取消按钮调用XMLHttpRequest
对象的abort()
方法,如下所示:
function CancelUpload() { xhr.abort(); }
处理progress
、load
、error
和abort
事件的事件处理函数如清单 11-12 所示。
***清单 11-12。*处理XMLHttpRequest
对象的事件
`function OnProgress(evt) {
if (evt.lengthComputable) {
var progress = Math.round(evt.loaded * 100 / evt.total);
$(“#uploadProgress”).attr(“value”, progress);
}
}
function OnComplete(evt) {
alert(evt.target.responseText);
}
function OnError(evt) {
alert(“Error Uploading File(s)!”);
}
function OnAbort(evt) {
alert(“File Upload Aborted!”);
}`
OnProgress()
事件处理函数接收一个类型为ProgressEvent
的事件参数,该参数提供上传操作的进度信息。lengthComputable
属性返回一个布尔值,表明操作的进度是否可以确定。loaded
和total
属性表示上传的字节数和要上传的总字节数。基于这两个值,计算进度百分比。使用 jQuery attr()
方法将<progress>
元素的value
属性设置为这个计算值。
其他事件处理函数很简单,只是向用户显示一条消息(成功、错误或取消)。
使用服务器发送的事件通知浏览器
到目前为止,在本书中,您已经使用了从客户端到服务器发起通信的技术(例如,$.ajax()
或XMLHttpRequest
)。在这种客户端到服务器的技术中,一旦发送了请求并且从服务器接收到响应,基础通信信道就被关闭。考虑一个服务器持续执行业务操作的情况。一个网页为用户显示处理的状态。因为操作在服务器上继续进行,所以您希望定期更新该状态。你如何完成这项任务?一种常见的方法是定期轮询服务器并检索操作的状态。您可以使用像setTimeout()
和setInterval()
这样的函数,向服务器发出请求,试图检索操作的状态。
这种轮询技术的缺点是有太多的请求-响应周期。客户端不断发送请求,服务器不断响应每个请求。每个请求-响应周期都需要自己的通信通道,一旦周期完成,该通道就会关闭。如果服务器在有趣的事情发生时通知您,而不需要任何轮询,这不是很好吗?这就是服务器发送的事件允许你做的事情。
顾名思义,服务器发送的事件是由服务器调度的。如果服务器上发生任何有趣的事情,服务器会通知客户端,而不是客户端定期检查服务器的更新。服务器发送的事件使用公共通信通道发送多个通知,从而避免连续的请求-响应循环。向客户机发送通知的服务器端资源被包装在一个EventSource
对象中。然后,EventSource
对象的open
、message
和error
事件分别用于打开通信通道、从服务器接收消息和处理错误。
让我们开发一个应用来说明如何使用服务器发送的事件。图 11-6 显示了应用的主 web 表单。
***图 11-6。*服务器发送的活动事件
web 表单包含一个开始监听按钮。单击该按钮打开与驻留在服务器上的通用处理程序(ClientNotifier.ashx
)的连接。通用处理程序的设计方式是,它在 1 分钟内每隔 15 秒向客户机发送一次服务器时间形式的通知。你会看到输出的时间间隔。包括通知数据在内的消息显示在一个<div>
元素中。
web 表单工作背后的 JavaScript 代码如清单 11-13 所示。
***清单 11-13。*发起服务器发送的事件
`$(document).ready(function () {
if (window.EventSource == undefined) {
alert(“This browser doesn’t support HTML5 Server Sent Events.”);
return;
}
KaTeX parse error: Expected 'EOF', got '#' at position 3: ("#̲btnListen").cli…(‘#targetDiv’).append(‘
Connection Opened.
’);},false);
source.addEventListener(“error”, function (event) {
if (event.eventPhase == EventSource.CLOSED) {
$(‘#targetDiv’).append(‘
Connection Closed.
’);}
},false);
source.addEventListener(“message”,function (event) {
$(‘#targetDiv’).append(‘
’ + event.data + ‘
’);},false);
});
});`
这段代码首先检查浏览器是否支持服务器发送的事件。这是通过检查window.EventSource
对象的存在来实现的。
然后,代码继续连接开始监听按钮的click
事件处理程序。这个事件处理程序创建一个新的EventSource
对象。创建EventSource
时,发送事件的 ASP.NET 通用处理程序的路径作为参数传递。
然后为EventSource
对象的三个事件连接事件处理程序:open
、message
和error
。当浏览器第一次向服务器资源发出请求时,会引发open
事件。当服务器发送的数据到达客户端时,引发message
事件。如果出现错误,比如连接关闭,就会引发error
事件。open
和error
事件处理程序向targetDiv
<div>
元素添加一条消息。错误处理程序使用eventPhase
属性来决定底层连接是否关闭。eventPhase
的可能值为CONNECTING
( 0
)、OPEN
( 1
)和CLOSED
( 2
)。message
事件处理程序接收一个event
参数,其data
属性返回服务器发送的数据。
将事件发送到客户端的服务器端代码驻留在 ASP.NET 通用处理程序ClientNotifier.ashx
中。该代码如清单 11-14 所示。
***清单 11-14。*向客户端发送事件
public void ProcessRequest(HttpContext context) { HttpResponse Response = context.Response; DateTime startDate = DateTime.Now; Response.ContentType = "text/event-stream"; while (startDate.AddMinutes(1) > DateTime.Now) { Response.Write(string.Format("data: {0}\n\n", DateTime.Now.ToString("hh:mm:ss"))); Response.Flush(); System.Threading.Thread.Sleep(15000); } Response.Close(); }
该代码将Response
的ContentType
设置为text/event-stream
。这样,客户端浏览器就知道这个响应属于服务器发送的事件。然后一个while
循环迭代 1 分钟。在循环中,事件数据被成批发送到客户端。Response.Write()
方法在响应流中写入事件数据,而Response.Flush()
确保事件数据被直接发送到客户端,没有任何缓冲。发送到客户端的事件数据必须采用预定义的格式。示例格式如下:
data: Hello World!\n\n
每条事件数据都应该以data:
开始,以两个换行符(\n\n
结束。事件数据也可以多行格式发送:
data: {\n data: "CustomerID": "ALFKI",\n data: "Country": "USA"\n data: }\n\n
这个标记以 JSON 格式向客户机发送数据。客户端可以解析数据来构造一个JSON
对象。
使用Thread.Sleep()
方法暂停操作 15 秒,以在通知之间引入延迟。除了开发人员定义的数据之外,服务器还向客户端发送唯一的事件 ID 和重试间隔。这些信息可以作为event
参数的id
和retry
属性来访问。
尽管在本例中您不需要这样做,但是您可以通过调用EventSource
对象的close()
方法来停止接收服务器发送的事件。
注意在本节讨论的例子中,服务器端处理发生 1 分钟,之后服务器终止底层连接。然而,浏览器认为存在连接问题,并在短暂的时间间隔后尝试与服务器重新连接,导致相同的逻辑再次运行。如果您不希望接收更多的事件,您需要调用EventSource
对象的close()
方法。
使用网络套接字的双向通信
通常,web 上的通信由两个不同的参与方组成:客户端和 Web 服务器。到目前为止,本章中你已经学习了两种客户机-服务器通信技术:单向通信和请求-响应通信。
服务器发送的事件使用单向通信模型。在单向通信中,一方与另一方进行通信。在服务器发送事件的情况下,服务器通过发送通知与客户机保持“对话”。这也被称为单工通信。单工通信的一个真实例子是无线电广播,无线电信号从无线电台发出,但电台不接收任何反馈。
postMessage API 和XMLHttpRequest
对象使用请求-响应模型与服务器通信。在这个模型中,客户端向服务器发起请求,以触发一些处理或获取一些数据。一旦处理完成,服务器就将响应发送回客户机。基础通信信道仅在一个请求-响应周期内保持打开。如果您向服务器发送多个请求,您实际上是在多次打开和关闭通信通道。这种通信模式有时被称为半双工通信,因为在任何时候,客户端或服务器都在与另一方通信。当然,在 web 应用中,浏览器必须发起与服务器的通信;只有这样,服务器才可以响应。半双工通信的一个真实例子是步话机:一次只能有一个人通话。
还有第三种类型的通信——双向或双工通信。这种情况下,双方可以同时沟通。现实生活中的一个例子是电话:双方可以同时通话。软件应用中双工通信的一个常见应用是聊天系统,如 MSN、Yahoo!Messenger 和 Google Talk。在任何聊天系统中,两个或更多的成员可以同时互相聊天。另一个例子是多人在线游戏,多个玩家可以同时参与。就 HTML5 而言,实现双向通信的技术是 Web Sockets。
了解 WebSocket
与请求-响应模型不同,WebSocket 在整个通信过程中保持底层通信通道开放。基于 WebSocket 的通信通常包括三个步骤:
- Establish a connection or handshake between client and server.
- The Web Socket server is required to listen for incoming communication.
- Send and receive data.
Web 应用使用 HTTP 协议来运行,HTTP 本质上使用请求-响应模型。普通 HTTP 不太适合执行双向通信。因此,WebSocket 需要将普通 HTTP 升级为 web socket 协议。这种升级发生在客户端和服务器之间建立连接的时候。WebSocket 是基于 TCP 的协议,仅在握手和升级过程中使用 HTTP。一旦在客户机和服务器之间建立了连接,WebSocket 通信就通过一个 TCP 连接进行。WebSocket 协议可以处理文本以及二进制数据。由于这些特性,WebSocket 协议提供了优于 HTTP 请求-响应模型的性能优势。
图 11-7 中的请求和响应头显示了升级是如何发生的。
***图 11-7。*握手过程中的请求和响应,显示在 Chrome 开发者工具中
注意请求和响应头是如何设置Connection
和Upgrade
头的。为了将通信从普通 HTTP 升级到 WebSocket,您需要一个能够进行这种升级的 web 服务器。Windows 8 附带的 IIS 8.0 可以接受 WebSocket 通信。如果您正在开发使用 HTML5 web Sockets 的 Web 应用,您可能需要在 IIS 8.0 中安装 WebSocket 支持。图 11-8 显示了如何使用控制面板中的“打开或关闭 Windows 功能”选项来安装 WebSocket 协议。
***图 11-8。*在 IIS 8.0 中启用 WebSocket 协议支持
如果 WebSocket 协议未启用,您的 ASP.NET 应用将无法接收和响应服务器上的 Web Socket 请求。
一旦客户端和服务器之间发生握手,并且在它们之间建立了通信信道,就可以使用升级后的连接进行通信。基于 Web Socket 的应用由分成两部分的代码组成:Web Socket 服务器端代码和 Web Socket 客户端代码。WebSocket 服务器端代码驻留在 Web 服务器上,并侦听来自客户端的传入通信。当从客户端接收到通信时,WebSocket 服务器端代码处理该通信,并且通常将通信发送回客户端。如果没有来自客户端的通信,Web Socket 服务器可以继续等待,也可以终止通信通道。
虽然您可以从头开始开发 Web Socket 服务器端代码,但是很多时候您可以使用第三方服务器。例如,如果您希望在 web 应用中实现在线聊天,您可以使用第三方或开源聊天服务器,并根据需要开发客户端网页。
注意,HTML5 将自己限制在开发 WebSocket 客户端。不同的 web 服务器和服务器端技术可能有自己开发 Web Socket 服务器的方式。WebSocket 客户端和 WebSocket 服务器现在可以相互通信并传输数据。Web Socket 客户端使用 HTML5 WebSocket
对象向 Web Socket 服务器发送数据,并从 Web Socket 服务器接收数据。
web socket 对象
HTML5 WebSocket
对象提供了属性、方法和事件,您可以使用它们来开发 WebSocket 客户端应用。表 11-4 显示了它们,供您快速参考。
当您创建一个WebSocket
的实例时,您需要提供将客户端连接到 Web Socket 服务器的端点 URL。从表 11-4 中可以看出,WebSocket
使用的数据发送和接收模式与早期的技术相似。一旦建立了连接,你就可以在需要的时候使用send()
发送数据。同时,message
事件处理函数继续接收服务器发送的消息。
在 ASP.NET 使用 WebSocket
如前所述,在开发 WebSocket 应用时,您有两段不同的代码:WebSocket 客户端和 WebSocket 服务器。Web Socket 客户端是使用 JavaScript 和 HTML5 WebSocket
对象开发的。因此,这段代码遵循相同的编码模式,不管您的 web 服务器软件是什么。然而,当您开发 Web Socket 服务器时,您需要使用 Web 服务器软件提供的框架和您正在使用的服务器端框架。就 ASP.NET 而言,IIS 8 和某些。NET framework 类一起允许您开发一个 WebSocket 服务器。
为了理解客户端和服务器端代码是如何结合在一起的,让我们开发一个简单的回显服务器,它会回显客户端发送给它的任何内容。虽然 Echo 服务器不像聊天应用那样执行同步双向通信,但它确实说明了 Web Socket 客户机和服务器是如何交互的。充当 web Socket 客户端的 Web 表单如图图 11-9 所示。
***图 11-9。*充当 Web Socket 客户端的 Web 表单
如您所见,Web Socket 客户端 Web 表单由一个文本框和一个按钮组成。用户在文本框中输入一些数据,然后单击发送按钮。位于 IIS 8.0 上的 WebSocket 服务器侦听传入的通信,接收客户端发送的数据,并将其回显给客户端。回显的数据通过将它附加到一个<div>
元素显示在 web 表单上。
注意虽然你可以使用安装在 Windows 7 上的 Visual Studio 2012 开发这个应用,但是你将无法运行和测试它。要运行该示例,您需要安装了 IIS 8.0 的 Windows 8。如果尚未启用 WebSocket 协议,您还必须在 IIS 8.0 中启用它。
开发 Echo 服务器
在开发 Web Socket 客户端之前,我们先来开发服务器部分。您将 Echo 服务器开发为 ASP.NET 通用处理程序(.ashx
)。通用处理程序的工作是触发监听代码。清单 11-15 展示了这是如何做到的。
***清单 11-15。*使用通用处理程序启动 Echo 服务器
public class WebSocketGenericHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { if (context.IsWebSocketRequest) { context.AcceptWebSocketRequest(EchoServer); } } … }
这段代码显示了一个通用处理程序——WebSocketGenericHandler
——它触发 Web Socket 服务器。通用处理程序的ProcessRequest()
方法首先检查传入的请求是否是 WebSocket 请求。这是通过检查HttpContext
对象的IsWebSocketRequest
属性来实现的。该属性与 IIS 8.0 WebSocket 模块协同工作,如果传入请求是 WebSocket 请求,则返回true
。WebSocket 请求不同于普通的 HTTP 请求,它不使用http://
协议,而是使用ws://
(WebSocket)协议。例如,对该通用处理程序的 WebSocket 请求如下所示:
ws://localhost:49428/WebSocketGenericHandler.ashx
如果IsWebSocketRequest
返回true
,则调用HttpContext
的AcceptWebSocketRequest()
方法。这个方法有一个参数——user
函数,它提供了一个监听和响应客户端请求的函数。在这种情况下,EchoServer
函数包含侦听传入数据并将其回显给客户端的逻辑。提供给AcceptWebSocketRequest()
方法的user
函数应该是一个异步函数,如清单 11-16 所示。
清单 11-16。 EchoServer 异步函数
public async Task EchoServer(AspNetWebSocketContext context) { WebSocket socket = context.WebSocket; while (true) { ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[1024]); WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, CancellationToken.None); if (socket.State == WebSocketState.Open) { string userMessage = Encoding.UTF8.GetString(buffer.Array, 0, result.Count); userMessage = "You sent: " + userMessage + " at " + DateTime.Now.ToLongTimeString(); buffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(userMessage)); await socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None); } else { break; } } }
EchoServer()
方法被标记为async
,表示其中的代码以异步方式运行。async
方法是一种执行长时间运行的操作而不阻塞主线程的便捷方式。在本例中,Echo 服务器应该持续监听传入的请求,也就是说,是一个长时间运行的操作。EchoServer()
返回一个Task
对象。Task
类充当异步代码的包装器。EchoServer()
接收一个AspNetWebSocketContext
类型的参数。AspNetWebSocketContext
类通过WebSocket
属性给你访问WebSocket
的权限。WebSocket
类是 HTML5 WebSocket
对象的服务器端对应物。然后开始一个无休止的while
循环,这样 Echo 服务器就可以连续监听传入的请求。
要接收传入的数据,可以使用WebSocket
类的ReceiveAsync()
方法。这个方法和await
操作符一起被调用。await
操作符表示调用方法的执行将被暂停,直到等待的任务完成。在这种情况下,等待的任务是接收传入的数据,并将其存储在一个ArraySegment
,一个字节数组中。接收操作的结果存储在一个WebSocketReceiveResult
对象中。
如果WebSocket
是打开的,如State
属性所示,接收到的数据将使用SendAsync()
方法回显给客户端。在将消息发送回客户端之前,您需要在消息中添加一个日期时间戳。如果State
属性具有除了Open
之外的任何值,则while
循环退出,从而终止服务器。
注意System.Net.WebSockets
和System.Web.WebSockets
名称空间包含处理服务器端 WebSocket 编程的类。中异步编程的详细讨论。NET 和使用 WebSocket 类超出了本书的范围。请参考 MSDN 文档以了解有关这些主题的更多信息。
开发 Web Socket 客户端
既然您已经完成了 Echo 服务器,那么让我们开发向 Echo 服务器发送数据和接收回显消息的客户机 web 表单。清单 11-17 显示了使用 HTML5 WebSocket
对象的 jQuery 代码。
***清单 11-17。*使用 HTML5 WebSocket
对象
var socket; $(document).ready(function () { if (!Modernizr.websockets) { alert("This browser doesn't support HTML5 Web Sockets!"); return; } socket = new WebSocket("ws://localhost:49428/WebSocketGenericHandler.ashx"); socket.addEventListener("open", function (evt) { $("#divHistory").append('<h3>Connection Opened with the Echo server.</h3>'); }, false); socket.addEventListener("message", function (evt) { $("#divHistory").append('<h3>' + evt.data + '</h3>'); }, false); socket.addEventListener("error", function (evt) { $("#divHistory").append('<h3>Unexpected Error.</h3>'); }, false); … });
这段代码声明了一个名为socket
的全局变量来保存对一个WebSocket
对象的引用。jQuery ready()
方法首先检查客户端浏览器是否支持 HTML5 WebSocket。它使用 Modernizr 的websockets
属性来实现。
然后通过传递WebSocketGenericHandler.ashx
的 URL 创建一个WebSocket
实例。注意 URL 是如何使用ws://
而不是http://
的。接下来,使用addEventListener()
方法连接三个事件的事件处理程序— open
、message
和error
。在message
事件处理程序中,使用evt.data
属性检索 Echo 服务器发送的数据。然后回显的数据被附加到一个<div>
元素中。其他事件处理程序在<div>
元素中输出指定的消息。
当您点击发送按钮时,来自客户端的数据被发送到服务器。发送按钮的click
事件处理程序如下所示:
$("#btnSend").clickhow(function () { if (socket.readyState == WebSocket.OPEN) { socket.send($("#txtMsg").val()); } else { $("#divHistory").append('<h3>The underlying connection is closed.</h3>'); } });
click
事件处理程序检查WebSocket
对象的readyState
属性。如果是OPEN
,click
事件处理程序调用 WebSocket 实例的send()
方法。文本框中输入的文本作为参数传递给send()
。虽然在这个应用中没有使用,但是您可以通过调用WebSocket
对象的close()
方法来关闭底层连接:
socket.close();
就这样!现在,您可以运行 web 表单,并通过向服务器发送消息来测试 Echo 服务器。
注视窗通讯基金会(WCF)在。NET Framework 4.5 也支持 WebSocket。为此,在 WCF 中添加了两个新绑定:NetHttpBinding
和NetHttpsBinding
。关于 WCF 如何支持 WebSocket 的详细讨论超出了本书的范围。有关更多详细信息,请参考 MSDN 文档。
总结
Web 应用经常需要与服务器通信。尤其是在 Ajax 驱动的应用中,需要在没有整页回发的情况下与服务器通信。HTML5 提供了几种实现客户机-服务器通信的方法。postMessage API 允许您执行跨文档消息传递,通过这种方式,您可以将数据发送到不同来源的网页。XMLHttpRequest
是客户端和服务器之间基于 Ajax 通信的基础。XMLHttpRequest
级别 2 允许您监控数据上传和下载操作的进度。它还允许通过 CORS 进行跨来源通信。
典型的 web 通信使用请求-响应模型。如果需要从服务器到客户端的单向通信,可以使用服务器发送的事件。这样,服务器可以通知客户端服务器上发生的事情。WebSocket 提供客户端和服务器之间的全双工通信。Windows 8 附带的 IIS 8.0 为 WebSocket 协议提供了服务器端支持。那个。NET framework 还提供了一组使用 WebSocket 的类。
下一章将探讨在某些类型的 web 应用中有用的另一个特性:地理定位。使用地理定位,您可以确定用户的地理位置,并相应地更改 web 应用的响应方式。
十二、使用地理定位 API 查找位置
今天的大多数 web 应用并不关心你从哪里访问它们。无论您的地理位置如何,它们在浏览器中呈现的内容都是相同的。但是,如果您将这些信息提供给 web 应用,它们可以对其进行创新性的使用。例如,一个社交网络应用可以推荐和你在同一个地方的朋友。用户位置信息也可以在工作门户中使用,以建议用户地理位置附近的工作。
跟踪用户位置的想法并不新鲜,但起初并没有找到这些信息的标准方法。幸运的是,多年来,一种标准化的方法——地理定位——已经发展到可以满足这种需求。地理定位 API 允许您通过使用各种位置源(如 IP 地址、全球定位系统(GPS)、全球移动通信系统(GSM)和通用分组无线业务(GPRS))来查找用户的位置。严格来说,地理定位 API 不是 HTML5 的一部分。然而,它通常与 HTML5 特性和技术一起使用。本章向您详细介绍了地理定位 API。具体来说,您将了解以下内容:
- What is geolocation?
- Use geolocation API to find and track user location
- Integrate geolocation API with Google Maps and Bing Maps
- Use geolocation API to present specific location data to users.
经纬度坐标系概述
为了传达用户的地理位置,您需要一个标准系统,该系统能够被参与该过程的所有各方所理解。在日常生活中,位置信息以城市、州、国家等形式表示。然而,这些信息对于计算来说用处不大。比如看两个城市名,分不清两者的距离。这就是使用地理坐标系统指定位置的原因。地理定位 API 使用的系统由纬度和经度坐标组成。
纬度坐标指定地球表面上一点的南北位置。纬度相同的点像平行于赤道的圆圈一样东西走向。纬度是从赤道的 0 度到两极的 90 度(北或南)的角度。
经度坐标指定了地球表面上一点的东西位置。经度相同的点位于从北极到南极的直线上。经度是一个角度,在本初子午线的 0°到向东或向西的 180°之间变化。
纬度和经度坐标以十进制/分钟/秒(DMS)格式或十进制度数指定。例如,在 DMS 格式中,孟买的纬度是 18 55 ’ N,经度是 72 54 ’ E,在十进制格式中,同样的坐标分别表示为 18.91667 和 72.9。正十进制数表示东北位置,而负十进制数表示西南位置。地理定位 API 使用十进制格式的纬度和经度值。
位置信息的来源
地理定位 API 并不规定从哪里获取位置信息。根据您使用的设备类型,位置信息的来源可能会有很大不同。例如,台式计算机可能使用 IP 地址作为信息源,而移动电话可能使用基于 GPS 的位置信息。位置信息的常见来源包括:
- Ip address
- 全球(卫星)定位系统
- 无线网络
- Mobile phone (GSM or CDMA)
这些位置源将在以下章节中简要讨论。
IP 地址
使用用户用来访问 web 应用的 IP 地址可能是确定用户位置的最古老的方法。通过这种技术,由互联网服务提供商(ISP)分配给用户的 IP 地址被用来检测请求的来源。这种技术只是猜测,而不是精确的结果,因为 ISP 可能位于远离用户实际位置的地方,因此,您不能依赖这种技术提供的位置信息。
除了被广泛使用之外,这种技术还提供了 IP 检测和位置查找逻辑发生在服务器端代码中的优势。客户端浏览器根本不在画面中。然而,由于它缺乏精确性,您不能在需要更高精确度的情况下使用这种技术。
全球定位系统
您可以使用来自全球各地的 GPS 卫星站的信号来确定用户的确切位置。虽然 GPS 比其他技术提供更精确的位置信息,但它不太适合封闭或室内位置。使用 GPS 的另一个缺陷是相关的更高的电池消耗,这可能需要用户频繁地给设备充电。
Wi-Fi
使用 Wi-Fi 技术,通过计算 Wi-Fi 接入点和用户之间的距离来确定用户的位置。这种技术在封闭和室内位置工作良好,并给出准确的结果。然而,它也有一个缺点,那就是并非所有地方都有 Wi-Fi 接入。特别是在农村地区,Wi-Fi 连接的可用性通常很差,位置信息可能变得不可用。
手机(GSM 或 CDMA)
这项技术利用手机信号塔和用户之间的距离来确定用户的位置。这项技术只适用于移动电话,并且只适用于移动站可用的区域。结果相当准确,可以用于专门为移动电话开发的应用。
注意地理定位 API 使用的具体技术是由使用地理定位 API 的设备和软件决定的,而不是由地理定位 API 本身决定的。
地理定位应用编程接口
地理位置 API 允许您执行三个操作:在给定时间点查找用户的位置,在用户从一个地方移动到另一个地方时跟踪用户的位置,以及停止跟踪用户的位置。API 封装在一个geolocation
对象中,该对象是浏览器窗口的navigator
对象的一个属性。这三项任务借助三种方法来完成:
、watchPosition()
、clearWatch()
。这三种方法在表 12-1 中描述。
看着表 12-1 ,你可能想知道这些结果如何使最终用户受益。地理定位 API 仅仅给你用户的位置。如何以创新的方式使用这些数据取决于你。考虑以下情况,地理定位 API 在这些情况下非常有用:
- Travel companies can use the location to provide a list of nearby pick-up points.
- The application can suggest driving directions to the user according to the location information.
- A work portal can only present those jobs that are within the distance specified by the user.
- E-commerce websites can use location information to suggest freight to users. Real estate applications can customize search results according to the location of users.
- Museums, theaters, assembly halls and other institutions can calculate the approximate travel time from the user’s location to the meeting place.
地理定位和用户隐私
用户的地理位置被视为私人信息,用户需要提供明确的批准,浏览器才能将数据发送到服务器进行进一步处理。每当您访问使用地理定位 API 的网页时,浏览器会通知您将要与该页面共享您的位置信息,并提示您确认此操作。例如,图 12-1 展示了 Firefox、Chrome 和 IE9 是如何提示用户确认使用地理定位 API 的。
***图 12-1。*浏览器请求允许向服务器发送位置信息
如您所见,除非用户明确授权在 web 上传输他们的位置,否则 Web 应用无法使用地理定位 API。用户可以使用浏览器选项随时撤销此权限。
在某些情况下,您可能还想将用户的位置信息发送给第三方系统。作为一个好的实践,最好让用户知道这样的共享。
使用地理定位 API 获取用户位置
现在您已经对地理定位 API 的功能有了基本的了解,让我们开发一个简单的基于 Web 表单的应用,演示如何使用getCurrentPosition()
方法找到用户的位置。应用的主 web 表单如图 12-2 所示。
**图 12-2。**使用getCurrentPosition()
方法找到用户的位置
web 表单由一个按钮和一个表格组成。点按“显示当前位置”按钮会用位置信息填充表格。请注意,一些表格单元格包含未定义的值,表明这些信息不可用。
负责检索位置信息的 jQuery 代码在清单 12-1 中给出。
***清单 12-1。*检索用户的位置信息
`$(document).ready(function () {
if (!Modernizr.geolocation) {
alert(“This browser doesn’t support the Geolocation API.”);
return;
}
$(“#btnShowCurrent”).click(function () {
var options = {
enableHighAccuracy: false,
timeout: 5000,
maximumAge: 3000
};
window.navigator.geolocation.getCurrentPosition(OnSuccess, OnError, options);
});
});`
这段代码显示了ready()
函数,它检查浏览器是否支持地理位置 API。这是使用Modernizr
对象的geolocation
属性完成的。然后代码将一个事件处理函数连接到 Show Current Location 按钮的click
事件。
click
事件处理函数定义了一个options
对象,有三种设置:enableHighAccuracy
、timeout
和maximumAge
。该对象指定了getCurrentPosition()
方法要使用的选项。这三个选项的重要性在表 12-2 中描述。
清单 12-1 中的代码然后调用geolocation
对象的getCurrentPosition()
方法。这个方法接受三个参数:一个在成功检索地理位置数据时调用的success
函数,一个在检索地理位置信息时出错时调用的error
函数,以及一个options
对象。指定error
函数和options
对象是可选的。
充当成功回调的OnSuccess()
函数如清单 12-2 中的所示。
清单 12-2。 OnSuccess()
回调函数
function OnSuccess(position) { var html = ""; html += "<tr><td>Latitude : </td>"; html += "<td>" + position.coords.latitude + "</td></tr>"; html += "<tr><td>Longitude : </td>"; html += "<td>" + position.coords.longitude + "</td></tr>"; html += "<tr><td>Accuracy : </td>"; html += "<td>" + position. coords.accuracy + "</td></tr>"; html += "<tr><td>Altitude : </td>"; html += "<td>" + position. coords.altitude + "</td></tr>"; html += "<tr><td>Altitude Accuracy : </td>"; html += "<td>" + position. coords.altitudeAccuracy + "</td></tr>"; html += "<tr><td>Heading : </td>"; html += "<td>" + position. coords.heading + "</td></tr>"; html += "<tr><td>Speed : </td>"; html += "<td>" + position. coords.speed + "</td></tr>"; html += "<tr><td>Timestamp : </td>"; html += "<td>" + new Date(position.timestamp).toString() + "</td></tr>"; $("#tblInfo").append(html); }
OnSuccess()
接收一个position
对象,该对象提供关于用户位置的信息,包括纬度和经度。这些属性中的大多数(除了时间戳)都可以在position
对象的coords
属性中获得。position
对象给出的信息在表 12-3 中列出。
并非所有设备都具备表 12-3 中讨论的所有属性。例如,台式计算机不提供属性,如speed
和heading
。四个属性latitude
、longitude
、accuracy
、timestamp
在所有设备上都可用;不保证支持其他属性。如果属性没有可用数据,则返回null
。
清单 12-2 中的代码读取各种属性,并将这些值添加到一个 HTML 表(tblInfo
)中。如果在查找用户位置时出现错误,则调用OnError()
函数。OnError()
如清单 12-3 所示。
清单 12-3。 OnError()
功能显示错误信息
function OnError(err) { alert(err.code + " : " + err.message); }
OnError()
函数非常简单。传递给函数的err
对象有两个属性,它们提供了关于错误的更多信息:code
和message
。可能的错误代码有1
( PERMISSION_DENIED
)、2
( POSITION_UNAVAILABLE
)和3
( TIMEOUT
)。message
属性给出了一个描述性的错误消息。图 12-3 显示了使用地理定位 API 的权限被拒绝时的错误消息。
***图 12-3。*使用地理定位 API 的权限被拒绝后的错误消息
图 12-3 显示了一个警告框中的code
和message
属性。当然,您可以检查code
属性,并为每个错误代码显示一条更友好的消息。
通过地图应用使用地理定位 API
地理定位 API 的一个常见用途是将地图服务与 web 应用集成在一起。这种集成对于显示用户相对于一组其他位置的位置、建议驾驶路线以及计算两个地方之间的距离是有用的。Google Maps 和 Bing Maps 地图服务很受欢迎,通常由开发人员在 web 应用中使用。使用这些服务包括以下步骤:
- Get the API key from the service provider (Google or Bing).
- Refers to the map API library, which allows you to program map services in web pages.
- 在你的 ASP.NET 网络应用中嵌入地图。
- Integrate map service with geolocation API, and customize the map according to your needs.
Google Maps 和 Bing Maps 要求您拥有一个 API 密钥,以便在您的 web 应用中使用它们的地图服务。API 密钥是为一个帐户分配的,您可以从相应的网站获取 API 密钥。本节稍后讨论的示例假设您已经从地图服务提供商处获得了有效的 API 密钥。
Google Maps 和 Bing Maps 允许您使用供应商开发的基于 JavaScript 的 API 对地图进行编程。您需要在 web 页面中添加对这个 JavaScript 库的引用,以使用它的特性。这些库提供的功能包括显示地图上的特定位置、设置地图的缩放级别、显示位置的标注等等。本节中的示例使用了库的一些基本特性。
使用 JavaScript 库,您可以轻松地将地图嵌入到自己的网页中。例如,您可以在<div>
元素中显示地图。
地理位置 API 和地图服务相互独立。但是,您可以集成它们以提供更好的用户体验。例如,与其显示一个固定位置的地图并期望用户放大或缩小他们的位置,不如默认地图突出显示用户的位置。
注意Google Maps API 和 Bing Maps API 提供了许多你可以在网络应用中使用的可编程特性。对这些特性的详细讨论超出了本书的范围。本章使用 Google Maps 和 Bing Maps 只是为了说明地理定位 API 与地图服务的集成。您可以访问地图服务提供商的网站([
developers.google.com/maps](https://developers.google.com/maps)
和[www.microsoft.com/maps/developers/web.aspx](http://www.microsoft.com/maps/developers/web.aspx)
)获取完整的文档。
将地理定位 API 与谷歌地图集成
在本节中,您将学习如何将地理定位 API 与 Google Maps 集成。图 12-4 显示了你开发的 web 表单。
***图 12-4。*将地理定位 API 与谷歌地图整合
如您所见,web 表单由一个按钮和一个保存地图的<div>
元素组成。当页面加载到浏览器中时,默认情况下,地图以孟买为中心。如果单击“显示当前位置”,将显示一个标注,指向用户的当前位置。
要开发这个应用,您需要在标记中引用 Google Maps JavaScript 库。清单 12-4 显示了这是如何做到的。
***清单 12-4。*参考谷歌地图 API 库
<html> <head> …
`** <script type=“text/javascript” src=“http://maps.googleapis.com/maps/api/**
** js?key=<%= ConfigurationManager.AppSettings[“GoogleMapsAPIKey”] %>&**
** sensor=true”>**
…
…
… `
粗体显示的标记行指的是 Google Maps API 库。URL 包括两个强制查询字符串参数:key
和sensor
。key
查询字符串参数指定了一个 API 键。因为密钥可能会根据使用的 Google 帐户而改变,所以它不会嵌入到标记中。相反,密钥存储在web.config
文件的<appSettings>
部分,并在<%=
和%>
块中检索。GoogleMapsAPIKey
是<appSettings>
部分中键的名称。sensor
查询字符串参数指示运行此 web 应用的设备是否使用传感器(如 GPS 定位器)来确定用户的位置。
清单 12-4 中的其他标记很简单,包括一个呈现显示当前位置按钮的<input>
标签和一个充当地图容器的<div>
。
当页面加载到浏览器中时,使用 Google Maps API 显示孟买的 jQuery 代码如清单 12-5 所示。
***清单 12-5。*使用谷歌地图 API 显示地图
`var map;
var defaultPos;
KaTeX parse error: Expected '}', got 'EOF' at end of input: …oogle.maps.Map((“#divMap”).get(0), mapOptions);
$(“#btnShowCurrent”).click(function () {
window.navigator.geolocation.getCurrentPosition(OnSuccess, OnError);
});
});`
这段代码从声明两个名为map
和defaultPos
的全局变量开始。map
变量保存对 Google 地图对象的引用,而defaultPos
变量保存地图上的默认位置。ready()
方法处理程序通过传递孟买的纬度和经度来创建一个由 Google Maps API 提供的LatLng
对象。这个LatLng
对象作为地图的默认中心。然后,代码创建一个包含地图配置设置的mapOptions
对象。center
选项定义了地图的中心坐标。zoom
设置影响地图的缩放级别。缩放值越大,地图越放大。mapTypeId
设置控制显示的地图类型。mapTypeId
的值可以从ROADMAP
、SATELLITE
、HYBRID
和TERRAIN
等常量中分配。ROADMAP
的值表示显示二维地图。然后通过将 DOM 引用传递给充当地图容器的<div>
元素和地图选项来构造一个Map
对象。这将在<div>
元素中显示一幅以孟买为中心的地图。
代码还显示了显示当前位置按钮的click
事件处理函数。click
事件处理程序调用geolocation
对象上的getCurrentPosition()
方法,并在参数中传递两个回调函数——OnSuccess()
和OnError()
。OnSuccess()
和OnError()
如清单 12-6 所示。
清单 12-6*。** OnSuccess()
和OnError()
回调函数*
`function OnSuccess(position) {
var curPos = new google.maps.LatLng(position.coords.latitude,
position.coords.longitude);
map.setCenter(curPos);
var callout = new google.maps.InfoWindow();
callout.setContent(“This is your current location.”);
callout.setPosition(curPos);
callout.open(map);
}
function OnError(err) {
alert(err.message);
map.setCenter(defaultPos);
var callout = new google.maps.InfoWindow();
callout.setContent(“This is the default location.”);
callout.setPosition(defaultPos);
callout.open(map);
}`
OnSuccess()
使用由coords
对象提供的latitude
和longitude
属性创建一个新的LatLng
对象。这个LatLng
对象代表用户的位置。然后使用map
对象的setCenter()
方法设置新LatLng
对象的中心。要向用户显示指向用户位置的标注,可以使用 Google Maps API 提供的InfoWindow
对象。InfoWindow
对象的setContent()
方法表示标注的内容。InfoWindow
对象的setPosition()
方法控制放置标注的位置。最后,InfoWindow
对象的open()
方法显示指定map
对象上的标注。
OnError()
类似于OnSuccess()
,但是在检索用户位置时发生错误时被调用。OnError()
向用户显示一条错误消息,并在默认位置显示一个标注(本例中为 Mumbai)。
将地理定位 API 与 Bing 地图集成
将地理定位 API 与 Bing 地图集成的过程类似于 Google 地图。但是,这一次,您需要使用 Bing Maps API 和相应的 API 键。下面一行标记显示了如何在网页中引用 Bing Maps API:
<script src="http://dev.virtualearth.net/mapcontrol/ mapcontrol.ashx?v=7.0" type="text/javascript"> </script>
请注意,Bing 地图文档将这个库称为 Ajax controlm,但它实际上是一个 JavaScript 库,可以使用普通的<script>
标记来引用,如图所示。这一次,API 键没有添加到库的 URL 中。相反,API 键是在映射选项中指定的,如清单 12-7 所示。
***清单 12-7。*使用 Bing 地图 API 显示地图
$(document).ready(function () { … defaultPos = new Microsoft.Maps.Location(18.916667, 72.9); var mapOptions = { credentials: '<%= ConfigurationManager.AppSettings["BingMapsAPIKey"] %>', center: defaultPos, mapTypeId: Microsoft.Maps.MapTypeId.road, zoom: 8 }; map = new Microsoft.Maps.Map($("#divMap").get(0), mapOptions); … });
这个清单类似于前面的例子。这里,代替LatLng
对象,使用Location
对象来表示地图位置。地图选项包括credentials
、center
、zoom
、mapTypeId
。credentials
设置保存存储在web.config
文件的<appSettings>
部分的 Bing 地图 API 密钥。其他设置显而易见,无需解释。
这次的OnSuccess()
和OnError()
函数使用一个Infobox
对象来显示位置标注。这些功能如清单 12-8 中的所示。
***清单 12-8。*使用必应地图 API 的Infobox
对象
`function OnSuccess(position) {
var curPos = new Microsoft.Maps.Location(position.coords.latitude,
position.coords.longitude);
var calloutOptions = {title: “Location Information”,
description: “This is your current location.”};
var callout = new Microsoft.Maps.Infobox(curPos, calloutOptions);
map.entities.push(callout);
}
function OnError(err) {
alert(err.message);
var calloutOptions = {title: “Location Information”,
description: “This is the default location.”};
var callout = new Microsoft.Maps.Infobox(defaultPos, calloutOptions);
map.entities.push(callout);
}`
基于用户的位置创建一个新的Location
对象。calloutOptions
对象保存标注中显示的标题和描述。通过传递期望的位置(在本例中是用户的位置)和calloutOptions
对象来创建一个Infobox
对象。最后,通过调用entities
对象的push()
方法并将Infobox
作为参数传递来显示标注。图 12-5 显示必应地图中的一张地图。
***图 12-5。*必应地图显示一个Infobox
如您所见,点击显示当前位置按钮打开了Infobox
。title
设置控制Infobox
的标题,description
设置控制其内容。
使用地理定位 API 呈现特定位置的数据
地理定位 API 并不局限于地图应用。您可以以创新的方式使用它,为您的 web 应用添加位置感知功能。在本节中,您将开发一个 ASP.NET MVC 应用,它根据用户的位置显示职位空缺。该应用由一个类似于图 12-6 的视图组成。
***图 12-6。*基于用户位置搜索工作
该视图顶部有一个数字<input>
字段。用户可以指定以米为单位的距离,以表明只列出指定距离内的作业。当用户点击 Show 按钮时,使用地理定位 API 捕获他们的位置;根据用户的位置和指定的距离,仅显示位于指定距离范围内的职务公告。例如,如果用户的位置是孟买,并且指定的距离是 150000 米(150 公里),那么将显示来自孟买和浦那的工作发布,因为这两个位置在指定的 150 公里范围内。班加罗尔和钦奈的职位发布将不会显示,因为这些位置不在指定范围内。
该应用使用两个表——Jobs
和Locations
——分别存储职位发布和位置、坐标。这些表格的实体框架数据模型如图图 12-7 所示。
***图 12-7。*实体框架数据模型Jobs
和位置表
Jobs
表存储JobTitle
、Description
和LocationName
,而Locations
表存储LocationName
、Latitude
和Longitude
。清单 12-9 中显示了执行查找用户位置和获取相关职位发布的代码。
***清单 12-9。*根据用户位置和距离查找招聘信息
$(document).ready(function () { … $("#btnShow").click(function () { window.navigator.geolocation.getCurrentPosition(function (position) { var lat1 = position.coords.latitude; var long1 = position.coords.longitude; var distance = $("#txtDistance").val(); var data = '{ "lat1" : "' + lat1 + '","long1":"' + long1 + '","distance":"' + distance + '"}'; $.ajax({ type: "POST", url: '/home/GetJobs', data: data, contentType: "application/json; charset=utf-8", dataType: "json", success: function (jobs) { $("#tblJobs").empty(); $("#tblJobs").append("<tr><th>Job Title</th><th>Description</th><th>Location</th></tr>"); for (var i = 0;i<jobs.length;i++) { $("#tblJobs").append("<tr><td>" + jobs[i].JobTitle + "</td><td>" + jobs[i].Description + "</td><td>" + jobs[i].LocationName + "</td></tr>"); } }, error: function (err) { alert(err.status + " - " + err.statusText); } });
}); }); });
这段代码显示了显示按钮的click
事件处理函数。它首先使用geolocation
对象的getCurrentPosition()
方法检索用户的位置。这一次,success
函数与getCurrentPosition()
调用一起提供,而不是作为一个单独的函数。用户位置的纬度和经度值存储在本地变量lat1
和long1
中。使用来自变量和<input>
字段的值形成一个带有三个键的 JSON 对象,这三个键是lat1
、long1
和distance
。
接下来,使用 jQuery $.ajax()
方法向GetJobs()
action 方法发出一个 Ajax 请求。GetJobs()
返回零个或多个Job
对象,稍后会讨论。$.ajax()
方法的success
函数接收GetJobs()
返回的Job
对象。然后,它遍历 jobs 数组,并将作业添加到 HTML 表中。该表包含Job
对象的Title
、Description
和LocationName
属性。
$.ajax()
方法的错误处理函数在一个警告框中显示错误消息。基于用户位置和指定距离返回相关作业的GetJobs()
动作方法如清单 12-10 所示。
清单 12-10。 GetJobs()
动作方法
`[HttpPost]
public JsonResult GetJobs(double lat1, double long1, double distance)
{
JobsDbEntities db = new JobsDbEntities();
var data = from item in db.Jobs
select item;
List selectedJobs = new List();
foreach(Job job in data)
{
var temp = from item in db.Locations
where item.LocationName==job.LocationName
select item;
double lat2 = (double)((Location)temp.SingleOrDefault()).Latitude;
double long2 = (double)((Location)temp.SingleOrDefault()).Longitude;
if (GetDistance(lat1, long1, lat2, long2) <= distance)
{
selectedJobs.Add(job);
}
}
var finalData = from obj in selectedJobs
orderby obj.LocationName
select obj;
return Json(finalData);
}`
来自家庭控制器的GetJobs()
动作方法采用三个类型为double
的参数。lat1
和long1
参数代表用户所在位置的纬度和经度。distance
参数代表用户指定的距离。然后,该方法遍历所有可用的职位发布。通过每次迭代,确定工作位置的纬度和经度。 GetDistance()
辅助方法确定用户位置(lat1
、long2
)和作业位置(lat2
、long2
)之间的距离。如果GetDistance()
返回的距离小于或等于用户指定的距离,则职位发布被添加到Job
对象的通用List
中。在过滤后的职位发布发送给用户之前,selectedJobs
通用List
在LocationName
上排序。最后,使用Json()
方法将Job
对象的排序列表以 JSON 格式发送给调用者。确定用户位置和工作位置之间距离的GetDistance()
方法如清单 12-11 所示。
***清单 12-11。*查找用户位置和工作位置之间的距离
private double GetDistance(double lat1,double long1,double lat2,double long2) { GeoCoordinate point1 = new GeoCoordinate(lat1, long1); GeoCoordinate point2 = new GeoCoordinate(lat2, long2); double distance = point1.GetDistanceTo(point2); return distance; }
这段代码使用了来自System.Device.Location
名称空间(位于System.Device.dll
程序集中)的GeoCoordinate
类。GeoCoordinate
s 代表一个地点的地理坐标。GeoCoordinate
构造函数接受一个位置的纬度和经度。GeoCoordinate
类的GetDistance()
方法返回该点和参数中指定的另一个点之间的距离。然后,以米为单位的距离被返回给调用者。
注意,为了简单起见,这段代码从表中检索所有的行,然后逐个遍历它们。更复杂的解决方案是计算最大纬度和经度值,然后只检索在该范围内的行。
注此。就属性而言,NET Framework 的GeoCoordinate
类类似于地理定位 API 的coords
对象。GeoCoordinate
使用哈弗辛公式计算距离。该公式将地球视为球形而非椭球形,并且不使用高度来计算距离。在计算长距离时,哈弗辛公式引入了小于 0.1%的误差。
使用地理定位应用接口追踪移动
在前面的例子中,您使用了geolocation
对象的getCurrentPosition()
方法来获取用户的当前位置。当您希望在调用方法时知道用户的位置,并且对持续跟踪用户不感兴趣时,这种方法非常有用。但是,在某些情况下,当用户从一个地方移动到另一个地方时,您需要一直监视用户的位置。例如,您可能想要监视用户从一个位置向另一个位置移动的距离,并且您可能想要定期通知用户到目标位置的剩余距离。在这种情况下,您可以使用geolocation
对象的watchPosition()
方法。这种方法在语法上类似于getCurrentPosition()
;但与getCurrentPosition()
不同的是,它每隔一段时间就会调用success
函数。连续调用success
函数的确切间隔由设备控制。只有当用户的位置改变时,才会调用success
回调函数。因此,在台式计算机上,getCurrentPosition()
和watchPosition()
的行为方式相同,因为计算机的位置不会改变。
如果基于某种条件,您希望停止监视用户的位置,该怎么办?watchPosition()
方法返回一个数字,该数字的作用类似于调用watchPosition()
的句柄。你可以将这个数字传递给clearWatch()
方法来停止监视用户的位置。清单 12-12 中的代码展示了如何使用watchPosition()
和clearWatch()
。
清单 12-12*。*使用watchPosition()
和clearWatch()
方法
`var watchId;
function StartWatch() {
watchId = window.navigator.geolocation.watchPosition(OnSuccess, OnError);
}
function StopWatch() {
window.navigator.geolocation.clearWatch(watchId);
};`
这段代码定义了一个名为watchId
的全局变量来存储watchPosition()
返回的手表句柄。StartWatch()
函数调用geolocation
对象的watchPosition()
方法,并像以前一样传递OnSuccess
和OnError
回调函数。返回的数字句柄存储在watchId
变量中。StopWatch()
调用geolocation
对象的clearWatch()
方法,并将watchId
作为参数传递,以清除该监视。
总结
地理定位 API 允许您根据纬度和经度找到用户的地理位置。navigator
对象的geolocation
对象属性公开了负责检索用户位置的方法。getCurrentPosition()
方法返回用户的当前位置。在调用clearWatch()
方法之前,watchPosition()
方法会一直监视那个位置。
使用地理位置 API,您可以构建基于用户位置显示数据的位置感知 web 应用。您还可以将地理定位 API 与谷歌地图和必应地图等地图服务集成在一起。
您已经了解了 HTML5 所有突出的可编程特性。尽管作为一名 web 开发人员,您的核心重点是可编程特性,但有时您也需要使用级联样式表(CSS)来设计 web 应用的样式。CSS3 提供了这方面的最新信息。下一章将介绍一些新的和改进的特性。
十三、使用 CSS3 设计 Web 表单和视图的样式
作为一名希望利用 HTML5 能力的 ASP.NET 开发人员,您最感兴趣的领域是 HTML5 的可编程特性。然而,作为开发真实世界的专业 web 应用的一部分,您还需要研究这些 web 应用的样式方面。当涉及到设计 web 表单和视图的样式时,层叠样式表(CSS)是事实上的标准,CSS3 增加了许多增强功能,使得样式更好。CSS3 不是 HTML5 的一部分,但是它们一起发展,很好地互补。
CSS3 规范将对 CSS 2.1 的改进分成了所谓的模块。CSS3 中大约有 50 个模块。将改进和添加的内容分组到模块中的想法是,浏览器供应商可以决定在他们的产品中实现哪些模块。当一个模块被实现时,开发者也知道他们可以使用哪些特性。因为 CSS3 仍然是一个不断发展的规范,这种模块化的方法使得浏览器供应商能够以一种渐进的方式支持和改进 CSS3 的特性。
CSS3 增加了许多新功能,包括圆角边框、web 字体、阴影、透明度和变换等。本章详细介绍了一些开发人员经常需要的重要 CSS3 特性,因为它们在设计网页样式时经常用到。使用这些特性,您可以更好地美化 web 表单和视图。具体来说,您将了解以下内容:
- Use CSS3 selector
- Use custom fonts automatically downloaded by clients.
- Use rounded corners, shadows, gradients and transparency enhancement boxes.
- Use transitions and transformations
- Media query for different devices
本章最后介绍了如何使用 Modernizr 来应用特定于 CSS3 的特性。
CSS3 选择器
在第二章中,您了解了 jQuery 选择器,它允许您选择元素,以便在 jQuery 代码中进一步操作。CSS 选择器做完全相同的工作,不同的是它们为选择元素是为了应用样式。事实上,jQuery 选择器是基于 CSS 选择器的思想。CSS3 为 CSS 中已经存在的选择器添加了许多新的选择器。本节介绍一些重要的。
如果您过去使用过 CSS,那么您可能已经熟悉了选择器的概念。考虑下面的 CSS 类:
#mydiv{ border: 2px #f00 solid; }
这个类使用了 ID 选择器,并应用于 ID 属性为mydiv
的 DOM 元素。另外两个常用的 CSS 选择器是元素选择器和类选择器。一个元素选择器选择特定标签名称的所有元素,而一个类选择器选择所有属性class
被设置为特定 CSS 类名的元素。以下示例显示了这两个选择器:
div{ border: 2px #f00 solid; } .myclass{ border: 2px #f00 solid; }
第一个 CSS 类使用一个元素选择器来选择所有的<div>
元素,并对它们应用指定颜色和宽度的边框。第二个类选择所有属性设置为myclass
的 DOM 元素,并对它们应用指定颜色和宽度的边框。
注除了刚才讨论的选择器,CSS 2.1 还提供了其他几个,但本章不讨论。这里我们只关注 CSS3 中新添加的选择器。
下面几节中讨论的 CSS3 新添加的选择器可以分为四类:
- Attribute-substring selector: Allows you to select an attribute value to specify a string.
- The beginning, end or element containing the specified string; Structural pseudo-class: allows you to select elements, such as specific sub-elements, according to their structural positions.
- Element state pseudo-class: allows you to select elements in a specific state, such as enabled, disabled or selected elements.
- Other pseudo-classes: provided by
以下部分详细讨论了每组选择器。
属性-子串选择器
属性-子字符串选择器允许您选择属性值以指定字符串开头、结尾或包含指定字符串的 DOM 元素。这三个选择器在表 13-1 中列出。
让我们用一个例子来说明这三个属性-子串选择器。假设您有一个包含多个超链接的网页。其中一些指向 URL,一些指向电子邮件地址,如下所示:
<a href="http://www.microsoft.com">Go to Microsoft's website.</a> <a href="mailto:contact@somedomain.com">Contact us here.</a>
现在假设您希望以红色显示所有以http://
开头的超链接,以蓝色显示所有以mailto:
开头的超链接。您可以使用属性子字符串选择器来实现这一点:
a[href^="http://"] { color:red; font-size:30px; } a[href^="mailto:"] { color:blue; font-size:30px; }
这些类中使用的 CSS 选择器使用^=
操作符将锚元素的href
属性的开始与特定的字符串相匹配。第一个选择器选择所有以http://
开头的超链接,并将它们的颜色设置为红色;另一个选择器选择所有以mailto:
开头的超链接,并将它们的颜色设置为蓝色。图 13-1 显示了这些链接在浏览器中的样子。
图 13-1。“属性以”选择器开始动作
现在,进一步假设同一个页面包含某些 PDF 文件的 URL,并且您希望用绿色呈现这些超链接。在这种情况下,您可以使用如下所示的“属性结尾”选择器:
a[href$=".pdf"] { color:green; font-size:30px; }
“属性结束于”选择器使用$=
操作符将属性值的结尾与指定的字符串匹配。在这个例子中,href
属性值的结尾与".pdf"
相匹配,所有匹配标准的超链接都以绿色显示。匹配超链接的示例如下:
<a href="downloads/ebook.pdf">Download eBook here.</a>
进一步扩展这个想法,您还可以显示包含(而不是以)特定文本开始或结束的超链接。例如,要用橙色显示所有包含单词 google 的超链接,可以使用
a[href*="google"] { color:orange; font-size:30px; }
如您所见,*=
操作符检查每个超链接元素的href
属性,查看它是否包含单词 google 。如果是这样,超链接的颜色将设置为橙色。下面是一个匹配超链接的示例:
<a href="http://www.somedomain.com/google/api">…</a>
指定的字符串可以位于属性值的开头、结尾或两者之间的任何位置。
结构伪类
有时,您希望根据元素在 DOM 树中的结构位置对元素应用样式。例如,您可能希望对表格的第一行和最后一行应用某种样式。在这种情况下,结构化伪类就派上了用场。表 13-2 列出了结构伪类。
让我们看一些使用结构伪类的例子。假设您希望以某种颜色显示 HTML 表格的最后一行。您可以使用:last-child
伪类来实现,如下所示:
tr:last-child { background-color:#808080; font-size:20px; }
这个 CSS 选择器选择作为其父元素的最后一个子元素的所有<tr>
元素(即<table>
元素),并对它们应用指定的背景颜色和字体大小。图 13-2 显示了应用了样式的表格的外观。
**图 13-2。**使用:last-child
伪类样式的表格的最后一行
处理表格时的一个常见要求是用不同的颜色显示交替行。这可以通过使用:nth-child
选择器来实现。以下 CSS 选择器对表格的奇数行和偶数行应用不同的样式:
tr:nth-child(odd) { background-color:#fff; } tr:nth-child(even) { background-color:#808080; }
这里,所有奇数行具有背景色#fff
,所有偶数行具有背景色#808080
。结果表格如图图 13-3 所示。
**图 13-3。**使用:nth-child
伪类应用了不同样式的奇数行和偶数行
odd
和even
关键字分别表示奇数行和偶数行。您也可以指定行号:例如,tr:nth-child(2) {…}
。编号从 1 开始。
类型选择器类似于子选择器,但是它们适用于指定类型的兄弟,而不是子。考虑这样一种情况,您有许多<p>
元素,并且您希望第一段的第一个字母以特定的方式显示。因为所有的<p>
元素都是彼此的兄弟,所以使用一个类型伪类。对于这种特殊情况,您可以如下使用:first-of-type
:
p:first-of-type::first-letter { font-size:50px; float:left; line-height:1; margin-right:5px; }
该:first-of-type
选择器选择第一个<p>
元素。然后,双冒号(::
)语法使用::first-letter
伪元素选择该段落的第一个字母,并应用指定的样式。图 13-4 显示了段落的外观。
***图 13-4。*使用:first-of-type
选择器选择第一段内容
注意,特定的样式只应用于第一段的第一个字母,因为使用了:first-of-type
伪类。其他<p>
元素不受样式的影响。
注意在 CSS3 之前,伪类和伪元素的前缀都是冒号(:
)。为了区分它们,CSS3 用单冒号(:
)给伪类加前缀,用双冒号(::
)给伪元素加前缀。这使您能够快速识别出:first-of-type
是一个伪类,而::first-letter
是一个伪元素。
元素状态伪类
元素状态伪类在处理表单字段时非常有用,因为它们根据状态选择元素。例如,假设您希望对所有选中的复选框应用特定的样式,或者以特定的方式显示禁用的元素。这些伪类在表 13-3 中列出。
为了理解如何使用元素状态伪类,考虑以下选择器:
input[type="text"]:enabled { background:#fff; } input[type="text"]:disabled { background:#808080; } input:checked { border:2px #f00 solid; }
当应用于类型为text
的输入元素时,:enabled
伪类返回所有已启用的文本框,并对它们应用指定的样式。类似地,:disabled
伪类选择所有被禁用的文本框,并应用指定的样式。:checked
伪类选择所有的复选框,并按照指定给它们添加一个边框。图 13-5 展示了这些伪类的作用。
图 13-5。:enabled``:disabled``:checked
动作中的伪类
杂项伪类
本节讨论的 CSS 伪类不属于前面讨论的任何类别。这些伪类包括否定伪类(:not()
)和一般的兄弟组合子伪类。
当您希望选择不匹配特定条件的元素时,可以使用 negation 伪类。例如,假设一个表单包含几个<input>
元素。在所有这些<input>
元素中,只有一个是submit
类型的;其余均为text
、checkbox
和radio
类型。如果希望将样式应用于除提交按钮之外的所有<input>
元素,可以使用如下所示的:not()
伪类来实现:
input:not([type="submit"]) { background-color:#808080; }
该选择器选择type
属性不是submit
的元素。这些元素的背景颜色被设置为#808080
。
一般的兄弟组合子由两个简单的选择器组成,用波浪符号(~
)分隔。它选择第一种类型的元素之前的第二种类型的元素。两个元素必须有相同的父元素,但是第二个元素不必紧接在第一个元素之前。例如,假设您有一个包含其他元素如<ul>
、<input>
和<span>
的<div>
元素,如下所示:
`
- One
- Two
- Three
- Four
您希望将样式应用于出现在<div>
元素之后并出现在<div>
中任何位置的<ul>
兄弟元素。您可以使用以下 CSS 规则来实现这一点:
div ~ ul { background-color:#ff6a00; }
图 13-6 显示了最终的结果。
***图 13-6。*通用兄弟组合器
如您所见,第一个<ul>
是<div>
的子节点,而不是兄弟节点,因此指定的样式没有应用于它。第二个<ul>
是<div>
元素的兄弟,所以通用兄弟组合器选择它并应用指定的背景颜色。
特定于浏览器的属性前缀
如前所述,CSS3 规范仍在发展中,不同的浏览器供应商对它们的支持程度也各不相同。目前,浏览器实现的特定于 CSS3 的特性分为两大类:完全符合规范的特性,以及已经实现但还不完全符合规范的特性。为了区分这两种类型,浏览器供应商对属于第二类的特性使用他们自己的前缀。这些前缀在表 13-4 中列出。
考虑一个名为rotate
的 CSS 类,如下所示:
.rotate { border-radius: 25px; -ms-transform: rotate(10deg); }
CSS 类使用了两个 CSS3 属性:border-radius
和-ms-transform
。border-radius
属性没有附加前缀,这表明它的特性是完整的。另一方面,transform
属性使用-ms-
前缀,表示应该使用特定于 Internet Explorer 的功能不完整的实现。在后面讨论的一些例子中,你可以使用表 13-4 中列出的特定于浏览器的前缀。
注意你可以使用 Modernizr 在 JavaScript 代码中检测浏览器对 CSS3 特性的支持,就像其他 HTML5 特性一样。然而,Modernizr 并没有为所有 CSS3 特性提供检测属性。
使用网络字体
通过恰当地使用字体,你可以使你的网页吸引人并且易于阅读。不幸的是,web 开发人员和设计人员不得不满足于一小部分字体,以确保他们的 web 应用在运行于各种操作系统的所有浏览器上看起来都是一样的。如果您在网页中使用的字体没有与操作系统捆绑在一起,则不能保证客户端机器安装了相同的字体。因此,您的网页在最终用户看来可能与在您的机器上不同。这就是为什么 web 开发人员和设计人员经常限制自己使用众所周知的 web 安全字体,这种字体可能在各种计算机系统上都可用;这些字体包括 Arial、Verdana 和 Times New Roman。
CSS3 提供了一种新的使用字体的方式,叫做网络字体。使用此功能,您可以在网站上承载网页使用的非标准字体。然后使用@font-face
CSS3 规则声明一个自定义字体定义。在运行时,客户端浏览器从样式表中读取自定义字体定义,并将字体下载到客户端机器。然后,该字体将用于您的网页。每个想要使用非标准字体的网站都必须在网站的样式表中定义它。
网络字体格式
尽管 CSS3 让你在网页中使用非标准字体变得很容易,但是还有一个地方需要解决:字体文件格式。就像媒体文件一样,字体文件包括多种字体,并且对于这些格式没有一个 web 标准。表 13-5 列出了目前使用的字体文件格式。
因为 web 字体没有单一的标准文件格式,并且开发人员可能需要在其网站上托管多种文件格式,所以一些网站提供这些文件格式的字体供下载。一个流行的网站是 Font Squirrel ( [www.fontsquirrel.com](http://www.fontsquirrel.com)
),它提供了所谓的@font-face
工具包——这些文件格式的字体集。图 13-7 显示了名为 Magenta 的字体松鼠下载页面。
**图 13-7。**从字体松鼠网站下载@font-face
工具包
您可以选择下载字体格式。默认情况下,会选择 TIF、EOT、WOFF 和 SVG 格式。下载的@font-face
工具包(一个 Zip 文件)包含所有的字体格式、许可信息和一个演示 HTML 页面。
除了从网站下载一个@font-face
工具包,然后上传到你自己的网站,还有一个选择:Google Web Fonts。谷歌网络字体提供了在你的网页中使用的网络表单,无需下载。你所需要做的就是在你的网页中添加一个样式表引用,就像 Google Web Font 网站上所指示的那样([www.google.com/webfonts](http://www.google.com/webfonts)
)。在运行时,Google Web 字体网站会自动检测浏览器,并以支持的格式向其发送字体。图 13-8 显示了谷歌网页字体网站。
***图 13-8。*谷歌网页字体网站
在接下来的部分中,你将学习如何使用@font-face
工具包文件以及 Google Web Fonts 网站。
使用@font-face 规则
现在你知道了什么是 web 字体以及如何获得它们,让我们看看如何在网页中使用它们。假设您已经下载了前面示例中提到的洋红色字体。要使用 web 字体,需要在样式表文件中创建一个自定义字体定义。这里显示了一个字体定义示例:
@font-face { font-family: MyWebFont; src: url('Fonts/Magenta_BBT-webfont.ttf'), url('Fonts/Magenta_BBT-webfont.eot'),
url('Fonts/Magenta_BBT-webfont.svg'), url('Fonts/Magenta_BBT-webfont.woff'); }
@font-face
规则从定义一个名为 MyWebFont 的字体系列开始。字体系列名称可以是开发人员定义的任何名称。如果您有多个@font-face
规则,每个规则应该有一个唯一的字体系列名称,以避免歧义。然后src
属性使用url()
函数指向字体文件所在的一个或多个 URL。在这个例子中,所有的字体文件都位于一个名为Fonts
的文件夹中。对于您希望支持的每种字体格式,您必须指定一个 URL。
一旦您使用@font-face
定义了一个自定义字体定义,您就可以在其他 CSS 规则中使用该字体。例如,下面的 CSS 规则将 MyWebFont 用于<h1>
标记:
h1 { font-family: 'MyWebFont'; font-size:40px; text-align:center; }
如您所见,在 CSS 规则中,您可以像使用任何其他标准字体一样使用自定义字体。在这种情况下,font-family
属性被设置为MyWebFont
,并且font-size
和text-align
属性也被设置。图 13-9 显示了浏览器中的顶层标题。
图 13-9。 @font-face
应用于页面标题
即使本地机器没有特定的字体,网页也可以用该字体显示文本,因为@font-face
提供了字体文件的位置。
如果你使用的是 Google Web Fonts 中的字体,那么你不需要用@font-face
来定义字体,因为已经为你定义好了。您所要做的就是参考 Google Web Fonts 提供的样式表,然后在 CSS 规则中使用字体系列名称。例如,如果单击字体 Peralta 的“快速使用”按钮,您会看到一个样式表引用链接和一个字体系列名称(Peralta)。有了这些信息,您可以按如下方式定义 CSS 规则:
h2 { font-family:'Peralta'; font-size:20px; text-align:center; }
在这里,您为<h2>
元素创建一个 CSS 规则,并将字体系列指定为 Peralta。然后,您可以引用网页中的样式表,如下所示:
<link href='http://fonts.googleapis.com/ css?family=Peralta' rel='stylesheet' type='text/css'> <link rel="stylesheet" type="text/css" href="StyleSheet.css" />
***图 13-10。*使用谷歌网络字体中的网络字体
请注意,在您自己的样式表(StyleSheet.css
)中使用 Peralta 字体定义之前,您应该参考 Google Web Fonts 提供的样式表。图 13-10 显示了<h2>
标题在浏览器中的样子。
如您所见,副标题是使用 Google Web Fonts 提供的 Peralta 字体显示的。
在前面的例子中,您使用了第三方提供的字体(字体松鼠或谷歌),但是如果需要,您也可以通过为它们生成一个@font-face
工具包来使用自己的字体。字体松鼠网站允许你上传自己的字体来生成自己的@font-face
工具包。一旦为这样的字体生成了@font-face
工具包,你就可以在你的网页中使用它们了,正如本节所讨论的。
圆角、阴影、渐变和透明度
应用于网页中 DOM 元素的最常见的效果之一是将它们放在一个框中。边框、边框宽度和样式、颜色等框属性是使用各种 CSS 属性配置的。CSS3 提供了一些功能,可以帮助您进一步增强框的布局。这些很酷的新增功能很可能会成为你最常用的 CSS3 特性。使用它们,您可以执行以下操作:
- A box that adds rounded corners to DOM elements.
- Add a shadow to the box.
- Set multiple images as the background.
- Add a gradient fill to the background.
让我们看看如何将这些特性应用于 DOM 元素。
圆角
每当您使用边框属性在元素周围放置一个框时,默认情况下,该框会出现尖角。您可以使用border-radius
属性将带有尖角的框变成带有圆角的框。使用border-radius
,您可以指定一个圆的半径,该圆的周长决定了拐角的圆角。下面的 CSS 规则显示了如何使用border-radius
属性:
.boxRoundedCorders { padding:15px; background-color:#d0bdbd; border: 2px solid #071394; ** border-radius: 25px 25px 25px 25px;** }
border-radius
按左上、右上、右下和左下的顺序设置矩形四个角的圆的半径。虽然这个例子将每个圆的半径设置为 25px,但这不是必需的。您可以指定不同的半径值,相应的角(左上、右上、右下或左下)将相应地改变其曲线。
也可以使用椭圆而不是圆来控制曲线。这样,平曲线和竖曲线可以不同。为此,您需要如下设置各个角的边界半径:
.boxRoundedCorders2 { padding:15px; background-color:#d0bdbd; border: 2px solid #071394; ** border-top-left-radius: 25px 15px;** ** border-top-right-radius: 25px 15px;** ** border-bottom-left-radius: 50px 40px;** ** border-bottom-right-radius: 50px 40px;** }
这一次,单个属性— border-top-left-radius
、border-top-right-radius
、border-bottom-left-radius
和border-bottom-right-radius
—分别指定椭圆的水平和垂直半径。图 13-11 显示了放置在<div>
元素中的两个 ASP.NETFormView
服务器控件,这些元素应用了boxRoundedCorners
和boxRoundedCorners2
CSS 类。
图 13-11。 FormView
带圆角的控件
第一个FormView
的边框都具有 25px 的相等半径,如在border-radius
属性中指定的。第二个FormView
的顶角半径分别为 25px、15px,底角半径分别为 50px、40px。
阴影
CSS3 允许你给文本框和文本添加阴影。box-shadow
和text-shadow
属性分别控制框阴影和文本阴影。您可以为这两种类型的阴影指定水平偏移、垂直偏移、模糊量和颜色。
下面的 CSS 规则使用了刚刚讨论过的box-shadow
和text-shadow
属性:
.shadow { padding:15px; background-color:#d0bdbd; border: 2px solid #071394; border-radius: 25px 25px 25px 25px; ** box-shadow: 5px 5px 5px #808080;** ** text-shadow: 2px 2px 2px #808080; ** }
如您所见,box-shadow
指定了 5px 的水平和垂直偏移。正偏移量意味着阴影相对于长方体向右下方下降,而负值意味着阴影向左上方下降。box-shadow
属性指定模糊量为 5px。值越大,阴影越模糊。最后,阴影颜色被指定为#808080
(灰色)。
text-shadow
属性类似,将水平和垂直偏移量指定为 2px。模糊值也设置为 2px。图 13-12 显示了结果框。
***图 13-12。*给文本框和文本添加阴影
注意,text-shadow
并不是所有的浏览器都支持(例如 IE9),但是box-shadow
在所有主流浏览器中都有很好的支持。
默认情况下,框阴影放置在框的外部。您可以使用关键字inset
来改变这种行为,如下所示:
box-shadow: 5px 5px 5px #808080 inset;
当你在最后添加inset
关键字时,阴影出现在框内而不是框外(图 13-13 )。
**图 13-13。**添加inset
关键字后的阴影
图像背景
在前面的例子中,CSS 规则为盒子的背景使用了一种颜色。您可以使用背景图像来代替指定背景颜色。CSS3 中的新特性是能够在一个背景中使用多个图像。例如,您可以使用四个不同的图像,并将它们放置在框的左上、右上、左下和左下区域。当然,图像所占据的实际面积取决于图像的大小。考虑以下 CSS 规则:
.imageBackground { padding:15px; font-size:20px; border: 2px solid #071394; border-radius: 25px 25px 25px 25px; ** background-image: url('img/RedFlower.png'), url('img/BlueFlower.png');** ** background-position: left top, right bottom;** ** background-repeat: no-repeat, no-repeat;** height:300px; }
这个 CSS 规则使用background-image
属性来指定两个图像 URL:RedFlower.png
和BlueFlower.png
。这两个图像的位置由background-position
属性控制。对于background-image
属性中的每个图像 URL,在background-position
属性中必须有一个条目。在这个例子中,RedFlower.png
放在盒子的左上角,BlueFlower.png
放在右下角。
您还需要指定background-repeat
属性。在本例中,因为您没有水平或垂直重复图像,所以您将background-repeat
设置为no-repeat
。图 13-14 显示了最终的盒子背景。
***图 13-14。*使用多张图片作为盒子背景
RedFlower.png
出现在左上角,BlueFlower.png
出现在右下角。
渐变
在第四章的中,你学会了在画布上绘制渐变——线性和径向。CSS3 提供了绘制渐变背景的方法。您可以使用两个 CSS 函数来实现这一点:linear-gradient()
和radial-gradient()
。他们分别用线性和径向渐变绘制方框背景。在撰写本文时,linear-gradient()
和radial-gradient()
还没有被一些浏览器完全实现,所以您需要将它们与前面讨论过的浏览器前缀(-ms-
、-moz-
、-webkit-
等等)一起使用。下面的 CSS 规则展示了如何使用linear-gradient()
:
.linearGradient { padding:15px; background-color:#d0bdbd; border: 2px solid #071394; background: -moz-linear-gradient(left, yellow, white); background: -webkit-linear-gradient(left, yellow, white); background: -o-linear-gradient(left, yellow, white); }
以-moz-
为前缀的函数针对 Firefox,以-webkit-
为前缀的函数针对 Chrome 和 Safari,以-o-
为前缀的函数针对 Opera。该函数的第一个参数指定绘制渐变的起始边缘。Left
表示从左侧开始绘制渐变,直到到达方框的右侧。如果指定top
,渐变的方向是从上到下。第二个和第三个参数指定渐变的开始和结束颜色。图 13-15 显示了这种渐变效果。
***图 13-15。*绘制线性渐变
示例渐变从黄色开始,逐渐渐变为白色。您也可以将起点(第一个参数)指定为角度。例如,0deg
、90deg
、180deg
、270deg
分别表示左、下、右、上。这些值之间的任何角度都会相应地移动拐角处的起点。此外,您可以为渐变指定一系列颜色,而不仅仅是开始和结束颜色:
-webkit-linear-gradient(left, orange, yellow, white);
在这个例子中,指定了三种颜色:橙色、黄色和白色。所以,渐变一开始是橙色,然后变成黄色,最后渐变为白色。
绘制径向渐变类似于绘制线性渐变。唯一的区别是,渐变不是从一个边缘开始,到另一个边缘结束,而是从中心开始,到框的边界结束。下面的 CSS 规则展示了如何使用radial-gradient()
:
`.radialGradient {
padding:15px;
background-color:#d0bdbd;
border: 2px solid #071394;
** background: -moz-radial-gradient(circle, yellow, white);**
** background: -webkit-radial-gradient(circle, yellow, white);**
** background: -o-radial-gradient(circle, yellow, white);**
}`
radial-gradient()
函数的第一个参数表示渐变中心的起始位置。最常见的值是center
,表示盒子的中心是起点。另外两个参数代表渐变的开始和结束颜色,就像linear-gradient()
一样。图 13-16 显示了径向梯度。
***图 13-16。*使用radial-gradient()
功能绘制径向渐变
请注意,本节仅讨论梯度函数的基本用法。这些函数提供了更复杂的绘制和微调渐变的方式。可以在网上多看看(比如[
developer.mozilla.org/en-US/docs/CSS/CSS_Reference](https://developer.mozilla.org/en-US/docs/CSS/CSS_Reference)
)。
透明度
除了显示背景,您还可以控制框的不透明度。CSS3 提供了两种方法来处理元素的不透明度:rgba()
函数和opacity
属性。rgba()
函数将红色、绿色和蓝色值指定为数字以及控制元素不透明度的 alpha 值。r
、g
和b
数字取 0 到 255 之间的值;alpha
参数可以是 0 到 1 之间的任意值,其中 0 表示完全透明,1 表示完全不透明。下面的 CSS 规则展示了如何使用rgba()
:
.transparency { padding:15px; ** background-color:rgba(10,200,0,0.3);** border: 2px solid #071394; }
该函数将红色、绿色和蓝色分量分别指定为10
、200
和0
。alpha
参数被指定为0.3
。图 13-17 显示了应用透明度 0.3 时一个盒子的样子。
**图 13-17。**使用rgba()
功能设置盒子的透明度
在图中,第二个框使用rgba()
进行样式化。请注意,由于0.3
的透明度alpha
值,背景不会通过第一个框显示,但会通过第二个框显示。
您可以使用opacity
属性代替rgba()
来获得类似的效果:
.transparency2 { padding:15px; background-color:#ffd800; opacity:0.75; border: 2px solid #071394; }
opacity
属性取 0 到 1 之间的任何值,就像前面讨论的alpha
参数一样。图 13-18 显示了结果框。
**图 13-18。**使用opacity
属性设置透明度
使用rgba()
和opacity
属性有一些不同。rgba()
仅使背景颜色透明,而opacity
使元素内的背景、边框和文本透明。如果您希望以编程方式控制不透明度(而不是背景颜色),那么opacity
属性也很方便(比如,通过使用 jQuery css()
方法)。这样,在不知道或不接触背景颜色的情况下,您可以设置opacity
属性并获得所需的透明度级别。以下代码行显示了如何做到这一点:
$("<jQuery_selector>").css("opacity", "0.75");
这段代码使用 jQuery css()
方法将 jQuery 选择器匹配的所有元素的不透明度设置为0.75
。
使用过渡和变换添加效果
专业网站经常通过添加爵士乐和吸引人的效果来增加页面的趣味。CSS3 提供了两种技术,允许你给你的网页添加酷炫的效果:过渡和变换。CSS3 过渡允许您在元素从一个 CSS 规则过渡到另一个 CSS 规则时向元素添加效果。另一方面,CSS3 变换通过添加旋转或倾斜元素等效果来改变元素的外观。
过渡
CSS 伪类如:hover
允许您在用户以特定方式与元素交互时更改应用于元素的样式规则。例如,使用:hover
伪类,您可以在用户将鼠标指针悬停在元素上时更改元素的外观。伪类的限制是它们将元素的外观从一种状态切换到另一种状态。您无法控制元素在“即将改变”和“已改变”阶段之间的外观。这意味着你不能在 CSS 规则改变期间添加动画效果。
CSS3 过渡是填补这一空白的手段。考虑下面的 CSS 规则,它们定义了在normal
和hover
状态下<div>
元素的样式:
.employeeData { padding:15px; background-color:#f3f3f3; border: 2px solid #071394; }
.employeeData:hover { color:#682020; background-color:#ff6a00; font-weight:bold; }
在employeeData
中指定的 CSS 属性在<div>
处于正常状态时应用(用户没有将鼠标悬停在其上),而在employeeData:hover
中指定的 CSS 属性在用户将鼠标悬停在<div>
上时应用。目前,使用这些 CSS 规则不会给<div>
添加任何过渡效果。要添加过渡效果,需要修改employeeData
规则,如下所示:
.employeeData { ... -moz-transition: color 3s,background-color 3s; -webkit-transition: color 3s,background-color 3s; -o-transition: color 3s,background-color 3s; }
修改后的employeeData
规则使用以特定于浏览器的前缀为前缀的transition
属性。transition
属性由一个逗号分隔的属性列表组成,这些属性包含在过渡效果和播放过渡效果的持续时间(以秒为单位)中。在这个例子中,color
和background-color
属性在三秒钟内从它们的旧值变为新值。图 13-19 显示了<div>
在转换前后的样子。
***图 13-19。*鼠标悬停时应用转场
在没有transition
属性的情况下,当悬停开始时,<div>
的背景颜色立即从#f3f3f3
变为#ff6a00
,当鼠标离开<div>
时,从#ff6a00
变为#f3f3f3
。然而,随着transition
的到位,背景颜色的相同变化在三秒钟内发生,而不是立即发生,从而产生动画效果。
注意你还可以给转场效果添加其他细节,比如定时功能。在[www.w3.org/TR/css3-transitions](http://www.w3.org/TR/css3-transitions)
和[
developer.mozilla.org/en-US/docs/CSS/CSS_transitions](https://developer.mozilla.org/en-US/docs/CSS/CSS_transitions)
可以阅读更多。
变换
在从一种样式过渡到另一种样式的过程中,过渡会添加效果。另一方面,变换使用旋转、倾斜和缩放等效果来改变元素的外观。考虑以下 CSS 规则:
.rotate { padding:15px; margin:20px; background-color:#f3f3f3;
border: 2px solid #071394; -ms-transform:rotate(5deg); -moz-transform:rotate(10deg); -webkit-transform:rotate(10deg); -o-transform:rotate(10deg); }
该规则使用以供应商前缀为前缀的transform
属性将元素旋转 10 度。rotate
函数接受元素旋转的角度。正数表示顺时针方向旋转,负数表示逆时针方向旋转。图 13-20 显示了最终的<div>
。
**图 13-20。**使用transform
属性旋转元素
可以看到,<div>
元素和其中的FormView
顺时针旋转了 10 度。
您还可以添加倾斜效果,并分别使用skew()
和scale()
功能更改缩放比例。您也可以一起使用多个函数,如下面的 CSS 规则所示:
.skew { padding:15px; margin:20px; background-color:#f3f3f3; border: 2px solid #071394; -ms-transform:skew(10deg) scale(0.9); -moz-transform:skew(10deg) scale(0.9); -webkit-transform:skew(-10deg) scale(0.9); -o-transform:skew(10deg) scale(0.9); }
该规则使用了skew()
函数和scale()
函数。skew()
以度为单位获取一个角度,指定元素倾斜的程度。scale()
获取一个数字,指示元素应该收缩或拉伸多少。例如,2
的缩放值意味着元素被拉伸到其原始大小的两倍。您也可以使用scaleX()
和scaleY()
功能分别控制 x 和 y 缩放值。图 13-21 显示了结果元素。
图 13-21。倾斜和缩放效果应用于一个元素
您也可以一起使用变换和过渡,如以下 CSS 规则所示:
`.myclass {
-moz-transition: all 3s;
-webkit-transition: all 3s;
-o-transition: all 3s;
}
.myclass:hover {
-ms-transform:rotate(10deg);
-moz-transform:rotate(10deg);
-webkit-transform:rotate(10deg);
-o-transform:rotate(10deg);
}`
.myclass:hover
规则使用rotate()
函数来转换元素。.myclass
规则使用transition
属性来指定来自:hover
伪类的所有属性都应该被激活三秒钟。这样,当您将鼠标指针悬停在<div>
上时,<div>
会顺时针旋转 10 度。
注意你可以在[
developer.mozilla.org/en-US/docs/CSS/transform](https://developer.mozilla.org/en-US/docs/CSS/transform)
阅读更多变换技巧。
使用媒体查询来定位不同的设备
随着智能手机和平板电脑等移动设备的使用增加,越来越多的人通过移动设备访问网站。这种情况下的一个问题是,为桌面浏览器设计的网页在移动浏览器上看起来不像预期的那样。例如,在桌面浏览器中看起来非常漂亮的带有圆滑动画效果的悬停菜单在移动浏览器中可能无法正确显示,并且可能会出现重叠和不相称的情况。处理用户通过桌面计算机和移动设备访问 web 应用的传统方法是开发 web 应用的两个版本——普通版本和移动版本。尽管这种方法可行,但缺点是您需要维护 web 应用的两个不同版本。
当您将 web 应用定位到移动设备时,CSS3 提供了一些帮助。对媒体查询的支持允许您检测请求设备的某些特性,并相应地应用样式规则。这样,您就不需要针对不同的设备创建不同版本的网站。您可以通过两种方式动态更改应用于页面元素的样式:
- If the requesting device meets certain criteria, you can attach different style sheets (
.css
) to the page.- You can change CSS rules according to the requesting device.
注意这本书不是关于为移动设备开发网站,因此讨论媒体查询只是为了让你熟悉 CSS3 的这个新特性。更多详情见[www.w3.org/TR/css3-mediaqueries](http://www.w3.org/TR/css3-mediaqueries)
。
通常,媒体查询使用以下参数来更改应用于页面或元素的样式规则:
- Minimum and maximum width of browser window (
min-width
max-width
)- Width of device screen (
max-device-width
)- Screen orientation (horizontal or vertical:
orientation
)- Height of equipment screen (
device-height
)
使用媒体查询更改样式表
使用前面讨论的媒体查询和设备属性,可以在运行时将不同的样式表附加到网页上。假设您有两个样式表Desktop.css
和Mobile.css
,分别包含当目标是桌面或移动设备时使用的 CSS 样式规则。如果网页是用最小宽度为 800 像素的设备(如果设备的屏幕至少有这么宽,很可能是台式电脑)来浏览的,你就想把Desktop.css
附加到网页上。同样,如果屏幕宽度最大为 480 像素(手机的典型值),那么您希望附加Mobile.css
。您可以通过参考如下样式表来完成此任务:
`
`第一个<link>
元素告诉浏览器,如果设备的最小宽度是 800 像素,它应该将Desktop.css
附加到页面上。第二个<link>
元素表示,如果这个设备的最大宽度为 480 像素,并且使用纵向模式,浏览器应该将Mobile.css
附加到页面上。
注意在第二个<link>
元素中使用了and
操作符来检查多个条件。您还可以使用all
、not
和only
关键字:all
表示样式表适用于所有媒体类型,not
否定查询结果,only
对旧浏览器隐藏样式表。尽管旧的浏览器会忽略这样的样式表,但支持 CSS3 媒体查询的浏览器会处理以only
开头的媒体查询,就好像only
关键字不存在一样。
如果您没有测试该页面的移动设备,您可以将min-device-width
属性更改为高于桌面屏幕分辨率的值,然后在桌面浏览器中查看该页面。因为没有一个<link>
元素符合标准,所以浏览器不会应用样式表,导致 HTML 元素的普通默认呈现。然后将min-device-width
更改为小于屏幕分辨率的值,再次查看页面。浏览器应该按照Desktop.css
中定义的样式规则显示 HTML 元素。
使用媒体查询更改样式规则
在前面的示例中,您根据设备属性在运行时更改了整个样式表。如果两个样式表不同,这种方法很有效。但是,如果只需要针对不同的设备调整一些 CSS 规则,那么可以将它们放在一个样式表中。在这种情况下,您需要在样式表中使用一个@media
块。以下示例说明了如何使用@media
模块:
`@media (min-device-width: 800px) {
body {
background-color:blue;
color:white;
}
}
@media (max-width: 480px) and (orientation:portrait) {
body {
background-color:red;
color:white;
}
}`
第一个@media
块为最小屏幕宽度为 800 像素的设备分组 CSS 规则。第二个@media
块为最大宽度为 480 像素、纵向的设备分组 CSS 规则。该样式表通常附加到网页上:
<link rel="stylesheet" type="text/css" href="StyleSheet.css" />
如果在台式计算机上运行带有此样式表的网页,应该会看到网页背景被涂成蓝色。在手机上,背景是红色的。
使用 Modernizr 应用 CSS3 特有的特性
如前所述,CSS3 是一个不断发展的规范。并非所有的浏览器都支持所有的 CSS3 特性。如果您知道您的 web 应用将被现代浏览器使用,您可以使用它们都支持的 CSS3 特性。这种方法的优点是,您可以在自己的 web 应用中使用大量的 CSS3 特性。然而,缺点是非常旧的浏览器可能无法按预期显示您的网页。解决这个问题的方法是创建不同的 CSS 类来封装传统的和特定于 CSS3 的属性。在运行时,根据浏览器是否支持特定的 CSS3 功能,将特定于 CSS3 的类或具有传统属性的类应用于元素。但是这样做需要更多的工作,因为它涉及到创建多个 CSS 类,并根据目标浏览器提供的支持级别在运行时应用它们。
为了帮助您完成这项工作,您可以使用 Modernizr 库。在本书中,您一直在使用 Modernizr 来检测 HTML5 的特性。同一个 Modernizr 库还允许您检测对 CSS3 特性的支持。让我们看看怎么做。
使用 Modernizr 的网页使用一个<script>
标签来引用它,如下所示:
<script type="text/javascript" src="scripts/modernizr.js"></script>
在运行时,Modernizr 库修改 HTML5 文档的<html>
标签,以包含所有支持的 HTML5 和 CSS3 特性以及给定浏览器不支持的特性。以下来自样本 HTML5 文档的<html>
标签显示了这是如何发生的:
<html class=" js flexbox canvas canvastext webgl **no-touch** geolocation postmessage websqldatabase indexeddb hashchange history draganddrop websockets rgba hsla multiplebgs backgroundsize borderimage borderradius boxshadow textshadow opacity cssanimations csscolumns cssgradients cssreflections csstransforms csstransforms3d csstransitions fontface generatedcontent **video** **audio** localstorage sessionstorage **webworkers** applicationcache svg inlinesvg smil svgclippaths">
在这个标记中,Modernizr 向<html>
标签添加了一个class
属性。属性class
的值是一个字符串,包含由空格分隔的 CSS 类。每个类代表一个 HTML5 或 CSS3 特性。给定浏览器不支持的功能以no-
为前缀。例如,这个标记包括一个no-touch
类,表示不支持触摸事件。简单的特性名称(如video
、audio
和webworkers
)表明它们受浏览器支持。
注意添加了class
属性的<html>
标签在网页的 HTML 源代码中是不可见的,因为它是由 Modernizr 以编程方式添加的。你需要在 Chrome 开发者工具之类的工具中检查页面。
现在,假设页面上有一个<div>
元素,您希望设置它的background-color
、text-align
、padding
、border
和border-radius
CSS 属性。其中,border-radius
是特定于 CSS3 的,而其他的是传统的 CSS 属性。如本章前面所讨论的,属性允许你创建圆角边框。您可以创建三个 CSS 类,如下所示:
.div { background-color: #d3a584; padding: 10px; text-align: center; } .borderradius .div { border: 2px #f00 solid; border-radius: 25px; } .no-borderradius .div { border: 2px #f00 solid; }
一个 CSS 类包含无论是否支持border-radius
都要应用的 CSS 属性,另一个包含支持border-radius
时要应用的 CSS 属性,第三个包含不支持border-radius
时要应用的 CSS 属性。注意,第二个(borderradius
)和第三个(no-borderradius
) CSS 类名与来自<html>
标签的相应类名相同。borderradius
类设置border
和border-radius
属性,而no-borderradius
类只设置border
属性。一旦创建了 CSS 类,就可以在<div>
元素上使用它们,如下所示:
<div class="div">Hello World!</div>
在运行时,div
CSS 类被应用于<div>
元素。此外,根据是否支持border-radius
属性,应用两个类中的一个(borderradius
或no-borderradius
)。图 13-22 显示了 Chrome 中网页的运行示例。
图 13-22。 Modernizr 在 Chrome 中应用了borderradius
CSS 类。
如你所见,因为 Chrome 支持border-radius
属性,来自div
和borderradius
CSS 类的 CSS 属性被应用到<div>
元素。
总结
CSS 是将样式规则应用于网页的事实上的标准。CSS3 为 CSS 2.1 增加了许多新特性,使其对 web 开发人员更具吸引力和实用性。本章介绍了 CSS3 的一些重要特性。
使用 CSS3,您可以通过使用@font-face
创建自定义字体定义来在网页中使用非标准字体。在运行时,字体文件在客户端下载,并用于显示网页的文本。CSS3 的另一个很好的改进是能够给盒子布局添加花哨的装饰,比如阴影、渐变和透明。您还可以使用过渡和变换来添加效果。
随着移动和手持设备变得越来越普遍,通过移动设备访问网站的情况也越来越多。使用介质查询,您可以检测设备属性,如宽度和方向。然后,您可以对网页或元素应用不同的样式规则,以确保它们在目标设备上看起来很棒。
毫无疑问,HTML5、JavaScript 和 CSS3 一起将改变人们开发和使用 web 应用的方式。势头已经开始,在未来你会看到这些技术获得越来越多的接受和广泛使用。这本书试图给你所有必要的技能来驾驭 HTML5 浪潮,并利用这种未来技术的力量。