写在前面

动态切换不同的主题色是项目中非常常见的功能,这次我们公司的后台管理系统中也需要实现这个功能。我们项目使用的是Vue2+ElementUI+scss的技术栈,切换主题的常见方法有三种:

  • 可以通过工具生成不同主题css文件,切换主题的时候动态切换css主题文件。如果不需要动态切换主题,也可以通过环境变量打包不同的主题,theme-chalk-preview 就是这样实现的。
  • 当然,也可以全部主题样式都打包在一起,然后通过classname来切换。
  • 最后,还可以通过CSS3变量实现ElementUI主题的切换。

最后一种,是我认为最好的一种方案,虽然可能有一些兼容性问题,但如果不需要兼容IE,这是一个完美的解决方案,即使要兼容IE,我觉得问题也不大,不过就是降级体验,使用默认的主题颜色而已。

这里重点讨论第三种方案,然后再说说怎么通过环境变量更改不同的主题。

方案一:使用 CSS3 变量实现 ElementUI 主题的动态切换

效果演示

最终的效果可以在预览地址中查看,也可以在element-custom-theme 中查看源码。

实现思路

我们都知道 ElementUI 的主题是可以通过scss变量来动态改变的,如果我们仅仅是为了在编译的时候修改 ElementUI 组件的主题样式,只需要定义一个 element-variables.scss 文件修改一下主题变量就可以了,但如果想要在运行时动态修改项目的主题色,就需要在此基础上通过 CSS3 变量来实现,这样是因为原本的scss变量,只支持编译时指定,但并不支持后期改变,也就不能完全满足我们动态切换主题的需求。

实现方式很简单,就三步:

  1. 使用 css 变量定制 ElementUI sass 变量
