跳到主要内容

Webpack UnoCSS

2025年01月03日
柏拉文
越努力,越幸运

一、认识


基于 Webpack, 我们支持 Css 以及 ScssLess 等预处理器还有 UnoCSS 原子样式。配置策略如下:

1. 插入/或者分离 Css 文件: 为了提高开发效率,Webpack 在开发模式下会启用热重载(Hot Module Replacement)和快速构建,CSS 会通过 style-loader 注入到页面中,而不是单独提取成文件,这样可以避免每次修改都重新加载整个 CSS 文件。在生产环境下,为了提升性能和缓存效果,CSS 会被提取成独立的文件,使用 MiniCssExtractPlugin 插件来实现。可以减小主产物体积, 也可以可以避免重复加载 CSS,提高页面的加载速度。MiniCssExtractPlugin 同时提供了 loader, 将这个 loader 放在最前面即可。MiniCssExtractPlugin 可以配置 filenamechunkFilename ,可以自定义 Css 样式名和位置。

2. 处理 Css 文件: 通过 css-loader 处理 Css 文件, 使其可以在JS中被引用和加载。并针对 *.module.css 的文件设置 modules: true / modules: { xx: 配置} 配置 Css Modules, 开启 CSS 样式模块化、局部化,解决了全局样式冲突的问题。

3. 优化、兼容、转换 Css: 通过 postcss-loader 优化、转换、兼容 Css。通过 Autoprefixer 插件自动为 CSS 添加浏览器前缀,保证不同浏览器的兼容性。postcss-preset-env:允许你使用未来的 CSS 特性,比如 CSS 自定义属性、嵌套等,并将其转换为当前浏览器可以理解的 CSS

4. SassLess 预处理 Css: 使用 sass-loaderless-loader 来预处理 .scss.less 文件, 使其转换为 .css 文件。

