如何实现自定义主题切换?

976 阅读3分钟

本文章是基于ElementUI theme-chalk-preview方法上的改进

效果展示

切换前:

截屏2021-11-20 21.16.38.png

切换后:

截屏2021-11-20 21.16.28.png

先来说一下为什么选择该方案吧,为什么有CSS变量替换等其他更优方法不用呢?

因为IE !!!2021了,居然还要支持IE(我不李姐😐 )

言归正传,先来了解一下原方法的思路吧😆


以下步骤代码摘抄至源码

1、使用XMLHttpRequest请求服务端的CSS样式文件(经步骤 2 处理后,将替换后的全部样式保存在 originalStyle 中)

getFile (url, isBlob = false) {
    return new Promise((resolve, reject) => {
        const client = new XMLHttpRequest()
        client.responseType = isBlob ? 'blob' : ''
        client.onreadystatechange = () => {
            if (client.readyState !== 4) {
                return 
            }
            if (client.status === 200) {
                const urlArr = client.responseURL.split('/')
                resolve({
                    data: client.response,
                    url: urlArr[urlArr.length - 1]
                })
            } else {
              reject(new Error(client.statusText))
            }
       }
       client.open('GET', url)
       client.send()
       })
},
getIndexStyle () { 
    this.getFile('//unpkg.com/element-ui/lib/theme-chalk/index.css') 
        .then(({ data }) => { 
            this.originalStyle = this.getStyleTemplate(data) 
        }) 
}

2、将获取到的样式中涉及到的颜色值替换成关键词

getStyleTemplate (data) {
    const colorMap = { 
        '#3a8ee6': 'shade-1', 
        '#409eff': 'primary', 
        '#53a8ff': 'light-1', 
        '#66b1ff': 'light-2', 
        '#79bbff': 'light-3', 
        '#8cc5ff': 'light-4', 
        '#a0cfff': 'light-5', 
        '#b3d8ff': 'light-6', 
        '#c6e2ff': 'light-7', 
        '#d9ecff': 'light-8', 
        '#ecf5ff': 'light-9' 
    } 
    Object.keys(colorMap).forEach(key => { 
        const value = colorMap[key] 
        data = data.replace(new RegExp(key, 'ig'), value) 
    }) 
  return data 
}

替换前:

h {
    color: #3a8ee6;
}

替换后

h {
    color: shade-1;
}

3、根据用户选择的颜色,自动生成每个关键词对应的颜色值

4、将新生成的颜色值替换回对应的关键词处

5、新增style标签,将样式写入

if (this.originalStylesheetCount === document.styleSheets.length) { 
    const style = document.createElement('style') 
    style.innerText = cssText document.head.appendChild(style) 
} else { 
    document.head.lastChild.innerText = cssText 
}

新增style标签前,先判别是否曾添加过,若无,则新建style,将其添加至末尾。若有,则直接覆盖末尾子节点的内容。

至此我们就完成了自定义主题切换的全部功能

现在我们来思考如何优化他

1、开发模式下支持主题色替换

一般我们在做开发时,都是在本地运行代码,此时上述方式便失效了(ElementUI项目运行时无法以上述请求方式拿到样式文件),一旦项目大起来,编译打包,再映射,查看该功能是否生效,一来费时,二来出现问题也不好定位,因此我们能不能在开发模式下,也支持该功能呢?

问题的关键就在于,如何拿到页面上的CSS样式,此时我们就要借助他的帮忙 styleSheets

截屏2021-11-19 22.16.08.png

关注其中的 cssRules 属性(如下图所示)

截屏2021-11-19 22.18.38.png

诶,cssText 属性中不就存着我们所需的样式嘛?由此我们可以遍历页面上所有的 styleSheets ,然后将cssRules.cssText一行一行的拼凑起来,不就是一个完整的样式了吗?

for (let j = 0; j < document.styleSheets.length; j++) {
    try {
        for (let i = 0; i < document.styleSheets[j].cssRules.length; i++) {
            try {
               this.originalStyle = this.originalStyle.concat(document.styleSheets[j].cssRules[i].cssText);
            } catch (error) {
                continue;
            };
        }
    } catch (error) {
        continue;
    };
}

2、支持 rgb 色值的更改

我们都知道色值有许多种表示方式,在 CSS 中最为常见的莫过于rgb 和 hex ,可上述的例子中仅支持了 hex 的修改,若是某些颜色需带有透明度(或者同种颜色)用了 rgb 的方式表示,就不能修改了。( hex 也可以表示透明度,只可惜 IE 依然不支持)

我们可以把 colorMap(不知道colorMap是什么的,赶紧往上翻!!!不认真!点名批评💢 )变为以下格式

const colorMap = { 
    '64,158,255': 'primary',   // 64,158,255 是 #409eff 对应 rgb 值
    '64, 158, 255': 'primary', 
    '#409eff': 'rgb(primary)'
}

ElementUI 颜色选择器,选择后传出的色值是 hex 格式的,因此我们的色值生成函数,需要进行 hex 到 rgb 的转换,具体函数这里就不贴了,网上很多。

3、删除指定的style标签

原方法是删除最后一个 style 标签,若最后一个 style 标签不是我们新增的呢?此时就会造成误删的情况。

if (document.querySelector("style[title='replaceStyle']")) {
    try {
        document.querySelector("style[title='replaceStyle']").remove();
    } catch (error) {
      document.querySelector("style[title='replaceStyle']").removeNode();
    }
}

新增 style标签 时,附加一个 title ,每次删除时,只删除指定 title 的 style标签。

1637414267267.jpg

4、提升IE浏览器下的替换速度

当新增过 style 标签后,后续的每次替换,我们就将样式覆盖进去,但这速度在 IE 上,不敢恭维,速度慢得令人崩溃😩 ,那有没有什么方法可以提速呢?

实测发现,直接删除原新增的 style 标签,然后再重新添加 style 标签,速度居然比我们直接覆盖更快!!!但这还是远远不够,IE表示,还是很吃力🙉 。此时我们可以利用 Fragment ,让我们先简单了解一下他。

DocumentFragment 是 DOM 节点。不是主 DOM 树的一部分。常用于创建文档片段,将元素附加到文档片段,然后将文档片段附加到 DOM 树。在 DOM 树中,文档片段被其所有的子元素所代替。因为文档片段存在于内存中,并不在 DOM 树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。

const dq = document.createDocumentFragment();  //新建fragment片段
const style = document.createElement('style');  //创建元素
style.title = 'replaceStyle';  //添加标题
style.innerHTML = cssText;
dq.appendChild(style);  //元素插入至fragment片段中
document.head.appendChild(dq);  //插入片段的所有子元素

处理过后,现在 IE 上的主题切换,也变得非常丝滑啦~

最后

本文到此结束,希望对你有所帮助。若有什么好的建议,可以多多交流呀。文中若有不正之处,欢迎指正。

前端萌新向各位大佬鞠躬了,谢谢啦 😘