🎨 前端多主题最佳实践:用 Less Map + generate-css 打造自动化主题系统

在日常前端开发中,我们经常要面对这样的场景:

  • 一个组件有几十个颜色、间距、大小变量要维护 💥
  • 需要支持多套主题(比如亮色 / 暗色 / 蓝色系) 🌗
  • 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);
    }
  });
}

🧩 基础思路

  1. 所有组件样式都用 Map 来维护

    less 复制代码
    @button-blue: {
      color: #fff;
      background: #333;
      @__pc__: {
        @hover: {
          background: #444;
        };
        @focus: {
          background: #555;
        };
      };
    };

    ✅ 优点:不用重复写 class,所有样式都集中在一个对象里。


  1. 统一生成 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,支持动态主题切换。


  1. 统一生成 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,就能完成整站的主题切换,非常优雅! ✨


🚀 优势总结

  1. 解耦样式与 class:开发者只需维护 Map,不需要重复写 class。
  2. 天然支持 CSS 变量:方便主题切换、运行时修改。
  3. 结构化管理:用 Map 层级清晰表达伪类/伪元素/嵌套规则。
  4. 高可维护性:新增一个状态只要加到 Map 里,自动生成对应规则。

📦 应用场景

  • 🏗️ 组件库主题系统(Button、Input、Modal ...)
  • 🌗 多主题切换(light/dark/blue ...)
  • ⚙️ 配置化 UI 引擎(用户可以在后台配置主题)

🎯 结语

多主题的最佳实践 = CSS 变量 + 自动化变量生成 + 组件化样式依赖 var()

本文介绍的 Less 工具代码,本质上是一个 DSL(领域专用语言) ,通过抽象和规则生成,解决了传统多主题实现中 变量管理混乱、扩展性差 的痛点。

在团队开发中,这样的方案可以大大降低样式维护成本,同时让多主题真正做到 即插即用

相关推荐
张人玉27 分钟前
XML 序列化与操作详解笔记
xml·前端·笔记
杨荧36 分钟前
基于Python的宠物服务管理系统 Python+Django+Vue.js
大数据·前端·vue.js·爬虫·python·信息可视化
YeeWang1 小时前
🎉 Eficy 让你的 Cherry Studio 直接生成可预览的 React 页面
前端·javascript
gnip1 小时前
Jenkins部署前端项目实战方案
前端·javascript·架构
Orange3015111 小时前
《深入源码理解webpack构建流程》
前端·javascript·webpack·typescript·node.js·es6
lovepenny2 小时前
Failed to resolve entry for package "js-demo-tools". The package may have ......
前端·npm
尚书2 小时前
全局核心状态 + 局部功能内聚模块化混合架构
架构
超凌2 小时前
threejs 创建了10w条THREE.Line,销毁数据,等待了10秒
前端
芒果1252 小时前
SVG图片通过img引入修改颜色
前端