一文搞懂前端多主题适配方案

9,518 阅读9分钟

多主题介绍

自 IOS 13.0 支持深色模式 (DarkMode) 后,多主题逐渐流行起来,用户可以手动或自动切换到自己喜欢的主题色,本篇文章详细介绍下前端页面如何实现多主题适配。

Mac 系统支持的浅色和深色主题示意图:

1641185056989-4b2ab4ed-0e3c-484b-ba63-a44fe854ec6c.png

在具体实施多主题方案之前,首先要了解两个问题

⭐️ 为什么会出现深色模式?

  • 在视力正常 (或矫正视力正常) 的人群中,浅色模式的视觉表现往往更好,而一些患有白内障和相关疾病的人可能在深色模式下表现更好。另一方面,在光模式下长期阅读可能与近视有关。
  • 日常需要长时间使用的应用,深色模式会减轻眼部疲劳。

⭐️ 我应该去实施深色主题的适配吗?

相关研究调查数据显示,超过 80% (在 Mac 上,而不是在 iOS 上,但这给出了一个趋势),尽管这个数字可能被高估了,但这意味着有很大比例的人在使用它。所以对于使用深色模式的用户来说,切换到不支持的应用真的很不爽,他们很可能会为此删除掉应用或关掉页面。

多主题成为用户体验提升基本手段,现在化前端都应该支持此场景,接下来会深入讲解多主题在 WEB 场景中如何适配。

多主题技术方案探索

示例.gif

浏览器基于操作系统支持浅色深色两种风格,但是前端页面的多主题需要开发者具体开发才可以实现主题切换。

操作系统支持情况

做具体实现之前,了解下系统层次支持暗黑模式的支持情况:

  • macOS 10.14 引入了 darkmode
  • ios13 2019 年 3 月发布的 ios13 版本加入了 darkmode
  • Android 10 (API 级别 29) 及更高版本中提供深色主题背景
  • window10 2018.10.10

在现代化设备中,系统层都支持了深色和浅色主题,所以无论是新项目还是已经跑了多年的老项目都应该去做这件事。

自定义样式适配思路

