【译】CSS对象模型(CSSOM)指南

作者:Louis Lazaris

原文链接:https://css-tricks.com/an-introduction-and-guide-to-the-css-object-model-cssom/

如果您已经写了一段时间的JavaScript,那么可以肯定您使用过用于处理文档对象模型(DOM)的脚本。DOM脚本利用网页提供的一组API(或接口)来操作和处理页面上的元素。

那么,您应该还熟悉另一个对象模型:CSS对象模型(CSSOM),可能你已经在无意识的时候使用过它。

在这篇文章中,我将介绍CSSOM的许多重要的功能,从最常见的开始,慢慢转向一些大家不清楚但很实用的小功能。

#什么是CSSOM?

根据MDN:

CSS对象模型是JavaScript处理CSS的API。它很像DOM,只是面向CSS而不是HTML。它允许用户动态地读取和修改CSS样式。

MDN的信息基于 官方的W3C CSSOM规范. 用W3C文档来熟悉CSSOM的是一种低效的方式,也不适用于那些正在寻找一些实际编码示例的人。

使用MDN显然会好得多,虽然在某些方面仍然很缺乏。因此,对于这篇文章,我试图尽力的去创建有用的代码示例来演示,这样您就可以通过实际代码解除疑惑。

就像上面所说,文章从大多数前端开发人员熟悉的内容开始。里面有一些公有属性常常和DOM脚本混在一起,但是CSSOM的API还是占了很大一部分。

#使用element.style获取内联样式

使用JavaScript操作和获取CSS属性和值的最基本方法是通过style对象或属性,这个对象和属性往往对于所有的HTML元素都适用。这里有一个例子:

1
document.body.style.background = 'lightblue';

大多数人之前可能已经看过或用过这种语法。我可以使用element.style.propertyName.的格式为页面上的任何对象添加或更改CSS 。

在上述例子中,我正在将background属性的值更改为lightblue。当然,background是“短属性”(只有一个单词组成的属性)。如果我只想更改background-color属性怎么办?对于任何带连字符的属性,只需将属性名称转换为驼峰法的形式,如下:

1
document.body.style.backgroundColor = 'lightblue';

在大多数情况下,“短属性”以小写单词的形式表示和访问,而“连字符属性”用驼峰法表示。但是有一个例外是使用float属性。因为float是JavaScript中的保留字,所以您需要使用cssFloat(如果您支持IE8及更早版本,请使用styleFloat)。这类似于HTML中的for属性,我们使用htmlFor 来表示。

下面是一个使用style属性来允许用户更改当前页面背景颜色的例子:

我们使用JavaScript定义CSS属性和值虽然简单方法,但是使用style属性有一个很大的前提:只适用于元素的内联样式

当您使用该style属性读取CSS 时,这一点变得清晰:

1
2
3
document.body.style.backgroundColor = 'lightblue';
console.log(document.body.style.backgroundColor);
// "lightblue"

在上面的例子中,我在<body>元素上定义了一个内联样式,然后我将相同的样式输出到控制台。但是如果我尝试去读取style的另一个属性的值时,它将不返回任何内容 - 除非是我之前在CSS或JavaScript中定义的内联样式。例如:

1
2
console.log(document.body.style.color);
// Returns nothing if inline style doesn't exist

即使我使用外部样式表的color 属性去修饰<body> 元素,也同样获取不到值,如下所示:

通过JavaScript使用element.style的方式去改变元素的属性是最简单和通用的方式,但是你也看到这里很明显有很大的限制,让我们继续来看一下JavaScript还有那些更好用的方式去读取和修改我们的样式。

#获取Computed样式

您可以使用window.getComputedStyle() 方法来读取元素上的任何CSS属性:

1
2
window.getComputedStyle(document.body).background;
// "rgba(0, 0, 0, 0) none repeat scroll 0% 0% / auto padding-box border-box"

哇!真实一个有意思的结果。在某种程度上,window.getComputedStyle()style的孪生兄弟。如果直接使用style属性给你提供的元素实际样式信息太少,那么使用window.getComputedStyle()会给您一些启发。

