electron+vue+iview实现动态切换主题

3,149 阅读7分钟

前言

刚刚搞完的一个项目。electron写的一个客户端软件,主要用来给公司客服与访客聊天用的。

做出第一个版本后,客服人员试用了几天,给我说,能不能加几个主题选,老是一个颜色有点腻。我脸上笑呵呵,心里一万那啥跑过(我勒个去,早干嘛不说,你是不知道我得改多少东西吗)。不过,既然需求提出来了,我就只能搞搞了。

一开始没想这个东西有多难。主题切换不就是样式切换嘛,我多搞几套内置样式就好了。本着这样得想法,很快就做出来了。

最早的实现方案,固定多几套主题

iview是vue得一个ui框架,css用的是less。在项目里头新建一个theme.less的文件,然后在main.js中引入

main.js-----------
import iView from 'iview';
import 'iview/dist/styles/iview.css'; 
import './theme.less'; //在iview的样式后面引入
theme.less--------
// 默认样式
  .default-theme{
    @mainColor:#16c497;
    @mainColorAct:rgba(23, 206, 159,0.3);
    .theme(@mainColor,@mainColorAct);
  }
  //蓝色
  .lan-theme{
    @mainColor:rgba(58, 95, 157,1);
    @mainColorAct:rgba(58, 95, 157,0.3);
    .theme(@mainColor,@mainColorAct);
  }
  //红色
  .red-theme{
    @mainColor:rgba(209, 62, 57,1);
    @mainColorAct:rgba(209, 62, 57,0.3);
    .theme(@mainColor,@mainColorAct);
  }
  //天空蓝
  .qlan-theme{
    @mainColor:rgba(86, 177, 252,1);
    @mainColorAct:rgba(86, 177, 252,0.3);
    .theme(@mainColor,@mainColorAct);
  }
  .theme(@mainColor,@mainColorAct){
    @import '~iview/src/styles/index.less';
    @primary-color: @mainColor; 
    .section{
      .sidebar{
        background: @mainColor;
      }
    }
    .title .btn > p:hover {
      color: @mainColor;
    }
    .title .openurl{
      img{
        filter: drop-shadow(85px 0 @mainColor);
      }
      &:hover img{
        filter: drop-shadow(85px 0 #56b1fc);
      }
    } 
    .chatMain{
      .charRenshuList{
        h2{
          color: @mainColor;
        }
        p{
          &.hover{
            color:@mainColor;
            // background: @mainColorAct;
          }
        }
      }
      .charWindowList{
        .infoXieru{
          .gongju{
            i{
              &:hover{
                color: @mainColor;
              }
            } 
          }
        }
      }
    }
    .charInfo {
      .item{
        h1{
          color: @mainColor;
          background: @mainColorAct;
        }
      }
      .ivu-collapse > .ivu-collapse-item > .ivu-collapse-header {
        background: @mainColorAct;
      }
    }
    .charInfo .kjyy li .icon-bianji, .charInfo .kjyy i{
      color: @mainColor;
    }
  
    .kjyy .edit_kjyy .titles{
      background: @mainColor;
    }
  
    .kjyy .edit_kjyy .content .caozuo span:nth-child(2) {
      background: @mainColor;
      border: 1px solid @mainColor;
    }
  
    .el-button:active ,.el-button:hover {
      color: @mainColor;
      border-color: @mainColor;
      outline: 0;
    }
  
    .chatMain .charWindowList .infoXieru .sumBtn > span{
      background: @mainColor;
    }
  
  
    /*定义滑块 内阴影+圆角*/
    ::-webkit-scrollbar-thumb {
      border-radius: 10px;
      background-color: @mainColor;
    }
    ::-webkit-scrollbar {
      width: 5px;
      height: 5px;
    }
  
    .infoShow{
      .char{
        &.charOther{
          .info{
            &::before{
              border-left: 1px solid @mainColor;
              border-bottom: 1px solid @mainColor;
            }
          }
        }
        &.charOwn{
          .info{
            background: @mainColor;
            &::before{
              border-right: 1px solid @mainColor;
              border-top: 1px solid @mainColor;
              background: @mainColor;
            }
          }
        }
        .info{
          border: 1px solid @mainColor;
          .time{
            color: @mainColor;
          }
        }
      }
    }
  
    .chatMain .charRenshuList p > .ivu-icon {
      background: @mainColor;
    }
  
    .settingBox .list_title .hover {
      color: @mainColor;
    }

    .title>.ivu-icon{
      color: @mainColor;
    }
  
  }

界面点击不同的主题按钮,更换class名。这应该是主题类需求最古老最常见的做法了。百度上只要一搜前端切换主题、iview切换主题、elementui切换主题,你能搜出来的,90%就是这种方案。它所存在的优缺点:

  • 优点
    • 实现容易,就是简单class类名切换
  • 缺点
    • 本地会存在多套主题样式,增加了代码体积。
    • 主题切换不够灵活,不能想用什么颜色就用什么颜色

动态主题切换

功能完成后,客服用了一个月,又跑来说,这个能不能不要固定几套主题,就是我想换什么颜色就什么颜色。我脸上笑呵呵,心里一万那啥跑过(我勒个去,不太容易搞啊)。没办法,需求提了,还得接着搞搞。
我可能是个文科生的缘故,没啥思路搞这个。百度上搜搜,找到一个相同功能的,里头讲的很好,是个大神,他就是 花裤衩。一开始我是在SegmentFault网站看到他的文章手摸手,带你用vue撸后台 系列 ,后面发现在掘金也有,哈哈哈😁。

vue-element-admin动态换肤,引用下他的文章原话:

简单说明一下它的原理: element-ui 2.0 版本之后所有的样式都是基于 SCSS 编写的,所有的颜色都是基于几个基础颜色变量来设置的,所以就不难实现动态换肤了,只要找到那几个颜色变量修改它就可以了。 首先我们需要拿到通过 package.json 拿到 element-ui 的版本号,根据该版本号去请求相应的样式。拿到样式之后将样色,通过正则匹配和替换,将颜色变量替换成你需要的,之后动态添加 style 标签来覆盖原有的 css 样式。

虽然我是用的iview,但是和element也差不多,lessscss也差不多,这个思路也许在我这里也能行。写着写着,嗯不对,这里有个请求,是不是我每次换肤都要请求一遍他的最新样式回来,然后在替换,我要是不停的切换,不是会产生很多的网络请求吗?(当时脑子抽风,没想请求缓存😂),不行不行,绝对不行,我就换个主题,绝对不能去请求。

就这样,暂时放弃这个思路。又想到,css好像有个自定义属性,也叫css变量,利用它是不是能搞,也不废话,试它一试。

html {
  --theme: #ff0000;
}
a,p,h1,h2{color:var(--theme)};

简单试用了下css原生变量可行之后,就他娘的兴奋,less不是也用变量吗,我那css的变量替换调less的变量不就行了嘛。然后,就狠狠的打脸了,替换完立马编译报错,我勒个去,less颜色混合器根本不认这个原生变量。我幼小的心灵受到了打击,还能不能好好玩了...
没办法了,接着换别的想法,那我能不能手动写less的混合器呢,搞搞。

查看iview官方文档,发现主题变量与less的Fade函数(透明度)、shade函数(混合颜色与黑色)、tint函数(混合颜色与白色)。默认主题的颜色值:#2d8cf0。在这之前,先将16进制颜色值换成rgba值。在过滤出r、g、b、a的数值。

const value = val.replace('rgba(','').replace(')','').split(',');
var theme_obj = {
  theme:[...value].map(Number), //主题原色
  fade90:[...value].map(Number), //90%透明
  fade20:[...value].map(Number), //20%透明
  shade5:[...value].map(Number), //与黑色混合5%
  tint20:[...value].map(Number), //与白色混合20%
  tint80:[...value].map(Number), //与白色混合80%
  tint90:[...value].map(Number), //与白色混合90%
};

编写less函数

fade函数

theme_obj.fade90[3] = .9;
theme_obj.fade20[3] = .2;

shade函数

//转换rgb三个数值
for(let i=0;i<3;i++){
    theme_obj.shade5[i] = Math.ceil(theme_obj.shade5[i]-theme_obj.shade5[i]*0.05);
}

tint函数

//转换rgb三个数值
for(let i=0;i<3;i++){
    theme_obj.tint20[i] = Math.ceil(theme_obj.tint20[i]+(255*0.2)-(theme_obj.tint20[i]*0.2));
    theme_obj.tint80[i] = Math.ceil(theme_obj.tint80[i]+(255*0.8)-(theme_obj.tint80[i]*0.8));
    theme_obj.tint90[i] = Math.ceil(theme_obj.tint90[i]+(255*0.9)-(theme_obj.tint90[i]*0.9));
}

转换完成后设置html中css的原生变量。

for(let key  in theme_obj){
    document.documentElement.style.setProperty('--'+key, 'rgba('+theme_obj[key].join()+')');
}

渲染结果为:

html {
    --theme: rgba(215,29,17,1);
    --fade90: rgba(215,29,17,0.9);
    --fade20: rgba(215,29,17,0.2);
    --shade5: rgba(205,28,17,1);
    --tint20: rgba(223,75,65,1);
    --tint80: rgba(247,210,208,1);
    --tint90: rgba(251,233,232,1);
}

得到了相应得变量后,利用正则表达式替换iview.css得颜色值。

//把iview主题色#2d8cf0与之混合后的颜色值放到对象里
var theme = {
    theme:'#2d8cf0',
    fade90: 'rgba(45,140,240,.9)',
    fade20: 'rgba(45,140,240,.2)',
    shade5: '#2b85e4',
    tint20: '#57a3f3',
    tint80: '#d5e8fc',
    tint90: '#eaf4fe'
}
let data = 'iview的css';
let reg = null;
for(let key  in theme){
  theme[key] = theme[key].replace('(','\\(').replace(')','\\)'); //rgba颜色值的括号需要转义
  let reg = new RegExp(theme[key],"g");
  data = data.replace(reg,'var(--'+key+')').replace(/\f/g,'\\f'); //css中的图标'/fxxx'字样需要转义
}

完成后,将data的值复制到iview.theme.css中,放到public下,在public下的index.html中引入这个样式。修改vue.config.js文件,添加

configureWebpack:{
    externals: {
      vue: 'Vue',//不打包vue
      'view-design': 'iview'  //不打包iview
    }
},

思路总结

  • 使用css的原生变量,js可以随意修改
  • iview必须要手动引入,主要是css部分,不经过打包编译
  • 利用主题颜色及混合后的颜色替换掉iview.css中的颜色值,替换成css变量。

如何更加灵活

现在的想法就是:可以用js实现颜色的rgba与16进制相互转换,electron可以使用node的模块,意味着也可以读写文件。 实现转换后,调用fs模块读取iview.css的内容,正则表达式替换颜色值为css原生变量值,在写入到iview.css文件中。