手记

推荐这个零运行时且支持 TS 的 CSS-in-JS 技术方案!

以前如果使用 CSS-in-JS 编写项目样式文件,优先会考虑 styled-components,它的特点是使用模版字符串编写样式组件,使用方便、上手简单。一个被反对的声音主要是 styled-components 采用了运行时机制,增加了产物的体积也担心运行时的开销带来一些性能损耗问题。

前段时间在个人项目中使用了 Vanilla Extract,不同于其它 CSS-in-JS 方案,它可以在编译时期编译出 CSS 样式文件,实现了零运行时且支持 TypeScript

下文,将为您介绍 Vanilla Extract 的特点及应用。

什么是 Vanilla Extract?

Vanilla Extract 是一个新的 CSS-in-JS 库,用来编写 CSS 样式文件,于 2021 年开源,在年度全球 CSS 报告中荣登 CSS-in-JS 满意度榜首。

框架友好

Vanilla Extract 是一个通用的库,没有绑定在任何 JavaScript 框架上,你可以在 React、Vue、Angular… 等框架中来使用它,但在这之前你需要先让自己的构建工具能够支持它。

Vanilla Extract 目前已经为最流行的前端构建工具做了集成,包括:webpack、esbuild、Vite 等。

本文以在 Vite 中使用为例,首先需要安装 @vanilla-extract/css、@vanilla-extract/vite-plugin 两个库。@vanilla-extract/css 是我们样式开发中主要用到的库

npm install @vanilla-extract/css @vanilla-extract/vite-plugin

之后将 vanillaExtractPlugin 插件添加到 vite 配置中。对 CSS 的编译和处理主要是通过该插件完成的,在编写样式文件时,文件名要以 **.css.ts** 结尾。

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

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

零运行时与 TS 支持

使用 TypeScript 是 Vanilla Extract 的核心,使得 Vanilla Extract 能够准确的定位到样式所在的位置,实现了在构建时生成所有的样式,就像 SaaS、Less 那样,也是与其它 CSS in JS 方案(Styled Components 和 Emotion)不同的地方。

使用 TypeScript 编写 CSS 样式的另一个好处是,带来了样式的类型安全,如果编写了错误的值,在编译时就会给我们报错。在编码的过程中编辑器也会给我们一些样式提示。

常用样式 API

介绍一些日常开发常用的样式 API 在 vanilla extract 中是如何编写的,@vanilla-extract/css 库的 style() 方法是常用的基础 API

样式文件需要以 **.css.ts** 结尾,CSS 样式属性名编写采用驼峰式,和正常写 CSS 样式区别也不是很大。

// styled.css.ts
import { style } from '@vanilla-extract/css';

export const todoList = style({
  marginTop: '20px',
  background: '#ccc',
});

export const todoInfo = style({
  paddingTop: '10px',
});

之后在我们的 App 组件内,导入样式文件,为标签的 className 属性赋值 value 就可以了,使用方式和其它的 CSS-in-JS 库还是有区别的,例如 styled-component 样式是以组件的方式来编写和使用的。

// app.tsx
import * as styled from './styled.css';

const App = () => {
  return <>
    <ul className={styled.todoList}>
      <li className={styled.todoInfo}>学习 React 开发</li>
      <li className={styled.todoInfo}>学习 Node.js 开发</li>
    </ul>
  </>
}

vanilla extract 支持一些伪类选择器、子选择器、媒体查询、**@supports**以及引用 style() 函数创建的其它类

export const todoInfo = style({
  paddingTop: '10px',
  // 伪类选择器
  ':hover': {
    color: 'red',
  },
  selectors: {
    // 选择自身的最后一个元素
    '&:last-child': {
      paddingBottom: '10px',
    },
    // 选择器还可以包含对其他作用域类名称的引用,这会改变 todoList 这个类的背景颜色
    [`${todoList} &`]: {
      background: 'yellow',
    },
  },
  // 媒体查询
  '@media': {
    'screen and (min-width: 568px)': {
      color: 'blue',
    },
  },
  '@supports': {
    // 摘自官网示例
    '(display: grid)': {
      display: 'grid',
    },
  },
});

文档中有声明:为了提高可维护性,每个样式块只能针对单个元素。意思也就是说你不能直接对其子元素或兄弟元素做调整,例如,如下需求:

.todo-list > li { // 期望的写法
    color: green !important;
}

export const todoList = style({
  marginTop: '20px',
  background: '#ccc',
  '& > li': { // 错误的实现
    color: green !important;
  }
});

Vanilla Extract 提供了 GlobalStyle() API 用于在全局范围内定位当前元素的子节点。这里你不需要担心重复问题,Vanilla Extract 具有局部范围的类名,就像 CSS Module 那样,不存在类名冲突的风险

import { style, globalStyle } from '@vanilla-extract/css';
export const todoList = style({ ... });

// 对局部作用类名的样式设置
globalStyle(`${todoList} > li`, {
  color: 'green !important',
});

Vanilla Extract 全局样式不支持嵌套,有些情况可能需要调用 globalStyle() 函数多次来设置样式

globalStyle('html, body', {
  margin: 0
});
globalStyle('a', {
  color: 'blue',
});
globalStyle('a:hover', {
  color: 'red',
});

样式组合,就像父类和子类的继承关系,将样式的通用部分抽象出来。

