Webpack UnoCSS
一、认识
基于 Webpack
, 我们支持 Css
以及 Scss
、Less
等预处理器还有 UnoCSS
原子样式。配置策略如下:
1. 插入/或者分离 Css
文件: 为了提高开发效率,Webpack
在开发模式下会启用热重载(Hot Module Replacement
)和快速构建,CSS
会通过 style-loader
注入到页面中,而不是单独提取成文件,这样可以避免每次修改都重新加载整个 CSS
文件。在生产环境下,为了提升性能和缓存效果,CSS
会被提取成独立的文件,使用 MiniCssExtractPlugin
插件来实现。可以减小主产物体积, 也可以可以避免重复加载 CSS
,提高页面的加载速度。MiniCssExtractPlugin
同时提供了 loader
, 将这个 loader
放在最前面即可。MiniCssExtractPlugin
可以配置 filename
和 chunkFilename
,可以自定义 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. Sass
、Less
预处理 Css
: 使用 sass-loader
、less-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-loader
和 style-loader
处理。在 webpack.config.js
文件中引入 @unocss/webpack
以 Webpack Plugin
的形式来使用, 可以传入 UnoCss
的配置文件地址,默认是从根目录查找。
-
Webpack
配置: 使用@unocss/webpack
插件, 可以传入uno.config.ts
的文件路径,默认是从项目根目录寻找。通过css-loader
、style-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
: RsPack
跟 Webpack
的引入方式几乎类似,@unocss/webpack
中提供了 @unocss/webpack/rspack
插件, 其他处理是一样的。
1.2 为什么要引入 UnoCss?
一、为什么要引入 UnoCss
: 在我们项目中, Css
样式文件的痛点如下:
-
Css
公共样式复用率很低 -
没有
Tree Shaking
机制,没有用到的样式也会打包(Css Module
已经支持Tree Shaking
) -
各位伙伴在各个项目中已经有意识的自己写了一些预设, 然后使用在
Sass
中使用@extend
或者@mixin/@include
来使用已经定义好的预设。但是有局限性, 其他伙伴并不知道。
因此, 我们引入了 UnoCss
原子化样式方案, 使用工程化的方式来提供预设的公共原子化样式, 并以内联的形式写入。UnoCss
并提供了 VsCode
插件,可以很好的提示我们定义好的 UnoCss
预设。
1.3 UnoCss Vs Tailwind?
Css
原子化方案其实有很多, 关注比较多的有 Tailwind
和 UnoCss
,他们都支持按需生成 Css
,都提供响应式,可以定制预设,并支持主题化和暗盒模式。但是 Tailwind
是重量级的,更注重定义系统,它提供了全面的预设样式,大部分在我们项目中是用不到的。强约束比较多,配置项也比较多,另外 Tailwind
会经过 PostCss
, 本质上 Tailwind
是 Postcss
插件。而我们的 UnoCss
非常灵活,通过在项目根目录下使用 JavaScript
灵活定义规则, 可以使用正则和函数,还有更加复杂的逻辑和条件判断。另外多个规则也可以组合成一个快捷键。UnoCSS
是一个同构引擎, 可以灵活的在不同的地方使用, 可以作为 Webpack Plugin
动态生成 Css
文件。只依赖于 css-loader
和 style-loader
或者 MiniCssExtractPlugin
等。UnoCss
直接操作字符串,不会转换成 AST
,也不需要在内存中保存 AST
结构,所以 UnoCss
会比 Tailwind
构建编译更快,而且在多次构建之间复用生成的样式,增量编译很快。
1.4 UnoCss 原理、工作机制?
UnoCss
工作机制:
-
类名扫描(类名提取):
UnoCSS
在构建时通过扫描项目中的代码(HTML
、JS
、TS
、Vue
、React
等),提取出所有实际使用到的CSS
类名。UnoCSS
会扫描你项目中的源代码,识别出所有的类名,并构建出一个类名-样式的映射。 -
样式生成: 根据扫描到的类名,
UnoCSS
会从预定义的规则中生成最小的CSS
文件。生成的CSS
只包含被实际使用的类和属性,避免了传统CSS
框架中的冗余样式。UnoCSS
通过一套精密的规则生成最小化的样式表,这意味着不需要生成所有可能的类,只生成实际用到的类。 -
即时生成(按需生成):
UnoCSS
是一个按需生成的CSS
框架。即使项目中存在很多潜在的CSS
类,UnoCSS
只生成当前项目中被实际使用的类的CSS
。这个特性保证了最终的输出非常精简。 -
不依赖
AST
转换:UnoCSS
的设计不依赖于传统的AST
(抽象语法树)转换。AST
转换通常是在工具(如PostCSS
、Babel
等)中对源码进行语法解析和抽象,进而进行优化、压缩等处理。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
函数,当匹配到 width
、height
等设置大小的时候,调用 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) }),
],
]
};