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

2023 年 9 月 7 日 星期四(已编辑)
/
12
这篇文章上次修改于 2023 年 10 月 30 日 星期一,可能部分内容已经不适用,如有疑问可询问作者。

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

之前学习了一下 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里的值把颜色设置好了。

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...