大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
高级前端进阶
最近,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 文件。
在 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,后续会单独出文介绍它)。
目前 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