Meta 新开源的 StyleX 全面解析

2,087 阅读3分钟

stylejs-main.gif

一、什么 StyleX?

StyleX 是一个强大的 CSS-in-JS 库,用于定义优化用户界面的样式,在 Meta 的 facebook 等多款应用中已经使用多年。StyleX 使用 JavaScript 实现,不需要 postcss/less/sass 等 css 处理器支持。

二、学习资源

三、一个 React 组件汇总使用方

1)定义变量

在一个组件开始之前,需要执行变量定义是特殊的。defineVars 需要单独定义在 xxx.stylex.ext 文件, ext 可以是以下 6 种形式:

  • .stylex.js
  • .stylex.mjs
  • .stylex.cjs
  • .stylex.ts
  • .stylex.tsx
  • .stylex.jsx

以下是一个示例:colors.stylex.ts

const colors = stylex.defineVars({
    accent: "blue",
    background: "white",
    line: "gray",
    textPrimary: "black",
    textSecondary: "#333",
});

每一个属性编译后都会产生一个css 变量:

屏幕截图 2023-12-14 061004.png

2)一个 React 组件展示大部部分 stylex 的写法

  • 导入方式
  • 动画帧
  • 主题
  • 动态样式
  • 静态样式
  • 动画帧
import * as stylex from "@stylexjs/stylex";
import React from "react";

export function Route() {
  // 动画帧
  const pulse = stylex.keyframes({
    "0%": { transform: "scale(1)" },
    "50%": { transform: "scale(1.1)" },
    "100%": { transform: "scale(1)" },
  });
    
  // 定义主题变量
  const theme = stylex.createTheme(colors, {
    accentColor: "red",
    backgroundColor: "gray",
    lineColor: "purple",
    textPrimaryColor: "black",
    textSecondaryColor: "brown",
  });

  // 创建样式
  const styles = stylex.create({
    // 静态样式
    root: {
      backgroundColor: "red",
      padding: "1rem",
      paddingInlineStart: "2rem",
      position: stylex.firstThatWorks("sticky", "-webkit-sticky", "fixed"), // 回退方案
    },
    // 动态样式
    dynamic: (r, g, b) => ({
      color: `rgb(${r}, ${g}, ${b})`,
      backgroundColor: colors.background,
    }),
    // 帧动画样式
    pulse: {
      animationName: pulse,
      animationDuration: "1s",
      animationIterationCount: "infinite",
    },
  });

  return (
    <div
        // 使用 props 消费对象
      {...(stylex.props(styles.root, styles.dynamic(state.opacity)), theme)} 
    >Your Html</div>
  );
}

经过编译之后输出的内容:

属性-动静态.png

注意:这是仅仅是一个示例,目标是将 stylex 的特性放在一个 React 组件展示。需要注意的是变量需要单独文件定义。

四、安装与使用

以 Remix Vite 为例,为什么用 Remix ? 因为可以很方便的测试服务端渲染的支持情况。

1)初始化项目

npx create-remix@latest --template remix-run/remix/templates/unstable-vite-express

2)安装依赖

pnpm add @stylexjs/stylex
pnpm add vite-plugin-stylex -D

3)定义指令 @stylex

@stylex stylesheet; // 然后将指令导入 root.tsx 中。

五、6 个 JS 核心 api

  • stylex.create():使用对象的形式 创建 静态和动态样式
  • stylex.props(): 使用对象的形式 设置 React 的 Props 对象
  • stylex.keyframes(): 创建 css 关键帧动画
  • stylex.firstThatWorks(): 定义样式回退方案
  • stylex.defineVars(): 定义变量
  • stylex.createTheme(): 定义主题

六、定义变量文件 var.styles.tsx

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

export const tokens = stylex.defineVars({
  accent: "blue",
  background: "white",
  line: "gray",
  textPrimary: "black",
  textSecondary: "#333",
});

定义静态样式不能使用 ESM 的默认输出,一般使用 export 输出一个变量即可。

七、定义静态样式

const styles = stylex.create({
  root: {
    backgroundColor: "red",
    padding: "1rem",
    paddingInlineStart: "2rem",
    position: stylex.firstThatWorks("sticky", "-webkit-sticky", "fixed"),
  },
});
export default function Route() {
  return (
    <div {...stylex.props(styles.root)}>
     <div>123</div>
    </div>
  );
}

使用 create 函数定义静态属性,并获取 styles,使用 props 函数消费 styles 中的对象。

八、定义与消费动态样式

const styles = stylex.create({
  dynamic: (r, g, b) => ({
    color: `rgb(${tokens.background}, ${g}, ${b})`, // 函数参数
    background: tokens.textPrimary, // 消费主题变量
  }),
});
export default function Route() {
  return (
    <div {...stylex.props(styles.root)}>
      <div {...stylex.props(styles.dynamic(23, 56, 65))}></div>
    </div>
  );
}

