antd-theme-webpack-plugin + Less + CSS 变量实现在线换肤

1,967 阅读4分钟

最近公司的项目需要实现在线切换主题色的功能,在这里记录一下实现的过程。

思路

换肤时主要需要考虑 antd 组件和自定义组件的主题色切换。对于 antd 组件,我们可以利用 less 变量可修改的特性;对于自定义组件则是利用 CSS 变量可在浏览器上解析的特性。

首先通过 antd-theme-webpack-plugin 插件自动生成与 antd 颜色相关的样式文件 color.less 并注入 html 页面中;接着引入 less,在切换主题色时,调用 less.modifyVars 函数动态修改 color.less 文件里面对应主题色变量的值,实现动态切换 antd 主题色的效果;最后定义与主题色变量对应的 CSS 变量,在编写自定义组件的样式时引用 CSS 变量,这样就可以将 modifyVars 对主题色变量的修改同步到 CSS 变量上,实现自定义组件的主题色切换效果。

首先看下实现的效果:

主题色.gif

实现步骤

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(() => {});
  };