2023年:CSS-in-JS与CSS Modules,谁将胜出?

发表时间: 2023-09-05 06:20

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

高级前端进阶

最近,Emotion 排名第二的维护者 Sam 所在公司弃用了 CSS-in-JS 方案,引起了不小的讨论。这也是我第一次开始重点关注 CSS-in-JS,我甚至在头条开了一个合集重点讨论 CSS-in-JS 的方案,下面是已经发表的关于 CSS-in-JS 的文章:

  • 《 2023 年的尽头是编译时 CSS-in-JS 方案么?》
  • 《 CSS vs. CSS-in-JS:2023 年你应该如何选择?》
  • 《 2023 年最受欢迎的 10 大 CSS-in-JS 库!》
  • 《 我们为何选择弃用 css-in-js ? 》

我希望通过系列文章的方式带着大家深入的了解 CSS-in-JS,包括它的优势、缺点、编译时运行时的不同等等,最终让大家对写下的每一行代码都持有足够的信心。话不多说,直接开始进入正题!

前言

在现代 React 应用程序开发中,有许多组织应用程序样式的方法, 比如: CSS-in-JS ,CSS Modules 等等。 本篇文章将尝试回答 CSS-in-JS 和 CSS Modules 哪个更好?

图片来自作者 Sergey:https://dev.to/

很久以前,当网页主要功能是存储文本文档并且不包括用户交互时,会引入属性来设置内容的样式。 随着时间的推移,Web 变得越来越流行,站点变得越来越大,并且有必要重用样式, 此时 CSS ,即级联样式表应运而生。

随着时间的流逝,Web 变得越来越复杂,开发者也面临着 CSS 带来的诸多问题。比如团队合作面临的:样式重用、样式覆盖、样式一致性等诸多问题。此时,一系列的样式命名约定横空出世,例如: Yandex 的 BEM 或 Atomic CSS,通过这些方式在一定程度上解决了样式的可预测性、同时防止命名重复。

但是,约定是脆弱的,没有强制性而且需要过度分散开发者的注意力,最重要的是很可能因为人为因素被破坏。不过幸运的是,后面出现了一系列的 CSS 样式组织技术,比如:CSS Modules(基于文件名、路径、样式名称的散列来转换样式)、CSS-in-JS(JS 运行时将样式添加到 head) 等等。从而从根本上解决了编写 CSS 的原子性、可重用性和副作用。

1.CSS Modules

1.1 什么是 CSS Modules

CSS Modules 是一个 CSS 文件,默认情况下,所有类名和动画名称都在本地范围内(scoped locally)有效。除非使用:gloabl 语法显示声明为全局样式,比如下面的例子:

// 本地有效:local(.title) {  color: red;}//全局有效//凡是类:global声明的class,都不会被编译成哈希字符串。:global(.title) {  color: green;}

CSS Modules 允许开发者在 CSS 文件中编写样式,但程序将它们作为 JavaScript 对象使用以进行额外的处理,同时保证安全性。 CSS Modules 目前非常流行,因为它们自动使类和动画名称唯一,因此开发者无需担心选择器名称冲突。

图片来自作者Gabriel Delight:https://blog.openreplay.com/

从等式上看,CSS Modules = JS 对象中的 CSS。CSS Modules 与 CSS 代码结构大致相同,主要区别是调用方法的差异。以静态站点生成器 SSG 的 Gatsby 为例,后缀为.module.css 的文件将自动当做 CSS Module 处理,这与大多数框架的处理机制非常类似。假如有下面名称为 container.module.css 的文件:

// src/components/container.module.css.container {  margin: 3rem auto;  max-width: 600px;}

下面是对 container.module.css 文件的引用:

// src/components/container.jsimport React from 'react';import * as containerStyles from './container.module.css';export default function Container({ children }) {  return <section className={containerStyles.container}>{children}</section>;}

在上面的 Container 组件示例中,导入了一个 CSS 模块并将其声明为名为 containerStyles 的 JavaScript 对象。 然后在带有 containerStyles.container 的 JSX className 属性中引用来自该对象的 CSS 类,它将自动替换为动态 CSS 类名称,比如:
container-module--container--3MbgH,最终渲染到 HTML 中。

1.2 CSS Modules 的优缺点

CSS Module 提供的最明显的好处是消除了对 CSS-in-JS 的依赖,以解决作用域(scoping)和特异性问题。以下总结了几点 CSS Modules 的独特优势:

  • 作用域和特异性:CSS Module 成功解决了传统、旧式 CSS 的作用域问题,由于规则写在 CSS Modules 文件中,因此很少有特异性问题。
  • 有组织的代码:保留单独的 CSS 文件促进了更好的样式代码组织,便于团队统一,特别有利于大型项目支撑。
  • 缓存:CSS Modules 最终构建生成的压缩 CSS 文件可以由浏览器缓存,以改善页面加载时间。
  • CSS 预处理:支持 PostCSS、SASS、Less 等 CSS 预处理器。
  • 零学习曲线:开发者可以使用 CSS Modules 而无需学习任何新内容。本质上,CSS Modules 不是将 CSS 改造成编程语言,只是加入了局部作用域和模块依赖而已。
  • 框架支持:开发者无需添加额外的包来使用 CSS Modules, 所有主要框架和库都提供内置支持。例如:React、Vue、Angular、svelte 、Qwik 、Nextjs 等等

虽然 CSS Modules 提供了以上声明的诸多好处,但它也并不是一个完美的解决方案,可以总结为以下几个点:

  • 非标准 :global 属性:在全局范围内定位选择器时,必须使用 :global 规则,这并非 CSS 规范的一部分,只是被 JavaScript 用来标记全局样式。
  • 没有动态样式:使用 CSS Modules,所有声明都进入单独的 CSS 文件。 因此不支持像 CSS-in-JS 的动态样式,因为不能在 CSS 文件中实现任何 JavaScript 相关的能力。
  • 外部 CSS 文件:不能在组件中忽略 CSS 文件和 CSS 模块的使用。 使用 CSS 模块唯一可行的方法是维护和导入外部 CSS 文件。
  • TypeScript 限制:要将 CSS Modules 与 TypeScript 一起使用,开发者必须在 index.d.ts 文件中添加模块定义或使用 webpack 加载器。

比如下面的例子:

/** index.d.ts **/declare module "*.module.css";// CSS 模块文件的 TS 模块declare module "*.module.scss";// SCSS 中 CSS 模块文件的 TS 模块

当然也可以充分利用
typescript-plugin-css-modules 插件,它是向 TypeScript 语言服务提供有关包含导入的 CSS Modules 文件的类选择器的信息的插件。 对于 VSCode,需要设置 TypeScript LS 来使用工作区版本。

// 插件添加到tsconfig.json文件{  "compilerOptions": {    "plugins": [{ "name": "typescript-plugin-css-modules" }]  }}

2.CSS-in-JS

2.1 什么是 CSS-in-JS

CSS-in-JS 允许开发者通过 JavaScript 为组件编写 CSS 属性。CSS-in-JS 始于 2015 年一个名为 JSS 的 JavaScript 库,该库仍在积极维护中。 开发者必须使用 JavaScript 语法向选择器提供 CSS 属性,然后在页面加载后自动将这些属性应用于它们各自的选择器。

图片来自Diogo Rodrigues《CSS-in-JS and the death of traditional CSS》

当 JavaScript 接管渲染和管理类 React 的前端库时,一个名为 styled-components 的 CSS-in-JS 解决方案应运而生。 另一种越来越流行的 Emotion 库也逐渐赢得开发者的好评。比如下面是使用 styled-components 的例子:

import styled from 'styled-components';const StyledButton = styled.a`  padding: 0.75em 1em;  background-color: ${({ primary }) => (primary ? '#07c' : '#333')};  color: white;  &:hover {    background-color: #111;  }`;export default StyledButton;

