最近公司的项目需要实现在线切换主题色的功能,在这里记录一下实现的过程。
思路
换肤时主要需要考虑 antd 组件和自定义组件的主题色切换。对于 antd 组件,我们可以利用 less 变量可修改的特性;对于自定义组件则是利用 CSS 变量可在浏览器上解析的特性。
首先通过 antd-theme-webpack-plugin 插件自动生成与 antd 颜色相关的样式文件 color.less 并注入 html 页面中;接着引入 less,在切换主题色时,调用 less.modifyVars 函数动态修改 color.less 文件里面对应主题色变量的值,实现动态切换 antd 主题色的效果;最后定义与主题色变量对应的 CSS 变量,在编写自定义组件的样式时引用 CSS 变量,这样就可以将 modifyVars 对主题色变量的修改同步到 CSS 变量上,实现自定义组件的主题色切换效果。
首先看下实现的效果:
实现步骤
1. 安装插件
yarn add antd-theme-webpack-plugin
事实上,如果使用版本最新的 antd-theme-webpack-plugin 插件,会出现不兼容的问题。 经过一番搜寻,才知道该插件内部依赖的核心插件 antd-theme-generator 的版本是 1.2.8 ,这个版本对标的是 antd 4,本次项目中使用的是 antd 3。 做法可以是降低 antd-theme-webpack-plugin 版本或直接降低 antd-theme-generator 的版本。
最后我的做法是降低 antd-theme-generator 版本。在 package.json 中添加如下配置:
{
"dependencies": {},
"devDependencies": {},
"resolutions": { "antd-theme-generator": "1.2.1" },
}
2. webpack 配置
// webpack.config.js
var path = require('path');
const AntDesignThemePlugin = require('antd-theme-webpack-plugin');
// 主题配置
const themeOptions = {
antDir: path.join(__dirname, '../node_modules/antd'), // antd 目录
stylesDir: path.join(__dirname, '../src/assets/css'), // 本地css目录
varFile: path.join(__dirname, '../src/assets/css/var.less'), // less 变量文件
mainLessFile: path.join(__dirname, '../src/assets/css/empty.less'), // 项目中其他自定义的样式
themeVariables: ['@primary-color'], // 需要修改的 antd 变量
};
const webpackConfig = (memo, { env, webpack, createCSSRule }) => {
memo.plugin('antd-theme-webpack-plugin').use(AntDesignThemePlugin, [themeOptions]);
};
export default webpackConfig;
项目是基于 umi 的,在其他脚手架上配置的语法会所有不同。mainLessFile 文件可以是一个空文件。
3. html 页面配置
引入 antd-theme-webpack-plugin 插件生成的 color.less 和本地 less 文件。modifyVars 方法是基于 less 在浏览器中的编译来实现,所以需要引入less 文件,才能基于 less.js ,使用 modifyVars 来进行修改变量。
<body>
<link rel="stylesheet/less" type="text/css" href="./color.less" />
<script>
window.less = {
async: false,
env: "production",
javascriptEnabled: true
};
</script>
<script type="text/javascript" src="./less.min.js"></script>
<div id="<%= context.config.mountElementId %>"></div>
</body>
4. 覆盖 antd 主题色,设置 CSS 变量
这一步可以根据需要自定义项目的默认主题色,同时可以自定义根据主题色计算出来的的其他颜色变量,这里我自定义了不同透明度的主题色。
最重要的一步是定义 CSS 变量,在编写 UI 视图时,涉及到主题色将会引用这里的 CSS 变量。
// var.less
@import '~antd/lib/style/themes/default.less';
// 覆盖 antd 的默认主题色
@primary-color: #397EF0;
//主题色透明度
@primary-1: ~"fade(@primary-color, 10%)";
@primary-2: ~"fade(@primary-color, 20%)";
@primary-3: ~"fade(@primary-color, 30%)";
@primary-4: ~"fade(@primary-color, 40%)";
@primary-5: ~"fade(@primary-color, 50%)";
@primary-6: ~"fade(@primary-color, 60%)";
@primary-7: ~"fade(@primary-color, 70%)";
@primary-8: ~"fade(@primary-color, 80%)";
@primary-9: ~"fade(@primary-color, 90%)";
:root {
--primary-color: @primary-color;
--primary-color-1: @primary-1;
--primary-color-2: @primary-2;
--primary-color-3: @primary-3;
--primary-color-4: @primary-4;
--primary-color-5: @primary-5;
--primary-color-6: @primary-6;
--primary-color-7: @primary-7;
--primary-color-8: @primary-8;
--primary-color-9: @primary-9;
}
5. 定义视图,引用 CSS 变量
// skinPage.js
import React, { Component } from 'react';
import styles from './index.less';
import { Button } from 'antd';
export default class SkinPage extends Component {
render() {
return (
<div className={styles.skinWrapper}>
<Button type="primary">切换主题色</Button>
<div className={styles.skinWrapperInner}>
<div />
<div className={`${styles.block} bg1`}>主题色</div>
<div className={`${styles.block} bg2`}>主题色 - 透明度 90%</div>
<div className={`${styles.block} bg3`}>主题色 - 透明度 80%</div>
<div className={`${styles.block} bg4`}>主题色 - 透明度 70%</div>
<div className={`${styles.block} bg5`}>主题色 - 透明度 60%</div>
<div className={`${styles.block} bg6`}>主题色 - 透明度 50%</div>
<div className={`${styles.block} bg7`}>主题色 - 透明度 40%</div>
<div className={`${styles.block} bg8`}>主题色 - 透明度 30%</div>
<div className={`${styles.block} bg9`}>主题色 - 透明度 20%</div>
</div>
</div>
);
}
}
编写视图对应的样式:
.skinWrapper {
text-align: center;
.skinWrapperInner {
display: flex;
justify-content: center;
flex-wrap: wrap;
.block {
width: 300px;
height: 300px;
padding: 16px;
color: aliceblue;
font-weight: bold;
font-size: 16px;
}
:global {
.bg1 {
background: var(--primary-color);
}
.bg2 {
background: var(--primary-color-9);
}
.bg3 {
background: var(--primary-color-8);
}
.bg4 {
background: var(--primary-color-7);
}
.bg5 {
background: var(--primary-color-6);
}
.bg6 {
background: var(--primary-color-5);
}
.bg7 {
background: var(--primary-color-4);
}
.bg8 {
background: var(--primary-color-3);
}
.bg9 {
background: var(--primary-color-2);
}
}
}
}
编写样式时,使用全局的 CSS 变量,而不能使用 less 变量。例如不能写成这样:
.bg9 {
background: @primary-color;
}
二者的区别在于:CSS 变量在浏览器中进行本机解析。而 less 变量在项目编译时会被编译成 CSS ,最终输出到浏览器时所有变量都转换为了值。
这意味着如果自定义组件的样式引用的是 CSS 变量,切换主题色修改 less 变量时,浏览器会解析出新的 CSS 变量值,所有引用了该 CSS 变量的样式会跟着发生改变;如果定义组件的样式引用的是 less 变量,这个变量在编译时就转化为值了,无法实现动态切换主题色的效果。
6. 切换主题色
最后给视图的按钮添加一个点击事件,然后调用 modifyVars 方法,最终实现主题色切换效果。
<Button type="primary" onClick={ this.setThemeColor }>切换主题色</Button>
//设置主题颜色
setThemeColor = (themeColor, siderTheme) => {
window.less
.modifyVars({
'@primary-color': themeColor,
})
.then(() => {});
};