从 BEM 谈大型项目中 CSS 的组织和管理

Web 应用发展到今天已经与传统的 Web 应用大不相同。依靠浏览器的支持以及前端技术和框架的发展,Web 应用可以执行强大的操作和提供丰富的交互性。很多应用已经把大量的逻辑从服务器端迁移到了浏览器端。服务器端通过 REST 服务暴露数据,浏览器端与用户进行交互来完成复杂的逻辑。由于这个发展趋势,Web 应用的前端代码的复杂度大大提高,尤其是 JavaScript 和 CSS 代码的数量大幅增加。对于大型 Web 应用来说,组织和管理如此大量的 JavaScript 和 CSS 代码是一个复杂的问题。本文主要讨论的是大型项目中 CSS 代码的组织方式。首先介绍的是前端项目开发方法学 BEM。

BEM

BEM 是一种前端项目开发的方法学,由 Yandex 公司提出。BEM 的名称来源于该方法学的三个组成部分的英文首字母,分别是块(Block)、元素(Element)和修饰符(Modifier)。这三个不同的组成部分称为 BEM 实体。

块即是通常所说的 Web 应用开发中的组件或模块。每个块在逻辑上和功能上都是相互独立的。块中封装了组件相关的 JavaScript、CSS 代码和 HTML 模板。由于块是独立的,可以在应用开发中进行复用,从而降低代码重复并提高开发效率。块可以放置在页面上的任何位置,也可以互相嵌套。同一个块可以在页面上存在多个实例。块的不同实例具有相似的结构。一个典型的块的示例是菜单。一个项目中可以有多个不同的菜单,具体相似的结构和样式。

元素

元素是块中的组成部分。元素不能离开块来使用。BEM 不推荐在元素中嵌套其他元素。在菜单块中,每个菜单项是块中的元素。

修饰符

修饰符用来定义块或元素的外观和行为。同样的块在应用不同的修饰符之后,会有不同的外观。当菜单块出现在页面上的不同位置时,可以有不同的样式。菜单块中的菜单项可以有选中或非选中的状态。

不同 BEM 实体可以互相嵌套,形成 BEM 树。不过 BEM 树和 HTML 页面中的 DOM 树是不同的。BEM 树的抽象层次更高。在实现中需要避免在两者之间建立硬性的对应关系。

CSS 命名规则

具体到 CSS 来说,BEM 的重要内容之一是其引入的 CSS 命名规范。一个良好的命名规范可以帮助开发人员快速的了解 CSS 代码中不同样式规则的含义。这对于团队协作和代码维护有很大好处。BEM 有自己独特的 CSS 命名规则,与 BEM 中的三种实体相对应。

首先每个 BEM 实体都有一个名称,由小写字母和数字组成。名称中的不同单词通过连字符(-)连接,如 property-editor 或 language-switcher 等。每个 CSS 类别选择器的名称包含了其所描述的元素的 BEM 相关的信息。因此从每个元素上的 class 属性的值就可以知道该元素的 BEM 信息,从而了解元素在组件中的作用。

BEM 中的 CSS 类名的第一部分是块的名称。块的名称作为 CSS 类名的命名空间。如表示菜单的块的名称可以是 menu。该块中的元素的 CSS 类名都会包含 menu 作为前缀。与块中元素对应的 CSS 类名是在块的名称后加上元素的名称,并通过两个下划线(__)分隔。如菜单块中的每个菜单项的名称是 item,则与之对应的 CSS 类名是 menu__item。修饰符可以用来修饰块或元素。在 CSS 类名中,修饰符紧跟在所修饰的实体的名称之后,并通过单个下划线(_)分隔。有两种类型的修饰符,分别是布尔修饰符和名值对修饰符。布尔修饰符直接出现在对应的实体的名称之后,表示某种状态。如当前选中的菜单项对应的 CSS 类名是 menu__item_selected,其中 selected 是布尔修饰符。名值对修饰符由名称和值两部分组成,通过单个下划线(_)分隔。比如表示订单的块 order,可以通过名称为 status 的修饰符表示订单的不同状态,如 CSS 类名 order_status_paid 对应 status 为 paid 的订单。需要注意的是在一个 CSS 类名中不应该同时出现块和元素的修饰符。

