CSS 设计模式:如何写出一套可维护的 CSS 库

一文搞懂主流 CSS 设计思想

本文由 墨然 发布于 2021-06-07

前言

如何写出一套可维护的 CSS 库?不妨通过 CSS 的设计模式/架构来理解。接下来将为你讲述三个主流的 CSS 设计思想和一个最近通用的 CSS 设计思想:OOCSSSMACSSBEMCSSMETACSS

OOCSS

OOCSS 字面意思是面向对象的 CSS,是由 Nicole Sullivan 提出的 css 理论。虽说是理论,实则更像一种程序员间约定的规范:

  • Separate structure and skin(分离结构和主题)减少对 HTML 结构的依赖
  • Separate container and content(分离容器和内容)增加样式的复用性

在 OOCSS 的观念中,强调重复使用 class,而应该避免使用 id 作为 CSS 的选择器。OOCSS 追求元件的复用,其 class 命名更为抽象,一般不体现具体事物,而注重表现层的抽取。

SMACSS

smacss 通过一个灵活的思维过程来检查你的设计过程和方式是否符合你的架构。

设计的主要规范有三点:

  • Categorizing CSS Rules(为 css 分类)
  • Naming Rules(命名规范)
  • Minimizing the Depth of Applicability(最小化适配深度)

Categorizing CSS Rules

这一点是 SMACSS 的核心。SMACSS 认为 css 有 5 个类别,分别是:

  1. Base
  2. Layout
  3. Module
  4. State
  5. Theme or Skin

基础规范(Base Rules)

基础规范描述的是任何场合下,页面元素的默认外观。它的定义不会用到 class 和 ID。css reset 也属于此类。常见的如 normalize.css、CSS Tools。

布局规范(Layout Rules)

元素是有层次级别之分的,Layout Rules 属于较高的一层,它可以作为层级较低的 Module Rules 元素的容器。左右分栏、栅格系统等都属于布局规范。布局是一个网站的基本,无论是左右还是居中,甚至其他什么布局,要实现页面的基本浏览功能,布局必不可少。SMACSS 还约定了一个前缀 l-/layout- 来标识布局的 class。举个最普遍的例子。

1
2
3
4
5
.layout-header {}
.layout-container {}
.layout-sidebar {}
.layout-content {}
.layout-footer {}

模块规范(Module Rules)

模块是 SMACSS 最基本的思想,同时也是大部分 CSS 理论的基本,将样式模块化就能达到复用和可维护的目的,但是 SMACSS 提出了更具体的模块化方案。SMACSS 中的模块具有自己的一个命名,隶属于模块下的类皆以该模块为前缀,例子如下:

1
2
3
4
.todolist{}
.todolist-title{}
.todolist-image{}
.todolist-article{}

可以看到 todolist 作为一个模块,包含了 title,image,article 等组件,同时还可以加上如 .todolist-background-danger 等修饰类,在模块内可以使用其名称做前缀任意组织模块结构,但目的是让其变得更易用,提高可扩展性和灵活度,如果只是为了修饰而修饰,写出大量没有任何复用性的类,便是一种弄巧成拙的做法。

状态规范(State Rules)

这个应该很多前端开发者都很好理解,描述的是任一元素在特定状态下的外观。例如,一个消息框可能有 success 和 error 等状态。与 OOCSS 抽取修饰类的方式的不同,SMACSS 是抽取更高级别的样式类,得到更强的复用性,如隐藏某个元素的写法:

1
2
3
.is-hidden{
display: none;
}

主题规范(Theme Rules)

主题规范描述了页面主题外观,一般是指颜色、背景图。Theme Rules 可以修改前面 4 个类别的样式,且应和前面 4 个类别分离开来(便于切换,也就是 “换肤”)。SMACSS 的 Theme Rules 不要求使用单独的 class 命名,也就是说,你可以在 Module Rules 中定义 .header { } 然后在 Theme Rules 中也用 .header { } 来定义需要修改的部分 (后加载覆盖前加载样式内容)

命名规范(Naming Rules)

按照前面 5 种的划分:

  • Base Rules (Pass)
  • Layout Rules 用 l-layout- 这样的前缀,例如:.l-header.l-sidebar
  • Module Rules 用模块本身的命名,例如图文排列的 .media.media-image
  • State Rules 用 is- 前缀,例如:.is-active.is-hidden
  • Theme Rules 如果作为单独 class,用 theme - 前缀,例如 .theme-a-background.theme-a-shadow

Minimizing the Depth of Applicability

Minimizing the Depth of Applicability,即最小适配深度原则,一个简单的例子:

1
2
3
4
5
/* depth 1 */
.sidebar ul h3 { }

/* depth 2 */
.sub-title { }

两段 css 的区别在于 html 和 css 的耦合度 (这一点上和 OOCSS 的分离容器和内容的原则不谋而合)。可以想到,由于上面的样式规则使用了继承选择符,因此对于 html 的结构实际是有一定依赖的。如果 html 发生重构,就有可能不再具有这些样式。对应的,下面的样式规则只有一个选择符,因此不依赖于特定 html 结构,只要为元素添加 class,就可以获得对应样式。

当然,继承选择符是有用的,它可以减少因相同命名引发的样式冲突(常发生于多人协作开发)。但是,我们不应过度使用,在不造成样式冲突的允许范围之内,尽可能使用短的、不限定 html 结构的选择符。这就是 SMACSS 的最小化适配深度的意义。

BEMCSS

