📢 新文章推送 · 每周更新优质内容 · 订阅更新 →
向下滚动
技术笔记

Lumin Blog 阅读进度条与返回顶部功能完全指南

AI 智能总结

Lumin Blog 阅读进度条与返回顶部功能完全指南

📌 概述

Lumin Blog 内置两个滚动相关的交互增强功能:

  1. 📜 阅读进度条(Reading Progress Bar) — 固定在页面顶部的细线,随文章阅读进度从左到右填充
  2. ⬆️ 返回顶部按钮(Back to Top) — 页面右下角的浮动按钮,滚动超过阈值后出现,点击平滑回到顶部

这两个功能均通过 main.js 中的独立模块驱动,在页面加载时自动初始化。

📜 一、阅读进度条

1.1 功能说明

属性说明
显示位置固定在浏览器窗口最顶端(position: fixed; top: 0),z-index: 9999
适用范围仅在文章详情页single.html)渲染,首页/列表页/独立页面不显示
计算方式基于文章容器 .single-post 的相对位置,而非整个页面高度
视觉效果渐变色填充条 + 发光阴影,暗黑模式自动适配

1.2 HTML 结构

定义在 baseof.html 中,通过条件判断仅渲染于文章页:

1{{/* 阅读进度条(仅文章页显示) */}}
2{{ if and .IsPage (ne .Kind "home") (ne .Layout "archives") }}
3<div id="reading-progress" class="reading-progress-bar">
4  <div class="reading-progress-fill"></div>
5</div>
6{{ end }}

渲染条件:必须同时满足:

  • .IsPage — 是页面(非列表)
  • .Kind != "home" — 不是首页
  • .Layout != "archives" — 不是归档页

1.3 CSS 样式

定义在 main.css:53-76

 1.reading-progress-bar {
 2  position: fixed;
 3  top: 0;
 4  left: 0;
 5  width: 100%;
 6  height: 3px;
 7  z-index: 9999;
 8  background: transparent;
 9  pointer-events: none;
10}
11
12.reading-progress-bar .reading-progress-fill {
13  height: 100%;
14  width: 0%;
15  background: linear-gradient(90deg, var(--accent-color), var(--accent-hover));
16  border-radius: 0 2px 2px 0;
17  transition: width 0.15s ease-out;
18  box-shadow: 0 0 6px rgba(59,130,246,0.4);
19}
20
21[data-theme="dark"] .reading-progress-bar .reading-progress-fill {
22  box-shadow: 0 0 8px rgba(96,165,250,0.5);
23}

关键设计点

  • pointer-events: none — 不阻挡点击事件穿透到下方元素
  • transition: width 0.15s — 平滑过渡而非生硬跳变
  • 使用 CSS 变量 --accent-color / --accent-hover 自动跟随主题色

1.4 JavaScript 实现

定义在 main.js:1647-1675

 1var ReadingProgress = (function() {
 2  var bar, fill, article;
 3
 4  function update() {
 5    if (!article || !fill) return;
 6    var articleRect = article.getBoundingClientRect();
 7    var articleTop = articleRect.top + window.scrollY;
 8    var articleHeight = article.offsetHeight;
 9    var windowHeight = window.innerHeight;
10    // 文章顶部进入视口 30% 后开始计算进度
11    var scrolled = Math.max(0, window.scrollY - articleTop + windowHeight * 0.3);
12    var total = articleHeight - windowHeight * 0.3;
13    var progress = Math.min(100, Math.max(0, (scrolled / total) * 100));
14    fill.style.width = progress.toFixed(2) + '%';
15  }
16
17  function init() {
18    bar = document.getElementById('reading-progress');
19    fill = document.querySelector('.reading-progress-fill');
20    if (!bar || !fill) return;
21
22    article = document.querySelector('.single-post');
23    if (!article) return;
24
25    window.addEventListener('scroll', function() { update(); }, { passive: true });
26    update();
27  }
28
29  return { init: init };
30})();

算法解读

1进度 = (已滚过文章内容 / 文章总可读内容) × 100%
2
3其中:
4  已滚过 = 当前滚动位置 - 文章顶部位置 + 视口高度的30%
5  总可读   = 文章总高度 - 视口高度的30%
6  
7"视口高度30%"的偏移量让进度条在文章头部刚进入阅读区时为 0%,
8文章尾部离开阅读区前达到 100%,体验更自然。

性能优化:使用 { passive: true } 标记 scroll 监听器,告知浏览器不会调用 preventDefault(),允许浏览器对滚动事件进行批量处理以提升性能。

⬆️ 二、返回顶部按钮

2.1 功能说明

