网页主题切换的实现方案

各位看官大家好,想必大家在工作中一定遇到了网页主题切换或者一键换肤的功能,接下来我为大家整理了以下几种实现方案供大家参考学习。也请大家评论补充。

方案一:通过link标签动态引入

其做法就是提前准备好几套CSS主题样式文件,在需要的时候,创建link标签动态加载到head标签中,或者是动态改变link标签的href属性。

优点:

  • 实现了按需加载,提高了首屏加载时的性能

缺点:

  • 动态加载样式文件,如果文件过大网络情况不佳的情况下可能会有加载延迟,导致样式切换不流畅。

  • 各个主题样式是写死的,后续针对某一主题样式表修改或者新增主题也很麻烦。可维护性不高。

方案二:提前引入主题的样式,做类名的切换

这种方案与第一种比较类似,为了解决反复加载样式文件问题提前将样式全部引入,在需要切换主题的时候将指定的根元素类名更换,相当于直接做了样式覆盖,在该类名下的各个样式就统一地更换了。其基本方法如下:

body.light{
  color: #f90;
  background: #fff;
}
.light .xxx{

}
body.dark{
  color: #eee;
  background: #333;
}
.dark .xxx{

}

优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿

缺点:

  • 首屏加载时会牺牲一些时间加载样式资源

  • 各个主题样式是写死的,后续针对某一主题样式表修改或者新增主题也很麻烦

方案三:CSS变量名称+类名切换

大体思路跟方案2相似,依然是提前将样式文件载入,切换时将指定的根元素类名更换。不过这里相对灵活的是,默认在根作用域下定义好CSS变量,只需要在不同的主题下更改CSS变量对应的取值即可。
在Vue3官网有一个暗黑模式切换按钮,点击之后就会平滑地过渡,虽然Vue3中也有一个v-bind特性可以实现动态样式绑定,但经过观察以后Vue官网并没有采取这个方案,针对Vue3的v-bind特性在接下来的方案中会细说。
具体实现如下:

* 定义根作用域下的变量 */
: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);
}

优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿
  • 在需要切换主题的地方利用var()绑定变量即可,不存在优先级问题
  • 新增或修改主题方便灵活,仅需新增或修改CSS变量即可,在var()绑定样式变量的地方就会自动更换

缺点:

  • IE兼容性(忽略不计)
  • 首屏加载时会牺牲一些时间加载样式资源

方案四:使用VUE3的v-bind特性

虽然这种方式存在局限性只能在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,大致如下:

// 定义暗黑主题变量
exportdefault { 
 fontSize: '16px', 
 fontColor: '#eee',
 background: '#333',};
