关于TailwindCSS的一些思考

tailwindcss是一个工具类(原子类)优先的 CSS 框架,与bootstrapbulma等组件类框架不同,tailwind 自身并没有提供可以直接使用的组件类如.btn-primary.table,而是提供了诸如px-4text-sm等原子类,由开发者通过组合的形式构建页面样式。

本文将先介绍样式类的编写变化,然后介绍 tailwind 的使用体验,以及关于现在是否能够将原子类用在生产项目中的一些思考。

<!--more-->

参考:

1. 写样式的几种方式

回到在 16 年刚入行前端的时候,从行内样式开始学习

<button style="color:red;font-size:20px"></span>

在伟大的”结构、样式、行为“分离的教条下,开始将样式写在单独的样式表中,

.btn {
    color: red;
    font-size: 20px;
}

后来发现了一些比较流程的 UI 库,比如 bootstrap,提供了丰富的组件样式,直接 link 标签引入进来,就可以快速的编写页面了

<button class="btn"></button>

当然,UI 库提供的样式并不能满足定制化设计需求,还是需要我们写各种样式表。原始的 CSS 写起来不是很快乐,中间还学习了各种预处理器(LESSSCSS等)和后处理器(PostCSS)。

如今,前端组件库盛行,在 B 端管理后台等项目中,已经很少需要前端手写样式了。

<el-button></el-button>

1.1. 原子类

在写样式表的时候,我已经注意到了 CSS 的一个难点:取一个好的 class 名字。在查找解决方案的时候,了解到了一种叫做“原子类”的命名风格,类似于

.btn {
    color: red;
    font-size: 20px;
}

/*拆分成颗粒原子类*/
.text-red {
    corlor: red;
}
.text-20 {
    font-size: 20px;
}

然后通过组合的方式构建页面

<button class="btn"></button> <button class="text-red text-20"></button>

在那个时候,我并不能接受这种风格,在这篇关于 CSS 的思考的博客中进行了详细介绍。

在我的刻板印象中,原子类介于行内样式和 class 样式之间

  • 避免了行内样式无法样式复用的问题,CSS 选择器一个非常重要的功能就是样式代码复用
  • 避免了类选择器的取名难题,不需要再想一个语义化的名字
  • 通过组合的方式可以快速的实现一个带样式的页面,却不需要写一行 CSS 代码(当前前提是原子类丰富,且你能够记得住那么多样式)
  • 样式写的少了,样式表的体积也就少了

纵然有这么多优点,但原子类的缺点也很明显

  • 需要花时间去熟悉,如果原子类的定义没有规范,则需要花费更多的时间。比如上面的text-red这个红色到底是哪个颜色,如果设计师定义了多个红色,改怎么处理?起码得有个文档吧
  • 每个标签的 class 都很长,没有语义化的可读性;class 很长会导致 HTML 文件变得很庞大,尽管 gzip 可以帮助压缩一部分重复的内容,但在控制台调试的时候也不太方便(sourcemap 也没用了)
  • 如果全都使用原子类来构建页面,则维护起来是很麻烦的,比如一个.btn 在 10 个地方使用,现在需要给这个类加一个 hover,常规的话只需要改这一个地方;而原子类需要先找到需要添加样式的标签,然后依次增加 hover 的原子类,删除同理
  • 选择器嵌套、后代选择器等 css 的特点看起来也是不太好用上了

第 3 点是我无法接受的硬伤,以至于 2020 前当我第一次了解到tailwindcss时,看了一下描述,嗤之以鼻,随后就关闭了官网。

1.2. 现代化 CSS 开发流程的问题

经过很长一段时间的摸索,总结出一套比较适合开发 CSS 的姿势。

借助@mixin@extend等预处理器提供的方法,可以复用 css 代码片段,甚至在 css 中写一些逻辑。

借助autoprefixerpx2rem等 postcss 工具,不需要写繁琐的前缀和单位转换。

借助css modulecss scoped等工具,不用考虑样式冲突,以至于不用过多地考虑命名了。

