CSS 就像 HTML 的一面镜子一样,嵌套的 CSS 选择器完美地映射了 HTML 结构。
原作者: Adam Wathan 发表于 2017年8月7日
原文链接:https://adamwathan.me/css-utility-classes-and-separation-of-concerns/(opens new window)
在过去的几年里,我编写 CSS 的方式已经从一种非常“语义”的方法过渡到更像通常所说的“功能性 CSS”。
以这种方式编写 CSS 会引起很多开发人员内心直接的抵触,所以我想解释一下自己是如何走到这一步的,并分享在这一过程中所获得的一些经验和见解。
第一阶段:“语义”CSS
当试图学习如何做好 CSS 时,你会听到的最佳实践之一就是“关注点分离”。
这个想法是 HTML 应该只包含关于内容的信息,而所有的样式决定都应该在 CSS 中进行。
看看这个 HTML:
<p class="text-center">
Hello there!
</p>
看到那个 .text-center 类了吗?将文本居中是一个设计决定,所以这段代码违反了“关注点分离”,因为我们让样式信息混入到了 HTML 中。
相反,推荐的做法是根据元素的内容给它们起类名,并在 CSS 中使用这些类作为钩子来设计 HTML 的样式。
<style>
.greeting {
text-align: center;
}
</style>
<p class="greeting">
Hello there!
</p>
这种方法的典型例子就是 CSS Zen Garden(opens new window),这个网站旨在表明,如果你“分开你的关注点”, 只需更换样式表,你就可以完全重新设计一个网站。
而我的工作流程看起来是这样的:
先写下我需要的一些新 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>
根据内容添加一两个描述类:
- <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>
在 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;
}
}
}
下面是最终结果的演示:
点击查看【codepen】
这种方法对我来说很有意义,有一段时间,我就是这样写 HTML 和 CSS 的。
不过最后,我开始觉得有些不对劲。
我已经“分离了我的关注点”,但 CSS 和 HTML 之间仍然存在着非常明显的耦合。大多数时候,CSS 就像 HTML 的一面镜子一样,嵌套的 CSS 选择器完美地映射了 HTML 结构。
HTML 并不关心样式决定,但 CSS 却非常关心 HTML 结构。
也许我的关注点并没有那么泾渭分明。
第二阶段:将样式从结构中解耦
在四处寻找这种耦合问题的解决方案之后,我开始发现越来越多的建议,即在你的 HTML 中添加更多的类,这样就可以直接针对它们,保持选择器的低特异性,并使 CSS 不那么依赖于特定 DOM 结构。
最著名的方法论就是 Block Element Modifer(opens new window),或者简称 BEM。
采用类似 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;
}
点击查看【codepen】
这对我来说是一个巨大的进步,我的 HTML 仍然是“语义的”,而且不包含任何样式决定,现在我的 CSS 感觉与我的 HTML 结构解耦了,而且还能避免不必要的选择器特异性。
但后来我遇到了一个难题。
处理类似的组件
假设需要给网站增加一个新的功能:以卡片的形式显示文章的预览,
假设文章的预览卡顶部有一个完整的出血图像,下面有一个填充的内容部分,一个粗体标题和一些较小的正文,
假设让它尽可能看起来像一个作者的简历:
如何处理这些问题,同时又能分离我们的关注点呢?
我们不能把 .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>
方案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 作者简历组件
.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;
}
点击查看【codepen】
一般情况下,根本不建议使用 @extend,但是除此之外,这似乎可以解决我们的问题,对吗?
我们已经删除了 CSS 中的重复内容,我们的 HTML 仍然不受样式决定的影响。
“关注点分离”是个稻草人
当你从“关注点分离”的角度来思考 HTML 和 CSS 之间的关系时,它们是黑白分明的。
你要么有关注点分离(好!),要么没有(坏!)。
这不是思考 HTML 和 CSS 的正确方式。
相反,要考虑依赖的方向。
有两种方法可以编写 HTML 和 CSS。
1. ~“关注点分离”~
依赖于 HTML 的 CSS。
根据你的内容来命名你的类(比如.author-bio),将你的 HTML 视为你的 CSS 的依赖。
HTML 是独立的,它并不关心你如何让它看起来,它只是暴露了 HTML 所控制的钩子,比如 .author-bio。
另一方面,你的 CSS 不是独立的,它需要知道你的 HTML 决定暴露哪些类,并需要针对这些类来为 HTML 设计样式。
在这个模型中,你的 HTML 可以重新样式化,但是你的 CSS 是不可重用的。
2. ~“混合关注”~
依赖于 CSS 的 HTML。
以内容无关的方式将类命名为你的 UI 中的重复模式(比如 .media-card ),将 CSS 视为你的 HTML 的依赖。
CSS 是独立的,它并不关心被应用于什么内容,而只是暴露了一组可以应用于 HTML 的构件。
HTML 不是独立的,它使用的是 CSS 提供的类,它需要知道有哪些类存在,以便根据需要将它们组合起来,以实现所需的设计。
在这种模式下,CSS 可以重用,但 HTML 不能重新设计。
CSS Zen Garden 采用的是第一种方法,而像 Bootstrap或 Bulma这样的 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
…等等
当我开始关注创建可重用的类时,也注意到了另外一件事。
一个组件做的事情越多,或者一个组件越具体,就越难重用。
这里有一个很直观的例子。
假设我们正在构建一个表单,有几个表单部分,底部有一个提交按钮。
如果我们把所有的表单内容都看作是 .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>
现在假设我们想让这个堆叠的表单看起来像在一个浮动的卡片中。
一种方法是创建一个修饰符并将其应用到这个表单中。
- <form class="stacked-form" action="#">
+ <form class="stacked-form stacked-form--card" action="#">
<!-- ... -->
</form>
但是如果我们已经有了一个 .card 类,为什么不使用现有的 card 和 stacked form 来组成这个新的 UI 呢?
+ <div class="card">
<form class="stacked-form" action="#">
<!-- ... -->
</form>
+ </div>
通过采取这种方法,我们有一个 .card,可以成为任何内容的家,还有一个不经意的 .stacked-form,可以在任何容器里面使用。
我们从组件中得到了更多的重用,而且我们不必编写任何新的 CSS。