前端换肤的一些思考

先看看大家怎么做的。下面是两篇别人写的文章,最后是我自己的方法。

第一篇:聊一聊前端换肤

之前在做网站换肤,所以想谈谈网站换肤的实现。网页换肤就是修改颜色值,因此重点就在于怎么来替换。

一般实现

 

image

 

如上图,我们会看到在某些网站的右上角会出现这么几个颜色块,点击不同的颜色块,网站的整体颜色就被替换了。要实现它,我们考虑最简单的方式:点击不同的按钮切换不同的样式表 ,如:

 

  • theme-green.css
  • theme-red.css
  • theme-yellow.css

可以看出,我们需要为每个颜色块编写样式表,那如果我要实现几百种或者让用户自定义呢,显而易见这种方式十本笨拙,且拓展性并不高,另外,如果考虑加载的成本,那其实这种方式并不可取。

ElementUI 的实现

 

image

 

 

ElementUI 的实现比上面的实现高了好几个level,它能让用户自定义颜色值,而且展示效果也更加优雅。当前我的实现就是基于它的思路来实现。 我们来看看他是怎么实现的(这里引用的是官方的实现解释):

下面我具体讲下我参考它的原理的实现过程 (我们的css 编写是基于 postcss 来编写的):

  1. 先确定一个主题色,其他需在在换肤过程中随主题色一起修改的颜色值就根据主题色来调用例如(上面已经说到了我们是基于postcss来编写的,所以就使用了如下函数来计算颜色值): tint(var(--color-primary), 20%)darken(var(--color-primary), 15%)shade(var(--color-primary), 5%) 等。这也类似就实现了上面的第一步
  2. 然后根据用户选择的颜色值来生成新的一轮对应的一系列颜色值: 这里我先把全部css文件中可以通过主题色来计算出其他颜色的颜色值汇总在一起,如下:
// formula.js
const formula = [
    {
        name: 'hoverPrimary',
        exp: 'color(primary l(66%))',
    },
    {
        name: 'clickPrimary',
        exp: 'color(primary l(15%))',
    },
    {
        name: 'treeBg',
        exp: 'color(primary l(95%))',
    },
    {
        name: 'treeHoverBg',
        exp: 'color(primary h(+1) l(94%))',
    },
    {
        name: 'treeNodeContent',
        exp: 'color(primary tint(90%))',
    },
    {
        name: 'navBar',
        exp: 'color(primary h(-1) s(87%) l(82%))',
    }  
];

export default formula;

这里的color函数 是后面我们调用了 css-color-function 这个包,其api使然。

既然对应关系汇总好了,那我们就来进行颜色值的替换。在一开始进入网页的时候,我就先根据默认的主题色根据 formula.js 中的 计算颜色汇总表 生成对应的颜色,以便后面的替换,在这过程中使用了css-color-function 这个包,

import Color from 'css-color-function';

componentDidMount(){
this.initColorCluster = ['#ff571a', ...this.generateColors('#ff571a')];
        // 拿到所有初始值之后,因为我们要做的是字符串替换,所以这里利用了正则,结果值如图2:
        this.initStyleReg = this.initColorCluster  
            .join('|')
            .replace(/\(/g, '\\(') // 括号的转义
            .replace(/\)/g, '\\)')
            .replace(/0\./g, '.');  // 这里替换是因为默认的css中计算出来的值透明度会缺省0,所以索性就直接全部去掉0
}

generateColors = primary => {
        return formula.map(f => {
            const value = f.exp.replace(/primary/g, primary);  // 将字符串中的primary 关键字替换为实际值,以便下一步调用 `Color.convert`
            return Color.convert(value);     // 生成一连串的颜色值,见下图1,可以看见计算值全部变为了`rgb/rgba` 值
        });
    };

图1:

image

 

 

图2,黑色字即为颜色正则表达式:

image

 

 