借助各种 MVVM 框架,基本上不用考虑通过选择器获取 DOM 节点进行操作,以至于不用过多地考虑命名了。

剩下需要命名的场景,BEM 就搞定了。

随着 figma、蓝湖等工具的兴起,我已经很久没有打开 PS 手动切图了。这些工具还提供了一键复制 CSS 代码的功能。

这些工具非常方便,以至于我越来越懒,现在的开发流程变成了:按照设计图定义好页面结构,随便取一个名字,从 figma 上复制粘贴、稍作修改,ctrl + s, 借助热更新,直接预览样式结果,OK,重复粘贴下一段...

从开发体验和交付效率上来说,这个流程简直无可挑剔——如果你去不看最后部署在生产环境的 CSS 样式表的话...

已经 500 多 kb 了!!

在多人大型项目中,CSS 样式冗余是一个非常常见的问题。社区有一些工具如purgecsscssnano可以剔除项目中一些无用的样式类,但无法在那些动态拼接类名的场景下正常使用。

时代在循环,原子类确实可以解决这个问题。

让我们来看看今天的主角tailwind,他有哪些优点,又修复了原子类的哪些缺点。

2. tailwind 初体验

先把 vscode 的插件Tailwind CSS IntelliSense装好,方便智能提示和语法高亮等。

该插件会在工作根目录有tailwind.config.js的情况下生效。如果不生效,可能需要重启一下 vscode,这里小坑了一下。

然后我们,根据文档可以快速启动一个项目

# 创建目录
mkdir tailwind_demo && cd tailwind_demo

# 安装依赖
npm install tailwindcss postcss -D

# 会自动生成一个`tailwind.config.js`
npx tailwindcss init

接着配置模板文件路径、创建入口index.css文件并引入基础模块(这些照着文档就可以)

# 命令较长,可以写在package.json script下面
npx tailwindcss -i ./src/index.css -o ./dist/output.css --watch

然后在模板文件通过 link 标签引入dist/output.css即可

接下来就是愉快的敲代码环节了。先拿个熟悉的 media 组件试试

<div class="flex w-96 p-2 m-10 bg-red-50">
    <div class="w-20 h-20 bg-slate-300 rounded-full mr-4 flex-shrink-0"></div>
    <div class="h-20 flex-grow bg-blue-100 p-2 text-gray-500 rounded">
        <h1 class="text-lg">title</h1>
        <p class="text-sm break-all overflow-ellipsis overflow-hidden">
            This is content.....
        </p>
    </div>
</div>

可以观察一下dist/output.css中的 css 代码,可以看见,tailwind 很智能地只包含了基础的样式重置类和在 HTML 模板中使用到的类名。

同一份 UI,按照我平常的编码习惯,先通过 BEM 定义 html 结构

<div class="media">
    <div class="media_sd"></div>
    <div class="media_mn">
        <h1 class="media_tt"></h1>
        <p class="media_ct"></p>
    </div>
</div>

然后在scss中堆样式(从 figma 上粘贴过来...)

.media {
    &_sd {
    }
    &_mn {
    }
    &_tt {
    }
    &_ct {
    }
}

两种写法有着非常明显的差异。

3. 在生产环境使用 tailwind

上面提高了原子类的硬伤,本章节看看如何解决这个问题。

3.1. 约定设计规范

使用 tailwind,需要借助智能提示和文档,找到对应的原子类名,然后写到 class 上

使用常规写法,需要先定义类名,然后为每个类名填充不同的样式,比如media_tt的字体是多大、media_ct的字体是多大。

常规写法的一个问题是,部分细小的样式冗余,我们往往会忽略这些重复,比如

.media {
    &_tt {
        font-size: 20px;
    }
}

现在有另外某个按钮类的字体,如果也是 20px

.btn-xxx {
    font-size: 20px;
}

在业务开发中,我们一般不会将这两行样式抽离出来复用。因为我们无法保证其他地方会出现另外的字号,如果把 12px~100px 的字体大小都枚举出来复用,感觉也没啥必要,就是多了一行样式规则嘛

