前端主题切换方案
现在我们经常可以看到一些网站会有类似暗黑模式/白天模式
的主题切换功能,效果也是十分炫酷,在平时的开发场景中也有越来越多这样的需求,这里大致罗列一些常见的主题切换方案并分析其优劣,大家可根据需求综合分析得出一套适用的方案。
本文共讲解了5个方案,以及优缺点对比,请耐心查看。
其做法就是提前准备好几套CSS
主题样式文件,在需要的时候,创建link
标签动态加载到head
标签中,或者是动态改变link
标签的href
属性。
表现效果如下:
网络请求如下:
这种方案与第一种比较类似,为了解决反复加载样式文件问题提前将样式全部引入,在需要切换主题的时候将指定的根元素类名更换,相当于直接做了样式覆盖,在该类名下的各个样式就统一地更换了。其基本方法如下:
/* day样式主题 */
body.day .box {
color: #f90;
background: #fff;
}
/* dark样式主题 */
body.dark .box {
color: #eee;
background: #333;
}
.box {
width: 100px;
height: 100px;
border: 1px solid #000;
}
复制代码
<div class="box">
<p>hello</p>
</div>
<p>
选择样式:
<button onclick="change('day')">day</button>
<button onclick="change('dark')">dark</button>
</p>
复制代码
function change(theme) {
document.body.className = theme;
}
复制代码
表现效果如下:
通过以上两个方案,我们可以看到对于样式的加载问题上的考量就类似于在纠结是做SPA单页应用还是MPA多页应用项目一样。两种其实都无伤大雅,但是最重要的是要保证在后续的持续开发迭代中怎样会更方便。因此我们还可以基于以上存在的问题和方案做进一步的增强。
在做主题切换技术调研时,看到了网友的一条建议:
因此下面的几个方案主要是针对变量来做样式切换
灵感参考:Vue3官网
在Vue3
官网有一个暗黑模式切换按钮,点击之后就会平滑地过渡,虽然Vue3
中也有一个v-bind
特性可以实现动态样式绑定,但经过观察以后Vue
官网并没有采取这个方案,针对Vue3
的v-bind
特性在接下来的方案中会细说。
大体思路跟方案2相似,依然是提前将样式文件载入,切换时将指定的根元素类名更换。不过这里相对灵活的是,默认在根作用域下定义好CSS变量,只需要在不同的主题下更改CSS变量对应的取值即可。
顺带提一下,在Vue3官网还使用了color-scheme: dark;
将系统的滚动条设置为了黑色模式,使样式更加统一。
html.dark {
color-scheme: dark;
}
复制代码
实现方案如下:
/* 定义根作用域下的变量 */
:root {
--theme-color: #333;
--theme-background: #eee;
}
/* 更改dark类名下变量的取值 */
.dark{
--theme-color: #eee;
--theme-background: #333;
}
/* 更改pink类名下变量的取值 */
.pink{
--theme-color: #fff;
--theme-background: pink;
}
.box {
transition: all .2s;
width: 100px;
height: 100px;
border: 1px solid #000;
/* 使用变量 */
color: var(--theme-color);
background: var(--theme-background);
}
复制代码
表现效果如下:
虽然这种方式存在局限性只能在Vue开发中使用,但是为Vue项目开发者做动态样式更改提供了又一个不错的方案。
<script setup>
// 这里可以是原始对象值,也可以是ref()或reactive()包裹的值,根据具体需求而定
const theme = {
color: 'red'
}
</script>
<template>
<p>hello</p>
</template>
<style scoped>
p {
color: v-bind('theme.color');
}
</style>
复制代码
Vue3
中在style
样式通过v-bind()
绑定变量的原理其实就是给元素绑定CSS变量,在绑定的数据更新时调用CSSStyleDeclaration.setProperty更新CSS变量值。
前面方案3基于CSS变量绑定样式是在:root
上定义变量,然后在各个地方都可以获取到根元素上定义的变量。现在的方案我们需要考虑的问题是,如果是基于JS层面如何在各个组件上优雅地使用统一的样式变量?
我们可以利用Vuex或Pinia对全局样式变量做统一管理,如果不想使用类似的插件也可以自行封装一个hook,大致如下:
// 定义暗黑主题变量
export default {
fontSize: '16px',
fontColor: '#eee',
background: '#333',
};
复制代码
// 定义白天主题变量
export default {
fontSize: '20px',
fontColor: '#f90',
background: '#eee',
};
复制代码
import { shallowRef } from 'vue';
// 引入主题
import theme_day from './theme_day';
import theme_dark from './theme_dark';
// 定义在全局的样式变量
const theme = shallowRef({});
export function useTheme() {
// 尝试从本地读取
const localTheme = localStorage.getItem('theme');
theme.value = localTheme ? JSON.parse(localTheme) : theme_day;
const setDayTheme = () => {
theme.value = theme_day;
};
const setDarkTheme = () => {
theme.value = theme_dark;
};
return {
theme,
setDayTheme,
setDarkTheme,
};
}
复制代码
使用自己封装的主题hook
<script setup lang="ts">
import { useTheme } from './useTheme.ts';
import MyButton from './components/MyButton.vue';
const { theme } = useTheme();
</script>
<template>
<div class="box">
<span>Hello</span>
</div>
<my-button />
</template>
<style lang="scss">
.box {
width: 100px;
height: 100px;
background: v-bind('theme.background');
color: v-bind('theme.fontColor');
font-size: v-bind('theme.fontSize');
}
</style>
复制代码
<script setup lang="ts">
import { useTheme } from '../useTheme.ts';
const { theme, setDarkTheme, setDayTheme } = useTheme();
const change1 = () => {
setDarkTheme();
};
const change2 = () => {
setDayTheme();
};
</script>
<template>
<button class="my-btn" @click="change1">dark</button>
<button class="my-btn" @click="change2">day</button>
</template>
<style scoped lang="scss">
.my-btn {
color: v-bind('theme.fontColor');
background: v-bind('theme.background');
}
</style>
复制代码
表现效果如下:
其实从这里可以看到,跟Vue的响应式原理一样,只要数据发生改变,Vue就会把绑定了变量的地方通通更新。
主要是运用SCSS的混合+CSS类名切换,其原理主要是将使用到mixin混合的地方编译为固定的CSS以后,再通过类名切换去做样式的覆盖,实现方案如下: 定义SCSS变量:
/* 字体定义规范 */
$font_samll:12Px;
$font_medium_s:14Px;
$font_medium:16Px;
$font_large:18Px;
/* 背景颜色规范(主要) */
$background-color-theme: #d43c33;//背景主题颜色默认(网易红)
$background-color-theme1: #42b983;//背景主题颜色1(QQ绿)
$background-color-theme2: #333;//背景主题颜色2(夜间模式)
/* 背景颜色规范(次要) */
$background-color-sub-theme: #f5f5f5;//背景主题颜色默认(网易红)
$background-color-sub-theme1: #f5f5f5;//背景主题颜色1(QQ绿)
$background-color-sub-theme2: #444;//背景主题颜色2(夜间模式)
/* 字体颜色规范(默认) */
$font-color-theme : #666;//字体主题颜色默认(网易)
$font-color-theme1 : #666;//字体主题颜色1(QQ)
$font-color-theme2 : #ddd;//字体主题颜色2(夜间模式)
/* 字体颜色规范(激活) */
$font-active-color-theme : #d43c33;//字体主题颜色默认(网易红)
$font-active-color-theme1 : #42b983;//字体主题颜色1(QQ绿)
$font-active-color-theme2 : #ffcc33;//字体主题颜色2(夜间模式)
/* 边框颜色 */
$border-color-theme : #d43c33;//边框主题颜色默认(网易)
$border-color-theme1 : #42b983;//边框主题颜色1(QQ)
$border-color-theme2 : #ffcc33;//边框主题颜色2(夜间模式)
/* 字体图标颜色 */
$icon-color-theme : #ffffff;//边框主题颜色默认(网易)
$icon-color-theme1 : #ffffff;//边框主题颜色1(QQ)
$icon-color-theme2 : #ffcc2f;//边框主题颜色2(夜间模式)
$icon-theme : #d43c33;//边框主题颜色默认(网易)
$icon-theme1 : #42b983;//边框主题颜色1(QQ)
$icon-theme2 : #ffcc2f;//边框主题颜色2(夜间模式)
复制代码
定义混合mixin:
@import "./variable.scss";
@mixin bg_color(){
background: $background-color-theme;
[data-theme=theme1] & {
background: $background-color-theme1;
}
[data-theme=theme2] & {
background: $background-color-theme2;
}
}
@mixin bg_sub_color(){
background: $background-color-sub-theme;
[data-theme=theme1] & {
background: $background-color-sub-theme1;
}
[data-theme=theme2] & {
background: $background-color-sub-theme2;
}
}
@mixin font_color(){
color: $font-color-theme;
[data-theme=theme1] & {
color: $font-color-theme1;
}
[data-theme=theme2] & {
color: $font-color-theme2;
}
}
@mixin font_active_color(){
color: $font-active-color-theme;
[data-theme=theme1] & {
color: $font-active-color-theme1;
}
[data-theme=theme2] & {
color: $font-active-color-theme2;
}
}
@mixin icon_color(){
color: $icon-color-theme;
[data-theme=theme1] & {
color: $icon-color-theme1;
}
[data-theme=theme2] & {
color: $icon-color-theme2;
}
}
@mixin border_color(){
border-color: $border-color-theme;
[data-theme=theme1] & {
border-color: $border-color-theme1;
}
[data-theme=theme2] & {
border-color: $border-color-theme2;
}
}
复制代码
<template>
<div class="header" @click="changeTheme">
<div class="header-left">
<slot name="left">左边</slot>
</div>
<slot name="center" class="">中间</slot>
<div class="header-right">
<slot name="right">右边</slot>
</div>
</div>
</template>
<script>
export default {
name: 'Header',
methods: {
changeTheme () {
document.documentElement.setAttribute('data-theme', 'theme1')
}
}
}
</script>
<style scoped lang="scss">
@import "../assets/css/variable";
@import "../assets/css/mixin";
.header{
width: 100%;
height: 100px;
font-size: $font_medium;
@include bg_color();
}
</style>
复制代码
表现效果如下:
可以发现,使用mixin混合在SCSS编译后同样也是将所有包含的样式全部加载:
这种方案最后得到的结果与方案2类似,只是在定义主题时由于是直接操作的SCSS变量,会更加灵活。
此方案较于前几种会更加灵活,不过视情况而定,这个方案适用于由用户根据颜色面板自行设定各种颜色主题,这种是主题颜色不确定的情况,而前几种方案更适用于定义预设的几种主题。 方案参考:vue-element-plus-admin 主要实现思路如下: 只需在全局中设置好预设的全局CSS变量样式,无需单独为每一个主题类名下重新设定CSS变量值,因为主题是由用户动态决定。
:root {
--theme-color: #333;
--theme-background: #eee;
}
复制代码
定义一个工具类方法,用于修改指定的CSS变量值,调用的是CSSStyleDeclaration.setProperty
export const setCssVar = (prop: string, val: any, dom = document.documentElement) => {
dom.style.setProperty(prop, val)
}
复制代码
在样式发生改变时调用此方法即可
setCssVar('--theme-color', color)
复制代码
表现效果如下:
这里还用了vueuse
的useCssVar
不过效果和Vue3
中使用v-bind
绑定动态样式是差不多的,底层都是调用的CSSStyleDeclaration.setProperty这个api,这里就不多赘述vueuse中的用法。
:root
上动态更改CSS变量而Vue3中会将CSS变量绑定到任何依赖该变量的节点上。:root
上动态更改CSS变量值即可,不存在优先级问题说明:两种主题方案都支持并不代表一定是最佳方案,视具体情况而定。
方案/主题样式 | 固定预设主题样式 | 主题样式不固定 |
---|---|---|
方案1:link标签动态引入 | √(文件过大,切换延时,不推荐) | × |
方案2:提前引入所有主题样式,做类名切换 | √ | × |
方案3:CSS变量+类名切换 | √(推荐) | × |
方案4:Vue3新特性(v-bind) | √(性能不确定) | √(性能不确定) |
方案5:SCSS + mixin + 类名切换 | √(推荐,最终呈现效果与方案2类似,但定义和使用更加灵活) | × |
方案6:CSS变量+动态setProperty | √(更推荐方案3) | √(推荐) |
关于本文
作者:Victor https://juejin.cn/post/7185539936722878525