StyleX 编程思想
核心原则
为了理解 StyleX 存在的原因及其决策背后的考量, 熟悉指导它的基本原则可能会有所裨益。 这或许能帮助您决定 StyleX 是否是适合您的解决方案。
在为 StyleX 设计新的 API 时,这些原则也会有所帮助。
Co-location
DRY(避免重复) 代码有其好处,但我们认为在编写样式时通常并非如此。 编写样式的最好、最易读的方法是将样式与 标记写在同一个文件中。
StyleX 专为在局部编写、应用和推导样式而设计。
确定性解析
CSS 是一门强大且富有表现力的编程语言。然而,它有时会让人感觉很脆弱。 这其中有一部分原因是源于对 CSS 工作原理的误解,但很大程度上 源于为了防止具有不同优先级的 CSS 选择器发生冲突, 而在代码规范与组织方面所必须付出的成本。
StyleX 旨在同时提升样式的连贯性与可预测性,并且 增强其表现力。我们相信通过构建工具可以实现这一目标。
StyleX 提供了一个完全可预测且具确定性的
跨文件样式系统。它不仅在合并多个选择器时能产生确定性的结果,
并且在合并多个简写(shorthand)属性与完整(longhand)
属性(例如 margin 与 margin-top)时也同样如此。最后应用的样式一定会生效。
低成本的抽象
当谈及 StyleX 的性能成本时,我们的指导原则是: StyleX 应该始终是实现特定模式的最快方案。常见的 模式应该没有运行时成本,而高级的模式应该尽可能快。 我们做出的权衡是在构建时执行更多工作,从而提升 运行时的性能。
以下是实际应用中的情况:
1. 局部创建和应用的样式
当在同一个文件内编写和使用样式时,StyleX 的性能成本为
零。这是因为除了在编译时消除 create 调用外,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' };.x1e2nbdu {
color: red;
}这里没有运行时开销。
2. 跨文件使用样式
跨文件传递样式会带来少量的开销,以此换取更强的
能力和表现力。create 调用并没有被完全消除,而是
留下了一个将 key 映射到 class 名称的对象。并且 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)} />;
}.x1e2nbdu {
color: red;
}
.x1t391ir {
background-color: blue;
}这虽然多写了一点代码,但由于 props 函数的执行速度极快,
其运行时成本依然微乎其微。
大多数其他的样式解决方案都不支持跨文件边界的样式组合。 目前最先进的做法也仅仅是合并一系列 class 名称。
精简的 API 集
我们的目标是让 StyleX 尽可能保持极简且易于学习。因此,我们 不想发明过多的 API。相反,我们希望尽可能依赖 常见的 JavaScript 编程模式,并提供尽可能精简的 API 集。
从本质上讲,StyleX 可以归结为两个函数:
stylex.createstylex.props
create 用于创建样式,而 props 用于将这些样式应用到
一个元素上。
在这两个函数中,我们选择依赖常见的 JS 编程模式,而不是 为 StyleX 引入独特的 API 或编程模式。例如,我们没有 创造专属的 API 来实现条件判断。相反,我们支持使用 布尔表达式或三元表达式来按条件应用样式。
在处理 JavaScript 对象和数组时,一切都应该如预期般运作。 不应该存在任何意料之外的情况。
Type-Safe styles
TypeScript has become massively popular due to the experience and safety it provides. Our styles, however, have largely remained untyped and unreliable. Other than some path-breaking projects such as Vanilla Extract, styles are just bags of strings in most styling solutions.
StyleX is authored in Flow with strong static types. Its packages on NPM come with auto-generated types for both Flow and TypeScript. When there are incompatibilities between the two type-systems, we take the time to ensure that we write custom TypeScript types to achieve the same level of power and safety as the original Flow.
All styles are typed. When accepting styles as props, types can be used to constrain what styles are accepted. Styles should be as type-safe as any other component props.
The StyleX API is strongly typed. The styles defined with StyleX are typed too. This is made possible by using JavaScript objects to author raw styles. This is one of the big reasons we have chosen objects over template strings.
These types can then be leveraged to set contracts for the styles that a
component will accept. For example, a component props can be defined to only
accept color and backgroundColor but no other styles.
import type { StyleXStyles } from '@stylexjs/stylex';
type Props = {
//...
style?: StyleXStyles<{ color?: string; backgroundColor?: string }>;
//...
};In another example, the props may disallow margins while allowing all other styles.
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;
}>;
//...
};Styles being typed enables extremely sophisticated rules about how a component's styles can be customized with zero-runtime cost.
Shareable constants
CSS class names, CSS variables, and other CSS identifiers are defined in a global namespace. Bringing CSS strings into JavaScript can mean losing type-safety and composability.
We want styles to be type-safe, so we've spent a lot of time coming up with APIs to replace these strings with references to JavaScript constants. So far this is reflected in the following APIs:
createAbstracts away the generated class names entirely. You deal with "opaque" JavaScript objects with strong types to indicate the styles they represent.defineVarsAbstracts away the names of CSS variables generated. They can be imported as constants and used within styles directly.keyframesAbstracts away the names of keyframe animations. Instead they are declared as constants and used by reference.
We're looking into ways to make other CSS identifiers such as container-name
and @font-face type-safe as well.
Framework-agnostic
StyleX is a CSS-in-JS solution, not a CSS-in-React solution. Although StyleX has been tailored to work best with React today, it is designed to be used with any JavaScript framework that allows authoring markup in JavaScript. This includes frameworks that use JSX, template strings, etc.
props returns an object with className and style properties. A wrapper
function may be needed to convert this to make it work with various frameworks.
Encapsulation
All styles on an element should be caused by class names on that element itself.
CSS makes it very easy to author styles in a way that can cause "styles at a distance":
.className > *.className ~ *.className:hover button
All of these patterns, while powerful, make styles fragile, less predictable and harder to debug. An element could be styled without having any classes applied to it.
StyleX enables these capabilities using a different pattern that doesn't have
the same trade-offs. The stylex.when.* APIs, rely on classes that can be used
to "mark" an element which can then be "observed at a distance" for conditional
styling. More concretely, instead of a selector like .className:hover button,
StyleX relies on selectors like .marked:hover .btn. The button is always
explicitly styled with a className (btn in this case) which observes the
hover state of the marked ancestor. Without the btn class, the presence of the
marked class would not affect the button at all.
Inheritable styles such as color will still be inherited, but that is the
only form of style-at-a-distance that StyleX allows. In those cases too, the
styles applied directly on an element always take precedence over inherited
styles.
Readability & maintainability over terseness
Some recent utility-based styling solutions are extremely terse and easy to write. StyleX chooses to prioritize readability and maintainability over terseness.
StyleX makes the choice to use familiar CSS property names to prioritize readability and a shallow learning curve. (We did decide to use camelCase instead of kebab-case for convenience.)
We also enforce that styles are authored in objects separate from the HTML
elements where they are used. We made this decision to help with the readability
of HTML markup and for appropriately named styles to indicate their purpose. For
example, using a name like styles.active emphasizes why styles are being
applied without having to dig through what styles are being applied.
This principle leads to trade-offs where authoring styles may take more typing with StyleX than some other solutions.
We believe these costs are worth the improved readability over time. Giving each HTML element a semantic name can communicate a lot more than the styles themselves.
One side benefit of using references to styles rather than using the styles inline is testability. In a unit-testing environment, StyleX can be configured to remove all atomic styles and only output single debugging class names to indicate the source location of styles rather than the actual styles.
Among other benefits, it makes snapshot tests more resilient as they won't change for every style change.
Modularity and composability
NPM has made it extremely easy to share code across projects. However, sharing CSS has remained a challenge. Third-party components either have styles baked in that are hard or impossible to customize, or are completely unstyled.
The lack of a good system to predictably merge and compose styles across packages has also been an obstacle when sharing styles within packages.
StyleX aims to create a system to easily and reliably share styles along with components within packages on NPM.
Avoid global configuration
StyleX should work similarly across projects. Creating project-specific configurations that change the syntax or behavior of StyleX should be avoided. We have chosen to prioritize composability and consistency over short-term convenience. We lean on linting and types to create project-specific rules.
We also avoid magic strings that have special meaning within a project globally. Instead, every style, every variable, and every shared constant is imported from a JavaScript module without needing unique names or project configuration.
One small file over many smaller files
When dealing with a large amount of CSS, lazy-loading CSS is a way to speed up the initial load time of a page. However, it comes at the cost of slower update times, or the Interaction to Next Paint (INP) metric. Lazy-loading any CSS on a page triggers a recalculation of styles for the entire page.
StyleX is optimized for generating a single, highly optimized, CSS bundle that is loaded upfront. Our goal is to create a system where the total amount of CSS is small enough that all the CSS can be loaded upfront without a noticeable performance impact.
Other techniques to make the initial load times faster, such as "critical CSS" are compatible with StyleX, but should normally be unnecessary.