造成这个原因的本质是缺少设计的规范。在组件库的开发中,字号、颜色等都有一些规范,比如流行的ElementUI等组件库

而在业务开发中,如果前端没有跟设计师约定好一套规范,就很容易造成样式的冗余。

换言之,如果有了约定的设计规范,就可以进行样式复用

$text-lg: 20px;
.media {
    &_tt {
        font-size: $text-lg;
    }
}
.btn-xxx {
    font-size: $text-lg;
}

而在 tailwind 中,就可以定义出一个原子类text-lg,当然具体的数值都是可以按照约定进行配置的

.text-lg {
    font-size: 20px;
}

当设计规范了然于胸之后,甚至不需要再进行切图和标注了,前端也可以做到一拿到设计图,就知道这个标题字号、颜色,这个盒子内边距、外边距之类的属性,从而节省大量的时间。

3.2. @apply 合并样式

前面提到了原子类的另一个问题:维护繁琐

原子类维护有三种,

  • 增加原子类,比如对某些特定的标签增加一个阴影效果
  • 移除原子类,比如之前 10 个标签上写了.text-lg,现在其中 5 个标签要改成text-md
  • 增加样式,比如希望对某些标签.text-lg的元素增加一个 hover 样式

由于原子类没有语义化,是通过组合的形式构建样式,因此对于原子类而言,无法将类名选择器与该选择器对应的标签一一对应起来,这就丢弃了 CSS 选择器本身提供的样式复用效果,导致维护起来就变得困难。

举个例子,比如一个.btn类,有 10 条样式规则,现在要对这个 btn 的类增加一个 hover 效果是非常简单的,一个组合选择器就可以了

.btn:hover {
    // ...
}

因此,要想使用 css 的选择来维护样式表,我们就不能抛弃语义化类名;但是我们也想使用原子类的样式复用等属性,鱼与熊掌可以兼得吗?

借助 tailwind 的@apply指令可以将一堆原子类合并到一个独立的类名中。

@layer components {
    .btn-primary {
        @apply py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75;
    }
}
<button
    class="py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75"
>
    button 1
</button>
<button class="btn-primary">button 2</button>

看一下控制台

可见是将原子类通过类似于 scss 中@mixin的形式合并在一起,即将原子类定义成最小可复用的混合器,

@mixin py-2 {
}
@mixin px-4 {
}

// ...

.btn-primary {
    @inclue py-2;
    @inclue px-4;
    //...
}

因此 tailwind 的@apply最后输出的 css 样式表还是会出现样式冗余的情况。也许可以通过@extend通过分组选择器进行样式复用,

.py-2 {
}
.px-4 {
}

// ...

.btn-primary {
    @extend py-2;
    @extend px-4;
    //...
}

最后输出的样式表是

.py-2,
.btn-primary {
}
.px-4,
.btn-primary {
}

看起来可能也节省不了多少体积的样子~不过这样确实算是利用了原子类的样式复用和语义化类名容易维护的优点。

3.3. 组件化开发

在前端组件化盛行的现在,将 HTML 组件化也是大势所趋,

const Button = ({ children }) => {
    return (
        <button class="py-2 px-4 bg-blue-500 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75">
            {children}
        </button>
    );
};

在其他地方使用的是<Button></Button>组件,当需要统一修改这个组件的样式时,直接修改组件源文件即可,也不用再到处去找原子类然后处理。

组件化不仅避免了原子类难以维护的问题,也避免了原子类在 CSS 文件缩小的同时却增大了 HTML 文件(如果不考虑 SSR 的话)。

4. 小结

时代在变化,确实应当对原子类刮目相看了。

没有任何一种技术是银弹,可以一劳永逸地解决所有问题。我们能做的只有权衡各种利弊,选择适合自己、适合项目的工具。

接下来打算有时间的话,将博客用 tailwind 重构一下(毕竟博客也停留在v0.7版本两三年了),深入体验一下 tailwind。

但也许 tailwind 也快过时了,毕竟下一代原子类windicssunocss都已经出来了~技术革新的真快啊