使用 Tailwind CSS 一年后,我的一些感受

27,404

注意:本文并不是一篇教你如何使用 Tailwind CSS 的文章。

Tailwind CSS 在过去两年的流行程度一直保持一个高速增长的态势,尤其是2020年,更是各大论坛/社区热烈讨论的话题。下图是 Tailwind CSS 在过去两年在 npm 上的下载量趋势图。

image.png

从 2020 年 5 月份开始,到目前为止我使用 Tailwind CSS 已接近 1 年的时间。如果用一句话来介绍 Tailwind CSS,我认为 Tailwind CSS 是一套以 Atomic/Utility-First CSS 为基础的完整的设计系统(Design System)。

Atomic/Utility-First CSS

「Atomic/Utility-First CSS」,是和「Semantic CSS 」(语义化 CSS ) 相对的一种 CSS 规范。不管你是否听说过「Semantic CSS 」这个名词,实际项目开发中,它都是我们最常用、也是最传统的 CSS 规范。

以 Tailwind CSS 官网上的一个示例为例,实现如下通知效果,一般的写法是:

<div class="chat-notification">
  <div class="chat-notification-logo-wrapper">
    <img class="chat-notification-logo" src="/img/logo.svg" alt="ChitChat Logo">
  </div>
  <div class="chat-notification-content">
    <h4 class="chat-notification-title">ChitChat</h4>
    <p class="chat-notification-message">You have a new message!</p>
  </div>
</div>

<style>
  .chat-notification {
    display: flex;
    max-width: 24rem;
    margin: 0 auto;
    padding: 1.5rem;
    border-radius: 0.5rem;
    background-color: #fff;
    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
  }
  .chat-notification-logo-wrapper {
    flex-shrink: 0;
  }
  .chat-notification-logo {
    height: 3rem;
    width: 3rem;
  }
  .chat-notification-content {
    margin-left: 1.5rem;
    padding-top: 0.25rem;
  }
  .chat-notification-title {
    color: #1a202c;
    font-size: 1.25rem;
    line-height: 1.25;
  }
  .chat-notification-message {
    color: #718096;
    font-size: 1rem;
    line-height: 1.5;
  }
</style>

这就是一个典型的 「Semantic CSS」 命名方式:为不同的 html 标签定义语义化的 class 名字,然后每个 class 中包含应用到对应 html 标签上的所有 css 样式。

但是,随着项目的开发过程, 这种 css 规范会让 css 的维护成本越来越高:

  1. 命名困难。越来越多的相似语义化场景,会导致越来越多类似 aa-title、bb-title、bb-b1-title、aa-content、bb-content 这样的 class 命名。开发人员一边需要保证 aa、bb、bb-b1 这样的名称能准确表达语义,一边需要小心翼翼地避免 css 全局作用域带来的冲突问题(例如,不同的 UI 区域都定义了 aa-title 导致的样式冲突;aa-content、 bb-content 无意识地嵌套使用,导致内层 class 继承了外层 class 预期之外的样式)。这给开发人员带来了很大的心智负担。
  2. 难以复用。css 样式很难通过语义化命名的 class 进行复用,因为一个 class 中包含了多条 css 样式,而多条 css 样式即使在同一语义环境下,也会因受到更大的上下文的影响,导致部分样式的差异化而无法直接复用 class。例如,企图通过 title、header-title 这类 class 命名来实现 「标题」语义下的 css 复用肯定是行不通的。继续沿着这条路走下去,势必又会导致更多的类似名称的 class 的出现:nav-title、nav-min-title、sider-title ... 而这些 class 很可能只是其中一条 css 规则不同,例如 font-size。
  3. 重构成本高。不一定是整体样式的大重构,即使是将所有字号增加 2px 这类需求,在「Semantic CSS」规范下,都需要修改大量文件才能实现。
  4. css 文件大小膨胀。每个 class 都包含大量重复的 css 样式,无法解决复用性,这些问题都会导致随着项目需求的增加, css 文件变得越来越大,而且很可能 css 文件膨胀的速度是大于代码仓库整体体积的增长速度的。

那么,「Atomic/Utility-First CSS」 是如何解决「Semantic CSS」所面临的问题的呢?我们来看下,使用 Tailwind CSS 实现同样的示例:

<div class="p-6 max-w-sm mx-auto bg-white rounded-xl shadow-md flex items-center space-x-4">
  <div class="flex-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-gray-500">You have a new message!</p>
  </div>
</div>

