Hexo-Next 主题修改 (1) -- 实现暗黑模式的手动、自动切换


虽然内置的暗黑模式非常棒,但我觉得这个功能依然有不足,还可以进一步完善,比如实现手动切换配色和记忆配色。 在一番搜索后,发现目前网上针对 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 加载完毕,切换主题为黑色,这个过程就会导致页面的闪烁。

根据 如何让静态网站的暗色主题换页不闪烁 所述,一个网页的加载流程是这样的:

  1. 用户请求网页
  2. 浏览器加载整个 HTML 文件,优先加载 <head> 部分,遇到外链 CSS JS 时,将会暂停解析 DOM,将外链文件下载到本地。
  3. 遇到 <style> 或刚刚装载了外部的 CSS 就把样式表存成 CSSOM 并重新渲染有影响的部分;
  4. 遇到 <script> 或刚刚装载了外部的 JS 就停止解析并运行脚本,运行完脚本后再继续解析、渲染;
  5. 直到页面加载完成

所以,将配色切换的 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.styl note 标签们的样式
  • themes/next/source/css/_variables/base.styl Stylus 变量们
  • themes/next/source/css/_colors.styl css 变量们

对于我来说,我用了最笨蛋的改法,即直接全部写死为变量。

/* _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,有优化建议欢迎留言~

/script/theme/index.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);
}

最后按照个人喜好自行调整即可,大概的效果你可以参考本站。

参考资料



评论加载中……