CSS
首发于CSS

如何编写易于拓展与维护的 CSS 代码

Replace "can you build this?" with "can you maintain this without losing your minds?"

这是前 Twitter 工程师 Nicolas 在 2013 年的一条 tweet,这句话可以应用在很多领域。两年前看到这句话时,我已经写了四年 CSS,简单的项目复杂的项目都写过,但是能让我自豪地说出 “I can maintain this without losing my minds.” 的项目一个都没有。所以就开始了一段关于 “如何编写易于拓展与维护的 CSS 代码” 的探索,选择的方案是 OOCSS + BEM + ITCSS 这三个理论。

现在,两年多的时间已经过去了,使用这三个理论完成了大大小小很多项目,也总结了一些经验。我觉得现在是一个很好的时间做阶段性总结,把经验分享出来,所以有了这篇文章。在第一部分里会介绍一个好 CSS 代码的三个特性,第二部分分别介绍 BEM,OOCSS,以及 ITCSS 这三个理论,以及它们对前面三个特性的影响,最后一部分是简单的总结。

首先,根据多年的经验,我认为一个易于拓展与维护的 CSS codebase 至少需要表现出三个特性:隔离性复用性,以及继承性

隔离性

隔离性表现为每一部分样式都要有明确的单一目的,不对目的之外的样式造成干扰,比如 OOCSS 中的结构样式不会去改变元素的颜色表现。由于 CSS 没有作用域,任何样式直接作用在全局生效,非常容易造成样式冲突。隔离性做得不好的项目中可能会遇到这样的问题:改动一处样式,除了实现目的需求,也对其他样式造成了破坏性干扰,之后不得不尝试一些变通方法来缩小影响范围,比如常见的调整属性权重等。当这类问题不断重现时,相信整个项目的 CSS 代码已经盘根错节,难以维护了。

目前 CSS Modules 等工具能够给选择器添加额外的标记限制作用域避免样式冲突,在成员较多的开发团队中这类工具也保障了新增代码的安全性,不过这不在本篇文章的讨论范围之内,这篇文章试图从理论层面介绍一些方法来规避上面提到的问题,增强对项目 CSS 代码库的规划和管理,这与 CSS Modules 等工具并不冲突。

复用性

我们常说 “Don't repeat yourself” 那就不用再赘述这一条的重要性了。CSS 样式的复用(或者说设计元素的复用)有个特点:很多情况下不是直接复用,而是在现有样式的基础上做出少许变化后的间接复用,关于这一点请大家留意下文中 BEM modifier 部分的介绍。

继承性

这是 CSS 与生俱来的特性,当不同选择器的样式共同作用在同一个元素上时,这些样式按照选择器权重 (specificity) 的高低叠加生效,合理利用这个特性能够大幅简化代码,令代码库条理清晰,而太过忽视这个特性很容易增加维护的复杂度,给自己制造不必要的麻烦。Harry Roberts 提出的 ITCSS 理论正是利用这个特性约束继承顺序,避免在开发过程中制造混乱。


介绍完这三个特性,还有三个理论,这三个理论与上面的三个特性并不是一一对应的关系,而是每个理论都对这三个特性有不同程度的辅助意义。

BEM

BEM 是 Yandex 团队开发的一套 self-documenting 的命名规范,它的全称是 “Block Element Modifier” 也就是用这三个部分组合命名 class 选择器。BEM 因为过长的选择器名以及 “丑陋的” 连字符 __-- 存在一些争议,不过我认为这不足以掩盖它的优点。不论是多人协作的大型项目,还是个人开发者维护自己之前的老项目,或者接管别人的项目,BEM 命名规范都能够达成一种以最直观的方式理解代码意图的默契,降低沟通成本。配合各种 CSS Preprocessors (Sass, LESS, Stylus 等) 的父选择器 & 功能还可以使代码更容易阅读:

.block {
  // block styles

  &__element-1 {
    // element styles

    &--modifier {
      // modifier styles
    }
  }

  &__element-2 {
    // element styles
  }
}

BEM Block

