写在前面

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

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

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

这里也重点讨论第三种方案,其他方案也稍微提一下思路。

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

效果演示

实现思路

我们都知道 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变量,然后将ElemententUI 的主题变量使用 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

然后,再在 vue.config.js 中导入并使用吧即可。

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

方案二:根据不同环境变量切换不同的scss文件

方案三: 通过生成不同的样式文件,根据主题替换不同的样式文件

在项目中改变 SCSS 变量

新建一个样式文件,例如 element-variables.scss,写入以下内容:

/* 改变主题色变量 */
$--color-primary: pink;
/* 改变 icon 字体路径变量,必需 */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import "~element-ui/packages/theme-chalk/src/index";

之后,在项目的入口文件中,直接引入以上样式文件即可(无需引入 Element 编译好的 CSS 文件):

import Vue from 'vue'
import Element from 'element-ui'
import './element-variables.scss'
 
Vue.use(Element)

需要注意的是,覆盖字体路径变量是必需的,将其赋值为 Element 中 icon 图标所在的相对路径即可。

命令行主题工具(生成变量)

切换node版本到12 以下,点击前往学习nvm 管理员方式打开终端

//全局安装 element-theme
npm i element-theme -g 

切换node版本到12 以上,点击前往学习nvm 管理员方式打开终端

# 在项目根目录安装 element-theme-chalk
npm i element-theme-chalk -D

切换node版本到12 以下,点击前往学习nvm 管理员方式打开终端

# 执行以下命令  会生成  element-variables.scss 文件
et -i

如果使用默认配置,执行后当前目录会有一个 element-variables.scss 文件。内部包含了主题所用到的所有变量,它们使用 SCSS 的格式定义。大致结构如下:

$--color-primary: #409EFF !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;
...

修改element-variables.scss文件变量值

# 执行下列命令会生成主题css文件
et

项目main.js文件引入主题文件

import "../themes/index.css"

此时文件变量就能用

但是改变变量值,不能实时刷新,且生成文件的目录不是理想的目录结构,因此可以执行下列操作

et -w -o ./src/theme

-w 可以实时实时编译,-o 可以自定义生成文件目录位置

然后重新引入对应路径的主题文件

至此我们可以使用我们自定义的主题。但是还不能实现切换不同主题

et命令是根据element-variables.scss 文件生成主题变量文件

每当想生成不同主题的变量值时,就得改到这个文件,为了避免混淆,我们得保存主题对应的变量文件

normal-element-variables.scss

night-element-variables.scss

这样需要编辑的时候,就可以把这个文件内容覆盖element-variables.scss,确定完再把element-variables.scss文件覆盖对应主题变量文件。

实现切换主题功能

实现主题切换有两种方式,一种就是使用命名空间的方法,将样式文件以命名空间的形式存在,然后切换类名

一种就是动态切换引入的主题样式文件,但是样式文件还是得使用命名空间的命名方式,因为相同类名,可能会导致样式不刷新

通过环境变量编译出不同的主题色

动态配置主题目录。

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

创建环境变量文件 .env.development

VUE_APP_THEME = dev-theme

配置打包方式。

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

创建变量文件: 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";

在文件中引入主题变量。

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

动态切换样式(运行时)