• 动态调整系统主题色(4): CssVar 与 Variant 方案的探索


    Image

    动态调整系统主题色(4): CssVar 与 Variant 方案的探索

    前言

    这篇已经是动态调整系统主题色的第四篇了,转眼距离第一篇发布已经过去了2年的时间。随着时间的流逝,对技术的理解也在不断的进步,方案也在不断的完善和推陈出新,本篇文章就是在探讨系统主题色的 2 方案: CssVarVariant

    方案的介绍与比较

    假如你没有看前面三篇文章的话,初看到这 2 个词可能不能直观的知道它们是什么意思,下面我先简单介绍一下它们是什么以及它们的原理。

    CssVar (CSS 变量方案)

    这个方案很简单,大意就是我们预先定义出,有多少种颜色会被使用,贯穿整个设计规范。然后把它们定义成 CSS 变量,然后再通过更改 CSS 变量的值,去改变整个主题。

    比如我们可以这样去定义:

    :root{
        --prism-background: #f4f4f4;
    }
    /*暗色*/
    html.dark{
        --prism-background: #181818;
    }
    
    body{
        background-color: var(--prism-background);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这种方式把其他的主题定义在了 :root/html 选择器之下,本质上是在利用选择器的优先级去覆盖原先定义CSS变量的值,从而达到切换的效果。这个方案非常的通用,你看到的很多的组件库都是使用这个方案的,比如 element-plus / vant 等等,(antd 不是)

    CSS变量会从它被使用的地方,从自己递归向上去寻找定义,所以我们想覆盖定义在 :root 里定义的变量,可以直接在它被使用到的地方再定义一个值,用来替换达到覆盖的效果。

    所以,我们可以去封装一个组件 ThemeProvider,里面就定义一个 div 元素包裹一个插槽,再把CSS变量动态设置在这个元素上,从而达到切换插槽内局部CSS变量的效果。(是不是感觉似曾相识?)

    CSS 变量方案与 tailwindcss 的结合

    当然CSS变量方案和 tailwindcss 能够得到非常好的结合,我们只需要预先在tailwind.config.jstheme 中预先去扩展定义变量即可。

    不过和上面那种方式有些不同,这里我们定义的 CSS 变量是字符串而不是直接的颜色,这是为了后续能和透明度做一个动态的运算。比如我们这里有多个主题,分别为 light(默认),deep,dark,fantasy,我们可以这样定义:

        enum ModeEnum {
          light = 'light',
          deep = 'deep',
          dark = 'dark',
          fantasy = 'fantasy'
        }
        // 要完全的动态可配置,可以把这些值保存在服务端,动态获取
        // 然后在前端写个页面,用用取色器这个组件去设置颜色,保存进数据库
        // 这样就不需要前端写死变量了,从而达到更灵活的配置效果
        const cssVarsMap: Record<ModeEnum, Record<string, string>> = {
          light: {
            '--ice-color-base': '191, 219, 254',
            '--ice-color-primary': '30, 58, 138',
            '--ice-color-primary-content': '255, 255, 255'
          },
          deep: {
            '--ice-color-base': '125, 211, 252',
            '--ice-color-primary': '79, 70, 229',
            '--ice-color-primary-content': '255, 255, 255'
          },
          dark: {
            '--ice-color-base': '2, 132, 199',
            '--ice-color-primary': '165, 180, 252',
            '--ice-color-primary-content': '0,0,0'
          },
          fantasy: {
            '--ice-color-base': '8, 47, 73',
            '--ice-color-primary': '224, 231, 255',
            '--ice-color-primary-content': '0,0,0'
          }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    然后把这些变量的值动态设置在 html 上或者传入 ThemeProvider 中,就能达到切换的目的了。

    简单的配置如下:

    /** @type {import('tailwindcss').Config} */
    module.exports = {
      theme: {
        extend: {
          colors: {
            primary: 'rgba(var(--ice-color-primary), )',
            'primary-content':
              'rgba(var(--ice-color-primary-content), )',
            base: 'rgba(var(--ice-color-base), )'
          }
        }
      },
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    同时由于我们定义的值是字符串,我们就可以利用这一点去自由的组合使用 rgba 构造函数,来决定它的透明度了。即利用CSS变量字符串的特性组合出, rgba(r,g,b,a) 的效果,这样写法上就非常自由了,同一个主题色,你可以写出 text-primary 直接使用 primary 色,也可以写出 text-primary/50 这样 50% 透明度的 primary 色。(这就是 占位符的目的)

    这样我们定义组件的时候就可以用一个类名去动态引用变量了:

    <template>
      <button
        class="bg-primary text-primary-content">
        <slot>slot>
      button>
    template>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这个组件背景主色,文字为主色的 content 色。

    Variant 方案

    不知道你有没有注意过,在 tailwindcss 自带的暗黑模式切换中,它使用的写法是 text-gray-800 dark:text-gray-200

    这种的语义上非常的直观,看到它你就知道这个元素 color 默认情况下是 text-gray-800,暗黑模式下colortext-gray-200

    同样它的生成也是在利用选择器/媒体选择器的优先级去覆盖,大体的原理如下:

    border-white {
        --tw-border-opacity: 1;
        border-color: rgb(255 255 255 / var(--tw-border-opacity));
    }
    
    :is(.dark .dark:border-slate-800) {
        --tw-border-opacity: 1;
        border-color: rgb(30 41 59 / var(--tw-border-opacity));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    其中,这个 dark: 打头的条件就被称为 Variant,我们可以根据自己的需求,创建很多个 Variant 来定义并生成不同的 CSS 区块。同样我们可以去基于这种方法去扩展我们的主题。

    比如我们这里我们还是有多个主题,分别为 light(默认),deep,dark,fantasy,我们可以这样定义:

    const plugin = require('tailwindcss/plugin')
    
    /** @type {import('tailwindcss').Config} */
    module.exports = {
      // 默认会添加 dark 的 Variant 和 .dark 的 css 生成块
      darkMode: 'class',
      plugins: [
        plugin(function ({ addVariant, addBase }) {
          addVariant('deep', ':is(.deep &)')
          addVariant('fantasy', ':is(.fantasy &)')
        }),
      ],
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这样我们像上面同样定义一个 button 组件就要这样写:

      <button
        class="
        bg-pink-900 
        deep:bg-pink-600 
        dark:bg-pink-300 
        fantasy:bg-pink-100 
        text-white 
        deep:text-gray-300 
        dark:text-gray-600 
        fantasy:text-gray-900">
        <slot>slot>
      button>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这样显然是非常冗长的,即使它还存在着一些可读性,但是还是瑜不掩瑕,而且使用 @apply 去提取的话,也是容易出错的。

    而且还有一个致命的问题,这种方案每多一个主题就要加一个 Variant,这显然非常的不灵活的。所以说在不确定主题数量的情况下,这个方案是个灾难。主题色显然不是Variant适合的场景。

    这个方案可能只适合暗黑模式这个特殊的场景。但是 CssVar 方案也能适配 暗黑模式 的。

    所以主题色方案还是选择 CssVar 就行了。

    2种方案在小程序上的示例

    #小程序://tailwind/RvdAJVby6DXgZpa

    复制到微信后打开,这里就不放小程序码了,省的被掘金判断是推广的文章。

    或者微信搜索 tailwind 小程序进入查看效果。

    之前的几篇

    1. 动态调整web系统主题? 看这一篇就够了

    2. 动态调整web主题(2) 萃取篇

    3. 动态调整web主题(3): 基于tailwindcss插件的主题色生成方案

  • 相关阅读:
    Python爬虫_Scrapy(一)
    JavaScript常用事件详解
    Revit二次开发插件如何打包并发布
    Java 环境配置
    Oracle 客户体验 (Oracle CX) 怎么样?
    基于java+springmvc+mybatis+vue+mysql的校园安全管理系统
    golang中的网络轮询器netpoll源码解析
    Dubbo基础
    postgresql源码学习(36)—— 事务日志11 - 日志归档
    superset运维中遇到的问题
  • 原文地址:https://blog.csdn.net/qq_33020601/article/details/133608682