前言
什么是UnoCss?
UnoCSS 是一个轻量级、高性能的 原子化 CSS 引擎,由 Anthony Fu(Vue 和 Vite 核心团队成员)开发。它灵感来源于 Tailwind CSS 和 Windi CSS,但设计更加灵活,性能更优,旨在提供更快速的样式开发体验。
项目地址:UnoCSS
作者为 UnoCSS 写的文章:重新构想原子化CSS
我刚开始用 UnoCss 的时候,觉得挺神奇的,很有意思!
现在准备学习一下源码,之前也没有接触过 vite 插件,这次正好一起学习下
本次目标:MVP
前期准备工作
先创建一个vite+vue的项目
pnpm create vite
在根目录下创建这么一个UnocssPlugin/vite.ts
import type { Plugin } from "vite";
export default function UnocssPlugin(): Plugin {
return {
name: "unocss-plugin",
enforce: "post",
load(id) {
console.log("11111111111id", id);
return null;
},
transform(code, id) {
console.log("22222222222id", id);
return null;
},
resolveId(id) {
console.log("33333333333id", id);
return null;
},
};
}
创建了这么一个插件后,当然是要在vite.config.ts注册使用了,所以
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import UnocssPlugin from "./UnocssPlugin/vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), UnocssPlugin()],
});
如果你也没有学过 vite 插件,可以在这里运行起来并打开网页,就能在控制台看到插件的打印内容了;vite 所有要处理的代码都会经过插件
开始核心代码开发
创建以下文件
UnocssPlugin\generator\index.ts
创建生成器的函数
import { UnocssRule } from "../types";
export function createGenerator(rules: UnocssRule[]) {
const cache = new Map<string, string | null>();
return (code: string) => {
// 把代码片段拆分成 token
const tokens = new Set(code.split(/[\s'"`;]/g));
// 生成的 CSS 代码
const css: string[] = [];
// 先从缓存中获取已经生成过的 token 的 CSS样式写入css数组
// 并从待处理 token 集合中删除
tokens.forEach((token) => {
if (cache.has(token)) {
const r = cache.get(token);
if (r) css.push(r);
tokens.delete(token);
}
});
// 处理剩余的 token
for (const [matcher, handler] of rules) {
tokens.forEach((token) => {
const match = token.match(matcher);
if (match) {
const r = handler(...Array.from(match));
if (r) {
css.push(r);
cache.set(token, r);
tokens.delete(token);
}
}
});
}
// 把剩余的 token 都缓存为 null,表示没有匹配到任何规则
tokens.forEach((token) => cache.set(token, null));
return css.join("\n");
};
}
创建UnocssPlugin\presets\index.ts
添加一个生成规则,只添加一个 padding 的样式,用于验证测试
import { UnocssRule } from "../types";
import { directionMap, e } from "../utils";
export const defaultRules: UnocssRule[] = [
[
/^p([trlb]?)-(\d+)$/,
(f, d, s) => `.${e(f)} { padding${directionMap[d] || ""}: ${+s / 4}rem; }`,
],
];
创建UnocssPlugin\utils\maps.ts
export const directionMap: Record<string, string> = {
'l': '-left',
'r': '-right',
't': '-top',
'b': '-bottom',
'': '',
}
创建UnocssPlugin\utils\object.ts
export function objToCss(obj: any) {
return `{${objToCssPart(obj)}}`
}
export function objToCssPart(obj: any) {
return Object.entries(obj)
.map(([key, value]) => value ? `${key}: ${value};` : undefined)
.filter(Boolean)
.join(' ')
}
创建UnocssPlugin\utils\index.ts
export * from "./cssEscape";
export * from "./maps";
export * from "./object";
创建UnocssPlugin\types.ts
export type UnocssRule = [RegExp, (...args: string[]) => string | undefined];
创建UnocssPlugin\utils\cssEscape.ts (这个可以不看,直接跳过)
对CSS 选择器转义,避免语法错误或安全问题
// https://drafts.csswg.org/cssom/#serialize-an-identifier
/**
* 这个函数的作用是对传入的字符串进行 CSS 选择器转义(escape),以确保字符串可以安全地作为 CSS 选择器的一部分使用。它会根据 CSSOM 规范 对特殊字符、控制字符、数字、连字符等进行转义,避免语法错误或安全问题。
* @param str
* @returns
*/
export function cssEscape(str: string): string {
const length = str.length;
let index = -1;
let codeUnit;
let result = "";
const firstCodeUnit = str.charCodeAt(0);
while (++index < length) {
codeUnit = str.charCodeAt(index);
// Note: there’s no need to special-case astral symbols, surrogate
// pairs, or lone surrogates.
// If the character is NULL (U+0000), then the REPLACEMENT CHARACTER
// (U+FFFD).
if (codeUnit === 0x0000) {
result += "\uFFFD";
continue;
}
// Comma
if (codeUnit === 44) {
result += "\\2c ";
continue;
}
if (
// If the character is in the range [\1-\1F] (U+0001 to U+001F) or is
// U+007F, […]
(codeUnit >= 0x0001 && codeUnit <= 0x001f) ||
codeUnit === 0x007f ||
// If the character is the first character and is in the range [0-9]
// (U+0030 to U+0039), […]
(index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
// If the character is the second character and is in the range [0-9]
// (U+0030 to U+0039) and the first character is a `-` (U+002D), […]
(index === 1 &&
codeUnit >= 0x0030 &&
codeUnit <= 0x0039 &&
firstCodeUnit === 0x002d)
) {
// https://drafts.csswg.org/cssom/#escape-a-character-as-code-point
result += `\\${codeUnit.toString(16)} `;
continue;
}
if (
// If the character is the first character and is a `-` (U+002D), and
// there is no second character, […]
index === 0 &&
length === 1 &&
codeUnit === 0x002d
) {
result += `\\${str.charAt(index)}`;
continue;
}
// If the character is not handled by one of the above rules and is
// greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
// is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to
// U+005A), or [a-z] (U+0061 to U+007A), […]
if (
codeUnit >= 0x0080 ||
codeUnit === 0x002d ||
codeUnit === 0x005f ||
(codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
(codeUnit >= 0x0041 && codeUnit <= 0x005a) ||
(codeUnit >= 0x0061 && codeUnit <= 0x007a)
) {
// the character itself
result += str.charAt(index);
continue;
}
// Otherwise, the escaped character.
// https://drafts.csswg.org/cssom/#escape-a-character
result += `\\${str.charAt(index)}`;
}
return result;
}
export const e = cssEscape;
创建统一导出UnocssPlugin\index.ts
export * from "./types";
export * from "./generator";
export * from "./presets";
export * from "./utils";
使用
我们已经写好了 CSS 代码的生成器,那现在用一下看看
开始改写插件
UnocssPlugin\vite.ts
import type { Plugin } from "vite";
import { createGenerator, defaultRules } from ".";
const VIRTUAL_PREFIX = "/@virtual/unocss/";
function getHash(input: string, length = 8) {
let hash = 0;
for (let i = 0; i < input.length; i++) {
hash = (hash << 5) - hash + input.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
return Math.abs(hash).toString(36).slice(0, length);
}
export default function UnocssPlugin(): Plugin {
const generate = createGenerator(defaultRules);
const map = new Map<string, [string, string]>();
return {
name: "unocss-plugin",
enforce: "post",
load(id) {
if (!id.startsWith(VIRTUAL_PREFIX)) return null;
const hash = id.slice(VIRTUAL_PREFIX.length, -".css".length);
const [source, css] = map.get(hash) || [];
if (source) this.addWatchFile(source);
return css;
},
transform(code, id) {
// 不处理css文件
if (id.endsWith(".css")) return null;
const style = generate(code);
// 不处理没有生成样式的文件
if (!style) return null;
// 对id进行hash,生成虚拟模块id
const hash = getHash(id);
// 将虚拟模块id和原始文件id、生成的样式映射起来
map.set(hash, [id, style]);
const virtualId = `${VIRTUAL_PREFIX}${hash}.css`;
// 生成虚拟模块的导入语句,并将其插入到代码的最前面
return `import "${virtualId}";${code}`;
},
resolveId(id) {
// 处理虚拟模块的请求
return id.startsWith(VIRTUAL_PREFIX) ? id : null;
},
};
}
最后简易改造下 App.vue
<script setup lang="ts"></script>
<template>
<div class="p-10">内容</div>
</template>
<style scoped></style>
运行起来结果如下
完成MVP
学到的
- 可以多用 Map, Set,优化效率
- 正则的使用
- vite 插件开发入门
通过这个简易实现,深入理解了 UnoCSS 的基本原理和 Vite 插件的工作机制。