五、使用和理解 HTML 元素属性
在第五章,你要准备好全面、深入、无拘无束地讨论与元素属性相关的一切。你在第四章学到的一切都将被证明是有用的,当你在你的属性之旅中应用这些知识的时候。我将确保您对属性有正确的理解,它们是如何产生并成为 HTML 的一部分的,以及它们如何适应 web API。此外,您将学习如何使用属性来定位 DOM 元素。虽然这在第四章中有所涉及,但你会发现这里的覆盖面要广泛得多。最后,我将深入研究属性,并演示如何在任何 DOM 元素上读取、添加和更新它们。还将包括关于data-
和class
属性的特殊部分。
除了几个例外,本章中的大多数 web API 代码都完全支持所有现代浏览器,甚至在许多情况下支持 Internet Explorer 8。完成本章后,您不仅会对属性有一个完整的理解,您还会有信心阅读它们、修改它们,并使用它们在所有浏览器中选择元素,即使是像 Internet Explorer 8 这样古老的浏览器。请继续阅读,继续探索超越 jQuery 的道路!
什么是属性?
从声明的角度来说,HTML 元素由三部分组成:名称、内容和属性,最后两部分是可选的。看一下下面这个简单的片段,我在解释这三个部分的时候会引用它。
1 <form action="/rest/login.php" method="POST">
2 <input name="username" required>
3 <input type="password" name="password" required>
4 </form>
在该标记中,您可以看到三个元素标签:一个<form>
和两个<input>
。<form>
元素的标签名为“form”。事实上,tagName
是 DOM 中实现了Element
接口的每个对象都有的属性。这个属性被标准化为 W3C 的 DOM Level 2 核心规范的一部分。 1 在前面的 HTML 中,表示为HTMLFormElement
对象的<form>
元素、 2 有一个值为“form”的tagName
属性。这两个<input>
元素被表示为HTMLInputElement
对象、 3 ,不出所料,它们每个都有tagName
的“输入”值。
内容是元素的第二部分,描述作为元素后代的任何其他节点。我的例子<form>
有两个<input>
元素作为内容,而两个<input>
元素没有内容。事实上,<input>
元素不允许有任何内容。这种限制很可能是因为<input>
元素最初是在 HTML 2 规范中引入的, 4 只是在 HTML 3 官方标准文档中首次明确提到。 5
A note about my example form markup
通常,您会希望将每个表单字段与一个<label>
相关联,该字段包含一个带有字段显示名称的文本节点。此外,提交按钮通常是谨慎的,但我将所有这些都从前面的标记中去掉了,以保持简单并专注于属性的讨论。
属性是元素的第三个也是最后一个部分,也是可选的,它提供了一种直接在标记中注释元素的方法。您可以使用它们来提供数据或状态。例如,上面的<form>
元素包含两个这样的属性:action
和method
,它们一起告诉表单在提交表单时向“/rest/login.php”服务器端点(action
)发送 POST 请求(method
)。第一个输入具有“用户名”的属性name
,第二个输入具有“密码”的属性name
。当服务器解析表单 submit 时,这些信息用于构造请求并将这些元素与它们的值联系起来。尽管在前面的 HTML 中并不明显,但您甚至可以创建自己的专有属性,并在代码中引用它们,以便将状态或数据与标记中的元素相关联。虽然没有严格要求,但更标准的方法是使用data-
属性,这将在本章后面提到。
除了提供数据或状态,一些属性还用于定义多用途元素的特定行为。请看前面片段中第二个输入的type
属性,这是一个例子。这个type
属性将第二个输入定义为密码输入,它通知浏览器屏蔽用户输入到这个字段中的任何字符。第一个输入可以包括一个值为“text”的type
属性,但这不是必需的,因为所有的<input>
元素都是默认的文本输入。这个缺省值在 HTML 出现时就已经存在了,在规范的早期草案中就可以看到。属性强加的行为的另一个例子可以在前面的两个输入中看到。注意每个输入上的required
属性——这是给任何支持constraints API
的浏览器的一个信号,如果用户将这些字段中的任何一个留空,它将阻止表单提交。 7
历史和标准化
属性一直是 HTML 的一部分,在第一个详述 HTML 标签的文档中有所描述,该文档由蒂姆·伯纳斯·李于 1992 年撰写。在这篇文章中,Berners-Lee 描述了今天在 HTML 中使用的两种通用类型的属性——布尔型和变量型——这两种属性我稍后会进一步阐述。调用属性的段落在文档的开头附近:
- 有些标签带有参数,称为属性。属性在标签后给出,用空格隔开。某些属性仅仅因为它们的存在而产生影响,其他的属性后面有一个等号和一个值。
Berners-Lee 继续提到了一些这样的属性,以锚标签的href
、type
和name
属性为例。不过请注意,<a>
元素上的name
属性不再可用,因为它在 HTML5 规范中被删除了。自从 HTML 的第一次描述以来,元素属性的数量和重要性都大大增加了。
任何 HTML 规范都不支持也从未正式支持过未绑定的自定义属性。但是,从 HTML5 规范开始,您可以在自己选择的属性名前面加上“data-”(稍后将详细介绍)。但是,如果您想在标记中引入一个纯粹的自定义属性,比如“myproject-uuid ”,您当然可以这样做。该页面将正常呈现,并且在您的浏览器的开发人员工具控制台中不会出现错误。一切都会好的。唯一的缺点是您的文档将无法通过验证,因为它将包含非标准属性——任何公认的 HTML 标准中都没有提到的属性。非标准定制属性实际上相当常见,甚至在流行的 JavaScript 框架中也很普遍,比如 AngularJS,它严重依赖非标准定制属性来促进与元素指令的通信。
最新推荐的 HTML 规范—HTML 5—定义了四种不同类型的属性。 10 一种通常被称为“布尔属性”的类型 11 被表示为没有任何显式值的元素属性。以常见于<input>
元素的required
属性为例(参见上一节的 HTML 片段)。如规范所述,“元素上布尔属性的存在代表真值,属性的缺失代表假值。”HTML 5.1 标准化的hidden
属性 12 指示浏览器不呈现任何带有该属性的元素,这是第一种类型的另一个例子。
第二种类型的属性被描述为“无引号的”一个鲜为人知的事实是,如果属性值不包含空格、等号、尖括号(<
和>
)或空字符串(以及其他不太重要的字符限制),您可以省略属性值两边的引号。因此,上一节中的 HTML 片段可以重写如下:
1 <form action=/rest/login.php method=POST>
2 <input name=username required>
3 <input type=password name=password required>
4 </form>
HTML 元素属性的最后两种类型非常相似:单引号和双引号。两者是相似的,因为它们在很大程度上有相同的限制,并且比不带引号的属性更常见,尽管双引号属性可以说是所有类型中最常见的。与未加引号的属性值相反,用单引号或双引号括起来的属性值可能包含空格、等号、尖括号或空字符串。最新的 W3C 规范中描述属性值的部分只提到它们不能包含任何模糊的&字符。“与号字符”是一个&
符号,后跟一个标准化的 ASCII 字符代码,以分号(;
)结束。“不明确的&字符”是指这个 ASCII 字符代码与规范的命名字符引用 13 部分中定义的任何字符代码都不匹配。
属性和特性有什么不同?
现在您已经对什么是元素属性有了很好的理解,但是您可能仍然对它们与元素“属性”的关系感到困惑,特别是如果您已经使用 jQuery 一段时间的话。 14 在一个非常基本的层面上,属性和属性彼此完全不同。虽然属性是在元素的标记中的 HTML 级别声明的,但是属性是在元素的对象表示中声明和更新的。例如,考虑以下元素:
1 <div class="bold">I'm a bold element</div>
<div>
有一个值为“bold”的class
属性。但是我们也可以在这个元素上设置属性。假设我们想将属性index
的值设为0
:
1 <div class="bold">I'm a bold element</div>
2
3 <script>
4 document.querySelector('.bold').index = 0;
5 </script>
在执行了前面的片段之后,我们的<div>
现在有了一个值为“bold”的class
属性和一个值为0
的index
属性。该属性是在底层 JavaScript 对象上设置的,在本例中,该对象是HTMLDivElement
15 接口的一个实现。像我们的index
这样的元素对象属性也被称为“expando”属性,这只是一种对非标准元素对象属性进行分类的简洁方法。请理解,并非所有元素属性都是 expando 属性。如果这还不完全清楚,不要担心。在本节结束之前,我将更多地讨论标准化元素的属性。
尽管属性和特性在概念上和语法上是不同的,但在某些情况下它们是紧密联系在一起的。事实上,所有标准化的元素属性都在元素的对象表示中定义了相应的属性。在大多数情况下,每个标准属性和属性对共享相同的值。除了一种情况之外,所有的属性和特性都使用相同的名称。这些标准化属性的特殊之处在于,您可以在不接触标记的情况下更新它们。更新相应的元素对象的属性值将导致浏览器更新文档中的属性值,更新属性值将依次增加元素的属性值。让我们来看一个简单的例子,我们定义了一个以href
开头的锚链接,然后使用 JavaScript 更新锚指向一个不同的位置:
1 <a href="http://www.widen.com/blog/">Read the Widen blog</a>
2
3 <script>
4 document.querySelector('A').href = 'http://www.widen.com/blog/ray-nicholus';
5 </script>
执行上述代码块中的脚本后,锚点现在出现在文档中,如下所示:
1 <a href="http://www.widen.com/blog/ray-nicholus">Read the Widen blog</a>
在这种情况下,HTMLAnchorElement
, 16 是<a>
的对象表示,在其原型上定义了一个href
属性,该属性直接连接到元素标签上的href
属性。这个href
属性实际上是从URLUtils
接口 17 继承而来的,HTMLAnchorElement
对象也实现了这个接口。URLUtils
是 WHATWG URL 生活标准 18 规范中正式定义的接口。
还有许多其他具有连接属性的元素属性,例如id
(所有元素)、action
(表单元素)和src
(脚本元素),等等。请记住,HTML 规范中出现的所有属性都属于这一类。但是有一些特殊情况和要点需要考虑。首先,class
属性有点不同,对应的属性名不是class
,而是className
。这是因为“类”在许多语言中都是保留字,比如 JavaScript。 19 关于class
属性的更多内容稍后介绍。还要记住,单选按钮和复选框输入元素共有的checked
属性最初只连接到相应的元素属性值。考虑下面的代码来更清楚地演示这种限制:
1 <input type="checkbox" checked>
2
3 <script>
4 // this does not remove the checked attribute
5 document.querySelector('INPUT').checked = false;
6 </script>
在执行了前面的脚本之后,您可能会期望从 input 元素中删除属性checked
,因为这也会发生在其他布尔属性上,比如required
和disabled
。然而,checked
属性仍然保留在元素上,即使属性值已经被更改为false
并且复选框确实没有被选中。
“自定义”属性,即未在任何公认规范中定义的属性,不会以任何方式链接到元素对象上类似命名的属性。为匹配非标准属性而创建的任何属性也被视为 expando 属性。
使用属性查找元素
基于第四章的类和 ID 选择器的例子,本节将提供一个更全面的使用 web API 选择任何和所有属性的指南。虽然 ID 和 class 属性的选择通常是使用特定于这两种类型属性的选择器语法来完成的,但是您也可以使用本章中介绍的更通用的属性选择方法。在某些情况下,当寻找遵循已知 ID 或类模式的多个元素时,这里展示的一些通用但强大的属性选择器是最合适的。
为了保持一致性和便于参考,本节将提供 jQuery 示例。但是不使用 jQuery 也可以用多种方式选择属性,只需使用querySelector
或querySelectorAll
即可。由于属性选择器最初是作为 W3C CSS 2 规范的一部分引入的, 20 从 Internet Explorer 8 开始,这里所有简单的 web API 示例(但不是所有更复杂的示例)都受到支持!您真的不需要 jQuery 来编写简单但功能强大的属性选择器。
使用属性名查找元素
我将很快详细介绍值,但是让我们首先关注属性名。为什么您可能只想关注属性名呢?也许有很多原因:
- 定位
disabled
元素或required
表单字段。 - 查找一个或多个包含自定义属性的元素,该属性以某种方式对这些元素进行分组。
- 定位文档中的无效标记,例如没有
src
属性的<img>
元素。
下面的 jQuery 和 web API 示例将围绕上面的#1 展开。为此,将使用一个小的 HTML 片段作为参考:
1 <form action="/submitHandler.php" method="POST">
2 <input name="first-name">
3 <input name="last-name" required>
4 <input type="email" name="email" required>
5 <button disabled>submit</button>
6 </form>
框架
有一种方法可以在 jQuery 中选择给定属性的元素,那就是将有效的 CSS 2+属性选择器字符串传递给 jQuery 函数:
1 var $result = $('[required], [disabled]');
前面的代码将产生一个包含“姓氏”和“电子邮件”元素的$result
jQuery 对象,以及被禁用的提交<button>
。为了防止选择器字符串中的逗号给你造成一些困惑,我在前一章的多元素选择器一节中介绍了这一点。这个 jQuery 代码完全依赖于幕后的 web API。
Web API
如同第四章中的许多本地解决方案一样,使用属性名定位元素所需的代码与您刚刚看到的 jQuery 解决方案惊人地相似(如清单 5-1 所示)。
1 var result = document.querySelectorAll('[required], [disabled]');
Listing 5-1.Selecting by Attribute Name: Web API, All Modern Browsers, and Internet Explorer 8
与 jQuery 示例类似,前面的代码将使用包含“姓氏”和“电子邮件”输入的NodeList
填充result
变量,以及禁用的提交按钮。
虽然disabled
和required
是布尔属性,但是即使我们给它们赋值,前面的代码也会产生相同的结果。属性选择器只是匹配属性名——值(或缺少值)无关紧要。这意味着您可以轻松地在文档中找到分配了 CSS 类的所有元素。例如,清单 5-2 显示了一个简单的属性选择器。
1 var result = document.querySelectorAll('[class]');
Listing 5-2.Selecting All Elements with a Class Attribute: Modern Browsers and Internet Explorer 8
给定以下 HTML:
1 <div class="bold">I'm bold</div>
2 <span>I'm not</span>
。。。前面选择器中的result
变量将产生一个元素:<div>
。但是要注意,简单地给<span>
添加一个空的class
属性可能会导致一个意外的结果集:
1 <div class="bold">I'm bold</div>
2 <span class>I'm not</span>
尽管没有给<span>
分配任何 CSS 类,但是属性class
的存在意味着我们的选择器将它和<div>
一起包含在结果集中。这可能不是我们想要的。这不是选择器 API 的缺陷,但是准确理解属性名选择器的工作方式是很重要的。注意,如果您没有牢牢掌握这个 CSS 选择器,使用 jQuery 也会遇到同样的“问题”。
使用属性名和值查找元素
有时,仅通过属性名定位一个元素或一组元素是不够的。例如,您可能想要定位所有密码输入字段,在这种情况下,您需要找到所有具有“password”属性的<input>
元素。或者您可能需要定位链接到特定端点的所有锚元素,在这种情况下,您需要键入所有<a>
元素的href
属性的期望值。
为了设置我们的 jQuery 和 web API 示例,让我们使用下面的 HTML 并声明我们的目标是定位链接到 ajax 表单 web 组件文档页面的所有锚点:
1 <section>
2 <h2>web components</h2>
3 <ul>
4 <li>
5 <a href="http://file-input.raynicholus.com/">file-input</a>
6 </li>
7 <li>
8 <a href="http://ajax-form.raynicholus.com/">ajax-form</a>
9 </li>
10 </ul>
11 </section>
12 <section>
13 <h2>no-dependency libraries</h2>
14 <ul>
15 <li>
16 <a href="http://ajax-form.raynicholus.com/">ajax-form</a>
17 </li>
18 <li>
19 <a href="http://fineuploader.com/">Fine Uploader</a>
20 </li>
21 </ul>
22 </section>
框架
为了找到所有指向 ajax 表单库页面的锚元素,我们将使用一个标准化的 CSS 选择器字符串传递到jQuery
函数中,就像我们以前多次看到的那样:
1 var $result = $('A[href="http://ajax-form.raynicholus.com/"]');
前面的选择器返回一个 jQuery 对象,该对象包含示例标记中的两个 ajax 形式的HTMLAnchorElement
对象。
Web API
您已经看到了在使用 jQuery 时,如何要求标准 CSS 选择器根据属性名和值进行选择,因此,当试图在没有 jQuery 的情况下查找特定的锚元素时,同样的选择器当然是最合适的。正如您在大多数其他元素选择示例中看到的那样,这里的解决方案几乎与 jQuery 方法相同,但效率更高:
1 var result =
2 document.querySelectorAll('A[href="http://ajax-form.raynicholus.com/"]');
result
变量是一个NodeList
,包含本节开头的示例 HTML 中的两个 ajax 表单锚。请注意,我将属性名称/值选择器与标记名称选择器结合在一起。这确保了可能包含非标准href
属性的任何其他元素都被忽略(以及任何<link>
元素),因为我们只关心锚链接。
还记得“按属性名选择”一节中的空类属性示例吗?在我们用 CSS 类搜索所有元素的过程中,我们无法用简单的属性名选择器忽略空的class
属性。但是如果我们将一个属性名称/值选择器与第四章的排除选择器配对,如清单 5-3 所示,我们可以有效地过滤掉空的class
属性。
1 var result = document.querySelectorAll('[class]:not([class=""]');
Listing 5-3.Find Anchors with Specific href Attributes: Web API, Modern Browsers
使用初始空类属性示例一节中的示例 HTML,前面的代码块result
包含一个NodeList
,该代码块只包含属性为“bold”的<div>
。属性为空的<span>
已被成功跳过。
通配符和模糊属性选择器的威力
属性选择器部分的最后一部分关注更高级的用例。在这一节中,我演示了四个非常强大的属性选择器技巧,它们也很容易理解,并且在所有现代浏览器以及 Internet Explorer 8 中都得到支持。您已经(多次)看到的 jQuery 和 web API 选择器代码之间的模式将在最后一组示例中继续。因此,让我们放弃 jQuery 和 web API 代码片段,因为在讨论元素选择器时,它们大多是多余的。如果您真的想以“jQuery 方式”运行下面的例子,只需用$()
替换document.querySelectorAll()
,并做好代码运行速度变慢的准备。
寻找特定的字符
还记得属性名和值部分的例子吗?我们希望定位文档中指向特定端点的锚链接。但是如果我们不关心整个 URL 呢?如果我们只关心领域呢?考虑下面的 HTML 片段:
1 <a href="http://fineuploader.com/">home page</a>
2 <a href="http://fineuploader.com/demos">demos</a>
3 <a href="http://docs.fineuploader.com/">docs</a>
4 <a href="http://fineuploader.com/purchase">purchase</a>
如果我们想在 http://fineuploader.com
定位所有锚链接,实例子串属性选择器,首先在 W3C CSS 3 规范中标准化, 21 允许我们这样做:
1 var result =
2 document.querySelectorAll('A[href*="http://fineuploader.com"]');
上面的result
变量是一个节点列表,包含除第三个之外的所有锚链接。这是为什么?我们正在寻找一个包含字符串"
http://fineuploader.com
"
的 href 属性。第三个锚链接不包含此字符串。也许这不是我们的意图,我们只是想找到所有的锚链接,以某种方式指向 fineuploader.com。简单!
1 var result =
2 document.querySelectorAll('A[href*="fineuploader.com"]');
寻找特定的单词
也许我们需要在属性值中定位一个特定的“单词”,而不是寻找字符组。例如,我们可以使用这个属性词选择器编写一个替代的 CSS 类选择器。考虑下面的 HTML 片段:
1 <div class="one two three">1 2 3</div>
2 <div class="onetwothree">123</div>
假设我们只想找到 CSS 类为“two”的元素。除了我在第四章中演示的 CSS 类选择器之外,我们还可以利用一个特殊的属性选择器来完成这个任务:
1 var result = document.querySelectorAll('[class∼=two]');
result
变量是一个包含一个条目的NodeList
——样本元素集合中的第一个<div>
——这正是我们要寻找的。但是为什么我们需要创建另一个类选择器呢?我们不知道,前面的例子也不太实际,尽管它很好地说明了这个选择器的行为。一个更现实的例子可能是在元素title
属性中定位一个特定的单词。考虑这组元素:
1 <a href="https://github.com/rnicholus/frame-grab.js"
2 title="frame-grab repo">frame-grab GitHub repo</a>
3
4 <a href="https://github.com/rnicholus/frame-grab.js/blob/master/docs/api.md"
5 title="frame-grab docs">frame-grab documentation</a>
6
7 <a href="https://www.youtube.com/watch?v=hHBhP03JHIQ"
8 title="frame-grab + fine-uploader">Video frame uploader</a>
9
10 <img src="https://travis-ci.org/rnicholus/frame-grab.js.svg?branch=master"
11 title="frame-grab build status">
12
13 <a href="https://foo.bar/subframe-grabber"
14 title="window-subframe-grabber">
15 Locates all iframes inside of a given iframe</a>
想象一下,这两个链接和一个图像,显然都与帧抓取库相关,存在于一个大文档中许多其他不相关的链接和图像之间。但是我们只想找到那些与帧抓取库直接相关的资源。我们不能使用子串属性选择器来选择“frame-grab.js ”,因为并不是所有的元素都包含带有“frame-grab.js”的href
或src
属性。我们也不想把重点放在短语“帧抓取”上,因为这将包括最后一个链接,它与帧抓取库无关。相反,我们需要选择所有具有包含特定短语“帧抓取”的title
属性的元素。
1 var result = document.querySelectorAll('[title∼=frame-grab]');
result
是一个NodeList
,它包含了 HTML 样本中除最后一个锚链接之外的所有元素,这正是我们要寻找的结果。
以开头或结尾的属性值。。。
需要注意的最后一组有用的高级属性选择器允许您在文档中定位属性值以一个或多个特定字符开头或结尾的元素。从实用的角度来说,也许您现在想知道为什么这样的选择器会有用。正如我们以前多次做过的那样,让我们从一点 HTML 开始,然后讨论这两个属性选择器对我们的重要性:
1 <img id="dancing-cat" srcimg/dancing-cat.gif">
2 <img id="still-cat" srcimg/still-cat.png">
3 <img id="dancing-zebra" src="dancing-zebra.gif">
4 <a href="#dancing-zebra">watch the zebra dance</a>
5 <a href="/logout">logout</a>
该片段很可能出现在一个大文档中,出现在许多其他锚链接和图像中,但是可以被认为是许多这样的元素的代表。假设我们想在这个文档中找到两件东西:
- 所有 GIF 图片。
- 引用当前页面上元素的所有锚点。
令人耳目一新的现实是,我们可以在不依赖任何第三方的情况下同时实现这两个目标:
1 var result = document.querySelectorAll('A[href^="#"], [src$=".gif"]');
前面的选择器使用第四章中介绍的多重选择器语法,分别组合了一个“开始于”和一个“结束于”属性值选择器。我们的“starts with”选择器以任何带有以散列标记开始的href
属性的锚元素为目标,这将只包括引用当前页面的锚。第二个选择器关注属性值以“.”结尾的元素。gif”。这将包括对 GIF 图像的引用,假设图像 URL 以预期的扩展名结尾。
读取和修改元素属性
所以现在您确切地知道了什么是属性(以及什么不是属性),并且您非常熟悉通过属性名称和值来选择元素。我要讨论的属性的最后一个方面包括读取和更新元素的属性,以及创建新的属性。您将发现解析、添加、删除和更改属性的适当方法可能取决于属性的类型。在这最后一节中,我将介绍三种不同类型的属性:类属性、数据属性以及所有其他通用的本地和定制属性。
类别属性
到目前为止,元素类属性似乎是“超越 jQuery”中的一个热门话题。第四章详细讨论了它们,我在本章前面的属性与属性部分提到了class
属性如何不同于它的元素属性名,我甚至在最近的用属性查找元素部分展示了如何使用类属性选择元素。好了,我们又在这里讨论阶级了。但是这次,我将向您展示如何读取特定元素的类,以及添加、切换和删除元素的类。
阅读课
“读取和修改属性”一节中的所有情况都假设您已经有了特定元素的句柄。因此,既然我们已经有了一个元素,也许我们想知道它与什么特定的 CSS 类相关联。或者也许我们只是想找出它是否与某个特定的类相关联。本节将研究这两种需求。
让我们从一个实际元素开始,以供参考:
1 <p class="product-name out-of-stock manual-tool">saw</p>
假设这个元素是一个大型文档中许多其他工具中的一个特定工具的名称。假设我们想知道关于我们已经登陆的特定工具元素的两件事情:
- 这种工具有存货吗?
- 这是手动工具还是电动工具?
jQuery 提供的解决方案利用了它的hasClass
API 方法:
1 var inStock = !$toolEl.hasClass('out-of-stock');
2 var type = $toolEl.hasClass('manual-tool') ? 'manual' : 'power';
布尔变量inStock
将被设置为值false
,因为该元素包含一个“缺货”类。而type
是“手动的”,因为存在一个“手动工具”类。这里没有惊喜。
但是我们不想用 jQuery!那么,如果没有它,我们怎么做呢?幸运的是,由于Element
接口上的classList
属性,现代 web API 提供了一个同样优雅的解决方案。22WHATWG 网络标准组织最初起草了classList
的规范,W3C 也将其包含在其 DOM4 文档 23 中。
注意,classList
属性是一个DomTokenList
。24DomTokenList
接口包含四个值得注意的方法,我将在本节中逐一演示。您将很快看到如何使用classList
对元素的class
属性执行各种操作,但是首先我将关注这样一个方法:contains
。 25 为了确定特定元素是否包含特定 CSS 类,DOM API 在classList
对象上提供了一个直观的属性:contains
。
1 var inStock = !toolEl.classList.contains('out-of-stock');
2 var type = toolEl.classList.contains('manual-tool') ? 'manual' : 'power';
前面的代码与 jQuery 示例相同——只需用classList.contains
替换hasClass
,就能获得性能优势! 26
如果您需要支持旧的浏览器,您将需要求助于正则表达式来确定您的目标元素是否包含某个类。幸运的是,这也相当简单(适用于任何浏览器):
1 var hasClass = function(el, className) {
2 return new RegExp('(^|\\s)' + className + '(\\s|$)').test(el.className);
3 };
4 var inStock = !hasClass(toolEl, 'out-of-stock');
5 var type = hasClass(toolEl, 'manual-tool') ? 'manual' : 'power';
无论您是否使用 jQuery,如果您想要获得与一个元素相关联的所有 CSS 类的列表,您必须直接访问Element
对象上的class
属性或className
属性。在这两种情况下,该值将是附加到该元素的所有 CSS 类的以空格分隔的字符串。
添加和删除类
接下来,我们有一个元素,我们需要删除“红色”类,并添加一个“蓝色”类:
1 <p class="red">I'm red. Make me blue!</p>
我们都知道,addClass
和removeClass
jQuery 函数分别用于在元素中添加和删除 CSS 类:
1 $pEl.removeClass('red').addClass('blue');
jQuery 解决方案非常漂亮,我们可以在一行代码中完成所有工作,而不会牺牲可读性。没有 jQuery 我们能做同样的事情吗?好吧,web API 方法有点冗长,链接不是它构建的,但它也一样简单。非 jQuery 解决方案也更快, 27 并且适用于除 IE9 之外的所有现代浏览器:
1 pEl.classList.remove('red');
2 pEl.classList.add('blue');
classList
再一次出手相救。也许你在对自己说,“原生解决方案让我多键入几个字符。这会大大影响我的工作效率。”真的吗?如果您频繁地通过 JavaScript 添加和删除类,那么多几个字符就会对敏捷性产生严重的负面影响,那么也许是时候重新评估您的应用设计了。
卡在支持 IE9 及更老版本?覆盖世界上所有浏览器的解决方案类似于上一节中的contains
的后备方案:
1 var removeClass = function(el, className) {
2 el.className =
3 el.className.replace(new RegExp('(^|\\s)' + className + '(\\s|$)'), ' ');
4 };
5 removeClass(pEl, 'red');
6 pEl.className += ' blue';
这比addClass(...)
和removeClass(...)
难多了。幸运的是,classList
是标准化的,是 jQuery 的类操作的合适替代品。
切换类别
假设我们有一个元素,我们想切换它的可见性,也许是为了响应一个按钮的点击。我稍后将介绍事件,所以让我们只关注切换该元素的可见性所需的逻辑:
1 <section class="hide">
2 <h1>User Info</h1>
3 </section>
jQuery 提供了大家熟悉的toggleClass()
方法,用法如下:
1 // removes "hide" class
2 $sectionEl.toggleClass('hide');
3
4 // re-adds "hide" class
5 $sectionEl.toggleClass('hide');
如果您使用的是现代浏览器(IE9 除外),没有 jQuery 也一样简单:
1 // removes "hide" class
2 sectionEl.classList.toggle('hide');
3
4 // re-adds "hide" class
5 sectionEl.classList.toggle('hide');
IE9 和更老版本的解决方案有点麻烦,但仍然可行。它包括检查该类是否存在,然后根据当前状态添加或删除它:
1 var toggleClass = function(el, className) {
2 var pattern = new RegExp('(^|\\s)' + className + '(\\s|$)');
3 if (pattern.test(el.className)) {
4 el.className = el.className.replace(pattern, ' ');
5 }
6 else {
7 el.className += ' ' + className;
8 }
9 };
10
11 // removes "hide" class
12 toggleClass(sectionEl, 'hide');
13
14 // re-adds "hide" class
15 toggleClass(sectionEl, 'hide');
请注意,您可以稍微重构前面的代码示例,直接引用前面代码示例中的 hasClass()和 removeClass()方法。
数据属性
尽管 CSS 类属性通常用于设计元素的样式,但是正如您所料,数据属性用于将数据附加到元素上。数据属性必须以“data-”为前缀,并且可以包含与特定元素相关联的字符串数据。任何有效的属性值都是可以接受的。虽然可以构造和使用非标准的元素属性,但是 W3C HTML5 规范声明自定义属性实际上应该是数据属性。 28
还有其他方法可以将更复杂的数据附加到元素上。第六章介绍了数据属性、HTML5 dataset
对象、元素数据的历史以及 jQuery 在解决这个问题中的作用,还有更多与元素数据相关的内容。
使用其他标准和自定义属性
正如您已经看到的,class
属性是特殊的属性,需要更具体的方法来正确地操作和读取它们。事实上,class
属性是两种“特殊”属性中的一种,data-
是另一种。但是所有其他的元素属性呢?我们如何才能最好地与他们合作?本节包括读取、写入、删除和创建标准和自定义属性。您可能已经熟悉并习惯了 jQuery 对这些任务的支持,但是您将会看到使用浏览器的功能处理属性是多么容易。
读取属性
让我们从一个简单的输入元素开始,它包括一个布尔属性和一个标准字符串值属性:
1 <input type="password" name="user-password" required>
假设我们得到了这个元素,我们想要两个问题的答案:
- 这个元素是什么类型的
<input>
? - 这
<input>
是必填字段吗?
这是 jQuery 在减轻开发人员负担方面失败的地方之一。虽然读取属性值很简单,但是没有专门的 API 方法来检测特定元素上属性的存在。虽然使用 jQuery 仍然可以做到这一点,但是这个解决方案不是很直观,可能需要库的新手做一些 web 搜索:
1 // returns "password"
2 $inputEl.attr('type');
3
4 // returns "true"
5 $inputEl.is('[required]');
jQuery 没有定义一个hasAttr
方法。相反,您必须使用 CSS 属性名称选择器来检查元素。web API 确实提供了这些便利,而且从 Internet Explorer 8:
1 // returns "password"
2 inputEl.getAttribute('type');
3
4 // returns "true"
5 inputEl.hasAttribute('required');
早在 1997 年,作为 W3C DOM Level 1 核心规范的一部分,getAttribute
方法首次在Element
接口上定义。 29 和hasAttribute
在 3 年后的 2000 年,在 DOM Level 2 核心规范中被添加到相同的接口。 30
我们可以让 jQuery 例子的后半部分更直观一点,只需跳出 jQuery 对象,直接在底层Element
上操作:
1 // returns "true"
2 $inputEl[0].hasAttribute('required');
因此,无论出于什么原因,如果您坚持使用 jQuery,可以将前面的例子视为一种更直接的方法来确定元素是否包含特定属性。作为一个额外的收获,你会发现在这里尽可能地绕过 jQuery 比完全依赖库更有效。
修改属性
我们在文档中有一个特定的<input>
元素的句柄,该元素如下所示:
1 <input name="temp" required>
我们想以三种方式修改这个HTMLInputElement
:
- 使其成为“电子邮件”输入字段。
- 确保它不是必需的。
- 将其重命名为“userEmail”。
jQuery 要求我们使用attr()
添加和更改属性,使用removeAttr()
删除属性来解决这个问题:
1 $inputEl
2 .attr('type', 'email') // #1
3 .removeAttr('required') // #2
4 .attr('name', 'userEmail'); // #3
没有 jQuery,我们的解决方案看起来几乎是一样的,并且具有同样广泛的浏览器支持。从 W3C 的 DOM Level 1 核心规范开始,Element
接口被定义为具有setAttribute
方法。 32 用这个方法,我们可以改变和添加元素属性,就像 jQuery 的attr()
方法一样。为了移除属性,我们使用了removeAttribute()
,这是在 DOM Level 1 Core 的Element
接口上定义的另一个方法。 33 通过这两种方法,我们可以很容易地修改我们的输入元素,如前所述:
1 inputEl.setAttribute('type', 'email'); // #1
2 inputEl.removeAttribute('required'); // #2
3 inputEl.setAttribute('name', 'userEmail'); // #3
除了缺乏链接支持之外,原生方法和依赖 jQuery 的方法一样直观。这是一个 web 标准已经足够的领域,而 jQuery 只提供了很小的便利性优势。正如您在本节中所看到的,在没有任何库帮助的情况下,处理属性通常非常容易。
Footnotes 1
www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-104682815
2
www.w3.org/TR/html5/forms.html#the-form-element
3
www.w3.org/TR/html5/forms.html#the-input-element
4
www.w3.org/MarkUp/html-spec/html-spec_toc.html
5
www.w3.org/MarkUp/html3/input.html
6
www.w3.org/MarkUp/HTMLPlus/htmlplus_41.html
7
www.w3.org/TR/html5/forms.html#constraints
8
www.w3.org/History/19921103-hypertext/hypertext/WWW/MarkUp/Tags.html
9
www.w3.org/TR/html5/text-level-semantics.html#the-a-element
10
www.w3.org/TR/html5/syntax.html#attributes-0
11
www.w3.org/TR/html5/single-page.html#boolean-attributes
12
www.w3.org/TR/html51/editing.html#the-hidden-attribute
13
www.w3.org/TR/html5/syntax.html#named-character-references
14
http://blog.jquery.com/2011/05/03/jquery-16-released/
15
www.w3.org/TR/html5/grouping-content.html#the-div-element
16
www.w3.org/TR/html51/semantics.html#the-a-element
17
https://url.spec.whatwg.org/#urlutils
18
19
www.ecma-international.org/ecma-262/6.0/#sec-keywords
20
www.w3.org/TR/CSS2/selector.html#attribute-selectors
21
www.w3.org/TR/css3-selectors/#attribute-substrings
22
https://dom.spec.whatwg.org/#dom-element-classlist
23
www.w3.org/TR/dom/#dom-element-classlist
24
https://dom.spec.whatwg.org/#domtokenlist
25
https://dom.spec.whatwg.org/#dom-domtokenlist-contains
26
http://jsperf.com/classlist-contains-vs-hasclass
27
http://jsperf.com/jquery-addclass-removeclass-vs-dom-classlist
28
www.w3.org/TR/html5/dom.html#embedding-custom-non-visible-data-with-the-data-%2A-attributes
29
www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-666EE0F9
30
www.w3.org/TR/DOM-Level-2-Core/core.html#ID-666EE0F9
31
http://jsperf.com/hasattribute-vs-jquery-is
32
www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-setAttribute
33
www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-removeAttribute
六、HTML 元素数据存储和检索
在第五章中,我在讨论 HTML 元素属性的三种类型时提到了数据属性。关于将任何类型的数据连接到文档元素的所有内容,请不要再看了。所有的细节都在这一章中,这一章也将建立在第五章的通用属性和元素属性上。在演示将重要的数据结构连接到标签的过程中,还将简要介绍 JavaScript 对象。
当你继续阅读这一章的时候,你会理解为什么将数据连接到你的文档元素既重要又潜在棘手。和往常一样,我将向您展示如何将数据附加到元素上,然后最初使用 jQuery 读回数据。但最重要的是,您将看到如何在没有 jQuery 的情况下完成所有这些工作。我还将解释 jQuery 如何利用 web API 和 JavaScript 来提供对元素数据的支持。
管理元素数据的未来令人兴奋。我将向您展示 web API 和 JavaScript 如何准备在不久的将来超越 jQuery 对所有浏览器的元素数据支持。这种“未来派”的原生支持已经可以在许多浏览器上使用。我一定会包含丰富的代码示例,详细说明如何在您的项目中利用这种内置的支持。
为什么要将数据附加到元素上?
尤其是在现代 web 应用中,确实需要将数据与页面上的元素联系起来。在这一节中,我们将探索将自定义数据附加到标记的常见原因,以及通常是如何在较高层次上完成的。您将会看到,有许多原因使您发现跟踪数据和元素是有用的。
跟踪状态
也许您正在为一个房地产经纪人维护一个页面,其中包含一个当前市场上的房产表。大概,您会希望将它们从最受欢迎到最不受欢迎进行排序,并且您可能希望能够通过直接在页面上拖放来调整这个顺序:
1 <table>
2 <thead>
3 <tr>
4 <th>Address</th>
5 <th>Price</th>
6 </tr>
7 </thead>
8 <tbody>
9 <tr>
10 <td>6911 Mangrove Ln Madison, WI</td>
11 <td>$10,000,000</td>
12 </tr>
13 <tr>
14 <td>1313 Mockingbird Ln Mockingbird Heights, CA</td>
15 <td>$100,000</td>
16 </tr>
17 </tbody>
18 </table>
在移动一行之后,或者甚至作为初始标记的一部分,您可能希望用表中的原始索引来注释每一行。这可能用于在不调用服务器的情况下恢复任何更改。这里最合适的是一个data-
或自定义属性(分别为data-original-idx
或original-idx
)。
您可能还想跟踪元素的初始样式信息,如尺寸。如果您允许用户动态地调整元素的宽度和高度,您可能会希望有一种简单的方法来重置这些尺寸,以防用户改变主意。您可以在元素旁边存储初始维度,可能使用data-
属性。
连接元件
看似不同的元素很可能需要相互了解。例如,两个元素,其中一个元素的可见性由另一个元素的可见性决定。换句话说,如果一个元素可见,另一个元素不可见。这些元素如何以这种方式共存?答:通过维护彼此的引用。
这个场景有许多可能的解决方案。一种是将每个元素的伙伴元素的 CSS 选择器字符串嵌入到一个data-
或自定义属性中。假设这是合理的,并且有唯一的选择器可用,这可能是最佳选择。如果这是不可能的,那么您将需要维护一个Node
对象的地图。借助 JavaScript 对象,这可以通过几种不同的方式来实现。稍后会详细介绍。
将模型直接存储在元素中
您在页面上有一个用户列表:
1 <ul>
2 <li>jack</li>
3 <li>jill</li>
4 <li>jim</li>
5 </ul>
现在,您可能希望将一些公共属性与这些用户元素相关联,以供页面上的其他 JavaScript 组件使用。例如:用户的年龄、ID 和电子邮件地址。这可能最好指定为 JSON,并且可以通过一个data-
或自定义属性直接附加到用户元素。您可能会发现,这样的解决方案不适合不重要的数据,比如 JavaScript objects/JSON。在这种情况下,在一个“普通”的 JavaScript 对象中为标识元素及其数据的惟一键配对更合适。你可以在本章的后面阅读更多关于这种方法的内容。
将数据与元素配对的常见陷阱
由于快速发展的 web 和 JavaScript 规范,将数据与元素配对变得越来越容易。别担心,我很快会谈到细节。但是即使有了这些进步,生活也不简单。如果这种新的权力没有被负责任地使用,仍然有可能出现麻烦。当然,在现代网络和 JavaScript 出现之前,生活要困难得多。将普通数据附加到元素上是使用原始方法完成的。存储复杂的数据,比如其他的Node
可能会导致内存泄漏。本节涵盖了所有这些内容。
内存泄漏
当将两个(或更多)元素连接在一起时,本能反应是简单地将对其他元素的引用存储在某个公共 JavaScript 对象中。例如,考虑以下标记:
1 <ul>
2 <li>Ford</li>
3 <li>Chevy</li>
4 <li>Mercedes</li>
5 </ul>
这些汽车类型中的每一种都会响应单击事件,当其中一辆汽车被单击时,被单击的汽车必须具有突出的样式,而所有其他汽车必须变得不那么突出。实现这一点的一种方法是将对所有元素的引用存储在一个 JavaScript 数组中,并在单击其中一个列表项时遍历数组中的所有元素。被单击的项目必须是红色的,而其他项目应该设置为默认颜色。我们的 JavaScript 可能看起来像这样:
1 var standOutOnClick = function(el) {
2 el.onclick = function() {
3 for (var i = 0; i < el.typeEls.length; i++) {
4 var currentEl = el.typeEls[i];
5 if (el === currentEl) {
6 currentEl.style.color = 'red';
7 }
8 else {
9 currentEl.style.color = '';
10 }
11 }
12 };
13 },
14 setupCarTypeEls = function() {
15 var carTypes = [],
16 carTypeEls = document.getElementsByTagName('LI');
17
18 for (var i = 0; i < carTypeEls.length; i++) {
19 var thisCarType = carTypeEls[i];
20 thisCarType.typeEls = carTypes;
21 carTypes.push(thisCarType);
22 standOutOnClick(thisCarType);
23 }
24 };
25
26 setupCarTypeEls();
Don’t use inline event handlers
在前面的例子中,我通过元素的onclick
属性为元素分配了一个点击处理程序。这被称为内联事件处理程序,您应该避免这样做。因为我还没有涉及到事件,所以我采用了这种快捷方式来使代码示例尽可能简单,但是您不应该在代码中使用内联事件处理程序赋值。要了解更多关于如何在没有 jQuery 的情况下正确处理事件的信息,请看第十章。
Avoid inline style assignment
在前面的例子中,我通过改变元素的style
属性的color
值来改变<li>
的颜色。我采用这种快捷方式是为了让示例尽可能简单,但是您应该在代码中尽量避免这种类型的样式赋值。正确的方法包括为每个元素删除或添加 CSS 类,并在样式表中为这些特定的类定义适当的样式/颜色。有关如何在没有 jQuery 的情况下正确操作元素 CSS 类的更多信息,请参见第五章。
尽管存在一个很大的隐藏问题,但上述代码可以在所有可用的浏览器中运行,包括 Internet Explorer 6。它演示了一个循环引用,涉及一个 DOM 对象(<li>
元素的 JavaScript 表示)和一个“普通”JavaScript 对象(carTypeEls
数组)。每个<li>
引用carTypeEls
数组,而数组又引用<li>
元素。这是一个很好的例子,说明了 Internet Explorer 6 和 7 中存在的大量内存泄漏。这种泄漏非常严重,以至于即使在页面刷新之后,内存也可能无人认领。幸运的是,微软在 Internet Explorer 8 中修复了这个问题,但这展示了与 HTML 元素一起存储数据的一些早期挑战。
管理数据
对于少量的数据,您可以利用data-
属性或其他定制属性。但是如果需要存储大量数据呢?您也许可以将数据附加到元素的自定义属性上。这被称为 expando 属性。这在前面的例子中有所说明。为了避免潜在的内存泄漏,您可以选择将数据与关联元素的选择器字符串一起存储在 JavaScript 对象中。这确保了对元素的引用是“弱”的。不幸的是,这两种方法都不是特别直观,您会觉得要么是在重新发明轮子,要么是在编写蹩脚的脆弱代码。肯定有更容易的路线。
话说回来,什么是“微不足道”的数据量?什么时候属性变成了不太可行的存储和检索机制?对于以前没有遇到过这个问题的开发人员来说,大量的方法可能有点让人不知所措。可以简单地对所有实例使用 expando 属性吗?与其他方法相比,一种方法的缺点和优点是什么?不要担心,在本章的最后两节中,您不仅会了解在存储元素数据时如何以及何时使用特定的方法,而且还会学习如何轻松有效地做到这一点。
使用适用于所有浏览器的解决方案
尽管在新规范中有一些非常好的方法来读取和跟踪元素数据,比如 ECMAScript 2015 和 HTML5,但我意识到浏览器对其中一些 API 的支持还不够全面。在相对较短的时间内,这些新工具将在绝大多数正在使用的浏览器中实现。在此之前,您应该了解如何使用最常用的 API 来完成这些相同的任务。在某些情况下,本节中描述的方法可能会经受住时间的考验,即使 web 标准在继续发展,它们仍然是最合适和最简单的。
使用数据属性存储少量数据
首先出现在 W3C HTML5 规范中的数据属性, 2 是现有标准的一个例子,它足够简单,可以在所有当前的浏览器中使用。它的简单性和灵活性使得它可以在未来的 web 规范中得到利用。事实上,由于 HTML5 规范中定义了一个相对较新的Element
接口属性,数据属性已经变得更加强大了。
HTML5 规范将data-
属性声明为自定义属性。这两者是一体的。唯一有效的定制属性是一个data-
属性。该规范对data-
属性描述如下:
- 自定义数据属性是名称空间中的属性,其名称以字符串“data-”开头,连字符后至少有一个字符。。。。
该规范还赋予了data-
属性一个特定的用途。它们“旨在存储页面或应用专用的自定义数据,对于这些数据没有更合适的属性或元素。”因此,如果您需要描述锚链接的标题,请使用title
属性。 3 如果您必须为一个段落定义一种不同于在<html>
元素上为文档其余部分定义的语言,您应该使用lang
属性。 4
但是如果您需要为一个<img>
存储一个替代 URL,当图像通过键盘获得焦点或者当用户用一个定点设备(比如鼠标)将鼠标悬停在图像上时,就会使用这个 URL。在这种情况下,没有标准属性来存储这些信息。因此,我们必须利用自定义数据属性:
1 <img src="default.png"
2 data-zoom-url="default-zoomed.png"
3 alt="default image">
聚焦/悬停时显示的图像存储在data-zoom-url
属性中。如果我们想要用场景变化的偏移来注释一个<video>
,我们可以遵循相同的方法:
1 <video src="my-video.mp4" data-scene-offsets="9,22,38">
根据我们绑定到元素的data-scene- offsets
自定义属性,前面的视频在 9 秒、22 秒和 38 秒标记处改变场景。
定义一个不符合 HTML5 规范中定义的data-
约定的定制元素不会产生严重的后果。浏览器不会抱怨或者无法呈现你的文档。但是您将失去利用基于这个约定的 API 的任何未来部分的能力,包括dataset
属性。稍后将详细介绍该特定属性。
用 jQuery 读取和更新数据属性
现在我们有了一种通过标记用一些数据来注释元素的方法,那么我们如何在代码中读取这些数据呢?如果您熟悉 jQuery,您可能已经知道了data()
API 方法。以防细节有点模糊,看看下面的例子:
1 <video src="my-video.mp4" data-scene-offsets="9,22,38">
2
3 <script>
4 // offsets value will be "9,22,38"
5 var offsets = $('VIDEO').data('sceneOffsets');
6 </script>
注意,我们必须通过引用属性名的唯一部分来访问data-
属性的值,作为一个驼峰式字符串。更改数据属性的值非常类似:
1 <video src="my-video.mp4" data-scene-offsets="9,22,38">
2
3 <script>
4 // Does NOT update the attribute. Updates jQuery
5 // internal data store instead.
6 $('VIDEO').data('sceneOffsets', '1,2,3');
7 </script>
请注意,jQuery 的data()
方法有些奇特和出乎意料的地方。当试图通过这个方法更新data-
属性时,似乎什么也没发生。也就是说,data-scene-offsets
属性值在文档中保持不变。相反,jQuery 将这个值和所有后续值存储在一个 JavaScript 数据存储中。这种实现有几个缺点:
- 我们的标记现在与元素的数据不同步。
- 我们对元素数据所做的任何更改只有 jQuery 可以访问。
虽然这种实现有一些很好的理由,但在这种情况下似乎很不幸。
使用 Web API 读取和更新数据属性
稍后,我将描述一种使用 JavaScript 读取和更新data-
属性的更现代的方法,它与 jQuery 的data()
方法一样优雅,但是没有缺点。同时,让我们探索一种适用于任何浏览器的解决方案:
1 <video src="my-video.mp4" data-scene-offsets="9,22,38">
2
3 <script>
4 // offsets value will be "9,22,38"
5 var offsets = document.getElementsByTagName('VIDEO')[0]
6 .getAttribute('data-scene-offsets');
7 </script>
我们已经在前一章的“读取属性”部分看到过这一点。当然,data-
属性只是一个元素属性,所以我们可以在任何使用getAttribute()
的浏览器中轻松读取它。
正如您所料,在没有 jQuery 的情况下更新data-
属性会使用setAttribute()
方法,这要归功于 web API 的Element
接口:
1 <video src="my-video.mp4" data-scene-offsets="9,22,38">
2
3 <script>
4 // updates the element's data attribute value to "1,2,3"
5 document.getElementsByTagName('VIDEO')[0]
6 .setAttribute('data-scene-offsets', '1,2,3');
7 </script>
在这种情况下,这种原始而有效的方法比 jQuery 的data()
方法有两个好处:
- 我们的标记总是与元素的数据同步。
- 任何 JavaScript 都可以访问我们对元素数据所做的任何更改。
因此,在这种情况下,本地解决方案可能是更好的路线。
复杂元素数据存储和检索
简单元素数据由一个短字符串组成,如短语、单词或字符或数字的短序列。也许甚至一个小的 JSON 对象或数组也可以被认为是简单的。但是复杂的数据呢?复杂数据到底是什么?
还记得本章前面内存泄漏部分的汽车列表吗?我演示了一种链接单个列表项元素的方法,这样我们可以很容易地突出显示被单击的项,同时使列表中的其他项不那么突出。我们将 HTML 元素的 JavaScript 表示与其他元素相关联。这当然可以被认为是“复杂”的数据。
如果我们用<video>
标签扩展前面的例子,可以演示复杂元素数据的另一个例子。除了场景偏移,我们还需要记录每个场景的简短描述,以及标题和位置。我们在这里描述的是需要一个适当的 JavaScript 对象,而不是存储为属性值的单个文本字符串。
我在内存泄漏一节中提出的解决方案涉及 expando 属性的使用,这在一定程度上导致了旧浏览器中的内存泄漏。尽管这个漏洞已经在所有现代浏览器中得到修补,但是不鼓励 expando 属性,以任何非标准方式修改元素的 JavaScript 表示也是如此。我之前详述的视频数据场景数据太多,无法存储在一个data-
属性中。当然,我们也不应该求助于 expando 属性。因此,将这些类型的复杂数据与元素相关联的正确方法是维护一个 JavaScript 对象,该对象通过一个data-
属性链接到一个或多个元素。这是 jQuery 采用的方法,没有 jQuery 我们也可以很容易地做到。
熟悉的 jQuery 方法
正如您可能已经猜到的,jQuery 解决方案涉及到了data()
方法:
1 $('VIDEO').data('scenes', [
2 {
3 offset: 9,
4 title: 'intro',
5 description: 'introducing the characters',
6 location: 'living room'
7 },
8 {
9 offset: 22,
10 title: 'the problem',
11 description: 'characters have some issues',
12 location: 'the park'
13 },
14 {
15 offset: 38,
16 title: 'the resolution',
17 description: 'characters resolve their issues',
18 location: 'the cemetery'
19 }
20 ]);
现在,如果我们想查找第二个场景的标题:
1 // variable will have a value of 'the problem'
2 var sceneTwoTitle = $('VIDEO').data('scenes')[1].title;
jQuery 在内部缓存对象中维护我们提供的数组。每个缓存对象都有一个“索引”,这个索引存储为 jQuery 添加到HTMLVideoElement
对象的 expando 属性的值,该对象是一个<video>
标记的 JavaScript 表示。
使用更自然的方法
在决定如何将复杂数据绑定到本节中的元素时,我们必须意识到我们的三个目标:
- 不是 jQuery。
- 必须适用于所有浏览器。
- 没有 expando 属性。
我们可以通过模仿 jQuery 存储元素数据的方法来实现前两个目标。为了尊重第三点,我们必须对 jQuery 的方法进行一些调整。换句话说,我们必须通过一个简单的data-
属性而不是 expando 属性将我们的元素绑定到底层 JavaScript 对象:
1 var cache = [],
2 setData = function(el, key, data) {
3 var cacheIdx = el.getAttribute('data-cache-idx'),
4 cacheEntry = cache[cacheIdx] || {};
5
6 cacheEntry[key] = data;
7 if (cacheIdx == null) {
8 cacheIdx = cache.push(cacheEntry) - 1;
9 el.setAttribute('data-cache-idx', cacheIdx);
10 }
11 };
12
13 setData(document.getElementsByTagName('VIDEO')[0],
14 'scenes', [
15 {
16 offset: 9,
17 title: 'intro',
18 description: 'introducing the characters',
19 location: 'living room'
20 },
21 {
22 offset: 22,
23 title: 'the problem',
24 description: 'characters have some issues',
25 location: 'the park'
26 },
27 {
28 offset: 38,
29 title: 'the resolution',
30 description: 'characters resolve their issues',
31 location: 'the cemetery'
32 }
33 ]);
这是怎么回事?首先,我创建了一个方便的方法(setData
函数)来处理数据与特定元素的关联,并创建了一个数组(cache
)来保存所有元素的数据。已经设置了setData
函数来接受元素、数据键和数据对象,而cache
数组为每个元素保存一个 JavaScript 对象,数据附加到(可能)多个键属性。
当处理一个调用时,我们首先检查元素是否已经绑定到我们的cache
中的数据。如果是,我们使用存储在元素的data-cache-idx
属性中的数组索引在cache
中查找现有的数据对象,然后向该对象添加一个包含传递数据的新属性。否则,我们将创建一个新的对象,该对象被初始化为包含传递的数据和传递的键。如果这个元素在cache
中还没有条目,那么还必须创建一个data-cache-idx
属性,其索引为cache
中的新对象。
与 jQuery 解决方案一样,我们希望查找第二个场景的标题,只需多一点代码就可以完成:
1 var cacheIdx = document.getElementsByTagName('VIDEO')[0]
2 .getAttribute('data-cache-idx');
3
4 // variable will have a value of 'the problem'
5 var sceneTwoTitle = cache[cacheIdx].scenes[1].title;
我们可以很容易地为我们的setData()
创建一个getData()
函数,使存储和查找元素数据更加直观。但是这个全浏览器非 jQuery 解决方案出奇的简单。对于一个更优雅的面向更现代浏览器的非 jQuery 方法,请查看下一节,在那里我将演示dataset
元素属性和WeakMap
API。
当从 DOM 中移除元素时,从缓存中移除数据
我刚才演示的方法的一个潜在问题是缓存将无限增长。当相应的元素从 DOM 中移除时,从缓存中移除项目将是有用的。理想情况下,我们可以简单地“监听”DOM 元素移除“事件”,并相应地从缓存中撤销元素。幸运的是,这在大多数现代浏览器中都是可以实现的,这要归功于MutationObserver
,它是 WHATWG 维护的一个 web 标准,是其 DOM 规范的一部分。5ie 9 和 10 都是钉子户,但是 polyfill 填补了 6 这两个缺口。在MutationObserver
之前,仍然有通过“突变事件”观察 DOM 变化的能力,但是这些被证明是非常低效的,并且不再是任何活动规范的一部分。我刚才提到的聚合填充可以追溯到 IE10 和 ie9 中的突变事件。
突变观察器允许在检测到任何 DOM 元素(或其子元素或后代元素)的任何变化时执行回调函数。这正是我们正在寻找的。更具体地说,当附加到缓存项的 DOM 元素被删除时,我们希望得到通知,以便清理缓存。以缓存示例中的<video>
元素为例。请记住,我们在缓存对象中存储了一些关于视频中出现的各种场景的数据。当<video>
被删除时,缓存条目也应该被删除,以防止我们的缓存不必要地增长。使用突变观察器,我们的代码可能看起来像这样:
1 var videoEl = document.querySelector('video'),
2 observer = new MutationObserver(function(mutations) {
3 var wasVideoRemoved = mutations.some(function(mutation) {
4 return mutation.removedNodes.some(function(removedNode) {
5 return removedNode === videoEl;
6 });
7 });
8
9 if (wasVideoRemoved) {
10 var cacheIdx = videoEl.getAttribute('data-cache-idx');
11 cache.splice(cacheIdx, 1);
12 observer.disconnect();
13 }
14 });
15
16 observer.observe(videoEl.parentNode, {childList: true});
在那里,我们的视频的父元素的子元素的所有变化都被观察到。如果我们直接观察视频元素,当它被删除时,我们不会被通知。传递给我们的观察者的childList
配置选项确保了每当我们的视频或它的任何兄弟被改变时,我们得到通知。当我们的回调函数被调用时,如果我们的视频元素被删除,我们将删除缓存中相应的条目,然后断开我们的突变观察器,因为我们不再需要它。更多关于MutationObserver
、、??、、?? 的信息,请看 Mozilla 开发者网络。
元素数据的未来
在没有 jQuery 的情况下,在所有浏览器中存储琐碎或复杂的数据并不特别困难,但也不是很优雅。对我们来说幸运的是,web 发展很快,两个新的 API 的存在应该会使我们的代码更漂亮,甚至可能更有性能。我将向您展示如何使用 HTML5 dataset
属性管理简单的元素数据,以及如何使用 ECMAScript 2015 集合管理复杂的数据。请记住,本节中的所有内容仅适用于最新的浏览器。在这两种情况下,都不能选择比 Internet Explorer 11 更早的版本。在很短的时间内,随着“现代浏览器”定义的演变以及 Internet Explorer 9 和 10 的退出,所有常见的浏览器都将得到支持。
HTML5 数据集属性
HTML5 规范在 2014 年 10 月成为推荐标准,它在HTMLElement
接口上定义了一个新的属性:dataset
。 8 把这个新属性想象成任何元素对象上都可用的 JavaScript 对象。事实上,它是一个对象,更确切地说是一个DOMStringMap
对象, 9 也在 HTML5 规范中定义。添加到dataset
对象中的任何属性都被反映为文档中元素标签上的data-
属性。您还可以通过检查元素的dataset
对象上的相应属性来读取元素标签上定义的任何data-
属性。在这方面,HTMLElement.dataset
提供了所有你喜欢的关于 jQuery 的data()
方法的行为。这是一种向元素读写数据的直观方式,没有缺点。因为对dataset
对象属性的更改总是与元素的标记同步,反之亦然,这个新的标准属性是处理琐碎元素数据的完美方式。
Element.dataset
目前在“现代”浏览器的子集上可用——不支持 Internet Explorer 9 和 10,但可以使用聚合填充,如 https://www.npmjs.com/package/dataset
。查看下面的代码示例时,请记住这一点。对于我们的第一个演示,让我们重写在前面关于使用 web API 读取和更新data-
属性的部分中显示的第一个代码块:
1 <video src="my-video.mp4" data-scene-offsets="9,22,38">
2
3 <script>
4 // offsets value will be "9,22,38"
5 var offsets = document.querySelector('VIDEO').dataset.sceneOffsets;
6 </script>
在这里,我们将前面的例子简化了很多。注意我们必须如何使用骆驼格形式的data-
属性。可以说,dataset
模型比 jQuery 的data()
方法更直观。我们将所有数据视为对象的属性,这正是 jQuery 在内部表示这些数据的方式。但是当使用 jQuery 的 API 时,我们应该调用函数,将键作为字符串参数传递。
看看第二个代码示例的更现代版本,它演示了如何更改元素或向元素添加数据:
1 <video src="my-video.mp4" data-scene-offsets="9,22,38">
2
3 <script>
4 // updates the element's data attribute value to "1,2,3"
5 document.querySelector('VIDEO').dataset.sceneOffsets = '1,2,3';
6 </script>
元素数据和相关的data-
属性已经更新,所有这些都是用一行简单而优雅的代码完成的。但是我们可以做得更多!因为dataset
是一个 JavaScript 对象,我们可以很容易地从我们的元素中移除数据,就像我们从任何其他 JavaScript 对象中移除属性一样:
1 <video src="my-video.mp4" data-scene-offsets="9,22,38">
2
3 <script>
4 // removes the element's data-scene-offsets attribute
5 delete document.querySelector('VIDEO').dataset.sceneOffsets;
6 </script>
您现在可以看到dataset
实际上是如何超越 jQuery 的data()
方法的便利性的。
利用 ECMAScript 2015 WeakMap 系列
您已经知道如何利用最新的 web 技术将琐碎的数据连接到元素。但是复杂的数据呢?我们可以利用前面的例子,但是也许最新最棒的 web 规范会给我们带来更优雅的解决方案,也许是更直观的解决这类问题的完美方法。
ECMAScript 2015 带来了一个名为 a WeakMap
的新系列。 10 一个WeakMap
可以包含对象的键和任何值——元素、对象、原语等等。在这个新的集合中,键被“弱”持有。这意味着如果没有其他对象引用它们,它们就有资格被浏览器进行垃圾回收。这允许我们安全地使用引用元素作为键!
虽然 WeakMap 仅在最新和最棒的浏览器(Internet Explorer 11+,Chrome 36+,Safari 7.1+)以及 Firefox 6+中受支持,但它提供了一种异常简单的方法来将 HTML 元素与数据相关联。还记得前面演示的全浏览器代码示例吗?让我们使用WeakMap
开始重写它们:
1 var cache = new WeakMap();
2 cache.set(document.querySelector('VIDEO'), {scenes: [
3 {
4 offset: 9,
5 title: 'intro',
6 description: 'introducing the characters',
7 location: 'living room'
8 },
9 {
10 offset: 22,
11 title: 'the problem',
12 description: 'characters have some issues',
13 location: 'the park'
14 },
15 {
16 offset: 38,
17 title: 'the resolution',
18 description: 'characters resolve their issues',
19 location: 'the cemetery'
20 }
21 ]});
多亏了WeakMap
,我们已经成功地消除了早期非 jQuery 示例中的所有样板文件。这种方法的优雅程度相当于 jQuery 的data()
方法,我之前也演示过。查找数据同样简单:
1 // variable will have a value of 'the problem'
2 var sceneTwoTitle = cache.get(document.querySelector('VIDEO')).scenes[1].title;
最后,我们可以通过一个简单的 API 调用来删除我们不想再跟踪的元素,从而进行自我清理:
1 cache.delete(document.querySelector('VIDEO'));
一旦从 DOM 中删除了元素,假设没有其他对该元素的引用,视频元素就应该有资格被浏览器进行垃圾收集。由于 WeakMap 持有的视频元素引用很弱,这本身并不能防止垃圾收集。因为一旦视频元素不再存在于 DOM 中,它就会自动从 WeakMap 中删除,所以我们甚至不需要显式删除这个条目。
没有 jQuery 的 web 看起来非常强大。
Footnotes 1
https://msdn.microsoft.com/en-us/library/dd361842(VS.85).aspx
2
www.w3.org/TR/html5/dom.html#embedding-custom-non-visible-data-with-the-data-%2A-attributes
3
www.w3.org/TR/html4/struct/global.html#edef-TITLE
4
www.w3.org/TR/html4/struct/dirlang.html#adef-lang
5
https://dom.spec.whatwg.org/#mutation-observers
6
https://github.com/webcomponents/webcomponentsjs/blob/v0.7.20/MutationObserver.js
7
https://developer.mozilla.org/en/docs/Web/API/MutationObserver
8
www.w3.org/TR/html5/dom.html#dom-dataset
9
www.w3.org/TR/html5/infrastructure.html#domstringmap-0
10
www.ecma-international.org/ecma-262/6.0/#sec-weakmap-objects
七、样式化元素
如果你习惯于使用 jQuery 的css()
方法来处理文档中的样式,这一章就是为你准备的。我当然可以理解对 API 这一神奇方面的盲目依赖。在 jQuery 的帮助下,调整尺寸、颜色、不透明度和任何其他可以想象的样式都非常容易。不幸的是,这种简单性有时要付出巨大的代价。
jQuery 支持其易于使用的 CSS API 的内部代码有一些值得注意的性能问题。如果您重视效率,并且希望为用户提供最佳体验,那么您应该学习如何使用 web API 正确地操作和读取元素样式。不要依赖“一刀切”的方法,您应该通过绕过 jQuery 的抽象来选择最精简的方法。
您可以继续依赖 jQuery,或者完全放弃它,采用更“自然”的编程方法。但是除了使用哪些 JavaScript 方法和属性之外,还有一些概念需要注意。考虑到 JavaScript 并不总是在文档中定义样式的最佳方式的可能性。除了 HTML 和 JavaScript,浏览器还提供了第三个有价值的工具:样式表。
这本书旨在让你更好地理解浏览器本身提供的选项,每一章都基于你的新知识。在这一章中,你将会学到一些关于使用元素样式的新东西,不管有没有 JavaScript。从这一章你将学到足够的东西来理解什么时候在样式表中使用 CSS 规则来定位元素,而不是每次都求助于 JavaScript。得益于前几章,您对选择器和属性的丰富知识将使这变得更加容易。
有三种方法可以设置元素的样式
在我深入研究与实际调整和从文档中的元素读取样式信息相关的示例和细节之前,首先弄清楚几个关键概念是很重要的。在这一章中,我向你展示了三种不同的处理元素样式的方法。第一种包括直接在标记中管理样式——这是不推荐的,但是可能的。另一种方法是对Element
对象的标准化属性进行修改——如果您打算按需读取或更新样式,可以选择这种方法。最后,我将在样式表中编写 CSS 规则作为第三种选择。
内嵌样式
几章前,我向您介绍了class
属性。尽管该属性通常用于设计元素的样式,但它也用于对元素进行选择和分类。本节介绍了style
属性,它专门用于调整元素的外观。这个属性不是新的;它于 1996 年作为第一个正式的 W3C CSS 规范的一部分首次引入。 1
假设您有一个非常简单的文档,只有几个标题元素和相关内容。你已经决定每个<h2>
应该是蓝色的,每个<h3>
应该是绿色的。作为一名 web 开发新手,或者对样式选项了解不多的开发人员,您可以选择使用style
属性来设置这些标题的颜色,该属性在所有元素上都可用,如清单 7-1 所示。
1 <h1>Fake News</h1>
2 <div>Welcome to fakenews.com. All of the news that's unfit to print.</div>
3
4 <h2 style="color: blue">World</h2>
5
6 <h3 style="color: green">Valdimir Putin takes up knitting</h3>
7 <div>The infamous leader of Russia appears to be mellowing with age as he reportedly joined a local knitting group in Moscow.</div>
8
9 <h2 style="color: blue">Science</h2>
10
11 <h3 style="color: green">Sun goes on vacation, moon fills in</h3>
12 <div>Fed up after over 4 billion years without a day off, the sun headed off to the Andromeda galaxy for a few weeks of rest and relaxation.</div>
Listing 7-1.Setting Styles Using the Style Attribute
在这个例子中,你可以看到标题是如何按照你的要求着色的。您可以在单个元素上放置多种样式,只需用分号分隔这些样式。例如,假设我们不仅要将每个<h2>
涂成蓝色,还要通过加粗来确保它们更加突出:
1 <h2 style="color: blue; font-weight: bold">World</h2>
2
3 ...
4
5 <h2 style="color: blue; font-weight: bold">Science</h2>
任何标准化的样式 2 都可以应用于任何元素,只需使用前面代码片段中所示的style
属性。但是还有其他方法来设计你的元素,你很快就会知道,为什么有人会选择这种特殊的方法呢?首先,直接在标记中指定元素的样式似乎是一种直观而合理的方法。但是使用style
属性很可能是出于懒惰或天真。很明显,用这种方式为元素指定样式是多么容易。
使用style
属性对文档进行样式化,也称为内联样式化,是您几乎应该避免的事情。尽管这种做法简单直观,但有很多原因会让你感到悲伤。首先,内联样式给你的标记增加了相当多的噪音。除了内容、标签和其他属性,现在您有了style
属性——可能是许多元素的属性。这些属性很可能包含许多分号分隔的样式。随着您的文档开始增长并变得更加复杂,这种干扰会变得更加明显。
除了弄乱文档之外,直接在标记中的每个元素上定义样式还会妨碍您轻松地重新设置页面的外观。假设一个设计者看了一下前面的代码,告诉您“绿色”和“蓝色”的颜色值有点太“普通”,应该替换为稍微不同的颜色。然后,设计人员为您提供新颜色的十六进制代码,这种调整需要更改文档中所有<h2>
和<h3>
元素的style
属性。这是不遵循软件开发的“不要重复自己”原则 3 的常见后果。过度使用style
属性会导致维护噩梦。
通过style
属性在文档中定义样式也是一个潜在的安全风险。如果您打算实现一个内容安全策略,在最基本(也是最安全的)策略定义中,严格禁止使用属性来设计元素的样式。强大的内容安全策略,也称为 CSP,现在变得越来越普遍,因为所有现代浏览器(除了 IE9)都至少支持该规范的初始版本。 5
最后,在页面中加入style
属性或者<style>
元素,可以包含不同的 CSS 规则集,会导致更多的开销。如果需要更改某个样式,那么下次用户加载页面时,浏览器必须重新获取整个文档。如果您的样式是在更具体的位置定义的,在您的标记之外,可以引入样式更改,同时仍然允许从浏览器的缓存中获取页面的一部分,避免不必要的服务器往返。
我强烈建议避免使用style
属性。还有其他更合适的选择。最初看得见的好处被困难所掩盖,这些困难将在今后成为残酷的现实。
直接在元素对象上使用样式
元素的对象表示上的style
属性最初是在 2000 年作为 DOM Level 2 的一部分引入的。 6 它被定义为一个新的ElementCSSInlineStyle
接口的唯一属性。Element
接口实现了ElementCSSInlineStyle
,它允许使用 JavaScript 以编程方式设计元素。所有 CSS 属性,比如opacity
和color
,都可以作为相关联的CSSStyleDeclaration
7 实例上的属性来访问,在那里它们可以被读取或更新。
如果对样式属性的所有讨论都不清楚,那么再看一下上一节的代码示例。清单 7-2 利用所有Element
对象上可用的style
属性重写了它。
1 <h1>Fake News</h1>
2 <div>Welcome to fakenews.com. All of the news that's unfit to print.</div>
3
4 <h2>World</h2>
5
6 <h3>Valdimir Putin takes up knitting</h3>
7 <div>The infamous leader of Russia appears to be mellowing with age as he report edly joined a local knitting group in Moscow.</div>
8
9 <h2>Science</h2>
10
11 <h3>Sun goes on vacation, moon fills in</h3>
12 <div>Fed up after over 4 billion years without a day off, the sun headed off to the Andromeda galaxy for a few weeks of rest and relaxation.</div>
13
14 <script>
15 var headings = document.querySelectorAll('h2, h3');
16
17 for (var i = 0; i < headings.length; i++) {
18 if (headings[i].tagName === 'H2') {
19 headings[i].style.color = 'blue';
20 }
21 else {
22 headings[i].style.color = 'green';
23 }
24 }
25 </script>
Listing 7-2.Setting Styles Using the style Property
: All Modern Browsers and Internet Explorer 8
这似乎有点笨拙,但它说明了如何使用 web API 以编程方式更新样式。
在上一节中,我扩展了初始代码片段,以说明如何在单个元素上定义多种样式。让我们看看如何使用style
属性来实现这一点:
1 <h2>World</h2>
2
3 ...
4
5 <h2>Science</h2>
6
7 <script>
8 var headings = document.querySelectorAll('h2');
9
10 for (var i = 0; i < headings.length; i++) {
11 headings[i].style.color = 'blue';
12 headings[i].style.fontWeight = 'bold';
13 }
14 </script>
请注意,font-weight
CSS 样式名称已经转换为 camel case,这是完全合法的,但是我们仍然可以使用虚线名称来更改这种样式,如果我们真的想这样做的话:headings[i].style['font-weight'] = 'bold'
。
我们还没有完成。还有一种方法可以使用style
属性在一个 HTML 元素上设置多种样式。CSSStyleDeclaration
接口定义了一个特殊的属性:cssText
。这允许您对关联的元素读写多种样式。值字符串看起来就像一组用分号分隔的 CSS 规则,如清单 7-3 所示。
1 <h2>World</h2>
2
3 ...
4
5 <h2>Science</h2>
6
7 <script>
8 var headings = document.querySelectorAll('h2');
9
10 for (var i = 0; i < headings.length; i++) {
11 headings[i].style.cssText = 'color: blue; font-weight: bold';
12 }
13 </script>
Listing 7-3.Setting Multiple Styles Using the
style.cssText Property
: All Modern Browsers and Internet Explorer 8
为什么您可能想要在一个元素(或多个元素)上使用style
属性?也许您正在编写一个 JavaScript 库,需要根据环境或用户输入对一些元素进行一些快速调整。为这些样式创建和依赖特定于库的样式表可能不方便。此外,使用此方法设置的样式通常会覆盖先前在元素上设置的任何其他样式,这可能是您的意图。
但是要小心过度使用这种力量。以这种方式设置的样式很难通过样式表规则覆盖。这可能是你的意图,但也可能不是。如果不是这样,并且您希望允许样式表轻松地对样式进行调整,那么您可能希望避免使用style
属性(或内联样式)来更改样式。最后,使用style
属性会使跟踪样式变化变得非常困难,并且会弄乱您的 JavaScript。您的代码专注于设置特定的元素样式似乎不太自然。这应该是一种罕见的做法。正如您将在下一节看到的,这项工作更适合样式表。
样式表
JavaScript 并不是解决浏览器样式问题的唯一方法。这甚至可能不是改变元素外观的最佳方式。浏览器提供了一种专门的机制来设计文档的样式:样式表。通过这个媒介,你可以在专用文件中为你的 web 文档定义所有的 CSS 样式,封装在一个特定的 HTML 元素中,或者甚至通过 JavaScript 按需将它们添加到文档中。在本节中,我将演示这三种处理样式的方法。
首先在 W3C CSS 1 规范中定义的<style>
元素, 8 让我们将整个文档的所有样式分组到一个方便的位置。清单 7-4 是对之前代码片段的重写,这一次添加了来自HTMLStyleElement
的样式。
1 <style>
2 h2 { color: blue; }
3 h3 { color: green; }
4 </style>
5
6 <h1>Fake News</h1>
7 <div>Welcome to fakenews.com. All of the news that's unfit to print.</div>
8
9 <h2>World</h2>
10
11 <h3>Valdimir Putin takes up knitting</h3>
12 <div>The infamous leader of Russia appears to be mellowing with age as he report edly joined a local knitting group in Moscow.<div>
13
14 <h2>Science</h2>
15
16 <h3>Sun goes on vacation, moon fills in</h3>
17 <div>Fed up after over 4 billion years without a day off, the sun headed off to the Andromeda galaxy for a few weeks of rest and relaxation.<div>
Listing 7-4.Setting Styles
Using the <style> Element: All Browsers
如您所见,前一节中用于样式化这些元素的所有 JavaScript 代码完全被两行 CSS 所取代。这不仅是一个更有效的解决方案,而且更加优雅和简单。如果我们想要添加额外的样式,我们可以很容易地将它们包含在现有的样式中,用分号分隔:
1 <style>
2 h2 {
3 color: blue;
4 font-weight: bold;
5 }
6 h3 {
7 color: green;
8 font-weight: bold;
9 }
10 </style>
11 ...
前面的样式甚至可以使用多重选择器的功能进行一点改进,您在前面已经了解过了:
1 <style>
2 h2, h3 { font-weight: bold; }
3 h2 { color: blue; }
4 h3 { color: green; }
5 </style>
6 ...
将样式塞进一个<style>
元素对于一小组规则来说可能没问题,但是对于一个复杂的文档来说可能不太理想。也许您甚至希望在文档/页面之间共享样式。在每个 HTML 文档中复制这些样式似乎不是一种可伸缩的方法。幸运的是,有一种更好的方法——样式表——如清单 7-5 和 7-6 所示。
1 h2 { color: blue; }
2 h3 { color: green; }
Listing 7-5.styles.css External
Style Sheet: All Browsers
1 <link href="styles.css" rel="style sheet">
2
3 <h1>Fake News</h1>
4 <div>Welcome to fakenews.com. All of the news that's unfit to print.</div>
5
6 <h2>World</h2>
7 ...
Listing 7-6.
index.html Setting Styles
Using an External CSS Style Sheet File: All Browsers
我们在这里定义了两个文件:styles.css 和 index.html。第一个存放我们的样式表,第二个包含我们的标记。在我们的索引文件中,我们可以简单地通过<link>
元素引用 styles.css 文件来引入所有这些样式,这可以在 HTML 2.0 规范中看到。 9 这对于你们很多人来说可能不是什么新知识,但是当你习惯于使用 jQuery 这样的工具时,很容易忽略全局,jQuery 有时似乎是所有浏览器问题的解决方案。
完全依赖任何形式的 JavaScript(包括通过 jQuery 的 API)来设计您的标记是不合适的。级联样式表就是为此而存在的。但这并不意味着永远不会出现直接通过 JavaScript 动态改变样式的情况。也许您已经构建了一个 web 应用,允许您的用户创建他们自己的自定义登录页面。你的用户需要用斜体显示所有的副标题。要轻松做到这一点,您可以使用CSSStyleSheet
接口上的insertRule
方法以编程方式将 CSS 规则添加到文档中:
1 // This grabs the first loaded style sheet on the current page.
2 // This also assumes the first style sheet is appropriate here.
3 var sheet = document.style Sheets[0]
4
5 sheet.insertRule(
6 'h2 { font-style: italic; }', sheet.cssRules.length - 1
7 )
前面的例子将创建一个新的样式,用斜体显示所有的<h2>
元素。该规则将被追加到样式表的末尾。style sheet
变量可以引用我们为这些动态样式按需创建的<style>
元素,或者甚至是使用<link>
标签导入的现有样式表。如果你需要支持 Internet Explorer 8,你必须使用addRule
,如果它是在浏览器的 DOM API 实现中定义的。
与只使用 JavaScript 的解决方案相比,使用样式表几乎总是更好的方法。即便如此,采取整体方法,根据情况将 JavaScript、HTML 和样式表合并到您的解决方案中,通常也是可以接受的。
既然您对可能性有了更完整的理解,那么您就能更好地在自己的项目中做出正确的决策。本章的其余部分将致力于更具体的样式化情况。按照 Beyond jQuery 的惯例,我使用熟悉的 jQuery 方法作为参考,后面是丰富的 web API 示例,作为备选方案讨论的一部分。
获取和设置通用样式
在描述和演示了向 HTML 元素添加样式的几种不同方法之后,现在是时候更深入地研究 CSS 了。如果您熟悉 jQuery(如果您正在阅读这本书,您可能已经熟悉了),那么您已经知道在使用 jQuery 时,通常有一条调整文档外观的捷径。我将提供一个演示,以供参考。但是浏览器栈提供的原生路由要丰富得多。在这一节中,您将看到如何在没有 jQuery 帮助的情况下正确地获取样式并动态地设置它们。
要设置下面的 jQuery 和非 jQuery 演示,让我们从一个简单的 HTML 片段开始:
1 <button>cookies</button>
2 <button>ice cream</button>
3 <button>candy</button>
假设您想在按钮被单击(或通过键盘选择)后对其进行稍微不同的样式化。被点击的按钮应该以某种方式来表示它已经被选中。我还没有介绍事件处理程序(尽管我将在后面的章节中介绍),所以只要假设已经存在一个函数,并且每当按钮被选中时,相关的按钮元素都作为参数传递进来。您的工作是通过将所选按钮的背景和边框颜色更改为蓝色,并将按钮文本更改为白色来填充该函数的实现。
为了演示阅读样式(并进一步演示如何设置它们),考虑一个已经被样式化为 box 的元素。每当这个框被点击,它变得稍微不透明,直到它完全消失。同样,假设每次单击盒子时都会向您传递一个函数。你的工作是每当这个函数被调用时,增加 10%的不透明度。在本节中,我将向您介绍这两种解决方案,从(可能)熟悉的 jQuery 方法开始。
使用 jQuery
jQuery 是一个非常受欢迎的 JavaScript 库,被太多的开发人员所依赖,它有责任(依我拙见)教这些开发人员调整元素样式的正确方法。不幸的是,它没有这样做。甚至 jQuery 学习中心关于样式化的文章 10 也只是简单地提到了如何正确地样式化元素,根本没有任何关于这种技术的真实演示。原因很简单:惯用的 jQuery 经常与最佳实践不一致。这个事实是这本书的几个灵感之一。让我们看看大多数关注 jQuery 的开发人员是如何解决这个问题的:
1 function onSelected($selectedButton) {
2 $selectedButton.css({
3 color: 'white',
4 backgroundColor: 'blue',
5 borderColor: 'blue'
6 });
7 }
当向元素写入样式时,css
方法充当了HTMLElement
接口上的style
属性的包装器。毫无疑问,这很优雅,但这真的是正确的方法吗?答案当然是“不”。我之前讨论过这个问题。当然,前面描述的方法并不是使用 jQuery 解决这个问题的唯一方法,但它是 jQuery 开发人员中最常见的方法。
现在,让我们看看如何使用 jQuery 解决第二个问题:
1 function onClicked($clickedBox) {
2 var currentOpacity = $clickedBox.css('opacity');
3
4 if (currentOpacity > 0) {
5 $clickedBox.css('opacity', currentOpacity - 0.1);
6 }
7 }
可惜 jQuery 的css
API 方法效率相当低。每次调用这个方法来查找一个样式都需要 jQuery 利用window
对象上的getComputedStyle()
方法,这在第一次调用之后是完全没有必要的,并且给这个解决方案增加了大量的处理开销。
不使用 jQuery
解决第一个问题的正确方法是将 CSS 规则包含在外部样式表中,并使用最少的 JavaScript 触发这些规则。请记住,我们正在寻找一个按钮的风格,当它被选中/按下时,突出。当按钮被按下时,我们可以期望调用一个函数,并将元素作为参数传递。
让我们从在样式表中为被按下的按钮定义样式开始,如清单 7-7 所示。
1 button.selected {
2 color: white;
3 background-color: blue;
4 border-color: blue;
5 }
Listing 7-7.
styles.css Pressed Button Styles
: All Browsers
当按钮被按下时,我们需要做的就是向元素添加一个 CSS 类来触发 styles.css 文件中定义的样式。现在我们需要实现将“selected”类添加到该按钮的函数,以便触发样式表中定义的样式规则:
1 function onSelected(selectedButton) {
2 selectedButton.className += ' selected';
3 }
接下来是一行代码,用于导入 CSS 文件、我们的 button 元素和函数,该函数在被调用时触发按钮上先前定义的样式规则:
1 <link rel="style sheet" href="styles.css">
2 <script src="button-handler.js"></script>
3 <button>demo button</button>
这种方法有几个优点。首先,它展示了关注点的分离。换句话说,显示规则属于样式表,行为属于 JavaScript 文件,内容属于 HTML 文件。这种分离使得维护更加简单,潜在的风险也更小。它还确保了,例如,如果调整了样式,浏览器会继续缓存 HTML 和 JavaScript 文件。如果所有这些逻辑都被塞进一个 HTML 文件中,那么不管更改的范围有多大,整个文件都必须由浏览器重新下载。
将这些样式绑定到 CSS 类并在外部样式表中定义的另一个优点是,这些样式可以在本文档或任何其他文档中方便地重用。惯用的 jQuery 方法让我们一遍又一遍地复制和粘贴相同的样式,因为我们是内联定义它们的。
第二种情况呢?记住,我们希望每次点击时增加 10%的不透明度。同样,我们得到了一个函数,每当单击 box 元素时都会调用这个函数:
1 function onClicked(clickedBox) {
2 var currentOpacity = clickedBox.style.opacity ||
3 getComputedStyle(clickedBox, null).opacity;
4
5 if (currentOpacity > 0) {
6 clickedBox.style.opacity = currentOpacity - 0.1;
7 }
8 }]
我们优化的非 jQuery 方法代码多一点,但比惯用的 jQuery 解决方案快得多。 11 这里,当元素的style
属性上没有定义样式时,我们只利用对getComputedStyle
12 的昂贵调用。getComputedStyle
不仅通过检查元素的style
属性,还通过查看任何可用的样式表来确定元素的实际样式。因此,这个操作可能有点昂贵,所以我们避免它,除非绝对必要。
设置和确定元素可见性
显示和隐藏元素是 web 开发中的一个常见问题。这些任务可能并不简单,但是通过编程来确定一个元素是否可见往往更加复杂。传统上,元素可见性是开发人员要处理的一个令人困惑的问题。但不一定要这样。处理元素可见性有两种方法:您一直使用的方法(使用 jQuery)和正确的方法(不使用 jQuery)。您将看到 jQuery 在这种情况下是多么低效,以及这如何说明为什么盲目相信这种类型的软件库是危险的。
典型的 jQuery 方法
使用 jQuery 显示、隐藏和确定元素可见性的好处是简单。你很快就会发现,这是唯一的好处。但是现在,让我们把重点放在这个优势上。
用 jQuery 隐藏和显示元素几乎总是分别使用show()
和hide()
API 方法来完成。没有必要创建一个 HTML 片段来演示这些方法,所以让我们来深入研究几个代码示例:
1 // hide an element
2 $element.hide();
3
4 // show it again
5 $element.show();
这些代码都不需要进一步阐述。真正需要进一步检查的是实际执行这些操作的底层代码。不幸的是,这两种方法都使用了window.getComputedStyle
,这是上一节讨论的方法。在某些情况下,尤其是使用hide()
,getComputedStyle()
可能会被多次调用。这会产生严重的性能后果。为什么仅仅隐藏或显示一个 DOM 元素就需要如此强大的处理能力?在很大程度上,这两个常用 API 方法下面的所有聪明但通常不必要的代码都是为了处理样式边缘情况,否则很难显示或隐藏目标元素。正如我之前所说,元素可见性不一定是一个复杂的问题。通过采用一种更简单的方法,我们可以避免 jQuery 隐藏和显示元素所需的所有 CPU 周期。在下一节中,我将讨论解决这个问题的“本地 web 方法”。
如果我们需要弄清楚一个特定的元素是否被隐藏了呢?jQuery 也让这变得非常简单:
1 // is the element visible?
2 $element.is(':visible');
3
4 // conversely, is the element hidden?
5 $element.is(':hidden');
jQuery 决定发明几个新的伪类来表示元素可见性。即使是 jQuery 的创造者 John Resig,也详细讲述了这种新的创新 jQuery 混合物的有用性。 13 但是就像show()
、hide()
和css()
API 方法一样,这两个非标准的伪类都相当慢。同样,他们再次委托给window.getComputedStyle()
,有时每次调用多次。
在下一节中,我将概述几种显示和隐藏元素以及确定元素可见性的非 jQuery 方法。本机方法和 jQuery 方法之间的性能差异也将包括在内,至少可以说,这些差异是显著的。
原生 Web 方法
最终,jQuery 长期以来切换元素可见性的方法异常复杂,这导致了潜在的严重性能问题。重新思考这个问题后,很明显最简单的方法是最好和最有效的方法。jQuery 3.0 发行说明甚至建议使用与适当的 CSS 相关联的类名来显示或隐藏元素。
jQuery 中隐藏、显示和评估元素可见性的简单性非常引人注目。在这一部分,您可能希望我说这样的话,“没有 jQuery 做所有这些有点困难”,或者“没有 jQuery 有一个简单的方法来解决这个问题,但是它需要使用最先进的浏览器。”实际上,在任何没有 jQuery 的浏览器中显示、隐藏和确定元素可见性都非常容易。jQuery 开发人员可能希望您相信这些是要解决的复杂问题,并且您需要 jQuery 来解决它们,但是这些都不是真的。在这一节中,我将演示一些简单的约定,它们将产生简单的解决方案。
有许多方法可以隐藏一个元素。一些非常规的方法包括将元素的opacity
设置为 0,或者将position
设置为“absolute”并将其放置在可见页面之外。这些和其他类似的方法可能是有效的,但是它们通常被认为是“拼凑的”因此,在试图隐藏元素时,通常不鼓励使用这些方法。请不要这样做;有更好的方法。
更合理的方法是将元素的display
style 属性设置为“none”。正如您已经了解到的,有许多不同的方法来调整元素的样式。但是您也知道了最好的方法是在外部样式表中定义这种样式。因此,最好的解决方案可能是在样式表中定义一个定制的 CSS 类或属性,为这个选择器包含一个display: none
样式,然后在需要隐藏时将相关的类或属性添加到这个元素中。
那么,我们应该选择哪个呢?属性还是 CSS 类?这真的重要吗?W3C HTML5 规范定义了一个hidden
布尔属性, 14 ,正如您所料,它允许您通过将该属性添加到元素中来隐藏元素。这种标准化的属性不仅允许您轻松隐藏元素,还增强了标记的语义,并为所有屏幕阅读器提供了有用的提示。 15 没错,它甚至让你的元素更易接近。因为hidden
属性是正式规范的一部分,它不仅仅是一个约定——它代表了处理元素可见性的标准化方法。
此时,您可能正在检查哪些浏览器支持该属性。让我给你省点事吧——不是全部。事实上,微软直到 Internet Explorer 11 才首次支持hidden
属性。幸运的是,标准化hidden
属性的 polyfill 非常简单和优雅:只需将清单 7-8 中所示的规则添加到您的全局样式表中。
1 [hidden] { display: none; }
Listing 7-8.Polyfill for Standardized Hidden Attribute
: All Browsers
Making sure your element is always hidden
native hidden
属性将一个元素标记为“不相关”,这并不总是意味着该元素对眼睛是不可见的。例如,如果一个元素有一个显式声明的display
样式,比如display: block
,那么原生的hidden
属性不会将它从视图中移除。此外,简单地为该属性包含前面的“polyfill”并不总是能确保元素从视图中隐藏。这是由于 W3C 的 CSS2 规范中概述的特异性规则。 16 特异性决定了与一个元素相关联的几个竞争样式中的哪一个“胜出”例如,如果一个display: block
规则指向具有更高特异性的相同元素,那么该元素将保持可见。如果您希望任何具有hidden
属性的元素永远不可见,那么您必须在样式表中利用以下规则:
1 [hidden] { display: none !important; }
给定前面的单行聚合填充,您可以使用以下 JavaScript 行隐藏任何浏览器中的任何元素:
1 element.setAttribute('hidden', '');
隐藏元素不可能更简单、更优雅或更高效。这种方法比 jQuery 的hide()
API 方法快得多。事实上,jQuery 的hide()
方法要慢 25 倍以上! 17 没有理由继续使用 jQuery 来隐藏元素。
由于隐藏元素的最简单和最有效的方法是添加属性,所以您可能不会惊讶地发现,只需通过删除相同的属性就可以显示相同的元素:
1 element.removeAttribute('hidden');
因为我们遵循这个惯例——添加一个属性来隐藏元素,然后删除它来再次显示元素——确定元素的可见性很简单。我们所需要做的就是检查元素中是否存在这个属性,这在所有著名的浏览器中都是一个简单的操作:
1 // the element is hidden if this returns true
2 element.hasAttribute('hidden');
是的,真的很简单。
确定任何元素的宽度和高度
在我回顾 jQuery 如何允许您检查元素的宽度和高度,以及如何在不使用 DOM 抽象的情况下轻松做到这一点之前,您需要理解一些基本概念。智能计算任何元素的宽度和高度所需的最关键的规范是盒子模型。 18
每个元素都是一个盒子。再说一次:每个元素都是一个盒子。这很简单,但对于许多 web 开发人员来说却非常令人惊讶。一旦你从这种认识的最初震惊中走出来,下一步就是理解一个元素的盒子是如何被分割的。这被称为盒子模型。看图 7-1 ,来自万维网联盟 CSS 2.1 规范的盒子模型图。
img/A430213_1_En_7_Fig1_HTML.jpg)
图 7-1。
The box model. Copyright 2015 W3C. License available at www.w3.org/Consortium/Legal/2015/copyright-software-and-document
.
正如您所看到的,一个元素,也是一个框,由四个“层”组成:内容、填充、边框和边距。简单地说,元素的内容、填充和边框用于确定其高度和宽度。边距不被视为元素“尺寸”的一部分,它们只是将其他元素推开,而不是影响元素的高度和宽度。你如何测量宽度和高度很大程度上取决于你关心的是前三层中的哪一层。一般来说,元素的维度可以考虑这三个层(内容层和填充层)的子集,也可以考虑所有三个层。jQuery 采用了不同的方法,只考虑内容维度。接下来会有更多。
使用 jQuery 检查元素
就像 web API 一样——在下一节中描述——有许多方法可以使用 jQuery 的 API 发现元素的维度。你可能已经知道一些甚至所有这些方法。您可能没有意识到 jQuery 内置元素维方法的糟糕性能——大多数开发人员都没有意识到。你为什么要这么做?这些方法是如此简单和优雅,以至于在性能方面付出巨大代价的可能性并不常见。您可能相信这个关键的库不会以任何明显的方式降低应用的效率。但是你错了。
最明显的 API 方法是width()
和height()
。还记得图 7-1 中的箱型图吗?这两个 jQuery 方法只测量元素框的“内容”部分。这听起来是一种合理的行为,但它不一定是完整的表示,因为内容只占元素实际宽度和高度的一部分。记住,边距是盒子模型中唯一不直接影响元素可见宽度和高度的元素。还要记住,jQuery 并不神奇——它必须将所有 API 方法委托给 web API。web API 没有提供一种简单的方法来确定元素内容的维度。因此,jQuery 必须执行一些令人不快的操作来确定这些值,结果牺牲了性能。当我在下一节演示如何使用 web API 计算元素的宽度和高度时,我将向您展示 jQuery 的其他宽度和高度 API 方法实际上是多么低效。
浏览器本身提供的选项
尽管 jQuery 的width
和height
是流行的方法,但是在任何 web 规范中都找不到类似的方法或属性对。这些方法的吸引力可能与它们暗示性的名字有关。
为了更好地说明本节中的代码,我将从一个简单的元素开始,它占据了盒子模型所有四个部分的空间:
1 <style>
2 .box {
3 padding: 10px;
4 margin: 5px;
5 border: 3px solid;
6 display: inline-block;
7 }
8 </style>
9 <span class="box">a box</span>
内容的宽度和高度+填充
要获得前一个框的宽度或高度,仅考虑内容和填充值,我们可以使用Element
界面上的clientWidth
19 和clientHeight
20 属性。这些可以与 jQuery 的innerWidth()
和innerHeight()
API 方法相媲美,但是 web API 比 jQuery 的解决方案具有显著的性能优势。 21 原生解快十倍左右!
这些属性首先在 W3C 起草的级联样式表对象模型(CSSOM)视图规范中定义。截至 2016 年年中,CSSOM 规范还不是一个推荐标准——事实上,它只是一个工作草案。但是这两个Element
属性,以及本规范中表示的许多其他项目,已经被浏览器支持了很长时间。例如,Element.clientWidth
和Element.clientHeight
属性从 Internet Explorer 6 开始就一直受到支持,然而它们目前只在这个工作草案规范中定义。这似乎有点奇怪,不是吗?确实如此,但是 CSSOM 规范是一个特殊的规范。它的存在主要是为了编纂和正式标准化长期存在的 CSS 相关的浏览器行为。Element.clientWidth
和Element.clientHeight
就是两个这样的例子,但你也会在本节中看到其他例子。
清单 7-9 显示了clientWidth
和clientHeight
在我们之前的标记中的<span>
上返回了什么。
1 // returns 38
2 document.querySelector('.box').clientHeight;
3
4 // returns 55
5 document.querySelector('.box').clientWidth;
Listing 7-9.Find width/height of Content + Padding: Web API, Modern Browsers, and Internet Explorer 8
请注意,前面的返回值可能会因浏览器而略有不同,因为默认字体和样式也可能会因浏览器而略有不同。这将最终导致元素内容大小的微小变化,这是意料之中的。
这里还有一些你可能没有意识到的东西。注意到附加在我们的<span>
元素上的display: inline- block
样式了吗?将其拆下,再次检查clientWidth
和clientHeight
的返回值。如果没有这个样式,这两个属性都报告一个值0
。默认情况下,所有浏览器都将<span>
元素呈现为display: inline
,内联元素总是将0
报告为它们的clientWidth
和clientHeight
。使用这些属性时,请记住这一点。注意,浮动一个默认的行内元素也将允许您以这种方式计算宽度和高度。
作为比较,jQuery 的width()
和height()
方法分别返回35
和18
。请记住,这些方法只考虑元素的内容,忽略填充、边框和边距。
内容的宽度和高度+填充+边框
如果在报告元素的宽度和高度时需要包含边框,该怎么办?也就是内容、填充、边框?简单—使用HTMLElement.offsetWidth
23 和HTMLElement.offsetHeight
。 24 这两个属性都可以与 jQuery 的outerWidth()
和outerHeight()
方法相媲美,如清单 7-10 所示。
1 // returns 44
2 document.querySelector('.box').offsetHeight;
3
4 // returns 61
5 document.querySelector('.box').offsetWidth;
Listing 7-10.Find width/height of Content + Padding + Border: Web API, Modern Browsers, and Internet Explorer 8
正如所料,这些值比clientHeight
和clientWidth
报告的值稍大,因为我们也考虑了边界。事实上,每个值正好大 6 个像素。这是预期的,因为在我们的<style>
元素中定义了每边 3 个像素的边框。
同样,由于浏览器对元素内容进行样式化的方式不同,上面的返回值可能会因浏览器而略有不同。另外,offsetHeight
和offsetWidth
不需要display: inline-block
——它们不会报告内联元素的零高度和宽度。
关于样式元素还有很多要讨论的,但是这本书讲的更多。我已经为您提供了一些重要的概念,这些概念将使您在面临其他与样式相关的挑战时不再依赖 jQuery。
Footnotes 1
www.w3.org/TR/REC-CSS1/#containment-in-html
2
3
4
https://developer.mozilla.org/en-US/docs/Web/Security/CSP/Introducing_Content_Security_Policy
5
www.w3.org/TR/2012/CR-CSP-20121115/
6
www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-ElementCSSInlineStyle
7
www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSStyleDeclaration
8
9
www.w3.org/MarkUp/html-spec/html-spec_toc.html#SEC5.2.4
10
https://learn.jquery.com/using-jquery-core/css-styling-dimensions/
11
http://jsperf.com/jquery-css-vs-optimized-non-jquery-approach3
12
www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-CSSview-getComputedStyle
13
http://ejohn.org/blog/selectors-that-people-actually-use/
14
www.w3.org/TR/html5/editing.html#the-hidden-attribute
15
www.html5accessibility.com/tests/hidden2013.html
16
www.w3.org/TR/CSS2/cascade.html#specificity
17
http://jsperf.com/jquery-hide-vs-setattribute-hidden
18
19
www.w3.org/TR/cssom-view/#dom-element-clientwidth
20
www.w3.org/TR/cssom-view/#dom-element-clientheight
21
http://jsperf.com/innerheight-vs-element-clientheight
22
23
www.w3.org/TR/cssom-view/#dom-htmlelement-offsetwidth
24
www.w3.org/TR/cssom-view/#dom-htmlelement-offsetheight
八、DOM 操作
web API 最令人困惑和误解的方面之一与 DOM 操作有关。我怀疑您已经习惯了通过 jQuery 使用 DOM 元素。但是在这方面有必要继续依赖一个库吗?在本章中,我将向您展示如何在没有第三方代码帮助的情况下创建、更新和移动元素和元素内容。您将会体会到在几乎所有浏览器中使用 DOM 是多么容易。
DOM:Web 开发的核心组件
你很可能听说过 DOM。这是 web 开发领域的一个常用术语。但是也许 DOM 对你来说仍然是一个非常神秘的词。DOM 到底是什么,为什么它如此重要?jQuery 如何以及为什么将 DOM API 从开发人员手中抽象出来?
正如 W3C 维护的早期文档之一所述, 1 “文档对象模型的历史,即众所周知的 DOM,与 JavaScript 和 JScript 脚本语言的起源紧密相关。”这个模型表达了一个 API,通常用 JavaScript 实现,用于 web 浏览器,允许对 HTML 文档进行编程访问。除了处理属性、选择元素和存储数据之外,DOM API 还提供了创建新内容、删除元素和在文档中移动现有元素的方法。DOM API 的这些特定方面是本章的主要焦点。
jQuery 的存在是因为 DOM API
好吧,这不是创建 jQuery 的唯一原因,但肯定是原因之一。jQuery 的创始人 John Resig 在 2009 年雅虎的一次演讲中称 DOM 为“一团乱麻”。因此,这可能会让我们对 jQuery 要解决的问题有所了解。在 Resig 演讲的几年前,jQuery 1.0 发布了(2006 年),它包含了大约 25 种特定于 DOM 操作的方法。这约占整个空气污染指数的 17%。即使 API 已经发展到可以处理 web API 的所有方面,DOM 操纵函数仍然只占 15%。
当然,jQuery 为使用 DOM 提供了一些优雅的解决方案。但是它们真的有必要吗?当然不是!DOM 还“破”吗?在我看来不是。DOM API 可能有些粗糙,但是它非常容易使用,非常可靠。没有 jQuery 的帮助,您可以轻松地操作 DOM。
DOM API 没有坏,只是被误解了
在瑞西格在雅虎的演讲中,他说“几乎每一个 DOM 方法在某些浏览器中都以某种方式被破坏了。”尽管这有点夸张,但在 2009 年可能还是有一定道理的。然而,当前的浏览器时代描绘了一幅截然不同的画面。浏览器还是会有 bug,但是所有软件都是这样,甚至 jQuery 也是。jQuery 团队显然已经知道这个库不再是抵御浏览器漏洞的屏障,这一点从他们对特定于浏览器的问题的回应中可以明显看出。 4
jQuery 的主要目标之一一直是保护开发人员免受 DOM API 的影响。为什么呢?因为它在历史上一直是一团糟。当试图以编程方式更新页面标记时,它也被视为一条混乱的路径。这有点道理,但只是因为许多开发人员没有花时间学习 DOM API。正如你将在本章看到的,使用 DOM 并不像你想象的那么难,并且在所有流行的浏览器中都得到了很好的支持。jQuery 的 API 所提供的便利是毋庸置疑的。一旦您对 jQuery 本身所依赖的底层 DOM API 方法有了更好的理解,您甚至可以更好地使用 jQuery,如果您继续使用它的话。
移动和复制元素
在第一部分中,我将重点关注移动和复制现有的元素。您将学习如何在 DOM 中的任何地方插入元素,改变相邻元素的顺序,以及克隆一个元素。我将演示在Document
、、 5 、、 6 、、 7 、等接口上出现的方法。您将看到如何使用 jQuery 执行基本的 DOM 操作,然后是单独使用 DOM API 的相同任务的解释和演示。
在 DOM 中移动元素
我将从我的一个专利(正在申请中的)样本文档开始这一部分。为了主要关注可演示方法的功能,我将保持简单明了。不可否认,这导致了一个有些做作的例子,但我觉得它将很容易让我们在 DOM 操纵的上下文中比较 jQuery 和 DOM API。
我们这个超级简单的文档由冰淇淋的几个不同类别和属性组成:口味、类型和一个未分配类型和口味的部分。这份文件代表了冰淇淋店顾客的一些选择。使用这个标记,我们将解决几个“问题”,首先是 jQuery,然后是普通的旧 DOM API。
第一个挑战是根据受欢迎程度,按降序对口味和种类进行重新排序。巧克力是最受欢迎的口味,其次是香草和草莓。我们必须改变口味列表项的顺序来反映它们的流行程度,但是类型列表已经有了正确的顺序。
第二,我们真的想首先向读者展示冰淇淋的种类,然后是口味。目前的订单,首先包括口味,众所周知不太理想,因为我们的客户希望在决定口味之前先被告知类型。
最后,我们需要将“未分配”部分中的项目分配到适当的类别中。“Rocky road”是一种没有香草那么受欢迎,但比草莓更受欢迎的口味。“gelato”是其中最不受欢迎的一种:
1 <body>
2 <h2>Flavors</h2>
3 <ul class="flavors">
4 <li>chocolate</li>
5 <li>strawberry</li>
6 <li>vanilla</li>
7 </ul>
8
9 <h2>Types</h2>
10 <ul class="types">
11 <li>frozen yogurt</li>
12 <li>custard</li>
13 <li>Italian ice</li>
14 </ul>
15
16 <ul class="unassigned">
17 <li>rocky road</li>
18 <li>gelato</li>
19 </ul>
20 </body>
解决了上述问题后,我们的文档应该是这样的:
1 <body>
2 <h2>Types</h2>
3 <ul class="types">
4 <li>frozen yogurt</li>
5 <li>Italian ice</li>
6 <li>custard</li>
7 <li>gelato</li>
8 </ul>
9
10 <h2>Flavors</h2>
11 <ul class="flavors">
12 <li>chocolate</li>
13 <li>vanilla</li>
14 <li>rocky road</li>
15 <li>strawberry</li>
16 </ul>
17
18 <ul class="unassigned">
19 </ul>
20 </body>
使用 jQuery Mobile 元素
为了正确排列口味,“香草”必须移到“巧克力”之后。为此,我们必须利用 jQuery 的after()
API 方法:
1 var $flavors = $('.flavors'),
2 $chocolate = $flavors.find('li').eq(0),
3 $vanilla = $flavors.find('li').eq(2);
4
5 $chocolate.after($vanilla);
对于我们的第二个挑战,我们必须将“类型”列表和标题(<h2>
)移到“口味”列表之前。我们可以利用这一事实,即这意味着标题和列表必须是<body>
元素中的第一组子元素。首先,我们使用prependTo()
方法在<body>
前添加“类型”标题,然后在新移动的标题后插入“类型”列表,再次使用 jQuery 的after()
方法:
1 var $typesHeading = $('h2').eq(1);
2
3 $typesHeading.prependTo('body');
4 $typesHeading.after($('.types'));
最后,我们需要将未赋值的“rocky road”移到口味列表中“strawberry”的正上方,将“gelato”移到“types”列表的末尾。对于第一步,我们可以再次使用 jQuery 的after()
方法。对于第二步,我们将对“gelato”元素使用appendTo
方法,将其作为“types”列表中的最后一个子元素插入:
1 var $unassigned = $('.unassigned'),
2 $rockyRoad = $unassigned.find('li').eq(0),
3 $gelato = $unassigned.find('li').eq(1);
4
5 $vanilla.after($rockyRoad);
6 $gelato.appendTo($('.types'));
前面的解决方案都不是特别优雅或直观。当然有可能想出更吸引人的例子来解决这些问题,但是我认为这种尝试在 jQuery 开发人员中很常见。我们也可以利用一些 jQuery 专有的伪类,比如:first
和:last
,但是我们已经知道这些选项是多么低效。
DOM API 对元素重新排序的解决方案
为了在没有 jQuery 的情况下对我们的冰淇淋店页面进行适当的调整,我将引入两个新的 DOM API 方法。你还会看到一些选择器和其他 DOM API 方法,这些在第四章中已经讨论过了。令人惊讶的是,本节中的所有代码都可以在所有现代浏览器和 Internet Explorer 8 中运行!在我们开始之前,我并没有忘记这个示例冰淇淋店标记令人眼花缭乱的本质,但是它允许我简洁地演示一些 DOM 操作操作,而不会陷入与手头问题无关的细节中。也就是说,让我们开始吧。
请记住,我们的第一个任务是将“香草”元素移动到“草莓”元素之前。为了实现这一点,我们可以使用insertBefore()
方法、 8 ,这是作为 W3C 的 DOM Level 2 核心规范的一部分添加到Node
接口中的。可以想象,这个方法允许我们在 DOM 中将一个元素移动到另一个元素之前。因为这在Node
接口上是可用的,我们有能力移动 DOM 中的任何东西,甚至是一个Text
或Comment
节点!看看我们是如何移动这个元素的——我将在下面的代码片段之后立即解释发生了什么:
1 var flavors = document.querySelector('.flavors'),
2 strawberry = flavors.children[1],
3 vanilla = flavors.children[2];
4
5 flavors.insertBefore(vanilla, strawberry);
在前面代码的顶部,我只是选择了移动操作所需的元素。最后一行是最重要的一行。因为insertBefore()
方法是在Node
对象的prototype
上定义的,所以我们必须在实现这个接口的 DOM 对象上调用insertBefore()
。事实上,这个元素必须是我们正在移动的Node
的父元素。因为我们正在移动“vanilla”<li>
元素,所以我们可以使用它的父元素——“flavors”<ul>
。
传递给insertBefore()
的第一个参数是我们想要重新定位的元素:“普通”列表项。第二个参数是“参考节点”这是在移动操作之后将成为我们的目标元素的下一个兄弟的Node
(“香草”<li>
)。因为我们想将“香草”放在“草莓”之前,“草莓”<li>
是我们的引用节点。
我们已经对口味进行了重新排序,但是我们仍然需要将口味标题和列表移动到文档的顶部。我们也可以用insertBefore()
方法轻松实现这个目标:
1 var headings = document.querySelectorAll('h2'),
2 flavorsHeading = headings[0],
3 typesHeading = headings[1],
4 typesList = document.querySelector('.types');
5
6 document.body.insertBefore(typesHeading, flavorsHeading);
7 document.body.insertBefore(typesList, flavorsHeading);
Note
关于前面代码清单中的这行代码——document . body . insert before(typesHeading,flavors heading)——其行为就像早期 jQuery 代码清单中的$typesHeading.prependTo('body ')。为什么呢?因为 flavorsHeading 恰好是 document.body 的第一个子级。
我们逻辑的核心包含在前面代码的最后两行中。首先,我们将“类型”移到文档的顶部。这个标题的父元素是<body>
元素,我们可以使用document.body
很容易地选择它。当然,我们的目标元素是“类型”标题。我们希望将它移到“flavors”<h2>
之前,这样它就成为了我们的参考元素。
第二个insertBefore()
将冰淇淋类型的<ul>
移动到最近移动的标题之后。同样,<body>
是我们的母元素。因为我们需要将这个列表移到“口味”标题之前,所以这又是我们的引用节点。
我们最后的任务是将未赋值的元素移动到它们各自的列表中。为了实现这一点,我们将再次使用insertBefore()
,但是您也将看到一个新的方法在起作用。W3C DOM Level 1 规范是一个相当老的规范,它首先定义了一个appendChild() method on the Node
接口。 9 当我们结束练习时,这个方法会对我们有些用处:
1 flavors.insertBefore(
2 document.querySelector('.unassigned > li'), strawberry);
3
4 document.querySelector('.types').appendChild(
5 document.querySelector('.unassigned > li'));
在第一条语句中,我们将“rocky road”元素从 unassigned 列表移到 flavors 列表中。正如所料,口味列表是我们的父元素。目标是 unassigned 列表的第一个列表项子,恰好是“rocky road”<li>
。引用节点是 flavors 列表中的 strawberry 项,因为我们希望将“rocky road”移动到该元素之前。
我们还想将未分配的“gelato”列表项移动到类型列表的末尾。最简单的方法是使用appendChild()
。与insertBefore()
方法一样,appendChild()
希望在我们计划移动的节点的父节点上被调用——“类型”列表。appendChild()
方法只接受一个参数——将成为父元素的最后一个子元素的元素。此时,“gelato”项是未赋值列表中的第一个<li>
子元素,因此我们可以使用与在insertBefore()
语句中定位目标元素相同的选择器。
这一切出乎意料的简单,不是吗?DOM API 可能不像许多人说的那样可怕!
制作元素的副本
为了演示使用 jQuery 和 DOM API 克隆元素的各种方法,请考虑以下标记:
1 <ol class="numbers">
2 <li>one</li>
3 <li>two</li>
4 </ol>
DOM API 提供了一种克隆<ol>
及其子对象的方法,以及一种只克隆<ol>
而不克隆其任何子对象/内容的方法。前者称为深度克隆,后者称为浅层克隆。jQuery 只提供了一种深度克隆的方法。
在 jQuery-land 中,我们必须使用$.clone()
:
1 // deep clone: return value is an exact copy
2 $('.numbers').clone();
如果您希望 jQuery 克隆元素上的任何数据和事件侦听器,您可以选择将布尔参数传递给前面的clone()
。但是要注意,jQuery 只会复制事件侦听器和通过 jQuery 附加到元素的数据。任何在 jQuery API 之外添加的侦听器和数据都将丢失。
DOM API 在Node
接口上提供了一个类似命名的方法cloneNode()
。它最初被标准化为 DOM Level 2 Core 的一部分, 10 ,这在 2000 年成为 W3C 的推荐标准。因此,任何浏览器都支持cloneNode()
。由于我使用了querySelector()
,下一个例子仅限于 Internet Explorer 8 和更高版本(尽管这几乎不是一个有问题的限制):
1 // shallow clone: return value is an empty <ol class="numbers">
2 document.querySelector('.numbers').cloneNode();
3
4 // deep clone: return value is an exact copy of the tree
5 document.querySelector('.numbers').cloneNode(true);
在这两种情况下,元素副本将包含标记中定义的所有内容,甚至类名和任何其他属性,如内联样式。事件侦听器不会包含在副本中,也不会在元素的 JavaScript 对象表示中专门设置任何属性。换句话说,cloneNode()
只复制你看到的东西:标记。
无论您使用的是 jQuery 还是 DOM API,由cloneNode()
创建的副本都不会添加到文档中。您需要使用本节前面演示的方法之一自己完成这项工作。
组成你自己的元素
既然我们已经探讨了移动和应对元素,那么如何创建和删除它们呢?您将看到这些常见问题是如何用 jQuery 解决的,以及如何用 DOM API 轻松解决它们。与上一节一样,这里的所有 DOM API 代码都可以在所有现代浏览器中工作,并且大多数代码在 Internet Explorer 8 中也受支持。
为了更好地演示最后一节中概述的所有概念,我将基于上一节中修改过的示例文档来演示移动元素。使用 jQuery 和裸 DOM API,我将向您展示如何对我们的示例文档执行各种操作,如下所示:
- 加入一些新的冰淇淋口味。
- 移除一些现有类型。
- 对我们的文档进行简单的文本调整。
- 将文档的部分内容读入一个字符串,以便保存。
- 创建一个新的部分来进一步分类我们的冰淇淋。
创建和删除元素
假设我们有几个新口味添加到我们的列表:开心果和那不勒斯。这些当然属于“口味”部分。为了完成这项任务,我们需要创建两个新的<li>
元素,其中的Text Node
包含这两种新口味的名称。简单地将这些新风格添加到列表的末尾是很好的,这样我们就可以专注于创建有代表性的元素。我们还想从类型列表的末尾删除“gelato”类型,因为我们不再销售 gelato 冰淇淋。
使用 jQuery 创建元素非常容易,由于链接,我们可以在两行中添加这两个元素:
1 var $flavors = $('.flavors');
2
3 // add two new flavors
4 $('<li>pistachio</li>').appendTo($flavors);
5 $('<li>neapolitan</li>').appendTo($flavors);
移除一个元素也不是很困难:
1 // remove the "gelato" type
2 $('.types li:last').remove();
这里我们使用了 CSS 选择器,部分是专有的。带有“types”CSS 类的元素下面的最后一个<li>
将从文档中删除。这正好是我们的“冰淇淋”类型。:last
pseduo-class 是特定于 jQuery 的,因此性能不是特别好。有一个我们可以使用的本地 CSS pseduo 类,您马上就会看到,但是许多 jQuery 开发人员可能不知道它的存在,因为 jQuery API 提供了这个专有的替代方法作为其 API 文档的一部分。
我们如何用 DOM API 达到同样的结果?根据所需的浏览器支持,我们可能有几个选项。虽然新的浏览器可能比旧的浏览器允许更优雅的选项,但情况并不总是这样,在所有现代浏览器(甚至旧的浏览器)中,这些操作都相对简单,不依赖于 jQuery。
我们可以将我们的两种新口味添加到“口味”列表的末尾,总共两行,就像 jQuery 解决方案一样,尽管这两行稍长一些:
1 var flavors = document.querySelector('.flavors');
2
3 // add two new flavors
4 flavors.insertAdjacentHTML('beforeend', '<li>pistachio</li>')
5 flavors.insertAdjacentHTML('beforeend', '<li>neapolitan</li>')
在前面的代码中,我使用了呈现在Element
接口原型上的insertAdjacentHTML
方法 11 。虽然这种方法可能已经在浏览器中存在多年,但它只是在 2014 年起草的 W3C DOM 解析和序列化规范 12 中首次标准化。
把“gelato”从我们的类型列表中去掉怎么样?在最新的浏览器中,我们有最优雅的解决方案:
1 // remove the "gelato" type
2 document.querySelector('.types li:last-child').remove();
前面的代码与 jQuery 解决方案非常相似,但有一些明显的不同。首先,我当然是使用querySelector
来定位要移除的元素。第二,我使用了:last-child
CSS3 伪类 13 选择器。出现在ChildNode
界面上的remove()
方法相对较新,仅在微软 Edge、Chrome、Firefox 和 Safari 7 中受支持。任何版本的 Internet Explorer 都不支持它,苹果 iOS 浏览器也不支持它。这种方法首先由 WHATWG 定义为其 DOM 生活标准 14 的一部分,尤其是我们在浏览器支持方面的限制因素。
幸运的是,我们有一个覆盖所有现代浏览器的解决方案,只需要多一点代码:
1 var gelato = document.querySelector('.types li:last-child');
2
3 // remove the "gelato" type
4 gelato.parentNode.removeChild(gelato);
我把ChildNode.remove()
换成了Node.removeChild()
,它从 DOM Level 1 Core、 15 开始就存在了,所以它在所有浏览器上都受支持。当然,要删除子节点,我们需要首先访问父节点。幸运的是,这真的很容易做到,正如你在第四章中学到的。在这种情况下,限制我们使用现代浏览器的代码是:last-child
CSS3 伪类,它在 Internet Explorer 8 中不可用。
为了支持 IE8,你必须用document.querySelectorAll('.types li')[3]
替换选择器。如果您不想硬编码 gelato 元素的索引,您必须将querySelectorAll()
的结果移入一个变量,并通过检查该变量的length
属性来访问返回集合中的最后一个元素。
文本内容
就元素文本而言,有两个工作流需要处理:更新和解析。虽然 jQuery 提供了一种特定的方法来完成这两项任务,但是 DOM API 提供了两种方法——两种方法都有不同的行为来满足不同的需求。在这一节中,我将演示 jQuery 的text()
方法、本机textContent
属性和本机innerText
属性。当我们对冰淇淋类型和口味的文档进行更改,然后将结果文档输出为文本时,您将看到这些不同之处。
首先,让我们检查 jQuery 的text()
方法,它允许我们读取和更新文档中的文本。请注意,我们的一种类型——“意大利冰”——以大写字母开头。其他类型或口味都没有这个特点。尽管“Italian”是一个恰当的形容词,通常应该以大写字母“I”开头,但让我们对其进行修改,以与我们的其他类型和口味保持一致:
1 $('.types li').eq(1).text('italian ice');
您可能已经知道,元素的文本可以简单地通过传递新文本作为text()
方法的参数来更新。这正是我所做的,为了使这种冰淇淋的情况正常化。如果我们使用 jQuery 的text()
方法输出修改后的文档会是什么样子?像这样:
1 "
2 Types
3
4 frozen yogurt
5 italian ice
6 custard
7 gelato
8
9
10 Flavors
11
12 chocolate
13 vanilla
14 rocky road
15 strawberry
16
17 "
添加了引号以显示输出的开始和结束位置。它们不是实际文本的一部分。注意,这个输出反映了我们的标记的结构。这可以通过检查文本的缩进以及文档末尾的换行符来验证。输出结束前的一系列换行符说明了空的“未赋值”列表。您将看到这个输出如何反映 DOM API 提供的两个本地文本操作属性之一的输出。
用于读取和更新文本的 DOM 元素有两个公共属性:textContent
和innerText
。这两种属性各有优缺点,但是它们的存在使得在处理文本时比单独使用 jQuery 的text()
方法更加灵活。接下来,我将这两个属性相互进行比较,并与 jQuery 的text()
方法进行对比,这样就可以清楚地知道什么时候应该选择其中一个。
我们先来考察一下textContent
,它被添加到了 W3C 的 DOM Level 3 Core 的Node
接口中)。 16 该属性允许在所有现代浏览器中读取和更新元素文本。将“意大利冰”列表项的文本改为“意大利冰”就像 jQuery 的text()
方法一样简单:
1 document.querySelectorAll('.types li')[1].textContent = 'italian ice';
textContent
属性不仅匹配 jQuery 的text()
方法在写入文本时的行为,它在读取文本时的功能也与 jQuery 完全一样。以我们之前的例子为例,在修改了“Italian ice”类型之后,我们输出了整个冰激凌文档。DOM API 的textContent
属性的输出与 jQuery 的text()
完全匹配:
1 "
2 Types
3
4 frozen yogurt
5 italian ice
6 custard
7 gelato
8
9
10 Flavors
11
12 chocolate
13 vanilla
14 rocky road
15 strawberry
16
17 "
如您所见,textContent
输出元素及其后代中的文本,并按照文档标记的结构进行格式化,就像 jQuery 的text()
一样。
第二个可用的属性是innerText
,它在HTMLElement
接口上可用,尽管有点奇怪,因为它还不是任何正式 web 规范的一部分。然而,它被所有浏览器的所有版本支持,除了 Firefox,它直到版本 45 才添加支持。 17 尽管innerText
还没有标准化,但是已经有了一个初步的提案草案 18 由 Mozilla 的 Robert O’Callahan 创建。
使用innerText
将“意大利冰”更改为“意大利冰”与textContent
或 jQuery 的text()
没有太大区别,除了增加了对 Internet Explorer 8 的支持以及缺少对 45:
1 document.querySelectorAll('.types li')[1].innerText = 'italian ice';
那么,如果我们试图使用innerText
输出我们的文档,会发生什么呢?您将看到结果看起来与从textContent
和 jQuery 的text()
获得的结果略有不同:
1 "Types
2
3 frozen yogurt
4 italian ice
5 custard
6 gelato
7 Flavors
8
9 chocolate
10 vanilla
11 rocky road
12 strawberry"
最初,前面的输出可能看起来有点奇怪,但是如果您理解它所代表的含义,它实际上是完全有意义的。我希望您将前面列出的修改过的文档中的标记粘贴到浏览器中,将呈现的结果复制到系统的剪贴板中,然后将其粘贴到文本编辑器中。您会注意到粘贴的文本与这里列出的输出格式相同。正如草案规范所描述的,innerText
“返回一个元素的‘渲染文本’。”
有一次有人问我“在处理读取元素文本时,有没有一个通用的解决方案来使用所有浏览器都支持的 web APIs?”嗯,那要看你的要求了。如果 textContent 的行为是适当的,并且您只需要现代浏览器支持,那么这可能是您的最佳选择。但是如前所述,确实存在 innerText 更合适的情况。jQuery 的 text()的行为类似于 textContent,因此,如果您想要反映 jQuery 的行为,并且需要支持所有现代浏览器,包括旧版本的 Firefox,这是支持 textContent 的另一个原因。
丰富的内容
HTML 只不过是按照一组 web 规范定义的约定格式化的文本。当我们需要序列化或反序列化文档或文档的一部分时,这种现实是有用的。当接收到响应 HTTP 请求的服务器生成的标记时,可能会发生 HTML 的反序列化。在这种情况下,响应中的 HTML 必须插入到 DOM 中的适当位置。我将演示这个特定的场景,并讨论如何在 DOM API 中可用的几种方法的帮助下完成这个过程。也许这个服务器生成的标记必须返回给服务器,并在以某种方式修改后保留下来供以后使用。这也可以用 DOM API 来完成,您将在最后一节中看到如何完成。
jQuery 提供了一种读写 HTML 的方法。这是使用名副其实的html()
函数完成的。首先,假设我们已经从服务器收到了一个 HTML 字符串,我们需要将它插入到我们的文档中。为了与本章的主题保持一致,这个标记代表了冰淇淋店页面的一个全新部分。我们只需要将它插入到现有的部分之后。来自我们服务器的标记只是一长串 HTML,比如“
容器
- cone
- cup
”。这个 HTML 字符串将被存储在一个名为container
的变量中。在这里,您可以看到如何使用 jQuery 将它插入到我们文档的末尾:
1 $('<div>').html(container).appendTo('body');
首先,我们创建一个新的<div>
,它与 DOM 断开连接,然后我们将这个断开连接的<div>
的内容设置为来自服务器的 HTML,最后这个元素被添加到我们的冰激凌商店页面的末尾。在以各种方式修改了我们的页面之后,我们现在想要将标记发送回我们的服务器,这也可以使用 jQuery 的html()
方法来完成:
1 var contents = $('body').html();
2 // ...send `contents` to server
jQuery-less DOM API 路线稍逊一筹,但仍然非常简单并得到广泛支持。为了读写相同的标记,我们将使用在Element
接口上定义的innerHTML
属性。这一特性虽然在所有可以想象的浏览器中都得到支持,但直到最近才实现标准化。innerHTML
最初是微软的 Internet Explorer 专有扩展,但现在是 W3C DOM 解析和序列化规范的一部分。 19
我们可以使用innerHTML
将服务器生成的 HTML 添加到页面的末尾:
1 var div = document.createElement('div');
2 div.innerHTML = container;
3 document.body.appendChild(div);
Document
接口的createElement
方法由 W3C 的 DOM Level 1 Core 20 规范提供,这意味着它在任何浏览器中都受支持。为持久性服务器端读回我们文档的标记也使用了innerHTML
,它和 jQuery 的html()
方法一样优雅:
1 var contents = document.body.innerHTML;
2 // ...send `contents` to server
在这个实例中,DOM API 比 jQuery 更灵活一些;它提供了更多的选择。例如,标准化的Element.outerHTML
属性将在读取或更新 HTML 时考虑引用元素。相反,innerHTML
只涉及引用元素的后代。如果我在上面的“添加一个字符串”演示中使用了outerHTML
,那么文档中的所有内容,包括<body>
元素,都将被替换为新的<div>
包装的冰淇淋容器部分。在最后一个 DOM API 示例中,我们读回了文档的内容,如果我们使用了outerHTML
,那么<body>
元素就会包含在 stringified-html 中。根据您的要求,这可能是可取的。
虽然我肯定没有展示 DOM API 提供的所有属性和方法,但我想说的是,浏览器已经为 DOM 操作提供了足够多的合理和直观的本机支持。
Footnotes 1
www.w3.org/2002/07/26-dom-article.html
2
http://ejohn.org/blog/the-dom-is-a-mess/
3
https://github.com/jquery/jquery/issues
4
https://github.com/jquery/jquery/issues/2679#issuecomment-152289474
5
https://developer.mozilla.org/en-US/docs/Web/API/Document
6
https://developer.mozilla.org/en-US/docs/Web/API/Element
7
https://developer.mozilla.org/en-US/docs/Web/API/Node
8
www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-952280727
9
www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-184E7107
10
www.w3.org/TR/DOM-Level-2-Core/core.html#ID-3A0ED0A4
11
12
https://w3c.github.io/DOM-Parsing/
13
www.w3.org/TR/css3-selectors/#last-child-pseudo
14
https://dom.spec.whatwg.org/#dom-childnode-remove
15
www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-removeChild
16
www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-textContent
17
https://developer.mozilla.org/en-US/Firefox/Releases/45
18
https://rocallahan.github.io/innerText-spec/index.html
19
www.w3.org/TR/DOM-Parsing/#widl-Element-innerHTML
20
www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#method-createElement