跳到主要内容

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. 处理 uno.css/virtual:uno.css 虚拟模块: 基于 UnPlugin resolveId Hooks: 解析 uno.css/virtual:uno.css 虚拟模块 IDresolveId HooksWebpack 的实现逻辑为: 在解析配置 compiler.options.resolve.plugins 中插入一个虚拟模块解析插件, 这个虚拟模块解析插件在 resolve Hook(resolver.getHook("resolve")) 阶段, 判断模块解析请求是否符合虚拟模块规则, 比如 virtual:uno.css, 如果符合, 那么, 将这个虚拟模块转换为绝对路径, 比如 xx/xx/__virtual__uno.css, 并基于 finalInputFileSystem._writeVirtualFile 写入到虚拟空间。通过 resolver.doResolve 重定向虚拟模块解析请求为转换后的绝对路径。基于UnPlugin load Hooks: load(id) 用于解析 uno.css/virtual:uno.css 虚拟模块时动态插入 #--unocss--{layer:${layer};escape-view:\\"\\'\\\ PlaceHolder 占位符标记, 最终会打包到 Bundle 文件中。load HooksWebpack 的实现逻辑为: 向 compiler.options.module.rules.unshift Loader 配置的最前面插入用于处理虚拟模块的 Loader, 并将 enforce: "pre", 该 loader 应该在所有其他类型的 loader 之前执行,尤其是在标准 loader 运行之前。在其他 loader 处理之前识别虚拟模块。在这个 Loader 中可以插入自定义的虚拟模块内容。

  2. 类名扫描(类名提取): 在 compiler.hooks.compilation.tap 阶段中的 compilation.hooks.processAssets(Webpack 5、6)/compilation.hooks.optimizeAssets(Webpack 4) 阶段, 扫描、解析项目中的代码(HTMLJSTSVueReact 等),将所有代码通过空格分割为数组, 存储到 Tokens。在通过 uno.presets 预设配置, 寻找出匹配的类名, 并构建出一个类名-样式的映射。所以, 这一步也是 UnoCss 按需生成样式的关键。也是 UnoCss 底层不依赖 AST 树的关键点, UnoCss 直接操作字符串,不会转换成 AST,也不需要在内存中保存 AST 结构,所以 UnoCss 会比 Tailwind 构建编译更快,而且在多次构建之间复用生成的样式,增量编译很快。UnoCSS 的设计不依赖于传统的 AST(抽象语法树)转换。AST 转换通常是在工具(如 PostCSSBabel 等)中对源码进行语法解析和抽象,进而进行优化、压缩等处理。UnoCSS 不需要这一过程,因为它是基于类名匹配规则来生成样式的,而不是通过对整个 CSS 文件进行分析和转换。这使得 UnoCSS 具有非常高的性能,因为它可以直接根据类名生成规则,而不需要做复杂的 AST 操作。

  3. 样式生成、替换: 根据扫描到的类名,UnoCSS 会从预定义的规则中生成最小的 CSS 样式字符串。生成的 CSS 字符串只包含被实际使用的类和属性,避免了传统 CSS 框架中的冗余样式。UnoCSS 通过一套精密的规则生成最小化的样式表,这意味着不需要生成所有可能的类,只生成实际用到的类。将生成的最小的 Css 样式字符串替换之前已经在 compilation.assets[file].source().toString() 中的 PlaceHolder 虚拟模块占位符。最后, 通过 new WebpackSources.SourceMapSource(code, file, compilation.assets[file].map()) 更新 compilation.assets[file] 文件。 UnoCSS 是一个按需生成的 CSS 框架。即使项目中存在很多潜在的 CSS 类,UnoCSS只生成当前项目中被实际使用的类的CSS`。这个特性保证了最终的输出非常精简。

注意: 我们在 Webpack/Rspack 中通过 css-loader/style-loader 处理的是 uno.css/virtual.css 虚拟文件模块。uno.css/virtual.css 虚拟模块中仅有一句 PlaceHolder 标记占位符 #--unocss--{layer:${layer};escape-view:\\"\\'\\\css-loader/style-loader 作用就是将这个标记占位符打包到最终的输出文件中。让 compilation.assets[file].source().toString() 可以访问到。

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,
],
})

配置内容

import { defineConfig } from 'unocss'

export default defineConfig({
content: {
plain: [], // 从纯内联文本中提取
pipeline: [], // 从构建工具的转换管道中提取。例如 Vite 和 Webpack
filesystem: ["src/**/*.{tsx,html}"] // 从文件系统中提取内容, 使用 glob 模式指定只匹配 src 目录下的 .tsx 和 .html 文件

}
})

配置监听

import { defineConfig } from 'unocss'

export default defineConfig({
watch: true
})

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) }),
],
]
};