非常規 - VUE 實現特定場景的主題切換

閱文前端團隊發表於2019-08-15

本文作者:劉文濤

原創宣告:本文為閱文前端團隊 YFE 成員出品,請尊重原創,轉載請聯絡公眾號 (id: yuewen_YFE) 獲取授權,並註明作者、出處和連結。

實現頁面皮膚切換,常見的方案有幾種:替換 css 連結、替換 className、改變 css 原生變數值、使用 less.modifyVars、props 引數下發等; 不同的業務場景,我們一般會選擇不同的方法來實現目標。最近在公司運營活動平臺上的主題功能的實現 ,我們嘗試了一種新的解決方案,實現了頁面主題的切換,目標是為了提高專案的可維護性、可擴充套件性,以及降低接入複雜度。

“主題”需求

在瞭解主題功能之前,我們先來解下業務場景:在運營活動後臺中,編輯活動配置頁面,拖拽選擇所需對應元件,並設定元件相應配置項,點選儲存,既完成活動頁面釋出活動,前臺就能訪問對應生成活動。 編輯頁面如下圖:

非常規 - VUE 實現特定場景的主題切換

在如上前提,我們的需求就是:在運營後臺配置頁面中,實現全域性切換主題功能,具體需求如下:

  1. 在配置頁面,初始化頁面時,實現主題一鍵切換所有元件的樣式;
  2. 頁面中的元件的配置,可配置對應元件樣式,覆蓋主題樣式;
  3. 再次點選設定主題,可以覆蓋已經設定樣式的元件樣式;

實現效果如下圖所示:

非常規 - VUE 實現特定場景的主題切換

那在瞭解完需求之後,對於 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
}
複製程式碼

最終實現了最終的主題切換的效果。

該方式帶來的優勢:

  1. 對元件程式碼幾乎無侵入性,元件只需要修改樣式相關引數帶上特殊標示既可,規則相對簡單;
  2. 引數無需一層層下發,易於維護;
  3. 主題與主線功能相對獨立,可以輕易移除主題功能,專案也可以正常執行;

總結

主題的實現,不管是常見的方式,還是上述專案中的主題的實現方式,我們往往需要了解業務特性,去尋找最合適的解決方案。不同專案,有不同的實現方式,但目標都是為了提高專案的可維護性、可擴充套件性,以及降低接入複雜度。

專案目前的實現方案,尚不失為一個好的解決方案,或者可以作為一種新的思路,供大家參考。

更多分享,請關注 YFE:

非常規 - VUE 實現特定場景的主題切換

想要了解關於團隊的更多資訊,可以檢視官網哦 ?

相關文章