当然在 CSS-in-JS 的细分场景又分为编译时和运行时方案,两者在输出样式的时机存在差异。

  • 编译时 CSS-in-JS:编译时 CSS-in-JS 的工作原理是在构建时静态分析代码,将其转换为编译组件,然后在运行时将样式代码移动到文档的头部。典型的代表如:Compiled、Vanilla Extract、Linaria、astroturf、style9 等等。
  • 运行时 CSS-in-JS :当组件渲染时,CSS-in-JS 库必须将样式“序列化”为可以插入到文档中的纯 CSS,从而占用额外的 CPU 周期,最终对应用程序的运行性能产生影响。典型的代表如:styled-components、JSS、Emotion、styled-system、Twin、Fela 等等

不论是编译时,还是运行时 CSS-in-JS 方案,两者都有一些各自的优缺点。

2.2 CSS-in-JS 优缺点

JavaScript 开发人员可能更喜欢使用 CSS-in-JS 来设计样式,而不是通过 CSS 类。 CSS-in-JS 方法解决的最大问题是全局范围。 如果您是 JavaScript 开发人员,它还有一些其他优势,这些优势非常有意义。

  • 没有作用域和特异性问题:由于样式在本地范围内可用,因此它们不易与其他组件的样式发生冲突。 开发者不必花费多余的精力来严格命名样式以避免可能的冲突。同时,由于样式是专门为特定组件编写的,没有预先设置子选择器,因此特异性问题很少见。
  • 动态样式:条件 CSS 是 CSS-in-JS 的另一个亮点,也是灵活性的突出表现,在 React、Vue 等前端项目中也是无处不在,比如:切换按钮样式等等。
  • 较少的 CSS 特异性:CSS-in-JS 可以帮助开发者将 CSS 声明的特殊性保持在最低水位,因为开发者唯一使用它设置样式的是元素本身。
  • 简单的主题:CSS-in-JS 允许开发者用 JavaScript 编写主题。 比如:使用 styled-components ThemeProvider 包装器可以快速对组件的主题进行颜色编码。
  • 无痛维护:考虑到 CSS-in-JS 提供的特性和优势,JavaScript 开发人员可能会发现 CSS-in-JS 比管理数百个 CSS 文件更方便。然而,事实仍然是开发者必须对 JavaScript 和 CSS 都有很好的理解才能有效地管理和维护由 CSS-in-JS 支持的大型项目。

CSS-in-JS 确实很好地解决了作用域等诸多问题, 但开发者仍然面临着更大的挑战。比如:渲染阻塞直接影响用户体验。 除此之外,CSS-in-JS 还需要解决一些其他问题。

  • 延迟渲染:CSS-in-JS 会执行 JavaScript 来解析 JavaScript 组件中的 CSS,然后将这些解析后的样式注入到 DOM 中。 组件越多,浏览器首次绘制页面所花费的时间就越多。
  • 缓存问题:CSS 缓存通常用于改进页面加载时间。 由于使用 CSS-in-JS 时不涉及 CSS 文件,因此缓存是一个大问题。 此外,动态生成的 CSS 类名使缓存问题更加复杂。
  • 不支持 CSS 预处理器:使用常规的组件化 CSS 方法,可以轻松添加对 SASS、Less、PostCSS 等 CSS 预处理器的支持, 而 CSS-in-JS 目前做不到。
  • 凌乱的 DOM:CSS-in-JS 将所有样式定义从 JavaScript 解析为普通 CSS,然后使用样式块将样式注入 DOM 。比如大型项目,可能有超过 100+ 样式块需要首先解析,然后注入。同时,以 Emotion 为例, 对于每个使用 css prop 的元素都将渲染 和 组件。 如果在许多元素上使用 css prop,Emotion 的内部组件会使 React DevTools 变得异常混乱,非常难调试。
  • 三方库体积依赖:开发者虽然可以使用外部库添加 CSS-in-JS 功能,但是在实际的 CSS 解析之前,将包含并运行大量 JavaScript,因为从 JavaScript 到 CSS 样式的解析依赖于三方库,从而带来额外的体积包大小。比如:Emotion 为 7.9 kB,styled-components 为 12.7 kB,而 react + react-dom 的体积是 44.5 kB 。
  • 学习曲线&框架支持:CSS-in-JS 缺少许多原生 CSS 和 SCSS 功能,这对于习惯了 CSS 和 SCSS 的开发者来说,要适配 CSS-in-JS 可能面临一些学习成本,甚至挑战。同时,相比于 CSS Modules 方案,框架的支持并不完善。

