CSS 工具类与“关注点分离”

1,061 阅读22分钟

原文链接:CSS Utility Classes and "Separation of Concerns",2017.08.07,by Adam Wathan

掘金首发,7 天内禁止任何形式的转载。也欢迎大家关注我的微信公众号 “写代码的宝哥”,每周至少 2 更,带你了解第一手前端资讯及常用技术学习 ;)。

在过去的几年里,我编写 CSS 的方式已经从一种非常“语义”的方法转变为通常被称为“功能性 CSS”的方法。

以这种方式编写 CSS 会引起许多开发人员的强烈反应,所以我想解释一下我是如何走到这一步的,并分享一些我沿着这一过程学到的经验和见解。

阶段1:“语义”CSS

当你试图学习如何写好 CSS 时,你会听到一个最佳实践是“关注点分离”。

这个实践倡导 HTML 只应该包含有关内容的信息,所有的样式决策都应该写在 CSS 中。

看看这个 HTML:

<p class="text-center">
    Hello there!
</p>

看到 .text-center 了吗?文本居中是一个设计决策,所以这段代码违反了“关注点分离”,因为样式信息被暴露到 HTML 中了。

相反,推荐的方法是根据元素的内容为元素给予类名,并将这些类用作 CSS 中的钩子来样式化标记:

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

<p class="greeting">
    Hello there!
</p>

这种方法的典型用例就是 CSS 禅宗花园——一个指导你使用“关注点分离”理念,这样一来,在你重新设计一个网站时,只要更换样式表就行了。

按照这个理念,我的工作流程通常是这样的:

  1. 为一些新的 UI 编写我所需的标记(以个人简介为例):
<div>
  <img src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
  <div>
    <h2>Adam Wathan</h2>
    <p>
      Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
    </p>
  </div>
</div>
  1. 根据内容添加一两个描述性的类:
- <div>
+ <div class="author-bio">
    <img src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
    <div>
      <h2>Adam Wathan</h2>
      <p>
        Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
      </p>
    </div>
  </div>
  1. 使用这些类作为 CSS/Less/Sass中 的“钩子”来给我的新标记设置样式:
.author-bio {
  background-color: white;
  border: 1px solid hsl(0,0%,85%);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  overflow: hidden;
  > img {
    display: block;
    width: 100%;
    height: auto;
  }
  > div {
    padding: 1rem;
    > h2 {
      font-size: 1.25rem;
      color: rgba(0,0,0,0.8);
    }
    > p {
      font-size: 1rem;
      color: rgba(0,0,0,0.75);
      line-height: 1.5;
    }
  }
}

以下是最终结果的一个线上演示:

这种方法对我来说很有意义,有一段时间我就是这样写 HTML 和 CSS 的。

不过随着进一步使用,就开始感觉有点不对劲了。

我已经“分离了我的关注点”,但是在我的 CSS 和 HTML 之间仍然存在一个非常明显的耦合。大多数时候我的 CSS 就像是标记的镜子——嵌套的 CSS 选择器间接反映了我所使用的 HTML 结构。

我的标记与样式设计无关,但标记结构却跟 CSS 耦合了。

也许我的关注点并没有那么分离。

阶段2:将样式与结构解耦

在寻找这种耦合的解决方案之后,我开始发现越来越多的建议,在标记上添加更多的类,这样就可以直接将这些标记作为目标选择器设置。使你的 CSS 更少地依赖于你的特定 DOM 结构,同时保持选择器的低权重。

主张这种思想的最著名方案就是“块-元素-修改器( Block Element Modifer)”了,或简称为 BEM。

采用类似于 BEM 的方法,我们的个人简介的标记看起来像这样:

<div class="author-bio">
  <img class="author-bio__image" src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
  <div class="author-bio__content">
    <h2 class="author-bio__name">Adam Wathan</h2>
    <p class="author-bio__body">
      Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
    </p>
  </div>
</div>

……我们的 CSS 看起来像这样:

.author-bio {
  background-color: white;
  border: 1px solid hsl(0,0%,85%);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  overflow: hidden;
}
.author-bio__image {
  display: block;
  width: 100%;
  height: auto;
}
.author-bio__content {
  padding: 1rem;
}
.author-bio__name {
  font-size: 1.25rem;
  color: rgba(0,0,0,0.8);
}
.author-bio__body {
  font-size: 1rem;
  color: rgba(0,0,0,0.75);
  line-height: 1.5;
}

以下是最终结果的一个线上演示:

这对我来说是一个巨大的进步。我的标记仍然是“语义”的,不包含任何样式决定,而我的 CSS 感觉与我的标记结构解耦了,也避免了不必要的选择器权重。

但后来我又陷入了两难境地。

处理类似的组件

假设我需要在网站上添加一个新功能:在卡片布局中显示文章的预览。

假设这篇文章预览卡的顶部有一个完整的图片,下面有一个填充的内容部分,一个粗体标题和一些较小的正文文本。

看起来就像下面这样的个人简介组件。

这个时候如果要做到关注点分离,我该如何处理呢?

我们无法将 .author-bio 类应用到文章预览中——语义上是不通的。所以我们还需要一个额外的 .article-preview 的组件。

我们的标记可能是下面这样的:

<div class="article-preview">
  <img class="article-preview__image" src="https://i.vimeocdn.com/video/585037904_1280x720.webp" alt="">
  <div class="article-preview__content">
    <h2 class="article-preview__title">Stubbing Eloquent Relations for Faster Tests</h2>
    <p class="article-preview__body">
      In this quick blog post and screencast, I share a trick I use to speed up tests that use Eloquent relationships but don't really depend on database functionality.
    </p>
  </div>
</div>

那 CSS 又该如何处理呢?

选项1:复制样式

一种方法是直接复制我们的 .author-bio 样式并重命名类:

.article-preview {
  background-color: white;
  border: 1px solid hsl(0,0%,85%);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  overflow: hidden;
}
.article-preview__image {
  display: block;
  width: 100%;
  height: auto;
}
.article-preview__content {
  padding: 1rem;
}
.article-preview__title {
  font-size: 1.25rem;
  color: rgba(0,0,0,0.8);
}
.article-preview__body {
  font-size: 1rem;
  color: rgba(0,0,0,0.75);
  line-height: 1.5;
}

这样是可行的,但不是很 DRY。这也使得这些组件很容易以稍微不同的方式(可能是不同的填充,或字体颜色)展现,很可能会导致外观表现上的不一致。

选项2: @extend 个人简介组件

另一种方法是使用预处理器的 @extend 特性——让你可以利用 .author-bio 组件中已经定义好的样式:

.article-preview {
  @extend .author-bio;
}
.article-preview__image {
  @extend .author-bio__image;
}
.article-preview__content {
  @extend .author-bio__content;
}
.article-preview__title {
  @extend .author-bio__name;
}
.article-preview__body {
  @extend .author-bio__body;
}

以下是最终结果的一个线上演示:

不过 @extend 的方式通常并不推荐,但撇开这一点,感觉它是解决了我们的问题。

我们已经删除了 CSS 中的重复,我们的标记也没有包含样式决定。

但让我们再看一个选项...

选项3:创建内容无关的组件

从“语义”的角度来看,我们的 .author-bio.article-preview 组件没有任何共同之处。一个是个人简介,另一个是一篇文章的预览。

但正如我们已经看到的,从设计的角度来看,它们有_很多_共同之处。

因此,我们还可以创建一个新的组件,提取上述两个组件的共同之处进行命名,并将该组件用于这两种类型的内容。

我们称之为 .media-card

下面是 CSS:

.media-card {
  background-color: white;
  border: 1px solid hsl(0,0%,85%);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  overflow: hidden;
}
.media-card__image {
  display: block;
  width: 100%;
  height: auto;
}
.media-card__content {
  padding: 1rem;
}
.media-card__title {
  font-size: 1.25rem;
  color: rgba(0,0,0,0.8);
}
.media-card__body {
  font-size: 1rem;
  color: rgba(0,0,0,0.75);
  line-height: 1.5;
}