上述给出的是 BEM 方法学提出者 Yandex 的命名规则,社区中还有其他不同的命名规则,区别在于 BEM 实体名称的命名规则以及 CSS 类名中不同部分的分隔符。比如有的命名规则使用 camel case 来命名 BEM 实体,如 PropertyEditor 和 LanguageSwitcher;有的命名规则使用两个连字符(–)来分隔修饰符,如 menu__item–selected。目前来说,比较流行的是由 Harry Roberts 提出的命名规范,也是 Google 的 Material Design Lite 库使用的命名规则。本文中的代码也使用该规则。该命名规则的原则如下:

  • BEM 实体名称全部是小写字母或数字。名称中的不同单词用单个连字符(-)分隔。
  • BEM 元素名称和块名称之间通过两个下划线(__)分隔。
  • 布尔修饰符和其所修饰的实体名称之间通过两个连字符(–)来分隔。不使用名值对修饰符。

通过使用上面 CSS 命名规则,所得到的是一系列只使用单个 CSS 类别选择器的样式规则。以菜单组件作为示例来说明。代码清单1代码清单2中给出了使用 BEM 命名规则的 HTML 代码和 CSS 代码。示例中包含了 BEM 块 menu、元素 item 和修饰符 selected。

清单 1. 使用 BEM 命名规则的 CSS 类的 HTML 代码
1
2
3
4
5
<ul class="menu">
<li class="menu__item menu__item--selected">Menu Item 1</li>
<li class="menu__item">Menu Item 2</li>
<li class="menu__item">Menu Item 3</li>
</ul>
清单 2. 使用 BEM 命名规则的 CSS 样式表
1
2
3
4
5
6
7
8
9
.menu {
 list-style: none;
}
.menu__item {
 font-weight: bold;
}
.menu__item--selected {
 color: red;
}

代码清单3代码清单4中给出的是另外一种 CSS 代码的组织方式。

清单 3. 使用传统命名规则的 CSS 类的 HTML 代码
1
2
3
4
5
<ul class="menu">
<li class="item selected">Menu Item 1</li>
<li class="item">Menu Item 2</li>
<li class="item">Menu Item 3</li>
</ul>
清单 4. 使用传统命名规则的 CSS 样式表
1
2
3
4
5
6
7
8
9
10
11
.menu {
 list-style: none;
}
 
.menu .item {
 font-weight: bold;
}
 
.menu .item.selected {
 color: red;
}

比较这两种不同的方式,BEM 的优点在于所产生的 CSS 类名都只使用一个类别选择器,可以避免传统做法中由于多个类别选择器嵌套带来的复杂的属性级联问题。在 BEM 命名规则中,所有的 CSS 样式规则都只用一个类别选择器。因此所有样式规则的特异性(specificity)都是相同的,也就不存在复杂的优先级问题。这可以简化属性值的层叠规则。代码清单中的命名规则的好处在于每个 CSS 类名都很简单明了,而且类名的层次关系可以与 DOM 节点的树型结构相对应。BEM 命名规则的可能的缺点在于 CSS 类名会比较长而且复杂。乍看之下,根据 BEM 命名规则产生的 CSS 类名都会很复杂,但实际上在熟悉了命名规则之后,可以很容易理解其含义。

OOCSS

OOCSS 表示的是面向对象 CSS(Object Oriented CSS),是一种把面向对象方法学应用到 CSS 代码组织和管理中的实践。面向对象方法学中的很多理念,如模块化、单一功能原则(Single responsibility principle)和关注点分离(Separation of concerns)等,对于需要组织和管理大量 CSS 代码的应用也是适用的。OOCSS 提出了 CSS 对象的概念,用来表示重复出现的可视化模式。这些模式可以被抽象成独立的 HTML、CSS 和 JavaScript 代码片段,并在整个项目中共享。

OOCSS 有两个重要的原则:

