阿里数据的umi + antd动态主题实践

随着嵌入阿里数据页面的需求增多,出现了一些要求较高的业务方,希望能指定阿里数据的主题色,来和他们的品牌色保持一致,让产品有更好的体验。于是我们开始了动态主题功能的调研,本来以为是个比较简单的事情,实际还是有遇到一些曲折的,不过最终效果挺满意的,符合我们的需求,同时很简单、很轻量。

一、现有方案

动手之前先调研了一波可选的方案,现有方案大致是两类:一类是编译构建的时候就有多份预设主题,在运行时只做样式切换,新增一类主题需重新构建发布;另一类是运行时可指定任意主题,新增一类主题几乎没有成本,很方便拓展。

1、编译多份预设主题

Class切换

编译出的CSS示例如下,通过给body设置theme-lighttheme-darkclass来切换主题。借助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.cssindex-dark.css,来达到切换主题的效果。但这个方案也不友好,需要侵入构建配置,然后构建耗时会增加。

在编译阶段预置多份主题的方案,实现还算简单,原理也很好理解,但都有存在明显的弊端,且拓展新主题会有成本。

2、运行时指定任意主题

如果要低成本拓展主题,就必须借助运行时的能力了,实现复杂度由难到易,大致有下面三种方案:

CSS模板+运行时替换

这类方案需要先准备一个css模板文件,然后根据用户指定的颜色值注入到模板里去,动态产出最终的css文件,Element-ui便是使用了这个方案,在切换颜色时,会把色值作为参数传给后端来得到新的css文件:

Element-ui动态主题.png

当然这个工作纯前端也可以做,方案还是挺不错的。

Less变量

这个方案借助less.js的运行时能力,来实现类似上面方案的效果,css模板在这里成了less文件,运行时替换由less.jsmodifyVars提供,我们的编码工作量变少了。使用大概是这样:

  • html主文档增加项目的less文件和less.js
<link rel="stylesheet/less" type="text/css" href="/styles.less" />
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></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组件主题

跟着文档指引,我们升级了antd4.17.1-alpha.1版本,importantd/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 变量教程:www.ruanyifeng.com/blog/2017/0…
Element-ui换肤方案:github.com/ElemeFE/ele…
聊一聊前端换肤:segmentfault.com/a/119000001…

猜你喜欢

转载自juejin.im/post/7034099025917771790