CSS-in-JS 真的不好吗?

在我成为开发人员的头几年里,我并不太喜欢 CSS。我害怕打开 CSS 文件。我对 z-index 抱怨不已,因为它给我带来了很多头疼。我希望有人帮我写 CSS。我会毫不犹豫地大量使用预先设计好的组件库。

现在我喜欢 CSS,我痴迷于微小的 CSS 细节,比如主题化滚动条。我在外面看到一个设计良好的 UI,第一件事就是打开开发工具,试图弄清楚它是如何制作的。

那么是什么改变了呢?还有 CSS 现在变得越来越好?甚至是最微小的事情,比如 flexbox gap,都让 CSS 更具人性化和易用。

不过,大多数新的 CSS 功能并没有提高其可维护性,至少直到最近…… 2021 年,我们得到了迫切需要的:where伪类,它让我们控制选择器的特异性。在 2022 年,我们得到了层叠层,它让我们完全绕过选择器的特异性(甚至是第三方样式!)直接控制大量样式的顺序。这是迄今为止我最喜欢的 CSS 最新功能之一。

作用域

CSS 的可维护性是一个二维问题。层叠解决了其垂直方面的问题(“顺序”),但我们仍然需要一些东西来管理其水平方面(“范围”)。就像我们不希望我们的全局重置样式覆盖我们的组件样式一样,同样地,我们也不希望一个组件的样式干扰其他组件。如果我们做得对,我们也会准确地知道谁在使用什么,以及在哪里使用,这意味着我们可以自信地删除死代码(在 CSS 中历史上曾经是难以做到的)。

有一个正在开发的 @scope 规范,将全面帮助解决这个问题。

作用域是开发人员试图解决的问题中的一个。其中一个较简单的作用域形式是通过使用命名约定,比如 BEM。更正式地说,它是通过工具解决的。CSS 模块(不是原生的)可能是最流行和最强大的作用域解决方案之一 —— 它已经被无数工具实现,并在许多其他工具中产生了类似的变体。它以前效果不错,现在仍然效果不错,尽管浏览器和开发人员生态系统在这么多年的创新中发生了很多变化。

共存

只为作用域提供解决方案通常已经足够了,但我们可以做得更好。我们经常想要更好。我们希望我们的样式与它们所作用的标记尽可能接近。这样可以更容易地保持它们同步,一起修改/删除它们等(这也是实用主义优先方法的整个承诺,对吗?)

这就是 BEMCSS 模块等开始感觉有点不人性化的地方。样式和标记位于不同的位置 —— 我们能做到的最接近的就是在同一个文件夹中的 CSS 文件。我们仍然需要手动进行关联。没有智能感知或“转到定义”在两者之间来回移动。东西很容易不同步。而且团队规模越大,人为错误的机会就越大。

诚然,Web 组件(特别是shadow DOM)在某种程度上解决了这个问题。但是,在我看来,shadow DOM 在样式封装方面做得有些过了头。我不想放弃整个层叠,我只是希望某些(但不是所有)部分与特定元素紧密联系。

这就是 CSS-in-JS 出现的原因。像 styled-components emotion* 这样的库带来了明显的开发体验(DX)好处。它们在 CSS 的全部功能基础上提供了作用域和共存。但请记住:那时 CSS 缺少一些关键功能,并且开发人员真的很喜欢编写 JavaScript。那么为什么不将JavaScript用作预处理器、后处理器、运行时处理器、主题、断点、”变体”、”样式对象”、”多态”、”关键 CSS“、”扩展样式”,甚至全局样式的一切处理器呢。
CSS-in-JS 的想法早于CSS模块。但与今天的styled-components和 emotion 相比,它是一种非常不同的CSS-in-JS形式。

性能

在运行时使用 JavaScript 计算所有样式会降低性能(可能显而易见)。即使使用 SSR,页面也需要时间才能变得可交互。我个人见过一些使用运行时CSS-in-JS的网站加载需要几秒钟的页面。

几年过去了,JavaScript 社区开始认识到了这个问题。一篇博客文章:“为什么我们放弃CSS-in-JS。在那篇文章中,深入探讨了性能问题,用实际的测量数据和内部视角说明了问题。

React 核心团队现在也建议使用静态解决方案而不是运行时解决方案:
“然而,您可以构建一个 CSS-in-JS 库,将静态规则提取到外部文件中使用编译器。这就是我们在 Facebook 使用的方式。”

编译所有的东西!

如果我们想要 CSS-in-JS DX 优势和静态 CSS 文件的UX优势,那么我们可能需要在编译/构建时进行操作。到目前为止,我们所见到的CSS-in-JS的所有缺点基本上都可以追溯到在运行时使用JavaScript进行操作。

回到当前的问题:记住,我们想要作用域、共存和性能。如果目标仍然专注于这三点,那么这就成为一个更狭窄、更简单的问题要解决。

由于作用域发生在类级别(或更广泛地说,选择器级别),我们将其用作 API 中的单一点。这有一个额外的好处,就是它是框架无关的。而且这就是我们真正需要的。

DX 的角度来看,API 也非常直观,特别是对于那些来自其他 CSS-in-JS 库的人。我们还可以在这里使用真正的CSS语法,因此编写的代码对所有人来说都是瞬间可识别的。没有学习曲线,样式可以在不同的项目和代码片中复制粘贴。

所以现在我们只需要用散列类名替换整个 CSS 字符串,并用它来填充一个 CSS 文件。听起来很简单。说实话?确实如此!构建原始的概念验证只需大约 50 行代码。

我想再次强调,这全部都是在构建时完成的,这使我们能够对提取的 CSS 进行各种花招,而不会影响运行时性能。这就是我们能够免费获得&嵌套的原因(使用 PostCSS)。我们可以利用这个想法再进一步,支持 Sass

所以我们基本上是原封不动地将本应该在 .css .scss文件中的内容放到了我们的 .jsx/.tsx 文件中,用有所有JavaScript变量的好处的作用域类名替换了它们 —— 可以导入、可tree-shakeable、可“转到定义”等等。就像内联(S)CSS 模块一样!

思考

具有作用域、共存和性能的样式完全应该是一个早已解决的问题。肯定已经有人尝试过:Linaria 是一个静态 CSS-in-JS 库,专门设计用于解决这个问题;但一直在尝试让它与层叠层或者 Vite SSR(比如 Astro)一起工作时遇到了问题。

大多数使用非 JSX 语言进行模板化的框架 —— 如 SvelteVueAstroAngularWebC —— 都有一种用于作用域 CSS 的机制。没有理由 React,以及JSX的扩展,不能像其他人一样支持作用域。