「译」CSS语义化是怎么往原子化进化的?

2,465 阅读26分钟
原文链接: www.zcfy.cc

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>

这种方法的典范是禅意花园。如果使用“关注点分离”原则,那么想要改变一个网站的样式只需要更换样式表就可以了。

我写码的过程大致如下:

  1. 根据设计稿把需要的标签写好(这里拿一个作者简介卡作为例子):
    <div>
      <img src="http://p0.qhimg.com/t01e2c15da7b6fa9aba.jpg" 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="http://p0.qhimg.com/t01e2c15da7b6fa9aba.jpg" 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;
        }
      }
    }

下面是结果演示:

See the Pen "Semantic" mapping layer (terrible idea!) by Adam Wathan (@adamwathan) on CodePen.

我认为这很有道理,所以很长一段时间都是这么写HTML和CSS的。

但后来,我感觉有点儿不对劲。

虽然我将“关注点分离”了,但HTML和CSS还是有很明显的耦合。大多数时候我的CSS看起来就像是HTML标签的镜子,嵌套的CSS选择器将HTML结构完全映射出来了。

我的标签确实与样式分离了,但我的CSS却与HTML结构有很强的联系。

也许是我的分离做的不够彻底。

第 2 阶段: 让样式与结构解耦

在寻找解决办法的过程中,我发现大家更倾向于给标签添加更多的类名,这样定义起来就会更直观。能够保持较低的优先级,并使CSS更少的依赖DOM结构。

倡导这种思想并且最广为人知的技术是 Block Element Modifer, 简称 BEM.

用类似BEM的方法,我们的作者简介标签看上去就会是这样:

<div class="author-bio">
  <img class="author-bio__image" src="http://p0.qhimg.com/t01e2c15da7b6fa9aba.jpg" 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;
}

View on CodePen

对我来说这已经是很大的进步了。我的标签看上去仍然很“语义化”且与样式分离,而且现在CSS也与标签结构解耦合了。额外的好处是避免了不必要的选择器嵌套,保持了低优先级。

但是很快我又陷入了两难境地。

处理相似的组件

话说我需要给网站加个新的内容:用一个卡片布局来展示文章预览。

话说这个文章预览卡片的头部是一张照片,下边是内容部分,包括一个粗体标题和一些较小的正文文本。

话说它看起来跟作者简介非常像。

如果用“关注点分离”的原则,如何处理最好呢?

我们不能将 .author-bio的类名用在文章预览卡中,因为这不符合语义化原则。所以我们肯定要用 .article-preview来给组件命名。

下边将是标签的样子:

<div class="article-preview">
  <img class="article-preview__image" src="http://p0.qhimg.com/t01f3a22fad3306d5cf.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;
}

这样没问题,但很明显不够简洁。如果这个组件在样式上与.author-bio只有稍微的不一样(可能是不同的填充或字体颜色),改起来也会非常容易。

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

View on CodePen

一般不建议使用 @extend属性。但撇开这件事, 这样可以解决我们的问题对吗?

我们移除了CSS中重复的部分,并且标签与样式仍然是分离的。

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

Option 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="http://p0.qhimg.com/t01e2c15da7b6fa9aba.jpg" 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="http://p0.qhimg.com/t01f3a22fad3306d5cf.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,但现在是不是“结构与样式混合”了?

我们希望两个文本片段都使用.media-card做样式。那如果我们想改变作者简介卡的样式,而不改变文章预览的,那该怎么办呢?

之前,我们只需要打开样式表,给需要改变的组件随便添加样式就可以了。但现在我们需要编辑HTML!这很糟!

但让我们先花几分钟换个角度想一想。

如果我们要添加一个有着相同样式的新内容,该怎么办呢?

如果用“语义化”方法,我们需要写一段新的HTML,添加一些根据内容起的类名,打开样式表,为新的内容添加一个新的CSS样式组件,并通过复制、@extend、mixin来借用已经存在的样式。

如果用与内容无关的 .media-card 类名, 我们所需要做的只是写一段新的HTML,不需要修改样式表。

如果我们真的将“结构与样式混合”了,那么无论HTML还是CSS,不都得修改吗? (译者:作者想表达的是,其实结构与样式还是分离的。)

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

当你用"关注点分离"的原则来思考HTML和CSS的关系时,就会是非黑即白的。

分离了(好!)或者没分离(糟糕!)。

这并不是思考HTML与CSS关系的正确方式。

相反, 要从依赖的角度来思考

有两种编写HTML和CSS方式:

  1. "关注点分离" CSS依赖HTML。

根据内容给类起名(例如 .author-bio),将CSS视为HTML的附属品。

HTML是独立的。他并不在乎你是怎么定义它的,它只暴露像.author-bio这样的钩子。