// element-variables.scss
--color-primary: #409eff;
$--color-primary: var(--color-primary, #409EFF);
  1. 覆盖 sass 内置的 mix 函数,使用 css 的 color-mix 函数代替。这里之所以要使用 color-mix 函数代替内置的 mix 函数,主要是因为内置的 mix 函数不支持 var(--color-primary, #409EFF) 作为参数,会导致编译通不过。
// element-variables.scss
@function mix($color1, $color2, $p: 50%) {
  @return color-mix(in srgb, $color1 $p, $color2);
}
  1. 在页面注入 css 变量,通过 js 动态修改 css 变量即可实现整个 ElementUI 的主题切换。
const root = document.documentElement;
root.style.setProperty('--color-primary', color);

具体实现

element-variables.scss 文件的完整代码如下,下面的代码首先在 :root 中定义css变量,然后将 ElementUI 的主题变量使用 css 变量进行定义,覆盖一下内置函数,最后就是修改ElementUI 主题的常规操作,定义字体文件路径和引入主题样式,想要了解更多修改ElementUI主题的内容可以查看官方文档

:root {
  --color-white: #fff;
  --color-primary: #409eff;
  --color-primary-light-1: color-mix(in srgb, var(--color-white) 10%, var(--color-primary));
  --color-primary-light-2: color-mix(in srgb, var(--color-white) 20%, var(--color-primary));
  --color-primary-light-3: color-mix(in srgb, var(--color-white) 30%, var(--color-primary));
  --color-primary-light-4: color-mix(in srgb, var(--color-white) 40%, var(--color-primary));
  --color-primary-light-5: color-mix(in srgb, var(--color-white) 50%, var(--color-primary));
  --color-primary-light-6: color-mix(in srgb, var(--color-white) 60%, var(--color-primary));
  --color-primary-light-7: color-mix(in srgb, var(--color-white) 70%, var(--color-primary));
  --color-primary-light-8: color-mix(in srgb, var(--color-white) 80%, var(--color-primary));
  --color-primary-light-9: color-mix(in srgb, var(--color-white) 90%, var(--color-primary));
  --color-success: #67c23a;
  --color-warning: #e6a23c;
  --color-danger: #f56c6c;
  --color-info: #909399;
}
// 覆盖 sass 内置的 mix 函数,使得其支持var变量
@function mix($color1, $color2, $p: 50%) {
  @return color-mix(in srgb, $color1 $p, $color2);
}
/* 改变主题色变量 */
$--color-white: var(--color-white) !default;
$--color-primary: var(--color-primary) !default;
$--color-primary-light-1: var(--color-primary-light-1) !default;
$--color-primary-light-2: var(--color-primary-light-2) !default;
$--color-primary-light-3: var(--color-primary-light-3) !default;
$--color-primary-light-4: var(--color-primary-light-4) !default;
$--color-primary-light-5: var(--color-primary-light-5) !default;
$--color-primary-light-6: var(--color-primary-light-6) !default;
$--color-primary-light-7: var(--color-primary-light-7) !default;
$--color-primary-light-8: var(--color-primary-light-8) !default;
$--color-primary-light-9: var(--color-primary-light-9) !default;
$--color-success: var(--color-success) !default;
$--color-warning: var(--color-warning) !default;
$--color-danger: var(--color-danger) !default;
$--color-info: var(--color-info) !default;
 
/* 改变 icon 字体路径变量,必需 */
$--font-path: "~element-ui/lib/theme-chalk/fonts";
@import "~element-ui/packages/theme-chalk/src/index";

如果需要在组件或其他样式文件中使用ElementUI的主题变量,直接引入element-variables.scss之后,直接使用即可。

<style lang="scss">
@import "@/styles/element-variables.scss";
 
.el-button {
  background: $--color-primary;
}
</style>

最后,这里我们使用 el-color-picker 组件来选择主题颜色,然后通过修改主题变量来实现主题的动态切换。这里还使用了process.env.VUE_APP_PRIMARY_COLOR定义了一个默认主题色,通过 localstorage 存储切换后的主题色,下次进入项目的时候首先获取 localstorage 中的主题色。

<template>
<el-color-picker v-model="primaryColor" :predefine="predefineColors" show-alpha size="mini" @change="updateThemeColor"></el-color-picker>
</template>
<script>
export default {
	data() {
		return {
            primaryColor: getStorage('primaryColor') || process.env.VUE_APP_PRIMARY_COLOR,
          predefineColors: [
              '#ff4500',
              '#ff8c00',
              '#ffd700',
              '#90ee90',
              '#00ced1',
              '#1e90ff',
              '#c71585',
              'rgba(255, 69, 0, 0.68)',
              'rgb(255, 120, 0)',
              'hsv(51, 100, 98)',
              'hsva(120, 40, 94, 0.5)',
              'hsl(181, 100%, 37%)',
              'hsla(209, 100%, 56%, 0.73)',
              '#c7158577'
            ]
		};
	},
	  beforeMount() {
	    this.updateThemeColor(this.primaryColor)
	  },
	methods: {
	    updateThemeColor(color) {
	      if(!color) {
	        color = process.env.VUE_APP_PRIMARY_COLOR
	        this.primaryColor = color
	      }
	      const root = document.documentElement;
	      root.style.setProperty('--color-primary', color);
	      setStorage('primaryColor', color)
	    }
	}
};
</script>

兼容性处理

如果你的项目不需要考虑IE浏览器的兼容性,或者可以允许IE降级体验,不需要动态切换主题,上面的内容已经能完全满足你需求了。但如果你非要让IE也支持切换主题,由于 css 变量和 color-mix 函数存在一些兼容性问题,可以通过写一个 postcss 插件来提供兜底方案。这样在不支持 css 变量和 color-mix 函数的浏览器中,就会使用固定的色值。

下面是 postcss-plugin.js 文件的完整定义:

//postcss-plugin.js
const Color = require('color')
 
// 处理 CSS3 变量和 color-mix 兼容性问题
// 将
// .el-alert--success.is-light {
//   background-color: color-mix(in srgb, #FFFFFF 90%, var(--color-success, #67C23A));
//   color: var(--color-success, #67C23A);
// }
//
// 转换为
// .el-alert--success.is-light {
//   background-color: #F0F9EB;
//   background-color: color-mix(in srgb, #FFFFFF 90%, var(--color-success, #67C23A));
//   color: #67C23A;
//   color: var(--color-success, #67C23A);
// }
// 这样在不支持 css 变量和 color-mix 函数的浏览器中,就会使用固定的色值
module.exports = (opts = {}) => {
  return {
    postcssPlugin: 'POSTCSS-PLUGIN',
    Declaration (decl, { Declaration }) {
      let newVal = decl.value
 
      const varArr = getVar(decl.value)
      if (varArr) {
        varArr.forEach(i => {
          const _i = i.match(/,(.*)\)/)
          if (_i) newVal = newVal.replace(i, _i[1].trim())
        })
      }
 
      const mixArr = getColorMix(newVal)
      if (mixArr) {
        mixArr.forEach(i => {
          const _i = getColorMixDefault(i)
          if (_i) newVal = newVal.replace(i, _i)
        })
      }
 
      if (newVal !== decl.value) {
        decl.before(new Declaration({ prop: decl.prop, value: newVal }))
      }
    }
  }
}
 
function getVar (value) {
  return value.match(/var\([^(]+(\([^(]+\))?[^(]*\)/g)
}
 
function getColorMix (value) {
  return value.match(/color-mix\([^(]+(\([^(]+\))?([^(]+\([^(]+\))?[^(]*\)/g)
}
 
function getColorMixDefault (value) {
  const arr = value.match(/[^,]+,([^(]+|[^(]+\([^)]+\)[^,]*),(.*)\)/)
  if (arr) {
    const index = arr[1].lastIndexOf(' ')
    const p = arr[1].slice(index).trim().replace('%', '') / 100
    const color1 = Color(arr[1].slice(0, index).trim())
    const color2 = Color(arr[2].trim())
    // 使用 color.js 的 mix 方法提前混出默认值
    return color2.mix(color1, p).hex()
  } else {
    return null
  }
}
 
module.exports.postcss = true

然后,再将 postcss-plugin.js 引入到 vue.config.js 中使用。

// vue.config.js
const plugin = require('./postcss-plugin')
 
css: {
	loaderOptions: {
	  postcss: {
		postcssOptions: {
		  plugins: [plugin]
		}
	  }
	}
}

方案二:根据不同环境变量使用不同的scss主题文件

这个方案的基本思路就是通过环境变量动态生成 alias@theme 路径,从而使用不同的scss主题文件。

下面是具体的实现步骤:

首先,创建环境变量文件 .env.development, 并配置一下的环境变量 VUE_APP_THEME

VUE_APP_THEME = dev-theme

然后,创建对应的scss变量文件: src\assets\themes\dev-theme\element-variables.scss

/* 改变主题色变量 */
$--color-white: #fff !default;
$--color-primary: pink !default;
$--color-primary-light-1: mix($--color-white, $--color-primary, 10%) !default; /* 53a8ff */
$--color-primary-light-2: mix($--color-white, $--color-primary, 20%) !default; /* 66b1ff */
$--color-primary-light-3: mix($--color-white, $--color-primary, 30%) !default; /* 79bbff */
$--color-primary-light-4: mix($--color-white, $--color-primary, 40%) !default; /* 8cc5ff */
$--color-primary-light-5: mix($--color-white, $--color-primary, 50%) !default; /* a0cfff */
$--color-primary-light-6: mix($--color-white, $--color-primary, 60%) !default; /* b3d8ff */
$--color-primary-light-7: mix($--color-white, $--color-primary, 70%) !default; /* c6e2ff */
$--color-primary-light-8: mix($--color-white, $--color-primary, 80%) !default; /* d9ecff */
$--color-primary-light-9: mix($--color-white, $--color-primary, 90%) !default; /* ecf5ff */
 
$--color-success: #67c23a !default;
$--color-warning: #e6a23c !default;
$--color-danger: #f56c6c !default;
$--color-info: #909399 !default;
 
// $--color-primary: #409eff;
/* 改变 icon 字体路径变量,必需 */
$--font-path: "~element-ui/lib/theme-chalk/fonts";
@import "~element-ui/packages/theme-chalk/src/index";

然后,在vue.config.js中动态配置主题目录。

// vue.config.js
resolve: {
  alias: {
	'@theme': path.resolve(__dirname, `src/assets/themes/${process.env.VUE_APP_THEME}`)
  }
}

在需要使用主题变量的文件中使用@theme引入变量。

<style lang="scss">
@import '@theme/element-variables';
 
.el-button {
  background: $--color-primary;
}
</style>

最后,再在package.json 文件中配置打包方式。

  {
    "build": "vue-cli-service build --mode development",
    "build:stage": "vue-cli-service build --mode staging",
    "build:prod": "vue-cli-service build --mode production",
  },

扩展阅读