UnoCss(原子化CSS引擎)

4,556 阅读6分钟

image.png

什么是原子化 CSS

概念

原子化 CSS 是一种 CSS 的架构方式,它倾向于小巧且用途单一的 class,并且会以视觉效果进行命名。 例如:

.ml-10px {
    margin-left: 10px;
}

.c-green {
    color: green;
}

.bg-blend-color-dodge {
  background-blend-mode: color-dodge;
}

.color-red {
  --un-text-opacity: 1;
  color: rgba(248, 113, 113, var(--un-text-opacity));
}

原子化 CSS 的优势

  1. 提高开发效率 利用原子化框架提供的预设原子类,在少量样式编写上可以极大的提高开发效率,不需要单独定义在样式文件中
  2. 免去起名烦恼 我们经常会因为起名而烦恼,之前也尝试过各种 css 命名方法,包括BEM。然而当html层级嵌套比较深的情况下,BEM 命名法也会有起名难、不直观的缺陷
  3. 避免样式堆积 可以很好的避免历史样式的堆积,不存在历史样式类不敢删除的问题,有效的减少 CSS 的体积
  4. 样式隔离 天然的支持组件间的样式隔离,没有自定义的 class 也就无需担心组件之间样式的影响

按需生成

为了解决 Tailwind 生成的 CSS 文件过大以及热更新的效率问题,Windi CSS 和 Tailwind JIT 通过预先扫描源代码的方式按需生成。 image.png 同时,按需生成的方式可以实现预处理编译生成方式不能实现的动态原子化的 CSS 的需求。按需生成的方式,Windi CSS 获得了比传统的 Tailwind CSS 快 100 倍左右 的性能。 预先扫描源代码例子:

import glob from 'fast-glob'
import { promises as fs } from 'fs'

// 通常这个是可以配置的
const include = ['src/**/*.{jsx,tsx,vue,html}']

async function scan() {
  const files = await glob(include)

  for (const file of files) {
    const content = await fs.readFile(file, 'utf8')
    // 将文件内容传递给生成器并配对 class 的使用情况
  }
}

await scan()
// 扫描会在构建/服务器启动前完成
await buildOrStartDevServer()

什么是 UnoCss

具有高性能且极具灵活性的即时原子化 CSS 引擎。UnoCSS 是一个原子 CSS 引擎而不是一个框架。一切都在设计时考虑了灵活性和性能。UnoCSS 中没有核心实用程序,所有功能都是通过预设提供的。

UnoCSS 的主要目标是直观性和可定制性。它可以让你在数十秒内,定义你自己的 CSS 工具。

默认情况下,UnoCSS 应用默认预设,它提供了流行的实用程序优先框架 Tailwind CSS、Windi CSS、Bootstrap、Tachyons 等的通用超集。

预设

unocss 采取预设的方式来模拟原子化 CSS 框架大部分所提供的功能,换言之,预设是其核心机制

// vite.config.ts
import Unocss from 'unocss/vite';
import {presetAttributify, presetUno} from 'unocss';
export default {
    plugins: [
        Unocss({
            presets: [
                presetAttributify({}),
                presetUno(),
                // 自定义预设
            ],
        }),
    ],
}

自定义规则

// 静态规则
rules: [
    ['m-10px', { margin: '0 10px' }]
]
.m-10px {
    margin: 0 10px;
}
// 使用正则匹配动态规则
rules: [
    [/^m-(\d+)px$/, ([, d]) => ({ margin: `0 ${d}px`})],
]

.m-20px {
    margin: 0 20px;
}

shortcuts 进一步封装的工具类

当你经常使用相同的工具类合集时,出现重复性是常见的,我们提供了 shortcuts 特性允许你把工具类的名字组合在一起,在你应用程序的任何地方使用,而不需要重复写。

shortcuts: [
    {
        border-primary: 'border border-color-primary',
        // .border {
        //     border-width: 1px;
        //     border-style: solid;
        // }
        // .border-color-primary {
        //     border-color: var(--primary-color);
        // }
    },
    // 动态 shortcuts
    [/^btn-(.*)$/, ([, c]) => `bg-${c}-200 text-${c}-100 px-5`],
]

属性化模式

通过引入 @unocss/preset-attriutify 预设可以使用属性化模式,该模式可以增加一串原子化 CSS 书写时的可读性

 <div
    class="bg-green-200 hover: bg-green-300 text-blue text-sm font-14 font-light
