带有香草提取物的 TypeScript 中的 CSS

源节点: 1121024

香草精 是一个新的与框架无关的 CSS-in-TypeScript 库。这是一种轻量级、健壮且直观的编写样式的方式。 vanilla-extract 不是一个规定性的 CSS 框架,而是一个灵活的开发工具。 CSS 工具在过去几年中一直是一个相对稳定的领域 后CSS, 萨斯, CSS 模块样式组件 全部在 2017 年之前发布(有些早于那之前),而且它们 保持流行顺风 是过去几年中为 CSS 工具带来变革的少数工具之一。

香草精旨在再次改变现状。它于今年发布,其优点是能够利用一些最新趋势,包括:

  • JavaScript 开发人员转向 TypeScript
  • 浏览器对 CSS 自定义属性的支持
  • 实用至上的造型

香草精中有一大堆巧妙的创新,我认为这很重要。

零运行时间

CSS-in-JS 库通常在运行时将样式注入到文档中。这有好处,包括 关键CSS 提取和动态造型。

但作为一般的经验法则, 单独的 CSS 文件会提高性能。 这是因为 JavaScript 代码需要经过更昂贵的解析/编译,而单独的 CSS 文件可以被缓存,而 HTTP2 协议降低了额外请求的成本。还, 自定义属性 现在可以免费提供很多动态造型。

因此,vanilla-extract 不是在运行时注入样式,而是遵循 利纳里亚人工草皮。这些库允许您使用 JavaScript 函数创作样式,这些函数在构建时被删除并用于构造 CSS 文件。尽管您使用 TypeScript 编写 vanilla-extract,但它不会影响生产 JavaScript 包的整体大小。

打字稿

香草精的一个重要价值主张是你可以打字。如果保持代码库其余部分的类型安全足够重要,那么为什么不对您的样式做同样的事情呢?

TypeScript 提供了许多好处。首先,有自动完成功能。如果你输入“fo”,那么在 TypeScript 友好的编辑器中,你会在下拉列表中看到字体选项列表 - fontFamily, fontKerning, fontWeight,或任何其他匹配的内容 - 可供选择。这使得您可以在编辑器中轻松地发现 CSS 属性。如果您不记得名字 fontVariant 但要知道它将以“字体”一词开头,您可以输入它并滚动浏览选项。在 VS Code 中,您无需下载任何额外的工具即可实现此目的。

这确实加快了样式的创作速度:

这也意味着您的编辑正在监视您,以确保您不会犯任何可能导致令人沮丧的错误的拼写错误。

vanilla-extract 类型还在其类型定义中提供了语法解释 链接到 MDN 文档 对于您正在编辑的 CSS 属性。这消除了当样式表现异常时疯狂谷歌搜索的步骤。

VSCode 的图像,其中光标悬停在 fontKerning 属性上,并弹出一个描述该属性用途的弹出窗口,并包含指向该属性的 Mozilla 文档的链接

使用 TypeScript 编写意味着您对 CSS 属性使用驼峰命名法,例如 backgroundColor。对于使用常规 CSS 语法的开发人员来说,这可能会带来一些变化,例如 background-color.

集成

vanilla-extract 为所有最新的捆绑器提供一流的集成。这是完整的列表 集成 目前它支持:

  • 的WebPack
  • 编译器
  • 螺丝钉
  • 积雪
  • 下一个JS
  • 盖茨比

它也完全与框架无关。您需要做的就是从 vanilla-Extract 导入类名,这些类名会在构建时转换为字符串。

用法

要使用 vanilla-Extract,您需要编写一个 .css.ts 您的组件可以导入的文件。对这些函数的调用在构建步骤中转换为散列和作用域类名字符串。这听起来可能类似于 CSS 模块,这并非巧合:香草提取物的创造者, 马克·达尔格利什,也是 CSS Modules 的共同创建者。

style()

