FrankKai / FrankKai.github.io

FE blog
https://frankkai.github.io/
362 stars 39 forks source link

[译]CSSOM(CSS Object Model)介绍和指南 #227

Open FrankKai opened 4 years ago

FrankKai commented 4 years ago

image

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

前言

如果你写了一段时间的JS了,大概率是接触过通过js去处理DOM(Document Objectl Model)了。DOM是很多用于操作页面上元素的API。 但其实除了DOM之外,还有一不被常人所知的CSSOM(CSS Object Model)。也可能你已经用过它了但其实并没有注意到。 在这篇文章中,我将列举许多CSSOM中最重要的特性,以一些常见的开始,然后逐渐列举一些不著名的但是实用的特性。

什么是CSSOM?

根据MDN的定义:

CSSOM(CSS Object Model)是一组通过JavaScript修改CSS的API集合。CSSOM和DOM很像,但CSSOM是对于CSS来说的,而不是对于HTML来说的。CSSOM允许用户动态读取和修改CSS的样式。

MDN的解释是基于W3C官方对CSSOM的定义来解释的。W3C文档是一个相当好的去熟悉CSSOM的方式,但是很难从中找到有用的代码片段去调用CSSOM的api。

MDN相对好很多,但是仍然有很多地方是缺失的。 因此在这个博文中,我将尽我的最大努力去创建有用的代码示例和demo,从而可以直观的搞清楚学明白CSSOM的这些api。

文章将首先从对于前端来说很常见的一些api。这些常见的特性常常被混淆成DOM的api,但它们其实是CSSOM。

通过element.style修改行内样式

通过JavaScript最基础的修改和获取CSS属性的方式是style对象,或者property,它对于所有HTML元素都使用。 比如这个例子:

document.body.style.background = 'lightblue'; // 这个颜色好看

大多数人都用过这个语法。可以通过这个语法去添加或者修改页面上任意对象的CSS:

element.style.propertyName

在这个例子中,我将背景色改为了lightblue。当然,background是是缩写。如果想修改background-color属性呢?对于任何有连字符的属性来说,只需将其转为驼峰格式即可:

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

通常来说,缩写属性直接小写单词就行,对于连字符属性需要通过驼峰获取。 有一个例外,那就是float属性,因为float是js中的一个保留字符串,需要通过cssFloat(或styleFloat <=ie8)。 这一点和HTML中对for属性的获取很像,通过getAttribute()获取属性的话,需要标记它为"htmlFor"。

这里有一个通过CSSOM动态修改body背景色的demo:https://codepen.io/impressivewebs/pen/mQbqGR

通过JavaScript去定义一个CSS属性和它的值是非常简单的。 但是通过这种直接访问style对象的方式有一个很大的限制:只能修改元素的行内样式,换句话说就是必须在标签的style属性上定义。

该怎么去理解这句话呢?

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

上面的例子中,我给body定义了一个行内样式,然后我在控制台打印出了相同的样式。 这是正常的,但是如果我尝试读取元素的另一个属性的话,它什么都不会返回给我们-除非我预先在css中定义或者预先在js中定义过。

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

这里有个demo:https://codepen.io/impressivewebs/pen/LXPewe 通过css样式表是无法获取的。

body {
  background-color: lightblue;
}

结果为:The current background color is:

但是如果在body标签上为它的style属性有设置,是可以获取的:

<body style="background-color:lightblue">
    <p>The current background color is: <output></output></p>
</body>

结果为:The current background color is: lightblue

虽然通过显示设置或者直接用element.style可以设置,但是其实这样是很笨拙的,接下来我们来看一些很有用的通过js去读取和修改css的技术。

<style></style>中定义能获取到吗? 不能。 仅适用于在标签style属性上定义的值,style标签上和css文件中都没有用。

<style></style>中定义,获取不到。

<!doctype html>
<html>
<head>
  <style>
    #foo{
      color: lightblue;
    }
  </style> 
</head>
<body>
  <p id="foo">This is my paragraph.</p>
</body>
<script>
  console.log(document.getElementById('foo').style.color); // 什么都不打印
  console.log(window.getComputedStyle(document.getElementById('foo')).color); // rgb(173, 216, 230)
</script>
</html>

在p标签上的style属性声明,可以获取到。

<!doctype html>
<html>
<body>
  <p id="foo" style="color: lightblue;">This is my paragraph.</p>
