动态换肤实现原理
今天被问到了一个动态换肤的问题,我只回答了动态修改scss
中代表色值的变量就可以实现,回答的有些笼统,过于简单了,所以回来以后查找资料、动手实践,将这一过程记录下来。
想要实现动态换肤的一个前置条件就是:色值不可以写死!
在scss
中,我们可以通过$变量名:变量值
的方式定义css
变量,然后通过该css变量
来指定某一块DOM
对应的颜色。如果我改变了该css
变量的值,那么是不是对应的DOM
颜色也会同步发生变化?
当大量的DOM
都依赖于这个css
变量设置颜色时,我们是不是只需要修改这个css变量
,那么所有的DOM
颜色也会发生变化,那么动态换肤这个功能是不是就实现了?
这就是动态换肤的实现原理。
动态换肤实现方案分析
明确了上面的实现原理以后我们可以得出一个结论,那就是在实现动态换肤的时候要兼顾两个方面:
动态换肤的关键是修改
css
变量的值换肤需要同时兼顾
element-plus
和非element-plus
那么根据以上关键信息,我们可以得出对应的解决方案:
创建一个组件
ThemeSelect
用来处理修改之后的css变量
的值根据新值修改
element-plus
的主题颜色根据新值修改非
element-plus
的主题颜色
创建ThemeSelect组件
我们在页面右上方有一个类似于衣服的icon图标,鼠标悬浮上去会显示提示文字“更改主题”。这里用到的组件是element-plus
中的下拉菜单el-dropdown
组件。实现过程如下代码所示:
1 | <template> |
组件封装好以后,在navbar组件中使用它。如下代码所示:
1 | <div class="right-menu"> |
1 | import ThemePicker from '@/components/ThemeSelect/index' |
创建SelectColor组件
1 | <template> |
这里会用到element-plus
中的colorPicker
取色器组件。在选择好颜色的色值以后,在确认事件的回调里面会做三件事,修改主题色,保存主题色,关闭dialog弹框。在封装好了这个组件以后呢,我们可以在前面的ThemeSelect
组件中使用它,让用户点击下拉菜单的时候显示这个取色器:
1 | <template> |
1 | <script setup> |
显示取色器组件没问题以后,我们就要开始思考如何将用户选择的新色值保存在我们的本地做一个缓存。在vue项目中常用的缓存方式有两种,第一种是放在vuex里面,第二种是本地存储比如localStorage。
创建theme相关的store模块
首先我们在constants/index
下面创建两个常量用来表示主题色的key
和默认色值:
1 | // 主题色的key |
在store
目录下创建主题相关的模块store/modules/theme
,这里面存放的都是主题相关的操作:
1 | // 引入封装好的本地存储相关的工具方法 |
为了方便访问,我们将mainColor
在getters
指定一下:
1 | mainColor: state => state.theme.mainColor |
在store/index
中导入theme
1 | ... |
SelectColor组件中引入store
1 | <script setup> |
处理element-plus主题变更原理与步骤分析
实现原理
在之前我们分析主题变更的实现原理时,我们说过,核心的原理是:通过修改 scss 变量 的形式修改主题色完成主题变更。
但是对于 element-plus
而言,我们怎么去修改这样的主题色呢?
其实整体的原理非常简单,分为三步:
- 获取当前
element-plus
的所有样式 - 找到我们想要替换的样式部分,通过正则完成替换
- 把替换后的样式写入到
style
标签中,利用样式优先级的特性,替代固有样式
实现步骤
那么明确了原理之后,我们的实现步骤也就呼之欲出了,对应原理总体可分为四步:
- 获取当前
element-plus
的所有样式 - 定义我们要替换之后的样式
- 在原样式中,利用正则替换新样式
- 把替换后的样式写入到
style
标签中
处理element-plus主题变更
创建utils/theme
工具类,写入两个方法:
1 | /** |
那么接下来我们先实现第一个方法 generateNewStyle
,在实现的过程中,我们需要安装两个工具类:
rgb-hex
:转换RGB(A)颜色为十六进制css-color-function
:在CSS中提出的颜色函数的解析器和转换器
然后还需要写入一个 颜色转化计算器 formula.json
创建 constants/formula.json
1 | { |
准备就绪后,我们来实现 generateNewStyle
方法:
1 | import color from 'css-color-function' |
接下来处理 writeNewStyle
方法:
1 | /** |
最后在SelectColor.vue
中导入这两个方法:
1 | ... |
处理element-plus新主题刷新页面不生效的问题
到目前我们已经完成了 element-plus
的主题变更,但是当前的主题变更还有一个小问题,那就是:在刷新页面后,新主题会失效
那么出现这个问题的原因,非常简单:因为没有写入新的 style
所以我们只需要在 应用加载后,写入 style
即可
那么写入的时机,我们可以放入到 app.vue
中
1 | <script setup> |
自定义主题变更
自定义主题变更相对来说比较简单,因为 自己的代码更加可控。
目前在我们的代码中,需要进行 自定义主题变更 为 menu
菜单背景色
而目前指定 menu 菜单背景色的位置在 layout/components/sidebar/SidebarMenu.vue
中
1 | <el-menu |
此处的 背景色是通过 getters 进行指定的,该 cssVar 的 getters 为:
1 | cssVar: state => variables, |
所以,我们想要修改 自定义主题 ,只需要从这里入手即可。
根据当前保存的 mainColor
覆盖原有的默认色值
1 | import variables from '@/styles/variables.scss' |
但是我们这样设定之后,整个自定义主题变更,还存在两个问题:
menuBg
背景颜色没有变化
这个问题是因为咱们的 sidebar
的背景色未被替换,所以我们可以在 layout/index
中设置 sidebar
的 backgroundColor
1 | <sidebar |
- 主题色替换之后,需要刷新页面才可响应
这个是因为 getters 中没有监听到 依赖值的响应变化,所以我们希望修改依赖值:
store/modules/theme
中:
1 | ... |
在getters
中:
1 | .... |
动态换肤方案总结
那么到这里整个自定义主题我们就处理完成了。
对于 自定义主题而言,核心的原理其实就是 修改scss变量来进行实现主题色变化
明确好了原理之后,对后续实现的步骤就具体情况具体分析了。
对于 element-plus:因为 element-plus 是第三方的包,所以它 不是完全可控 的,那么对于这种最简单直白的方案,就是直接拿到它编译后的 css 进行色值替换,利用 style 内部样式表 优先级高于 外部样式表 的特性,来进行主题替换
对于自定义主题:因为自定义主题是 完全可控 的,所以我们实现起来就轻松很多,只需要修改对应的 scss变量即可
那么在之后大家遇到 自定义主题 的处理时,就可以按照我们所梳理的方案进行处理了。