好了,当我们拿到了原始值之后,就可以开始进行替换了,这里的替换源是什么?由于我们的网页是通过如下 内嵌style标签 的,所以替换原就是所有的style标签,而 element 是直接去请求网页 打包好的的css文件

 

image

 

 

注:并不是每次都需要查找所有的 style 标签,只需要一次,然后,后面的替换只要在前一次的替换而生成的 style 标签(使用so-ui-react-theme来做标记)中做替换

下面是核心代码:

changeTheme = color => {
        // 这里防止两次替换颜色值相同,省的造成不必要的替换,同时验证颜色值的合法性
        if (color !== this.state.themeColor && (ABBRRE.test(color) || HEXRE.test(color))) {
            const styles =
                document.querySelectorAll('.so-ui-react-theme').length > 0
                    ? Array.from(document.querySelectorAll('.so-ui-react-theme')) // 这里就是上说到的
                    : Array.from(document.querySelectorAll('style')).filter(style => {  // 找到需要进行替换的style标签
                          const text = style.innerText;
                          const re = new RegExp(`${this.initStyleReg}`, 'i');
                          return re.test(text);
                      });

            const oldColorCluster = this.initColorCluster.slice();
            const re = new RegExp(`${this.initStyleReg}`, 'ig');  // 老的颜色簇正则,全局替换,且不区分大小写

            this.clusterDeal(color);  // 此时 initColorCluster 已是新的颜色簇

            styles.forEach(style => {
                const { innerText } = style;
                style.innerHTML = innerText.replace(re, match => {
                    let index = oldColorCluster.indexOf(match.toLowerCase().replace('.', '0.'));

                    if (index === -1) index = oldColorCluster.indexOf(match.toUpperCase().replace('.', '0.'));
                    // 进行替换
                    return this.initColorCluster[index].toLowerCase().replace(/0\./g, '.');
                });

                style.setAttribute('class', 'so-ui-react-theme');
            });
          

            this.setState({
                themeColor: color,
            });
        }
    };

效果如下:

image

 

 

至此,我们的颜色值替换已经完成了。正如官方所说,实现原理十分暴力😂,同时感觉使用源css通过 postcss 编译出来的颜色值不好通过 css-color-function 这个包来计算的一模一样,好几次我都是对着 rgba 的值一直在调🤣🤣,( 👀难受

antd 的实现

antd 的样式是基于 less 来编写的,所以在做换肤的时候也利用了 less 可以直接 编译css 变量 的特性,直接上手试下。页面中顶部有三个色块,用于充当颜色选择器,下面是用于测试的div块。

image

 

 

下面div的css 如下,这里的 @primary-color@bg-color 就是 less 变量:

.test-block {
    width: 300px;
    height: 300px;
    text-align: center;
    line-height: 300px;
    margin: 20px auto;
    color: @primary-color;
    background: @bg-color;
}

当我们点击三个色块的时候,直接去加载 less.js,具体代码如下(参考antd的实现):

import React from 'react';
import { loadScript } from '../../shared/utils';
import './index.less';
const colorCluters = ['red', 'blue', 'green'];

export default class ColorPicker extends React.Component {
    handleColorChange = color => {
        const changeColor = () => {
            window.less
                .modifyVars({  // 调用 `less.modifyVars` 方法来改变变量值
                    '@primary-color': color,
                    '@bg-color': '#2f54eb',
                })
                .then(() => {
                    console.log('修改成功');
                });
        };
        const lessUrl =
            'https://cdnjs.cloudflare.com/ajax/libs/less.js/2.7.2/less.min.js';

        if (this.lessLoaded) {
            changeColor();
        } else {
            window.less = {
                async: true,
            };

            loadScript(lessUrl).then(() => {
                this.lessLoaded = true;
                changeColor();
            });
        }
    };

    render() {
        return (
            <ul className="color-picker">
                {colorCluters.map(color => (
                    <li
                        style={{ color }}
                        onClick={() => {
                            this.handleColorChange(color);
                        }}>
                        color
                    </li>
                ))}
            </ul>
        );
    }
}

然后点击色块进行试验,发现并没有生效,这是why?然后就去看了其文档,原来它会找到所有如下的less 样式标签,并且使用已编译的css同步创建 style 标签。也就是说我们必须吧代码中所有的less 都以下面这种link的方式来引入,这样less.js 才能在浏览器端实现编译。

<link rel="stylesheet/less" type="text/css" href="styles.less" />

这里我使用了 create-react-app ,所以直接把 less 文件放在了public目录下,然后在html中直接引入:

image

 

 

 

image

 

 

点击blue色块,可以看见 colorbackground 的值确实变了:

 

image

 


并且产生了一个 id=less:color 的style 标签,里面就是编译好的 css 样式。紧接着我又试了link两个less 文件,然后点击色块:

 

 

image

 

 

从上图看出,less.js 会为每个less 文件编译出一个style 标签。 接着去看了 antd 的实现,它会调用 antd-theme-generator 来把所有antd 组件 或者 文档 的less 文件组合为一个文件,并插入html中,有兴趣的可以去看下 antd-theme-generator 的内部实现,可以让你更加深入的了解 less 的编程式用法。

注:使用less 来实现换肤要注意 less 文件html 中编写的位置,不然很可能被其他css 文件所干扰导致换肤失败

基于 CSS自定义变量 的实现

先来说下 css自定义变量 ,它让我拥有像less/sass那种定义变量并使用变量的能力,声明变量的时候,变量名前面要加两根连词线(--),在使用的时候只需要使用var()来访问即可,看下效果:

 

image

 

 

如果要局部使用,只需要将变量定义在 元素选择器内部即可。具体使用见使用CSS变量关于 CSS 变量,你需要了解的一切

使用 css 自定义变量 的好处就是我们可以使用 js 来改变这个变量:

  • 使用 document.body.style.setProperty('--bg', '#7F583F'); 来设置变量
  • 使用 document.body.style.getPropertyValue('--bg'); 来获取变量
  • 使用 document.body.style.removeProperty('--bg'); 来删除变量

有了如上的准备,我们基于 css 变量 来实现的换肤就有思路了:将css 中与换肤有关的颜色值提取出来放在 :root{} 中,然后在页面上使用 setProperty 来动态改变这些变量值即可。

上面说到,我们使用的是postcss,postcss 会将css自定义变量直接编译为确定值,而不是保留。这时就需要 postcss 插件 来为我们保留这些自定义变量,使用 postcss-custom-properties,并且设置 preserve=true 后,postcss就会为我们保留了,效果如下:

 

image

 

 

 

image

 

 

这时候就可以在换肤颜色选择之后调用 document.body.style.setProperty 来实现换肤了。

不过这里只是替换一个变量,如果需要根据主颜色来计算出其他颜色从而赋值给其他变量就可能需要调用css-color-function 这样的颜色计算包来进行计算了。

import colorFun from "css-color-function"

document.body.style.setProperty('--color-hover-bg', colorFun.convert(`color(${value} tint(90%))`));

其postcss的插件配置如下(如需其他功能可自行添加插件):

module.exports = {
    plugins: [
        require('postcss-partial-import'),
        require('postcss-url'),
        require('saladcss-bem')({
            defaultNamespace: 'xxx',
            separators: {
                descendent: '__',
            },
            shortcuts: {
                modifier: 'm',
                descendent: 'd',
                component: 'c',
            },
        }),

        require('postcss-custom-selectors'),
        require('postcss-mixins'),
        require('postcss-advanced-variables'),
        require('postcss-property-lookup'),
        require('postcss-nested'),
        require('postcss-nesting'),
        require('postcss-css-reset'),
        require('postcss-shape'),
        require('postcss-utils'),

        require('postcss-custom-properties')({
            preserve: true,
        }),

        require('postcss-calc')({
            preserve: false,
        }),
    ],
};

聊下 precsspostcss-preset-env

它们相当于 babelpreset

precss 其包含的插件如下:

使用如下配置也能达到相同的效果,precss 的选项是透传给上面各个插件的,由于 postcss-custom-properties 插件位于 postcss-preset-env 中,所以只要按 postcss-preset-env 的配置来即可:

plugins:[
require('precss')({
            features: {   
                'custom-properties': {
                    preserve: true,
                },
            },
        }),
]
复制代码

postcss-preset-env 包含了更多的插件。这了主要了解下其 stage 选项,因为当我设置了stage=2 时(precss 中默认 postcss-preset-envstage= 0 ),我的 字体图标 竟然没了:

image

 

 

这就很神奇,由于没有往 代码的编写 上想,就直接去看了源码

它会调用 cssdb,它是 CSS特性 的综合列表,可以到各个css特性 在成为标准过程中现阶段所处的位置,这个就使用 stage 来标记,它也能告知我们该使用哪种 postcss 插件 或者 js包 来提前使用css 新特性。cssdb 包的内容的各个插件详细信息举例如下

{ id: 'all-property',
    title: '`all` Property',
    description:
     'A property for defining the reset of all properties of an element',
    specification: 'https://www.w3.org/TR/css-cascade-3/#all-shorthand',
    stage: 3,
    caniuse: 'css-all',
    docs:
     { mdn: 'https://developer.mozilla.org/en-US/docs/Web/CSS/all' },
    example: 'a {\n  all: initial;\n}',
    polyfills: [ [Object] ] }
复制代码

当我们设置了stage的时候,就会去判断 各个插件的stage 是否大于等于设置的stage,从而筛选出符合stage的插件集来处理css。最后我就从stage小于2的各个插件一个一个去试,终于在 postcss-custom-selectors 时候试成功了。然后就去看了该插件的功能,难道我字体图标的定义也是这样?果然如此:

 

image

 

 

总结

上面介绍了四种换肤的方法,个人更加偏向于 antd 或者基于 css 自定义变量 的写法,不过 antd 基于 less 在浏览器中的编译,less 官方文档中也说到了:

This is because less is a large javascript file and compiling less before the user can see the page means a delay for the user. In addition, consider that mobile devices will compile slower.

所以编译速度是一个要考虑的问题。然后是 css 自定义变量 要考虑的可能就是浏览器中的兼容性问题了,不过感觉 css 自定义变量 的支持度还是挺友好了的🤣🤣。

ps:如果你还有其他换肤的方式,或者上面有说到不妥的地方,欢迎补充与交流🤝🤝


作者:大搜车无线开发中心
链接:https://juejin.im/post/5ca41617f265da3092006155
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

第二篇:一文总结前端换肤换主题

最近项目涉及换主题/换肤的工作, 查了查资料,总结出五种换肤方案:

序号 方法 特点
1 利用class 命名空间 最简单的换肤方案
2 准备多套CSS主题 传统前端最常用
3 利用CSS预处理生成多套主题样式 现代前端最常用
4 动态换肤 支持浏览器热换肤,最酷炫
5 CSS变量换肤 不考虑IE,最佳换肤方式

这是五种均为通用方案,可以适用于各种前端框架,脚手架中

1. 利用class 命名空间

这是最简单的换肤方式, 看下面示例即可轻松理解。

1.利用class 名称准备两个主题:


<style>

  p.red-theme {

    color: red

  }

  p.blue-theme {

    color: blue

  }
</style>

2.如果用红色主题, 给body增加 red-theme标签


<body class="red-theme">

    <p> 这里是红色主题 </p>

     ...

</body>

3.如果用蓝色主题, 用 blue-theme 代替 red-theme


<body class="blue-theme">

    <p> 这里是蓝色主题 </p>

     ...

</body>

优缺点

  • 优点: 简单,好理解,好实现
  • 缺点: CSS中需多写主题的class,代码容易混乱;需手动编写

参考

基于element动态换肤

2.准备多套CSS主题

本地存放多套主题, 根据需要切换加载不同的样式

  1. 准备份默认样式主题

/*theme-default.css*/
p {
  color: #333
}
...
  1. 准备各主题的样式
/* theme-red.css */
p {
  color: #red
}
...
/* theme-blue.css */
p {
  color: #blue
}
...
  1. 页面加载后,根据用户需求加载不同的样式列表
   var link = document.createElement('link');
   link.type = 'text/css';
   link.id = "theme-blue";  
   link.rel = 'stylesheet';
   link.href = '/css/theme-blue.css';
   document.getElementsByTagName("head")[0].appendChild(link);
  1. 有时候需要保存用户使用的主题,可以通过如下方式:
    • 利用路由标记
    • 利用cookie标记
    • 利用localstorage
    • 保存到后端服务器

优缺点

  • 优点: 简单,好理解,好实现
  • 缺点: 需要手写两份以上CSS配色样式; 切换样式需要下载CSS的时间

参考

web网页中主题切换的实现思路 中有更多细节

3. 利用CSS预处理生成多套主题样式

这是“准备多套CSS主题”的优化方案,利用CSS预处理生成多套主题样式,再根据需要切换

  1. 利用Less,stylus 或 sass 的变量代替颜色值

  2. 配置多个主题颜色配置
  3. 利用webpack等工具输出多套主题样式
  4. 页面加载后,根据用户需求加载不同的样式列表(同方案2)

优缺点

  • 优点: 不用手写两套CSS
  • 确定: 配置复杂;生成冗余的CSS文件; 切换样式需要下载CSS的时间

参考

webpack的配置比较复杂,可以看这篇文章:webpack 换肤功能多主题/配色样式打包解决方案
ant 环境下可以利用antd-theme-generator 快速配置,详见:antd-theme-generatorantd在线换肤定制功能

4.动态换肤

这是element ui中的换肤方案,支持浏览器热换肤。生成一套主题, 将主题配色配置写在js中,在浏览器中用js动态修改style标签覆盖原有的CSS。

  1. 准备一套默认theme.css样式
/* theme.css */
.title {
  color: #FF0000
}
  1. 准备主题色配置
var colors = {
     red: {
       themeColor: '#FF0000'
     },
     blue: {
       themeColor: '#0000FF'
     }
   }
  1. 异步获取 theme.css ,将颜色值替换为关键词
    关键字可以确保以后能多次换色
var styles = ''
axios.get('theme.css').then((resp=> {
 const colorMap = {
   '#FF0000': 'themeColor'
 }
 styles = resp.data
 Object.keys(colorMap).forEach(key => {
   const value = colorMap[key]
   styles = styles.replace(new RegExp(key, 'ig'), value)
 })
}))

style 变为:

.title {
  color: theme-color
}
  1. 把关键词再换回刚刚生成的相应的颜色值,并在页面上添加 style 标签
 // console 中执行 writeNewStyle (styles, colors.blue)  即可变色
 function writeNewStyle (originalStyle, colors) {
      let oldEl = document.getElementById('temp-style')
      let cssText = originalStyle
       // 替换颜色值
      Object.keys(colors).forEach(key => {
        cssText = cssText.replace(new RegExp(key, 'ig'), colors[key])
      })
      const style = document.createElement('style')
      style.innerText = cssText
      style.id = 'temp-style'
 
      oldEl ? document.head.replaceChild(style, oldEl) : 
      document.head.appendChild(style)  // 将style写入页面
    }

此时style 变为:

.title {
  color: '#0000FF'
}

优缺点

  • 优点: 只需一套CSS文件; 换肤不需要延迟等候;可自动适配多种主题色;
  • 缺点: 稍难理解; 需准确的css颜色值;可能受限于浏览器性能;

参考

本文最后有该方案的完整代码
Vue 换肤实践
elementUI 及 vuetifyjs动态换色实践
vue-element-admin 动态换肤
webpack 插件抽取CSS中的主题色

5. CSS 变量换肤

利用CSS 变量设置颜色, 用js动态修改CSS变量,进而换色。
如果不考虑IE兼容,这是最佳换肤方案
看下面的例子,很好理解

<html>
  <head>
    <title>CSS varies</title>
    <style>
      :root {
        --theme-color: red /* css 变量赋值位置 */
      }
      .title {
        color: var(--theme-color) /* 用css变量标记颜色 */
      }
    </style>
  </head>
  <body>
    <h3 class="title">CSS 变量换肤</h3>
    <script>
      // console 中执行 changceColor('blue') 即可变色
      function changeColor(color = 'blue') {
        document.documentElement.style.setProperty("--theme-color",color);
      }
    </script>
  </body>
</html>

优缺点

  • 优点:只需一套CSS文件; 换肤不需要延迟等候;对浏览器性能要求低;可自动适配多种主题色;
  • 缺点: 不支持IE, 2016年前的chrome,safari; 兼容性参见Can I Use CSS Variables

参考

附A: 方案四 态换换肤完整代码

dynamic.html

<html lang="en">
<head>
  <title>js 动态换肤</title>
   <!-- 利用axios 实现异步加载样式-->
  <script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
</head>
<body>
 <h3 class="title">js 动态换肤</h3>
 <script>
   // 1. 主题颜色配置
   var colors = {
     red: {
       themeColor: '#FF0000' 
     },
     blue: {
       themeColor: '#0000FF'
     }
   }

   // 2. 异步获取样式
   var styles = ''
   axios.get('theme.css').then((resp=> {
     const colorMap = {
       '#FF0000': 'themeColor'
     }
     styles = resp.data
     Object.keys(colorMap).forEach(key => {
       const value = colorMap[key]
       styles = styles.replace(new RegExp(key, 'ig'), value)
       console.log(styles)
     })
     writeNewStyle (styles, colors.red)
   }))

   // 3.换色
   // console.log 中输入 writeNewStyle (styles, colors.blue)可以换蓝色主题
   // console.log 中输入 writeNewStyle (styles, colors.blue)可以换红色主题
   function writeNewStyle (originalStyle, colors) {
     let oldEl = document.getElementById('temp-style')
     let cssText = originalStyle

     Object.keys(colors).forEach(key => {
       cssText = cssText.replace(new RegExp(key, 'ig'), colors[key])
     })
     const style = document.createElement('style')
     style.innerText = cssText
     style.id = 'temp-style'

     oldEl ? document.head.replaceChild(style, oldEl) : document.head.appendChild(style)
   }
 </script>
</body>
</html>

theme.css

.title {
  color: #FF0000
}


作者:seaasun
链接:https://www.jianshu.com/p/35e0581629d2
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

我的方法

根据上面两篇文章的分析和总结。从易用性和复杂度上面权衡,我更倾向与使用class结合动态写入样式表的方案。

比如我们有下面这个结构代码:

<div class="theme">
...
  <div class="th-bg-primary">
    <p class="th-primary">title</p>
    <p class="th-text">desc</p>
  </div>
...
</div>

我们把所有会根据主题改变的颜色的地方,都使用一个特定的class标记出来。然后维护一个class到颜色的css文件模板。比如这样:

const bgPrinmaryColor = '#cecece'
const primaryColor = '#333333'
const textColor = '#666666'
let themeTemplate = `
  .theme.th-bg-primary {
    background-color: ${bgPrinmaryColor} !important;
  }
  .theme.th-primary {
    color: ${bgPrinmaryColor};
  }
  .theme.th-text {
    color: ${textColor};
  }`

const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = themeTemplate;
document.getElementsByTagName('HEAD').item(0).appendChild(style);

然后当用户切换主题的时候,就修改themeTemplate里的变量值,然后重更新把样式表添加到界面上。当然添加之前可以删除之前添加的样式表。

这样只用写一份样式表,又能动态切换。

优点:

1、简单直观,不编写多个样式文件

2、也不需要引入第三方插件来动态编译样式表

3、没有新概念,新语法,新占位符

缺点:

1、颜色和class绑定了

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章