一个 BEM block 可以看做是一个组件的整体,使用 block 的名称命名组件最外层元素的 class 选择器(.block {}),每个 block 需要使用独一无二的选择签名作为 namespace 避免与其他 blocks 命名重复而造成样式冲突,也就是隔离样式。创建 block 时要特别注意界定它的边界以确保它始终作为独立的整体复用。

BEM Element

BEM element 通过两个下划线 __ 与 BEM block 连接(.block__element {}),由于 block 名作为 namespace 不会出现样式冲突,所以 element 的命名并没有太多限制,唯一需要注意的是不可以将 element 视为 block 嵌套子元素,例如 .block__element__nested-block {}。此外,BEM element 不可以脱离 block 作为一个整体单独复用。

BEM element 可以继承来自 BEM block 的样式,但是 BEM block 只可以继承公共样式,不可以继承其他 blocks 的样式,后面会有更详细的解释。

BEM Modifier

BEM modifier 是我最喜欢的部分,它用于标记样式的变化。假设某个 UI 元素复用在其他地方时样式需要发生少许变化,我们仍然要保持只写一套样式,并使用 modifier 标记出新样式的变化。

举个例子,假设 hero 部分有两套设计,一套文本居左显示,另一套文本居中显示同时上下 padding 值也更小一些,除此之外它们的样式完全相同。我们可以将第一种样式作为默认样式,然后创建一个 modifier 负责处理居中后的样式变化:

.hero {
  padding-top: 60px;
  padding-bottom: 60px;
  // 其他样式

  &--centered {
    padding-top: 40px;
    padding-bottom: 40px;
    text-aligned: center;
  }
}

使用时:

<div class="hero">...</div>

<div class="hero hero--centered">...</div>

CSS 中直接覆盖样式的成本非常低,所以有些开发者会比较随意地对待这件事情,稍不注意就容易造成混乱,理不清楚。使用 modifier 就是为了隔离样式,同时它也等同于通知其他团队成员,或者提醒自己,这个部分有一处样式变化,对后期维护有非常大的帮助,开发过程也会变得更加严谨。


OOCSS

Objected Oriented CSS 即 “面向对象的 CSS”,作者 Nicole Sullivan。它只有两条原则,我认为也是提高 CSS 代码复用性最重要的两条原则。

1. Separate structure and skin

第一条,分离结构样式与皮肤样式。举一个最典型的例子:Bootstrap 的按钮样式可以看做由两类样式共同组成:结构类样式与皮肤类样式。结构类样式包含 .btn .btn-lg .btn-sm 等选择器,皮肤类样式包含 .btn-primary .btn-secondary .btn-success .btn-danger 等选择器。这两类样式能够按需组合实现出更多种不同的按钮样式,并且提升后期的拓展空间。当然能够这样组合的,并不仅仅只有按钮,还有很多其他样式。

分离出来的 object class 必须要保持它的样式只有单一目的,才能实现它最大化的复用。我们也可以使用 BEM modifier 标记结构或者皮肤的不同状态。

按照当下流行的组件化思想,以 React 项目为例,会将 .btn 做成一个按钮组件,然后 .btn-lg .btn-sm .btn-primary .btn-secondary 等等都是这个按钮组件的 props (modifier),不过在处理 CSS 样式时,仍然将样式按照结构与皮肤两类分离,因为这是最实用的两类。

我们再看一个例子:

这两套组件拥有完全一致的皮肤样式,区别只是组件的结构不同而已,所以我们可以只创建一套皮肤样式,然后再创建两套结构样式组合使用。这里我们可以选择一种结构作为默认样式,然后将另一种以 BEM modifier 标记仅包含样式的变化,以便统一维护。如果后续有第三种结构变化,或者皮肤变化,处理办法也显而易见:

以上三张截图均来自 Rob Turlinckx 的设计,已授权使用,在此表示感谢。

2. Separate container and content

