什么是现代化的CSS?
在 10 年前可能是 Sass / Less / Stylus 等 CSS 预处理器,以及逐渐落地中的 CSS3 标准。
在 5 年前则是 PostCSS 后处理器,相当于 CSS 界中的 Babel,帮助前段开发摆脱了 Vendor Prefix,如 -webkit-xxx 这样的样式写法。
Sass 等预处理器提升了 CSS 的便利程度;PostCSS 及 Webpack 等工程化套件提升了其可靠性;
在它们的基础之上, 2022 年的今天,现代化的 CSS,关注的重点转向了 CSS 的架构、设计模式方面。怎么才能降低大型项目 CSS 的复杂度,提高可维护性?CSS 能不能被组件化、工具化?
2021 年大放异彩的 Tailwind CSS 及衍生框架给出了一些具有特色的解决方案,TailwindCSS 在 4 年的时间内,拿下了 53.7K 的 Star。下面我结合 Tailwind CSS 中的一些设计,谈谈我对 CSS 架构的理解。
为了方便理解,首先让我们看看 Tailwind 的一个简单示例,对它有一个粗略的了解:
<div class="p-6 max-w-sm mx-auto bg-white rounded-xl shadow-lg flex items-center space-x-4">
<div class="shrink-0">
<img class="h-12 w-12" src="/img/logo.svg" alt="ChitChat Logo">
</div>
<div>
<div class="text-xl font-medium text-black">ChitChat</div>
<p class="text-slate-500">You have a new message!</p>
</div>
</div>
说是 CSS 的示例,但是 Demo 却是不折不扣的 HTML,这是因为 Tailwind CSS 是将样式直接写到了 HTML 之内。
不难看出 class
中的 font-medium text-black bg-white
等都是描述样式用的。
可能有些同学看到这里已经开始皱眉头了,可能会有一些这样的疑问:
- 这不都是 bootstrap 玩剩下的吗?早就被淘汰了
- 代码和样式应该分离,这样不符合最佳实践
- 看起来太乱了
在解释这些问题之前,让我们先试着回溯一下历史,讲讲 CSS 架构的发展历程,这对于理解 Tailwind CSS 背后的“哲学”有非常大的帮助。
CSS 是一门很古老、应用很广泛的语言,当然也有很多人说它算不上一门语言,不过无论怎样,样式领域中,它是当之无愧的霸主。
在它漫长的发展历程中,诞生了许许多多的“最佳实践”,譬如 className 要语义化,以及内容和样式分离等等。但是还是有很多人反映 CSS 难懂难学,虽然入门简单,一看即会,但是要想深入掌握,则需要耗费巨大的精力,且往往并不被认可。
具有其他领域编程经验的人触碰到前端时,往往可以通过其他领域的经验,快速地掌握 HTML / JS 等语法,却很难做到不畏惧 CSS,更不要提做好 CSS 的架构。也许在不少人的心中,CSS 是没有什么架构的概念的。
所以,有没有这么一种可能,CSS 的“最佳实践”们,并不好使?
过去的十年是前端日新月异的十年,相比于最初的 HTML / CSS / Javascript 三板斧,伴随着 React 的流行而深入人心的是 JSX 的语法,HTML 和 Javascript 的主次地位发生了变化。然而这么多年以来,CSS 的写法却基本未产生明显的变化,依旧是独立于 HTML / Javascript 存在,通过 class / id
等锚点对页面进行装饰。
让我们暂时抛去既有的成见,重新从软件工程中可靠性、可理解性、可维护性、可重用性等角度,剖析历史中出现的一些 CSS 架构,寻找各种架构的优缺点,最后达成一个对 CSS 架构的更深入的理解。
先看有哪些主流的 CSS 架构。
主流的模块级架构是 Block Element Modifer,简称 BEM,它的前身是 OOCSS(面向对象的 CSS)及 SMACSS(可扩展模块化架构的 CSS)等。
从原理上来说,是将页面元素首先划成不同的区块(可重用组件),如电商商品页面会有 预览区(.preview
)、SKU列表区(.sku-list
)、评论区(.comments
)、详情区(.detail
)等等。然后再于区块的内部,定义一些元素,如 SKU 列表区中的 Button (.sku-list__button
),最后还可以加状态修饰符,如禁用状态的按钮(.sku-list__button--disabled
)。
它有两个比较核心的原则:
- 结构和样式分离
- 容器和内容分离
BEM 的规范易于理解,即使是新手,也很容易看懂并模仿。它也有缺点,就是类名特别长,尤其是在有 Sass / Less 嵌套的时候。
大部分的“最佳实践”,如语义化的 className,就是为使 BEM 架构能够正常运转而提出来的。
BEM 架构具有如下的优点:
- 可读性强
- 模块隔离(手动保证唯一性,或者启用 CSS Module)
- 可维护性强(质量角度)(修改 A 模块不担心影响到 B 模块)
但是它也有如下的缺点:
- 冗余度高(区块内的相似元素难以复用样式,容易催生 CV 工程师,先复制再修改)
- 过于自由,缺乏规范(举例来说,有人统计过Gitlab网站中有几百种颜色值)
- class 名称比较长
- 难以实现响应式设计
- 难以组件化
需要注意的是,由于 antd 等组件库的兴起,BEM 模式的元素样式难以复用的问题,得到了很大的缓解。
另外对于 BEM 模式尝试解决的一个问题:HTML结构与样式分离,它解决的也不够好,HTML确实不关心CSS怎么写,但是CSS常常需要关心 HTML 的内部细节,因此在对 HTML 结构做调整的时候,一般都需要对 CSS 对相应的调整。
也称为功能性 CSS,通过定义一系列原子化的全局样式,实现精简样式开发工作的目的。
如 Bootstrap 中的 Label 示例:
<span class="label label-default">Default</span>
<span class="label label-primary">Primary</span>
<span class="label label-success">Success</span>
<span class="label label-info">Info</span>
<span class="label label-warning">Warning</span>
<span class="label label-danger">Danger</span>
它的原理很容易理解,带有相同 class
的 HTML
结构会共享同一段样式,不同样式的差异通过 class
的不同组合体现。
原子化 CSS 具有以下的优点:
- 样式规范化
- 易于实现响应式设计
- 组件化
- 样式隔离性更高
- 不易产生无用样式代码堆积
但是它也有如下的缺点:
- 学习成本高
- 无法覆盖所有样式需求,这会导致如下两种可能性:
- 混用原子化与 BEM,做成四不像。
- 自行扩展原子化语法,进一步增大学习成本。
- 生成的 HTML 可读性差(尤其是对不熟练的人)
- 会有一些用不到的样式被引入
另外回到上面提的页面结构和样式分离方面,原子化 CSS 其实也一样做不到分离。
BEM 模式是 CSS 依赖于 HTML 的结构,而原子化 CSS 是 HTML 依赖于 CSS。
但是它有一个稍微优于 BEM 的地方,因为 HTML 和 样式(即 class)写在了一起,当 HTML 结构发生变化的时候,CSS 会被一并修改。
这是伴随着 React 兴起的一种 CSS 架构,代表产品是 styled component。
它将 css 与 jsx 写在同一个文件之中,可以实现如下的好处:
- 隔离性更高(强制性的局部样式,相当于默认开启的CSS Module。)
- 不易产生无用样式代码堆积
- 基于状态动态计算样式变得简单易读
当然它也有一些坏处:
- 生成的 HTML 难以阅读
- 缺乏统一标准
- 学习成本高
- 运行时消耗
- 难以实现响应式设计等
基于上面的对比,我们大致可以得出如下的印象:
- BEM 模式自由度高,但比较依赖于“最佳实践”进行约束
- 原子化模式比较规范且开箱即用,但有学习成本且较难支撑全部需求。
- CSS in JS 优势不太明显
- 从过往经验来看,Bootstrap这种原子化模式随着 React 和 组件库的兴起,已经比较没落了。
下面看看同样是原子化模式的 Tailwind 是怎么做到优于 Bootstrap,并再次像 BEM 发起挑战的。
Tailwind CSS 有许多的亮点,相比于 Bootstrap,无论是 API 的丰富度和清晰度,还是可扩展性,乃至性能,都有不俗的表现。
它的 API 让人愿意投入时间学习,不会轻易过时。
不同于 bootstrap 从组件的角度出发,设计了诸如 .btn
、.btn-primary
等风格的 API,Tailwind 做得更底层一些,它设计了一套用于替换 CSS 基本能力,含有规律的 API 集合。
再次回顾上面提到的Demo:
<div class="p-6 max-w-sm mx-auto bg-white rounded-xl shadow-lg flex items-center space-x-4">
<div class="shrink-0">
<img class="h-12 w-12" src="/img/logo.svg" alt="ChitChat Logo">
</div>
<div>
<div class="text-xl font-medium text-black">ChitChat</div>
<p class="text-slate-500">You have a new message!</p>
</div>
</div>
这个里面:
- p-6 代表 padding: 1.5em,即 6 * 0.25em; m-6 代码 margin,其余与 p-6 一致。
- h-12 代表 height: 3em,即 12 * 0.25em; w-12 代表宽度,其余与 h-12 一致。
- bg-white 代表 background-color: white;
看到这里肯定会有很多问题,如:
- 如何确定 padding 后面可以跟哪些数字呢?比如 p-10000 行不行
- 可以使用哪些颜色呢?bg-white 代表白色,bg-blue 代表蓝色,那淡蓝色怎么表示呢?
- 需要记住所有的 class 名称吗?
- 支持扩展样式吗?怎么扩展?
先讲一下 Tailwind 的 API 设计原则,这些问题就比较容易弄明白了。
Tailwind 的 API 是有很强的规律的,一般来说,可以简单地认为它的规律是 {修饰符}:{类型}-{值}
如 hover:dark:w-12 代表当进入 hover 态时,将宽度 (w) 设置为 12 (单位是 0.25em),即 3em。
类型是固定的,但是值和部分修饰符可以自定义或基于默认的进行扩展。
API具有如下规律:
- 修饰符无限叠加:如 dark:hover:w-12,指在黑暗模式下才为 hover 态设置宽度 3em,它也等价于 hover:dark:w-12,即修饰符顺序无关。
- 同类型随机组合:相同性质的类型共享所有值,可随机组合。如只要有 w-12(宽 3em),就会有 h-12(高3em)。有 text-blue (字体蓝色)就会有 bg-blue(背景蓝色)。
- 值可扩展,如默认情况下没有 p-10000,但是可以通过自定义扩展实现,且只要有 p-10000,就同时会有 px-10000 py-10000 m-10000 max-m-10000 h-10000 w-10000 等等。
棕上,学习 API 可分为如下三个部分:
- 学习有哪些修饰符,如 暗黑模式为
dark:
,hover 态为hover:
等。 - 学习 CSS 的属性对应的 Tailwind 类型值,如 padding 为 p,padding-left 为 pl,margin 为 m,宽高分别为 w / h 等。
- 学习几个主要的值域,如 color 有哪些,spacing 有哪些,font-family有哪些等。另外因为这一部分可以扩展,其实完全可以在团队内自定义一套。
举例来说,假设在 tailwind.config.js
中加入如下的自定义配置:
module.exports = {
theme: {
colors: {
primary: var(--primary-color, '#f7fafc'),
secondary: '#1a202c'
// ...
}
}
}
就可以使用 bg-primary
实现 background-color: #f7fafc
的效果。
修饰符顺序无关,就代表 CSS 中既需要有 dark:hover:w-12
,也需要有 hover:dark:w-12
,还有如下各个 spacing 的所有可能组合:
module.exports = {
theme: {
spacing: {
'1': '0.25em',
'2': '0.5em',
'3': '0.75em',
'4': '1em',
'5': '1.25em',
'6': '1.5em',
// ...
'96': '24em'
}
}
}
那么里面必然含有很多不必要的样式,数量级非常地大,体积会很快膨胀到一个不可接受的地步。
那么 tailwind css 是怎么解决的这个问题呢?它有两种模式:
- 预先生成全集CSS定义,然后检查代码中未使用的 class,对不需要的 CSS 进行裁剪。
- 从空白CSS开始,检查代码中使用的class,按需生成相应的CSS代码。也称为 JIT 模式。
随着 Tailwind CSS 支持的 API 和修饰符越来越多,方式1出现了较大的性能问题,往往需要用户主动关闭一些不需要的功能(如flex)或修饰符(如dark),因此从 v3 开始,JIT 模式称为默认配置。
这一点也决定了代码中不能把单个 class 名称动态拼接,即如果用到了 bg-black
和 bg-white
,代码中必须有这两个字符串的明文,而不能是:
const bg = { pending: 'white', ready: 'black'}
const className = `bg-${bg[state]}`
return <div className={className}><div>
这样的动态 className。
但是动态组合 className 是可以的:
const bg = { pending: 'bg-white', ready: 'bg-white'}
const className = `${bg[state]} p-2 text-lg`
return <div className={className}><div>
JIT 模式给 Tailwind CSS 带来的能力提升是飞跃式的,因为一般来说,单个项目中使用的样式其实不会有很多种(尤其是在有原子化CSS进行规范的情况下),所以 Tailwind CSS 可以不断地完善 API,扩大其能力,而不用担心生产环境 CSS 体积变得臃肿。
在 Tailwind 中设置响应式设计是很简单的,例:
<div class="bg-green-500 md:bg-red-500 lg:bg-yellow-500">
<!-- ... -->
</div>
示例中设置了默认的背景色为绿色,中等屏背景色为红色,大屏背景色为黄色。
默认的显示器大小配置如下:
module.exports = {
theme: {
screens: {
'sm': '640px',
// => @media (min-width: 640px) { ... }
'md': '768px',
// => @media (min-width: 768px) { ... }
'lg': '1024px',
// => @media (min-width: 1024px) { ... }
'xl': '1280px',
// => @media (min-width: 1280px) { ... }
'2xl': '1536px',
// => @media (min-width: 1536px) { ... }
}
}
}
所谓移动优先,就是带有 screen 修饰符(即 sm, md等)的属性,只应用于大于等于它的显示屏上。做响应式设计的时候,应该优先以不加修饰符的形式完成小屏的设计,再逐渐放大屏幕,针对出现问题的地方,添加额外的带 screen 修饰符的 class.
如上所述,Tailwind 不仅可以扩展各种值的枚举项,也可以扩展部分修饰符。甚至还可以写插件和 preset。
因此可以比较方便地将团队的样式配置抽取成公共的 NPM 包进行共享。
和Bootstrap类似,学习曲线陡峭依然是 Tailwind CSS的一个劣势,在使用它的早期阶段,会经常需要查阅官方资料,寻找某个 CSS 属性对应的 class 是什么。
以上就是关于现代化的 CSS 框架 Tailwind CSS 的介绍,希望通过对不同的 CSS 架构的对比分析,形成对 Tailwind CSS 的更深入的理解。
以上。