常用Vue UI框架的主題切換
在如今,很多網頁已經可以手動切換明亮模式和黑暗模式,網頁的主題切換已經成為了一個常用的需求,因此,本文將從常見框架的處理方式總結一些相關的操作,並探究其本質。
AntD 的方式
AntD 的 config-provider
似乎不能直接修改主題,目前網路上的教程有兩類,一類是通過 less 的修改變數修改來完成,另一類是操作 DOM 改變對檔案的引用,由於 ant-design-vue 已經提供了黑暗主題,而且直接修改 less 檔案的變數涉及到了太多變數,不太方便,因此參考了 @杭州程式設計師張張的文章,使用動態新增 CSS 引用 DOM 的方式實現黑暗主題的切換,原理是新增一個 DOM,引用我們自定義的 CSS 樣式,後面的引用會覆蓋前面的引用,在切換到亮色主題時把這個 DOM 刪除。
1. 建立黑暗主題檔案
在目錄下的適當位置建立一個黑暗主題的檔案,後面要引用這個檔案,這裡直接把官方黑暗主題複製過來就可以了,它的位置在 node_modules\ant-design-vue\dist\antd.dark.min.css
,假設我們把它複製到了 public/styles/dark.css
中。
2. 建立切換主題的函式
有了主題檔案,我們就可以建立切換主題的函數了,在目錄中的合適位置建立一個 js 檔案,例如 src/utils/theme.ts
,其中的程式碼為:
const darkThemeSwitch = () => { // id要與前面一致 const theme_element = document.querySelector('#dark-theme-style'); if (!theme_element) { const new_dark_Theme = document.createElement('link') new_dark_Theme.setAttribute('rel', 'stylesheet'); //剛剛設定的css路徑 new_dark_Theme.setAttribute('href', '/styles/dark.css'); // id可以自由設定 new_dark_Theme.setAttribute('id', 'dark-theme-style'); const docHead = document.querySelector('head'); docHead?.append(new_dark_Theme); } else { const parentNode = theme_element?.parentNode; parentNode?.removeChild(theme_element); } } export default darkThemeSwitch;
3. 切換主題
然後,我們在需要切換主題的地方使用這個方法就可以了:
<template>
<a-button @click="toggleDark">切換主題</a-button>
</template>
<script setup lang="ts">
import darkThemeSwitch from '../utils/theme';
const toggleDark = () => {
darkThemeSwitch();
};
</script>
Quasar 的方式
quasar 的主題切換非常簡單,只要使用 quasar 官方提供的 dark 外掛就可以了,使用方法如下:
import { useQuasar } from 'quasar'
const $q = useQuasar()
// get status
console.log($q.dark.isActive) // true, false
// get configured status
console.log($q.dark.mode) // "auto", true, false
// set status
$q.dark.set(true) // or false or "auto"
// toggle
$q.dark.toggle()
通過研究 Quasar 的原始碼,可以發現,Quasar 的黑暗有兩種設定方式,自動和手動,在自動模式下,外掛通過監聽 prefers-color-scheme: dark
的狀態來決定當前網頁的主題,通過這種方式,可以實現網頁跟隨系統主題自動變換淺色和深色模式,對應部分的程式碼如下:
Plugin.mode = val
if (val === 'auto') {
if (Plugin.__media === void 0) {
Plugin.__media = window.matchMedia('(prefers-color-scheme: dark)')
Plugin.__updateMedia = () => { Plugin.set('auto') }
Plugin.__media.addListener(Plugin.__updateMedia)
}
val = Plugin.__media.matches
}
上面的程式碼中,Plugin.mode
代表設定方式,Plugin.__media
的值預設就是 void 0
,這裡的判斷的作用是當第一次執行時給相應的 Media 新增一個監聽來獲得實時的變化。這裡呼叫的 Plugin.set('auto')
就是手動設定的函式,這個函式接受一個布林值,然後根據布林值的真假對主題進行設定,相應的實現如下:
document.body.classList.remove(`body--${ val === true ? 'light' : 'dark' }`)
document.body.classList.add(`body--${ val === true ? 'dark' : 'light' }`)
顯然,這裡的實現方式也比較常規,即在 body 標籤新增或刪除相應的樣式選擇器類名來實現主題的切換,quasar 值得我們學習的地方在於它把所有的功能整合到 useQuasar
中,使用者可以方便地使用各種外掛,而不需要寫很多程式碼。完整的程式碼如下:
import defineReactivePlugin from '../utils/private/define-reactive-plugin.js'
import { isRuntimeSsrPreHydration } from './Platform.js'
const Plugin = defineReactivePlugin({
// 外掛的屬性
isActive: false,
mode: false
}, {
__media: void 0,
// 外掛的核心方法
set(val) {
if (__QUASAR_SSR_SERVER__) { return }
Plugin.mode = val
// 自動主題切換
if (val === 'auto') {
if (Plugin.__media === void 0) {
Plugin.__media = window.matchMedia('(prefers-color-scheme: dark)')
Plugin.__updateMedia = () => { Plugin.set('auto') }
Plugin.__media.addListener(Plugin.__updateMedia)
}
val = Plugin.__media.matches
}
else if (Plugin.__media !== void 0) {
Plugin.__media.removeListener(Plugin.__updateMedia)
Plugin.__media = void 0
}
Plugin.isActive = val === true
// 手動指定主題
document.body.classList.remove(`body--${val === true ? 'light' : 'dark'}`)
document.body.classList.add(`body--${val === true ? 'dark' : 'light'}`)
},
// 主題的切換
toggle() {
if (__QUASAR_SSR_SERVER__ !== true) {
Plugin.set(Plugin.isActive === false)
}
},
install({ $q, onSSRHydrated, ssrContext }) {
const { dark } = $q.config
if (__QUASAR_SSR_SERVER__) {
this.isActive = dark === true
$q.dark = {
isActive: false,
mode: false,
set: val => {
ssrContext._meta.bodyClasses = ssrContext._meta.bodyClasses
.replace(' body--light', '')
.replace(' body--dark', '') + ` body--${val === true ? 'dark' : 'light'}`
$q.dark.isActive = val === true
$q.dark.mode = val
},
toggle: () => {
$q.dark.set($q.dark.isActive === false)
}
}
$q.dark.set(dark)
return
}
$q.dark = this
if (this.__installed === true && dark === void 0) {
return
}
this.isActive = dark === true
const initialVal = dark !== void 0 ? dark : false
if (isRuntimeSsrPreHydration.value === true) {
const ssrSet = val => {
this.__fromSSR = val
}
const originalSet = this.set
this.set = ssrSet
ssrSet(initialVal)
onSSRHydrated.push(() => {
this.set = originalSet
this.set(this.__fromSSR)
})
}
else {
this.set(initialVal)
}
}
})
export default Plugin
ElementUI 的方式
在 2.2.0 版本,Element UI 也可以進行黑暗模式切換了,操作如下:
-
在
main.ts
引入官方的 CSS:import 'element-plus/theme-chalk/dark/css-vars.css'
-
更改
html
標籤的類,新增dark
即為黑暗模式,去掉 dark 即為明亮模式,這一過程有很多實現方法,官方推薦使用VueUse
這個函式庫中的useDark
來實現,這是一個非常強大的官方工具庫,裡面提供了許多常用功能的實現以及一些小外掛,非常值得學習,請參考VueUse的官方文件:<template> <el-button @click="toggleDark()">切換主題</el-button> </template> <script setup lang="ts"> import { useDark, useToggle } from '@vueuse/core'; const isDark = useDark(); const toggleDark = useToggle(isDark); </script>
NaiveUI 的方式
NaiveUI 的黑暗主題是通過其提供的 Config Provider
元件來實現的,通過設定其 theme
屬性為 null
或者 darkTheme
(需要從 naive-ui 匯入),可以更改其內部元件的主題,配合 Global Style
元件,可以設定全域性的主題:
<template>
<n-config-provider :theme="theme">
<n-card>
<n-space>
<n-button @click="theme = darkTheme">
深色
</n-button>
<n-button @click="theme = null">
淺色
</n-button>
</n-space>
</n-card>
<!-- 加上這個後可以切換全域性主題 -->
<n-global-style />
</n-config-provider>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { darkTheme } from 'naive-ui'
import type { GlobalTheme } from 'naive-ui'
const theme = ref<GlobalTheme | null>(null)
</script>
還可以通過 useOsTheme
獲取系統的主題作為元件的主題:
<template>
<n-config-provider :theme="theme">
<n-card> 當前作業系統的主題是 {{ osTheme }}。 </n-card>
</n-config-provider>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { useOsTheme, darkTheme } from 'naive-ui'
export default defineComponent({
setup () {
const osThemeRef = useOsTheme()
return {
theme: computed(() => (osThemeRef.value === 'dark' ? darkTheme : null)),
osTheme: osThemeRef
}
}
})
</script>
讓我們來看一下它的實現,好吧,我水平實在是有限,真的看不懂,但是看起來真的很厲害。