Skip to content

Instantly share code, notes, and snippets.

@banyudu
Created February 18, 2022 06:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save banyudu/32792f05c07cc7f32575f51198e510d4 to your computer and use it in GitHub Desktop.
Save banyudu/32792f05c07cc7f32575f51198e510d4 to your computer and use it in GitHub Desktop.
现代化的CSS

现代化的 CSS

CSS

什么是现代化的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 架构的理解。

Demo

为了方便理解,首先让我们看看 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 等都是描述样式用的。

可能有些同学看到这里已经开始皱眉头了,可能会有一些这样的疑问:

  1. 这不都是 bootstrap 玩剩下的吗?早就被淘汰了
  2. 代码和样式应该分离,这样不符合最佳实践
  3. 看起来太乱了

在解释这些问题之前,让我们先试着回溯一下历史,讲讲 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 架构。

模块级 CSS

主流的模块级架构是 Block Element Modifer,简称 BEM,它的前身是 OOCSS(面向对象的 CSS)及 SMACSS(可扩展模块化架构的 CSS)等。

从原理上来说,是将页面元素首先划成不同的区块(可重用组件),如电商商品页面会有 预览区(.preview)、SKU列表区(.sku-list)、评论区(.comments)、详情区(.detail)等等。然后再于区块的内部,定义一些元素,如 SKU 列表区中的 Button (.sku-list__button),最后还可以加状态修饰符,如禁用状态的按钮(.sku-list__button--disabled)。

它有两个比较核心的原则:

  1. 结构和样式分离
  2. 容器和内容分离

BEM 的规范易于理解,即使是新手,也很容易看懂并模仿。它也有缺点,就是类名特别长,尤其是在有 Sass / Less 嵌套的时候。

大部分的“最佳实践”,如语义化的 className,就是为使 BEM 架构能够正常运转而提出来的。

BEM 架构具有如下的优点:

  • 可读性强
  • 模块隔离(手动保证唯一性,或者启用 CSS Module)
  • 可维护性强(质量角度)(修改 A 模块不担心影响到 B 模块)

但是它也有如下的缺点:

  • 冗余度高(区块内的相似元素难以复用样式,容易催生 CV 工程师,先复制再修改)
  • 过于自由,缺乏规范(举例来说,有人统计过Gitlab网站中有几百种颜色值)
  • class 名称比较长
  • 难以实现响应式设计
  • 难以组件化

需要注意的是,由于 antd 等组件库的兴起,BEM 模式的元素样式难以复用的问题,得到了很大的缓解。

另外对于 BEM 模式尝试解决的一个问题:HTML结构与样式分离,它解决的也不够好,HTML确实不关心CSS怎么写,但是CSS常常需要关心 HTML 的内部细节,因此在对 HTML 结构做调整的时候,一般都需要对 CSS 对相应的调整。

原子化 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>

它的原理很容易理解,带有相同 classHTML 结构会共享同一段样式,不同样式的差异通过 class 的不同组合体现。

原子化 CSS 具有以下的优点:

  1. 样式规范化
  2. 易于实现响应式设计
  3. 组件化
  4. 样式隔离性更高
  5. 不易产生无用样式代码堆积

但是它也有如下的缺点:

  1. 学习成本高
  2. 无法覆盖所有样式需求,这会导致如下两种可能性:
    1. 混用原子化与 BEM,做成四不像。
    2. 自行扩展原子化语法,进一步增大学习成本。
  3. 生成的 HTML 可读性差(尤其是对不熟练的人)
  4. 会有一些用不到的样式被引入

另外回到上面提的页面结构和样式分离方面,原子化 CSS 其实也一样做不到分离。

BEM 模式是 CSS 依赖于 HTML 的结构,而原子化 CSS 是 HTML 依赖于 CSS。

但是它有一个稍微优于 BEM 的地方,因为 HTML 和 样式(即 class)写在了一起,当 HTML 结构发生变化的时候,CSS 会被一并修改。

CSS in JS

这是伴随着 React 兴起的一种 CSS 架构,代表产品是 styled component。

它将 css 与 jsx 写在同一个文件之中,可以实现如下的好处:

  1. 隔离性更高(强制性的局部样式,相当于默认开启的CSS Module。)
  2. 不易产生无用样式代码堆积
  3. 基于状态动态计算样式变得简单易读

当然它也有一些坏处:

  1. 生成的 HTML 难以阅读
  2. 缺乏统一标准
  3. 学习成本高
  4. 运行时消耗
  5. 难以实现响应式设计等

小结

基于上面的对比,我们大致可以得出如下的印象:

  1. BEM 模式自由度高,但比较依赖于“最佳实践”进行约束
  2. 原子化模式比较规范且开箱即用,但有学习成本且较难支撑全部需求。
  3. CSS in JS 优势不太明显
  4. 从过往经验来看,Bootstrap这种原子化模式随着 React 和 组件库的兴起,已经比较没落了。

下面看看同样是原子化模式的 Tailwind 是怎么做到优于 Bootstrap,并再次像 BEM 发起挑战的。

Tailwind CSS 亮点介绍

Tailwind CSS 有许多的亮点,相比于 Bootstrap,无论是 API 的丰富度和清晰度,还是可扩展性,乃至性能,都有不俗的表现。

它的 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;

看到这里肯定会有很多问题,如:

  1. 如何确定 padding 后面可以跟哪些数字呢?比如 p-10000 行不行
  2. 可以使用哪些颜色呢?bg-white 代表白色,bg-blue 代表蓝色,那淡蓝色怎么表示呢?
  3. 需要记住所有的 class 名称吗?
  4. 支持扩展样式吗?怎么扩展?

先讲一下 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 可分为如下三个部分:

  1. 学习有哪些修饰符,如 暗黑模式为 dark:,hover 态为 hover:等。
  2. 学习 CSS 的属性对应的 Tailwind 类型值,如 padding 为 p,padding-left 为 pl,margin 为 m,宽高分别为 w / h 等。
  3. 学习几个主要的值域,如 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 是怎么解决的这个问题呢?它有两种模式:

  1. 预先生成全集CSS定义,然后检查代码中未使用的 class,对不需要的 CSS 进行裁剪。
  2. 从空白CSS开始,检查代码中使用的class,按需生成相应的CSS代码。也称为 JIT 模式。

随着 Tailwind CSS 支持的 API 和修饰符越来越多,方式1出现了较大的性能问题,往往需要用户主动关闭一些不需要的功能(如flex)或修饰符(如dark),因此从 v3 开始,JIT 模式称为默认配置。

这一点也决定了代码中不能把单个 class 名称动态拼接,即如果用到了 bg-blackbg-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 包进行共享。

Tailwind CSS 劣势

和Bootstrap类似,学习曲线陡峭依然是 Tailwind CSS的一个劣势,在使用它的早期阶段,会经常需要查阅官方资料,寻找某个 CSS 属性对应的 class 是什么。

总结

以上就是关于现代化的 CSS 框架 Tailwind CSS 的介绍,希望通过对不同的 CSS 架构的对比分析,形成对 Tailwind CSS 的更深入的理解。

以上。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment