npm package: @mega-apps/postcss-theme-helper
Description: Postcss theme helper
Author: sunzhifeng
Latest version: 1.0.0-rc.9

# @mega-apps/postcss-theme-helper

PostCSS 主题助手

是基于PostCSS框架提供的PostCSS插件，主要解决主题切换的问题。

<p align="left">
  <a href="https://www.npmjs.com/package/@mega-apps/postcss-theme-helper"><img src="https://img.shields.io/npm/dm/@mega-apps/postcss-theme-helper" alt="Downloads"></a>
  <a href="https://www.npmjs.com/package/@mega-apps/postcss-theme-helper"><img src="https://img.shields.io/npm/v/@mega-apps/postcss-theme-helper" alt="Version"></a>
  <a href="https://www.npmjs.com/package/@mega-apps/postcss-theme-helper"><img src="https://img.shields.io/npm/l/@mega-apps/postcss-theme-helper" alt="License"></a>
</p>


## 特性

- [x] 支持主题颜色替换
- [ ] (路线)支持主题字体替换
- [ ] (路线)支持主题布局替换
- [ ] (路线)支持主题特效（动画）替换
- [x] 与前端JS框架无关（支持React, Vue2/3, Angular, SolidJS ...)
- [x] 支持对Vue ElementUI Ver2.x 组件的主题颜色替换（可选用：内置预设）
- [x] 支持对Vue ElementUI-Plus Ver1.x 组件的主题颜色替换（可选用：内置预设）
- [x] 支持对Vue Ant Design Ver1.x 组件的主题颜色替换（可选用：内置预设）
- [x] 支持多主题组颜色替换及单独颜色替换
- [x] 支持CSS选择器过滤多种方式：如：数组包含，正则匹配，glob模式匹配，函数调用
- [x] 支持初始化默认匹配颜色，用于替换默认颜色
- [x] 支持针对颜色值的严格模式和非严格模式指定，例如：匹配颜色，rgb(255,255,255)，rgba(255,255,255,0.5)在严模式下，只会处理完全匹配的一种，使用非严格模式，都会处理。
- [x] 支持指定挂载的DOM节点的选择器，
- [x] 支持指定颜色变量的包裹器，如：`:root`, `div`, `.class`, `#id` 等；
- [x] 支持排除指定的CSS选择器
- [x] 支持排除指定的颜色CSS属性，如：`color`, `background-color`, `border-color`
- [x] 支持调试模式，调试模式下，会在控制台输出颜色变更的信息
- [x] 提供[`Webpack`](https://webpack.js.org)插件，自动注入主题替换代码
- [x] 提供[`Vite`](https://vitejs.dev/)插件，自动注入主题替换代码
- [x] 性能较 `@mega-apps/webpack-theme-color-replacer`， `webpack-theme-color-replacer`，大幅度提升。提升较基线至少 `100X`
- [x] 代码基于`TDD`、`E2E`模式开发，版本迭代，质量有保证

## 安装

```bash
yarn add @mega-apps/postcss-theme-helper --dev

# 或使用 pnpm
pnpm add -D @mega-apps/postcss-theme-helper
```

## 使用说明


### Vite 项目

#### 配置示例

- `vite.config.js` 文件
  ```js
  // vite.config.js
  
  import { defineConfig } from 'vite';
  
  // 引入主题助手Unplugin
  import themePluginBuilder from '@mega-apps/postcss-theme-helper/unplugin';
  
  // 定义配置
  export default defineConfig({
    // 加载主题的vite插件
    plugins: [themePluginBuilder.vite({debug: false})],
  });
  ```

- `postcss.config.js` 文件
```js
const themePlugin = require("@mega-apps/postcss-theme-helper");
const themeOptions = {
  debug: false,
  useShareColorVariableReplaceBuilder: true,
  colorVariableReplacer: [
    // 类似于CSS的解析，从上而下，最小优先级最高
    {
      matchColors: ["#409EFF","rgb(239, 68, 68)"],
      matchSelectors: [{ type: "RegExpMatch", data: [/.custom/i] }],
      initVarColors: ["red","blue"],
      strict: true,
      cssVarPrefix: "--eColor",
      wrapper: ":root",
      debug: false,
    },
    // {
    //   matchColors: ["black" ],
    //   matchSelectors: [{ type: "RegExpMatch", data: [/.my-color/i] }],
    //   initVarColors: ["yellow"],
    //   strict: true,
    //   cssVarPrefix: "--hiColor",
    //   wrapper: "div",
    // },
    // {
    //   matchColors: ["#0ea8d0"],
    //   matchSelectors: [],
    //   initVarColors: ["green"],
    //   strict: true,
    //   cssVarPrefix: "--antColor",
    // }
  ],
}

module.exports = {
  plugins: [require('tailwindcss'), require('autoprefixer'), themePlugin.default(themeOptions)],
};

```




### Nuxt.js 项目

#### 配置实例1

``` js
// nuxt.config.js

build: {
  plugins: [
    // 引入自动注入代码
    require('@mega-apps/postcss-theme-helper/unplugin').default.webpack({debug: false})
  ],
  postcss: {
        postcss: {
          plugins: {
            "@mega-apps/postcss-theme-helper": {
              debug: false,
              colorVariableReplacer: [
                // 类似于CSS的解析，从上而下，最小优先级最高
                {
                  matchColors: ["#409EFF","rgb(239, 68, 68)"],
                  matchSelectors: [],
                  initVarColors: ["red","blue"],
                  strict: true,
                  cssVarPrefix: "--eColor",
                  wrapper: ":root",
                },
                {
                  matchColors: ["black" ],
                  matchSelectors: [{ type: "RegExpMatch", data: [/.my-color/i] }],
                  initVarColors: ["yellow"],
                  strict: true,
                  cssVarPrefix: "--hiColor",
                  wrapper: "div",
                },
                {
                  matchColors: ["#0ea8d0"],
                  matchSelectors: [],
                  initVarColors: ["green"],
                  strict: true,
                  cssVarPrefix: "--antColor",
                }
              ],
              // 预设值
              colorVariableReplacerPresets: ['VueElementUI_V2', 'VueAntdUI_V1']
            },
          },
          // 调整postcss插件的次序，参照文档：https://nuxtjs.org/docs/configuration-glossary/configuration-build#postcss
          // 插件必须在 tailwindcss 之后，这样才能对tailwindcss的样式进行处理替换
          order: ["tailwindcss", "@mega-apps/postcss-theme-helper"],
        },
  }
}


```

### 浏览器客户端

#### JS 代码示例

```js
/**
 * 获得全局主题样式助手
 */
export function getThemeHelper() {
  // @ts-ignore
  return window["@mega-apps/theme-helper"];
}

/**
 * 获得主题颜色的配置
 * @param key 唯一ID，一般特指挂载在Style中的id
 * @returns {Object}
 */
export function getColorThemeConfig(key = "config-for-theme") {
  const themeHelper = getThemeHelper();
  if (themeHelper) {
    const configMap = themeHelper.getColorThemeConfigMap();
    if (configMap) {
      return configMap[key];
    }
  }
  return {};
}

/**
 * 获得主题颜色的详细的配置项，组Key列表
 * @param key
 * @returns
 */
export function getColorConfigGroupKeys(key = "config-for-theme") {
  const config = getColorThemeConfig(key);
  return Object.keys(config);
}

/**
 * 改变指定GroupKey的主题颜色
 * @note：groupKey = `wrapper + "#*#" + cssVarPrefix` 的格式
 */
export function changeColorsWithGroupKey(
  groupKey,
  configKey = "config-for-theme"
) {
  const themeHelper = getThemeHelper();
  console.log("themeHelper = ", themeHelper);
  const config = getColorThemeConfig(configKey);
  const { themeChange } = themeHelper;
  // 获得参数设置的匹配颜色
  const matchColors = config[groupKey]?.options?.matchColors || [];

  const rColor = () => Math.floor(Math.random() * 256);

  // 生成随机替换颜色
  const withColors = [];
  matchColors.forEach(() => {
    const newColor = `rgb(${rColor()}, ${rColor()}, ${rColor()})`;
    withColors.push(newColor);
  });

  // 根据匹配的颜色，随机生成替换的颜色
  themeChange.changeColors({
    matchColors,
    withColors,
    // filterWrappers: [":root"], // 过滤包裹器的样式
    // filterCssVarPrefixes: [],  // 过滤指定的cssVarPrefix
    debug: true,
  });
}

window.changeColorsWithGroupKey = changeColorsWithGroupKey;

// 调用示例
changeColorsWithGroupKey(':root#*#--eColor');

```

#### Typescript 代码示例

##### 获得全局主题样式助手

``` ts
/**
 * 获得全局主题样式助手
 */
export function getThemeHelper(): any {
  // @ts-ignore
  return window["@mega-apps/theme-helper"];
}
```

##### 获得主题颜色的配置

``` ts
/**
 * 获得主题颜色的配置
 * @param key 唯一ID，一般特指挂载在Style中的id
 * @returns {Object}
 */
export function getColorThemeConfig(key = "config-for-theme"): any {
  const themeHelper = getThemeHelper();
  if (themeHelper) {
    const configMap = themeHelper.getColorThemeConfigMap();
    if (configMap) {
      return configMap[key];
    }
  }
  return {};
}
```

##### 获得主题颜色的详细的配置项，组Key列表

``` ts
/**
 * 获得主题颜色的详细的配置项，组Key列表
 * @param key
 * @returns
 */
export function getColorConfigGroupKeys(key = "config-for-theme"): string[] {
  const config = getColorThemeConfig(key);
  return Object.keys(config);
}
```

##### 改变指定GroupKey的主题颜色

``` ts
/**
 * 改变指定GroupKey的主题颜色
 * @note：groupKey = `wrapper + "#*#" + cssVarPrefix` 的格式
 */
export function changeColorsWithGroupKey(
  groupKey: string,
  configKey = "config-for-theme"
) {
  const themeHelper = getThemeHelper();
  const config = getColorThemeConfig(configKey);
  const { themeChange } = themeHelper;
  // 获得参数设置的匹配颜色
  const matchColors: string[] = config[groupKey]?.options?.matchColors || [];

  const rColor = () => Math.floor(Math.random() * 256);

  // 生成随机替换颜色
  const withColors: string[] = [];
  matchColors.forEach(() => {
    const newColor = `rgb(${rColor()}, ${rColor()}, ${rColor()})`;
    withColors.push(newColor);
  });

  // 根据匹配的颜色，随机生成替换的颜色
  themeChange.changeColors({
    matchColors,
    withColors,
    // filterWrappers: [":root"], // 过滤包裹器的样式
    // filterCssVarPrefixes: [],  // 过滤指定的cssVarPrefix
    debug: true,
  });
}

```

## 参数说明

### Webpack 注入插件参数

``` ts
/**
 * 主题注入代码选项
 */
export interface IInjectThemeCodeOptions {
  /**
   * 注入样式元素的唯一ID值
   *
   * @example
   *
   * ``` html
   * <html>
   * <head>
   *   <style id="theme-style-element" type="text/css">
   *     :root {--myColor-cef4444-b: 18;--myColor-cef4444-g: 10;--myColor-cef4444-r: 239;--myColor-cef4444: blue;--myColor-c000000: red;}div {--hiColor-c000000: yellow;}
   *   </style>
   * </head>
   * <body></body>
   * </html>
   * ```
   * 关联主题样式的元素ID 为 "theme-style-element"；
   */
  injectStyleElementId: string;

  /**
   * 挂载在window对象上的变量名称
   *
   * @example
   *
   * ``` typescript
   * const windowGlobVarName = "___HELP";
   *
   * // 最终的结果是 window.___HELP 将被占用赋值
   * ```
   */
  windowGlobVarName?: string;
}
```

### 主题颜色替换参数

#### 参数接口原型

``` ts

export enum EMatchFilterItemType {
  ArrayIncludes = "ArrayIncludes", // 数组包含
  RegExpMatch = "RegExpMatch", // 正则匹配
  GlobMatch = "GlobMatch", // glob匹配
  Function = "Function", // 函数调用
}
export type TMatchFilterItemType =
  | EMatchFilterItemType
  | keyof typeof EMatchFilterItemType
  | [EMatchFilterItemType.ArrayIncludes, EMatchFilterItemType.RegExpMatch];

export type TMatchBaseSupportFilter =
  | string
  | readonly string[]
  | RegExp
  | readonly RegExp[];

export interface IMatchFilterBaseItem {
  type: TMatchFilterItemType;
  data: TMatchBaseSupportFilter;
}

export type TMatchSupportFilterBuilder = (
  ...args: any[]
) => IMatchFilterBaseItem[];
export type TMatchSupportFilter =
  | TMatchBaseSupportFilter
  | TMatchSupportFilterBuilder;

export interface IMatchFilterItem extends IMatchFilterBaseItem {
  data: TMatchBaseSupportFilter;
}



export enum EColorVariablesReplacerPreset {
  VueElementUI_V2, // vue element-ui v2.x 系列
  VueElementUI_Plus_V1, // vue element-ui plus v1.x 系列
  VueAntdUI_V1, // vue antd (ant-design-vue) v1.x 系列
  VueAntdUI_V3, // vue antd (ant-design-vue) v3.x 系列
}

export type TColorVariablesReplacerPresetName = keyof typeof EColorVariablesReplacerPreset;


/**################### 主题替换 ####################**/
/**
 * 主题配置选项
 */
export interface IThemeHelperOptions {
  /**
   * 是否输出调试日志信息
   * @default false, 不输出调试日志信息
   * */
  debug?: boolean;

  /**
   * 是否开启性能跟踪
   * @default false, 不开启性能跟踪
   */
  tracePerformance?: boolean;

  /**
  * 是否使用共享的颜色变量替换构建器（单一实例）
  */
  useShareColorVariableReplaceBuilder?: boolean;

  /**
   * 颜色替换, 支持单一配置，多个配置, 也支持关闭
   */
  colorVariableReplacer?:
    | IColorVariablesReplacerOptions
    | IColorVariablesReplacerOptions[]
    | boolean;

  colorVariableReplacerPresets?: EColorVariablesReplacerPreset[] | TColorVariablesReplacerPresetName[];
}
```

#### 示例代码

``` js

// 示例代码
plugins: {
  "@mega-apps/postcss-theme-helper": {
      debug: false,//
      // tracePerformance: false,
      // useShareColorVariableReplaceBuilder: false,
      colorVariableReplacer: [
        // 类似于CSS的解析，从上而下，优先级高低：按序降低
        {
          matchColors: ["black", "rgb(239, 68, 68)"],
          initVarColors: ["red", "blue"],
          strict: true,
          cssVarPrefix: "--myColor",
          wrapper: ":root",
        },
        {
          matchColors: ["black"],
          initVarColors: ["yellow"],
          strict: true,
          cssVarPrefix: "--hiColor",
          wrapper: "div",
        }
     ],
     colorVariableReplacerPresets: [], //   VueElementUI_V2, VueElementUI_Plus_V1, VueAntdUI_V1,
  }
}
```

#### 参数：matchColors

特指要匹配的颜色值数组; 这些颜色值将支持主题替换

- 支持颜色值的模式：
  - 主要：
    - 16进制（3位，4位，6位，8位）：#fff, #ffff, #ffffff, #ffffffff
    - RGB/RGBA 字符串：rgb(192, 192, 192), rgba(192, 192, 192, 0.5)
    - HSL/HSLA 字符串: hsl(0, 50%, 50%), hsl(0, 50%, 50%, 0.25)
    - 颜色名称: red, black, blue, green
  - 其他：
    - HWB 字符串
    - CMYK 字符串
    - LCH 字符串
    - LAB 字符串
    - XYZ 字符串
- 自动去重

#### 参数：matchProps

特指需要匹配的包含颜色的属性名称

- 默认值为: [DEFAULT_COLOR_PROPS], 表示所有CSS属性都会被遍历
- 可以指定自己的默认的属性包含, 如：`background`, `color`

```js
  // 注意，匹配模式，要求指定过滤方式，否则无法匹配：如：ArrayIncludes，
  matchProps: [{ type: "ArrayIncludes", data: DEFAULT_COLOR_PROPS }] // 只查找css样式中，包含background的规则
```

#### 参数：excludeProps

特指需要排除的属性名称

- 默认值: [], 表示不排除任何项
- 支持的值形式：字符串数组、正则表达式、正则表达式数组、函数

```js
  excludeProps: [{ type: "RegExpMatch", data: [/^border/i] }] // 以border开头的全部排除在外
```

#### 参数：matchSelectors

特指需要匹配的选择器

- 默认值为: []
- 支持的值形式：字符串数组、正则表达式、正则表达式数组、函数

```js

// 字符串数组形式，使用glob形式，参见：https://www.npmjs.com/package/micromatch
['ele-*']

// 正则表达式形式
/ele/gi

// 正则表达式数组
[/^ele/gi, /^ant/gi]

// 函数形式
(...args) => { return []}
```

``` ts

matchSelectors: [{ type: "RegExpMatch", data: [/.my-color/i] }]

```

#### 参数：excludeSelectors

特指需要排除的CSS选择器

- 默认：[], 表示不排除任何项
- 支持的值形式：字符串数组、正则表达式、正则表达式数组、函数 
- 与 `matchSelectors` 的匹配规则雷同

```js
  excludeSelectors: [{ type: "RegExpMatch", data: [/^ele/gi] }] // 以ele开头的全部排除在外
```


#### 参数：initVarColors

特指：默认初始化颜色， 例如：匹配到 `black`，可以让生成的颜色变成 `red`
默认替换颜色与matchColors成对出现

```js
matchColors: ['black', 'rgb(22,22,22)', 'rgb(44,44,23)']
initVarColors: ['red', 'green']

// 转换结果： black -> red; rgb(22,22,22) -> green; rgb(44,44,23) -> green
```

#### 参数：strict

特指是否严格模式，如果为true，则忽略alpha的颜色相近处理

- 默认是true

```js
// 设置strict: false
matchColors: ['black']
strict: false

// 结果
// rgba(0,0,0, 0.5) 也会被匹配上
```

```js
// 设置strict: true
matchColors: ['black']
strict: true

// 结果
// rgba(0,0,0, 0.5) 不会被匹配上
```

#### 参数：cssVarPrefix

生成的css变量的前缀

```css
/* cssVarPrefix: --myVar */
:root {
  --myVar-name: 'black'
}
```

#### 参数：useMatchColorValueForCssVar

cssVar 使用matchColors中的颜色值

- 默认：true

```css
/* useMatchColorValueForCssVar: true*/
:root {
  --myVar-black: 'black'
}

/* useMatchColorValueForCssVar: false */
:root {
  --myVar-black: '#000'
}

```

#### 参数：wrapper

定义的css变量值，外部包装器，例如: :root

```css
/* wrapper: ':root' */
:root {
  --myVar-black: 'black'
}

/* wrapper: 'body > div' */
body > div {
  --myVar-black: 'black'
}

```

#### 参数：debug

是否单独输出调试日志

- 默认值：false

## 示例



### 启用 Ant Design Vue 主题替换

当前内置预设，支持的 [`Ant Design Vue`](https://www.antdv.com/)版本是:

- "VueAntdUI_V1": 对应 "ant-design-vue": "^1.x"
- "VueAntdUI_V3": 对应 "ant-design-vue": "^3.x"

```js
// postcss plugins
plugins: {
  "@mega-apps/postcss-theme-helper": {
    colorVariableReplacer: [
      // 类似于CSS的解析，从上而下，优先级高低：按序降低
      {
        matchColors: ["black", "rgb(239, 68, 68)"],
        initVarColors: ["red", "blue"],
        strict: true,
        cssVarPrefix: "--myColor",
        wrapper: ":root",
      }
    ],
    colorVariableReplacerPresets: ['VueAntdUI_V1'], // 预设为数组，可以将 'vue-antd-ui.v1' 添加进来
  }
}
```



### 启用 Element UI 主题替换

当前内置预设，支持的 [`Element UI`](https://element-plus.org/)版本是:

- "VueElementUI_V2": 对应 "element-ui": "^2.x"
- "VueElementUI_Plus_V1": 对应 "element-plus": "^1.3.0"

```js
// postcss plugins
plugins: {
  "@mega-apps/postcss-theme-helper": {
    colorVariableReplacer: [
      // 类似于CSS的解析，从上而下，优先级高低：按序降低
      {
        matchColors: ["black", "rgb(239, 68, 68)"],
        initVarColors: ["red", "blue"],
        strict: true,
        cssVarPrefix: "--myColor",
        wrapper: ":root",
        preset: ['vue-element-ui.v2'], // 预设为数组，可以将 'vue-element-ui.v2' 添加进来
      }
    ],
    colorVariableReplacerPresets: ['VueElementUI_V2'], // 预设为数组，可以将 'element-ui.v2' 添加进来
  }
}
```

### 启用多个UI库主题替换

```js
// postcss plugins
plugins: {
  "@mega-apps/postcss-theme-helper": {
    colorVariableReplacer: [
      // 类似于CSS的解析，从上而下，优先级高低：按序降低
      {
        matchColors: ["black", "rgb(239, 68, 68)"],
        initVarColors: ["red", "blue"],
        strict: true,
        cssVarPrefix: "--myColor",
        wrapper: ":root"
      }
    ],
    colorVariableReplacerPresets: ['VueElementUI_V2', 'VueAntdUI_V1']
  }
}
```

### 启用多组主题替换

```js
postcss: {
  plugins: {
    "@mega-apps/postcss-theme-helper": {
      useShareColorVariableReplaceBuilder: true, // 开启共享CSS变量
      colorVariableReplacer: [
        // 类似于CSS的解析，从上而下，优先级高低：按序降低
        {
          matchColors: ["#409EFF","rgb(239, 68, 68)"],
          matchSelectors: [],
          initVarColors: ["red","blue"],
          strict: true,
          cssVarPrefix: "--myColor",
          // wrapper: ":root",
        },
        {
          matchColors: ["black" ],
          matchSelectors: [{ type: "RegExpMatch", data: [/.my-color/i] }],
          initVarColors: ["yellow"],
          strict: true,
          cssVarPrefix: "--hiColor",
          // wrapper: "div"
        },
        {
          matchColors: ["#0ea8d0"],
          matchSelectors: [],
          initVarColors: ["green"],
          strict: true,
          cssVarPrefix: "--antColor",
          // wrapper: "div"
        }
      ],
      colorVariableReplacerPresets: ['VueElementUI_V2', 'VueAntdUI_V1']
    },
  },
  // 调整postcss插件的次序，参照文档：https://nuxtjs.org/docs/configuration-glossary/configuration-build#postcss
  // 插件必须在 tailwindcss 之后，这样才能对tailwindcss的样式进行处理替换
  order: ["tailwindcss","@mega-apps/postcss-theme-helper"],
},
```

## Demo

- [Vite 项目使用Demo](https://stackblitz.com/edit/vitejs-vite-gahygj)

## 变更日志

[CHANGELOG](./CHANGELOG.md)