vanilla-extract教程。在TypeScript中创建零运行时间的样式表

807 阅读11分钟

在本教程中,我们将深入了解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的操作,以帮助建立以下页面。

Stylized Theme Created With 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 ,有以下属性。

vanilla-extract Theme Variables Translated to CSS

它们的进一步用法可以在检查其余元素后发现,比如在其中一个menuItemStyle:

vanilla-extract Theme Variables Being Used in HTML Elements

使用Sprinkles与vanilla-extract

到此为止,你应该明白vanilla-extract是怎么回事,以及如何使用它来创建你的静态样式和主题。

然而,这些样式还是有点混乱和重复的。让我们通过介绍Sprinkles,即vanilla-extract的零运行时间原子CSS框架来解决这个问题。

Sprinkles提供了一套实用的类,可以和原子一起被定制和组成可重用的样式。如果你以前使用过Styled SystemTailwind 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 函数是有帮助的。

看看条件原子,如flexDirectionfontSize ,等等。由于我们在Sprinkles文件中配置了桌面与移动的条件,现在,每次你需要根据设备尺寸设置不同的样式时,你可以分别通过desktopmobile 属性来定义它。

例如,对于我们的menuItems 安排,元素的移动处置应该使用CSSgrid 显示,而不是flex ,这样我们就可以显示,如下图所示。

Stylized Theme for a Mobile App Created Using vanilla-extra

这同样适用于字体大小,在移动设备上会更小。

你可能想知道我们之前在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博客上。