前端 “一鍵換膚“ 的 N 種方案
點擊上方?前端Q,關(guān)注公眾號
回復(fù)加群,加入前端Q技術(shù)交流群
前言
現(xiàn)在越來越多的網(wǎng)站都提供了擁有換膚(切換主題)功能,如 ElementUI[2],既是為了迎合用戶需求,或是為了凸顯自己特點,因此提供了個性化定制的功能.
其實之前就想了解和實現(xiàn) “一鍵換膚” 功能,但是由于種種原因一直拖到了現(xiàn)在.

CSS 樣式覆蓋實現(xiàn)
核心
通過切換 css 選擇器的方式實現(xiàn)主題樣式的切換.
在組件中保留不變的樣式,將需要變化的樣式進行抽離 提供多種樣式,給不同的主題定義一個對應(yīng)的 CSS 選擇器 根據(jù)不同主題設(shè)置不同的樣式
實現(xiàn)
下面通過 vuex 存儲和控制全局的主題色,其代碼如下:
import?{?createStore?}?from?'vuex'
//?創(chuàng)建一個新的?store?實例
const?store?=?createStore({
??state?()?{
????return?{
??????theme:?'light'
????}
??},
??mutations:?{
????setTheme?(state,?payload)?{
??????state.theme?=?payload
??????document.querySelector('body').className?=?payload
????}
??}
})
export?default?store
復(fù)制代碼
在 template 模板中通過 vuex 中的主題設(shè)置對應(yīng)類名,如頭部代碼如下:
<template>
??<div?:class="['header',?store.state.theme]">
????<span>{{title}}span>
????<input?v-model="checked"?type="checkbox"?class="switch"?@change="changeTheme"?/>
??div>
template>
復(fù)制代碼
下面 theme.css 中通過 .light 和 .dark 兩個類選擇器來區(qū)分明亮主題和暗黑主題,并且事先準備了它們對應(yīng)的樣式,如下:
/*?light?默認主題*/
body.light?{
??background-color:?#fff;
}
.header.light?{
??background-color:?#fff;
??border-bottom:?1px?solid?#d6d6d6;
??color:?rgb(51,?50,?50);
}
.list.light?.title?{
??color:?rgb(51,?50,?50);
}
.list.light?.describe{
??color:?rgb(158,?158,?158);
}
.list.light?.left{
??border:?1px?solid?rgb(51,?50,?50);
}
/*?dark?暗黑主題?*/
body.dark?{
??background-color:?rgb(51,?50,?50);
}
.header.dark?{
??background-color:?rgb(51,?50,?50);
??border-bottom:?1px?solid?#fff;
??color:?#fff;
}
.list.dark?.title?{
??color:?#fff;
}
.list.dark?.describe{
??color:?rgb(201,?201,?201);
}
.list.dark?.left{
??border:?1px?solid?#fff;
??background-color:?#fff;
}
復(fù)制代碼
缺點
多種主題樣式都要引入,導致代碼量增大 樣式不易管理 查找樣式復(fù)雜 開發(fā)效率低 拓展性差 ...
實現(xiàn)多套 CSS 主題樣式
核心
實現(xiàn)多套 CSS 主題樣式,根據(jù)用戶切換操作,通過 link 標簽動態(tài)加載不同的主題樣式,主要解決了多個主題色被編譯到一個文件中導致單個文件過大.
實現(xiàn)
css 部分直接拆分成 ligth.css 和 dark.css 兩個文件:

設(shè)置主題部分的 setTheme.js 代碼如下:
export?default?function?setTheme(theme?=?'ligth')?{
??let?link?=?document.querySelector('#theme-link')
??let?href?=?"/theme/"?+?theme?+?".css"
??
??if?(!link)?{
????let?head?=?document.querySelector('head')
????link?=?document.createElement('link')
????link.id?=?'#theme-link'
????link.rel?=?"stylesheet"
????link.href?=?href
????head.appendChild(link)
??}?else?{
????link.href?=?href
??}
}
復(fù)制代碼
缺點
需要重復(fù) CV 多份樣式文件進行單獨修改 沒有單獨提取出可變的樣式部分 需要提前知道打包后的文件路徑,否則可能導致主題樣式引入錯誤 ...
CSS 變量實現(xiàn)
核心
通過 body.style.setProperty(key, value) 動態(tài)修改 body 上的 CSS 變量,使得頁面上的其他部分可以應(yīng)用最新的 CSS 變量對應(yīng)的樣式.

