前端(Vue)动态换肤的通用解决方案及原理分析(2)

news2024/12/29 9:00:35

文章目录

  • 动态换肤的主题解决方案总结
  • 处理 第三方( element-plus )主题变更原理与步骤分析
    • **实现原理**
    • 实现步骤
    • 处理 element-plus 主题变更
      • 补充 => 步骤 2:获取当前 element-plus 的默认样式表,并且把需要进行替换的色值打上标记
      • 补充=>步骤 3:遍历生成的样式表,在 默认CSS 的原样式中进行全局替换
  • 遇到的坑:刷新页面后,主题丢失
  • 自定义主题变更
      • 存在坑:主题色替换之后,需要刷新页面才可响应

动态换肤的主题解决方案总结

对于 自定义主题而言,核心的原理其实就是 修改**scss**变量来进行实现主题色变化
明实现的步骤:

  1. 对于第三方 UI 组件( 如element-plus):以它 不是完全可控 的(版本频繁更新会有变化),那么对于这种最简单直白的方案,就是直接拿到它编译后的 css 进行色值替换,利用 style 内部样式表 优先级高于 外部样式表 的特性,来进行主题替换
  2. 对于自定义主题:因为自定义主题是 完全可控 的,所以我们实现起来就轻松很多,只需要修改对应的 scss变量即可

处理 第三方( element-plus )主题变更原理与步骤分析

  1. 实现原理
  2. 实现步骤
  3. 实现过程

实现原理

核心的原理是:通过修改 **scss** 变量 ** 的形式修改主题色完成主题变更。
对于第三方 UI 组件,它内部其实就是通过**scss**
【预处理器】去控制主题色的。**
**如: element-plus **
http://element-plus.org/zh-CN/guide/theming.html
image.png
如果修改主题色,可以通过此种方式。但是此种方式无法应用到当前的项目中,因为它的颜色是预先定义好的。
image.png
image.png

通过–el-color-primary 修改【但是是它预先定义的颜色,因此无法使用】
项目的主题色,是用户随意选择的,不是欲先定义好的。它选中什么颜色就替换成什么颜色。因此不能使用官网提供的此种方式来进行实现。
image.png
=>

其实整体的原理非常简单,分为三步:


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

找到element-plus 的默认主题色,然后全局替换
image.png
直接替换
image.png

实现步骤

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

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

处理 element-plus 主题变更

工具类,写入两个方法
利用第二个方法生成样式表,再利用第一个方法写入到样式中。

/**
 * 写入新样式生成到 style 中
 * @param {*} elNewStyle  element-plus 的新样式
 */
export const writeNewStyle = elNewStyle => {
  
}

/**
 * 根据主色值,生成最新的样式表
 */
export const generateNewStyle =  primaryColor => {
 
}

需要安装两个工具类:

  1. rgb-hex:转换RGB(A)颜色为十六进制
  2. css-color-function:在CSS中提出的颜色函数的解析器和转换器

写入一个 **颜色转化计算器 ****formula.json**
如下指定了一堆变量,比如 “color(primary tint(80%))” => 当前的色值里面添加80%的白色。
“shade-1”: “color(primary shade(10%))” => primary色添加10%的黑
这里其实以primary为基准色,添加不同程度的黑色和白色。这样做出了一个简单的颜色转化的计算表。

假设我们有一个色值表 formula,其中存储了一些与主色相关的 CSS 变量公式。这些公式中使用了 primary 作为占位符,我们需要将这个占位符替换成实际的主色值。

{
  "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)"
}

整体实现如下:

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

/**
 * 根据主色值,生成最新的样式表
 * 分成三部分
 */
export const generateNewStyle = async primaryColor => {
  // 1、根据当前主色生成色值表
  const colors = generateColors(primaryColor)
  // 2、获取当前 element-plus 的默认样式表,并且把需要进行替换的色值打上标记(后续补充详细原理)
  let cssText = await getOriginalStyle()

  // 3、遍历生成的样式表,在 默认CSS 的原样式中进行全局替换(补充解释)
  Object.keys(colors).forEach(key => {
    // 通过正则,把所有的不论前面有多少个空格,然后包含了key,都进行全局替换。替换成当前处理完毕的 color,获取好的对应的值
    cssText = cssText.replace(
      new RegExp('(:|\\s+)' + key, 'g'),
      '$1' + colors[key]
    )
  })

  return cssText
}