在上述示例中,<body>元素的background属性是使用background: papayawhip;定义。但getComputedStyle()方法返回background属性中的所有值,未在CSS中明确定义的那些将返回这个属性的初始(或默认)值。

这意味着,对于任何属性,即使CSS中没有定义它们,使用window.getComputedStyle()仍可以返回所有初始值:

类似地,比如widthheight这样的属性,不管这些值是否在CSS中的任何位置定义,它将显示元素对应属性的计算值,如下面的示例所示:

尝试调整上面示例中的父元素大小以查看结果。这类似于读取window.innerWidth值,只是在这边是去获取指定元素上指定属性的CSS值,而不仅仅是一般的窗口或视口。

这里我们使用window.getComputedStyle()方法中的几种方式来访问。我已经演示了其中的一种方法,它使用点符号+驼峰式属性名称的方式添加到方法的末尾。您可以在以下代码中看到三种不同的方法:

1
2
3
4
5
6
7
8
// dot notation, same as above
window.getComputedStyle(el).backgroundColor;

// square bracket notation
window.getComputedStyle(el)['background-color'];

// using getPropertyValue()
window.getComputedStyle(el).getPropertyValue('background-color');

上面第一种方法的形式和先前的那个例子一样。

第二种方式使用中括号将CSS属性用单引号包裹起来,这种方式不推荐,代码编译器会告警。

第三种使用getPropertyValue() 的方式。

在第一个例子中,我们使用骆驼法(在这个例子中,floatcssFloat属性都可以工作),而接下来的两个方法使用相同的语法形式,使用CSS中的属性(连字符,通常被称为“烤肉箱”)。

下面的示例与前一个相同,但这次使用getPropertyValue()访问两个元素的宽度:


See the Pen
pGeQOy
by 宣浙华 (@zhehuaxuan)
on CodePen.

#获取伪元素的计算样式

一个鲜为人知的事情是window.getComputedStyle()能够获取伪元素的样式信息。你可以看到 window.getComputedStyle() 方法像如下形式的声明:

1
window.getComputedStyle(document.body, null).width;

请注意第二个参数, null, Firefox的版本4之前的需要第二个参数,这就是为什么你经常在老代码中看到它(目前所有的浏览器都不需要这个参数)。

第二个参数可选,允许我们去获得当前元素的伪元素的属性,如下所示:

1
2
3
4
5
.box::before {
content: 'Example';
display: block;
width: 50px;
}

在这里,我们给.box 元素添加了一个::before 的伪元素,下面的JavaScript代码用于计算伪元素的属性值:

1
2
3
let box = document.querySelector('.box');
window.getComputedStyle(box, '::before').width;
// "50px"

您也可以为其他伪元素执行此操作::first-line,如以下代码和演示:

1
2
let p = document.querySelector('.box p');
window.getComputedStyle(p, '::first-line').color;

And here’s another example using the ::placeholder pseudo-element, which apples to <input>elements:

这里有另外一个例子获取input标签中的::placeholder伪元素的样式信息:

1
2
let input = document.querySelector('input');
window.getComputedStyle(input, '::placeholder').color

以上工作在最新的Firefox中,但不适用于Chrome或Edge(我已经为Chrome 提交了错误报告)。

还应该注意的是,尝试访问不存在(但有效)的伪元素的样式时,浏览器会有不同的结果(::banana伪元素)。您可以使用以下演示在各种浏览器中查看效果:

作为本节的一个小点,有一个名为Firefox的方法getDefaultComputedStyle(),它不是规范的一部分,可能永远不会。

#CSS样式声明API

上面我向您展示如何通过style对象或使用getComputedStyle()函数访问属性,在这两种情况下都暴露了CSSStyleDeclaration接口。

换句话说,以下两行都将返回CSS样式文档body元素上的对象:

1
2
document.body.style;
window.getComputedStyle(document.body);

下面是这两个对象返回的快照:

The CSSStyleDeclaration API in the DevTools console

getComputedStyle()函数返回的值是只读的,使用element.style可以获取和设置属性,但是像先前提到的,这些只会影响document的内联样式

#setProperty(), getPropertyValue(), 和item()

一旦您以上述方式之一暴露了CSS样式声明对象,您就可以访问许多有用的方法来读取或操作这些值。同样,getComputedStyle()返回值是只读的,但是当通过style属性使用时,可用于获取和设置属性值。

