随着嵌入阿里数据页面的需求增多,出现了一些要求较高的业务方,希望能指定阿里数据的主题色,来和他们的品牌色保持一致,让产品有更好的体验。于是我们开始了动态主题功能的调研,本来以为是个比较简单的事情,实际还是有遇到一些曲折的,不过最终效果挺满意的,符合我们的需求,同时很简单、很轻量。
一、现有方案
动手之前先调研了一波可选的方案,现有方案大致是两类:一类是编译构建的时候就有多份预设主题,在运行时只做样式切换,新增一类主题需重新构建发布;另一类是运行时可指定任意主题,新增一类主题几乎没有成本,很方便拓展。
1、编译多份预设主题
Class切换
编译出的CSS示例如下,通过给body设置theme-light
或theme-dark
的class
来切换主题。借助Less/Sass的能力可以较方便批量处理,但弊端也很明显,CSS文件大小会倍增。
// index.css
.theme-light div {
color: #000;
}
.theme-dark div {
color: #fff;
}
构建多份CSS
对于class切换方案的弊端,我们可以通过构建多份css文件来解决,比如我们的less代码如下:
div {
color: @primary-color;
}
然后构建阶段指定@primary-color
为不同色值,来得到多份css产物:
// index-light.css
.div {
color: #000;
}
// index-dark.css
div {
color: #fff;
}
在运行期间,通过切换加载index-light.css
和index-dark.css
,来达到切换主题的效果。但这个方案也不友好,需要侵入构建配置,然后构建耗时会增加。
在编译阶段预置多份主题的方案,实现还算简单,原理也很好理解,但都有存在明显的弊端,且拓展新主题会有成本。
2、运行时指定任意主题
如果要低成本拓展主题,就必须借助运行时的能力了,实现复杂度由难到易,大致有下面三种方案:
CSS模板+运行时替换
这类方案需要先准备一个css模板文件,然后根据用户指定的颜色值注入到模板里去,动态产出最终的css文件,Element-ui便是使用了这个方案,在切换颜色时,会把色值作为参数传给后端来得到新的css文件:
当然这个工作纯前端也可以做,方案还是挺不错的。
Less变量
这个方案借助less.js
的运行时能力,来实现类似上面方案的效果,css模板在这里成了less文件,运行时替换由less.js
的modifyVars
提供,我们的编码工作量变少了。使用大概是这样:
- html主文档增加项目的less文件和
less.js
<link rel="stylesheet/less" type="text/css" href="/styles.less" />
<script src="https://cdn.jsdelivr.net/npm/less@4.1.1"></script>
- 使用
modifyVars
修改颜色变量
window.less.modifyVars({
'@primary-color': '#0035ff'
})
该方案的弊端不少,一是要引入40kb的less.js
运行时;二是对编码和打包一定侵入,我们需要把所有less文件加到html中;三是主题切换不会很流畅,因为modifyVars
后涉及到less文件的重新编译。
CSS变量
CSS变量是CSS3标准的新功能,通过它做主题很简单,比如下面是我们的样式:
div {
// 使用--primary-color变量的颜色,无值则用默认的#000
color: var(--primary-color, #000);
}
切换主题时,只需通过document
的API设置新的颜色值即可
document.body.style.setProperty('--primary-color', '#fff')
感觉这个方案是上述所有方案里最简单的,功能强大也很好理解。
此外也有类似类似styled-components的css in js方案,但对项目改动太大了,且指定antd组件的主题会很麻烦,可以直接忽略。
二、我们的选择
编译多份预设主题类型的方案首先被我们拍死了,我们对低成本拓展的要求比较高,否则业务方如果换主题色,我们还得跟着发版...
运行时指定任意主题的方案里,功能上都能满足我们的需求,其中CSS变量方案成本最低,且:
- 主流浏览器都已支持,然后我们产品的用户99%+都是chrome,兼容性可以不考虑;
- 然后我们本身已经使用了less,通过把
@primary: #ff6a00
改成@primary: var(--primary-color, #ff6a00)
可以很方便使用,不需要大量改动项目中已有的less文件; - 我们发现antd也已经支持了CSS变量动态指定主题;
完美,所以我们最终选择了CSS变量方案。
三、动手实践
1、修改global.less
把写死的less变量的值,改成CSS变量的方式,原先的值放入默认值
- @primary: #ff6a00;
- @primary5: #ff6a000d;
- @primary15: #ff6a0026;
- @primary75: #ff6a00BF;
+ @primary: var(--primary-color, #ff6a00);
+ @primary5: var(--primary-color-5, #ff6a000d);
+ @primary15: var(--primary-color-15, #ff6a0026);
+ @primary75: var(--primary-color-75, #ff6a00BF);
本地跑一下,没啥问题,颜色都正常
2、修改CSS变量
我们新增了一个theme.ts
文件,在入口会执行它的setupTheme
来使用URL参数上指定的主题色。
// theme.ts
import { getUrlParams } from '@/utils/utils';
export let primaryColor = '#ff6a00';
export let primaryColor5 = '#ff6a000d';
export let primaryColor15 = '#ff6a0026';
export let primaryColor75 = '#ff6a00BF';
export function setupTheme() {
const params = getUrlParams() as any;
if (params?.primaryColor) {
primaryColor = `#${params?.primaryColor.toLocaleLowerCase()}`;
primaryColor5 = `${primaryColor}0d`;
primaryColor15 = `${primaryColor}25`;
primaryColor75 = `${primaryColor}BF`;
}
document.body.style?.setProperty('--primary-color', primaryColor);
document.body.style?.setProperty('--primary-color-5', primaryColor5);
document.body.style?.setProperty('--primary-color-15', primaryColor15);
document.body.style?.setProperty('--primary-color-75', primaryColor75);
}
本地跑一下,指定primaryColor参数为其它色值,除了antd系列组件都已经生效了
3、指定antd组件主题
跟着文档指引,我们升级了antd
到4.17.1-alpha.1
版本,import
了antd/dist/antd.variable.min.css
,然后在theme.ts
里新增了ConfigProvider
来设置主题色,刷新下页面,antd系列组件也生效了,完美!打包发到预发感受下...
发到预发后问题来了,antd只有部分组件主题生效了,有些组件如分页器没生效,调试发现antd样式有冗余,部分组件的CSS变量版样式被覆盖了,我们的umi.css
大概是这样:
// CSS变量版的样式在前,被后面的覆盖了,导致指定的主题色没生效
.ant-pagination-item-active {
border-color: var(--ant-primary-color);
}
.ant-pagination-item-active {
border-color: #ff6a00;
}
我的第一反应是修改下CSS顺序,让CSS变量版的样式在后,折腾了一番发现不行,发现webpack打包后的CSS顺序不和import顺序一致,最后看到这个issue就决定放弃了,webpack成员表示他们不能保证这个顺序。
4、关闭antd的按需加载
如antd的文档所写,antd动态主题需要关闭按需加载:
注:如果你使用了 babel-plugin-import,需要将其去除。
看来只能这样了,从umi文档得知,@umijs/plugin-antd
会对antd做按需引入,翻了下源码目前无法配置关闭,我们于是提了个PR,钉钉私聊了期贤,期贤了解了背景后很快合并了PR并发了新版本,非常赞。
接着我们升级了@umijs/preset-react
,在.umirc.ts
里新增了disableBabelPluginImport
配置禁用了按需加载,可喜的是打包后的产物竟然还有一些减少,看来大量使用antd组件时没必要开启按需加载...
antd: {
disableBabelPluginImport: true
}
应该稳了,发到预发再感受一下...
分页器是好了,但按钮的居然坏了,一调试发现还是因为冗余样式,CSS变量样式被覆盖导致,最终定位到是@ant-design/pro-layout
引入的
5、指定pro-layout主题
原因是pro-layou
有引用lib|es 目录下的 less 文件,按antd文档所说,需要在less中注入@root-entry-name: variable
变量,在.umirc.ts
中配置如下:
theme: {
'root-entry-name': 'variable'
}
再次发到预发,OK,全妥了!
注意在theme里配置了'root-entry-name': 'variable'后,不能再配置'primary-color'的值
四、总结
可以看到,基于CSS变量的动态主题方案还是比较简单的,对于已经使用less的项目,接入成本很低,方案轻量好拓展,并且antd也已经给了官方支持,进一步降低了使用成本,相信以后会成为更多人的选择。
拓展阅读:
CSS 变量教程:https://www.ruanyifeng.com/blog/2017/05/css-variables.html
Element-ui换肤方案:https://github.com/ElemeFE/element/issues/3054
聊一聊前端换肤:https://segmentfault.com/a/1190000018593994