跳至主要内容

StyleX 思维

核心原则

为了理解 StyleX 存在的意义及其决策背后的原因,熟悉指导它的基本原则可能会有所帮助。这可能有助于您决定 StyleX 是否适合您的解决方案。

在为 StyleX 设计新的 API 时,这些原则也应该有所帮助。

代码共置

DRY 代码有很多好处,但我们认为在编写样式时通常并非如此。编写样式的最佳且最易读的方式是在与标记相同的文件中编写它们。

StyleX 旨在用于本地编写、应用和推理样式。

确定性解析

CSS 是一种强大且富有表现力的语言。但是,有时它会让人感觉很脆弱。其中一部分原因是对 CSS 工作原理的误解,但很大一部分原因在于需要保持不同特异性的 CSS 选择器不冲突所需的纪律和组织。

大多数现有的解决此问题的方法都依赖于规则和约定。

BEM 和 OOCSS 约定BEM 和 OOCSS 引入了命名约定以避免这些问题,这些问题依赖于开发人员始终如一地遵循规则,通常完全避免合并样式。这可能导致 CSS 膨胀。
实用程序类像 Tailwind CSS 和 Tachyons 这样的原子实用程序类名依赖于约定和 lint 规则来确保在同一元素上不应用冲突的类名。此类工具对样式的应用位置和方式施加了约束,对样式设置了架构限制。

StyleX 旨在提高样式的一致性和可预测性 *以及* 可用的表达能力。我们认为这可以通过构建工具实现。

StyleX 提供了一个完全可预测且确定性的样式系统,该系统可在所有文件中使用。它不仅在合并多个选择器时产生确定性结果,而且在合并多个简写和完整属性时也产生确定性结果。(例如,marginmargin-top)。

“最后应用的样式始终有效”。

低成本抽象

在 StyleX 的性能成本方面,我们的指导原则是 StyleX 应该始终是实现特定模式的最快方法。常见模式不应产生运行时成本,高级模式应尽可能快。我们权衡在构建时做更多工作以提高运行时性能。

以下是它在实践中的体现方式

1. 本地创建和应用样式

在同一文件中编写和使用样式时,StyleX 的成本为零。这是因为除了编译掉 stylex.create 调用之外,StyleX 还在可能的情况下编译掉 stylex.props 调用。

所以,

import * as stylex from '@stylexjs/stylex';
const styles = stylex.create({
red: {color: 'red'},
});
let a = stylex.props(styles.red);

编译成

import * as stylex from '@stylexjs/stylex';

let a = {className: 'x1e2nbdu'};

这里没有运行时开销。

2. 在文件之间使用样式

跨文件边界传递样式会产生少量成本,以换取额外的功能和表达能力。stylex.create 调用不会被完全删除,而是保留一个将键映射到类名的对象。并且 stylex.props() 调用在运行时执行。

例如,此代码

import * as stylex from '@stylexjs/stylex';

const styles = stylex.create({
foo: {
color: 'red',
},
bar: {
backgroundColor: 'blue',
},
});

function MyComponent({style}) {
return <div {...stylex.props(styles.foo, styles.bar, style)} />;
}

编译成

import * as stylex from '@stylexjs/stylex';

const styles = {
foo: {
color: 'x1e2nbdu',
$$css: true,
},
bar: {
backgroundColor: 'x1t391ir',
$$css: true,
},
};

function MyComponent({style}) {
return <div {...stylex.props(styles.foo, styles.bar, style)} />;
}

这有点多代码,但运行时成本仍然很小,因为 stylex.props() 函数的速度非常快。

大多数其他样式解决方案都不允许跨文件边界组合样式。最先进的技术是组合类名列表。

较小的 API 表面

我们的目标是使 StyleX 尽可能简洁易学。因此,我们不想发明太多 API。相反,我们希望能够在可能的情况下依赖常见的 JavaScript 模式,并提供尽可能小的 API 表面。

从核心来看,StyleX 可以归结为两个函数

  1. stylex.create
  2. stylex.props

stylex.create 用于创建样式,stylex.props 用于将这些样式应用于元素。

在这两个函数中,我们选择依赖常见的 JS 模式,而不是为 StyleX 引入唯一的 API 或模式。例如,我们没有用于条件样式的 API。相反,我们支持使用布尔值或三元表达式有条件地应用样式。

在处理 JavaScript 对象和数组时,事情应该按预期工作。不应该有任何意外。

类型安全的样式

TypeScript 由于其提供的体验和安全性而变得非常流行。但是,我们的样式在很大程度上仍然是无类型的且不可靠的。除了 Vanilla Extract 等一些突破性的项目之外,在大多数样式解决方案中,样式都只是字符串的集合。

StyleX 使用 Flow 和强大的静态类型编写。它在 NPM 上的包附带了为 Flow 和 TypeScript 自动生成的类型。当这两个类型系统之间存在不兼容性时,我们会花时间确保编写自定义 TypeScript 类型以实现与原始 Flow 相同的强大功能和安全性。

所有样式都已类型化。在将样式作为 props 接受时,可以使用类型来约束接受的样式。样式应该像任何其他组件 props 一样类型安全。

StyleX API 是强类型的。使用 StyleX 定义的样式也是类型化的。这是通过使用 JavaScript 对象编写原始样式来实现的。这是我们选择对象而不是模板字符串的主要原因之一。

然后,这些类型可以用来为组件将接受的样式设置契约。例如,可以将组件 props 定义为仅接受 colorbackgroundColor,而不接受其他样式。

import type {StyleXStyles} from '@stylexjs/stylex';

type Props = {
//...
style?: StyleXStyles<{color?: string; backgroundColor?: string}>;
//...
};

在另一个示例中,props 可能不允许边距,但允许所有其他样式。

import type {StyleXStylesWithout} from '@stylexjs/stylex';

type Props = {
//...
style?: StyleXStylesWithout<{
margin: unknown;
marginBlock: unknown;
marginInline: unknown;
marginTop: unknown;
marginBottom: unknown;
marginLeft: unknown;
marginRight: unknown;
marginBlockStart: unknown;
marginBlockEnd: unknown;
marginInlineStart: unknown;
marginInlineEnd: unknown;
}>;
//...
};

样式被类型化可以实现关于如何以**零运行时成本**自定义组件样式的极其复杂的规则。

可共享常量

CSS 类名、CSS 变量和其他 CSS 标识符是在全局命名空间中定义的。将 CSS 字符串引入 JavaScript 可能意味着失去类型安全性和可组合性。

我们希望样式是类型安全的,因此我们花费了大量时间来提出 API 以用 JavaScript 常量的引用替换这些字符串。到目前为止,这反映在以下 API 中

  1. stylex.create 完全抽象了生成的类名。您使用具有强类型的“不透明”JavaScript 对象来指示它们表示的样式。
  2. stylex.defineVars 抽象了生成的 CSS 变量的名称。它们可以作为常量导入并直接在样式中使用。
  3. stylex.keyframes 抽象了关键帧动画的名称。相反,它们被声明为常量并通过引用使用。

我们正在研究使其他 CSS 标识符(如 container-name@font-face)也类型安全的方法。

框架不可知

StyleX 是一种 CSS-in-JS 解决方案,而不是 CSS-in-React 解决方案。尽管 StyleX 现已针对 React 量身定制,但它旨在与任何允许在 JavaScript 中编写标记的 JavaScript 框架一起使用。这包括使用 JSX、模板字符串等的框架。

stylex.props 返回一个包含 classNamestyle 属性的对象。可能需要一个包装函数将其转换为使其适用于各种框架。

封装

元素上的所有样式都应该由该元素本身上的类名引起。