这个实现版本,对比第一版,有以下区别:

  1. 我们没有自定义任何的 css class,使用的所有的 css class 都直接来源于 Tailwind CSS,这样就没有了命名的困扰问题,同时也解决了 css 膨胀的问题。当然 html 体积也变大了,但是因为 class 中使用的是有限集合内的、高度重复的 class 名称,在 Gzip、Brotli 这些压缩算法的作用下,是可以基本忽略的。
  2. 每一个 class 一般只对应一条 css 规则,如 p-6 对应 padding: 1.5rem,h-12对应 height: 3rem,原子性的 class 颗粒度自然更容易在其他地方复用,而且原子化的 css 规范/思想,强制开发人员在为 html 标签定义样式时,写全所有需要的 class ,大大减少了不同 html 标签的 class 之间的相互影响。
  3. 「Atomic/Utility-First CSS」的使用,让样式重构/整体修改变得更加容易。我们可以通过覆盖原子颗粒度的 class ,变更应用的整体样式,例如,覆盖 text-xl 为 2rem,这样所以使用到 text-xl class 的字体大小都会变成 2rem。

现在看来, 「Atomic/Utility-First CSS」比 「Semantic CSS 」的可维护性高很多。但是有一件事情需要注意:「Atomic/Utility-First CSS」并不是 Tailwind CSS 提出的,实际上,「Atomic/Utility-First CSS」的诞生远远早于 Tailwind CSS :basscss(2013年)、tachyons(2014年),当然还有很多其他类似的 CSS 库。

那么,为什么在这么长的时间里,「Atomic/Utility-First CSS」并没有真正流行起来呢?

因为,只有 「Atomic/Utility-First CSS」是不够的,前端开发需要一套完整的设计系统(Design System)。

Tailwind CSS 设计系统

一套完整的设计系统,需要能提供开箱即用的功能,满足 80% 的业务场景;同时需要支持良好的扩展机制,满足另外 20% 的业务场景。Tailwind CSS 不仅是一个「 Atomic/Utility-First CSS」的解决方案,同时也是一套完整的设计系统。

Tailwind 的开箱即用主要体现在:

  1. 精心设计的 design token。Tailwind 的 design token 既包含常规的工具类 css class,如控制 css 盒模型的 box-border、box-content,控制显示方式的 block、inline,控制浮动定位的 float-right、float-left、float-none 等;也包含对颜色(color)、间距(spacing)等通用样式属性的预先设置,例如颜色下有 gray、black、white、red 等常用的颜色 token,基于这些 token 会自动生成 css class,用于字体颜色、背景颜色、边框颜色等各种需要颜色设置的 css 规则中,如对于 gray 这个颜色 token,Tailwind 会生成 text-gray(字体色)、bg-gray(背景色)、border-gray(边框色) 等 css class。此外,Tailwind 设计的 css class 名称,也是比较容易记忆和分辨的, 基本上看到名称就能推测出对应的 css 规则。
  2. 支持响应式。Tailwind 提供了常用的屏幕宽度断点 token:sm、md、lg、xl、2xl ,这些断点 token 可以和 Tailwind 提供的其他 css class 组合使用,实现响应式支持。如:
<!-- Width of 16 by default, 32 on medium screens, and 48 on large screens -->
<img class="w-16 md:w-32 lg:w-48" src="...">
  1. 支持伪类等 CSS 状态。Tailwind 提供了常用的伪类 token:hover、focus、active、first-child、last-child 等,这些 token 和 Tailwind 提供的其他 css class 组合使用,实现伪类效果。如下面的按钮默认为黑色,鼠标悬浮状态下为红色:
<button class="bg-black hover:bg-red ...">
  Hover me
</button>
  1. 支持 Dark 模式。Tailwind 提供了 Dark 模式下的的伪类 token:dark。使用示例如下:
<div class="bg-white dark:bg-gray-800">
  <h1 class="text-gray-900 dark:text-white">Dark mode is here!</h1>
  <p class="text-gray-600 dark:text-gray-300">
    Lorem ipsum...
  </p>
</div>

「Atomic/Utility-First CSS」方案在解决 80% 的业务场景问题上,大多做得都不错(class 命名规范上,个人感觉 Tailwind 要优于其他大部分方案)。但是,大部分的「Atomic/Utility-First CSS」方案,关注的只是 utility class 的提供上,并不是一个完整的 design system,缺乏良好的扩展机制,导致没能解决好剩下的 20% 的业务场景。

