以前在作网站换肤,因此今天想聊聊网站换肤的实现。网页换肤就是修改颜色值,所以重点就在于怎么来替换。javascript
如上图,咱们会看到在某些网站的右上角会出现这么几个颜色块,点击不一样的颜色块,网站的总体颜色就被替换了。要实现它,咱们考虑最简单的方式:点击不一样的按钮切换不一样的样式表
,如:css
能够看出,咱们须要为每一个颜色块编写样式表,那若是我要实现几百种或者让用户自定义呢,显而易见这种方式十本笨拙,且拓展性并不高,另外,若是考虑加载的成本,那其实这种方式并不可取。html
ElementUI
的实现ElementUI
的实现比上面的实现高了好几个level,它能让用户自定义颜色值,并且展现效果也更加优雅。当前个人实现就是基于它的思路来实现。
咱们来看看他是怎么实现的(这里引用的是官方的实现解释):vue
style
标签,把生成的样式填进去:https://github.com/ElementUI/theme-preview/blob/master/src/app.vue#L198-L211 下面我具体讲下我参考它的原理的实现过程 (咱们的css 编写是基于 postcss
来编写的):java
tint(var(--color-primary), 20%)
,darken(var(--color-primary), 15%)
,shade(var(--color-primary), 5%)
等。这也相似就实现了上面的第一步这里我先把所有css文件中能够经过主题色来计算出其余颜色的颜色值汇总在一块儿,以下:react
// 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使然。git
既然对应关系汇总好了,那咱们就来进行颜色值的替换。在一开始进入网页的时候,我就先根据默认的主题色根据 formula.js
中的 计算颜色汇总表
生成对应的颜色,以便后面的替换,在这过程当中使用了css-color-function 这个包,github
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:ajax
图2,黑色字即为颜色正则表达式:正则表达式
好了,当咱们拿到了原始值以后,就能够开始进行替换了,这里的替换源是什么?因为咱们的网页是经过以下 内嵌style标签
的,因此替换原就是全部的style标签
,而 element
是直接去请求网页 打包好的的css文件
:
注:并非每次都须要查找全部的 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, }); } };
效果以下:
至此,咱们的颜色值替换已经完成了。正如官方所说,实现原理十分暴力😂,同时感受使用源css经过 postcss
编译出来的颜色值很差经过 css-color-function
这个包来计算的如出一辙,好几回我都是对着 rgba
的值一直在调🤣🤣,( 👀难受
antd
的实现antd
的样式是基于 less
来编写的,因此在作换肤的时候也利用了 less
能够直接 编译css 变量
的特性,直接上手试下。页面中顶部有三个色块,用于充当颜色选择器,下面是用于测试的div块。
下面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中直接引入:
点击blue
色块,能够看见 color
和 background
的值确实变了:
而且产生了一个 id=less:color
的style 标签,里面就是编译好的 css
样式。紧接着我又试了link两个less 文件,而后点击色块:
从上图看出,less.js 会为每一个less 文件编译出一个style 标签。
接着去看了 antd
的实现,它会调用 antd-theme-generator 来把全部antd 组件
或者 文档
的less 文件组合为一个文件,并插入html中,有兴趣的能够去看下 antd-theme-generator 的内部实现,可让你更加深刻的了解 less 的编程式用法。
CSS自定义变量
的实现先来讲下 css自定义变量
,它让我拥有像less/sass
那种定义变量并使用变量的能力,声明变量的时候,变量名前面要加两根连词线(--
),在使用的时候只须要使用var()
来访问便可,看下效果:
若是要局部使用,只须要将变量定义在 元素选择器内部便可。具体使用见使用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就会为咱们保留了,效果以下:
这时候就能够在换肤颜色选择以后调用 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, }), ], };
它们至关于 babel
的preset
。
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-env
的 stage= 0
),个人 字体图标
居然没了:
这就很神奇,因为没有往 代码的编写
上想,就直接去看了源码
它会调用 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 时候试成功了。而后就去看了该插件的功能,难道我字体图标的定义也是这样?果真如此:
上面介绍了四种换肤的方法,我的更加偏向于 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:若是你还有其余换肤的方式,或者上面有说到不妥的地方,欢迎补充与交流🤝🤝