前端主题切换-css var

608 阅读6分钟

主题切换的基本原理

很多网站都有切换主题的功能,比如知乎、简书、掘金等等。这些网站的主题切换功能都是通过切换 CSS 变量来实现的。
CSS 变量是 CSS3 新增的特性,它可以在 CSS 中定义变量,然后在需要的地方使用。CSS 变量的语法如下:

:root {  
--main-color: #06c;  
--accent-color: #006;  
}  

在上面的代码中,我们定义了两个 CSS 变量,一个是 --main-color,一个是 --accent-color。在定义 CSS 变量时,需要在变量名前面加上两个短横线(--),这是 CSS 变量的命名规范。CSS 变量的值可以是任意的 CSS 值,比如颜色、长度、百分比等等。
定义好 CSS 变量后,我们就可以在任意需要的地方使用它们了,比如:

body {  
color: var(--main-color);  
}  
.container {  
background-color: var(--accent-color);  
border: var(--border-width) solid var(--main-color);  
}  

在上面的代码中,我们使用 var() 函数来使用 CSS 变量。var() 函数接受一个参数,就是我们定义的 CSS 变量的名称,比如 var(--main-color)。在使用 var() 函数时,如果 CSS 变量没有定义,或者变量值是无效的,那么 var() 函数会使用它的第二个参数作为默认值,比如:

.container {  
background-color: var(--accent-color, #f00);  
}  

在上面的代码中,如果 --accent-color 没有定义,或者变量值是无效的,那么 background-color 属性的值就是 #f00
CSS 变量的值可以在运行时修改,比如:

document.body.style.setProperty('--main-color', '#f00');  

在上面的代码中,我们使用 setProperty() 方法修改了 --main-color 的值,这样就可以实现主题切换的功能了。

主题切换的实现

我们可以使用 CSS 变量来实现主题切换的功能,当我们修改主题的时候,其实修改的是一组 CSS 变量的值。
我们可以把 CSS 变量的值保存在一个对象中,比如:

const themes = {  
light: {  
'--main-color': '#06c',  
'--accent-color': '#006',  
'--border-width': '1px'  
},  
dark: {  
'--main-color': '#f3f3f3',  
'--accent-color': '#f00',  
'--border-width': '3px'  
}  
};  

在上面的代码中,我们定义了两个主题,一个是 light,一个是 dark。每个主题都是一个对象,对象的属性就是 CSS 变量的名称,属性值就是 CSS 变量的值。
当我们需要切换主题时,只需要修改 :root 伪类的样式,比如:

document.querySelector(':root').style.cssText = Object.keys(themes[theme]).map(key => `${key}: ${themes[theme][key]}`).join(';');  

在上面的代码中,我们使用 Object.keys() 方法获取主题对象的所有属性名,然后使用 map() 方法遍历属性名,最后使用 join() 方法把属性名和属性值拼接成字符串,然后设置给 :root 伪类的 style 属性。

以上这种方式只适用于我们预设的主题,如果用户可以自定义主题,那么我们就需要自动生成一些列的色值,比如:字体颜色、边框颜色、背景颜色等等
这种情况下我们可以使用第三方库@ant-design/colors来生成色值,比如:

import { generate } from '@ant-design/colors';  
  
const themeObj = {}  
  
const primaryList = generate('#f00'); // 生成红色相关色值  
  
// 修改主题色  
themeObj['--primary-color-light'] = primaryList[0]  
themeObj['--primary-color-focus'] = primaryList[1]  
themeObj['--primary-color-disabled'] = primaryList[2]  
themeObj['--primary-color-hover'] = primaryList[4]  
themeObj['--primary-color-normal'] = primaryList[5]  
themeObj['--primary-color'] = primaryList[5]  
themeObj['--primary-color-active'] = primaryList[6]  
// 其他的主题色  
  
// 设置主题色  
document.querySelector(':root').style.cssText = Object.keys(themeObj).map(key => `${key}: ${themeObj[key]}`).join(';');  

在上面的代码中,我们通过 generate() 方法生成了一系列的色值,然后把这些色值设置给 :root 伪类的 style 属性,这样就可以实现主题切换的功能了。

考虑到兼容性问题,我们可以使用css-vars-ponyfill这个库来实现主题切换的功能,这个库会自动把 CSS 变量转换成浏览器支持的语法,比如:

import cssVars from 'css-vars-ponyfill';  
  
cssVars({  
// Options...  
});  

我们现在结合@ant-design/colorscss-vars-ponyfill来实现主题切换的功能,具体代码如下:

import { generate } from '@ant-design/colors';  
import cssVars from 'css-vars-ponyfill';  
  
// 默认亮色主题  
export const defaultLightTheme = {  
"#165DFF":['#E8F3FF','#DBECFF','#BEDAFF','#86AFFF','#4080FF','#165DFF','#0E42D1','#022FAC','#002897','#00217B'],  
"#5760FE": ['#F2F3FF','#D9D9FF','#AAB4FF','#8996F9','#7980FE','#5760FE','#454CCB','#3F3F9F','#2F2F93','#1E1E71',]  
}  
// 默认暗色主题  
export const defaultDarkTheme = {  
"#4582E6": ["#1B2F51","#173463","#143975","#103D88","#2667D4","#4582E6","#699EF5","#96BBF8","#C1D9FF","#E2EDFF"],  
"#9ACFC7": ["#003936","#004F4B","#0A6C67","#2A958F","#59BDB7","#9ACFC7","#A9D9D2","#CAD9D6","#DAE5E3","#F0F3F3"],  
}  
  
export const getPrimaryList = function (theme, color) {  
if(theme == 'dark'){  
// 暗色  
if(defaultDarkTheme[color]){  
return defaultDarkTheme[color]  
}  
return generate('color', {  
theme,  
backgroundColor: '#242424'  
})  
}else {  
// 亮色,  
if(defaultLightTheme[color]){  
return defaultLightTheme[color]  
}  
return generate(color)  
}  
}  
  
export const initTheme = (color, theme) => {  
theme = theme ? theme:localStorage.getItem('themeMode')  
if(!theme || theme=='undefined'){  
theme = 'light'  
}  
color = color ? color: localStorage.getItem('themeColor')  
if(!color || color=='undefined'){  
color = theme=='light'?'#165DFF':'#4582E6'  
}  
localStorage.setItem('themeMode',theme)  
localStorage.setItem('themeColor',color)  
let primaryList = getPrimaryList(theme, color)  
let themeObj = {}  
themeObj['--primary-color-light'] = primaryList[0]  
themeObj['--primary-color-focus'] = primaryList[1]  
themeObj['--primary-color-disabled'] = primaryList[2]  
themeObj['--primary-color-hover'] = primaryList[4]  
themeObj['--primary-color-normal'] = primaryList[5]  
themeObj['--primary-color'] = primaryList[5]  
themeObj['--primary-color-active'] = primaryList[6]  
themeObj['--bg-global'] = theme=='light' ? "#EEE":"#181818" // 全局背景  
themeObj['--bg-main'] = theme=='light' ? "#FFF":"#242424" // 主要容器背景  
themeObj['--bg-secondary'] = theme=='light' ? "#F3F3F3":"#383838" // 次要容器背景  
themeObj['--bg-section'] = theme=='light' ? "#E7E7E7":"#242424" // 组件背景  
themeObj['--warn-color'] = "#F38100"  
themeObj['--warn-color-focus'] = "#FFECD6"  
themeObj['--warn-color-light'] = "#FEF5EA"  
themeObj['--success-color'] = "#00B57D"  
themeObj['--success-color-focus'] = "#D8F9F2"  
themeObj['--success-color-light'] = "#E6F8F4"  
themeObj['--error-color'] = "#ED453C"  
themeObj['--error-color-focus'] = "#FFDCD9"  
themeObj['--error-color-light'] = "#FFEEED"  
cssVars({  
watch: true, // 当添加,删除或修改其<link>或<style>元素的禁用或href属性时,ponyfill将自行调用  
variables: {  
...themeObj  
},  
onlyLegacy: true, // false 默认将css变量编译为浏览器识别的css样式 true 当浏览器不支持css变量的时候将css变量编译为识别的css  
});  
};  

上述代码,暴露出一个initTheme方法,这个方法接收两个参数,第一个参数是主题色,第二个参数是主题模式,主题模式有两个值,一个是light,一个是dark,分别代表亮色主题和暗色主题。在每种模式下,又分别对应了两种主题色,亮色主题色有#165DFF#5760FE,暗色主题色有#4582E6#9ACFC7
那么用户在点击切换主题的时候,我们可以调用initTheme方法,来切换主题,具体代码如下:

import { initTheme } from '@/utils/theme';  
  
const themeColor = '#5760FE';  
const themeMode = 'dark';  
initTheme(themeColor, themeMode);  

或者可以让用户在拾色器中选择主题色,然后调用initTheme方法,代码实现大家可以自行尝试。

这就是我目前在使用的主题切换方案,如果大家有更好的方案,欢迎留言讨论。