UnoCSS 是一个轻量级、高性能、按需生成的原子 CSS 框架,旨在为前端开发提供快速、灵活的样式解决方案。它的核心理念是 原子化(atomic,意味着每个 CSS 类只应用单一的样式属性。UnoCSS 的设计目标是提供极致的性能,同时保持高度的可定制性和灵活性。

UnoCSS 受到了许多现代工具和框架(如 Tailwind CSS)启发,但它在设计上更注重极简主义和灵活性。与 Tailwind CSS 不同,UnoCSS 是按需生成类名的,这意味着它只会生成你实际使用的类,从而减小了 CSS 文件的大小。

Webpack 中,只需要通过动态导入 @unocss/webpack, 获取 UnoCSS 插件, 在 Plugins 中注册即可。同时, 需要创建 UnoCSS 配置文件, 可以自定义符合项目的 preset, preset 中可以定义 rules 单个样式规则, shortcuts 多个规则的合并等。

1.1 UnoCss 是如何引入的?

Webpack 引入 UnoCss: Webpack 中通过 @unocss/webpack 插件来引入 UnoCss, 在入口文件中需要引入 import "uno.css" , uno.css 需要 css-loaderstyle-loader 处理。在 webpack.config.js 文件中引入 @unocss/webpackWebpack Plugin 的形式来使用, 可以传入 UnoCss 的配置文件地址,默认是从根目录查找。

  • Webpack 配置: 使用 @unocss/webpack 插件, 可以传入 uno.config.ts 的文件路径,默认是从项目根目录寻找。通过 css-loaderstyle-loader / MiniCssExtractPlugin.loader 处理 import "uno.css" 文件, uno.css 会在构建中生成虚拟文件 _virtual_%2F__uno.css

    const unoCssRules = [
    {
    test: /\.css$/i,
    include: Path.resolve(process.cwd(), "_virtual_%2F__uno.css"),
    use: [...combineCssLoader({ devMode, modules: false, postcss: true })],
    },
    ];

    module.exports = async function () {
    const module = await import("@unocss/webpack");
    const { default: UnoCSS } = module;

    return merge(config, {
    plugins: [UnoCSS(Path.resolve(process.cwd(), "./uno.config.ts"))],
    optimization: {
    realContentHash: true,
    },
    });
    };
  • UnoCss 配置文件: 配置预设、转换器

    export default defineConfig({
    presets: [
    preset1,
    ],
    transformers: [],
    })
  • 入口文件引入 uno.css:

    import "uno.css";

RsPack 引入 UnoCss: RsPackWebpack 的引入方式几乎类似,@unocss/webpack 中提供了 @unocss/webpack/rspack 插件, 其他处理是一样的。

1.2 为什么要引入 UnoCss?

一、为什么要引入 UnoCss: 在我们项目中, Css 样式文件的痛点如下:

  1. Css 公共样式复用率很低

  2. 没有 Tree Shaking 机制,没有用到的样式也会打包(Css Module 已经支持 Tree Shaking

  3. 各位伙伴在各个项目中已经有意识的自己写了一些预设, 然后使用在 Sass 中使用 @extend 或者 @mixin/@include 来使用已经定义好的预设。但是有局限性, 其他伙伴并不知道。

因此, 我们引入了 UnoCss 原子化样式方案, 使用工程化的方式来提供预设的公共原子化样式, 并以内联的形式写入。UnoCss 并提供了 VsCode 插件,可以很好的提示我们定义好的 UnoCss 预设。

1.3 UnoCss Vs Tailwind?

Css 原子化方案其实有很多, 关注比较多的有 TailwindUnoCss,他们都支持按需生成 Css,都提供响应式,可以定制预设,并支持主题化和暗盒模式。但是 Tailwind 是重量级的,更注重定义系统,它提供了全面的预设样式,大部分在我们项目中是用不到的。强约束比较多,配置项也比较多,另外 Tailwind 会经过 PostCss , 本质上 TailwindPostcss 插件。而我们的 UnoCss 非常灵活,通过在项目根目录下使用 JavaScript 灵活定义规则, 可以使用正则和函数,还有更加复杂的逻辑和条件判断。另外多个规则也可以组合成一个快捷键。UnoCSS 是一个同构引擎, 可以灵活的在不同的地方使用, 可以作为 Webpack Plugin 动态生成 Css 文件。只依赖于 css-loaderstyle-loader 或者 MiniCssExtractPlugin 等。UnoCss 直接操作字符串,不会转换成 AST,也不需要在内存中保存 AST 结构,所以 UnoCss 会比 Tailwind 构建编译更快,而且在多次构建之间复用生成的样式,增量编译很快。

1.4 UnoCss 原理、工作机制?

UnoCss 工作机制:

  1. 类名扫描(类名提取)UnoCSS 在构建时通过扫描项目中的代码(HTMLJSTSVueReact 等),提取出所有实际使用到的 CSS 类名。UnoCSS 会扫描你项目中的源代码,识别出所有的类名,并构建出一个类名-样式的映射。

  2. 样式生成: 根据扫描到的类名,UnoCSS 会从预定义的规则中生成最小的 CSS 文件。生成的 CSS 只包含被实际使用的类和属性,避免了传统 CSS 框架中的冗余样式。UnoCSS 通过一套精密的规则生成最小化的样式表,这意味着不需要生成所有可能的类,只生成实际用到的类。

  3. 即时生成(按需生成)UnoCSS 是一个按需生成的 CSS 框架。即使项目中存在很多潜在的 CSS 类,UnoCSS 只生成当前项目中被实际使用的类的 CSS。这个特性保证了最终的输出非常精简。

  4. 不依赖 AST 转换UnoCSS 的设计不依赖于传统的 AST(抽象语法树)转换。AST 转换通常是在工具(如 PostCSSBabel 等)中对源码进行语法解析和抽象,进而进行优化、压缩等处理。UnoCSS 不需要这一过程,因为它是基于类名匹配规则来生成样式的,而不是通过对整个 CSS 文件进行分析和转换。这使得 UnoCSS 具有非常高的性能,因为它可以直接根据类名生成规则,而不需要做复杂的 AST 操作。

注意: 我们在 Webpack/Rspack 中通过 css-loader/style-loader 处理的是入口文件显示引入的 uno.css 文件,而不是基于扫描到的类名动态生成的 Css 文件。

Webpack UnoCss 工作机制: 基于 compilation.hooks.processAssets(Webpack 4)compilation.hooks.optimizeAssets(5、6) Webpack 产物优化处理阶段, 等待 UnoCss 完成初始化后, 进行类名扫描,生成样式,随后遍历 Webpack 文件列表, 根据占位符替换成生成的样式,并重新生成源映射(SourceMap) 。也就是说: 在 Webpack 中, 基于扫描到的类名动态生成的样式会直接插入到 Webpack 最终的产物中。

1.5 UnoCss 在项目中时如何使用的?

我们项目中, UnoCss 仅提供公共样式的预设,它的作用类似于 Scss 中的 @mixin/@import 。可以方便快捷的使用我们定义好的公共样式,可以帮助我们按需生成样式。是一个非常好的公共样式工具。UnoCss 不应该完全替代 Sass, 而是应该作为辅助 Css 工具来解决 Css 痛点。我们通过 UnoCss 的特性对通用样式更好的统一、减少样式重名导致的样式污染、避免重复加载 Css、减少 Css 文件体积。

引入 UnoCss 遇到了哪些问题?: UnoCss 通过 ClassName 的形式, ClassName 会变得很长, 如果使用不当,Css 的可读性会变得很差。

二、准备


安装相关依赖如下

# Css 相关依赖 
pnpm add css-loader style-loader sass-loader sass less less-loader postcss-loader mini-css-extract-plugin postcss-preset-env autoprefixer postcss-pxtorem -D

# React 相关依赖
pnpm add react react-dom -D

# Webpack 相关依赖
pnpm add cross-env webpack webpack-cli webpack-merge html-webpack-plugin @unocss/webpack -D

# Typescript 相关依赖
pnpm add @swc/cli @swc/core typescript swc-loader @types/react @types/react-dom -D

三、配置


3.1 preset

import { Preset } from "unocss";

const getSizeValue = (value: string): string => {
if (!value) {
return "";
}
if (value.includes("-")) {
const values = value.split("-");
return values.map(getSizeValue).join(" ");
}
if (isNaN(Number(value))) {
return value;
}

const remSize = parseFloat(value) / 100;
return remSize ? `${remSize}rem` : "0";
};

const getColorVars = () => {
try {
let fileContent = `
// 功能色
$BrandLight: var(--umu-gold-2, rgba(250, 180, 0, 0.08));
$BrandFocus: var(--umu-gold-3, rgba(250, 180, 0, 0.24));
$BrandNormal: var(--umu-color-primary, #fab400);
$BrandEnhanced: var(--umu-color-primary-hover, #f2af2e);
$BrandDark: var(--umu-color-primary-active, #eaa900);
$BrandRangeNormal: #fff8e5;
$BrandRangeBorderNormal: #ffe296;

$SubLightHover: rgba(30, 110, 230, 0.08);
$SubLightFocus: rgba(30, 110, 230, 0.18);
$SubFocus: var(--umu-color-link-hover, #4a93ff);
$SubHover: #297dfc;
$SubColor: var(--umu-color-link, #1e6ee6);
$SubRangeColor: #e8f0fd;
$SubRangeBorderColor: #c3daff;
$Warning: #ff860c;
$Error: #dd4e40;
$Success: #21a564;

// 文字色
$Text1: var(--umu-color-text, #222222);
$Text2: var(--umu-color-text-secondary, #666666);
$Text3: var(--umu-color-text-tertiary, #999999);

// 中性色
$Grey1: #cccccc;
$Grey2: var(--umu-color-border, #e0e0e0);
$Grey3: var(--umu-color-fill-secondary, #eeeeee);
$Background: var(--umu-color-fill, #f5f5f5);
$ZebraStriping: #fcfcfc;
$White: var(--umu-color-bg-base, #ffffff);
`;
fileContent = fileContent
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/\/\/.*$/gm, "");

const colorRegex = /\$([A-Za-z][A-Za-z0-9]*)\s*:\s*([^;]+);/g;

const colorMap: Record<string, string> = {};

let match;
while ((match = colorRegex.exec(fileContent)) !== null) {
const key = match[1].trim();
const value = match[2].trim();

colorMap[key] = value;
}

return colorMap;
} catch (error) {
console.error("读取 color var 时发生错误:", error);
return {};
}
};

export const colorVars = Object.assign({}, getColorVars(), {
Transparent: "transparent",
});

export const colorKeys = Object.keys(colorVars);
export const commonColors = [
...colorKeys,
"fff",
"eee",
"e0e0e0",
"f5f5f5",
"ddd",
"ccc",
"bbb",
"aaa",
"222",
"000",
];

export const getColor = (value: string) => {
if (colorVars[value]) {
return colorVars[value];
}
return /^[0-9a-zA-Z]+$/.test(value) ? `#${value}` : value;
};

const boxSizeValue = [
"border-box",
"content-box",
"clip",
"visible",
"scroll",
].join("|");

export const preset1: Preset = {
name: "preset1",
rules: [
[
/^w-([.\d]+)$/,
([_, num]) => ({ width: getSizeValue(num) }),
{ autocomplete: "w-<num>" },
],
[
/^max-w-([.\d]+)$/,
([, value]) => ({ "max-width": getSizeValue(value) }),
{ autocomplete: "max-w-<num>" },
],
[
/^min-w-([.\d]+)$/,
([, value]) => ({ "min-width": getSizeValue(value) }),
{ autocomplete: "min-w-<num>" },
],
[
/^h-([.\d]+)$/,
([, value]) => ({ height: getSizeValue(value) }),
{ autocomplete: "h-<num>" },
],
[
/^max-h-([.\d]+)$/,
([, value]) => ({ "max-height": getSizeValue(value) }),
{ autocomplete: "max-h-<num>" },
],
[
/^min-h-([.\d]+)$/,
([, value]) => ({ "min-height": getSizeValue(value) }),
{ autocomplete: "min-h-<num>" },
],
[
new RegExp(`^(?:box)-(${boxSizeValue})$`),
([, v]) => (boxSizeValue.includes(v) ? { "box-sizing": v } : undefined),
{
autocomplete: [`(box)-(${boxSizeValue})`],
},
],
[
/^m-([\d]+)$/,
([, value]) => ({ margin: getSizeValue(value) }),
{ autocomplete: "m-<num>" },
],
[
/^m-t-([\d]+)$/,
([, value]) => ({ "margin-top": getSizeValue(value) }),
{ autocomplete: "m-t-<num>" },
],
[
/^m-r-([\d]+)$/,
([, value]) => ({ "margin-right": getSizeValue(value) }),
{ autocomplete: "m-r-<num>" },
],
[
/^m-b-([\d]+)$/,
([, value]) => ({ "margin-bottom": getSizeValue(value) }),
{ autocomplete: "m-b-<num>" },
],
[
/^m-l-([\d]+)$/,
([, value]) => ({ "margin-left": getSizeValue(value) }),
{ autocomplete: "m-l-<num>" },
],
[
/^p-([\d]+)$/,
([, value]) => ({ padding: getSizeValue(value) }),
{ autocomplete: "p-<num>" },
],
[
/^p-t-([\d]+)$/,
([, value]) => ({ "padding-top": getSizeValue(value) }),
{ autocomplete: "p-t-<num>" },
],
[
/^p-r-([\d]+)$/,
([, value]) => ({ "padding-right": getSizeValue(value) }),
{ autocomplete: "p-r-<num>" },
],
[
/^p-b-([\d]+)$/,
([, value]) => ({ "padding-bottom": getSizeValue(value) }),
{ autocomplete: "p-b-<num>" },
],
[
/^p-l-([\d]+)$/,
([, value]) => ({ "padding-left": getSizeValue(value) }),
{ autocomplete: "p-l-<num>" },
],
[
/^font-size-([\d]+)$/,
([, value]) => ({ "font-size": getSizeValue(value) }),
],
[/^font-bold$/, () => ({ "font-weight": "550" })],
[/^bold$/, () => ({ "font-weight": "550" })],
[/^weight-([\d]+)$/, ([, value]) => ({ "font-weight": value })],
[
/^line-height-([0-9].*)$/,
([, value]) => ({ "line-height": getSizeValue(value) }),
],
[
/^color-(.*)$/,
([, c], { theme }) => ({
color: (theme as any).colors[c] || getColor(c),
}),
{
autocomplete: [`color-(${commonColors.join("|")})`],
},
],
[
/^bg-color-(.*)$/,
([, c], { theme }) => ({
"background-color": (theme as any).colors[c] || getColor(c),
}),
{
autocomplete: [`bg-color-(${commonColors.join("|")})`],
},
],
[
/^overflow-(hidden|auto|scroll|visible)$/,
([_, value]) => ({ overflow: value }),
],
[
/^overflow-x-(hidden|auto|scroll|visible)$/,
([_, value]) => ({ "overflow-x": value }),
],
[
/^overflow-y-(hidden|auto|scroll|visible)$/,
([_, value]) => ({ "overflow-y": value }),
],
[
/^-webkit-line-clamp-(\d+)$/,
([_, lines]) => ({ "-webkit-line-clamp": lines }),
],
[
/^-webkit-box-orient-(vertical)/,
([_, type]) => ({ "-webkit-box-orient": type }),
],
[
/^text-overflow-(clip|ellipsis)/,
([_, type]) => ({ "text-overflow": type }),
],
[
/^display-(-webkit-box|-webkit-inline-box)/,
([_, type]) => ({ display: type }),
],
],
variants: [],
theme: {
colors: colorVars,
},
shortcuts: [
[
/^text-ellipsis-(\d+)$/,
([_, lines]) => [
"overflow-hidden",
"display--webkit-box",
"text-overflow-ellipsis",
`-webkit-line-clamp-${lines}`,
"-webkit-box-orient-vertical",
],
],
[
/^text-ellipsis-(\d+)-inline$/,
([_, lines]) => [
"overflow-hidden",
"text-overflow-ellipsis",
"display--webkit-inline-box",
`-webkit-line-clamp-${lines}`,
"-webkit-box-orient-vertical",
],
],
],
};

3.2 definitions

definitions/style.d.ts

declare module "*.css" {
const content: { [className: string]: string };
export default content;
}

declare module "*.scss" {
const content: { [className: string]: string };
export default content;
}

declare module "*.less" {
const content: { [className: string]: string };
export default content;
}

declare module "*.module.css" {
const content: { [className: string]: string };
export default content;
}

declare module "*.module.scss" {
const content: { [className: string]: string };
export default content;
}

declare module "*.module.less" {
const content: { [className: string]: string };
export default content;
}

3.3 package.json

"scripts": {
"build": "cross-env NODE_ENV=production webpack --config ./webpack.config.js",
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}' --quiet"
}

