超越 jQuery(三)

随笔3周前发布 琴声细语
30 0 0

原文:Beyond jQuery

协议:CC BY-NC-SA 4.0

九、AJAX 请求:动态数据和页面更新

AJAX,即异步 JavaScript 和 XML,是 web API 提供的一个特性,它允许在不重新加载整个页面的情况下从服务器更新或检索数据。浏览器最初没有这种功能。没有这个特性的时代标志着 web 的婴儿期,随之而来的是不太理想的用户体验,导致大量冗余字节在客户机和服务器之间循环。这种原始模式的低效率由于互联网带宽受到当今标准的极大限制而变得更加复杂。早在 1999 年,当微软首次将XMLHTTP作为 ActiveX 控件引入 Internet Explorer 5.0 时, 1 大约 95%的互联网用户受到 56 Kbps 或更慢的拨号连接的限制。 2

是在微软的 Internet Explorer 浏览器中实现的专有 JavaScript 对象,它代表了 web 开发技术和用户体验的巨大飞跃。它是第一个全功能的内置传输,用于集中客户端/服务器通信,允许在不替换整个页面的情况下进行更新。以前,即使页面上只有一小部分数据发生了变化,也必须重新加载整个页面。这种新传输的初始 API 与其现代的标准化表亲相匹配:XMLHttpRequest。本质上,这个对象允许开发人员构造一个new传输实例,向任何端点(在同一个域上)发送 GET、POST、PUT、PATCH 或 DELETE 请求,然后以编程方式检索服务器响应的状态和消息体。尽管老化的XMLHttpRequest最终将被 Fetch API3所取代,但它在没有对手的情况下茁壮成长,并且在大约 15 年的时间里基本没有变化。

掌握 AJAX 通信的概念

在处理 AJAX 通信时,理解几个关键概念至关重要:

  1. 异步操作。
  2. 超文本传输协议,也称为 HTTP。
  3. JSON、URL 编码和多部分格式编码。
  4. 同源政策。

除了对 web 套接字的介绍之外,前两项将在本节中直接讨论(web 套接字不像其他一些概念那样重要,但仍然有潜在的用处)。列表中的最后两个将在本章稍后讨论。

异步很难

根据我在 AJAX 通信方面的丰富经验,以及对其他开发人员在 web API 方面的观察,这个特性最吸引人的地方也是最令人困惑的地方。JavaScript 对异步操作的抽象不如其他更传统的语言,比如 Java。除了对发生在带外的任务(比如 AJAX 请求)缺乏直观的本地支持之外,目前有三种不同的常见方法来处理这些类型的异步操作。这些方法包括回调、承诺和异步函数。尽管对异步操作的本机支持已经随着时间的推移而改进,但大多数开发人员仍然必须显式地处理这些类型的任务,这可能是一个挑战,因为它通常需要相应地构造所有周围的代码。这通常会使软件开发人员处理异步调用的工作变得笨拙,并导致代码变得复杂。这当然增加了风险,并可能给底层应用带来更多的错误。

回调将在本章中演示,承诺也是如此。承诺和回调都在第十一章中有更详细的介绍,还有异步函数,这是 ECMAScript 2017 规范中定义的一个功能,旨在使处理异步操作(如 AJAX 请求)变得异常简单。然而,一些开发人员没有使用异步函数的奢侈(由于截至 2016 年缺乏当前的浏览器支持),因此处理 AJAX 请求的现实仍然是,您必须接受它们的异步特性,而不是躲避它。起初,这很令人费解。即使在您成功地掌握了这个概念之后,也要预料到在不太重要的情况下会经常遇到挫折,比如在处理嵌套的异步请求时。如果这一点在之前的经历中还不清楚,当你完成这一章的时候,你甚至会意识到这种复杂性。尽管如此,在处理 AJAX 请求时,这个概念可能是最重要的。

超文本传送协议

用于浏览器和服务器之间通信的主要协议当然是 HTTP,它代表超文本传输协议。Web 之父蒂姆·伯纳斯·李于 1991 年创建了第一个官方 HTTP 规范 4 。第一个版本是和 HTML 一起设计的,第一个 web 浏览器只有一个方法:GET。当浏览器请求一个页面时,将发送一个 GET 请求,服务器将使用构成请求页面的 HTML 进行响应,然后 web 浏览器将呈现该页面。在 AJAX 作为补充规范被引入之前,HTTP 主要局限于这个工作流。

尽管 HTTP 最初只有一个方法——GET——但随着时间的推移,又增加了几个。目前,HEAD、POST、PUT、DELETE 和 PATCH 都是当前规范第 2 版的一部分,该规范由互联网工程任务组(IETF)维护为 RFC 7540。 5 GET 请求应该有一个空的消息体(请求有效负载),以及一个描述请求 URI(通用资源指示符)中引用的资源的响应。这是一种“安全”的方法,因此在处理该请求时,服务器端不会对资源进行任何更改。HEAD 与 GET 非常相似,只是它返回一个空的消息体。然而,HEAD 是有用的,因为它包括一个响应头—Content-Length—其值等于请求被 GET 所传输的字节数。例如,这对于在不实际返回整个文件的情况下检查文件的大小很有用。头,正如你所料,也是一个“安全”的方法。

DELETE、PUT、POST 和 PATCH 是不安全的,因为它们可能会更改服务器上的相关资源。在这四个“不安全”的方法中,有两个——PUT 和 DELETE——被认为是幂等的,这意味着即使它们被多次调用,它们也将总是产生相同的结果。PUT 通常用于替换资源,而 DELETE 显然用于删除资源。PUT 请求应该有一个描述更新的资源内容的消息体,而 DELETE 不应该有有效负载。POST 不同于 PUT,它将创建一个新的资源。最后,补丁,一种相对较新的 HTTP 请求方法, 6 允许以非常特定的方式修改资源。这个请求的消息体准确地描述了应该如何修改资源。PATCH 不同于 PUT 方法,因为它不完全替换引用的资源。

所有 AJAX 请求都将使用其中一种方法与服务器进行动态通信。请注意,旧浏览器可能不支持补丁等新方法。在本章的后面,我将更详细地介绍如何正确使用这些方法,以及如何将 AJAX 请求规范与这些方法结合使用,以生成高度动态的 web 应用。

预期和意外反应

理解客户机和服务器之间的通信协议是一个非常重要的概念。当然,发送请求是其中的一部分,但是对这些请求的响应也同样重要。请求有消息头和可选的消息体(有效负载),而响应由三部分组成:响应消息体、消息头和状态代码。状态代码对于响应是唯一的,并且通常可以在底层传输实例上访问(比如XMLHttpRequestfetch)。状态代码通常是三位数,通常可以根据最高有效位进行分类。200 级状态代码表示成功,300 用于重定向,而 400 级和 500 级状态表示某种错误。这些都在 RFC 2616 中有详细的正式定义。 7

正如用try / catch块处理代码异常很重要一样,处理异常的 AJAX 响应也同样重要。虽然 200 级的响应通常是预期的,或者至少是期望的,但是您还必须考虑意外的或者不期望的响应,比如 400 级和 500 级,或者甚至是状态为0的响应(如果请求由于网络错误而终止或者服务器返回完全空的响应,这种情况可能会发生)。我观察到,简单地忽略异常情况似乎很常见,这不仅限于 HTTP 响应的处理。其实我自己也为此感到内疚。

Web 套接字

与传统的 AJAX 请求相比,Web 套接字是一个相对较新的 web API 特性。它们于 2011 年由 IETF(互联网工程任务组)在 RFC 6455 8 中首次标准化,目前受到除 Internet Explorer 9 之外的所有现代浏览器的支持。Web 套接字在许多方面不同于纯粹的 HTTP 请求,最显著的是它们的生存期。尽管 HTTP 请求的寿命通常很短,但 web 套接字连接意味着在应用实例或网页的生命周期内保持开放。web 套接字连接以 HTTP 请求开始,这是初始握手所必需的。但是在这个握手完成之后,客户机和服务器就可以随意交换数据了,无论它们同意什么格式。这个 web 套接字协议允许客户端和服务器之间真正的实时通信。尽管本章没有更深入地探讨 web 套接字,但我觉得至少提到它们是有用的,因为它们确实说明了 JavaScript 发起的异步通信的另一种方法。

发送获取、发布、删除、上传和修补请求

jQuery 通过恰当命名的ajax()方法为 AJAX 请求提供了一流的支持。通过这种方法,您可以发送任何类型的 AJAX 请求,但是 jQuery 也为一些标准化的 HTTP 请求方法提供了别名——比如get()post()——这样可以节省一些击键的时间。web API 提供了两个对象,XMLHttpRequestfetch,用于从浏览器向服务器发送任何类型的异步请求。所有浏览器都支持XMLHttpRequest,但fetch相对较新,并非所有现代浏览器都支持,尽管有一种实心 polyfill 可为所有浏览器提供支持。

使用 jQuery 向服务器端点发出一个简单的 GET 请求,带有一个简单的响应处理程序,如下所示:

1  $.get('/my/name').then(
2    function success(name) {
3      console.log('my name is ' + name);
4    },
5    function failure() {
6      console.error('Name request failed!');
7    }
8  );

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Note

除非开发人员工具已打开,否则在 Internet Explorer 9 和更早版本中,console对象不可用。

在前面的代码中,我们向“/my/name”服务器端点发送一个 GET 请求,并期待一个包含名称值的明文响应,然后我们将它打印到浏览器控制台。如果请求失败(比如服务器返回一个错误状态代码),就会调用failure函数。在这种情况下,我使用 jQuery 的ajax()方法及其别名返回的promise-like object (or “thenable”)。jQuery 提供了几种处理响应的方法,但是我将特别关注前面演示的那种。第十一章讲述了更多关于承诺的内容,这是 JavaScript 的标准化部分。

同样的请求,在没有 jQuery 的情况下发送,可以在所有浏览器中使用,需要更多的输入,但肯定不会太难:

 1  var xhr = new XMLHttpRequest();
 2  xhr.open('GET', '/my/name');
 3  xhr.onload = function() {
 4    if (xhr.status >= 400) {
 5      console.error('Name request failed!');
 6    }
 7    else {
 8      console.log('my name is ' + xhr.responseText);
 9    }
10  };
11  xhr.onerror = function() {
12    console.error('Name request failed!');
13  };
14  xhr.send();

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

当请求完成并且收到一些响应时,调用onload。不过,这可能是一个错误响应,所以我们必须通过查看响应状态代码来确认。如果请求在某个非常低的级别失败,比如由于 CORS 错误,就会调用onerroronload属性是我们可以轻松设置响应处理程序的地方。从这里,我们可以确保响应已经完成,然后可以确定请求是否成功,并获得响应数据的句柄。所有这些都可以在我们创建并分配给xhr变量的XMLHttpRequest实例中获得。从前面的代码中,您可能会对 web API 不支持像 jQuery 这样的承诺感到有点失望。这是真的,直到 WHATWG 创建了fetch API 9 。Fetch API 为老化的XMLHttpRequest传输提供了一个现代的本地替代品,目前它受到 Firefox、Chrome、Opera 和 Microsoft Edge 的支持,Safari 的支持也即将到来。让我们用fetch来看看这个例子:

 1  fetch('/my/name').then(function(response) {
 2    if (response.ok) {
 3      return response.text();
 4    }
 5    else {
 6      throw new Error();
 7    }
 8  }).then(
 9    function success(name) {
10      console.log('my name is ' + name);
11    },
12    function failure() {
13      console.error('Name request failed!');
14    }
15  );

超越 jQuery(三)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

代码不仅包含了 promises 规范,还删除了很多常见于XMLHttpRequest的样板文件。在这一章中,你会看到更多关于fetch的例子。请注意,除了方法说明符之外,前面的任何示例对于 HEAD 请求都是相同的。您还会注意到fetch返回一个Promise,类似于 jQuery 的ajax()。不同之处在于,当执行非成功状态代码时,jQuery 会执行 error 函数。fetch仅当发生低级网络错误时,才会像XMLHttpRequest一样执行此操作。但是,当服务器返回一个带有错误代码的响应时,我们可以通过传递给第一个 promise 处理程序的Response对象上的ok属性,通过抛出一个异常来触发我们的错误函数。

许多通过 jQuery 学习 web 开发的开发人员可能认为,当您调用$.ajax()方法时,这个库正在做一些神奇而复杂的事情。这与事实相去甚远。所有繁重的工作都由浏览器通过XMLHttpRequest对象来完成。jQuery 的ajax()只是对XMLHttpRequest的包装。使用浏览器对 AJAX 请求的内置支持并不困难,您马上就会看到这一点。即使是跨来源的请求,没有 jQuery 也不简单——您将看到没有 jQuery 它们实际上是如何变得更容易。

发送发布请求

我已经演示了如何使用 jQuery、XMLHttpRequestfetch从服务器端点获取信息。但是其他一些可用的 HTTP 方法呢?假设您想向服务器添加一个新名称,而不是获取一个名称。对于这种情况,最合适的方法是 POST。要添加的名称将包含在我们的请求有效负载中,这是发送帖子时包含此类数据的常见位置。为了简单起见,我们将使用文本/普通的 MIME 类型来发送名称(我将在本章后面介绍更高级的编码技术)。我在这里也将省略响应处理代码,以便我们可以专注于关键概念:

