了解 Facebook 在 CSS 方面面临的问题将有助于我们更多地了解 StyleX 背后的设计决策。
三年前,Facebook设计系统组件背后的人面临着一个问题。他们正在对 Facebook 的整个 Web 前端进行完整的 React 重写,他们需要一种方法来处理 CSS。这是许多大型项目和公司面临的难题。有很多选择;构建时与运行时,CSS 与 JavaScript 中声明的 CSS,以及是否使用像 Tailwind 这样的实用程序优先系统。
Facebook决定做的是建立一个新的CSS平台------这是他们应用程序架构的第三个支柱。GraphQL 和 Relay 处理数据,React 处理 DOM,现在 StyleX 将处理样式。另外,他们希望这个新的CSS系统能够从过去的错误中吸取教训。
Facebook 之前的架构类似于 CSS modules。但是,这种方法存在扩展问题。为了解决缩放问题,他们根据需要在运行时加载了 CSS。但是延迟加载会导致选择器优先级问题,即通过不同的路由导航站点会以不同的顺序加载 CSS,从而产生意外的样式。
StyleX 通过提供""Deterministic Resolution(确定性分辨率)"来解决这个问题,您始终可以保证获得所需的样式。要了解这种确定性分辨率的值,让我们从一个简单的按钮开始
StyleX 按钮
让我们看一个 StyleX Button
组件的例子:
jsx
import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({
base: {
appearance: "none",
borderWidth: 0,
borderStyle: "none",
backgroundColor: "blue",
color: "white",
borderRadius: 4,
paddingBlock: 4,
paddingInline: 8,
},
});
export default function Button({
onClick,
children,
}: Readonly<{
onClick: () => void;
children: React.ReactNode;
}>) {
return (
<button {...stylex.props(styles.base)} onClick={onClick}>
{children}
</button>
);
}
首先,样式与使用它们的组件位于同一位置,如果您喜欢编写 CSS 的emotion
风格,那么从 DX 和代码可读性的角度来看,这是一个巨大的胜利。但是,您仍然可以获得像 emotion
这样的运行时系统所无法获得的编译时 CSS 胜利。
不幸的是,如果你想使用这些速记样式,你就无法获得真正的 Tailwind 易用性(尽管支持设计令牌,如果你愿意,你可以创建这些速记)。然而,我们没有 Tailwind 短手,我们失去了对styling的控制权。
对样式的控制
使用 Tailwind 作为组件系统基础的问题在于允许对样式进行受控的细化。假设我们希望允许按钮的使用者只更改Button
的color
和backgroundColor
。有了 Tailwind 组件,你可以为此设置特定的道具,但如果你想让人们能够调整的不仅仅是几种样式,那就无法扩展了。因此,一些作者允许的是一个extraClasses
属性,您可以在其中添加任何您喜欢的内容。但是,您可以不受限制地更改任何您喜欢的内容,这使得以后很难对组件进行版本控制。
StyleX 对此有一个很棒的解决方案:
jsx
import type { StyleXStyles } from "@stylexjs/stylex/lib/StyleXTypes";
export default function Button({
onClick,
children,
style,
}: Readonly<{
onClick: () => void;
children: React.ReactNode;
style?: StyleXStyles<{
backgroundColor?: string;
color?: string;
}>;
}>) {
return (
<button {...stylex.props(styles.base, style)} onClick={onClick}>
{children}
</button>
);
}
我们添加了另一个名为 style
的属性,并将其限制为我们想要可重写的样式。由于我们在stylex.props
调用中将 style
放在 styles.base
之后,因此可以保证重写样式将适当地重写基本样式。
看看我们对造型的控制力有多大?这意味着我们可以放心地对 Button
进行版本控制,因为我们对 CSS 可以更改和不能更改的内容有明确的界限。
当我们想要覆盖样式时,使用我们的Button
是这样的:
jsx
const buttonStyles = stylex.create({
red: {
backgroundColor: "red",
color: "blue",
},
});
<StyleableButton onClick={onClick} **style={buttonStyles.red}**>
Styleable Button
</StyleableButton>
很明显,我们正在更改什么,TypeScript 强制要求我们只能覆盖组件创建者希望我们覆盖的样式。
Design Tokens And Theming
能够在粒度级别覆盖样式是很棒的,但任何合理的设计系统都需要对design tokens 和 theming支持,而 StyleX 对这两者都有出色的类型安全支持。
让我们从定义一些tokens开始:
jsx
import * as stylex from "@stylexjs/stylex";
export const buttonTokens = stylex.defineVars({
bgColor: "blue",
textColor: "white",
cornerRadius: "4px",
paddingBlock: "4px",
paddingInline: "8px",
});
请注意,我们可以使用像 bgColor
这样的名称,而不是局限于特定的 CSS 属性。然后,我们可以将此tokens映射到我们的Button
中,如下所示:
jsx
import * as stylex from "@stylexjs/stylex";
import type { StyleXStyles, Theme } from "@stylexjs/stylex/lib/StyleXTypes";
import "./ButtonTokens.stylex";
import { buttonTokens } from "./ButtonTokens.stylex";
export default function Button({
onClick,
children,
style,
theme,
}: {
onClick: () => void;
children: React.ReactNode;
style?: StyleXStyles;
theme?: Theme<typeof buttonTokens>;
}) {
return (
<button {...stylex.props(theme, styles.base, style)} onClick={onClick}>
{children}
</button>
);
}
const styles = stylex.create({
base: {
appearance: "none",
borderWidth: 0,
borderStyle: "none",
backgroundColor: buttonTokens.bgColor,
color: buttonTokens.textColor,
borderRadius: buttonTokens.cornerRadius,
paddingBlock: buttonTokens.paddingBlock,
paddingInline: buttonTokens.paddingInline,
},
});
因此,现在,我们正在基于硬编码值(如 borderWidth
)和主题值color
(如基于 textColor
设计令牌)的组合,为我们的Button
创建styles
。
我们还支持通过添加theme
属性并将其用作stylex.props
的基础来使用该主题。
在消费者方面,我们可以使用 createTheme
创建一个主题,并将该主题基于按钮标记:
jsx
const DARK_MODE = "@media (prefers-color-scheme: dark)";
const corpTheme = stylex.createTheme(buttonTokens, {
bgColor: {
default: "black",
[DARK_MODE]: "white",
},
textColor: {
default: "white",
[DARK_MODE]: "black",
},
cornerRadius: "4px",
paddingBlock: "4px",
paddingInline: "8px",
});
我们甚至可以使用对象语法来根据媒体查询指定主题值。例如,在这种情况下,在深色模式下,我们反转按钮颜色。
我们甚至可以使用对象语法来根据媒体查询指定主题值。例如,在这种情况下,在深色模式下,我们反转按钮颜色。
然后在我们的页面代码中,我们可以将主题直接发送到组件:
jsx
<Button onClick={onClick} **theme={corpTheme}**>
Corp Button
</Button>
或者我们可以将按钮放在指定主题的容器中:
jsx
<div {...stylex.props(corpTheme)}>
<Button onClick={onClick}>
Corp Button
</Button>
</div>
通过CSS variables 的魔力,该div
中的任何Button
现在都会获得该主题。
当然,所有这些都适用于 React 服务器组件和服务器端渲染,因为它们都是在编译时计算的,并且类作为字符串注入到代码中。
条件样式和动态样式
通常,我们认为构建时 CSS 是静态的,但 StyleX 同时支持条件样式和动态样式。让我们在原始按钮上添加一个emphasis
标志:
jsx
import * as stylex from "@stylexjs/stylex";
const styles = stylex.create({
...,
emphasized: {
fontWeight: "bold",
},
});
export default function Button({
onClick,
children,
emphasized,
}: Readonly<{
onClick: () => void;
children: React.ReactNode;
emphasized?: boolean;
}>) {
return (
<button
{...stylex.props(styles.base, emphasized && styles.emphasized)}
onClick={onClick}
>
{children}
</button>
);
}
我们需要做的就是在styles
定义中为强调的样式添加另一个部分,然后检查标志并有条件地添加样式。就是这么简单!
我只是触及了 StyleX 可以做什么的表面。如果需要在运行时生成位置或颜色等值,则样式可以是动态的。只需添加另一个stylex.create
来定义变体,然后使用基于道具的正确变体样式,即可轻松支持像 variant
这样的选项。
StyleX 团队还将所有 OpenProps 移植到 StyleX,让大量的间距选项、颜色、动画等触手可及。
结论
构建StyleX并将其用作Facebook.com React重写的关键组件的最终结果是,该网站在大约130Kb的CSS上运行。虽然这看起来很多,但CSS涵盖了每条路线的每个功能。浏览器加载一次就完成了。不再有加载顺序问题。经过三年的开发,现在已经达到了 170Kb,但当你考虑开发人员和功能的数量时,那里的趋势令人印象深刻
StyleX 已经在 Facebook 使用了三年,而且已经久经沙场。现在,它正在进入开源领域,您可以在其中利用所有的工作和经验。
我相信你们中的许多人会对 StyleX 不屑一顾,因为它不像 Tailwind 那样容易使用。我不否认情况确实如此,但是,在我看来,这两个非常强大的系统,Tailwind 和 StyleX,是为光谱的两端而设计的。
我认为 Tailwind 对于快速工作的小型团队来说具有很大的价值,而且很可能除了一些颜色、间距和断点值之外没有固定的设计系统。
StyleX 旨在支持更大的项目、团队甚至团队组。StyleX 为我们提供了宝贵的工具。我对此非常感激。我曾在大公司工作过,跨团队构建设计系统既不容易也不容易理解。很高兴看到 Meta 将这一端的新工具带入开源。非常感谢。
更多
官方文档:stylexjs.com/docs/learn/ github:github.com/facebook/st...