likes
comments
collection
share

原子类 CSS 的前世今生

作者站长头像
站长
· 阅读数 19

state-of-css 2021 中,满意度最高的 CSS 框架就是 Tailwind CSS ,并且近几年一直高居榜首。今天就来一探究竟吧!

可以看出Tailwind CSS 在过去两年的流行程度一直保持一个高速增长的态势,可见其原子化 CSS 的特性的确很好用。

原子类 CSS 的前世今生

css 思想的演进。来源:Tailwind CSS 作者 adamwathan.me/css-utility…

阶段: 语义化的 class

最开始,最佳实践之一是“关注点分离”(separation of concerns),也就是html 只包含结构, CSS 只包含样式。HTML 的 class 应该有自己的语义,不应该把样式或者逻辑附在上面。

<p class="text-center">
    Hello there!
</p>

看到.text-center了吗?

text-center 是一个样式,但是放在了 html 里。所以这段代码违反了“样式与结构分离”,因为我们让样式信息渗入了我们的 HTML。

相反,推荐的方法是根据元素的内容为元素命名,并使用这些类作为CSS 来设置标记样式:

<style>.greeting {
    text-align: center;
}
</style>

<p class="greeting">
    Hello there!
</p>

一个典型例子是 CSS Zen Garden,换换 class 就给网站换上不同的风格。

工作流程

这个时候你的的工作流程:

  1. 编写一些新 UI 所需的HTML:
<div>
  <img src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
  <div>
    <h2>Adam Wathan</h2>
    <p>
      Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
    </p>
  </div>
</div>
  1. 根据内容添加一两个描述性 class:
- <div>
+ <div class="author-bio">
    <img src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
    <div>
      <h2>Adam Wathan</h2>
      <p>
        Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
      </p>
    </div>
  </div>
  1. 在 CSS/Less/Sass 中使用这些 class 来设置新标记的样式:
.author-bio {
  background-color: white;
  border: 1px solid hsl(0,0%,85%);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  overflow: hidden;
  > img {
    display: block;
    width: 100%;
    height: auto;
  }
  > div {
    padding: 1rem;
    > h2 {
      font-size: 1.25rem;
      color: rgba(0,0,0,0.8);
    }
    > p {
      font-size: 1rem;
      color: rgba(0,0,0,0.75);
      line-height: 1.5;
    }
  }
}

最终结果:

This content is only supported in a Feishu Docs

问题

这种方法一开始看来很直观,但是仔细想想还是有一些问题。

例子中,用了 scss 提供的层级选择器写法。这是为了方便定位到元素。但写着写着,不算是又写了一遍 html 层级结构吗?

但 CSS 和 HTML 之间仍然存在非常明显的耦合,CSS 就像我们标记的一面镜子,使用嵌套的 CSS 选择器完美地反映了 HTML 结构。

所以层级选择器乃至后代选择器的写法,带来了不完整的「分离」实践:

  • html 不关注样式

  • 但 css 文件却关注着 html 结构

第二阶段:将样式与结构分离

有什么办法能够解耦呢?

那就不得不提到大名鼎鼎的 Block Element Modifer,简称 BEM

块(block)、元素(element)、修饰符(modifier)的简写,由 Yandex 团队提出的一种前端 CSS 命名方法论。

采用类似 BEM 的方法, html 会变成这样:

<div class="author-bio">
  <img class="author-bio__image" src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
  <div class="author-bio__content">
    <h2 class="author-bio__name">Adam Wathan</h2>
    <p class="author-bio__body">
      Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
    </p>
  </div>
</div>

... CSS 像这样:

.author-bio {
  background-color: white;
  border: 1px solid hsl(0,0%,85%);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  overflow: hidden;
}
.author-bio__image {
  display: block;
  width: 100%;
  height: auto;
}
.author-bio__content {
  padding: 1rem;
}
.author-bio__name {
  font-size: 1.25rem;
  color: rgba(0,0,0,0.8);
}
.author-bio__body {
  font-size: 1rem;
  color: rgba(0,0,0,0.75);
  line-height: 1.5;
}

This content is only supported in a Feishu Docs

那么限制不用后代选择器,通过 bem class 命名规范来尽可能做到精确定位需要装饰的元素,效果如何?

通过写 .author-boe__name 这样的类名,成功让 css 选择器不再关注 html 结构了!

某种程度上少关注了 html;

但 block-element__module 本质也是映射了一个区块的结构

样式复用

下面问题来了,图中有两种语义不同,但结构相似的模块。如何复用它们之间的样式?

原子类 CSS 的前世今生