3.4 tsconfig.json

{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"noEmit": true,
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*", "definitions/**/*"],
"exclude": ["node_modules", "dist"]
}

3.5 uno.config.ts

import { defineConfig } from 'unocss'
import { preset1 } from './unocss/preset'

export default defineConfig({
presets: [
preset1,
],
transformers: [
],
})

3.6 webpack.config.ts

const Path = require("path");
const { merge } = require("webpack-merge");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

const devMode = process.env.NODE_ENV !== "production";

const combineCssLoader = (params = {}) => {
const { modules = false, postcss = true, devMode = true } = params;

const baseLoaders = [
devMode ? "style-loader" : MiniCssExtractPlugin.loader,
{
loader: "css-loader",
options: {
modules: modules ? { namedExport: false } : false,
},
},
];

if (postcss) {
baseLoaders.push("postcss-loader");
}

return baseLoaders;
};

const cssRules = [
{
test: /\.css$/,
include: [Path.resolve(__dirname, "src")],
exclude: [/\.module\.css$/, /node_modules/],
use: [...combineCssLoader({ devMode, modules: false, postcss: true })],
},
{
test: /\.module\.css$/,
exclude: [/node_modules/],
include: [Path.resolve(__dirname, "src")],
use: [...combineCssLoader({ devMode, modules: true, postcss: true })],
},
];

