天天看點

Web前端主題切換方案

作者:程式猿最幽默

前端主題切換方案

Web前端主題切換方案

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

方案1:link标簽動态引入

其做法就是提前準備好幾套CSS主題樣式檔案,在需要的時候,建立link标簽動态加載到head标簽中,或者是動态改變link标簽的href屬性。

Web前端主題切換方案

表現效果如下:

Web前端主題切換方案

網絡請求如下:

Web前端主題切換方案

優點:

  • 實作了按需加載,提高了首屏加載時的性能

缺點:

  • 動态加載樣式檔案,如果檔案過大網絡情況不佳的情況下可能會有加載延遲,導緻樣式切換不流暢
  • 如果主題樣式表内定義不當,會有優先級問題
  • 各個主題樣式是寫死的,後續針對某一主題樣式表修改或者新增主題也很麻煩

方案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;
}           

表現效果如下:

Web前端主題切換方案

優點:

  • 不用重新加載樣式檔案,在樣式切換時不會有卡頓

缺點:

  • 首屏加載時會犧牲一些時間加載樣式資源
  • 如果主題樣式表内定義不當,也會有優先級問題
  • 各個主題樣式是寫死的,後續針對某一主題樣式表修改或者新增主題也很麻煩

方案小結

通過以上兩個方案,我們可以看到對于樣式的加載問題上的考量就類似于在糾結是做SPA單頁應用還是MPA多頁應用項目一樣。兩種其實都誤傷大雅,但是最重要的是要保證在後續的持續開發疊代中怎樣會更友善。是以我們還可以基于以上存在的問題和方案做進一步的增強。

在做主題切換技術調研時,看到了網友的一條建議:

Web前端主題切換方案

是以下面的幾個方案主要是針對變量來做樣式切換

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

表現效果如下:

Web前端主題切換方案

優點:

  • 不用重新加載樣式檔案,在樣式切換時不會有卡頓
  • 在需要切換主題的地方利用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>           

表現效果如下:

Web前端主題切換方案

其實從這裡可以看到,跟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>           

表現效果如下:

Web前端主題切換方案

可以發現,使用mixin混合在SCSS編譯後同樣也是将所有包含的樣式全部加載:

Web前端主題切換方案

這種方案最後得到的結果與方案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)           

表現效果如下:

Web前端主題切換方案

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

Web前端主題切換方案

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

Web前端主題切換方案

優點:

  • 不用重新加載樣式檔案,在樣式切換時不會有卡頓
  • 仔細琢磨可以發現其原理跟方案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) √(推薦)