1  $.ajax({
2    method: 'POST',
3    url: '/user/name',
4    contentType: 'text/plain',
5    data: 'Mr. Ed'
6  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

我们使用 jQuery 的通用ajax()方法,所以我们可以指定各种参数,比如请求的Content-Type头。这将发送一个 POST 请求,其明文正文包含文本“Mr. Ed”。在这种情况下,我们必须显式指定Content-Type,因为 jQuery 会将这个头设置为“application/x-www-form-urlencoded ”,这不是我们想要的。

使用XMLHttpRequest的相同 POST 请求如下所示:

1  var xhr = new XMLHttpRequest();
2  xhr.open('POST', '/user/name');
3  xhr.send('Mr. Ed');

  • 1
  • 2
  • 3
  • 4

不使用 jQuery 发送这个请求实际上需要更少的代码行。默认情况下,XMLHttpRequest10Content-Type设置为“text/plain ”,因此我们不需要弄乱任何请求头。我们可以方便地将请求体作为参数传递给send方法,如前所述。

如果你的目标是接受最新最好的网络标准,你可以试着用fetch来发送这篇文章:

1  fetch('/user/name', {
2    method: 'POST',
3    body: 'Mr. Ed'
4  });

  • 1
  • 2
  • 3
  • 4
  • 5

发送这个请求看起来类似于 jQuery,但是没有太多的样板文件。像XMLHttpRequestfetch基于请求负载智能地设置请求Content-Type(在某些情况下),所以我们不需要指定这个请求头。 11

发送上传请求

POST 请求通常用于创建新资源,而 PUT 请求通常用于替换现有资源。例如,PUT 更适合于替换现有产品的信息。请求的 URI 标识要用位于主体中的新信息替换的资源。为了简单地说明使用 jQuery、XMLHttpRequestfetch发送 PUT 请求,我将演示更新现有用户记录的手机号码:

1  $.ajax({
2    method: 'PUT',
3    url: '/user/1',
4    contentType: 'text/plain',
5    data: //complete user record including new mobile number

6  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这个使用 jQuery 的 PUT 请求看起来与之前展示的 POST 几乎相同,除了method属性。这个用户是通过他们的 ID 来标识的,这个 ID 恰好是 1。看到用XMLHttpRequest发送 PUT 与前面的例子相似,您可能不会感到惊讶:

1  var xhr = new XMLHttpRequest();
2  xhr.open('PUT', '/user/1');
3  xhr.send(/* complete user record including new mobile number */);

  • 1
  • 2
  • 3
  • 4

正如所料,Fetch API 提供了最简洁的方法:

1  fetch('/user/1', {
2    method: 'PUT',
3    body: //complete user record including new mobile number

4  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

发送删除请求

删除请求类似于放置请求,因为要操作的资源是在请求 URI 中指定的。主要区别在于,尽管 RFC 7231 没有明确规定,但删除请求通常不包含消息体。IETF 文档提到了消息体实际上给请求者带来问题的可能性: 13

  • 删除请求消息中的负载没有定义的语义;在删除请求中发送有效负载正文可能会导致一些现有的实现拒绝该请求。

这意味着,除了要操作的资源之外,指定参数的唯一安全方法是将它们作为查询参数包含在请求 URI 中。除了这种例外情况,删除请求以与 put 相同的方式发送。和 PUT 请求一样,DELETE 请求也是幂等的。请记住,幂等请求无论被调用多少次,其行为都是一样的。如果删除同一资源的多次调用导致例如删除不同的资源,这将是非常令人惊讶的。

使用 jQuery 删除资源的请求如下所示:

1  $.ajax('/user/1', {method: 'DELETE'});

  • 1
  • 2

同样的简单请求,使用XMLHttpRequest,只需要两行额外的代码就可以实现:

1  var xhr = new XMLHttpRequest();
2  xhr.open('DELETE', '/user/1');
3  xhr.send();

  • 1
  • 2
  • 3
  • 4

最后,我们可以使用非常优雅的 Fetch API 在许多现代浏览器中本地发送这个删除请求(或者在任何带有对fetch(在 GitHub 上维护) 14 的 polyfill 的浏览器中)

1  fetch('/user/1', {method: 'DELETE'});

  • 1
  • 2

fetch发送这个请求就和$.ajax()一样简单;我们可以很容易地在一行中写出全部内容,而不会失去可读性。

发送补丁请求

如前所述,补丁请求在 HTTP 场景中相对较新,用于更新现有资源的一部分。以我们之前的 PUT 请求为例,我们只想更新用户的手机号码,但是还必须在我们的 PUT 请求中包含所有其他用户数据。对于小记录,这可能没问题,但是对于大记录,这可能是对带宽的浪费。一种方法可能是为用户数据的每个部分定义特定的端点,或者根据 URI 查询参数确定要更新的数据,但这只会使我们的 API 变得混乱。对于这种情况,最好使用补丁请求。

让我们重新看看 PUT 示例,我们需要更新一个现有用户的手机号码,这次用的是 PATCH。jQuery 方法——使用简单的基于明文的键值消息体来指示要随新属性值一起更改的属性——如下所示:

1  $.ajax({
2    method: 'PATCH',
3    url: '/user/1',
4    contentType: 'text/plain',
5    data: 'mobile: 555-5555'
6  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Note

如果您的底层模型是 JSON,那么发送一个 JSON 补丁文档更合适。 15 但是我们还没怎么谈 JSON。我将在这一章的后面讨论这个问题。

请记住,我们可以使用我们为补丁请求主体选择的任何格式来指定要更新的数据,只要客户机和服务器达成一致。如果我们更喜欢使用XMLHttpRequest,同样的请求看起来像这样:

1  var xhr = new XMLHttpRequest();
2  xhr.open('PATCH', '/user/1');
3  xhr.send('mobile: 555-5555');

  • 1
  • 2
  • 3
  • 4

为了完整起见,我将向您展示如何使用 Fetch API 发送完全相同的请求:

1  fetch('/user/1', {
2    method: 'PATCH',
3    body: 'mobile: 555-5555'
4  });

  • 1
  • 2
  • 3
  • 4
  • 5

编码请求和读取编码的响应

在上一节中,我谈到了所有编码方法中最简单的一种——text/plain——它是用于构成 HTTP 请求或响应的无格式文本的多用途 Internet 邮件扩展(MIME)。纯文本的简单是福也是祸。也就是说,它很容易使用,但只适合非常小和简单的情况。缺乏任何标准化的结构限制了它的表现力和实用性。对于更复杂(也更常见)的请求,有更合适的编码类型。在本节中,我将讨论另外三种 MIME 类型:“application/x-www-form-urlencoded”、“application/json”和“multipart/form-data”。在本节结束时,您不仅会熟悉这些额外的编码方法,还会理解如何在没有 jQuery 的情况下编码和解码消息,尤其是在发送 HTTP 请求和解析响应时。

URL 编码

URL 编码可以发生在请求的 URL 中,也可以发生在请求/响应的消息体中。这种编码方案的 MIME 类型是“application/x-www-form-urlencoded”,数据由简单的键/值对组成。每个键和值由等号(=)分隔,而每对键和值由&符号(&)分隔。但是编码算法远不止这些。键和值可以进一步编码,这取决于它们的字符构成。非 ASCII 字符以及一些保留字符被替换为百分号(%),后跟字符相关字节的十六进制值。W3C HTML5 规范的 HTML 表单部分对此做了进一步的定义。 16 这个描述可能有点过于简单,但对于本章的目的来说,它足够恰当和全面。如果您想了解更多关于这种 MIME 类型的编码算法,请看看规范,虽然它有点枯燥,可能需要一些阅读才能正确解析。

对于 GET 和 DELETE 请求,URL 编码的数据应该包含在 URI 的末尾,因为这些请求方法通常不应该包含有效负载。对于所有其他请求,消息体是放置 URL 编码数据的最合适位置。在这种情况下,请求或响应必须包含“application/x-www-form-urlencoded”的Content-Type头,这是编码方案的 MIME 类型。URL 编码的消息预计相对较小,尤其是在处理 GET 和 DELETE 请求时,因为现实世界中浏览器和服务器上存在 URI 长度限制。 17 虽然这种编码方式比 text/plain 更优雅,但是缺乏层次性意味着这些消息在某种程度上也限制了它们的表现力。但是,可以使用括号将子属性绑定到父属性。例如,具有一组子键/值对(即“child1”和“child2”)的值的“parent”键可以编码为“parent[child 1]= child 1 val&parent[child 2]= child 2 val”。事实上,这就是 jQuery 在将 JavaScript 对象编码成 URL 编码的字符串时所做的事情。

jQuery 的 API 提供了一个函数,该函数获取一个对象并将其转换为 URL 编码的字符串:$.param。例如,如果我们想将一对简单的键/值对编码成一个 URL 编码的字符串,我们的代码应该是这样的:

1  $.param({
2    key1: 'some value',
3    'key 2': 'another value'
4  });

  • 1
  • 2
  • 3
  • 4
  • 5

这一行将产生一个字符串“key 1 = some+value & key+2 = other+value”。application/x-www-form-urlencoded MIME 类型的规范声明空格是保留字符,应该转换为“加号”字符。然而,在实践中,ASCII 字符代码也是可以接受的。因此,同样的一对键/值对也可以表示为“key1 =某个%20 值& key % 202 =另一个% 20 值”。当我用 web API 介绍 URL 编码时,您将会看到一个这样的例子。

如果我们想要创建一个具有三个属性的新用户——姓名、地址和电话——我们可以向我们的服务器发送一个 POST 请求,其中包含 URL 编码的请求正文,其中包含新用户的信息。对于 jQuery,请求看起来像这样:

1  $.ajax({
2    method: 'POST',
3    url: '/user',
4    data: {
5      name: 'Mr. Ed',
6      address: '1313 Mockingbird Lane',
7      phone: '555-555-5555'
8    }
9  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

jQuery 确实让这变得相对直观,因为它允许您传入一个描述新用户的 JavaScript 对象。没有必要使用$.param方法。如前所述,jQuery 的$.ajax() API 方法假设一个“application/x-www-form-urlencoded”的Content-Type,它将您的data属性的值编码为自动匹配这个假设。在这种情况下,您根本不必考虑编码或编码类型。

尽管 web API 确实要求您了解编码类型,并且要求您在发送请求之前对数据进行编码,但是这些任务并不太复杂。我已经向您展示了 jQuery 如何允许您使用$.param()将一个文本字符串编码为“application/x-www-form-urlencoded”——并且您可以使用全局名称空间上可用的encodeURI()encodeURIComponent()方法在没有 jQuery 的情况下完成同样的工作。这些方法在 ECMAScript 规范中定义,并且自 1999 年完成的 ECMA-262 第三版规范、 18 以来就已经可用。

encodeURI()encodeURIComponent()都执行相同的常规任务——URL 编码一个字符串。但是它们各自决定了字符串的哪一部分编码有所不同,所以它们与特定的用例联系在一起。encodeURI()意在用于一个完整的 URL,比如或一串由一个&符号(&)分隔的键值对,比如“first=ray & last=nicholus”。然而,encodeURIComponent()只用于需要 URL 编码的单个值,比如“雷·尼科尔斯”或“艾德先生”。如果您使用encodeURIComponent()对本段前面列出的完整 URL 进行编码,那么冒号、正斜杠、问号和&符号都将被 URL 编码,这可能不是您想要的(除非整个 URL 本身就是一个查询参数)。

回顾一下本节中使用 jQuery 的简单 URL 编码示例,我们可以使用 web API 使用encodeURI()对相同的数据进行编码:

1  encodeURI('key1=some value&key 2=another value');

  • 1
  • 2

关于encodeURI的输出,它产生“key 1 = some % 20 value&key % 202 = another % 20 value”。首先,请注意,jQuery 用加号(+)替换空格,encodeURI()(和encodeURI-Component)用“%20”替换空格。这是完全正确的,但也是一个显著的区别。其次,jQuery 允许将数据编码为 JavaScript 对象,encodeURI()要求用等号(=)将键与值分开,用 and 符号(&)将键/值对分开。更进一步,我们可以复制之前发送的相同 POST 请求,添加一个新名称,首先使用XMLHttpRequest:

1  var xhr = new XMLHttpRequest(),
2      data = encodeURI(
3        'name=Mr. Ed&address=1313 Mockingbird Lane&phone=555-555-5555');
4  xhr.open('POST', '/user');
5  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
6  xhr.send(data);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

XMLHTTPRequest路由和 jQuery 的$.ajax()路由的另一个显著区别是,我们还必须设置请求头的Content-Type头。jQuery 默认为我们设置,需要让服务器知道如何解码请求数据。幸运的是,XMLHttpRequest提供了一种设置请求头的方法——名副其实的setRequestHeader()

我们可以从 Fetch API 中获得一些相同的好处,但是我们仍然需要自己编码。不用担心——这可以很容易地完成:

1  var data =
2    encodeURI('name=Mr. Ed&address=1313 Mockingbird Lane&phone=555-555-5555');
3    fetch('/user', {
4    method: 'POST',
5    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
6    body: data
7  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

XMLHttpRequest一样,Content-Type头也必须在这里指定,因为fetch发起的带有String正文的请求的默认Content-Type也是“文本/普通”。但是,Fetch API 允许 AJAX 请求以更优雅、更简洁的形式构造,类似于 jQuery 已经提供了一段时间的解决方案。Fetch在 Firefox,Chrome,Opera,和 Edge 都支持,有一个开放的案例增加了对 Safari 的支持。 19 在不久的将来,XMLHttpRequest将成为历史的神器,与 jQuery 的 AJAX 支持相匹敌的fetch将成为 AJAX 传输的首选。

JSON 编码

JavaScript 对象符号,更好的说法是 JSON,被认为是一种“数据交换语言”(如 json.org 上所描述的)。如果这个晦涩的描述让你有点困惑,不要难过——这不是一个特别有用的总结。可以把 JSON 想象成一个转化成字符串的 JavaScript 对象。还有一点需要解释,但这在我看来是一个合理的高层次定义。在 web 开发中,如果基于浏览器的客户端希望轻松地将 JavaScript 对象发送到服务器端点,这将非常有用。反过来,服务器可以响应来自这个客户机的请求,并在响应体中提供 JSON,客户机可以很容易地将其转换成 JavaScript 对象,以便进行简单的编程解析和操作。虽然“application/x-www-form-urlencoded”要求数据以平面格式表示(或者用非标准括号符号表示父/子关系),但“application/json”允许数据以层次格式表示。一个键可以有许多子键,这些子键也可以有子键。从这个意义上说,JSON 比 URL 编码的数据更具表现力,而 URL 编码的数据本身比纯文本更具表现力和结构化。

如果您还不熟悉这种通用的数据格式,让我们用 JSON 表示一个用户:

1  {
2    "name": "Mr. Ed",
3    "address": "1313 Mockingbird Lane",
4    "phone": {
5      "home": "555-555-5555"
6      "mobile": "444-444-4444"
7    }
8  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注意,JSON 让我们能够轻松地表达子键,这是明文和 URL 编码的字符串所缺乏的特性。Ed 先生有两个电话号码,使用 JSON,我们可以很好地将这两个号码与父“电话”属性关联起来。从上一节扩展我们的 AJAX 示例,让我们使用 jQuery 向我们的服务器添加一个新的名称记录,这次使用 JSON 编码的有效负载:

 1  $.ajax({
 2    method: 'POST',
 3    url: '/user',
 4    contentType: 'application/json',
 5    data: JSON.stringify({
 6      name: 'Mr. Ed',
 7      address: '1313 Mockingbird Lane',
 8      phone: {
 9        home: '555-555-5555',
10        mobile: '444-444-4444'
11      }
12    });
13  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

请注意,我们的代码看起来比前一个 URL 编码的示例要难看一些。有两个原因,一个是我们必须通过一个contentType属性显式地指定Content-Type头,这个抽象实际上没有太大的帮助。其次,我们必须离开舒适熟悉的 jQuery API,将 JavaScript 对象转换成 JSON。因为 jQuery 没有提供实现这一点的 API 方法(这很奇怪,因为它提供了一种将 JSON 字符串转换成 object – $的方法。parseJSON()),我们必须利用JSON对象,它已经被标准化为 ECMAScript 规范的一部分。JSON对象提供了两种将 JSON 字符串转换成 JavaScript 对象的方法。它最早出现在 ECMAScript 5.1 规范中, 20 ,这意味着它在 Node.js 以及所有现代浏览器中都受到支持,包括 Internet Explorer 8。在前面的 jQuery POST 示例中使用的JSON.stringify()方法获取用户记录,该记录表示为一个 JavaScript 对象,并将其转换为适当的 JSON 字符串。在我们将记录发送到服务器之前,这是必需的。

如果您想发送一个接收 JSON 数据的 GET 请求,那么使用get JSON就可以很简单地做到:

1  $.getJSON('/user/1', function(user) {
2    // do something with this user JavaScript object

3  });

  • 1
  • 2
  • 3
  • 4
  • 5

不使用 jQuery 发送之前的 POST 请求怎么办?首先,用XMLHttpRequest:

 1  var xhr = new XMLHttpRequest(),
 2      data = JSON.stringify({
 3        name: 'Mr. Ed',
 4        address: '1313 Mockingbird Lane',
 5        phone: {
 6          home: '555-555-5555',
 7          mobile: '444-444-4444'
 8        }
 9      });
10  xhr.open('POST', '/user');
11  xhr.setRequestHeader('Content-Type', 'application/json');
12  xhr.send(data);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

XHR 没什么好惊讶的。这个尝试看起来非常类似于我们在上一节中发送的 URL 编码的 POST 请求,除了字符串化我们的 JavaScript 对象和设置适当的Content-Type头。正如您已经看到的,无论我们是否使用 jQuery,我们都必须解决相同的问题。

但是发送 JSON 编码的请求只是所需知识的一半。我们也必须准备好接收和解析 JSON 响应。看起来就像这样:

1  var xhr = new XMLHttpRequest();
2  xhr.open('GET', '/user/1');
3  xhr.onload = function() {
4    var user = JSON.parse(xhr.responseText);
5    // do something with this user JavaScript object

6  };
7  xhr.send();

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

请注意,我们发送了一个获取用户数据的请求,并期望服务器以 JSON 编码的字符串形式返回该记录。在前面的代码中,我们利用了onload函数,该函数将在请求完成时被调用。此时,我们可以通过 XHR 实例上的responseText属性获取响应体。要将它转换成合适的 JavaScript 对象,我们必须使用 JSON 对象的另一个方法— parse()。在现代浏览器中(除了 Internet Explorer),用XMLHttpRequest接收 JSON 数据甚至更容易:

1  var xhr = new XMLHttpRequest();
2  xhr.open('GET', '/user/1');
3  xhr.onload = function() {
4    var user = xhr.response;
5    // do something with this user JavaScript object

6  };
7  xhr.send();

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

前面的例子假设服务器已经包含了“application/json”的Content-Type头。这让XMLHttpRequest知道如何处理响应数据。它最终将其转换成一个 JavaScript 对象,并使转换后的值在我们的XMLHttpRequest实例的response属性上可用。

最后,我们可以使用 Fetch API 将这个新的用户记录添加到我们的服务器,同样使用 JSON 编码的请求:

 1  fetch('/user', {
 2    method: 'POST',
 3    headers: {'Content-Type': 'application/json'},
 4    body: JSON.stringify({
 5      name: 'Mr. Ed',
 6      address: '1313 Mockingbird Lane',
 7      phone: {
 8        home: '555-555-5555',
 9        mobile: '444-444-4444'
10      }
11    });
12  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这并不奇怪,也很简单,但是从我们的服务器收到 JSON 编码的响应怎么办?让我们使用fetch发送一个简单的 GET 请求来获取一个用户记录,期望我们的服务器将响应一个编码为 JSON 字符串的用户记录:

1  fetch('/user/1').then(function(request) {
2      return request.json();
3    }).then(function(userRecord) {
4        // do something with this user JavaScript object

5  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

Fetch API 为我们约定的回调提供了一个Request对象。 21 因为我们期待 JSON,所以我们在这个对象上调用json()方法,它本身返回一个Promise。注意,json()方法实际上是在Body接口上定义的。22Request对象实现了Body接口,所以我们在这里可以访问两个接口上的方法。通过返回该承诺,我们可以链接另一个约定的处理程序,并期望在最后一次成功回调中接收响应有效负载的 JavaScript 对象表示作为参数。现在我们有了来自服务器的用户记录。挺简单大方的!同样,如果承诺仍然有点含糊不清,不要担心——我会在第十一章中详细介绍。

有趣的是,ECMAScript 2016 提供了使用替代语法的能力,使上述(任何其他代码示例)更加优雅。下面,我用“箭头函数”重写了前面的例子:

1  fetch('/user/1')
2    .then(request => request.json())
3    .then(userRecord => {
4      // do something with this userRecord object

5    });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

箭头函数超出了本书的范围,但是我认为向还不知道这种语言特性的读者指出这一点会很好。注意,并不是所有的浏览器都支持箭头函数,所以您可能需要使用一个编译时编译器来将这些函数转换成“传统的”函数。一些 JavaScript 编译器包括 TypeScript、Closure 编译器、Babel 和 Bublè。其中一些会在本书后面提到,比如第十一章。

多部分编码

另一种通常与 HTML 表单提交相关的常见编码方案是 multipart/- form-data,也称为 multipart 编码。IETF 在 RFC 2388 中正式定义了这种数据传递方法的算法。 23 该算法在 HTML 表单上下文中的使用由 W3C 在 HTML 规范中进一步描述。多部分/格式数据消息中的非 ASCII 字符不必转义。相反,消息的每一部分都被分割成字段,每个字段都包含在一个多部分边界内。边界由浏览器生成的唯一 ID 分隔,并且保证它们在请求中的所有其他数据中是唯一的,因为浏览器分析请求中的数据以确保不会生成冲突的 ID。多部分编码消息中的字段通常是 HTML <form>字段。每个字段都位于其自己的多部分边界内。每个边界内部都有一个标题,其中存放着关于字段的元数据(如名称/键),还有一个正文(其中存放着字段值)。

考虑以下形式:

 1  <form action="my/server" method="POST" enctype="multipart/form-data">
 2    <label>First Name:
 3      <input name="first">
 4    </label>
 5
 6    <label>Last Name:
 7      <input name="last">
 8    </label>
 9
10    <button>Submit</button>
11  </form>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

当点击提交按钮时,任何输入的数据都将被提交到服务器。假设用户在第一个文本输入中输入“Ray”,在第二个文本输入中输入“Nicholus”。点击提交后,请求正文可能如下所示:

1  -----------------------------1686536745986416462127721994
2  Content-Disposition: form-data; name="first"
3
4  Ray
5  -----------------------------1686536745986416462127721994
6  Content-Disposition: form-data; name="last"
7
8  Nicholus
9  -----------------------------1686536745986416462127721994--

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

服务器知道如何通过在请求体中查找标记每个部分的惟一 ID 来找到每个表单字段。这个 ID 由浏览器包含在Content-Type头中,在本例中是“multipart/form-data;边界=————————1686536745986416462127721994”。请注意,邮件正文的 MIME 类型由多部分边界 ID 用分号分隔。

但是 HTML 表单提交并不是我们希望使用 multipart/form-data MIME 类型编码请求消息的唯一实例。因为这种 MIME 类型在所有服务器端语言中实现起来都很简单,所以它可能是从客户端向服务器传输键/值对的安全选择。但是,最重要的是,多部分编码非常适合将键/值对与二进制数据(如文件)混合在一起。我将在下一节中讨论更多关于上传文件的内容。

那么我们如何使用 jQuery 的$.ajax()方法发送多部分编码的请求呢?很快您就会看到,这很难看,而且 jQuery 通常提供的抽象层在这种情况下是不完整的,因为无论如何您都必须直接委托给 web API。继续前面的一些例子,让我们向我们的服务器发送一个新的用户记录——一个由用户名、地址和电话号码组成的记录:

 1  var formData = new FormData();
 2  formData.append('name', 'Mr. Ed');
 3  formData.append('address', '1313 Mockingbird Lane');
 4  formData.append('phone', '555-555-5555');
 5
 6  $.ajax({
 7    method: 'POST',
 8    url: '/user',
 9    contentType: false,
10    processData: false,
11    data: formData
12  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

要发送一个多部分编码的 AJAX 请求,我们必须发送一个包含我们的键/值对的FormData对象,浏览器会处理剩下的事情。这里没有 jQuery 抽象;您必须直接使用 web API 的FormData。请注意,Internet Explorer 9 不支持FormData。缺乏抽象是 jQuery 的一个漏洞,尽管FormData相对直观且非常强大。事实上,您可以向它传递一个<form>元素,键/值对就会为您创建,并准备好异步提交到您的服务器。Mozilla Developer Network 在FormData上有一篇很棒的文章。更多细节你应该读一下。

用 jQuery 发送 MPE 请求的最大问题是必须设置模糊的选项才能让它工作。processData: false?这到底是什么意思?如果不设置这个选项,jQuery 会尝试将FormData转换成 URL 编码的字符串。至于contentType: false,这是确保 jQuery 不插入自己的内容类型头所必需的。请记住引言部分,浏览器必须为您指定内容类型,因为它包含服务器解析请求时使用的计算出的多部分边界 ID。

同样的请求和普通的旧的XMLHttpRequest没有什么不同,坦率地说,并不比 jQuery 的解决方案更直观:

1  var formData = new FormData(),
2      xhr = new XMLHttpRequest();
3
4  formData.append('name', 'Mr. Ed');
5  formData.append('address', '1313 Mockingbird Lane');
6  formData.append('phone', '555-555-5555');
7
8  xhr.open('POST', '/user');
9  xhr.send(formData);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

事实上,使用 XHR 会产生更少的代码,我们不必包含无意义的选项,比如contentType: falseprocessData: false。正如所料,Fetch API 甚至更简单:

1  var formData = new FormData();
2  formData.append('name', 'Mr. Ed');
3  formData.append('address', '1313 Mockingbird Lane');
4  formData.append('phone', '555-555-5555');
5
6  fetch('/user', {
7    method: 'POST',
8    body: formData
9  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

看到了吗?如果你能稍微看看 jQuery 之外的东西,你会发现 web API 并不总是像有些人说的那样可怕。在这种情况下,jQuery 的 API 就显得不够优雅。

上传和操作文件

异步文件上传,这个我颇有经验的话题, 26 是 jQuery 经常无法有效包装 web API 并为用户提供证明使用该库的体验的又一个例子。这确实是一个复杂的主题,虽然我不能在这里涵盖与文件上传相关的所有内容,但我一定会解释基本知识,并展示如何在现代浏览器和古代浏览器中使用 jQuery、XMLHttpRequestfetch上传文件。在这种特殊且有点不寻常的情况下,请注意 Internet Explorer 9 被排除在“现代浏览器”的定义之外。这样做的原因很快就会清楚了。

在古代浏览器中上传文件

在我们开始在旧浏览器中上传文件之前,让我们定义一个非常重要的术语:浏览上下文。例如,浏览上下文可以是windowiframe。因此,如果我们有一个window,并且在这个window中有一个iframe,我们就有两个浏览上下文:父window和子iframe

在古代浏览器(包括 Internet Explorer 9)中上传文件的唯一方法是在<form>中包含一个<input type="file">元素并提交这个表单。默认情况下,服务器对表单提交的响应会替换当前的浏览上下文。当使用高度动态的单页面 web 应用时,这是不可接受的。我们需要能够在旧浏览器中上传文件,并且仍然保持对当前浏览上下文的完全控制。不幸的是,没有办法阻止表单提交替换当前的浏览上下文。但是我们当然可以创建一个子浏览上下文,在这里我们提交表单,然后监视这个浏览上下文,通过监听更改来确定我们的文件何时被上传。

这种方法很容易实现,只需让表单指向文档中的一个<iframe>。为了确定文件何时完成上传,将一个“onload”事件处理程序附加到<iframe>。为了演示这种方法,我们需要做一些假设,以使这相对不那么痛苦。首先,假设我们的主浏览上下文包含如下所示的标记片段:

 1  <form action="/upload"
 2        method="POST"
 3        enctype="multipart/form-data"
 4        target="uploader">
 5
 6    <input type="file" name="file">
 7
 8  </form>
 9
10  <iframe name="uploader" style="display: none;"></iframe>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

请注意,enctype属性被设置为“多部分/表单数据”。您可能还记得上一节,带有文件输入元素的表单必须生成一个多部分编码的请求,以便将文件字节正确地传递给服务器。

第二个假设:我们有一个函数——upload()——当用户通过 file input元素选择一个文件时,这个函数被调用。我现在不打算讨论这个具体的细节,因为我们还没有讨论事件处理。我在第十章中讨论事件。

好的,那么我们如何用 jQuery 实现这一点呢?像这样:

 1  function upload() {
 2    var $iframe = $('IFRAME'),
 3        $form = $('FORM');
 4
 5    $iframe.on('load', function() {
 6      alert('file uploaded!')
 7    });
 8
 9    $form.submit();
10  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

如果需要的话,我们可以用 JavaScript/jQuery 完成更多的工作,比如设置target属性。如果我们只使用一个文件输入元素,我们也可以动态地创建表单并将文件输入移动到表单中。但是这些都是不必要的,因为我们的标记已经包含了我们需要的一切。jQuery 为我们节省了多少工作量和复杂性?让我们看一下非 jQuery 版本进行比较:

 1  function upload() {
 2     var iframe = document.getElementsByTagName('IFRAME')[0],
 3         form = document.getElementsByTagName('FORM')[0]
 4
 5     iframe.onload = function() {
 6       alert('file uploaded!');
 7     }
 8
 9     form.submit();
10  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

jQuery 并没有为我们做多少事情。web API 解决方案几乎与最初的 jQuery 代码相同。在这两种情况下,我们都必须选择 iframe 和表单,附加一个onload处理程序,在上传完成后执行一些操作,然后提交表单。在这两种情况下,我们的主要浏览上下文/窗口保持不变。服务器的响应被埋在我们隐藏的<iframe>里。相当整洁!

在现代浏览器中上传文件

有一种更现代的方式来异步上传文件,这在所有现代浏览器中都是可能的,除了 Internet Explorer 9。由于文件 API、 27XMLHttpRequest Level 2(在 API 方面对原始规范的一个小的、不间断的更新),通过 JavaScript 上传文件的能力是可能的。web API 的这两个元素都是由 W3C 规范标准化的。jQuery 并没有让上传文件变得更容易。将文件上传到浏览器的现代本地 API 优雅、易用且功能强大。jQuery 并没有试图在这里提供一个抽象层,实际上让文件上传变得有些尴尬。

典型的工作流程包括以下步骤:

  1. 用户通过<input type="file" multiple>元素选择一个或多个文件。请注意,multiple布尔属性允许用户选择多个文件,前提是浏览器支持该属性。
  2. JavaScript 用于指定一个“更改”事件监听器,当用户选择一个或多个文件时会调用该监听器。
  3. 当调用“change”监听器时,从<input type="file">元素获取一个或多个文件。这些作为File对象、 29 提供,它们扩展了Blob接口。 30
  4. 使用您选择的 AJAX 传输上传File对象。

因为我们还没有涉及到事件,所以假设有一个函数存在,当被调用时,它发出信号,表明我们的用户已经选择了我们正在监视的<input type="file">上的一个或多个文件。目标是上传这些文件。为了保持这个例子的重点和简单,还假设用户只能选择一个文件。这意味着我们的<input type="file">元素将不包含multiple布尔属性。在我刚才描述的环境中,可以使用 jQuery 在现代浏览器(除了 IE9)中上传文件,如下所示:

 1  function onFileInputChange() {
 2    var file = $('INPUT[type="file"]')[0].files[0];
 3
 4    $.ajax({
 5      method: 'POST',
 6      url: '/uploads',
 7      contentType: false,
 8      processData: false,
 9      data: file
10    });
11  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

前面的代码将向“/uploads”端点发送一个 POST 请求,请求体将包含用户选择的文件的字节。同样,我们必须使用模糊的contentType: false选项来确保 jQuery 不处理 Content-Type 头,以便浏览器可以设置它来反映文件的 MIME 类型。另外,processData: false是防止 jQuery 对File对象进行编码所必需的,这样会破坏我们试图上传的文件。我们也可以将文件包含在一个FormData对象中,然后上传。如果我们需要在一个请求中上传多个文件,或者如果我们想在文件旁边轻松地包含其他表单数据,这将是一个更好的选择。

没有 jQuery,使用XMLHttpRequest,文件上传其实简单多了:

1  function onFileInputChange() {
2    var file = document.querySelector('INPUT[type="file"]').files[0],
3        xhr = new XMLHttpRequest();
4
5    xhr.open('POST', '/uploads');
6    xhr.send(file);
7  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

与 jQuery 示例一样,我们从文件输入的files属性中获取选择的File,作为文件 API 的一部分,并通过将它传递给send()方法将其发送到我们的端点,该方法从XMLHttpRequest级别 2 开始支持Blob

使用 Fetch API 也可以上传文件。让我们来看看:

1  function onFileInputChange() {
2    var file = document.querySelector('INPUT[type="file"]').files[0];
3
4    fetch('/uploads', {
5      method: 'POST',
6      body: file
7    });
8  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

读取和创建文件

一般来说,熟悉 jQuery 的开发人员经常试图用 jQuery 解决他们所有的前端开发问题。他们有时看不到图书馆之外的网络。当开发人员变得依赖于这个安全网时,如果问题无法通过 jQuery 解决,通常会导致沮丧。这就是我在第一章中提到的压迫性魔法。您刚刚看到了 jQuery 在上传文件时,充其量几乎不提供任何帮助。假设您想要读取一个文件,或者甚至创建一个新文件或者修改一个现有文件以发送到服务器端点?这是 jQuery 完全没有覆盖的领域。对于读取文件,你必须依赖于FileReader接口, 31 ,它被定义为文件 API 的一部分。在浏览器端创建“文件”需要使用Blob构造函数。 32

最简单的FileReader例子是向控制台读取一个文本文件,这个例子足以满足这里的演示目的。假设用户通过<input type"file">选择了这个文本文件,文本File对象被发送给一个函数进行输出。读取该文件并将其输出到开发人员工具控制台所需的代码包括以下代码:

1  function onTextFileSelected(file) {
2    var reader = new FileReader();
3
4    reader.onload = function() {
5      console.log(reader.result);
6    }
7
8    reader.readAsText(file);
9  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

不需要 jQuery,甚至不可能读取文件。你为什么需要它?读取文件相当容易。假设您想获取同一个文本文件,然后在将它上传到服务器之前,在文件末尾添加一些文本。令人惊讶的是,这也非常简单:

1  function onTextFileSelected(file) {
2    var modifiedFile = new Blob([file, 'hi there!'], {type: 'text/plain'});
3    // ...send modifiedFile to uploader

4  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这里的modifiedFile是所选文件的副本,文本为“你好!”加到最后。这是在总共一行代码中完成的。

跨域通信:一个重要的话题

随着越来越多的逻辑被卸载到浏览器,web 应用从多个 API 提取数据变得越来越常见,其中一些 API 作为第三方服务的一部分存在。一个很好的例子是一个 web 应用(像 Fine Uploader) 33 直接上传文件到亚马逊 Web 服务(AWS)简单存储服务(S3)桶。服务器到服务器的跨域请求很简单,没有任何限制。但是对于从浏览器发起的跨域请求,情况就不一样了。对于想开发一个从浏览器直接向 S3 发送文件的 web 应用的开发人员来说,有一个障碍:同源策略。 34 该策略对 JavaScript 发起的请求进行限制。更具体地说,禁止域间的XMLHttpRequest请求。比如从 https://mywebapp.comhttps://api.github.com 发送请求,因为同源策略被浏览器阻止。虽然这种限制是为了增加安全性,但这似乎是一个主要的限制因素。如果不首先通过域 A 上的服务器,如何从域 A 向域 B 发出合法请求呢?接下来的两节将介绍实现这一目标的两种具体方法。

早期(JSONP)

同源策略防止脚本在其当前浏览上下文的域之外发起请求。虽然这涵盖了 AJAX 传输,比如XMLHttpRequest,但是像<a><img><script>这样的元素不受同源策略的约束。带填充的 JavaScript 对象表示法(JSONP)利用了这些异常中的一种,允许脚本发出跨源 GET 请求。

如果您不熟悉 JSONP,这个名称可能会有点误导。这里实际上根本不涉及 JSON。一个很常见的误解是,当客户端发起 JSONP 调用时,JSON 必须从服务器返回,但这并不正确。相反,服务器返回一个函数调用,这不是有效的 JSON。

JSONP 本质上只是一个丑陋的黑客,它利用了从服务器加载内容的<script>标签不受同源策略约束的事实。为了正常工作,客户端和服务器端需要合作并理解约定。您只需要将一个<script>标签的src属性指向一个支持 JSONP 的端点,并包含一个现有全局函数的名称作为查询参数。然后,服务器必须构造一个字符串表示,当浏览器执行该字符串表示时,它将调用全局函数,传入请求的数据。

在 jQuery 中利用这种 JSONP 方法实际上非常容易。假设我们想从不同域中的服务器获取用户信息:

1  $.ajax('http://jsonp-aware-endpoint.com/user/1', {
2    jsonp: 'callback',
3    dataType: 'jsonp'
4  }).then(function(response) {
5    // handle user info from server

6  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

jQuery 负责为我们创建<script>标签,还创建和跟踪一个全局函数。当收到来自服务器的响应后调用全局函数时,jQuery 会将它传递给前面提到的响应处理程序。这实际上是一个非常好的抽象。在没有 jQuery 的情况下完成同样的任务当然是可能的,但是没有那么好:

1  window.myJsonpCallback = function(data) {
2    // handle user info from server

3  };
4
5  var scriptEl = document.createElement('script');
6  scriptEl.setAttribute('src',
7    'http://jsonp-aware-endpoint.com/user/1?callback=myJsonpCallback');
8  document.body.appendChild(scriptEl);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

既然您有了这个新发现的知识,我建议您忘记它,并完全避免使用 JSONP。事实证明这是一个潜在的安全问题。 35 还有,在现代浏览器中,CORS 是一条好得多的路线。你很幸运:CORS 是下一小节的特色。对 JSONP 的解释主要是作为一个历史教训,并说明在 web 规范的现代发展之前,jQuery 是多么有用和重要。

现代(CORS)

CORS 是跨源资源共享的缩写,是从浏览器发送域间 AJAX 请求的更现代的方式。CORS 实际上是一个相当复杂的话题,即使是经验丰富的 web 开发人员也很容易误解。虽然 W3C 规范 36 可能很难解析,但是 Mozilla 开发者网络有一个很好的解释。 37 我在这里只打算触及一些 CORS 的概念,但是如果你想更详细地了解这个主题,MDN 的文章是有用的。

对 CORS 有了合理的理解,在现代浏览器中通过 JavaScript 发送跨来源的 AJAX 请求并不特别困难。不幸的是,在 Internet Explorer 8 和 9 中,这个过程并不容易。在 IE7 和更早的版本中,跨源 AJAX 请求只能通过 JSONP 实现,并且您被限制在这些浏览器中获取请求(因为这是 JSONP 的固有限制)。在所有非 JSONP 的情况下,jQuery 不提供任何帮助。

对于现代浏览器,所有的工作都委托给服务器代码。浏览器在客户端为你做一切必要的事情。在最基本的情况下,当使用 jQuery 的ajax() API 方法,或者直接使用 web API 的XMLHttpRequest传输,甚至使用 Fetch API 时,现代浏览器中跨来源 AJAX 请求的代码与同源 AJAX 请求是相同的。所以,我就不在这里展示了。

CORS 请求可以分为两种不同的类型:简单请求和非简单请求。简单请求由 GET、HEAD 和 POST 请求组成,内容类型为“text/plain”或“application/x-www-form-urlencoded”。“简单”请求中不允许使用非标准头,如“X-”头。这些 CORS 请求由浏览器发送,带有包含发送域的Origin报头。服务器必须确认来自这个来源的请求是可接受的。否则,请求失败。非简单请求包括 PUT、PATCH 和 DELETE 请求,以及其他内容类型,比如“application/json”。此外,正如您刚刚了解到的,非标准头会将 CORS 请求标记为“不简单”事实上,例如,如果 GET 或 POST 请求包含非标准的请求头,那么它也可能是不简单的。

非简单的 CORS 请求必须由浏览器“预先检查”。预检是浏览器在发送基本请求之前发送的选项请求。如果服务器正确地确认了预检,浏览器将发送底层/原始请求。非简单的跨源请求,例如带有 X-header 的 PUT 或 POST/GET 请求,不能从 CORS 规范之前的浏览器发送。因此,对于这些类型的请求,预检的概念被写入规范中,以确保服务器在没有明确选择的情况下不会接收这些类型的非简单跨来源的基于浏览器的请求。换句话说,如果您不想允许这些类型的请求,您不必对您的服务器进行任何更改。浏览器首先发送的预检请求将会失败,并且浏览器永远不会发送底层请求。

还有一点很重要,那就是跨源 AJAX 请求默认不发送 cookies。您必须在XMLHttpRequest传输上设置withCredentials标志。例如:

1  $.ajax('http://someotherdomain.com', {
2    method: 'POST',
3    contentType: 'text/plain',
4    data: 'sometext',
5    beforeSend: function(xmlHttpRequest) {
6      xmlHttpRequest.withCredentials = true;
7    }
8  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

jQuery 在这里提供了一些有漏洞的抽象。我们必须在 jQuery 管理的底层xmlHttpRequest上设置withCredentials属性。这也可以通过向 xhrFields 设置对象添加值为 true 的 withCredentials 属性来实现。ajax 方法文档中提到了这一点,但是可能很难定位,除非您确切知道在哪里查找。web API 路径是熟悉的,正如所料,我们必须设置withCredentials标志,以确保 cookies 被发送到我们的服务器端点:

1  var xhr = new XMLHttpRequest();
2  xhr.open('POST', 'http://someotherdomain.com');
3  xhr.withCredentials = true;
4  xhr.setRequestHeader('Content-Type', 'text/plain');
5  xhr.send('sometext');

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Fetch API 使得跨源 AJAX 请求的凭证发送变得更加简单:

1  fetch('http://someotherdomain.com', {
2    method: 'POST',
3    headers: {
4      'Content-Type': 'text/plain'
5    },
6    credentials: 'include'
7  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这里使用的credentials选项确保任何凭证(比如 cookies)都与 CORS 请求一起发送。注意,即使对于同源请求,fetch在默认情况下也不会向服务器端点发送 cookies。对于同源请求,您必须包含一个credentials: 'same-origin'选项,以确保fetch随请求一起发送 cookies。credentials选项的默认值是“省略”,这就是为什么默认情况下fetch不发送带有任何请求的 cookies。

当我们需要在 IE8 或 IE9 中发送跨域 AJAX 请求时,jQuery 实际上变得令人头疼。如果您将 jQuery 用于此目的,那么您实际上是在试图将一个方钉装进一个圆孔中。要理解为什么 jQuery 不适合 IE9 和 IE8 中的跨源请求,考虑几个底层要点很重要:

  1. IE8 和 IE9 中的跨源 AJAX 请求只能使用 IE 专有的XDomainRequest传输来发送。我将把为什么这是 IE 开发团队的一个巨大错误的咆哮留到另一本书里。无论如何,XDomainRequestXMLHttpRequest的精简版本,在 IE8 和 IE9 中进行跨源 AJAX 请求时必须使用它。对这种传输有很大的限制,比如除了 POST 和 GET 请求之外不能发送任何东西,以及缺少设置请求头或访问响应头的 API 方法。
  2. jQuery 的ajax()方法(以及所有相关的别名)只是XMLHttpRequest的包装器。它对XMLHttpRequest有很强的依赖性。我在这一章的前面提到了这一点,但是根据上下文,在这里再次指出这一点是有用的。

因此,在 IE8/9 中需要使用XDomainRequest来发送跨原点请求,但是jQuery.ajax()被硬编码为使用XMLHttpRequest。这是一个问题,在 jQuery 的上下文中解决它并不容易。幸运的是,对于那些坚决使用 jQuery 进行这种调用的人来说,有几个插件可以在这方面“修复”jQuery。本质上,插件必须通过$.ajaxTransport()方法覆盖 jQuery 的 AJAX 请求发送/处理逻辑。

当试图在旧浏览器中发送跨来源 AJAX 请求时,不要与 jQuery 较劲,坚持使用 web API。以下代码演示了一种简单的方法来确定是否需要使用XDomainRequest而不是XMLHttpRequest(仅在需要时使用):

1  if (new XMLHttpRequest().withCredentials === undefined) {
2      var xdr = new XDomainRequest();
3      xdr.open('POST', 'http://someotherdomain.com');
4      xdr.send('sometext');
5  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

native web 不仅为启动 AJAX 请求提供了一个合理的 API,使用XMLHttpRequest时更是如此,在这种情况下,它有时甚至比 jQuery 更直观,尤其是在发送一些跨来源的 AJAX 请求时。

Footnotes 1

https://blogs.msdn.microsoft.com/ie/2006/01/23/native-xmlhttprequest-object/

2

www.websiteoptimization.com/bw/0403/

3

https://fetch.spec.whatwg.org

4

www.w3.org/Protocols/HTTP/AsImplemented.html

5

https://httpwg.github.io/specs/rfc7540.html

6

https://tools.ietf.org/html/rfc5789

7

www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10

8

https://tools.ietf.org/html/rfc6455

9

https://fetch.spec.whatwg.org

10

www.w3.org/TR/XMLHttpRequest#dom-xmlhttprequest-send

11

https://fetch.spec.whatwg.org/#body-mixin

12

https://tools.ietf.org/html/rfc7231

13

https://tools.ietf.org/html/rfc7231#section-4.3.5

14

https://github.com/github/fetch

15

https://tools.ietf.org/html/rfc6902

16

www.w3.org/TR/html5/forms.html%23application/x-www-form-urlencoded-encoding-algorithm

17

http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers

18

http://www.ecma-international.org/publications/files/ECMA-ST-ARCH/ECMA-262%2C3rdedition%2CDecember1999.pdf

19

https://bugs.webkit.org/show_bug.cgi?id=151937

20

www.ecma-international.org/ecma-262/5.1/#sec-15.12

21

https://fetch.spec.whatwg.org/#request-class

22

https://fetch.spec.whatwg.org/#body

23

www.ietf.org/rfc/rfc2388.txt

24

www.w3.org/TR/html5/forms.html#multipart-form-data

25

https://developer.mozilla.org/en-US/docs/Web/API/FormData

26

http://fineuploader.com

27

www.w3.org/TR/FileAPI/

28

www.w3.org/TR/XMLHttpRequest2/

29

www.w3.org/TR/FileAPI/#dfn-file

30

www.w3.org/TR/FileAPI/#dfn-Blob

31

www.w3.org/TR/FileAPI/#dfn-filereader

32

www.w3.org/TR/FileAPI/#dfn-Blob

33

http://fineuploader.com

34

https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy

35

http://security.stackexchange.com/a/23439

36

www.w3.org/TR/cors/

37

https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS

38

http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx

十、浏览器事件

对页面上的变化做出反应是现代 web 应用开发的一个重要部分。虽然这在某种程度上可以通过锚链接和表单提交按钮来实现,但是事件系统的引入使得编写适应用户输入的代码成为可能,而无需重新加载或更改当前页面。您可能还会看到这是如何补充发送 AJAX 请求的能力的。自从 Internet Explorer 版本 4(引入了微软的 Trident 布局引擎)和 Netscape Navigator 版本 2(以及第一个 JavaScript 实现)以来,为了在浏览器中提供更动态的用户体验,可以侦听 DOM 事件。这个模型的第一次实现非常有限。但是随着浏览器通过标准化的发展,事件系统也在发展。今天,我们有了一个相当优雅的本地 API 来监听和触发标准化的 DOM 事件和定制事件。在本章中,你会看到如何在现代浏览器中利用事件来完成各种任务。

虽然现代 Web 提供了一套强大而直观的方法来处理事件,但情况并非总是如此。历史上这一不幸的时刻,再加上 Internet Explorer 和其他浏览器在事件 API 方面缺乏对等性,使得 jQuery 成为处理所有流行浏览器中的事件的绝佳库。除了规范化事件之外,jQuery 还通过支持事件委托和一次性事件监听器绑定来提供额外的帮助。不再需要 jQuery 来规范化浏览器之间的重大事件 API 实现差异,但它仍然通过额外有用的功能提供了额外的便利。在本章中,我还将向您展示如何简单地依靠本地 web API 来镜像最重要的与事件相关的 jQuery 特性。

在接下来的几节中,您将学习如何创建、触发和监听标准浏览器事件和自定义事件。jQuery 方法将与原生的 web API 方法进行比较,您会觉得自己处理事件比使用 jQuery 更舒服。至少,即使你决定在你的项目中继续使用 jQuery 进行事件处理,本章也会让你对浏览器提供的事件系统有一个全面的了解。在这一章中,我将主要关注现代浏览器,但是最后的一节将包括一些有用的信息,这些信息将帮助您理解 events API 在古代浏览器中是如何工作的,以及它与现代系统有何不同。

事件是如何工作的?

在我介绍使用事件解决常见问题之前,我认为谨慎的做法是首先概述浏览器事件是如何“工作”的这个事件系统遵循基本的发布-订阅模式,但是浏览器事件远不止于此。首先,浏览器事件有多种分类。两个最广泛的类别被称为“习俗”和“本土”(我)。

“本地”浏览器事件可以进一步分配给子组,例如鼠标事件、键盘事件和触摸事件(仅举几个例子)。除了事件类型之外,浏览器事件还有一个独特的属性:将它们分发给注册的侦听器的过程。事实上,浏览器事件可以通过两种不同的方式在页面上传播。个人听众也可以以不同的方式影响这些事件。除了事件类型之外,我还将在本节中解释事件传播。完成第一部分后,您将对浏览器事件的核心概念有一个很好的理解。这将允许您有效地遵循概述事件 API 的更具体用途的后续部分。

事件类型:自定义和本机

为了开始我对浏览器事件的全面介绍,我现在将向您介绍所有事件都适合的两个高级类别:“自定义”和“本地”本地事件是在官方 web 规范中定义的事件,例如由 WHATWG 或 W3C 维护的事件。在 W3C 维护的 DOM Level 3 UI 事件规范 1 中可以找到大多数事件的列表。请注意,这不是一个详尽的列表;它只包含今天可用事件的子集。一些本地事件包括“click”,当 DOM 元素通过定点设备或键盘激活时,浏览器触发的鼠标事件。另一个常见的事件是“load”,当一个<img>、文档、window<iframe>(以及其他)成功加载时,就会触发这个事件。还有很多其他的本地活动。在 Mozilla Developer Network events 页面上可以看到一个很好的资源,它提供了所有当前可用的本地 DOM 事件的列表。 2

如您所料,自定义事件是专为特定应用或库创建的非标准事件。它们可以按需创建,以支持基于事件的动态工作流。例如,考虑一个文件上传库,它希望在文件上传开始时触发一个事件,然后在文件上传完成时触发另一个事件。就在上传开始之后(或者可能就在之前),库可能想要触发一个“uploadStart”事件,然后在文件成功上传到服务器上之后触发“uploadComplete”事件。如果文件上传过早结束,它甚至会触发“uploadError”事件。确实没有任何本地事件提供这种情况所需的语义,所以自定义事件是最好的解决方案。幸运的是,DOM API 确实提供了触发定制事件的方法。尽管在一些没有聚合填充的浏览器中触发自定义事件有点不优雅,但这种情况正在改变。稍后会详细介绍。

不使用 jQuery 创建和触发的自定义事件可以使用 jQuery 的事件 API 进行观察和处理。然而,在处理定制事件时,jQuery 有一个有趣的限制,这是在 jQuery 文档中找不到的。如果不使用 jQuery 的事件 API,就无法观察和处理用 jQuery 的事件 API 创建和触发的自定义事件。换句话说,由 jQuery 创建的定制事件完全是专有的和非标准的。这样做的原因其实很简单。虽然 jQuery 可以为本地 DOM 事件触发特定元素上的所有事件处理程序,但是对于自定义事件来说却不可能,也不可能查询特定的 HTML 元素以获取其附加的事件侦听器。因此,jQuery 自定义事件只能由 jQuery 自定义事件处理程序使用。

有一个对象将自定义事件和本地事件联系在一起。这个中心对象是由 W3C 制定的规范中描述的the Event对象、 3 。每一个 DOM 事件,无论是自定义的还是本地的,都由一个Event对象表示,这个对象本身有许多用于识别和控制事件的属性和方法。例如,type属性使自定义或本地事件的名称可用。一个“点击”事件在其对应的Event对象上有一个“点击”的type。同一个Event对象实例还将包含一个stopPropagation()方法,调用该方法可以防止点击事件被进一步传播到页面上的其他侦听器。

事件传播:冒泡与捕获

在 Web 的早期,Netscape 提供了一种在整个 DOM 中分散事件的方法——事件捕获,而 Internet Explorer 提供了一种相反的过程——事件冒泡。在标准化之前,浏览器本质上是在实现特性时做出自己专有的选择,这就是导致这两种不同方法的原因。这一切在 2000 年 W3C 起草 DOM Level 2 Events 规范时都改变了。这个文档描述了一个包括事件捕获和冒泡的事件模型。所有遵循该规范的浏览器都遵循在 DOM 中分发事件的过程。目前,所有现代浏览器都实现了 DOM Level 2 事件。仅支持事件冒泡的古老浏览器将在本章末尾介绍。

在所有现代浏览器中,根据 DOM Level 2 Events 规范,当一个 DOM 事件被创建时,捕获阶段就开始了。假设事件在被触发后的某个时刻没有被取消,它从window开始,接着是document,向下传播,以触发事件的元素结束。捕获阶段完成后,冒泡阶段开始。从这个目标元素开始,事件在 DOM 中“冒泡”,触及每个祖先,直到事件被取消或再次触及window

如果关于事件进展的描述仍然有点混乱,让我用一个简单的演示来解释一下。考虑下面的 HTML 文档:

 1  <!DOCTYPE html>

 2  <html>
 3  <head>
 4    <title>event propagation demo</title>
 5  </head>
 6  <body>
 7    <section>
 8      <h1>nested divs</h1>
 9      <div>one
10        <div>child of one
11          <div>child of child of one</div>
12        </div>
13      </div>
14    </section>
15  </body>
16  </html>

超越 jQuery(三)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

假设点击了<div>child of child of one</div>元素。单击事件在 DOM 中采用以下路径:

捕获阶段
  1. window
  2. document
  3. <html>
  4. <body>
  5. <section>
  6. <div>one
  7. <div>child of one
  8. <div>child of child of one
起泡阶段
  1. <div>child of child of one
  2. <div>child of one
  3. <div>one
  4. <section>
  5. <body>
  6. <html>
  7. document
  8. window

那么,什么时候关注捕获阶段而不是冒泡阶段是合适的,或者反之亦然?最常见的选择是在冒泡阶段拦截事件。压倒性地关注冒泡阶段的一个原因是由于历史原因。在 Internet Explorer 9 之前,这是唯一可用的阶段。随着标准化和现代浏览器的出现,这不再是一个障碍。除了缺乏对古代浏览器中捕获的支持,jQuery 也缺乏对这一阶段的支持。这也许是它不是特别受欢迎的另一个原因,冒泡是默认的选择。但不可否认,事件冒泡的概念比捕捉更直观。设想一个在 DOM 树中向上移动的事件,从创建该事件的元素开始,似乎比以创建它的事件结束的事件更明智一些。事实上,在描述浏览器事件模型时很少讨论捕获。在使用 web API 监听事件时,将处理程序附加到事件冒泡阶段也是默认行为。

尽管事件冒泡阶段通常是首选,但在某些情况下,捕获是更好的(或唯一的)选择。利用捕获阶段似乎有性能优势。因为事件捕获发生在冒泡之前,这似乎是有意义的。Basecamp,一个基于网络的项目管理应用,已经利用事件捕获来提高他们项目的性能,例如 5 。使用捕获阶段的另一个原因:事件委托给“聚焦” 6 和“模糊” 7 事件。虽然这些事件不会冒泡,但是通过挂钩到捕获阶段,处理程序委托是可能的。我将在本章后面详细介绍事件委托。

捕获还可以用于对冒泡阶段中被取消的事件做出反应。取消事件的原因有很多,也就是说,为了防止它到达任何后续的事件处理程序。事件几乎总是在冒泡阶段被取消。我将在本章稍后讨论这个问题,但是现在想象一下在你的 web 应用中一个第三方库取消的点击事件。如果您仍然需要在另一个处理程序中访问该事件,您可以在捕获阶段在元素上注册一个处理程序。

jQuery 不幸地选择了人工冒泡事件。换句话说,当通过库触发事件时,它会计算预期的冒泡路径,并在该路径中的每个元素上触发处理程序。jQuery 没有利用浏览器提供的对冒泡和捕获的本地支持。这无疑增加了库的复杂性和膨胀,并可能带来性能后果。

创建和触发 DOM 事件

为了演示在有和没有 jQuery 的情况下触发 DOM 事件,让我们使用下面的 HTML 片段:

 1  <div>
 2    <button type="button">do something</button>
 3  </div>
 4
 5  <form method="POST" action="/user">
 6    <label>Enter user name:
 7      <input name="user">
 8    </label>
 9    <button type="submit">submit</button>
10  </form>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

用 jQuery 触发 DOM 事件

jQuery 的 events API 包含两个方法,允许创建 DOM 事件并在整个 DOM 中传播:triggertriggerHandler。最常用的方法是trigger。它允许创建一个事件,并通过冒泡传播到原始元素的所有祖先。请记住,jQuery 人为地冒泡所有事件,它不支持事件捕获。triggerHandler方法与trigger的不同之处在于,它只在被调用的元素上执行事件处理程序;事件不会冒泡到祖先元素。jQuery 的triggerHandler在其他一些方面也不同于trigger,但是我提供的定义对于本节来说已经足够了。

在接下来的几个清单中,我使用 jQuery 的trigger方法来:

  1. 以两种方式提交表单。
  2. 聚焦文本输入。
  3. 将焦点从输入元素上移开。
 1  // submits the form

 2  $('FORM').trigger('submit');
 3
 4  // submits the form by clicking the button

 5  $('BUTTON[type="submit"]').trigger('click');
 6
 7  // focuses the text input

 8  $('INPUT').trigger('focus');
 9
10  // removes focus from the text input

11  $('INPUT').trigger('blur');
Listing 10-1.Triggering DOM Events: jQuery

超越 jQuery(三)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

公平地说,还有第二种方法可以用 jQuery 触发相同的事件,即使用库的 API 中定义的这些事件的别名。

 1  // submits the form

 2  $('FORM').submit();
 3
 4  // submits the form by clicking the button

 5  $('BUTTON[type="submit"]').click();
 6
 7  // focuses the text input

 8  $('INPUT').focus();
 9
10  // removes focus from the text input

11  $('INPUT').blur();
Listing 10-2.Another Way of Triggering DOM Events: jQuery

超越 jQuery(三)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

如果我们想点击出现在表单前的按钮,但不想触发任何附加到祖先元素的点击处理程序,该怎么办?假设父节点<div>包含一个我们不想在这个实例中触发的点击处理程序。有了 jQuery,我们可以使用triggerHandler()来完成这项工作,如清单 10-3 所示。

1  // clicks the first button - the click event does not bubble

2  $('BUTTON[type="button"]').triggerHandler('click');

Listing 10-3.Triggering DOM Events without Bubbling: jQuery

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Web API DOM 事件

有两三种方法可以在不使用 jQuery 的情况下触发刚才演示的相同事件。(有时候)有选择是好的。无论如何,使用 web API 触发上述事件的最简单方法是调用目标元素上相应的本地方法。清单 10-4 显示了与清单 10-2 非常相似的代码。

 1  // submits the form

 2  document.querySelector('FORM').submit();
 3
 4  // submits the form by clicking the button

 5  document.querySelector('BUTTON[type="submit"]').click();
 6
 7  // focuses the text input

 8  document.querySelector('INPUT').focus();
 9
10  // removes focus from the text input

11  document.querySelector('INPUT').blur();
Listing 10-4.Triggering DOM Events: Web API, All Modern Browsers, and Internet Explorer 8

超越 jQuery(三)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

由于使用了querySelector(),前面的代码只限于 IE8 和更新的版本,但是考虑到当前浏览器支持的年份和状态,这已经足够了。从HTMLElement继承而来的所有 DOM 元素对象都可以使用click()focus()blur()方法。8submit()方法只对<form>元素可用,因为它是在HTMLFormElement接口上定义的。 9 用这些方法触发“点击”和“提交”事件时,它们会冒泡。根据 W3C 规范, 10 “模糊”和“聚焦”事件不会冒泡,但它们可用于编码为利用捕获阶段的事件处理程序。

前面的事件也可以通过使用document上可用的Event()构造函数或the createEvent()方法来创建。 11 除了任何版本的 Internet Explorer 之外,所有现代浏览器都支持前者。在下一个代码演示中,我将向您展示如何以编程方式确定是否支持Event构造函数,然后返回到触发事件的备选路径。也许您想知道为什么您甚至需要使用不同于这里概述的简单方法来触发事件。如果你想以某种方式改变事件的默认行为,需要构造一个Event对象。例如,为了模仿 jQuery 的triggerHandler()方法的行为并防止事件冒泡,我们必须在构造“click”事件时将特定的配置属性传递给它。您将在清单 10-5 的末尾看到这一点,它展示了触发事件的第二种方法。

 1  var clickEvent;
 2
 3  // If the `Event` constructor function is not supported,

 4  // fall back to `createEvent` method.

 5  if (typeof Event === 'function') {
 6    clickEvent = new Event('click', {bubbles: false});
 7  }
 8  else {
 9      clickEvent = document.createEvent('Event');
10      clickEvent.initEvent('click', false, true);
11  }
12
13  document.querySelector('BUTTON[type="button"]')
14  .dispatchEvent(clickEvent);
Listing 10-5.
Triggering DOM

Events

without Bubbling: Web API, All Modern Browsers

超越 jQuery(三)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

在清单中,当我们必须返回到initEvent()时,第二个参数是bubbles,如果我们不想让事件冒泡,必须将它设置为false。第三个参数设置为 true,表示该事件确实是可取消的。换句话说,可以使用 event 对象上的 preventDefault()方法来阻止与该事件相关联的任何默认浏览器操作。我将在本章的后面解释取消事件。Event构造函数提供了一种更优雅的方式,使用一组对象属性来设置这个选项和其他选项。一旦 Internet Explorer 11 寿终正寝,我们可以专注于Event的构造者,忘记initEvent()的存在。但在此之前,如果您必须用特殊的配置选项构造事件,前面的检查将允许您选择正确的路径。

创建和触发自定义事件

请记住,自定义事件是那些没有作为公认的 web 规范的一部分进行标准化的事件,例如由 W3C 和 WHATWG 维护的事件。让我们想象一个场景,我们正在编写一个第三方库,处理从图片库中添加和删除项目。当我们的库被集成到一个更大的应用中时,我们需要提供一种简单的方法来通知任何侦听器我们的库添加或删除了一个项目。在这种情况下,我们的库将包装图片库,这样我们只需通过触发祖先元素可以观察到的事件,就可以发出删除或添加的信号。这里没有合适的标准化 DOM 事件,所以我们需要创建自己的事件,一个自定义事件。与删除图像相关联的自定义事件将被恰当地命名为“image-removed ”,并且需要包含被删除图像的 ID。

jQuery 自定义事件

让我们首先使用 jQuery 触发这个事件。我们假设我们已经有了一个由库控制的元素的句柄。我们的事件将由以下特定元素触发:

1  // Triggers a custom "image-removed" element,

2  // which bubbles up to ancestor elements.

3  $libraryElement.trigger('image-removed', {id: 1});

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这看起来与用于触发原生 DOM 事件的代码相同,而且确实如此。jQuery 有一个简单、优雅和一致的 API 来触发所有类型的事件。但是这里有一个问题——在我们的 jQuery 库之外监听这个事件的代码也必须使用 jQuery 来观察这个事件。这是 jQuery 自定义事件系统的一个限制。如果我们的库的用户正在使用其他库,或者即使用户不想在这个库之外使用 jQuery,这也没有什么关系。也许我们的用户不清楚必须使用 jQuery 来监听这个事件。他们被迫只依靠 jQuery 和 jQuery 来接受来自我们库的消息。

使用 Web API 触发自定义事件

用 web API 触发定制事件就像触发本地 DOM 事件一样。这里的区别在于创建自定义事件,尽管过程和 API 仍然非常相似。

1  var event = new CustomEvent('image-removed', {
2    bubbles: true,
3    detail: {id: 1}
4  });
5  libraryElement.dispatchEvent(event);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在这里,我们可以轻松地创建自定义事件,触发它,并确保它冒泡到祖先元素。因此,我们的“图像移除”事件在我们的图书馆之外是可观察到的。我们还在事件有效负载的detail属性中传递了图像 ID。稍后将详细介绍如何访问这些数据。但这里有一个问题:这在任何版本的 Internet Explorer 中都不适用。不幸的是,正如我在上一节提到的,Explorer 不支持Event构造函数。因此,我们必须退回到以下跨浏览器支持方法:

1  var event = document.createEvent('CustomEvent');
2  event.initCustomEvent('image-removed', false, true, {id: 1});
3  libraryElement.dispatchEvent(event);

  • 1
  • 2
  • 3
  • 4

我们必须创建一个“客户事件”,而不是创建一个“事件”,正如我们在前面的部分中尝试在 Internet Explorer 中触发本机 DOM 事件时所做的那样这公开了一个在CustomEvent接口上定义的initCustomEvent()方法。这个特殊的方法允许我们将定制数据和这个事件一起传递,比如我们的图像 ID。

前面的代码目前(截至 2016 年年中)在所有现代浏览器中都能工作,但是一旦CustomEvent构造函数在所有浏览器中得到支持,这种情况可能会改变。它可能会从任何未来的浏览器版本中删除。为了使我们的代码经得起未来的考验,并且仍然确保它在 Internet Explorer 中工作,我们需要检查CustomEvent构造函数的存在,就像我们在上一节中对Event构造函数所做的那样:

 1  var event;
 2
 3  // If the `CustomEvent` constructor function is not supported,

 4  // fall back to `createEvent` method.

 5  if (typeof CustomEvent === 'function') {
 6    event = new CustomEvent('image-removed', {
 7      bubbles: true,
 8      detail: {id: 1}
 9    });
10  }
11  else {
12      event = document.createEvent('CustomEvent');
13      event.initCustomEvent('image-removed', false, true, {
14        id: 1
15      });
16  }
17
18  libraryElement.dispatchEvent(event);

超越 jQuery(三)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

在 Internet Explorer 逐渐过时,微软 Edge 取而代之之后,你可以独占使用CustomEvent构造函数,前面的代码将不再需要。

监听(和不监听)事件通知

触发事件是在 DOM 中传递消息的一个重要部分,但是这些事件通过相应的侦听器提供了更多的价值。在这一节中,我将介绍如何处理 DOM 和定制事件。您可能已经熟悉了用 jQuery 注册事件观察器的过程,但是我将首先演示这是如何完成的,这样当完全依赖于 web API 时,区别就很明显了。

当用户改变页面视图时,resize 事件处理程序对于调整复杂的应用可能很重要。当用户调整浏览器大小时,这个“resize”事件在window上被触发,它将为我们提供一个演示注册和注销事件监听器的好方法。

jQuery 事件处理程序

jQuery 的on API 方法提供了观察元素上触发的 DOM 和定制事件所需的一切:

1  $(window).on('resize', function() {
2    // react to new window size

3  });

  • 1
  • 2
  • 3
  • 4
  • 5

如果在将来的某个时候,我们不再关心窗口的大小,我们可以使用 jQuery 的适当命名的off处理程序来删除这个处理程序,但是这比添加一个新的侦听器要简单得多。我们有两个选择:

  1. 移除所有调整事件监听器(简单)。
  2. 只移除我们的 resize 事件监听器(有点难)。

让我们先来看看选项 1:

1  // remove all resize listeners - usually a bad idea

2  $(window).off('resize');

  • 1
  • 2
  • 3
  • 4

第一个选项非常简单,但是我们冒着给页面上仍然依赖窗口大小调整事件的其他代码带来问题的风险。换句话说,选项 1 通常是一个糟糕的选择。这就给我们留下了选项 2,它要求我们存储一个对处理函数的引用,并将其提供给 jQuery,这样它就可以只解除对侦听器的绑定。因此,我们需要重写之前的事件侦听器调用,以便以后可以轻松地注销我们的侦听器:

1  var resizeHandler = function() {
2      // react to new window size

3  };
4
5  $(window).on('resize', resizeHandler);
6
7  // ...later

8  // remove only our resize handler

9  $(window).off('resize', resizeHandler);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

将一个事件侦听器绑定到一个特定的元素,然后在以后不再需要它时移除它,这通常就足够了,但是我们可能会遇到这样的情况,一个事件处理程序只需要一次。在第一次执行之后,它可以而且应该被删除。也许我们有一个元素,一旦被点击,就会以这样一种方式改变状态,以至于后续的点击是不谨慎的。jQuery 为这种情况提供了一个one API 方法:

1  $(someElement).one('click', function() {
2    // handle click event

3  });

  • 1
  • 2
  • 3
  • 4
  • 5

执行附加的处理函数后,将不再观察到 click 事件。

使用 Web API 观察事件

自古以来(几乎如此),有两种简单的方法可以将事件处理程序附加到特定的 DOM 元素,这两种方法都可以被认为是“内联的”第一种,如清单 10-6 所示,包括将事件处理函数作为元素的属性值包含在文档标记中:

1  <button onclick="handleButtonClick()">click me</button>
Listing 10-6.

Inline Event Handler

:

Web API

, All Browsers

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这种方法有几个问题。首先,它需要一个全局handleButtonClick()函数。如果您有许多按钮或其他元素需要特定的点击处理函数,您将得到一个混乱的全局名称空间。应该始终限制全局变量和函数,以防止冲突和对内部逻辑的不受控制的访问。因此,这种类型的内联事件处理程序是朝着错误方向迈出的一步。

第二个不好的原因是:它需要在同一个文件中混合 JavaScript 和 HTML。一般来说,这是不鼓励的,因为它违背了关注点分离的原则。也就是说,内容属于 HTML 文件,行为属于 JavaScript 文件。这隔离了代码,从而降低了更改的风险,并且缓存得到了改进,因为对 JavaScript 的更改不会使标记文件无效,反之亦然。

注册同一个 click 事件的另一种方法需要将一个处理函数附加到元素的相应事件属性:

1  buttonEl.onclick = function() {
2    // handle button click

3  };

  • 1
  • 2
  • 3
  • 4
  • 5

虽然这种方法比基于 HTML 的事件处理程序稍好,但由于我们没有被强制绑定到全局函数,所以它仍然不是最佳的解决方案。不能为同一元素上的同一事件指定基于属性的事件处理程序和元素属性处理程序。最后指定的处理程序将有效地移除给定事件类型的元素上的任何其他内联事件处理程序。事实上,对于给定元素上的给定事件,只能指定一个 total inline 事件处理程序。对于现代 web 应用来说,这可能是一个大问题,因为在同一个页面上存在多个不协调的模块是很常见的。也许不止一个模块需要将同一类型的事件处理程序附加到同一元素上。对于内联事件处理程序,这是不可能的。

从 Internet Explorer 9 开始,EventTarget接口上就有了一个addEventListener()方法。所有的Element对象都实现了这个接口,就像Window(在其他 DOM 对象中)一样。EventTarget接口首先出现在 W3C DOM Level 2 Events 规范中, 12addEventListener()方法是这个接口初始版本的一部分。使用这种方法可以注册自定义和 DOM 事件,语法与 jQuery 的on()方法非常相似。继续按钮示例:

1  buttonEl.addEventListener('click', function() {
2    // handle button click

3  });

  • 1
  • 2
  • 3
  • 4
  • 5

这个解决方案没有任何困扰内联事件处理程序的问题。不需要绑定到全局函数。您可以将任意数量的不同点击处理程序附加到这个按钮元素上。处理程序是纯 JavaScript 附带的,所以它可能只存在于 JavaScript 文件中。在上一节中,我提醒过您如何用 jQuery 将“resize”事件绑定到window。使用现代 web API 的相同处理程序如下所示:

1  window.addEventListener('resize', function() {
2    // react to new window size

3  });

  • 1
  • 2
  • 3
  • 4
  • 5

这看起来很像我们用 jQuery 绑定到同一个事件的例子,除了一个更长的事件绑定方法(on()addEventListener())。您可能很高兴知道,如果我们在某个时候需要解绑处理程序,有一个适当命名的 web API 方法可以解绑我们的处理程序。EventTarget接口还定义了一个removeEventListener()方法。removeEventListener()方法与 jQuery 的off有一个显著的不同:没有办法从特定元素中移除给定类型的所有事件侦听器。也许这是一件好事。因此,为了删除我们的window“resize”处理程序,我们必须像这样构造我们的代码:

1  var resizeHandler = function() {
2      // react to new window size

3  };
4
5  window.addEventListener('resize', resizeHandler);
6
7  // ...later

8  // remove only our resize handler

9  window.removeEventListener('resize', resizeHandler);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

还记得我们用 jQuery 的one API 方法创建的一次性点击处理程序吗?web API 中存在类似的东西吗?好消息:是的!坏消息:这是一个相当新的添加(添加到 WHATWG 的 DOM live standard(https://DOM . spec . WHATWG . org/# interface-event target)中)。截至 2016 年年中,只有 Firefox 提供支持,但这是一个令人兴奋的功能:

1  someElement.addEventListener('click', function(event) {
2     // handle click event

3  }, { once: true });

  • 1
  • 2
  • 3
  • 4
  • 5

为了获得更好的跨浏览器支持,特别是因为没有任何优雅的方式来以编程方式确定对侦听器选项的支持,请改用以下方式:

1  var clickHandler = function() {
2    // handle click event

3    // ...then unregister handler

4    someElement.removeEventListener('click', clickHandler);
5  };
6  someElement.addEventListener('click', clickHandler);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在附加的 handler 函数被执行后,click 事件将不再被观察到,就像 jQuery 的one()方法一样。有更好的方法来解决这个问题,但是这个解决方案只利用了你在本书中学到的知识。在完成本书之后,尤其是在阅读了 JavaScript 工具之后,您可能会发现一种更优雅的方法。

控制事件传播

在前几节中,我已经向您展示了如何触发和观察事件,但是有时您需要做的不仅仅是创建或监听事件。有时,您需要影响冒泡/捕获阶段,或者甚至将数据附加到事件上,以便让后续的侦听器可以使用它。

作为一个虚构的例子(但在一个由不了解 web 的项目经理操纵的非常扭曲的 web 应用中可能是现实的),假设您被要求阻止用户选择整个页面上的任何文本或图像。你怎么能做到这一点?也许通过某种方式干扰一些鼠标事件。但是哪一个事件,如何发生?也许要集中精力的事件是“点击”。如果这是你的第一个猜测,你很接近,但不太正确。

根据 W3C DOM Level 3 Events 规范,“mousedown”事件 13 启动拖动或文本选择操作作为其默认动作。因此,我们必须防止“鼠标按下”事件的默认动作。我们可以通过简单地在window上注册一个“mousedown”事件监听器,并调用Event对象上的preventDefault()方法来防止使用 jQuery 或纯 web API 在整个页面上选择/拖动文本和图像,一旦我们的处理程序被执行,这个对象就会被传递给我们的处理程序:

1  $(window).on('mousedown', function(event) {
2    event.preventDefault();
3  });
4
5  // ...or...

6  $(window).mousedown(function(event) {
7    event.preventDefault();
8  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

防止默认事件操作- web API -现代浏览器

1  window.addEventListener('mousedown', function(event) {
2    event.preventDefault();
3  });

  • 1
  • 2
  • 3
  • 4

jQuery 方法与仅依赖于本地 web API 的方法几乎相同。无论哪种方式,我们都满足了要求:不能在页面上选择或拖动文本或图像。

这可能是开始讨论Event对象的好时机。之前,当 jQuery 或浏览器执行一个Event实例时,它被传递给我们的事件处理函数。W3C DOM4 规范中定义了本地Event接口。 14 当创建自定义或本地 DOM 事件时,浏览器会创建一个Event实例,并在捕获和冒泡阶段将其传递给每个注册的侦听器。

传递给每个侦听器的事件对象包含许多属性,例如描述相关事件的属性,例如:

  • 事件类型(单击、鼠标按下、焦点)
  • 创建事件的元素
  • 当前事件阶段(捕获或冒泡)
  • 处于冒泡或捕获阶段的当前元素

还有其他类似的属性,但该列表代表了更值得注意的属性的一个很好的样本。除了描述事件的属性之外,还有许多允许控制事件的方法。一个这样的方法是preventDefault(),正如我刚才演示的。但是还有其他的,我很快就会谈到。

jQuery 有自己版本的Event接口(当然)。 15 根据 jQuery 的文档,他们的事件对象“根据 W3C 标准对事件对象进行规范化”这对古代的浏览器可能很有用。但是对于现代浏览器来说,就不是这样了。在很大程度上,除了一些属性和方法之外,这两个接口非常相似。

接下来的两个示例演示了如何防止特定事件到达其他已注册的事件处理程序。为了防止单击事件到达后续 DOM 节点上的任何事件处理程序,只需在传递的Event对象上调用stopPropagation()。该方法存在于 jQuery Event接口和标准化 web API Event接口中:

1  $someElement.click(function(event) {
2      event.stopPropagation();
3  });
4
5  // ...or...

6
7  $someElement.on('click', function(event) {
8      event.stopPropagation();
9  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

使用 jQuery,您可以用stopPropagation()阻止事件冒泡,但是您不能在捕获阶段阻止事件,除非您遵从 web API,如清单 10-7 所示。

1  // stop propagation during capturing phase

2  someElement.addEventListener('click', function(event) {
3      event.stopPropagation();
4  }, true);
5
6  // stop propagation during bubbling phase

7  someElement.addEventListener('click', function(event) {
8      event.stopPropagation();
9  });
Listing 10-7.Stop a Click Event from Propagating: Web API, Modern Browsers

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

web API 能够在捕获或冒泡阶段停止事件传播。但是stopPropagation()不会阻止事件到达同一元素上的任何后续侦听器。对于这个任务,stopImmediatePropagation()事件方法是可用的,它阻止事件到达任何进一步的处理程序,不管它们是在当前 DOM 节点还是后续节点上注册的。同样,jQuery(清单 10-8 )和 web API(清单 10-9 )共享相同的方法名,但是 jQuery 一如既往地被限制在冒泡阶段。

1  $someElement.on('click', function(event) {
2      event.stopImmediatePropagation();
3  });
Listing 10-8.
Stop a Click Event from Reaching Any Other Handlers:

jQuery

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
1  someElement.addEventListener('click', function(event) {
2      event.stopImmediatePropagation();
3  });
Listing 10-9.Stop a Click Event from Reaching Any Other Handlers: Web API, Modern Browsers

  • 1
  • 2
  • 3
  • 4
  • 5

请注意,jQuery 和 web API 都提供了一种快捷方式来阻止事件的默认操作,并阻止事件到达后续 DOM 节点上的处理程序。通过在事件处理程序中返回false,可以有效地调用event.preventDefault()event.stopPropagation()

将数据传递给事件处理程序

有时,与事件相关的标准数据是不够的。事件处理程序可能需要关于它们正在处理的事件的更具体的信息。还记得我前面详述的“uploadError”自定义事件吗?这是从嵌入在页面上的库中触发的,而“uploadError”事件的存在是为了向库外的侦听器提供有关文件上传失败的信息。假设我们使用的文件上传库附加到一个容器元素,我们的应用包装这个容器元素并注册一个“uploadError”事件处理程序。当一个特定的文件上传失败时,这个事件被触发,我们的处理程序向用户显示一条信息性消息。为了定制此消息,我们需要失败文件的名称。上传库可以将文件名传递给我们在Event对象中的处理程序。

首先,让我们回顾一下如何使用 jQuery 将数据传递给事件处理程序:

1  // send the failed filename w/ an error event

2  $uploaderElement.trigger('uploadError', {
3    filename: 'picture.jpeg'
4  });
5
6  // ...and this is a listener for the event

7  $uploaderParent.on('uploadError', function(event, data) {
8    showAlert('Failed to upload ' + data.filename);
9  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

jQuery 通过传递给处理程序的第二个参数使传递给trigger()函数的对象对任何事件监听器都可用。这样,我们可以访问传递的对象的任何属性。

为了用 web API 达到同样的结果,我们将利用CustomElement及其内置的处理数据的能力:

 1  // send the failed filename w/ an error event

 2  var event = new CustomEvent('uploadError', {
 3    bubbles: true,
 4    detail: {filename: 'picture.jpeg'}
 5  });
 6  uploaderElement.dispatchEvent(event);
 7
 8  // ...and this is a listener for the event

 9  uploaderParent.addEventListener('uploadError', function(event) {
10    showAlert('Failed to upload ' + event.detail.filename);
11  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

这不像 jQuery 解决方案那样简洁,但至少在除 IE 之外的所有现代浏览器中是有效的。对于更跨浏览器的解决方案,您可以依赖旧的自定义事件 API,如前所述。在下面的演示中,我将只关注旧的 API,但是我鼓励您阅读前面提到的一种更经得起未来考验的创建CustomEvent实例的方法:

 1  // send the failed filename w/ an error event

 2  var event = document.createEvent('CustomEvent');
 3  event.initCustomEvent('uploadError', true, true, {
 4    filename: 'picture.jpeg'
 5  });
 6  uploaderElement.dispatchEvent(event);
 7
 8  // ...and this is a listener for the event

 9  uploaderParent.addEventListener('uploadError', function(event) {
10    showAlert('Failed to upload ' + event.detail.filename);
11  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

在这两种情况下,附加到CustomEvent的数据都可以通过标准化的detail属性提供给我们的侦听器。 16 通过CustomEvent构造函数,在创建新实例时传递的对象的detail属性上提供该数据。这个对象上的detail属性与我们的侦听器可用的CustomEvent对象上的detail属性相匹配,这很好而且一致。当使用旧的事件创建 API 设置我们的“uploadError”事件时,我们的侦听器仍然可以访问这个相同的detail属性,但是它被隐藏在传递给initCustomEvent()的大量参数中。根据我的经验,任何超过两个参数的东西都是令人困惑和不直观的。这并不是一个不常见的偏好,这可以解释为什么更现代的CustomEvent构造函数只要求两个参数,第二个参数是一个提供所有定制配置和数据的对象。

事件委托:功能强大且未被充分利用

有很多次,我都在回避一个非常重要的话题:事件委派。简而言之,事件委托涉及将单个事件处理程序附加到顶级元素,目的是处理从后代元素冒泡出来的事件。此顶级元素中的事件处理程序逻辑可能包含基于事件目标元素(首先接收事件的元素)而不同的代码路径。但是为什么要这样做呢?为什么不直接将特定的事件处理程序附加到适当的元素上呢?

已经讨论得令人生厌的一个原因是委托事件处理程序的潜在性能优势。通过绑定单个事件处理程序来节省 CPU 周期,该处理程序负责监视许多后代元素上的事件,而不是查询每个元素并将专用的处理程序函数直接附加到每个元素。这个理论很有道理,当然也是真的。但是,从 CPU 周期的角度来看,这里真正节省了多少时间呢?我想这个问题的答案是:视情况而定。首先,这取决于您打算监控多少个元素。

很难想象一个常见的场景,其中委托事件处理既是可取的,又对性能有害。这种做法已经流行起来,部分是因为预期的性能原因,也是因为能够将事件处理代码集中到一个特定的根元素,而不是分散到整个 DOM。以 React 为例。React 是一个 JavaScript 库,专门关注典型模型视图控制器 web 应用的“视图”部分。在事件处理方面,React 实现了一个有趣的抽象: 17

  • React 实际上并不将事件处理程序附加到节点本身。当 React 启动时,它开始使用单个事件监听器监听顶层的所有事件。

换句话说,附加到带有 React 的元素的所有事件处理程序都被提升为公共父元素上的单个委托事件处理程序。也许您仍然看不到适合委托事件处理程序的实例。在本节的其余部分,我将重点关注一个简单的例子,它展示了事件委托的强大功能。

假设您有一个充满列表项的列表,每个列表项都有一个从列表中删除项的按钮。您可以为每个列表项的按钮附加一个单击处理程序。但是,循环遍历所有按钮元素并为每个元素附加完全相同的点击处理函数,这难道不是一种错误的方法吗?你可能会认为这不是不合理的,甚至很容易做到。但是,如果在初始页面加载之后,新的项目可以动态地添加到这个列表中呢?现在,在添加新的列表项后,给每个新的列表项附加一个新的事件处理程序变得不那么吸引人了。

这里的最佳解决方案是使用事件委托。换句话说,将一个单击处理程序附加到列表元素。当单击列表项元素中的任何删除按钮时,事件将冒泡到列表元素。此时,您的一个事件处理程序将被触发,通过检查事件对象,您可以很容易地确定哪个列表项被单击,并通过删除关联的列表项做出适当的响应。在这一节中,我们使用一些文本来使我们的删除按钮更容易访问,以及关闭/删除 Ionicons 网站 18 中的图标来增强我们按钮的外观。

此类列表的 HTML 可能如下所示:

 1  <link href="http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"
 2        rel="stylesheet">
 3  <ul id="cars-list">
 4      <li>Honda
 5        <button>
 6          <span>delete</span>
 7          <span class="ion-close-circled"></span>
 8        </button>
 9      </li>
10      <li>Toyota
11        <button>
12          <span>delete</span>
13          <span class="ion-close-circled"></span>
14        </button>
15      </li>
16      <li>Kia
17        <button>
18          <span>delete</span>
19          <span class="ion-close-circled"></span>
20        </button>
21      </li>
22      <li>Ford
23        <button>
24          <span>delete</span>
25          <span class="ion-close-circled"></span>
26        </button>
27      </li>
28  </ul>

超越 jQuery(三)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

使用 jQuery,我们可以使用click别名将一个点击处理程序附加到<ul>上,并通过检查事件对象来删除适当的汽车列表项:

1  $('#cars-list').on('click', 'button', function() {
2      $(this).closest('li').remove();
3  });

  • 1
  • 2
  • 3
  • 4

但是等等,我们根本不需要检查事件对象!jQuery 通过将事件处理函数的上下文(this)设置为 click 元素目标,提供了一个很好的特性。注意,这个点击事件可能指向“delete”span 元素或“x”图标,这取决于用户选择了这些元素中的哪一个。无论哪种情况,我们只对点击<button>或其子节点感兴趣。jQuery 确保我们的事件处理程序只有在这种情况下才会被调用,此时我们可以使用 jQuery 的closest()方法找到关联的<li>并将其从 DOM 中移除,如清单 10-10 所示。

1  document.querySelector('#cars-list')
2    .addEventListener('click', function(event) {
3      if (event.target.closest('BUTTON')) {
4        var li = event.target.closest('LI');
5        li.parentElement.removeChild(li);
6      }
7    });
Listing 10-10.

Delegated Event Handling

: Web API, All Modern Browsers (with closest Shim)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

上面的纯 web API 解决方案比 jQuery 的 API 要罗嗦一点(好吧,罗嗦很多)。但是它展示了如何在没有 jQuery 的情况下完成一些常见的目标:

  1. 将 click 事件处理程序附加到元素。
  2. 检查一个Event对象,关注仅由我们的<button>元素之一触发的事件。
  3. 移除与点击的删除按钮相关联的<li>

我们在这里利用Element.closest很容易地找到父<li>并确定事件目标的父是否确实是一个<button>,所有这些都不需要明确处理事件目标可能在<button><li>之下的多个层次的事实。由于 Internet Explorer、Microsoft Edge(至少从版本 13 开始)或 iOS Safari 和 Android 的旧版本不支持Element.closest,如果您需要可靠的跨浏览器支持,您将需要使用第四章中演示的垫片。

与 jQuery 相比,这可能看起来有点不雅,这可能确实是真的。但是请记住,这本书的使命不一定是强迫你避开 jQuery 或任何其他库,而是向你展示如何在没有第三方依赖的帮助下自己解决同样的问题。从这些练习和演示中获得的知识将通过提供对 web API 的洞察力来增强您作为 web 开发人员的能力,并允许您在决定您的项目是否将受益于一些外部帮助时做出更好的决策。也许你会选择引入小而集中的填充(比如前面演示的Element.closest polyfill ),而不是依赖 jQuery 这样的大型库。

处理和触发键盘事件

如您所料,键盘事件是当用户按下键盘上的一个键时浏览器触发的本地 DOM 事件。就像所有其他事件一样,键盘事件会经历捕获和冒泡两个阶段。目标元素是按下键时聚焦的元素。你可能想知道为什么我专门为键盘事件开辟了一个特殊的部分。很快您就会看到,键事件与前面讨论的其他 DOM 和定制事件有些不同。另外,键盘事件比其他 DOM 事件更难处理。

这主要是由于最容易被误解的多键盘事件类型,以及用于标识不同浏览器支持的按键的令人困惑的事件属性阵列。不要担心——在本节之后,您将对键盘事件有一个相当好的理解。您将完全理解何时使用三种类型的键盘事件,如何识别按下的键,甚至如何将这些知识用于解决实际问题。

三种类型的键盘事件

键盘上的每个键都不是独立的事件类型。相反,键盘触发的动作被附加到三种可能的键盘特定事件类型之一:

  1. 击键
  2. 好好享受吧
  3. 键击器

在按下的键被释放之前,浏览器触发“keydown”事件。如果该键被按住,它可能会重复触发,并且对于键盘上的任何键(甚至 Shift/Ctrl/Command/other)都会触发。虽然有些键在被按住时不会触发多个“keydown”事件,例如 Shift/Command/Option/Function。jQuery 在其 API 中提供了一个keydown别名来处理这些事件:

1  $(document).keydown(function(event) {
2    // do something with this event

3  });

  • 1
  • 2
  • 3
  • 4
  • 5

同样的事件可以在没有 jQuery 的情况下处理(对于 web API 和现代浏览器):

1  document.addEventListener('keydown', function(event) {
2    // do something with this event

3  });

  • 1
  • 2
  • 3
  • 4
  • 5

在按下位置的一个键(任何键)被释放后,浏览器触发一个“keyup”事件。处理该事件的逻辑与“keydown”相同,只需在刚才给出的代码示例中用“keyup”替换“keydown”即可。事实上,“keypress”事件也是如此,它是最终的键盘事件类型。“keypress”事件与“keydown”非常相似,当按下的键处于按下位置时,也会触发该事件。唯一的区别是“按键”事件只为可打印字符触发。例如,按下“a”、“Enter”或“1”键将触发“按键”事件。相反,“Shift”、“Command”和箭头键不会导致“keypress”事件。

识别按下的按键

假设我们正在构建一个模态对话框。如果我们对话框的用户按下“Esc”键,我们将关闭对话框。为了实现这一点,我们需要做几件事:

  1. 侦听文档上的“keydown”事件。
  2. 确定“keydown”事件是否对应于“Esc”键。
  3. 如果“keydown”事件是按“Esc”的结果,则关闭模式对话框。

如果我们使用 jQuery,我们的代码看起来会像这样:

1  $(document).keydown(function(event) {
2    if (event.which === 27) {
3      // close the dialog...

4    }
5  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

数字 27 对应于 ESC 键的键码。 19 我们可以在没有 jQuery 的情况下使用相同的关键代码(用于 web API 和现代浏览器),方法是查看我们的处理程序接收到的KeyboardEvent上的which属性:

1  document.addEventListener('keydown', function(event) {
2    if (event.which === 27) {
3      // close the dialog...

4    }
5  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

但是 web API 在这方面开始超越 jQuery。W3C 维护的 UI 事件规范 20 定义了一个关于KeyboardEvent : key的新属性。 21 当一个键被按下时,KeyboardEvent(在支持的浏览器中)将包含一个key属性,其值对应于被按下的确切键(对于可打印字符)或描述被按下的键的标准化字符串(对于不可打印字符)。例如,如果按下“a”键,相应的KeyboardEvent上的key属性将包含值“a”。在我们的例子中,Esc 键被表示为字符串“Escape”。这个值以及其他不可打印字符的key值在 DOM Level 3 Events 规范中定义, 22 也由 W3C 维护。如果我们能够使用这个key属性,我们的代码将如清单 10-11 所示。

1  document.addEventListener('keydown', function(event) {
2    if (event.key === 'Escape') {
3      // close the dialog...

4    }
5  });
Listing 10-11.Closing a Modal Dialog on Esc: Web API, All Modern Browsers Except Safari

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

目前,Safari 是唯一不支持这种KeyboardEvent属性的现代浏览器。随着 WebKit 引擎的发展,这种情况可能会改变。与此同时,在 Safari 更新之前,您可能会考虑继续使用which属性。

用 Web API 制作可访问的图像转盘键盘

掌握键盘事件的一个重要好处是:可访问性。可访问的 web 应用是那些具有不同需求的人可以容易地使用的应用。也许最常见的可访问性考虑包括确保那些不能使用定点设备的人能够完全有效地导航 web 应用。在某些情况下,这需要监听键盘事件并做出适当的响应。

假设您正在构建一个图像轮播库。图像 URL 的数组被传递到 carousel,第一个图像呈现在全屏模式对话框中,用户可以通过单击当前图像任一侧的按钮来移动到下一个或上一个图像。使用键盘在图像中循环成为可能允许那些不能使用定点设备的人使用转盘。它还为那些不想使用鼠标或触控板的人增加了便利。

为了简单起见,假设我们的图片库 HTML 模板如下所示:

1  <div class="image-gallery">
2    <button class="previous" type="button">Previous</button>
3    <img>
4    <button class="next" type="button">Next</button>
5  </div>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

当按钮被点击时,JavaScript 循环显示图像,如下所示(适用于现代浏览器):

 1  // assuming we have an array of images in an `images` var

 2  var currentImageIndex = 0;
 3  var container = document.querySelector('.image-gallery');
 4  var updateImg = function() {
 5    container.querySelector('IMG').src =
 6      images[currentImageIndex];
 7  };
 8  var moveToPreviousImage = function() {
 9    if (currentImageIndex === 0) {
10      currentImageIndex = images.length - 1;
11    }
12    else {
13      currentImageIndex--;
14    }
15    updateImg();
16  };
17  var moveToNextImage = function() {
18    if (currentImageIndex === images.length - 1) {
19      currentImageIndex = 0;
20    }
21    else {
22      currentImageIndex++;
23    }
24    updateImg();
25  };
26
27  updateImg();
28
29  container.querySelector('.previous')
30    .addEventListener('click', function() {
31      moveToPreviousImage();
32    });
33
34  container.querySelector('.next')
35    .addEventListener('click', function() {
36      moveToNextImage();
37    });

超越 jQuery(三)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

如果我们想让用户用左右箭头键在图像组中移动,我们可以将键盘事件处理程序附加到左右箭头上,并适当地委托给现有的moveToPreviousImage()moveToNextImage()函数:

 1  // add this after the block of code above:

 2  document.addEventListener('keydown', function(event) {
 3    // left arrow

 4    if (event.which === 37) {
 5      event.preventDefault();
 6      moveToPreviousImage();
 7    }
 8    // right arrow

 9    else if (event.which === 39) {
10      event.preventDefault();
11      moveToNextImage();
12    }
13  });

超越 jQuery(三)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

添加的event.preventDefault()确保了我们的箭头键只改变这个上下文中的图像,而不提供任何不需要的默认动作,比如滚动页面。在我们的例子中,不清楚我们的转盘何时不再使用,但是我们可能会提供一些机制来关闭转盘。一旦传送带关闭,不要忘记使用removeEventListener()来注销 keydown 事件处理程序。您需要重构事件侦听器代码,以便将逻辑移到独立的函数中。通过将“keydown”事件类型作为第一个参数,将事件监听器函数变量作为第二个参数传递给removeEventListener(),这将使取消注册 keydown 处理程序变得容易。有关使用removeEventListener()的更多信息,请查看前面关于观察事件的章节。

确定某物何时装载

作为一名 web 开发人员,您可能会在某个时候想到以下问题:

  • 什么时候页面上的所有元素都完全加载并使用应用的样式呈现?
  • 什么时候所有的静态标记都放在页面上了?
  • 页面上的特定元素何时被完全加载?什么时候元素加载失败?

所有这些问题的答案都在浏览器的本地事件系统中。W3C UI Events 规范中定义的“加载”事件, 23 允许我们确定元素或页面何时被加载。还有一些其他的相关事件,比如“DOMContentLoaded”和“beforeunload”。我将在本节中讨论这两个问题。

什么时候页面上的所有元素都完全加载并使用应用的样式呈现?

为了回答这个特殊的问题,我们可以依靠由window对象触发的“load”事件。此事件将在以下时间后触发:

  1. 所有标记都已放置在页面上。
  2. 所有样式表都已加载。
  3. 所有的<img>元素都已加载。
  4. 所有的<iframe>元素都已完全加载。

jQuery 为“load”事件提供了一个别名,类似于许多其他 DOM 事件:

1  $(window).load(function() {
2    // page is fully rendered

3  });

  • 1
  • 2
  • 3
  • 4
  • 5

但是您可以(也应该)使用通用的 on()方法,并传入事件的名称——“load”——特别是因为 load()别名已被弃用,并在 jQuery 3.0 中被删除。下面是同一个侦听器,但没有不推荐使用的别名:

1  $(window).on('load', function() {
2    // page is fully rendered

3  });

  • 1
  • 2
  • 3
  • 4
  • 5

web API 解决方案看起来几乎与前面的 jQuery 代码一模一样。我们正在使用所有现代浏览器都可用的addEventListener(),并传递事件的名称,然后在页面加载后调用一个回调函数:

1  window.addEventListener('load', function() {
2    // page is fully rendered

3  });

  • 1
  • 2
  • 3
  • 4
  • 5

但是我们为什么要关心这个事件呢?为什么知道页面何时完全加载很重要?对 load 事件最常见的理解是,应该使用它来确定何时执行 DOM 操作是安全的。这在技术上是正确的,但是等待“load”事件可能是不必要的。在操作文档之前,您真的需要确保所有的图像、样式表和 iframes 都已经加载了吗?可能不会。

什么时候所有的静态标记都放在页面上了?

这里我们可以问的另一个问题是:我可以安全地操作 DOM 的最早时间点是什么时候?这个问题的答案和这个标题中的问题是一样的:等待浏览器触发“DOMContentLoaded”事件。此事件在所有标记都放置在页面上后触发,这意味着它通常比“加载”早得多。

jQuery 提供了一个“就绪”函数,它反映了本机“DOMContentLoaded”的行为。但是在幕后,它在现代浏览器中委托给“DOMContentLoaded”本身。下面是您确定页面何时可以与 jQuery 交互的方法:

1  $(document).ready(function() {
2    // markup is on the page

3  });

  • 1
  • 2
  • 3
  • 4
  • 5

你甚至可能熟悉速记版本:

1  $(function() {
2    // markup is on the page

3  });

  • 1
  • 2
  • 3
  • 4
  • 5

因为 jQuery 只是利用现代浏览器中浏览器的本地“DOMContentLoaded”事件来提供它的ready API 方法,所以我们可以使用“DOMContentLoaded”和addEventListener()来构建我们自己的ready:

1  document.addEventListener('DOMContentLoaded', function() {
2    // markup is on the page

3  });

  • 1
  • 2
  • 3
  • 4
  • 5

请注意,您可能希望确保将注册“DOMContentLoaded”事件处理程序的脚本放在任何样式表<link>标记之前,因为加载这些样式表会阻止任何脚本执行,并延长“DOMContentLoaded”事件,直到定义的样式表完全加载完毕。

页面上的特定元素何时被完全加载?什么时候加载失败了?

除了window,加载事件还与许多元素相关联,例如<img><link><script>。这个事件在window之外最常见的用途是确定特定图像何时被加载。适当命名的“错误”事件用于表示加载图像失败(例如,<link><script>)。

正如您所料,jQuery 的 API 中有“load”和“error”事件的别名,但这两个别名都被弃用,并在 jQuery 3.0 中从库中删除。因此,要确定 jQuery 是否加载了图像,我们只需依靠 on()方法。我们的代码应该是这样的:

 1  $('IMG').on('load', function() {
 2    // image has successfully loaded

 3  });
 4
 5  $('IMG').on('error', function() {
 6    // image has failed to load

 7  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

jQuery 中使用的事件名称和浏览器本地事件系统中使用的事件名称之间存在一对一的映射。正如您所料,jQuery 依赖浏览器的“load”和“error”事件来分别表示成功和失败。因此,通过向addEventListener()注册这些事件,可以在没有 jQuery 的情况下达到相同的目的:

1  document.querySelector('IMG').addEventListener('load', function() {
2    // image has successfully loaded

3  });
4
5  document.querySelector('IMG').addEventListener('error', function() {
6    // image has failed to load

7  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

正如我们以前多次看到的,jQuery 和现代浏览器的 web API 之间的语法惊人地相似。

防止用户意外离开当前页面

把自己想象成一个用户(我们不可能总是开发者)。您正在填写一系列(长)表单域。完成表格需要十分钟,但你终于完成了。还有。。。然后。。。你。。。意外地。。。关闭。。。浏览器选项卡。你所有的工作都没了!将“填表”替换为“写文档”或“画图”不管情况如何,这是一个悲剧性的转折。作为一个开发者,你怎样才能把你的用户从这个错误中拯救出来?你能吗?你可以!

“beforeunload”事件在当前页面卸载之前在window上触发。通过观察此事件,您可以强制用户确认他们确实想要离开当前页面,或者关闭浏览器,或者重新加载页面。他们将看到一个确认对话框,如果他们选择取消,他们将安全地停留在当前页面。

在 jQuery-land 中,您可以使用on API 方法观察这个事件,并在确认对话框中为用户返回一条消息:

1  $(window).on('beforeunload', function() {
2    return 'Are you sure you want to unload the page?';
3  });

  • 1
  • 2
  • 3
  • 4

请注意,并非每个浏览器都会显示此特定消息。有些总是显示硬编码的消息,jQuery 对此无能为力。

web API 方法是相似的,但是我们必须处理在不同浏览器之间实现这个事件的一个小差别:

1  window.addEventListener('beforeunload', function(event) {
2    var message = 'Are you sure you want to unload the page?';
3    event.returnValue = message;
4    return message;
5  });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

一些浏览器接受处理函数的返回值作为显示给用户的文本,而另一些浏览器采用更非标准的方法,要求在事件的returnValue属性上设置该消息。尽管如此,这并不是一个可以跳过的铁环。无论哪种方式都很简单。

历史课:古代浏览器支持

在事件章节的最后一节,我描述了 jQuery 是 web 应用必需的库的时代。本节仅适用于古代浏览器。当普遍支持 Internet Explorer 8 时,web API 在某些情况下有点混乱。在处理浏览器的事件系统时尤其如此。在这一节中,我将讨论如何在古老的浏览器中管理、观察和触发事件。因为对古代浏览器的担心变得不那么重要了,所有这些都更像是一堂历史课,而不是教程。请在阅读本节时记住这一点,因为它并不打算成为超老式浏览器中事件处理的全面指南。

用于监听事件的 API 是非标准的

请看下面的代码片段,它注册了一个点击事件:

1  someElement.attachEvent('onclick', function() {
2    // do something with the click event...

3  });

  • 1
  • 2
  • 3
  • 4
  • 5

您会注意到这与现代浏览器方法之间的两个明显区别:

  1. 我们依靠的是attachEvent而不是addEventListener
  2. 点击事件名称包括前缀“on”。

方法attachEvent()24是微软 ie 浏览器的专利。事实上,直到(包括)Internet Explorer 10,它仍然受到技术支持。attachEvent()从未成为任何官方标准的一部分。除非你必须支持 IE8 或更高版本,否则完全避免使用attachEvent()。W3C 标准化的addEventListener()为观察事件提供了更加优雅和全面的解决方案。

也许您想知道如何根据当前浏览器的功能以编程方式使用正确的事件处理方法。如果你是专门为现代浏览器开发应用,这不是一个问题。但是,如果出于某种原因,您必须使用 IE8(或更早版本)等古老的浏览器,您可以使用以下代码在任何浏览器中注册一个事件:

 1  function registerHandler(target, type, callback) {
 2    var listenerMethod = target.addEventListener
 3          || target.attachEvent,
 4
 5        eventName = target.addEventListener
 6          ? type
 7          : 'on' + type;
 8
 9    listenerMethod(eventName, callback);
10  }
11
12  // example use

13  registerHandler(someElement, 'click', function() {
14    // do something with the click event...

15  });

超越 jQuery(三)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

而如果你想在一个古老的浏览器中移除一个事件处理程序,你必须使用detachEvent()而不是removeEventListener()。detachEvent()是另一种非标准的专有 web API 方法。如果您正在寻找跨浏览器删除事件侦听器的方法,请尝试以下方法:

 1  function unregisterHandler(target, type, callback) {
 2    var removeMethod = target.removeEventListener
 3          || target.detachEvent,
 4
 5        eventName = target.removeEventListener
 6          ? type
 7          : 'on' + type;
 8
 9    removeMethod(eventName, callback);
10  }
11
12  // example use

13  unregisterHandler(someElement, 'click', someEventHandlerFunction);

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

表单字段更改事件是一个雷区

非常旧版本的 Internet Explorer 有一些严重的变更事件缺陷。下面是你可能遇到的两个大问题(如果你还没有遇到的话):

  1. 旧版本 IE 中的更改事件不会冒泡。
  2. 在旧版本的 IE 中,复选框和单选按钮可能根本不会触发更改事件。

请记住,在很长一段时间内将 jQuery 与 IE7 和 ie8 一起使用时,第二个问题也会重现。据我所知,jQuery 的当前版本确实很好地解决了这个问题。但是这又一次提醒我们,jQuery 并不是没有缺陷。

要解决变更事件问题,您必须将一个变更处理程序直接附加到您想要监视的任何表单域,因为事件委托是不可能的。为了解决复选框和单选按钮的难题,最好的办法可能是将单击处理程序直接附加到单选/复选框字段(或者将处理程序附加到父元素并利用事件委托),而不要依赖于 change 事件的发生。

Note

通常,回车键会触发一个点击事件(例如在一个按钮上)。换句话说,点击事件不仅仅是由定点设备触发的。出于可访问性的原因,通过键盘激活一个元素也会触发一个点击事件。在复选框或单选按钮输入元素的情况下,“enter”键不会激活表单域。相反,需要“空格键”来激活复选框或单选按钮并触发单击事件。

事件对象也是非标准的

在老版本的浏览器中,Event对象实例的一些属性有些不同。例如,虽然现代浏览器中的事件目标可以通过检查Event实例的target属性来找到,但 IE8 和更早版本包含了该元素的不同属性:srcElement。跨浏览器事件处理函数的相关部分可能如下所示:

1  function myEventHandler(event) {
2    var target = event.target || event.srcElement
3    // ...

4  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在现代浏览器中,event.target将会是 truthy,它缩短了刚刚给出的条件求值。但是在 IE8 和更早的版本中,Event对象实例将不包含target属性,所以target变量将是EventsrcElement属性的值。

在控制事件方面,stopPropagation()方法在 IE8 和更早版本的Event对象实例上不可用。如果您想阻止事件冒泡,您必须在Event实例上设置非标准的cancelBubble属性。跨浏览器解决方案如下所示:

1  function myEventHandler(event) {
2    if (event.stopPropgation) {
3        event.stopPropagation();
4    }
5    else {
6        event.cancelBubble = true;
7    }
8  }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

IE8 和更老的版本也没有stopImmediatePropagation()方法。没有太多的方法可以解决这个限制。然而,我个人并不认为缺乏这种方法是一个大问题。使用stopImmediatePropagation()对我来说就像是一种代码味道,因为这个调用的行为完全依赖于多个事件处理程序附加到相关元素的顺序。

本章的要点是:在没有 jQuery 的现代浏览器中处理事件非常简单,但是如果您不幸支持 Internet Explorer 8 或更早版本,可以考虑使用这里演示的跨浏览器功能,或者引入一个可靠的事件库来满足更复杂的事件处理需求。

Footnotes 1

www.w3.org/TR/DOM-Level-3-Events

2

https://developer.mozilla.org/en-US/docs/Web/Events

3

www.w3.org/TR/uievents/#h-event-interfaces

4

www.w3.org/TR/DOM-Level-2-Events/

5

https://signalvnoise.com/posts/3137-using-event-capturing-to-improve-basecamp-page-load-times

6

www.w3.org/TR/uievents/#event-type-focus

7

www.w3.org/TR/uievents/#event-type-blur

8

www.w3.org/TR/html5/dom.html#htmlelement

9

www.w3.org/TR/html5/forms.html#the-form-element

10

www.w3.org/TR/DOM-Level-2-Events/

11

www.w3.org/TR/DOM-Level-3-Events/#widl-DocumentEvent-createEvent

12

www.w3.org/TR/DOM-Level-2-Events/

13

www.w3.org/TR/DOM-Level-3-Events/#event-type-mousedown

14

www.w3.org/TR/dom/#interface-event

15

https://api.jquery.com/category/events/event-object/

16

https://dom.spec.whatwg.org/#dom-customevent-detail

17

https://facebook.github.io/react/docs/interactivity-and-dynamic-uis.html#under-the-hood-autobinding-and-event-delegation

18

http://ionicons.com

19

https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html#fixed-virtual-key-codes

20

www.w3.org/TR/uievents/

21

www.w3.org/TR/uievents/#widl-KeyboardEvent-key

22

www.w3.org/TR/DOM-Level-3-Events-key/

23

www.w3.org/TR/uievents/#event-type-load

24

https://msdn.microsoft.com/en-us/library/ms536343(VS.85).aspx

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...