CSS3 var() 实现主题色换肤实践

4,619 阅读5分钟

一、问题背景

  • 在新的需求中,视觉设计师的要求把项目中原有的主题色需要从黄色 替换为 蓝色
  • 换肤这个动作在项目打包前是不知道,而是在打包之后根据条件进行切换的。因此需要保留两套样式:黄色主题色皮肤 和 蓝色主题色皮肤
  • element-ui 组件库 也需要实现主题色换肤

二、现阶段的换肤方案

  • 由于时间关系,现阶段采用的换肤方案是写了两套 主题色皮肤,使用最简单的强覆盖的方案
/**
 * 蓝色 主题样式
 */
@themeColor: blue;
.au-bu-theme {
    // 引入的样式文件已经经过 样式变量的抽取
    @import '../less/com.less';
    ...
    @import '../less/bottom-button.less';
}
  • 最突出的问题:代码超级难维护 !!!

三、更简单的 换肤方案 探索

1. 方案一:ant-design 的换肤方案

  • ant-design 的官网地址:www.antdv.com/docs/vue/cu…
  • 考虑的原因,ant-design 组件库的样式是采用 less 编写的,和目前的项目有相似性,具有参考性
  • 代码的实现:
// vue-cli2
// build/utils.js
- less: generateLoaders('less'),

+ less: generateLoaders('less', {
+   modifyVars: {
+     'primary-color': '#1DA57A',
+     'link-color': '#1DA57A',
+     'border-radius-base': '2px',
+   },
+   javascriptEnabled: true,
+ }),
  • 遇到的问题:ant-design 采用的是打包前 全局样式变量 替换的换肤方法 不符合我们的需求,因此 pass

2. 方案二:var() 结合 :root 伪元素 换肤方案

:root 这个 CSS 伪类匹配文档树的根元素。对于 HTML 来说,:root 表示 元素,除了优先级更高之外,与 html 选择器相同。 ---mdn:链接 developer.mozilla.org/zh-CN/docs/…

  • 该方案最简单,最容易理解

  • 实现代码:

// 直接在 .less 文件中编写。:root 始终会变成 <html> 标签 的伪元素
:root {
    '--primary-color': #1DA57A;
}
// 兼容之前的变量写法
@themeColor: var(--primary-color);
// 修改变量 --primary-color 以实现换肤
let docEle = document.documentElement
const auTheme = {
    '--primary-color': 'bule';
    '--hello-color': 'red';
}
// 批量覆盖样式
for (const key in auTheme) {
    if (auTheme.hasOwnProperty(key)) {
        const value = auTheme[key];
        // 关键
        docEle.style.setProperty(key, value)
    }
}
// 释放内存,避免内存溢出
docEle = null
  • 问题: ie 不支持 var()

3. 方案三:element-ui 的换肤实现

  • 组件库官网: element.eleme.cn/2.7/#/zh-CN…
  • 首页有组件库切换皮肤的实现效果,若能参考到实现的方案,换肤的方案会简单很多
    换肤效果如下 GIF

饿了么换肤

  • 遇到的问题:官方的文档只介绍了在打包前如何替换样式
  • 饿了么 github 官方回复

项目仓库在这:github.com/ElementUI/t…
实现其实很暴力:
先把默认主题文件中涉及到颜色的 CSS 值替换成关键词:github.com/ElementUI/t…
根据用户选择的主题色生成一系列对应的颜色值:github.com/ElementUI/t…
把关键词再换回刚刚生成的相应的颜色值:github.com/ElementUI/t…
直接在页面上加 style 标签,把生成的样式填进去:github.com/ElementUI/t…

  • 通读完饿了么解决的方案,关键点:拿到所有组件的 cssText, 正则匹配需要修改的颜色值,颜色值替换完毕后,再对 cssText 进行覆盖,再往 标签里生成一个 标签,标签里的 innerText = cssText

四、思考

1. 结合自身的项目

  • 饿了么组件库主题色覆盖,采用的方案是先引入样式再对相应的主题色值进行覆盖
  • 做第一次换肤方案的时候,已经对全局的样式进行了主题色整理
  • 不会有用户主动的换肤操作,换肤的操作只会在特定的情形下触发,属于低频行为

2. 初步可行的想法

  • 考虑到工程的大小以及工作量,对覆盖组件库主题色的方法暂不替换。而方案一不支持动态换肤,方案三实现起来工作量大而且繁琐,而且面临的问题还有样式强覆盖带来的一些副作用。最后决定使用方案三:采有 CSS3 var() 结合 :root 属性来实现主题色。
  • 目前的优势是在已经抽取了全局主题色样式。采用方案三的实现方法实现,改动极小。

