前端主題切換方案
現在我們經常可以看到一些網站會有類似暗黑模式/白天模式
的主題切換功能,效果也是十分炫酷,在平時的開發場景中也有越來越多這樣的需求,這裏大致羅列一些常見的主題切換方案並分析其優劣,大家可根據需求綜合分析得出一套適用的方案。
方案 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 官網 [1]
在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[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 就會把綁定了變量的地方通通更新。
優點:
-
不用重新加載樣式文件,在樣式切換時不會有卡頓
-
在需要切換主題的地方利用 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>
<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 變量,會更加靈活。
優點:
-
不用重新加載樣式文件,在樣式切換時不會有卡頓
-
在需要切換主題的地方利用 mixin 混合綁定變量即可,不存在優先級問題
-
新增或修改主題方便靈活,僅需新增或修改 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 主題切換源碼:
這裏還用了vueuse
的useCssVar
不過效果和Vue3
中使用v-bind
綁定動態樣式是差不多的,底層都是調用的 CSSStyleDeclaration.setProperty 這個 api,這裏就不多贅述 vueuse 中的用法。
優點:
-
不用重新加載樣式文件,在樣式切換時不會有卡頓
-
仔細琢磨可以發現其原理跟方案 4 利用 Vue3 的新特性 v-bind 是一致的,只不過此方案只在
:root
上動態更改 CSS 變量而 Vue3 中會將 CSS 變量綁定到任何依賴該變量的節點上。 -
需要切換主題的地方只用在
:root
上動態更改 CSS 變量值即可,不存在優先級問題 -
新增或修改主題方便靈活
缺點:
-
IE 兼容性(忽略不計)
-
首屏加載時會犧牲一些時間加載樣式資源(相對於前幾種預設好的主題,這種方式的樣式定義在首屏加載基本可以忽略不計)
方案總結
說明:兩種主題方案都支持並不代表一定是最佳方案,視具體情況而定。
參考資料
[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