Antd 动态主题

1,655 阅读4分钟

Antd Dynamic Theme

demo 仓库

原贴链接

方案要点

  1. 使用 antd-theme-generator 插件
  2. 放弃 css-module (该插件不支持), 改用 CSS-BEM

步骤

1.安装依赖

yarn add @craco/craco craco-antd antd-theme-generator@1.2.6 -D
yarn add less antd@^3

由于项目使用的是 antd@3 , 需要安装指定的版本 antd-theme-generator@1.2.6 , 如果安装最新版会有报错.

如果使用 antd@4 , 则直接安装最新版本的 antd-theme-generator 即可.

2. 创建变量定义文件

src/styles/variables.less

这个文件包含了所有的 antd 定义变量(函数)以及项目内自定义的变量

// 需要先引入 antd 的变量
@import '~antd/lib/style/themes/default.less';

// 配置 antd 主题变量的默认值
@primary-color: rgb(42, 187, 103);

// 项目内部的变量(不要与antd的变量冲突)
@theme-color: rgb(42, 187, 103);

3.创建脚本文件

scripts/color.js

这个文件用来生成在生产模式下可运行的 less 样式文件

// antd 主题色文件生成脚本

const fs = require('fs')
const path = require('path')
const { generateTheme } = require('antd-theme-generator')

const themeVariables = ['@primary-color', '@theme-color']

// 由于 antd@3.x 的库不一样, 需要使用 1.2.6 版本
const options = {
  // antd 库的路径
  antDir: path.join(__dirname, '../node_modules/antd'),

  // 需要检索的所有 less 文件的根目录
  stylesDir: path.join(__dirname, '../src'),

  // 自定义变量的文件
  varFile: path.join(__dirname, '../src/styles/variables.less'),

  // 哪些变量值是需要动态修改的
  themeVariables,
}

// 由于插件提取的样式内容有很多是冗余的, 不使用插件默认的导出功能
// 需要先处理数据, 然后再自己导出文件
generateTheme(options)
  .then((less) => {
    console.log("less 文件内容提取成功");

    // 生成的 less 存放的位置
    const outputFilePath = path.join(__dirname, "../public/color.less");

    let content = less.replace(/([,{])\n/g, "$1");

    const arr = content
      .split("\n")
      .map((str) => str.trim())
      .filter((str) => {
        // 纯类样式或变量定义
        const isClassStyleOrVars =
          /^\.((?!\(\)).)*\{.*?\}$/gm.test(str) || /^@.*?:.*?;$/gm.test(str);

        // 字符串中不包含任意的主题变量
        const excludeThemeVars = themeVariables
          .map((k) => k.slice(1))
          .every((k) => !str.includes(k));

        // 其实还有其他的冗余内容, 但是影响不大
        return !(isClassStyleOrVars && excludeThemeVars);
      });

    content = arr.join("\n");
    fs.writeFileSync(outputFilePath, content);

    console.log("主题样式文件编译成功");
  })
  .catch((error) => {
    console.log("Error", error);
  });

4.打包配置

目的: 让框架支持 less ; 并引入 antd 的样式文件(less)

由于项目是基于 create-react-app 生成的, 并且不想使用 yarn eject ,所以使用了 craco 进行自定义配置.

当然也可以使用其他的方式, 只要保证能正常使用 antd 组件即可.

新建 craco.config.js

const CracoAntDesignPlugin = require("craco-antd");

module.exports = {
  plugins: [
    // antd 按需加载(不使用这个插件, 使用全局引入 antd 样式也可以)
    {
      plugin: CracoAntDesignPlugin,
      options: {
        babelPluginImportOptions: {
          libraryName: "antd",
          libraryDirectory: "es",
          style: true,
        },
      },
    },
  ],
};

修改 package.json

{
  "scripts": {
    "dev": "node ./scripts/color.js && craco start",
    "build": "node ./scripts/color.js && craco build",
    "color": "node ./scripts/color.js",
  },
}

先运行项目, 检测是否配置成功

yarn dev

5.修改 index.html 入口文件

public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
  </head>
  <body>
    <!-- antd theme less -->
    <link rel="stylesheet" type="text/less" href="%PUBLIC_URL%/color.less" />
    <script>
       // https://less.bootcss.com/usage/#browser-usage-setting-options
       window.less = { javascriptEnabled: true, logLevel: 3 }
    </script>
    <!-- antd theme less end -->

    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

注意 引入的 less 标签需要写在 body 标签下, 否则会被动态引入的样式文件覆盖(CSS 权重的问题)

6.在项目入口文件引入 less

scr/index.jsx

import React from 'react';
import ReactDOM from 'react-dom';

// 引入 less, 初始化主题配置
import 'less'

import './index.css';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

7.运行查看效果

yarn color
yarn dev

运行 yarn color 之后, 会生成 public/color.less 文件, 然后在运行的项目中, 初始化时, 会自动根据这个 less 文件生成一个对应的 css 样式标签

8.动态修改主题色

使用 less.modifyVars() 即可修改 less 变量. 这里提供了一个组件供参考.

import React, { useCallback } from 'react'
import { Button, DatePicker, Pagination } from 'antd'
import less from 'less'

const colors = ['#a12356', '#0215a6', '#f120a1']

const ThemeSetter = () => {
  const onColorChange = useCallback((color) => {
    less.modifyVars({
      '@primary-color': color,
      '@theme-color': color,
    })
  }, [])

  return (
    <>
      <h1>color setter</h1>
      {colors.map((c) => (
        <button key={c} onClick={() => onColorChange(c)}>
          {c}
        </button>
      ))}

      <hr />
      <Button>A</Button>
      <Button type='primary'>A</Button>
      <DatePicker />
      <Pagination total={100} />
    </>
  )
}

export default ThemeSetter

总结

接下来梳理一下原理. 首先, 确保项目可以编译 less 样式文件, 并正常引入了 antd.

核心文件 scripts/color.js ; public/color.less

  1. 根据配置项 varFile: path.join(__dirname, '../src/styles/variables.less') 找到所有需要跟踪的 less 变量
  2. antDir: path.join(__dirname, '../node_modules/antd') 配置项中指定 antd 库文件中遍历所有的 less 文件, 提取所有包含 需跟踪变量(主要是antd的) 的代码
  3. stylesDir: path.join(__dirname, '../src') 配置项中指定的路径遍历所有的 less 文件, 提取出所有包含 需跟踪变量(主要是自定义的) 的代码
  4. 生成 public/color.less 文件
  5. public/index.html 引用了 public/color.less ; 并在页面初始化时, 通过 less 编译出对应的 css 样式.
  6. 在页面中, antd 组件或使用了主题变量的组件先通过组件中引入的 less 文件已经编译的 css 样式首次渲染, 然后被public/color.less 编译出来的样式覆盖, 重新渲染. 达到修改颜色样式的效果
  7. 在此期间, 通过 less.modifyVars() 再次编译出新的样式, 再次覆盖样式后渲染. 以此实现动态修改主题色的功能

需要注意的是, 每次修改或新增一个自定义变量或样式文件, 都需要执行 yarn color 重新生成主题文件. 但是在开发环境中一般需要这么做, 只需要在打包前执行一次即可.