分享些平常写 CSS 遇到的问题和一些解决方案。

样式冲突

样式冲突的本质是一个 dom 匹配到了多条 CSS 规则,而我们没办法确定最后生效的样式有哪些。 这带来的问题有:

  • 由于 CSS 作用域和优先级的原因,导致期望的样式被覆盖了
  • 引入的非期望的样式

作用域和优先级

有时候我们需要去覆盖样式,所以我们不得不考虑优先级的问题。

<style>
  .container h1 {
    color: #f00;
  }
  .title {
    color: #000;
  }
</style>
<div class="container">
  <h1 class="title">title</h1>
</div>

计算优先级的规则是:

  1. !important 的优先级最高,inline style 其次
  2. 不同选择器有不同的权重,以权重最高的选择器数值决定优先级
  3. 如果根据选择权重算出来优先级一样,后声明的样式优先级更高。

在 CSS 有哪些反人类的地方?的评论区里有关于按照 html 结构的远近来决定优先级的讨论。 如果按照 html 结构的远近来决定优先级,那么对于下面这种情况要如何处理?

<style>
  div div p {
    color: red;
  }
  div p {
    color: black;
  }
</style>
<div>
  <div>
    <p>balabala</p>
  </div>
</div>

为了解决这个问题,我们写 CSS 时一般会遵守一些约定:

  • 不用!important
  • 尽量不用 inline style
  • class 选择器数量最好不超过 3 个
  • 选择器声明更具体,必要时可以用 > 选择器
<style>
  .article .title {
    font-size: 30px;
    color: black;
    font-weight: bold;
    margin: 16px 0;
  }
  /* 后来加的 */
  .article .title:hover {
    background: red;
  }
  /*.article .header .header-title:hover {
   background: red;
   } */
  .article .content .title {
    font-size: 24px;
    /* color: black; */
    /*  font-weight: bold;*/
  }
</style>
<div class="article">
  <header class="header">
    <h1 class="title header-title">title</h1>
  </header>
  <div class="content">
    ...
    <h1 class="title content-title"></h1>
  </div>
</div>

将选择器写的更宽泛来达到样式复用其实是不好的,更好的做法是将公共的部分单独抽一个 class,然后在 html 上使用这个 class

命名冲突

1. BEM 命名规范

BEM 是块 (block)、元素 (element)、修饰符 (modifier) 的简写,由 Yandex 团队提出的一种前端 CSS 命名方法论。 在 BEM 命名规范中 - 中划线仅作为单词的连接符,__ 双下划线用来连接块和子元素,_ 单下划线用来表示状态。在 BEM 的基础上,社区衍生出类似的命名规范,比如有的命名规范使用 camel case 来命名实体,有的用两个连字符 (--) 来分隔修饰符。

<nav class="navgation">
  <a class="navgation__link navgation__link_selected">
    <i class="navgation__icon"></i>
    <span class="navgation__text"></span>
  </a>
  <a class="navgation__link">
    <i class="navgation__icon"></i>
    <span class="navgation__text"></span>
  </a>
  <a class="navgation__link">
    <i class="navgation__icon"></i>
    <span class="navgation__text"></span>
  </a>
</nav>
<style>
  // SCSS
  .navgation {
    &__link {
      &_selected {
      }
    }
    &__icon {
    }
    &__text {
    }
  }
</style>

可以看出,使用 BEM 命名规范,在 HTML 中有一定的语义化。但是缺点也很明显,那就是太长了...... 即使配合 CSS 预处理器,写起来也还是非常难受。

2. CSS Module

你无需通过 class 嵌套来限定样式的作用范围,CSS Module 会将你 CSS 文件里的 class 编译成一个独一无二的名字,然后在组件里 import 这个 CSS 文件得到一个对象,这个对象就是编译前的 class 和编译后的 class 的映射。

// component.module.css
.title {
  font-size: 1.2rem;
}
.name {
  color: #222;
}
import styles from './component.module.css';
// styles = {
//  title: 'component**title**b8bW2',
//  name: 'component\_\_name_t0gZ1'
// }
function Component() {
  return (
    <div>
      <h2 className={styles.title}></h2>
      <span className={styles.name}></span>
    </div>
  );
}

CSS Module 的优点是侵入性小,因为不需要修改你的 CSS,并且可以无缝配合预处理器,PostCSS 来使用。CSS Module 还提供 :local(.className):global(.className) 来声明局部样式和全局样式,composes 也可以将多个 class 组合,方便样式的复用。 需要的注意的是,因为 kebab-case 风格的命名对 JavaScript 不友好,所以推荐 class 命名采用 camelCase 风格。 另外还有一点,由于 CSS Module 需要 className={styles.xxx} 这样来使用。这对编辑器的 Emmet 插件不是很友好。

