自定义主题色实现之 CSS Variable

背景

  • 需求背景:动态配置页面主题相关颜色,而不是定制主题模式(类似黑夜模式,白天模式)。
  • 兼容性问题:在 IE 中 var()报错显示异常成透明色,(css-vars-ponyfill 插件 ,使页面展示一个默认色)
  • 第三方 UI 库支持 CSS Variable,([email protected] 以上,[email protected] 以上 )

1. 实现原理

1.1. 自定义属性 (--*): CSS 变量

带有前缀 -- 的属性名,比如--example--name,表示的是带有值的自定义属性,其可以通过var 函数在全文档范围内复用的。

CSS 自定义属性是可以级联的:每一个自定义属性可以多次出现,并且变量的值将会借助级联算法和自定义属性值运算出来。

注意,规则集所指定的选择器定义了自定义属性的可见作用域。通常的最佳实践是定义在根伪类:root 下,这样就可以在 HTML 文档的任何地方访问到它。

:root 这个 CSS 伪类匹配文档树的根元素。对于 HTML 来说,:root 表示 元素,除了优先级更高之外,与 html 选择器相同。

1.2. ConfigProvider.config() (config-provider/index.js)
  ConfigProvider.config = setGlobalConfig;
  var setGlobalConfig = function setGlobalConfig(_ref) {
  var prefixCls = _ref.prefixCls,
        iconPrefixCls = _ref.iconPrefixCls,
        theme = _ref.theme;

  if (prefixCls !== undefined) {
     globalPrefixCls = prefixCls;
  }

  if (iconPrefixCls !== undefined) {
     globalIconPrefixCls = iconPrefixCls;
  }

  if (theme) {
     (0, _cssVariables.registerTheme)(getGlobalPrefixCls(), theme); // 更改主题色
  }
1.3. registerTheme() (node_modules/mkui-fd/lib/config-provider/cssVariables.js)

创建所有 css 变量,可参考简易版 customCssVariable.js

import { updateCSS } from 'rc-util/lib/Dom/dynamicCSS';
import { TinyColor } from '@ctrl/tinycolor';
import { generate } from '@ant-design/colors';

function setThemeColor({ themeColor, varName }) {
  const variables = {};

  // ================ Primary Color ================
  if (themeColor) {
    // 转成 TinyColor 色值格式
    const primaryColor = new TinyColor(themeColor);
    // 10个不同阶梯色值
    const colorPalettes = generate(primaryColor.toRgbString());
    // Legacy - We should use semantic naming standard
    variables[`${varName}`] = themeColor;
    colorPalettes.forEach((color, index) => {
      variables[`${varName}-${index + 1}`] = color;
    });
  }

  // Convert to css variables
  // cssList ['--theme-color: xxx','--theme-color-1: xxx',...,'--theme-color-10: xxx' ]
  const cssList = Object.keys(variables).map(
    key => `--${key}: ${variables[key]};`,
  );

  // updateCSS 更新业务代码中的主题色: 创建 style 标签,值为cssList,并作为最后一个子元素插在head标签下
  updateCSS(
    '\n  :root {\n    '.concat(cssList.join('\n'), '\n  }\n  '),
    '-mkui-fd-'.concat(Date.now(), '-').concat('-dynamic-theme'),
  );
}
1.4. updateCSS() (node_modules/rc-util/lib/Dom/dynamicCSS.js)

创建 style 标签,值为 cssList,作为最后一个子元素插在 head 标签下,详见 node_modules/rc-util/lib/Dom/dynamicCSS.js

1.5. 动态改变颜色另外一种写法(我们项目中不推荐)

在 body 标签中增加 style 属性,改变 body 中所有 --mkui-primary-color 使用值, 优先级高于 html

document.body.style.setProperty('--mkui-primary-color', '#ffff00');

        ↓ ↓ ↓ ↓ ↓ ↓


2. 如何使用

2.1 替换当前项目引入样式文件为 CSS Variable 版本,并在 .babel.config 中去除 babel-plugin-import 配置。

{ "libraryName": "antd" }

     import { Button } from 'antd';

     ReactDOM.render();

           ↓ ↓ ↓ ↓ ↓ ↓

     var button = require('antd/lib/button');
     ReactDOM.render();

{ "libraryName": "antd", style: "css" }

     import { Button } from 'antd';

     ReactDOM.render();

           ↓ ↓ ↓ ↓ ↓ ↓

     var button = require('antd/lib/button');
     require('antd/lib/button/style/css');
     ReactDOM.render();

{ "libraryName": "antd", style: true }

     import { Button } from 'antd';

     ReactDOM.render();

           ↓ ↓ ↓ ↓ ↓ ↓

     var button = require('antd/lib/button');
     require('antd/lib/button/style');
     ReactDOM.render();

注意:

Antd 默认支持基于 ES modules 的 tree shaking,对于 js 部分,直接引入 import { Button } from 'antd' 就会有按需加载的效果。

如今webpack,rollup等构建工具都具备了摇树功能(Tree Shaking), 其原理是利用ESM模块的import语法的特性,通过AST语法树进行分析然后去除未使用到的代码。但tree shaking方式也只是处理了js部分,对于组件的css加载还需要手动引入.

2.2 引入包含css 变量的组件库 css 文件(app.js)
import 'mkui-fd/dist/mkui-fd.variable.min.css';
import 'mkui-ext/dist/mkui-ext.variable.min.css';
2.3.定义颜色变量(variables.less)
  1. 使用 mkui-fd 组件中已有变量

:root 下已经默认生成了部分可全局使用的变量:--mkui-primary-1, ... --mkui-primary-10

使用 var() 插入 CSS 变量的值

     @theme-color: var(--mkui-primary-color, #1890ff); 

     @theme-color-selected-bg: var(--mkui-primary-1, #f0f8ff);
  1. 自定义创建可全局使用的 CSS 变量

    // color.less

    为了兼容 IE 使用 css-vars-ponyfill, 必须使用:root 而不是 html

    :root {
    --theme-color: #1890ff;
    --theme-color-selected-bg: #f0f8ff;
    }

  // variables.less

  @theme-color: var(--theme-color);

  @theme-color-selected-bg: var(--theme-color-selected-bg, #f0f8ff);
2.4. 调用配置方法

1) 调用 ConfigProvider 配置方法设置主题色(cssVariable.js):

   // 修改 mkui-fd 组件颜色
   ConfigProvider.config({
     theme: { primaryColor: themeColor },
  });
  
  // 修改 mkui-ext 组件颜色
   ExtConfigProvider.config({
    theme: { primaryColor: themeColor },
  });

Note: primaryColor: 组件基本色,successColorwarningColorerrorColorinfoColor特定情况下的颜色。

2) 对于自定义颜色变量,动态更改自定义变量颜色(customCssVariable.js)

setThemeColor({ 
    themeColor, 
    varName: 'theme-color' 
})
2.5. 色值相关
  • 与主题色相关的颜色,设计图中会给一个主色(C6),及对应的10个色阶阶梯的哪一阶

3. 注意点:

  1. 为什么是:root 而不是 html

    • CSS 不仅用于样式化 HTML 文档, 它也用于 XML 和 SVG 文件。对于 XML 和 SVG 文件,:root 不是选择 html元素,而是选择它们的根(例如 SVG 文件中的 svg 标签)。

    • :root 选择器优先级更高

    • 有助于将使用的 CSS 变量与项目中使用样式的选择器分开

    • css-vars-ponyfill 中要求:自定义属性声明支持仅限于:root:host 规则集

  2. fade(),dark()等函数不支持变量参数

      fade('1199ff', 90%);
      fade(var(--theme-color), 90%); // 错误使用
    
  3. mkui-fd, mkui-ext以及项目中颜色相关样式,不要使用内联和 !important

  1. IE 11下取色器组件 [email protected]报错,降低[email protected]组件版本
    SCRIPT438: Object doesn't support property or method 'contains'

4. 其他扩展点

4.1 polyfill 和 ponyfill 的区别

polyfill 在原有的墙壁上打补丁,ponyfill 的策略则是另起炉灶,不会在原有的墙壁上修补,而是重新建一面墙,保证原来的墙壁还是原始纯净无污染。

例如:Array.isArray(), 此方法 IE8 浏览器并不支持

polyfill 策略

  if (!Array.isArray) {
     Array.isArray = function(arg) {
        return Object.prototype.toString.call(arg) === '[object Array]';
     };
  }

ponyfill 策略

  // 避免使用原生 API
  // 基本上,为了避免全局命名的污染,Ponyfill都是建议采用独立的模块化的方式开发与调用的
  function isArray (arg) {
     return Object.prototype.toString.call(arg) === '[object Array]';
  }

什么时候使用 Ponyfill?

  1. 有些原生 API 完全没法模拟,此时只能使用 Ponyfill 策略,例如 IE9 浏览器无法模拟 indexDB 能力,history.pushState()方法
  2. 有些原生 API 规范还没稳定,或者处于快速迭代中,或者是浏览器部分支持
4.2 css-vars-ponyfill 的使用

作用:

  • CSS 自定义属性到静态值的转换
  • 现代和旧版浏览器中运行时值的实时更新
  • 转换