如何通过JavaScript API处理CSS

发布于 大漠

很多场景我们是需要借助JavaScript相关的API来帮助我们处理Web页面中的CSS。CSSOM(CSS Object Model)中提供了一些JavaScript的API处理CSS。除此之外,还可以通过JavaScript来操作DOM元素中的attribute样式和类来操作CSS。由于JSX和无数JavaScript框架等概念的出现,使得JavaScript API与DOM交互变得越来越流行,但是对CSS使用类似的技术似乎变得没有那么多人关注。当然, CSS-in-JS解决方案是存在的,但是最流行的方案都是基于编译的,在生产中输出CSS时不需要任何额外的运行时。这当然对性能有好处,因为使用CSS API可能会导至额外的重游,这使得它和使用DOM API一样需要。但这并不是我们想要的。如果我告诉您,您不仅可以操作DOM元素的样式和CSS类,而且还可以创建完整的样式,就像使用HTML和JavaScript一样,那会怎么样呢?

在这一篇文章中,我们就来一起重新聊聊如何借助JavaScript相关的API来处理CSS。

CSS的使用方式

熟悉CSS的同学应该都知道,使用CSS有多种方式,常见的主要有:

  • 行内样式:这是一种较老的方式,就是在DOM元素中通过style的属性来给元素设置样式。这是一种不好的使用习惯,会让你的样式难以维护和扩展。当然它并不是一无事处,在一些特殊的场景,行内样式会让你变得更为容易。大多数情况之下,都是通过JavaScript来给元素添加行内样式
  • CSS类:借助元素的类(class)来添加或删除样式,在Web中也是常见的,特别是在一些交互场景之下,会通过添加和删除类的方式来控制DOM元素的样式
  • 样式表:大部分场景,样式都是以样式表来承载,这样更易于维护你的CSS,也更易于达到结构、表现和行为的相互分离

事实上,任何一个Web页面都可以看到HTML(DOM元素)、CSS和JavaScript三者的身影。而他们之间的关系,我想任何一位前端开发同学都清楚,用张图来描述,或许会更为形象:

针对上述的相应的场景,JavaScript提供相应的API,可以更好的帮助CSSer操作CSS。让HTML,CSS和JavaScript结合在一起,做一些更有意思的事情。接下来,我们来看看JavaScript操作CSS提供了哪些API。

行内样式

在探索复杂的操作之前,我们先从简单的入手。

行内样式(也有人称之为内联样式)是显式的给DOM元素设置style的属性。这样一来,我们就可以通过修改DOM元素(即**HTMLElement**)的style属性的值,从而达到修改DOM元素的行内样式。

如果通过JavaScript相关的API来修改HTMLElement的行内样式,也有多种方式:

  • 修改HTMLElementstyle对象中对应的CSS属性
  • 修改HTMLElementstylecssText的值
  • 借助DOM属性setAttribute()来修改HTMLElementstyle

具体来看看代码:

const bodyEle = document.body

// 通过'.style.property'方式添加行内样式
bodyEle.style.backgroundColor = '#000'

// 通过'.style.cssText'方式添加行内样式
bodyEle.style.cssText = 'color: red'

// 通过`setAttribute('style', 'property: value')`方式添加行内样式
bodyEle.setAttribute('style', 'font-size: 1rem')

上面的代码向大家演示了修改行内样式的多种方式。不管是哪种方式,都们都有一个共性,即通过JavaScript相应的API来修改HTMLElement元素的style。虽然手段不同,但达到的效果是一致的,不过在使用的细节上有所差异。

如果使用HTMLElement.style设置样式的话,对应的CSS属性名需要采用驼峰的方式,比如:

// 正确的方式
ele.style.borderColor = 'red'

// 不正确的方式
ele.style.border-color = 'red'

如果使用.style这种方式要给HTMLElement添加多个行内样式时,需要显式的书写多次:

bodyEle.style.backgroundColor = '#000'  // 添加`background-color`
bodyEle.style.borderColor = '#000'      // 添加`border-color`
bodyEle.style.borderWidth = '2px'       // 添加`border-width`