.container>.header>.title>span{hello}
<div className="container">
  <div className="header">
    <div className="title"><span>hello</span></div>
  </div>
</div>
ul>li.item*5
<ul>
  <li className="item"></li>
  <li className="item"></li>
  <li className="item"></li>
  <li className="item"></li>
  <li className="item"></li>
</ul>

React CSS Modules 库提供了一个高阶组件,可以将你的 CSS Module 绑定到 styleName 属性上。当你需要在组件上添加一个全局的 className 的时候,你可以很方便的声明 CSS Modules 的样式和全局样式,我们就不需要 classnames 库来完成多个 className 的拼接。

import React from "react"
import CSSModules from "react-css-modules"
import styles from "./table.css"
class Table extends React.Component {
  render() {
    return (
      <div styleName="table">
        <div styleName="row">
          <div styleName="cell">A0</div>
          <div styleName="cell">B0</div>
        </div>
      </div>
    )
  }
}
export default CSSModules(Table, styles)

值得一提的是,CSS Modules 在生产环境可以压缩 class 名字的长度,这对于一些,class 名字特别长,选择器嵌套特别深的项目,能优化很多。

3. Scoped CSS

Vue SFC 借助 Vue Loader 实现了 Scoped CSS。和 CSS Modules 类似,不同的是 CSS Modules 是运行时将 className 绑定到 dom 上,而 Scoped CSS 在编译时会在组件 dom 上添加一个特殊的属性作为 id,CSS 选择器声明中会带上这个 id,从而将样式限制在局部范围内。

<template>
  <div>
    <h1 class="title" data-v-4c6132b9></h1>
    <span class="name" data-v-4c6132b9></span>
  </div>
</template>
<style>
.title[data-v-4c6132b9] {
}
.name[data-v-4c6132b9] {
}
</style>

当需要在组件外影响组件内的样式时,可是通过 /deep/ 来告诉编译器,这样在编译这一条 CSS 规则时选择器就不会加上 data-v-4c6132b9 这种标识。这比 CSS Modules 使用起来更加方便一些。

动态性

最常见的需要动态的场景就是主题切换了。相比与通过预处理器的变量来编译出多套 CSS,CSS Variables 更方便使用一些。但是 CSS Variables 也存在问题...

// less
@primary: #80e619;
.button {
  background: @primary;
  &:hover {
    background: lighten(@primary, 20%); // #b3f075
  }
}
:root {
  --primary: #3d7e9a;
  --primary-light: #b3f075;
}

CSS Variables 的值是在运行时确定的,而预处理器是在编译器工作的。在不借助 JS 的情况下,这需要你事先计算好衍生出来的变量值。如果要把主题的颜色变量暴露给用户来配置,那就只能借助于 JS。而用 JS 来维护 CSS Variables 仍然是有点麻烦且不优雅 (个人观点),而且你在 CSS 里用到的变量居然要去查看你的 JS 代码才能知道它在哪里被声明以及它的值,这也带来了一个维护性的问题。

于是 Vue rfcs 有一个提案 Component State Driven CSS Variables,借助 Vue Loader 的编译能力,可以通过声明将组件的 state 编译成 CSS Variables 的声明,算是解决了一部分问题。

CSS in JS

React 社区流行 ALL IN JS,CSS 也不例外。

CSS in JS 的一个优点是,不需要单独创建一个 CSS 文件,然后通过 JS 引入,所有的样式都写在组件里。作为一个组件库的话,就不需要用户手动去引入组件的 CSS 了。另外一个是,样式和组件写在一起是不是更容易维护就仁者见仁智者见智了。

除此之外,我觉得动态性是 CSS in JS 最大的优势。由于是通过 JS 里生成样式,所以极度灵活,你可以很方便的切换样式主题,或根据不同的 props 声明不同的规则。

styled-components 是一个 CSS in JS 库,它有个诱人的特性是,CSS 是在组件渲染时才注入的。这等于天生支持代码分离,你页面上的 CSS,全是当前渲染的组件里会用到的,时刻保持的最小集。

对于组件库,现有的组件库打包方式一般是 CSS 和 JS 分开打包,因此用户在使用组件库时,需要手动去 import CSS 文件。而 CSS in JS 可以不产出 CSS (一些 CSS in JS 库也支持通过 babel 插件编译出 CSS 文件,如 emotion),因此也就无需用户手动引入 CSS 文件。

// styled components
const Title = styled.h1`
  font-size: 1.5em;
  text-align: center;
  color: palevioletred;
`
const Wrapper = styled.section`
  padding: 4em;
  background: papayawhip;
`
render(
  <Wrapper>
    <Title>Hello World!</Title>
  </Wrapper>
)