以上 CSS-in-JS 面临的诸多问题可能会共同导致产品性能低下、难以维护,其中就包含 UI 和 UX 的不一致。

3.CSS-in-JS 和 CSS Modules 如何选择

3.1 什么时候用 CSS Modules

如果有一个具有大型 UI 、性能关键的应用程序,那么 CSS Modules 是一个不错的选择。 由于 CSS Module 提供的所有内容最终都是基于传统的、非实验性的基础能力,因此这种方法可以更轻松地监控和修复性能。

同时,CSS Modules 文件很容易从选择的任何 CSS 框架中改进代码,因为所处理的只是 CSS。 如前所述,CSS 的一些基本知识足以让开发者快速完成任务。

目前 CSS 的诸多最新特性也值得开发者持续关注,例如:作用域指令和@scope 伪元素,其旨在解决传统 CSS 的老问题。

<div class="card">  <img src="..." />  <div class="content">    <p>...</p>  </div>  <!-- 内容 --></div><!-- 卡片 --><style>  /* :scope CSS 伪类表示作为选择器匹配参考点的元素。 */  @scope (.card) {    :scope {      display: grid;    }    img {      object-fit: cover;    }    .content {    }  }</style>

随着这个新功能的引入,以后开发者可能不再需要 CSS Module 或 CSS-in-JS 来解决作用域问题,大家拭目以待。

注意:当在样式表中使用时,:scope 与 :root 作用相同,因为此时没有明确建立作用域元素的方法。 当从 DOM API(例如: querySelector()、querySelectorAll()、matches() 或 Element.closest() 中使用时,:scope 匹配调用该方法的元素。

3.2 什么时候使用 CSS-in-JS

当在处理性能优先级较低、体积较小的应用程序时,CSS-in-JS 解决方案是理想的选择。 而在处理具有庞大设计系统的、性能关键型的应用程序时可以尽量避免使用 CSS-in-JS。

因为,随着应用程序变得越来越大,使用 CSS-in-JS 很容易将问题变得复杂。 将设计系统转换为 CSS-in-JS 需要做大量工作,在我看来,没有 JavaScript 开发人员愿意处理这个问题。

4.本文总结

本文主要和大家探讨 “2023年CSS-in-JS 和 CSS Modules 谁才是最终赢家?”这个问题。因为篇幅有限,文章并没有过多展开,如果有兴趣,文末的参考资料提供了优秀文档以供学习。最后,欢迎大家点赞、评论、转发、收藏!

参考资料

https://marketplace.visualstudio.com/items?itemName=mrmlnc.vscode-scss

https://dev.to/fyapy/sass-vs-css-modules-vs-css-in-js-vs-compile-time-css-in-js-who-wins-4cl

https://dev.to/alexsergey/css-modules-vs-css-in-js-who-wins-3n25

https://www.gatsbyjs.com/docs/how-to/styling/css-modules/

https://blog.logrocket.com/css-vs-css-in-js/

https://blog.logrocket.com/css-vs-css-in-js/#recommendations-where-use-css-in-js

https://github.com/mrmckeb/typescript-plugin-css-modules#visual-studio-code

https://developer.mozilla.org/en-US/docs/Web/CSS/:scope

https://www.ruanyifeng.com/blog/2016/06/css_modules.html

https://www.diogorodrigues.dev/blog/css-in-js-and-the-death-of-traditional-css