CSS 使以可能导致“远距离样式”的方式编写样式变得非常容易

  • .className > *
  • .className ~ *
  • .className:hover > div:first-child

所有这些模式虽然功能强大,但会使样式变得脆弱且不可预测。在一个元素上应用类名可能会影响完全不同的元素。

可继承的样式(如 color)仍将被继承,但这是 StyleX 允许的 *唯一* 形式的远距离样式。在这些情况下,直接应用于元素的样式始终优先于继承的样式。

当使用复杂的选择器时,情况通常并非如此,因为复杂的选择器通常比用于直接应用于元素的样式的简单类选择器具有更高的特异性。

StyleX 不允许使用这类选择器。这目前使得某些 CSS 模式无法使用 StyleX 实现。我们的目标是在不牺牲样式封装的情况下支持这些模式。

StyleX 不是 CSS 预处理器。它有意限制 CSS 选择器的功能,以构建一个快速且可预测的系统。基于 JavaScript 对象而不是模板字符串的 API 旨在使这些约束感觉自然。

可读性和可维护性优先于简洁性

一些最近的基于实用程序的样式解决方案非常简洁且易于编写。StyleX 选择优先考虑可读性和可维护性而不是简洁性。

StyleX 选择使用熟悉的 CSS 属性名称来优先考虑可读性和较浅的学习曲线。*(我们确实决定使用 camelCase 而不是 kebab-case 以方便起见。)*

我们还强制要求样式在与使用它们的 HTML 元素分开的对象中编写。我们做出此决定是为了帮助提高 HTML 标记的可读性,并使适当命名的样式指示其用途。例如,使用 styles.active 之类的名称强调了应用样式的 *原因*,而不必深入了解应用样式的 *内容*。

此原则会导致权衡,即使用 StyleX 编写样式可能需要比某些其他解决方案键入更多内容。

我们相信,随着时间的推移,这些成本是值得提高可读性的。为每个 HTML 元素提供语义名称可以传达比样式本身更多的信息。

信息

使用样式引用而不是内联样式的一个额外好处是 **可测试性**。在单元测试环境中,StyleX 可以配置为移除所有原子样式,并且只输出单个调试类名来指示样式的源位置,而不是实际的样式。

除其他好处外,它使快照测试更具弹性,因为它们不会因每次样式更改而改变。

模块化和可组合性

NPM 使得跨项目共享代码变得极其容易。然而,共享 CSS 仍然是一个挑战。第三方组件要么具有难以或无法自定义的内置样式,要么完全没有样式。

缺乏一个良好的系统来预测性地合并和组合跨包的样式,在包内共享样式时也一直是一个障碍。

StyleX 旨在创建一个系统,以便轻松可靠地在 NPM 上与包中的组件一起共享样式。

避免全局配置

StyleX 应该在不同项目中以类似的方式工作。应避免创建更改 StyleX 语法或行为的特定于项目的配置。我们选择优先考虑可组合性和一致性而不是短期便利性。我们依靠 lint 和类型来创建特定于项目的规则。

我们还避免了在项目中全局具有特殊含义的魔法字符串。相反,每个样式、每个变量和每个共享常量都是一个 JavaScript 导入,无需唯一名称或项目配置。

一个较小的文件胜过多个较小的文件

在处理大量 CSS 时,延迟加载 CSS 是一种加快页面初始加载时间的方法。但是,它以更新时间变慢或交互到下一次绘制 (INP) 指标为代价。在页面上延迟加载任何 CSS 都会触发对整个页面的样式重新计算。

StyleX 针对生成单个高度优化的 CSS 包进行了优化,该包会在前端加载。我们的目标是创建一个系统,其中 CSS 的总量足够小,以至于所有 CSS 都可以在前端加载,而不会对性能产生明显的负面影响。

其他加快初始加载时间的技术,例如“关键 CSS”,与 StyleX 兼容,但通常不需要。