使用「Atomic/Utility-First CSS」方案,最经常遇到的一个问题是:当默认提供的 css class 不够用时,开发人员如何扩展、或者是编写新的 css class ? 解决这个问题的一般做法是:开发人员在自己的业务代码中,定义额外的自定义 css 样式(css class 、内联样式等 )。这破坏了样式层的 Single Source of Truth:当需要做样式修改时,开发人员需要同时兼顾 「Atomic/Utility-First CSS」库 和 自定义 css 两个来源的样式规则。

使用 Tailwind CSS 时,可以非常方便的在现有的 design token 上做扩展。例如,下面的配置对颜色和间距分别做了扩展:

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        'regal-blue': '#243c5a',
      },
      spacing: {
        '13': '3.25rem',
      }
    }
  }
}

这样一来,就可以生成新的 css class: text-regal-blue(设置字体颜色)、m-13(设置margin)、p-13(设置padding)等。Tailwind CSS 的扩展机制,保证了样式层的规范都来源于同一个地方——Tailwind。 当然,Tailwind CSS 的扩展机制要比上面的示例强大的多。

更好的样式复用方式

虽然前面已经讲到,「Atomic/Utility-First CSS」可以更好的进行 css class 的复用。但是因为复用的级别是原子颗粒级别的 css class,在不同的地方重复写多个原子性 css class ,显然也不是一个好的维护性。例如,没有人希望在每次使用 button 时,都要写一遍下面的一堆 class...

<!-- Repeating these classes for every button can be painful -->
<button class="py-2 px-4 bg-green-500 text-white font-semibold rounded-lg shadow-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-opacity-75">
  Click me
</button>

HTML、CSS、JS 这前端三剑客中,HTML + CSS 负责展现,JS 负责逻辑。就展现而言,HTML 是基础和中心,CSS 本质上负责的是「美化」HTML 的工作。所以,大部分情况下,CSS 不适合脱离了 HTML 而做(语义化)复用。CSS 的复用,需要借助 HTML 模板、或 JS 组件的形态进行复用。例如上面button 的例子,我们就可以通过封装独立的 Button 组件完成 CSS 的复用。

有些开发人员认为,为了复用 CSS 样式,硬封装出一个 Button 组件“太重”了。这也不无道理。对于像 button 这类很轻量的组件,直接通过定义一个 css class ,组合相关 css 规则也是可以的。例如,在Tailwind CSS 中可以这样做:

  .btn-indigo {
    @apply py-2 px-4 bg-indigo-500 text-white font-semibold rounded-lg shadow-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:ring-opacity-75;
  }

通过 Tailwind CSS 提供的 @apply 指令,组合已有的 css class,封装成 btn-indigo 这个 class。这样保证了 Tailwind CSS 依然是样式层的 Single Source Of Truth。

CSS in JS

对于喜欢「CSS in JS」写法的开发人员,可以借助 twin.macro 这个库,在「CSS in JS」中引用 Tailwind CSS。这样就可以既享受「CSS in JS」的优势,又能保证 Tailwind CSS 作为样式层的 Single Source Of Truth。下面是一个示例:

import 'twin.macro'

const Input = () => <input tw="border hover:border-black" />

Tailwind CSS 缺点

  1. 每个 class 中要声明多个 css class 的写法,会让人感觉代码丑陋,尤其是对于刚接触 Tailwind 的用户。然而,一段时间过后(对我而言大概1周时间),对于绝大部分开发者来说,都是可以习惯这种写法的。
  2. 构建速度问题。尤其是开发阶段,当修改了 tailwind.config.js 时,如扩展新的 class,会触发Tailwind 重新构建,这个构建速度比较慢(十几秒甚至几十秒),对开发体验有一定影响。不过 Tailwind CSS v2.1 引入了 Just-in-Time Mode 特性,可以有效解决这个问题。

总结

个人感觉 Tailwind CSS 并没有太多让人「惊讶」的特性,但是在打造一个完整的设计系统这件事情上,Tailwind CSS 在每个方面都做了精心的设计,所以让人用起来很「舒服」。如果你还没有使用过 Tailwind CSS,建议可以尝试一下,相信会给你带来耳目一新的感受!

招聘时间

我所在的团队:字节跳动的 Web Infra 团队,长期招聘各个级别的前端工程师,可以 base 在北京、上海、杭州、广州、深圳、新加坡。有意向的同学可以加我微信(xuchaobei)咨询,备注「掘金」。当然,字节跳动的其他部门和岗位也欢迎找我内推。