过去几年,我编写 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 就是一个经典的例子,它展示了如果你 “分离关注点” ,只需更换样式表就能完全重设计一个网站。
我的工作流程大致如下:
- 为新的 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 中使用这些类进行样式化:
.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 结构。
我的 HTML 并不关心样式决策,但我的 CSS 非常关心我的 HTML 结构。
也许我的关注点并没有那么分开。
第二阶段:解耦样式和结构
在寻找解决这种耦合的方法时,我开始越来越多地看到建议,即在 HTML 中添加更多的类,以便直接针对它们进行定位,降低选择器的特定性,并使 CSS 更少依赖于特定的 DOM 结构。
最知名的倡导这种理念的方法是块级元素修饰符(Block Element Modifier,简称 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;
}
对我来说,这是一个巨大的改进。我的 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>
但是,我们应该如何处理 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
扩展组件
另一种方法是利用预处理器(如 Sass 或 Less)的 @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 仍然没有包含样式决策。
但让我们再考虑一个选项……
方案 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 组件,并通过复制或使用@extend 或混入(mixin)来应用共享样式。
而使用与内容无关的 .media-card
类,我们只需要编写新的 HTML
;根本无需打开样式表。
如果我们真的在 “混合关注点”,难道不应该需要在多个地方做出更改吗?
"关注点分离"是一个错误的靶子
当你从"关注点分离"的角度考虑 HTML 和 CSS 之间的关系时,事情似乎非黑即白。
你要么实现了关注点的分离(好!),要么就没有(不好!)。
但这并不是思考 HTML 和 CSS 之间关系的正确方式。
相反,应该考虑依赖方向。
编写 HTML 和 CSS 有两种方式:
- “关注点分离” 基于 HTML 的 CSS。
根据内容命名类(如.author-bio)将你的 HTML 视为 CSS 的依赖项。
HTML 是独立的;它不在乎你如何使其呈现外观,它只是暴露由 HTML 控制的“钩子”如.author-bio。
另一方面,你的 CSS 并不独立;它需要知道 HTML 决定暴露哪些类,并需要针对这些类来样式化 HTML。
在这种模型中,你的 HTML 是可以重新样式的,但你的 CSS 不具备可重用性。
- “混合关注点” 基于 CSS 的 HTML。
以 UI 中的重复模式为依据,以与内容无关的方式命名类(如 .media-card
),将你的 CSS 视为 HTML 的依赖项。
CSS 是独立的;它不在乎被应用于什么内容,它只是提供了一套构建块,你可以将它们应用到你的标记中。
而你的 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
类,为什么不利用已有的卡片和堆叠表单来构建新的 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__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>
但如果有两个动作列表,一个需要左对齐,另一个需要右对齐,我们如何使用组合来解决这个问题?
阶段 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
一样,它们都根据如何影响标记的呈现命名,我们在 HTML 中使用这些类是为了达到特定的呈现效果。
我们正在编写依赖 CSS 的 HTML。如果想将表单从 .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
,.actions-list__item
就显得有点奇怪。有没有办法在不创建 .actions-list__item
组件的情况下解决原始问题?
回想一下,创建这个组件的原因是在两个按钮之间添加一点间距。.actions-list
作为按钮列表的相当不错的比喻,因为它通用且可复用,但肯定存在需要相同间距但不是“动作”的情况吧?也许一个更通用的名字是 .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;
+ }
现在,我们的表单和头部看起来像这样:
<!-- 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
一旦我理解了这一点,不久我就建立了一整套我所需的常见视觉调整的辅助类,比如:
- 字体大小、颜色和粗细
- 边框颜色、宽度和位置
- 背景颜色
- Flexbox 工具
- 填充和外边距辅助
令人惊奇的是,很快你就可以构建全新的 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 { ... }
当然不行,那太荒谬了。相反,我们可能会像之前讨论的那样,将其分解为更小的组件。
这些组件可能是什么样子的呢?
嗯,它可能被封装在卡片中。但并不是所有的卡片都有阴影,所以我们可能有 .card--shadowed
修饰符,或者我们可以创建一个 .shadow
辅助类,可以应用于任何元素,这样听起来更通用,那就这么做吧。
至此,我已经分享了从内容驱动的类名到以辅助类为主的 CSS 组件设计的过程,通过不断优化和组合,我们不仅提高了代码的复用性,也使得 HTML 和 CSS 更加清晰易读。
原来我们网站上的一些卡片并没有圆角,但这个有。我们可以将其标记为 .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 Text
哇哦。 让我分解一下这个:
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
类,将这 7 个辅助工具打包在一起,比承诺为网站上的每一个小部件都进行模板化要简单得多。
...但先用辅助工具构建
我将我采取的 CSS 方法称为 首先使用辅助工具 的原因是我试图尽可能地用辅助工具构建一切,并且 只有在重复模式出现时才提取。
如果你使用 Less 作为预处理器,你可以使用现有的类作为混合器。这意味着创建 .btn-purple
组件只需在编辑器中使用多光标魔法即可:
不幸的是,在 Sass 或 Stylus 中,你不能直接这样做,而不为每个辅助工具类创建单独的混合器,所以那里的工作量会更多一些。
当然,不是每个组件中的每个声明都必须来自一个辅助工具。仅用辅助工具处理元素之间的复杂交互,如当鼠标悬停在父元素上时改变子元素的属性,是困难的,所以根据情况判断,做你觉得更简单的事。
再无过早抽象
采用组件优先的 CSS 方法意味着即使某些东西永远不会被重用,你也为其创建组件。这种过早的抽象是样式表中大量冗余和复杂性的来源。
以导航栏为例。在你的应用程序中,主导航的 HTML 标记有多频繁需要重写?
在我的项目中,我通常只在主布局文件中这样做一次。
如果你先用辅助工具构建,只有在看到令人担忧的重复时才提取组件,你可能永远不需要提取一个导航栏组件。
相反,你的导航栏可能看起来像这样:
<nav class="bg-brand py-4 flex-spaced">
<div><!-- Logo goes here --></div>
<div>
<!-- Menu items go here -->
</div>
</nav>
里面没有任何值得提取的内容。
这不就是内联样式吗?
很容易看一眼这个方法,认为它就像在 HTML 元素上添加 style 标签,然后添加所需的属性,但实际上,根据我的经验,这是非常不同的。
使用内联样式,你对选择的值没有任何限制。
一个可能是 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 种。
我发现,首先用辅助工具构建东西,通常会导致比组件优先的工作方式下设计出更一致的外观,尽管乍听起来可能不太直观。
从哪里开始
如果你对这种方法感兴趣,这里有一些值得一看的框架:
- Tachyons
- Basscss
- Beard
- turretcss
最近,我也发布了自己免费开源的 PostCSS 框架 Tailwind CSS,它围绕着首先使用辅助工具和从重复模式中提取组件的理念设计。
如果你有兴趣,可以访问 Tailwind CSS 网站并试一试。
Comments