在本教程中,我们将深入了解vanilla-extract的主要功能,并通过使用React和webpack构建一个示例应用来演示如何创建零运行时间的样式表。
什么是vanilla-extract?
vanilla-extract是关于TypeScript(或JavaScript)的零运行时间样式表。如果你过去使用过Sass或Less,你已经知道它们在主题化、组织和预处理你的应用程序的样式等方面是多么强大。
就像这些CSS预处理器一样,vanilla-extract会在你的构建过程中生成所有样式。在它的好处中,你会发现本地范围内的类名、CSS变量、CSS规则、对同步主题的支持(没有globals)、计算工具表达式等等。
设置vanilla-extract
为了演示如何使用vanilla-extract,我创建了一个已经添加了React和webpack的支架式简单项目。你可以在本地克隆/分叉它,并把它作为我们接下来要在vanilla-extract上尝试的东西的起点。
一旦你完成了,请确保运行npm install
命令来下载所有需要的Node包。
然后,运行下一个命令来添加vanilla-extract的主要依赖项(webpack集成)。
npm install @vanilla-extract/css @vanilla-extract/babel-plugin @vanilla-extract/webpack-plugin
当命令完成后,你会在你的package.json
文件中看到新添加的依赖项。
"dependencies": {
"@vanilla-extract/babel-plugin": "^1.0.1",
"@vanilla-extract/css": "^1.2.1",
"@vanilla-extract/webpack-plugin": "^1.1.0",
...
}
至于vanilla-extract的webpack/Babel配置,你还需要在webpack.config.js
文件中升级以下三个改动。
const { VanillaExtractPlugin } = require("@vanilla-extract/webpack-plugin");
上面的第一个改动必须添加到文件的开头,在导入部分。这一行代表webpack插件来处理vanilla-extract相关的操作。
它将和文件中的plugins
部分一起使用,其简单的实例化如下。
plugins: [
new HtmlWebpackPlugin(),
new MiniCssExtractPlugin(),
new VanillaExtractPlugin(),
],
最后,你需要将vanilla-extract Babel插件添加到babel-loader
loader的"js|ts|tsx
" 测试规则中,如下所示。
{
test: /\.(js|ts|tsx)$/,
...
use: [
{
loader: "babel-loader",
options: {
...
plugins: ["@vanilla-extract/babel-plugin"],
},
},
],
},
vanilla-extract官方文档中的一个警告提到,你的webpack配置需要对CSS进行处理。不用担心这个;我们已经在支架式的默认项目设置中处理了CSS文件。
如果你想验证一切设置是否正确,只需运行npm run start
命令并在浏览器中检查结果。
创建一个风格化的主题
vanilla-extract作为一个预处理器工作,但是,你得到的不是通常的Less或Sass,而是TypeScript。
让我们通过创建一个非常简单的风格化主题来看看vanilla-extract的操作,以帮助建立以下页面。
让我们从样式开始。要编写它们,首先在src
文件夹下创建一个新文件,名为styles.css.ts
。这种类型的文件在构建时被评估,当你向它添加以下代码时,你会看到它是TypeScript。
import { createTheme, style } from "@vanilla-extract/css";
export const [themeClass, vars] = createTheme({
color: {
primary: "#764abc",
secondary: "white",
},
font: {
menu: "1.5em",
},
});
export const menuStyle = style({
backgroundColor: vars.color.primary,
fontSize: vars.font.menu,
display: "block",
width: "100%",
color: "white",
padding: 20,
});
export const menuItemsStyle = style({
float: "right",
});
export const menuItemStyle = style({
backgroundColor: vars.color.primary,
color: vars.color.secondary,
margin: 10,
":hover": {
cursor: "pointer",
color: "orange",
},
});
export const sectionStyle = style({
display: "inline-block",
width: "100%",
textAlign: "center",
marginTop: "20%",
});
样式总是被style
函数引用,它接收带有该特定样式对象所需的所有CSS属性的对象。它们必须总是被导出,因为它们在被注入到最终输出之前会被vanilla-extract预处理。
createTheme
函数是一个非常有用的资源,当你需要更通用的、集中的样式管理时,你可以从lib中使用。例如,你可能需要切换主题,所以这是一个完美的场景,说明主题可以提供帮助。
每个样式都在使用主题变量,但它们也可以从外部的TypeScript文件中访问,因为我们也在导出它们。
看看为一个样式添加一个hover
效果是多么容易。随意增加主题,如你所愿。
现在是时候利用我们的样式了。为此,在src
文件夹下创建另一个文件,命名为App.tsx
(主要的React应用组件),并向其中添加以下内容。
import {
themeClass,
menuStyle,
menuItemsStyle,
menuItemStyle,
sectionStyle,
} from "./styles.css";
export const App = () => (
<div className={themeClass}>
<header className={menuStyle}>
<h1 className={menuItemStyle}>Hello World!</h1>
<ul className={menuItemsStyle}>
<li>
<a className={menuItemStyle} href="#">
Hello
</a>
</li>
<li>
<a className={menuItemStyle} href="#">
World
</a>
</li>
<li>
<a className={menuItemStyle} href="#">
Vanilla
</a>
</li>
<li>
<a className={menuItemStyle} href="#">
Extract
</a>
</li>
</ul>
</header>
<section className={sectionStyle}>
<p>Body contents here!</p>
</section>
</div>
);
同样,如果你愿意,可以自由地把它分解成更多的组件。不过,为了简单起见,我们将保持它的集中性。
值得注意的是,被导入的样式是作为CSS类而不是内联样式添加的。这是因为整个过程是静态完成的,如果你用内联的方式,这就不可能了。
然而,vanilla-extract支持一个动态的API来实现动态的运行时主题化,如果你感到好奇的话。
你也可以使用css
包中的globalStyle
函数来确定你的应用程序是否必须遵循一般元素的全局风格,比如页面body
。要做到这一点,创建另一个名为global.css.ts
的文件,并将以下内容放入其中。
import { globalStyle } from "@vanilla-extract/css";
globalStyle("body, body *", {
all: "unset",
boxSizing: "border-box",
fontFamily: "Segoe UI",
color: "black",
padding: 0,
margin: 0,
});
在你把所有东西都集中到index.tsx
文件中之前,这个例子还不能被测试,如下所示。
import { render } from "react-dom";
import { App } from "./App";
import "./global.css";
const root = document.createElement("div");
document.body.appendChild(root);
render(<App />, root);
要测试它,只需重新执行npmrun start
命令并刷新你的浏览器。你应该看到如上图所示的相同页面。
但是,这种对类的样式映射最终是如何在浏览器中体现的呢?
如果你用浏览器检查页面上的HTML/CSS元素,你可能会看到,我们注入了themeClass
类的第一个div
,有以下属性。
它们的进一步用法可以在检查其余元素后发现,比如在其中一个menuItemStyle:
使用Sprinkles与vanilla-extract
到此为止,你应该明白vanilla-extract是怎么回事,以及如何使用它来创建你的静态样式和主题。
然而,这些样式还是有点混乱和重复的。让我们通过介绍Sprinkles,即vanilla-extract的零运行时间原子CSS框架来解决这个问题。
Sprinkles提供了一套实用的类,可以和原子一起被定制和组成可重用的样式。如果你以前使用过Styled System或Tailwind CSS,那么你会对其方法感到熟悉。
首先,将包的依赖关系添加到你的项目中。
npm install @vanilla-extract/sprinkles
接下来,我们将集中我们的应用程序到目前为止所使用的许多变量。这将有利于重复使用,特别是对于最通用的基础值。
在src
文件夹下创建另一个名为vars.css.ts
的文件,并将以下内容放入其中。
import { createGlobalTheme } from "@vanilla-extract/css";
export const vars = createGlobalTheme(":root", {
space: {
none: "0",
small: "4px",
medium: "8px",
large: "16px",
"1/2": "50%",
"1/5": "20%",
},
color: {
white: "#fff",
black: "#000",
orange: "#FFA500",
primary: "#764abc",
},
fontFamily: {
body: '-apple-system, "Segoe UI", Verdana, Arial',
},
fontSize: {
small: "1em",
medium: "1.4em",
large: "1.8em",
},
gridRepeat: {
"4x": "repeat(4, 1fr)",
},
});
createGlobalTheme
函数有助于建立CSS变量,省去了手动操作的麻烦(就像我们在前一版本的应用程序中做的那样)。
在这里,你可以添加任何你认为可以在整个应用程序样式中重复使用的变量,无论它们将被放置在你的CSS文件中还是直接放入React组件。
在这个例子中,我们为字体大小和家族、颜色和整体空间(用于填充、边距等)设置基本的全局值。
请记住,你为它们指定的名称由你决定;这里没有严格的语法系统,所以一定要给出有意义的名称,解释每个值的作用。
现在是创建自定义原子的时候了,将其放入一个名为sprinkles.css.ts
的新文件。粘贴以下内容。
import { createAtomicStyles, createAtomsFn } from "@vanilla-extract/sprinkles";
import { vars } from "./vars.css";
const responsiveStyles = createAtomicStyles({
conditions: {
mobile: {},
tablet: { "@media": "screen and (min-width: 768px)" },
desktop: { "@media": "screen and (min-width: 1024px)" },
},
defaultCondition: "mobile",
properties: {
display: ["none", "flex", "block", "inline", "grid"],
flexDirection: ["row", "column"],
gridTemplateColumns: vars.gridRepeat,
justifyContent: [
"stretch",
"flex-start",
"center",
"flex-end",
"space-around",
"space-between",
],
textAlign: ["center", "left", "right"],
alignItems: ["stretch", "flex-start", "center", "flex-end"],
paddingTop: vars.space,
paddingBottom: vars.space,
paddingLeft: vars.space,
paddingRight: vars.space,
marginTop: vars.space,
marginRight: vars.space,
marginLeft: vars.space,
marginBottom: vars.space,
fontFamily: vars.fontFamily,
fontSize: vars.fontSize,
// etc.
},
shorthands: {
padding: ["paddingTop", "paddingBottom", "paddingLeft", "paddingRight"],
paddingX: ["paddingLeft", "paddingRight"],
paddingY: ["paddingTop", "paddingBottom"],
placeItems: ["justifyContent", "alignItems"],
},
});
const colorStyles = createAtomicStyles({
conditions: {
lightMode: {},
darkMode: { "@media": "(prefers-color-scheme: dark)" },
},
defaultCondition: "lightMode",
properties: {
color: vars.color,
background: vars.color,
// etc.
},
});
export const atoms = createAtomsFn(responsiveStyles, colorStyles);
花点时间浏览一下这个文件的全部内容--我保证,这将是值得的。createAtomicStyles
函数有助于配置周围的预定义属性集。
conditions
- 样式是否必须应用于移动、桌面或仅有平板电脑的设备上properties
- 已知的React风格道具及其各自的值(或从先前创建的vars
文件中提取的图元)的列表shorthands
- 顾名思义,这将有助于为具有类似值的各种属性创建快捷方式,以便一次性应用。
请注意,最后一个函数,createAtomsFn
,接收两个原子样式,第二个与主题光/暗模式有关(现在很多应用程序都处理这个问题)。为了简单起见,让我们保持这部分的简短,只应用光照模式作为默认。
现在,style.css.ts
文件中已经没有主题了,我们需要更新它以处理新创建的洒落。下面是文件中要使用的新代码。
import { composeStyles, style } from "@vanilla-extract/css";
import { atoms } from "./sprinkles.css";
export const menu = composeStyles(
atoms({
display: "flex",
justifyContent: "space-between",
padding: "large",
// Conditional atoms:
flexDirection: {
mobile: "column",
desktop: "row",
},
fontSize: {
desktop: "large",
mobile: "medium"
},
background: {
lightMode: "primary",
},
})
);
export const menuItems = composeStyles(
atoms({
display: {
desktop: "flex",
mobile: "grid",
},
gridTemplateColumns: {
mobile: "4x",
},
paddingX: {
desktop: "small",
mobile: "none",
},
paddingY: {
mobile: "medium",
},
flexDirection: {
mobile: "column",
desktop: "row",
},
})
);
export const menuItem = composeStyles(
atoms({
display: "flex",
alignItems: "center",
paddingX: "medium",
color: "white",
flexDirection: {
mobile: "column",
desktop: "row",
},
}),
style({
":hover": {
cursor: "pointer",
color: "orange",
},
})
);
让我们来看看。只要你需要将多个样式连接成一个,就可以使用composeStyles
这个函数。
对于代码文件中创建的第一个样式,我们只有一个原子被添加。然而,当你向下浏览时,与项目菜单相关的最后一个样式也通过style
函数添加了一些额外的属性。
每当你需要添加不属于当前主题原子的自定义CSS属性时,这都很有用。如果你觉得这是一个非常特殊的情况,而且该属性不应该属于主题,那么使用style
函数是有帮助的。
看看条件原子,如flexDirection
,fontSize
,等等。由于我们在Sprinkles文件中配置了桌面与移动的条件,现在,每次你需要根据设备尺寸设置不同的样式时,你可以分别通过desktop
和mobile
属性来定义它。
例如,对于我们的menuItems
安排,元素的移动处置应该使用CSSgrid
显示,而不是flex
,这样我们就可以显示,如下图所示。
这同样适用于字体大小,在移动设备上会更小。
你可能想知道我们之前在style.css.ts
文件中的section
样式怎么了。好吧,我们把它移到了App.tsx
文件中,只是为了演示Sprinkles如何处理内联原子。
让我们看看现在的文件内容是怎样的。
import * as styles from "./styles.css";
import { atoms } from "./sprinkles.css";
export const App = () => (
<main>
<header className={styles.menu}>
<h1 className={styles.menuItem}>Hello World!</h1>
<ul className={styles.menuItems}>
<li>
<a className={styles.menuItem} href="#">
Hello
</a>
</li>
<li>
<a className={styles.menuItem} href="#">
World
</a>
</li>
<li>
<a className={styles.menuItem} href="#">
Vanilla
</a>
</li>
<li>
<a className={styles.menuItem} href="#">
Extract
</a>
</li>
</ul>
</header>
<section
className={atoms({
display: "block",
marginTop: {
desktop: "1/5",
mobile: "1/2",
},
textAlign: "center",
})}
>
<p>Body contents here!</p>
</section>
</main>
);
我们对这个文件做的第一个改动是导入。现在,我们不是逐一导入(这个列表会很快变大),而是将它们全部导入到styles
常量中。然后,你可以直接调用类名中的每一个。
请注意新的section
组件。是的,直接在组件中定制你的原子是完全可以的,Sprinkles会帮你处理这些问题。
请注意,我们正在重复桌面/移动的用法--这次是针对margin-top的值,因为从不同的设备尺寸查看时,它们会发生变化。
最后,由于为应用程序的变量建立了新的集中存储库,global.css.ts
文件有了些许变化。这是它的更新内容。
import { globalStyle } from "@vanilla-extract/css";
import { vars } from "./vars.css";
globalStyle("body, body *", {
all: "unset",
boxSizing: "border-box",
fontFamily: vars.fontFamily.body,
color: vars.color.black,
padding: 0,
margin: 0,
});
是时候再次测试一下了!你会看到,表现形式几乎没有变化,这就是目标。然而,我们设法在可重用性和组织方面大大改善了代码。
总结
关于vanilla-extract还有很多东西需要探索,然而,我相信我们建立了一个强大的知识基础来帮助你开始使用它。
你可以阅读更多关于vanilla-extract用于动态运行时主题化的动态API,以及它用于计算表达式等方面的优秀实用函数。
你可以在GitHub上找到我们示例项目的最终版本。
你已经尝试过vanilla-extract了吗?如果是的话,你目前的想法是什么?
帖子vanilla-extract教程。在TypeScript中创建零运行时间的样式表》首次出现在LogRocket博客上。