前言
最近给博客添加了 Giscus 评论系统并从 Utterances 做了迁移。看到 Giscus 官网 上支持白天、夜间模式,在晚上浏览这个网站,系统自动调整到深色模式,整个网站和浏览器黑色主题融为一体,观感非常好。回到自己的站点,浏览器顶部是深色,网页确实非常刺眼的白色,非常不搭。正好趁着最近有空维护,顺带优化一下。
方案
自定义深色模式 CSS 样式
周六上午花了两个多小时的时间把深色模式的 css 样式给写好了,准备加模式切换功能的时候才发现,原生主题的四种主题样式中就已经包含了深色模式。遂放弃了自定义深色模式样式,转用原生的深色主题。
样式切换
初始进入站点时,根据用户系统偏好设置自动加载浅色或深色模式。在导航中增加一个按钮 ☀️ | 🌙 用来手动切换主题,新增一个 JavaScript 文件用于给按钮新增监听事件。手动切换之后,浏览器本地缓存设置,用于后续页面加载时加载对应的样式。
不过由于原主题浅色 / 深色模式样式是写在不同的文件中的,而 Hexo 默认只会编译其中一个样式。因此要想实现切换,就必须将所有主题样式都编译出来,再动态的通过 JavaScript 改变。
这里采用的方案是新增几个和主题色对应的 styl 文件,分别 import 对应的主题样式,然后在 theme/_config.yml 文件中加入配置使几个样式文件同时编译:
1 2 3 4 5 6
| stylus: include: - 'source/css/theme-classic.styl' - 'source/css/theme-dark.styl' - 'source/css/theme-light.styl' - 'source/css/theme-wihte.styl'
|
样式文件如 theme-dark.styl 文件则在原样式名前增加一个前缀 theme–dark 以避免同时编译多个样式文件导致属性名称冲突报错。
1 2 3 4 5 6 7 8 9 10 11 12 13
| .theme--dark @import "_variables" @import "_colors/dark" @import "_util" @import "_mixins" @import "_extend" @import "_fonts"
global-reset()
*, *:before, *:after box-sizing: border-box
|
这样编译完之后,在 public/css 目录下就同时有了几套主题样式,别忘记在 head 相关的 ejs 文件中全部引入。
核心功能放在 switch-theme.js 文件中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
| const themes = [ { name: 'dark', className: 'theme--dark' }, { name: 'white', className: 'theme--white' } ];
document.addEventListener('DOMContentLoaded', () => { const toggleButton = document.getElementById('btn-theme-switch'); if (!toggleButton) return;
const userPref = localStorage.getItem('color-scheme'); const systemPref = window.matchMedia('(prefers-color-scheme: dark)').matches;
let isDark = false;
if (userPref === 'dark') { isDark = true; } else if (userPref === 'white') { isDark = false; } else { isDark = systemPref; }
const nextColorScheme = isDark ? 'dark' : 'white'; const nextTheme = themes.find(t => t.name === nextColorScheme);
setTheme(null, nextTheme); syncGiscusTheme(nextTheme);
toggleButton.textContent = isDark ? '☀️' : '🌙';
toggleButton.addEventListener('click', () => { const currentColorScheme = isDark ? 'dark' : 'white'; const currentTheme = themes.find(t => t.name === currentColorScheme); const nextColorScheme = isDark ? 'white' : 'dark'; const nextTheme = themes.find(t => t.name === nextColorScheme);
isDark = !isDark; toggleButton.textContent = isDark ? '☀️' : '🌙';
setTheme(currentTheme, nextTheme); syncGiscusTheme(nextTheme);
localStorage.setItem('color-scheme', nextTheme.name); });
window.addEventListener('message', (event) => { if (event.origin !== 'https://giscus.app') return; const colorScheme = localStorage.getItem('color-scheme'); const theme = themes.find(t => t.name === colorScheme); syncGiscusTheme(theme); });
const html = document.documentElement; const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.attributeName === 'class') { const isDark = html.classList.contains('theme--dark'); syncGiscusTheme(isDark ? themes[0] : themes[1]); } }); }); observer.observe(html, { attributes: true });
function setTheme(currentTheme, newTheme) { if (!newTheme) return; if (currentTheme) { document.documentElement.classList.remove(currentTheme.className); } document.documentElement.classList.add(newTheme.className); localStorage.setItem('color-scheme', newTheme.name); }
function syncGiscusTheme(theme) { if (!theme) return; const giscusFrame = document.querySelector('iframe.giscus-frame'); if (!giscusFrame) { if (window.giscusRetryCount === undefined) window.giscusRetryCount = 0; if (window.giscusRetryCount < 10) { window.giscusRetryCount++; setTimeout(() => syncGiscusTheme(theme), 100); } return; }
if (giscusFrame.contentWindow) { const frameOrigin = new URL(giscusFrame.src).origin;
giscusFrame.contentWindow.postMessage({ giscus: { setConfig: { defaultCommentOrder: "newest", theme: theme.name === 'white' ? 'light' : 'dark' } } }, frameOrigin); } else { setTimeout(() => syncGiscusTheme(themeName), 100); } } document.body.style.visibility = 'visible'; });
|
Giscus 深色模式切换
前面引入的 Giscus 使用 data-theme 预先定义好了主题样式。要手动切换,需要通过监听按钮事件,向 iframe 传递消息来实现。逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| function syncGiscusTheme(theme) { if (!theme) return; const giscusFrame = document.querySelector('iframe.giscus-frame'); if (!giscusFrame) { if (window.giscusRetryCount === undefined) window.giscusRetryCount = 0; if (window.giscusRetryCount < 10) { window.giscusRetryCount++; setTimeout(() => syncGiscusTheme(theme), 100); } return; }
if (giscusFrame.contentWindow) { const frameOrigin = new URL(giscusFrame.src).origin;
giscusFrame.contentWindow.postMessage({ giscus: { setConfig: { defaultCommentOrder: "newest", theme: theme.name === 'white' ? 'light' : 'dark' } } }, frameOrigin); } else { setTimeout(() => syncGiscusTheme(themeName), 100); } }
|
踩坑记录
加载新页面时白屏问题
测试的时候发现在深色模式下进入其他页面时,在页面加载的时候会出现白屏,页面加载完成后才会变成指定的颜色模式。
参考 日夜间模式切换功能 提出的优化方案,在 head 部分增加一个 style,给 body 设置一个不可见属性,页面加载完成后去掉这个不可见属性,顺利的解决了白屏问题。
head.ejs
1 2 3
| <style> body { visibility: hidden; } </style>
|
theme-switch.js
1 2 3 4
| document.addEventListener('DOMContentLoaded', () => { document.body.style.visibility = 'visible'; });
|
Giscus 样式切换不生效
本站点的颜色切换 OK,就是 Giscus 评论区一直不能动态切换,控制台报错 https://giscus.app/white 返回 404。
在参考 Hogo 中 Giscus 配置 时,发现该站点可以正常切换。原想发个评论请教一下怎么做的,随便打开控制台切换了两下,看到网络请求地址是 https://giscus.app/theme/dark.css 和 https://giscus.app/theme/light.css 。回到我的站点确认后发现我调用的地址是 https://giscus.app/theme/white.css ,是我的浅色主题名称和 Giscus 浅色主题名称不一致导致请求 404。做了名称转换就没问题了。
本地保存的颜色模式未清除
本地保存的颜色模式没有自动清理机制,导致在深色模式访问过站点,第二天白天访问时仍然读取到上一次的深色模式标识,没有自动切换主题颜色。
这个问题很好解决,每次加载页面保存颜色模式到本地时,另外保存一个有效期:30分钟。每次进入站点时自动检查该变量是够过期,如果过期则删除,走跟随系统主题颜色的逻辑即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function initColorThemeStorage() { const storedTheme = localStorage.getItem('color-scheme'); const expiryTime = localStorage.getItem('color-scheme-expiry');
if (storedTheme && expiryTime && Date.now() > Number(expiryTime)) { localStorage.removeItem('color-scheme'); localStorage.removeItem('color-scheme-expiry'); } }
function saveColorTheme(theme) { const expiry = Date.now() + 30 * 60 * 1000; localStorage.setItem('color-scheme', theme); localStorage.setItem('color-scheme-expiry', expiry.toString()); }
|
自定义 Giscus 样式
研究了许久未能实现,暂时放弃。
其他调整
将首页的 description 换成了一些自己喜欢的句子,并可以在 source/_data/sentence.json 中定义,每次进入页面随机展示其中一句。