const sassRules = [
{
test: /\.(sa|sc)ss$/,
include: [Path.resolve(__dirname, "src")],
exclude: [/\.module\.(sa|sc)ss$/, /node_modules/],
use: [
...combineCssLoader({ devMode, modules: false, postcss: true }),
"sass-loader",
],
},
{
exclude: [/node_modules/],
test: /\.module\.(sa|sc)ss$/,
include: [Path.resolve(__dirname, "src")],
use: [
...combineCssLoader({ devMode, modules: true, postcss: true }),
"sass-loader",
],
},
];

const lessRules = [
{
test: /\.less$/,
include: [Path.resolve(__dirname, "src")],
exclude: [/\.module\.less$/, /node_modules/],
use: [
...combineCssLoader({ devMode, modules: false, postcss: true }),
"less-loader",
],
},
{
test: /\.module\.less$/,
exclude: [/node_modules/],
include: [Path.resolve(__dirname, "src")],
use: [
...combineCssLoader({ devMode, modules: true, postcss: true }),
"less-loader",
],
},
];

const unoCssRules = [
{
test: /\.css$/i,
include: Path.resolve(process.cwd(), "_virtual_%2F__uno.css"),
use: [...combineCssLoader({ devMode, modules: false, postcss: false })],
},
];

const config = {
entry: {
main: Path.resolve(process.cwd(), "src/index.tsx"),
},
output: {
clean: true,
path: Path.resolve(process.cwd(), "dist"),
filename: devMode ? "[name]/[name].js" : "[name]/[name].[contenthash:8].js",
chunkFilename: devMode
? "[name]/[name].js"
: "[name]/[name].[contenthash:8].js",
},
resolve: {
extensions: [".js", ".jsx", ".ts", ".tsx"],
alias: {
"@": Path.resolve(__dirname, "../", "src"),
},
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: "swc-loader",
options: {
jsc: {
parser: {
syntax: "typescript",
tsx: true,
dynamicImport: true,
},
transform: {
react: {
runtime: "automatic",
},
},
},
},
},
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
modules: "commonjs", // 转换为 CommonJS 模块
targets: "defaults", // 可以根据项目的目标环境调整
},
],
],
},
},
},
{
type: "asset",
test: /\.(png|jpg|jpeg|gif|svg)$/,
parser: {
dataUrlCondition: {
maxSize: 1 * 1024,
},
},
generator: {
filename: "images/[name].[hash:8].[ext]",
},
},
{
type: "asset",
test: /\.(woff|woff2|eot|ttf|otf)$/,
parser: {
dataUrlCondition: {
maxSize: 1 * 1024,
},
},
generator: {
filename: "fonts/[name].[hash:8].[ext]",
},
},
...cssRules,
...sassRules,
...lessRules,
...unoCssRules,
],
},
plugins: [
new HtmlWebpackPlugin({
filename: "index.html",
template: Path.resolve(process.cwd(), "public", `index.html`),
}),
...(devMode
? []
: [
new MiniCssExtractPlugin({
filename: "[name]/[name].[contenthash:8].css",
chunkFilename: "[name]/[name].[contenthash:8].css",
}),
]),
],
};

