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

1,363 阅读6分钟

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

一种构建灵活的系统页面主题方案

前置技术点

阅读此篇文章前,最好有下列知识

  1. css 基础知识
  2. dart-sass 预处理器编程
  3. webpack 以及 postcss
  4. tailwindcss 含有 jitv2/v3

前言

我们在日常生活中,不论是访问网站,手机App,还是小程序,时常会用到 切换主题 这个功能。它能够为用户提供一定的自定义显示界面的能力,同时手机系统级别的主题也能够更换,比如 light(明亮模式)dark(黑暗模式)

那么如何让我们编写的应用,在改动不大的情况下,能够快速的适配多个主题呢?

这就需要设计一个方案了。

方案设计

方案参考

这里我们以程序员们最熟悉的 Github 为例,它的主题切换是这样做的:

它在 根元素 那预设了几套 css 变量值, 然后通过 js 去动态修改 html 根元素上的 data-color-modedata-dark-theme 这些属性的值,从而让不同的 css 选择器选中这个根元素,并以此来动态的切换 :root 中的 css 变量的值。

同时这些变量都被广泛的使用在各种的 原子化的 class@apply 中,一旦变量一换,所有使用到这些class的控件和布局都收到影响,自然整个主题就改变了。

1. 提炼css变量

首先我们第一步要做的就是提炼css变量,这些主要由设计师提供。

这里以颜色为例,主要包含 同个颜色的多态控件各个状态的颜色提示警告错误字体中,标题,副标题,正文,提示的颜色 等等。当然像字体大小,阴影这类也是同样的。

这方面就不细说了,在提取到变量之后我们就可以开始进行命名工作:

// constants.scss
// 这是一个 scss 的 map数据结构,保存默认的初始值
$root-vars:(
  --color-fg-default: #adbac7,
  --color-fg-muted: #768390,
  --color-fg-subtle: #545d68,
  --color-fg-on-emphasis: #cdd9e5,
  --color-scale-gray-0: #cdd9e5,
  --color-scale-gray-1: #adbac7,
  --color-scale-gray-2: #909dab,
  --color-scale-gray-3: #768390,
  // ...
)

可以注意到,在维护的变量中,颜色占了绝大部分,而且我们保存的都是颜色的hex格式,并没有按照rgba的格式,把透明度(opacity)保存下来, 这是为什么? 答案会在后面揭晓。

接着,维护完这个sass:map ,我们编写一个工具类 util.scss 来把颜色变量转化为字符串:

// util.scss
@use 'sass:color';
@use 'sass:meta';

@function getRgbString($color) {
  @if (meta.type-of($color) == color) {
    @return color.red($color) color.green($color) color.blue($color);
  } @else {
    @return $color;
  }
}

然后在全局样式 global.scss 中添加:

// global.scss
@use './constants.scss' as C;
@use './util.scss' as Util;

:root {
  @each $var, $color in C.$root-vars {
    #{$var}: Util.getRgbaString($color);
  }
}

这样我们的那些变量默认值字符串就添加进了 :root 根元素中:

/* result */
:root{
  --color-canvas-default-transparent: 34 39 46;
  --color-marketing-icon-primary: 108 182 255;
  --color-marketing-icon-secondary: 49 109 202;
  --color-diff-blob-addition-num-text: 173 186 199;
  --color-diff-blob-addition-fg: 173 186 199;
  --color-diff-blob-addition-num-bg: 87 171 90;
  --color-diff-blob-addition-line-bg: 70 149 74;
  --color-diff-blob-addition-word-bg: 70 149 74;
  --color-diff-blob-deletion-num-text: 173 186 199;
  ...
}

这里注意全局变量中存储的是字符串,并不是颜色变量本身。

但是有了这些,没有对应的 classscss 变量,我们还是很不好使用这些变量的,那么怎么进行工程化来提升我们的开发效率呢?接下来重点来了。

2. scss 与 js通信,动态生成 scss 变量与原子化 class

首先编写 export.scss 用于暴露对象给 js 使用:

// export.scss
@use './constants.scss' as C;
@use './util.scss' as Util;

:export {
  @each $var, $color in C.$root-vars {
    #{$var}: Util.getRgbaString($color);
  }
}

然后利用 webpack sass-loaderjsscss 的通信方法,就可以生成:

  • variables.scss (全局scss变量文件)
  • extendColors.cjs (tailwindcss colors 配置文件)
// generator.js 生成器
import variables from '@/assets/scss/export.scss'
// 简易的去除前缀
removeColorPrefix(str) {
  return str.substring(8)
}
// 此时的 variables 是一个 object
// 那么scss全局变量的模板生成为:
scssFilterShadow(str) {
  return `rgb(var(${str}))`
}
// scss模板为
${{ removeColorPrefix(k) }}:{{ scssFilterShadow(k) }};
// 此时 原子化的 `tailwindcss colors` 文件生成为:
jsFilterShadow(str) {
  return `withOpacityValue('${str}')`
}
// tailwindcss模板为
'{{ removeColorPrefix(k) }}':{{ jsFilterShadow(k) }},

