仅需几行代码,为网站添加黑暗模式

March 08, 2020

前言

在上一年,黑暗模式的概念席卷而来,随着系统级别的支持,其他主流应用程序的适配也陆续展开,它们大多提供了相应的入口,让用户可以切换整个主题,以此获得最舒适的体验。

为什么要使用黑暗模式?

这个问题,我刚开始也不明白,黑乎乎的界面不仅难辨识,而且还加重开发和 UI 的负担,但回过头想一想,为什么 macOS,iOS 还引入了黑暗模式,Chrome、Gmail 等主流应用还提供支持黑暗模式的特性? 其背后还是有着一定的思考空间:

  • 护眼!没错,就是传说中的护眼。在夜晚,能够让用户的眼睛较为舒适,提高可视性
  • 可大幅减少耗电量(具体取决于设备的屏幕技术)
  • 一些特殊场景下,提高用户的体验度(小说阅读,视频观看)

通过 CSS 来支持黑暗模式

如果仅仅想针对黑暗模式,来更改网站的配色,那么 CSS 是一个不错的方法,前提是 系统开启了黑暗模式

在 CSS 文件中,写入以下媒体查询代码:

css
@media (prefers-color-scheme: dark) {
/* 黑暗模式下的样式代码 */
}
css
@media (prefers-color-scheme: dark) {
/* 黑暗模式下的样式代码 */
}

当系统开启黑暗模式后,媒体查询内的样式就会默认生效。

PC 端实践

创建一个 HTML 文件,代码如下:

html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<style>
@media (prefers-color-scheme: dark) {
p {
color: red;
}
}
</style>
<body>
<p>我在黑暗模式下会变红色</p>
</body>
</html>
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<style>
@media (prefers-color-scheme: dark) {
p {
color: red;
}
}
</style>
<body>
<p>我在黑暗模式下会变红色</p>
</body>
</html>

打开浏览器,显示字是黑色的,没什么问题,接下来让我们对系统开启黑暗模式。

这里以 windows 10 为例,我们只需在 开始-设置-个性化-颜色-选择默认应用模式(暗)

再次打开浏览器,我们的样式已经成功生效!

苹果电脑用户,从 macOS Mojava 版本起,也可开启黑暗模式。

移动端实践

我们以 iPhone 为例,代码不变,只需在 设置-显示与亮度 开启黑暗模式即可,效果如图:

不足之处

根据 CanIUse.com(可以查询各浏览器的 CSS 属性支持情况)的数据显示:市面上的浏览器对该特性的支持率是 80%,IE 和 非 Chromium 内核版本的 Edge 不支持。所以如果你的网站面向 C 端,用户的浏览器各式各样,一定注意要做兼容处理。

并且用户无法在浏览器上主动地切换模式,只能被动依赖于系统的主题模式。

使用 JavaScript 切换样式表

使用 JavaScript 切换样式表来实现黑暗模式,我们需要创建两个不同的样式表,对应不同的主题(light and dark)。

第一步,在 head 标签中插入默认样式表:

html
<head>
...
<link id="theme" rel="stylesheet" type="text/css" href="light-theme.css" />
</head>
html
<head>
...
<link id="theme" rel="stylesheet" type="text/css" href="light-theme.css" />
</head>

然后,创建一个按钮来切换样式表,为了能让用户快速地找到,应尽可能置于网页的头部位置。

html
<button id="theme-toggle">Switch to dark mode</button>
html
<button id="theme-toggle">Switch to dark mode</button>

继续添加以下 JavaScript 代码段:

js
// 当 DOM 加载完成后触发回调函数
document.addEventListener("DOMContentLoaded", () => {
const themeStylesheet = document.getElementById("theme");
const themeToggle = document.getElementById("theme-toggle");
themeToggle.addEventListener("click", () => {
// if it's light -> go dark
if (themeStylesheet.href.includes("light")) {
themeStylesheet.href = "dark-theme.css";
themeToggle.innerText = "Switch to light mode";
} else {
// if it's dark -> go light
themeStylesheet.href = "light-theme.css";
themeToggle.innerText = "Switch to dark mode";
}
});
});
js
// 当 DOM 加载完成后触发回调函数
document.addEventListener("DOMContentLoaded", () => {
const themeStylesheet = document.getElementById("theme");
const themeToggle = document.getElementById("theme-toggle");
themeToggle.addEventListener("click", () => {
// if it's light -> go dark
if (themeStylesheet.href.includes("light")) {
themeStylesheet.href = "dark-theme.css";
themeToggle.innerText = "Switch to light mode";
} else {
// if it's dark -> go light
themeStylesheet.href = "light-theme.css";
themeToggle.innerText = "Switch to dark mode";
}
});
});

