零運行時且支持 TS 的 CSS-in-JS 框架

以前如果使用 CSS-in-JS 編寫項目樣式文件,優先會考慮 styled-components,它的特點是使用模版字符串編寫樣式組件,使用方便、上手簡單。一個被反對的聲音主要是  styled-components 採用了運行時機制,增加了產物的體積也擔心運行時的開銷帶來一些性能損耗問題。

前段時間在個人項目中(訪問 https://github.com/qufei1993/compressor 查看這個項目)使用了 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 變量聲明與使用
: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 方案也用的該框架,感興趣的可 “閱讀原文” 關注下 https://github.com/qufei1993/compressor。

Reference

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/cs0mr0rXsJRjARV-WGbMPQ