style9:构建时 CSS-in-JS

Avatar of Johan Holmerin
Johan Holmerin

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 免费信用额度!

去年 4 月,Facebook 公布了其重大的全新设计。这是一个雄心勃勃的项目,是对一个拥有大量用户的庞大网站进行重建。为了完成这项工作,他们使用了他们创建并开源的几项技术,例如 React、GraphQL、Relay,以及一个名为 stylex 的全新 CSS-in-JS 库。

这个新库是 Facebook 内部的,但他们已经分享了足够的信息来创建开源实现,即 style9

为什么要使用另一个 CIJ 库?

已经有很多 CSS-in-JS (CIJ) 库,因此可能并不明显为什么需要另一个库。style9 与所有其他 CIJ 解决方案具有相同的优势,正如 Christopher Chedeau 所阐述的那样,包括作用域选择器、死代码消除、确定性解析,以及在 CSS 和 JavaScript 之间共享值的能力。

但是,style9 有几件事是独特的。

最小的运行时

虽然样式是在 JavaScript 中定义的,但它们被编译器提取到一个普通的 CSS 文件中。这意味着,您的最终 JavaScript 文件中没有运送任何样式。唯一剩下的只是最终的类名,最小的运行时将有条件地应用它们,就像您通常做的那样。这会导致更小的代码包,减少内存使用,并加快渲染速度。

由于值是在编译时提取的,因此无法使用真正动态的值。这些幸运的是并不常见,而且由于它们是唯一的,因此不会受到内联定义的影响。更常见的是有条件地应用样式,这当然得到支持。由于 babel 的 path.evaluate,局部常量和数学表达式也得到了支持。

原子输出

由于 style9 的工作方式,每个属性声明都可以被设置为具有单个属性的独立类。因此,例如,如果我们在代码中的多个地方使用 opacity: 0,它只会出现在生成的 CSS 中一次。这样做的好处是,CSS 文件的大小会随着唯一声明的数量而增长,而不是随着总声明数量的增长而增长。由于大多数属性被多次使用,因此这会导致生成的 CSS 文件明显更小。例如,Facebook 旧主页使用 413 KB 的压缩 CSS。重新设计后的所有页面使用 74 KB。同样,更小的文件大小会导致更好的性能。

来自 使用 React 和 Relay 构建新的 Facebook,作者:Frank Yan,在 13:23 展示了原子 CSS 的对数尺度。

有些人可能会抱怨这一点,生成的类名没有语义,它们是不透明的,并且忽略了级联。这是真的。我们将 CSS 视为编译目标。但这是有充分理由的。通过质疑先前被认为最佳的做法,我们可以改善用户和开发人员体验。

此外,style9 还有许多其他很棒的功能,包括:使用 TypeScript 的类型化样式、未使用样式消除、使用 JavaScript 变量的能力,以及对媒体查询、伪选择器和关键帧的支持。

使用方法

首先,像往常一样安装它

npm install style9

style9 具有 Rollup、Webpack、Gatsby 和 Next.js 的插件,它们都基于 Babel 插件。有关如何使用它们的说明,请参见 存储库。这里,我们将使用 webpack 插件。

const Style9Plugin = require('style9/webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  module: {
    rules: [
      // This will transform the style9 calls
      {
        test: /\.(tsx|ts|js|mjs|jsx)$/,
        use: Style9Plugin.loader
      },
      // This is part of the normal Webpack CSS extraction
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
      }
    ]
  },
  plugins: [
    // This will sort and remove duplicate declarations in the final CSS file
    new Style9Plugin(),
    // This is part of the normal Webpack CSS extraction
    new MiniCssExtractPlugin()
  ]
};

定义样式

创建样式的语法与其他库非常相似。我们首先使用样式对象调用 style9.create

import style9 from 'style9';

const styles = style9.create({
  button: {
    padding: 0,
    color: 'rebeccapurple'
  },
  padding: {
    padding: 12
  },
  icon: {
    width: 24,
    height: 24
  }
});

由于所有声明都会导致原子类,因此 flex: 1background: blue 之类的简写将不起作用,因为它们设置了多个属性。可以扩展的属性(例如 paddingmarginoverflow 等)将自动转换为其长格式变体。如果使用 TypeScript,则在使用不支持的属性时会收到错误。

解析样式

要生成类名,我们现在可以调用由 style9.create 返回的函数。它接受我们想要使用的样式键作为参数

const className = styles('button');

该函数的工作方式是,右边的样式优先,并将与左边的样式合并,就像 Object.assign 一样。以下将导致一个元素,其填充为 12px,文本为 rebeccapurple

const className = styles('button', 'padding');

我们可以使用以下任何一种格式有条件地应用样式

// logical AND
styles('button', hasPadding && 'padding');
// ternary
styles('button', isGreen ? 'green' : 'red');
// object of booleans
styles({
  button: true,
  green: isGreen,
  padding: hasPadding
});

这些函数调用将在编译期间被删除,并用直接字符串串联替换。上面代码中的第一行将被替换为类似 'c1r9f2e5 ' + hasPadding ? 'cu2kwdz ' : '' 的内容。没有留下运行时。

组合样式

我们可以通过使用属性名称访问样式对象并将其传递给 style9 来扩展样式对象。

const styles = style9.create({ blue: { color: 'blue; } });
const otherStyles = style9.create({ red: { color: 'red; } });

// will be red
const className = style9(styles.blue, otherStyles.red);

就像函数调用一样,右边的样式优先。但是,在这种情况下,类名无法静态解析。相反,属性值将被替换为类,并在运行时连接。属性就像以前一样被添加到 CSS 文件中。

总结

CSS-in-JS 的好处是实实在在的。也就是说,当我们在代码中嵌入样式时,我们正在承担性能成本。通过在构建时提取值,我们可以同时获得两全其美。我们从将样式与标记代码并置以及使用现有 JavaScript 基础设施的能力中获益,同时也能生成最佳的样式表。

如果 style9 对您来说听起来很有趣,请查看存储库并尝试一下。如果您有任何问题,请随时打开一个 issue 或与我们联系。

鸣谢

感谢 Giuseppe Gurgone 对 style-sheetdss 的贡献,感谢 Nicolas Gallagher 对 react-native-web 的贡献,感谢 Satyajit Sahoo 和 Callstack 的所有人对 linaria 的贡献,感谢 Christopher Chedeau、Sebastian McKenzie、Frank Yan、Ashley Watkins、Naman Goel 以及 Facebook 上所有在 stylex 上工作的人,感谢他们愿意公开分享他们的经验教训。还有其他任何我可能遗漏的人。