给你的主题来点暗色!

我自己对暗色模式其实是没什么兴趣的,因为设计一种配色就已经让我绞尽脑汁了,还要我设计另一种。但是我也确实意识到暗色模式在晚上玩手机实在是很方便,而且做这个也很流行,于是我也做了一个,只是因为我能做到。

跟着系统变色就行了吗?

现在的系统大概都支持暗色模式(Linux 的桌面环境早就有这种设置了,Firefox 可以直接读取我的系统设置,Android/iOS 也都有暗色模式开关),浏览器也紧跟潮流提供了 @media查询属性(IE:那……是……什……么……)。理论上来说只要简单地在 CSS 里面查询然后编写修改颜色的代码就可以了,问题只是如何修改颜色比较轻松。

CSS 变量是好东西!

使用 CSS 变量 当然是最简单的解决方案了,就像我们平时编程一样使用变量作为 colorbackground 的值,然后在查询到暗色模式的代码块里给这些变量重新赋值,一切都十分简单有条理,而且最重要的是你不需要一个一个选择器查找有哪些需要变色的属性,所有的颜色变量都是放在一起的。

真是恨死 IE 这废物了!

虽然 IE 有很多不支持的选项,但是不支持 CSS 变量真是让我工作量剧增的一件事情。虽然我从不测试我的网站在 IE 上能否运行,但是在最新版 IE 上一般都是没问题的,因此我也会放弃一些最新版 IE 不支持的新特性,CSS 变量就是其中之一。

既然没有办法用 CSS 变量,那就只能自己一个一个找选择器下面和颜色相关的属性,然后给它们重新设置属性了,真是找的人头晕眼花啊。

可能有人会说你不是用 CSS 预处理器吗,预处理器不是也有变量吗?但是预处理器是在生成阶段把变量编译掉了啊!不方便到运行时(浏览器)里去替换变量。

我就是想在暗色浏览器里用亮色啊!

在全部重新调整过颜色并能看之后(其实就是把浅色的色块换成深色,颜色层次基本不变,背景图搞个反色,至于那些彩色的按钮标签我实在没精力重新配色了,把透明度调低一点就好了),我自己还是比较喜欢自己一开始设计的样子,但我又是个习惯电脑全局暗色的人,这怎么能忍!

CSS 的媒体属性不像一般的属性,只能是浏览器设置我们读取,没有办法用 JS 控制,于是也就没法简单地利用这个添加切换按钮。上网搜了半天也只有曲线救国的方案。

曲线救国

如果并不是想那么和系统的设置同步而只是给自己的网站添加切换的话,并不需要媒体查询。只要设计一个按钮给 <html><body> 添加删除 class/attribute 就行了。然后如果要和系统同步,在 JavaScript 里也有 相关的 API 可以做到查询和监听,在检测到变化的时候也修改 class/attribute 即可。

我选择的是给 <html>data-theme="dark"data-theme="light" 属性,不选 <body> 是因为 WebKit 那些该死的不遵循标准的 scrollbar 伪类,文档没有说他们到底依附哪个元素,我尝试得到的是 <html>。接下来只要把之前的 CSS 里面的媒体查询选择器改成 html[data-theme="dark"] 就行了。

不过还要注意继承关系,这样写的话有些属性并不是继承外面的,而是在这个选择器里面就近继承。比如假如修改了 html[data-theme="dark"] a 的边框,那 html[data-theme="dark"] a.cls 的边框会优先继承这个,而不是 a.cls。我知道有些人可能会笑我半懂不懂了,但是我确实遇到了这个问题,并且思考了一下找到了原因。

还有一个比较尴尬的事情,我给一些元素设置了 transition 用于 :hover 之后加一个渐变颜色的效果,现在暗色模式也是修改颜色,导致这些元素会比其他元素慢变一下,没什么好办法因为你分不开两种 color 变化。我的解决方案是一个一个找切换暗色模式时候会变属性的选择器,给它们也添加 transition。效果还不错,不过 Chrome 在处理这种 CSS 动画时候竟然会掉帧???

不管了,反正我用 Firefox,Firefox 效果好得很,完全不掉帧。