……下面是个人简介的标记:

<div class="media-card">
  <img class="media-card__image" src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
  <div class="media-card__content">
    <h2 class="media-card__title">Adam Wathan</h2>
    <p class="media-card__body">
      Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
    </p>
  </div>
</div>

……下面是文章预览的标记:

<div class="media-card">
  <img class="media-card__image" src="https://i.vimeocdn.com/video/585037904_1280x720.webp" alt="">
  <div class="media-card__content">
    <h2 class="media-card__title">Stubbing Eloquent Relations for Faster Tests</h2>
    <p class="media-card__body">
      In this quick blog post and screencast, I share a trick I use to speed up tests that use Eloquent relationships but don't really depend on database functionality.
    </p>
  </div>
</div>

这种方法也消除了 CSS 中的重复,但我们现在不是在“混合关注点”吗?

我们的标记反映我们希望这两块内容都被设计为媒体卡。如果我们想改变个人简介的外观,而不改变文章预览的外观,该怎么办?

以前,我们可以打开样式表,为两个组件中的任何一个设置新的样式。而现在我们需要编辑 HTML!

但我们先不考虑这个问题,看看另一个方面。

如果我们需要添加一种新类型的内容,它也需要相同的样式,该怎么办?

使用“语义”方法,我们需要编写新的 HTML,添加一些特定于内容的类作为样式“钩子”,打开样式表,为新的内容类型创建一个新的 CSS 组件,并应用共享样式,通过复制或使用 @extendmixin

而使用我们的内容无关的 .media-card 类,我们需要编写的只是新的 HTML——我们根本不需要打开样式表。

如果我们真的是“混合关注点”,难道我们不需要在多个地方做出改变吗?

“关注分离”并不是正确的思考方式

当你从“关注点分离”的角度考虑 HTML 和 CSS 之间的关系时,它是黑白的。

你可以将关注点分离(很好!),或者你不分离(很不好!)。

这不是思考 HTML 和 CSS 的正确方式。

相反,考虑依赖方向(dependency direction)。

有两种方法可以编写 HTML 和 CSS:

  1. ~~“关注点分离”~~

CSS 依赖于 HTML。

根据内容命名类(比如 .author-bio)将 HTML 视为 CSS 的依赖项。

HTML 是独立的——它并不关心你如何修饰它,而只是在 HTML 暴露了控制的钩子,如 .author-bio

另一方面,你的 CSS 并不独立——它需要知道 HTML 结构决定添加哪些类,并且需要针对这些类来设置 HTML 的样式。

在这个模型中,HTML 是可以重新设计的(restyleable),但是 CSS 不能重用。

  1. ~~“混合关注点”~~

HTML 依赖于 CSS。

提取 UI 中重复的模式(如 .media-card)编写与内容无关的类名,将 CSS 视为 HTML 的依赖项。

CSS 是独立的——它并不关心它应用于什么内容,它只是 添加了一组可以应用于标记的构建块(building blocks)。

你的 HTML 不是独立的——它使用提供出来 CSS 类修饰自己,它需要知道有哪些类,以便根据需要将它们有机的组合起来,以实现所需要的设计。

在这个模型中,CSS 是可重用的,但 HTML 是不梦重新设计的。

CSS Zen Garden 采用是第一种方法,而像 BootstrapBulma 这样的 UI 框架采用的是第二种方法。

两种方案没有绝对的对错之分,因为基于项目本身的上下文环境决定的,需要辨别出什么对你来说是更重要的。

对于你正在做的项目,什么会更有价值:可重新设计的 HTML,还是可重用的 CSS?

选择可重用性

当我读到 Nicolas Gallagher 的《关于 HTML 语义和前端架构》(About HTML semantics and front-end architecture)时,我的转折点出现了。

我不会在这里重申他的所有观点,但不用说,我从那篇博客文章中完全相信,对我所参与的各个项目进行 CSS 可重用优化将是一个正确选择。

阶段3:内容无关的 CSS 组件

在这一点上,我的目标是显式地避免创建基于我内容的类,而是尝试以尽可能可重用的方式命名所有内容。

这导致了类名称,例如:

  • .card
  • .btn, .btn--primary, .btn--secondary
  • .badge
  • .card-list, .card-list-item
  • .img--round
  • .modal-form, .modal-form-section

……等等等等。

当我开始专注于创建可重用类时,我注意到了另外一件事:

一个组件做的越多,或者一个组件越具体,就越难重用。

这里有一个直观的例子。

假设我们正在构建一个表单,其中有几个表单部分,底部有一个提交按钮。

如果我们认为所有表单内容都是 .stacked-form 组件的一部分,我们可能会给予提交按钮一个类似 .stacked-form__button 的类:

<form class="stacked-form" action="#">
  <div class="stacked-form__section">
    <!-- ... -->
  </div>
  <div class="stacked-form__section">
    <!-- ... -->
  </div>
  <div class="stacked-form__section">
    <button class="stacked-form__button">Submit</button>
  </div>
</form>

但也许我们的网站上还有一个按钮,它不是表单的一部分,我们需要以同样的方式展示样式。

在那个按钮上使用 .stacked-form__button 类没有太大意义,它不是堆叠表单的一部分。

这两个按钮都是它们各自部份的主要操作,所以如果我们根据组件的共同点命名按钮,并将其命名为 .btn--primary,完全删除 .stacked-form__ 前缀会怎么样?

  <form class="stacked-form" action="#">
    <!-- ... -->
    <div class="stacked-form__section">
-     <button class="stacked-form__button">Submit</button>
+     <button class="btn btn--primary">Submit</button>
    </div>
  </form>

现在假设我们想让这个堆叠的表单要表现出浮动的片的效果。

一种方法是创建修改器(modifier)并将其应用到这个表单:

- <form class="stacked-form" action="#">
+ <form class="stacked-form stacked-form--card" action="#">
    <!-- ... -->
  </form>

但是如果我们已经有了一个 .card 类,为什么我们不使用现有的卡片和堆叠表单来组成这个新的 UI 呢?

+ <div class="card">
    <form class="stacked-form" action="#">
      <!-- ... -->
    </form>
+ </div>

通过采用这种方法,我们有了一个可以包含任何内容的 .card 组件,以及一个可以在任何容器内部使用表现都一致的 .stacked-form

我们的组件得到了更多的重用,我们不必编写任何新的 CSS

子组件上的合成

假设我们需要在堆叠表单的底部添加另一个按钮,并且我们希望它与现有按钮间隔开一点:

<form class="stacked-form" action="#">
  <!-- ... -->
  <div class="stacked-form__section">
    <button class="btn btn--secondary">Cancel</button>
    <!-- Need some space in here -->
    <button class="btn btn--primary">Submit</button>
  </div>
</form>

一种方法是创建一个新的子组件,如 .stacked-form__footer,向每个按钮添加一个额外的类,如 .stacked-form__footer-item,并使用后代选择器添加一些边距:

  <form class="stacked-form" action="#">
    <!-- ... -->
-   <div class="stacked-form__section">
+   <div class="stacked-form__section stacked-form__footer">
-     <button class="btn btn--secondary">Cancel</button>
-     <button class="btn btn--primary">Submit</button>
+     <button class="stacked-form__footer-item btn btn--secondary">Cancel</button>
+     <button class="stacked-form__footer-item btn btn--primary">Submit</button>
    </div>
  </form>

CSS 可能类似下面这样:

.stacked-form__footer {
  text-align: right;
}
.stacked-form__footer-item {
  margin-right: 1rem;
  &:last-child {
    margin-right: 0;
  }
}

但是如果我们在某个子导航或标题中遇到同样的问题呢?

我们不能在 .stacked-form 之外重用 .stacked-form__footer,所以也许我们可以在头部内创建一个新的子组件:

  <header class="header-bar">
    <h2 class="header-bar__title">New Product</h2>
+   <div class="header-bar__actions">
+     <button class="header-bar__action btn btn--secondary">Cancel</button>
+     <button class="header-bar__action btn btn--primary">Save</button>
+   </div>
  </header>

……但现在我们必须在新的 .header-bar__actions 组件中重复 .stacked-form__footer 的工作。

这感觉很像我们在开始时遇到的内容驱动类名的问题,不是吗?

解决这个问题的一种方法是提取出一个更容易重用的全新组件,并使用组合方式带入。

也许,我们可以创建一个类似于 .actions-list 的类:

.actions-list {
  text-align: right;
}
.actions-list__item {
  margin-right: 1rem;
  &:last-child {
    margin-right: 0;
  }
}

现在我们可以完全摆脱 .stacked-form__footer.header-bar__actions 组件,而在两种情况下都使用 .actions-list

<!-- Stacked form -->
<form class="stacked-form" action="#">
  <!-- ... -->
  <div class="stacked-form__section">
    <div class="actions-list">
      <button class="actions-list__item btn btn--secondary">Cancel</button>
      <button class="actions-list__item btn btn--primary">Submit</button>
    </div>
  </div>
</form>

<!-- Header bar -->
<header class="header-bar">
  <h2 class="header-bar__title">New Product</h2>
  <div class="actions-list">
    <button class="actions-list__item btn btn--secondary">Cancel</button>
    <button class="actions-list__item btn btn--primary">Save</button>
  </div>
</header>

但是,如果这些动作列表中的一个是左对齐的,另一个应该是右对齐的呢?添加 .actions-list--left.actions-list--right 修改器?

阶段4:内容无关的组件+工具类

一直试图想出这些组件名让人很累。

当你创建像 .actions-list--left 这样的修饰符时,只是为了分配一个 CSS 属性。它的名字中包含了 left,所以你也不会欺骗任何人说它是“语义”的。

如果我们有另一个需要左对齐和右对齐修饰符的组件,我们是否也要为它创建新的组件修饰符?

这又回到了我们当初移除 .stacked-form__footer.header-bar__actions 并使用 .actions-list 替换它们时所面临的相同问题:

我们喜欢合成而不喜欢重复。

因此,如果我们有两个动作列表,一个需要左对齐,另一个需要右对齐,我们如何用组合方式来解决这个问题?

对齐工具

为了使用组合解决这个问题,我们需要向我们的组件添加一个新的可重用类,来提供我们想要的效果。

我们已经将修饰符命名为 .actions-list--left.actions-list--right,所以没有理由不将这些新类命名成 .align-left.align-right

.align-left {
  text-align: left;
}
.align-right {
  text-align: right;
}

现在我们可以使用合成来使我们的堆叠表单按钮左对齐:

<form class="stacked-form" action="#">
  <!-- ... -->
  <div class="stacked-form__section">
    <div class="actions-list align-left">
      <button class="actions-list__item btn btn--secondary">Cancel</button>
      <button class="actions-list__item btn btn--primary">Submit</button>
    </div>
  </div>
</form>

……我们的标题按钮右对齐:

<header class="header-bar">
  <h2 class="header-bar__title">New Product</h2>
  <div class="actions-list align-right">
    <button class="actions-list__item btn btn--secondary">Cancel</button>
    <button class="actions-list__item btn btn--primary">Save</button>
  </div>
</header>

不要害怕

如果在 HTML 中看到单词“left”和“right”会让你感到不舒服,请记住,我们在 UI 中使用以视觉模式命名的组件已经有很长时间了。

没有人会假装认为 .stacked-form.align-right 更“语义”——它们都是根据如何影响标记的方式来命名的,我们在标记中使用这些类来实现特定的表现。

我们正在独立于 CSS 的 HTML。如果我们想将表单从 .stacked-form 更改为 .horizontal-form,我们可以在标记中直接完成,而不是在 CSS 中。

删除无用的抽象

