前端主題切換方案

現在我們經常可以看到一些網站會有類似暗黑模式/白天模式的主題切換功能,效果也是十分炫酷,在平時的開發場景中也有越來越多這樣的需求,這裏大致羅列一些常見的主題切換方案並分析其優劣,大家可根據需求綜合分析得出一套適用的方案。

其做法就是提前準備好幾套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 官網 [1]

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);
}

表現效果如下:

圖片過大,截圖處理

優點:

缺點:

方案 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[2] 更新 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 就會把綁定了變量的地方通通更新。

優點:

缺點:

方案 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>
    <slot >中間</slot>
    <div class="header-right">
      <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 變量,會更加靈活。

優點:

缺點:

方案 6:CSS 變量 + 動態 setProperty

此方案較於前幾種會更加靈活,不過視情況而定,這個方案適用於由用戶根據顏色面板自行設定各種顏色主題,這種是主題顏色不確定的情況,而前幾種方案更適用於定義預設的幾種主題。

方案參考:vue-element-plus-admin[3]

主要實現思路如下:

只需在全局中設置好預設的全局 CSS 變量樣式,無需單獨爲每一個主題類名下重新設定 CSS 變量值,因爲主題是由用戶動態決定。

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

定義一個工具類方法,用於修改指定的 CSS 變量值,調用的是 CSSStyleDeclaration.setProperty[4]

export const setCssVar = (prop: string, val: any, dom = document.documentElement) ={
  dom.style.setProperty(prop, val)
}

在樣式發生改變時調用此方法即可

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

表現效果如下:

vue-element-plus-admin 主題切換源碼:

這裏還用了vueuseuseCssVar不過效果和Vue3中使用v-bind綁定動態樣式是差不多的,底層都是調用的 CSSStyleDeclaration.setProperty 這個 api,這裏就不多贅述 vueuse 中的用法。

優點:

缺點:

方案總結

說明:兩種主題方案都支持並不代表一定是最佳方案,視具體情況而定。

E2dk7D

參考資料

[1] Vue3 官網: https://staging-cn.vuejs.org/

[2] CSSStyleDeclaration.setProperty: https://developer.mozilla.org/zh-CN/docs/Web/API/CSSStyleDeclaration/setProperty

[3] vue-element-plus-admin: https://gitee.com/kailong110120130/vue-element-plus-admin

[4] CSSStyleDeclaration.setProperty: https://developer.mozilla.org/zh-CN/docs/Web/API/CSSStyleDeclaration/setProperty

作者:四相前端團隊

https://juejin.cn/post/7134594122391748615

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/fj02N5qSPKfkkFvRTA2Rcw