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