create 函数传入的映射对象是一个函数。注意当前 Remix 插件中,仅仅支持 create 直接传入静态属性,如果是动态属性,推荐使用 函数 形式定义。

九、定义主题

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

export const tokens = stylex.defineVars({
  accent: "blue",
  background: "white",
  line: "gray",
  textPrimary: "black",
  textSecondary: "#333",
});

1)定义动画帧

const pulse = stylex.keyframes({
  "0%": { transform: "scale(1)" },
  "50%": { transform: "scale(1.1)" },
  "100%": { transform: "scale(1)" },
});

十、定义伪元素和伪类

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

const styles = stylex.create({
  button: {
    backgroundColor: {
      default: "lightblue",
      ":hover": "blue",
      ":active": "darkblue",
    },
    input: {
      // pseudo-element
      "::placeholder": {
        color: "#999",
      },
      color: {
        default: "#333",
        // pseudo-class
        ":invalid": "red",
      },
    },
  },
});

十一、源码解析

1)构建形式

  • stylex-monorepo 构建
  • 基于 flow 构建静态类型
  • apps/ 中包含文档与示例
  • packages/ 中包含源码实现
  • 同时基于 vercel + nextjs 部署。

2)重要依赖 styleq

styleQ 是一个快速、小型的 JavaScript 运行时,用于合并 CSS 编译器生成的 HTML 类名称。

stylex 目前使用基于 js 构建使用 flow 作为类型检查。默认输出 _styles:

export const stylex: IStyleX = _stylex;
export default (_stylex: IStyleX);

3)什么是猴子补丁

monkey_patch 是指在运行时修改或扩展现有的代码,通常是在不修改原始源代码的情况下对现有类、模块或对象进行修改。这种技术通常在需要临时修复或改进第三方库或模块时使用,而不必直接修改它们的源代码。

const __implementations: { [string]: $FlowFixMe } = {};

export function __monkey_patch__(
  key: string,
  implementation: $FlowFixMe,
): void {
  if (key === 'types') {
    Object.assign(types, implementation);
  } else {
    __implementations[key] = implementation;
  }
}

猴子补丁在:

  • gen-types 生成类型时中使用
  • dev-runtime 中运行实现 stylex 的各种方法

4)create 函数

export const create: Stylex$Create = stylexCreate;

create 时创建 css 属性对象的函数,与 props 函数一起消费。

5)props 函数实现

props 其实是消费 create 函数的内容的函数,从返回值类型就能知道是当前元素的 className 和样式 style 组合成的对象:

 $ReadOnly<{
  className?: string,
  style?: $ReadOnly<{ [string]: string | number }>,
}

props 生成 className 和 style 就是之前的用 styleq 实现的:

const [className, style] = styleq(styles)
 
const create: Stylex$Create = __implementations.create;
return create<S>(styles);

主要函数其实就是 create/props 两个函数。其他的函数实现也是类似。props 函数输出的对象大致是这样的:

stylex props 渲染.png

十二、vite 插件 vite-plugin-stylex 源码

vite-plugin-stylex 插件源码,基于 trubo 构建,核心源码在 /packages/vite-plugin-stylex 中,vite 插件源码是基于 babel 能力进行转换:

  • "@babel/core": "^7.23.5",
  • "@babel/plugin-syntax-flow": "^7.23.3",
  • "@babel/plugin-syntax-jsx": "^7.23.3",
  • "@babel/plugin-syntax-typescript": "^7.23.3",
  • "@stylexjs/babel-plugin": "^0.3.0"

在 Vite 种使用 babel 的转换器:

const result = await babel.transformAsync(inputCode, {
    babelrc: false,
    filename: id,
    plugins: [
      /\.jsx?/.test(path.extname(id))
        ? flowSyntaxPlugin
        : typescriptSyntaxPlugin,
      jsxSyntaxPlugin,
      [
        stylexBabelPlugin,
        {
          dev: !isProd,
          unstable_moduleResolution,
          importSources: stylexImports,
          runtimeInjection: !isCompileMode,
          ...options,
        },
      ],
    ],
  });

十三、第三方支持

  • babel 插件
  • runtime-dev 运行时
  • eslint 插件
  • nextjs 插件
  • open-props 属性
  • rollup 插件
  • webpack 插件支持

当然 vite 中第三方插件,其中值得一提的是 open-propsopen-props 中包含了众多的变量、动画等等内容,如果你熟悉 open-props 这个库很有用。

十四、小结

本文比较全面的解析了 StyleX 的内容,包含如何使用: 创建/消费/变量/主题/帧动画/... 等等。当然除了使用也包含 vite 与 stylex 的部分前端工程化实现。希望能够帮助读者能够比较全面的理解 stylex。