虽然内置的暗黑模式非常棒,但我觉得这个功能依然有不足,还可以进一步完善,比如实现手动切换配色和记忆配色。 在一番搜索后,发现目前网上针对 Next 主题切换配色的教程比较少而且都非常古老。大部分都采用了 hexo-next-darkmode。
虽然但是,实现确实是实现了切换配色,但实现的方式完全不是我想要的效果。需要手动点击切换按钮,更要命的是无法根据系统偏好方案自动切换。所以我就自己写了一个。
一些思路
CSS
经过一番调研(主要是看看国外各大码农聚集地是怎么实现的),大部分的网站都会在 html 标签内指定 <html class="dark" data-theme="dark"> CSS。然后使用 CSS 变量来实现配色的切换。
JS
JS 的需求也不是非常困难,喊个 GPT 当黑奴就好了,读取 localStorage 的值,如果有值就使用,没有就尊重系统偏好方案。
不正确的 JS 载入位置可能会导致页面加载闪烁。
举个例子,用户 A localStorage 里面存了一个为 dark 的值,而 Chrome Firefox 等浏览器在发现 html 中没有指定配色方案时,将会以白色为背景色渲染页面。
所以,在 JS 脚本完成主题方案的切换前,整个页面均为白色,而后,JS 加载完毕,切换主题为黑色,这个过程就会导致页面的闪烁。
根据 如何让静态网站的暗色主题换页不闪烁 所述,一个网页的加载流程是这样的:
- 用户请求网页
- 浏览器加载整个 HTML 文件,优先加载
<head>部分,遇到外链 CSS JS 时,将会暂停解析 DOM,将外链文件下载到本地。- 遇到
<style>或刚刚装载了外部的 CSS 就把样式表存成 CSSOM 并重新渲染有影响的部分;- 遇到
<script>或刚刚装载了外部的 JS 就停止解析并运行脚本,运行完脚本后再继续解析、渲染;- 直到页面加载完成
所以,将配色切换的 JS 放在 <body> 就会造成页面的闪烁,本质上就是一个 JS 加载顺序的问题。
nunjucks 模板(.njk)
由于我们需要手动切换配色,则必须要注入 HTML 用于 JS 绑定事件。 本来我是不太情愿直接在主题源文件下动刀的,以后得维护可能会变得很吃屎。
但是又一想,HTML 和 JS 可以采用注入、自定义脚本替换来改动,这样的实现也不会太脏。而 CSS 使用 _config.next.yml 中指定的 custom_file_path.style引入自定义文件的话,生成后的 main.css 文件将会变得非常大,而且涉及到非常多样式的覆盖,再加上我还想再手动改改 Next 主题的某些配色,一番综合考虑后,我觉得直接修改源文件是最好的选择。
修改 CSS
非常幸运的是,Next 自带了暗色模式的 CSS,而且绝大部分都是以变量的形式存在的,它们在这个位置 themes/next/source/css/_colors.styl。
所以只需要非常简单的将 @media (prefers-color-scheme: dark) 替换为 :root[data-theme='dark'],CSS 部分的大部分修改就结案了。
需要注意的是,如果你对 {% note %}…{% endnote %} 等等标签使用有需求的话,还需要在原来的样式上暴改一通。
大概需要改这几个文件:
themes/next/source/css/_common/scaffolding/tags/note.stylnote 标签们的样式themes/next/source/css/_variables/base.stylStylus 变量们themes/next/source/css/_colors.stylcss 变量们
对于我来说,我用了最笨蛋的改法,即直接全部写死为变量。
/* _colors.styl */
:root {
...;
--note-bg-default: $note-bg-default;
--note-bg-primary: $note-bg-primary;
--note-bg-info: $note-bg-info;
--note-bg-success: $note-bg-success;
--note-bg-warning: $note-bg-warning;
--note-bg-danger: $note-bg-danger;
}
:root[data-theme='dark'] {
...;
--note-bg-default: $note-bg-default-dark;
--note-bg-primary: $note-bg-primary-dark;
--note-bg-info: $note-bg-info-dark;
--note-bg-success: $note-bg-success-dark;
--note-bg-warning: $note-bg-warning-dark;
--note-bg-danger: $note-bg-danger-dark;
}/* base.styl */
$note-bg-default = lighten(spin($note-border.default, 0), 94% + $lbg);
$note-bg-primary = lighten(spin($note-border.primary, 10), 92% + $lbg);
$note-bg-info = lighten(spin($note-border.info, -10), 91% + $lbg);
$note-bg-success = lighten(spin($note-border.success, 10), 90% + $lbg);
$note-bg-warning = lighten(spin($note-border.warning, 10), 88% + $lbg);
$note-bg-danger = lighten(spin($note-border.danger, -10), 92% + $lbg);
$note-bg-default-dark = mix($note-bg-default, $body-bg-color-dark, 10%);
$note-bg-primary-dark = mix($note-bg-primary, $body-bg-color-dark, 10%);
$note-bg-info-dark = mix($note-bg-info, $body-bg-color-dark, 10%);
$note-bg-success-dark = mix($note-bg-success, $body-bg-color-dark, 10%);
$note-bg-warning-dark = mix($note-bg-warning, $body-bg-color-dark, 10%);
$note-bg-danger-dark = mix($note-bg-danger, $body-bg-color-dark, 10%);/* note.styl */
&.default {
background: var(--note-bg-default);
border-color: $note-border.default;
& h2, & h3, & h4, & h5, & h6 {
color: $note-border.default;
}
}
...
/* 如法炮制,一路给 primary info success warning danger 都写下去 */修改 JS
正如上文所述的,如果 JS 位置不对将会导致闪烁,所以注意 JS 注入的位置。下面核心代码都是 GPT 搓的,我是彩笔不会 JS,有优化建议欢迎留言~
hexo.extend.filter.register('theme_inject', function (injects) {
injects.head.raw('theme_switcher_head', `
<script>
// 获取当前实际主题
function getActualTheme(theme) {
const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (theme === 'system') {
return prefersDarkScheme ? 'dark' : 'light';
}
return theme;
}
function setCurrentThemeMark(currentTheme) {
const dropdownItems = document.querySelectorAll('.theme-dropdown .dropdown-item');
dropdownItems.forEach(item => {
item.classList.remove('current-theme'); // 移除所有项的 current-theme 类
if (item.dataset.theme === currentTheme) {
item.classList.add('current-theme'); // 给当前主题项添加 current-theme 类
}
});
}
// 设置主题
function setTheme(theme, needStoredTheme) {
const htmlElement = document.documentElement;
const actualTheme = getActualTheme(theme);
const isDark = actualTheme === 'dark';
htmlElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
htmlElement.classList.remove('light', 'dark');
htmlElement.classList.add(isDark ? 'dark' : 'light');
localStorage.setItem('theme', needStoredTheme);
document.addEventListener('DOMContentLoaded', function () {
setCurrentThemeMark(needStoredTheme);
});
}
let theme;
// 获取存储的主题
let storedTheme = localStorage.getItem('theme');
// 获取用户首选的颜色方案
const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (storedTheme === null) {
theme = prefersDarkScheme ? 'dark' : 'light';
storedTheme = 'system';
} else if (storedTheme === 'system') {
theme = prefersDarkScheme ? 'dark' : 'light';
} else {
theme = storedTheme;
}
setTheme(theme, storedTheme);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
let storedTheme = localStorage.getItem('theme');
if (storedTheme === 'system') {
setTheme('system', 'system');
} else if (storedTheme == null) {
setTheme('system', 'system');
}
});
</script>
`, {}, { cache: true });
injects.bodyEnd.raw('theme_switcher_body_end', `
<script>
document.addEventListener('DOMContentLoaded', function() {
function toggleTheme() {
const dropdown = document.querySelector('.theme-dropdown');
dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block';
}
document.querySelector('.theme-switcher')?.addEventListener('click', toggleTheme);
document.querySelectorAll('.theme-dropdown .dropdown-item').forEach(item => {
item.addEventListener('click', function() {
setTheme(this.dataset.theme, this.dataset.theme);
setCurrentThemeMark(this.dataset.theme);
});
});
document.addEventListener('click', function(e) {
if (!e.target.closest('.menu-item-theme-switcher')) {
document.querySelector('.theme-dropdown').style.display = 'none';
}
});
});
</script>
`, {}, { cache: true });
});修改 HTML
在你想要的地方加上这些 HTML:
<li class="menu-item menu-item-theme-switcher">
<a role="button" class="theme-switcher">
<i class="fa fa-solid fa-circle-half-stroke"></i>
</a>
</li>
<li class="menu-item theme-dropdown" style="display: none;">
<div class="dropdown-item" data-theme="system"><i class="fa fa-solid fa-wand-magic"></i><span>跟随系统</span></div>
<div class="dropdown-item" data-theme="light"><i class="fa fa-solid fa-sun"></i><span>总是浅色</span></div>
<div class="dropdown-item" data-theme="dark"><i class="fa fa-solid fa-moon"></i><span>总是深色</span></div>
</li>然后再加上这些 CSS:
.theme-dropdown {
display: none;
position: absolute;
top: 100%;
margin-top: 15px;
min-width: 96px;
padding: 10px 0;
background-color: var(--content-bg-color);
font-size: .9rem;
-webkit-touch-callout: none;
-webkit-user-select: none;
z-index: 1000;
}
.dropdown-item {
text-align: center;
&:hover {
background-color: var(--menu-item-bg-color);
}
}
.dropdown-item .fa {
margin-right: 5px;
}
.dropdown-item.current-theme {
color: var(--link-color);
}最后按照个人喜好自行调整即可,大概的效果你可以参考本站。
参考资料
- 如何让静态网站的暗色主题换页不闪烁
- 苏卡卡 抄了苏卡卡的样式和 HTML 结构