这种方式是原生css+css变量的换肤方案,且在不支持css变量的浏览器上也可以换肤。
方案实现思路:
- 可以维护多个css文件(也只能写在css文件中),在index.html中引入
- 维护多个主题css变量文件
- 支持css变量的浏览器:页面切换主题-引用不同css变量文件
不支持css变量:结合css样式文件与css变量文件生成一个新的替换了css变量的样式文件
demo是基于vue写的。具体实现:
themeUtils.js:
import axios from 'axios' //用于读取css文件内容
//支持css变量类class SupTheme { /** * 不同的主题加载不同的css变量文件 */ constructor() { this.link = null this._curVarMap = {} } switchThemeByUrl(url) { this._addLink(url) } _addLink(href) { if (this.link) { this.link.href = href return } this.link = document.createElement('link') this.link.id = 'themeStyle' this.link.rel = 'stylesheet' this.link.href = href document.head.appendChild(this.link) } _removeLink() { if (this.link) { document.head.removeChild(this.link) this.link = null } }}
//浏览器不支持css变量类
class NoSupTheme { //大概思路 // 1.main.css中用css变量存着各个元素的css属性 // 2.切换主题时候,读取对应的css变量文件,将变量键值建立map // 3.将不支持的css变量替换为map中对应的色值 /** * 代码的主要逻辑: * 1. 读取出页面引用的关于主题的css文件 * 2. 根据主题信息,加载不同的关于主题色值的css变量文件。 * 之后通过正则读取所有:root中定义的变量。/:root[^\}]*\{[^\}]*\}/gm * 然后依据正则拿出每个css变量和与之对应的色值信息/\s\S*(--[^;\}]*)(;)/gs * 最后形成一个css变量名称与css色值的map对象 * 3. 根据变量文件切割成不同的变量名称-css色值的map对象 * 4. 循环这个map对象,key为css文件中对应的css变量,value为对应的css色值。
每遍历一个key都通过正则替换变量为对应的css色值var\\(${name}\\) * 5. 最后将替换之后的css样式重新加载到文档中 */ constructor() { this._style = null this._init() } async switchThemeByUrl(url) { let data = (await axios.get(url)).data const newStyle = await this._getNewStyle(this._allStyle, data) this._addStyle(newStyle) } async _init() { const allStyle = await this._getAllStyle() const newStyle = await this._getNewStyle(allStyle) this._addStyle(newStyle) this._allStyle = allStyle.replace(/:root[^\}]*\{[^\}]*\}/gm, '') } async _getNewStyle(styleStr, data2) { let result = styleStr let rootsStr = data2 || styleStr const roots = rootsStr.match(/:root[^\}]*\{[^\}]*\}/g) || [] for (const root of roots) { result = this._replaceVar(result, await this._getThemeMap(root)) } return result } async _getThemeMap(data) { let styleItems = data.match(/\s\S*(--[^;\}]*)(;)/gs) const result = {} if (!data) { return result } for (const styleItem of styleItems) { const items = styleItem.split(':') result[items[0].replace(/\s*/g, '')] = items[1].replace(/[\s|;]/g, '') } return result } _replaceVar(text, themeMap) { let result = text for (const name in themeMap) { const pattern = `var\\(${name}\\)` result = result .replace(new RegExp(pattern, 'g'), themeMap[name]) } return result } async _getAllStyle() { //获取元素样式文件内容 let result = '' const links = document.querySelectorAll("[themeflag='theme']") for (let index = links.length - 1; index >= 0; index--) { const link = links[index] result = (await axios.get(link.href)).data + result link.parentElement.removeChild(link); } return result } _addStyle(str) { if (!this._style) { this._style = document.createElement('style') document.head.appendChild(this._style) } this._style.innerHTML = str }}
//切换主题的类
class ThemeUtil { constructor() { this.isSupported = window.CSS && window.CSS.supports && window.CSS.supports('--a', 0) if (this.isSupported) { this.theme = new SupTheme(); } else { this.theme = new NoSupTheme() } } switchThemeByUrl(url) { this.theme.switchThemeByUrl(url) }}export const themeUtil = new ThemeUtil()
css文件:
/** * 新建css文件,放在public中 * main.css--页面元素的css样式。 * depp.css--深色主题css变量文件 * light.css--浅色主题css变量文件 */
//main.css:需要换肤的元素属性用css变量控制
.mainBtn{ background-color: var(--bg-white1) !important;}
.primaryBtn{
background-color: var(--bg-white2) !important;
}//light.css
:root { --bg-white4: #688b79; --bg-white1: lightblue; --bg-white2: #d5afaf; --bg-white3: rgba(203, 149, 178, 0.73);}
//deep.css
:root { --bg-white4: #688b79; --bg-white1: black; --bg-white2: #d5afaf; --bg-white3: rgba(203, 149, 178, 0.73);}
index.html:
<link rel="stylesheet" themeflag="theme" href="./theme/main.css">
页面切换主题:
vue:
<script>
import {themeUtil} from "./themeUtil.js";
methods:{
changeTheme(theme){ //theme:deep/light
themeUtil.switchThemeByUrl(`./theme/${theme}.css`) }
}
</script>
再说下换肤的页面逻辑:
- 首先在index.html引入控制元素样式的css文件
- 换肤的触发事件,changeTheme,调用 switchThemeByUrl方法,走到themeUtils.js中
- 不支持css变量的换肤实现,其实就是两点
a. 读取css样式文件内容
b. 读取css变量文件,形成一个map,类似于
{
--bg-white1: "lightblue",
--bg-white2: "#d5afaf",
--bg-white3: "rgba(203,149,178,0.73)",
--bg-white4: "#688b79"
}
c. 把读到的css样式文件内容中的var( --bg-white1)替换为 lightblue
d. 把替换完成后的内容以style的方式插入到dom中
这种实现的方式,优点是能避免硬编码css样式文件,只是通过不同的css变量就可以控制不同主题下的色值。而且还能够兼容ie。
但是缺点也很明显:
首先就是每次换肤,都需要读取对应主题的css变量,然后替换css变量的值,一旦页面的样式非常多,换肤的成本也会随之增大。
再就是,为了避免每次换肤都去读取css文件,把内容存了下来,这样一旦css文件较大,内容很多,很容易就内容泄漏。而且demo中还是一个css文件,如果三四个css样式文件,内存泄漏的风险会随之增高。
最后就是因为要将换肤的元素的css抽离出来成单独的css,在后期修改样式的时候还得从css文件中找,还会涉及到相互之间css权重问题,子组件的css样式给覆盖掉的问题
不过,总归是一种兼容方案,小记一下