在日常前端开发中,我们经常要面对这样的场景:
- 一个组件有几十个颜色、间距、大小变量要维护 💥
- 需要支持多套主题(比如亮色 / 暗色 / 蓝色系) 🌗
- CSS 变量 + class 规则要保持同步,否则改起来很痛苦 😭
如果你还在手写 .btn:hover { ... }
这样的代码,那就太浪费啦。
今天我们来看看,如何用 Less 的 Map + 函数 ,配合 .generate-css
和 .generate-css-vars
,实现一套 全自动的主题生成体系。
🎨 核心代码
less
.is-magic(@key) when (default()) {
@is-magic: if(
(@key = __pc__) or (@key = __dot__) or (@key = __nest__) or (@key = __pe__) or
(@key = __rename__) or (@key = __free__) or (@key = __custom__) or (@key = __important__) or
(@key = __target__) or (@key = __space__),
true,
false
);
}
.process-magic(@is-magic, @key) when (default()) {
@magic-key: if(@is-magic, replace(replace(@key, '^__', ''), '__$', ''), '');
}
.process-css-var() when (default()) {
// 判断是否规则集
@is-ruleset: isruleset(@value);
// 只在规则集时清理 @ 符号,否则保留原 key
@new-key: if(@is-ruleset, replace(@key, '^@', ''), @key);
// 判断是否 magic 属性
.is-magic(@new-key);
// 处理 magic 属性,以便添加到 var-path 中
.process-magic(@is-magic, @new-key);
// 统一处理变量路径
@var-path: if(
@css-var = '',
if(@is-magic, ~'@{magic-key}', ~'@{new-key}'),
if(@is-magic, ~'@{css-var}@{conjunction}@{magic-key}', ~'@{css-var}@{conjunction}@{new-key}')
);
}
.generate-css-vars(@map, @css-var: '', @conjunction: '-') when (default()) {
each(@map, .(@value, @key) {
.process-css-var();
// 根据是否规则集调用
& when (@is-ruleset) {
.generate-css-vars(@value, @var-path, @conjunction);
}
& when not (@is-ruleset) and not (@is-magic) {
--@{var-path}: @value;
}
});
}
// name-map 的嵌套规则集名不能和 prefix 引用的变量一致,不然会导致循环引用
.generate-css(@map, @prefix: '', @conjunction: '-', @name-map: '', @css-var: @prefix, @no-conjunction: false, @is-rename: false, @is-important: false)
when
(default()) {
.@{prefix} when not (@prefix = '') {
each(@map, .(@value, @key) {
.process-css-var();
@important-suffix: if(@is-important = true, ' !important', '');
& when not (@is-ruleset) and not (@is-magic) {
@{new-key}: ~"var(--@{var-path})@{important-suffix}";
}
});
}
each(@map, .(@value, @key, @index) {
.process-css-var();
& when (@is-ruleset) {
@content: if(@new-key = __pc__, ':',
if(@new-key = __pe__, '::',
if(@new-key = __dot__, '.',
if(@new-key = __nest__, ' .',
if(@new-key = __free__, '',
if(@new-key = __space__, ' ',
if(@new-key = __rename__, '',
if(@new-key = __important__, '',
if(@new-key = __custom__, @value[__target__], // 自定义的处理直接交给 __target__ 属性, @__custom__ 规则集下必须要有 __target__ 属性
if(@is-rename = true, if(isruleset(@name-map), @name-map[@@new-key], ~"@{new-key}"), // rename 的判断需要在 no-conjunction 前,因 __rename__ 也为 magic
if(@no-conjunction = true, ~'@{new-key}',
~"@{conjunction}@{new-key}")))))))))));
@need-rename: if(@new-key = __rename__, true, false);
@add-important: if(@new-key = __important__, true, false);
.generate-css(@value, ~"@{prefix}@{content}", @conjunction, @name-map, @var-path, @is-magic, @need-rename, @add-important);
}
});
}
🧩 基础思路
-
所有组件样式都用 Map 来维护:
less@button-blue: { color: #fff; background: #333; @__pc__: { @hover: { background: #444; }; @focus: { background: #555; }; }; };
✅ 优点:不用重复写 class,所有样式都集中在一个对象里。
-
统一生成 CSS 变量
less:root { .generate-css-vars(@button-blue, btn); }
会输出:
css:root { --btn-color: #fff; --btn-background: #333; --btn-hover-background: #444; --btn-focus-background: #555; }
✨ 每个属性都变成了
--var
,支持动态主题切换。
-
统一生成 CSS 规则
less.generate-css(@button-blue, btn);
会输出:
css.btn { color: var(--btn-color); background: var(--btn-background); } .btn:hover { background: var(--btn-hover-background); } .btn:focus { background: var(--btn-focus-background); }
🔥 这一步帮你省去了大量重复的
.btn { ... }
手写代码。
🪄 Magic Key 的秘密
这里的关键点是 @__pc__
、@__pe__
、@__dot__
等一组特殊 key(我称它们为 magic key)。
Magic Key | 作用 |
---|---|
@__pc__ |
: 伪类(:hover, :focus ...) |
@__pe__ |
:: 伪元素(::before, ::after ...) |
@__dot__ |
. 子类 |
@__nest__ |
. 嵌套 |
@__important__ |
自动加 !important |
@__custom__ |
自定义生成规则 |
... | ... |
比如 @__pc__
:
less
@button-blue: {
@__pc__: {
@hover: { color: red; };
};
};
生成:
css
.btn:hover {
color: var(--btn-hover-color);
}
是不是感觉像写 JSON 一样方便? 🤩
🛠️ 最终效果
在入口文件中只要写一次:
less
:root {
.generate-css-vars(@button-blue, btn);
}
.generate-css(@button-blue, btn);
就可以得到:
- 一整套
:root
下的 CSS 变量 🌈 - 一整套完整的
.btn
样式规则 ⚡
而且要切换主题,只需要在 @button-dark
、@button-light
里改变量,然后重新调用 generate-css-vars
即可。
看到这里你是不是感觉:
👉 「哇塞,这不就是一个 mini Tailwind + CSS Vars 引擎吗?」
没错,它就是一套基于 Less 的「样式编译器」。
下面就到了我们喜闻乐见的实战环节:
⚛️ 如何在 React 中使用多主题
有了上面生成的 CSS 变量和规则,我们只需要在 React 里切换 className,就能实现动态换肤。
1️⃣ 定义两套主题
less
// light 主题
@theme-light: {
button: {
color: #333;
background: #fff;
@__pc__: {
@hover: { background: #f5f5f5; };
};
};
};
// dark 主题
@theme-dark: {
button: {
color: #fff;
background: #333;
@__pc__: {
@hover: { background: #444; };
};
};
};
// 生成变量 & 规则
:root.light {
.generate-css-vars(@theme-light, btn);
}
:root.dark {
.generate-css-vars(@theme-dark, btn);
}
.generate-css(@theme-light, btn);
.generate-css(@theme-dark, btn);
编译后会得到两套 .light
和 .dark
下的 --btn-*
变量。
2️⃣ React 组件
javascript
import React, { useState, useEffect } from "react";
export default function App() {
const [theme, setTheme] = useState<"light" | "dark">("light");
useEffect(() => {
// 切换主题 class
document.documentElement.classList.remove("light", "dark");
document.documentElement.classList.add(theme);
}, [theme]);
return (
<div style={{ padding: "20px" }}>
<h1>🎨 多主题 Demo</h1>
<button className="btn" style={{ marginRight: "10px" }}>
按钮
</button>
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
切换主题(当前 {theme} 🌗)
</button>
</div>
);
}
3️⃣ 效果
- 默认
light
模式:白色背景按钮 🕊️ - 点击切换 → 自动应用
dark
主题:黑色背景按钮 🌌
只需要 切换根节点的 className,就能完成整站的主题切换,非常优雅! ✨
🚀 优势总结
- 解耦样式与 class:开发者只需维护 Map,不需要重复写 class。
- 天然支持 CSS 变量:方便主题切换、运行时修改。
- 结构化管理:用 Map 层级清晰表达伪类/伪元素/嵌套规则。
- 高可维护性:新增一个状态只要加到 Map 里,自动生成对应规则。
📦 应用场景
- 🏗️ 组件库主题系统(Button、Input、Modal ...)
- 🌗 多主题切换(light/dark/blue ...)
- ⚙️ 配置化 UI 引擎(用户可以在后台配置主题)
🎯 结语
多主题的最佳实践 = CSS 变量 + 自动化变量生成 + 组件化样式依赖 var() 。
本文介绍的 Less 工具代码,本质上是一个 DSL(领域专用语言) ,通过抽象和规则生成,解决了传统多主题实现中 变量管理混乱、扩展性差 的痛点。
在团队开发中,这样的方案可以大大降低样式维护成本,同时让多主题真正做到 即插即用。