请考虑以下代码和演示:

1
2
3
4
5
6
let box = document.querySelector('.box');

box.style.setProperty('color', 'orange');
box.style.setProperty('font-family', 'Georgia, serif');
op.innerHTML = box.style.getPropertyValue('color');
op2.innerHTML = `${box.style.item(0)}, ${box.style.item(1)}`;

在这个例子中,我使用了三种不同的style对象方法:

  • setProperty()方法。这需要两个参数,每个参数都是一个字符串:属性(以常规CSS表示法)和属性值
  • getPropertyValue()方法。这需要一个参数,你想要获得的属性。和前面提到的getComputedStyle()方法一样,返回 CSSStyleDeclaration 对象。
  • item()方法。这需要使用一个参数,这需要一个参数,它是一个正整数,表示您要访问的属性的索引。返回值是该索引处的属性名称。

注意,在我上面的简单示例中,只有两个样式添加到元素的内联CSS中。这意味着如果我要访问item(2),返回值将是一个空字符串。如果我getPropertyValue()以前访问未在该元素的内联样式中设置的属性,也会得到相同的结果。

# 使用removeProperty()

除了上面提到的三种方法之外,还有另外两种方法暴露CSS样式声明对象。在下面的代码和示例中,我将使用removeProperty()方法:

1
2
3
4
5
box.style.setProperty('font-size', '1.5em');
box.style.item(0) // "font-size"

document.body.style.removeProperty('font-size');
document.body.style.item(0); // ""

In this case, after I set font-size using setProperty(), I log the property name to ensure it’s there. The demo then includes a button that, when clicked, will remove the property using removeProperty().

在这种例子中,我们使用setProperty()设置font-size之后,我记录属性名称,然后使用一个按钮,单击该按钮使用removeProperty()删除该属性。

#获取和设置属性的优先级

最后,这是我在写这篇文章的时候发现了一个有趣的特性:使用getPropertyPriority(),看下面CodePen演示的示例:

1
2
3
4
5
box.style.setProperty('font-family', 'Georgia, serif', 'important');
box.style.setProperty('font-size', '1.5em');

box.style.getPropertyPriority('font-family'); // important
op2.innerHTML = box.style.getPropertyPriority('font-size'); // ""

在该代码的第一行,您可以看到我使用setProperty()方法,就像我之前一样。但是,我们在这里包含了第三个参数。第三个参数是一个可选字符串,用于定义是否希望该属性附加!important关键字。

在我为属性设置!important之后,我们使用getPropertyPriority()方法检查该属性的优先级。如果您觉得该属性不重要,可以省略第三个参数,使用关键字undefined,或将第三个参数包含为空字符串。

我应该在这里强调,这些方法可以与已经直接放在HTML元素style属性上的任何内联样式一起使用。

所以,如果我有如下HTML:

1
<div class="box" style="border: solid 1px red !important;">

我可以使用本节中讨论的任何方法来读取或修改该样式。这里应该注意的是,由于我使用了这种内联样式的简写属性并将其设置为!important,因此我们使用getPropertyPriority()就可以把长属性的important返回出来,请参阅下面的代码和演示:

1
2
3
4
5
6
// These all return "important"
box.style.getPropertyPriority('border'));
box.style.getPropertyPriority('border-top-width'));
box.style.getPropertyPriority('border-bottom-width'));
box.style.getPropertyPriority('border-color'));
box.style.getPropertyPriority('border-style'));

在这个例子中,即使我只在border属性中显式设置了style属性的important,所有关联border的长属性也都返回important

# CSS样式表API

到目前为止,我所考虑的方法都涉及内联样式(通常不那么有用)和计算样式(这些方式很有用,但通常过于具体)。

还有一个更有用的API,允许您获取具有可读写性的样式表,而不仅仅是内联样式,这就是CSS样式表。从文档样式表访问文档属性的最简单方法是使用styleSheets属性。这就是CSS样式表API。

例如,下面的使用该length属性来查看当前文档具有多少个样式表:

1
document.styleSheets.length; // 1

我可以使用从零开始的索引来引用任何文档的样式表:

1
document.styleSheets[0];

如果我们用console打印stylesheet ,我们将会看到下面的这些方法和属性:

The CSSStyleSheet Interface in the DevTools Console

这里最有用的属性是cssRules。此属性提供样式表中包含的所有CSS规则(包括声明块,规则,媒体规则等)。在下面,我将详细介绍如何利用此API来操作和读取外部样式表中的样式。

#使用样式表对象

为了简单起见,让我们使用外部样式表,其中只包含少量规则。这里请允许我演示如何使用CSSOM访问样式表的不同部分,和DOM脚本的方式类似。

这是我使用下面的样式表:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
* {
box-sizing: border-box;
}

body {
font-family: Helvetica, Arial, sans-serif;
font-size: 2em;
line-height: 1.4;
}

main {
width: 1024px;
margin: 0 auto !important;
}

.component {
float: right;
border-left: solid 1px #444;
margin-left: 20px;
}

@media (max-width: 800px) {
body {
line-height: 1.2;
}

.component {
float: none;
margin: 0;
}
}

a:hover {
color: lightgreen;
}

@keyframes exampleAnimation {
from {
color: blue;
}

20% {
color: orange;
}

to {
color: green;
}
}

code {
color: firebrick;
}

我可以在这个例子中尝试很多事情,首先,我将循环遍历样式表中的所有样式规则,并记录每个样式的选择器的值:

1
2
3
4
5
6
7
8
let myRules = document.styleSheets[0].cssRules,
p = document.querySelector('p');

for (i of myRules) {
if (i.type === 1) {
p.innerHTML += `<code>${i.selectorText}</code><br>`;
}
}

在上面的示例中需要注意的几件事情。首先,我定义记录外部样式表的cssRules对象的引用。然后我遍历该对象中的所有规则,检查每个规则的类型。

在这种情况下,我想要类型的规则1,它代表STYLE_RULE常量。其他常量包括IMPORT_RULE(3),MEDIA_RULE(4),KEYFRAMES_RULE(7)等。您可以在此MDN文章中查看这些常量的完整表。

当我确认规则是样式规则时,我打印每个样式规则的selectorText属性。展示如下结果:

1
2
3
4
5
6
*
body
main
.component
a:hover
code

selectorText属性表示选择器样式规则的名称。这是一个可写属性,所以如果我用for使用以下代码更改原始循环内特定规则的选择器:

1
2
3
if (i.selectorText === 'a:hover') {
i.selectorText = 'a:hover, a:active';
}

在这个例子中,我查询一个选择器(链接),把 :hover 属性查询出来,应用到 :active 中。而且,我可以使用字符串方法甚至是正则表达式来查找所有实例的:hover,然后从那里做一些事情。但这个例子已经足以证明它的工作原理。

#使用CSSOM访问@media规则

您会注意到我的样式表还包括媒体查询规则:@keyframes at-rule。当我搜索样式规则(类型1)时,这两个都被跳过了。我们现在找到所有@media规则:

1
2
3
4
5
6
7
8
9
10
let myRules = document.styleSheets[0].cssRules,
p = document.querySelector('.output');

for (i of myRules) {
if (i.type === 4) {
for (j of i.cssRules) {
p.innerHTML += `<code>${j.selectorText}</code><br>`;
}
}
}

基于给定的样式表,上面将产生:

1
2
body
.component

正如您所看到的,在我遍历所有规则以查看是否存在@media规则(类型4)之后,我遍历cssRules的每个媒体规则对象(在这种情况下,只有一个)并记录在那个媒体规则里面的每个规则的选择器文本。

因此,@media规则上公开的接口类似于样式表上公开的接口。@media中的规则还包含一个conditionText属性,如以下代码段和演示中所示:

1
2
3
4
5
6
7
8
9
let myRules = document.styleSheets[0].cssRules,
p = document.querySelector('.output');

for (i of myRules) {
if (i.type === 4) {
p.innerHTML += `<code>${i.conditionText}</code><br>`;
// (max-width: 800px)
}
}

此代码循环遍历所有媒体查询规则,并记录确定该规则何时适用的文本(即条件)。还有一个mediaText返回相同值的属性。根据规范,您可以获得或设置其中任何一个。