这个解决方案的有趣之处在于,我们的 .actions-list 组件现在基本上没用了,它之前所做的只是将内容向右对齐。

- .actions-list {
-   text-align: right;
- }
  .actions-list__item {
    margin-right: 1rem;
    &:last-child {
      margin-right: 0;
    }
  }

但是现在有一个没有 .actions-list.actions-list__item 有点奇怪。有没有其他方式可以解决我们最初的问题,而不需要再创建一个 .actions-list__item 组件?

如果你回想一下,我们创建这个组件的原因只是想在两个按钮之间添加一点空白。 .actions-list 是一个相当不错的按钮列表抽象,因为它是通用的,但在有些情况下,我们只是需要相互间隔一定距离的项目,而不想具有表现“actions”的意思,对吗?

也许要想一个更加通用、看重用的名称,比如 .spaced-horizontal-list?我们已经删除了 .actions-list 组件,因为现在只有子组件才真正需要设置样式。

间隔工具类

如果只有子元素需要样式化,也许独立地为子元素设置样式会更简单,而不是使用花哨的伪选择器将它们作为一个组来设置样式?

最好的可重用的方式就是去声明一个类名,表示“这个元素旁边应该有一些空白”。

我们已经添加了像 .align-left.align-right 这样的工具类名,如果我们创建一个新的工具类来添加一些右边距会怎么样?

让我们创建一个新的工具类,类似于 .mr-sm,用于在元素的右侧添加少量边距:

- .actions-list__item {
-   margin-right: 1rem;
-   &:last-child {
-     margin-right: 0;
-   }
- }
+ .mr-sm {
+   margin-right: 1rem;
+ }

下面是我们的表单和头部组件现在的样子:

<!-- Stacked form -->
<form class="stacked-form" action="#">
  <!-- ... -->
  <div class="stacked-form__section align-left">
    <button class="btn btn--secondary mar-r-sm">Cancel</button>
    <button class="btn btn--primary">Submit</button>
  </div>
</form>

<!-- Header bar -->
<header class="header-bar">
  <h2 class="header-bar__title">New Product</h2>
  <div class="align-right">
    <button class="btn btn--secondary mr-sm">Cancel</button>
    <button class="btn btn--primary">Save</button>
  </div>
</header>

.actions-list 被整个移除了,我们的 CSS 也更小了,而且类名也更加可重用了。

阶段5:工具类优先的 CSS

一旦我点击了这个,不久我就构建了一整套工具类,用于我常见的一些视觉调整,比如:

  • 文本大小、颜色和粗细
  • 边框颜色、宽度和位置
  • 背景颜色
  • Flexbox 工具类
  • padding 和 margin 工具类

令人惊叹的是,在使用这些工具类之后,可以在完全不需要编写任何新的 CSS 的情况下,构建一个全新的 UI 组件。

看看我的一个项目中的这种“产品卡(product card)”组件:

下面是我所使用的标记:

<div class="card rounded shadow">
    <a href="..." class="block">
        <img class="block fit" src="...">
    </a>
    <div class="py-3 px-4 border-b border-dark-soft flex-spaced flex-y-center">
        <div class="text-ellipsis mr-4">
            <a href="..." class="text-lg text-medium">
                Test-Driven Laravel
            </a>
        </div>
        <a href="..." class="link-softer">
            @icon('link')
        </a>
    </div>
    <div class="flex text-lg text-dark">
        <div class="py-2 px-4 border-r border-dark-soft">
            @icon('currency-dollar', 'icon-sm text-dark-softest mr-4')
            <span>$3,475</span>
        </div>
        <div class="py-2 px-4">
            @icon('user', 'icon-sm text-dark-softest mr-4')
            <span>25</span>
        </div>
    </div>
</div>

这里使用的类的数量可能会让你一开始会有些犹豫,它确实是一个真实存在的 CSS 组件,而不是用工具类来组织的一块内容。我们该如何称呼叫它?

我们不想使用特定于内容的名称,因为这样我们的组件就只能限制在一个上下文中去使用了。

