探索原因背景
首先自然是項目有需求,這是必須去做的原因
其次,是我們項目沒有直接使用市面上現成的基于element-ui或者ant-design的第三方UI框架,比如avue,而是有著自己的UI組件庫
第三,我們的組件庫基于ant-design-vue,而ant-design-vue并沒有很好的支持主題動態切換(主題總體模式僅支持深色和淺色,其他顏色只支持主色切換,不支持其他顏色和屬性動態定制)
第四,我們還在使用Vue2,這也許是最痛苦的,因為vue3的UI庫比如ant-design-vue,直接支持動態主題
期望目標和效果
1、用戶可以自定義主題色
2、用戶可以切換內置好的主題
3、用戶可以同時自定義配置主題其他顏色細節(包括除主題以外的其他顏色,比如警告色,錯誤顏色等)
4、用戶可以同時自定義非顏色的細節,比如輸入框圓角,placeholder顏色,組件尺寸等
5、不同的組件可以單獨定制
6、同時支持ant-design-vue和自研組件庫的主題定制
方案和思路探索
在開始之前,我們先研究下現有的方案,參照下別人的思路
可用方案參考
1、樣式覆蓋
通過切換 css 選擇器的方式實現主題樣式的切換
2、多套css切換
即寫多套css,覆蓋原有的,并且根據條件切換css
比如 bladex 的主題皮膚切換就是這樣做的,不同的皮膚主題,對應不同的css
這里iview主題,對應view.scss,d2主題,對應d2.scss
3、css變量實現
- 變量聲明
:root { --primary-color: #878ef6; --warning-color: #98efd7; } div{ --primary-color: #878ef6; --warning-color: #98efd7; }
其中 :root表示全局變量,而使用其他的比如在div里面聲明的,只對div標簽有效
- 變量的使用
使用時用var()函數獲取
.button-primary {
color: var(--primary-color);
}
.button-warning {
color: var(--warning-color, "#e98321");
}
其中,var函數的第二個參數表示默認值
- 使用javascript操作
// 設置變量
document.body.style.setProperty('--primary-color', '#878ef6');
// 讀取變量
document.body.style.getPropertyValue('--primary-color').trim();
// '#7F583F'
// 刪除變量
document.body.style.removeProperty('--primary-color');
- 兼容性處理
css變量在低端瀏覽器不被支持,需要使用 css-vars-ponyfill 做兼容處理
方案優缺點
以上方案和方案優缺點參考文章:前端 “一鍵換膚“ 的幾種方案
總體來說,使用變量覆蓋和多套css切換,都不能很自由的滿足主題樣式定制,css變量可以動態的進行樣式設置,是最優方案
a、要切換的目標主題得是我們首先定制好的,比如黑色,白色主題。
b、用戶無法自己設置自己的主題,比如自己制定一個藍色主題
c、css變量能滿足這些需求,理論上我們可以讓用戶自己設置他喜歡的任意部位的顏色樣式
我們面臨的其他問題
如果我們的UI組件庫完全是自己基于vue原生開發的,那選用css變量方案,無疑是最佳方案。
然而,我們的組件庫是基于ant-design-vue 1.x 封裝的,由于一些原因目前還不能使用vue3。這里強調1.x,是因為基于vue3 的 ant-design-vue 3.x已經提供了css變量來實現動態主題定制
ant-design-vue 1.x 的主題變量使用less的變量,
如上圖,在ant-design-vue 的 style/theme里面定義了default.less,如果我們只是想靜態的改變主題,那重寫或者覆蓋這些變量即可
那能不能動態的改變less的變量顏色,達到類似css變量的效果呢?有,我們來看看別人的參考方案
-
在線編譯less變量方案
該方案參考文章:動態改變主題顏色,less變量動態改變
該方案的實現,參考上述文章,經實踐確實可以達到動態改變Less變量的目的,然而對我們確無法適用。該方案缺點:
a、由于是動態編譯less,因此我們需要類似如下的結構。在public中新建一個color.less文件,在文件中定義好變量,并且將對應的樣式寫在該less文件中
image.png
這是無法接受的
第一是項目樣式考慮兼容性等原因,需經編譯器處理后,統一輸出成css
第二是如果一個龐大的項目使用了這種在線編譯Less的結構,在性能上估計會有不少耗損(暫時沒有去實踐和驗證大項目的在線編譯)
第三我們使用的有自己的組件庫以及組件庫的基礎UI庫 ant-design-vue,組件庫使用npm管理,且有基礎組件庫和業務組件庫之分,這種結構不利于組件庫的使用,也不利于樣式的管理(動態編譯時需要去查找color.less的路徑)
b、無法覆蓋ant-design-vue的主題樣式。原因是less作為css預編譯語言使用,在ant-design-vue里面只存在于開發模式下,生產環境打包后的只有css,類似@primary-color這樣的變量也不會存在,已經被轉換為 #D04A02這樣的具體顏色
-
使用webpack-theme-color-replacer webpack插件動態修改顏色
首先ant-design-vue-pro UI框架就是使用了這個插件來動態修改顏色,至于其他參考文章,可能你看到的大部分都是關于element-ui的實現,因為這個插件里面直接包含了一個forElementUI工具函數庫
webpack-theme-color-replacer webpack 基本思路就是,webpack構建時,在emit事件(準備寫入dist結果文件時)中,將即將生成的所有css文件的內容中 帶有指定顏色的css規則單獨提取出來,再合并為一個theme-colors.css輸出文件。然后在切換主題色時,下載這個文件,并替換為需要的顏色,應用到頁面上
所以我們先直接使用ant-design-vue-pro的修改主題代碼來測試一下
首先我們在項目根目錄建文件夾config,在文件夾下建文件plugin.config.js,用來注冊插件
plugin.config.js
const ThemeColorReplacer = require("webpack-theme-color-replacer")
const generate = require("@ant-design/colors/lib/generate").default
const getAntdSerials = (color) => {
// 淡化(即less的tint)
const lightens = new Array(9).fill().map((t, i) => {
return ThemeColorReplacer.varyColor.lighten(color, i / 10)
})
console.log("lightens", lightens)
const colorPalettes = generate(color)
console.log("colorPalettes", colorPalettes)
const rgb = ThemeColorReplacer.varyColor.toNum3(color.replace("#", "")).join(",")
// console.log("rgb", rgb)
const matchColors = lightens.concat(colorPalettes).concat(rgb)
// console.log("matchColors", matchColors)
return matchColors
}
const themePluginOption = {
matchColors: [
...getAntdSerials("#1890ff"), // 主色系列 1890ff
], // 主色系列 1890ff
}
const createThemeColorReplacerPlugin = () => new ThemeColorReplacer(themePluginOption)
module.exports = createThemeColorReplacerPlugin
這里特別要注意的是getAntdSerials("#1890ff"),getAntdSerials 函數傳遞的顏色參數,一定要是ant-design-vue的色系顏色,否則webpack-theme-color-replacer webpack 無法提取出正確的css字符。這里傳遞的#1890ff 正是ant-design-vue的默認主題藍色
然后在vue.config.js中注冊插件
vue.config.js
const createThemeColorReplacerPlugin = require("./config/plugin.config")
module.exports = {
configureWebpack: config => {
config.plugins.push(createThemeColorReplacerPlugin())
},
css: {
loaderOptions: {
less: {
javascriptEnabled: true
}
}
},
}
新建測試頁面theme-example,寫幾行測試效果的代碼
<template>
<div>
<a-button type="primary">主色</a-button>
<a-button type="danger">警告色</a-button>
<a-button @click="changeTheme">點擊切換</a-button>
</div>
</template>
<script lang="ts">
import { Component, Vue, Ref } from "vue-property-decorator"
import { updateTheme } from "@ant-design-vue/pro-layout"
@Component()
export default class ThemeExample extends Vue {
isDefault= true // 是否默認
/**
* 改變主題色
*/
changeTheme() {
this.isDefault= !this.isDefault
updateTheme(this.isDefault? "#1890ff" : "#cf56d7")
}
}
</script>
然后隨便找個路由頁面引入上述組件或者直接把他做成路由組件測試效果吧
[video(video-z7hFSmxg-1673186996605)(type-csdn)(url-https://live.csdn.net/v/embed/268750)(image-https://video-community.csdnimg.cn/vod-84deb4/0968a0708f5471edbfdf0764a0ec0102/snapshots/58c240f0130f4e5aa42669f280d26105-00001.jpg?auth_key=4826782667-0-0-a0b017aeb824a925545af9b494d65564)(title-20230108_205657)]
確實可以成功切換,但是只能切換主題色,如果我們想切換其他顏色,比如危險顏色,怎么做呢?
可以將 getAntdSerials 函數的顏色參數換成 ant-design-vue的危險色 #F5222D,即 getAntdSerials("#F5222D"),然后我們重啟下項目,記得要重啟,因為vue.config.js要重新注冊插件,重啟后看效果
[video(video-01qtjoiC-1673186985298)(type-csdn)(url-https://live.csdn.net/v/embed/268751)(image-https://video-community.csdnimg.cn/vod-84deb4/42e4be408f5671edbfe26723b78e0102/snapshots/1f6a1bdd772b4982beaff5237fd4f741-00001.jpg?auth_key=4826783623-0-0-2c7d84dac6ca370e08da569abbf25e47)(title-20230108_210855)]
可以看到這次切換的顏色是危險色,即a-button type="danger" 時的顏色,而默認的主題色type="primary"并沒有被切換
那該方案是否能滿足我們的需求和期望呢?
答案是否定的,對比下文章開頭,我們的需求和期望,只有第一點可以滿足,就是自定義主題色,危險色雖然也能定制,但是我們卻無法同時分別定制主題色和危險色。
那我們不妨來研究下 webpack-theme-color-replacer webpack 的實現原理,看看是否可以改進后達到我們的目標和需求。內容比較長,我們在下一篇文章 《前端組件庫自定義主題切換探索-02--webpack-theme-color-replacer webpack 的實現邏輯和原理》 來研究下