动态换肤


实现动态换肤的前提:

色值不能写死,需要通过一个动态的值,来进行指定。


    ...
  

实现原理:

在 scss 中,我们可以通过 $变量名:变量值 的方式定义 css 变量,然后通过该 css 变量 来去指定某一块 DOM 对应的颜色。
当大量的 DOM 都依赖于这个 css 变量 设置颜色时,只需要改变这个 css 变量,那么所有 DOM 的颜色都会发生变化,所谓的 动态换肤 就可以实现了!

variables.scss:

// sidebar
$menuText: #bfcbd9;
$menuActiveText: #ffffff;
$subMenuActiveText: #f4f4f5;

$menuBg: #304156;
$menuHover: #263445;

$subMenuBg: #1f2d3d;
$subMenuHover: #001528;

$sideBarWidth: 210px;
$hideSideBarWidth: 54px;
$sideBarDuration: 0.28s;

// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
// JS 与 scss 共享变量,在 scss 中通过 :export 进行导出,在 js 中可通过 ESM 进行导入
:export {
  menuText: $menuText;
  menuActiveText: $menuActiveText;
  subMenuActiveText: $subMenuActiveText;
  menuBg: $menuBg;
  menuHover: $menuHover;
  subMenuBg: $subMenuBg;
  subMenuHover: $subMenuHover;
  sideBarWidth: $sideBarWidth;
}

在vuex的getter中获取到上面variables.scss导出的(:export)的数据:

import variables from '@/styles/variables.scss'
const getters = {
  token: state => state.user.token,
  userInfo: state => state.user.userInfo,
  /**
   * @returns true 表示已存在用户信息
   */
  hasUserInfo: state => {
    return JSON.stringify(state.user.userInfo) !== '{}'
  },
  cssVar: state => variables,
  sidebarOpened: state => state.app.sidebarOpened,
  language: state => state.app.language
}
export default getters

处理内容

想要实现动态换肤,需要同时处理两个方面的内容:

  1. element-plus 主题 (ui框架)
  2. 非 element-plus 主题 (非ui框架)

实现方案

根据以上关键信息,我们就可以得出对应的实现方案

  1. 创建一个组件 ThemeSelect 用来处理修改之后的 css 变量 的值
  2. 根据新值修改 element-plus 主题色
  3. 根据新值修改非 element-plus 主题色

修改element-plus主题色

  1. 获取当前 element-plus 的所有样式
  2. 找到我们想要替换的样式部分,通过正则完成替换
  3. 把替换后的样式写入到 style 标签中,利用样式优先级的特性,替代固有样式

那么明确了原理之后,我们的实现步骤也就呼之欲出了,对应原理总体可分为四步:

  1. 获取当前 element-plus 的所有样式
  2. 定义我们要替换之后的样式
  3. 在原样式中,利用正则替换新样式
  4. 把替换后的样式写入到 style 标签中

实现修改element-plus主题色,我们需要安装两个工具类:
rgb-hex:转换RGB(A)颜色为十六进制
css-color-function:在CSS中提出的颜色函数的解析器和转换器

npm install rgb-hex --save
npm install css-color-function --save

然后还需要写入一个颜色转换器formula.json:

{
  "shade-1": "color(primary shade(10%))",
  "light-1": "color(primary tint(10%))",
  "light-2": "color(primary tint(20%))",
  "light-3": "color(primary tint(30%))",
  "light-4": "color(primary tint(40%))",
  "light-5": "color(primary tint(50%))",
  "light-6": "color(primary tint(60%))",
  "light-7": "color(primary tint(70%))",
  "light-8": "color(primary tint(80%))",
  "light-9": "color(primary tint(90%))",
  "subMenuHover": "color(primary tint(70%))",
  "subMenuBg": "color(primary tint(80%))",
  "menuHover": "color(primary tint(90%))",
  "menuBg": "color(primary)"
}

创建utils/theme工具类,实现颜色替换:

import color from 'css-color-function'
import rgbHex from 'rgb-hex'
import formula from '@/constant/formula.json'
import axios from 'axios'

/**
 * 写入新样式到 style
 * @param {*} elNewStyle  element-plus 的新样式
 * @param {*} isNewStyleTag 是否生成新的 style 标签
 */
let isFirstWriteStyle = true
export const writeNewStyle = elNewStyle => {
  if (isFirstWriteStyle) {
    const style = document.createElement('style')
    style.innerText = elNewStyle
    document.head.appendChild(style)
    isFirstWriteStyle = false
  } else {
    document.head.lastChild.innerText = elNewStyle
  }
}

/**
 * 根据主色值,生成最新的样式表
 */
export const generateNewStyle = async primaryColor => {
  // 根据主色生成色值表
  const colors = generateColors(primaryColor)
  // 获取当前 element-plus的默认样式表,并且把需要进行替换的色值打上标记
  let cssText = await getOriginalStyle()

  // 遍历生成的样式表,在 CSS 的原样式中进行全局替换
  Object.keys(colors).forEach(key => {
    cssText = cssText.replace(
      new RegExp('(:|\\s+)' + key, 'g'),
      '$1' + colors[key]
    )
  })

  return cssText
}

/**
 * 根据主色生成色值表
 */
export const generateColors = primary => {
  if (!primary) return
  const colors = {
    primary
  }
  Object.keys(formula).forEach(key => {
    const value = formula[key].replace(/primary/g, primary)
    colors[key] = '#' + rgbHex(color.convert(value))
  })
  return colors
}

/**
 * 获取当前 element-plus 的默认样式表
 */
const getOriginalStyle = async () => {
  // 获取当前项目中element-plus的版本
  const version = require('element-plus/package.json').version
  // 获取当前element-plus版本的css
  const url = `https://unpkg.com/element-plus@${version}/dist/index.css`
  const {
    data
  } = await axios(url)
  // 把获取到的数据筛选为原样式模板
  return getStyleTemplate(data)
}

/**
 * 返回 style 的 template
 */
const getStyleTemplate = data => {
  // element-plus 默认色值
  const colorMap = {
    '#3a8ee6': 'shade-1',
    '#409eff': 'primary',
    '#53a8ff': 'light-1',
    '#66b1ff': 'light-2',
    '#79bbff': 'light-3',
    '#8cc5ff': 'light-4',
    '#a0cfff': 'light-5',
    '#b3d8ff': 'light-6',
    '#c6e2ff': 'light-7',
    '#d9ecff': 'light-8',
    '#ecf5ff': 'light-9'
  }
  // 根据默认色值为要替换的色值打上标记
  Object.keys(colorMap).forEach(key => {
    const value = colorMap[key]
    data = data.replace(new RegExp(key, 'ig'), value)
  })
  return data
}

进行变更:

import { generateNewStyle, writeNewStyle } from '@/utils/theme'

/**
 * 确定
 * 1. 修改主题色
 * 2. 保存最新的主题色
 * 3. 关闭 dialog
 */
const comfirm = async () => {
  // 1. 修改主题色并写入新样式中
  const newStyleText = await generateNewStyle(mColor.value)
  writeNewStyle(newStyleText)
  // 2. 保存最新的主题色
  store.commit('theme/setMainColor', mColor.value)
  // 3. 关闭 dialog
  closed()
}

由于要实现element-plus动态换肤的过程较为困难,对于理解与否,因人而异,先做到会用就行了