迈向Atomic CSS-in-JS:从运行时回到编译时

3,012 阅读18分钟

自2016年Styled Component这一代表性的运行时CSS-in-JS库发布以来已经过去了5年多,这期间涌现出大量迥异的方案,并且在发展的过程中各自API设计也趋于接近和稳定,大量的React组件库和项目都深度使用CSS-in-JS,可以算得上是主流的样式方案选择。

而近年来社区又开始流行像Tailwind CSS这类Utility First的方案,基本思想就是预设海量的基本Utility类,一个类通常包含只一条或几条相关的CSS规则,编写时只需要组合复用这些类,无需离开HTML。

image.png

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

并不新鲜的Atomic CSS

在Utility First基础上更激进的做法就是每一个类仅包含一个且唯一的CSS规则,因此大大提升样式复用的程度,CSS体积不会随着组件的增加而线性增加,在各类方案中能够得到最小的CSS体积。代价是增加了HTML体积,但对于大量重复出现的类名,gzip的压缩率很高,所以实际影响不大。
image.png

在Facebook.com的重构中,通过内部的Atomic CSS框架styleX将原本单主页的413 kB减少到全部页面只有74kB(包括深色模式)

这种做法最早由Yahoo!的Thierry Koblentz于2013年提出,并在Yahoo!内部沿用至今。Atomic CSS (ACSS)通过编译器Atomizer解析约定语法的HTML来生成Atomic CSS类

<div class="Mb(4px) D(f)">Hello world!</div>

编译为2个CSS类

.Mb\(4px\) {
  margin-bottom: 4px;
}

.D\(f\) {
  display: flex;
}

除了开头提到的极致体积,Atomic CSS还能解决以下困扰:

  • 样式耦合

在一般的项目中可能并不能直观看出某一块CSS是否还在使用和被多少组件使用,导致谁也不敢轻易改动,随着时间积累这些“死代码”可能越来越多,可能也耦合得越来越深,无形中增加了维护成本,埋下了大量的坑。
而大多Utility First/Atomic CSS框架的编译器能够在构建时只引入实际使用到的CSS,比如著名的tailwind CSS JITwindiCSS以及实验性的unoCSS,移除了组件同时也移除了CSS。

  • 优先级问题(CSS specificity issue)

浏览器通过优先级来判断哪些属性值与一个元素最为相关,从而在该元素上应用这些属性值。优先级是基于不同种类选择器组成的匹配规则。

以上是MDN上的简要定义,实际上浏览器的优先级算法会相当的复杂。
https://specifishity.com/。将X-Y-Z拼在一起就是优先级
而且和CSS的层叠(cascade)密切相关,光从选择器还不一定能判断出到底哪个规则会生效

.blue {
  color: blue;
}

.red {
  color: red;
}

在例子class="blue red"中,实际生效的color取决于他们在样式表中的顺序。

如果要覆盖一个已有样式,通常有以下做法:

  • 粗暴的用内联样式style或者!important来获得最高优先级,但代价是“污染”了样式,没有办法再被覆盖。
  • 编写优先级更高的选择器,但毕竟选择器是由人来编写的,一般不会套用这套复杂算法(要是写个花里胡哨的选择器等同于给后面维护的人埋了个大坑),更多的是朴素地把选择器加长,加嵌套,比如在原来基础上再加一个类选择器。这样操作下来很容易积累出选择器屎山(特别是打算覆盖第三方组件库样式时),而且复杂选择器也意味着加深和HTML的耦合。

image.png
而对于Atomic CSS,产出的所有属性都只对应一个类里的规则,原则上不会有重复的属性出现,所以不用担心样式冲突的问题。只有类选择器意味着优先级始终是0-Y-0这一级别,更容易被覆盖。

当然,著名的BEM (Block.Element.Modifier)规范同样也能解决该类问题。但个人觉得这类规范性的约束可操作性不是很高,就算加了lint也不如编译器生成的可靠。

