使用BEM和实用类构建可扩展的CSS架构

Avatar of Sebastiano Guerriero
Sebastiano Guerriero

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200美元免费额度!

维护大型CSS项目很困难。多年来,我们见证了旨在简化编写可扩展CSS过程的不同方法。最终,我们都试图实现以下两个目标

  1. 效率:我们希望减少花费在思考如何做事的时间,并增加实际做事的时间。
  2. 一致性:我们希望确保所有开发人员都步调一致。

在过去一年半的时间里,我一直在开发一个 组件库 和一个名为 CodyFrame 的前端框架。我们目前拥有220多个组件。这些组件不是孤立的模块:它们是可重用的模式,通常相互合并以创建复杂的模板。

这个项目的挑战迫使我们的团队开发了一种构建可扩展CSS架构的方法。这种方法依赖于CSS全局样式、BEM和实用类

我很乐意分享它!👇

30秒了解CSS全局样式

全局样式是包含适用于所有组件的规则的CSS文件(例如,间距比例、排版比例、颜色等)。全局样式使用令牌来保持所有组件之间设计的一致性,并减少其CSS的大小。

这是一个排版全局规则的示例

/* Typography | Global */
:root {
  /* body font size */
  --text-base-size: 1em;


  /* type scale */
  --text-scale-ratio: 1.2;
  --text-xs: calc((--text-base-size / var(--text-scale-ratio)) / var(--text-scale-ratio));
  --text-sm: calc(var(--text-xs) * var(--text-scale-ratio));
  --text-md: calc(var(--text-sm) * var(--text-scale-ratio) * var(--text-scale-ratio));
  --text-lg: calc(var(--text-md) * var(--text-scale-ratio));
  --text-xl: calc(var(--text-lg) * var(--text-scale-ratio));
  --text-xxl: calc(var(--text-xl) * var(--text-scale-ratio));
}


@media (min-width: 64rem) { /* responsive decision applied to all text elements */
  :root {
    --text-base-size: 1.25em;
    --text-scale-ratio: 1.25;
  }
}


h1, .text-xxl   { font-size: var(--text-xxl, 2.074em); }
h2, .text-xl    { font-size: var(--text-xl, 1.728em); }
h3, .text-lg    { font-size: var(--text-lg, 1.44em); }
h4, .text-md    { font-size: var(--text-md, 1.2em); }
.text-base      { font-size: --text-base-size; }
small, .text-sm { font-size: var(--text-sm, 0.833em); }
.text-xs        { font-size: var(--text-xs, 0.694em); }

30秒了解BEM

BEM(块、元素、修饰符)是一种旨在创建可重用组件的命名方法。

这是一个示例

<header class="header">
  <a href="#0" class="header__logo"><!-- ... --></a>
  <nav class="header__nav">
    <ul>
      <li><a href="#0" class="header__link header__link--active">Homepage</a></li>
      <li><a href="#0" class="header__link">About</a></li>
      <li><a href="#0" class="header__link">Contact</a></li>
    </ul>
  </nav>
</header>
  • 一个是一个可重用组件
  • 一个元素是块的子元素(例如,.block__element
  • 一个修饰符是块/元素的变体(例如,.block--modifier,.block__element--modifier)。

30秒了解实用类

实用类是一种只做一件事的CSS类。例如

<section class="padding-md">
  <h1>Title</h1>
  <p>Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
</section>


<style>
  .padding-sm { padding: 0.75em; }
  .padding-md { padding: 1.25em; }
  .padding-lg { padding: 2em; }
</style>

您可以使用实用类构建整个组件

<article class="padding-md bg radius-md shadow-md">
  <h1 class="text-lg color-contrast-higher">Title</h1>
  <p class="text-sm color-contrast-medium">Lorem ipsum dolor sit amet consectetur adipisicing elit.</p>
</article>

您可以将实用类连接到CSS全局样式

/* Spacing | Global */
:root {
  --space-unit: 1em;
  --space-xs:   calc(0.5 * var(--space-unit));
  --space-sm:   calc(0.75 * var(--space-unit));
  --space-md:   calc(1.25 * var(--space-unit));
  --space-lg:   calc(2 * var(--space-unit));
  --space-xl:   calc(3.25 * var(--space-unit));
}

/* responsive rule affecting all spacing variables */
@media (min-width: 64rem) {
  :root {
    --space-unit:  1.25em; /* 👇 this responsive decision affects all margins and paddings */
  }
}

/* margin and padding util classes - apply spacing variables */
.margin-xs { margin: var(--space-xs); }
.margin-sm { margin: var(--space-sm); }
.margin-md { margin: var(--space-md); }
.margin-lg { margin: var(--space-lg); }
.margin-xl { margin: var(--space-xl); }

.padding-xs { padding: var(--space-xs); }
.padding-sm { padding: var(--space-sm); }
.padding-md { padding: var(--space-md); }
.padding-lg { padding: var(--space-lg); }
.padding-xl { padding: var(--space-xl); }

一个真实案例

使用基本示例解释方法并不能揭示实际问题,也不能体现该方法本身的优势。

让我们一起构建一些东西!

我们将创建一个卡片元素的画廊。首先,我们只使用BEM方法来实现,并指出仅使用BEM可能面临的问题。接下来,我们将了解全局样式如何减少CSS的大小。最后,我们将通过引入实用类来使组件可定制。

这是最终结果的预览

让我们从只使用BEM创建画廊开始这个实验

<div class="grid">
  <article class="card">
    <a class="card__link" href="#0">
      <figure>
        <img class="card__img" src="/image.jpg" alt="Image description">
      </figure>


      <div class="card__content">
        <h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1>


        <p class="card__description">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
      </div>


      <div class="card__icon-wrapper" aria-hidden="true">
        <svg class="card__icon" viewBox="0 0 24 24"><!-- icon --></svg>
      </div>
    </a>
  </article>


  <article class="card"><!-- card --></article>
  <article class="card"><!-- card --></article>
  <article class="card"><!-- card --></article>
</div>

在这个示例中,我们有两个组件:.grid.card。第一个用于创建画廊布局。第二个是卡片组件。

首先,我想指出使用BEM的主要优势:低特异性作用域

/* without BEM */
.grid {}
.card {}
.card > a {}
.card img {}
.card-content {}
.card .title {}
.card .description {}


/* with BEM */
.grid {}
.card {}
.card__link {}
.card__img {}
.card__content {}
.card__title {}
.card__description {}

如果您不使用BEM(或类似的命名方法),最终会创建继承关系(.card > a)。

/* without BEM */
.card > a.active {} /* high specificity */


/* without BEM, when things go really bad */
div.container main .card.is-featured > a.active {} /* good luck with that 😦 */


/* with BEM */
.card__link--active {} /* low specificity */

在大型项目中处理继承和特异性很痛苦。当您的CSS似乎不起作用,并且您发现它被另一个类覆盖时,那种感觉😡!另一方面,BEM为您的组件创建了一种作用域,并保持了较低的特异性。

但是……仅使用BEM有两个主要缺点:

  1. 命名太多东西令人沮丧
  2. 进行细微的自定义并不容易或难以维护

在我们的示例中,为了设置组件的样式,我们创建了以下类

.grid {}
.card {}
.card__link {}
.card__img {}
.card__content {}
.card__title-wrapper {}
.card__title {}
.card__description {}
.card__icon-wrapper {}
.card__icon {}

类的数量不是问题。问题在于想出如此多的有意义的名称(并让所有团队成员使用相同的命名标准)。

例如,假设您必须修改卡片组件,并在其中包含一个额外的较小的段落

<div class="card__content">
  <h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1>
  <p class="card__description">Lorem ipsum dolor...</p>
  <p class="card__description card__description--small">Lorem ipsum dolor...</p> <!-- 👈 -->
</div>

您如何称呼它?您可以将其视为.card__description元素的变体,并使用.card__description .card__description--small。或者,您可以创建一个新元素,例如.card__small, .card__small-p.card__tag。明白我的意思了吗?没有人想花时间思考类名。只要您不必命名太多东西,BEM就非常棒

第二个问题是处理细微的自定义。例如,假设您必须创建一个卡片组件的变体,其中文本居中对齐。

您可能会这样做

<div class="card__content card__content--center"> <!-- 👈 -->
  <h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1>
  <p class="card__description">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
</div>


<style>
  .card__content--center { text-align: center; }
</style>

您的一个团队成员在处理另一个组件(.banner)时也遇到了同样的问题。他们也为其组件创建了一个变体

<div class="banner banner--text-center"></div>


<style>
  .banner--text-center { text-align: center; }
</style>

现在假设您必须将横幅组件包含到页面中。您需要文本居中对齐的变体。在不检查横幅组件的CSS的情况下,您可能会本能地写banner banner--center到您的HTML中,因为在创建文本居中对齐的变体时,您总是使用--center。不起作用!您唯一的选择是打开横幅组件的CSS文件,检查代码,并找出应该应用哪个类才能使文本居中对齐。

需要多长时间,5分钟?将5分钟乘以您和所有团队成员每天发生这种情况的次数,您就会意识到浪费了多少时间。此外,添加执行相同操作的新类会导致CSS膨胀。

CSS全局样式和实用类来拯救

设置全局样式的第一个好处是有一组应用于所有组件的CSS规则。

例如,如果我们在间距和排版全局样式中设置响应式规则,这些规则也将影响网格和卡片组件。在CodyFrame中,我们在特定断点处增加正文字体大小;因为我们对所有边距和填充使用“em”单位,所以整个间距系统会立即更新,从而产生 级联效果

间距和排版响应式规则——组件级别无需媒体查询

因此,在大多数情况下,您无需使用媒体查询来增加字体大小或边距和填充的值!

/* without globals */
.card { padding: 1em; }


@media (min-width: 48rem) {
  .card { padding: 2em; }
  .card__content { font-size: 1.25em; }
}


/* with globals (responsive rules intrinsically applied) */
.card { padding: var(--space-md); }

不仅如此!您可以使用全局样式来存储可以与所有其他组件组合的行为组件。例如,在CodyFrame中,我们定义了一个 .text-component 类用作“文本包装器”。它负责行高、垂直间距、基本样式和其他内容。

如果我们回到我们的卡片示例,.card__content元素可以用以下内容替换

<!-- without globals -->
<div class="card__content">
  <h1 class="card__title-wrapper"><span class="card__title">Title of the card</span></h1>
  <p class="card__description">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
</div>


<!-- with globals -->
<div class="text-component">
  <h1 class="text-lg"><span class="card__title">Title of the card</span></h1>
  <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
</div>

文本组件将负责文本格式,并使项目中所有文本块保持一致。此外,我们已经消除了几个BEM类。

最后,让我们将实用类引入组合中!

如果您希望能够以后自定义组件而不必检查其CSS,则实用类特别有用。

如果我们将一些BEM类替换为实用类,则卡片组件的结构将发生如下变化

<article class="card radius-lg">
  <a href="#0" class="block color-inherit text-decoration-none">
    <figure>
      <img class="block width-100%" src="image.jpg" alt="Image description">
    </figure>


    <div class="text-component padding-md">
      <h1 class="text-lg"><span class="card__title">Title of the card</span></h1>
      <p class="color-contrast-medium">Lorem ipsum dolor sit amet consectetur adipisicing elit. Tempore, totam?</p>
    </div>


    <div class="card__icon-wrapper" aria-hidden="true">
      <svg class="icon icon--sm color-white" viewBox="0 0 24 24"><!-- icon --></svg>
    </div>
  </a>
</article>

BEM(组件)类的数量已从9个减少到3个

.card {}
.card__title {}
.card__icon-wrapper {}

这意味着您无需过多地处理命名。也就是说,我们无法完全避免命名问题:即使您使用实用类创建Vue/React/其他框架组件,您仍然需要命名组件。

所有其他 BEM 类已被实用程序类替换。如果您需要制作一个标题更大的卡片变体怎么办?将 text-lg 替换为 text-xl。如果要更改图标颜色怎么办?将 color-white 替换为 color-primary。如何将文本居中对齐?将 text-center 添加到文本组件元素中。减少思考时间,增加行动时间!

为什么我们不只使用实用程序类呢?

实用程序类可以加快设计流程,并使自定义操作更容易。那么,为什么我们不忘记 BEM,只使用实用程序类呢?主要有两个原因。

通过将 BEM 与实用程序类一起使用,HTML 更易于阅读和自定义。

使用 BEM 用于

  • 使 HTML 中不打算自定义的 CSS 代码干燥(例如,类似于行为的 CSS 过渡、定位、悬停/焦点效果),
  • 高级动画/效果。

使用实用程序类用于

  • “经常自定义”的属性,通常用于创建组件变体(如填充、边距、文本对齐等),
  • 难以使用新的有意义的类名识别的元素(例如,您需要一个具有 position: relative 的父元素 → 创建 <div class="position-relative"><div class="my-component"></div></div>)。

示例:

<!-- use only Utility classes -->
<article class="position-relative overflow-hidden bg radius-lg transition-all duration-300 hover:shadow-md col-6@sm col-4@md">
  <!-- card content -->
</article>


<!-- use BEM + Utility classes -->
<article class="card radius-lg col-6@sm col-4@md">
  <!-- card content -->
</article>

由于这些原因,我们建议您不要将 !important 规则添加到您的实用程序类中。使用实用程序类不必像使用锤子一样。您认为在 HTML 中访问和修改 CSS 属性是否有益?使用实用程序类。您是否需要一堆不需要编辑的规则?在您的 CSS 中编写它们。此过程不必在您第一次执行时就完美无缺:如果需要,您可以在以后调整组件。听起来“不得不决定”很费力,但当您付诸实践时,它非常简单明了。

在创建独特的特效/动画方面,实用程序类并不是您的最佳盟友。

考虑使用伪元素,或制作需要自定义贝塞尔曲线的独特运动效果。对于这些,您仍然需要打开您的 CSS 文件。

例如,考虑一下我们设计的卡片的动画背景效果。使用实用程序类创建这样的效果有多难?

图标动画也是如此,它需要动画关键帧才能工作。

.card:hover .card__title {
  background-size: 100% 100%;
}


.card:hover .card__icon-wrapper .icon {
  animation: card-icon-animation .3s;
}


.card__title {
  background-image: linear-gradient(transparent 50%, alpha(var(--color-primary), 0.2) 50%);
  background-repeat: no-repeat;
  background-position: left center;
  background-size: 0% 100%;
  transition: background .3s;
}


.card__icon-wrapper {
  position: absolute;
  top: 0;
  right: 0;
  width: 3em;
  height: 3em;
  background-color: alpha(var(--color-black), 0.85);
  border-bottom-left-radius: var(--radius-lg);
  display: flex;
  justify-content: center;
  align-items: center;
}


@keyframes card-icon-animation {
  0%, 100% {
    opacity: 1;
    transform: translateX(0%);
  }
  50% {
    opacity: 0;
    transform: translateX(100%);
  }
  51% {
    opacity: 0;
    transform: translateX(-100%);
  }
}

最终结果

这是卡片画廊的最终版本。它还包括网格实用程序类以自定义布局。

文件结构

以下是使用本文中描述的方法构建的项目的结构示例。

project/
└── main/
    ├── assets/
    │   ├── css/
    │   │   ├── components/
    │   │   │   ├── _card.scss
    │   │   │   ├── _footer.scss
    │   │   │   └── _header.scss
    │   │   ├── globals/
    │   │   │   ├── _accessibility.scss
    │   │   │   ├── _breakpoints.scss
    │   │   │   ├── _buttons.scss
    │   │   │   ├── _colors.scss
    │   │   │   ├── _forms.scss
    │   │   │   ├── _grid-layout.scss
    │   │   │   ├── _icons.scss
    │   │   │   ├── _reset.scss
    │   │   │   ├── _spacing.scss
    │   │   │   ├── _typography.scss
    │   │   │   ├── _util.scss
    │   │   │   ├── _visibility.scss
    │   │   │   └── _z-index.scss
    │   │   ├── _globals.scss
    │   │   ├── style.css
    │   │   └── style.scss
    │   └── js/
    │       ├── components/
    │       │   └── _header.js
    │       └── util.js
    └── index.html

您可以将每个组件的 CSS(或 SCSS)存储到一个单独的文件中(并且,可以选择使用 PostCSS 插件将每个新的 /component/componentName.css 文件编译到 style.css 中)。您可以随意组织全局样式,您也可以创建一个 globals.css 文件,避免将全局样式分离到不同的文件中。

结论

如果您希望在几个月后打开文件并且不会迷路,那么处理大型项目需要一个可靠的架构。有很多方法可以解决这个问题(CSS-in-JS、实用优先、原子设计等)。

我今天与您分享的方法依赖于创建交叉规则(全局样式),使用实用程序类进行快速开发,以及使用 BEM 进行模块化(行为)类。

您可以在 CodyHouse 上详细了解此方法。欢迎任何反馈!