#使用CSSOM访问@keyframes规则

上述我已经演示了如何读取@media规则中的信息,现在让我们考虑如何访问@keyframes规则。我们从下面的代码开始:

1
2
3
4
5
6
7
8
9
10
let myRules = document.styleSheets[0].cssRules,
p = document.querySelector('.output');

for (i of myRules) {
if (i.type === 7) {
for (j of i.cssRules) {
p.innerHTML += `<code>${j.keyText}</code><br>`;
}
}
}

在这个例子中,我正在查询类型为7(i.e. @keyframes规则)的规则。当找到一个规则时,遍历所有规则cssRules并记录每个规则的keyText属性。这种情况下的日志将是:

1
2
3
"0%"
"20%"
"100%"

您会注意到我的原始CSS使用fromto作为第一个和最后一个关键帧,但是keyText属性计算这些0%100%keyText也可以设置值。在我的示例样式表中,我可以像这样硬编码:

1
2
3
4
5
6
7
8
// Read the current value (0%)
document.styleSheets[0].cssRules[6].cssRules[0].keyText;

// Change the value to 10%
document.styleSheets[0].cssRules[6].cssRules[0].keyText = '10%'

// Read the new value (10%)
document.styleSheets[0].cssRules[6].cssRules[0].keyText;

使用此功能,我们可以动态更改Web应用程序流中的动画关键帧,也可以响应用户操作。

访问@keyframes规则时可用的另一个属性是name

1
2
3
4
5
6
7
8
let myRules = document.styleSheets[0].cssRules,
p = document.querySelector('.output');

for (i of myRules) {
if (i.type === 7) {
p.innerHTML += `<code>${i.name}</code><br>`;
}
}

回想一下,在CSS中,@keyframes规则如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
@keyframes exampleAnimation {
from {
color: blue;
}

20% {
color: orange;
}

to {
color: green;
}
}

因此,name属性允许我读取以@keyframes和自定义名称该规则。animation-name特定元素上启用动画时在属性中使用的名称相同。

我在这里要提到的最后一件事是能够获取单个关键帧内的特定样式。这是一些带有演示的示例代码:

1
2
3
4
5
6
7
8
9
10
let myRules = document.styleSheets[0].cssRules,
p = document.querySelector('.output');

for (i of myRules) {
if (i.type === 7) {
for (j of i.cssRules) {
p.innerHTML += `<code>${j.style.color}</code><br>`;
}
}
}

在这个例子中,在我找到@keyframes规则之后,我遍历关键帧中的每个规则(例如“from”规则,“20%”规则等)。然后,在每个规则中,我访问一个单独的style属性。在这种情况下,因为我知道color是在@keyframes定义的唯一属性。

这个例子的主要内容是使用style属性或对象。之前我展示了如何使用此属性来访问内联样式。但在这种情况下,我使用它来访问单个关键帧内的各个属性。

你可能会看到这允许您动态修改单个关键帧的属性,这可能会由于某些用户操作或应用程序或基于Web的游戏中发生的事件而发生改动。

#添加和删除CSS声明

CSSStyleSheet可以访问两种方法,允许您从样式表中添加或删除整个规则:insertRule()deleteRule()。让我们看看在示例中,他们两个如何操作样式表:

1
2
3
4
5
let myStylesheet = document.styleSheets[0];
console.log(myStylesheet.cssRules.length); // 8

document.styleSheets[0].insertRule('article { line-height: 1.5; font-size: 1.5em; }', myStylesheet.cssRules.length);
console.log(document.styleSheets[0].cssRules.length); // 9

在这种情况下,我正在记录cssRules属性的长度(显示样式表最初有8个规则),然后我使用以下insertRule()方法将以下CSS添加为单独的规则:

1
2
3
4
article {
line-height: 1.5;
font-size: 1.5em;
}

cssRules再次记录属性的长度以确认添加了规则。

insertRule()方法将字符串作为第一个参数(这是必需的),该参数包括待插入的完整样式规则(包括选择器,花括号等),包括嵌套在at规则中的各个规则可以包含在此字符串中。

