FlavorCSS: 一种运行时编译原子样式的方案

2,485 阅读6分钟

初见

市面上有一种CSS方案叫做 Atomic CSS,如果你用过它;有的人会觉得非常荒唐,重新回到他习惯的BEM风格怀抱中。

有的人会爱上它。

我就是后者。

一个极端一点的,复杂一点的Atomic CSS,它长这个样子:

<button class="
  px-lg py-sm radius-sm m-md pc:m-lg
  bg-blue-600 hover:bg-blue-500 active:bg-blue-400 
  shadow hover:shadow-lg 
  transform hover:move-y--px active:move-y-px 
  c-white cursor-pointer transition-500"
>
  Touch Me
</button>

我相信,正常的前端小伙伴见到这一大块的 class,会很头痛,乍眼一看和天书没区别,立刻换成了下面这块代码:

<button class="im-a-button">Touch Me</button>

这多简洁,心里舒坦多了。

但是这其实并没有完整解决问题,因为我们还是需要添加下面这段css代码才等价于刚刚那串头疼的代码:

/* 首先我们需要为我们的项目设计一套单位规范和颜色规范 */
:root {
    --px: 1px;
    --sm: 2px;
    --md: 8px;
    --lg: 16px;
    --blue-600: #6677ff;
    --blue-500: #4455ff;
    --blue-400: #3355ee;
    --shadow-color: #000000;
    --shadow-opacity: 0.15;
    --white: #fff;
    --ease: cubic-bezier(0.23, 1, 0.32, 1);
}

.im-a-button {
    /* 所有单位使用 css-values 是为了更好的统一整体风格、尺寸,并且更容易的调整整个项目的样式 */
    padding: var(--sm) var(--lg) var(--sm) var(--lg);
    border-radius: var(--sm);
    margin: var(--md);
    background: var(--blue-600);
    box-shadow: 0 1px 3px 0 rgba(var(--shadow-color), var(--shadow-opacity)), 0 1px 2px 0 rgba(var(--shadow-color), calc(var(--shadow-opacity) / 2));
    /* 白色也使用 css values 是为了更好的做 dark 模式 */
    color: var(--white);
    transition: all 500ms var(--ease);
    will-change: transform;
}

@media (min-width: 640px) {
    .im-a-button {
        margin: var(--lg);
    }
    
    /* hover 在移动端会导致hover的样式被touch响应后不被释放,为了更好的体验建议 hover 仅在桌面端起效果 */
    .im-a-button:hover {
        background: var(--blue-500);
        box-shadow: 0 10px 15px -3px rgba(var(--shadow-color), var(--shadow-opacity)), 0 4px 6px -2px rgba(var(--shadow-color), calc(var(--shadow-opacity) / 2));
        tramsform: translateY(calc(0px - var(--px)));
        margin: var(--2xl);
    }    
}

.im-a-button:active {
    background: var(--blue-400);
    box-shadow: 0 10px 15px -3px rgba(var(--shadow-color), var(--shadow-opacity)), 0 4px 6px -2px rgba(var(--shadow-color), calc(var(--shadow-opacity) / 2));
    tramsform: translateY(var(--px));
}  

当我们看到这个 css 代码块,才回到了我们真正的场景:

  • css 的代码量是线性增加的,我们不但得写不少css代码,最后还需要对项目的 css 做拆封,以提高首屏的加载时间
  • css 是需要预先做设计和规范的,并且还需要为每个参与者做一定的培训/教育
  • 大块的css阅读其实体验并不好,一个包含媒体查询、伪类、动画的单一元素的css样式甚至无法在一屏显示完
  • css values 非常棒,但是如果贯穿项目,它会影响整个 css 的阅读体验和 css 文件体积
  • 编写 css 需要取非常多的名字,比我们整个项目的变量名还多,而且为了减少 css 污染,我们还需要引入 css-modules 或者 BEM,会继续添加心智负担

现在我们再重新阅读这块 Atomic CSS 风格的代码:

<button class="
  px-lg py-sm radius-sm m-md pc:m-lg
  bg-blue-600 hover:bg-blue-500 active:bg-blue-400 
  shadow hover:shadow-lg 
  transform hover:move-y--px active:move-y-px 
  c-white cursor-pointer transition-500"
>
  Touch Me
</button>

我们可以发现Atomic CSS的阅读体验还是不错的(50步笑100步),并且我们现在大概可以猜出上面大部分 class 的含义。

那么如果我们有一个非常好的规范,设计好的,有逻辑 Atomic CSS 风格的样式库,那我们可以非常高效的编写我们的界面,并且解决了我们刚刚提出来的常见问题。

这就是作者爱上 Atomic CSS 风格的原因, 一套好的 Atomic CSS 相当于 css 的 vim.

快乐

  • 用了 Atomic CSS 之后,我们就不需要再去苦思冥想一堆 BEM 名称了
  • 我们可以用非常有简短、有描述性的class类直接描述我们的UI,绘制界面的效率得到一个大的提升。
  • 我们可以非常容易的修改整个项目的风格
  • 我们的项目仅仅剩下下 Javascript/Typescript 文件
  • 由于规范统一,我们甚至可以直接复制其他项目的Atomic CSS风格的 className

痛苦

市面最流行的Atomic CSSTailwind CSS.

如果大伙在掘金、某乎、油管上搜索一下,有不少的拥护者,我就是其中之一。

但是用的久了,慢慢发现它的一个核心痛点:我们如何确定原子类个数的边界?

Atomic CSS 的本质是预设了一系列有规则的 css 样式,但是预设多少样式才足够我们不需要编写95%的常规css代码呢?这基本上要把我们可能的组合枚举出来,即使在有规范限定的前提下大概需要2.5MBcss 代码量,虽然当中 99% Atomic CSS 都是用不到的,而这个代码量之大导致于我们不可能直接使用这类方案。

为此有一个使用 Purgecss 去清理未使用 CSS 的方案,可以在编译期间对我们的业务代码进行正则匹配,仅保留我们用到的 Atomic CSS,最后大概只需要 10~50kbcss 代码。

这也是有代价的,我们需要注意 class 的书写方式,如果我们使用变量的方式动态创建 className, Purgecss 就无法在编译期间通过正则找到我们使用过的此类 css。

并且由于一般情况我们不合适在开发环境做这个动作,不然我们每添编一写一个组件就得编译一次才能看到效果,而我们若只在编译生产代码时才做这个动作其实是有风险的,若有正则未匹配到的对象,可能会导致不可预知的生产bug,例如一个模块无法呈现。

Flavorcss 的方案很好的规避了如何确定原子类个数的边界这个问题,Flavorcss 是在运行时逐步编译绝大部分 Atomic Class, 并且整个编译和插入过程做了很好的分段编译,让用户无感知。

Flavorcss 是零配置开箱即用的,我们只需要引入:

<script src="https://unpkg.com/flavorcss@0.4.0/umd/index.js"></script>

或者:

yarn add flavorcss
import 'flavorcss'

Flavorcss 会在页面渲染过程中动态创建 Atomic CSS

踌躇

其他问题,Tailwind CSS 的作者写过一片对 Atomic CSS 风格的思考文章,可以建议扩展阅读:

adamwathan.me/css-utility…

这里我先简要回答一部分比较常见的问题

  • 为什么我们不直接写内联样式?
    • 因为内联样式无法直接编写媒体查询和伪类
  • css-in-js 比起这种方案如何?
    • css-in-js 并没有默认帮我们设计好尺寸、颜色规范,也并没有大幅度减少总代码量,把css迁移·到了js,为的是提供更多的控制力度,它们的目的不一样。
  • 性能如何
    • 减少了首次加载大量css的开销,增加了运行时动态添加原子类的开销
    • 运行时添加原子类是没有感知开销的,体验可以浏览官网。

Flavorcss

以下是 Flavorcss 的官方文档,上面有所有的样式说明和描述,读者可以在上面动态编辑示例,以更简单的了解所有规范。Flavorcss 承诺现有的 API 不会更改。

flavor.writeflowy.com/