七、使用 WebSocket API
在这一章中,我们将探索 HTML5 规范中最强大的通信功能可以做什么: WebSocket ,它定义了一个全双工通信通道,通过 web 上的单个套接字进行操作。WebSocket 不仅仅是传统 HTTP 通信的另一个增量增强;它代表了一个巨大的进步,特别是对于实时的、事件驱动的 web 应用。
WebSocket 对用于模拟浏览器中全双工连接的古老、复杂的“黑客”进行了改进,这促使谷歌的伊恩·希克森(HTML5 规范负责人)说:
“将数千字节的数据减少到 2 字节……并将延迟从 150 毫秒减少到 50 毫秒远不止是微不足道。事实上,仅这两个因素就足以让 WebSocket 引起谷歌的严重兴趣。”
—
[www.ietf.org/mail-archive/web/hybi/current/msg00784.html](http://www.ietf.org/mail-archive/web/hybi/current/msg00784.html)
我们将向您详细展示为什么 WebSocket 提供了如此巨大的改进,并且您将看到 WebSocket 如何—一举—使所有旧的 Comet 和 Ajax 轮询、长轮询和流解决方案变得过时。
web socket 概述
让我们通过将 HTTP 解决方案与使用 WebSocket 的全双工“实时”浏览器通信进行比较,来看看 WebSocket 如何减少不必要的网络流量和延迟。
实时和 HTTP
通常,当浏览器访问一个网页时,一个 HTTP 请求被发送到承载该网页的 web 服务器。web 服务器确认该请求并发回响应。在许多情况下,例如,对于股票价格、新闻报道、门票销售、交通模式、医疗设备读数等等,浏览器呈现页面时,响应可能已经过时。如果您想获得最新的实时信息,您可以不断地手动刷新该页面,但这显然不是一个很好的解决方案。
当前提供实时 web 应用的尝试主要围绕轮询和其他服务器端推送技术,其中最著名的是“Comet ”,它延迟 HTTP 响应的完成以向客户端传递消息。
通过轮询,浏览器定期发送 HTTP 请求,并立即收到响应。这项技术是浏览器传递实时信息的首次尝试。显然,如果知道消息传递的确切时间间隔,这是一个好的解决方案,因为您可以将客户端请求同步为仅在服务器上有信息时发生。然而,实时数据通常不可预测,这使得不必要的请求不可避免,因此,在低消息速率的情况下,许多连接被不必要地打开和关闭。
对于长轮询,浏览器向服务器发送一个请求,服务器在一段设定的时间内保持请求打开。如果在此期间收到通知,则包含该消息的响应将被发送到客户端。如果在设定的时间段内没有收到通知,服务器将发送响应以终止打开请求。但是,理解这一点很重要,当您的消息量很大时,长轮询与传统轮询相比不会提供任何实质性的性能改进。
使用流式传输,浏览器发送一个完整的请求,但是服务器发送并维护一个打开的响应,该响应不断更新并无限期地(或在一段设定的时间内)保持打开。然后,每当消息准备好发送时,响应就会更新,但是服务器永远不会发出完成响应的信号,从而保持连接打开以传递未来的消息。但是,由于流仍然封装在 HTTP 中,中间的防火墙和代理服务器可能会选择缓冲响应,从而增加了消息传递的延迟。因此,在检测到缓冲代理服务器的情况下,许多流解决方案会退回到长轮询。或者,可以使用 TLS (SSL)连接来保护响应不被缓冲,但是在这种情况下,每个连接的建立和断开会加重可用服务器资源的负担。
最终,所有这些提供实时数据的方法都涉及 HTTP 请求和响应头,它们包含大量额外的、不必要的头数据,并引入了延迟。最重要的是,全双工连接不仅仅需要从服务器到客户端的下行连接。为了在半双工 HTTP 上模拟全双工通信,今天的许多解决方案使用两个连接:一个用于下游,一个用于上游。这两个连接的维护和协调在资源消耗方面引入了大量开销,并增加了许多复杂性。简单地说,HTTP 不是为实时、全双工通信而设计的,正如您在图 7-1 中看到的,该图显示了构建一个 web 应用的复杂性,该应用使用半双工 HTTP 上的发布/订阅模型显示来自后端数据源的实时数据。
图 7-1 。实时 HTTP 应用的复杂性
当您尝试横向扩展这些解决方案时,情况会变得更糟。模拟 HTTP 上的双向浏览器通信容易出错且复杂,并且所有这些复杂性都无法扩展。即使您的最终用户可能喜欢看起来像实时 web 应用的东西,这种“实时”体验也有很高的价格。这是额外的延迟、不必要的网络流量和 CPU 性能下降的代价。
了解 WebSocket
伊恩·希克森(HTML5 规范的主要作者)首先在 HTML5 规范的通信部分将 WebSocket 定义为“TCP 连接”。该规范发展并更改为 WebSocket,它现在是一个独立的规范(就像地理定位、Web 工作器 等等),以保持讨论的重点。
TCPConnection 和 WebSocket 都是指较低级别的网络接口的名称。TCP 是互联网的基本传输协议。WebSocket 是 web 应用的传输协议。它提供按顺序到达的双向数据流,很像 TCP。与 TCP 一样,更高级别的协议可以在 WebSocket 上运行。作为 Web 的一部分,WebSocket 连接到 URL,而不是连接到互联网主机和端口。
WEBSOCKET 和模型火车有什么共同点?
彼得说:“伊恩·希克森是模型火车的狂热爱好者;自 1984 年马克林首次推出数字控制器以来,他就一直在计划用电脑控制火车的方法,这远远早于网络的存在。
当时,Ian 将 TCPConnection 添加到 HTML5 规范中,他正在编写一个程序,以便从浏览器控制模型火车组,并且他正在使用 WebSocket 出现之前流行的“hanging GET”和 XHR 技术来实现浏览器到火车的通信。如果有一种方法可以在浏览器中进行套接字通信,那么列车控制器程序的构建就会容易得多——很像在“胖”客户端中发现的传统的异步客户端/服务器通信模型。因此,受可能的启发,(火车)轮子已经启动,网络插座火车已经离开车站。下一站:实时网络。"
web socket 握手
为了建立 WebSocket 连接,客户端和服务器在初始握手期间从 HTTP 协议升级到 WebSocket 协议,如图图 7-2 所示。请注意,此连接描述代表协议草案 17。
***图 7-2。*web socket 升级握手
***清单 7-1。*web socket 升级握手
`From client to server:
GET /chat HTTP/1.1
Host: example.com
Connection: Upgrade
Sec-WebSocket-Protocol: sample
Upgrade: websocket
Sec-WebSocket-Key: 7cxQRnWs91xJW9T0QLSuVQ==
Origin: http://example.com
[8-byte security key]
From server to client:
HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 7cxQRnWs91xJW9T0QLSuVQ==
WebSocket-Protocol: sample`
一旦建立,WebSocket 消息就可以以全双工模式在客户机和服务器之间来回发送。这意味着基于文本的消息可以同时双向全双工发送。在网络上,每条消息以一个0x00
字节开始,以一个0xFF
字节结束,中间包含 UTF-8 数据。
web socket 接口
除了 WebSocket 协议的定义,该规范还定义了在 JavaScript 应用中使用的 WebSocket 接口。清单 7-2 显示了WebSocket
界面。
***清单 7-2。*web socket 接口
`[Constructor(DOMString url, optional DOMString protocols),
Constructor(DOMString url, optional DOMString[] protocols)]
interface WebSocket : EventTarget {
readonly attribute DOMString url;
// ready state
const unsigned short CONNECTING = 0;
const unsigned short OPEN = 1;
const unsigned short CLOSING = 2;
const unsigned short CLOSED = 3;
readonly attribute unsigned short readyState;
readonly attribute unsigned long bufferedAmount;
// networking
[TreatNonCallableAsNull] attribute Function? onopen;
[TreatNonCallableAsNull] attribute Function? onerror;
[TreatNonCallableAsNull] attribute Function? onclose;
readonly attribute DOMString extensions;
readonly attribute DOMString protocol;
void close([Clamp] optional unsigned short code, optional DOMString reason);
// messaging
[TreatNonCallableAsNull] attribute Function? onmessage;
attribute DOMString binaryType;
void send(DOMString data);
void send(ArrayBuffer data);
void send(Blob data);
};`
使用WebSocket
接口很简单。要连接一个远程主机,只需创建一个新的WebSocket
实例,为新对象提供一个 URL,表示您希望连接的端点。注意,ws://
和wss://
前缀分别表示 WebSocket 和安全 WebSocket 连接。
在客户端和服务器之间的初始握手期间,通过相同的底层 TCP/IP 连接,通过从 HTTP 协议升级到 WebSocket 协议来建立 WebSocket 连接。一旦建立,WebSocket 数据帧就可以以全双工模式在客户机和服务器之间来回发送。连接本身通过由WebSocket
接口定义的message
事件和send
方法公开。在您的代码中,使用异步事件侦听器来处理连接生命周期的每个阶段。
myWebSocket.onopen = function(evt) { alert("Connection open ..."); }; myWebSocket.onmessage = function(evt) { alert( "Received Message: " + evt.data); }; myWebSocket.onclose = function(evt) { alert("Connection closed."); };
大幅减少不必要的网络流量和延迟
那么 WebSocket 能有多高效呢?让我们并排比较一下轮询应用和 WebSocket 应用。为了说明轮询,我们将研究一个 web 应用,其中一个网页使用传统的轮询模型从 web 服务器请求实时股票数据。它通过轮询 web 服务器上托管的 Java Servlet 来实现这一点。消息代理从一个虚构的股票价格提要接收数据,并不断更新价格。web 页面连接并订阅特定的股票频道(消息代理上的一个主题),并使用 XMLHttpRequest 每秒轮询一次更新。当接收到更新时,执行一些计算并显示股票数据,如图图 7-3 所示。
图 7-3。【JavaScript 股票行情应用示例
这听起来很棒,但是深入了解一下就会发现这个应用存在一些严重的问题。例如,在带有 Firebug 的 Mozilla Firefox 中,您可以看到 GET 请求以一秒的间隔敲打服务器。查看 HTTP 头可以发现与每个请求相关的惊人的开销。清单 7-3 和 7-4 显示了单个请求和响应的 HTTP 头数据。
清单 7-3。 HTTP 请求头
GET /PollingStock//PollingStock HTTP/1.1 Host: localhost:8080 User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.5) Gecko/20091102 Firefox/3.5.5 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 300 Connection: keep-alive Referer: http://www.example.com/PollingStock/ Cookie: showInheritedConstant=false; showInheritedProtectedConstant=false; showInheritedProperty=false; showInheritedProtectedProperty=false; showInheritedMethod=false; showInheritedProtectedMethod=false; showInheritedEvent=false; showInheritedStyle=false; showInheritedEffect=false
清单 7-4。 HTTP 响应头
HTTP/1.x 200 OK X-Powered-By: Servlet/2.5 Server: Sun Java System Application Server 9.1_02 Content-Type: text/html;charset=UTF-8 Content-Length: 21 Date: Sat, 07 Nov 2009 00:32:46 GMT
只是为了好玩(哈!),我们可以把所有的人物都算进去。总的 HTTP 请求和响应头信息开销包含 871 个字节,这还不包括任何数据。当然,这只是一个例子,您可以拥有少于 871 字节的头数据,但也有头数据超过 2,000 字节的常见情况。在这个示例应用中,典型的股票主题消息的数据只有大约 20 个字符长。正如您所看到的,它实际上被过多的头信息淹没了,而这些头信息本来就不是必需的。
那么,当您将这个应用部署给大量用户时会发生什么呢?让我们来看看在三个不同的用例中与这个轮询应用相关的 HTTP 请求和响应头数据的网络开销。
- 用例 A :每秒 1000 个客户端轮询:网络流量为(871×1000)= 871000 字节=每秒 6968000 比特(6.6 Mbps)
- 用例 B :每秒 10000 个客户端轮询:网络流量为(871×10000)= 8710000 字节= 69680000 比特每秒(66 Mbps)
- 用例 C :每秒 10 万个客户端轮询:网络流量为(871 × 10 万)= 8710 万字节= 69680 万比特每秒(665 Mbps)
这是大量不必要的网络开销。考虑一下,如果我们重新构建应用以使用 WebSocket,向 web 页面添加一个事件处理程序来异步侦听来自消息代理的股票更新消息(稍后会详细介绍)。这些消息中的每一条都是一个 WebSocket 帧,只有两个字节的开销(而不是 871)。看看这在我们的三个用例中是如何影响网络开销的。
- 用例 A:1000 个客户端每秒接收 1 条消息:网络流量为(2×1000)= 2000 字节=每秒 16000 比特(0.015 Mbps)
- 用例 B:10000 个客户端每秒接收 1 条消息:网络流量为(2×10000)= 20000 字节= 160000 比特每秒(0.153 Mbps)
- 用例 C: 10 万个客户端每秒接收 1 条消息:网络流量为(2 × 10 万)= 20 万字节=每秒 160 万比特(1.526 Mbps)
正如你在图 7-4 中看到的,与轮询解决方案相比,WebSocket 大大减少了不必要的网络流量。
***图 7-4。*轮询 WebSocket 流量之间不必要的网络开销比较
那么延迟的减少呢?看一下图 7-5 。在上半部分,您可以看到半双工轮询解决方案的延迟。对于这个例子,如果我们假设一条消息从服务器到浏览器需要 50 毫秒,那么轮询应用就会引入很多额外的延迟,因为当响应完成时,必须向服务器发送一个新的请求。这个新请求又需要 50 毫秒,在此期间,服务器无法向浏览器发送任何消息,从而导致额外的服务器内存消耗。
在图的下半部分,您可以看到 WebSocket 解决方案减少了延迟。一旦连接升级到 WebSocket,消息就可以在到达时从服务器流向浏览器。消息从服务器传输到浏览器仍然需要 50 毫秒,但是 WebSocket 连接仍然保持打开,因此不需要向服务器发送另一个请求。
***图 7-5。*轮询和 WebSocket 应用之间的延迟比较
WebSocket 在实时 web 的可伸缩性方面向前迈进了一大步。正如您在本章中所看到的,WebSocket 可以提供 500:1 甚至 1000:1 的不必要 HTTP 报头流量减少率和 3:1 的延迟减少率,具体取决于 HTTP 报头的大小。
编写一个简单的 Echo WebSocket 服务器
在使用 WebSocket API 之前,您需要一个支持 WebSocket 的服务器。在这一节中,我们将看看如何编写一个简单的 web socket“echo”服务器。为了运行本章的例子,我们包含了一个用 Python 编写的简单的 WebSocket 服务器。以下示例的示例代码位于图书网站的 WebSocket 部分。
WEBSOCKET 服务器
已经有很多 WebSocket 服务器实现,甚至还有更多正在开发中。以下只是现有 WebSocket 服务器的一部分:
- Kaazing WebSocket 网关—基于 Java 的 WebSocket 网关
- mod _ pyweb socket—Apache HTTP 服务器的基于 Python 的扩展
- Netty—一个包含 WebSocket 支持的 Java 网络框架
- node . js—一个服务器端的 JavaScript 框架,上面写了多个 WebSocket 服务器
Kaazing 的 WebSocket 网关包括对没有 WebSocket 本机实现的浏览器的完整客户端 WebSocket 仿真支持,这允许您根据当前的 WebSocket API 进行编码,并让您的代码在所有浏览器中工作。
要在ws://localhost:8000/echo
运行接受连接的 Python WebSocket echo 服务器,请打开命令提示符,导航到包含该文件的文件夹,并发出以下命令:
python websocket.py
我们还包含了一个广播服务器,它在ws://localhost:8080/broadcast
接受连接。与 echo 服务器相反,发送到这个特定服务器实现的任何 WebSocket 消息都将被反弹回当前连接的每个人。这是向多个听众广播消息的一种非常简单的方式。要运行广播服务器,请打开命令提示符,导航到包含该文件的文件夹,并发出以下命令:
python broadcast.py
这两个脚本都利用了websocket.py
中的示例 WebSocket 协议库。您可以为实现其他服务器端行为的其他路径添加处理程序。
注意这只是一个 WebSocket 协议的服务器,它不能响应 HTTP 请求。握手解析器不完全符合 HTTP。但是,因为 WebSocket 连接以 HTTP 请求开始,并且依赖于 Upgrade 头,所以其他服务器可以在同一个端口上同时服务于 WebSocket 和 HTTP。
让我们看看当一个浏览器试图与这个服务器通信时会发生什么。当浏览器向 WebSocket URL 发出请求时,服务器发回完成 WebSocket 握手的头。WebSocket 握手响应必须包含一个HTTP/1.1 101
状态代码和升级连接头。这通知浏览器,对于 TCP 会话的剩余部分,服务器正在从 HTTP 握手切换到 WebSocket 协议。
 注意如果你正在实现一个 WebSocket 服务器,你应该参考 IETF 在``tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol``的协议草案或者最新的规范。
`# write out response headers
self.send_bytes(“HTTP/1.1 101 Switching Protocols\r\n”)
self.send_bytes(“Upgrade: WebSocket\r\n”)
self.send_bytes(“Connection: Upgrade\r\n”)
self.send_bytes(“Sec-WebSocket-Accept: %s\r\n” % self.hash_key(key))
if “Sec-WebSocket-Protocol” in headers:
protocol = headers[“Sec-WebSocket-Protocol”]
self.send_bytes(“Sec-WebSocket-Protocol: %s\r\n” % protocol)`
WebSocket 框架
握手之后,客户端和服务器可以随时发送消息。在这个服务器中,每个连接都由一个WebSocketConnection
实例来表示。WebSocketConnection
的send
函数,如图图 7-6 所示,根据 WebSocket 协议写出一条消息。数据有效载荷之前的字节标记了帧的长度和类型。文本框架是 UTF-8 编码的。在这个服务器中,每个 WebSocket 连接都是一个asyncore.dispatcher_with_send
,它是一个异步套接字包装器,支持缓冲发送。
从浏览器发送到服务器的数据被屏蔽。屏蔽是 WebSocket 协议的一个不寻常的特性。有效负载数据的每个字节都与随机掩码进行异或运算,以确保 WebSocket 流量看起来不像其他协议。像 Sec-WebSocket-Key 散列一样,这是为了减轻对不兼容的网络基础设施的跨协议攻击的神秘形式。
***图 7-6。*web socket 框架的组件
注意Python 和其他语言还有很多其他的异步 I/O 框架。选择 Asyncore 是因为它包含在 Python 标准库中。还要注意的是,这个实现使用了协议草案 10。这是一个为测试和说明而设计的简单示例。
WebSocketConnection
继承了asyncore.dispatcher_with_send
并覆盖了send
方法,以便构造文本和二进制消息。
def send(self, s): if self.readystate == "open": self.send_bytes("\x00")
self.send_bytes(s.encode("UTF8")) self.send_bytes("\xFF")
websocket.py
中WebSocketConnections
的处理程序遵循一个简化的调度程序接口。处理程序的dispatch()
方法是用连接接收到的每个帧的有效负载来调用的。EchoHandler
将每条消息发送回发送者。
`class EchoHandler(object):
“”"
The EchoHandler repeats each incoming string to the same WebSocket.
“”"
def init(self, conn):
self.conn = conn
def dispatch(self, data):
self.conn.send("echo: " + data)`
基本广播服务器broadcast.py
的工作方式大致相同,但在这种情况下,当广播处理器接收到一个帧时,它会在所有连接的 WebSockets 上发回该帧,如下例所示:
`class BroadcastHandler(object):
“”"
The BroadcastHandler repeats incoming strings to every connected
WebSocket.
“”"
def init(self, conn):
self.conn = conn
def dispatch(self, data):
for session in self.conn.server.sessions:
session.send(data)`
broadcast.py
中的处理程序提供了一个轻量级的消息广播器,它简单地发送和接收任何数据。这对于我们的例子来说已经足够了。请注意,这个广播服务不执行任何输入验证,而这在生产消息服务器中是需要的。生产 WebSocket 服务器至少应该验证传入数据的格式。
为了完整起见,清单 7-5 和清单 7-6 提供了websocket.py
和broadcast.py
的完整代码。请注意,这只是一个示例服务器实现;它不适合生产部署。
清单 7-5。【websocket.py 的完整代码
`#!/usr/bin/env python
import asyncore
import socket
import struct
import time
from hashlib import sha1
from base64 import encodestring`
`class WebSocketConnection(asyncore.dispatcher_with_send):
TEXT = 0x01
BINARY = 0x02
def init(self, conn, server):
asyncore.dispatcher_with_send.init(self, conn)
self.server = server
self.server.sessions.append(self)
self.readystate = “connecting”
self.buffer = “”
def handle_read(self):
data = self.recv(1024)
self.buffer += data
if self.readystate == “connecting”:
self.parse_connecting()
elif self.readystate == “open”:
self.parse_frame()
def handle_close(self):
self.server.sessions.remove(self)
self.close()
def parse_connecting(self):
“”"
Parse a WebSocket handshake. This is not a full HTTP request parser!
“”"
header_end = self.buffer.find(“\r\n\r\n”)
if header_end == -1:
return
else:
header = self.buffer[:header_end]
# remove header and four bytes of line endings from buffer
self.buffer = self.buffer[header_end + 4:]
header_lines = header.split(“\r\n”)
headers = {}
# validate HTTP request and construct location
method, path, protocol = header_lines[0].split(" ")
if method != “GET” or protocol != “HTTP/1.1” or path[0] != “/”:
self.terminate()
return
# parse headers
for line in header_lines[1:]:
key, value = line.split(": ")
headers[key] = value`
` headers[“Location”] = “ws://” + headers[“Host”] + path
self.readystate = “open”
self.handler = self.server.handlers.get(path, None)(self)
self.send_server_handshake_10(headers)
def terminate(self):
self.ready_state = “closed”
self.close()
def send_server_handshake_10(self, headers):
“”"
Send the WebSocket Protocol draft HyBi-10 handshake response
“”"
key = headers[“Sec-WebSocket-Key”]
# write out response headers
self.send_bytes(“HTTP/1.1 101 Switching Protocols\r\n”)
self.send_bytes(“Upgrade: WebSocket\r\n”)
self.send_bytes(“Connection: Upgrade\r\n”)
self.send_bytes(“Sec-WebSocket-Accept: %s\r\n” % self.hash_key(key))
if “Sec-WebSocket-Protocol” in headers:
protocol = headers[“Sec-WebSocket-Protocol”]
self.send_bytes(“Sec-WebSocket-Protocol: %s\r\n” % protocol)
def hash_key(self, key):
guid = “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”
combined = key + guid
hashed = sha1(combined).digest()
return encodestring(hashed)
def parse_frame(self):
“”"
Parse a WebSocket frame. If there is not a complete frame in the
buffer, return without modifying the buffer.
“”"
buf = self.buffer
payload_start = 2
# try to pull first two bytes
if len(buf) < 3:
return
b = ord(buf[0])
fin = b & 0x80 # 1st bit
# next 3 bits reserved
opcode = b & 0x0f # low 4 bits
b2 = ord(buf[1])
mask = b2 & 0x80 # high bit of the second byte
length = b2 & 0x7f # low 7 bits of the second byte`
` # check that enough bytes remain
if len(buf) < payload_start + 4:
return
elif length == 126:
length, = struct.unpack(“>H”, buf[2:4])
payload_start += 2
elif length == 127:
length, = struct.unpack(“>I”, buf[2:6])
payload_start += 4
if mask:
mask_bytes = [ord(b) for b in buf[payload_start:payload_start + 4]]
payload_start += 4
# is there a complete frame in the buffer?
if len(buf) < payload_start + length:
return
# remove leading bytes, decode if necessary, dispatch
payload = buf[payload_start:payload_start + length]
self.buffer = buf[payload_start + length:]
# use xor and mask bytes to unmask data
if mask:
unmasked = [mask_bytes[i % 4] ^ ord(b)
for b, i in zip(payload, range(len(payload)))]
payload = “”.join([chr© for c in unmasked])
if opcode == WebSocketConnection.TEXT:
s = payload.decode(“UTF8”)
self.handler.dispatch(s)
if opcode == WebSocketConnection.BINARY:
self.handler.dispatch(payload)
return True
def send(self, s):
“”"
Encode and send a WebSocket message
“”"
message = “”
# always send an entire message as one frame (fin)
b1 = 0x80
# in Python 2, strs are bytes and unicodes are strings
if type(s) == unicode:
b1 |= WebSocketConnection.TEXT
payload = s.encode(“UTF8”)
elif type(s) == str:
b1 |= WebSocketConnection.BINARY
payload = s`
` message += chr(b1)
# never mask frames from the server to the client
b2 = 0
length = len(payload)
if length < 126:
b2 |= length
message += chr(b2)
elif length < (2 ** 16) - 1:
b2 |= 126
message += chr(b2)
l = struct.pack(“>H”, length)
message += l
else:
l = struct.pack(“>Q”, length)
b2 |= 127
message += chr(b2)
message += l
message += payload
if self.readystate == “open”:
self.send_bytes(message)
def send_bytes(self, bytes):
try:
asyncore.dispatcher_with_send.send(self, bytes)
except:
pass
class EchoHandler(object):
“”"
The EchoHandler repeats each incoming string to the same WebSocket.
“”"
def init(self, conn):
self.conn = conn
def dispatch(self, data):
try:
self.conn.send(data)
except:
pass
class WebSocketServer(asyncore.dispatcher):
def init(self, port=80, handlers=None):
asyncore.dispatcher.init(self)
self.handlers = handlers
self.sessions = []
self.port = port
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.set_reuse_addr()
self.bind((“”, port))
self.listen(5)
def handle_accept(self):
conn, addr = self.accept()
session = WebSocketConnection(conn, self)
if name == “main”:
print “Starting WebSocket Server”
WebSocketServer(port=8080, handlers={“/echo”: EchoHandler})
asyncore.loop()`
您可能已经注意到了 WebSocket 握手中一个不寻常的密钥计算。这是为了防止跨协议攻击。简而言之,这将阻止恶意 WebSocket 客户端代码欺骗到非 WebSocket 服务器的连接。散列一个 GUID 和一个随机值就足以确定响应服务器理解 WebSocket 协议。
清单 7-6。【broadcast.py 的完整代码
`#!/usr/bin/env python
import asyncore
from websocket import WebSocketServer
class BroadcastHandler(object):
“”"
The BroadcastHandler repeats incoming strings to every connected
WebSocket.
“”"
def init(self, conn):
self.conn = conn
def dispatch(self, data):
for session in self.conn.server.sessions:
session.send(data)
if name == “main”:
print “Starting WebSocket broadcast server”
WebSocketServer(port=8080, handlers={“/broadcast”: BroadcastHandler})
asyncore.loop()`
现在我们已经有了一个工作的 echo 服务器,我们需要编写客户端。web 浏览器实现 WebSocket 协议的连接部分。我们可以使用 JavaScript 中的 API 与我们的简单服务器进行通信。
使用 WebSocket API
在这一节中,我们将更详细地探索 WebSocket 的使用。
检查浏览器支持
在使用 WebSocket API 之前,您需要确保浏览器支持您将要做的事情。这样,您可以提供一些替代文本,提示应用的用户升级到更新的浏览器。清单 7-7 显示了一种测试浏览器支持的方法。
***清单 7-7。*检查浏览器支持
function loadDemo() { **if (window.WebSocket) {** document.getElementById("support").innerHTML = "HTML5 WebSocket is supported in your browser."; } else { document.getElementById("support").innerHTML = "HTML5 WebSocket is not supported in your browser."; } }
在这个例子中,您在loadDemo
函数中测试浏览器支持,这个函数可能在应用的页面加载时被调用。对window.WebSocket
的调用将返回WebSocket
对象(如果它存在的话),或者如果它不存在就触发失败案例。在这种情况下,通过用合适的消息更新页面上先前定义的support
元素,页面被更新以反映是否有浏览器支持。
查看 WebSocket 在您的浏览器中是否受支持的另一种方法是使用浏览器的控制台(例如 Firebug 或 Chrome 开发工具)。图 7-7 展示了如何测试 WebSocket 在 Google Chrome 中是否被原生支持(如果不支持,window.WebSocket
命令返回“未定义”))
***图 7-7。*在谷歌 Chrome 开发者工具中测试 WebSocket 支持
基本 API 用法
以下示例的示例代码位于图书网站的 WebSocket 部分。该文件夹包含一个 websocket.html 文件和一个 broadcast.html 文件(以及一个 tracker.html 文件,在下一节中使用)以及前面显示的可以在 Python 中运行的 WebSocket 服务器代码。
创建 WebSocket 对象并连接到 WebSocket 服务器
使用 WebSocket 接口非常简单。要连接到一个端点,只需创建一个新的 WebSocket 实例,为新对象提供一个表示您希望连接的端点的 URL。您可以使用ws://
和wss://
前缀分别表示 WebSocket 和 WebSocket 安全连接。
url = "ws://localhost:8080/echo"; w = new WebSocket(url);
当连接 WebSocket 时,您可以选择列出您的应用可以使用的协议。WebSocket 构造函数的第二个参数可以是一个字符串或字符串数组,其中包含应用理解并希望用来通信的“子协议”的名称。
w = new WebSocket(url, protocol);
您甚至可以列出几个协议:
w = new WebSocket(url, [“proto1”, “proto2”]);
假设,proto1 和 proto2 是定义良好的,甚至可能是注册和标准化的,客户机和服务器都能理解的协议名称。服务器将从列表中选择一个首选协议。当套接字打开时,它的协议属性将包含服务器选择的协议。
onopen = function(e) { // determine which protocol the server selected log(e.target.protocol) }
您可能使用的协议包括可扩展消息和存在协议(XMPP,或 Jabber)、高级消息队列协议(AMQP)、远程帧缓冲区(RFB,或 VNC)和面向流文本的消息协议(STOMP)。这些是许多客户端和服务器使用的真实协议。使用标准协议可以确保不同组织的 web 应用和服务器之间的互操作性。它也为公共 WebSocket 服务打开了大门。您可以使用已知的协议与服务器对话。理解相同协议的客户端应用可以连接并参与。
这个例子没有使用标准协议。我们不会引入外部依赖,也不会占用空间来实现完整的标准协议。例如,它直接使用 WebSocket API,就像您开始为新协议编写代码一样。
添加事件监听器
WebSocket 编程遵循异步编程模型;一旦你有一个打开的套接字,你只需要等待事件。您不必再主动轮询服务器。为此,您向WebSocket
对象添加回调函数来监听事件。
一个WebSocket
对象调度三个事件:打开、关闭和消息。open 事件在建立连接时触发,message 事件在收到消息时触发,close 事件在 WebSocket 连接关闭时触发。error 事件触发以响应意外失败。与大多数 JavaScript APIs 一样,在调度事件时会调用相应的回调函数(onopen
、onmessage
、onclose
和onerror
)。
w.onopen = function() { log("open"); w.send("thank you for accepting this websocket request"); } w.onmessage = function(e) { log(e.data); } w.onclose = function(e) { log("closed"); } w.onerror = function(e) { log(“error”); }
让我们再来看看这个消息处理程序。如果 WebSocket 协议消息被编码为文本,则消息事件的数据属性是一个字符串。对于二进制消息,数据可以是 Blob 或 ArrayBuffer,这取决于 WebSocket 的binaryType
属性的值。
w.binaryType = "arraybuffer"; w.onmessage = function(e) { // data can now be either a string or an ArrayBuffer log(e.data); }
发送消息
当套接字打开时(即在调用onopen
监听器之后和调用onclose
监听器之前),您可以使用send
函数发送消息。在发送一条或多条消息后,您也可以调用close
来终止连接,或者您也可以保持连接打开。
document.getElementById("sendButton").onclick = function() { w.send(document.getElementById("inputMessage").value); }
就这样。双向浏览器通信变得简单。为了完整起见,清单 7-8 显示了带有 WebSocket 代码的整个 HTML 页面。
在 WebSocket 的更高级的应用中,您可能希望在调用send()
之前测量在传出缓冲区中备份了多少数据。bufferedAmount
属性表示已经在 WebSocket 上发送但尚未写入网络的字节数。这对于限制应用发送数据的速率非常有用。
document.getElementById("sendButton").onclick = function() { if (w.bufferedAmount < bufferThreshold) { w.send(document.getElementById("inputMessage").value); } }
除了字符串,WebSocket 还可以发送二进制数据。这对于实现二进制协议特别有用,例如通常位于 TCP 之上的标准互联网协议。WebSocket API 支持将 Blob 和 ArrayBuffer 实例作为二进制数据发送。
var a = new Uint8Array([8,6,7,5,3,0,9]); w.send(a.buffer);
清单 7-8。【websocket.html 代码】??
`
Send
Running the WebSocket Page`
要测试包含 WebSocket 代码的websocket.html
页面,请打开命令提示符,导航到包含 WebSocket 代码的文件夹,并发出以下命令来托管 HTML 文件:
python -m SimpleHTTPServer 9999
接下来,打开另一个命令提示符,导航到包含 WebSocket 代码的文件夹,发出以下命令来运行 Python WebSocket 服务器:
python websocket.py
最后打开一个原生支持 WebSocket 的浏览器,导航到[
localhost:9999/websocket.html](http://localhost:9999/websocket.html)
。
图 7-8 显示了运行中的网页。
图 7-8。websocket.html 在行动
示例代码文件夹还包含一个网页,该网页连接到在上一节中创建的广播服务。要查看该操作,请关闭运行 WebSocket 服务器的命令提示符,导航到包含 WebSocket 代码的文件夹,并发出以下命令来运行 python WebSocket 服务器。
python broadcast.py
打开两个本地支持 WebSocket 的独立浏览器,并(在每个浏览器中)导航到[
localhost:9999/broadcast.html](http://localhost:9999/broadcast.html)
。
图 7-9 显示了在两个独立的网页上运行的广播 WebSocket 服务器。
图 7-9。【broadcast.html 在两个浏览器中的表现】??
构建 WebSocket 应用
现在我们已经看到了 WebSocket 的基础知识,是时候解决一些更实质性的问题了。之前,我们使用 HTML5 地理定位 API 构建了一个应用,允许我们直接在网页中计算行进的距离。我们可以利用这些相同的地理定位技术,结合我们对 WebSocket 的新支持,创建一个简单的应用来保持多个参与者的连接:位置跟踪器。
注意我们将使用上面描述的广播 WebSocket 服务器,所以如果你不熟悉它,你应该考虑花些时间学习它的基础知识。
在这个应用中,我们将通过确定我们的位置并将其广播给所有可用的侦听器来组合 WebSocket 和地理定位。加载这个应用并连接到同一个广播服务器的每个人都会定期使用 WebSocket 发送他们的地理位置。与此同时,应用将监听来自服务器的任何消息,并为它听到的每个人实时更新显示条目。在比赛场景中,这种应用可以让跑步者知道所有竞争对手的位置,并提示他们跑得更快(或更慢)。
这个微小的应用不包括除经纬度位置之外的任何个人信息。姓名、出生日期和最喜欢的冰淇淋口味都是严格保密的。
你被警告了!
Brian 说:“这个应用是关于分享你的个人信息的。当然,只有一个位置是共享的。然而,如果您(或您的用户)不理解首次访问地理位置 API 时出现的浏览器警告,那么这个应用应该是一个严峻的教训,告诉您将敏感数据传输到远程位置是多么容易。确保您的用户了解同意提交位置数据的后果。
当有疑问时,在你的应用中让用户知道如何使用他们的敏感数据。让选择退出成为最简单的行动方式。"
但是警告已经够多了…让我们深入研究代码。和往常一样,整个代码示例都在网上供您阅读。我们将在这里集中讨论最重要的部分。完成的应用将看起来像图 7-10 。尽管在理想情况下,将它叠加在地图上会得到增强。
***图 7-10。*位置跟踪器应用
编码 HTML 文件
该应用的 HTML 标记将刻意保持简单,以便我们可以专注于手头的数据。有多简单?
`
HTML5 WebSocket / Geolocation Tracker
HTML5 Geolocation is not supported in your browser.
WebSocket is not supported in your browser.
</body>
简单到我们只包括一个标题和几个状态区域:一个状态区域用于地理位置更新,另一个用于记录任何 WebSocket 活动。当消息被实时接收时,位置数据的实际图像将被插入到页面中。
默认情况下,我们的状态消息表明查看者的浏览器不支持地理定位或 WebSocket。一旦我们检测到对这两种 HTML5 技术的支持,我们会用一些更友好的东西来更新状态。
`
// reference to the WebSocket
var socket;
// a semi-unique random ID for this session
var myId = Math.floor(100000*Math.random());
// number of rows of data presently displayed
var rowCount = 0;`
这个应用的主要部分再次通过脚本代码完成。首先,我们将建立几个变量:
- 对我们的
socket
的全局引用,这样任何函数都可以在以后访问它。 - 0 到 100,000 之间的随机数字
myId
,用于在线识别我们的位置数据。该号码仅用于将位置随时间的变化关联回相同的来源,而不使用更多的个人信息,例如姓名。足够大的号码池使得一个以上的用户不太可能具有相同的标识符。 - 一个
rowCount
,它保存有多少独立用户已经将他们的位置数据传输给我们。这主要用于可视化格式化。
接下来的两个函数应该看起来很熟悉。与其他示例应用一样,我们提供了一些实用程序来帮助我们更新状态消息。这一次,有两条状态消息需要更新。
` function updateSocketStatus(message) {
document.getElementById(“socketStatus”).innerHTML = message;
}
function updateGeolocationStatus(message) {
document.getElementById(“geoStatus”).innerHTML = message;
}`
每当位置检索出错时,包含一组用户友好的错误消息总是很有帮助的。如果你需要更多关于地理定位相关的错误处理的信息,请查阅第五章。
function handleLocationError(error) { switch(error.code) { case 0: updateGeolocationStatus("There was an error while retrieving your location: " +
error.message); break; case 1: updateGeolocationStatus("The user prevented this page from retrieving a location."); break; case 2: updateGeolocationStatus("The browser was unable to determine your location: " + error.message); break; case 3: updateGeolocationStatus("The browser timed out before retrieving the location."); break; } }
添加 WebSocket 代码
现在,让我们检查一些更实质性的东西。在页面初始加载时调用loadDemo
函数,使其成为应用的起点。
` function loadDemo() {
// test to make sure that sockets are supported
if (window.WebSocket) {
// the location of our broadcast WebSocket server
url = “ws://localhost:8080”;
socket = new WebSocket(url);
socket.onopen = function() {
updateSocketStatus(“Connected to WebSocket tracker server”);
}
socket.onmessage = function(e) {
updateSocketStatus("Updated location from " + dataReturned(e.data));
}
}`
我们在这里做的第一件事是设置我们的 WebSocket 连接。与任何 HTML5 技术一样,在直接进入之前检查支持是明智的,因此我们测试以确保window.WebSocket
是该浏览器中受支持的对象。
验证完成后,我们使用上述连接字符串格式连接到远程广播服务器。连接存储在我们全局声明的socket
变量中。
最后,我们声明两个处理程序,当我们的 WebSocket 接收到更新时采取行动。onopen
处理程序将仅仅更新状态消息,让用户知道我们成功连接了。onmessage
将类似地更新状态,让用户知道消息已经到达。它还将调用我们即将到来的dataReturned
函数,在页面中显示到达的数据,但我们将在后面处理。
添加地理位置代码
下一节你应该从第五章开始就很熟悉了。这里,我们验证对地理定位服务的支持,并适当地更新状态消息。
` var geolocation;
if(navigator.geolocation) {
geolocation = navigator.geolocation;
updateGeolocationStatus(“HTML5 Geolocation is supported in your browser.”);
}
// register for position updates using the Geolocation API
geolocation.watchPosition(updateLocation,
handleLocationError,
{maximumAge:20000});
}`
和以前一样,我们观察当前位置的变化,并注册我们希望在发生变化时调用updateLocation
函数。错误被发送到handleLocationError
函数,位置数据被设置为每二十秒过期一次。
下一段代码是处理程序,每当有新位置可用时,浏览器都会调用该处理程序。
` function updateLocation(position) {
var latitude = position.coords.latitude;
var longitude = position.coords.longitude;
var timestamp = position.timestamp;
updateGeolocationStatus("Location updated at " + timestamp);
// Send my location via WebSocket
var toSend = JSON.stringify([myId, latitude, longitude]);
sendMyLocation(toSend);
}`
本节与第五章中的相同处理程序相似,但更简单。这里,我们从浏览器提供的位置获取纬度、经度和时间戳。然后,我们更新状态消息以表明新值已经到达。
把所有这些放在一起
最后一部分计算发送到远程广播 WebSocket 服务器的消息字符串。这里的字符串将被 JSON 编码:
"[<id>, <latitude>, <longitude>]"
ID 将是已经创建的用于标识该用户的随机计算值。纬度和经度由地理定位位置对象提供。我们将消息作为 JSON 编码的字符串直接发送给服务器。
将位置发送到服务器的实际代码驻留在sendMyLocation()
函数中。
function sendMyLocation(newLocation) { if (socket) {
socket.send(newLocation); } }
如果成功地创建了一个套接字,并存储起来供以后访问,那么就可以安全地将传递给这个函数的消息字符串发送给服务器。一旦它到达,WebSocket 消息广播服务器将把位置字符串分发给当前连接并监听消息的每个浏览器。每个人都会知道你在哪里。或者,至少,一个很大程度上匿名的“你”,只能用一个随机数来识别。
现在我们正在发送消息,让我们看看当这些消息到达浏览器时应该如何处理。回想一下,我们在套接字上注册了一个onmessage
处理程序,将任何传入的数据传递给一个dataReturned()
函数。接下来,我们将更详细地查看最终函数。
function dataReturned(locationData) { // break the data into ID, latitude, and longitude var allData = JSON.parse(locationData); var incomingId = allData[1]; var incomingLat = allData[2]; var incomingLong = allData[3];
dataReturned
功能有两个目的。它将在页面中创建(或更新)一个显示元素,显示传入消息字符串中反映的位置,并且它将返回发出该消息的用户的文本表示。用户名将由调用函数socket.onmessage
处理程序在页面顶部的状态消息中使用。
这个数据处理函数采取的第一步是使用 JSON.parse 将传入的消息分解成它的组成部分。虽然更健壮的应用需要检查意外的格式,但我们将假设所有发送到服务器的消息都是有效的,因此我们的字符串可以清晰地分为一个随机 id、一个纬度和一个经度。
// locate the HTML element for this ID // if one doesn't exist, create it var incomingRow = document.getElementById(incomingId); if (!incomingRow) { incomingRow = document.createElement('div'); incomingRow.setAttribute('id', incomingId);
我们的演示用户界面将为接收消息的每个随机 ID 创建一个可见的<div>
。这包括用户的 ID 本身;换句话说,用户自己的数据也只有在从 WebSocket 广播服务器发送并返回后才会显示。
相应地,我们对消息字符串中的 ID 做的第一件事就是用它来定位与之匹配的显示行元素。如果不存在,我们就创建一个,并将其 id 属性设置为从我们的 socket 服务器返回的 id,以便将来检索。
` incomingRow.userText = (incomingId == myId) ?
‘Me’ :
'User ’ + rowCount;
rowCount++;`
要在数据行中显示的用户文本很容易计算。如果 ID 和用户的 ID 匹配,就简单的说是‘我’。否则,用户名是一个普通字符串和行数的组合,我们将增加行数。
document.body.appendChild(incomingRow);
}
一旦新的显示元素准备好了,它就被插入到页面的末尾。不管显示元素是新创建的还是已经存在的——由于位置更新对于特定用户来说不是第一次——都需要用当前的文本信息更新显示行。
` // update the row text with the new values
incomingRow.innerHTML = incomingRow.userText + " \ Lat: " +
incomingLat + " \ Lon: " +
incomingLong;
return incomingRow.userText;
}`
在我们的例子中,我们将使用反斜杠(当然是正确转义的)将用户文本名称与纬度和经度值分开。最后,显示名称被返回给调用函数以更新状态行。
我们简单的 WebSocket 和地理位置混搭现在已经完成。尝试一下,但是请记住,除非有多个浏览器同时访问应用,否则您不会看到很多更新。作为对读者的一个练习,考虑更新这个例子,在全球 Google 地图上显示即将到来的位置,以了解 HTML5 在这个非常时刻的兴趣所在。
最终代码
为了完整起见,清单 7-9 提供了完整的tracker.html
文件。
清单 7-9。【tracker.html 电码】??
`
HTML5 WebSocket / Geolocation Tracker
HTML5 Geolocation is not supported in your browser.
WebSocket is not supported in your browser.
总结
在本章中,您已经看到了 WebSocket 如何提供一个简单而强大的机制来创建引人注目的实时应用。
首先,我们看了协议本身的性质,以及它如何与现有的 HTTP 流量进行互操作。我们比较了当前基于轮询的通信策略的网络开销需求和 WebSocket 的有限开销。
为了说明 WebSocket 的作用,我们探索了一个 WebSocket 服务器的简单实现,以展示在实践中实现这个协议是多么简单。类似地,我们研究了客户端 WebSocket API,注意到它提供的与 JavaScript 集成的便利性。
最后,我们浏览了一个更复杂的示例应用,它结合了地理定位和 WebSocket 的强大功能,展示了这两种技术如何很好地协同工作。现在我们已经看到了 HTML5 如何将 TCP 风格的网络编程引入浏览器,我们将把注意力转向收集更有趣的数据,而不仅仅是用户的当前位置。在下一章,我们来看看 HTML5 中对表单控件的增强。
八、使用表单 API
在这一章中,我们将探索一项由来已久的技术:HTML 表单所带来的所有新功能。自从表单第一次出现以来,它一直是网络爆炸的支柱。如果没有表单控件,web 商务交易、社交讨论和高效搜索根本就不可能实现。
可悲的是,HTML5 表单是规范和实现中变化最大的领域之一,尽管已经设计了很多年。有好消息也有坏消息。好消息是,这一领域的进步虽然是渐进的,但增长速度相当快。坏消息是,您需要仔细寻找可以在所有目标浏览器中工作的新表单控件的子集。表单规范详细说明了一大组 API,并且不难发现,符合 HTML5 的 web 浏览器的每个主要新版本都增加了对一个或多个表单控件和一些有用的验证功能的支持。
不管怎样,我们将利用这一章来帮助你在虚拟的控件海洋中导航,并找到哪些控件今天就可以使用,哪些控件即将发布。
html 5 表单概述
如果您已经熟悉 HTML 中的表单——如果您对专业 HTML 编程感兴趣,我们假设您已经熟悉——那么您会发现 HTML5 中的新功能非常适合坚实的基础。如果您还不熟悉表单的基本用法,我们推荐您阅读大量关于创建和处理表单值的书籍和教程。这个主题在这一点上得到了很好的阐述,您会很高兴地知道:
- 表单仍然应该封装在一个设置了基本提交属性的
<form>
元素中。 - 当用户或应用程序员提交页面时,表单仍然会将控件的值发送到服务器。
- 所有熟悉的表单控件——文本字段、单选按钮、复选框等等——仍然存在,并像以前一样工作(尽管增加了一些新功能)。
- 对于那些希望编写自己的修饰符和处理程序的人来说,表单控件仍然是完全脚本化的。
HTML 表单与 XForms
在过去几年里,早在 HTML5 取得很大进展之前,您可能就听说过 XForms。XForms 是一个以 XML 为中心的、强大的、有点复杂的标准,用于指定客户端表单行为,已经在它自己的 W3C 工作组中发展了近十年。XForms 利用 XML Schema 的全部功能来定义精确的验证和格式化规则。不幸的是,如果没有额外的插件,目前主流浏览器都不支持 XForms。
HTML5 表单不是 XForms。
功能形式
相反,HTML5 Forms 专注于发展现有的简单 HTML 表单,以包含更多类型的控件,并解决 web 开发人员今天面临的实际限制。有一点需要记住,特别是在比较不同浏览器的表单实现时。
注意关于 HTML5 表单,要掌握的最重要的概念是,规范处理的是功能行为和语义,而不是外观或显示。
例如,虽然规范详细说明了诸如颜色和日期选择器、数字选择器和电子邮件地址输入等元素的功能 API,但是规范并没有说明浏览器应该如何将这些元素呈现给最终用户。从多个层面来看,这是一个很好的选择。它允许浏览器在提供用户交互的创新方式上竞争;它将样式和语义分开;并且它允许未来或专门的用户输入设备以对其操作自然的方式进行交互。但是,在目标浏览器平台支持应用中的所有表单控件之前,请确保为用户提供足够的上下文信息,以便他们知道如何与回退呈现进行交互。有了正确的提示和描述,用户使用您的应用将不会有任何问题,即使它在出现未知输入类型时退回到替代内容。
HTML5 表单包含了大量新的 API 和元素类型,现在对它们的支持无处不在。为了理解所有的新功能,我们将把它分成两类
- 新输入类型
- 新功能和属性
然而,在我们开始之前,让我们快速评估一下当今的浏览器是如何支持 HTML5 表单规范的。
浏览器支持 HTML5 表单
浏览器对 HTML5 表单的支持正在增长,但仍然有限。主要的浏览器供应商都支持许多表单控件,Opera 在早期实现中处于领先地位。但是,规格是稳定的。
检查浏览器支持在新表单的上下文中用处不大,因为它们被设计为在旧浏览器中优雅地降级。很大程度上,这意味着现在使用新的元素是安全的,因为旧的浏览器对于它们不理解的任何输入类型都会退回到简单的文本字段显示。然而,正如我们将在本章后面看到的,这提高了多层表单验证的重要性,因为仅仅依靠浏览器验证器来强制表单控件的数据类型是不够的,即使您假设完全支持现代浏览器。
现在我们已经了解了浏览器的前景,让我们来看看 HTML5 规范中添加的新表单控件。
一个输入目录
获得 HTML5 中所有新增和变更元素的目录的最佳地方之一是 W3C 站点本身维护的标记列表。W3C 在[
dev.w3.org/html5/markup/](http://dev.w3.org/html5/markup/)
保存一个目录页文件
这个页面表示 HTML 页面中所有当前和未来的元素。目录列表中注明了新的和更改的元素。然而,这个列表中的“新”仅仅意味着该元素是在 HTML4 规范之后添加的——并不意味着该元素已经在浏览器或最终规范中实现。有了这个警告,让我们看看 HTML5 带来的新表单元素,从今天实现的表单元素开始。表 8-1 列出了新的type
属性。例如,许多 HTML 开发者会非常熟悉<input type="text">
和<input type="checkbox">
。新的输入类型遵循与现有输入类型相似的模型。
这些新的输入类型提供了什么?就编程 API 而言…不是很多。事实上,就tel
、email
、url
和search
的类型而言,没有属性将它们与最简单的输入类型text
区分开来。
那么,通过指定一个输入是专用类型,您到底得到了什么呢?你有专门的输入控制。(可能会有限制。在许多桌面浏览器中提供 void。)
让我们用一个例子来说明。通过指定输入是类型email
<input type="email">
而不是使用传统的标准,即字段仅仅是文本类型
<input type="text">
你向浏览器提供一个提示,在适当的时候呈现不同的用户界面或输入。您还为浏览器提供了在提交之前进一步验证字段的能力,但是我们将在本章的后面讨论这个主题。
移动设备浏览器是支持这些新的表单输入类型最快的浏览器之一。在手机上,对于没有完整键盘的用户来说,每一次按键都是更大的负担。因此,移动设备浏览器通过基于声明的类型显示不同的输入界面来支持这些新的输入类型。在苹果 iPhone 中,输入类型为text
的标准屏幕键盘显示如图 8-1 中的所示。
***图 8-1。*输入文本的屏幕键盘显示
然而,当一个输入字段被标记为e-mail
类型时,iPhone 会呈现一个为电子邮件输入定制的不同键盘布局,如图图 8-2 所示。
***图 8-2。*电子邮件输入的屏幕键盘显示
请注意对键盘空格键区域的细微调整,以允许@符号和方便地访问句点。对类型URL
和类型search
的键盘布局进行了类似的调整。然而,在桌面版本的 Safari 浏览器中,以及在任何没有明确支持e-mail
、URL
、search
和tel
类型的浏览器中,只会显示普通的文本输入栏。未来的浏览器,甚至是桌面版本,可能会向用户提供视觉提示或线索,以指示该字段是某个子类型。例如,Opera 会在一个字段旁边显示一个小信封图标,表示它需要一个电子邮件地址。然而,在今天的 web 应用中使用这些类型是安全的,因为任何浏览器要么针对该类型进行优化,要么干脆什么都不做。
另一种在浏览器中日益流行的特殊类型是<input type="range">
。这种专门的输入控件旨在让用户从一系列数字中进行选择。例如,可以在表单中使用范围控件,从限制 18 岁以下未成年人访问的范围中选择年龄。通过创建一个范围输入并设置其特殊的min
和max
值,开发人员可以请求页面显示一个受约束的数字选择器,该选择器只能在指定的范围内操作。例如,在 Opera 浏览器中,控件:
<input type="range" min="18" max="120">
为受年龄限制的材料选择合适的值提供了一种便捷的方法。在 Opera 浏览器中,它显示如下:
不幸的是,范围输入本身并不显示浏览器的数字表示。此外,如果没有,用户实际上不可能知道当前选择的值是什么。要解决这个问题,我们可以很容易地添加一个onchange
处理程序,根据当前范围值的变化来更新显示字段,如清单 8-1 中的所示。
 注意为什么`range`元素默认不包含可视化显示?也许是这样,用户界面设计者可以定制显示器的确切位置和外观。使显示可选会增加一些工作量,但更具灵活性。
新的表单控件现在包括一个简单的输出元素,它就是为这种类型的操作而设计的。输出是一个表单元素,它只保存一个值。因此,我们可以用它来显示范围控件的值。
清单 8-1。 onchange
处理程序更新一个输出
<label for="age">Age</label> <input id="age" type="range" min="18" max="120" value="18" onchange="ageDisplay.value=value"> <output id="ageDisplay">18</output>
这很好地显示了我们的范围输入,如下所示:
Opera 和基于 WebKit 的浏览器——Safari 和 Chrome——现在增加了对 type range
元素的支持。Firefox 支持是计划中的,但在撰写本文时还没有计划。当呈现一个range
输入类型时,Firefox 将退回到一个简单的文本元素。
另一个获得广泛支持的新表单元素是 progress 元素。progress 元素完全按照您的预期工作;它以方便的可视化格式显示任务完成的百分比。
进步可以是确定的,也可以是不确定的。可以把不确定的进度想象成一个花费未知时间的任务,但是你要向用户保证已经取得了一些进展。要显示不确定的进度元素,只需包含一个没有属性的元素:
<progress></progress>
不确定的进度条通常显示一个移动的进度条,但没有总完成百分比的指示器。
另一方面,一个确定的进度条以百分比的形式显示已完成的工作。要触发一个确定的进度条显示,在元素上设置value
和max
属性。通过将您设置的value
除以您设置的max
来计算显示为已完成的条形的百分比。为了便于计算,它们可以是您选择的任何值。例如,要显示 30%的完成情况,我们可以创建一个进度元素,如:
<progress value=”30” max=”100”></progress>
通过设置这些值,用户可以快速看到长时间运行的操作或多步骤流程完成了多少。使用脚本来更改 value 属性,可以很容易地更新显示来指示朝着最终目标的进展。
这里有龙
布莱恩说:“据说历史上‘这里有龙’这个短语被用来在地图上标示潜伏着未知危险的危险区域。下面的表单元素也是如此。尽管它们被详细说明,并且已经存在了很长时间,但是大多数都缺乏实际的执行。
因此,从现在到浏览器开发人员有机会参与设计、磨平粗糙的边缘,并以反馈和变化作出回应的这段时间,预计会有很大的变化。不要认为下面的组件是不可避免的,把它们看作 HTML5 表单发展方向的标志。如果你今天试图使用它们,风险由你自己承担……”
计划中但尚未得到广泛支持的其他表单元素包括表 8-2 中列出的元素。
尽管这些元素的一些早期实现开始出现在前沿浏览器中(例如,Opera 中的日期时间显示,如图 8-3 中所示),但我们在本章中不会关注它们,因为它们可能会经历重大的变化。敬请关注未来的改版!
***图 8-3。*显示日期时间类型的输入
使用 HTML5 表单 API
现在我们已经花了一些时间来熟悉新的表单元素类型,让我们来看看新旧表单控件上都存在的属性和 API。其中许多都是为了减少创建强大的 web 应用用户界面所需的脚本数量。您可能会发现,新的属性为您提供了从未考虑过的增强用户界面的能力。或者,至少,您可以删除现有页面中的脚本块。
新的表单属性和功能
首先,我们将考虑新的属性、功能和一些以前在 HTML 早期版本中不存在的元素。像新的输入类型一样,现在使用这些属性通常是安全的,不管您的目标浏览器是否支持它们。这是因为如果浏览器不理解这些属性,那么今天市场上的任何浏览器都会安全地忽略这些属性。
占位符属性
属性为输入控件提供了一种简单的方式来提供描述性的替代提示文本,只有当用户还没有输入任何值时才会显示。这在许多现代用户界面框架中很常见,流行的 JavaScript 框架也提供了对这一特性的模拟。然而,现代浏览器内置了这一功能。
要使用这个属性,只需将它添加到一个带有文本表示的输入中。这包括基本的文本类型,以及诸如email
、number
、url
等语义类型。
<label>Runner: <input name="name" placeholder="First and last name"></label>
在现代浏览器中,这会导致该字段显示一个模糊的占位符文本,当用户或应用将焦点放在该字段上时,或者当存在一个值时,该文本就会消失。
当在不支持的浏览器中运行时,相同的属性将被忽略,导致显示默认的字段行为。
同样,只要在字段中输入值,占位符文本就不会出现。
自动完成属性
Internet Explorer 5.5 中引入的autocomplete
属性终于被标准化了。万岁!(浏览器几乎从一开始就支持该属性,但是拥有一个指定的行为对每个人都有帮助。)
autocomplete
属性告诉浏览器是否应该保存这个输入的值以备将来使用。例如:
<input type="text" name="creditcard" autocomplete="off">
应该使用autocomplete
属性来保护敏感的用户数据不被不安全地存储在本地浏览器文件中。表 8-3 显示了不同的行为类型。
自动对焦属性
属性让开发人员指定给定的表单元素应该在页面加载时立即获得输入焦点。每页只有一个属性应该指定autofocus
属性。如果多个控件设置为自动聚焦,则行为未定义。
注意如果您的内容被呈现到门户或共享内容页面中,每页仅一个自动对焦控件是难以实现的。如果你不能完全控制页面,不要依赖自动对焦。
要将焦点自动设置到一个控件上,比如一个搜索文本字段,只需在该元素上设置autofocus
属性:
<input type="search" name="criteria" autofocus>
像其他布尔属性一样,真实情况下不需要指定值。
注意如果用户不希望改变焦距,自动对焦会让他们很恼火。许多用户利用击键来导航,而将焦点切换到表单控件会破坏这种能力。只有当一个表单控件应该接受所有默认键时,才使用它。
拼写检查属性
可以在具有文本内容的输入控件以及 textarea 上设置拼写检查属性。当设置时,它向浏览器建议是否应该给出拼写反馈。这个元素的一个正常表示是在文本下画一条红色虚线,它不映射当前设置的字典中的任何条目。这提示用户仔细检查拼写或从浏览器本身获得建议。
注意,spellcheck
属性需要一个值。不能只在元素上单独设置属性。
<textarea id=”myTextArea” spellcheck=”true”>
还要注意,大多数浏览器默认打开拼写检查,所以除非元素(或其父元素之一)关闭拼写检查,否则它将默认显示。
列表属性和数据列表元素
属性和元素结合起来让开发者为输入指定一个可能值的列表。要使用这种组合:
- 在文档中创建一个
datalist
元素,将它的id
设置为一个惟一的值。数据列表可以位于文档中的任何位置。 - 根据需要用尽可能多的
option
元素填充datalist
,以表示控件值的完整建议集。例如,代表电子邮件联系人的datalist
应该包含所有联系人的电子邮件地址,作为单独的option
子节点。<datalist id="contactList"> <option value="x@example.com" label="Racer X"> <option value="peter@example.com" label="Peter"> </datalist>
- 通过将
list
属性设置为相关datalist
的id
值,将输入元素链接到datalist
。<input type="email" id="contacts" list="contactList">
在支持的浏览器上,这将生成如下所示的自定义列表控件:
最小值和最大值属性
正如我们之前在<input type="range">
的例子中看到的,min
和max
属性允许将数字输入限制为最小和最大值。可以根据需要提供这些属性中的一个、两个或两个都不提供,并且输入控件应该相应地调整以增加或减少可接受值的范围。例如,要创建一个表示从 0%到 100%的能力置信度的范围控件,可以使用以下代码:
<input id="confidence" name="level" type="range" min="0" max="100" value="0">
这将创建一个范围控件,其最小值为 0,最大值为 100,巧合的是,这两个值都是该控件的默认值。
步骤属性
此外,对于需要数值的输入类型,step
属性指定了调整范围时值的增量或减量的粒度。例如,我们上面列出的置信水平范围控制可以用五的step
属性来设置,如下所示:
<input id="confidence" name="level" type="range" min="0" max="100" step="5" value="0">
这将把可接受的值限制为从起始值开始的 5 个增量。换句话说,根据输入的浏览器表示,通过键入的输入或通过滑块控件,只允许 0、5、10、15、… 100。
默认的step
值取决于它所应用的控制类型。对于range
输入,默认步长为一步。为了配合step
属性,HTML5 在 input 元素上引入了两个允许控制值的函数:stepUp
和stepDown
。
如您所料,这些函数分别递增或递减当前值。如您所料,该值增加或减少的量就是该步骤的值。因此,数字输入控件的值可以在没有用户直接输入的情况下进行调整。
valueas number 函数
新的valueAsNumber
函数是一种将控件的值从文本转换成数字的简便方法……反之亦然!这是因为valueAsNumber
既是 getter 函数也是 setter 函数。当作为 getter 调用时,valueAsNumber
函数将输入字段的文本值转换成允许计算的数字类型。如果文本值没有完全转换成number
类型,那么将返回NaN
值(不是数字)。
valueAsNumber
也可用于将输入值设置为数字类型。例如,我们的置信范围可以使用以下呼叫来设置:
document.getElementById("confidence").valueAsNumber(65);
确保数字满足min
、max
和step
的要求,否则将抛出错误。
所需属性
如果任何输入控件设置了required
属性,那么在提交表单之前必须设置一个值。例如,要根据需要设置文本输入字段,只需添加如下所示的属性:
<input type="text" id="firstname" name="first" required>
如果这个字段没有设置值,无论是通过编程还是由用户设置,提交这个表单的能力将被阻止。属性是最简单的表单验证类型,但是验证的能力非常强大。现在让我们更详细地讨论表单验证。
通过验证检查表单
在我们深入细节之前,让我们回顾一下表单验证到底需要什么。表单验证的核心是一个检测无效控制数据并为最终用户标记这些错误的系统。换句话说,表单验证是一系列的检查和通知,让用户在将表单提交给服务器之前更正表单的控件。
但是什么是表单验证呢?
这是一种优化。
表单验证是一种优化,因为它本身不足以保证提交给服务器的表单是正确和有效的。这是一种优化,因为它旨在帮助 web 应用快速失败。换句话说,最好是使用浏览器的内置处理来通知用户页面中包含无效的表单控件。为什么要花费网络往返的费用,只是为了让服务器通知用户数据输入中有一个打字错误呢?如果浏览器拥有在错误离开客户端之前捕捉错误的所有知识和能力,我们应该利用这一点。
但是,浏览器表单检查不足以处理所有错误。
恶意还是误解?
Brian 说:“尽管 HTML5 规范在提高浏览器中检查表单的能力方面取得了很大进步,但它仍然不能取代服务器验证。可能永远不会。
显然,有许多错误情况需要服务器交互来验证,比如信用卡是否被授权进行购买,甚至是基本的身份验证。然而,即使是普通的验证也不能仅仅依赖于客户端。一些用户可能正在使用不支持表单验证功能的浏览器。一些人可能会完全关闭脚本,这最终会禁用除了最简单的基于属性的验证器之外的所有验证器。然而,其他用户可以利用各种工具,如 Greasemonkey 浏览器插件来修改页面内容,以适应他们的需要。呃,内容。这可能包括删除所有表单验证检查。最终,依赖客户端验证作为检查任何重要数据的唯一方法是不够的。如果它存在于客户端,就可以被操纵。
HTML5 表单验证让用户快速获得重要的反馈,但不要依赖它来获得绝对的正确性!"
也就是说,HTML5 引入了八种简便的方法来加强表单控件输入的正确性。让我们依次检查它们,从让我们访问它们状态的对象开始:ValidityState
。
可以从支持 HTML5 表单验证的浏览器中的任何表单控件访问ValidityState
:
var valCheck = document.myForm.myInput.validity;
这个简单的命令获取一个名为myInput
的表单元素的ValidityState
对象的引用。该对象包含对八种可能的有效性状态的方便引用,以及一个全面的有效性汇总检查。您可以通过调用以下命令来获取该表单的整体状态:
valCheck.valid
这个调用将提供一个布尔值,通知我们这个特定的表单控件当前是否满足所有的有效性约束。把valid
标志看作一个总结:如果所有八个约束都通过了,那么valid
标志将为真。否则,如果任何有效性约束失败,valid
属性将为假。
 注`ValidityState`物体是一个活的物体。一旦您获取了对它的引用,您就可以保持对它的控制,当发生变化时,它返回的有效性检查将根据需要进行更新。
如前所述,任何给定的表单元素都有八种可能的有效性约束。通过访问具有适当名称的字段,可以从ValidityState
访问每一个。让我们看看它们是什么意思,如何在表单控件上执行它们,以及如何使用ValidityState
来检查它们:
值缺失
目的:确保在这个表单控件上设置了一些值
用法:将表单控件上的required
属性设置为 true
用法举例 : <input type="text" name="myText" **required>**
细节:如果在表单控件上设置了required
属性,那么控件将处于无效状态,除非用户或编程调用为该字段设置一些值。例如,空白文本字段将无法通过要求的检查,但只要输入任何文本,就会通过检查。为空时,valueMissing
将返回 true。
类型不匹配
目的:保证值的类型符合预期(数字、电子邮件、URL 等等)
用法:在表单控件上指定一个合适的type
属性
用法举例 : <input type="email" name="myEmail">
细节:特殊的表单控件类型不只是针对定制的手机键盘!如果浏览器可以确定输入到表单控件中的值不符合该类型的规则(例如,没有@符号的电子邮件地址),浏览器可以将该控件标记为类型不匹配。另一个例子是不能解析为有效数字的数字字段。无论哪种情况,typeMismatch
都将返回true
。
模式匹配
目的:在表单控件上强制执行任何模式规则集,该规则集详细说明了特定的有效格式
用法:用合适的模式设置表单控件上的pattern
属性
用法举例 : <input type="number" name="creditcardnumber" pattern="[0-9]{16}" title="A credit card number is 16 digits with no spaces or dashes">
细节:pattern
属性为开发人员提供了一种强大而灵活的方式,在表单控件的值上实施正则表达式模式。当在控件上设置模式时,只要值不符合模式的规则,patternMismatch
就会返回 true。为了帮助用户和辅助技术,你应该在任何模式控制的字段上设置title
来描述格式的规则。
工具长
目的:确保一个值不包含太多字符
用法:在表单控件上放置一个maxLength
属性
用法举例 : <input type="text" name="limitedText" **maxLength="140"**>
细节:如果值长度超过了maxLength
,这个幽默命名的约束将返回 true。虽然表单控件通常会尝试在用户输入时强制最大长度,但某些情况(包括编程设置)可能会导致该值超过最大值。
支流
目的:强制数值控制的最小值
用法:设置一个min
属性的最小允许值
用法举例 : <input type="range" name="ageCheck" **min="18"**>
细节:在任何进行数值范围检查的表单控件中,数值都有可能被临时设置在允许的范围之下。在这些情况下,ValidityState
将为rangeUnderflow
字段返回 true。
范围溢出
目的:强制数值控制的最大值
用法:设置一个max
属性的最大允许值
用法举例 : <input type="range" name="kidAgeCheck" **max="12"**>
细节:类似于它的对应物rangeUnderflow
,如果一个表单控件的值大于max
属性,这个有效性约束将返回true
。
stepMismatch
目的:保证一个值符合min
、max
、step
的组合
用途:设置步长属性,指定数值的粒度步长
用法举例 : <input type="range" name="confidenceLevel" min="0" max="100" **step="5"**>
细节:该约束增强了min
、max
和step
组合的健全性。具体来说,当前值必须是最小值加上步长的倍数。例如,从 0 到 100 的范围,步长为每 5 步,如果stepMismatch
不返回 true,则不允许值为 17。
客户错误
目的:处理应用代码明确计算和设置的错误
用法:调用setCustomValidity(message)
将表单控件置于customError
状态
用法举例 : passwordConfirmationField.setCustomValidity("Password values do not match.");
细节:对于内置有效性检查不适用的情况,自定义有效性错误就足够了。每当一个字段不符合语义规则时,应用代码应该设置一个自定义的有效性消息。
自定义有效性的一个常见用例是当控件之间没有实现一致性时,例如,如果密码确认字段不匹配。(我们将在“实用的额外功能”部分深入研究这个具体的例子。)无论何时设置自定义有效性消息,控件都将无效,并将customError
约束作为true
返回。要清除错误,只需用空字符串值调用控件上的setCustomValidity("")
。
验证字段和功能
总之,这八个约束允许开发人员找出给定表单控件未通过验证检查的确切原因。或者,如果您不关心是哪个具体原因导致了失败,只需访问ValidityState
上的布尔值valid
;它是其他八个约束的集合。如果所有八个约束都返回false
,那么valid
字段将返回true
。表单控件上还有一些其他有用的字段和函数,可以帮助您进行验证检查编程。
will validate 属性
属性仅仅表明是否在这个表单控件上检查验证。如果存在上述任何约束条件,例如required
属性、pattern
属性等。-----------------------------------------------------------------------------,那么,-----------------------------------------------------------------------《??》将会让您知道将会强制执行验证检查。
检查有效性功能
checkValidity
函数允许您在没有任何显式用户输入的情况下检查表单的有效性。通常,每当用户或脚本代码提交表单时,都会检查表单的有效性。这个函数允许在任何时候进行验证。
 注意在表单控件上调用`checkValidity`并不仅仅是检查有效性,它会导致所有的结果事件和 UI 触发器发生,就好像表单已经被提交了一样。
验证消息属性
目前的浏览器版本还不支持这个属性,但是当你读到这篇文章的时候,可能已经支持了。validationMessage
属性允许您以编程方式查询浏览器基于当前验证状态显示的本地化错误消息。例如,如果一个required
字段没有值,浏览器可能会向用户显示一条错误消息“这个字段需要一个值。”一旦得到支持,这就是由validationMessage
字段返回的文本字符串,它将根据控件的当前验证状态进行调整。
验证反馈
关于验证反馈……到目前为止,我们避免的一个话题是浏览器应该如何以及何时向用户提供关于验证错误的反馈。该规范没有规定如何更新用户界面来显示错误信息,现有的实现也有很大的不同。以歌剧为例。在 Opera 10.5 中,浏览器通过用弹出消息和闪烁的红色字段标记错误字段来指示发生了验证错误:
相比之下,在撰写本文时,谷歌 Chrome 13 浏览器只导航到违规字段,并在发现错误时将焦点放在那里。什么是正确的行为?
两者都没有指定。但是,如果您想在验证错误发生时控制显示给用户的反馈,有一个合适的处理程序可以帮您做到这一点:invalid
事件。
每当检查表单的有效性时——无论是由于表单被提交,还是由于直接调用了checkValidity
函数——任何处于无效状态的表单都将被传递一个invalid
事件。可以忽略、观察甚至取消该事件。要将一个事件处理程序添加到接收该通知的字段中,添加一些类似于清单 8-2 的代码。
***清单 8-2。*为无效事件添加事件处理程序
`// event handler for “invalid” events
function invalidHandler(evt) {
var validity = evt.srcElement.validity;
// check the validity to see if a particular constraint failed
if (validity.valueMissing) {
// present a UI to the user indicating that the field is missing a value
}
// perhaps check additional constraints here…
// If you do not want the browser to provide default validation feedback,
// cancel the event as shown here
evt.preventDefault();
}
// register an event listener for “invalid” events
myField.addEventListener(“invalid”, invalidHandler, false);`
让我们把这段代码分解一下。
首先,我们声明一个处理程序来接收invalid
事件。我们在处理程序中做的第一件事是检查事件的来源。回想一下,invalid
事件是在表单控件上触发的,带有一个验证错误。因此,事件的srcElement
将是行为不端的表单控件。
从源开始,我们获取validity
对象。使用这个ValidityState
实例,我们可以检查它的单个约束字段,以确定到底哪里出错了。在这种情况下,因为我们知道我们的字段有一个required
属性,我们首先检查是否违反了valueMissing
约束。
如果检查成功,我们可以修改页面上的用户界面,通知用户需要为出错的字段输入一个值。也许可以显示一个警告或信息错误区域?这由你来决定。
一旦我们告诉用户错误是什么以及如何纠正它,我们需要决定是否希望浏览器本身显示其内置的反馈。默认情况下,浏览器会这样做。为了防止浏览器显示自己的错误信息,我们可以调用evt.preventDefault()
来停止默认处理,完全由我们自己来处理。
再一次,这里的选择是你的。HTML5 表单 API 为您提供了实现自定义 API 或恢复默认浏览器行为的灵活性。
关闭验证
尽管验证 API 背后有强大的功能,但还是有……(咳咳)正当的理由让你想关闭对一个控件或整个表单的验证。最常见的原因是,您可能会选择提交表单的临时内容,以便以后保存或检索,即使这些内容还不是非常有效。
想象一下这样一种情况,一个用户正在输入一个复杂的订单输入表单,但是在这个过程的中途需要跑一趟腿。理想情况下,您可以为用户提供一个“保存”按钮,通过将表单的值提交给服务器来保存这些值。但是,如果表单只完成了一部分,验证规则可能会阻止提交内容。如果用户由于意外中断而不得不完成或放弃表单,她会非常不高兴。
为了处理这个问题,可以用属性noValidate
编程设置表单本身,这将导致它放弃任何验证逻辑,只提交表单。当然,这个属性可以通过脚本或原始标记来设置。
关闭验证的一个更有用的方法是在控件上设置一个formNoValidate
属性,比如表单提交按钮。以下面的提交按钮,设置为"保存"按钮为例:
<input type="submit" formnovalidate name="save" value="Save current progress"> <input type="submit" name="process" value="Process order">
这个代码片段将创建两个普通外观的提交按钮。第二个将像往常一样提交表单。然而,第一个按钮被标记了noValidate
属性,导致在使用它时所有的验证都被绕过。这允许将数据提交给服务器,而无需检查正确性。当然,您的服务器需要设置为处理未经验证的数据,但是最佳实践表明这应该是始终如此。
使用 HTML5 表单构建应用
现在,让我们使用本章描述的工具创建一个简单的注册页面,展示 HTML5 表单的新特性。回到我们熟悉的 Happy Trails Running Club,我们将创建一个包含新表单元素和验证的比赛注册页面。
和往常一样,我们在这里展示的演示文件的源代码可以在 code/forms 文件夹中找到。因此,我们将减少对 CSS 和外围标记的关注,更多地关注页面本身的核心。话虽如此,让我们先来看看图 8-4 所示的完成页面,然后把它分成几个部分,逐一解决。
***图 8-4。*带有比赛注册表单的示例页面
这个注册页面展示了我们在本章中探索过的许多元素和 API,包括验证。尽管实际显示在您的浏览器上可能会有所不同,但即使浏览器不支持某个特定功能,它也会正常降级。
继续编码!
页眉、导航和页脚在我们之前的例子中已经出现过了。该页面现在包含一个<form>
元素。
<form name="register"> <p><label for="runnername">Runner:</label> <input id="runnername" name="runnername" type="text" placeholder="First and last name" required></p> <p><label for="phone">Tel #:</label> <input id="phone" name="phone" type="tel" placeholder="(xxx) xxx-xxx"></p> <p><label for="emailaddress">E-mail:</label> <input id="emailaddress" name="emailaddress" type="email" placeholder="For confirmation only"></p> <p><label for="dob">DOB:</label> <input id="dob" name="dob" type="date" placeholder="MM/DD/YYYY"></p>
在第一部分,我们看到了四个主要输入的标记:姓名、电话、电子邮件和生日。对于每个控件,我们都设置了一个带有描述性文本的<label>
,并使用for
属性将它绑定到实际控件。我们还设置了占位符文本,向用户显示内容类型的描述。
对于跑步者姓名文本字段,我们通过设置required
属性使其成为必需值。如果没有输入任何内容,这将导致表单验证使用一个valueMissing
约束。在电话输入上,我们已经声明它的类型是tel
。您的浏览器可能会以不同的方式显示该字段,也可能不会提供优化的键盘。
类似地,电子邮件字段被标记为类型e-mail
。任何特定的处理都取决于浏览器。如果一些浏览器检测到输入的值不是有效的电子邮件,它们会抛出一个typeMismatch
约束。
最后,出生日期字段被声明为类型date
。还没有多少浏览器支持这一点,但是当它们支持时,它们会在这个输入上自动呈现一个日期选择控件。
<fieldset> <legend>T-shirt Size: </legend> <p><input id="small" type="radio" name="tshirt" value="small"> <label for="small">Small</label></p> <p><input id="medium" type="radio" name="tshirt" value="medium"> <label for="medium">Medium</label></p> <p><input id="large" type="radio" name="tshirt" value="large"> <label for="large">Large</label></p> <p><label for="style">Shirt style:</label> <input id="style" name="style" type="text" list="stylelist" title="Years of participation"></p> <datalist id="stylelist"> <option value="White" label="1st Year"> <option value="Gray" label="2nd - 4th Year"> <option value="Navy" label="Veteran (5+ Years)"> </datalist> </fieldset>
在下一节中,我们将设置用于 t 恤选择的控件。前几个控件是用于选择衬衫尺码的一组标准单选按钮。
下一节更有趣。这里,我们使用了list
属性和它对应的<datalist>
元素。在<datalist>,
中,我们声明了一组应该显示在这个列表中的类型,它们具有不同的值和标签,代表基于退伍军人身份的可用 t 恤类型。虽然这个列表非常简单,但是同样的技术也可以用于很长的动态元素列表。
<fieldset> <legend>Expectations:</legend> <p> <label for="confidence">Confidence:</label> <input id="confidence" name="level" type="range" onchange="confidenceDisplay.value=(value +'%')" min="0" max="100" step="5" value="0"> <output id="confidenceDisplay">0%</output></p> <p><label for="notes">Notes:</label> <textarea id="notes" name="notes" maxLength="140"></textarea></p> </fieldset>
在控件的最后一部分,我们为用户创建了一个滑块来表达他或她完成比赛的信心。为此,我们使用类型为range
的输入。因为我们的置信度是用百分比来衡量的,所以我们在输入上设置了一个minimum
、maximum
和step
值。这些强制约束在正常的百分比范围内。此外,我们将值的移动限制为 5%的步长增量,如果您的浏览器支持范围滑块界面控件,您将能够观察到这一点。尽管不可能通过简单的控件交互来触发它们,但对于rangeUnderflow
、rangeOverflow
和stepMismatch
,该控件上可能存在验证约束。
因为默认情况下,范围控件不显示其值的文本表示,所以我们将为我们的应用添加一个
。将通过范围控件的onchange
处理程序来操纵confidenceDisplay
,但是我们将在一分钟后看到它的运行。
最后,我们添加一个<textarea>
来包含注册人的任何额外注释。通过在 notes 控件上设置一个maxLength
约束,我们允许它实现一个tooLong
约束,如果一个很长的值被粘贴到字段中的话。
<p><input type="submit" name="register" value="Register"></p> </form>
我们用一个提交按钮来结束我们的控件部分,这个按钮将发送我们的表单注册。在这个默认示例中,注册实际上没有被发送到任何服务器。
我们仍然需要描述一些脚本:我们将如何覆盖浏览器内置的表单验证反馈,以及我们将如何监听事件。尽管您可能会发现浏览器对表单错误的默认处理是可以接受的,但了解您的选择总是有好处的。
`
function invalidHandler(evt) {
// find the label for this form control
var label = evt.srcElement.parentElement.getElementsByTagName(“label”)[0];
// set the label’s text color to red
label.style.color = ‘red’;
// stop the event from propagating higher
evt.stopPropagation();
// stop the browser’s default handling of the validation error
evt.preventDefault();
}
function loadDemo() {
// register an event handler on the form to
// handle all invalid control notifications
document.register.addEventListener(“invalid”, invalidHandler, true);
}
window.addEventListener(“load”, loadDemo, false);
`
这个脚本展示了我们如何覆盖验证错误的处理。我们从注册特殊事件类型invalid
的事件监听器开始。为了捕获所有表单控件上的invalid
事件,我们在表单上注册了处理程序,确保注册了事件捕获,以便事件到达我们的处理程序。
// register an event handler on the form to
// handle all invalid control notifications document.register.addEventListener("invalid", invalidHandler, true);
现在,只要我们的任何表单元素触发了验证约束,我们的invalidHandler
就会被调用。为了提供比一些主流浏览器默认情况下更微妙的反馈,我们将违规表单字段的标签涂成红色。为此,首先我们通过遍历父节点来定位<label>
。
`// find the label for this form control
var label = evt.srcElement.parentElement.getElementsByTagName(“label”)[0];
// set the label’s text color to red
label.style.color = ‘red’;`
将标签设置为可爱的红色后,我们希望阻止浏览器或任何其他处理程序重复处理我们的无效事件。利用 DOM 的强大功能,我们调用preventDefault()
来停止任何浏览器对事件的默认处理,调用stopPropagation()
来阻止其他处理程序访问。
`// stop the event from propagating higher
evt.stopPropagation();
// stop the browser’s default handling of the validation error
evt.preventDefault();`
通过几个简单的步骤,我们提供了一个经过验证的表单,带有我们自己的特殊界面验证代码!
实用的临时演员
有时有些技术不适合我们的常规例子,但仍然适用于许多类型的 HTML5 应用。我们在这里向你展示一些简短但常见的实用附加功能。
密码是:验证!
为自定义验证器使用 HTML5 表单验证支持的一种简便方法是实现在密码更改期间验证密码的常用技术。标准技术是提供两个密码字段,这两个字段必须匹配才能成功提交表单。这里,我们提供了一种方法来利用setCustomValidation
调用来确保在表单提交之前两个密码字段匹配。
回想一下,customError
验证约束让您有机会在标准约束规则不适用时在表单控件上设置错误。具体来说,触发customError
约束的一个很好的理由是当验证依赖于多个控件的并发状态时,比如这里的两个密码字段。
因为一旦获得对对象的引用,就假定对象是活动的,所以每当密码字段不匹配时,在对象上设置自定义错误,每当字段再次匹配时,立即清除错误是一个好主意。实现这一点的一个好方法是对密码字段使用 onchange 事件处理程序。
<form name="passwordChange"> <p><label for="password1">New Password:</label> <input type="password" id="password1" onchange="checkPasswords()"></p>
`
正如您在这里看到的,在一个有两个密码字段的简单表单上,我们可以注册一个函数,在每次其中一个密码的值发生变化时执行。
`function checkPasswords() {
var pass1 = document.getElementById(“password1”);
var pass2 = document.getElementById(“password2”);
if (pass1.value != pass2.value)
pass1.setCustomValidity(“Your passwords do not match. Please recheck that your
new password is entered identically in the two fields.”);
else
pass1.setCustomValidity(“”);
}`
这里有一种处理密码匹配的方法。只需获取两个密码字段的值,如果它们不匹配,就设置一个自定义错误。出于验证例程的考虑,只在两个密码字段中的一个上设置错误可能是可以接受的。如果它们匹配,将空字符串设置为自定义错误以清除它;这是删除自定义错误的指定方式。
一旦您在字段上设置了错误,您就可以使用本章前面描述的方法向用户显示反馈,并让她按照预期更改密码。
表单是有样式的
为了帮助开发人员区分具有特定验证特征的表单控件,CSS 的开发人员添加了一组伪类,用于根据表单控件的有效性状态来设置表单控件的样式。换句话说,如果您希望页面上的表单元素根据它们当前是否符合验证来自动更改样式,您可以在规则中设置这些样式伪类。这些函数与链接上的:visited
和:hover
等长期存在的伪类非常相似。表 8-4 显示了为 CSS 选择器 4 级规范提出的新的伪类可以用来选择表单元素。
有了这些伪类,很容易在页面中用可视样式标记表单控件,这些样式会随着表单元素本身的调整而改变。例如,要用红色背景显示所有无效的表单元素,只需使用 CSS 规则:
:invalid { background-color:red; }
这些伪类将在用户输入时自动调整。不需要代码!
总结
在这一章中,你已经看到了如何利用 HTML5 中的新元素、属性和 API 将旧的 HTML 表单变成新的东西。我们已经看到了高级输入类型的新控件,甚至更多。我们已经看到了如何将客户端验证直接集成到表单控件中,以防止不必要的服务器往返处理坏数据。总的来说,我们已经看到了减少创建全功能应用用户界面所需的脚本数量的方法。
在下一章中,我们将研究浏览器如何给你能力产生独立的执行环境来处理长时间运行的任务:HTML5 Web 工作器。
九、使用 HTML5 拖放
自最初的苹果麦金塔电脑问世以来,传统的拖放操作一直很受用户欢迎。但是今天的计算机和移动设备有更复杂的拖放行为。拖放用于文件管理、传输数据、绘制图表和许多其他操作,在这些操作中,用手势移动对象比用键盘命令更自然。在街上问开发人员拖放包含什么,你可能会得到无数不同的答案,取决于他们最喜欢的程序和当前的工作任务。问非技术用户关于拖拽的问题,他们可能会茫然地盯着你;这个特性现在已经在计算机中根深蒂固,以至于不再经常被点名。
然而,HTML 在其存在的许多年中并没有将拖放作为核心特性。虽然一些开发人员已经使用内置功能来处理低级鼠标事件,作为破解原始拖放功能的一种方式,但与桌面应用中已经存在了几十年的拖放功能相比,这些努力就相形见绌了。随着一组精心设计的拖放功能的出现,HTML 应用向与桌面应用的功能相匹配又迈进了一步。
网络拖放:故事到此为止
你可能已经在网上看到过拖放的例子,并且想知道这些是不是 HTML5 拖放的用法。答案?可能不会。
原因是 HTML 和 DOM 从 DOM 事件的早期就已经公开了低级的鼠标事件,这对于有创造力的开发人员来说已经足够制作一个基本的拖放功能了。当与 CSS 定位结合使用时,通过创建复杂的 JavaScript 库和对 DOM 事件的深入了解,可以近似一个拖放系统。
例如,通过处理以下 DOM 事件,如果您编写了一组逻辑步骤(和一些注意事项),就有可能在网页中移动项目:
mousedown
:用户正在开始一些鼠标操作。(是拖还是只是点?)mousemove
:如果鼠标还没有抬起,移动操作开始。(是拖还是选?)mouseover
:鼠标已经移动到一个元素上。(是我想顺道拜访的人之一吗?)- 鼠标留下了一个元素,它将不再是一个可以放置的地方。(我需要画反馈吗?)
mouseup
:鼠标已经释放,可能触发了拖放操作。(根据起点,是否应在此位置完成卸货?)
虽然使用低级事件来建模一个粗糙的拖放系统是可能的,但是它有一些明显的缺点。首先,处理鼠标事件所需的逻辑比您想象的要复杂,因为每个列出的事件都有许多必须考虑的边缘情况。尽管有些人在之前的名单中,但现实是他们中有足够多的人值得拥有自己的一章。在这些事件中,CSS 必须小心地更新,以向用户提供关于在任何特定位置拖放的可能性的反馈。
然而,一个更严重的缺点是,这种特定的拖放实现依赖于对系统的完全控制。如果你试图将你的应用内容和其他内容混合在同一个页面中,当不同的开发者开始利用事件来达到他们自己的目的时,事情很快就会失控。类似地,如果您试图从别人的代码中拖放内容,您可能会遇到麻烦,除非这两个代码库事先仔细协调。此外,即席拖放不与用户的桌面交互,也不跨窗口工作。
新的 HTML5 拖放 API 旨在解决这些限制,借鉴了其他用户界面框架中提供的拖放方式。
注意即使实现得当,也要注意拖放在任何应用中的局限性。如果拖动行为被覆盖,使用拖动手势导航的移动设备可能无法正常工作。此外,拖放会干扰拖动选择。小心谨慎、适当地使用它。
html 5 拖放概述
如果您在 Java 或 Microsoft MFC 等编程技术中使用过拖放 API,那么您很幸运。新的 HTML5 拖放 API 严格按照这些环境的概念建模。开始很容易,但是掌握新的功能意味着您需要熟悉一组新的 DOM 事件,尽管这次是在更高的抽象层次上。
大局
学习新 API 最简单的方法是将它映射到您已经熟悉的概念上。如果你正在阅读一本关于 pro HTML5 编程的书,我们将大胆假设你在日常计算中对使用拖放很有经验。尽管如此,我们可以从一些主要概念的标准术语开始。
如图图 9-1 所示,当你(作为用户)开始一个拖放操作时,你是通过点击拖动指针开始的。开始拖动的项目或区域被称为拖动源。当您释放指针并完成操作时,您最终瞄准的区域或项目被称为拖放目标。当鼠标在页面上移动时,您可能会在实际释放鼠标之前遍历一系列拖放目标。
***图 9-1。*拖动源和放下目标
目前为止,一切顺利。但是简单地按住鼠标并把它移动到应用的另一部分并不构成拖放。相反,是操作过程中的反馈促成了成功的交互。考虑你自己在过去的经历中对拖放的使用;最直观的是系统不断更新,让你知道如果你在这个时间点释放会发生什么:
- 光标是否表明当前位置是有效的放置目标,或者它是否用“禁止”光标指示器暗示拒绝?
- 光标是否向用户暗示操作将是移动、链接或复制,例如光标上的“加号”指示符?
- 如果你现在释放鼠标,你所悬停的区域或目标是否会以任何方式改变它的外观,以表明它当前被选择为拖放?
为了在 HTML 拖放操作过程中向用户提供类似的反馈,浏览器将在单次拖动过程中发出一系列事件。这证明是非常方便的,因为在这些事件中,我们将完全有权力改变页面元素的 DOM 和样式,以给出用户期望的反馈类型。
除了拖放源和拖放目标之外,新 API 中还有一个关键概念需要学习:数据传输。该规范将数据传输描述为一组对象,用于公开拖放操作背后的拖动数据存储。然而,把数据传输看作是拖放的中央控制可能更容易。操作类型(例如,移动、复制或链接)、拖动过程中用作反馈的图像以及数据本身的检索都在这里进行管理。
关于数据本身,完成拖放的数据传输机制直接解决了前面描述的旧的专门拖放技术的一个限制。数据传输机制的工作方式类似于网络协议协商,而不是强制所有的拖放源和拖放目标知道彼此。在这种情况下,协商是通过多用途互联网邮件交换(MIME)类型执行的。
注意 MIME 类型与用于在电子邮件中附加文件的类型相同。它们是在所有类型的网络流量中普遍使用的互联网标准,在 HTML5 中非常常见。简而言之,MIME 类型是标准化的文本字符串,用于对未知内容的类型进行分类,例如“text/plain”表示纯文本,“image/png”表示 png 图像。
使用 MIME 类型的目的是允许源和目标协商哪种格式最适合放置目标的需要。如图 9-2 所示,在拖动启动期间,dataTransfer 对象加载了代表所有合理类型或“风格”的数据,通过这些数据可以进行传输。然后,当 drop 完成时,drop 处理程序代码可以扫描可用的数据类型,并决定哪种 MIME 类型格式最适合它的需要。
例如,假设网页中的列表项代表一个人。有许多不同的方式来表示一个人的数据;有些是标准的,有些不是。当拖动开始于一个特定的人的列表项时,拖动开始处理程序可以声明这个人的数据有几种格式,如表 9-1 所示。
当放下完成时,放下处理程序可以查询可用数据类型的列表。从提供的列表中,处理程序可以选择最合适的类型。文本列表放置目标可以选择获取文本/普通“味道”的数据来检索人员的姓名,而更高级的控件可以选择检索并显示人员的 PNG 图像作为放置的结果。而且,如果源和目标在非标准类型上协调一致,目标也可以检索下落时人的年龄。
图 9-2 。数据“风味”的拖放谈判
正是这个协商过程允许拖放源和拖放目标分离。只要拖动源以多种 MIME 类型提供数据,拖放目标就可以选择最适合其操作的格式,即使这两种格式来自不同的开发人员。在本章的后面部分,我们将探索如何使用更不寻常的 MIME 类型,比如文件。
要记住的事件
既然我们已经探索了拖放 API 的关键概念,那么让我们把重点放在可以在整个过程中使用的事件上。正如您将看到的,这些事件在比以前用来模拟拖放系统的鼠标事件更高的层次上操作。但是,拖放事件扩展了 DOM 鼠标事件。因此,如果需要的话,您仍然可以访问低级别的鼠标信息,比如坐标
传播与预防
但是在我们关注拖放本身之前,让我们回顾一下自从浏览器对 DOM Level 3 事件进行标准化以来就存在的两个 DOM 事件函数:stopPropagation 和 preventDefault 函数。
考虑页面中的一个元素嵌套在另一个元素中的情况。我们将它们分别称为子元素和父元素。子元素占据了父元素的部分可视空间,但不是全部。尽管在我们的例子中我们只提到了两个元素,但实际上一个网页通常有许多嵌套层次。
当用户在孩子身上点击鼠标时,哪个元素应该实际接收事件:孩子,父母,还是两者?如果两者都有,按什么顺序?这个问题的答案是由万维网联盟(W3C)在 DOM events 规范中确定的。在称为“事件捕获”的过程中,事件从父节点开始,通过中介,向下传递到最具体的子节点一旦孩子访问了事件,事件通过一个称为“事件冒泡”的过程流回元素层次结构这两个流一起允许开发人员以最适合其页面架构的方式捕捉和处理事件。只有实际注册了处理程序的元素才会处理事件,这使系统保持轻量级。总体方法是来自多个浏览器厂商的不同行为的折衷,并且与其他本地开发框架一致,其中一些是捕获的,一些是冒泡的。
但是,任何时候处理程序都可以对事件调用 stopPropagation 函数,这将阻止它进一步向下遍历事件捕获链或向上遍历冒泡阶段。
注意微软在
[
ie.microsoft.com/testdrive/HTML5/ComparingEventModels](http://ie.microsoft.com/testdrive/HTML5/ComparingEventModels)
提供了一个很棒的事件模型互动演示
浏览器对于如何处理一些事件也有默认的实现。例如,当用户单击页面链接时,默认行为是将浏览器导航到该链接指定的目的地。开发人员可以通过拦截处理程序中的事件并对其调用 preventDefault 来防止这种情况。这允许代码重写某些内置事件的默认行为。这也是开发人员在事件处理程序中取消拖放操作的方式。
在我们的拖放 API 示例中,stopPropagation
和preventDefault
都很方便。
拖放事件流
当用户在 HTML5 浏览器中启动拖放操作时,一系列事件在开始时触发,并在整个操作过程中持续。我们将在这里依次检查它们。
拖启动
当用户开始在页面中的元素上拖动时,dragstart 事件在该元素上触发。换句话说,一旦鼠标按下,用户移动鼠标,就会启动dragstart
。dragstart 事件非常重要,因为它是唯一一个可以使用setData
调用在dataTransfer
上设置数据的事件。这意味着在一个dragStart
处理程序中,需要设置可能的数据类型,以便在拖放结束时可以查询它们,如前所述。
拦截!
Brian 说:“如果你想知道为什么数据类型只能在dragStart
事件期间设置,实际上有一个很好的理由。
因为拖放被设计成跨窗口和跨各种来源的内容工作,如果drag
事件监听器能够在拖动经过它们时插入或替换数据,这将是一个安全风险。想象一下,插入了事件监听器的恶意代码段查询并替换了经过的任何拖动的拖动数据。这将歪曲拖动源的意图,因此禁止在开始后进行任何数据替换。"
拖动
拖动事件可以被认为是拖动操作的连续事件。当用户在页面上移动鼠标光标时,在拖动源上重复调用drag
事件。在操作过程中,拖动事件将每秒触发几次。虽然拖动反馈的视觉效果可以在drag
事件中修改,但是dataTransfer
上的数据是禁止的。
拖曳器
当拖动进入页面上的新元素时,会在该元素上触发一个dragenter
事件。此事件是根据元素是否可以接收放下来设置放下反馈的好时机。
休假
相反,每当用户将拖动移出先前调用dragenter
的元素时,浏览器将触发一个dragleave
事件。此时可以恢复拖放反馈,因为鼠标不再位于该目标上方。
疏浚
在拖动操作过程中,当鼠标移动到一个元素上时,会频繁地调用dragover
事件。与在拖动源上调用的对应拖动事件不同,此事件在鼠标的当前目标上调用。
下降
当用户释放鼠标时,在当前鼠标目标上调用drop
事件。基于dataTransfer
对象的结果,这是处理拖放的代码应该执行的地方。
承载
链中的最后一个事件dragend
在拖动源上触发,表示拖动完成。它特别适合清理拖动过程中使用的state
,因为不管拖放是否完成都会调用它。
总之,有很多方法可以拦截拖放操作并采取行动。拖拽事件链总结在图 9-3 中。
***图 9-3。*拖放事件流
拖参
既然您已经看到了在拖放操作中可以触发的不同事件,您可能想知道如何将 web 应用中的元素标记为可拖动的。那很简单!
除了少数元素(如文本控件)之外,页面中的元素在默认情况下是不可拖动的。然而,为了将特定元素标记为可拖动的,您需要做的就是添加一个属性:draggable。
<div id=”myDragSource” **draggable=”true”**>
只需添加该属性,就可以让浏览器触发上述事件。然后,您只需要添加事件处理程序来管理它们。
转移和控制
在进入我们的例子之前,让我们更详细地评估一下dataTransfer
对象。每个拖放事件都可以进行数据传输,如清单 9-1 所示。
***清单 9-1。*正在检索数据传输对象
Function handleDrag(evt) { var transfer = evt.dataTransfer; // … }
如清单 9-1 中所述,dataTransfer
用于在源和目标之间的协商过程中获取和设置实际的丢弃数据。这是使用下列函数和属性完成的:
setData(format, data)
:在 dragStart 过程中调用该函数,可以注册一个 MIME 类型格式的传输项目。getData(format)
:该功能允许检索给定类型的注册数据项。types
:该属性返回所有当前注册格式的数组。items
:这个属性返回一个所有项目及其相关格式的列表。files
:该属性返回任何与放置相关的文件。这将在后面的章节中详细讨论。clearData()
:不带参数调用此函数,清除所有注册数据。用格式参数调用它只会移除该特定的注册。
拖动操作期间,还有两个函数可用于改变反馈:
setDragImage(element, x, y)
:告诉浏览器使用现有的图像元素作为拖动图像,该图像将显示在光标旁边,向用户提示拖动操作的效果。如果提供了 x 和 y 坐标,那么这些坐标将被视为鼠标的放置点。- 通过用提供的页面元素调用这个函数,你告诉浏览器把这个元素绘制成一个拖动反馈图像。
最后一组属性允许开发者设置和/或查询所允许的拖动操作的类型:
effectAllowed
:将该属性设置为 none、copy、copyLink、copyMove、Link、linkMove、Move 或 all 中的一个,告诉浏览器只允许用户执行此处列出的操作类型。例如,如果设置了复制,则只允许复制操作,而移动或链接操作将被阻止。dropEffect
:该属性可用于确定当前正在进行哪种类型的操作,或者设置为强制特定的操作类型。操作类型包括复制、链接和移动。或者,可以设置值 none 来防止在该时间点发生任何丢弃。
总之,这些操作提供了对拖放的精细控制。现在,让我们看看他们的行动。
使用拖放功能构建应用
使用我们已经学过的概念,我们将在 Happy Trails Running Club 的主题中构建一个简单的拖放页面。该页面允许俱乐部比赛组织者将俱乐部成员拖入两个列表之一:赛车手和志愿者。为了将他们划分到不同的组别,参赛者将按照他们的年龄进行分类。另一方面,志愿者只按他们的名字排序,因为当他们不参加比赛时,他们的年龄并不重要。
列表的排序是自动完成的。应用本身将显示反馈,指示成员在两个列表中的适当放置区域,如图 9-4 所示。
***图 9-4。*显示分类成列表的参赛者的示例页面
本示例的所有代码都包含在 code/draganddrop 目录中的本书示例中。我们将浏览页面并解释它在实践中是如何工作的。
首先,让我们看看页面的标记。在顶部,我们已经声明了我们俱乐部成员的数据(参见清单 9-2 )。
***清单 9-2。*显示可拖动成员姓名和年龄的标记
`
Drag members to either the Racers or Volunteers list.
- Brian Albers
- Frank Salim
- Jennifer Clark
- John Kemble
- Lorraine Gaunce
- Mark Wang
- Morgan Stephen
- Peter Lubbers
- ` `
- Vanessa Combs
- Vivian Lopez
如您所见,每个成员列表元素都被标记为draggable
。这告诉浏览器让拖动在它们中的每一个上开始。接下来您会注意到,给定成员的年龄被编码为一个数据属性。data
-符号是在 HTML 元素上存储非标准属性的标准方式。
我们的下一部分包含目标列表(见清单 9-3 )。
***清单 9-3。*下拉列表目标的标记
`
Racers (by Age):被标识为racers
和volunteers
的无序列表是我们的成员将被插入的最终目的地。围绕它们的fieldsets
在功能上相当于城堡周围的护城河。当用户拖入fieldset
时,我们将知道他们已经退出了包含的列表,我们将相应地更新我们的视觉反馈。
说到反馈,我们的页面中有一些 CSS 样式值得注意(见清单 9-4 )。
***清单 9-4。*拖放演示的样式
`#members li {
cursor: move;
}
.highlighted {
background-color: yellow;
}
.validtarget {
background-color: lightblue;
}`
首先,我们确保源列表中的每个成员都显示一个移动光标。这给用户一个提示,即项目是可拖动的。
接下来,我们定义两个样式类:highlighted
和validtarget
。这些用于在拖放过程中绘制列表的背景颜色。在整个拖动过程中,validtarget
背景将显示在我们的目的地列表中,以提示它们是有效的拖放目标。当用户实际上在目标列表上移动一个成员时,它将改变为highlighted
样式,表明用户实际上在一个放置目标上。
为了跟踪页面上的状态,我们将声明几个变量(见清单 9-5 )。
***清单 9-5。*清单项目申报
` // these arrays hold the names of the members who are
// chosen to be racers and volunteers, respectively
var racers = [];
var volunteers = [];
// these variables store references to the visible
// elements for displaying who is a racer or volunteer
var racersList;
var volunteersList;`
前两个变量将作为内部数组,用来记录哪些成员在参赛者和志愿者列表中。后两个变量只是用来方便地引用无序列表,这些列表包含了各个列表中成员的可视化显示。
现在,让我们设置所有的页面项目来处理拖放(见清单 9-6 )。
***清单 9-6。*事件处理程序注册
` function loadDemo() {
racersList = document.getElementById(“racers”);
volunteersList = document.getElementById(“volunteers”);
// our target lists get handlers for drag enter, leave, and drop
var lists = [racersList, volunteersList];
[].forEach.call(lists, function(list) {
list.addEventListener(“dragenter”, handleDragEnter, false);
list.addEventListener(“dragleave”, handleDragLeave, false);
list.addEventListener(“drop”, handleDrop, false);
});
// each target list gets a particular dragover handler
racersList.addEventListener(“dragover”, handleDragOverRacers, false);
volunteersList.addEventListener(“dragover”, handleDragOverVolunteers, false);
// the fieldsets around our lists serve as buffers for resetting
// the style during drag over
var fieldsets = document.querySelectorAll(“#racersField, #volunteersField”);
[].forEach.call(fieldsets, function(fieldset) {
fieldset.addEventListener(“dragover”, handleDragOverOuter, false);
});
// each draggable member gets a handler for drag start and end
var members = document.querySelectorAll(“#members li”);
[].forEach.call(members, function(member) {
member.addEventListener(“dragstart”, handleDragStart, false);
member.addEventListener(“dragend”, handleDragEnd, false);
});
}
window.addEventListener(“load”, loadDemo, false);`
当窗口最初加载时,我们调用一个loadDemo
函数来设置所有的拖放事件处理程序。它们中的大多数不需要事件捕获,我们将相应地设置捕获参数。racersList
和volunteersList
都将收到dragenter, dragleave
和drop
事件的处理程序,因为这些事件是针对投放目标的。每个列表都将接收一个单独的拖拽事件监听器,因为这将允许我们基于用户当前拖拽的目标轻松地更新拖拽反馈。
如前所述,我们还在目标列表周围的字段集上添加了dragover
处理程序。我们为什么要这样做?当一个拖拽退出我们的目标列表时,让detect
变得更容易。虽然我们很容易检测到用户在我们的列表上拖动了一个项目,但是确定用户何时将项目从我们的列表中拖出就不那么容易了。这是因为当一个项目被拖出我们的列表和时,dragleave 事件都会被触发。实质上,当您从父元素拖动到它包含的一个子元素上时,拖动将退出父元素并进入子元素。虽然这提供了很多信息,但实际上很难知道拖动何时离开父元素的外部边界。因此,我们将使用一个通知,通知我们正在拖动列表周围的元素*,通知我们已经退出列表。稍后将提供更多相关信息。*
往出口走
Brian 说:“拖放规范的一个更反直觉的方面是事件的顺序。虽然您可能认为被拖动的项目会在进入另一个目标之前退出一个目标,但是您错了!
在从元素 A 拖动到元素 B 的过程中,事件的触发顺序是在元素 A 上触发dragleave
事件之前在元素 B 上触发dragenter
事件。这保持了与 HTML 鼠标事件规范的一致性,但这是设计中比较奇怪的方面之一。可以肯定的是,未来还会有更多这样的怪癖。"
我们的最后一组处理程序在我们的初始列表中注册每个draggable
俱乐部成员的dragstart
和dragend
监听器。我们将使用它们来初始化和清理任何拖动。您可能会注意到,我们没有为drag
事件添加处理程序,该事件会在拖动源上定期触发。因为我们不会更新被拖动项目的外观,所以在我们的例子中没有必要。
现在,我们将根据事件处理程序通常触发的顺序,依次检查实际的事件处理程序(见清单 9-7 )。
清单 9-7。 dragstart 事件处理程序
` // called at the beginning of any drag
function handleDragStart(evt) {
// our drag only allows copy operations
evt.effectAllowed = “copy”;
// the target of a drag start is one of our members
// the data for a member is either their name or age
evt.dataTransfer.setData(“text/plain”, evt.target.textContent);
evt.dataTransfer.setData(“text/html”, evt.target.dataset.age);
// highlight the potential drop targets
racersList.className = “validtarget”;
volunteersList.className = “validtarget”;
return true;
}`
在用户开始操作的draggable
项上调用dragstart
的处理程序。它是一个有点特殊的处理程序,因为它设置了整个流程的功能。首先,我们设置了effectAllowed
,它告诉浏览器从这个元素拖动时只允许复制——不允许移动或链接。
接下来,我们预加载所有可能的数据类型,这些数据可能会在成功放下后被请求。自然,我们希望支持元素的文本版本,所以我们设置 MIME 类型text/plain
来返回节点draggable
中的文本(即俱乐部成员的名字)。
对于我们的第二种数据风格,我们希望 drop 操作传输关于拖动源的另一种类型的数据;在我们的例子中,是俱乐部成员的年龄。不幸的是,由于缺陷,并不是所有的浏览器都支持用户定义的 MIME 类型,比如application/x-age
,这是最适合这种任意风格的。相反,我们将重用另一种普遍支持的 MIME 格式——text/html
——暂时代表一种时代风格。希望 WebKit 浏览器将很快解决这个限制。
不要忘记dragstart
处理器是唯一可以设置数据传输值的处理器。为了防止恶意代码在拖动过程中更改数据,在其他处理程序中尝试这样做将会失败。
我们在 start 处理程序中的最后一个动作纯粹是为了演示。我们将改变潜在拖放目标列表的背景颜色,给用户一个可能的提示。我们的下一个处理程序将在被拖动的项目进入和离开页面元素时处理事件(见清单 9-8 )。
清单 9-8。 dragenter 和 dragleave 事件处理程序
` // stop propagation and prevent default drag behavior
// to show that our target lists are valid drop targets
function handleDragEnter(evt) {
evt.stopPropagation();
evt.preventDefault();
return false;
}
function handleDragLeave(evt) {
return false;
}`
我们的演示不使用dragleave
事件,我们处理它纯粹是为了说明的目的。
然而,dragenter
事件可以通过在有效的放置目标上触发时调用preventDefault
来处理和取消。这通知浏览器当前目标是有效的放下目标,因为默认行为是假设任何目标都不是有效的放下目标。
接下来,我们将看看拖拽处理程序(见清单 9-9 )。回想一下,每当拖动鼠标悬停在相关元素上时,这些按钮就会定时触发。
***清单 9-9。*外集装箱拖拽搬运机
` // for better drop feedback, we use an event for dragging
// over the surrounding control as a flag to turn off
// drop highlighting
function handleDragOverOuter(evt) {
// due to Mozilla firing drag over events to
// parents from nested children, we check the id
// before handling
if (evt.target.id == “racersField”)
racersList.className = “validtarget”;
else if (evt.target.id == “volunteersField”)
volunteersList.className = “validtarget”;
evt.stopPropagation();
return false;
}`
我们的三个dragover
处理程序中的第一个将仅用于调整拖动反馈。回想一下,当一个拖拽体离开了一个目标时,很难检测到,比如我们想要的参赛者和志愿者名单。因此,我们在列表周围的字段集上使用拖动移动来表示拖动已经离开了列表附近。这允许我们相应地关闭列表上的拖放高亮显示。
请注意,如果用户停留在字段集区域,我们列出的简单代码将重复更改 CSS className
。出于优化的目的,最好只改变一次className
,因为这可能会导致浏览器做更多不必要的工作。
最后,我们停止将事件传播到页面中的任何其他处理程序。我们不希望任何其他处理程序覆盖我们的逻辑。在接下来的两个dragover
处理程序中,我们采用不同的方法(见清单 9-10 )。
***清单 9-10。*目标列表拖拽处理程序
` // if the user drags over our list, show
// that it allows copy and highlight for better feedback
function handleDragOverRacers(evt) {
evt.dataTransfer.dropEffect = “copy”;
evt.stopPropagation();
evt.preventDefault();
racersList.className = “highlighted”;
return false;
}
function handleDragOverVolunteers(evt) {
evt.dataTransfer.dropEffect = “copy”;
evt.stopPropagation();
evt.preventDefault();
volunteersList.className = “highlighted”;
return false;
}`
这两个处理程序虽然有些冗长,但还是完整地列出来,以阐明我们的演示。第一个处理参赛者列表中的拖拽事件,第二个处理志愿者列表中的dragover
事件。
我们采取的第一个动作是设置dropEffect
来表明在这个节点上只允许复制,不允许移动或链接。这是一个很好的实践,尽管我们最初的dragstart
处理程序已经将拖放操作限制为只复制。
接下来,我们阻止其他处理程序访问该事件并取消它。取消拖拽事件有一个重要的功能:它告诉浏览器默认操作——不是允许在这里拖拽——是无效的。本质上,我们是在告诉浏览器,它不应该不允许拖放;因此,下降是允许的。虽然这看起来似乎违背直觉,但回想一下,preventDefault
是用来告诉浏览器不要为一个事件做正常的内置操作。例如,在点击一个链接时调用preventDefault
会告诉浏览器不要导航到该链接的引用。规范设计者本可以为这个dragover
创建一个新的事件或 API,但是他们选择保留已经在整个 HTML 中使用的 API 模式。
每当用户拖动我们的列表时,我们还会通过highlighted
CSS 类将背景颜色改为黄色来给用户视觉反馈。拖放的主要工作是在 drop 处理程序中完成的,我们接下来将在清单 9-11 中研究它。
***清单 9-11。*目标列表的删除处理器
` // when the user drops on a target list, transfer the data
function handleDrop(evt) {
evt.preventDefault();
evt.stopPropagation();
var dropTarget = evt.target;
// use the text flavor to get the name of the dragged item
var text = evt.dataTransfer.getData(“text/plain”);
var group = volunteers;
var list = volunteersList;
// if the drop target list was the racer list, grab an extra
// flavor of data representing the member age and prepend it
if ((dropTarget.id != “volunteers”) &&
(dropTarget.parentNode.id != “volunteers”)) {
text = evt.dataTransfer.getData(“text/html”) + ": " + text;
group = racers;
list = racersList;
}
// for simplicity, fully clear the old list and reset it
if (group.indexOf(text) == -1) {
group.push(text);
group.sort();
// remove all old children
while (list.hasChildNodes()) {
list.removeChild(list.lastChild);
}
// push in all new children
[].forEach.call(group, function(person) {
var newChild = document.createElement(“li”);
newChild.textContent = person;
list.appendChild(newChild);
});
}
return false;
}`
同样,我们从防止默认的放下行为和防止控件传播到其他处理程序开始。默认的放置事件取决于放置元素的位置和类型。例如,拖放从另一个源拖入的图像会在浏览器窗口中显示该图像,默认情况下,将链接拖放到窗口中会导航到该图像。我们希望在演示中完全控制拖放行为,所以我们取消了任何默认行为。
回想一下,我们的演示展示了如何从拖放的元素中检索在dragstart
中设置的多种数据类型。在这里,我们可以看到检索是如何完成的。默认情况下,我们使用 text/plain MIME 格式获取代表俱乐部成员姓名的纯文本数据。如果用户进入志愿者列表,这就足够了。
然而,如果用户将俱乐部成员放入赛车列表,我们需要额外的步骤来获取俱乐部成员的年龄,这是我们之前在dragstart
期间使用文本/html 风格设置的。我们将它添加到俱乐部成员的名字前面,以在参赛者列表中显示年龄和姓名。
我们的最后一个代码块是一个简单但未经优化的例程,用于清除目标列表中所有以前的成员,添加新成员(如果他还不存在),排序,并重新填充列表。最终结果是一个排序列表,包含旧成员和新删除的成员(如果他以前不存在)。
不管用户是否完成了拖放,我们都需要一个 dragend 处理程序来清理(见清单 9-12 )。
***清单 9-12。*用于清理的拖拉装卸机
` // make sure to clean up any drag operation
function handleDragEnd(evt) {
// restore the potential drop target styles
racersList.className = null;
volunteersList.className = null;
return false;
}`
在拖动结束时会调用一个dragend
处理程序,不管拖放是否真的发生。如果用户取消了拖动或完成了拖动,仍会调用dragend
处理程序。这给了我们一个很好的地方来清理我们在过程开始时改变的任何状态。毫不奇怪,我们将列表的 CSS 类重置为默认的非样式状态。
分享就是关爱
Brian 说:“如果你想知道拖放功能是否值得所有的事件处理程序代码,不要忘记 API 的一个关键好处:跨窗口甚至跨浏览器共享拖拽。
因为 HTML5 拖放的设计是为了反映桌面功能而构建的,所以它也支持跨应用共享就不足为奇了。您可以通过在多个浏览器窗口中加载我们的示例,并将成员从一个源列表拖到另一个窗口的参赛者和志愿者列表中来进行尝试。虽然我们简单的突出显示反馈不是为这种情况设计的,但是实际的拖放功能可以跨窗口工作,甚至跨浏览器工作,如果它们支持 API 的话。“我们的拖放示例很简单,但它展示了 API 的全部功能。
进入空投区
如果你认为处理所有的拖放事件很复杂,你并不孤单。该规范的作者设计了一种替代的、简化的机制来支持放下事件:dropzone 属性。
dropzone 为开发人员提供了一种简洁的方式来注册元素是否愿意接受拖放,而无需编写冗长的事件处理程序。该属性由几个空格分隔的模式组成,当提供这些模式时,允许浏览器自动为您连接拖放行为(见表 9-2 )。
借用我们的示例应用,racers 列表元素可以被指定为具有以下属性:
<ul id="racers" dropzone=”copy s:text/plain s:text/html” ondrop=”handleDrop(event)”>
这提供了一种快速的方式来告诉浏览器,支持纯文本或 HTML 数据格式的元素的复制操作可以从我们的列表中删除。
在撰写本文时,dropzone
还不被大多数主流浏览器厂商支持,但对它的支持很可能即将到来。
处理文件的拖放
如果您曾经想要一种更简单的方式将文件添加到您的 web 应用中,或者您想知道一些最新的网站如何允许您将文件直接拖动到页面中并上传它们,答案就是 HTML5 文件 API。尽管整个 W3C 文件 API 的大小和状态超出了本次讨论的范围,但是许多浏览器已经支持该标准的一个子集,它允许将文件拖入应用中。
注意W3C 文件 API 在
[www.w3.org/TR/FileAPI](http://www.w3.org/TR/FileAPI)
在线文档化。
File API 包含异步读取网页中的文件、在跟踪进程的同时将文件上传到服务器以及将文件转换为页面元素的功能。然而,拖放等附属规范使用了文件 API 的一个子集,这也是我们在本章中关注的地方。
回想一下,我们已经在本章中两次提到了文件拖放。首先,dataTransfer
对象包含一个名为files
的属性,如果合适的话,该属性将包含一个附加到拖动的文件列表。例如,如果用户将一个或一组文件从桌面拖到应用的网页中,浏览器将在dataTransfer.files
对象有值的地方触发拖放事件。此外,支持前面提到的 dropzone 属性的浏览器通过使用f
: MIME 类型前缀,允许特定 MIME 类型的文件有效地拖放到元素上。
注意目前 Safari 浏览器只支持对文件的拖放操作。在页面内启动的拖动将触发大多数拖放事件,但只有当拖动类型为文件时,才会发生拖放事件。
通常,在大多数拖放事件中,您无法访问这些文件,因为出于安全原因,它们受到保护。尽管有些浏览器可能允许您在拖动事件期间访问文件列表,但没有浏览器允许您访问文件数据。此外,在拖动源元素处触发的dragstart, drag
和 dragend 事件不会在文件拖放中触发,因为源是文件系统本身。
文件列表中的文件项支持以下属性:
- 名称:带扩展名的完整文件名
- 类型:文件的 MIME 类型
- size :文件的大小,以字节为单位
- lastModifiedDate :最后一次修改文件内容的时间戳
让我们看一个简单的文件拖放的例子,我们将展示任何被拖放到页面上的文件的特征,如图 9-5 所示。该代码包含在本书附带的 fileDrag.html 示例中。
***图 9-5。*显示丢失文件特征的演示页面
我们演示的 HTML 实际上非常简单(见清单 9-13 )。
***清单 9-13。*文件拖放演示的标记
`
页面中只有两个元素。放置文件的放置目标和状态显示区域。
和上一个例子一样,我们将在页面加载期间注册拖放事件处理程序(参见清单 9-14 )。
***清单 9-14。*文件拖放演示的加载和初始化代码
` var droptarget;
// set the status text in our display
function setStatus(text) {
document.getElementById(“status”).innerHTML = text;
}
// …
function loadDemo() {
droptarget = document.getElementById(“droptarget”);
droptarget.className = “validtarget”;
droptarget.addEventListener(“dragenter”, handleDragEnter, false);
droptarget.addEventListener(“dragover”, handleDragOver, false);
droptarget.addEventListener(“dragleave”, handleDragLeave, false);
droptarget.addEventListener(“drop”, handleDrop, false);
setStatus(“Drag files into this area.”);
}
window.addEventListener(“load”, loadDemo, false);`
这一次,放置目标接收所有的事件处理程序。只需要处理程序的子集,我们可以忽略在拖动源发生的事件。
当用户拖动文件到我们的拖放目标时,我们将显示我们所知道的关于拖放候选对象的信息(见清单 9-15 )。
***清单 9-15。*文件拖放进入处理程序
` // handle drag events in the drop target
function handleDragEnter(evt) {
// if the browser supports accessing the file
// list during drag, we display the file count
var files = evt.dataTransfer.files;
if (files)
setStatus(“There are " + evt.dataTransfer.files.length +
" files in this drag.”);
else
setStatus(“There are unknown items in this drag.”);
droptarget.className = “highlighted”;
evt.stopPropagation();
evt.preventDefault();`
return false; }
虽然有些浏览器允许在拖动过程中访问dataTransfer
文件,但我们会处理禁止访问该信息的情况。当计数已知时,我们将在状态中显示它。
处理dragover
和dragleave
事件很简单(参见清单 9-16 )。
***清单 9-16。*文件删除拖拽和拖拽离开处理程序
` // preventing the default dragover behavior
// is necessary for successful drops
function handleDragOver(evt) {
evt.stopPropagation();
evt.preventDefault();
return false;
}
// reset the text and status when drags leave
function handleDragLeave(evt) {
setStatus(“Drag files into this area.”);
droptarget.className = “validtarget”;
return false;
}`
和往常一样,我们必须取消dragover
事件,以允许拖放由我们自己的代码处理,而不是浏览器的默认行为,通常是内联显示。对于一个dragleave
,我们只设置状态文本和样式来表示鼠标离开时拖放不再有效。我们的大部分工作是在 drop handler 中完成的(见清单 9-17 )。
***清单 9-17。*文件删除处理器
` // handle the drop of files
function handleDrop(evt) {
// cancel the event to prevent viewing the file
evt.preventDefault();
evt.stopPropagation();
var filelist = evt.dataTransfer.files;
var message = “There were " + filelist.length + " files dropped.”;
// show a detail list for each file in the drag
message += “
- ”;
[].forEach.call(filelist, function(file) {
message += “
- ”;
message += “” + file.name + " ";
message += “(” + file.type + ") : ";
message += "modified: " + file.lastModifiedDate;
message += “ - ”;
});message += “”;
setStatus(message);
droptarget.className = “validtarget”;return false;
}`如前所述,有必要使用
preventDefault
取消事件,这样浏览器的默认丢弃代码就不会被触发。然后,因为我们在拖放处理程序中比在拖动过程中能访问更多的数据,所以我们可以检查附加到
dataTransfer
的files
并发现被拖放文件的特征。在我们的例子中,我们将仅仅显示文件的属性,但是通过充分使用 HTML5 文件 API,您可以读入本地显示的内容,或者将它们上传到支持您的应用的服务器。实用的临时演员
有时有些技术不适合我们的常规例子,但仍然适用于许多类型的 HTML5 应用。在这里,我们向您呈现一个简短、普通、实用的附加内容。
定制拖动显示
通常,浏览器将默认拖动操作的可视光标指示器。图像或链接会随着光标移动(有时为了实际查看会缩小尺寸),或者被拖动元素的重影图像会悬停在拖动位置。
但是,如果您需要更改默认的拖动图像显示,API 为您提供了一个简单的 API 来实现这一点。只可能在 dragstart 处理程序中更改拖动图像——同样是出于安全考虑——但是您可以通过简单地将表示光标外观的元素传递给
dataTransfer
来轻松实现。var dragImage = document.getElementById("happyTrails"); evt.dataTransfer.setDragImage(dragImage, 5, 10);
注意传递给
setDragImage
调用的偏移坐标。这些 x 和 y 坐标告诉浏览器将图像中的哪个像素用作鼠标光标下的点。例如,通过分别为 x 和 y 传入值 5 和 10,图像将被定位成光标距离左边 5 个像素和顶部 10 个像素,如图 9-6 中的所示。***图 9-6。*演示页面,拖动图像设置为快乐小径标志
然而,拖动图像不必是图像。任何元素都可以设置为拖动图像;如果它不是一个图像,浏览器将创建一个可视的快照来作为光标显示。
总结
拖放 API 可能很难掌握。它涉及到许多事件的正确处理,如果你的拖放目标布局很复杂,其中的一些可能很难管理。但是,如果您正在寻找跨窗口或浏览器的拖动操作,甚至与桌面交互,您将需要学习 API 的微妙之处。从设计上来说,它结合了本机应用拖放功能,同时还能在必须保护数据免受第三方代码攻击的环境的安全限制内工作。
有关使用拖放文件作为应用数据的更多信息,请务必查看 W3C 文件 API。在下一章中,我们将研究 Web 工作器 API,它将允许您在主页之外生成后台脚本,以加快执行速度并改善用户体验。