實現(xiàn)
theme.css 中負責定義全局的 CSS 變量,代碼如下:
/*?實現(xiàn)方式一?*/
:root?{
??--theme-bg:?initial;?//?背景色
??--theme-color:?initial;?//?字體色
??--theme-boder-color:?initial;?//?邊框色
}
====================================================
/*?實現(xiàn)方式二?*/
/*?默認值:light */
:root?{
??--theme-bg:?#fff;
??--theme-color:?rgb(51,?50,?50);
??--theme-img-bg:?#fff;
??--theme-boder-color:?#d6d6d6;
}
/*?暗黑:dark */
[data-theme='dark']?{
??--theme-bg:?rgb(51,?50,?50);
??--theme-color:?#fff;
??--theme-boder-color:?#fff;
}
復(fù)制代碼
themeUtil.js 中負責獲取當前對應(yīng)樣式值,以及設(shè)置 body 上的 CSS 變量值,如下:
const?darkTheme?=?'rgb(51,?50,?50)'
const?lightTheme?=?'#fff'
const?lightBorderTheme?=?'#d6d6d6'
//?獲取對應(yīng)的主題色值
export?const?getThemeMap?=?(isLight)?=>?{
??return?{
????'theme-bg':?isLight???lightTheme?:?darkTheme,
????'theme-color':?isLight???darkTheme?:?lightTheme,
????'theme-boder-color':?isLight???lightBorderTheme?:?lightTheme,
??}
}
//?設(shè)置主題色值
export?const?setTheme?=?(isLight?=?true)?=>?{
??const?themeMap?=?getThemeMap(isLight)
??const?body?=?document.body
??/*?實現(xiàn)方式一?*/
??Object.keys(themeMap).forEach(key?=>?{
????body.style.setProperty(`--${key}`,?themeMap[key])
??})
??
??/*?實現(xiàn)方式二?*/
??//?body.style.setProperty('data-theme',?isLight???'light'?:?'dark')
}
復(fù)制代碼
通過 var() 在組件中應(yīng)用對應(yīng) CSS 變量,比如在頭部中的使用:
<style?scoped>
.header?{
??...省略
??color:?var(--theme-color);
??border-bottom:?1px?solid?var(--theme-boder-color);
??background-color:?var(--theme-bg);
}
...省略
style>
復(fù)制代碼
缺點
缺點就是兼容性不好

兼容
通過 css-vars-ponyfill 對 CSS 變量進行兼容處理,themeUtil.js 中代碼改變?nèi)缦拢?/p>
import?cssVars?from?"css-vars-ponyfill";
const?darkTheme?=?'rgb(51,?50,?50)'
const?lightTheme?=?'#fff'
const?lightBorderTheme?=?'#d6d6d6'
//?這里定義的?鍵/值?對,是為了給?cssVars?傳參
export?const?getThemeMap?=?(isLight)?=>?{
??return?{
????'--theme-bg':?isLight???lightTheme?:?darkTheme,
????'--theme-img-bg':?lightTheme,
????'--theme-color':?isLight???darkTheme?:?lightTheme,
????'--theme-boder-color':?isLight???lightBorderTheme?:?lightTheme,
??}
}
export?const?setTheme?=?(isLight?=?true)?=>?{
??const?themeMap?=?getThemeMap(isLight)
??const?body?=?document.body
??
??/*?實現(xiàn)方式一?*/
??Object.keys(themeMap).forEach(key?=>?{
????body.style.setProperty(key,?themeMap[key])
??})
??
??/*?實現(xiàn)方式二?*/
??//?body.style.setProperty('data-theme',?isLight???'light'?:?'dark')
??
??//?實現(xiàn)兼容方案
??cssVars({
????watch:?true,?//?添加、刪除、修改??或?