第二个参数是可选的。这是一个整数,表示您希望插入规则的位置或索引。如果未包括,则默认为0(意味着规则将插入规则集合的开头)。如果索引恰好大于规则对象的长度,则会引发错误。

deleteRule()方法使用起来更简单:

1
2
3
4
5
let myStylesheet = document.styleSheets[0];
console.log(myStylesheet.cssRules.length); // 8

myStylesheet.deleteRule(3);
console.log(myStylesheet.cssRules.length); // 7

在这个例子中,这个方法接受一个参数,该参数表示我要删除的规则的索引。

使用任一方法,由于基于零的索引,作为参数传入的选定索引必须小于cssRules对象的长度,否则将引发错误。

#重新访问 CSS样式声明 API

之前我解释了如何访问声明为内联样式的单个属性和值。这是通过element.style,暴露CSS样式声明接口完成的。

CSSS样式声明API作为CSS样式表API的子集暴露一个单独的样式规则,当我向您展示如何访问@keyframes规则内的属性时,我已经提到了这一点。要了解其工作原理,请比较以下两个代码段:

1
<div style="color: lightblue; width: 100px; font-size: 1.3em !important;"></div>
1
2
3
4
5
.box {
color: lightblue;
width: 100px;
font-size: 1.3em !important;
}

第一个例子是获取内联样式的集合如下所示:

1
document.querySelector('div').style

CSS样式声明API,允许我获取element.style.colorelement.style.width等属性对象。

但我可以在外部样式表中的单个样式规则上公开完全相同的API。这意味着我将我对style属性的使用与CSS样式表API结合起来。

因此,上面第二个示例中的CSS使用与内联版本完全相同的样式,可以像这样访问:

1
document.styleSheets[0].cssRules[0].style

这里打开一个外链样式表的一个样式规则的CSS样式声明对象,如果有多个样式规则,可以使用cssRules[1], cssRules[2], cssRules[3]等方式来获取。

因此,在外部样式表中,我可以访问前面提到的所有方法和属性。这包括SetProperty(),getPropertyValue(),item(),removeProperty()getPropertyPriority()。除此之外,这些相同的功能可用于@keyframes或@media规则内的单个样式规则。

这是一个代码片段和演示,演示了如何在我们的示例样式表中的单个样式规则上使用这些方法:

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
// Grab the style rules for the body and main elements
let myBodyRule = document.styleSheets[0].cssRules[1].style,
myMainRule = document.styleSheets[0].cssRules[2].style;

// Set the bg color on the body
myBodyRule.setProperty('background-color', 'peachpuff');

// Get the font size of the body
myBodyRule.getPropertyValue('font-size');

// Get the 5th item in the body's style rule
myBodyRule.item(5);

// Log the current length of the body style rule (8)
myBodyRule.length;

// Remove the line height
myBodyRule.removeProperty('line-height');

// log the length again (7)
myBodyRule.length;

// Check priority of font-family (empty string)
myBodyRule.getPropertyPriority('font-family');

// Check priority of margin in the "main" style rule (!important)
myMainRule.getPropertyPriority('margin');

#CSS类型对象模型…未来?

在上面提过的所有内容之后,我不得不说在将来的某一天,我们所知道的CSSOM很可能也会过时。

那是因为有一个被称为CSS Typed OM的出现,它是Houdini项目的一部分。虽然有些人已经注意到新的Typed OM与目前的CSSOM相比更加冗长,但Eric Bidelman在本文中概述的好处包括:

  • 更少的错误
  • 算术运算和单位转换
  • 更好的性能
  • 错误处理
  • CSS属性名称始终是字符串

有关这些功能的完整详细信息和语法的一瞥,请务必查看完整的文章

在撰写本文时,CSS Typed OM仅在Chrome中受支持。您可以在本文档中查看浏览器支持的进度。

#最后小结

通过JavaScript操作样式表肯定不是你在每个项目中都要做的事情。使用我在这里介绍的方法和属性实现的一些复杂交互有一些非常具体的用例。

如果您已经构建了某种使用这些API的工具,我很乐意听到它。我的研究只是触及了表面上的东西,但我很想知道在现实世界的例子中如何使用它。

我已将本文中的所有演示文稿放入CodePen集合中,因此您可以根据自己的喜好随意使用它们。