然而 CSS in JS 也是有缺点的:

  • 不支持 px 自动转换成 rem,虽然 rem 不是什么好的方案
  • 有运行时的开销
  • CSS in JS 库有一定的体积
  • 不好复用,CSS 容易冗余

Atomic CSS

Tailwind

Tailwind 是一个高度可定制的基础层 CSS 框架,与其它 CSS 框架不同的是,Tailwind 并不提供预先设计好的内建组件。相反,Tailwind 采用的 Atomic CSS,提供了更基础的工具类 (utility classes),可以让你直接在 HTML 源码上构建一个完全定制化的设计。它可以把帮助你构建出自己的设计系统 (Design System)

<div class="md:flex">
  <div class="md:flex-shrink-0">
    <img
      class="rounded-lg md:w-56"
      src="https://i.loli.net/2019/05/01/5cc8c3fd1ba36.jpg"
    />
  </div>
  <div class="mt-4 md:mt-0 md:ml-6">
    <div class="uppercase tracking-wide text-sm text-indigo-600 font-bold">
      Marketing
    </div>
    <a
      href="#"
      class="block mt-1 text-lg leading-tight font-semibold text-gray-900 hover:underline"
    >
      Finding customers for your new business
    </a>
    <p class="mt-2 text-gray-600">
      Getting a new business off the ground is a lot of hard work. Here are five
      ideas you can use to find your first customers.
    </p>
  </div>
</div>

Atomic CSS 几个特点:

复用性强,易于管理

以往我们编写 CSS 时,都是 CSS 依赖 HTML。例如 BEM 命名规范,因为其 class 命名往往和组件名还有 HTML 结构相关。这导致我们在编写 CSS 时,关注的是 HTML 的结构,而不是实际的样式。更要命的是,如果两个不同的组件,他们大部分样式是一样,只有小部分是不一样的,那该怎么办?将重复的样式提取出来?那么按照 BEM 命名规范的话,重复的这一部分要怎么命名呢?

推荐阅读 Tailwind 作者之一 Adam Wathan 的一遍文章 CSS Utility Classes and Separation of Concerns

不会有作用域的问题

Atomic CSS 每个声明都只有一条语句,且每个声明都是独立的,互不影响。样式都是通过组合来完成,所以你只需编写很少量的 CSS,几乎不需要你手动维护 CSS。

覆盖样式的话,足够简单

你只要在 html 上添加一个你自己的 class,最多使用两个选择器就可以覆盖掉工具类的样式。另外是 Tailwind 也支持自定义配置加一些工具类。更合理的修改样式的应该是通过动态切换来实现。className={isRed ? 'color-red' : 'color-white' }

CSS 体积小

这本是 Tailwind 的缺点,因为它编译后默认的大小有 783.5kb。但我们可以通过一些手段,减少打包出来的尺寸。例如控制颜色的数量,移除不需要的断点。只要做好这两点。大小就可以控制在 100kb 以内,配合 PurgeCSS 移除不需要的样式,最终尺寸会更小。 另外,由于 Atomic CSS 的组合性和复用性,CSS 是不会有冗余代码的。

<div class="circle" />
<style>
  .circle {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 50px;
    height: 50px;
    background: #fff;
    border-radius: 50%;
  }
</style>
<div class="flex justify-center items-center w-50 h-50 rounded-full bg-white" />
<style>
  .flex {
    display: flex;
  }
  .justify-center {
    justify-content: center;
  }
  .items-center {
    align-items: center;
  }
  .w-50 {
    width: 50px;
  }
  .h-50 {
    height: 50px;
  }
  .rounded-full {
    border-radius: 50%;
  }
  .bg-white {
    background: #fff;
  }
</style>

乍一看明明代码更多了。然而在实际项目中,第一种写法容易造成样式冗余的问题,导致最后体积增大。由于这里不适合放太多代码,所以不明显。 存在的问题: 很遗憾目前 Tailwind 很难做到只把首屏需要的样式渲染出来。不过上面说实际上 CSS 体积也不会很大,即使不分包加载,大部分情况下问题也不大。

Tailwind 还有另外的小问题就是和预处理器 (主要是 stylus) 不能完美配合,由于 PostCSS 是后处理器,所以 Tailwind 部分指令会先被预处理器解析,这可能会有冲突,被预处理器编译掉或者直接报错。不过不算大问题,因为自己手动编写样式只占很少的一部分。相比 Tailwind 的其他优点来说,比如帮助你构建设计系统,提取你的 Design Token。分离关注点,关注样式等,影响不大。

classy-ui


总结

总体来说在组件化的时代,无论哪种方案都 work 的不错,没有明显的痛点。根据使用的框架,技术栈等选择一个合适的方案就行了。