// 定义白天主题变量
exportdefault {  
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 setuplang="ts">
import { useTheme } from'./useTheme.ts';
importMyButtonfrom'./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 setuplang="ts">
import { useTheme } from'../useTheme.ts';
const { theme, setDarkTheme, setDayTheme } = useTheme();
constchange1 = () => {
  setDarkTheme();
};
constchange2 = () => {
  setDayTheme();
};
</script>

<template>
<button class="my-btn" @click="change1">dark</button>
<button class="my-btn" @click="change2">day</button>
</template>

<style scopedlang="scss">
.my-btn {
color: v-bind('theme.fontColor');
background: v-bind('theme.background');
}
</style>

其实从这里可以看到,跟Vue的响应式原理一样,只要数据发生改变,Vue就会把绑定了变量的地方通通更新。

优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿

  • 在需要切换主题的地方利用v-bind绑定变量即可,不存在优先级问题

  • 新增或修改主题方便灵活,仅需新增或修改JS变量即可,在v-bind()绑定样式变量的地方就会自动更换

缺点:

  • IE兼容性(忽略不计)

  • 首屏加载时会牺牲一些时间加载样式资源

这种方式只要是在组件上绑定了动态样式的地方都会有对应的编译成哈希化的CSS变量,而不像方案3统一地就在:root上设置(不确定在达到一定量级以后的性能),也可能正是如此,Vue官方也并未采用此方式做全站的主题切换

方案五:mixin+scss+类名切换

主要是运用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:
```vue
@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>
exportdefault {    
name: 'Header',    
methods: {      
changeTheme () {
document.documentElement.setAttribute('data-theme', 'theme1')      
}    
}  
}</script>
<style scopedlang="scss">
@import"../assets/css/variable";
@import"../assets/css/mixin";
.header{width: 100%;height: 100px;
font-size: $font_medium;
@include bg_color();
}
</style>

这种方案最后得到的结果与方案2类似,只是在定义主题时由于是直接操作的SCSS变量,会更加灵活。

优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿

  • 在需要切换主题的地方利用mixin混合绑定变量即可,不存在优先级问题

  • 新增或修改主题方便灵活,仅需新增或修改SCSS变量即可,经过编译后会将所有主题全部编译出来

缺点:

  • 首屏加载时会牺牲一些时间加载样式资源

方案六:CSS变量+动态setProperty

此方案较于前几种会更加灵活,不过视情况而定,这个方案适用于由用户根据颜色面板自行设定各种颜色主题,这种是主题颜色不确定的情况,而前几种方案更适用于定义预设的几种主题。
方案参考:vue-element-plus-admin
主要实现思路如下:
只需在全局中设置好预设的全局CSS变量样式,无需单独为每一个主题类名下重新设定CSS变量值,因为主题是由用户动态决定。

:root {  --theme-color: #333;  --theme-background: #eee;}

定义一个工具类方法,用于修改指定的CSS变量值,调用的是CSSStyleDeclaration.setProperty

exportconstsetCssVar = (prop: string, val: any, dom = document.documentElement) => {  dom.style.setProperty(prop, val)}

在样式发生改变时调用此方法即可

setCssVar('--theme-color', color)

优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿

  • 仔细琢磨可以发现其原理跟方案4利用Vue3的新特性v-bind是一致的,只不过此方案只在:root上动态更改CSS变量而Vue3中会将CSS变量绑定到任何依赖该变量的节点上。

  • 需要切换主题的地方只用在:root上动态更改CSS变量值即可,不存在优先级问题

  • 新增或修改主题方便灵活

缺点:

  • IE兼容性(忽略不计)

  • 首屏加载时会牺牲一些时间加载样式资源(相对于前几种预设好的主题,这种方式的样式定义在首屏加载基本可以忽略不计)

方案七:利用prefers-color-schemes特性

prefers-color-scheme是CSS 媒体特性【@media】用于检测用户是否有将操作系统的主题色设置为亮色【light】或者暗色【dark】,这俩个也是prefers-color-scheme重要属性。W3C 在 2020 年 7 月 31日首次提到css的prefers-color-scheme新特性。
当前prefers-color-scheme新特性支持各大主流电脑【window和IOS系统,Linux系统可以用第三方工具】端浏览器谷歌、火狐等,包括手机端的安卓和苹果,足以说明prefers-color-scheme属性已经稳定成熟,可以用于生产环境了。
如何正确使用prefers-color-scheme属性呢?答:需要在全局css文件内部写入下面代码即可,用于监听系统主题变化结果:

:root {
    --color-background: #1b1b1b;
    --white-color-background: #fff;
    color-scheme: light dark;
}
/* 监听操作系统主题模式 */
@media (prefers-color-scheme: dark) {
    body {
        background-color: var(--color-background);
    }
}

@media (prefers-color-scheme: light) {
    body {
        background-color: var(--white-color-background);
    }
} 

当操作系统电脑端或者手机操作系统主题颜色变化时就能监听到主题,然后就会按照你预期设置的颜色进行渲染主题色。比如这里的body暗色主题是background-color: var(--color-background),亮色主题是background-color: var(--white-color-background);
提示::root表示根元素,拥有更高的优先级,这里可以设置全局样式变量,通过css的var方法来获取对应变量且获得相应的样式。
那么除了媒体监听主题变化,能不能自定义主题呢?答:能,可以通过js来操作操作,实现自由切换主题颜色,具体细节请往下看。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>系统的主题色设置为亮色或者暗色</title>
    <style lang="css">
        /* 整个页面配置为使用用户的配色方案首选项 */
        /* 根元素,优先级最高,与html选择器相同 */
        :root {
            --color-background: #1b1b1b;
            --color-border: #cacfd5;
            --color-text-default: #0b1016;
            --color-base: #f4f5f6;
            --color-accent: #ba0d37;
            --white-color-background: #fff;
            color-scheme: light dark;
        }
        * {
            margin: 0;
            padding: 0;
        }
        body {
            text-align: center;
            height: 100vh;
        }
        .light-scheme {
            background: var(--white-color-background);
            color: var(--color-text-default);
        }
        .dark-scheme {
            background: var(--color-background);
            color: white;
        }
        h2 {
            margin: 50px auto;
            color: var(--color-accent)
        }

        /* 监听操作系统主题模式 */
        @media (prefers-color-scheme: dark) {
            body {
                background-color: var(--color-background);
            }
        }
        @media (prefers-color-scheme: light) {
            body {
                background-color: var(--white-color-background);
            }
        }
        .tab-type {
            display: flex;
            justify-content: center;
            padding-top: 30px;
        }
        .tab-type > li {
            cursor: pointer;
            color: #fff;
            background-color: darkred;
            border-radius: 12px;
            padding: 5px;
            margin: 0px 20px;
            max-width: 300px;
        }
</style>
</head>

<body>
    <div class="content" id="content">
        <ul class="tab-type">
            <li id="light">浅色主题</li>
            <li id="dark">暗色主题</li>
        </ul>
        <h2>微信公众号:[布依前端],布衣前端,专注于前端知识分享的up</h2>
        <div class="scheme-tip"></div>
    </div>
    <script>
        // 手动修改主题颜色
        const light = document.getElementById('light')
        const dark = document.getElementById('dark')
        const content = document.getElementsByTagName('body')[0]
        const tipText = document.getElementsByClassName('scheme-tip')[0]
        let lightTip = '当前自定义主题:light亮色', darkTip = '当前自定义主题:dark暗色'

        light.onclick = function (params) {
            content.setAttribute('class', 'light-scheme')
            tipText.innerHTML = lightTip
        }
        dark.onclick = function (params) {
            content.setAttribute('class', 'dark-scheme')
            tipText.innerHTML = darkTip
        }

        // js 监听系统主题模式
        const scheme = window.matchMedia('(prefers-color-scheme: dark)')
        if (scheme.matches) {
            // 深色模式业务处理代码
            console.log('深色模式');
            tipText.innerHTML = darkTip
        } else {
            // 浅色模式业务处理代码
            console.log('浅色模式');
            tipText.innerHTML = lightTip
        }
</script>
</body>
</html> 

当网站不满足系统默认主题,还想提供用户自己切换主题的功能,代码中有两个按钮,【浅色主题】和【暗色主题】就是干这件事情的。用户可以随意切换,具体切换代码可以到js部分查看。

除了css媒体能监听操作系统主题变化,js也能监听的,当监听到后可以增强js业务逻辑,比如设置某个主题下布局变化、语音播报当前模式等等和其他业务处理逻辑,需要设置页面加载后添加如下代码即可:

// js 监听系统主题模式
const scheme = window.matchMedia('(prefers-color-scheme: dark)')
if (scheme.matches) {
    // 深色模式业务处理代码
    console.log('深色模式');
    tipText.innerHTML = darkTip
} else {
    // 浅色模式业务处理代码
    console.log('浅色模式');
    tipText.innerHTML = lightTip
} 

总结prefers-color-scheme监听方式:

css里通过@media监听

js里面通过matchMedia监听

两种方式都能监听到操作系统主题变化后的值.

posted @ 2023-06-01 13:58  Cxymds  阅读(322)  评论(0编辑  收藏  举报