/**
 * 根据主色生成色值表
 */
export const generateColors = primary => {
  if (!primary) return
  const colors = {
    primary
  }
  // 遍历色值表,取出色值表做对应的替换,替换成当前的parimary色值(全局替换成当前导入的色值)。
  Object.keys(formula).forEach(key => {
    // formula 对象中有几个键值对,每个值都是一个带有 primary 占位符的字符串,表示 CSS 颜色的计算公式。
    const value = formula[key].replace(/primary/g, primary)
    // 到替换后的色值,需要转换为16进制,用第三方。
    colors[key] = '#' + rgbHex(color.convert(value))
  })
  return colors
}

/**
 * 获取当前 element-plus 的默认样式表
 */
const getOriginalStyle = async () => {
  const version = require('element-plus/package.json').version
  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 => {
    // 在遍历过程中,对于每一个颜色值 key,代码会使用 data.replace() 方法将 data(传入的 CSS 样式数据)中所有匹配该颜色值的部分替换为对应的标记符号 value
    const value = colorMap[key]
    // 使用正则表达式创建一个模式,以不区分大小写的全局模式(ig)查找 key 的所有出现位置。
    data = data.replace(new RegExp(key, 'ig'), value)
  })
  return data
}

把新样式插入到head中。

/**
 * 写入新样式到 style
 * @param {*} elNewStyle  element-plus 的新样式
 * @param {*} isNewStyleTag 是否生成新的 style 标签
 */
export const writeNewStyle = elNewStyle => {
  const style = document.createElement('style')
  style.innerText = elNewStyle
  document.head.appendChild(style) // 把style插入到head中
}

这样整个主题色替换功能就完成了。
在SelectColor 组件中点击确定的时候调用即可。 =>

...

<script setup>
  ...
  import { generateNewStyle, writeNewStyle } from '@/utils/theme'
  ...
  /**
 * 确定
 * 1. 修改主题色
 * 2. 保存最新的主题色
 * 3. 关闭 dialog
 */

  const comfirm = async () => {
    // 1.1 获取主题色
    const newStyleText = await generateNewStyle(mColor.value)
    // 1.2 写入最新主题色
    writeNewStyle(newStyleText)
    // 2. 保存最新的主题色
    store.commit('theme/setMainColor', mColor.value)
    // 3. 关闭 dialog
    closed()
  }
</script>

补充 => 步骤 2:获取当前 element-plus 的默认样式表,并且把需要进行替换的色值打上标记

需要分为两个部分:

  • 定义一个方法获取 获取当前 element-plus 的默认样式表
  • 根据默认色值为要替换的色值打上标记

image.png
打开样式地址
image.png
现在就可以得到所有的element plus中所有的颜色了。
可以放到项目中查看(拷贝到 vs-code,然后自动格式化)【具体不展示了】
因为每个人安装的时候版本不一定一致,因此不能使用固定的格式。
对于下面两部分,引用的时候应该都不会变化的。唯一变化的其实就是中间这块版本。
image.png
可以获取当前项目中element-plus的版本,然后替换到这里。
这样你安装什么版本的element-plus,也会拿到对应版本的样式表了。这样就不会与版本进行强绑定了。
有了这个url之后,就可以利用axios直接请求了。
image.png
拿到了 element-plus 的默认样式表了。
继续第二步,根据默认色值为要替换的色值打上标记
打标记其实就是:比如替换这个颜色,得根据这个色值,找到这个对应的key。
image.png
给它的value打上标记
image.png
然后在所有里边找到要替换掉的这个标记,全部替换就成功了。
前提:需要知道哪些色值需要打上标记。
指定需要打标记的色值

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 => {
    // 在遍历过程中,对于每一个颜色值 key,代码会使用 data.replace() 方法将 data(传入的 CSS 样式数据)中所有匹配该颜色值的部分替换为对应的标记符号 value
    const value = colorMap[key]
    // 使用正则表达式创建一个模式,以不区分大小写的全局模式(ig)查找 key 的所有出现位置。
    data = data.replace(new RegExp(key, 'ig'), value)
  })
  return data
}