3. 需要解决的问题:

  • CSS3 的 var() 语法在 ie浏览器上不支持,需要进行处理

五、解决方案

  • 配置全局的less样式变量
  • 需要对所有的主题色变量进行替换(利用 vscode 的一键替换能力)
  • 在需要换肤的场景往 便签中 新的主题样式,利用 css 同名覆盖的原则,达到换肤目的

六、具体实现

前提:项目采取 vue-cli2 实现,以下代码基于 vue-cli2

1. 配置 :root

// yellow.css
:root {
    --theme-color: yellow;
}
// blue.css
:root {
    --theme-color: blue;
}
// index.html
<html>
    <head>
        // 直接写到html文件,避免页面加载完毕后,颜色闪烁
        <link rel="stylesheet" type="text/css" href="yellow.css">
    </head>
</html>

2. 配置全局less变量

  • vue-cli2
// 安装插件
$ yarn add sass-resources-loader -D
// variables.less
@themeColor: var('--theme-color'); // 拿到全局的 css3 变量
// util.js
exports.cssLoaders = function (options) {
	function generateLoaders (loader, loaderOptions) {
    let loaders = [cssLoader]
        if (loader) {
            loaders.push({
                loader: loader + '-loader',
                options: Object.assign({}, loaderOptions, {
                    sourceMap: options.sourceMap
                })
            })
        }

        /**
         * 在 less-loader 前置一个 sass-resources-loader,达到注入 less 变量的目的
         */
        if (loader === "less") {
            loaders.push({
                // Not Only For Sass
                // https://github.com/shakacode/sass-resources-loader/issues/31
                loader: "sass-resources-loader",
                options: {
                    resources: path.resolve(__dirname, '../src/variables.less')
                }
            })
        }

        // Extract CSS when that option is specified
        // (which is the case during production build)
        if (options.extract) {
            return ExtractTextPlugin.extract({
                use: loaders,
                fallback: 'vue-style-loader'
            })
        } else {
            return ['vue-style-loader'].concat(loaders)
        }
    }
}

  • vue-cli3
$ yarn add style-resources-loader -D
$ yarn add vue-cli-plugin-style-resources-loader -D
// vue.config.js

pluginOptions: {
    'style-resources-loader': {
        'preProcessor': 'less',
        // 配置全局的less变量以及方法 写在 variable.less 文件中。支持多个文件
        'patterns': [
            // 换成自己的地址
            path.resolve(__dirname, './src/common/assets/css/variable.less')
        ]
    }
}

3. 换肤

经过网上资料的查找,发现npm上有 css3 var 的垫片插件css-vars-ponyfill!!!
垃圾 ie 终于有救了

// 安装插件
$ yarn add css-vars-ponyfill
$ yarn add mutationobserver-shim
// changeTheme.js
import 'mutationobserver-shim' // 兼容ie8
import cssVars from 'css-vars-ponyfill' // css var 的垫片

// 动态生成link标签
function createLink (name) {
    let docu = documeent
    douc.createElement('link')
    link.setAttribute('rel', 'stylesheet')
    link.setAttribute('type', 'text/css')
    link.setAttribute('href', '`./${name}.css`') // 自己的文件地址
    douc.head.appendChild(link)
    docu = null
}

// 判断是否是ie浏览器
function isIe () {

}

// 加载新的css变量文件,覆盖默认的css变量文件,达到换肤的目的
function changeTheme () {
    createLink('blue')
}

export default function () {
    if (isIe()) {
        // cssVars工作是在DomContentload 事件之后,放入一个宏任务,确保能执行
        setTimeout(function () {
            changeTheme()
            cssVars({
                watch: true
            })
        }, 0)
    } else {
        changTheme()
    }
}
// main.js 实现调用
import changeTheme from 'changeTheme.js'

changeTheme()

七、总结

  • 对全局less变量整理,打通所有的.vue.less文件。而全局less变量引用时css3 变量,我们只需要改变css3变量进行修改,就可以达到换肤的目的
  • 优点:改造之后,一劳永逸。暗黑模式等等多种皮肤都可以采取这样的方案实现
  • 缺点:换肤的时候,有点点轻微的闪烁,后续可以根据场景进行优化
  • css-vars-ponyfill兼容ie浏览器的原理:使用jsvar(*)变量转换为具体的值,再通过<style>标签插入到<head>标签中,至于性能问题(ie你们看我干嘛?)。

image.png

巨人的肩膀(参考资料)

www.cnblogs.com/leiting/p/1…
www.sitepoint.com/css-theming…
github.com/ElemeFE/ele…
github.com/ElementUI/t…
www.cnblogs.com/jofun/p/119…
zhuanlan.zhihu.com/p/149033179

最后,好好学习不会差!我是爱你们的航少,看完记得点个赞