史上最简单的Ant-Design换肤方案

14,335 阅读12分钟

前言

线上预览地址:Dynamic-Antd-Theme-Demo 看起来是不是还可以,如果你知道如何使用,相信你会更觉得我不是一个标题党。

// 如何使用
import DynamicAntdTheme from 'dynamic-antd-theme';

render() {
    ...
    <DynamicAntdTheme />
    ...  
}

没错,就是这么简单,为了方便大家,我已经弄成了即插即用的组件~欢迎大家star + issue + pr。dynamic-antd-theme

为什么要做ant-design的换肤方案

这里是我很想说的,我还没有自负到或者说贪心到有能力去实现一个完美的合理的antd动态换肤方案。比如开源社区里的antd-theme-webpack-pluginantd-theme-generator就是很成熟的方案,利用webpack的方式在页面里通过引入less.js实现动态换肤,换的很彻底。

那么为什么我还要来搞一个所谓的动态换肤呢?理由如下:

  • 首先,上面两个方案并不能覆盖所有的场景,他所需要的入口文件index.html,作为nextjs党的我,就怎么找也找不到,所以在我的nextjs脚手架项目里就没有配置成功。
  • 其次,个人认为配置以及使用稍微繁琐以及额外引入太多内容。html文件必须引入less.js文件,因为这样才能使用window.less对主题进行动态更换。还有配置的时候各种css文件也是不太清楚意义。
  • 最后,也是最重要的。有人在我的文章里提问了,也就是与Next-Antd-Scaffold这个脚手架相关的文章,感兴趣的可以去看看前面我写的文章。小伙伴没有配置成功,又没有思路,怎么办呢,作为热心作者,肯定要帮忙解决呀~

这里安利一波,Next-Antd-Scaffold目前已经基本开发完成了,我个人也在用这个架子写项目,有很多小伙伴也在写项目,应该还可以,对Nextjs感兴趣的同学可以加入文章底部的微信群一起沟通哦~最主要的,你给我一个star,我尽心尽力尽我所能解决问题,😄

综上所述,个人花费了周末的时间,搞出来一个即插即用的antd换肤方案,一键安装直接使用。当然了,既然是这么简单,肯定也有弊端,毕竟我的主题是最简单的antd换肤方案而不是最完美的antd换肤方案,详细的听我娓娓道来。

实现过程

具体的实现过程,我将会从思路,解决方案、细节难点以及项目存在的弊端问题来进行说明。

这里我所有的例子都是通过Next-Antd-Scaffold来进行编写的~

思路

先来说思路,既然我想标新立异,最简单的antd的换肤方案,那肯定不能有复杂的配置过程以及表述不清的文档。当然,说实话,在没深读源码以及webpack机制之前,我还没有能力写出上面两个的那种水平的插件,哈哈😄。

所以,就只能另辟蹊径了。我的想法就是,能不能在系统运行过程中,通过类覆盖的方式来动态修改颜色,因为CSS加载机制是由上至下,同名会覆盖对应属性嘛,利用这个特性就来简单的尝试一下。

以button按钮为例,可以看到,我们设置的@primaryColor = #524c1a,类名是.ant-btn-primary,那么我们来进行覆盖。

// 样式变换代码
const styleDom = document.createElement('style');
styleDom.innerHTML = `
    .ant-btn-primary {
        border-color: #0000ff;
        background-color: #0000ff;
    }
`;
document.getElementsByTagName('head')[0].appendChild(styleDom);

可以看到,页面在未刷新的前提下,按钮实现了变色。因此,这个思路是可行的,接下来我们要考虑的就是细节问题了。

方案

通过上述实践,我们确定了思路,这里就来确定可行性方案。说实话,类名覆盖这种问题大部分前端开发应该都能想到,应该也有一部分人在用,毕竟换肤的需求是很多中后台系统的基本需求,不过即使是类名覆盖不同人也有不同的做法,而且类名覆盖的难点在于 —— 覆盖基础颜色还好,如果为每一个伪类元素如:hover :active :focus等都覆盖一个合适的color,不仅实现困难,而且工作量也很大。我这里想的就是,我来给大家实现一个类名覆盖的普适方案,你们再也不需要繁琐的一个页面一个页面去实现,或者各种修改css文件,只需要引入这个插件,自动把所有的类都覆盖好。