第二条,分离容器样式与内容样式,这是一条样式的解耦法则。还是先举例子,以网格系统为例,没有人会在网格的样式中加入颜色属性,因为对内容的破坏完全不受控制。这里的网格就是容器,而嵌套在里面的部分就是内容。现实中我们很容易能够避免网格这类常用容器破坏内容样式,而忽视项目中的自定义容器。当一个 BEM block 中嵌套其他 block 时,请注意不要让外层容器干涉嵌套部分的样式,而嵌套部分也不要依赖它外层 block 的样式。为了处理好容器与内容的关系,建议:

  1. 界定好 BEM block 的边界,block 需要使用的公共样式通过 object class 获得,而不是它的外层 block;
  2. 在设计容器时,尽量多考虑不同内容嵌套在其中的情况,并且避免使用可继承属性 (Inherited Properties);

此外,在重构样式时,可能会重组一些 BEM block 或者说组件,遵循这条原则一定能够提高重构后代码的安全性。


ITCSS

ITCSS 的全拼 “Inverted Triangle CSS”,是一种将 CSS 代码库按照倒三角形状管理的方法,作者 Harry Roberts。倒三角默认分为 7 层,每一层基于权重、复用性等因素考量放置不同的样式:越基础、权重越低、复用性越强的样式越要往上层放置;反之,越特殊,权重越高,复用性越弱的样式越要往下层放置。

图片来源:speakerdeck.com/dafed/m

以 SCSS 为例,ITCSS 的文件结构如下所示:

