[TailWind CSS] CSS 工具类和 “关注点分离”

kongxiangyan(孔祥岩) · 2020年01月05日 · 最后由 cmlanche 回复于 2020年01月09日 · 296 次阅读
本帖已被管理员设置为精华贴

计划中准备使用 TailWind CSS 重构 Thoughts Lab 的 UI 系统,在操作之前,有必要说服自己确实要这样做!Adam Wathan 是 TailWind CSS 的作者,在他发布 TailWind 之前写过一篇文章,这篇文章详实地梳理了 CSS 诸多方案之间的演变历程和权衡取舍,深有感触,想来大家概有相似的考虑,故译为中文分享给大家!

文中无法添加外链,而我不喜欢将链接作为注释添加的做法,所以将相关的资料梳理成了文档上传到云端,包含文中涉及到的所有相关组件和文章的链接,以及逐句逐段翻译的文档,供大家学习参考!公众号后台回复 TailWind20200105 获取!公众号叫 Cigaret

这篇文章译自:CSS Utility Classes and "Separation of Concerns" - Adam Wathan - August 7, 2017

在过去的几年里,我书写 CSS 的方式已经从一种非常 “语义化” 的方法转变为一种更类似于 “功能性 CSS” 的方法。

这种新的方式能够引起广大开发者由衷的反应!我想解释一下我如何做到这一点,并且分享我在此过程中获得的经验和教训。

阶段一:“语义化” 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 Zen Garden,它旨在向我们展示,一个良好遵循 “关注点分离” 的站点,仅仅通过替换样式表就能重新对其进行设计。这种方式下,工作流程大概如下:

  1. 为新 UI 编写 HTML,以 “作者信息卡片” 为例:
<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. 根据内容添加描述性的 CSS 类:
<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 中完善类的样式以在 HTML 中呈现出来:
.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 就像是 HTML 的一面镜子,HTML 的结构在 CSS 选择器的嵌套中体现的淋漓尽致。

用一句话来描述就是:我的 HTML 确实不用关注样式了,但是我的 CSS 却不得不非常关心 HTML 的结构。

也许是我没有成功地彻底分离关注点吗?

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

为这种耦合寻找解决方案花了不少时间,许多人的建议都是向 HTML 中添加更多的类,这样我就可以直接定义它们,同时,尽可能降低 CSS 选择器权重以降低 CSS 对于 DOM 结构的依赖。

最提倡这种想法的方法是 BEM(Block Element Modifer),按照 BEM 的书写方式,之前的作者信息卡片的 HTML 可以变成:

<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;
}

这确实带来了巨大的进步,我的 HTML 依然保持 “语义化” 并且不包含任何样式信息,同时 CSS 也不必过于关注 DOM 结构了,还降低了不必要的 CSS 选择器权重的使用!

但我很快又左右为难了 😥

处理相似的组件

假设我要为网站添加一个新功能:用卡片布局展示文章预览。文章的预览卡片顶部有一个完整出血的图像,下面有一个填充内容部分,一个粗体标题和一些较小字号的正文。其实跟作者信息卡片看起来差不多:

在遵循关注点分离的原则下如何处理这种问题呢?显然,我们不能直接将 .author-bio 套用到文章预览上,这不符合语义化。我们需要单独定义一个 .article-preview 的类来表示这个新的组件。完成后, HTML 看起来如下:

<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 呢?

第一种方式:Ctrl + C & Ctrl + V

最直接的实现方式就是复制 .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;
}

这种方式非常不优雅,也会带来另外的隐患,即这些组件之间很容易产生表现不一致的情况,哪怕只是稍微改动一点间距或者字体颜色。

第二种方式:拓展作者信息卡片组件

另外一种方式是使用预处理器的 @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 的简单复制,HTML 也免于掺杂样式信息。

此外,我们其实还有另外一种选择 😁

第三种方式:创建一个内容无涉的组件

从 “语义化” 角度考虑,我们的 .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;
}

现在作者信息卡片的 HTML 看起来是这样的:

<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>

文章预览卡片的 HTML 是这样的:

<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 样式表中为它创建新的样式类,然后通过复制或 @extend 抑或混合起来复用共通的样式。假如使用我们内容无涉的 .media-card 样式类,我们只需要编写新的 HTML,然后将类写上去就好了,甚至都不需要打开样式表文件 ┑( ̄Д  ̄)┍ 如果我们真的混合了关注点,我们不应该忙于修改多处文件吗 🧐

“关注点分离” 是一个稻草人

当你基于 “关注点分离” 的原则去考虑 HTML 和 CSS 的关系时会发现,它们是非黑即白的。你要么遵循了关注点分离,要么没有遵循,我相信这不是正确的权衡方式。

