之前学习了一下TailwindCSS,在它的黑暗模式页里详细地列出了黑暗模式的实现方式,深以为然,于是也想给自己的博客所使用的主题搞一个。
思路
首先一般有个按钮可以切换当前的主题,主题有三个状态:黑暗、浅色、跟随系统。
如何检测系统的主题?使用这个方法:
window.matchMedia('(prefers-color-scheme: dark)').matches)返回值为true即为黑暗模式,false即为浅色模式。
然后就是如何读取主题了,这里我使用的是localStorage,在localStorage里存储一个theme的值,然后在页面加载时检测这个值,如果有值就使用这个值,如果没有就使用系统的主题。
如何切换主题?这里我使用的是document.documentElement.classList,在<html>标签上添加dark类。
有两种方法可以区分颜色:
- 在
:root和:root.dark里定义颜色变量,然后在CSS里使用var(--<variable-name>)来使用变量。 - 或者在CSS里使用
<selector>.dark来选择黑暗模式下的样式。
推荐使用第一种方法,因为第二种方法会导致CSS文件变得很大,而且不利于维护。
主要部分代码实现
在主题的main.js中:
初始时,检测localStorage里是否有theme的值,如果有就使用这个值,如果没有就把theme设置为auto。
data(){ return { theme: localStorage.getItem("theme") || "auto", }}在页面加载时,检测theme,如果为auto则检测系统的主题并设置颜色,不是则直接设置颜色。
在beforeunload事件里,如果theme为auto则移除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.ejsWaline.init({ //..... dark: "html.dark",})这样Waline就会检测html标签是否有dark类,如果有就使用黑暗模式,没有就使用浅色模式。就适配完成了。
HighLightJS
HighLightJS的样式文件本身就是通过<link>引入的,官网上也提供了许多不同的主题,我们可以导入浅色和深色两套主题,在浅色时disabled深色那套,深色时取消disabled即可。
# 给出两套主题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属性即可:
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里的值把颜色设置好了。
