前端多主题实现及切换方案
随着操作系统支持暗色模式之后,多主题逐渐流行起来,恰好最近在产品内实现了兼容多套主题的方案,于是记录一下
什么是主题?简单来说,就是应用的UI风格,比如最常见的亮色Light Mode
和黑夜Dark Mode
主题,为了用户的阅读体验,如果背景白色、文字就要深色;反之如果背景是深色、文字就要浅色。
从代码实现上来看,每一套主题实际上就是对应了一套CSS样式表,因此多主题的切换,实际上就是多套CSS的切换。
由于CSS属于DSL,处理逻辑条件的能力比较弱,因为本文主要讨论的就是存在多套主题时,CSS样式的实现、切换和维护。
多主题实现
最简单的主题切换就是通过父类选择器来限制样式的作用范围,比如
.light-theme {
.content {
background: #fff;
color: #000;
}
// ...其他需要展示不同主题样式的类
}
.dark-theme {
.content {
background: #000;
color: #fff;
}
// ...其他需要展示不同主题样式的类
}
.content {
// 与主题无关的其他样式
padding: 10px;
}
SCSS变量
所有需要根据当前主题展示不同UI的组件,都需要编写多套主题UI代码,可以借助scss,来维护相同的变量如类型、颜色等信息
$theme-light: '.theme-light';
$theme-dark: '.theme-dark';
$light-bg-color: #fff;
$light-text-color: #000;
$dark-bg-color: #000;
$dark-text-color: #fff;
// 对应主题
#{$theme-light} {
.content {
background: $light-bg-color;
color: $light-text-color;
}
// ...其他需要展示不同主题样式的类,可以复用$light-*相关的变量
}
#{$theme-dark} {
.content {
background: $dark-bg-color;
color: $dark-text-colo;
}
}
这样,即使修改了主题类名和主题变量,只需要在原始定义的地方修改,无需再深入到每个组件文件中去改动了。
CSS变量
尽管SCSS变量已经节省了大量后续迭代的维护工作,但由于每套主题都需要编写对应的样式规则,因此上面的写法存在很多重复的地方,比如
.xxx {
background: xxx;
color: xxx;
}
要解决样式规则重复的问题,可以使用CSS变量。具体的CSS变量规则及使用,可以参考CSS 变量教程 - 阮一峰
SCSS变量与CSS变量最大的区别就在于:CSS变量是浏览器默认支持的,可以近似认为增强了CSS的逻辑表达能力。
因此,上面的代码可以直接改成
.light-theme {
--bg-color: #fff;
--text-color: #000;
}
.dark-theme {
--bg-color: #000;
--text-color: #fff;
}
.content {
background: var(--bg-color);
color: var(--text-color);
padding: 10px;
}
这样的话,父类就只负责维护CSS变量,不再承担限制样式类范围的功能,组件只需要在需要切换主题的地方使用CSS变量替代就可以,无需再进行父类的嵌套。
实际业务中,在同一个主题下,可能需要定义数十个CSS与主题相关的CSS变量。CSS变量很适合存在颜色、通用尺寸等主题基础信息,但可能由于某些特殊的业务场景需求,在不同主题下,要求布局也发生变化。
如果用css变量来控制布局,根节点需要定义非常多信息,十分繁琐。因此通过父类选择器来限定不同主题下样式的作用范围,还是有用武之地的。
#{$theme-light} {
.container {
display: flex;
width: 100px; // 这些特定尺寸信息就不太适合放在css变量中
}
}
#{$theme-dark} {
.container {
width: 200px;
display: flex;
flex-direction: row-reverse;
}
}
如果在不同主题下两个组件在布局、数据源等地方差异非常大,这个时候就不太适合用CSS来处理了,更合理的方案可能是动态组件:按主题拆分成不同的组件,在对应主题下切换成对应的组件。
原子类
tailwind
、windicss
等框架提供了配置theme
主题的功能,大概原理就是在配置文件中,预先定义一批与主题相关的变量,然后在编译css的时候,用主题变量替换对应的占位符,与SCSS变量比较类似。
theme
配置项可配置主要包括颜色集、字体、边框、断点等。
下面演示了windicss
中使用主题变量的方式
// windi.config.js
import { defineConfig } from 'windicss/helpers'
import colors from 'windicss/colors'
import plugin from 'windicss/plugin'
export default defineConfig({
darkMode: 'class', // or 'media'
theme: {
extend: {
colors: {
blue: colors.sky,
red: colors.rose,
pink: colors.fuchsia,
}
},
},
}
然后就可以通过theme
指令来读取对应的配置变量
.btn-blue {
background-color: theme("colors.blue.500");
}
最后会被编译成
.btn-blue {
background-color: #3b82f6;
}
于是,我们可以配置多套主题色,然后编译出多套CSS代码,多套CSS代码在主题切换时要更麻烦一点,因此这个方案一般不做考虑。
但借助原子类的思想,我们可以把与主题相关的样式抽象为多个原子类,如bg-primary
、text-primary
,
.bg-primary {
background: var(--bg-color);
}
.text-primary {
color: var(--text-color);
}
这样就不用一遍遍地在各个样式类中重复var(--bg-color)
等CSS变量了
多主题切换
主题切换比较简单,这里只大概介绍一下。
媒介查询自动切换主题
如果只需要跟随系统实现亮色和暗色模式的主题自动切换,可以使用@media(prefers-color-scheme)
媒介查询来实现
@media (prefers-color-scheme: light) {
:root {
--bg-color: #fff;
--text-color: #000;
}
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #000;
--text-color: #fff;
}
}
定义好对应主题的CSS变量就行了,剩下的交给操作系统。
这个方案的缺点在于不灵活,无法支持用户主动切换主题,比如要在操作系统亮色模式下使用产品的暗色模式之类的场景。
JavaScript切换主题
通过JavaScript动态切换根节点的主题类名,是一种更普遍的做法
document.body.classList.toggle('light-theme')
document.body.classList.toggle('dark-theme')
然后在不同的主题类下面定义对应的CSS变量
.light-theme {
--bg-color: #fff;
--text-color: #000;
}
.dark-theme {
--bg-color: #000;
--text-color: #fff;
}
基于这个思路,甚至可以实现让用户自定义对应主题的功能。
link alternate 按需加载
如果为不同的主题准备了不同的样式表,那使用link来切换主题也许是个不错的方案
link标签有一个rel的属性字段,可以指定值为alternate
,参考
下面演示了大致的主题切换方案,假设有dark.css
和light.css
这两个主题对应的样式表
首先指定rel="alternate stylesheet"
,这样浏览器就只会加载文件,但不会渲染样式表
<link href="light.css" rel="alternate stylesheet" type="text/css" title="红色">
<link href="dark.css" rel="alternate stylesheet" type="text/css" title="绿色">
然后在切换事件触发时,修改对应节点的disbaled
属性
// 切换成light这个样式表
document.querySelector('link[href="light.css"]').disabled = false;
document.querySelector('link[href="dark.css"]').disabled = true;
由于多主题多套样式表目前看起来并不是很主流的方案,这里就不再展开了。
小结
本文主要整理了多主题的实现思路和切换方案,欢迎交流。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。