更多文章,參見大搜車技術博客:blog.souche.com/javascript
大搜車無線開發中心持續招聘中,前端,Nodejs,android 均有 HC,簡歷直接發到:sunxinyu@souche.comcss
以前在作網站換膚,因此想談談網站換膚的實現。網頁換膚就是修改顏色值,所以重點就在於怎麼來替換。html
點擊不一樣的按鈕切換不一樣的樣式表
,如:
能夠看出,咱們須要爲每一個顏色塊編寫樣式表,那若是我要實現幾百種或者讓用戶自定義呢,顯而易見這種方式十本笨拙,且拓展性並不高,另外,若是考慮加載的成本,那其實這種方式並不可取。前端
ElementUI
的實現ElementUI
的實現比上面的實現高了好幾個level,它能讓用戶自定義顏色值,並且展現效果也更加優雅。當前個人實現就是基於它的思路來實現。 咱們來看看他是怎麼實現的(這裏引用的是官方的實現解釋):vue
style
標籤,把生成的樣式填進去:github.com/ElementUI/t…下面我具體講下我參考它的原理的實現過程 (咱們的css 編寫是基於 postcss
來編寫的):java
tint(var(--color-primary), 20%)
,darken(var(--color-primary), 15%)
,shade(var(--color-primary), 5%)
等。這也相似就實現了上面的第一步// 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使然。react
既然對應關係彙總好了,那咱們就來進行顏色值的替換。在一開始進入網頁的時候,我就先根據默認的主題色根據 formula.js
中的 計算顏色彙總表
生成對應的顏色,以便後面的替換,在這過程當中使用了css-color-function 這個包,android
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: git
圖2,黑色字即爲顏色正則表達式: github
好了,當咱們拿到了原始值以後,就能夠開始進行替換了,這裏的替換源是什麼?因爲咱們的網頁是經過以下 內嵌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 的編程式用法。
注:使用less 來實現換膚要注意
less 文件
在html
中編寫的位置,否則極可能被其餘css 文件所幹擾致使換膚失敗
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:若是你還有其餘換膚的方式,或者上面有說到不妥的地方,歡迎補充與交流🤝🤝