接到“網(wǎng)站動(dòng)態(tài)換主題”的需求,我是如何踩坑的
需求背景
隨著業(yè)務(wù)的發(fā)展,客戶的需求也會(huì)變得更加多樣化,產(chǎn)品后期就需要有自定義界面的能力,于是出現(xiàn)了“動(dòng)態(tài)換主題”的需求。
設(shè)計(jì)部門的同事讓我們可以參考Ant Design色板生成算法演進(jìn)之路
后面我們動(dòng)態(tài)計(jì)算色板也是采用了目前 Ant Design 的算法, @ant-design/colors
但是切換主題的方式,經(jīng)驗(yàn)證并不能很完美的適用于我們微前端項(xiàng)目。
設(shè)計(jì)標(biāo)準(zhǔn)



以上色系變量表是我們本次最終需要的全部變量
其中每種色系分為兩種,h開頭的和a開頭的,a開頭的通過調(diào)整透明度來生成,h 開頭的一組由 base 色通過ant-design 的動(dòng)態(tài)計(jì)算生成
本色系設(shè)計(jì)由合思設(shè)計(jì)團(tuán)隊(duì) 出品,中性色為直接定義死的,不做計(jì)算;
可配置的基礎(chǔ)色分為
品牌色(brand-base):#22B2CC
警告色(warning-base):#FAAD14
危險(xiǎn)色(danger-base):#F5222D
提示色(info-base):#1890FF
成功色(success-base):#52C41A
前端方案
我在接到需求后,經(jīng)過和公司架構(gòu)師及其他同事的探討后,漸漸產(chǎn)出了以下幾種方案,一步步踩坑過來。
方案一:
兩種主題模式(light/dark),需要分別兩個(gè) less 文件來定義這兩套顏色變量
Light-colors.less

dark-colors.less

兩種模式下,值固定不變的顏色變量單獨(dú)定義一個(gè)文件 common-colors.less ,然后我選擇將三個(gè)文件引入到同一個(gè)index 中輸出使用,需要使用的地方只需要引入index.less 即可。

但是問題來了
1、如何在index.less 中來判斷使用light-colors 還是 dark-colors 呢?
@import 只能定義在文件頂部,也沒有任何可以做條件引入的方法
2、如何根據(jù)品牌色動(dòng)態(tài)計(jì)算色系變量值呢?
計(jì)算為色系變量值是通過js產(chǎn)出一個(gè)數(shù)組,想要導(dǎo)入到一個(gè)less文件中,再引入使用,想要?jiǎng)討B(tài)切換的話,需要用到 less的modifyVars方法, 也是Ant Design 官方提供的方式,接著我們嘗試
方案二:
less 的modifyVars方法是是基于 less 在瀏覽器中的編譯來實(shí)現(xiàn)。所以在引入less文件的時(shí)候需要通過link方式引入,然后基于less.js中的方法來進(jìn)行修改變量
less.modifyVars({
'@themeColor': '#22B2CC'
});<link rel="stylesheet/less" type="text/css" href="./src/less/theme-colors.less" />// color 傳入顏色值
changeTheme (color) {
less.modifyVars({ // 調(diào)用 `less.modifyVars` 方法來改變變量值'
@themeColor':color
})
.then(() => {
console.log('修改成功');
});
};需要引入
less編譯器,太大了,嚴(yán)重影響性能;需要
webpack配置,無法多個(gè)進(jìn)程間共享變量,不適用于微前端項(xiàng)目。這種方法僅限于用
less的項(xiàng)目才能使用,如果你項(xiàng)目使用的是sass,是沒有類似less.modifyVars這種解決方案的。
方案三:
1、在webpack構(gòu)建時(shí),通過 webpack-theme-color-replacer這個(gè)插件從所有輸出的css文件中提取主題顏色樣式,并創(chuàng)建一個(gè)僅包含顏色樣式的'theme-colors.css'文件。在網(wǎng)頁的運(yùn)行時(shí),客戶端部分下載此css文件,然后將顏色動(dòng)態(tài)替換為新的自定義顏色,能夠滿足更靈活豐富的功能場景,性能出色。
2、@ant-design/colors 來動(dòng)態(tài)計(jì)算出品牌色系和功能色系。
3、可以動(dòng)態(tài)的切換品牌色來獲取整個(gè)主題的切換。

色系通過 提供的基準(zhǔn)色, 自動(dòng)計(jì)算及輸出的顏色集合:

通過計(jì)算就可以輸出整個(gè)色系數(shù)組如下:

需要設(shè)置顏色的地方就可以直接使用定義的這些變量,需要切換主題或者顏色的時(shí)候,傳入主題模式、品牌色重新計(jì)算,就可以實(shí)現(xiàn)動(dòng)態(tài)切換主題了。
看似沒啥問題,但是在我們的系統(tǒng)里,問題來了。
因?yàn)槲覀兪俏⑶岸隧?xiàng)目,拆包出大概二三十個(gè)項(xiàng)目,創(chuàng)建一個(gè)僅包含顏色樣式的theme-colors.css文件這一步是運(yùn)行在編譯時(shí)的,那么每個(gè)子項(xiàng)目如果沒有配置這個(gè)webpack,就無法共享該變量,在開發(fā)編譯階段就會(huì)報(bào)錯(cuò)!即使每個(gè)項(xiàng)目都配置了這樣的webpack構(gòu)建,也會(huì)創(chuàng)建各自的 theme-colors.css 文件,更改主題時(shí)候也無法同步切換,一樣的坑爹?。?!
由此可見,即使一個(gè)方案很好很成熟,也不是滿足所有項(xiàng)目的。落實(shí)一個(gè)方案的時(shí)候,要根據(jù)自己的項(xiàng)目情況做分析,做出一個(gè)符合自身項(xiàng)目的解決方案才是硬道理,而不是一味的生搬硬套。
于是該方案斃掉,繼續(xù)思考下一個(gè)方案。
方案四:
時(shí)代好了,瀏覽器普遍支持Css3變量了,基于Css3 Variable 共享全局主題變量看起來就是一個(gè)很通用的方案了。
首先定義一個(gè)全局變量,改變這個(gè)變量的值,頁面中所有引用這個(gè)變量的元素都會(huì)進(jìn)行改變,既沒有 less 的編譯過程,也不存在什么性能問題,這不就是我們最期望的動(dòng)態(tài)換膚方案嗎?
Css3 Variable的用法就是給變量加--前綴,涉及到主題色的都改成var(--themeColor)這種方式
我們先查一下兼容性

主流瀏覽器基本全部兼容,對于大多數(shù)互聯(lián)網(wǎng)企業(yè)產(chǎn)品完全夠用了,但是對于某些還在使用IE 瀏覽器的產(chǎn)品就需要ponyfill 方案兼容了。
也確實(shí)有這樣一個(gè) polyfill 能兼容IE: css-vars-ponyfill
這個(gè)polyfill 只會(huì)在不支持Css3 Variable 的環(huán)境會(huì)生效
我們開始寫代碼了:
1、建一個(gè)存放公共css變量的js文件(variable.js),將需要定義的css變量存放到該js文件,品牌色及功能色等通過antd算法計(jì)算獲得;
import { getAlphaColor } from "./themeUtils";
const { generate } = require("@ant-design/colors");
import baseTheme from "./baseTheme";
import lightTheme from "./lightTheme";
import darkTheme from "./darkTheme";
import { functionalColorsBase, grayBase } from "./colors";
const themeModes = {
light: undefined,
dark: {
theme: "dark",
backgroundColor: grayBase,
},
};
// 獲取品牌色系
export const getBrandColors = (color, mode) => {
let options = themeModes[mode];
return generate(color, options);
};
// 獲取功能色系
export const getFunctionalColors = (mode) => {
let options = themeModes[mode];
let { success, warning, danger, info } = functionalColorsBase;
const successColors = generate(success, options);
const warningColors = generate(warning, options);
const dangerColors = generate(danger, options);
const infoColors = generate(info, options);
return {
success: successColors,
warning: warningColors,
danger: dangerColors,
info: infoColors,
};
};
// 輸出色板
export const modifyVars = (color, mode) => {
const brandColors = getBrandColors(color, mode);
const { success, warning, danger, info } = getFunctionalColors(mode);
const colors = {
...baseTheme,
"--brand-base": brandColors[5],
"--success-base": success[5],
"--warning-base": warning[5],
"--danger-base": danger[5],
"--info-base": info[5],
"--h-brand-1": brandColors[0],
"--h-brand-2": brandColors[1],
"--h-brand-3": brandColors[2],
"--h-brand-4": brandColors[3],
"--h-brand-5": brandColors[4],
"--h-brand-6": brandColors[5],
"--h-brand-7": brandColors[6],
"--h-brand-8": brandColors[7],
"--h-brand-9": brandColors[8],
"--h-brand-10": brandColors[9],
"--h-success-1": success[0],
"--h-success-2": success[1],
"--h-success-3": success[2],
"--h-success-4": success[3],
"--h-success-5": success[4],
"--h-success-6": success[5],
"--h-success-7": success[6],
"--h-success-8": success[7],
"--h-success-9": success[8],
"--h-success-10": success[9],
"--h-warning-1": warning[0],
"--h-warning-2": warning[1],
"--h-warning-3": warning[2],
"--h-warning-4": warning[3],
"--h-warning-5": warning[4],
"--h-warning-6": warning[5],
"--h-warning-7": warning[6],
"--h-warning-8": warning[7],
"--h-warning-9": warning[8],
"--h-warning-10": warning[9],
"--h-danger-1": danger[0],
"--h-danger-2": danger[1],
"--h-danger-3": danger[2],
"--h-danger-4": danger[3],
"--h-danger-5": danger[4],
"--h-danger-6": danger[5],
"--h-danger-7": danger[6],
"--h-danger-8": danger[7],
"--h-danger-9": danger[8],
"--h-danger-10": danger[9],
"--h-info-1": info[0],
"--h-info-2": info[1],
"--h-info-3": info[2],
"--h-info-4": info[3],
"--h-info-5": info[4],
"--h-info-6": info[5],
"--h-info-7": info[6],
"--h-info-8": info[7],
"--h-info-9": info[8],
"--h-info-10": info[9],
};
const darkConfigableTheme = {
"--a-brand-1": getAlphaColor(brandColors[5], 0.04),
"--a-brand-2": getAlphaColor(brandColors[5], 0.08),
"--a-brand-3": getAlphaColor(brandColors[5], 0.16),
"--a-brand-4": getAlphaColor(brandColors[5], 0.24),
"--a-brand-5": getAlphaColor(brandColors[5], 0.32),
"--a-brand-6": getAlphaColor(brandColors[5], 0.4),
"--a-brand-7": getAlphaColor(brandColors[5], 0.52),
"--a-brand-8": getAlphaColor(brandColors[5], 0.64),
"--a-brand-9": getAlphaColor(brandColors[5], 0.76),
"--a-brand-10": getAlphaColor(brandColors[5], 0.88),
"--a-success-1": getAlphaColor(success[5], 0.04),
"--a-success-2": getAlphaColor(success[5], 0.08),
"--a-success-3": getAlphaColor(success[5], 0.16),
"--a-success-4": getAlphaColor(success[5], 0.24),
"--a-success-5": getAlphaColor(success[5], 0.32),
"--a-success-6": getAlphaColor(success[5], 0.4),
"--a-success-7": getAlphaColor(success[5], 0.52),
"--a-success-8": getAlphaColor(success[5], 0.64),
"--a-success-9": getAlphaColor(success[5], 0.76),
"--a-success-10": getAlphaColor(success[5], 0.88),
"--a-warning-1": getAlphaColor(warning[5], 0.04),
"--a-warning-2": getAlphaColor(warning[5], 0.08),
"--a-warning-3": getAlphaColor(warning[5], 0.16),
"--a-warning-4": getAlphaColor(warning[5], 0.24),
"--a-warning-5": getAlphaColor(warning[5], 0.32),
"--a-warning-6": getAlphaColor(warning[5], 0.4),
"--a-warning-7": getAlphaColor(warning[5], 0.52),
"--a-warning-8": getAlphaColor(warning[5], 0.64),
"--a-warning-9": getAlphaColor(warning[5], 0.76),
"--a-warning-10": getAlphaColor(warning[5], 0.88),
"--a-danger-1": getAlphaColor(danger[5], 0.04),
"--a-danger-2": getAlphaColor(danger[5], 0.08),
"--a-danger-3": getAlphaColor(danger[5], 0.16),
"--a-danger-4": getAlphaColor(danger[5], 0.24),
"--a-danger-5": getAlphaColor(danger[5], 0.32),
"--a-danger-6": getAlphaColor(danger[5], 0.4),
"--a-danger-7": getAlphaColor(danger[5], 0.52),
"--a-danger-8": getAlphaColor(danger[5], 0.64),
"--a-danger-9": getAlphaColor(danger[5], 0.76),
"--a-danger-10": getAlphaColor(danger[5], 0.88),
"--a-info-1": getAlphaColor(info[5], 0.04),
"--a-info-2": getAlphaColor(info[5], 0.08),
"--a-info-3": getAlphaColor(info[5], 0.16),
"--a-info-4": getAlphaColor(info[5], 0.24),
"--a-info-5": getAlphaColor(info[5], 0.32),
"--a-info-6": getAlphaColor(info[5], 0.4),
"--a-info-7": getAlphaColor(info[5], 0.52),
"--a-info-8": getAlphaColor(info[5], 0.64),
"--a-info-9": getAlphaColor(info[5], 0.76),
"--a-info-10": getAlphaColor(info[5], 0.88),
};
const lightModeColors = { ...lightTheme, ...colors };
const darkModeColors = { ...darkTheme, ...darkConfigableTheme, ...colors };
console.log(lightModeColors, "=====", darkModeColors);
return mode == "light" ? lightModeColors : darkModeColors;
};
2、頁面使用css變量,無論是web主項(xiàng)目,還是各個(gè)plugin子項(xiàng)目都可以共享變量,不需要引入任何依賴,設(shè)計(jì)圖標(biāo)注與代碼對應(yīng)關(guān)系:
| UI | CODE |
| h-brand-1 | var(--h-brand-1) |
3、封裝切換主題的js,在項(xiàng)目入口做初始化調(diào)用,支持更改light和dark模式,及變更品牌色基準(zhǔn)色
import { brandBase, modifyVars } from "./variable";
import cssVars from "css-vars-ponyfill";
const key = "data-theme";
// 獲取當(dāng)前主題
export const getTheme = (mode, color) => {
const localTheme = localStorage.getItem(key);
const dataTheme = localTheme
? JSON.parse(localTheme)
: {
color: color || brandBase,
mode: mode || "light",
};
return dataTheme;
};
// 初始化主題
export const initTheme = (mode, color) => {
const dataTheme = getTheme(mode, color);
document.documentElement.setAttribute("data-theme", dataTheme.mode);
cssVars({
watch: true,
// 當(dāng)添加,刪除或修改其<link>或<style>元素的禁用或href屬性時(shí),ponyfill將自行調(diào)用
variables: modifyVars(dataTheme.color, dataTheme.mode), // variables 自定義屬性名/值對的集合
onlyLegacy: false, // false 默認(rèn)將css變量編譯為瀏覽器識(shí)別的css樣式 true 當(dāng)瀏覽器不支持css變量的時(shí)候?qū)ss變量編譯為識(shí)別的css
});
};
// 變更主題
export const changeTheme = (mode, color) => {
const dataTheme = {
color: color || brandBase,
mode: mode || "light",
};
localStorage.setItem(key, JSON.stringify(dataTheme));
document.documentElement.setAttribute("data-theme", dataTheme.mode);
cssVars({
watch: true,
variables: modifyVars(dataTheme.color, dataTheme.mode),
onlyLegacy: false,
});
};4、在切換主題的按鈕組件中調(diào)用 changeTheme切換主題
最終效果,目前只有部分掃雷了部分頁面,控制開關(guān)為臨時(shí)征用側(cè)邊欄:

總結(jié)
至此,一個(gè)微前端項(xiàng)目的動(dòng)態(tài)換膚方案已經(jīng)實(shí)現(xiàn),大家如果有更好的方案,歡迎補(bǔ)充哦~
注:該方案出自合思大前端團(tuán)隊(duì) ,北京和南昌均有技術(shù)團(tuán)隊(duì),如果你有考慮新的工作機(jī)會(huì),歡迎投簡歷!
1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的「點(diǎn)贊,在看」是我創(chuàng)作的動(dòng)力。
2.關(guān)注公眾號
程序員成長指北,回復(fù)「1」加入高級前端交流群!「在這里有好多 前端 開發(fā)者,會(huì)討論 前端 Node 知識(shí),互相學(xué)習(xí)」!3.也可添加微信【ikoala520】,一起成長。
“在看轉(zhuǎn)發(fā)”是最大的支持
