Lumin Blog 阅读进度条与返回顶部功能完全指南
📌 概述
Lumin Blog 内置两个滚动相关的交互增强功能:
- 📜 阅读进度条(Reading Progress Bar) — 固定在页面顶部的细线,随文章阅读进度从左到右填充
- ⬆️ 返回顶部按钮(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 按钮:
| # | 位置 | 行数 | 状态 | 问题 |
|---|---|---|---|---|
| A | main.js BackToTop (L1148) | ~10行 | ✅ 生效 | 最终生效的版本,使用 .visible 类 |
| B | main.js RightSide (L737) | ~35行 | ⚠️ 冗余 | 使用 .show 类但 CSS 无对应规则,完全无效;额外绑定了一个无用的 scroll 监听器 |
| C | assets/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 精简方案(推荐)
| 步骤 | 操作 | 效果 |
|---|---|---|
| 1 | 从 RightSide.init() 中删除 back-to-top 相关逻辑 | 减少 ~35 行冗余代码,消除重复 scroll 监听 |
| 2 | 删除 assets/js/back-to-top.js | 清理 51 行死代码 |
| 3 | 将 ReadingProgress.init 暴露到 window | 修复 encrypt.js 兼容性 |
| 4 | 在 hugo.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;
📁 六、相关文件索引
| 文件 | 路径 | 作用 |
|---|---|---|
| 进度条 HTML | layouts/_default/baseof.html L59-63 | 条件渲染进度条 DOM |
| 进度条 CSS | assets/css/main.css L53-76 | 进度条样式 |
| 进度条 JS | assets/js/main.js L1647-1675 | ReadingProgress 模块 |
| 返回顶部 HTML | layouts/partials/toolbar.html L3-9 | 按钮模板 |
| 返回顶部 CSS | assets/css/main.css L4722-4797 | 按钮样式(含响应式) |
| 返回顶部 JS(主) | assets/js/main.js L1148-1159 | BackToTop 模块(生效版) |
| 返回顶部 JS(冗余A) | assets/js/main.js L737-772 | RightSide 模块中的重复逻辑 |
| 返回顶部 JS(死代码B) | assets/js/back-to-top.js | 独立文件,未被引用 |
| 模块注册 | assets/js/main.js L1696-1727 | DOMContentLoaded 统一初始化 |
| 加密兼容 | 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 / 模块(闭包变量) |
留言评论
期待你的想法评论加载中