还是说要像这样?

.image-card-with-a-full-width-section-and-a-split-section { ... }

当然不是,这太荒谬了。相反,我们可能想用更小的组件来合成它,就像我们之前讨论过的那样。

这些组件可能是什么?

也许它是一个卡片组件里的一部分。不是所有的卡片都有阴影,所以我们可能还需要一个 .card--shadowed 修饰符,或者我们可以创建一个可以应用于任何元素的 .shadow 工具类。这听起来可重用程度更好,OK 那就这么做。

原来我们网站上的一些卡片没有圆角,但这张卡片有。我们可以给它一个 .card--rounded,但我们在网站上有其他元素包含圆角的,数量也不少,但不是卡片。 所有感觉抽象出一个 .rounded 工具类更好一些。

那上面的图片呢?也许这是一个类似于 .img--fitted 的东西,所以它填充了卡?好吧,在网站上还有一些其他的地方,我们需要一个适应它的父宽度的东西,它并不总是一个图像。或许 .fit 工具类会更好点。

……你应该明白我的意思了。

如果你沿着这条路走得足够远,并关注可重用性,那么使用可重用工具类去构建组件就会发现是一件自然而然的事情了。

强制一致性

使用小型的、可组合的工具类的最大好处之一是团队中的每个开发人员总是从一组固定的选项中选择。

有多少次你在设计一些 HTML 的样式,想着“这个文本需要稍微暗一点”,然后找到 darken() 函数来调整一些基础 $text-color?

或者,“这个字体应该小一点”,并将 font-size: .85em 添加到你正在处理的组件中?

感觉就像你做的事情是“正确的”,因为你使用的是相对颜色或相对字体大小,而不是一个任意值。

但是,如果你决定将您的组件的文本变暗 10%,而其他人将其组件的文本变暗 12%,该怎么办?在你知道它之前,你的样式表已经有 402 种独特的文本颜色了。

这种情况发生在每个代码库中,你的风格是总是写新的 CSS:

  • GitLab:402 种文本颜色,239 种背景颜色,59 种字体大小
  • Buffer:124 种文本颜色,86 种背景颜色,54 种字体大小
  • HelpScout:198 种文本颜色,133 种背景颜色,67 种字体大小
  • Gumroad:91 种文字颜色,28 种背景颜色,48 种字体大小
  • Stripe:189 种文字颜色,90 种背景颜色,35 种字体大小
  • GitHub:163 种文本颜色,147 种背景颜色,56 种字体大小
  • ConvertKit:128 种文本颜色,124 种背景颜色,70 种字体大小

这是因为你写的每一个新的 CSS 块都是一个空白的画布——没有什么人能阻止你使用任何你想要的值。

你可以尝试通过变量或混合函数来实现一致性,但每一行新 CSS 都是一个增加复杂性的机会——添加更多的 CSS 永远不会让你的 CSS 更简单。

相反,如果样式化解决方案是应用现有的类,那么空白画布的问题就会突然消失。

想把一些深色文字静音一点吗?添加 .text-dark-soft 类吧。

需要把字体变小一点吗?使用 .text-sm 类吧。

当一个项目中的每个人都是从一组精心涉及的有限选项中选择他们的样式时,你的 CSS 就不会随着你项目规模线性增长而增长,同时你又能获得一致性显示体验。

你仍然可以创建组件

我的观点与一些真正顽固的函数式 CSS 倡导者有一点不同的地方是,我认为你不应该_只用_工具类构建东西。

如果你看看一些流行的基于工具类的框架,比如 Tachyons(这是一个很棒的项目),你会发现它们甚至创建了纯工具类组合而成的按钮样式:

<button class="f6 br3 ph3 pv2 white bg-purple hover-bg-light-purple">
  Button Text
</button>