具体实现过程

  • 提取antd的所有color相关类

    我把antd-v3.19.0版本的css文件下载到了本地,大概有2W+行代码,从中我细心耐心的提取了所有@primaryColor相关颜色(包括:hover :focus :active)等。因为只保留了类下的color相关属性,所以精简到了900行代码左右。

    这里就简单截图,不给大家展示了,想看的话地址在这theme.css

    这里重点说明的是,:root{ ... }下面的几个colorVar,这也算是设计方案吧,因为这样,我只需要获取用户设置的颜色,然后生成相关颜色替换colorVar变量即可。不然的话我需要写一个正则,匹配所有的color,效率肯定没这个好

  • 动态获取用户颜色,然后进行替换

    这里其实很简单,就是改造我们要插入的style标签内容,具体代码如下:

    const cssVar = `
      :root {
          --primary-color: ${primaryColor};
          --primary-hover-color: ${hoverColor};
          --primary-active-color: ${activeColor};
          --primary-shadow-color: ${shadowColor};
      }
    `;
    // 给插入的标签赋id,避免多次插入<style>
    let styleNode = document.getElementById('dynamic_antd_theme_custom_style');
    if (!styleNode) {
      // avoid repeat insertion
      styleNode = document.createElement('style');
      styleNode.id = 'dynamic_antd_theme_custom_style';
      styleNode.innerHTML = `${cssVar}\n${cssContent}`;
      document.getElementsByTagName('head')[0].appendChild(styleNode);
    } else {
      styleNode.innerHTML = `${cssVar}\n${cssContent}`;
    }
    

    上面的标签内容是:<style>${cssVar}\n${cssContent}</style>cssVar是我们定义的颜色相关的几个变量,然后cssContent就是我提取的所有css代码toString()一下成为字符串变量。

    动态获取颜色这一块,我使用的是react-color这个插件,然后既然是换肤,肯定应该保存用户选择方案,所以搭配的就是localStorage进行客户端缓存,最后效果就是这样:

有人可能说了,你说了半天@primaryColor相关颜色,到现在还是只有一个@primaryColor,是不是在这扯犊子呢,😄别急,我都说了是最重要的地方,肯定是放在难点里了。

难点

这里值得跟大家分享一下,难点并不是从两万多行代码里抽离出所有与@primaryColor相关的属性,而是你需要通过选中的@primaryColor来动态生成浅颜色的@primaryHoverColor和深颜色的@primaryActiveColor

这里使用过的人应该都明白我的意思,antd的button按钮,a标签等等,:hover/:focus/:active/:visited等等这些属性都拥有自己的颜色,一些是与@primaryColor相比更浅,一些是与@primaryColor相比更深~具体看下面的动图,hover的时候颜色更浅一些,active的时候颜色更深一些

你不能单纯的把所有颜色相关属性统一变成一个颜色,虽然那样很简单,但是从体验上来讲就失去了一些用户体验感,那样的话还不如不做换肤了。所以接下来就详细说一下这块的实现过程。

去看antd的源码可以发现,他其实并没有相关:hover :active :focus颜色的详细设置,而是所有颜色都是通过@primaryColor转换而来的。

可以看到,它将@primaryColor分成了是个颜色级别,以level6作为分界线,<6的颜色会相比@primaryColor浅一些,适合:hover这种,>6的颜色会比@primaryColor深一些,适合:active这种。所有颜色都是通过colorPalette这个方法进行生成的,所以我们详细要说的就是这一块。

事先声明以及甩锅,我尽力去理解去尝试了,不过最后我实现的也只是简单的四种颜色,并没有像原来那样分成10个级别,不喜勿喷,欢迎感兴趣的提PR,弄的越来越好~

/**
 * 下面这些代码谨代表我个人的事先过程以及能力水平
 * 我没仔细看官方实现,所以antd肯实现的更高级
 **/
// 获取hover的浅颜色
function getHoverColor (color, index = 5) {
  return tinycolor.mix(
    '#ffffff',
    color,
    currentEasing(index) * 100 / primaryEasing
  ).toHexString();
}
// 获取active的深颜色
function getActiveColor (color, index = 7) {
  return tinycolor.mix(
    '#333333',
    color,
    (1 - (currentEasing(index) - primaryEasing) / (1 - primaryEasing)) * 100
  ).toHexString();
}

上面有两个函数,一个是获取浅颜色,一个是获取深颜色。两个函数内部调用的都是一个叫做tinycolor.mix的方法,并且我们看一下参数就非常容易理解了,这个mix方法其实就是让我们的主色@primaryColor跟另一个颜色去融合,比如跟#ffffff去融合,即使我不懂代码不懂计算机,学过画画的应该也都知道,如果有颜色的跟白色的混合,颜色会变浅,但是不会变成其他色系,也就是蓝色 -> 浅蓝色,红色 -> 浅红色等等,另一个也同理,跟#000000等黑灰色系融合,就会加深。接下来就看这个mix函数了

