探索 StyleX 和新一代样式库

315 阅读13分钟

前言

在网络开发领域,CSS 不断发展,塑造了网络的视觉景观。本指南以 StyleX 为重点,探讨了样式库的优缺点,以及如何应对大型企业项目所面临的挑战。今日前端早读课文章由 @飘飘翻译分享。

正文从这开始~~

在浩瀚而充满活力的网站开发世界中,走在潮流前面不仅仅是一种优势 —— 它是至关重要的。多年来,CSS 已经发展并持续塑造着网络的视觉格局。但是,就像每种技术一样,随着 CSS 的发展,我们可用的工具也在不断发展。

图片

在本指南中,我们将深入研究样式库领域,但重点关注 StyleX。我们将介绍你需要了解的与 StyleX 相关的知识,包括其优点和缺点,以便你决定是否将此解决方案作为你的选择。

你可以在此 GitHub 库中查看项目代码,了解本教程中会讲解的代码示例。让我们开始吧。

理解开发趋势

随着 CSS 持续演进,了解何时使用简化解决方案以及何时使用复杂框架至关重要。

例如,当项目需要高水平的组织和可扩展性时,开发人员经常会转向复杂框架。然而,对于较快速开发来说,尤其是在较小项目环境中,开发人员倾向于采用更加分散的方法。

我们应当能够根据我们正在处理的项目要求来访问和调整我们偏好的解决方案。对此循环性质的了解对于我们保持现状和高效工作至关重要。

CSS 和现有库

在探讨 StyleX 之前,让我们先来讨论一下普通 CSS 所面临的挑战及其带来的问题,以及与现有库相关的问题。

了解纯 CSS 的痛点

虽然原生 CSS 已发展到包含嵌套等功能,但它仍然存在一些问题,妨碍了样式设计的便捷性。

请看下面的例子。你认为哪种颜色适用于标题文本?先看看 HTML:

 <h1 class="red blue">Text</h1>

然后是 CSS:

 .blue {
   color: blue;
 }

 .red {
   color: red
 }

在代码中,红色和蓝色类的样式冲突,可能导致元素以我们不希望的方式呈现。

由于两个选择器具有相同的特定性,因此 CSS 文件中的类的顺序优先于它们在 HTML 类属性中的显示顺序。这种行为可能看起来并不直观,导致在我们真正想要蓝色的时候,却使用了红色。

如果这些类被分割到两个样式表中,那么现在的优先级取决于它们插入页面的顺序。这种缺乏可预测性的情况可能会导致意想不到的结果,从而增加管理样式的难度,尤其是在大型项目中。

纯 CSS 可能产生的其他问题包括与全局范围的命名冲突、特殊性问题等。虽然我们可以通过像 BEM 这样更具结构化的方法来解决这些问题,但它可能会导致 CSS 变得臃肿。

CSS/Sass 模块

CSS 模块解决了命名冲突问题,并有助于避免与普通 CSS 相关的全局范围问题。它允许开发人员创建自带样式的组件,以加强代码组织。

不过,CSS 模块的缺点之一是无法防止不同文件中重复的 CSS 定义,这可能会导致样式冗余。

Tailwind CSS

Tailwind CSS 以其实用优先的方法而闻名,已成为许多开发人员的首选样式解决方案。它有助于解决前一种解决方案的弊端,例如,它可以通过自动清除未使用的样式来缓解冗余样式问题。

此外,Tailwind CSS 还可以利用现成的实用工具类来实现快速原型开发。同样,它还促进了样式与 HTML 组件的搭配(这有助于代码审查),以及单个组件的样式,而无需在 CSS 文件之间进行导航。

虽然这种方法往往能改善开发人员的体验,但稍有不慎,就会导致 HTML 和 JSX 代码臃肿。这种方法的另一个小缺点是学习曲线陡峭 -- 你必须学习 CSS 的抽象。

CSS-in-JS 解决方案

CSS-in-JS 在构建基于组件的项目(如 React)时很受欢迎。这种方法具有多种优势,例如改进了组件封装,允许应用程序的 CSS 充分利用 JavaScript 的表现力。

CSS-in-JS 解决方案可分为运行时解决方案和构建时解决方案:

  • 运行时解决方案(如 Emotions 和 styled-components)将在运行时解析和注入样式。然而,这可能会引入运行时开销,从而导致性能瓶颈
  • vanilla-extract 和 Linaria 等构建时解决方案通过在构建时将 CSS 从 JavaScript 或 TypeScript 文件中提取到静态 CSS 文件中来解决这一问题,从而消除了运行时的瓶颈。

虽然这些库的原理相似,但方法却各不相同。例如,vanilla-extract 利用 TypeScript 提供类型安全样式,而 Linaria 则缺乏 CSS 属性的类型检查。

