本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
现在我们经常可以看到一些网站会有类似暗黑模式/白天模式的主题切换功能,效果也是十分炫酷,在平时的开发场景中也有越来越多这样的需求,这里大致罗列一些常见的主题切换方案并分析其优劣,大家可根据需求综合分析得出一套适用的方案。
本文共讲解了 5 个方案,以及优缺点对比,请耐心查看。
方案 1:link 标签动态引入
其做法就是提前准备好几套CSS主题样式文件,在需要的时候,创建link标签动态加载到head标签中,或者是动态改变link标签的href属性。
表现效果如下:
网络请求如下:
优点:
- 实现了按需加载,提高了首屏加载时的性能
缺点:
动态加载样式文件,如果文件过大网络情况不佳的情况下可能会有加载延迟,导致样式切换不流畅
如果主题样式表内定义不当,会有优先级问题
各个主题样式是写死的,后续针对某一主题样式表修改或者新增主题也很麻烦
方案 2:提前引入所有主题样式,做类名切换
这种方案与第一种比较类似,为了解决反复加载样式文件问题提前将样式全部引入,在需要切换主题的时候将指定的根元素类名更换,相当于直接做了样式覆盖,在该类名下的各个样式就统一地更换了。其基本方法如下:
/* 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 多页应用项目一样。两种其实都无伤大雅,但是最重要的是要保证在后续的持续开发迭代中怎样会更方便。因此我们还可以基于以上存在的问题和方案做进一步的增强。
在做主题切换技术调研时,看到了网友的一条建议:
因此下面的几个方案主要是针对变量来做样式切换
方案 3:CSS 变量 + 类名切换
灵感参考: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);}复制代码表现效果如下:
优点:
不用重新加载样式文件,在样式切换时不会有卡顿
在需要切换主题的地方利用 var() 绑定变量即可,不存在优先级问题
新增或修改主题方便灵活,仅需新增或修改 CSS 变量即可,在 var() 绑定样式变量的地方就会自动更换
缺点:
IE 兼容性(忽略不计)
首屏加载时会牺牲一些时间加载样式资源
方案 4: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,大致如下:
// 定义暗黑主题变量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 就会把绑定了变量的地方通通更新。
优点:
不用重新加载样式文件,在样式切换时不会有卡顿
在需要切换主题的地方利用 v-bind 绑定变量即可,不存在优先级问题
新增或修改主题方便灵活,仅需新增或修改 JS 变量即可,在 v-bind() 绑定样式变量的地方就会自动更换
缺点:
IE 兼容性(忽略不计)
首屏加载时会牺牲一些时间加载样式资源
这种方式只要是在组件上绑定了动态样式的地方都会有对应的编译成哈希化的 CSS 变量,而不像方案 3 统一地就在: root 上设置(不确定在达到一定量级以后的性能),也可能正是如此,Vue 官方也并未采用此方式做全站的主题切换
方案 5:SCSS + mixin + 类名切换
主要是运用 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 >右边</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 变量,会更加灵活。
优点:
不用重新加载样式文件,在样式切换时不会有卡顿
在需要切换主题的地方利用 mixin 混合绑定变量即可,不存在优先级问题
新增或修改主题方便灵活,仅需新增或修改 SCSS 变量即可,经过编译后会将所有主题全部编译出来
缺点:
- 首屏加载时会牺牲一些时间加载样式资源
方案 6:CSS 变量 + 动态 setProperty
此方案较于前几种会更加灵活,不过视情况而定,这个方案适用于由用户根据颜色面板自行设定各种颜色主题,这种是主题颜色不确定的情况,而前几种方案更适用于定义预设的几种主题。
方案参考: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)复制代码表现效果如下:
vue-element-plus-admin 主题切换源码:
这里还用了vueuse的useCssVar不过效果和Vue3中使用v-bind绑定动态样式是差不多的,底层都是调用的 CSSStyleDeclaration.setProperty 这个 api,这里就不多赘述 vueuse 中的用法。
优点:
不用重新加载样式文件,在样式切换时不会有卡顿
仔细琢磨可以发现其原理跟方案 4 利用 Vue3 的新特性 v-bind 是一致的,只不过此方案只在
:root上动态更改 CSS 变量而 Vue3 中会将 CSS 变量绑定到任何依赖该变量的节点上。需要切换主题的地方只用在
:root上动态更改 CSS 变量值即可,不存在优先级问题新增或修改主题方便灵活
缺点:
IE 兼容性(忽略不计)
首屏加载时会牺牲一些时间加载样式资源(相对于前几种预设好的主题,这种方式的样式定义在首屏加载基本可以忽略不计)
方案总结
说明:两种主题方案都支持并不代表一定是最佳方案,视具体情况而定。
| 方案 / 主题样式 | 固定预设主题样式 | 主题样式不固定 |
|---|---|---|
| 方案 1:link 标签动态引入 | √(文件过大,切换延时,不推荐) | × |
| 方案 2:提前引入所有主题样式,做类名切换 | √ | × |
| 方案 3:CSS 变量 + 类名切换 | √(推荐) | × |
| 方案 4:Vue3 新特性(v-bind) | √(性能不确定) | √(性能不确定) |
| 方案 5:SCSS + mixin + 类名切换 | √(推荐,最终呈现效果与方案 2 类似,但定义和使用更加灵活) | × |
| 方案 6:CSS 变量 + 动态 setProperty | √(更推荐方案 3) | √(推荐) |
关于本文
作者:Victor