Web 主题切换与个性化定制

主题能力分两类:固定主题切换(开发者预先定义有限几套,如亮色、暗色、业务皮肤)和用户个性化定制(用户可输入任意色值,组合没有上限)。前者可以在构建期生成样式,后者必须在运行时改变量或覆盖 CSS。

固定主题切换(有限套主题)

主题集合由开发提前定义,用户只能在预设主题中选择。

Class 命名空间

核心思路:为不同主题生成带主题类名的样式规则;切换时,通过 JS 修改 body、根容器的 class,从而激活对应主题样式。

缺点:手写多套主题维护成本高、容易漏写样式,一般借助构建工具自动生成带命名空间的样式。

postcss-themes(推荐)

把主题颜色、尺寸等变量抽离独立主题文件,postcss-themes 会自动复制受变量影响的选择器,并追加主题命名空间前缀。

// postcss.config.js
module.exports = {
  plugins: {
    'postcss-themes': {
      // themes 可以为数组,配置多个主题
      themes: {
        filePath: 'red_theme.css'
      }
    },
    'postcss-css-variables': {},
  }
}
/* 默认主题 */
:root {
  --main-color: green;
}
.foo {
  font-size: 16px;
  color: var(--main-color);
}
/* red_theme.css */
:root {
  --main-color: red;
}

构建输出结果:

.foo {
  font-size: 16px;
  color: green;
}

/* 新增带命名空间的主题样式 */
.red_theme .foo {
  color: red;
}

JS 切换主题:

// 给body追加主题class
document.body.classList.add('red_theme');

// 切换其他主题时需要移除旧主题class
document.body.classList.remove('red_theme');

Less Mixin 混合

利用 Less 的 mixin 混入能力,封装一套通用主题规则,传入不同参数生成多套主题。

缺点:需要业务手动把所有主题相关样式收拢到 mixin 内部,业务规模大时维护成本很高。

