LOADING

加载过慢请开启缓存 浏览器默认开启

给主题添加黑暗模式,以及白屏和首屏问题

2023/9/7 hexo

之前学习了一下TailwindCSS,在它的黑暗模式页里详细地列出了黑暗模式的实现方式,深以为然,于是也想给自己的博客所使用的主题搞一个。

思路

首先一般有个按钮可以切换当前的主题,主题有三个状态:黑暗、浅色、跟随系统。

如何检测系统的主题?使用这个方法:

window.matchMedia('(prefers-color-scheme: dark)').matches)

返回值为true即为黑暗模式,false即为浅色模式。

然后就是如何读取主题了,这里我使用的是localStorage,在localStorage里存储一个theme的值,然后在页面加载时检测这个值,如果有值就使用这个值,如果没有就使用系统的主题。

如何切换主题?这里我使用的是document.documentElement.classList,在<html>标签上添加dark类。

有两种方法可以区分颜色:

  1. :root:root.dark里定义颜色变量,然后在CSS里使用var(--<variable-name>)来使用变量。
  2. 或者在CSS里使用<selector>.dark来选择黑暗模式下的样式。

推荐使用第一种方法,因为第二种方法会导致CSS文件变得很大,而且不利于维护。

主要部分代码实现

在主题的main.js中:

初始时,检测localStorage里是否有theme的值,如果有就使用这个值,如果没有就把theme设置为auto

data(){
  return {
    theme: localStorage.getItem("theme") || "auto",
  }
}

在页面加载时,检测theme,如果为auto则检测系统的主题并设置颜色,不是则直接设置颜色。
beforeunload事件里,如果themeauto则移除localStorage里的theme,否则就设置localStorage里的theme为当前的theme

created() {
  if (this.theme === 'auto')
    this.isSystemDarkMode() ? this.setDarkMode(true) : this.setDarkMode(false);
  else
    this.theme === "dark" ? this.setDarkMode(true) : this.setDarkMode(false);
  window.addEventListener("beforeunload", () => {
    if (this.theme === "auto")
      localStorage.removeItem("theme");
    else
      localStorage.setItem("theme", this.theme)
  });
},

判断系统是否为黑暗模式、设置颜色、切换主题的方法:

methods: {
        // 判断系统是否为黑暗模式
        isSystemDarkMode() {
            return window.matchMedia("(prefers-color-scheme: dark)").matches;
        },
        /**
         * @param {boolean} dark 
         */
        setDarkMode(dark) {
            if (dark) {
                document.documentElement.classList.add("dark");
                document
                .getElementById("highlight-style-dark")
                .removeAttribute("disabled");
            } else {
                document.documentElement.classList.remove("dark");
                document
                .getElementById("highlight-style-dark")
                .setAttribute("disabled", "");
            }
        },
        // 点击按钮切换主题
        handleThemeSwitch() {
            this.theme = ((theme) => {
                switch (theme) {
                case "auto": // auto -> light
                    this.setDarkMode(false);
                    return "light";
                case "light": // light -> dark
                    this.setDarkMode(true)
                    return "dark";
                case "dark": // dark -> auto
                    this.isSystemDarkMode() ? this.setDarkMode(true) : this.setDarkMode(false);
                    return "auto";
            }})(this.theme)
        },
    },

适配第三方组件

页面上有一些引入的第三方组件,比如说评论组件,HighLightJS这些组件的样式是在组件内部写死的,所以我们需要在组件加载时检测主题并设置颜色。

Waline

Waline是一个基于Vercel Serverless的评论系统,它的文档里有提到如何适配黑暗模式。我们是在html下加上dark类,所以只需要在配置里写上我们的适配方法即可:

// comments.ejs
Waline.init({
  //.....
  dark: "html.dark",
})

这样Waline就会检测html标签是否有dark类,如果有就使用黑暗模式,没有就使用浅色模式。就适配完成了。

HighLightJS

HighLightJS的样式文件本身就是通过<link>引入的,官网上也提供了许多不同的主题,我们可以导入浅色和深色两套主题,在浅色时disabled深色那套,深色时取消disabled即可。

# _config.yml
# 给出两套主题
highlight:
    enable: true
    style: github
    styleDark: github-dark

然后引入两套主题:(这里是import.ejs)

<% if (theme.highlight.enable) { %>
<script src="https://cdn.staticfile.org/highlight.js/11.8.0/highlight.min.js"></script>
<script src="https://cdn.staticfile.org/highlightjs-line-numbers.js/2.8.0/highlightjs-line-numbers.min.js"></script>
<link
    rel="stylesheet"
    href="https://cdn.staticfile.org/highlight.js/11.8.0/styles/<%- theme.highlight.style %>.min.css"
/>
<link 
    rel="stylesheet"
    id="highlight-style-dark"
    disabled
    href="https://cdn.staticfile.org/highlight.js/11.8.0/styles/<%- theme.highlight.styleDark %>.min.css"
/>
<script src="<%- url_for("/js/lib/highlight.js") %>"></script>
<% } %>

然后在切换颜色时切换disabled属性即可:

// main.js
setDarkMode(dark) {
    if (dark) {
        document.documentElement.classList.add("dark");
        document
        .getElementById("highlight-style-dark")
        .removeAttribute("disabled");
    } else {
        document.documentElement.classList.remove("dark");
        document
        .getElementById("highlight-style-dark")
        .setAttribute("disabled", "");
    }
},

白屏闪屏问题

我在自己的本地测试完成后,满心欢喜地推送到了GitHub,然后打开线上的博客,发现了一个问题:点击刷新后,背景变成白色立马变成黑色,几次都是如此,十分影响观感。

观察页面源代码可知,负责切换主题的逻辑main.js是在body中,在本地调试时,请求非常迅速,main.js能够立即被请求到并执行,而线上有延迟,加载完head后可能要过好一会才能拿到main.js,再执行,所以会出现先白色背景后闪回深色的问题。

解决方式就是把这一块单独放进head里执行。
(在layout.ejs中,略去了其它部分:)

<head>
  <script>
    if (localStorage.getItem('theme') === "dark" ||
      (!("theme" in localStorage) &&
        window.matchMedia("(prefers-color-scheme: dark)").matches)
    ) {
        document.documentElement.classList.add("dark");
    }
  </script>
</head>

这样就可以保证页面出现时就已经根据localStorage里的值把颜色设置好了。