这种方式是一种低效而又冗余,甚至是难于维护的方式。事实上如果需要通过使用JavaScript的API给HTMLElement同时添加多个样式,除了给元素添加一个类名(后面会介绍)之外,还可以使用.style.cssText = ''这种方式或者使用.setAttribute('style', '')方式:

// 使用
bodyEle.style.cssText = 'background-color: red; color: green; font-size: 1rem'

// 或使用
bodyEle.setAttribute('style', 'font-size: 1rem;color: green; background-color: yellow')

请注意:不管使用上面哪种方式,都将完全重置HTMLElement元素的内联样式,因此需要在参数中包含所有需要的样式(甚至是以前未更改的样式)

这是较为古老的使用方式,但随着浏览器的发展,我们可以使用JavaScript的一些新的API来达到同样的效果,比如Object.assign()HTMLelement.style一次性添加多个行内样式:

Object.assign(bodyEle.style, {
    backgroundColor: '#f36', 
    margin: '20px', 
    border: '1rem solid green'
})

使用Object.assign(HTMLElement.style, {})方式给HTMLElement添加行内样式和前面介绍的几种方式有所不同,该方式并不会覆盖HTMLElement原有的行内样式

const bodyEle = document.body

bodyEle.style.cssText = 'background-color: red; color: yellow; font-size: 1rem'

Object.assign(bodyEle.style, {
    backgroundColor: '#f36', 
    margin: '20px', 
    border: '1rem solid green'
})

结果如下:

而在新的CSS Typed Object Model(Typed OM)中,可以通过向CSS值添加类型、方法和适当的对象模型来扩展。即使用HTMlelement.attributeStyleMap.set('property', vaule)来替代HTMLelement.style给元素设置样式:

bodyEle.attributeStyleMap.set('opacity', .3);

// 或

bodyEle.attributeStyleMap.set('opacity', CSS.number(0.3));

带有单位的可以这样写:

bodyEle.attributeStyleMap.set('padding', CSS.px(42));

另外,.attributeStyleMap类似于Map对象,所以它们支持对象常有的一些方法,比如getsetkeysvaluesentry等。这样让我们的工作也变得更为灵活:

$0.attributeStyleMap.set('background-color', 'green') // 设置background-color的值为green 
$0.attributeStyleMap.get('background-color').value === 'green' // => false 
$0.attributeStyleMap.has('background-color') // => true 
$0.attributeStyleMap.delete('background-color') // => 删除background-color 
$0.attributeStyleMap.clear() // => 删除所有样式

新的CSS Typed Object Model(Typed OM)中CSS Houdini中的Typed OM的一部分。如果你从未接触过CSS Houdini,而又对这部分知识感兴趣,可以点击这里获取相关的教程

这些虽然是一些基础,事实上这些基础比你想象中的要更复杂得多。.style对象实现了CSSStyleDeclaration接口。这意味着它带有一些有趣的属性方法!包括前面提到的.cssText之外还包括.length(因为HTMLelement.style是一个对象)和.item().getPropertyValue()以及.setPropertyValue()等方法。这些属性和方法都允许你对元素的内联样式进行操作。

const bodyEle = document.body

const propertiesCount = bodyEle.style.length

for (let i = 0; i < propertiesCount; i++) {
    const name = bodyEle.style.item(i)
    const value = bodyEle.style.getPropertyValue(name)
    const priority = bodyEle.style.getPropertyPriority(name)

    if (priority === 'important') {
        bodyEle.style.removeProperty()
    }

    console.log(`${name}: ${value}`)
}

上面的代码,浏览器输出的结果如下:

在迭代中,使用.item()方法时具有索引值,那么下面两种方式是相同的:

bodyEle.style.item(0) === bodyEle.style[0] // => true

上面我们采用了for循环来遍历bodyEle.style中的各个属性名称和属性值,在JavaScript中还有其他一些遍历方式:

有关于CSSStyleDeclaration中提供的方法更详细的介绍可以阅读MDN上的相关文档

CSS类

使用CSS的类给相应的HTMLelement元素添加相应的样式比在行内添加样式要显得更高级的多,而且也更易于维护和管理你的CSS。同样的,JavaScript中可以使用.className相关的属性和方法来给元素添加、删除或修改类名。

首先我们可以使用HTMLelement.className设置相关的字符串(字符串就是你想要的类名名称),给元素添加类名:

bodyEle.className = 'theme-dark'

如果要同时添加多个类名时,需要用空隔符来分隔:

bodyEle.className = 'theme theme-dark'

使用.className来给元素添加类名时有一个细节需要注意,它和.style.cssText给元素添加内联样式有点类似,如果元素原有类名的话,需要把原有的类名一起附上,否则.className的值将会覆盖原有的类名。如果不想覆盖的话,可以借助JavaScript的运算符+来实现:

bodyEle.className += ' class1 class2'

另外,也可以使用HTMLelement.setAttribute('class', 'className1 className2')给元素添加类名。使用这个属性添加类名时和.className一样,如果不想覆盖元素原有的类名的话,需要把原有的类名添加上。

这样一来,就可以通过类名来给元素添加或删除样式。

除此之外,JavaScript还提供了一个更优秀的API,即**.classList**属性。.classList属必实现了DOMTokenList,提供了一大堆有用的方法:

  • .item():根据传入的索引值返回一个值,如果索引值大于等于符号列表的长度(length),则返回undefinednull
  • .contains():判断元素是否包含相应的类,如果包含则返回true,否则返回false
  • .add():给元素添加一个或多个类名,如果添加多个类名时,需要以空格分隔开
  • .remove():从元素中删除一个或多个类名,如果删除多个类名时,需要以空格分隔开
  • .toggle():从元素中移除类名,并返回false,如果传入的类名在元素中不存在,则会将相应的类名添加到元素中,并返回true
  • .replace():替换元素中的类名

另外,.classList还具有length属性,可以判断元素class属性有几个值,另外还可以使用.item().entries().forEach()对元素的类进行遍历:

const bodyEle = document.body
const classNames = ['theme', 'them-dark']

classNames.forEach(className => {
    if (!bodyEle.classList.contains(className)) {
        bodyEle.classList.add(className)
    }
})

有关于JavaScript操作元素的类名更详细的介绍可以阅读《样式和类》一文。

样式表

有的时候我们需要通过JavaScript的API来操作样式表。在Web中我们一般通过<link><style>标签来管理样式表:

  • <link>:通过href来引用样式表,可以是项目内的也可以是第三方的相关样式表,主要以.css文件为主
  • <style>:通过使用<style>标签,将样式内嵌到Web文档中

在JavaScript中,有一个StyleSheetList接口,我们可以通过document.styleSheets属性来获取到文档中所有的样式表。在一个Web文档中有可能会使用多个样式表,无论是来自外部样式表还是使用<style>管理的样式,都可以使用document.styleSheets为把Web文档中所有样式表获取:

使用document.styleSheets获取的样式表集合是一个类数组对象,可以通过一些遍历的方式将每个样式表遍历出来,比如使用for循环:

for (styleSheet of document.styleSheets) {
    console.log(styleSheet)
}

这就是StyleSheetList的全部内容,我来把重点放到CSSStyleSheet身上。因为CSSStyleSheet让事情变得更有趣 —— CSSStyleSheet扩展了StyleSheet接口,并且只有一些只读属性与此关联。比如.ownerNode.href.title.type,这些属性大多直接取自声明给定样式表的地方。

而我们更感兴趣的内容都集中在CSSStyleSheet这个接口中。该接口允许有两个方法:.insertRule().deleteRule()

const ruleIndex = styleSheet.insertRule('div {background-color: #f36}')
styleSheet.deleteRule(ruleIndex)

这些方法使用索引和类似CSS的字符串进行操作。由于CSS规则的顺序对于决定使用哪个样式规则非常重要。而.insertRule()允许你为新规则传递可选索引。

另外CSSStyleSheet还有两个属性:.ownerRule.cssRules。其中.ownerRule@import相关,也就是说,一般情况之下我们使用.cssRules更多。简单地说,它是CSSRulesCSSRuleList,这样一来,可以使用前面提到的两个方法.insertRule().deleteRule()方法来修改。

注意,有些浏览器可能会阻止你访问来自不同来源(域外)的外部CSSStyleSheet.cssRules

对于CSSRueList来说,它也是CSSRules的可迭代集合,也可以对其进行遍历。比如说通过它们的索引或.item()方法访问它的CSSRules。但是你不能直接修改CSSRuleList,如果要修改CSSRuleList得使用前面提到的相关方法来操作,否则没有别的方法。

CSSRuleList包含实现CSSRule接口的对象,它带有像.pparentStyleSheet这样的属必,还有可以包含给定规则的所有CSS代码的.cssText属性,这是一个我们非常熟悉也是很重要的属性(比如我们前面给元素添加样式时就看到了.cssText身影)。另外还有.type属性。它根据指定的常量指示给定CSSRule的类型。你可能也知道,我们在写CSS的时候,除了会使用瑟标准相关的CSS规则之外,还会通过@import@keyframe添加样式规则。不同类型的CSSRule具有相应的接口。由于我们不会直接创建它们,而是只会使用类似CSS的字符串,所以不必要了解这些扩展接口提供的相应属性。

对于CSSStyleRule而言,它有.selectorText.style属性。第一个是以字符串的形式指示用于CSS规则的选择器;第二个选项是实现CSSStyleDeclaration接口的对象。

const ruleIndex = styleSheet.insertRule("div {background-color: #f36}");
const rule = styleSheet.cssRules.item(ruleIndex);

rule.selectorText;              // => "div"
rule.style.backgroundColor;     // => "#f36"

上面有关于操作CSS规则或CSS样式表相关的JavaScript API都有所涉及。大家对其也有一定的了解,如果还想更深入的了解,可以阅读CSSOM相关的规范或者下面几篇文章:

使用JavaScript API给样式表添加样式规则

现在的Web应用程序都会涉及大量的JavaScript,我们也在不断的在寻找更多的方法来保持它们的事度(性能),比如:

  • 使用事件委托来保持事件监听的效率
  • 拆解函数来限制给定方法可使用的次数
  • 使用JavaScript加载器只加载所需的资源
  • ...等等

而使用JavaScript来操作CSS又是非常常见的,为了提高页面效率和速度的方法是动态地直接向样式表添加或删除样式规则,而不是不断的查询DOM中的元素来添加样式或删除样式。前面也一起探讨有关于JavaScript操作CSS样式表相关的API,在这里我们来小结一下,因为以下几个场景是我们在实际使用中会常用到的。

获取样式表

前面我们也聊到,可以使用document.styleSheets来获取Web文档中所有样式表(不管是<link>的还是<style>)的。通过该方式获取的是一个类似数组的对象,除了使用for循环这样的方式来遍历之外,还可以通过索引号来获取,比如:

const sheet = document.styleSheets[0]

除此之外,还可以通过获取linkstyle之样的标签元素的sheet属性来获得CSSStyleSheet对象:

document.querySelector('link[rel="stylesheet"]').sheet

如果使用这种方式来获取样式表,最好能在<link><style>声明id值,这样更便于JavaScript选择器相关的API更易于获取到你想要的样式。

创建新的样式表

很多情况下会采用动态创建一个<style>元素给Web文档添加新的样式表,这样操作简单:

var sheet = (() => {
    // 创建`<style>`标签元素
    let style = document.createElement('style')

    // 根据需要添加你所要的相关属性,比如
    // 1. media 添加媒体查询值 => style.setAttribute('media', 'all')

    // 将新创建的`<style>`标签元素添加到Web文档中
    document.head.appendChild(style)

    return style.sheet

})()

上面的代码给文档创建了一个空的(没有任何样式规则)样式表,并且插入到</head>中:

插入样式规则

前面我们看到了,新插入到Web文档中的sheet样式表是一个空的样式表,在<style></style>中没有任何样式规则。如果我们需要给样式表插入样式规则可以使用.insertRule()

sheet.insertRule('header{background-color: #f36;}', 1)

.insertRule()中有两个参数,第一个参数就是所需要的样式规则,第二个参数index表示新插入的样式规则的索引。这个很有帮助,有助于我们插入相同的样式规则哪个权限更大(会使用)。index的默认值是-1,这意味着在集末的尾末插入样式规则。

除了.insertRule()方法之外,CSSStyleSheet对象还有一个.addRule()方法(该方法还不是一个标准方法),也允许我们在样式表中添加CSS规则。.addRule()方法接受三个参数:

  • 第一个参数是选择器
  • 第二个参数是CSS样式规则
  • 第三个是新添加样式规则的位置,索引值从0开始

就上面的代码如果使用.addRule()方法实现的话会是下面这样:

sheet.addRule('div', 'background-color: #f36;', 1)

注意,.addRule()调用在所有情况下都返回-1的结果

记住,这里的优势是,从页面中添加的元素会自动应用样式;也就是说,不需要在元素被注入页面时将它们添加到元素中。效率高

如果想更安全的给样式表添加样式规则,可以写个函数,将.insertRule().addRule()两个方法结合在一起:

function addCSSRule(sheet, selector, rules, index) {
    if ('insertRule' in sheet) {
        sheet.insertRule(selector + '{' + rules + '}', index)
    } else if ('addRule' in sheet) {
        sheet.addRule(selector, rules, index)
    }
}

addCSSRule(document.styleSheets[0], 'header', 'border: 2px solid red')

如果你担心在应用程序中动态添加样式表规则会有一定的风除,那么可以将此方法封装在try {} catch(e) {}中。

实战一把

随着React、Vue这些优秀的框架出现,很多同学都喜欢CSS-in-JS的模式,特别是React开发的同学。那么我们是否可以借助JavaScript给CSS提供的相关API的能力来创建一个小型的CSS-in-JS呢?

思考一下,它的基本思想应该是:我需要创建一个函数,给它传递的是一个简单的样式配置对象,然后输出一个新创建的CSS类,供以后使用

工作流程很简单。先来创建能访问某种样式表的函数,并且只需要使用.insertRule()方法让可配置的CSS样式规则能正常运行。

// 创建一个随机名称函数
function createRandomName() {
    const code = Math.random().toString(36).substring(7);
    return `css-${code}`;
}

// 创建一个样式编译的函数
function phraseStyle(style) {
    const keys = Object.keys(style);
    const keyValue = keys.map(key => {
        const kebabCaseKey = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
        const value = `${style[key]}${typeof style[key] === 'number' ? 'px' : ''}`;
        return `${kebabCaseKey}:${value};`;
    })
    return `{${keyValue.join('')}}`;
}

// 创建类名名称函数
function createClassName(style) {
    const className = createRandomName();
    let styleSheet;
    for (let i = 0; i < document.styleSheets.length; i++) {
        if (document.styleSheets[i].CSSInJS) {
            styleSheet = document.styleSheets[i];
            break;
        }
    }

    if (!styleSheet) {
        const style = document.createElement('style');
        document.head.appendChild(style);
        styleSheet = style.sheet;
        styleSheet.CSSInJS = true;
    }

    styleSheet.insertRule(`.${className}${phraseStyle(style)}`);
    return className;
}

来验证一下,上面的函数是否起作用:

<!-- HTML -->
<div id="app"></div>

// JavaScript
const el = document.getElementById('app');
const styleRules = createClassName({
    width: 300,
    height: 200,
    backgroundColor: '#f36'
})

el.classList.add(styleRules)

特别声明,上面示例代码来自于@areknawo的《Messing with CSS through its JavaScript API》一文。

如果使用浏览器调试器查看代码的话,你会发现div#app添加了一个随机生成的类(通过createRandomName())。同时createClassName()函数中通过.insertRule()创建的新样式表添加到</head>之前。另外还需要一个类似编译的函数,将createClassName()中定义的样式编译出来,添加相应随机生成的类中。而个样式编译的功能就是phraseStyle()函数来完成:

小结

文章内容主要聊了一些操作CSS的一些JavaScript API,当然这些只是一些常见的,也是一些基础相关的API,但往往这些基础的API给我们项目的开发时带来无穷的力量。在解决实际问题时能给我们提供更好的思路和灵感。文章中在学习操作CSS的相关JavaScript API时还涉及到了很多DOM相关的知识,借此机会再次重温了前面学习的一些内容,让我有一个新的体感(很多东西还是要动手写写,不然忘得快)。当然,有关于CSS相关的JavaScript API,其实大部分都在CSSOM中,正好前段时间也整理一些有关于CSSOM相关的内容,建议你可以结合起来一起阅读。如果您在这方面有更多的经验,欢迎在下面的评论中与我们一起共享。