假设传入的 data 是以下样式字符串:

.background {
  background-color: #409eff;
}
.border {
  border-color: #66b1ff;
}

经过 getStyleTemplate 处理后,返回的结果将是:

.background {
  background-color: primary;
}
.border {
  border-color: light-2;
}

此时,primarylight-2 是标记符号,代表可以在后续步骤中根据需要替换为不同的颜色值,实现动态换肤效果。

补充=>步骤 3:遍历生成的样式表,在 默认CSS 的原样式中进行全局替换

  // 3、遍历生成的样式表,在 默认CSS 的原样式中进行全局替换(补充解释)
  Object.keys(colors).forEach(key => {
    // 通过正则,把所有的不论前面有多少个空格,然后包含了key,都进行全局替换。替换成当前处理完毕的 color,获取好的对应的值
    cssText = cssText.replace(
      new RegExp('(:|\\s+)' + key, 'g'),
      '$1' + colors[key]
    )
  })
  • 这一部分遍历 colors 对象的所有键(即颜色标记符),并在 cssText(即获取到的原始样式表)中进行全局替换。
  • new RegExp('(:|\\s+)' + key, 'g') 是用来匹配样式表中与当前键 key 相符的所有位置。正则表达式的作用是匹配形如 :primary 或者前面带有空格的 primary 形式的颜色标记符。
  • $1 是正则表达式的捕获组,表示匹配的冒号或空格(保持原样),然后跟上 colors[key](即替换后的新颜色值)。

例如:

  • 假设 colors 对象中的 primary 键对应的值是 #3498db,原始 CSS 中有一行 color: primary;。经过替换后,它会变成 color: #3498db;

遇到的坑:刷新页面后,主题丢失

在刷新页面后,新主题会失效
更换主题会在页面写入新的style标签
一旦刷新页面这个新的style就会消失。
那么出现这个问题的原因,非常简单:**因为没有写入新的 ****style**
解决 =>
只需要在 应用加载后,写入 **style** 即可
那么写入的时机,我们可以放入到 app.vue
根据generateNewStyle生成最新的样式,它是异步函数,然后成功后再写入样式中即可。

<script setup>
import { useStore } from 'vuex'
import { generateNewStyle, writeNewStyle } from '@/utils/theme'

const store = useStore()
generateNewStyle(store.getters.mainColor).then(newStyleText => {
  writeNewStyle(newStyleText)
})
</script>

自定义主题变更

自定义主题变更相对来说比较简单,因为 自己的代码更加可控
比如 menu 部分:

  <el-menu
    :default-active="activeMenu"
    :collapse="!$store.getters.sidebarOpened"
    :background-color="$store.getters.cssVar.menuBg"
    :text-color="$store.getters.cssVar.menuText"
    :active-text-color="$store.getters.cssVar.menuActiveText"
    :unique-opened="true"
    router
  >

此处的 背景色是通过 getters 进行指定的,该 cssVargetters 为:
想要修改 自定义主题 ,只需要从这里入手即可。(原先cssVar是写死的)

cssVar: state => variables,

根据当前保存的 **mainColor** 覆盖原有的默认色值

import variables from '@/styles/variables.scss'
import { MAIN_COLOR } from '@/constant'
import { getItem } from '@/utils/storage'
import { generateColors } from '@/utils/theme'

const getters = {
  ...
    cssVar: state => {
      return {
        ...variables,
        ...generateColors(getItem(MAIN_COLOR))
      }
    },
  ...
}
export default getters

存在坑:主题色替换之后,需要刷新页面才可响应

这个是因为 vuex 的 getters (相当于它的计算属性)中没有监听到 依赖值的响应变化,所以我们希望修改依赖值
variable是一个固定值