另一方面,你的CSS并不是独立的。他需要知道你的HTML暴露了哪些类名,然后通过这些类名给HTML添加样式。

在这个模型中,你可以任意编写HTML,但CSS却不能被复用。

  1. "结构与样式混合" HTML依赖CSS

根据设计稿提炼出样式相同的部分,并用与内容无关的名字作为类名,就是将HTML作为CSS的附属品。

CSS是独立的。它并不关注自己被应用的地方内容是什么。他只是提供一系列代码块,哪个标签需要就添加在哪。

HTML不是独立的。它需要使用由CSS提供的类, 它需要知道有哪些类是定义好的,并且将这些类结合起来,来完成跟设计稿一样的样式。

在这个模型中,你的CSS是可复用的,但你的HTML不能随意编写。

CSS禅意花园采用第一种方式,而UI框架如BootstrapBulma 则用的第二种方式。

两种方法本质上都不是“错误的”。只是你要弄清楚你所在的语境中,什么才是最重要的。基于这点,再决定你要用哪种方法。

对于你正在编写的项目, 什么会更有价值: 可以随意编写的HTML,还是可复用的CSS?

选择可重用性

当我阅读了Nicolas Gallagher的 关于HTML语义化和前端架构一文后,我改变了自己的观点。

我不会在这里重申他的所有观点。但毫无疑问,这个博客给了我深刻印象。我完全相信,对于我所编写的各种项目来说,选择可复用的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 类名就不太合适了,它不是.stacked-form组件的一部分。

这两个按钮在各自的页面上都是主要行为,那么如果我们根据组件的共同特征来给按钮命名的话,就是.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>

话说现在我们想让.stacked-form组件跟前文中的作者简介、文章预览一样,使用卡片样式的布局。

一种方法是创建一个修饰符并将其应用于此表单:

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

但是如果我们已经有了.card类,那我们为何不用已经存在的.card.stacked-form组合起来,实现设计稿的样式呢?

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

那如果我们在子导航或者header中遇到相同的问题时,怎么办?

我们不能在.stacked-form组件之外复用.stacked-form__footer, 因此也许我们会在header里新建一个新的子组件:

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

...但现在我们不得不把已经写好的.stacked-form__footer类,复制到新的.header-bar__actions子组件中。

这非常像刚开始我们根据内容来给类命名时遇到的问题,对不对?

解决这个问题的一个方法是,想出一个全新的组件,这个组件易于复用,并且易于组合。

例如我们可以给它起名 .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支持左对齐,而另一个支持右对齐,我们应该怎么办呢?难道要用.actions-list--left.actions-list--right 来作修饰吗?

第4阶段: 内容无关组件 + 通用类

给这些组件起名字总是很耗费时间。

当你创建例如.actions-list--left这样的修饰类名时, 你完全就是在创建一个新的组件,这个组件只是为了适配一个CSS属性。 因为类名里已经有了left,因此你不可能欺骗任何人说这是“语义化”的(译者:作者的意思是结构与样式在这里是混合的)。

如果我们有一个组件需要使用left-align和right-align来修饰,那我们用这个当做新的组件类名来用,合适吗?

这个就回到了当初我们面临的一个相同问题,那个问题我们是用.actions-list子组件替代 .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标签里的按钮右对齐:

<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"会使你感到不舒服,请记住,我们已经不是在使用“关注点分离”方法给类命名了,我们是根据设计稿提炼出通用的部分来给类命名的。所有类名中带有样式的描述是很自然的。

不要在假装.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的通用类, 那我们再加一个只能添加右边距的新通用类怎么样?

让我们来创建一个通用类,例如 .mar-r-sm。功能是在一个元素的右边添加一段距离的间距:

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

下面是我们的表单和header现在的样子:

<!-- 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量更小, 类更利于复用。

第5阶段: 实用性第一的 CSS

在达到这个阶段的时候,我刚刚建立了一整套实用类,用于常见的视觉微调,例如:

  • 文字的 大小、颜色、粗细

  • 边框的 颜色、宽度、位置

  • 背景的 颜色

  • Flexbox 通用类

  • Padding 和 margin 的助手

你甚至可以在不写新的CSS的情况下,创建一个全新的UI组件,这真是件令人兴奋的事情。

从我的一个项目中来看下这个“产品卡”组件:

以下是我的标记:

<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容器里。 并不是所有卡片都有阴影,所以我们可以用.card--shadowed来修饰作为类名。或者我们可以创建一个新的通用类 .shadow 。这个类可以用在任何元素上。这样看起来更加利于复用,那我们就这么做吧!

我们的网站大部分卡片样式没有圆角,但是这个有。我们可以给他起名.card--rounded。但我们的网站会有一些别的元素也有相同幅度的圆角样式,但他们不是卡片。一个 .rounded 通用类的复用率也许更高。