第一个原则是把结构和外观分开。重复出现的可视化模式应该只关注外观,而与 DOM 结构无关。这就要求 CSS 对象中的每个组成部分都有名称,并在 DOM 结构中通过 CSS 类名与之对应。因此在 OOCSS 中的样式规则都是使用类别选择器,而不依赖特定的 DOM 结构。这样可以提高 CSS 对象的可复用性。

第二个原则是把容器和内容分开。这就要求在 CSS 样式中不应该出现与 DOM 树中的位置相关的规则。CSS 样式应该只关注内容,而不是 DOM 元素及其层次关系。

OOCSS 除了这一套指导理论之外,还提供了一些 CSS 对象作为示例。比如多媒体对象,用来描述图片视频及其相关的文字。多媒体对象在 Facebook、Twitter 和微博上会经常出现。每条微博的左侧是用户的头像,右侧是文本内容。多媒体 CSS 对象中的 CSS 类有 media、img、bd 和 imgExt。这几个 CSS 类的含义如下:

  • media – 最外层的 CSS 类,用来表明这是一个多媒体对象。
  • img – 表示出现在左侧的链接、图片或视频对象。
  • bd – 表现出现在右侧的主要内容。
  • imgExt – 表示额外的链接、图片或视频对象,出现在 bd 表示的主要内容的右侧。

代码清单5给出了使用多媒体 CSS 对象的 HTML 片段。

清单 5. 使用多媒体 CSS 对象的 HTML 代码
1
2
3
4
5
6
7
8
<div class="media">
<a href="http://oocss.org/" class="img">
<img src="https://placehold.it/50x50"/>
</a>
<div class="bd">
OOCSS media object
</div>
</div>

代码清单6给出了多媒体 CSS 对象中的样式声明。

清单 6. 多媒体 CSS 对象的样式声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.media {
 margin: 10px;
}
 
.media, .bd {
 overflow: hidden;
}
 
.media .img {
 float: left;
 margin-right: 10px;
}
 
.media .img img {
 display: block;
}
 
.media .imgExt {
 float: right;
 margin-left: 10px;
}

SMACSS

SMACSS 表示的是可扩展和模块化 CSS(Scalable and Modular Architecture for CSS)。Jonathan Snook 在其同名的书中提出了这一思想。SMACSS 的基本理念是可扩展和模块化,并给出了在大型项目中管理和组织 CSS 文件的一些原则。SMACSS 把 CSS 样式规则分成若干个不同的类别:

  • 基础:该类别中包含的是默认的 CSS 样式。作为其他样式的基础。
  • 布局:该类别中包含与页面布局相关的 CSS 样式,用来进行模块的排列。
  • 模块:该类别中包含的是可复用的模块的 CSS 样式。
  • 状态:该类别中的 CSS 样式用来描述布局和模块在不同状态下的外观。比如在不同的屏幕尺寸下,布局会发生变化。标签式模块的每个标签页可以有显示或隐藏的状态。
  • 主题:该类别和状态类似,只不过是用来改变布局和模块的视觉效果。

对于不同类别的 CSS 样式,SMACSS 有不同的命名规则。基础类别中样式一般使用元素类型选择器,用来规范元素的初始样式。布局类别中的样式一般使用“l-”作为前缀。状态类别中的样式一般使用“is-”作为前缀。而对于不同的模块,则使用模块的名称作为前缀。

ACSS

ACSS 表示的是原子化 CSS(Atomic CSS),是 Yahoo 提出来的一种独特的 CSS 代码组织方式,应用在 Yahoo 首页和其他产品中。ACSS 的独特性在于它的理念与一般开发人员的理解有很大的不同,并挑战了传统意义上编写 CSS 的最佳实践,也就是关注点分离原则。ACSS 认为关注点分离原则会导致冗余、繁琐和难以维护的 CSS 代码。

ACSS 的原则是把 CSS 样式打散成尽可能小的部分,每个 CSS 类只对应一条样式规则,从而达到最大化的可复用性。比如 CSS 类 M(10px)所对应的样式规则是 margin: 10px。在应用 CSS 样式时,只需要在把所需要的原子化 CSS 类名添加到 DOM 元素上即可。ACSS 提供了 Atomizer 工具来生成最终的 CSS 样式文件。

