0
0

你好黑暗,我的老朋友 —— 为网站添加用户友好的深色模式支持

skk 发表于 2020年05月26日 14:05 | Hits: 519
Tag: 技术向 | JavaScript | CSS | Media Query | 深色模式

随着越来越多的操作系统、浏览器开始支持 Dark Mode、支持相应的 Media Queries Level 5,越来越多的网站开始添加深色模式。当然我的博客也不甘落下,趁着新的主题完工不到一个月、还看得懂自己写的代码,也加上了深色模式。

什么是「深色模式」

很多操作系统在日落后会自动切换到「深色模式」、并不意味着「深色模式」就是「夜间模式」。「夜间模式」用于夜晚的弱光环境,主要目的是保护眼睛、减少强光刺激、避免影响睡眠,不难理解为什么 macOS 的 Night Shift 会自动调节屏幕色温、Android(AOSP)到了夜间可以选择启用系统级「琥珀色」滤镜。

macos-settings

「深色模式」更像是一个主题,即使在白天也可以使用。不论是为了在 OLED 屏幕上省电、亦或是减少白光刺激护眼、亦或是暗色模式对色盲用户更加友好,总之 macOS 率先提出了系统级的「暗色模式」、并在 WebKit 中增加了对应的 Media Query,而后 Chromium、Firefox 先后跟进,如今兼容prefers-color-scheme的浏览器占有率已经高达 81.82%。

利用 Media Query 简单实现深色模式

CSS 媒体查询@media是一个足够强大的特性,可以有条件地将样式应用于文档和各种上下文中。Media Queries Level 5 草案中提出了深色模式的判断方式prefers-color-scheme,包含light、dark、no-preference三种值。而不支持 Media Queries 5 的浏览器会直接无视 CSS 中的prefers-color-schemeMedia Query,无需额外的代码即可优雅降级。

还记得我刚刚说过「深色模式更像一个主题」么?为网站新增深色模式就如同换肤功能;搭配prefers-color-scheme,编写深色模式的思路就如同编写响应式一般、无需赘述,结合几段 Code Snippet 一笔带过:

CSS Variable 的方法实现深色模式

