优秀框架都在使用的CSS规范: BEM、OOCSS、SMACSS

1,690 阅读11分钟

我是前端下饭菜,两娃的爸创业中。公众号“绘个球”(各种号全网同名)实时分享创业动态,提供军事、地理、地产短视频工具。

前言

规范系列文章:

很好奇为Vue、React框架实现的各个UI开源库,例如element-plusant-design, 他们有没有基于什么规范编写CSS,采用的哪一种CSS规范?如果查看这些框架生成的CSS,不难发现BEMOOCSSSMACSS的痕迹。本文将为你介绍什么是BEMOOCSSSMACSS,如何将它们运用到CSS编写中。

element-plus CSS: 采用BEM、SMACSS规则

image.png

ant-design CSS片段:类似于BEM(不完全是)

image.png

为什么要遵守CSS规范?

使用BEM、OOCSS、SMACSS等CSS规范可以提高代码的可读性、维护性、扩展性以及开发体验。通过采用一致且定义明确的命名规范,开发者可以创建出易于理解、维护和协作的CSS代码,这对于保持代码库的整洁和清晰至关重要。

BEM(Block Element Modifier)

BEM是一个使用非常广泛的CSS命名规范,通过BEM可创建易维护、高复用、自解释的CSS代码。BEM是Block Element Modifier的缩写,它将用户界面拆分为独立的块(Block),每一个块有自己的元素(Element)和修饰器(Modifier)

在介绍BEM之前,可通过下面的代码片段对BEM如何拆分界面有直观的了解。

<nav class="nav"> 
    <ul class="nav__list"> 
        <li class="nav__item nav__item--active"> 
            <a href="/about" class="nav__link">About</a> 
        </li> 
        <li class="nav__item"> 
            <a href="/pricing" class="nav__link">Pricing</a> 
        </li> 
        <li class="nav__item"> 
            <a href="/contact" class="nav__link">Contact</a> 
        </li> 
    </ul> 
</nav>

Block

Block是构成用户界面的组成部分,每一个Block代表一个独立的组件,可以重复使用。Block使用小写字符命名,如果有多个单词,则用连接符-连接。Block下包含有自己的元素(Element)和修饰符(Modifier)。

常用的Block命名有headercontainer、 menu、 checkbox、 inputelement-plus每个组件可理解为一个Block,例如el-cardel-dialog等。and-designelement-plus的区别就是前缀,例如代表card的ant-card

Element

Element在语义上代表Block的一部分,必须与Block相关联,不能独立存在。Element使用双下线__后跟元素名称来命名。

  • 常规的命名如header_logomenu_item
  • 示例中的nav__listnav__itemnav__link,配合Block nav一起使用
  • element-plus中的el-card__headel-card__bodyel-card_foot等,配合Block el-card一起使用

Modifier

修饰符(Modifier)是块或元素的变体。用于改变外观或行为。它们代表块或元素的不同状态,但不会影响现有块或元素的布局。可以使用双破折号 (--) 后跟修饰符名称来命名(例如,menu__item--selected)。

常用的修饰符命名如disabled、 highlightedcheckedfixedsize bigcolor yellow等。

在上面的示例中,nav__item--active 类是修饰符,使用样式高亮当前使用的导航是哪一个。

element-plus中Tag标签的Block名称为el-tag,其下包含el-tag--primaryel-tag--successel-tag--infoel-tag--warning等表示各种状态的修饰符。

对于button组件,通常也会用修饰符标识出各种状态。

<button class="button">
	Normal button
</button>
<button class="button button--state-success">
	Success button
</button>
<button class="button button--state-danger">
	Danger button
</button>

为什么使用BEM

  • 模块化:Block样式不依赖于页面上的其他元素,因此您永远不会遇到级联问题。您还可以将块从已完成的项目转移到新项目。

  • 重复性:以不同的方式组成独立的块,不同页面都可以重用它们,减少您需要维护的CSS代码量。有了一套语义化、明确的样式命名规则,您就可以构建单独模块,使您的CSS完全独立于页面。

  • 结构化:BEM可以基于SMACSS为您的CSS代码提供了一套稳定的结构,并且简单易于理解。

OOCSS(Object-Oriented CSS)

OOCSS是一个以面向对象方式开发模块化、可重用、关注点分离的CSS代码。它把同类的作为对象,按高类聚低耦合方式封StructureSkin,例如button相关的css仅有一份,定义Class el-button(structure)、el-button--primary(skin),不同页面复用这一份css,从而提升了css的可复用性。

Structure、Skin定义

Structure代表了一个页面的骨架,它不关注元素的细节部分(例如color、border或其他装饰骨架的属性),在页面后续的开发过程中Structure也不会有太大的变动,长期保持一个稳定状态。

image.png

而Skin作为页面的装饰部分,让你的页面变得更加美观。

Structure:

  • height
  • width
  • margins
  • padding
  • overflow

Skin:

  • colors
  • fonts
  • shadows
  • gradients