页面基本是由自定义样式 + UI库来呈现整体视觉。 先从自定义样式说起:自定义样式使用CSS变量控制多主题(CSS变量基本使用在此不再过多赘述,入门请参考阮一峰老师写的CSS 变量教程

<!-- html 节点添加主题自定义属性 -->
<html data-theme="light">
  <!-- 使用CSS变量控制样式 -->
	<body style="background: var(--body-background)"></body>
</html>
// 跟主题无关的变量放到root里
:root {
    --border-radius-base: 6px;
}

// 跟主题相关变量,通过属性选择器提升优先级
html[data-theme='default']:root {
    --body-background: #efefef;
}

html[data-theme='dark']:root {
    --body-background: #000;
}

UI库适配思路

开源社区提供的UI库主题适配方案比较流行的就是通过CSS变量,但是由于CSS变量形成规范较晚,所以较老的项目是不支持的。Vue 的 Element UI 和 React 的 Ant Design 是社区发展较好的两个 UI 库,因为项目较老的原因都不支持CSS变量主题方案。

拿Ant Design来说,内部实现多主题是通过定义 less 变量,这样的做法是无法动态切换主题(下面会具体讨论动态切换主题)

🎉 Ant Design 目前正在着手将所有组件支持CSS变量,详细进展可以参考这里

但是我们不能等官方支持后在着手做适配工作,需求不等人,以 antd 为例,适配浅色和深色主题目前可以通过分别构建不同主题样式,通过添加属性选择器前缀来控制antd样式的优先级,以达到适配主题的目的。

<html data-theme="light"></html>
html[data-theme='light'] .ant-button {color: #fff}
html[data-theme='dark'] .ant-button {color: #000}

动态切换主题思路

页面切换主题具体需要从下面三个维度来考虑:

  • 系统主题更换
  • 页面提供主题切换按钮,用户主动切换
  • 通过URL控制当前主题

切换主题的核心思路是通过控制CSS变量,在不同主题下显示不同的样式。

⭐️ 系统主题切换


浏览器暴露主题切换接口:

CSS媒体查询 @media(prefers-color-scheme: dark)
JavaScriptwindow.matchMedia("(prefers-color-scheme: dark)")

api支持程度:

1641311929979-9c3f751a-0aab-4ed7-b72c-0dfaf7b88e31.png

⭐️⭐️ 通过CSS媒体查询控制CSS变量:

body {
  background: var(--body-background);
  transition: background 0.3s;
}

@media (prefers-color-scheme: light) {
  :root {
    --body-background: #efefef;
    --text-color: #333;
  }
}

@media (prefers-color-scheme: dark) {
  :root {
    --body-background: #000;
    --text-color: #ededed;
  }
}

优点是实现简单,识别交给浏览器去做,简单页面直接使用这个方案即可。

缺点是不利于扩展,后续支持用户主动切换主题比较乏力,所以我们下面按照属性选择器加CSS变量实现控制。

⭐️⭐️ 通过JS暴露接口:

通过JS识别当前系统主题,对于CSS变量的控制不应该使用JS去写,因为当主题色多的话,需要对每个属性都执行这行代码:

document.documentElement.style.setProperty('--theme-color', '#YOURCOLOR');

JS控制CSS变量不利于后续扩展。

较合适的方案是上面提到的:通过属性选择器控制根节点CSS变量

/* 浅色模式 */
html[data-theme="light"]:root {
  --body-background: #efefef;
  --text-color: #333;
}

/* 深色模式 */
html[data-theme="dark"]:root {
  --body-background: #000;
  --text-color: #ededed;
}

JS通过识别系统主题,设置页面主题标识

// 给HTML DOM节点添加自定义主题,标识当前主题
const toggleTheme = (isDarkMode) => {
  htmlEl.setAttribute("data-theme", isDarkMode ? "dark" : "light");
};

const themeMedia = window.matchMedia("(prefers-color-scheme: dark)");

// 页面初始化切换
toggleTheme(themeMedia.matches);

// 监听系统切换
themeMedia.addListener((e) => {
  toggleTheme(e.matches);
});

⭐️ 页面提供切换按钮,用户主动切换

需要在html节点添加自定义属性,并根据当前主题色通过CSS变量控制。

点击切换按钮后设置自定义属性值即可。

/* 浅色模式 */
html[data-theme="light"]:root {
  --body-background: #efefef;
}

/* 深色模式 */
html[data-theme="dark"]:root {
  --body-background: #000;
}

body {
  background: var(--body-background);
  transition: background 0.3s;
}
const htmlEl = document.documentElement;
const buttonEl = document.getElementById("btn");

buttonEl.addEventListener("click", () => {
  const currentTheme = htmlEl.getAttribute("data-theme");
  const nextTheme = currentTheme === "dark" ? "light" : "dark";

  htmlEl.setAttribute("data-theme", nextTheme);
});

⭐️ 通过URL控制当前主题

页面加载后根据URL query参数动态添加到html节点自定义属性,并根据当前主题色通过CSS变量控制。

const search = new URLSearchParams(location.search);
const theme = search.get("theme") || "light";

document.documentElement.setAttribute("data-theme", theme);

上面三种场景结合起来,基本满足目前多主题所有需求。

在线DEMO: codesandbox.io/s/multiple-…

多主题方案实现

如果项目中没有使用UI库或使用了支持CSS变量主题适配的UI库,可以本章内容,请直接参考上面技术方案探索。

根据上面「多主题技术方案探索」结论,我们从「UI库适配」和「自定义样式适配」来介绍下页面如何从0到1实现多主题适配。

先放一张最终实现效果图,源码已放到 github 仓库,有条件的看官可以边看源码,边阅读下面的示例代码

完整主题.gif

源码仓库:github.com/zj1024/mult…

在线演示:multiple-theme.vercel.app/

UI库主题适配实现

基于Ant Design具体讲UI库如何实现多主题方案。对于其他不支持的UI库(Element UI等等),实现的思路大致相同。

共分为三个步骤:

  1. 配置多主题样式
  2. 重写组件样式
  3. 构建样式文件

通过配置深色和浅色两套主题文件,分别对需要自定义样式的less变量赋值。最终构建两套主题样式,分别加上属性选择器前缀。

# 文件目录结构
.
├── src
│   └── theme
│       ├── antd.css # 构建产物
│       ├── antd.custom.variable.less # 自定义antd less变量
│       ├── antd.dark.base.less # 自定义 antd 深色主题
│       ├── antd.light.base.less # 自定义 antd 浅色主题
│       ├── antd.less # 构建入口文件
└── theme.sh

构建两套样式无疑增加了构建产物大小,最终构建两套样式文件大小约为1.4M,gzip后300K左右。

如果你的项目是刚启动并且比较小,可以使用支持CSS变量的UI库。

如果项目预期会很庞大,而且希望使用antd周边生态比较完善的库,那么我建议你做这件事情。

⭐️ 配置多主题样式

antd.custom.variable.less 自定义antd less变量文件

为什么要单独设置一份antd的less变量设置,由于上面提到过的:antd作为老项目基本都支持CSS变量,主题方案是通过less变量实现的,所以我们要自己设置不同主题的less变量。

@live-primary-color: #ff0040;

// default
@live-body-background: #efefef;
@live-text-color: #666;

// dark
@live-body-background-dark: #000;
@live-text-color-dark: #bfbfbf;

antd.dark.base.less 自定义 antd 深色主题

@import './antd.custom.variable.less';
@import '../../node_modules/antd/lib/style/themes/dark.less';
@import '../../node_modules/antd/dist/antd.less';

// 覆盖antd less变量值
@primary-color: @live-primary-color;
@body-background: @live-body-background-dark;

antd.light.base.less 自定义 antd 浅色主题

@import "./antd.custom.variable.less";
@import "../../node_modules/antd/lib/style/themes/default.less";
@import "../../node_modules/antd/dist/antd.less";

// 覆盖antd less变量值
@primary-color: @live-primary-color;
@body-background: @live-body-background;

构建:分别构建浅色和深色主题

# '编译暗黑主题样式'
npx lessc --js ./src/theme/antd.dark.base.less ./src/theme/antd.dark.base.css
# '编译白色主题样式'
npx lessc --js ./src/theme/antd.default.base.less ./src/theme/antd.default.base.css

构建产物:antd.dark.base.css antd.default.base.css

⭐️ 自定义UI库样式

variable.css 自定义CSS变量

此文件用于自定义样式和UI库样式的CSS变量合集

:root {
  --live-primary-color: #ff0040;
  --live-font-size-base: 14px;
  --live-font-size-lg: 16px;
  --live-font-size-sm: 12px;
}

html[data-theme="default"]:root {
  --live-body-background: #efefef;
}

html[data-theme="dark"]:root {
  --live-body-background: #000;
}

ant.less 自定义UI库样式文件

@import (less) "./variable.css";

html[data-theme="default"] {
  @import (less) "./antd.default.base.css";
}

html[data-theme="dark"] {
  @import (less) "./antd.dark.base.css";
}

html[data-theme="dark"],
html[data-theme="default"] {
  .ant-btn {
    font-size: var(--font-size-lg);

    &-lg {
      font-size: 18px;
    }

    &-sm {
      font-size: var(--font-size-base);
    }
  }
}

构建:给浅色和深色分别添加作用域,并加入自定义UI样式。

因为我们整体的切换主题要结合html标签的自定义属性 data-theme 的值为 lightdark 来实现,所以要结合构建的浅色和深色的的两份主题文件添加作用域。

npx lessc --js -clean-css ./src/theme/antd.less ./src/theme/antd.css

构建产物: antd.css ,在项目head标签中引入,就完成了UI库的主题适配。

自定义样式主题适配实现

因为「UI库主题适配实现」中,variable.css设置了CSS变量,所以自定义样式的节点上直接使用var(CSS变量)即可。

<div style="color: var(--text-color)"></div>

总结

本篇文章重点讲「不支多主题的UI库」适配,相信未来所有的UI库都会支持CSS变量实现多主题切换,目前antd正在做这件事,等完全支持后,多主题适配需要关心的是自定义UI库样式和自定义样式主题适配。

如果你有更好的主题适配方案,欢迎一起讨论。