/* variables.less */
.base_theme(@background: #e2e9f9, @color: #337ab7){
  .class_1 {
    background: @background;
  }
  .class_1 a:hover{
    color: saturate(@color, 20%);
  }
  .class_2 {
    color: @color;
  }
}
@import 'variables.less';
.default_theme {
  .base_theme();
}
.coffee_theme {
  .base_theme(#f5e2c9, #9f754d);
}
.purple_theme {
  .base_theme(#e5d3ed, #b88bcb);
}

多 CSS 独立文件

每个主题单独编译输出一份完整 CSS,运行时通过 JS 控制样式表的加载、启用,切换不同皮肤。

核心问题:webpack + mini-css-extract-plugin 不支持单份源码直接多次实例化输出多份 CSS

生成多份CSS主题文件

多次执行webpack编译,每次通过alias替换主题变量源文件,分别输出对应主题CSS产物,JS源码保持共用。

// webpack.prod.conf.js
const path = require('path');
module.exports = function(theme) {
  return {
    plugins: [
      new MiniCssExtractPlugin({
        filename: `[name]_${theme}.css`,
      }),
    ],
    resolve: {
      alias: {
        themeVars: path.join(__dirname, `../src/style/themes/theme-${theme}.css`),
      }
    }
  }
}

less 中引用主题变量:

@import "~themeVars";

脚本批量触发多主题构建:

const webpack = require('webpack');
const webpackConfig = require('./webpack.prod.conf');

['default', 'broker'].forEach(theme => {
  webpack(webpackConfig(theme), (err, stats) => {
    if(err) console.error(err);
  })
})

JS 改变 link 标签的 href 属性。

缺点:浏览器会重新请求 CSS 资源,存在网络加载延迟,切换有卡顿感;生产环境 CSS 带 hash 文件名,JS 很难维护文件名;路由懒加载模块切换主题适配复杂。

<link id="skin-link" href="skin-default.css" rel="stylesheet">
const link = document.getElementById('skin-link');
link.href = 'skin-red.css';

预加载备选样式

JS 修改 link 标签的 disabled 属性。

浏览器支持备选样式表机制,提前把所有主题 CSS 全部预加载到浏览器,通过修改 disabled 属性控制启用、关闭,无网络等待

优点:切换无网络请求;缺点:一次性预加载全部主题 CSS,首屏体积开销大。

<!-- 全局通用样式,永远生效 -->
<link href="reset.css" rel="stylesheet">

<!-- 默认启用的主题 -->
<link href="default.css" rel="stylesheet" title="Default Style">

<!-- 备选主题,预加载,默认禁用 -->
<link href="fancy.css" rel="alternate stylesheet" title="Fancy" disabled>
<link href="basic.css" rel="alternate stylesheet" title="Basic" disabled>

用户个性化定制主题(无限主题)

用户可以自由输入任意颜色值,没有预设主题集合,颜色组合理论上无限,不能提前全部构建出 CSS 产物。

动态生成 style 标签覆盖样式

原理:JS 动态创建 <style> 标签,填充自定义样式,挂载到 document.head,覆盖原有样式。

缺点:如果项目使用 CSS Modules,类名被哈希,无法直接写选择器;业务样式量大时,需要维护大量选择器。

<div class="text-color">测试文本</div>
.text-color {
  color: red;
}
const styleEl = document.createElement('style');
styleEl.innerHTML = '.text-color { color: cyan; }';
document.head.appendChild(styleEl);

Element UI(Vue 2)在线换肤方案核心逻辑:

  1. Ajax 拉取组件库完整 CSS 文本;
  2. 将原始 CSS 中固定颜色值替换为占位关键词;
  3. 根据用户输入主色,通过颜色公式,计算整套衍生色(hover、active、边框、背景色);
  4. 将关键词替换为新生成色值;
  5. 生成 style 标签,插入页面,覆盖原有样式。

Element Plus、Ant Design 等现行组件库已经把主题色做成 CSS 变量,不必再拉整份 CSS 做字符串替换。

function changeThemeByCssUrl(cssUrl, colorMap) {
  const xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function(res) {
    const target = res.target;
    if(target.readyState !== 4) return;
    if(target.status === 200) {
      let cssText = target.response;
      Object.keys(colorMap).forEach(oldColor => {
        const reg = new RegExp(oldColor, 'ig');
        cssText = cssText.replace(reg, colorMap[oldColor]);
      });
      const styleDom = document.createElement('style');
      styleDom.innerText = cssText;
      document.head.appendChild(styleDom);
    }
  }
  xhr.open('GET', cssUrl);
  xhr.send();
}

// 使用:替换两套颜色
changeThemeByCssUrl('https://xxx/theme.css', {
  '#508cee': '#ff5521',
  '#D2DFFE': '#FFDDD2'
})

缺点:需要请求完整 CSS 文本,存在网络开销;懒加载组件的样式无法被覆盖。

Less 浏览器端在线修改变量

浏览器引入 less.js,直接加载 .less 源码文件,浏览器实时编译;调用 less.modifyVars() 动态修改变量,实现换肤。

缺点:

  • 在浏览器实时编译 Less,性能损耗大,不适合生产环境
  • Sass 没有等价的浏览器修改变量 API;
  • 需要把项目全部主题相关 Less 抽离为单独文件,工程化成本高;
  • 社区如 antd-theme-generator,通过 webpack 插件在构建期抽取 less 变量文件,仅适用于构建阶段。
<link rel="stylesheet/less" href="theme.less" />
<script src="https://cdn.jsdelivr.net/npm/less@4.1.1/dist/less.min.js"></script>

<div class="text-color">LESS在线换肤演示</div>
/* theme.less */
@major-color: red;
.text-color {
  color: @major-color;
}

JS 修改变量:

window.less.modifyVars({
  "@major-color": "cyan"
})

CSS Variables(现代浏览器优先方案)

核心原理:在 :root 定义一系列 CSS 变量(自定义属性),业务样式全部使用 var() 引用变量;运行时 JS 修改根节点上的变量值,页面样式自动更新。

优点:

  • 同时支持固定主题切换用户无限个性化定制
  • 无需重新加载 CSS 资源,切换即时生效;
  • 无需构建阶段生成多套产物,工程成本低;
  • 支持局部覆盖,可以给任意 DOM 节点设置局部变量,不限于根节点。
:root {
  --major-color: red;
}
.text-color {
  color: var(--major-color);
}

JS 读写 CSS 变量:

// 设置变量
document.documentElement.style.setProperty('--major-color', 'purple');

// 获取内联style上设置的变量
document.documentElement.style.getPropertyValue('--major-color');

// 获取css文件中定义的变量(computedStyle)
getComputedStyle(document.documentElement).getPropertyValue('--major-color');

// 删除变量
document.documentElement.style.removeProperty('--major-color');

总结

  • CSS 变量(优先推荐):实现简单、体验好。现行主流浏览器均已支持;只有必须兼容 IE 时才需要降级到 class 或多份 CSS。
  • class 命名空间(推荐):切换无延迟;搭配 postcss-themes,仅生成用到主题变量的选择器,体积可控。缺点:原生会打包全部主题样式。
  • 多 CSS 文件:首屏无多余主题样式。缺点:切换有加载延迟;打包带 hash,JS 难以动态引入;按需模块适配麻烦。
  • 动态创建 style 标签:灵活性最强,不依赖预定义变量。缺点:元素多则维护成本高;不兼容 CSS Module;拉取 CSS 替换色值会有延迟;按需模块不好处理。
  • LESS 浏览器修改变量(不推荐生产):浏览器实时编译性能差,样式抽离改造繁琐。

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-08-31 20:29:53

results matching ""

    No results matching ""