大家可以自行实践,这里就不展示了,那有没有优化的空间呢?

有!试想当用户切换到黑暗模式后,随后关闭了网站,当他们第二次访问时,又变成了默认主题,需要再次手动切换。不过,我们可以用 LocalStorage 来快速解决上述问题。

在 localStorage 中保存用户的选择

LocalStorage 能存储键值对,如下所示:

js
localStorage.setItem("theme", "dark-theme.css");
js
localStorage.setItem("theme", "dark-theme.css");

让我们对之前的代码做出一些优化:

js
// 当 DOM 加载完成后触发回调函数
document.addEventListener("DOMContentLoaded", () => {
const themeStylesheet = document.getElementById("theme");
const storedTheme = localStorage.getItem("theme");
if (storedTheme) {
themeStylesheet.href = storedTheme;
}
const themeToggle = document.getElementById("theme-toggle");
themeToggle.addEventListener("click", () => {
// if it's light -> go dark
if (themeStylesheet.href.includes("light")) {
themeStylesheet.href = "dark-theme.css";
themeToggle.innerText = "Switch to light mode";
} else {
// if it's dark -> go light
themeStylesheet.href = "light-theme.css";
themeToggle.innerText = "Switch to dark mode";
}
// 保存用户选择的主题
localStorage.setItem("theme", themeStylesheet.href);
});
});
js
// 当 DOM 加载完成后触发回调函数
document.addEventListener("DOMContentLoaded", () => {
const themeStylesheet = document.getElementById("theme");
const storedTheme = localStorage.getItem("theme");
if (storedTheme) {
themeStylesheet.href = storedTheme;
}
const themeToggle = document.getElementById("theme-toggle");
themeToggle.addEventListener("click", () => {
// if it's light -> go dark
if (themeStylesheet.href.includes("light")) {
themeStylesheet.href = "dark-theme.css";
themeToggle.innerText = "Switch to light mode";
} else {
// if it's dark -> go light
themeStylesheet.href = "light-theme.css";
themeToggle.innerText = "Switch to dark mode";
}
// 保存用户选择的主题
localStorage.setItem("theme", themeStylesheet.href);
});
});

这里不使用 sessionStorage 是因为 sessionStorage 的生命周期只存在于该域名下的标签页中,当标签页或浏览器关闭时,sessionStorage 会被清空,而 localStorage 不会,除非用户主动删除。

题外话,大家知道 localStorage 的最大容量是多少吗?当超出最大容量,又会发生什么?有什么解决方案吗?

需要注意的是,localStorage 严格遵守 同源策略,你在通过 HTTP 访问站点时保存的主题,将会在通过 HTTPS 访问站点时消失。

使用 JavaScript 切换类名

如果,我们只想用一个样式表,同样可以做到黑暗模式的切换。

添加以下 JavaScript 代码段:

js
button.addEventListener("click", () => {
document.body.classList.toggle("dark");
localStorage.setItem(
"theme",
document.body.classList.contains("dark") ? "dark" : "light"
);
});
if (localStorage.getItem("theme") === "dark") {
document.body.classList.add("dark");
}
js
button.addEventListener("click", () => {
document.body.classList.toggle("dark");
localStorage.setItem(
"theme",
document.body.classList.contains("dark") ? "dark" : "light"
);
});
if (localStorage.getItem("theme") === "dark") {
document.body.classList.add("dark");
}

CSS 文件,如下:

css
/* Light mode */
body {
background: #fff;
color: #000;
}
/* Dark mode */
body.dark {
background: #000;
color: #fff;
}
css
/* Light mode */
body {
background: #fff;
color: #000;
}
/* Dark mode */
body.dark {
background: #000;
color: #fff;
}

我们知道,CSS 样式的优先级取决于其选择器的权重叠加,哪个权重较大,就展示相应的样式:

(body = 1) < (body.dark = 1 + 10 = 11)

!important > 行内样式 > ID 选择器 > Class、伪类、属性选择器 > 元素、伪元素选择器

参考资料:


B2D1 (包邦东)

Written by B2D1 (包邦东)