更新(2020-08-07 10:50:00):我怀疑 Chrome 想做新时代的 IE,其实并不是性能问题导致掉帧,WebKit 对于继承来的属性的 transition 存在问题,会导致不是同时变换而是有延迟的变换,效果糟透了,StackOverflow 上也有人遇到这个问题,看起来是 bug 并且不打算解决,而 Firefox 就没有这个问题。使用 CSS 变量在 WebKit 下效果会好一点,不过也不能给所有变色的元素加 transition,还是会卡,只能给 <body> 加一个,因为我的链接有 :hover 时变色的 transition,都是 color 就没办法在那时 transition 而暗色模式时候不 transition。总之由于 WebKit 的存在导致没法让全部元素同步 transition,只能近似。两权相害取其轻,还是让 IE 用户只能用亮色吧,我最后还是选择了 CSS 变量。

怎么你这破网站换个页面还要重新点一次?

一切看起来都十分美好,直到我把一个暗色页面切成亮色然后点了个链接,下一个页面并不会理会我们上一个页面设置了什么主题,又变成了暗色。每个页面点一下切换按钮也太烦人了,我们得来点持久化。

某个域名想在用户的浏览器里存点是完全可行的,使用 localStorage 就行了,就是简单的键值对。但是这样我们就有了多种可能切换亮暗的动作:localStorage 里面存的选项,网页加载时浏览器媒体查询的结果,用户点了网页上的切换按钮,用户点了系统切换亮暗的设置。这些的判断顺序要好好处理一下,不然某些就会被忽视掉变成“我点了怎么不动啊!!!”。

经过我考虑一下之后,这个玩意的逻辑应该是这样的:

  1. 假如 localStorage 里面没有值,用户是首次打开网站,此时读取媒体查询按照系统的主题设置。
  2. 否则说明用户之前打开过网站,已经有他自己的喜好了,按照 localStorage 里面的值设置。
  3. 上两个步骤结束之后注册一个媒体查询监听器,用于响应用户修改系统设置。
  4. 注册一个按钮监听器,用于响应用户点击网页切换按钮设置。

以上 1 和 2 的顺序不能反了,并且每一个设置动作里都要把这次设置的值写入 localStorage 用于后续加载用户的选择。

现在看起来一切都很满意,是时候发布了!

用户:啊!我的眼睛!

我躺在床上用手机测试的时候又发现了一个问题。因为网页在生成的时候总有一个初始状态(JS 要等到 document 加载完成才开始处理 DOM,不像那些单页应用),假如我设置成暗色然后切换到别的页面,网页就会以亮色加载然后变成暗色,用户在晚上看起来就像是个闪光弹(伏拉什棒!)。

其实没什么好的解决办法,因为这是传统 HTML 页面的限制之一,我也不想找有没有什么新的东西能解决这些问题,最后的方案其实相当简单,既然亮色到暗色会让人受不了,我搞成暗色到亮色不就行了。

于是就是把模板里按钮的初始状态修改一下,渲染的时候出来的是 data-theme="dark",假如用户选择亮色,加载页面时会有一个暗色到亮色的变化。反正我自己都不在意。

更新(2020-08-06 18:23:00):我后来阅读了一些其它主题的代码,看它们是怎么在不使用单页应用的前提下解决这个问题的,结果方法相当简单,我自己也想得到:不用等到 DOMContentLoaded 事件之后,反正只是修改 <html> 标签,直接在 <script> 标签里面编辑 document.documentElement 是可以的,因为反正加载到 <script> 的时候肯定也已经加载到 <html> 了。所以就把修改这个属性和修改按钮的 DOM 分开了两部分,并且添加了一个 storage 的监听器,这样假如打开了多个页面,一个页面切换其它页面也会跟随切换。

不要过度设计!

经过这么一大堆折腾(代码行数++++++++),我甚至在想要不要支持每个页面单独设置亮暗初值,反正只要添加一个 front matter 然后在模板里判断一下嘛。不过后来想了想,就算有这么个功能又有什么用?实际意义几乎为零,徒增复杂度,所以还是不要过度设计了。

Alynx Zhou

A Coder & Dreamer

既然看了喵写的文章,不打算投喂一下再走吗?哼!