</body>
<script>
  console.log(document.getElementById('foo').style.color);// lightblue
  console.log(window.getComputedStyle(document.getElementById('foo')).color);// rgb(173, 216, 230)
</script>
</html>

获得普通元素的计算样式

可以通过window.getComputedStyle()方法去读取任意元素的CSS属性。 例如我们上面通过document.body.style.color无法读取color属性,可以改写为:

window.getComputedStyle(document.body).color;
// rgb(255, 255, 255)

但是对于一些缩写属性来说,通过window.getComputedStyle()查询到的属性比较有趣。

window.getComputedStyle(document.body).background;
"rgb(173, 216, 230) none repeat scroll 0% 0% / auto padding-box border-box"

window.getComputedStyle()查询到的结果,是“过于仁慈的双胞胎”。 如果说element.style给你太少的话,那么window.getComputedStyle(element)就是对你爱的太深,给你太多太多。

这里有一个demo:https://codepen.io/impressivewebs/pen/XyWKJE

这里例子中,background是一个”lightblue“这样的单值,而window.getComputedStyle()给了太多太多,把background缩写属性的所有值都返回了回来。 多余的返回回来的这些默认值,往往是这些css属性的初始值(默认值)。

下面这个例子展示了background、animation、flex通过window.getComputed()获取的结果: https://codepen.io/impressivewebs/pen/OaJXjR

window.getComputed(p).background  
// rgba(0, 0, 0, 0) none repeat scroll 0% 0% / auto padding-box border-box

window.getComputed(p).animation 
// none 0s ease 0s 1 normal none running

window.getComputed(p).flex 
// 0 1 auto

同样,对于类似width和height的属性,它揭露出元素的计算维度,不管这些值在CSS中有没有明确的定义,就像下面的demo这样: image

某种程度上相当于读取window.innerWidth的值,除了这是指定元素的指定属性的计算css,而不仅仅是一个window或者viewport的测量值。

通过window.getComputedStyle(),有几种不同的获取属性的方式。我们已经看了一种通过点语法和驼峰属性名的方式。其实还有另外两种:

// 第一种
window.getComputedStyle(el).backgroundColor
// 第二种
window.getComputedStyle(el)['background-color']
// 第三种
window.getComputedStyle(el).getPropertyValue('background-color')

第二种方式会警告。 建议通过第三种:1.不转换为驼峰获取属性值。 2. 不用转换为cssFloat获取值

获得伪元素的计算样式

有一个知者甚少的关于window.getComputedStyle()的小知识点:可以查询到伪元素的样式。 比如你看到下面这样的代码:

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

注意第二个参数null。 Firefox4之前的兼容代码和降级代码会有这样的写法。但是在现代的浏览器中,不再需要这样写了。

第二个可选的参数现在用于声明:我在获取一个伪元素的计算CSS。

看一下下面的代码:

.box::before {
 content: 'Example';
  display: block;
  width: 50px;
}

通过下面的代码,我可以获得.box元素的伪元素的计算样式:

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

可以参考这个demo: https://codepen.io/impressivewebs/pen/YRXzdm

不仅仅可以获得 ::before,::after这种常见的伪元素,还可以获得类似::first-line这种伪元素的计算样式:

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

可以参考这个demo:https://codepen.io/impressivewebs/pen/rQVajQ

这里有一个firefox正常其他浏览器出bug的使用::placeholder的方式:

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

https://codepen.io/impressivewebs/pen/ZmGGXG

需要注意的是,浏览器在获取不存在的(但是有效的)浏览器不支持的伪元素时,会有不同的表现,比如自定义的伪元素(::banana)

https://codepen.io/impressivewebs/pen/VVLLgx

::banana, firefox不打印,chrome打印出默认值。

firefox有一个getDefaultComputedStyle()方法,不过不是规范的而且很少用到。

CSSStyleDeclaration API

前面我展示了如何通过style对象或者是window.getComputedStyle()去获取属性,它们两个其实都是通过CSSStyleDeclaration interface去暴露出去的。

用其他话说,下面的代码返回的,都是document.body元素的CSSStyleDeclaration对象:

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

看下面的截屏,你可以看到:style大多是空的,而window.getComputedStyle()是都有值的。 image

但是window.getComputedStyle()得到的值,是只读(read-only)的。 element.style得到的值,setter和getter是都有的,但是遗憾的是这些值仅仅影响document的inline style。

setProperty(),getPropertyValue()和item()