...
import variables from '@/styles/variables.scss'
export default {
  namespaced: true,
  state: () => ({
    ...
    variables
  }),
  mutations: {
    /**
     * 设置主题色
     */
    setMainColor(state, newColor) {
      ...
      // 这样只要setMainColor发生变化,state.variables就也会变化,就能触发计算属性了。
      state.variables.menuBg = newColor // 新增
      ...
    }
  }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2051064.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Android 手机恢复出厂设置后,还能恢复其中数据吗?

天津鸿萌科贸发展有限公司从事数据安全服务二十余年&#xff0c;致力于为各领域客户提供专业的数据恢复、数据备份、网络及终端数据安全等解决方案与服务。 同时&#xff0c;鸿萌是众多国际主流数据恢复软件的授权代理商&#xff0c;为专业用户提供正版的数据恢复软件。 对于 A…

网络版计算器(理解协议与序列化与反序列化)

一、理解协议 在网络层面&#xff0c;协议&#xff08;Protocol&#xff09;是一组规则、标准或约定&#xff0c;它们定义了在网络环境中&#xff0c;计算机、服务器、路由器、交换机等网络设备之间如何相互通信和交换信息。这些规则涵盖了数据格式、数据交换的顺序、速度、以及…

调研-音视频

音视频 基础概念主要内容音频基础概念音频量化过程音频压缩技术视频基础概念视频bug视频编码H264视频像素格式YUVRGB参考文献基础概念 ● 实时音视频应用环节 ○ 采集、编码、前后处理、传输、解码、缓冲、渲染等很多环节。 主要内容 音频 基础概念 三要素:音调(音频)、…

阿里云注册、认证、短信资质、签名、模板申请过程

一、帐号注册 输入“帐号密码注册”中的相关信息即可。 手机号是必须的&#xff0c;先确定好手机号。 正常的可以直接注册成功的。 二、实名认证 注册成功之后&#xff0c;就可以点击上述的“快速实名认证”。 这次选择的是“企业认证”。 有几种方式&#xff0c;如下&#x…

学习嵌入式第二十八天

有名管道 在C语言中&#xff0c;有名管道&#xff08;Named Pipe&#xff09;是一种特殊的文件类型&#xff0c;它允许进程间通信。有名管道与匿名管道&#xff08;Anonymous Pipe&#xff09;不同&#xff0c;它在文件系统中有一个路径名&#xff0c;因此可以被多个进程访问。…

项目实战-Linux部署-安装jdk以及shell脚本检查jdk

&#x1f339;作者主页&#xff1a;青花锁 &#x1f339;简介&#xff1a;Java领域优质创作者&#x1f3c6;、Java微服务架构公号作者&#x1f604; &#x1f339;简历模板、学习资料、面试题库、技术互助 &#x1f339;文末获取联系方式 &#x1f4dd; 往期热门专栏回顾 专栏…

STM32之MPU6050实战

MPU6050 MPU6050是一个6轴姿态传感器&#xff0c;可以测量芯片自身X、Y、Z轴的加速度、角速度参数&#xff0c;通过数据融合&#xff0c;可进一步得到姿态角&#xff0c;常应用于平衡车、飞行器等需要检测自身姿态的场景 3轴加速度计&#xff08;Accelerometer&#xff09;&a…

Python从0到100(五十二):逻辑回归及鸢尾花数据集预测

逻辑回归是⼀种⽤于解决⼆分类问题的监督学习算法&#xff0c;其基本原理是使⽤ 逻辑函数&#xff08;也称为Sigmoid函数&#xff09; 来建模 因变量&#xff08;输出&#xff09;与⾃变量&#xff08;输⼊&#xff09;之间的概率关系。逻辑回归的⽬标是估计某个事件发⽣的概率…

YOLOV8网络结构|搞懂Backbone-SPPF

SPPF SPP衍生而来。 因为速度快&#xff0c;所以是SPPF-Fast CONV 3个Maxpool串联 Concat 最后又Conv

QT 控件使用案例

常用控件 表单 按钮 Push Button 命令按钮。Tool Button&#xff1a;工具按钮。Radio Button&#xff1a;单选按钮。Check Box&#xff1a;复选框按钮。Command Link Button&#xff1a;命令链接按钮。Dialog Button Box&#xff1a;按钮盒。 容器组控件(Containers) Group Box…

JavaEE 的相关知识点(一)

一、过滤器 过滤器&#xff08;Filter&#xff09;是一个用于对请求和响应进行预处理的组件。过滤器可以在 Java Servlet 规范中使用&#xff0c;通常用于执行一些通用的任务 1、过滤器的作用 过滤器是一种javaEE规范中定义的一种技术&#xff0c;可以让请求达到目标servlet之…

Open3D 格网法计算点云的占地面积

目录 一、概述 1.1原理 1.2实现步骤 二、代码实现 2.1关键函数 2.2完整代码 三、实现效果 3.1原始点云 3.2数据显示 Open3D点云算法汇总及实战案例汇总的目录地址&#xff1a; Open3D点云算法与点云深度学习案例汇总&#xff08;长期更新&#xff09;-CSDN博客 一、概…

林小茶 C语言程序设计 8.48.58.6答案

【8.4】用结构体表示日期&#xff0c;编写程序计算北京奥运会倒计时的天数并输出&#xff08;2008年8月8日北京奥运会开幕&#xff0c;输入的日期范围是2008年1月1日-2008年8月7日&#xff09;。 #include<stdio.h> struct Date{int year;int month;int day; }; int mai…

硬件电路仿真-LTspice官方软件使用-运放电路仿真实战

文章目录 一&#xff1a;LTspice简介1.1 推荐先简单运用1.2 课程配套资料1.3 仿真过程1.4 SPICE模型1.5 LTSPICE工具栏和快捷键1.6 LTSPICE数量级 二&#xff1a;基本功能&#xff08;探索功能如何使用&#xff09;2.1 瞬态分析(.tran)2.2 交流分析&#xff08;.ac&#xff09;…

基于Springboot3 +vue2的民宿酒店预订系统

这个一个大数据库课程设计&#xff0c;也是计算机软件课程设计大作业&#xff0c;Springboot vue民宿酒店预订系统 本系统是采用Springboot3 vue2的酒店预订系统 &#xff0c;数据库mysql ,用户权限分为系统管理员&#xff0c;客房操作人员、和 普通用户&#xff08;游客&…

如何在Python中使用情感分析API

情感分析 API 服务是一种借助人工智能技术的工具&#xff0c;能够自动识别并衡量文本数据&#xff08;像社交媒体的帖子、产品的评论、新闻文章等等&#xff09;所蕴含的情感色彩。在本文里&#xff0c;我们会一同探讨怎样在 Python 中集成情感分析 API &#xff0c;并且展示它…

【Python机器学习】利用SVD简化数据——示例:菜肴推荐引擎

现在&#xff0c;构建一个推荐引擎&#xff0c;该推荐引擎关注的是餐馆食物的推荐。假设一个人决定外出吃饭&#xff0c;但并不知道去哪吃什么&#xff0c;我们这个推荐系统就可以帮他做到这两点。 首先我们构建一个基本的推荐引擎&#xff0c;它能够寻找用户没有尝过的菜肴&a…

C++(11)类语法分析(2)

C(10)之类语法分析(2) Author: Once Day Date: 2024年8月17日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 全系列文章可参考专栏: 源码分析_Once-Day的博客-CSDN博客 …

有关缓存的一些面试知识

1、讲一讲Redis各种数据类型与底层实现 底层数据结构一共有 7 种&#xff0c;分别是简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组、快速列表。它们和数据类型的对应关系如下图所示 String 类型的底层实现只有一种数据结构&#xff0c;也就是简单动态字符串。而…

57qi5rW35LqRZUhS pc.mob SQL注入漏洞复现

0x01 产品简介 57qi5rW35LqRZUhS是大中型企业广泛采用人力资源管理系统。某云是国内顶尖的HR软件供应商,是新一代eHR系统的领导者。 0x02 漏洞概述 57qi5rW35LqRZUhS pc.mob 接口存在SQL注入漏洞,未经身份验证的远程攻击者除了可以利用 SQL 注入漏洞获取数据库中的信息(例…