对 CSS 样式定义作语义化 API 联想支持
技术CSS, DX

对 CSS 样式定义作语义化 API 联想支持

Published On
| 更新于
Updated On
12分钟阅读概述:作者通过对比传统的 CSS Modules 样式定义方案、当下流行的原子类方案,进而阐述了对样式定义做语义化 API 联想支持的必要性和便捷性,并给出简单实现的参考例

(封面图来自 Class Variance Authority | CVA

最前

推荐关注下面两个第三方库 👍 :

背景

行文开始内容有必要适当离题,先通过对传统的 CSS Modules 样式定义方案、当下国外流行的原子类方案作简单对比,来分析当下普遍的 CSS 样式定义的开发痛点具体在哪里。

什么是原子类

什么是原子类,一言以蔽之,一个类名下是一行 CSS 定义(多数情况下如此)。当前典型的原子类第三方库中:tailwindcss 的原子类名 absolute 的定义如下:

.absolute {
  position: absolute;
}

而开发者要落地使用像这样的原子类库,就需要将库提供的初始原子类名组合到一起。

更多的关于原子类库的使用细节/综合体验/可能疑问(比如:原子类的组合和通过 style={{/* ... */}书写内联样式有何区别)则不在本文主题下,这里不再赘述。

通过原子类定义样式

虽然 tailwindcss 这一 UI 库可以预见的,在国内公司中采用的并不多。

倒不是有什么上手难度,恰恰相反,这样的原子类库实际上手很友好,且对 tailwindcss 推出 JIT 模式后,书写体验上更加的便利亲民。

社区里一些后端开发人员就表达过这样的观点,临时处理前端页面时,更愿意使用像这样的原子类库,而不是原始的 CSS 样式定义语句;

而吾辈简要分析,国内无法推广开来这样的原子类库的原因。主要两点,其一,不同开发人员对其侵入项目代码后,项目后续长期的可维护性上存在分歧;

其二,就是原子类的长类名组合会极大的“污染” HTML / JSX / TSX 代码。呈现在代码编辑界面的效果就是,或许本来主要负责着逻的组件代码,其间大量充斥着原子类名

第二个原因,也是吾辈之前一直犹豫要不要在自己的个人项目中使用这类原子类库来定义页面样式的主要原因。同时自己熟悉于使用 CSS/SCSS Modules 方案。

来看下面的简单例子,一个动效按钮,如果使用传统的 CSS/SCSS Modules 方案, 则 JSX 代码如下:

  1. CSS Modules 方案
button.jsx
// import ...
import React from "react";

import styles from "~/path/to/x.module.scss";

export const Button = ({onClick}) => (
  <button className={styles.wrapper} onClick={onClick}>
    <span className={styles.placeholder}></span>
    <span className={styles.buttonText}>SIGN IN</span>
  </button>
);

样式表:

x.module.css

而如果使用原子类,则可能呈现如下(如果你厌恶这样的风格,完全可以把这样的冗长类名说成是代码主体无关的样式定义“噪声”):

button.jsx
// import ...
import React from "react";

import styles from "~/path/to/x.module.scss";

export const Button = ({onClick}) => (
  <button
    className="group relative inline-block border border-red-400 px-4 py-2 font-bold"
  >
    <span
      className={`
        absolute inset-0 h-full w-full translate-x-2 translate-y-2
        bg-gradient-to-br from-red-400 to-red-400 transition-all
        group-hover:translate-x-1 group-hover:translate-y-1
        group-hover:from-red-300 group-hover:to-blue-400
      `}
    />
    <span className="relative text-gray-900 transition-colors group-hover:text-white">
      SIGN IN
    </span>
  </button>
);

文章后面给出的对 CSS 样式定义作语义化 API 联想支持的简易方案,就能够有效的解决上面提到的“噪声”问题。


🧐 以上代码,带来的动效按钮为如下的显示效果

使用原子类的痛点

显然的,各花入各眼,有的开发者觉得这样的长类名在代码里的充斥也没什么。或者有抱怨的,也容易被官网推崇的理念说服:

吾辈依着记忆大致转述如:开发者无需抱怨类名过长问题,因为当下的业界开发风格是组件化开发风格,你的长类名多数情况下在最佳实践方案下应该出现在对“单元组件/样式组件”的定义上,而在更顶层的页面组件,则负责对 UI 组件的业务调用上,所以这种长类名出现在前端各处页面实现代码里的情况——实际很少或不会出现。

但吾辈至今还是对这种类名充斥在代码里的风格是不喜的。

这也是一部分人始终支持如 CSS Modules 这样的样式定义方案的原因:代码来得更加清爽。

使用传统自定义类名和使用原子类的通用痛点

两种方案的通用痛点,在吾辈看来是缺少联想和提示,这里的联想不是指 CSS Modules 方案下: styles.someName 这样的联想,而是指:能够支持对样式定义做语义化的 API 联想

当然,以下给出的简易原始的方案实现,并不是对原子类、或传统 CSS Modules 的替代,而是基于两者的基础上,更“上一层”的应用。

吾辈将其描述为:对外暴露一个“样式变体函数”。

正题

原子类名方案下

仍然以 tailwindcss 举例, 官方虽然提供了 @apply 的语法糖,支持我们将长类名从页面代码中抽离:

<button class="my-atom">CLICK</button>
.my-atom {
  @apply inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-col4ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50;
}

但是这仍然属于传统的自定义类名方案。

这里我们试着如此定义:

buttonVariants.js
import clsx from "clsx";

export function buttonVariants({ intent = "default", size = "default" }) {
  const mapper = {
    intentCtx: {
      default: "bg-primary text-primary-foreground hover:bg-primary/90",
      destructive:
        "bg-destructive text-destructive-foreground hover:bg-destructive/90",
    },
    sizeCtx: {
      default: "h-10 px-4 py-2",
      sm: "h-9 rounded-sm px-3",
      lg: "h-11 rounded-sm px-8",
    },
  };

  return clsx(mapper.intentCtx[intent], mapper.sizeCtx[size]);
}

然后在样式组件或其他层级组件中近似下面的风格调用:

code block

直观可见的,通过buttonVariants 这样的样式变体函数,能够支持我们在样式定义上,通过语义化更明显、可读性更友好的 intent, size 等约定字段,来实现样式的自定义。同时有效消除了原本原子类样式定义方案下——冗长类名造成的代码主体无关的庞杂噪声。

传统自定义类名方案下

而对于传统的 CSS Modules 自定义类名方案下,我们同样可以用这样的思路,让代码书写获得样式定义的语义化 API 联想支持:

buttonVariants.js
import clsx from "clsx";
import styles from "~/path/to/styles/x.module.scss";

export function buttonVariants({ intent = "default", size = "default" }) {
  const mapper = {
    intentCtx: {
      default: styles.buttonDefault,
      destructive: styles.buttonDestructive,
    },
    sizeCtx: {
      default: styles.buttonSizeDefault,
      sm: styles.buttonSizeSm,
      lg: styles.buttonSizeLg,
    },
  };

  return clsx(mapper.intentCtx[intent], mapper.sizeCtx[size]);
}

显然的,外部的调用风格同上保持不变。

第三方库分享

目前对于这种方案实践的不错的第三方库,吾辈推荐/分享:Class Variance Authority。很好的匹配 Typescript 下开发。

本站点的样式定义也主要依托于 cva 对样式变体函数的高程度支持 👍。

延伸

关注 API 颗粒度问题

上面给出的方案,仍然以 button 组件为例,最终调用效果如:

() => (
  <button className={buttonVariants({ intent: "default" })}>CLICK ME</button>
);

而一些 UI 库也给出了另一种对样式定义作语义化 API 联想风格的方案实现, 比如来自 UI 库: chakraUI

() => (
  <Button
    size='md'
    height='48px'
    width='200px'
    border='2px'
    borderColor='green.500'
  >
)

吾辈个人角度并不推崇使用这样的 UI 库,觉得这样的方案存在另一不可忽视弊端:那就是组件对外暴露的,服务于样式定义的 API 颗粒度过细问题。

从吾辈的个人使用偏好上来讲,还是觉得组件对外暴露的 api 中,样式主要就交由如 style 或者 className 或者 visible 这样的字段即可,而其他更多的 api 则应主要服务于业务/逻辑实现。而像上面的示例中,服务于 UI 样式的 api 就过于离散了。

对 CSS 样式定义作语义化 API 联想支持

https://blog.ninoh.cc/blog/a5-css-variants-api[Copy]
本文作者
ninohx96
创建/发布于
Published On
更新/发布于
Updated On
许可协议
CC BY-NC-SA 4.0

转载或引用本文时请遵守“署名-非商业性使用-相同方式共享 4.0 国际”许可协议,注明出处、不得用于商业用途!分发衍生作品时必须采用相同的许可协议。