您可以使用以下命令创建自动限定范围的 CSS 类 style() 功能。您传入元素的样式,然后导出返回的值。将此值导入到用户代码中的某个位置,它会转换为作用域类名。

// title.css.ts
import {style} from "@vanilla-extract/css"; export const titleStyle = style({ backgroundColor: "hsl(210deg,30%,90%)", fontFamily: "helvetica, Sans-Serif", color: "hsl(210deg,60%,25%)", padding: 30, borderRadius: 20,
});
// title.ts
import {titleStyle} from "./title.css"; document.getElementById("root").innerHTML = `<h1 class="${titleStyle}">Vanilla Extract</h1>`;

媒体查询和伪选择器也可以包含在样式声明中:

// title.css.ts
backgroundColor: "hsl(210deg,30%,90%)",
fontFamily: "helvetica, Sans-Serif",
color: "hsl(210deg,60%,25%)",
padding: 30,
borderRadius: 20, "@media": { "screen and (max-width: 700px)": { padding: 10 }
}, ":hover":{ backgroundColor: "hsl(210deg,70%,80%)"
}

博曼 style 函数调用是对 CSS 的简单抽象——所有属性名称和值都映射到您熟悉的 CSS 属性和值。需要习惯的一个变化是,有时可以将值声明为数字(例如 padding: 30)默认为像素单位值,而某些值需要声明为字符串(例如 padding: "10px 20px 15px 15px").

样式函数内部的属性只能影响单个 HTML 节点。这意味着您不能使用嵌套来声明元素的子元素的样式 - 您可能习惯于此 萨斯 or 后CSS。相反,您需要单独为子级设置样式。如果子元素需要不同的样式 基于 在父级上,您可以使用 selectors 属性来添加依赖于父级的样式:

// title.css.ts
export const innerSpan = style({ selectors:{[`${titleStyle} &`]:{ color: "hsl(190deg,90%,25%)", fontStyle: "italic", textDecoration: "underline" }}
});
// title.ts
import {titleStyle,innerSpan} from "./title.css";
document.getElementById("root").innerHTML = `<h1 class="${titleStyle}">Vanilla <span class="${innerSpan}">Extract</span></h1>
<span class="${innerSpan}">Unstyled</span>`;

或者,您也可以使用 Theming API(我们接下来会介绍)在父元素中创建由子节点使用的自定义属性。这听起来可能有限制,但故意这样做是为了提高更大代码库的可维护性。这意味着您将确切地知道项目中每个元素的样式声明位置。

主题化

您可以使用 createTheme 在 TypeScript 对象中构建变量的函数:

// title.css.ts
import {style,createTheme } from "@vanilla-extract/css"; // Creating the theme
export const [mainTheme,vars] = createTheme({ color:{ text: "hsl(210deg,60%,25%)", background: "hsl(210deg,30%,90%)" }, lengths:{ mediumGap: "30px" }
}) // Using the theme
export const titleStyle = style({ backgroundColor:vars.color.background, color: vars.color.text, fontFamily: "helvetica, Sans-Serif", padding: vars.lengths.mediumGap, borderRadius: 20,
});

然后 vanilla-extract 允许您制作主题的变体。 TypeScript 帮助它确保您的变体使用所有相同的属性名称,因此如果您忘记添加 background 属性到主题。

VS Code 的图像,其中显示已声明主题但缺少背景属性,导致大量红色波浪线警告该属性已被遗忘

这是创建常规主题和深色模式的方法:

// title.css.ts
import {style,createTheme } from "@vanilla-extract/css"; export const [mainTheme,vars] = createTheme({ color:{ text: "hsl(210deg,60%,25%)", background: "hsl(210deg,30%,90%)" }, lengths:{ mediumGap: "30px" }
})
// Theme variant - note this part does not use the array syntax
export const darkMode = createTheme(vars,{ color:{ text:"hsl(210deg,60%,80%)", background: "hsl(210deg,30%,7%)", }, lengths:{ mediumGap: "30px" }
})
// Consuming the theme export const titleStyle = style({ backgroundColor: vars.color.background, color: vars.color.text, fontFamily: "helvetica, Sans-Serif", padding: vars.lengths.mediumGap, borderRadius: 20,
});