module.exports = async function () {
const module = await import("@unocss/webpack");
const { default: UnoCSS } = module;

return merge(config, {
plugins: [UnoCSS(Path.resolve(process.cwd(), "./uno.config.ts"))],
optimization: {
realContentHash: true,
},
});
};

四、使用


import "uno.css";
import "./App.css";
import "./App.scss";
import city1 from "./images/city1.png";
import AppStyle from "./App.module.scss";

function App() {
return (
<div className="app">
App 页面
<div className={AppStyle.div1}></div>
<img src={city1} />
<div className="m-1 color-SubColor">嘻嘻哈哈</div>
<div className="p-2 color-Text2 bg-BrandDark">哈哈嘻嘻</div>
<div className="box"></div>
<div className="text-ellipsis-2 w-100">
敷设电缆;范德萨范德萨;林凤娇了;附件都说了;就发了;三等奖发了发;啦束带结发;拉数据;
发;苏妲己;啊;发的酸;拉法基;了发的酸;浪费啊;减肥的;是佛i额文峰街道舒服了扩大升级
</div>
<span className="text-ellipsis-2-inline w-100">
敷设电缆;范德萨范德萨;林凤娇了;附件都说了;就发了;三等奖发了发;啦束带结发;拉数据;
发;苏妲己;啊;发的酸;拉法基;了发的酸;浪费啊;减肥的;是佛i额文峰街道舒服了扩大升级
</span>
</div>
);
}