首先,之前定义的 .author-bio 系列 class,肯定不能直接用在这个文章预览的模块上,因为不符合语义。

选项 1:copy-paste

不复用。改改类名,copy 出一整份 .article-preview 系列的样式。

  • 不符合 DRY(Don't Repeat Yourself)原则
  • 如果以后卡片的风格需要调整,会修改多处。比如下方内容的 padding,要修改两处。
.article-preview {
  background-color: white;
  border: 1px solid hsl(0,0%,85%);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  overflow: hidden;
}
.article-preview__image {
  display: block;
  width: 100%;
  height: auto;
}
.article-preview__content {
  padding: 1rem;
}
.article-preview__title {
  font-size: 1.25rem;
  color: rgba(0,0,0,0.8);
}
.article-preview__body {
  font-size: 1rem;
  color: rgba(0,0,0,0.75);
  line-height: 1.5;
}

选项 2: @extend

也不用直接 copy,一般 css 预处理器都有诸如 @extend 这样的语法,可以让一个选择器内的样式继承另一个选择器。

.article-preview {
  @extend .author-bio;
}
.article-preview__image {
  @extend .author-bio__image;
}
.article-preview__content {
  @extend .author-bio__content;
}
.article-preview__title {
  @extend .author-bio__name;
}
.article-preview__body {
  @extend .author-bio__body;
}

This content is only supported in a Feishu Docs

使用@extend一般不建议 使用,比如在毫无语义关系的两者间建立依赖、样式优先级混乱等等问题。

选项 3:抽象出一套只关心样式的 class

从“语义”的角度来看,我们.author-bio.article-preview组件没有任何共同之处。

但从设计的角度来看,它们有很多共同点。

因此,我们可以抽象出一套只关心样式的 class,来进行样式复用。

我们称它为.media-card

这是 CSS:

<!-- 原 author-bio -->
<div class="media-card">
  <img class="media-card__image" src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
    <div class="media-card__content">
      <h2 class="media-card__title">Adam Wathan</h2>
      <p class="media-card__body">
        Adam is a rad dude who likes TDD, Active Record, and garlic bread with cheese. He also hosts a decent podcast and has never had a really great haircut.
      </p>
    </div>
</div>
<!-- 原 article-preview -->
<div class="media-card">
  <img class="media-card__image" src="https://i.vimeocdn.com/video/585037904_1280x720.webp" alt="">
  <div class="media-card__content">  
    <h2 class="media-card__title">Stubbing Eloquent Relations for Faster Tests</h2>  
    <p class="media-card__body">  
      In this quick blog post and screencast, I share a trick I use to speed up tests that use Eloquent relationships but don't really depend on database functionality.
    </p>
  </div>
</div>
<!-- 样式 -->
<style>
.media-card {
  background-color: white;
  border: 1px solid hsl(0,0%,85%);
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  overflow: hidden;
}
.media-card__image {
  display: block;
  width: 100%;
  height: auto;
}
.media-card__content {
  padding: 1rem;
}
.media-card__title {
  font-size: 1.25rem;
  color: rgba(0,0,0,0.8);
}
.media-card__body {
  font-size: 1rem;
  color: rgba(0,0,0,0.75);
  line-height: 1.5;
}
</style>

这种方法的确消除了我们 CSS 中的重复,但是说好的「样式与结构之关注点分离」原则呢?

如果我们想改变作者卡片而不改变文章卡片,该怎么办呢?

比如要给文章卡片换一套展示风格 media-card2,需要去给 html 文件加上对应的 class 。

小结

总结一下,如果是原来的语义化类名写法,如果我们想复用样式:

  • 写必要的 html 结构

  • 挂上一套设计好的语义化的类名

  • 打开 css 文件,走硬复制或 @extend 之类的路

如果是的非语义化命名 .media-card 写法:

  • 写必要的 html 结构
  • 挂上 .media-card 类名

这样,不仅减少了类名的思考,而且我们只改一处代码。

相比于第一种「关注点分离」的语义化写法,是不是更友好了?

重新思考关注点分离 separation of concerns

那么「关注点分离」到底是为了什么?语义化的类名规范,就是对这一原则的全部诠释吗?

我们得重新思考 html 与 css 之间的连接关系,也对应了不同 class 的设计。

大致有两种角度:

关注点分离 css 依赖于 html(阶段一)

这种角度下,class 只需关心 html 内容的语义即可。根据内容命名 class,比如 .author-bio

这种结构下,html 很容易被重新更新 class 样式,而 css 很难被复用。

典型例子就是之前提到过的 www.csszengarden.com/

混合关注 html 依赖于 css(阶段二的选项三)

类名根据其想表达的样式设计模式来设计,如 .media-card (多媒体卡片);html 则要参考当前的设计系统,挑选合适的样式挂载到内容上。

这种结构下,css 复用是很自然的事;而 html 要改样式只得改代码。

典型例子就是 Bootstrap 这样的样式框架。

所以耦合是必然存在的(class html),两种角度都做到了一定程度的「关注点分离」,在实现这一原则上没有优劣之分。

我们再考虑一下,对你的网站来说,是方便切换样式重要;还是复用样式代码重要?

最后,html 语义前端架构 这篇文章深刻影响了 tailwind 作者,并彻底转向基于「复用样式」的 class 实践。简单摘录一点内容:

  • html/jsx 的元素标签、内容本身已经反映其内容的语义;
  • 对于代码维护者而言,下面的 .news class 能带来的有效信息极其有限;其所承载的样式也基本无法被复用到其他场景。
export default function News({newsList}) {
  return <div class="news"> // class news 其实是语义重复
    <h2>News</h2>
    <ul>
         {newsList.map(n => <li>{n}</li>)}
    </ul>
  </div>
}

Tailwind CSS 优势

tailwindcss.com/

统一设计规范

首先,设计同学在出设计稿的时候都是有自己的规范的,例如边距只会是4px的倍数,字体只会从常规大小里面去取:

原子类 CSS 的前世今生

颜色,有一套标准的模版:

原子类 CSS 的前世今生

而 Tailwind 支持配置自己的设计规范。

在掌握了封装 CSS class 这种技术之后,我们如鱼得水。而且就算后续设计修改了某一种字体的大小,或者某一种颜色的值时,我们都能够很快的应对,甚至只需要修改一行代码。

module.exports = {
  content: ['./src/**/*.{html,js}'],
  theme: {
    colors: {
      'blue': '#1fb6ff',
      'purple': '#7e5bef',
      'pink': '#ff49db',
      'orange': '#ff7849',
      'green': '#13ce66',
      'yellow': '#ffc82c',
      'gray-dark': '#273444',
      'gray': '#8492a6',
      'gray-light': '#d3dce6',
    },
    fontFamily: {
      sans: ['Graphik', 'sans-serif'],
      serif: ['Merriweather', 'serif'],
    },
    extend: {
      spacing: {
        '8xl': '96rem',
        '9xl': '128rem',
      },
      borderRadius: {
        '4xl': '2rem',
      }
    }
  },
}

除此之外,Tailwind还有一个极大的优势,就是我们可以方便的使用config的方式,来快速扩展和修改基础样式,例如我想新增一个叫做flex-center的样式,这个样式具有flex,align-items: center和justify-content: center的属性,可以直接这样写:

// tailwind.config.js
const plugin = require('tailwindcss/plugin');

/** @type { import('@types/tailwindcss/tailwind-config').TailwindConfig } */ module.exports = {
  plugins: [
    plugin(function ({ addUtilities }) {
      const newUtilities = {
        '.flex-center': {
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        },
      };

      addUtilities(newUtilities);
    }),
  ],
};

再也不用花时间想命名了

原子类 CSS 的前世今生

命名可以算是开发中最难的事情之一,尤其是在组件化开发已经深入人心的今天,你其实完全没必要给你的 div 起一个有意义的名字。使用这个组件的页面并不会关心你组件的顶部叫 header,底部叫 footer(除非你是些基础组件需要给外界复用),你只需要把样式放上去就好了。

你起过多少 wrapper、container、content、box、section、fragment 这种没意义的 className?

同样一段代码,不用起名字可以少掉不少头发

减少代码

所有使用 CSS(包括 CSS modules)的解决方案其实都有一个问题,就是不好删代码:你很难确定这段样式是不是真的没用了,直到出线上事故为止。使用 tailwind css 你可以让样式到死都跟着组件走,组件删了样式也就去掉了,几乎零成本的降低了冗余代码的可能性。

常见问题

Q:类名太长了

解决方法 @apply Functions & Directives - Tailwind CSS

Q:和行内 CSS 有什么区别?

  1. 支持伪类
<div class="hover:bg-blue-500"></div>
  1. 支持响应式
<div class="w-5 lg:w-10"></div> // 屏幕大于 1024px 宽时设更宽的宽度
  1. 规范你的设计体系

使用 tailwind 带来的整套设计系统,能让你的网站的整体布局、颜色、字体风格更加和谐和一致。让开发者随意增添 css 规则,哪怕一些知名网站也会难以管理其使用的字体大小规范。

  • GitLab: 402 text colors, 239 background colors, 59 font sizes

  • Buffer: 124 text colors, 86 background colors, 54 font sizes

  • HelpScout: 198 text colors, 133 background colors, 67 font sizes

  • Gumroad: 91 text colors, 28 background colors, 48 font sizes

  • Stripe: 189 text colors, 90 background colors, 35 font sizes

  • GitHub: 163 text colors, 147 background colors, 56 font sizes

  • ConvertKit: 128 text colors, 124 background colors, 70 font sizes、

Q:可以自己写 class 吗?

当然可以。

有些场景我们确实不希望在 html/jsx 里分离出样式相关的内容;此时,我们依然可以抽象出传统语义化的 class,并在 css 内 apply tailwind 的规则。这样依然可以享受到 tailwindcss 的绝大多数好处。

<!-- Using utilities -->
<button class="py-2 px-4 font-semibold rounded-lg shadow-md text-white bg-green-500 hover:bg-green-700">
  Click me
</button>
<!-- Extracting classes using @apply -->
<button class="btn btn-green"> 
  Button
</button>
<style>
  .btn {
    @apply py-2 px-4 font-semibold rounded-lg shadow-md;
  }
  .btn-green {
    @apply text-white bg-green-500 hover:bg-green-700;
  }
</style>

但在基于组件的 web 框架中,一般我们会这样抽象 button。有点类似 styledComponent 的做法,但还可以完整地使用组件能力。

function ConfirmButton() {
  // logic
  return <button class="py-2 px-4 font-semibold rounded-lg shadow-md text-white bg-green-500 hover:bg-green-700">
    Click me
  </button>
}

Q:造成新的记忆负担?

这个问题就仁者见仁智者见智了,在 Vue 的 template 语法中也经常出现此类问题,很多人会对一些命名上的约定,特别是自己不太喜欢的约定天然排斥,这也无可厚非。

解决方法 IntelliSense- Tailwind CSS,通过插件降低学习成本 Tailwind CSS IntelliSense 、Tailwind CSS Transpiler。

插件提供了预览提示功能,减少记忆负担。

原子类 CSS 的前世今生

Figma 也有对应的 Tailwind 插件。

随便找一个区域,选中,插件,Figma to Code, Tailwind 2。

原子类 CSS 的前世今生

Q:初期很爽,但是后期维护困难?

  • 团队有统一的规范,更易于维护。
  • 调试,可轻松使用  chrome ``devtools,还是可以一眼望到底的,而且没有以前各种 class 存在属性重复覆盖,造成调试困难,从下图可看出 tailwindcss 调试一目了然

原子类 CSS 的前世今生

适用场景

总体上,如果是基础组件开发或者是“设计和开发结合在一起”的开发场景比较适合Tailwind这种原子化class去开发。Tailwind提供的是设计规范,相当于理念层面的素材。比如一个button组件,如果完全自己设计样式,首先需要对button进行拆解,它的边框、字体、阴影、圆角等等,你要从框架里面把这些元素挑出来进行组合(堆积木的感觉)。

所以它更适合有统一设计的团队,因为对于这些团队而言,写好设计规范,接下来就是拼凑,如果设计师直接通过组合得出效果,对研发人员来说也节省了时间。

以乐小活为例,目前我们的项目大部分是用 ByDeign 组件默认封装的样式,只有当默认样式与设计稿不符合时,需要自己覆盖。我认为Tailwind暂不适用目前 PC 项目的原因有以下几点:

  1. 目前项目处于不断提需求、功能开发阶段,由于迭代速度快,开箱即用的组件和配套的组件化样式更满足于当前情况,前端使用的时候不需要关注太多样式细节,而Tailwind有一定学习成本,且团队所有人都对 className 达成一定共识时,才能更好发挥它的作用(比如当设计稿变更时,任意人员都可以更改原子类映射的值,降低协作成本)。

  2. 目前项目样式方面我认为一个比较明显的痛点,是需要写较多样式覆盖的代码,甚至有时为了使样式生效,需要“挖”比较深,但若仅仅将组件默认样式换成组件默认样式+Tailwind并不能解决样式覆盖问题

  3. Tailwind样式的单位是rem,rem 是相对于根元素 html 的 font-size 来计算的,所以Tailwind的响应式做的很好,但是目前项目对响应式的要求没那么高,且设计稿一般是给定固定宽高,在这种需求下,用Tailwind设置宽高还需自己换算一下单位,反而有些麻烦

而 Tailwind 比较适合项目工程较大(页面多)、有标准化设计(色卡、字体、sizing、layout)、重复率高、页面结构较简单的项目,目前看来官网、C端的业务目前来看比较符合这些特点,而 SaaS 、后台管理则不适用。