谈谈原子类
There are only two hard things in Computer Science: cache invalidation and naming things. – Phil Karlton
从定义开始
Atomic CSS is the approach to CSS architecture that favors small, single-purpose classes with names based on visual function.
原子化 CSS 是一种让 CSS 架构偏好更细粒度、更单一职责、按视觉命名类的方法论
原子类(Atomic CSS)这个词,由 Thierry Koblenz 在文章 Challenging CSS Best Practices 中首次提出和定义。按照定义可以知道,原子类是一种以命名(Naming)为手段,构建 CSS 架构的方法论。
一个原子类方法定义的类名有这些特点:
- 细粒度
- 单一职责
- 按视觉效果命名
实践的例子
一个最简单的例子:
.bgr-blue {
background-color: #357edd;
}
.bgr-blue
类只有一个目的:把背景色变为蓝色,其命名也按照视觉上的作用,所以是一个原子类。
基本思想
Atomic CSS offers a straightforward, obvious, and simple methodology. Classes are immutable – they don’t change. This makes the application of CSS predictable and reliable as classes will always do exactly the same thing.
– Callum Jefferies
让 CSS 变得直接了当、明显、简单。类名变得更静态、可预测。
高度复用的样式
我想从一个例子入手来介绍原子类如何做到样式的复用。
我们现在要做一个小组件展示猫的名称以及猫入驻的时间:
传统的 CSS “最佳实践”
我们遵循关注点分离的原则,先编写结构:
关注点分离(Separation of concerns,SoC),是将计算机程序分隔为不同部分的设计原则。每一部分会有各自的关注焦点。
<div class="cat">
<img class="figure" width="60" src="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?&auto=format&fit=crop&w=200&h=200"></img>
<div class="name">可爱的猫猫</div>
<div class="time">一周以前</div>
</div>
然后我们引入一个针对这个组件的样式表,来负责这个组件的样式实现:
.cat {
display: inline-block;
/** for BFC */
overflow: hidden;
}
.cat .figure {
margin-right: 16px;
border-radius: 8px;
float: left;
}
.cat .name {
font-size: 16px;
color: black;
}
.cat .time {
font-size: 12px;
color: gray;
}
css 的每个 class 都是由元素的语义决定的,比如猫的名称就叫 cat-name
,猫的图片就叫cat-figure
,这种方法让 html 只关注结构,css 根据 html 定义的结构实现对应的样式。
原子类方法
使用原子类的思想,我们编写一个相同的组件:
<div class="inline BFC">
<img class="float-start bdrs-md mr-md" width="60" src="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?&auto=format&fit=crop&w=200&h=200"></img>
<div class="text-primary">可爱的猫猫</div>
<div class="text-secondary">一周以前</div>
</div>
同样我们有一个样式表,差别是这个样式表应该对全局生效
.BFC {
overflow: hidden;
}
.bdrs-md {
border-radius: 8px;
}
.mr-md {
margin-right: 16px;
}
.float-start {
float: left;
}
.text-primary {
font-size: 16px;
color: black;
}
.text-secondary {
font-size: 12px;
color: gray;
}
.inline-block {
display: inline-block;
}
对比传统关注点分离方法,这里的 CSS 的每个类是根据它的视觉效果命名,并且拆分出了一些更小的类,让每个类实现的样式保持单一的职责。
这两种方法在这个阶段编写的代码量是差不多的,也都能实现目标的组件 UI:
当变更发生
继续,我们现在需要一个尺寸更大的组件,以便更好的展示猫:
对于传统的关注点分离方法,我们重新建立一个结构,并使用 class 为 html 结构中的各个元素命名
<div class="cat-card">
<img class="cat-figure" width="160" src="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?&auto=format&fit=crop&w=200&h=200"></img>
<div class="cat-info">
<div class="label">猫的名称</div>
<div class="value">可爱的猫猫</div>
<div class="label">入住日期</div>
<div class="value">一周以前</div>
</div>
</div>
这样的变化使得样式几乎要完全重写,代码可以参考 sandbox。
类按照 HTML 结构命名,使用 css 选择器按照 html 嵌套结构来编写。一旦结构发生变化,原来的类就会失效。
而原子类方法呢?
<div class="bdrs-md BFC inline-block border-card">
<img class="block mb-md" width="160" src="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?&auto=format&fit=crop&w=200&h=200"></img>
<div class="pl-lg pb-lg">
<div class="text-secondary mt-sm mb-xs">猫的名称</div>
<div class="text-primary">可爱的猫猫</div>
<div class="text-secondary mt-sm mb-xs">入住日期</div>
<div class="text-primary">一周以前</div>
</div>
</div>
可以看到复用了很多之前的类,需要新增的类很少,代码可以参考 sandbox
margin、padding 类命名方式参考 emmet cheat sheet。比如
mb-*
代表margin-bottom:*
设计规范先行
前端项目的样式通常由设计师画出设计稿,然后前端同学来实现。
对原子类方法常见一个困惑是:“要求开发者把 margin-bottom:16px
这种原本很自由的 css 语句,写成mb-md
这种类,会降低编写样式的自由度呀。如果设计稿上出现 15px、17px、18px 这种没有包含在原子类中的边距怎么办呢?“
有这种疑惑,说明很多前端同学可能对设计规范不太了解。以间距为例几个例子。
设计是崇尚规范的
从两个非常流行的前端组件库中看他们的设计规范如何定义:
-
material design 作为业界标杆,定义了一套基于栅格的间距规范,非常细致和明确。
-
国内流行的 ant-design 的设计规范,明确定义了纵向间距为:小 8px,中 16px,大 24px。
从这些例子可以看出,其实设计是推崇规范的。如果原子类的定义是建立在设计和开发对设计规范的共识上,就不会出现本段最开始担忧的问题。换种说法,原子类应该是设计规范的落地,是设计规范先行。
设计推崇规范很大程度来源于有广泛认同的 Robin Williams 的设计四大基本原则: 亲密、对齐、重复、对比。
设计的重复原则指出:设计的某些方面需要在整个作品中重复。 重复元素可能是一种粗字体、 一条粗线、 某个项目符号、 颜色、 设计要素、 某种格式、 空间关系等。 读者能看到的任何方面都可以作为重复元素。
设计的对齐原则指出:任何元素都不能在页面上随意安放。 每一项都应当与页面上的某个内容存在某种视觉联系。应当找一条明确的对齐线, 并用它来对齐。
这也是原子类的优点,当我们要求开发者使用原子类实现样式时,也能很大程度上避免因为看错、写错导致的样式不还原。毕竟写错 mb-md
比写错 margin-bottom: 16px
难得多。
原子类的定义来自设计规范
从另一个方面说明什么是设计规范先行的观点,我们对比理想情况下原子类和常规方案编写组件的过程:
左侧为传统方案,右侧为原子类方案
理想情况下,原子类方案将定义样式类放到了编写 HTML 结构之前。原子类的定义不按照设计稿,而是按照设计规范,设计师再按照定好的规范进行设计,自然也能恰好落在原子类中。
我们都用过微软的文字编辑器 word,可以打个比方:
-
原子类就相当于段落格式——先定义后使用,每一段的样式都是在已定义的格式之一。
-
语义类相当于更自由的格式编辑——我们可以先编写文章内容,最后再来排版
保持可读性
原子类的可读性问题
需要承认的一点是,使用原子类编写的HTML结构,确实没有像传统方法命名整洁,元素写在类里的语义被丢掉了。许多对于原子类的讨论也指出这一点,甚至有指责原子类破坏了可读性。
以前文猫猫组件为例,可以对比两种方法的差异:
<!--按元素语义命名的传统方法-->
<div class="cat">
<img class="figure" width="60" src="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?&auto=format&fit=crop&w=200&h=200"></img>
<div class="name">可爱的猫猫</div>
<div class="time">一周以前</div>
</div>
<!--按元素视觉表现命名的原子类方法-->
<div class="inline BFC">
<img class="float-start bdrs-md mr-md" width="60" src="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?&auto=format&fit=crop&w=200&h=200"></img>
<div class="text-primary">可爱的猫猫</div>
<div class="text-secondary">一周以前</div>
</div>
替代方案
讨论这个问题首先必须指出的是在类中添加语义这种做法并不是 HTML 标准推荐的。事实上几乎没有屏幕阅读器、搜索引擎会按照类名来判断元素语义。而更标准、更推荐的做法是使用 HTML 语义元素和 ARIA 属性。
继续之前的例子,我们完全可以使用语义元素来为组件增加可读性和语义:
<figure class="inline BFC">
<img class="float-start bdrs-md mr-md" width="60" src="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?&auto=format&fit=crop&w=200&h=200"></img>
<figcaption class="text-primary">可爱的猫猫</figcaption>
<mark data-label="入住时间" class="text-secondary">一周以前</mark>
</figure>
如果考虑兼容性,也可以采用 ARIA 属性:
<div role="figure" aria-labelledby="caption" class="inline BFC">
<img class="float-start bdrs-md mr-md" width="60" src="https://images.unsplash.com/photo-1529778873920-4da4926a72c2?&auto=format&fit=crop&w=200&h=200"></img>
<div id="caption" class="text-primary">可爱的猫猫</div>
<div role="mark" aria-label="入住时间" class="text-secondary">一周以前</div>
</div>
关于 ARIA,我之前也写过一篇 ARIA 介绍和实践的文章,不太了解的同学可以读读
语义类名不是银弹
语义类名是在 jQuery 时代前端开发需要用类名来划分组件、执行 dom 操作的技术背景下总结出来的一套规范。当时人们编写的组件是这样的:
<button class="ui-button ui-widget ui-corner-all">
Button with icon on the right <span class="ui-icon ui-icon-gear"></span>
</button>
<script>
$(
function(){
$("button.ui-button").on("click", function(event){
console.log(event)
})
}
)
</script>
用这种方法编写组件,选择一个合适的类名是刚需。而现在组件的划分已经是 React、Vue 等等前端框架的职责了,甚至还出现了很多 css-in-js 的自动生成类名的方案。语义类名确实很漂亮,但在目前的技术下已经不是必要的。
同时,给每个元素找到合适的语义命名是一种思想负担。很多时候找不到合适的元素名称,只能取名叫”container”、”wrap” 之类的没有意义的类名。
原子类库实践 —— DIY
前面分析了原子类的特点,接下来我们就看看如何自建搭建一套原子类库。我们实际也做了一套,负责一套业务组件库,以及相关的几个业务系统的支撑:
接下来讲几个在 DIY 原子类库的思考
高效生成类
原子类通常会包含很多按照一定的规则生成的的类,而 CSS 是没有很好的方法去做这件事。我们在实践中采用了预处理器 SASS 来做这件事。
SASS 语法可以参考SASS 官网
举个例子,要做一套 flex 布局的原子类,justify-start
代表 justify-content: start
等等,使用 sass 可以会非常高效:
@mixin justify($name, $value) {
.justify-#{$name} {
display: flex;
justify-content: $value;
}
}
@mixin align($name, $value) {
.align-#{$name} {
display: flex;
align-items: $value;
}
}
$flex-align: (
'start': flex-start,
'end': flex-end,
'between': space-between,
'around': space-around,
'center': center,
'stretch': stretch,
'baseline': baseline,
);
// 循环出所有 14 种 flex 布局
@each $name, $val in $flex-align {
@include justify($name, $val);
@include align($name, $val);
}
编译结果为:
.justify-start {
display: flex;
justify-content: start;
}
.align-start {
display: flex;
align-items: start;
}
.justify-end {
display: flex;
justify-content: end;
}
.align-end {
display: flex;
align-items: end;
}
...
再举个例子,我们还提供了基于 grid 的多列布局,使用 sass 可以这样写:
@mixin divide-cols($repeat) {
grid-template-columns: repeat(#{$repeat}, 1fr);
}
@for $i from 1 through 9 {
.grid-cols-#{$i} {
display: grid;
@include divide-cols($i)
}
}
编译结果为:
.grid-cols-1 {
display: grid;
grid-template-columns: repeat(1, 1fr);
}
.grid-cols-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
.grid-cols-3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
...
与设计师共建
之前也讨论过原子类和设计的关系——原子类是设计规范的落地。在项目初期,可能设计本身也没有很多的规范,这就需要开发和设计合作。建议几个可以在项目前期首先确定的方向,比如:
- 主题色盘的规范
- 标题级别的规范
- 间距的规范
- 卡片的规范
- 成功、失败、警告、超链接、禁用等功能文字的规范
- 分割线的规范
- 投影级别的规范
实际可以和设计师一起参考调研业界的一些成熟方案。
原子类库实践 —— 开源库
目前业界有很多基于原子类思想的通用开源库方案。比如:
这些开源库都是开箱即用的,安装就能为项目添加一整套原子类库,同时也提供了一些配置的方法。具体用法各家都不相同,可以查看文档,不一一介绍了。
DIY vs 开源库
DIY 的好处
-
更好地控制原子类库的规模
开源的原子类库因为面向通用场景,往往是大而全。 tailwindCSS 全量大小有 3.7Mb,使用的时候需要搭配 purgecss 等 css shaking 工具。DIY 原子类库可以根据项目实际需求做实现。
-
通用的开源原子类库粒度都比较小,而 DIY 的原子类库可以根据实际情况改变。
以段落为例,如果设计规范中一级标题是 1.25rem bold 深灰色,使用 tailwindcss 不配置的情况下会这样写:
<div class="text-xl font-bold text-gray-900">我是一级标题</div>
DIY 的情况下完全可以编写更大粒度、语义更好的类:
<div class="ui-h1">我是一级标题</div>
-
最终产物是通用的 css,而不是像开源库的 js 配置文件,方便迁移
使用开源库的好处
- 通过配置生成原子类库,实现更快
- 很多业界共识的良好的命名实践,学习成本低
- 文档完善,方便查询
- 生态更完善,插件、编辑器拓展很多
- 提供了一些私有的 postcss 语法糖
总结
原子类库是一种搭建 CSS 架构的方法,偏好更细粒度、单一职责、按视觉命名的类。使用原子类方法可以让样式更可复用、更符合设计规范,但同时也要面对可读性降低的问题。