跳到主要内容

StyleX 编程思想

核心原则

To understand why StyleX exists and the reasoning behind its decisions, it may be beneficial to familiarize oneself with the fundamental principles that guide it. This may help you decide if StyleX is the right solution for you.

要了解 StyleX 存在的原因及其决策背后的理由,熟悉指导 StyleX 的基本原则可能会有所裨益。这可以帮助你决定 StyleX 是否是适合你的解决方案。

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

Co-location

DRY(避免重复) 代码有其好处,但我们认为在编写样式时通常并非如此。 编写样式的最好、最易读的方法是将样式与 标记写在同一个文件中。

StyleX is designed for authoring, applying, and reasoning about styles locally. StyleX 被设计用于编写、应用和推理样式。

确定性解析

CSS is a powerful and expressive language. However, it can sometimes feel fragile. Some of this stems from a misunderstanding of how CSS works, but a lot of it stems from the discipline and organization required to keep CSS selectors with different specificities from conflicting. CSS 是一门功能强大、表现力丰富的语言。不过,它有时 也会让人感觉脆弱。这其中有一部分原因是对 CSS 工作原理的误解,但 CSS 选择器在解决样式问题上存在很多困难,其中很大一部分原因是,要使具有不同特性的 CSS 选择器不发生冲突,就需要对其进行规范和组织。

现有的大多数解决方案都依赖于规则和约定。

BEM 和 OOCSS 约定BEM 和 OOCSS 引入了命名约定来避免这些问题,这依赖于 开发人员始终如一地遵守这些规则,并完全避免合并样式。 这会导致 CSS 变得臃肿。
实用工具类Atomic utility class names like Tailwind CSS and Tachyons rely on conventions and lint rules to ensure that conflicting class names are not applied on the same element. Such tooling adds constraints on where and how styles can be applied, putting architectural limitations on styling. 像 Tailwind CSS 和 Tachyons 这样的原子实用类名称依赖于约定和内核规则,以确保不会在同一元素上应用相互冲突的类名。此类工具会对样式的应用位置和方式增加限制,从而对样式设计造成架构限制。

StyleX 的目标是提高样式的一致性和可预测性 以及表达能力。我们相信通过构建工具可以做到 这一点。

StyleX provides a completely predictable and deterministic styling system that works across files. It produces deterministic results not only when merging multiple selectors, but also when merging multiple shorthand and longhand properties. (e.g. margin vs margin-top). StyleX 提供了一个完全可预测且确定的样式 系统,它可以跨文件工作。不仅在合并多个选择器时,而且在合并多个速记和长手属性时,它都能产生确定的结果。(例如,“边距 ”与 “边距上”)。

“最后赋予的样式起决定作用!”

低成本的抽象

说到 StyleX 的性能开销,我们的指导原则是 StyleX 应始终是实现特定模式的最快方法。 常规模式不应产生运行时成本,高级模式则应 尽可能快。为了提高运行时性能,我们会在构建时完成更多的 工作。

以下是实际应用中的情况:

1. Styles created and applied locally

When authoring and consuming styles within the same file, the cost of StyleX is zero. This is because in addition to compiling away stylex.create calls, StyleX also compiles away stylex.props calls when possible.

So,

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

Compiles down to:

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

let a = {className: 'x1e2nbdu'};

There is no runtime overhead here.

2. Using styles across files

Passing styles across file boundaries incurs a small cost for the additional power and expressivity. The stylex.create call is not deleted entirely and instead leaves behind an object mapping keys to class names. And the stylex.props() calls are executed at runtime.

This code, for example:

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)} />;
}

Compiles down to:

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)} />;
}

This is a little more code, but the runtime cost is still minimal because of how fast the stylex.props() function is.

Most other styling solutions don't enable composition of styles across file boundaries. The state of the art is to combine lists of class names.

Small API surface

Our goal is to make StyleX as minimal and easy-to-learn as possible. As such we don't want to invent too many APIs. Instead, we want to be able to lean on common JavaScript patterns where possible and provide the smallest API surface possible.

At its core, StyleX can be boiled down to two functions:

  1. stylex.create
  2. stylex.props

stylex.create is used to create styles and stylex.props is used to apply those styles to an element.

Within these two functions, we choose to rely on common JS patterns rather than introduce unique APIs or patterns for StyleX. For example, we don't have an API for conditional styles. Instead, we support applying styles conditionally with boolean or ternary expressions.

Things should work as expected when dealing with JavaScript objects and arrays. There should be no surprises.

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:

  1. stylex.create Abstracts away the generated class names entirely. You deal with "opaque" JavaScript objects with strong types to indicate the styles they represent.
  2. stylex.defineVars Abstracts away the names of CSS variables generated. They can be imported as constants and used within styles directly.
  3. stylex.keyframes Abstracts 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.

stylex.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 > div:first-child

All of these patterns, while powerful, make styles fragile and less predictable. Applying class names on one element can affect a completely different element.

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.

This is often not the case when using complex selectors, as the complex selectors usually have higher specificity than the simple class selectors used for styles applied directly on the element.

StyleX disallows this entire class of selectors. This currently makes certain CSS patterns impossible to achieve with StyleX. Our goal is to support these patterns without sacrificing style encapsulation.

StyleX is not a CSS pre-processor. It intentionally puts constraints on the power of CSS selectors in order to build a fast and predictable system. The API, based on JavaScript objects instead of template strings, is designed to make these constraints feel natural.

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 a JavaScript import 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.