Front-end theme switching scheme

Front-end theme switching scheme

theme.gifNow we can often see that some websites have similar 暗黑模式/白天模式theme switching functions, and the effect is also very cool. There are more and more such needs in the usual development scenarios. Here are some common theme switching schemes and analyze their advantages. Inferior, we can comprehensively analyze a set of applicable plans according to the needs.

Option 1: Dynamic introduction of link tags

The method is to prepare several sets of CSStheme style files in advance, and when needed, create linktags and load them dynamically into headtags, or dynamically change the attributes of linktags . The performance is as follows: The network request is as follows:href
image.png
theme-switch1.gif
theme-switch1-2.gif

advantage:

  • Implemented on-demand loading and improved performance when loading above the fold

shortcoming:

  • Dynamically load style files. If the file is too large and the network conditions are poor, there may be a loading delay, resulting in unsmooth style switching.
  • If the theme is not properly defined in the style sheet, there will be priority issues
  • Each theme style is written to death, and it is also very troublesome to modify or add a theme for a certain theme style sheet later.

Option 2: Introduce all theme styles in advance and switch class names

This solution is similar to the first one. In order to solve the problem of repeatedly loading style files, all styles are introduced in advance, and when the theme needs to be switched, the specified root element class name is replaced, which is equivalent to directly overwriting the style. All styles below are replaced uniformly. The basic method is as follows:

/* 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;
}
复制代码

The performance is as follows:

theme-switch2.gif

advantage:

  • No need to reload the style file, there will be no lag when switching styles

shortcoming:

  • It takes some time to load style resources when loading above the fold
  • There is also a priority problem if it is not properly defined in the theme style sheet
  • Each theme style is written to death, and it is also very troublesome to modify or add a theme for a certain theme style sheet later.

Program summary

Through the above two solutions, we can see that the consideration of style loading is similar to the tangle of whether to make a SPA single-page application or an MPA multi-page application project. Both are actually wrong, but the most important thing is to ensure that it will be more convenient in subsequent continuous development iterations. Therefore, we can make further enhancements based on the above existing problems and solutions.

When doing research on theme switching technology, I saw a suggestion from netizens:

Flexible switching styles.pngTherefore, the following schemes are mainly for the style switching of variables

Option 3: CSS variable + class name switch

灵感参考:Vue3官网
Vue3官网有一个暗黑模式切换按钮,点击之后就会平滑地过渡,虽然Vue3中也有一个v-bind特性可以实现动态样式绑定,但经过观察以后Vue官网并没有采取这个方案,针对Vue3v-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);
}
复制代码

表现效果如下: theme-switch3.gif

优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿
  • 在需要切换主题的地方利用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>
复制代码

表现效果如下: theme-switch4.gif
其实从这里可以看到,跟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 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>
复制代码

表现效果如下:

theme-switch5.gif
可以发现,使用mixin混合在SCSS编译后同样也是将所有包含的样式全部加载:
image1.png
这种方案最后得到的结果与方案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)
复制代码

表现效果如下:

theme-switch6.gif

vue-element-plus-admin主题切换源码:

image2.png
这里还用了vueuseuseCssVar不过效果和Vue3中使用v-bind绑定动态样式是差不多的,底层都是调用的CSSStyleDeclaration.setProperty这个api,这里就不多赘述vueuse中的用法。
image4.png

优点:

  • 不用重新加载样式文件,在样式切换时不会有卡顿
  • 仔细琢磨可以发现其原理跟方案4利用Vue3的新特性v-bind是一致的,只不过此方案只在:root上动态更改CSS变量而Vue3中会将CSS变量绑定到任何依赖该变量的节点上。
  • 需要切换主题的地方只用在:root上动态更改CSS变量值即可,不存在优先级问题
  • 新增或修改主题方便灵活

缺点:

  • IE兼容性(忽略不计)
  • When loading the first screen, it will sacrifice some time to load style resources (compared to the previous preset themes, the style definition in this way can be ignored when loading the first screen)

Program summary

Note: The support of both thematic schemes does not necessarily mean that it is the best scheme, depending on the specific situation.

Scheme/Theme Style Fixed default theme styles Theme style is not fixed
Option 1: Dynamic introduction of link tags √ (The file is too large, the switching delay is not recommended) ×
Option 2: Introduce all theme styles in advance and switch class names ×
Option 3: CSS variable + class name switch √ (recommended) ×
Solution 4: Vue3 new features (v-bind) √ (Performance uncertain) √ (Performance uncertain)
Solution 5: SCSS + mixin + class name switching √ (recommended, the final presentation effect is similar to scheme 2, but the definition and use are more flexible) ×
Scenario 6: CSS variables + dynamic setProperty √ (More recommended option 3) √ (recommended)

Guess you like

Origin juejin.im/post/7134594122391748615