好久好久之前写过一篇文章,介绍的是网站的深色模式。
当时想给网站做一个简单的深浅色模式切换功能,想法是:
- 白天(早上 6 点到傍晚 6 点)的时候用纯 CSS 的
@media (prefers-color-scheme: dark)
检测用户设备有没有开启深色模式。 - 而晚上(傍晚 6 点过后到第二天早上 6 点)的时候则加载另一套晚上用的 CSS 文件。
看起来很直观简单方便,也解决了长期晚上打开网页眼睛就被闪瞎的问题。一开始用的效果也还不错,但是一段时间之后就发现了两个很严重的问题:
- 用这个方法实现的深浅色模式切换功能需要长期需要维护两份 CSS 文件,一份白天一份晚上。这就意味着每次需要添加一点新的功能或者删掉一点过时的样式的话,要给两份 CSS 文件都同时加上。 CDN 那边的缓存也要刷新两份文件才能看到完整的效果。每过几个月还需要对比一下两份文件看看有没有写漏的样式。
- 晚上的深色模式不能切换回白天的浅色模式。如果晚上修改了样式就需要手动在控制台修改
<link>
的内容来重新加载回去白天的 CSS 文件。
于是这几天着手升级了一次深浅色模式切换功能。
Ver 0.1#
刚开始重写深浅色模式切换功能的时候重点着眼于 『晚上的深色模式不能切换回白天的浅色模式』 这个问题,所以想到用 localStorage
来储存当前的页面深浅色状态。 Light
,Dark
,System
分别对应浅色模式,深色模式,跟随系统这三个模式。第一次加载进来使用跟随系统模式,可以从跟随系统模式切换到深浅色模式。接着对应加载 light.css , dark.css ,style.css 三个文件就好了。
<link rel="stylesheet" id="linkCSS"/>
<script>
if (localStorage.getItem("Lighting")) {
if (localStorage.getItem("Lighting") === 'Light') document.getElementById('linkCSS').href = 'https://cdn.vinking.top/css/light.css';
else if(localStorage.getItem("Lighting") === 'System') document.getElementById('linkCSS').href = 'https://cdn.vinking.top/css/style.css';
else if(localStorage.getItem("Lighting") === 'Dark') document.getElementById('linkCSS').href = 'https://cdn.vinking.top/css/dark.css';
}else{
localStorage.setItem("Lighting", "System");
document.getElementById('linkCSS').href = 'https://cdn.vinking.top/css/style.css';
}
</script>
把上面这段代码放到 <head>
中,使其在页面渲染前执行,加载对应的 CSS 文件。切换按钮方面也同理,通过改变 localStorage
来实现模式相互切换,如果在跟随系统的模式下点击时,先用 window.matchMedia('(prefers-color-scheme: dark)').matches
检测设备是深色模式还是浅色模式再切换到相反的模式即可。
在本地测试的时候感觉效果还可以,但是放到服务器实际使用就发现问题了:
- 第一个依然是需要长期需要维护多份 CSS 文件。这次的方法甚至比一开始的方法多了一份 CSS 文件,导致后来添加按钮样式的时候痛苦 X3。
- 没有考虑到资源加载的时候 CSS 跟 js 优先级的问题。当资源加载的时候 CSS 是最高优先级(Highest priority),script 是高优先级(High priority),这就导致页面其他 CSS 资源已经加载完毕,开始执行
<script>
里面的代码再开始加载最关键的 CSS 文件。加上 CSS 资源的加载时间,页面就会出现一瞬间是没有加载到对应模式的 CSS 文件,非常影响观感。
修修改改就有了下一个版本。
Ver 0.6#
需要知道的是 light.css , dark.css ,style.css 这三个文件不同的地方就在于 :root
选择器对应变量的值不同,既然如此那就可以把三份文件的 :root
内容按照特定顺序放到同一份文件中,不同模式只需要在 <html>
加上对应的 class 来加载。根据 CSS 后边的样式会覆盖前面的样式这个原则,可以写出这个顺序。
:root{
...
}
@media (prefers-color-scheme:dark){:root{
...
}}
.Light{
...
}
.Dark{
...
}
第一次加载进来使用跟随系统模式时, <html>
不加任何 class ,这样可以同时使用 :root
选择器以及设备开启深色模式的时候使用 @media (prefers-color-scheme:dark)
里 :root
选择器的内容。当切换到深色模式或者浅色模式时则 class 添加 Dark
或者 Light
。这时后面的 .Dark
/.Light
样式就可以覆盖前面的 :root
样式。
同时, <head>
方面可以直接引用对应的 CSS 文件,不需要再用 js 判断。同时打开开发者工具的性能页可以看到:当执行 <script>
里面代码的时候 Timings 尚未到达首次绘制(FP)时间线,所以当页面首次绘制的时候会直接使用对应模式的样式而不会出现样式缺失问题。
<link rel="stylesheet" href='https://cdn.vinking.top/css/style.css'/>
<script>
if (localStorage.getItem("Lighting")) {
if (localStorage.getItem("Lighting") === 'Light') document.getElementsByTagName('html')[0].classList.add("Light");
else if(localStorage.getItem("Lighting") === 'Dark') document.getElementsByTagName('html')[0].classList.add("Dark");
}else{
localStorage.setItem("Lighting", "System");
}
</script>
Ver 1.0#
实际使用时存在点击切换模式的按钮后,背景加载未完成导致一段时间的背景缺失问题。这个问题可以使用预加载的方式解决,通过给按钮一个 onmouseover
事件,当鼠标移入的时候添加 <link rel="prefetch" href="">
以实现资源的预加载。最后记得加上 XXX.onmouseover = null
来避免重复触发。
至此,深浅色模式切换功能的主要框架已经完成,剩下就只有切换时加载新代码高亮之类修修补补的小问题。
Ps. 小小吐槽一下,怎么现在 Pixiv 好看的横屏图片越来越难看见了,推送全是竖屏的图片....
此文由 Mix Space 同步更新至 xLog
原始链接为 https://www.vinking.top/posts/codes/dark-mode-css-implementation