# 动态调整web主题(2) 萃取篇 有更好的方法

通过这种方式,我们把生成的结果写入 variables.scssextendColors.cjs 文件内,从而便捷把第一步中维护的如此之多的 css变量,全部快速方便的转化为同等的 scss变量tailwindcss 配置

3. 全局scss文件变量注入

生成 variables.scss后,我们可以配置一下 sass-loader 来让其中的变量无需显式引入,即可在全局生效:

// sass-loader
{
  additionalData: '@use "@/assets/scss/variables.scss"  *;',
}

这样我们就可以在任意的 vue <style lang="scss">, 或者 .scss 文件内使用到所有 variables.scss 中声明的变量了。

4. 原子化的 class 生成

生成 extendColors.cjs 后,我们在里面添加:

function withOpacityValue(variable) {
  return ({ opacityValue }) => {
    if (opacityValue === undefined) {
      return `rgb(var(${variable}))`
    }
    return `rgb(var(${variable}) / ${opacityValue})`
  }
}

这是为了结合 jit引擎,来动态的调整所有颜色的透明度。有了它,我们就能编写出以下的代码:

h1{
  @apply text-header-text; 
  // 等价于
  // color: rgb(var(--color-header-text))
}
h2{
  @apply text-header-text/70;
  // 等价于
  // color: rgb(var(--color-header-text) / 0.7)
}

这也是我们要给根元素中的 css变量 赋值为 R G B 格式的原因了!

本质上讲,是我们在利用原生 cssrgb(rgbargb的别名) 构造方法来创建颜色变量:

/* rgb的函数Syntax */
rgb(255,255,255) /* white */
rgb(255,255,255,.5) /* white with 50% opacity */
rgb(255 255 255) /* CSS Colors 4 space-separated values */
rgb(255 255 255 / .5); /* white with 50% opacity, using CSS Colors 4 space-separated values */

从上面这段代码片段中,我们可以看到,列出的 rgb(R G B / A)就是现在使用的方案。

当然我们也可以更改上述的 getRgbStringwithOpacityValue 这2个方法,把 , 这个分隔符加入进去,再把 / 去除,从而使用它 rgb(R,G,B,A) 这个构造方式。

这样我们在使用时就可以生成出这样的样式:

.neutral{
  background-color: rgb(var(--color-neutral-muted));
  &:hover{
    background-color: rgb(var(--color-neutral-muted) / 0.4);
  }
}

是不是非常的灵活?

接下来只要把 extendColors.cjs 导入进 tailwind.config.js 配置中,就可以自动生成 classvscode 的智能提示了:

// tailwind.config.js
const extendColors = require('./client/theme/extendColors.cjs')
const colors = require('tailwindcss/colors')

module.exports = {
  // ...
  theme:{
    extend:{
      colors:{
        ...extendColors.colors,
      }
      //...
    },
    colors:{
      transparent: 'transparent',
      current: 'currentColor',
      black: colors.black,
      white: colors.white,
      gray: colors.gray,
    },
    // ...
  }
  // ... 
}

这样,我们只需要把主题变更依赖的变量们,写进各种控件,layout,容器中去,所有的 css 变量就生效了,切换主题就非常的方便。

5. 动态修改根节点变量

很多场景下我们的应用主题,不是从前端维护的几套预设方案中进行选择,而是由用户自定义配置,保存在数据库中,每次请求后端才能获取到。

这种获取方式意味着前端这里,只保留一套默认的预设方案。所以我们通常会在获取到后端给的主题数据后,动态的修改 css变量 的值。

具体怎么做呢?本质上就是调用 CSSStyleDeclaration.setProperty(),来设置 document.documentElement 的变量值。

为了让它更好用,我们可以进行封装,并建立一套浏览器本地的缓存机制,这些在此不再叙述,条条大道通罗马。

兼容性

注意此方案是放弃 IE 的! (都上 tailwindcss 了),其余浏览器兼容良好。

总结

这种方式,实际上利用了很多的 css, sass, webpack,tailwindcss 的特性,笔者回过头来看,发现这个方案在实现后,好用是非常好用的。

变量,原子化class, 公共提取和智能提示一应俱全,就是要对上面一些技术点有比较充分的理解。

如果您对此篇文章有建议或者更好的方案,也欢迎联系笔者一起探讨。

笔者的联系方式

下一篇,将进一步优化这个方案,并提炼出 npm 包,详情请看 动态调整web主题(2) 萃取篇

附录

源码