或者我们应该考虑一下 “依赖方向”,归纳一下,我们写 HTML 和 CSS 的时候有两种方式:

  1. CSS 依赖 HTML,基于内容定义类名,将 HTML 作为 CSS 的依赖。HTML 是独立的,它毫不关心自己长什么样子,它只是开放了一个可用来定义样式的类。CSS 必须知道 HTML 暴露的类名是什么,然后根据这些类名来定义 HTML 的样式。在这种模式中, HTML 是很方便修改样式的,但 CSS 很难重用!

  2. HTML 依赖 CSS,以一种内容无涉的方式命名样式类,将 CSS 作为 HTML 的依赖。CSS 是独立的,它完全不用关心有哪些内容将会使用它,它只是开放一系列内置的样式块,你可以将它们应用到 HTML 标记中。HTML 需要关心 CSS 开放了那些类给它,它如何将这些类组合起来以达到预期的效果。在这种模式中,CSS 是可复用的,但 HTML 修改样式不那么方便了。

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

两种方式都不能说是错误的,它体现的是一种抉择而已,你需要考虑在你的应用情境中,哪一种更加重要。在你参与的项目中,什么更有价值呢?是便捷地重设计 HTML 样式吗?还是 CSS 样式的复用呢?

选择复用性

Nicolas Gallagher 写的 『关于 HTML 语义化和前端架构』 对我来说是一个转折点。这里我不打算复述他的观点,感兴趣的同学可以去看。对我而言,从这篇文章中,我更加确信了优化可复用的 CSS 是我目前做的各种项目的最佳选择。

阶段三:内容无涉的 CSS 组件

在这一点上,我的目标是尽可能避免基于内容创建样式类,而不是一味追求样式的最大化复用。现在类名看起来是这样子的:

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

我注意到,当我专注于创建可复用的样式类时,一个组件做的事情越多,或者说一个组件越明确、具体,它就越难复用

The more a component does, or the more specific a component is, the harder it is to reuse.

这里有一个直观的例子,假设我们要构建一个表单,它有很多个部分,最后是一个提交按钮。如果我们把所有的部分都视为 .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__buttom 类显然没有多大意义,因为它们不是表单的一部分。不过,既然这些按钮在各自的页面中都承担着主要的操作,我们是否可以尝试按照这一共同特性将它命名为 .btn--primary 呢,尝试如下:

  <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>

然后,我们想让这个表单看起来像是在一个浮动卡片中。第一种实现方式是创建一个修饰符,将它运用在表单上:

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

但如果我们已经有一个 .card 样式类的话,为什么我们不将它们组合使用呢?

+ <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;
  }
}

使用 .actions-list 可以完全替换 .stacked-form__footer.header-bar__actions

<!-- 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,另一个使用 .action-list--right

阶段四:内容无涉的组件和工具类

不停提取可复用组件和添加修饰符实现组件之间微小差异性的方式让人筋疲力竭,当我们使用修饰符的时候,相当于仅仅是为了修改一个 CSS 属性就创建了一个全新的组件。

组件会有更多个,如果它们也需要区分左对齐和右对齐呢,要创建同样的修饰符吗?我们再次陷入到了提取相同特性的困境中,就像我们当初从 .stacked-form__footer.header-bar__actions 中提取出 .actions-list 一样。

显然,我们更喜欢组合而不是无谓的重复。考虑一下,我们应该如何从组合的思想出发,解决行为列表不同对齐方式的实现呢?

对齐工具类

我们需要创建一个可复用的类,让它可以为组件提供期望的效果。这些类完全可以叫做 .align-left.align-right,就像我们之前创建修饰符一样,它们的样式定义如下:

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

现在可以通过组合来实现列表中按钮的不同对齐方式了!

<!-- our stacked form buttons left-aligned -->
<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>
<!-- ...and our header buttons right-aligned -->
<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 更加 “语义化”,它们都是根据它们如何影响 HTML 的表示而命名的,并且我们也确实使用它们实现特定的样式表现需求。

这就是 HTML 依赖 CSS,如果我们想将表单从 .stacked-form 改为 .horizontal-form,需要修改的地方是 HTML,而不是 CSS。

删除无用的抽象

有趣的是,我们先前创建的 .actions-list 组件已经基本没用了,它之前提供的功能就是将其中的内容右对齐。现在完全可以把它删掉:

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

但删掉之后,.actions-list__item 成为了孤儿类,显得很奇怪。是否有更好的方式可以解决我们创建它们所有解决的问题呢?

回过头来想,创建它们的目的就是为了在两个按钮之间添加一点间距。.actions-list 在当时看来确实是不错的抽象,具备一定的通用性且复用性良好,但说实话,如果其它情境中也需要实现类似的功能呢,即在列表项之间添加间距,而这些列表项可能不是 "actions"。

要让它具备更进一步的复用性,或许可以叫做 .spaced-horizontal-list? 但我们已经把这个组件删除了,实际上只有它的后代需要样式。

间距类

如果只有后代需要添加样式的话,或许直接为它们指定样式会比通过按组使用伪类添加更加简单直接。在两个元素之间添加间距复用性最高的表述应该是 “该元素附近应该有一些间距”,这将是我们要创建的类的特征。

我们已经创建过 .align-left.align-right 这样的类了,或许我们也可以创建只添加右边距的功能类 🤔

操作起来,创建一个 .mar-r-sm,它可以在一个目标右边添加一些边距:

- .actions-list__item {
-   margin-right: 1rem;
-   &:last-child {
-     margin-right: 0;
-   }
- }
+ .mar-r-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 mar-r-sm">Cancel</button>
    <button class="btn btn--primary">Save</button>
  </div>
</header>

.actions-list 现在已经无影无踪了,我们的 CSS 文件也更小了,创建的类也具有了前所未有的高复用性。

阶段五:工具类优先的 CSS

Get 到这个点不久之后,我便创建出一整套的常用工具类,用以解决常见的视觉呈现需求:

  • Text sizes, colors, and weights
  • Border colors, widths, and positions
  • Background colors
  • Flexbox utilities
  • Padding and margin helpers

令人惊奇的是,在不了解它的情况下,您可以构建全新的 UI 组件,而无需编写任何新的 CSS。比如这个 “产品卡片” 组件:

它的 HTML 是这样的:

<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 { ... }

太荒谬了 😂 按照之前讨论过的内容,我们可能希望它由一些较小的组件组成。这些组件会是什么样子呢?

  1. 它可能寄宿在一个卡片之上。并不是所有的卡片都具有阴影,所以我们需要通过修饰符实现(.card--shadowed),或者创建一个 .shadow 的工具类,它可以应用在任何一个目标上,显然后一种方式复用性更强。
  2. 同样的,并不是所有的卡片都是圆角的,所以我们需要区分开来,使用修饰符或者工具类,显然后一种更优雅,即定义一个 rounded 工具类。
  3. 上方的图片呢?使用 .img--fitted 让它撑满卡片吗?网站中或许还有其它地方需要撑满父元素,它们可能并不是图片,这样的话,将它叫做 .fit 会更好一些!
  4. ……你可以看出来我在干什么。

如果你对可复用性的关注度足够高,将组件类逐步拆分为工具类就是很自然的选择。

强制一致性

使用小的、可组合的工具类的一个好处是,团队中的成员可以从一个固定的集合中取值。

回想一下你平时会如何将样式的改进意见传递给你的同事,“这段文本需要更暗一点”,“这里的字号需要更小一点”……看起来像是正确的方式,因为你使用了相对颜色或者相对字号,而不是随意指定的值。

但经常发生的情况是,你想让某个组件的文本暗 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 的大小就不会随项目增长而继续膨胀,而我们也就获得了天然的一致性。

你仍然需要创建组件

我的立场和一些真正固执的 “Functional CSS” 的倡导者还有区别,我并不认为所有的东西都只应该使用工具类构建。

如果你查看一些流行的基于工具类的框架,像是 Tachyons(这是一个相当优秀的项目),它们的按钮都是用最基本的工具类实现的:

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

让我们来分解一下上面的示例:

  • f6: 在字体大小梯度中使用第六种(在 Tachyons 中为 .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 样式类要比将其抽象为一个单独的组件方便地多。

但是优先使用工具类构建它们

之所以将这种方式称作工具类优先的 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>

并没有什么东西是值得提取的。

这不就是内联样式吗?

这种方式很容易被理解为与内联样式无异,因为我们在做的事情就像是直接将需要的属性添加到标记中,但在我的实践中,二者是完全不同的。

在内联样式中,属性的取值没有任何的限制。一个标签的字号可能是 font-size: 14px,另一个有可能是 font-size: 13px,还有 font-size: .9emfont-size: .85rem……这和为每一个新组件书写新的 CSS 一样,面临着从零开始的 “空白画布” 问题。

而组件类强制你进行选择:字号是 text-sm 还是 text-xs?该使用 py-3 还是 py-4?想要 text-dark-soft 还是 text-dark-faint?你必须从一个预设列表中选择其中一项,而不是信手拈来一个任意值。你最终只会有 10 - 12 种颜色,而不是 380 种。

我的经验是,基于工具类优先的方式比基于组件优先的方式更容易维护一致性程度高的设计体系,虽然它一开始并不那么直观。

从哪里开始

如果你对这种方式感兴趣,以下是一些值得尝试的框架:

最近,我也在准备我的开源 PostCSS 框架,它叫做 Tailwind CSS,就是围绕工具类优先的想法设计的,只在必要的时候提取重复模式。

如果你对它感兴趣,请前往 Tailwind CSS 的官网,并上手尝试一下!

cmlanche 将本帖设为了精华贴。 01月05日 17:50
cmlanche 前端 CSS 框架:Tailwind CSS,一个工具集 CSS 框架 提及了此话题。 01月05日 17:50
需要 登录 后方可回复, 如果你还没有账号请 注册新账号