本文作者:劉文濤
原創宣告:本文為閱文前端團隊 YFE 成員出品,請尊重原創,轉載請聯絡公眾號 (id: yuewen_YFE) 獲取授權,並註明作者、出處和連結。
實現頁面皮膚切換,常見的方案有幾種:替換 css 連結、替換 className、改變 css 原生變數值、使用 less.modifyVars、props 引數下發等; 不同的業務場景,我們一般會選擇不同的方法來實現目標。最近在公司運營活動平臺上的主題功能的實現 ,我們嘗試了一種新的解決方案,實現了頁面主題的切換,目標是為了提高專案的可維護性、可擴充套件性,以及降低接入複雜度。
“主題”需求
在瞭解主題功能之前,我們先來解下業務場景:在運營活動後臺中,編輯活動配置頁面,拖拽選擇所需對應元件,並設定元件相應配置項,點選儲存,既完成活動頁面釋出活動,前臺就能訪問對應生成活動。 編輯頁面如下圖:
在如上前提,我們的需求就是:在運營後臺配置頁面中,實現全域性切換主題功能,具體需求如下:
- 在配置頁面,初始化頁面時,實現主題一鍵切換所有元件的樣式;
- 頁面中的元件的配置,可配置對應元件樣式,覆蓋主題樣式;
- 再次點選設定主題,可以覆蓋已經設定樣式的元件樣式;
實現效果如下圖所示:
那在瞭解完需求之後,對於 VUE 專案,要實現主題功能,一般想到的實現方式就是 theme 引數通過 prop 下發來實現。 那我們就先來聊下常規實現方式:prop 下發實現方式。
常規實現方式
定義主題
首先我定義 theme.js 為主題相關引數,如下:
const DEFAULT_THEME = {
primary: '#2F54EB',
subPrimary: '#D6E4FF',
error: '#F5222D',
success: '#52C41A',
warning: '#FAAD14',
background: '#FFFFFF',
text: '#222222'
}
export default {
DEFAULT: DEFAULT_THEME,
FIRST: {
...DEFAULT_THEME,
background: '#2590ff'
}
}
複製程式碼
主模組下發 theme 給予元件
接著需要在主模組中,下發 theme 引數,和元件相關配置引數 給到元件,點選按鈕,切換主題:
<template>
<div>
<div @click="changeTheme">換主題</div>
<Component
v-for="(item, index) in componentList"
:theme="theme"
:key="index"
...item.config // 業務相關引數都在config中
/>
</div>
</template>
<script>
import theme from 'theme.js'
export default {
name: "themeChange",
data() {
return {
theme: theme['DEFAULT']
}
},
methods: {
changeTheme() {
this.theme = theme['FIRST']
}
}
}
</script>
複製程式碼
元件監聽 theme 改變元件樣式
元件中,獲取上級元件傳遞下來的配置引數及主題引數,並監聽 theme 的變化,當發生改變,重置樣式引數值為主題樣式:
<template>
<div>
<div :style="{ background: config.bgColor }">主題</div>
</div>
</template>
<script>
import theme from 'theme.js'
const initTheme = theme['DEFAULT']
export default {
name: "themeSwitch",
props: {
theme: {
type: Object,
default: () => ({})
},
bgColor: {
type: String,
default: initTheme.background
},
...
},
data() {
return {
config: {
bgColor: this.bgColor,
...
}
}
},
watch: {
'theme' (to, from) {
this.config.bgColor = this.theme.bgColor
}
}
}
</script>
複製程式碼
看到這裡大家會說,為什麼需要在 watch 中監聽主題的變化,而不是在元件初始化的時候引數就直接指向主題對應的引數呢?
因為主題需求裡面所說的,在元件裡面也是可以改變元件相關樣式的,上述 demo 程式碼中的 bgColor 引數,既可以通過點選切換主題可以設定 ,也可以是元件自己設定的,有多個來源(這裡不對元件的配置實現做詳細展開);要做到設定主題的時候,元件的樣式會設定相應的主題色,就需要在 watch 中進行監聽 theme 引數的變化,發生變化,重置相應引數,但是這種方式在每個元件都需要有相同程式碼片段,監聽引數,達到我們的效果,程式碼非常冗餘。
綜上,我們對程式碼進一步優化,把監聽 theme 引數的方法統一封裝,這裡會有另一個問題:每個元件對應顏色的引數是不可定的,且引數層級也是不可定的,幾乎每個元件需要維護一整個變數陣列。這樣定義的規則會相對複雜,維護成本過高,且極易弄錯。
很顯然這樣的實現方式並不是一種很好的方法,那要如何實現?
“非常規”實現方式
在嘗試上面的方式之後,我在想我的思路是否正確,是不是切入角度有點問題,那我們換一個角度去切入。
配置引數入手
當我們發現頁面整個 this.componentList 引數 ( 裡面儲存了所有元件的相關配置 ) 我是可以拿到的時候,我們是不是可以從資料入手? ok,說到這裡,那其實思路就出現了, this.componentList 裡面的引數規則:
[
{
componentName: 'xx',
config: {
color: 'xxx',
background: 'xxx',
...
}
},
...
]
複製程式碼
我們會發現,在開發元件的時候就已經是把顏色相關引數提取到配置裡面了,那也就是說我修改配置引數的值,其實就可以達到設定主題的效果? 因為所有元件的配置引數都是由this.componentList 引數下發的。
引數給予特殊標識
定義 theme.js 相關引數,和上面一致,故不在多說,主要做的就是,在元件中,我們把相關引數進行修改,改為有特殊標示的引數, 如下:
<template>
<div>
<div :style="{ background: this['bgColor.t.background']}">主題</div>
</div>
</template>
<script>
import theme from 'theme.js'
const initTheme = theme['DEFAULT']
export default {
name: "themeSwitch",
props: {
'bgColor.t.background': { // .t.: 為特殊標識 ;background: 為主題裡面對應的欄位名 background: '#FFFFFF'
type: String,
default: initTheme.background
}
}
}
</script>
複製程式碼
遍歷引數替換特殊標識引數值
當點選主題切換的時候,會去遍歷 this.componentList 引數,修改有特殊標示的引數為新主題對應的引數,程式碼如下:
/*
* 根據主題重製componentsConfig
* @method changeTheme
* */
changeTheme () {
this.theme = theme['FIRST']
this.componentList.forEach(component => {
this._setThemeChangeConfig(component.config || {})
})
},
_setThemeChangeConfig (obj) {
Object.keys(obj).map(name => {
if (Object.prototype.toString.call(obj[name]) === '[object Object]') {
this._setThemeChangeConfig(obj[name])
} else {
const themeColorArr = name.match(/\.t\.(\S*)/)
if (themeColorArr && this.isThemeColorName(themeColorArr[1])) {
this.$set(obj, name, this.theme[themeColorArr[1]])
}
}
})
},
/*
* 判斷顏色name是否在主題裡面
* @method isThemeColorName
* */
isThemeColorName (name) {
let has = false
Object.keys(this.theme).forEach((paramsName) => {
if (paramsName === name) has = true
})
return has
}
複製程式碼
最終實現了最終的主題切換的效果。
該方式帶來的優勢:
- 對元件程式碼幾乎無侵入性,元件只需要修改樣式相關引數帶上特殊標示既可,規則相對簡單;
- 引數無需一層層下發,易於維護;
- 主題與主線功能相對獨立,可以輕易移除主題功能,專案也可以正常執行;
總結
主題的實現,不管是常見的方式,還是上述專案中的主題的實現方式,我們往往需要了解業務特性,去尋找最合適的解決方案。不同專案,有不同的實現方式,但目標都是為了提高專案的可維護性、可擴充套件性,以及降低接入複雜度。
專案目前的實現方案,尚不失為一個好的解決方案,或者可以作為一種新的思路,供大家參考。