border-2 border-black-200 dark:bg-green-400 dark:hover:bg-green-500"
>属性化模式</div>
<!--使用属性化模式可以改写为-->
<div
    bg="green-200 hover:green-300 dark:green-400 dark:hover:green-500"
    text="blue sm"
    font="14 light"
    border="2 black-200"
>属性化模式</div>

这样写会出现 ts 类型报错,需要使用声明文件解决

// shims.d.ts
import type {AttributifyAttributes} from '@unocss/preset-attributify';
declare module 'vue' {
    interface HTMLAttributes<T> extends AttributifyAttributes {}
}

这样实现可读性增加了,但是代码显得有点乱,组件中如果少于三个样式 推荐用 unocss 这种原子化 CSS,多的话还是建议放在样式文件中维护。

性能

10/21/2021, 2:17:45 PM
1656 utilities | x50 runs

none                            8.75 ms /    0.00 ms
unocss       v0.0.0            13.72 ms /    4.97 ms (x1.00)
windicss     v3.1.9           980.41 ms /  971.66 ms (x195.36)
tailwindcss  v3.0.0-alpha.1  1258.54 ms / 1249.79 ms (x251.28)

从结果来看,UnoCSS 可以比 Tailwind 的 JIT 引擎快 200 倍!说实话,在按需生成的情况下,Windi 和 Tailwind JIT 都已经算是超快了,UnoCSS 的性能提升感知度可能没有那么高。然而,几乎为零的开销意味着你可以将 UnoCSS 整合到你现有的项目中,作为一个增量解决方案与其他框架一同协作,而不需要担心性能损耗。

跳过解析,不使用 AST

从内部实现上看,TailWind 依赖于 PostCSS 的 AST 进行修改,而 Windi 则是编写了一个自定义解析器和 AST。考虑到在开发过程中,这些工具的 CSS 不经常变化,UnoCSS 通过非常高效的字符串拼接起来直接生成对应的 CSS 而非引入整个编译过程。同时,UnoCSS 对类名和生成的 CSS 字符串进行了缓存,当再次遇到相同的实用工具类时,它可以绕过整个匹配和生成的过程

单次迭代

正如前文所述,Windi CSS 和 Tailwind JIT 都依赖于对文件系统的预扫描,并使用文件系统监听器来实现 HMR。文件 I/O 不可避免地会引入开销,而你的构建工具实际上需要再次加载它们。那么我们为什么不直接利用已经被工具读取过的内容呢?

在 Vite 中,transform 的钩子将与所有的文件及其内容一起被迭代。因此,我们可以写一个插件来收集它们,比如:

export default {
  plugins: [
    {
      name: 'unocss',
      transform(code, id) {
        // 过滤掉无需扫描的文件
        if (!filter(id))
          return

        // 扫描代码(同时也可以处理开发中的无效内容)
        scan(code, id)

        // 我们只需要内容,所以不需要对代码进行转换
        return null
      },
      resolveId(id) {
        return id === VIRTUAL_CSS_ID ? id : null
      },
      async load(id) {
        // 生成的 css 会作为一个虚拟模块供后续使用
        if (id === VIRTUAL_CSS_ID)
          return { code: await generate() }

      }
    }
  ]
}

由于 Vite 也会处理 HMR,并在文件变化时再次执行 transform 钩子,这使得 UnoCSS 可以在一次加载中就完成所有的工作,没有重复的文件 I/O 和文件系统监听器。此外,通过这种方式,扫描会依赖于模块图而非文件 glob。这意味着只有构建在你应用程序中的模块才会影响生成的 CSS,而并非你文件夹下的任何文件。

默认单位

默认单位为 rem,如何处理成 px

// 默认情况
.mt-1 {
    // 0.25 * 16px = 4px
    margin-top: 0.25rem; 
}

rem 指向的是根元素的字体大小,所以可以设置 html 的字体大小 4px,1单位 = 0.25rem = 1px

html {
    font-size: 4px;
}
.mt-1 {
    // 0.25 * 4px = 1px
    margin-top: 0.25rem;
}

直接使用 CSS 变量

<div class="c-[var(--primary-color)]">example</div>
c-[var(--primary-color)] {
    color: var(--primary-color);
}

参考文档

重新构想原子化 CSS