那位于上边的图片呢?因为它要保持与父元素宽度一样,所以给它起名叫.img--fitted合适吗? 这个网站上有些元素也是需要保持与父元素宽度一样的,但并不一定就是个图片。 那么仅仅起名为 .fit 也许会更合适。

...你可以看出我们最终想要的效果。

如果您将足够的重点放在可复用性上,那么就会很自然的做到用可复用的通用类来创建组件。

加强一致性

使用较小的,可组合的通用类的最大好处是,你团队的所有开发者可以从一个固定的列表里面选择类名。

想想有多少次你写样式的时候心里想着“这个文本需要更深一点的颜色”,然后用darken() 函数去微调变量 $text-color的值?

又或者, "这个字号应该再小一点," 然后给你正在开发的组件添加了 font-size: .85em

看上去你做的很“对”,因为你使用了相对的颜色和相对的字号,而不是固定的值。

但如果你想在组件中把字色调深10%,而其他人则想调深12%,那该怎么办?当你回过神来的时候,发现你的样式表中 有380种独特的文字颜色.

只要你正在写新的CSS,这种情况在每个代码库中都会发生:

  • GitLab: 380个文本颜色,202个背景颜色,58个字体大小

  • Buffer: 124个文本颜色,86个背景颜色,54个字体大小

  • HelpScout: 198个文字颜色,133个背景颜色,67个字体大小

  • Gumroad: 91个文字颜色,28个背景颜色,48个字体大小

  • Stripe: 189个文字颜色,90个背景颜色,35个字体大小

  • GitHub: 157个文字颜色,155个背景颜色,56个字体大小

  • ConvertKit: 124个文本颜色,123个背景颜色,69个字体大小

这是因为你要写的每个CSS代码块都可以视为一个空白的画布,你可以不受限制的随意使用任何值。

你可以通过声明变量和使用mixins加强代码的一致性。但 每一行新的CSS仍然有可能会使样式变得更复杂,添加更多的CSS是永远不会使CSS更简单的。

相反,用已经存在的CSS类名来给HTML添加样式,用这种解决方式的话,空白画布的问题就不存在了。

想要颜色更深的字色吗?添加 .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: 使用字体大小中的第6个字号 ( 在 Tachyons 是.875rem)

  • br3: 使用圆角弧度中的第3号(.5rem)

  • ph3: 使用上下padding中的第3号(1rem)

  • pv2: 使用左右padding中的第2号(.5rem)

  • white: 使用白色字色

  • bg-purple: 使用字色背景色

  • hover-bg-light-purple: 鼠标悬浮时使用浅紫色背景作为高亮

如果你需要多个有相同类的按钮(译者:这个例子里的标签由多个类名组合修饰。),Tachyons 建议的方法是创建一个抽象的HTML模版,而不是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组件比 创建一个基于HMTL模版的组件更加实用

在我工作的这些项目中,会将这7个通用类组合起来,创建一个新的 .btn-purple类。这要比给标签一个一个添加颗粒度很小的类名要简单的多。(译者:一旦其中一个类名不需要了,每个用到这个组合类的标签都要被修改,而作者的方法只要改新添加的 .btn-purple类就可以了,不需要改HTML)。

...但是先要用通用类构建样式

我的方法需要先使用通用类给标签添加样式的原因是,我希望使用通用类来构建网站样式(译者:言外之意,不添加任何新样式)。而且只有当他们出现时才会精确的复用重复的部分

如果你用 Less 作为你的预处理器,你可以将现有的类用minxins混合。那意味着你只需要利用编辑器的多光标功能就可以创建 .btn-purple 组件了:

但如果你用的是Sass或者其他没有这种可以将多个通用类进行混合而生成新类的方法的话,就会稍微麻烦一些,你需要做一些别的工作,才能支持。

当然并不是每个组件类都可以通过多个通用类组合而来。元素间复杂的交互是很难只是用通用类来解决的。例如当鼠标悬浮在父元素时希望子元素改变属性时。所以你要经过思考,选择你认为最简单的方法。

不要提前抽象

使用“组件优先”的方法写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-3py-4 吗?

我是想用 text-dark-soft 还是 text-dark-faint?

你不能随意使用你想用的值,而是只能从一个策划好的列表里选择。

你最终只用了10个或者12个文字颜色,而不是380个。

我的经验是先创建通用类,再创建组件类,可以使设计保持一致性,刚开始时这听起来可能不太直观。

如何开始

如果你对这个方法感兴趣,下边是一些值得参考的框架,不妨一试:

我在开发一个免费的开源LESS框架,叫做 Tailwind CSS 这个框架是围绕本文的主旨开发的,即先创建通用类,然后从重复的部分中提取可复用的组件: