一种构建灵活的系统页面主题方案
阅读此篇文章前,最好有下列知识
css
基础知识dart-sass
预处理器编程webpack
以及 postcss
tailwindcss
含有 jit
的 v2/v3
我们在日常生活中,不论是访问网站,手机 App,还是小程序,时常会用到 切换主题 这个功能。它能够为用户提供一定的自定义显示界面的能力,同时手机系统级别的主题也能够更换,比如 light(明亮模式)
和 dark(黑暗模式)
。
那么如何让我们编写的应用,在改动不大的情况下,能够快速的适配多个主题呢?
这就需要设计一个方案了。
这里我们以程序员们最熟悉的 Github
为例,它的主题切换是这样做的:
它在 根元素 那预设了几套 css
变量值,然后通过 js
去动态修改 html
根元素上的 data-color-mode
和 data-dark-theme
这些属性的值,从而让不同的 css
选择器选中这个根元素,并以此来动态的切换 :root
中的 css
变量的值。
同时这些变量都被广泛的使用在各种的 原子化的 class
和 @apply
中,一旦变量一换,所有使用到这些class
的控件和布局都收到影响,自然整个主题就改变了。
首先我们第一步要做的就是提炼 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;
...
}
这里注意全局变量中存储的是字符串,并不是颜色变量本身。
但是有了这些,没有对应的 class
和 scss
变量,我们还是很不好使用这些变量的,那么怎么进行工程化来提升我们的开发效率呢?接下来重点来了。
首先编写 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-loader
中 js
和 scss
的通信方法,就可以生成:
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) }},
通过这种方式,我们把生成的结果写入 variables.scss
和 extendColors.cjs
文件内,从而便捷把第一步中维护的如此之多的 css变量
,全部快速方便的转化为同等的 scss变量
和 tailwindcss 配置
生成 variables.scss
后,我们可以配置一下 sass-loader
来让其中的变量无需显式引入,即可在全局生效:
// sass-loader
{
additionalData: '@use "@/assets/scss/variables.scss" *;',
}
这样我们就可以在任意的 vue <style lang="scss">
, 或者 .scss
文件内使用到所有 variables.scss
中声明的变量了。
生成 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
格式的原因了!
本质上讲,是我们在利用原生 css
中 rgb
(rgba
是rgb
的别名) 构造方法来创建颜色变量:
/* 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)
就是现在使用的方案。
当然我们也可以更改上述的 getRgbString
和 withOpacityValue
这 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
配置中,就可以自动生成 class
和 vscode
的智能提示了:
// 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
变量就生效了,切换主题就非常的方便。
很多场景下我们的应用主题,不是从前端维护的几套预设方案中进行选择,而是由用户自定义配置,保存在数据库中,每次请求后端才能获取到。
这种获取方式意味着前端这里,只保留一套默认的预设方案。所以我们通常会在获取到后端给的主题数据后,动态的修改 css变量
的值。
具体怎么做呢?本质上就是调用 CSSStyleDeclaration.setProperty()
,来设置 document.documentElement
的变量值。
为了让它更好用,我们可以进行封装,并建立一套浏览器本地的缓存机制,这些在此不再叙述,条条大道通罗马。
注意此方案是放弃 IE
的!(都上 tailwindcss
了),其余浏览器兼容良好。
这种方式,实际上利用了很多的 css
, sass
, webpack
,tailwindcss
的特性,笔者回过头来看,发现这个方案在实现后,好用是非常好用的。
变量,原子化 class, 公共提取和智能提示一应俱全,就是要对上面一些技术点有比较充分的理解。
如果您对此篇文章有建议或者更好的方案,也欢迎联系笔者一起探讨。