背景
实现主题切换有几种不同的方案,比如使用CSS变量,使用JavaScript动态加载对应的主题样式文件等。本文主要讲的是如何使用Sass实现主题切换。
前置知识
了解Sass的基本使用
- variable
- mixin
- map
本质
Sass作为CSS预处理器,需要编译成CSS后,才能被浏览器识别和解析。因此无法在浏览器中直接使用Sass实现类似CSS变量那种动态切换。本质上来说,项目中有几个主题就要提前定义好几份主题样式并全部引入。
思路
首先,我们需要给应用的顶层元素添加一个主题标识,用于标识当前的主题,用于之后应用上对应的主题样式。该标识可以是数据属性,也可以是类,也可以是id,这里采用数据属性。
然后,每次切换主题时,通过更新该标识,页面就会应用样式文件中提前定义好的对应的主题样式。
.app {
&[data-theme='light'] {
color: #333;
}
&[data-theme='dark'] {
color: #fff;
}
}
实现
基础版
基于主题切换的本质和思路,我们可以通过硬编码,实现一个简单的主题切换。
Hello, World
当前主题:亮色
首先给应用添加一个主题标识,这里我通过给body
元素添加一个数据属性data-theme
表示当前的主题。默认为light
然后提前定义好所有主题样式:
// 所有主题样式
$bg-color-light: #ffffff;
$bg-color-dark: #091a28;
$title-color-light: #363636;
$title-color-dark: #ffffff;
$subtitle-color-light: #4a4a4a;
$subtitle-color-dark: cyan;
.app {
// 默认主题样式(light主题)
background-color: $bg-color-light;
// dark主题
[data-theme='dark'] & {
background-color: $bg-color-dark;
}
}
.title {
color: $title-color-light;
[data-theme='dark'] & {
color: $title-color-dark;
}
}
.subtitle {
color: $subtitle-color-light;
[data-theme='dark'] & {
color: $subtitle-color-dark;
}
}
最后,当我们点击不同主题按钮时,就会更新body
上的主题标识data-theme
,这样,样式文件中对应的主题样式就会被应用上了。
完整代码和实现效果可以参考Codepen:
CodePen: SASS实现主题换肤/主题切换-基础版
不过该实现有点粗糙,存在几个小问题:
-
每个需要应用主题样式的CSS选择器中,都要写一遍对应主题需要的样式,比较繁琐
-
如果有多个主题,代码量会极具增加,并且很多都是重复的“模板代码”
针对这些问题,我们可以利用Sass的一些特性实现一个进阶版的主题切换。
进阶版
首先,针对基础版暴露出的问题。我们需要对Sass变量做一点小小的调整。这里我们将主题样式封装成了map格式,map中每一个元素都对应着不同主题下的样式。
// 所有主题样式
$bg-color: (
// 亮色
light: #fff,
// 暗色
dark: #091a28
);
$title-color: (
light: #363636,
dark: #ffffff
);
$subtitle-color: (
light: #4a4a4a,
dark: cyan
);
针对重复的模版代码和代码繁琐的问题,Sass中有个特性mixin
,正好可以利用上。
接下来,我们要封装一个mixin,专门解决基础版1-手写代码的繁琐的问题。
这里使用了Sass中的插值表达#{}
和map-get
方法,#{}
类似于JavaScript中的计算属性,可以动态设置属性名,map-get
方法用于从map中获取某一个属性对应的值。
@mixin themify($key, $valueMap) {
// 默认主题
#{$key}: map-get($valueMap, 'light');
// dark主题
[data-theme='dark'] & {
#{$key}: map-get($valueMap, 'dark');
}
}
themify
主要封装了默认主题样式light
,和dark
主题样式,这样我们在选择器里,只需要include
这些样式即可。
.app {
@include themify('background-color', $bg-color);
}
.title {
@include themify('color', $title-color);
}
.subtitle {
@include themify('color', $subtitle-color);
}
现在看这些代码是不是简洁多了?省去了自己手写那些繁琐的模板代码!
针对“多主题模版代码会更多”的问题,解决起来也就很容易了。只需要简单修改下该mixin,添加上对应的主题样式即可。
@mixin themify($key, $valueMap) {
// light主题
#{$key}: map-get($valueMap, 'light');
// dark主题
[data-theme='dark'] & {
#{$key}: map-get($valueMap, 'dark');
}
// dark1主题
[data-theme='dark1'] & {
#{$key}: map-get($valueMap, 'dark1');
}
// dark2主题
[data-theme='dark2'] & {
#{$key}: map-get($valueMap, 'dark2');
}
}
当然,我们还可以对mixin做一下优化,可以将主题封装成list格式,然后通过遍历主题,简化mixin:
@mixin themify($key, $valueMap) {
// theme list
$themes: light, dark;
@each $theme in $themes {
[data-theme=#{$theme}] & {
#{$key}: map-get($valueMap, $theme);
}
}
}
这样看起来就清爽多了。
完整代码和实现效果可以参考Codepen:
CodePen: SASS实现主题换肤/主题切换-进阶版
总结
Sass作为一款流行的CSS预处理器,提供了插值表达#{}
和map
类型等特性,在实现主题切换方面提供了不少便利。
当然,Sass实现主题切换还有很多可以优化的点。这里随便列两条常见的:
-
如果有多条主题样式需要应用,每一条都要写一遍
@include
,感觉有点麻烦,能不能只写一遍@include
?.app { @include themify('background-color', $bg-color); @include themify('color', $text-color); // ... } // 希望可以只写一遍@include .app { @include themify( ( 'background-color': $bg-color, 'color': $text-color ) ); }
-
如果还需要使用
!important
去覆盖一些因为权重问题无法应用的样式(比如使用了外部UI库,外部UI库中使用了!important
,需要覆盖该样式),怎么解决?// 这里提供一个思路,可以添加一个参数$important @mixin themify($key, $valueMap: null, $important: false) { // xxx }