让我来分解一下:

  • f6:使用字体大小刻度中的第六个字体大小(在超光速子中为 .875rem
  • br3:使用半径刻度中的第三个边界半径(.5rem
  • ph3:使用填充比例中的第三个大小进行水平填充(1rem
  • pv2:垂直填充使用填充刻度中的第二个尺寸(.5rem
  • white:使用白色文本
  • bg-purple:使用紫色背景
  • hover-bg-light-purple:悬停时使用浅紫背景

如果你需要多个具有相同类组合的按钮,Tachyons 推荐使用的方法是通过模板抽象而不是通过 CSS。

例如,如果你使用的是 Vue.js,在一个组件中你是这样使用的:

<ui-button color="purple">Save</ui-button>

……会这样定义:

<template>
  <button class="f6 br3 ph3 pv2" :class="colorClasses">
    <slot></slot>
  </button>
</template>

<script>
  export default {
    props: ['color'],
    computed: {
      colorClasses() {
        return {
          purple: 'white bg-purple hover-bg-light-purple',
          lightGray: 'mid-gray bg-light-gray hover-bg-light-silver',
          // ...
        }[this.color]
      }
    }
  }
</script>

对于很多项目来说,这是一个很好的方法,但我仍然认为在很多用例中,创建 CSS 组件比创建基于模板的组件更实用

对于我参与的项目类型,创建一个新的 .btn-purple 类来捆绑这 7 个工具类通常比模板化站点上的每个小部件要简单得多。

……但首先要使用工具类进行构建

我之所以将这种 CSS 的方法称为工具类优先的原因是,我试图从工具类中构建所有我能构建的东西,并且只在出现时提取重复的模式。

如果你使用 Less 作为预处理器,你可以使用现有的类作为 mixin。这表示创建这个 .btn-purple 组件只需要在编辑器中使用一点多光标技巧:

不幸的是,在 Sass 或 Stylus 中,如果不为每个工具类创建单独的 mixin,就无法做到这一点,因此需要做更多的工作。

当然,组件中的每个声明并不总是可能来自一个工具类。元素之间的复杂交互,比如当鼠标悬停在父元素上时更改子元素的属性,很难只用工具类来完成,所以最好的判断原则是,尽量更简单的完成更手里的事情。

不要过早抽象

对 CSS 采用以组件优先的方法进行组织,就表示它们永远不能被重用,这种过早的抽象是样式表中大量臃肿和复杂的根源。

以导航栏为例。在你的应用程序中,你重写了几次主导航的标记?

在我的项目中,我通常只做一次——在我的主布局文件中。

如果你首先是采用工具类来进行构建的,并且只在看到令人担忧的重复时才提取组件,那么你可能永远都不需要提取导航栏组件

相反,你的导航栏可能看起来就像这样:

<nav class="bg-brand py-4 flex-spaced">
  <div><!-- Logo goes here --></div>
  <div>
    <!-- Menu items go here -->
  </div>
</nav>

这里没有什么值得提取的东西。

这不就是内联样式吗?

很容易看到这种说法,并感觉这不就是在 HTML 元素上写样式吗,把任何你想加上的属性都加上去,但在我的经验中,这是非常不同的。

对于内联样式,你选择的值没有任何限制。

一个标签可以是 font-size: 14px,也可以是 font-size: 13px,还可以是 font-size: .9em,还能是 font-size: .85rem

因此,为每个新组件编写新 CSS 的,会面临跟“空白画布”一样的问题。

而使用工具类,会迫使你选择:

text-sm 还是 text-xs

我应该使用 py-3 还是 py-4

text-dark-soft 还是 text-dark-faint

你不能随便挑一个值,而必须要从精心设计的名单中选择。

不会出现 380 个文本颜色,最终只会用到 10 或 12 个。

我的经验是,以实用程序为先构建东西,比以组件为先构建东西,会带来_更_一致的外观设计,虽然这听起来很不直观。

从哪里开始

如果这种方法听起来对你感兴趣,那么可以试试我免费开源的 Tailwind CSS 项目,它是围绕工具类优先和从重复模式中提取组件的思想设计出来的。

如果有兴趣,请前往 Tailwind CSS 网站并尝试一下吧。