:root {  --text: #333;}@media (prefers-color-scheme: dark) {  :root {    --color-text: #fff;  }}body {  color: var(--color-text);}

通过维护两套 CSS Variable,可以快速切换不同的配色方案。这种方法特点是所需代码较少,缺点是 CSS Variable 的兼容性较差,可能还需要引入额外的 Polyfill。

为深色模式单独编写样式

body {  color: #333;}@media (prefers-color-scheme: dark) {  body {    color: #fff;  }}

直接维护两套样式的方法清晰直观、任何网站都可以基于这种方法进行改造。但会造成冗余代码、较难实现统一的风格、后期不易维护。

条件性加载深色模式的 CSS 文件

/* main.css */body {  color: #333;}/* dark.css */body {  color: #fff;}
<link rel="stylesheet" href="main.css"><link rel="stylesheet" href="dark.css" media="(prefers-color-scheme: dark)">

利用<link>标签的 Media Query,甚至可以单独加载暗色模式的 CSS 文件。

需要注意 CSS 选择器的权重,因此作为可选的dark.css一定要放在main.css之后加载。

除了上述三种方式以外,使用 CSSfilter或mix-blend-mode还可以实现对网站整体色调的改变,可以确保配色风格的统一性。

「深色模式」的兼容性

虽然有了优雅的prefers-color-scheme可以识别操作系统的显示模式,但是对于用户来说,仅依赖 Media Query 的「深色模式」并不能带来很好的体验。
首先是浏览器兼容性。虽然支持该特性的浏览器的市场占有率非常喜人,但是从版本号上来看却并不乐观:

考虑到使用 Chormium 70 内核甚至 Tencent X5 内核的国产浏览器,大部分用户并没有机会体验到深色模式。除此以外,操作系统级别的「深色模式」实现也会受到 OEM 厂商的影响 —— 虽然 Android 10(AOSP)提供「深色模式」,但是一加的 OxygenOS 却将其深藏在系统主题设置里,没有自动切换、在 Quick Settings 里也没有快速的切换开关。

设计一个用户友好的「深色模式」

受限于兼容性和复杂的操作系统,大部分网站依然在使用更传统的「开关」切换 —— 通过 toggle<html>或
<body>的 class 属性实现在两套样式之间切换、并将开关的状态记忆在 localStorage 中的方法虽然有效,却是无奈之举,手动切换开关相比prefers-color-scheme也不够优雅。如果将「开关」和prefers-color-scheme结合起来,就可以带来更好的用户体验:

  • 对于不兼容的浏览器或操作系统,访客依然可以通过开关手动切换显示模式
  • 对于兼容的浏览器或操作系统,Media Query 能够实现在两种显示模式之间切换
  • 在兼容的浏览器或操作系统上,用户还可以通过开关 override 当前的显示模式

在将两者组合在一起时,不能简单地用「开关」覆盖prefers-color-scheme,否则用户触发开关、状态被永久记忆在 localStorage 之后,就变成了僵硬的手动模式。
举个例子。访客可能在操作系统还没有自动切换到「深色模式」时通过网站上的开关切换显示模式,经过一个夜晚后到了次日白天、访客再度访问网站时,自然希望不需要再切换开关、网站就能以常规的浅色模式显示。因此设计思路是当prefers-color-scheme的值发生改变(从 与用户需要的显示模式不同 变成 相同)时清空 localStorage 中储存的开关状态,此时显示模式切换回基于 Media Query 的「自动」模式。

Talk is cheap, here goes the code.

首先是 CSS:

:root {  --color-mode: 'light';  --text: #333;}@media (prefers-color-scheme: dark) {  :root {    --color-mode: 'dark';  }  :root:not([data-user-color-scheme]) {    --text: #eff;  }}[data-user-color-scheme='dark'] {  --text: #eff;}body {  color: var(--color-text);}

真是令人看的头大,让我们逐行来看都是些什么:

  • 在:root下定义了一个 CSS Variable--color-mode: light和在浅色模式下用到的 CSS Variable(比如使用深色#333作为主要字体颜色)。
  • 使用prefers-color-scheme的 Media Query 定义深色模式下的 CSS Variable:--color-mode: light。深色模式的样式(如浅色#eff作为主要字体颜色)要定义在:not([data-user-color-scheme])伪类下以避免「开关」的行为覆盖浏览器的样式。
  • 为[data-user-color-scheme='dark']再定义一遍深色模式下用到的样式。

有了这段 CSS,不难理解深色模式何时会生效:当操作系统使用「深色模式」且<html>或<body>标签上没有data-user-color-scheme属性时、或者存在data-user-color-scheme属性且值为dark时。

然后是困难的部分了:编写 JavaScript 为「开关」添加行为。

先定义一些常量:

const rootElement = document.documentElement; // <html>const darkModeStorageKey = 'user-color-scheme'; // 作为 localStorage 的 keyconst darkModeMediaQueryKey = '--color-mode';const rootElementDarkModeAttributeName = 'data-user-color-scheme';const darkModeTogglebuttonElement = document.getElementById(/* element id */);

接下来,用try {} catch (e) {}封装一下 localStorage 的操作,以应对 HTML5 Storage 被禁用、localStorage 被写满、localStorage 实现不完整的情况:

const setLS = (k, v) => {  try {    localStorage.setItem(k, v);  } catch (e) { }}const removeLS = (k) => {  try {    localStorage.removeItem(k);  } catch (e) { }}const getLS = (k) => {  try {    return localStorage.getItem(k);  } catch (e) {    return null // 与 localStorage 中没有找到对应 key 的行为一致  }}

我们还需要一个函数读取当前prefers-color-scheme的方法。由于已经在 CSS 中定义了--color-mode,所以在 JS 中直接读取就好了:

const getModeFromCSSMediaQuery = () => {  const res = getComputedStyle(rootElement).getPropertyValue(darkModeMediaQueryKey);  if (res.length) return res.replace(/\"/g, '').trim();  return res === 'dark' ? 'dark' : 'light';    // 使用 matchMedia API 的写法会优雅的多  // return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'}

还记得我们需要自动取消手动模式回到prefers-color-scheme么?意味着我们需要一个函数清掉 LS、删掉<html>存在的data-user-color-scheme属性:

const resetRootDarkModeAttributeAndLS = () => {  rootElement.removeAttribute(rootElementDarkModeAttributeName);  removeLS(darkModeStorageKey);}

接下来是起主要作用的函数了,负责为<html>标签修改data-user-color-scheme属性:

const validColorModeKeys = {  'dark': true,  'light': true}const applyCustomDarkModeSettings = (mode) => {  // 接受从「开关」处传来的模式,或者从 localStorage 读取  const currentSetting = mode || getLS(darkModeStorageKey);  if (currentSetting === getModeFromCSSMediaQuery()) {    // 当用户自定义的显示模式和 prefers-color-scheme 相同时重置、恢复到自动模式    resetRootDarkModeAttributeAndLS();  } else if (validColorModeKeys[currentSetting]) { // 相比 Array#indexOf,这种写法 Uglify 后字节数更少    rootElement.setAttribute(rootElementDarkModeAttributeName, currentSetting);  } else {    // 首次访问或从未使用过开关、localStorage 中没有存储的值,currentSetting 是 null    // 或者 localStorage 被篡改,currentSetting 不是合法值    resetRootDarkModeAttributeAndLS();  }}

当然,「开关」还需要一个函数,这个函数负责获取相反的显示模式,同时还要将新的模式写入 localStorage 存储起来:

const invertDarkModeObj = {  'dark': 'light',  'light': 'dark'}const toggleCustomDarkMode = () => {  let currentSetting = getLS(darkModeStorageKey);    if (validColorModeKeys[currentSetting]) {    // 从 localStorage 中读取模式,并取相反的模式    currentSetting = invertDarkModeObj[currentSetting];  } else if (currentSetting === null) {    // localStorage 中没有相关值,或者 localStorage 抛了 Error    // 从 CSS 中读取当前 prefers-color-scheme 并取相反的模式    currentSetting = invertDarkModeObj[getModeFromCSSMediaQuery()];  } else {    // 不知道出了什么幺蛾子,比如 localStorage 被篡改成非法值    return; // 直接 return;  }  // 将相反的模式写入 localStorage  setLS(darkModeStorageKey, currentSetting);  return currentSetting;}

相关的函数都定义完了,是时候添加函数执行了:

// 当页面加载时,将显示模式设置为 localStorage 中自定义的值(如果有的话)applyCustomDarkModeSettings();darkModeTogglebuttonElement.addEventListener('click', () => {  // 当用户点击「开关」时,获得新的显示模式、写入 localStorage、并在页面上生效  applyCustomDarkModeSettings(toggleCustomDarkMode());})

我的博客也使用的这种实现,通过 Navbar 中的按钮体验一下吧!

你好黑暗,我的老朋友 —— 为网站添加用户友好的深色模式支持
本文作者
Sukka
发布于
2020-05-25
更新于
2020-05-26
许可协议
转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!

原文链接: https://blog.skk.moe/post/hello-darkmode-my-old-friend/

0     0

我要给这篇文章打分:

可以不填写评论, 而只是打分. 如果发表评论, 你可以给的分值是-5到+5, 否则, 你只能评-1, +1两种分数. 你的评论可能需要审核.

评价列表(0)