SCSS+WindiCSS实现主题色切换

最近在给自己写主页（同时也是博客），我做了一个切换主题色的功能。每次进入页面时，会随机选择一套配色，让页面显得灵动一些，就像下面这样：

这是如何实现的呢？不妨先从自定义颜色入手

WindiCSS 自定义颜色

定义一个固定的颜色

Copy
// windi.config.js

export default defineConfig({
theme: {
extend: {
colors: {
primary: "#2196f3",
},
},
},
})

这样就定义了一个 primary 的颜色，之后就能正常使用了，（如 bg-primary / text-primary）

当然，不仅可以传递字符串，还能够使用对象定义一组颜色：（如 bg-primary-light）

Copy
// windi.config.js

export default defineConfig({
theme: {
extend: {
colors: {
primary: {
extralight: "#d3eafd",
light: "#b2dafb",
medium: "#6ebbf7",
DEFAULT: "#2196f3"
},
},
},
},
})

使用 CSS 变量

为了使颜色可变，使用 CSS 变量会方便许多，WindiCSS 当然也是支持的：

Copy
:root {
--color-primary-extralight: #d3eafd;
--color-primary-light: #b2dafb;
--color-primary-medium: #6ebbf7;
--color-primary: #2196f3;
--color-primary-dark: #27415b;
}

Copy
// windi.config.js

export default defineConfig({
theme: {
extend: {
colors: {
primary: {
extralight: 'var(--color-primary-extralight)',
light: 'var(--color-primary-light)',
medium: 'var(--color-primary-medium)',
DEFAULT: 'var(--color-primary)',
dark: 'var(--color-primary-dark)',
},
},
},
},
})

这样就能够基本实现在 WindiCSS 中使用 CSS 变量了，不过还有一个小问题：

WindiCSS 支持为颜色设置透明度，例如 bg-gray-800/80 bg-gray-800 bg-opacity-80 这两种写法。上面的配置方式会导致这种语法失效（丢失透明度）。所以，我们需要给 CSS 变量换一种形式。同时，需要一个高阶工具函数来包装一下变量：

Copy
:root {
--color-primary-extralight: 211 234 253;
--color-primary-light: 178 218 251;
--color-primary-medium: 110 187 247;
--color-primary: 33 150 243;
--color-primary-dark: 39 65 91;
}

Copy
// windi.config.js

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

export default defineConfig({
theme: {
extend: {
colors: {
primary: {
extralight: withOpacityValue('--color-primary-extralight'),
light: withOpacityValue('--color-primary-light'),
medium: withOpacityValue('--color-primary-medium'),
DEFAULT: withOpacityValue('--color-primary'),
dark: withOpacityValue('--color-primary-dark'),
},
},
},
},
})

如此一来，每当使用 primary 颜色时，WindiCSS 都会调用函数来生成样式，通过对 opacityValue 的判断来实现对透明度语法的支持。

SCSS 生成 CSS 变量

显然，如果手动为 light extralight 等颜色变种指定颜色值是不现实的，况且现在需要用 R G B 三个数字来表示颜色，编辑器没有高亮，不直观，也会导致维护困难。

这时候 SCSS 就能派上用场了！SCSS 提供了基础的 CSS 数据类型，判断、遍历语法，同时也提供了海量的工具函数（例如 red() blue() green()等用于通道分离，mix()用于颜色混合）

首先来实现一个工具函数，将传入的十六进制颜色转换成 R G B 三个数字的形式

Copy
@function getColorValue($color) {
@return #{red($color)} #{green($color)} #{blue($color)};
}

/* getColorValue(#2196f3) -> 33 150 243 */

我预想中的情况是 —— 只要给一个 primary 的基础色，SCSS 就能帮我把 light extralight 等颜色变种都生成出来。我是用 mix 方法来实现：

Copy
@mixin spread-theme-map($map: ()) {
@each $key, $value in $map {
#{"--"+$key}: $value;
}
}

@function theme-primary-map($primary-color: #2196f3) {
@return (
color-primary-dark: getColorValue(mix($primary-color, black, 30%)),
color-primary: getColorValue($primary-color),
color-primary-medium: getColorValue(mix($primary-color, white, 70%)),
color-primary-light: getColorValue(mix($primary-color, white, 35%)),
color-primary-extralight: getColorValue(mix($primary-color, white, 15%))
);
}

/* spread-theme-map(theme-primary-map(#2196f3)) */

这样，就能针对某一个颜色生成对应的系列颜色属性了。接下来，只需要定义一个数组，把需要的主题色放进去，跑个循环即可（从 Material Design 的文档里随便挑了几个养眼的颜色）：

Copy
$themeColorList: (
#2196f3,
#f44336,
#9c27b0,
#4caf50,
#3f51b5,
#795548,
#607d8b,
#009688
);

@for $i from 1 through length($themeColorList) {
$color: nth($themeColorList, $i);
.theme-#{$i} {
@include spread-theme-map(theme-primary-map($color));
}
}

在 VSCode 中，看起来是这样的：

显然舒服多了。

剩下的工作该划掉了

如果希望修改主题色，只需要给根元素（html 或 body）增加对应类名即可（例如 theme-1 / theme-2），实现的方式很多，因为我使用了 Nuxt.js，下面是我的解决方案。

Copy
const randomThemeColorIndex = useState('randomThemeColorIndex', () =>
Math.floor(Math.random() * themeColorList.length) + 1
)

useHead({
bodyAttrs: {
class: 'theme-' + randomThemeColorIndex.value,
}
})