export default App;

五、高阶 主题化


5.1 理论

1. 自定义预设中, 增加 theme.colors 配置: 配置如下:

theme: {
colors: {
BrandNormal: var(--umu-color-primary, #fab400),
BrandDark: var(--umu-color-primary-active, #eaa900),
BrandFocus: var(--umu-gold-3, rgba(250, 180, 0, 0.24)),
BrandLight: var(--umu-gold-2, rgba(250, 180, 0, 0.08)),
BrandEnhanced: var(--umu-color-primary-hover, #f2af2e),
}
}

2. 自定义预设中, rules 中使用主题: 配置如下:

[
/^color-(.*)$/,
([, c], { theme }) => ({
color: (theme as any).colors[c],
}),
{
autocomplete: [`color-(${commonColors.join("|")})`],
}
],
[
/^bg-color-(.*)$/,
([, c], { theme }) => ({
"background-color": (theme as any).colors[c],
}),
{
autocomplete: [`bg-color-(${commonColors.join("|")})`],
}
],

3. 开发中, 使用主题预设规则:

<div className="p-2 color-Text2 bg-BrandDark">哈哈嘻嘻</div>

5.2 实践

my-preset.ts

import { Preset } from "unocss";

const getColorVars = () => {
try {
let fileContent = `
// 功能色
$BrandLight: var(--umu-gold-2, rgba(250, 180, 0, 0.08));
$BrandFocus: var(--umu-gold-3, rgba(250, 180, 0, 0.24));
$BrandNormal: var(--umu-color-primary, #fab400);
$BrandEnhanced: var(--umu-color-primary-hover, #f2af2e);
$BrandDark: var(--umu-color-primary-active, #eaa900);
$BrandRangeNormal: #fff8e5;
$BrandRangeBorderNormal: #ffe296;

$SubLightHover: rgba(30, 110, 230, 0.08);
$SubLightFocus: rgba(30, 110, 230, 0.18);
$SubFocus: var(--umu-color-link-hover, #4a93ff);
$SubHover: #297dfc;
$SubColor: var(--umu-color-link, #1e6ee6);
$SubRangeColor: #e8f0fd;
$SubRangeBorderColor: #c3daff;
$Warning: #ff860c;
$Error: #dd4e40;
$Success: #21a564;

// 文字色
$Text1: var(--umu-color-text, #222222);
$Text2: var(--umu-color-text-secondary, #666666);
$Text3: var(--umu-color-text-tertiary, #999999);

// 中性色
$Grey1: #cccccc;
$Grey2: var(--umu-color-border, #e0e0e0);
$Grey3: var(--umu-color-fill-secondary, #eeeeee);
$Background: var(--umu-color-fill, #f5f5f5);
$ZebraStriping: #fcfcfc;
$White: var(--umu-color-bg-base, #ffffff);
`;
fileContent = fileContent
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/\/\/.*$/gm, "");

const colorRegex = /\$([A-Za-z][A-Za-z0-9]*)\s*:\s*([^;]+);/g;

const colorMap: Record<string, string> = {};

let match;
while ((match = colorRegex.exec(fileContent)) !== null) {
const key = match[1].trim();
const value = match[2].trim();

colorMap[key] = value;
}

return colorMap;
} catch (error) {
console.error("读取 color var 时发生错误:", error);
return {};
}
};

export const colorVars = Object.assign({}, getColorVars(), {
Transparent: "transparent",
});

export const colorKeys = Object.keys(colorVars);
export const commonColors = [
...colorKeys,
"fff",
"eee",
"e0e0e0",
"f5f5f5",
"ddd",
"ccc",
"bbb",
"aaa",
"222",
"000",
];

export const getColor = (value: string) => {
if (colorVars[value]) {
return colorVars[value];
}
return /^[0-9a-zA-Z]+$/.test(value) ? `#${value}` : value;
};

export const preset1: Preset = {
name: "preset1",
rules: [
[
/^color-(.*)$/,
([, c], { theme }) => ({
color: (theme as any).colors[c] || getColor(c),
}),
{
autocomplete: [`color-(${commonColors.join("|")})`],
},
],
[
/^bg-color-(.*)$/,
([, c], { theme }) => ({
"background-color": (theme as any).colors[c] || getColor(c),
}),
{
autocomplete: [`bg-color-(${commonColors.join("|")})`],
},
],
],
theme: {
colors: colorVars,
},
};

六、高阶 Px To Rem


6.1 理论

可以实现一个 getSizeValue 函数,当匹配到 widthheight 等设置大小的时候,调用 getSizeValue。 在 getSizeValue 实现 px 转化为 rem 的逻辑。

6.2 实践

my-preset.ts

import { Preset } from "unocss";

const getSizeValue = (value: string): string => {
if (!value) {
return "";
}
if (value.includes("-")) {
const values = value.split("-");
return values.map(getSizeValue).join(" ");
}
if (isNaN(Number(value))) {
return value;
}

const remSize = parseFloat(value) / 100;
return remSize ? `${remSize}rem` : "0";
};

const boxSizeValue = [
"border-box",
"content-box",
"clip",
"visible",
"scroll",
].join("|");

export const preset1: Preset = {
name: "preset1",
rules: [
[
/^w-([.\d]+)$/,
([_, num]) => ({ width: getSizeValue(num) }),
{ autocomplete: "w-<num>" },
],
[
/^max-w-([.\d]+)$/,
([, value]) => ({ "max-width": getSizeValue(value) }),
{ autocomplete: "max-w-<num>" },
],
[
/^min-w-([.\d]+)$/,
([, value]) => ({ "min-width": getSizeValue(value) }),
{ autocomplete: "min-w-<num>" },
],
[
/^h-([.\d]+)$/,
([, value]) => ({ height: getSizeValue(value) }),
{ autocomplete: "h-<num>" },
],
[
/^max-h-([.\d]+)$/,
([, value]) => ({ "max-height": getSizeValue(value) }),
{ autocomplete: "max-h-<num>" },
],
[
/^min-h-([.\d]+)$/,
([, value]) => ({ "min-height": getSizeValue(value) }),
{ autocomplete: "min-h-<num>" },
],
[
new RegExp(`^(?:box)-(${boxSizeValue})$`),
([, v]) => (boxSizeValue.includes(v) ? { "box-sizing": v } : undefined),
{
autocomplete: [`(box)-(${boxSizeValue})`],
},
],
[
/^m-([\d]+)$/,
([, value]) => ({ margin: getSizeValue(value) }),
{ autocomplete: "m-<num>" },
],
[
/^m-t-([\d]+)$/,
([, value]) => ({ "margin-top": getSizeValue(value) }),
{ autocomplete: "m-t-<num>" },
],
[
/^m-r-([\d]+)$/,
([, value]) => ({ "margin-right": getSizeValue(value) }),
{ autocomplete: "m-r-<num>" },
],
[
/^m-b-([\d]+)$/,
([, value]) => ({ "margin-bottom": getSizeValue(value) }),
{ autocomplete: "m-b-<num>" },
],
[
/^m-l-([\d]+)$/,
([, value]) => ({ "margin-left": getSizeValue(value) }),
{ autocomplete: "m-l-<num>" },
],
[
/^p-([\d]+)$/,
([, value]) => ({ padding: getSizeValue(value) }),
{ autocomplete: "p-<num>" },
],
[
/^p-t-([\d]+)$/,
([, value]) => ({ "padding-top": getSizeValue(value) }),
{ autocomplete: "p-t-<num>" },
],
[
/^p-r-([\d]+)$/,
([, value]) => ({ "padding-right": getSizeValue(value) }),
{ autocomplete: "p-r-<num>" },
],
[
/^p-b-([\d]+)$/,
([, value]) => ({ "padding-bottom": getSizeValue(value) }),
{ autocomplete: "p-b-<num>" },
],
[
/^p-l-([\d]+)$/,
([, value]) => ({ "padding-left": getSizeValue(value) }),
{ autocomplete: "p-l-<num>" },
],
[
/^font-size-([\d]+)$/,
([, value]) => ({ "font-size": getSizeValue(value) }),
],
[/^font-bold$/, () => ({ "font-weight": "550" })],
[/^bold$/, () => ({ "font-weight": "550" })],
[/^weight-([\d]+)$/, ([, value]) => ({ "font-weight": value })],
[
/^line-height-([0-9].*)$/,
([, value]) => ({ "line-height": getSizeValue(value) }),
],
]
};