import { style } from '@vanilla-extract/css';
const base = style({ padding: 12 });
export const primary = style([
  base,
  { background: 'blue' }
]);
export const secondary = style([
  base,
  { background: 'aqua' }
]);

CSS 变量与主题

CSS 变量

CSS 变量有时也被称作 CSS 自定义属性,当定义一个 CSS 变量后,可以在整个网站的样式文件中重复使用

例如,在一个网站开发中,可能会有很多重复的值,比如 color,用原生的 CSS 编写如下所示:

  • 使用两个减号 -- 声明一个自定义属性,属性值则可以是任何有效的 CSS 值,注意属性名是大小写敏感的
  • 使用 var() 函数访问一个自定义的 CSS 变量
  • 在 JavaScript 中也可操作 CSS 变量,就像操作普通 CSS 属性一样
// css 变量声明与使用
:root {
  --blue-color: blue;
}
.one {
  color: var(--blue-color);
}
.two {
  color: var(--blue-color);
}

// javascript 操作 css
element.style.getPropertyValue("--blue-color");// 获取一个 Dom 节点上的 CSS 变量
getComputedStyle(element).getPropertyValue("--blue-color");// 获取任意 Dom 节点上的 CSS 变量
element.style.setProperty("--blue-color", jsVar + 4);// 修改一个 Dom 节点上的 CSS 变量

vanilla extract 没有重复造轮子,而是大量使用了浏览器内置的原生 CSS 变量功能。由于 vanilla extract 具有局部作用域的类名,另一个好处是能够在样式块内限定 CSS 变量的范围

创建主题

当应用程序具有单一的全局主题时,推荐使用 createGlobalTheme 方法,使用也不复杂,直接看文档即可。

有时我们的应用程序会存在多个主题的情况,例如 “暗黑模式”,首先需要做的是使用 createGlobalThemeContract() 方法创建一个主题契约,这些 key 的值可以先设置为 null,这也是确保创建的主题能有正确的 key。之后将 “主题契约” 返回的值做为 createTheme() 方法的第一个参数传入。

import { createTheme, createThemeContract } from '@vanilla-extract/css';

const colors = createThemeContract({
  color: null,
  backgroundColor: null,
});

export const lightTheme = createTheme(colors, {
  color: '#000000',
  backgroundColor: '#ffffff',
});

export const darkTheme = createTheme(colors, {
  color: '#ffffff',
  backgroundColor: '#000000',
});
export const vars = { colors };

应用主题

上面的 createTheme() 方法返回的值是一个类名,可以应用于 HTML 标签的 className 中来声明 CSS 变量,在应用需要获取 CSS 样式的根标签中,设置这个类名。

import { darkTheme, lightTheme } from './styles/theme.css';
import { useState } from 'react';

const App = () => {
  const [isDarkTheme, setIsDarkTheme] = useState(false);
  return (
    <div id="app" className={isDarkTheme ? darkTheme : lightTheme}>
      <button
        type="button"
        onClick={() => setIsDarkTheme((currentValue) => !currentValue)}
       >
         Switch to {isDarkTheme ? 'light' : 'dark'} theme
       </button>
    </div>
  );
};

上面代码片段中,以一个简单的 Demo 来展示如何应用主题,想做的体验好一点的,还可以通过监听系统主题的方式设置默认的主题颜色,参考项目
https://github.com/qufei1993/compressor/blob/main/client/src/hooks/index.ts#L27

export const useSystemTheme = () => {
  const [name, setName] = useState<TThemeName>(Theme.Light);

  useEffect(() => {
    if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
      setName(Theme.Dark);
    } else {
      setName(Theme.Light);
    }

    window
      .matchMedia('(prefers-color-scheme: dark)')
      .addEventListener('change', (event: MediaQueryListEvent) => {
        if (event.matches) {
          setName(Theme.Dark);
        } else {
          setName(Theme.Light);
        }
      });
  }, []);

  return {
    name,
    isDarkMode: name === Theme.Dark,
    isLightMode: name === Theme.Light,
  };
};

切换主题按钮,可以看到在 CSS 样式表中会包含以下两个主题的 CSS 变量。


使用主题

在组件样式中使用声明好的主题变量,vars 是 theme.css.ts 文件中导出的变量。

import { style } from '@vanilla-extract/css';
import { vars } from '../../styles/theme.css';

export const todoList = style({
  marginTop: '20px',
  backgroundColor: vars.colors.backgroundColor,
  color: vars.colors.color,
});

让我们看下效果,就像直接写原生 CSS 变量一样,使用 var 获取 CSS 变量,而不是直接的值替换。

总结

在本文中,我们主要介绍了 Vanilla Extract 的一些特点及在 React 中的应用,不同于其它任何的 CSS-in-JS 方案,它的样式不是在 JavaScript 运行时生成的,而是在编译阶段已经完成

Vanilla Extract 是一个新的 CSS-in-JS,尽管目前在使用率上远不及它的竞争对手,但在 2021 年全球 CSS 满意度调查中还是位列榜首的,也是一个可以关注的技术。

本文我们也只介绍了它的皮毛,更多使用参考以下链接,可以去官网阅读更多,笔者最近写的一个项目,CSS 方案也用的该框架,感兴趣的可关注下。

Reference


1人推荐
随时随地看视频
慕课网APP