scss/
|-- _base.fonts.scss
|-- _components.carousel.scss
|-- _components.header.scss
|-- _components.pagination.scss
|-- _generic.box-sizing.scss
|-- _generic.normalize.scss
|-- _objects.buttons.scss
|-- _objects.grids.scss
|-- _objects.typography.scss
|-- _settings.colors.scss
|-- _tools.mixins.scss
|-- _trumps.animations.scss
`-- style.scss

"style.scss" 作为主文件,引用 partials 的顺序为:

// Settings Layer
@import 'settings.colors';

// Tools Layer
@import 'tools.mixins';

// Generic Layer
@import 'generic.box-sizing';
@import 'generic.normalize';

// Base Layer
@import 'base.fonts';

// Objects Layer
@import 'objects.grids';
@import 'objects.buttons';
@import 'objects.typography';

// Components Layer
@import 'components.header';
@import 'components.carousel';
@import 'components.pagination';

// Trumps Layer
@import 'trumps.animations';

Settings Layer and Tools Layer

Settings Layer 与 Tools Layer 是倒三角最上方的两层,适合放在一起介绍,前者存放全局变量,后者存放 mixins,functions,或者第三方 libraries。这两层的特点是不会编译输出任何一行代码到最终的 CSS 文件中,放在倒三角的最上方也是为了保障项目中任何一个地方都可以全局访问这些代码。

Generic Layer

这是最早开始编译输出 CSS 代码的一层,主要用于统一不同浏览器默认样式的差异,比如 Reset CSS (估计现在也没人用了)与 Normalize.css,或者重置 box-sizing。这一层在项目初期添加后,基本不会再有任何改动。

Base Layer

有时也被称作 Elements Layer,原则上只能出现 type 选择器。这一层可以看作是只针对当前项目的 Reset CSS,例如根据设计稿给 h[1-6]inputbuttonulolpblockquote 等元素添加项目内的默认样式。Web fonts 也添加在这一层。

Objects Layer

这一层仅包含基于 OOCSS 原则创建的 object class,只能出现 class 选择器,这也是第一个开始出现 class 选择器的一层。建议使用 o- 作为前缀以便与其他样式分离,例如 .o-btn。也可以使用 BEM modifier 标记样式的不同状态,例如 .o-btn--primary。这些 object class 可以使用在项目中的任何地方,它们属于全局样式。

不过在 React 项目中,object 常常封装成组件复用,如 Button, List, Container, Column/Grid 等等,不再适合统一在一个地方管理。

如果把根据 OOCSS 第一条原则分离出来的结构样式作为公共的 object 样式,例如 media object,可能会遇到这样一个问题:没有办法单独控制它们的响应式变化。假设一组 media object 被复用在了两个不同的组件中,其中一个组件的结构需要比另一个更早地做出响应式变化,那这种复用方式容易将响应式变化隔离在两个不同的地方(文件里),增加维护的难度。这里有两个解决办法:一种不复用结构,结构样式直接包含在组件样式中;另一种将结构的不同变化借助 BEM modifier 标记清楚,仍然统一在一个地方管理。当结构有特别强的复用需求时建议使用第二种办法。

Components Layer

从这一层开始,样式不再具有复用性,只剩下组件级别的复用。这一层的样式只针对某个特定的组件,每一个组件就是一个 BEM block,应该完全遵循 BEM 命名规范。

这一层已经有足够多的样式可以从前面各层继承,所以在处理这一层的代码时,尽可能多继承公共样式,在共用样式的基础上补充特殊样式。以组件内(假设名为 'Block')的一个按钮为例:

<div class="block">
  <button class="o-btn o-btn--large o-btn--primary block__btn">...</button>
</div>

先使用 button object 组合出按钮的基础样式,再借助 .block__btn 补充额外样式,例如添加 margin 或者 float 等属性。

React 项目中只是表象稍有不同而已,本质还是一样的:

<div className="block">
  <Button className="block__btn" size="large" color="primary">...</Button>
</div>

Pages Layer

这一层并不包含在 ITCSS 默认的 7 层里面,不过 ITCSS 允许我们根据项目需要添加或删除层。开发 marketing site 时常常有以页面为单位的改动,放在这一层里非常合适。

Trumps Layer

这一层是倒三角结构的最下方,必须保障权重的绝对优势,只有在这一层可以使用 !important 强制提升权重。比较常见的做法是把 hide/show 这类样式放在这一层中。而 React 项目中,使用 inline style 就可以了,不需要保留这一层。

ITCSS 中的样式继承

ITCSS 的任何一层都会或多或少继承它前面各层的样式,然后在当前层对样式做出补充。下图展示了基于 ITCSS 管理的项目的选择器权重变化,以往权重冲突的问题被很自然地规避掉了,而且是通过 CSS 自身的特性。

图片来源:speakerdeck.com/dafed/m

Base Layer 继承 Generic Layer 的样式,保证各浏览器默认样式统一的前提下补充各类元素的基础样式;Objects Layer 继承前面两层的样式,补充能够独立复用的 object class;Components Layer 继承前面各层的样式,然后补充自身没有复用性只针对某个组件的特殊样式;以此类推。这样的继承过程能够使项目的 CSS 代码变得条理清晰不再混乱,也更安全。


拓展与维护

在 ITCSS 中,Objects Layer 以及之前的各层可以认为是项目的公共样式,自 Components Layer 之后的各层不再是公共样式。此外,我认为 Components Layer 中的 BEM modifier 也是它所修改的组件的公共样式,因为 modifier 具有复用性。所以下面提到的公共样式,主要指 Objects Layer 中的 object class 以及 Components Layer 中的 modifier 样式。

项目迭代时,我们先判断某个需求是属于公共样式的改动,还是非公共样式的改动。对于公共样式的改动,可以尝试借鉴 “不可变对象” (immutable object)的理念,团队中的开发成员需要达成默契 object class 与 modifier 是不可变的,一旦创建后便不再允许改动或者删除,除非有明确的目的需要全局改变这部分样式,否则只能补充新的 object class 或者 modifier,只有这样才能保障团队协作的安全性。


总结

这套方案已经不断改进、优化实施了两年,对我个人的帮助非常大,主要体现在四个方面:

  • 良好的可拓展性与可维护性;
  • 最大限度地复用代码,编译生成的 CSS 文件体积极其小;
  • OOCSS,BEM,ITCSS 三个理论组合使用,保障代码的健壮性;
  • 项目前期整理公共样式虽然需要多花一点时间,但是之后的开发速度会变得飞快,也很顺畅;

还有很多其他的 CSS 理论,如 SMACSS,Enduring CSS, Atomic CSS, RSCSS 等等,但目前我最喜欢的还是 OOCSS + BEM + ITCSS 这三条,简单、实用、有效。

我试着把自己的经验全部写进这篇文章,但还有一些表达不尽人意的地方,如果有疑问欢迎讨论。之后也想写一套新的 CSS 框架,请保持关注 ;)

文章被以下专栏收录