theme: juejin
同一前端项目实现多套主体色设置
应用背景
昨天一个前端同事问我,为什么他本地项目读取到的管理端租户的主题色配置是错的。我倒还从没深究过这个问题,即同一个前端项目中如何读取不同的主题色配置。正好遇到了这个机会,把目前项目中的解决方案记录下,方便自己理解消化,也算扩宽思路。
这个产品是一个多租户场景的管理系统,因此在登陆时会根据当前域名请求后台租户信息接口,得到对应的系统主题色配置,存入本地缓存。每一个租户的主题色都不一样。
租户A的主题色是黄色,租户B的主题色是红色,但是前端的代码都是同一套。究竟是怎么实现登录不同的租户系统,加载不同的主题色的?
系统的按钮/侧边栏/背景/按钮等都会根据主题色进行设置,这次就着眼于按钮是如何设置主题色的,触类旁通,会了一个,其他的都迎刃而解了。
前端项目框架为vue2.0,组件为IView3.5.4
当我声明type="primary"时,颜色从哪来?
前端组件用的IView,好像还是老版本。
先直接说实现方法:全局覆写 .ivu-btn-primary 这个类的样式
但其实一开始我想知道的是,当我声明按钮组件Button的type为primary时,这个颜色值#2d8cf0是在哪设置的。
翻了一圈官方文档,好像也没找到设置primary颜色的api,那就看下源码吧。
iview/src/styles/components/button.less
less
.@{btn-prefix-cls} {
.btn;
.btn-default;
/**
* 省略无关代码
**/
&-ghost&-primary{
color: @primary-color;
&:hover{
color: tint(@primary-color, 20%);
background: fade(tint(@primary-color, 95%), 50%);
}
}
}
不是很熟悉less,不过按照关键词搜索到-primary这个选择器
得了,没有显式的import语句,那这个@primary-color应该是一个全局变量了
问了下神奇海螺chatGPT,给我指路到了这儿
iview/src/styles/custom.less
找一圈算是把这个primary-color的定义位置找到了
其实知道了按钮的样式类名.ivu-btn-primary 就已经可以做这个需求了
我折腾看了下源码,主要是加深下印象
毕竟实现的时候,又不能真的侵入去修改组件的源码
但知其然,知其所以然
页面渲染时,是如何读取主题色并全局覆盖的?
我根据primary-color这个关键词在项目中搜了下,果不其然找到了
在项目全局的预处理.less文件中,有一个与primary-color相关的theme.less文件,位于src/styles/theme.less
src/styles这个目录下存放了许多自定义的预处理样式文件.less,除了按钮样式覆盖外还有许多其他内容,不详细介绍了
less
@import "~view-design/src/styles/index.less";
@import "./reset.less";
@import "./loading.less";
//https://github.com/view-design/ViewUI/blob/master/src/styles/custom.less
@primary-color: #0072bc;
.ivu-btn-primary {
color: #fff;
background-color: var(--main-button-color, #0072bc)!important;
border-color: var(--main-button-color, #0072bc)!important;
transition: all .2s ease-in!important;
}
.ivu-btn-primary:hover {
color: #fff;
background-color: var(--main-button-color, #0072bc);
border-color: var(--main-button-color, #0072bc);
opacity: .8;
}
// 省略其余
.ivu-btn-primary,找到这个样式的覆写了
在这里又引用了一个自定义的CSS属性--main-button-color,如果 --main-button-color 自定义属性没有定义,则使用默认的颜色值 #0072bc。
另外加上了 !important 来强制覆盖其他样式
和我想的实现方法大致差不多一样了
在 CSS 中,以两个连字符(--)开头的变量名是 CSS 自定义属性(CSS Custom Properties)的语法。在 CSS Custom Properties 中,你可以自定义变量,并在样式规则中使用这些变量。
定义自定义属性的语法如下:
--variable-name: value;
使用自定义属性的语法如下:
selector { property: var(--variable-name); }
--感谢chatGPT的馈赠
覆盖的样式已定义好了,挂载的步骤是怎么做的?
搜索下这个自定义的main-button-color属性,bingo,找到了!
在vue项目根目录下的index.html,也就是入口html的head标签里,找到了之前一位老哥自定义的script标签
xml
<!-- index.html -->
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title></title>
<meta name="keywords" content="" />
<meta name="description" content="" />
<link
rel="shortcut icon"
id="favicon"
href="./favicon.ico"
type="image/x-icon"
/>
<!-- <style id="css-vars">
:root {
--main-sub-menu-color: #0072bc;
--main-sub-menu-title-color: #0072bc;
--main-menu-active-color: #2D8cF0;
--main-menu-active-head-color: #2D8cF0;
--main-menu-title-hover-color: #2D8cF0;
}
</style> -->
<script src="./css-vars.js"></script>
</head>
<!-- 省略若干代码 -->
</body>
甚至可以看到,刚开始项目未应用多主题色的时候,这些自定义属性值是写死的,注释都还保留着呢
后来用一段自定义的js代码来设置这些自定义属性了。看看这个css-vars.js的代码
javascript
// css-vars.js
// 读取本地缓存,获取主题色信息
var platInfo = localStorage.platInfo ? JSON.parse(localStorage.platInfo) : {};
var themeBtnColor = platInfo.style.buttonColor || '';
// 拼接 style字符串
var styleStr = `
<style id="css-vars">
:root {
`
/**
* 省略其余和按钮颜色不相干的代码
**/
// 设置自定义CSS属性
if (themeBtnColor) {
styleStr += `
--main-button-color: ${themeBtnColor};
`
}
styleStr += `
}
<\/style>
`
document.write(styleStr)
说白了,这段js代码做的事情就是在进行手动拼接,替换原来样式中写死的自定义属性的颜色,
拼成如下的格式,然后渲染到页面上。
在我的探寻过程中,我只关心--main-button-color这一个属性
但其实到了这一步,已经发现了,侧边栏/菜单的颜色都是取自定义属性来的
xml
<style id="css-vars">
:root {
--main-sub-menu-color: #0072bc;
--main-sub-menu-title-color: #0072bc;
--main-menu-active-color: #2D8cF0;
--main-menu-active-head-color: #2D8cF0;
--main-menu-title-hover-color: #2D8cF0;
--main-button-color: #2D8cF0;
}
</style>
除此之外,css-vars.js还封装了一个函数,这个函数实现的逻辑和挂载的逻辑是几乎一模一样
javascript
function setThemeColor () {
console.log('setThemeColor')
var style = document.getElementById('css-vars')
var platInfo = localStorage.platInfo ? JSON.parse(localStorage.platInfo) : {}
var themeBtnColor = platInfo.style.buttonColor || ''
var styleStr = `
:root {
`
/**
* 省略其余和按钮颜色不相干的代码
**/
if (themeBtnColor) {
styleStr += `
--main-button-color: ${themeBtnColor};
`
}
styleStr += `
}
`
style.textContent = styleStr
}
每当用户手动更改了主题色时,前端再去请求一次后端主题色的接口,拿到更新后的数据,调用这个函数,就可以立即重新渲染。
赛后总结
总结一下实现这个功能的流程
- 声明了一个自定义的样式预处理.less,在这个文件中声明类.ivu-btn-primary,覆盖IView原生的样式
- 在自定义的.ivu-btn-primary样式中,使用了CSS的自定义属性--main-button-color
- 在项目的根html中,添加一个全局样式.root,这个样式中定义了自定义属性--main-button-color的具体变量值
- 页面准备渲染时,执行自定义的js代码段,读取本地缓存中的主题色配置,并拼接成style格式的字符串,覆盖掉原root的样式
有没有忘了些什么?
到了这,好像还是不能解答我那前端同事的疑惑:为什么本地调试时,取到的主题色不对?
好吧,最后发现这其实是数据问题。
他本地调试的时候设置了错误的token,而后端的接口是根据token解析返回不同系统的主题色的
怎么感觉像是白忙活一趟?算了,有所收获,善莫大焉~