假如不同页面同时定义了button的样式.button.button-2:

.button { 
  width: 150px;
  height: 50px; 
  background: #FFF; 
  border-radius: 5px; 
} 

.button-2 { 
  width: 150px; 
  height: 50px; 
  background: #000; 
  border-radius: 5px; 
}

两个class包含有重复的样式定义,如果产品要求调整按钮的大小、颜色,一个个页面单独去改,这样的做法就非常难维护。基于OOCSS的概念,我们可以将其拆分为Structure、Skin两部分。改造后的结果为:

.btn { 
  width: 150px; 
  height: 50px; 
  border-radius: 5px; 
}

.btn-light { 
  background: #FFF; 
} 

.btn-dark{ 
  background: #000; 
} 

.btn作为Structure部分,而.btn-light.btn-dark作为Skin部分。在html中使用开起来也变得很丝滑。

<a class="btn btn-light" href="#">Home</a> 
<a class="btn btn-dark" href="#">Blog</a>

Structure再抽象:Container、Content

Structure和Skin是比较容易区分的两个概念,但就Structure自身,包含了width、height、spacing、border、padidngs、margins等属性,因此它的范围就变得有些复杂。为了将Structure进一步明确,可将其再细化为两个类别:Container、Content。

Container

Container可理解为语义化的虚拟容器,对用户不可见,其他可见元素将包含在这些容器中。例如<div><span><article><navigation><sidebar>

content

content表示实际的内容呈现,例如<img><p><input>button。这些元素可能需要额外的样式,例如定位和排版。 content经常面临的问题是它与skin相混淆。这是因为像排版这样的东西可以既是structure又是skin,这取决于它的使用方式。 OOCSS 的一般规则是,如果它是重复属性,那么它就是strcuture。如果它是变化性的,那么它就是skin

SMACSS(Scalable Modular Architecture for CSS)

不同于OOCSS,SMACSS代表了易扩展、模块化的CSS架构。SMACSS要求将CSS拆分为明确的五个部分:baselayoutmodulestatetheme。通过明确目录拆分,在编写CSS时,哪些CSS该放到哪个目录下是一目了然的事,使用这种方式来管理大型CSS代码库就显得得心应手。

Base

定义全局性初始化默认样式,例如常用的reset.css文件,统一重置paddingmarginorder以及默认的排版样式。

/* base.css */

/* Box sizing rules */
*,
*::before,
*::after {
  box-sizing: border-box;
}

/* Remove default margin */
body,
h1,
h2,
h3,
h4,
p,
figure,
blockquote,
dl,
dd {
  margin: 0;
}

/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
ul[role='list'],
ol[role='list'] {
  list-style: none;
}

Layout

Layout定义结构化、容器化元素的样式,例如headerfooterarticlessidebarsasides等。为了标识出是layout模块样式,一种实践方式是为这一类的样式添加前缀l-

.l-full-width {
  width: 100%;
}

.l-half-width {
 width: 50%;
}

Module

如果一类样式在界面上多次重复出现,那这一类样式就可归纳到一个模块,例如模块menuswidgetsforms等等。模块可认为是按特定规则将一类可见要素的样式分到同一组中,并且在不同的地方可重复使用。

例如开发Dialog组件,通常使用.el-dialog表示Dialog对应的CSS模块。模块下还包含其他组成Dialog的子要素:

/** Module: el-dialog **/
.el-dialog {}

/** 子模块:header **/
.el-dialog__header {}

/** 子模块:header **/
.el-dialog_title {}

/** 子模块:headerbtn **/
.el-dialog__headerbtn {}

/** 子模块:close **/
.el-dialog__close {}

/** 子模块:body **/
.el-dialog__body {}

/** 子模块:footer **/
.el-dialog__footer {}

element-plus中每一个组件都可以理解为一个CSS模块, 例如el-buttonel-card等。

State

状态用于描述我们的模块在不同情况下的外观(skin),类似于BEM中的Modifier部分。 SMACSS 的State部分用于定义界面交互性的CSS。 State不必附加到特定对象,例如不必附加到button、card等元素,可独立存在并复用。

element-plus中的checkbox元素,其CSS模块为.el-checkbox,我们可以为每一个checkbox设置不同的状态,例如选中、可用等。根据SMACSS的State规则,可以为这些状态定义.is-disabled.is-checked.is-bordered。这些状态类CSS除了在checkbox中使用,也可以在radiobutton等组件中复用。

.is-disabled {
    cursor: not-allowed
}

.is-bordered {
    padding: 0 15px 0 9px;
    border-radius: var(--el-border-radius-base);
    border: var(--el-border);
    box-sizing: border-box
}

is-checked {
    border-color: var(--el-color-primary)
}

Theme

主题(Theme)会统筹一体化考虑你的页面风格,在平台级别定义统一的字体颜色、边框、阴影等效果。通过主题化方式,能结合用户不同的喜好定制化多类呈现风格。

image.png

image.png

