banner
Vinking

Vinking

你写下的每一个BUG 都是人类反抗被人工智能统治的一颗子弹

升級!重構深淺色模式切換功能

好久好久之前寫過一篇文章,介紹的是網站的深色模式。

當時想給網站做一個簡單的深淺色模式切換功能,想法是:

  • 白天(早上 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

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。