属性说明
显示位置固定在页面右下角(bottom: 32px; right: 32px
触发条件页面滚动距离超过 300px 时淡入显示
交互方式点击后使用 window.scrollTo({ behavior: 'smooth' }) 平滑回顶
视觉样式圆形按钮 + SVG 箭头图标 + 弹性缩放动画

2.2 HTML 结构

定义在 toolbar.html 中,受配置开关控制:

 1<div class="rightside" id="rightside">
 2  {{ if .Site.Params.backToTop.enable }}
 3  <button class="rightside-btn" id="back-to-top" aria-label="返回顶部">
 4    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"
 5         viewBox="0 0 24 24" fill="none" stroke="currentColor"
 6         stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 7      <line x1="12" y1="19" x2="12" y2="5"></line>
 8      <polyline points="5 12 12 5 19 12"></polyline>
 9    </svg>
10  </button>
11  {{ end }}
12</div>

⚠️ 注意:需要确保 hugo.toml 中配置了 [params.backToTop] enable = true 才会渲染此按钮。

2.3 CSS 样式

定义在 main.css:4722-4797

 1.back-to-top {
 2  position: fixed;
 3  bottom: 32px;
 4  right: 32px;
 5  width: 48px;
 6  height: 48px;
 7  background: var(--accent-color, #3b82f6);
 8  color: white;
 9  border: none;
10  border-radius: 50%;
11  cursor: pointer;
12  display: flex;
13  align-items: center;
14  justify-content: center;
15  box-shadow: 0 4px 12px rgba(0,0,0,0.15), 0 2px 6px rgba(0,0,0,0.1);
16  opacity: 0;
17  visibility: hidden;
18  transform: translateY(10px) scale(0.9);  /* 初始状态:下移+缩小 */
19  transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);  /* 弹性曲线 */
20  z-index: 100;
21}
22
23.back-to-top.visible {
24  opacity: 1;
25  visibility: visible;
26  transform: translateY(0) scale(1);  /* 显示状态:归位+正常大小 */
27}
28
29.back-to-top:hover {
30  background: var(--accent-hover, #2563eb);
31  transform: translateY(-4px) scale(1.05);  /* 悬停上浮+微放大 */
32  box-shadow: 0 8px 24px rgba(59,130,246,0.35), 0 4px 8px rgba(0,0,0,0.1);
33}
34
35.back-to-top:active {
36  transform: translateY(-2px) scale(0.98);  /* 按压反馈 */
37}

动画设计亮点

  • 使用 cubic-bezier(0.34, 1.56, 0.64, 1) 弹性缓动函数,产生轻微的过冲弹跳效果
  • 三种状态各有独立的 transform:隐藏(下移缩小)、可见(正常)、悬停(上浮放大)
  • 移动端自适应缩小至 44×44px,位置调整为 bottom: 20px; right: 20px

2.4 JavaScript 实现(当前生效版本)

定义在 main.js:1148-1159,这是当前实际生效的版本:

 1var BackToTop = (function() {
 2  var btn;
 3
 4  function init() {
 5    btn = document.getElementById('back-to-top');
 6    if (!btn) return;
 7
 8    window.addEventListener('scroll', function() {
 9      btn.classList.toggle('visible', window.scrollY > 300);
10    });
11
12    btn.addEventListener('click', function() {
13      window.scrollTo({ top: 0, behavior: 'smooth' });
14    });
15  }
16
17  return { init: init };
18})();

特点:极简实现,10 行核心代码。滚动超过 300px 时添加 .visible 类触发 CSS 过渡显示。

🔧 三、代码架构分析

3.1 模块注册机制

两个功能都注册在 main.js:1696-1727 的统一模块初始化系统中:

 1document.addEventListener('DOMContentLoaded', function() {
 2  var coreModules = [
 3    // ... 其他模块
 4    { name: 'BackToTop',        fn: BackToTop.init },
 5    // ...
 6    { name: 'ReadingProgress',  fn: ReadingProgress.init }
 7  ];
 8
 9  for (var i = 0; i < coreModules.length; i++) {
10    try {
11      coreModules[i].fn();
12    } catch(e) {
13      console.warn('[Lumin] ❌ 模块 ' + coreModules[i].name + ' 初始化失败:', e.message || e);
14    }
15  }
16});

每个模块用 try-catch 包裹,单个模块报错不会影响其他模块运行。

3.2 文件依赖关系图

 1baseof.html
 2├── 渲染 #reading-progress DOM(条件:文章页)
 3
 4├── 加载 main.js(包含全部 JS 模块)
 5   ├── ReadingProgress.init()
 6      ├── 查找 #reading-progress + .reading-progress-fill
 7      ├── 查找 .single-post 容器
 8      └── 绑定 scroll  update()
 9   
10   ├── BackToTop.init()           当前生效 
11      ├── 查找 #back-to-top
12      └── 绑定 scroll + click
13   
14   └── RightSide.init()            冗余副本 ⚠️
15       ├── 也查找 #back-to-top     ← 同一元素!
16       └── 也绑定 scroll + click    重复监听!
17
18└── partial toolbar.html
19    └── 渲染 #back-to-top 按钮(条件:backToTop.enable)

⚠️ 四、已知问题与技术债务

4.1 返回顶部:三重重复实现

当前存在 3 份返回顶部代码 操作同一个 #back-to-top 按钮:

#位置行数状态问题
Amain.js BackToTop (L1148)~10行✅ 生效最终生效的版本,使用 .visible
Bmain.js RightSide (L737)~35行⚠️ 冗余使用 .show 类但 CSS 无对应规则,完全无效;额外绑定了一个无用的 scroll 监听器
Cassets/js/back-to-top.js (独立文件)51行💀 死代码从未被任何模板引用加载,包含精美的 SVG 圆环进度功能

后果

  • 每次 scroll 事件触发 两次 回调(A 和 B 各一个)
  • back-to-top.js 占用磁盘空间但永远不执行
  • RightSide 的 rAF 节流逻辑白白消耗资源

4.2 encrypt.js 引用不存在的全局函数

encrypt.js:123-124 在密码解密成功后尝试重新初始化进度条:

1if (typeof window.initReadingProgress === 'function') {
2  window.initReadingProgress();
3}

ReadingProgress 是闭包内的局部变量,没有暴露到 window,所以这个条件永远不会满足。解密后进度条不会重新计算文章位置。

4.3 配置项缺失

hugo.toml 中缺少 [params.backToTop] enable = true 配置,如果模板严格遵循条件渲染,返回顶部按钮可能不会出现在页面上。

🛠️ 五、优化建议

5.1 精简方案(推荐)

步骤操作效果
1RightSide.init() 中删除 back-to-top 相关逻辑减少 ~35 行冗余代码,消除重复 scroll 监听
2删除 assets/js/back-to-top.js清理 51 行死代码
3ReadingProgress.init 暴露到 window修复 encrypt.js 兼容性
4hugo.toml 添加 [params.backToTop] enable = true确保按钮正确渲染
5(可选)将 back-to-top.js 中的 SVG 圆环进度合并进 BackToTop增强视觉效果

5.2 合并后的理想代码结构

BackToTop 模块(合并增强版)

 1var BackToTop = (function() {
 2  var btn, ring;
 3
 4  function update() {
 5    var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
 6    var docHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
 7    var pct = docHeight > 0 ? scrollTop / docHeight : 0;
 8
 9    // 按钮显隐
10    btn.classList.toggle('visible', scrollTop > 300);
11
12    // 圆环进度(可选)
13    if (ring) {
14      var c = 2 * Math.PI * 18;
15      ring.style.strokeDashoffset = c - (pct * c);
16    }
17  }
18
19  function init() {
20    btn = document.getElementById('back-to-top');
21    if (!btn) return;
22
23    ring = btn.querySelector('.btt-progress');
24
25    var ticking = false;
26    window.addEventListener('scroll', function() {
27      if (!ticking) {
28        requestAnimationFrame(update);
29        ticking = true;
30        ticking = false;  // 注意:实际应在回调内重置
31      }
32    }, { passive: true });
33
34    btn.addEventListener('click', function(e) {
35      e.preventDefault();
36      window.scrollTo({ top: 0, behavior: 'smooth' });
37    });
38
39    update();
40  }
41
42  return { init: init };
43})();
44
45// 暴露给外部调用(如 encrypt.js 解密后重新初始化)
46window.initReadingProgress = ReadingProgress.init;

📁 六、相关文件索引

文件路径作用
进度条 HTMLlayouts/_default/baseof.html L59-63条件渲染进度条 DOM
进度条 CSSassets/css/main.css L53-76进度条样式
进度条 JSassets/js/main.js L1647-1675ReadingProgress 模块
返回顶部 HTMLlayouts/partials/toolbar.html L3-9按钮模板
返回顶部 CSSassets/css/main.css L4722-4797按钮样式(含响应式)
返回顶部 JS(主)assets/js/main.js L1148-1159BackToTop 模块(生效版)
返回顶部 JS(冗余A)assets/js/main.js L737-772RightSide 模块中的重复逻辑
返回顶部 JS(死代码B)assets/js/back-to-top.js独立文件,未被引用
模块注册assets/js/main.js L1696-1727DOMContentLoaded 统一初始化
加密兼容assets/js/encrypt.js L123-124解密后重新初始化进度条
配置开关hugo.toml [params.backToTop]控制按钮是否渲染

📊 七、性能指标参考

指标数值
进度条 DOM 节点数2 个(bar + fill)
返回顶部 DOM 节点数1 个(button + 内联 svg)
scroll 监听器数量(当前)3 个(应精简为 1 个)
单次 scroll 回调耗时< 0.1ms(纯 DOM 计算,无重排)
进度条更新频率每次 scroll 事件触发(由浏览器控制,通常 60fps)
内存占用(JS 对象)~200 bytes / 模块(闭包变量)
4 / 14
版权声明

本文作者 Lumin

本文链接 https://www.zhengquan.xyz/tech/reading-progress-backtotop/

许可协议 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

请作者喝杯咖啡 ☕

  • 微信打赏
    微信支付
  • 支付宝打赏
    支付宝
点击按钮查看打赏二维码
🎁 推荐工具
试试这些实用在线工具,提升工作效率
前往工具集 →

留言评论

期待你的想法

评论加载中