由于之前使用的文章导读插件性能过于臃肿,导致资源消耗高且难以维护,因此我决定自行开发一个轻量级的文章导读功能。参考了郄郄私语的 案例文章,我实现了以下四个主要功能:
- 根据文章内容自动生成目录
- 点击目录跳转至相应章节
- 滚动阅读时,目录随当前章节自动切换
- 目录跟随文章内容滚动
目标
本文旨在实现以下功能:
- 自动生成文章目录
- 点击目录跳转至对应章节
- 目录根据当前阅读章节自动高亮
- 目录随文章内容滚动
根据文章内容自动生成目录
实现自动生成目录的前提是文章内容须有清晰的层次结构,这需要使用 h1
至 h6
标签来定义标题。通过 JavaScript,我们可以提取这些标题来生成目录:
var articleTitleList = $('.joe_detail__article').find('h1, h2, h3, h4, h5, h6');
articleTitleList.each(function() {
const headingLevel = $(this).prop("tagName").toLowerCase();
const headingName = $(this).text().trim();
console.log(headingLevel, headingName);
$('#catalogs').append(`<div class="catalog catalog-${headingLevel}">${headingName}</div>`);
});
在样式表中为目录项定义缩进样式:
.catalog-h2 { margin-left: 1em; }
.catalog-h3 { margin-left: 2em; }
.catalog-h4 { margin-left: 3em; }
.catalog-h5 { margin-left: 4em; }
.catalog-h6 { margin-left: 5em; }
点击目录跳转至对应章节
点击目录跳转至相应章节可以通过为每个标题添加锚点。我们将在 h1
至 h6
标签上添加 id
:
articleTitleList.each(function() {
const headingName = $(this).text().trim();
const enHeadingName = encodeURIComponent(headingName);
$(this).attr('id', `${enHeadingName}`);
$('.posts-nav-lists>ul').append(`<li><a href="#${enHeadingName}">${headingName}</a></li>`);
});
目录自动切换至当前章节
为了让目录随着阅读位置自动切换,我们将监控用户的滚动位置并依据此更新目录的高亮条目:
const catalogTrack = () => {
let $currentHeading = $('h1');
articleTitleList.each(function() {
const $heading = $(this);
if ($heading.offset().top - $(document).scrollTop() > 20) return false; // 退出条件
$currentHeading = $heading;
});
// 更新目录状态
const anchorName = $currentHeading.attr('id');
$('.posts-nav-lists>ul>li').removeClass('active');
$(`#title-${anchorName}`).parent().addClass('active');
};
目录随文章内容滚动
为确保当前章节始终可见,我们控制目录区域的滚动:
if ($catalog.length > 0) {
$('.posts-nav-box').scrollTop($catalog[0].offsetTop - 50); // 确保可见
}
性能优化
为了避免在滚动时频繁调用 catalogTrack
导致性能降低,我们可以使用函数节流技术:
function throttle(fn, wait) {
let lastTime = 0;
return function() {
const now = Date.now();
if (now - lastTime >= wait) {
fn.apply(this, arguments);
lastTime = now;
}
};
}
window.addEventListener('scroll', throttle(catalogTrack, 500)); // 每500毫秒调用一次
自定义跳转行为
考虑到固定导航条可能遮挡目标章节,我们可以自定义跳转行为,确保目标元素不会被遮挡:
document.querySelectorAll('.posts-nav-lists>ul>li>a').forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault(); // 阻止默认跳转
const targetId = link.getAttribute('href').substring(1);
const targetElement = document.getElementById(targetId);
const headerHeight = document.querySelector('.joe_header').offsetHeight;
const scrollTop = $(targetElement).offset().top - headerHeight - 10;
window.scrollTo({ top: scrollTop, behavior: 'smooth' });
});
});
总结
本文记录了生成和控制博客文章目录的思路与方法,针对不同需求可能会有其他解决方案。欢迎大家在评论区讨论和分享建议,感谢阅读!
完整代码示例
HTML 部分
<section class="joe_aside__item posts-nav-box">
<div class="joe_aside__item-title"><i class="fa fa-list-ul"></i><span class="text">文章目录</span><span class="line"></span></div>
<div class="joe_aside__item-contain">
<div class="posts-nav-lists"><ul class="bl nav"></ul></div>
</div>
</section>
JavaScript 部分
var articleTitleList = $('.joe_detail__article').find('h1, h2, h3, h4, h5, h6');
if (articleTitleList.length > 0) {
// 目录生成及事件绑定
(function () {
for (let heading of articleTitleList) {
const headingLevel = heading.tagName.toUpperCase();
const $heading = $(heading);
// console.log($heading);
const headingName = $heading.text().trim();
const enHeadingName = encodeURIComponent(headingName);
$heading.attr('id', `${enHeadingName}`);
$('.posts-nav-lists>ul').append(`<li class="n-${headingLevel}"><a id="title-${enHeadingName}" href="#${enHeadingName}">${headingName}</a></li>`);
// const anchorName = $heading.attr('id');
// console.log(headingLevel, headingName);
}
if (Joe.IS_MOBILE) {
$('.joe_action').append(`<div class="joe_action_item posts-nav-switcher"><i class="fa fa-list-ul"></i></div>`)
let joe_aside = $('.joe_aside .joe_aside__item.posts-nav-box .joe_aside__item-contain').html();
$('.joe_aside .joe_aside__item.posts-nav-box').remove();
let html = document.createElement('div');
html.className = 'posts-nav-box';
html.innerHTML = joe_aside;
// console.log(html);
$('.posts-nav-switcher').append(html);
$('.joe_action_item .posts-nav-box').css({
'position': 'absolute',
'display': 'none',
'right': '50px',
'bottom': '0px',
'padding': '15px',
'border-radius': 'var(--radius-wrap)',
'box-shadow': '0 0 10px 8px var(--main-shadow)',
'overflow': 'auto',
'max-height': '50vh',
'max-width': '80vw'
})
$('.joe_action_item.posts-nav-switcher').click(() => {
$('.joe_action_item.posts-nav-switcher .posts-nav-box').fadeToggle(200);
});
}
const catalogTrack = () => {
// console.log('页面滚动标题监听');
let $currentHeading = $('h1');
for (let heading of articleTitleList) {
const $heading = $(heading);
if ($heading.offset().top - $(document).scrollTop() > $('.joe_header').height()) {
break;
}
$currentHeading = $heading;
const anchorName = $currentHeading.attr('id');
const $catalog = $(document.getElementById(`title-${anchorName}`)).parent();
if (!$catalog.hasClass('active')) {
$('.posts-nav-lists>ul>li').removeClass('active');
$catalog.addClass('active');
}
if ($catalog.length > 0) {
if ($('.posts-nav-box .joe_aside__item-contain').length > 0) {
$('.posts-nav-box .joe_aside__item-contain').scrollTop($catalog[0].offsetTop - 50);
} else {
$('.posts-nav-box').scrollTop($catalog[0].offsetTop - 50);
}
} else {
$('.posts-nav-lists').scrollTop(0);
}
}
};
/**
* 函数节流,时间戳方案
* @param {*} fn
* @param {*} wait
* @returns
*/
function throttle(fn, wait) {
var pre = Date.now();
return function () {
var context = this;
var args = arguments;
var now = Date.now();
if (now - pre >= wait) {
fn.apply(context, args);
pre = Date.now();
}
}
}
window.addEventListener('scroll', throttle(() => {
catalogTrack();
}, 500));
// 监听文章目录a标签点击
document.querySelectorAll('.posts-nav-lists>ul>li>a').forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault(); // 阻止默认跳转行为
// 获取目标元素 ID
const targetId = link.getAttribute('href').substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
// 获取目标元素距离页面顶部的距离
const targetTop = $(targetElement).offset().top;
// console.log(targetTop);
// 获取顶栏高度,考虑动态高度变化和内部元素
const headerHeight = document.querySelector('.joe_header').offsetHeight;
// 计算滚动位置
const scrollTop = (targetTop - headerHeight) - 10; // 预留10px的美观距离
window.scrollTo({
top: scrollTop,
behavior: 'smooth'
});
} else {
console.error('目标元素未找到:', targetId);
}
});
});
}())
} else {
$('.joe_aside__item.posts-nav-box').remove();
}
通过以上修改,文章的可读性、结构性和实用性得以提升,使读者能够更好地理解和使用所提供的功能。