BEM 分别代表着:Block(块)、Element(元素/子块/组成部分)、Modifier(修饰符),是一种组件化的 CSS 命名方法和规范,由俄罗斯 Yandex 团队所提出。其目的是将用户界面划分成独立的(模)块,使开发更为简单和快速,利于团队协作开发。

特点

组件化/模块化的开发思路。书写方式解耦化,不会造成命名空间的污染,如:.xxx ul li 写法带来的潜在嵌套风险。命名方式化扁平,避免样式层级过多而导致的解析效率降低,渲染开销变大。组件结构独立化,减少样式冲突,可以将已开完成的组件快速应用到新项目中。有着较好的维护性、易读性、灵活性。规则

BEM 的命名模式在社区中有着不同方式,以下为 Yandex 团队所提出的命名规则为(本文中的代码使用该规则):

  • .[Block 块]__[Element 元素]_[Modifier 修饰符] 不同的命名模式,区别在于 BEM 之间的连接符号不同,依个人而定:
  • .[Block 块]__[Element 元素]--[Modifier 修饰符] 任何一种规范,都是基于实际需求而定,便于团队开发和维护扩展,每个规范都是经过合理评估后所得出的一种思路和建议。

Block(块)

是一个独立的实体,即通常所说的模块或组件。例:header、menu、search。

  • 规则:块名需能清晰的表达出其用途、功能或意义,具有唯一性。块名称之间用 - 连接。每个块名前应增加一个前缀,这前缀在 CSS 中有命名空间(如:m-u-、分别代表:mod 模块和 ui 元件)。每个块在逻辑上和功能上都相互独立。由于块是独立的,可以在应用开发中进行复用,从而降低代码重复并提高开发效率。块可以放置在页面上的任何位置,也可以互相嵌套。同类型的块,在显示上可能会有一定的差异,所以不要定义过多的外观显示样式,主要负责结构的呈现。这样就能确保块在不同地方复用和嵌套时,增加其扩展性。综上所述,最终我们可以把 BEM 规则最终定义成:.[命名空间]-[组件名/块]__[元素名/元素]--[修饰符]
  • 情景:需要构建一个 search 组件。
  • 写法:.m-search {} 结构

如果打算开发一套框架,可以使用具有代表性的缩写,用来表示命名空间:Element UI (el-)、Ant Design (ant-)、iView (ivu-)。

Element(元素)

是块中的组成部分,对应块中的子元素/子节点。例:header title、menu item、list item。

  • 规则:元素名需能简单的描述出其结构、布局或意义,并且在语义上与块相关联。块与元素之间用 __ 连接,不能与块分开单独使用。块的内部元素,都被认为是块的子元素。一个块中元素的类名必须用父级块的名称作为前缀,因此不能写成 block__elem1__elem2
  • 情景:search 组件中包含 input 和 button,是列表中的一个子元素。
  • 写法:.m-search {}.m-search__input {}.m-search__button {} 结构
1
2
3
4
5
6
7
<!-- search 组件 -->
<form class="m-search">
<!-- input 是 search 组件的子元素 -->
<input class="m-search__input">
<!-- button 是 search 组件的子元素 -->
<button class="m-search__button">Search</button>
</form>

原则上书写时不会出现两层以上的嵌套,所有样式都为平级,嵌套只出现在 .m-block_active ,状态激活时的情况。

Modifier(修饰符)

定义块和元素的外观、状态或类型。例:color、disabled、size。

  • 规则:修饰符需能直观易懂表达出,其外观、状态或行为。修饰符用 _ 连接块与元素。修饰符不能单独使用。在必要时可进行扩展,书写成 block__elem_modifier_modifier,第一个 modifier 表示其命名空间。
  • 情景:假定 search 组件有多种外观,我们选择其中一种。并且在用户未输入内容时,button 显示为禁用样式。
  • 写法:.m-search {}.m-search_dark {}.m-search__input {}.m-search__button {}.m-search__button_disabled {} 结构
1
2
3
4
5
6
<!-- dark 表明 search 组件的外观 -->
<form class="m-search m-search-form_dark">
<input class="m-search__input">
<!-- disabled 表明 search__button 的状态 -->
<button class="m-search__button m-search__button_disabled">Search</button>
</form>

小结

很多人觉得 BEM 写法难看,审美本是智者见智,仁者见仁的事。刚刚接触可能是会觉得有点奇怪,但所有东西都有一个适应过程。如果仅仅为了好看,规避其优点,我认为得不偿失。个人建议可以尝试使用 BEM 规范来书写代码。

BEM 命名会使得 Class 类名变长,但经过 GZip 等压缩后,文件的体积其实并无太大影响。

就和早年提出 CSS 语义化一样,不要为了语义而去语义,语义化本身的作用就是帮助大家更好的识别代码,所有的规范都是基于项目的发展和团队的协作,团队可以根据成员的意愿选择最合适的方式。

METACSS

一些写在全局的通用方法,是 SMACSS 中通用方法思想的分支,一般以 css 属性、Emmet css 缩写或功能来命名,通常以一个 css 属性为一个单位

表示属性的:

1
.df { display: flex; }

表示功能的:

1
2
3
4
5
.tcut {  
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}

以此类推,封装好放到全局来使用,快速添加属性来开发页面。

总结

通过上面的梳理和阐述,总结如下:

  • smacss 覆盖了所有的细节点
  • bemcss 着重 css 的命名和语义化
  • oocss 着重可复用,把每一个 dom 节点当成一个对象,是 css 返璞归真的思想
  • metacss 着重快速开发快速添加属性,颗粒度更细,通过在 html 代码中添加类名来添加属性,不必再去找相对应的选择器中的 css 代码来修改样式

全文完,感谢阅读。


更多精彩文章