相反,vanilla-extract 与 Linaria 不同,它强制 CSS 样式保留在单独的 .css.ts 文件中。这就失去了搭配的好处,因为我们往往会在 CSS 和组件文件之间切换。

什么是 StyleX?

正如我们已经简要探讨过的,上述每种解决方案都有其独特的功能、优势和权衡。现在的问题是:StyleX 能提供什么?

StyleX 是一个构建时类型安全的 CSS-in-JS 库,最近由 Meta 公司开源。Meta 团队开发该库的目的是解决大型企业项目面临的一些主要挑战。

前面我们提到了 CSS-in-JS 如何帮助改进组件封装。这种模式还可以实现组件的模块化和可组合性。反过来,它也允许在不同项目中重复使用用户界面代码。这固然很好,但定制预定义样式也是一项挑战。

这就是 StyleX 的用武之地。它的亮点在于能够跨包可预测地合并和组合样式。

StyleX 的简单演示

我们将使用 Vite 进行实际演示。因此,请先安装 Vite。然后,安装 StyleX 运行时软件包:

 npm install --save @stylexjs/stylex

根据您使用的捆绑程序,我们需要安装集成 StyleX 的插件。对于 Vite,请安装以下插件:

 npm install --save-dev vite-plugin-stylex

然后将插件添加到 Vite 配置文件(vite.config.ts)中:

 // ... other imports

 import styleX from "vite-plugin-stylex";

 export default defineConfig({
   plugins: [react(), styleX()],
 });
StyleX 语法和用法

让我们从使用两个 StyleX API 开始:stylex.create() 用于建立样式规则,stylex.props() 用于将这些样式应用到元素中。

我们将导入 stylex 并这样使用它:

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

 const styles = stylex.create({
   base: {
     color: 'blue',
     fontSize: 30,
   },
 });

 export function SimpleText() {
   return <h1 {...stylex.props(styles.base)}>I am a heading text</h1>;
 }

结果应该是这样的

图片

stylex.create() 会生成无碰撞的原子 CSS,其样式规则会在构建时被提取到一个静态文件中。这样,我们就只剩下优化组件和单独文件中生成的原子 CSS 了。我们消除了 CSS-in-JS 的运行时成本,并保留了与 SSR 的兼容性。

如果我们运行 npm run build 命令,就会生成一个包含生产就绪文件(包括静态 CSS 文件)的构建文件夹:

图片

这种实现方式可确保并行加载 CSS 和 JavaScript 资源,从而提高性能。通过原子 CSS 方法,StyleX 可以最大限度地减少 CSS 包的整体大小,从而获得额外的性能优势。

检查冲突样式

如果我们在 stylex.create() 中包含额外的键,StyleX 将考虑样式应用到元素的顺序,而不是样式的定义方式:

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

 const styles = stylex.create({
   colorRed: {
     color: 'red',
   },
   base: {
     color: 'blue',
     fontSize: 30,
   },
 });

 export function SimpleText() {
   return (
     <h1 {...stylex.props(styles.base, styles.colorRed)}>
       I am a heading text
     </h1>
   );
 }

这使得 StyleX 具有可预测性和直观性。因此,在上述代码中,最后应用的样式获胜!看看吧

图片

再也不用担心风格规则的冲突了。

利用 StyleX 满足更复杂的样式需求

特别是当你使用过 Tailwind CSS 等其他库时,你会想到的一点是,StyleX 似乎比其他样式解决方案更复杂。但是,如果与可重复使用的用户界面组件一起使用,或者在设计系统和样式变体方面更加复杂时,StyleX 的优势就显而易见了。

让我们看看如何通过对可重复使用的用户界面组件进行样式化来编写更复杂的代码。

可重用按钮组件

下面的代码定义了一个可重复使用的按钮组件:

 import { ComponentProps } from 'react';

 type CustomButtonProps = {} & ComponentProps<'button'>;

 export function Button({ ...props }: CustomButtonProps) {
   return <button {...props} />;
 }

为了确保类型安全,我们利用 ComponentProps 类型来继承标准的按钮元素道具(如 onCLick 和 className),同时还允许根据需要使用其他自定义道具。然后,我们可以像这样渲染组件:

 <Button>Button</Button>

请注意,我们还没有使用 StyleX!下一步我们将讨论这个问题。\n\n 应用默认样式 \n 让我们为按钮应用默认的 StyleX 样式:

 // ... other imports
 import * as stylex from '@stylexjs/stylex';

 type CustomButtonProps = {} & ComponentProps<'button'>;

 const btnStyles = stylex.create({
   default: {
     color: '#fff',
     border: 'none',
     backgroundColor: '#0f172a',
     borderRadius: '.25rem',
     height: '2.5rem',
     padding: '0.5rem 1rem',
     cursor: 'pointer',
   },
 });

 export function Button({ ...props }: CustomButtonProps) {
   return <button {...stylex.props(btnStyles.default)} {...props} />;
 }

