自2016年Styled Component这一代表性的运行时CSS-in-JS库发布以来已经过去了5年多,这期间涌现出大量迥异的方案,并且在发展的过程中各自API设计也趋于接近和稳定,大量的React组件库和项目都深度使用CSS-in-JS,可以算得上是主流的样式方案选择。
而近年来社区又开始流行像Tailwind CSS这类Utility First的方案,基本思想就是预设海量的基本Utility类,一个类通常包含只一条或几条相关的CSS规则,编写时只需要组合复用这些类,无需离开HTML。
<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的压缩率很高,所以实际影响不大。
在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 JIT、windiCSS以及实验性的unoCSS,移除了组件同时也移除了CSS。
- 优先级问题(CSS specificity issue)
浏览器通过优先级来判断哪些属性值与一个元素最为相关,从而在该元素上应用这些属性值。优先级是基于不同种类选择器组成的匹配规则。
以上是MDN上的简要定义,实际上浏览器的优先级算法会相当的复杂。
而且和CSS的层叠(cascade)密切相关,光从选择器还不一定能判断出到底哪个规则会生效
.blue {
color: blue;
}
.red {
color: red;
}
在例子class="blue red"
中,实际生效的color
取决于他们在样式表中的顺序。
如果要覆盖一个已有样式,通常有以下做法:
- 粗暴的用内联样式
style
或者!important
来获得最高优先级,但代价是“污染”了样式,没有办法再被覆盖。 - 编写优先级更高的选择器,但毕竟选择器是由人来编写的,一般不会套用这套复杂算法(要是写个花里胡哨的选择器等同于给后面维护的人埋了个大坑),更多的是朴素地把选择器加长,加嵌套,比如在原来基础上再加一个类选择器。这样操作下来很容易积累出选择器屎山(特别是打算覆盖第三方组件库样式时),而且复杂选择器也意味着加深和HTML的耦合。
而对于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年初开源
还有些备受关注,但暂时没有被大规模应用的开源框架:
- style9:受styleX启发的,有着相似的API
- linaria
- compiled
- vanilla-extract + Sprinkles
这里主要介绍下griffel和vanilla-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的道路
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,通过useContext
和useeMemo
来全局共享唯一的renderer
以及其缓存的样式。只有useClasses
首次渲染时才会向文档插入样式- griffel的类名哈希算法来自Emotion,将属性名和属性值作为输入来确定哈希值,因此所有Atomic类都有稳定的类名。
其中关键的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的类型安全和推导。
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插件
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",
},
]);
生成了以下原子类:
下面仅展示每个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解析。以下是不包含conditions
,shorthands
等特性的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输出一样的类。上面的例子中存在
mobile
和desktop
,相当于生成两倍体积的CSS - 同一个原子类不能重复定义,构建时会出错
- 定义多个具有相同CSS规则的Sprinkles时会输出多份一样的Atomic类(因为哈希,所以类名不同),Sprinkles本身没有做任何冲突处理
- 如果定义了大量的原子类,比如对色板每一个颜色生成
background
和color
的类,还加上了:hover
等选择器,就可能导致构建时超内存限制,详见讨论
总体而言,Sprinkles能够满足80%常用Utility样式
其他上层框架/应用
- macaron:零运行时,Styled React风格的CSS-in-JS写法,基于vanilla-extract,加上了babel的AST解析。
- A prototype for a foundational design system at Shopify:Design Token + React Component,基于vanilla-extract和Sprinkles
没有银弹 🥄
从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相关项目。
推荐阅读 📖
- The Making of Atomic CSS: An Interview With Thierry Koblentz
- RF21 – Naman Goel – Rethinking CSS - Introducing Stylex
- RF21 – Mark Dalgleish – Zero-runtime CSS-in-TypeScript with vanilla-extract
- Nicolas Gallagher - Twitter Lite, React Native, and Progressive Web Apps
- Building the New Facebook with React and Relay | Frank Yan
- Vanilla Extract - CSS in JS at compile time | React India 2021
- Reimagine Atomic CSS
- A thorough analysis of all the current CSS-in-JS solutions with SSR & TypeScript support for Next.js
- [RFC] Media Queries and Pseudo Classes