当我们在设计平台时就应该考虑CSS支持主题化,这要求研发在编写CSS时应更加离子化的考虑CSS样式拆分。

element-plus在设计CSS时,将skin相关的属性通过scss等CSS框架配置化,例如colorbackgroundborderfont-size等。后期如果想为用户提供其他主题,可直接替换如下代码各个skin变量的值即可。

/** element-plus **/

$colors: () !default;
$colors: map.deep-merge(
  (
    'white': #ffffff,
    'black': #000000,
    'primary': (
      'base': #409eff,
    ),
    'success': (
      'base': #67c23a,
    ),
    'warning': (
      'base': #e6a23c,
    ),
    'danger': (
      'base': #f56c6c,
    ),
    'error': (
      'base': #f56c6c,
    ),
    'info': (
      'base': #909399,
    ),
  ),
  $colors
);

BEM、OOCSS、SMACSS不适用于现代化框架?

现代化CSS框架: CSS原子化

原子化 CSS 是一种 CSS 的架构方式,它倾向于小巧且用途单一的 class,并且会以视觉效果进行命名。

目前提供原子化的框架有Tailwind CSSWindi CSSUnoCSSUnoCSS是作者Anthony Fu基于Windi CSS演变出来的框架, 也是目前主流的原子化CSS框架。CSS原子化和element-plus等UI库CSS实现方式有什么区别?

image.png

上图是UnoCSS官网提供的Demo,其实现代码如下所示, 代码采用UnoCSS提供的CSS原子化方式编写。 UnoCSS将不同维度的style属性值拆分成离子化的class,如h-fulltext-centerjustify-centertext-2xl等,这些class看起来七离八碎。

<div h-full text-center flex select-none all:transition-400>
  <div ma>
    <div text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s>
      UnoCSS
    </div>
    <div op30 text-lg fw300 m1>
      The instant on-demand Atomic CSS engine.
    </div>
    <div m2 flex justify-center text-2xl op30 hover="op80">
      <a
        i-carbon-logo-github
        text-inherit
        href="https://github.com/unocss/unocss"
        target="_blank"
      ></a>
    </div>
  </div>
</div>
<div absolute bottom-5 right-0 left-0 text-center op30 fw300>
  on-demand · instant · fully customizable
</div>

如果以BEMOOCSSSMACSS方式实现同样的效果,其伪HTML代码如下。

 <!-- 使用BEM命名方式: block__element--modifier -->
  <div class="hero">
    <div class="hero__container">
      <!-- 主要内容区块 -->
      <div class="hero__title">
        UnoCSS
      </div>
      <div class="hero__subtitle">
        The instant on-demand Atomic CSS engine.
      </div>
      <div class="hero__social">
        <a
          class="hero__github-link"
          href="https://github.com/unocss/unocss"
          target="_blank"
        >
          <span class="icon icon--github"></span>
        </a>
      </div>
    </div>
  </div>
  <!-- 底部标语使用独立模块 -->
  <div class="slogan">
    on-demand · instant · fully customizable
  </div>

对比两种实现方式,CSS原子化方式的缺点:

  • CSS冗长:为了实现一个元素的交互效果,样式会附加大量的原子化CSS Class,如一长串的text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s;
  • 理解成本比较高:维护这些CSS时,需要花时间理解每个名字背后的样式,除非借助IDE插件查看每个class包含的样式;
  • 不易维护:对于一个大型项目,如果要扩展或修改同类型元素的样式时,面对一大堆原子化CSS就显得束手无策;

以上这些体现在CSS原子化框架上的缺点,在BEMOOCSSSMACSS面前都不是问题,结构化、语义化的CSS天生就显得简洁,容易理解,方便维护。

总结

BEM提供命名规则,OOCSS、SMACSS提供方法论,这三者之间不是互斥关系,一个平台完全可以结合三者各自的优势来设计。SMACSS从宏观角度考虑CSS按哪种模块拆分合理,而OOCSS确定一个模块中的CSS按什么结构来组织,在具体落地到CSS编写时通过BEM来规范命名。

BEMOOCSSSMACSS对比现代化的原子化CSS原子化CSS可理解为是结构化、模块化的CSS的更细粒度的裂变,例如将10个class裂变为30个class,提升更细粒度的复用。这种拆分为明确的、有规律的css的方式,非常适用于小型、低代码或前端AI生成项目。相反,BEMOOCSSSMACSS更适合于需长期维护的大型项目。

参考

  1. Understanding CSS naming conventions: BEM, OOCSS, SUIT CSS, and SMACSS
  2. What is BEM
  3. Scaling Down The BEM Methodology For Small Projects
  4. CSS 架构之OOCSS
  5. CSS 模块化方案探讨(BEM、OOCSS、CSS Modules、CSS-in-JS ...)
  6. What is OOCSS and How OCSS Works
  7. What Is SMACSS and How Does It Work?

我是前端下饭菜,原创不易,各位看官动动手,帮忙关注、点赞、收藏、评论!