如果你通过上面的几种方式之一暴露出了CSSStyleDeclaration对象,会有很多有用的方法去读取和修改这些值。 再强调一遍,getComputedStyle()是只读的,但是通过style属性得到的属性既有setter,也有getter。 考虑以下的代码:

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)}`;

image 有一个在线demo:https://codepen.io/impressivewebs/pen/vQOKxb

在这个例子中,我们用了3个style的方法:

使用removeProperty()

除了上面的3个setProperty(), getPropertyValue()和item(),CSSStyleDeclaration还暴露了两个其他的方法。 代码和demo中,我将使用removeProperty()方法:

box.style.setProperty('font-size', '1.5em');
box.style.item(0) // "font-size"

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

通过js移除一个css属性: https://codepen.io/impressivewebs/pen/dQoBZq

获取和设置属性的优先级

还有一个很有趣的方法,叫做getPropertyPriority()。

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'); // ""

https://codepen.io/impressivewebs/pen/EOjqKd

box.style.setProperty('font-family', 'Georgia, serif', 'important');设置了!important,可以设置成空或者空字符串去没有important优先级。

如果我有以下的代码:

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

会为所有border缩写相关的属性返回important

// 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'));

CSSStyleSheet Interface

inline样式不常用,computed style常用但是会有困扰。

还有一个很好好用的查询样式表的api:CSSStyleSheet。 从document的样式表查询信息的最简方式是通过styleSheets属性。

可以这样查看当前页面有多少样式表:

document.styleSheets.length; // 1

可以通过这样去得到样式表的引用:

document.styleSheets[0];

image

cssRules是最有用的属性。 cssRules中包括了block声明,at-rule和media rules等等。 在后面的章节中,我将详细介绍如何通过stylesheet object去读取和修改样式。

与Stylesheet Object一起工作

* {
  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;
}
let myRules = document.styleSheets[0].cssRules,
    p = document.querySelector('p');

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

https://codepen.io/impressivewebs/pen/VVemNb

The selector text for each style rule in the stylesheet is:
*
body
main
.component
a:hover
code

type为1的意思是:STYLE _RULE。 其他的type类型分别为:IMPORT_RULE(3), MEDIA_RULE(4),KEYFRAMES_RULE(7)等等。

seletcorText是规则关联的选择器。它是可写的属性。 如果我想修改a:hover为a:hover, a:active,可以这样写:

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

https://codepen.io/impressivewebs/pen/oQbYKZ

通过CSSOM获取@media规则

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 += `<c​ode>${j.keyText}</c​ode><br>`;
    }
  }
}

https://codepen.io/impressivewebs/pen/MzyeWd

还可以查到媒体查询的查询条件:conditionText/mediaText

let myRules = document.styleSheets[0].cssRules,
    p = document.querySelector('.output');

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

https://codepen.io/impressivewebs/pen/OaNXgo

通过CSSOM获取@Keyframes规则

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 += `<c​ode>${j.keyText}</c​ode><br>`;
    }
  }
}

https://codepen.io/impressivewebs/pen/MzybxL

"0%"
"20%"
"100%"

from和to代表的是0%和100%。

// 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;

还可以查到keyframe的name:

let myRules = document.styleSheets[0].cssRules,
    p = document.querySelector('.output');

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

https://codepen.io/impressivewebs/pen/oQxWGg

可以打印出帧动画的颜色:

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 += `<c​ode>${j.style.color}</c​ode><br>`;
    }
  }
}

https://codepen.io/impressivewebs/pen/jQqmXp

增加和删除CSS声明

可以通过insertRule()和deleteRule()去增删规则。

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

https://codepen.io/impressivewebs/pen/QJNMgN

insertRule的第二个参数是可选的,用来标记插入的位置,默认从头部0位置插入,类似数组的unshift操作。

let myStylesheet = document.styleSheets[0];
console.log(myStylesheet.cssRules.length); // 8

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

https://codepen.io/impressivewebs/pen/OaNjxL

重访CSSStyleDeclaration API

<div style="color: lightblue; width: 100px; font-size: 1.3em !important;"></div>
.box {
  color: lightblue;
  width: 100px;
  font-size: 1.3em !important;
}
document.querySelector('div').style
document.styleSheets[0].cssRules[0].style
// 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');

https://codepen.io/impressivewebs/pen/aQZvXB

CSS Typed Object Model...未来?

CSS Typed OM