/* tinycolor-mix */
tinycolor.mix = function(color1, color2, amount) {
  amount = (amount === 0) ? 0 : (amount || 50);

  var rgb1 = tinycolor(color1).toRgb();
  var rgb2 = tinycolor(color2).toRgb();

  var p = amount / 100;

  var rgba = {
    r: ((rgb2.r - rgb1.r) * p) + rgb1.r,
    g: ((rgb2.g - rgb1.g) * p) + rgb1.g,
    b: ((rgb2.b - rgb1.b) * p) + rgb1.b,
    a: ((rgb2.a - rgb1.a) * p) + rgb1.a
  };
  return tinycolor(rgba);
};

这里就是将两个颜色和一个权重输入进去,最后输出一个rgba的颜色值。这里面的tinycolor是一个颜色相关的插件,感兴趣的可以去看看。

ok,然后那个权重又是什么东西啊,这里说实话我数学不是很好,就真看不懂了,反正他是一个贝叶斯曲线,就是为了让我们的颜色变换的更平滑~其他文章的解释大概也就是这样了,还有个图片:

这里更加深入的我就不说了,也说不明白,反正我是照猫画虎画出来的。需要强调一下的是曲线需要选中一个基线,antd的基线是如下:

/* basic-easiing */
const baseEasing = BezierEasing(0.26, 0.09, 0.37, 0.18);

// 主色基线
const primaryEasing = baseEasing(0.6);
// 融合颜色的基线
const currentEasing = index => baseEasing(index * 0.1);

也不知道我讲没讲清楚,上述繁杂的一系列操作过后,你就能根据你输入的主色生成对应的相关主色系颜色值,然后进行cssVar替换即可~

其他特性

你可以通过如下方式进行直接使用:

npm install dynamic-antd-theme
or
yarn add dynamic-antd-theme

组件可设置属性如下:

组件使用起来确实称得上史上最简单了,最后的效果说实话也超出了我最初的想法,真的还挺不错的。

弊端和遗留问题

  • 能力精力有限,只做了@primaryColor相关的覆盖
  • 其他覆盖仍然需要:global或者组件内覆盖这种方式
  • 没有做所有组件的效果测试,可能存在某些场景效果出现偏差,提issue会及时解决
  • 会向项目内暴力插入一段900行左右的<style>标签

紧急更新

颜色计算反馈

这篇文章发布之后,评论区出现了蚂蚁金服的伙伴,给出了人家已经开源的color计算插件,哈哈,我这费时费力的,就当是自己研究了一遍吧。后续可能会替换成ant-design官方计算出来的color,为了更接近原版~**

部分局限性问题

这里要说的就是,比如我设置了主题色,antd组件样式的主题色确实修改了,但是如果是我本地的css样式,其实是没有被修改的。比如我有一个Header组件,初始化背景色跟主题色一致,但是我更换主题色的时候,只变换了antd组件的主题色,并没有更换我这个Header组件的主题色,这样看起来就很突兀~如下所示:

更为详细的请见下方评论,感谢 @myran 掘友的点评

得知这个场景之后,我想到了解决方案,动态添加了一个属性:themeChangeCallback,传入一个函数,参数是改变后的主题色,做一些覆盖我们本地样式的内容就可以了~

 // 主题色修改过后把系统名称的背景色更换 
 themeChangeCallback(color) {
   document.getElementById('sys_name').style.backgroundColor = color;
 }
 ...
 <DynamicAntdTheme
    style={themeStyle}
    placement='bottomLeft'
    themeChangeCallback={this.themeChangeCallback}
 />

嗯,这样看起来就完美多了,还是希望大家多提意见,如果自己有想法就PR,没想法发现问题就评论或直接issue。都是可以哒~

兼容性问题

因为平时开发没有兼容IE的习惯,所以这个组件不支持IE浏览器,不过群里一些小伙伴说自己的项目还是需要兼容IE的,没办法,更新一下,兼容IE了。

目前最新版是v0.2.4

总结

有的人看完可能觉得没什么技术含量,就是做了很多重复大量的css操作来覆盖类名而已,说实话,我也承认,但是我觉得有一句名言说的好:这个世界上本没有路,走的人多了,就变成了路。这个方案也同样如此,每个人都愿意在自己的项目里进行大量的复杂类替换,却不愿意嫌麻烦去弄一套通用antd覆盖类文件,而我只是愿意把路给大家走出来,仅此而已。并不是有什么技术含量的插件,我也承认,哈哈😄。

另外,希望大家能多给star,多提issue,为什么呢,因为想做好的话一个人肯定是能力有限的,不可能把所有组件所有场景都是适配好,如果大家在使用的时候能告诉我哪个版本那个组件效果不对了,我可以及时的修改上线。另外如果感兴趣,也可以多提pr,一起维护。当前版本支持 antd version <= 3.19.0的绝大多数组件效果。

如有问题,及时联系,谢谢🙏。点star不迷路dynamic-antd-theme

Next.js小交流群地址: