一.前言
之前项目需要为其增加主题换色功能,接手这个需求后,就开始分析项目的代码内容。我把他们主要分成两个大类,一是elementUI下的组件,二是自定义模板或者组件。这两类都需要对其样式进行切换来达到换色效果。为了做到尽可能的优雅,最好是只改变一处地方,也不用重复写一样的样式,就可以实现换色。我这种菜鸡真是花了不少时间在上面。
二.方案设计
方案1.link标签动态引入或使用不同类名切换不同主题
设计几套不同主题样式的文件,在需要的时候,创建link标签动态加载到head标签中,或者是动态改变link标签的href属性。或者在需要切换主题的时候根标签切换对应的类名来切换不同的样式。
但是这样子的操作的话,如果需要更改某一样式,所有主题都需要更改,麻烦且容易出错,这可以说是最笨的方法。
方案2.使用css变量+类名切换
这是方案1的改进方案。大体思路跟方案1相似,依然是提前将样式文件载入,切换时将指定的根元素类名更换。不过这里相对灵活的是,默认在根作用域下定义好CSS变量,只需要在不同的主题下更改CSS变量对应的取值即可。
这样子操作的好处就是通过CSS变量,可以达到只改一次就能全部更改的效果。
方案3.使用CSS变量+动态setProperty
方案3较于前两种会更加灵活,不过视情况而定,这个方案适用于由用户根据颜色面板自行设定各种颜色主题,这种是主题颜色不确定的情况,而前两种方案更适用于定义预设的几种主题。
三.方案实操
综合考虑之后,决定使用第三种方案,因为第三种方案足够灵活,可以通过js设置几个默认主题,也可以交给用户去取色来设置新主题。这样再以后需求有变的时候修改起来更有优势。
以下内容将以方案3来展开:
在项目内新建theme文件夹,放置如下文件内容:
如上,index.css与fonts内的字体文件,是预设好的elementUI样式模板,根据用户的取色或者预设的颜色来替换文件内对应颜色,再全局放置到html的style标签内,即可完成elementUI的主题颜色替换。而自定义模板或者组件的颜色样式则是由另外的文件来完成。
首先先看看elementUI的主题如何替换:
// 安装css-color-function,后面会用来转换颜色是rgb还是16进制
npm css-color-function // yarn add css-color-function
通过formula.json获取颜色变化规则,再用generateColors函数转化颜色
// formula.json
{
"primary": "color(primary)",
"shade-1": "color(primary shade(10%))",
"shade-2": "color(primary shade(20%))",
"shade-3": "color(primary shade(30%))",
"shade-4": "color(primary shade(40%))",
"shade-5": "color(primary shade(50%))",
"shade-6": "color(primary shade(60%))",
"shade-7": "color(primary shade(70%))",
"shade-8": "color(primary shade(80%))",
"shade-9": "color(primary shade(90%))",
"alpha-1": "color(primary alpha(.1))",
"alpha-2": "color(primary alpha(.2))",
"alpha-3": "color(primary alpha(.3))",
"alpha-4": "color(primary alpha(.4))",
"alpha-5": "color(primary alpha(.5))",
"alpha-6": "color(primary alpha(.6))",
"alpha-7": "color(primary alpha(.7))",
"alpha-8": "color(primary alpha(.8))",
"alpha-9": "color(primary alpha(.9))",
"light-1": "color(primary tint(10%))",
"light-2": "color(primary tint(20%))",
"light-3": "color(primary tint(30%))",
"light-4": "color(primary tint(40%))",
"light-5": "color(primary tint(50%))",
"light-6": "color(primary tint(60%))",
"light-7": "color(primary tint(70%))",
"light-8": "color(primary tint(80%))",
"light-9": "color(primary tint(90%))"
}
// color.js
import color from 'css-color-function'
import formula from './formula.json'
export function generateColors(primary) {
let colors = {}
Object.keys(formula).forEach((key) => {
const value = formula[key].replace(/primary/g, primary)
const c = color.convert(value)
colors[key] = c.indexOf('rgba') > -1 ? c : colorRgbToHex(c)
})
return colors
}
/* 将rgb颜色转成hex */
export function colorRgbToHex(rgb) {
let [r, g, b] = rgb.replace(/(?:\(|\)|rgb|RGB)*/g, '').split(',')
return '#' + ((1 << 24) + (Number(r) << 16) + (Number(g) << 8) + Number(b)).toString(16).slice(1)
}
预设了各个主题的默认颜色供调用。若有多种主题,直接写即可。后续若主题较多,可使用混入功能。
//model.js
let themes = {
default: {
color_block: "#07b6b5",
color_line: "#009aa6"
}
}
export { themes }
elementUI主题色替换函数代码:
//index.js
import { generateColors } from './color'
let style = require('!!css-loader?sourceMap=false!postcss-loader?{"postcssOptions":{"plugins":{"cssnano":{}}}}!./index.css').toString();
import formula from './formula.json'
let originalStyle = ''
export function writeNewStyle(themeColor) {
let colors = generateColors(themeColor)
let cssText = originalStyle
let colorsCssText = ''
Object.keys(colors).forEach((key) => {
cssText = cssText.replace(new RegExp('(:|\\s+)' + key, 'g'), '$1' + colors[key])
colorsCssText += `
.color-${key}{color: ${colors[key]}!important;}
.bg-${key}{background-color: ${colors[key]}!important;}
.border-${key}{border-color: ${colors[key]}!important;}
`
})
console.log("cssText", cssText)
let styleTag = document.getElementById('ide-theme-style')
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', 'ide-theme-style')
document.head.appendChild(styleTag)
}
styleTag.innerText = cssText + colorsCssText
}
export function getIndexStyle(themeColor) {
if (!originalStyle) {
originalStyle = getStyleTemplate(style, themeColor)
}
writeNewStyle(themeColor)
}
export function getStyleTemplate(data, themeColor) {
let colors = generateColors(themeColor)
const colorMap = new Map()
Object.keys(formula).forEach((key) => {
colorMap.set(colors[key], key)
})
for (let [key, value] of colorMap) {
data = data.replace(new RegExp(key, 'ig'), value)
}
return data
}
自定义模板或者组件替换主题色函数代码:
//theme.js
import { themes } from "./model";
import { getIndexStyle } from "./index.js"
const changeStyle = (obj) => {
for (let key in obj) {
document
.getElementsByTagName("body")[0]
.style.setProperty(`--${key}`, obj[key]);
}
};
// 设置主题的颜色
const setThemeColor = (theme) => {
localStorage.setItem("theme", JSON.stringify(theme)); // 保存主题到本地,下次进入使用该主题
changeStyle(theme); // 改变样式
}
const setFontFamily = (FontFamilyName) => {
localStorage.setItem("FontFamily", FontFamilyName);
const themeConfig = {
themeFontFamily: FontFamilyName
};
changeStyle(themeConfig);
}
export const setTheme = (type = 'themeColor') => {
if (type === 'themeColor') {
let currentTheme = localStorage.getItem("theme") && JSON.parse(localStorage.getItem("theme"));
if (!currentTheme) {
currentTheme = themes.default
}
setThemeColor(currentTheme);
getIndexStyle(currentTheme.color_block)
} else if (type === 'FontFamily') {
let FontFamily = localStorage.getItem("FontFamily");
setFontFamily(FontFamily);
}
};
以上为theme文件夹文件介绍。调用的入口函数是theme.js文件里的setTheme函数。
需要在项目工程的main.js文件内引入setTheme函数。
如:
import { setTheme } from "@/theme/theme";
//然后在created生命周期里调用函数
// 初始化主题色
setTheme && setTheme();
注:需要注意的是,index.css文件的调用可以有两种模式,一种是从后台获取,一种是从本地调用。后台获取比较好理解,但是前端获取CSS文件的纯文本内容就要折腾一下了。当时也花了些时间去解决。现在给出一个自己的解决方案以供参考:
使用require方式获取文本再转为string:
//css引入
let style = require('!!css-loader?sourceMap=false!postcss-loader?{"postcssOptions":{"plugins":{"cssnano":{}}}}!./index.css').toString();
//scss引入
let style = require('!!css-loader?sourceMap=false!postcss-loader?{"postcssOptions":{"plugins":{"cssnano":{}}}}!sass-loader!./style.scss').default.toString();
//less引入
let style = require('!!css-loader?sourceMap=false!postcss-loader?{"postcssOptions":{"plugins":{"cssnano":{}}}}!less-loader!./style.less').default.toString();