天天看点

聊聊2023年最火的前端编译时 CSS-in-JS 库

作者:高级前端进阶

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

聊聊2023年最火的前端编译时 CSS-in-JS 库

高级前端进阶

最近,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 ? 》
  • 《 聊聊2023年最火的前端编译时 CSS-in-JS 库 》
  • 《 2023年CSS-in-JS 和 CSS Modules 谁才是最终赢家? 》

我希望通过系列文章的方式带着大家深入的了解 CSS-in-JS,包括它的优势、缺点、编译时运行时的不同等等,最终让大家对写下的每一行代码都持有足够的信心。而今天的 CSS-in-JS 相关主题是“聊聊2023年最火的前端编译时 CSS-in-JS 库”,即 vanilla-extract。

话不多说,直接开始进入正题!

1.什么是 vanilla-extract

vanilla-extract 是 TypeScript 的零运行时样式表,是典型的编译时 CSS-in-JS方案。其使用局部作用域的类名和 CSS 变量在 TypeScript(或 JavaScript)中编写样式,然后在构建时生成静态 CSS 文件。

聊聊2023年最火的前端编译时 CSS-in-JS 库

在 2022 年,vanilla-extract 在所有的 CSS-in-JS 库中热度排名第 10,而 2021 年同期热度排名第 12。vanilla-extract 是 TypeScript 中的 CSS 模块,同时具有作用域的 CSS 变量和堆栈。vanilla-extract 还具有以下典型特点:

  • 在构建时生成所有样式,与 Sass、Less 等 CSS 预处理类似
  • ✨ 对标准 CSS 的最小抽象
  • 适用于任何前端框架,同时也不依赖于框架
  • 局部作用域的类名,就像 CSS Module 一样
  • 局部作用域的 CSS 变量、@keyframes 和 @font-face 规则
  • 支持同步主题的高级主题系统,同时没有全局变量
  • 用于生成基于变量的计算表达式的实用程序
  • 通过 CSSType 的类型安全样式
  • ‍♂️ 用于开发和测试的可选运行时版本
  • 用于动态运行时主题的可选 API

与 Compiled、Linaria、astroturf、style9 等 CSS-in-JS 编译时方案相比,从过去一年 NPM 的周平均下载量来看 vanilla-extract 也是处于迅猛增长期,增长速度排名第一、远远高于其他编译时方案(排名第二的是 Linaria,后续会单独出文介绍它)。

聊聊2023年最火的前端编译时 CSS-in-JS 库

目前 vanilla-extract 在 Github 上有超过 8.0k 的 star、18.7k 的项目使用量,NPM 周平均下载量 533k,是一个妥妥的前端明星项目。

2.使用 vanilla-extract

2.1 打包器集成

Vanilla-extract 要求开发者设置一个打包器并将其配置为用于处理 CSS,从而允许样式与代码中的任何其他依赖项一样处理,如仅导入和打包所需的内容。Vanilla-extract 支持的打包器包括(关于每一个打包器的详细介绍可以在我的主页阅读):

  • vite
  • esbuild
  • webpack
  • next
  • parcel
  • rollup
  • gatsby 等等

集成相应的构建工具后,可以通过将 .css.ts 后缀文件添加到项目中来创建样式表。以下面的代码为例:

// app.css.ts
import { style } from '@vanilla-extract/css';
export const container = style({
  padding: 10,
});           

构建时生成的 CSS 样式内容如下:

.app_container__sznanj0 {
  padding: 10px;
}           

打包过程发生了两件事情:首先创建了一个本地作用域的 className 类,然后导出生成的类名。但是要将样式应用于元素,需要从样式表中导入它。 通过导入样式,接收到生成的作用域 className 名称,开发者就可以将其应用于元素的 class 属性。

import { container } from './styles.css.ts';
document.write(`
  <section class="${container}">
    ...
  </section>
`);           

在导入的同时,CSS 也由选定的打包器集成处理。

2.2 CSS 变量

在常规 CSS 中,变量(或 CSS 自定义属性)可以与规则中的其他属性一起设置。 在 Vanilla Extract 中,CSS 变量必须嵌套在 vars 键内,从而为其他 CSS 属性提供更准确的静态类型。

以下面的 styles.css.ts 文件内容为例:

import { style } from '@vanilla-extract/css';
const myStyle = style({
  vars: {
    '--my-global-variable': 'purple',
  },
});           

构建时生成的 CSS 样式内容如下:

.styles_myStyle__1hiof570 {
  --my-global-variable: purple;
}           

vars 键还接受通过 createVar API 创建的作用域内的 CSS 变量。

import { createVar, style } from '@vanilla-extract/css';
export const accentVar = createVar();
// 创建一个单一作用域的 CSS 变量引用
export const blue = style({
  vars: {
    [accentVar]: 'blue',
  },
});
export const pink = style({
  vars: {
    [accentVar]: 'pink',
  },
});           

构建时生成的 CSS 样式内容如下:

// 两个属性的l3kgsb0后缀一致
.accent_blue__l3kgsb1 {
  --accentVar__l3kgsb0: blue;
}
.accent_pink__l3kgsb2 {
  --accentVar__l3kgsb0: pink;
}           

2.3 CSS 主题

主题通常被认为是全局的、应用范围广泛的概念。 虽然 vanilla-extract 主题对此非常有用,但它们也可以用于更集中、更低级别的用例。 例如,一个组件以不同的配色方案渲染。

vanilla-extract 中的主题实际上只是在 createVar 提供的作用域 CSS 变量创建之上的一组工具。为了理解它是如何工作的,看下面的例子:

// theme.css.ts
import { createTheme } from '@vanilla-extract/css';
export const [themeClass, vars] = createTheme({
  color: {
    brand: 'blue',
  },
  font: {
    body: 'arial',
  },
});           

构建时生成的 CSS 样式内容如下:

// theme.css
.theme_themeClass__z05zdf0 {
  --color-brand__z05zdf1: blue;
  --font-body__z05zdf2: arial;
}           

这个示例使用了主题实现调用 createTheme, 基于此,vanilla-extract 将做两件事:

  • 类名:提供的主题变量的容器类,如:theme_themeClass__z05zdf0
  • 主题契约:CSS 变量的类型化数据结构,与提供的主题实现的对象相匹配,如:--color-brand__z05zdf1、--font-body__z05zdf2 等等

处理完这个文件后,生成的编译 JS 将如下所示:

// 编译后JS示例
import './theme.css';
export const vars = {
  color: {
    brand: 'var(--color-brand__l520oi1)',
  },
  font: {
    body: 'var(--font-body__l520oi2)',
  },
};

export const themeClass = 'theme_themeClass__l520oi0';           

要创建此主题的替代版本,可以再次调用 createTheme。但这次传递现有的主题契约(即 vars)以及新值。

//theme.css.ts
export const [themeClass, vars] = createTheme({
  color: {
    brand: 'blue',
  },
  font: {
    body: 'arial',
  },
});
export const otherThemeClass = createTheme(vars, {
  color: {
    brand: 'red',
  },
  font: {
    body: 'helvetica',
  },
});           

构建时生成的 CSS 样式内容如下:

.theme_themeClass__z05zdf0 {
  --color-brand__z05zdf1: blue;
  --font-body__z05zdf2: arial;
}
.theme_otherThemeClass__z05zdf3 {
  --color-brand__z05zdf1: red;
  --font-body__z05zdf2: helvetica;
}           

通过传入一个现有的主题契约,而不是创建新的 CSS 变量,现有的变量被重用,但被分配给一个新的 CSS 类中的新值。

最重要的是,vanilla-extract 知道现有主题合约的类型,并要求开发者完全正确地实现它。处理更新后的文件后,生成的编译 JS 将如下所示:

// Example result of the compiled JS
import './theme.css';
export const vars = {
  color: {
    brand: 'var(--color-brand__l520oi1)',
  },
  font: {
    body: 'var(--font-body__l520oi2)',
  },
};
export const themeClass = 'theme_themeClass__l520oi0';
export const otherThemeClass = 'theme_otherThemeClass__l520oi3';           

可以看出,这里唯一添加的是对新主题类名称的引用。

2.4 样式组合

样式组合是 vanilla-extract 的一项特殊功能,可以轻松地最大限度地重复使用样式。它允许开发者传递一组类名和样式,但继续将它们视为单个类名。比如下面的例子:

// style.css.ts
import { style } from '@vanilla-extract/css';
const base = style({ padding: 12 });
const primary = style([base, { background: 'blue' }]);
const text = style({
  selectors: {
    [`${primary} &`]: {
      color: 'white',
    },
  },
});           

构建时生成的 CSS 样式内容如下:

// css
.styles_base__1hiof570 {
  padding: 12px;
}
.styles_primary__1hiof571 {
  background: blue;
}
.styles_primary__1hiof571 .styles_text__1hiof572 {
  color: white;
}           

2.5 测试集成

执行 .css.ts 文件时,vanilla-extract 将按预期返回类名标识符,如果在类似浏览器的环境(如 jsdom、pupeteer、playwright 等无头浏览器环境)中运行,则会将真实样式注入到 document 中。 但是,要使原始提取样式在测试环境中工作,需要对代码应用转换。

目前,Jest 和 Vitest 已经有官方集成。比如 jest.config.js 的配置内容:

{
  "transform": {
    "\\.css\\.ts#34;: "@vanilla-extract/jest-transform"
  }
}           

vitest.config.ts 的配置如下所示:

import { defineConfig } from 'vitest/config';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';

export default defineConfig({
  plugins: [vanillaExtractPlugin()],
});           

除了以上重点介绍的的诸多特性外,Vanilla Extract 还支持:媒体查询(@media)、供应商前缀(-webkit-tap-highlight-color)、简单/复杂选择器(&:hover:not(:active))、伪选择器(:hover、first-of-type、::before 等)、容器查询(@container)、后备样式(Fallback Styles)等前端常用的样式集成方式。这里不再深入展开,可以通过文末的资料自行查看用法。

3.本文总结

Vanilla Extract 是一个 CSS-in-JS 框架,可让开发者使用 TypeScript 创建类样式。 Vanilla Extract 结合了 Tailwind 之类的实用程序类方法和 TypeScript 的类型安全性,允许开发者创建自定义但一致的样式。 Vanilla Extract 生成的样式在本地作用域内,并在构建时编译为单个样式表。

本文向您介绍了在 React 应用程序中如何使用 Vanilla Extract ,但它是一个与框架无关的库。 在任何可以包含类名的地方,它都应该可以正常工作。

因为篇幅有限,文章并没有过多展开,如果有兴趣,文末的参考资料提供了优秀文档以供学习。最后,欢迎大家点赞、评论、转发、收藏!

参考资料

https://stackdiary.com/css-in-js-libraries/

https://www.thisdot.co/blog/introduction-to-vanilla-extract-for-css

https://vanilla-extract.style/documentation/test-environments/

https://github.com/naistran/css-in-js-loader

https://github.com/stitchesjs/stitches

https://npmtrends.com/@compiled/css-vs-@linaria/core-vs-@vanilla-extract/css-vs-astroturf

https://davidcai.github.io/dcai-blog/posts/css-in-js-and-testability/

https://blog.mayank.co/is-css-in-js-actually-bad