按钮现在应该是这样的:

图片

应用变量和条件样式

变体提供了一种根据特定条件动态调整按钮外观的方法。在我们的示例中,我们将定义轮廓、破坏和重影等变体。这种灵活性确保了动态和个性化的用户界面。

让我们在默认命名空间中定义每个变量:

 const btnStyles = stylex.create({
   // ...default style here

   outline: {
     color: '#000',
     backgroundColor: '#feffff',
     border: '1px solid #dbdbdb',
   },
   destructive: {
     backgroundColor: '#f15756',
   },
   ghost: {
     color: '#000',
     backgroundColor: 'transparent',
   },
 });

接下来,我们可以使用 btnStyles 对象中的 variant prop 作为键来应用相关样式。如果没有提供变量,我们将保留默认样式:

 type CustomButtonProps = {
   variant?: 'outline' | 'destructive' | 'ghost';
 } & ComponentProps<'button'>;

 const btnStyles = stylex.create({
   // styles...
 });

 export function Button({ variant, ...props }: CustomButtonProps) {
   return (
     <button
       {...stylex.props(
         btnStyles.default,
         variant && btnStyles[variant]
       )}
       {...props}
     />
   );
 }

在 stylex.props() 中,我们利用 && 运算符在提供变体道具时有条件地应用样式。这些样式将与默认样式合并,从而产生预期的视觉输出。

这是许多库面临的共同挑战。例如,Tailwind CSS 就很难有效地合并类,从而导致非预期的行为。不过,Tailwind 开发人员通常会求助于第三方解决方案(如 tailwind-merge)来克服这一障碍。

如果我们在组件元素中添加变体道具:

 <Button>Default</Button>
 <Button variant="destructive">Destructive</Button>
 <Button variant="ghost">Ghost</Button>
 <Button variant="outline">Outline</Button>

结果应该是这样的

图片

允许自定义样式

与大多数库不同,StyleX 简化了最终用户覆盖预定义组件样式的过程。它可以跨组件边界智能合并样式。

在 Button 组件中,我们将传递一个样式道具,并将其追加到 stylex.props() 函数中的本地样式之后:

 type CustomButtonProps = {
   variant?: 'outline' | 'destructive' | 'ghost';
   styles?: stylex.StyleXStyles;
 } & ComponentProps<'button'>;

 const btnStyles = stylex.create({
   // ...
 });

 export function Button({
   variant,
   styles,
   ...props
 }: CustomButtonProps) {
   return (
     <button
       {...stylex.props(
         btnStyles.default,
         variant && btnStyles[variant],
         styles
       )}
       {...props}
     />
   );
 }

我们为道具样式命名,但您也可以根据自己的喜好随意命名。请注意,我们还利用了 StyleXStyles 来接受任何任意的 StyleX 样式。

现在,我们可以向按钮组件传递自定义样式:

 const styles = stylex.create({
   button: {
     backgroundColor: 'red',
   },
 });

 export default function App() {
   return (
     <Button variant="destructive" styles={styles.button}>
       Destructive
     </Button>
   );
 }
限制可接受的样式

StyleX 简化了限制可传递给组件的样式的过程。如果我们特别希望只允许使用某些样式,我们可以向 StyleXStyles<{...}> 传递一个包含所需属性的对象类型:

 styles?: stylex.StyleXStyles<{
   color?: string;
   backgroundColor?: string;
 }>;

在这种情况下,我们只能向组件提供如下颜色和 backgroundColor:

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

如果尝试传递其他样式,如下面代码中的 fontSize,则会导致类型错误:

 const styles = stylex.create({
   button: {
     // ...
     fontSize: 30
   },
 });

相反,我们可能不希望使用 StyleXStyles 允许某些样式,而是希望使用 StyleXStylesWithout 禁止特定属性:

 styles?: stylex.StyleXStylesWithout<{
   backgroundColor: unknown;
 }>;

在这种情况下,我们可以传递除了 backgroundColor 之外的任何 StyleX 属性。否则,我们将收到一个类型错误:

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

这种额外的类型安全性非常好!

嵌套样式值