然后,使用 JavaScript,您可以动态应用 vanilla-extract 返回的类名来切换主题:

// title.ts
import {titleStyle,mainTheme,darkMode} from "./title.css"; document.getElementById("root").innerHTML = `<div class="${mainTheme}" id="wrapper"> <h1 class="${titleStyle}">Vanilla Extract</h1> <button onClick="document.getElementById('wrapper').className='${darkMode}'">Dark mode</button>
</div>`

这在幕后是如何工作的?您在中声明的对象 createTheme 函数被转换为附加到元素类的 CSS 自定义属性。这些自定义属性经过哈希处理以防止冲突。我们的输出 CSS mainTheme 示例如下所示:

.src__ohrzop0 { --color-brand__ohrzop1: hsl(210deg,80%,25%); --color-text__ohrzop2: hsl(210deg,60%,25%); --color-background__ohrzop3: hsl(210deg,30%,90%); --lengths-mediumGap__ohrzop4: 30px;
}

我们的 CSS 输出 darkMode 主题看起来像这样:

.src__ohrzop5 { --color-brand__ohrzop1: hsl(210deg,80%,60%); --color-text__ohrzop2: hsl(210deg,60%,80%); --color-background__ohrzop3: hsl(210deg,30%,10%); --lengths-mediumGap__ohrzop4: 30px;
}

因此,我们需要在用户代码中更改的只是类名。应用 darkmode 父元素的类名,以及 mainTheme 自定义属性被替换为 darkMode 的。

食谱API

stylecreateTheme 函数提供了足够的功能来设计自己的网站,但 vanilla-extract 提供了一些额外的 API 来提高可重用性。 Recipes API 允许您为元素创建一堆变体,您可以在标记或用户代码中进行选择。

首先需要单独安装:

npm install @vanilla-extract/recipes

这是它的工作原理。您导入 recipe 函数并传入具有属性的对象 basevariants:

// button.css.ts
import { recipe } from '@vanilla-extract/recipes'; export const buttonStyles = recipe({ base:{ // Styles that get applied to ALL buttons go in here }, variants:{ // Styles that we choose from go in here }
});

base,您可以声明将应用于的样式 所有 变种。里面 variants,您可以提供不同的方式来自定义元素:

// button.css.ts
import { recipe } from '@vanilla-extract/recipes';
export const buttonStyles = recipe({ base: { fontWeight: "bold", }, variants: { color: { normal: { backgroundColor: "hsl(210deg,30%,90%)", }, callToAction: { backgroundColor: "hsl(210deg,80%,65%)", }, }, size: { large: { padding: 30, }, medium: { padding: 15, }, }, },
});

然后您可以在标记中声明要使用的变体:

// button.ts
import { buttonStyles } from "./button.css"; <button class=`${buttonStyles({color: "normal",size: "medium",})}`>Click me</button>

vanilla-extract 利用 TypeScript 提供自动完成功能 你自己 变体名称!

您可以根据自己的喜好命名您的变体,并在其中添加您想要的任何属性,如下所示:

// button.css.ts
export const buttonStyles = recipe({ variants: { animal: { dog: { backgroundImage: 'url("./dog.png")', }, cat: { backgroundImage: 'url("./cat.png")', }, rabbit: { backgroundImage: 'url("./rabbit.png")', }, }, },
});

您可以看到这对于构建设计系统非常有用,因为您可以创建可重用的组件并控制它们的变化方式。使用 TypeScript 可以轻松发现这些变体 - 您只需输入即可 CMD/CTRL + Space (在大多数编辑器上),您将获得自定义组件的不同方法的下拉列表。

实用至上,洒水

Sprinkles 是一个构建在 vanilla-extract 之上的实用程序优先框架。这就是 vanilla-extract 文档的方式 形容它:

基本上,这就像构建您自己的零运行时、类型安全版本 顺风, 风格系统等等。

所以,如果你不喜欢给事物命名(我们都做过创建一个名字的噩梦) outer-wrapper div 然后意识到我们需要用 .div 包裹它。 。 。 outer-outer-wrapper )洒可能是您使用香草精的首选方式。

Sprinkles API 还需要单独安装:

npm install @vanilla-extract/sprinkles

现在我们可以创建一些构建块供我们的实用函数使用。让我们通过声明几个对象来创建颜色和长度的列表。 JavaScript 键名可以是我们想要的任何名称。这些值需要是我们计划使用它们的 CSS 属性的有效 CSS 值:

// sprinkles.css.ts
const colors = { blue100: "hsl(210deg,70%,15%)", blue200: "hsl(210deg,60%,25%)", blue300: "hsl(210deg,55%,35%)", blue400: "hsl(210deg,50%,45%)", blue500: "hsl(210deg,45%,55%)", blue600: "hsl(210deg,50%,65%)", blue700: "hsl(207deg,55%,75%)", blue800: "hsl(205deg,60%,80%)", blue900: "hsl(203deg,70%,85%)",
}; const lengths = { small: "4px", medium: "8px", large: "16px", humungous: "64px"
};

我们可以使用以下命令来声明这些值将应用于哪些 CSS 属性 defineProperties 功能:

  • 向其传递一个带有 a 的对象 properties 属性。
  • In properties,我们声明一个对象,其中 是用户可以设置的 CSS 属性(这些属性必须是有效的 CSS 属性)以及 价值观 是我们之前创建的对象(我们的列表 colorslengths).
// sprinkles.css.ts
import { defineProperties } from "@vanilla-extract/sprinkles"; const colors = { blue100: "hsl(210deg,70%,15%)" // etc.
} const lengths = { small: "4px", // etc.
} const properties = defineProperties({ properties: { // The keys of this object need to be valid CSS properties // The values are the options we provide the user color: colors, backgroundColor: colors, padding: lengths, },
});

那么最后一步就是传递返回值 definePropertiescreateSprinkles 函数,并导出返回值:

// sprinkles.css.ts
import { defineProperties, createSprinkles } from "@vanilla-extract/sprinkles"; const colors = { blue100: "hsl(210deg,70%,15%)" // etc.
} const lengths = { small: "4px", // etc. } const properties = defineProperties({ properties: { color: colors, // etc. },
});
export const sprinkles = createSprinkles(properties);

然后我们可以通过调用内部组件开始内联样式 sprinkles 类属性中的函数并为每个元素选择我们想要的选项。

// index.ts
import { sprinkles } from "./sprinkles.css";
document.getElementById("root").innerHTML = `<button class="${sprinkles({ color: "blue200", backgroundColor: "blue800", padding: "large",
})}">Click me</button>
</div>`;

JavaScript 输出保存每个样式属性的类名字符串。这些类名与输出 CSS 文件中的单个规则匹配。

<button class="src_color_blue200__ohrzop1 src_backgroundColor_blue800__ohrzopg src_padding_large__ohrzopk">Click me</button>

正如您所看到的,此 API 允许您使用一组预定义的约束来设置标记内元素的样式。您还可以避免为每个元素提供类名称的艰巨任务。结果感觉很像 Tailwind,但也受益于围绕 TypeScript 构建的所有基础设施。

Sprinkles API 还允许您编写 条件速记 使用实用程序类创建响应式样式。

结束了

vanilla-extract 感觉像是 CSS 工具的一个重大新进步。我们花了很多心思将其构建为一个直观、强大的样式解决方案,利用静态类型提供的所有功能。

深入阅读

来源:https://css-tricks.com/css-in-typescript-with-vanilla-extract/

时间戳记:

更多来自 CSS技巧