迈向Atomic CSS-in-JS ⚛️

不过Atomic CSS本身也有不少问题:

  • 一般的Utility/Atomic CSS需要遵循一套命名的约定(或者依赖于编辑器的插件辅助),相当于再学习一门DSL
  • Utility First CSS框架的一次性样式和定制化较为麻烦,比如tailwindCSS中仅提供有限的预设,超出的部分要么去配置主题,要么另外写普通的类,不如CSS-in-JS灵活。

而CSS-in-JS更不用说,相关争论一直没停过:

  • CSS-in-JS需要引入体积巨大的JavaScript运行时来动态插入CSS(styled-component gziped后还有12.7kB)。
  • CSS-in-JS带来额外的性能开销,特别是对于需要频繁更新渲染的组件。并且在React 18的Concurrent Mode中有额外的性能问题,参考这个讨论
  • CSS-in-JS对SSR不友好,比如至今仍有许多CSS-in-JS不兼容React 18 Streaming SSR,其中包括使用最广泛的styled-component(相关issue)。SSR兼容性问题涉及多方面,这里不作展开。
  • Emotion(第二受欢迎的CSS-in-JS库)的活跃维护者,Sam Magura最近发表了why-were-breaking-up-wiht-css-in-js这篇文章,抛弃了Emotion转向使用SASS Module,引发了不少的讨论和担忧。