在 HTML 页面中,按照 ACSS 的命名方式添加所需要的原子化 CSS 类名,再使用 Atomizer 工具来解析 HTML 页面并生成对应的 CSS 文件。代码清单7中是使用 ACSS 的多媒体对象示例。

清单 7. 使用 ACSS 的多媒体对象
1
2
3
4
5
6
7
8
<div class="BfcHack M(10px)">
<a href="http://oocss.org/" class="Fl(start) Mend(10px)">
<img src="https://placehold.it/50x50"/>
</a>
<div class="BfcHack">
OOCSS media object
</div>
</div>

在使用 Atomizer 工具之后,生成的 CSS 文件如代码清单8所示。

清单 8. Atomizer 工具生成的 CSS 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.Fl\(start\) {
 float: left;
}
.M\(10px\) {
 margin: 10px;
}
.Mend\(10px\) {
 margin-right: 10px;
}
.BfcHack {
 display: table-cell;
 width: 1600px;
 *width: auto;
 zoom: 1;
}

ACSS 的好处在于所生成的 CSS 文件只包含必须的内容,而且冗余很少,可以减少 CSS 文件的尺寸,提高性能。另外 CSS 类所对应的样式规则是不变的,这使得在不同的项目和组件之间共享 CSS 变得很容易。比如在使用传统的方式时,同样是名称为 header 的 CSS 类,其所实际表示的样式规则在不同的项目中可能完全不同。而在 ACSS 里面,名称为 M(10px)的 CSS 类所表示的样式规则永远都是 margin: 10px。ACSS 可能的缺点在于它与大多数开发人员所理解的最佳实践差异很大,可能不容易被接受。

CSS 组织和管理

上面介绍了 BEM、OOCSS、SMACSS 和 ACSS 等几种不同的 CSS 命名规则,各有优缺点。对于开发团队来说,最重要的是找到最适合的组织和管理的方式。不需要盲目的遵循所谓的最佳实践,而是要找到最适合的方式。笔者根据个人经验推荐下面的组织和管理方式。

ACSS 的思想虽然有很多的优点,也在 Yahoo 这样的大公司得到了生产实践,但是 ACSS 的做法可能比较难以被大多数开发人员所理解,因此除非是团队的决策,否则不推荐使用。比较推荐的做法还是模块化,更容易让人所理解。

首先需要一个基准的 CSS 样式表。这可以使用 Normalize.css 和 Bootstrap 4 中的 Reboot。然后是进行模块划分。对每个模块中采用 BEM 来作为命名规则。在现在的 Web 项目中,一般不直接编写 CSS,而是使用 SASS 或 LESS 这样的 CSS 预处理语言来编写。以代码清单2中的菜单的 CSS 样式为例,对应的 SCSS 代码如代码清单9所示。

清单 9. 生成菜单 CSS 的 SCSS 代码
1
2
3
4
5
6
7
8
9
10
11
.menu {
 list-style: none;
 
 &__item {
   font-weight: bold;
 
   &--selected {
     color: red;
   }
 }
}

代码清单9中,SCSS 代码中通过嵌套的方式表示了 BEM 树的结构,通过 SASS 中的&可以创建出连接多个部分的 CSS 类名。SASS 使得编写 BEM 命名规则的 CSS 样式变得类似传统的 CSS。这既易于模块中样式的组织,又避免了代码重复。

小结

现在的 Web 应用中 CSS 代码的数量越来越大,开发团队的规模也相应扩大。在 CSS 开发团队中需要一个统一和固定的 CSS 代码组织和管理规范。这其中也包括 CSS 样式的命名规则。BEM、OOCSS、SMACSS 和 ACSS 都是很不错的 CSS 代码组织和管理的指导实践。开发团队可以根据成员的意愿选择最合适的方式。

css选择器first-child的使用

设有如下HTML

欲给第一个LI设定属性,可以写为:

需要注意的是,下面的写法是容易出现的错误。