文章目录
动态换肤的主题解决方案总结
对于 自定义主题而言,核心的原理其实就是 修改
**scss**
变量来进行实现主题色变化
明实现的步骤:
- 对于第三方 UI 组件( 如
element-plus
):以它 不是完全可控 的(版本频繁更新会有变化),那么对于这种最简单直白的方案,就是直接拿到它编译后的css
进行色值替换,利用style
内部样式表 优先级高于 外部样式表 的特性,来进行主题替换 - 对于自定义主题:因为自定义主题是 完全可控 的,所以我们实现起来就轻松很多,只需要修改对应的
scss
变量即可
处理 第三方( element-plus )主题变更原理与步骤分析
- 实现原理
- 实现步骤
- 实现过程
实现原理
核心的原理是:**通过修改 **
**scss**
** 变量 ** 的形式修改主题色完成主题变更。
对于第三方 UI 组件,它内部其实就是通过
**scss**
【预处理器】去控制主题色的。
**如: element-plus **
http://element-plus.org/zh-CN/guide/theming.html
如果修改主题色,可以通过此种方式。但是此种方式无法应用到当前的项目中,因为它的颜色是预先定义好的。
通过–el-color-primary 修改【但是是它预先定义的颜色,因此无法使用】
项目的主题色,是用户随意选择的,不是欲先定义好的。它选中什么颜色就替换成什么颜色。因此不能使用官网提供的此种方式来进行实现。
=>
其实整体的原理非常简单,分为三步:
- 获取当前
**element-plus**
的所有样式 - 找到我们想要替换的样式部分,通过正则完成替换
- 把替换后的样式写入到
**style**
标签中,利用样式优先级的特性,替代固有样式
找到
element-plus
的默认主题色,然后全局替换
直接替换
实现步骤
那么明确了原理之后,我们的实现步骤也就呼之欲出了,对应原理总体可分为四步:
- 获取当前
element-plus
的所有样式 - 定义我们要替换之后的样式
- 在原样式中,利用正则替换新样式
- 把替换后的样式写入到
style
标签中
处理 element-plus 主题变更
工具类,写入两个方法
利用第二个方法生成样式表,再利用第一个方法写入到样式中。
/**
* 写入新样式生成到 style 中
* @param {*} elNewStyle element-plus 的新样式
*/exportconstwriteNewStyle=elNewStyle=>{}/**
* 根据主色值,生成最新的样式表
*/exportconstgenerateNewStyle=primaryColor=>{}
需要安装两个工具类:
- rgb-hex:转换RGB(A)颜色为十六进制
- 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'/**
* 根据主色值,生成最新的样式表
* 分成三部分
*/exportconstgenerateNewStyle=asyncprimaryColor=>{// 1、根据当前主色生成色值表const colors =generateColors(primaryColor)// 2、获取当前 element-plus 的默认样式表,并且把需要进行替换的色值打上标记(后续补充详细原理)let cssText =awaitgetOriginalStyle()// 3、遍历生成的样式表,在 默认CSS 的原样式中进行全局替换(补充解释)
Object.keys(colors).forEach(key=>{// 通过正则,把所有的不论前面有多少个空格,然后包含了key,都进行全局替换。替换成当前处理完毕的 color,获取好的对应的值
cssText = cssText.replace(newRegExp('(:|\\s+)'+ key,'g'),'$1'+ colors[key])})return cssText
}/**
* 根据主色生成色值表
*/exportconstgenerateColors=primary=>{if(!primary)returnconst 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 的默认样式表
*/constgetOriginalStyle=async()=>{const version =require('element-plus/package.json').version
const url =`https://unpkg.com/element-plus@${version}/dist/index.css`const{ data }=awaitaxios(url)// 把获取到的数据筛选为原样式模板returngetStyleTemplate(data)}/**
* 返回 style 的 template
*/constgetStyleTemplate=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 样式数据)中所有匹配该颜色值的部分替换为对应的标记符号 valueconst value = colorMap[key]// 使用正则表达式创建一个模式,以不区分大小写的全局模式(ig)查找 key 的所有出现位置。
data = data.replace(newRegExp(key,'ig'), value)})return data
}
把新样式插入到head中。
/**
* 写入新样式到 style
* @param {*} elNewStyle element-plus 的新样式
* @param {*} isNewStyleTag 是否生成新的 style 标签
*/exportconstwriteNewStyle=elNewStyle=>{const style = document.createElement('style')
style.innerText = elNewStyle
document.head.appendChild(style)// 把style插入到head中}
这样整个主题色替换功能就完成了。
在SelectColor 组件中点击确定的时候调用即可。 =>
...
<scriptsetup>...import{ generateNewStyle, writeNewStyle }from'@/utils/theme'.../**
* 确定
* 1. 修改主题色
* 2. 保存最新的主题色
* 3. 关闭 dialog
*/constcomfirm=async()=>{// 1.1 获取主题色const newStyleText =awaitgenerateNewStyle(mColor.value)// 1.2 写入最新主题色writeNewStyle(newStyleText)// 2. 保存最新的主题色
store.commit('theme/setMainColor', mColor.value)// 3. 关闭 dialogclosed()}</script>
补充 => 步骤 2:获取当前 element-plus 的默认样式表,并且把需要进行替换的色值打上标记
需要分为两个部分:
- 定义一个方法获取 获取当前 element-plus 的默认样式表
- 根据默认色值为要替换的色值打上标记
打开样式地址
现在就可以得到所有的element plus中所有的颜色了。
可以放到项目中查看(拷贝到 vs-code,然后自动格式化)【具体不展示了】
因为每个人安装的时候版本不一定一致,因此不能使用固定的格式。
对于下面两部分,引用的时候应该都不会变化的。唯一变化的其实就是中间这块版本。
可以获取当前项目中element-plus的版本,然后替换到这里。
这样你安装什么版本的element-plus,也会拿到对应版本的样式表了。这样就不会与版本进行强绑定了。
有了这个url之后,就可以利用axios直接请求了。
拿到了 element-plus 的默认样式表了。
继续第二步,根据默认色值为要替换的色值打上标记
打标记其实就是:比如替换这个颜色,得根据这个色值,找到这个对应的key。
给它的value打上标记
然后在所有里边找到
要替换掉
的这个标记,全部替换就成功了。
前提:需要知道哪些色值需要打上标记。
指定需要打标记的色值
constgetStyleTemplate=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 样式数据)中所有匹配该颜色值的部分替换为对应的标记符号 valueconst value = colorMap[key]// 使用正则表达式创建一个模式,以不区分大小写的全局模式(ig)查找 key 的所有出现位置。
data = data.replace(newRegExp(key,'ig'), value)})return data
}
假设传入的
data
是以下样式字符串:
.background{background-color: #409eff;}.border{border-color: #66b1ff;}
经过
getStyleTemplate
处理后,返回的结果将是:
.background{background-color: primary;}.border{border-color: light-2;}
此时,
primary
和
light-2
是标记符号,代表可以在后续步骤中根据需要替换为不同的颜色值,实现动态换肤效果。
补充=>步骤 3:遍历生成的样式表,在 默认CSS 的原样式中进行全局替换
// 3、遍历生成的样式表,在 默认CSS 的原样式中进行全局替换(补充解释)
Object.keys(colors).forEach(key=>{// 通过正则,把所有的不论前面有多少个空格,然后包含了key,都进行全局替换。替换成当前处理完毕的 color,获取好的对应的值
cssText = cssText.replace(newRegExp('(:|\\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生成最新的样式,它是异步函数,然后成功后再写入样式中即可。
<scriptsetup>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
进行指定的,该
cssVar
的
getters
为:
想要修改 自定义主题 ,只需要从这里入手即可。(原先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))}},...}exportdefault getters
存在坑:主题色替换之后,需要刷新页面才可响应
这个是因为 vuex 的
getters
(相当于它的计算属性)中没有监听到 依赖值的响应变化,所以我们希望修改依赖值
variable是一个固定值
...import variables from'@/styles/variables.scss'exportdefault{namespaced:true,state:()=>({...
variables
}),mutations:{/**
* 设置主题色
*/setMainColor(state, newColor){...// 这样只要setMainColor发生变化,state.variables就也会变化,就能触发计算属性了。
state.variables.menuBg = newColor // 新增...}}}
版权归原作者 溜_x_i_a_o_迪 所有, 如有侵权,请联系我们删除。