StyleX — Meta 的可扩展 CSS 解决方案

928 阅读7分钟

20231206203821_rec_.gif 了解 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 组件的例子:

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 作为组件系统基础的问题在于允许对样式进行受控的细化。假设我们希望允许按钮的使用者只更改ButtoncolorbackgroundColor。有了 Tailwind 组件,你可以为此设置特定的道具,但如果你想让人们能够调整的不仅仅是几种样式,那就无法扩展了。因此,一些作者允许的是一个extraClasses属性,您可以在其中添加任何您喜欢的内容。但是,您可以不受限制地更改任何您喜欢的内容,这使得以后很难对组件进行版本控制。

StyleX 对此有一个很棒的解决方案:

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是这样的:

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开始:

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中,如下所示:

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 创建一个主题,并将该主题基于按钮标记:

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",  
});

我们甚至可以使用对象语法来根据媒体查询指定主题值。例如,在这种情况下,在深色模式下,我们反转按钮颜色。

我们甚至可以使用对象语法来根据媒体查询指定主题值。例如,在这种情况下,在深色模式下,我们反转按钮颜色。

然后在我们的页面代码中,我们可以将主题直接发送到组件:

<Button onClick={onClick} **theme={corpTheme}**>
  Corp Button
</Button>

或者我们可以将按钮放在指定主题的容器中:

<div {...stylex.props(corpTheme)}>  
    <Button onClick={onClick}>  
        Corp Button  
    </Button>  
</div>

通过CSS variables 的魔力,该div中的任何Button现在都会获得该主题。

当然,所有这些都适用于 React 服务器组件和服务器端渲染,因为它们都是在编译时计算的,并且类作为字符串注入到代码中。

条件样式和动态样式

通常,我们认为构建时 CSS 是静态的,但 StyleX 同时支持条件样式和动态样式。让我们在原始按钮上添加一个emphasis标志:

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…