前端主题切换功能在许多网站和应用中都非常重要,它允许用户根据自己的喜好或需求来改变界面的视觉效果。它是提升用户使用体验的重要指标,通过网站个性化定制,可以增加用户的忠诚度和使用黏性,提高用户留存率,提高产品形象。
本文从技术实现上总体来盘点一下前端目前都有哪些已经落地的主题方案。
Link 动态引入
提前准备好各个主题的 CSS 文件,在切换的时候加载,比如:
js
// js
const autoThemeChange = (e) => {
const isDark = e.matches;
document.getElementById('theme').href = `${isDark ? 'dark' : 'light'}.css`; // 这里的文件,可以是远程的,也可以是项目中的静态文件
}
mediaQueryListDark = window.matchMedia('(prefers-color-scheme: dark)');
mediaQueryListDark.addListener(autoThemeChange);
html
<!-- html -->
<head>
<link rel="stylesheet" src="https://cdn.xxx.com/dark.css">
<head>
优点:
- 实现简单,只需要关注维护 css 即可
- 可以实现主题样式按需加载
缺点:
- 如果 css 文件过大, 可能会导致页面样式闪烁
- 各个主题文件相互独立,修改样式比较麻烦
- 在服务端渲染时,没办法直接操作客户端 DOM 元素,会影响加载性能
- 不够灵活,不能满足现代前端开发的需求
提前加载样式,类名切换方案
先引入各个主题的 css,通过切换类名实现主题切换。
我们来看一个 antd 的实现案例:antd-theme-change-demo
他的原理是切换主题时,引入不同的主题算法(css 文件):
然后配置 ThemeProvider
:
js
return (
<ConfigProvider
theme={{
algorithm: themeLight ? theme.defaultAlgorithm : theme.darkAlgorithm,
}}
>
{children}
</ConfigProvider>
);
在两种模式下,相同的元素,但是类名不同了,则使用不同的 css 主题文件:
亮:
暗:
此外,还可以配置前缀来自定义组件样式:
js
export const PREFIX = 'tech-theme';
...
<ConfigProvider
prefixCls={prefixCls}
>
{children}
</ConfigProvider>
默认:
自定义前缀:
此方案的样式还可以通过 hook 动态获取为 token 使用。
优点:
- 样式切换会更加顺畅
- 便于组件封装与状态管理
- 可配置性比较强,支持动态自定义样式
缺点:
- 首屏加载会慢一些
- 除了样式切换不会闪烁,其余缺点与动态 link 方案缺点一样
CSS 变量
这个方案是目前主流推荐的方案。在介绍之前,先讲一下浏览器的几个新特性。
color-scheme 方案
color-scheme 是浏览器普遍支持的配置主题的属性。他表示调用操作系统默认的主题方案覆盖当前元素样式:
自定义方案
如果要自定义变量,可以这样定义自己的样式变量:
css
// theme.css
// 夜间模式
html[data-theme='dark'] {
--text-color: #fff;
--brand-primary: #1668dc;
}
// 普通模式
:root {
--text-color: #333;
--brand-primary: #1677ff;
}
然后在使用时:
js
// 入口文件
import './theme.css';
// 组件样式
a {
color: var(--brand-primary);
}
在切换主题时:
js
document.documentElement.dataset.theme = 要设置的theme
你可以维护一个 useTheme
来动态获取当前的主题。
这里有一个成熟的案例:daisyUI
优点:
- 原生支持,是现在主流方案
- 轻量级,部署方便,可定制化程度高
- 不存在优先级冲突问题
- 可实现热替换和更改
缺点:
- 老的浏览器存在兼容性问题
CSS 预处理器
我们来看一个例子:sass 预编译 theme
他将所有的样式文件使用 Sass 预先定义好:
然后写一个 buildThemes.js
脚本在打包前编译为 css,然后将这个文件动态插入到 head 里:
js
const createLinkElementWithKey = (key) => {
const linkEl = document.createElement('link');
linkEl.setAttribute('rel', 'stylesheet');
linkEl.classList.add(getClassNameForKey(key));
document.head.appendChild(linkEl);
return linkEl;
};
...
// 切换时调用
const setStyle = (key = 'theme', path = 'white') => {
getLinkElementForKey(key).setAttribute('href', `sass-theme/assets/themes/${path}.css`);
}
示意图:
脚本的写法见 Demo
最终效果:
在我这个 demo 里,其实也是把主题样式挂载为全局的 css 变量,只是使用 css 预处理器过度了一下
优点:
- 样式切换流畅,不会卡顿
- 语法更多样,开发成本低
- 与 css 变量一样,新增或修改样式,只需要改动 Scss/Less 变量即可
缺点:
- 需要在编译时手动编译,运行时没办法热替换
- 学习成本高一些
CSS-IN-JS
使用过 React 的朋友应该听说过 emotion,他的设计理念是 All styles in Js
,我之前讲过你的 UI组件库 就用的是这个:手把手搭建基于React的前端UI库 (二)-- 主题配置.
他与 Antd 的实现理念一致,也是维护一个 ThemeProvider
来完成可配置的主题设置:
js
import { ThemeProvider as EThemeProvider } from 'emotion-theming';
render() {
const { theme: _theme, ...rest } = this.props;
const { theme } = this.state;
return <EThemeProvider theme={theme} {...rest} />;
}
Antd 是切换后使用不同的 css 文件,emotion 是切换后使用不同的样式 js,本质上没有区别
上面的 theme 主题可以使用一个 designTokens.ts
配置各种尺寸,颜色等:
然后将文件导出后,全局来访问:
js
mport { useTheme } from 'emotion-theming';
import { defaultTheme } from '../../style';
const useDesignTokens = () => {
// 拿到Themeprovider的theme
const theme = useTheme();
if (!Object.keys(theme).length) return defaultTheme.designTokens;
return theme.designTokens;
};
export default useDesignTokens;
优点:
- 不会存在 css 加载部署的问题,适合微前端这种需要隔离样式的场景
- ...[Antd 方案优点]
缺点:
- 学习曲线比较高
- 增加了运行时的开销和打包体积
- 源码可读性可能会变差
使用 CSS 框架
如果你不想自己配置这些乱七八糟的配置,又苦于不会设计主题颜色,那么 Tailwindcss 就可以帮你完成这个任务。
使用 css 框架,原则上不算是一个新技术分类,他还是上面所讲的方案中的一种或几种,是对这些方案的企业级封装
我们借用官方的夜间主题的例子:
js
<div class="bg-white dark:bg-slate-800 rounded-lg px-6 py-8 ring-1 ring-slate-900/5 shadow-xl">
<div>
<span class="inline-flex items-center justify-center p-2 bg-indigo-500 rounded-md shadow-lg">
<svg class="h-6 w-6 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><!-- ... --></svg>
</span>
</div>
<h3 class="text-slate-900 dark:text-white mt-5 text-base font-medium tracking-tight">Writes Upside-Down</h3>
<p class="text-slate-500 dark:text-slate-400 mt-2 text-sm">
The Zero Gravity Pen can be used to write in any orientation, including upside-down. It even works in outer space.
</p>
</div>
使用 dark:
来声明在主题为夜间时的样式,可以配置为自动识别系统样式或者是可控的切换模式,切换为夜间模式后,会在 html 根元素上添加类 dark
:
这样,Tailwind 就能识别并修改渲染样式了:
这种切换方式,本质上还是类名切换模式,只是使用框架更加方便快捷,支持的生态也更好
详细的 Tailwindcss 的使用,可以见 小肚带您 Tailwind CSS 快速入门
优势:
- 大势所趋,前景广阔
- 易于管理和维护,使得定制主题变得随心所欲
- 代码冗余小
- 更好的性能与可扩展性
不足:
- 框架本身可能会与已有项目架构冲突
- 框架约束了可定制的灵活性
- 学习成本较高
- 需要有额外的维护版本升级工作
设计原则
方案设计,除了技术上实现外,还需要注意其业务场景和用户群体的使用体验,大体上应该遵循如下原则:
- 应遵循用户使用流畅原则,不应添加额外的使用负担
比如,页面中由用户定制化的内容,主题切换后导致色差而无法看清;或者切换按钮藏得太深,用户找不到等
- 不应过度影响页面加载速度
比如,样式 css 太大,网页的白屏速度大大增加,反而影响了使用体验
- 应根据业务场景和用户群体决定
比如,客户群体是 To B 的商务人士,年轻人很少,这种花里胡哨的切换反而不适应,对于商旅人士,移动端适配做得好可能更重要。
注意事项
在主题方案落地后,开发过程中还应注意如下事项:
- 应设置检查规范(比如 CI/CD script),避免开发者自定义样式
- 上线前需 UI 样式走查