为了使用 StyleX 处理伪选择器,我们可以在 StyleX 样式属性中嵌套选择器。例如,我们可以这样处理可复用组件 Button 的 hover 状态伪类:

 const btnStyles = stylex.create({
   default: {
     // ...
     opacity: {
       default: 1,
       ':hover': 0.8,
     },
   },
   outline: {
     color: '#000',
     backgroundColor: {
       default: '#feffff',
       ':hover': '#f3f3f3',
     },
     border: '1px solid #dbdbdb',
   },
   destructive: {
     backgroundColor: '#f15756',
   },
   ghost: {
     color: '#000',
     backgroundColor: {
       default: 'transparent',
       ':hover': '#f3f3f3',
     },
   },
 });

在 default 命名空间中,我们添加了一个伪类以更改悬停时的不透明度。这适用于各种场景中的按钮。我们还针对 outline 和 ghost 命名空间,并应用伪类以更改悬停时的背景颜色。

同样,我们可以应用伪元素和媒体查询:

 const styles = stylex.create({
   button: {
     width: {
       default: 200,
       '@media (max-width: 400px)': '100%',
     },
   },
 });
使用 CSS 变量

StyleX 允许我们在专用的 .stylex.ts 或 .stylex.js 文件中使用 stylex.defineVars API 来定义自定义属性。例如,我们可以创建一个 tokens.stylex.ts 文件并定义我们的变量:

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

 const DARK = '@media (prefers-color-scheme: dark)';

 export const colors = stylex.defineVars({
   primaryColor: { default: 'white', [DARK]: 'black' },
   primaryDarkColor: { default: 'black', [DARK]: 'white' },
   lightGreyColor: { default: '#f3f3f3', [DARK]: '#605e5e' },
 });

 export const spacing = stylex.defineVars({
   sizeSm: '.25rem',
   sizeXl: '2.5rem',
 });

我们根据用户的首选配色方案或设备的首选配色方案为变量定义了不同的值。StyleX 将在编译时处理 stylex.defineVars,并自动为相应的标记生成 CSS 变量名。

要使用这些变量,我们可以导入它们并在 stylex.create 中使用它们:

 import { colors, spacing } from '../../tokens.stylex';
 // ...
 const btnStyles = stylex.create({
   default: {
     color: colors.primaryColor,
     border: 'none',
     backgroundColor: colors.primaryDarkColor,
     borderRadius: spacing.sizeSm,
     height: spacing.sizeXl,
     // ...
   },
   outline: {
     color: colors.primaryDarkColor,
     backgroundColor: {
       default: colors.primaryColor,
       ':hover': colors.lightGreyColor,
     },
     border: '1px solid #dbdbdb',
   },
   destructive: {
     backgroundColor: '#f15756',
   },
   ghost: {
     color: colors.primaryDarkColor,
     // ...
   },
 });
动态样式

StyleX 还支持运行时的动态样式,其灵感来自 Linaria 生成 CSS 自定义属性的方法。

要实现动态样式,我们将样式定义为一个函数,并传入动态值。在以下代码中,我们利用组件状态来确定按钮的不透明度,模拟异步表单提交:

 const styles = stylex.create({
   // ...
   dynamicStyle: (value) => ({
     opacity: value,
   }),
 });

 export default function App() {
   const [isSubmitting, setIsSubmitting] = useState(false);

   const handleButtonClick = async () => {
     // Simulate an asynchronous form submission
     setIsSubmitting(true);
     await new Promise((resolve) => setTimeout(resolve, 2000)); // Simulating a delay
     setIsSubmitting(false);
   };

   return (
     <div>
       {/* ... */}
       <Button
         onClick={handleButtonClick}
         styles={styles.dynamicStyle(isSubmitting ? 0.5 : 1)}
       >
         {isSubmitting ? 'Submitting...' : 'Submit'}
       </Button>
     </div>
   );
 }

请看下面的结果:

图片

StyleX:Tailwind 的终结者?

一些人误以为 StyleX 是 Tailwind 的终结者。然而,这种说法并不准确。StyleX 并不是旨在取代 Tailwind,而是在样式领域有不同的用途。

Tailwind 擅长促进快速入门,并且适用于独立项目,而 StyleX 则解决了大型企业项目中常见的重大挑战。具体来说,StyleX 能够跨包无缝预测、合并和组合样式。

StyleX 适用于那些可能不同意 Tailwind 的方法,但正在寻找一种类型安全的 CSS-in-JS 解决方案且不会产生运行时开销的。

结论

随着 CSS 的不断发展,我们的工具也在不断更新。本指南深入探讨了样式库,重点介绍了 StyleX。

在整个教程中,我们讨论了 StyleX 的细微差别,研究了它的优点和缺点。了解这些信息,你就可以针对 StyleX 是否是你满足不断变化的 Web 样式世界需求的正确解决方案做出明智的决定了。

如果你有任何问题或见解,请在评论区分享你的想法。如果你喜欢这篇文章,请分享给更多的人。

在 GitHub 上查看项目代码:github.com/Ibaslogic/s…