那么有没有一个关于CSS的“银弹”呢?近期涌现出不少构建时(build time/zero runtime)Atomic CSS-in-JS方案或许是React社区的下一个大趋势。毕竟在上面Sam Magura引发的讨论中,React Core Team就推荐使用基于构建时的CSS-in-JS方案,新版的React文档近期也禁用了CSS-in-JS(PR

其中大规模应用于生产的Atomic CSS-in-JS框架有:

  • react-native-web:2015年Twitter用于twitter.com的重构的一整套React技术栈,包含Atomic样式部分。后面许多都受到这个库的影响。
  • styleX:2017年Meta内部用于重构Facebook.com的方案,暂未开源(21年说21年底开源
  • griffel:微软用于Fluent UI React v9组件库的CSS框架,于2022年初开源

image.png

还有些备受关注,但暂时没有被大规模应用的开源框架:

这里主要介绍下griffelvanilla-extract这两个新兴的CSS框架。

griffel

griffel.js 是微软开源的Atomic CSS-in-JS框架,

值得一提的是,在Fluent UI React v9的样式选型过程中FluentUI团队比较了BEM、CSS Module、CSS-in-JS、Atomic CSS等方案,最后选中了Meta的styleX,但因为styleX迟迟没有开源,最终还是走向了自研griffel.js的道路

image.png
griffel默认情况下是开箱即用运行时的CSS-in-JS,但也提供了基于webpack和babel的AOT编译和实验性的CSS文件提取。

基本用法:

import { makeStyles } from '@griffel/react';

const useClasses = makeStyles({
  icon: { color: 'red', paddingTop: '5px' },
});

function Component() {
  const classes = useClasses();

  return <span className={classes.icon} />;
}

然后griffel会在文档中插入以下样式

.fe3e8s9 {
  color: red;
}

.f10ra9hq {
  padding-top: 4px;
}
  • griffel的核心API makeStyles使用的是Object Syntax而不是一般CSS-in-JS的模板字符串(就像styled-component那样),因此能通过TypeScript带来强类型约束和提示。
  • makeStyles返回的是一个React Hook,通过useContextuseeMemo来全局共享唯一的renderer以及其缓存的样式。只有useClasses首次渲染时才会向文档插入样式
  • griffel的类名哈希算法来自Emotion,将属性名和属性值作为输入来确定哈希值,因此所有Atomic类都有稳定的类名。

image.png
其中关键的renderer定义如下,用于缓存和插入样式。因为是生成的Atomic CSS,所有定义的规则都会被缓存下来,避免重复插入。

export interface GriffelRenderer {
  id: string;
  
  insertionCache: Record<string, StyleBucketName>;

  stylesheets: { [key in StyleBucketName]?: IsomorphicStyleSheet } & Record<string, IsomorphicStyleSheet>;

  insertCSSRules(cssRules: CSSRulesByBucket): void;

  compareMediaQueries(a: string, b: string): number;
}

React 渲染时每次调用useClasses都会去获取最终生成的类名

import { makeStyles as vanillaMakeStyles } from '@griffel/core';

export function makeStyles<Slots extends string | number>(stylesBySlots: Record<Slots, GriffelStyle>) {
  const getStyles = vanillaMakeStyles(stylesBySlots);

  return function useClasses(): Record<Slots, string> {
    const dir = useTextDirection();
    const renderer = useRenderer();

    return getStyles({ dir, renderer });
  };
}

vanillaMakeStyles实际上也有一级缓存,因此只有两级缓存都没有命中,renderer才会真正生成和插入样式

// @griffel/core vanillaMakeStyles(省略了ltr和rtl相关逻辑)
export function makeStyles<Slots extends string | number>(stylesBySlots: StylesBySlots<Slots>) {
  const insertionCache: Record<string, boolean> = {};

  let classesMapBySlot: CSSClassesMapBySlot<Slots> | null = null;
  let cssRules: CSSRulesByBucket | null = null;

  let ltrClassNamesForSlots: Record<Slots, string> | null = null;

  let sourceURL: string | undefined;

  function computeClasses(options: MakeStylesOptions): Record<Slots, string> {
    const { dir, renderer } = options;

    if (classesMapBySlot === null) {
      [classesMapBySlot, cssRules] = resolveStyleRulesForSlots(stylesBySlots);
    }
    
    const rendererId = renderer.id;

    if (ltrClassNamesForSlots === null) {
      ltrClassNamesForSlots = reduceToClassNameForSlots(classesMapBySlot, dir);
    }

    if (insertionCache[rendererId] === undefined) {
     	// 缓存没有命中时插入该CSS规则,保证只有首次渲染才会插入,
    	// renderer本身缓存命中的话也会跳过
      renderer.insertCSSRules(cssRules!);
      insertionCache[rendererId] = true;
    }
    const classNamesForSlots = ltrClassNamesForSlots as Record<Slots, string>)

    return classNamesForSlots;
  }

  return computeClasses;
}

提前编译

griffel另一大特点就是提前编译,目前支持babel和webpack的插件。

配置好webpack后,代码不需要任何改动即可享受到提前编译带来的进一步性能提升

import { makeStyles, shorthands } from "@griffel/react";

const useClasses = makeStyles({
  container: {
    ...shorthands.padding("16px"),
  },
  main: {
    minHeight: "100vh",
    width: '100vw',
  },
  title: {
    color: '#786efe',
    textAlign: "center",
  },
});

同样的makeStyles调用会被@griffel/webpack-loader提前编译为所有定义的Atomic CSS规则和对应的哈希类名。
而且makeStyles本身也会在打包阶段被替换为轻量级的函数__styles,只用于类名的拼接和CSS插入,舍弃了计算、生成和缓存样式相关的运行时代码。

也就是上面的[classesMapBySlot, cssRules] = resolveStyleRulesForSlots(stylesBySlots)部分。

const useClasses = (0, _griffel_react__WEBPACK_IMPORTED_MODULE_1__.__styles)({
  "container": {
    "z8tnut": "fqag9an",
    "z189sj": ["f1gbmcue", "f1rh9g5y"],
    "Byoj8tv": "fp67ikv",
    "uwmqm3": ["f1rh9g5y", "f1gbmcue"]
  },
  "main": {
    "sshi5w": "f1vhk6qx",
    "a9b677": "fr97h3j"
  },
  "title": {
    "sj55zd": "f5yc125",
    "fsow6f": "f17mccla"
  }
}, {
  "d": [".fqag9an{padding-top:16px;}",
    ".f1gbmcue{padding-right:16px;}",
    ".f1rh9g5y{padding-left:16px;}",
    ".fp67ikv{padding-bottom:16px;}",
    ".f1vhk6qx{min-height:100vh;}",
    ".fr97h3j{width:100vw;}",
    ".f5yc125{color:#786efe;}",
    ".f17mccla{text-align:center;}"]
});

CSS提取

griffel的提前编译仍然需要JS在运行时插入,所以也存在以下缺陷:

  • 样式不能像CSS文件那样被浏览器缓存
  • 如果多个makeStyle就算是定义完全一样的样式,也会在编译时生成多份一样的CSS规则,增加了打包后的体积。
import { makeStyles, shorthands } from "@griffel/react";

const useClasses = makeStyles({
  main: {
    minHeight: "100vh",
    width: '100vw',
  },
});

const useClassesCopy = makeStyles({
  main: {
    minHeight: "100vh",
    width: '100vw',
  },
});
const useClasses = (0,_griffel_react__WEBPACK_IMPORTED_MODULE_2__.__styles)({
  "main": {
    "sshi5w": "f1vhk6qx",
    "a9b677": "fr97h3j"
  }
}, {
  "d": [".f1vhk6qx{min-height:100vh;}", ".fr97h3j{width:100vw;}"]
});
const useClassesCopy = (0,_griffel_react__WEBPACK_IMPORTED_MODULE_2__.__styles)({
  "main": {
    "sshi5w": "f1vhk6qx",
    "a9b677": "fr97h3j"
  }
}, {
  "d": [".f1vhk6qx{min-height:100vh;}", ".fr97h3j{width:100vw;}"]
});

griffel实验性的CSS提取插件能够把makeStyle提取编译的所有样式提取到单独的一个CSS文件中,打包后的JavaScript不包含任何CSS规则,只保留类名的映射。

得益于Atomic CSS特性,提取后不存在重复的规则,随着组件数的增多,打包后的体积大大减小这个优势会更加凸显。但遗憾的是,目前CSS提取并不支持Code Splitting,否则后面加载的Atomic Class将和文档中原有的冲突,导致整体样式会随着不同chunk的加载而改变,没办法保证样式表的稳定。

vanilla-extract 🧁

vanilla-extract是2021年推出的CSS "TypeScript 预处理器" 库,给CSS的编写带来了TypeScript的类型安全和推导。
image.png
vanilla-extract需要把样式写在一个单独的.css.ts文件,然后内部通过esbuild编译出CSS,所以vanilla-extract本身是一个零运行时的CSS-in-JS框架。相比其他CSS预处理器(SCSS/LESS),vanilla-extract不需要额外学习新的语法,只是普通的TypeScript

来看看基础的用法

// text.css.ts
import { style } from "@vanilla-extract/css";

export const textStyle = style({
  display: "flex",
  backgroundColor: "grey",
  fontSize: "24px",
  padding: "16px",
  gap: "8px",
});

因为基于TypeScript,如果使用VSCode这种对TypeScript友好的编辑器,就能得到开箱即用的类型安全和类型提示,不需要任何额外的CSS-in-JS插件
image.png
image.png

import { textStyle } from "./text.css";
// 或者像CSS Modules一样
import * as styles from './text.css';

const Text = () => <div className={textStyle}></div>

可以看到写法相当于TypeScript里的CSS Module,产出的CSS类名也保证只在局部范围有效。

不是巧合,vanilla-extract的作者之一Mark Dalgleish正是CSS Module的作者

vanilla-extract限制所有样式必须定义在.css.ts,让编译器能够区分运行时和编译时的代码,同时可以享受到所有TypeScript的特性,比如:

  • 可以在别的color.ts定义所有主题色和其名字的对象,然后import进来通过转换函数生成background或者color属性的样式
  • 将一系列类名导出为数组,在React组件中渲染为列表

但也有以下限制:

  • 不能有任何副作用
  • 只能导出可序列化的值,像plain objects, arrays, strings, numbers, null/undefined

然后text.css.ts的所有逻辑会在编译时运行,只产出CSS样式和导出的类名,不影响打包后的体积,开发环境会有完整的类名,而生产环境下仅保留最后的hash。

/* text.css */
._1u43sud0 {
  display: flex;
  background-color: gray;
  font-size: 24px;
  padding: 16px;
  gap: 8px;
}
// test.js
import './test.css'

export const textStyle = '_1u43sud0'

而且.css.ts实际上不会真的单独在磁盘构建出一个test.css,所以上面的import是利用了vite这些bundler的虚拟模块特性指向插件编译时内存里的CSS。

同时还支持基于CSS Variables的主题特性,定义的变量只会在生成的类名下有效。

// theme.css.ts
import { createTheme } from '@vanilla-extract/css';

export const [themeClass, vars] = createTheme({
  color: {
    brand: 'blue'
  },
  font: {
    body: 'arial'
  }
});

export const themeClass2 = createTheme(vars, {
  color: {
    brand: "yellow",
  },
  font: {
    body: "sans",
  },
});
/* theme.css */
.theme_themeClass__17uk7pt0 {
  --color-brand__17uk7pt1: blue;
  --font-body__17uk7pt2: arial;
}

.theme_themeClass2__17uk7pt3 {
  --color-brand__17uk7pt1: red;
  --font-body__17uk7pt2: helvetica;
}
// theme.ts
import "./theme.css";
export const themeClass = "theme_themeClass__17uk7pt0";
export const themeClass2 = "theme_themeClass2__17uk7pt3";
export const vars = {
  color: { brand: "var(--color-brand__17uk7pt1)" },
  font: { body: "var(--font-body__17uk7pt2)" },
};

可以看到,编译出来的vars就是普通的对象带有var(--xxx)的字符串

// 返回的变量具有结构化的类型定义
const vars: MapLeafNodes<{
  color: {
    brand: string;
  };
  font: {
    body: string;
  };
}, CSSVarFunction>

Sprinkles

在此之上,vanilla-extract官方推出了Sprinkles,一个简单的构建时Atomic CSS框架。

import { defineProperties, createSprinkles } from "@vanilla-extract/sprinkles";

const spaces = {
  none: 0,
  small: "4px",
  medium: "8px",
  large: "16px",
};

const colors = {
  blue50: "#eff6ff",
  blue100: "#dbeafe",
  blue200: "#bfdbfe",
  // etc.
}

const properties = defineProperties({
  properties: {
    background: colors,
    color: colors,
    display: ["block", "flex"],
    gap: spaces,
    paddingTop: spaces,
    paddingBottom: spaces,
    paddingLeft: spaces,
    paddingRight: spaces,
    marginLeft: spaces,
    marginRight: spaces,
    alignItems: ["stretch", "flex-start", "center", "flex-end"],
    justifyContent: ["stretch", "flex-start", "center", "flex-end"],
  },
  shorthands: {
    padding: ["paddingTop", "paddingBottom", "paddingLeft", "paddingRight"],
    paddingX: ["paddingLeft", "paddingRight"],
    paddingY: ["paddingTop", "paddingBottom"],
    marginX: ["marginLeft", "marginRight"],
    placeItems: ["alignItems", "justifyContent"],
  },
});

export const sprinkles = createSprinkles(properties);
export type Sprinkles = Parameters<typeof sprinkles>[0];

同时也支持通过conditions使用媒体查询区分设备大小等

declare type ConditionKey = '@media' | '@supports' | '@container' | 'selector';
declare type Condition = Partial<Record<ConditionKey, string>>;
type Conditions extends {
    [conditionName: string]: Condition;
}
import { defineProperties, createSprinkles } from "@vanilla-extract/sprinkles";

const colorProperties = defineProperties({
  conditions: {
    mobile: {},
    desktop: { "@media": "screen and (min-width: 1024px)" },
  },
  defaultCondition: "desktop",
  properties: {
    // 和上面的例子一样
  },
});

export const sprinkles = createSprinkles(colorProperties);
export type Sprinkles = Parameters<typeof sprinkles>[0];

Sprinkles还可以用在运行时,仅仅是查找已生成的类名,几乎没有额外开销。或者是在.css.ts中和其他样式组合。

import { sprinkles } from "./sprinkles.css";

function App() {
  return (
    <div
      className={sprinkles({
        display: "flex",
        placeItems: "center",
        paddingTop: {
          mobile: "small",
          desktop: "large",
        },
      })}
      >
      TEST
    </div>
  );
}

// test.css.ts
import { style } from "@vanilla-extract/css";
import { sprinkles } from "./sprinkles.css";

export const containerStyle = style([
  sprinkles({
    display: "flex",
    marginX: "small",
    placeItems: "center",
    background: 'blue50',
  }),
  {
    fontSize: "18px",
  },
]);

image.png
生成了以下原子类:

下面仅展示每个Property的部分输出,实际上像paddingTop会生成4个原子类

.sprinkles_background_blue50__7uijof0 {
  background: #eff6ff;
}
.sprinkles_background_blue200__7uijof2 {
  background: #bfdbfe;
}
.sprinkles_color_blue50__7uijof3 {
  color: #eff6ff;
}
.sprinkles_display_block__7uijof6 {
  display: block;
}
.sprinkles_display_flex__7uijof7 {
  display: flex;
}
.sprinkles_gap_none__7uijof8 {
  gap: 0;
}
.sprinkles_paddingTop_small__7uijofd {
  padding-top: 4px;
}
.sprinkles_marginLeft_large__7uijofv {
  margin-left: 16px;
}
.sprinkles_alignItems_center__7uijof12 {
  align-items: center;
}
.sprinkles_alignItems_flex-end__7uijof13 {
  align-items: flex-end;
}
.sprinkles_justifyContent_stretch__7uijof14 {
  justify-content: stretch;
}

Sprinkles仅仅是在vanilla-extract上面的一层简单封装,没有引入额外的AST解析。以下是不包含conditionsshorthands等特性的defineProperties简单实现,其实只是对properties对象里的每个属性值用vanilla-extract的style方法在构建时生成对应的类。

import { style, type StyleRule } from "@vanilla-extract/css";

export function defineProperties(options: any): any {
  let styles: any = {};

  for (const key in options.properties) {
    const property = options.properties[key as keyof typeof options.properties];
    styles[key] = {
      values: {},
    };

    const processValue = (
      valueName: keyof typeof property,
      value: string | number | StyleRule
    ) => {
      const styleValue: StyleRule =
        typeof value === "object" ? value : { [key]: value };

      styles[key].values[valueName] = {
        defaultClass: style(styleValue, `${key}_${String(valueName)}`),
      };
    };

    if (Array.isArray(property)) {
      for (const value of property) {
        processValue(value, value);
      }
    } else {
      for (const valueName in property) {
        const value = property[valueName];
        processValue(valueName, value);
      }
    }
  }

  return { styles };
}

所以Sprinkles天然存在这些缺陷:

  • vanilla-extract生成类名的哈希方式是按定义的顺序输出(从上面的例子也可以看出来),所以构建后生成的类名并不稳定。
  • 定义的所有Properties无论最终是否用到,都会在构建时输出。
  • 如果定义里存在Conditions,那就会每个Condition输出一样的类。上面的例子中存在mobiledesktop,相当于生成两倍体积的CSS
  • 同一个原子类不能重复定义,构建时会出错
  • 定义多个具有相同CSS规则的Sprinkles时会输出多份一样的Atomic类(因为哈希,所以类名不同),Sprinkles本身没有做任何冲突处理
  • 如果定义了大量的原子类,比如对色板每一个颜色生成backgroundcolor的类,还加上了:hover等选择器,就可能导致构建时超内存限制,详见讨论

总体而言,Sprinkles能够满足80%常用Utility样式

其他上层框架/应用

没有银弹 🥄

从griffel和vanilla-extract这两个库的侧重点也可以看出Atomic CSS-in-JS这个方向还有很多改进的空间,至少在现在这个时间点,不是一个十全十美的方案,CSS仍然没有银弹:

  • 重构建时也意味着对各个bundler的支持格外重要,这点和后续的迭代维护非常依赖社区和维护团队的投入。上面提到了很多库,但只有vanilla-extract的支持相对较全的,像style9、linaria、compiled、griffel都没支持vite。
  • Atomic CSS-in-JS需要在动态样式和静态提取上权衡,像styleX/style9这些构建比较强的几乎完全放弃了动态样式的支持,只能用CSS Variables或者inline style。
  • Atomic CSS静态提取和Code Splitting的并存现阶段还是一个待解决的难题,好在Atomic CSS本身体积也较小(但是很多Atomic CSS-in-JS并不能自动剔除没用到的样式🥲)。
  • Object Syntax带来了强约束和类型提示,但也舍弃了部分编写时的便利,比如从老代码或者devTools调试完再直接拷贝样式过来用就行不通了。
  • 一些CSS规则(像padding: 4px这类shorthands缩写)不能直接用于Atomic CSS,否则会出现冲突和优先级问题。
    • twitter的react-native-web允许冲突的类同时存在,但实际生效的类还得看优先级算法。
    • 像griffel会直接通过TypeScript来禁止,同时提供一些预设的shorthands API。
    • styleX会将create中定义的shorthands展开,所以实际生效的是stylex函数参数中最后的类。
  • 本文没有覆盖到CSS媒体查询,伪类这些,涉及到这些时构建出来的Atomic类要复杂的多。

所以,现在我们是否应该转而使用Atomic CSS-in-JS呢?答案可能还是不,但vanilla-extract用于替换CSS Module倒是一个不错的选择,opt-in的Sprinkles也能覆盖80%的Uility类场景。

尾声 📕

刚好在11月1日,众望所归的styleX终于对外放出了第一个Beta版本@stylexjs/stylex [0.1.0-beta](https://www.npmjs.com/package/@stylexjs/stylex/v/0.1.0-beta.1)🎉🎉🎉(尽管尚未开源)。

同时带来的还有一个新的媒体查询和伪类的Fluent API RFC

// 以前的和现在大多数CSS-in-JS的做法
const styles = stylex.create({
  foo: {
    position: 'absolute',
    '@media (max-width: 600px)': {
      position: 'sticky',
    },
  },
});

// RFC的Fluent API
const styles = stylex.create({
  foo: {
    position:
      stylex.defaultValue('absolute')
        .containerWidth([0, 768], 'sticky')
        .containerWidth([768, 1260], 'fixed'),
    opacity:
      stylex.defaultValue(0.5)
        .containerWidth(containerName, [0, 768], 'sticky')
        .hover(0.75)
        .active(1),
  }
})

不得不说,styleX在Meta内部大规模应用2年多后在各方面还是走在了前面。

随着React 18的Streaming SSR和Server Component等重要改动深入整个生态之后(实际上不少meta framework已经深度集成,像上个月发布的next.js 13被戏称为真正的React 18),下一个5年或许是Atomic CSS-in-JS百花齐放的时候,就像当年styled-component为首的CSS-in-JS库发展至今占据了几乎六成的React相关项目。

推荐阅读 📖