1. 实现思路

首先先要判断当前页面是否是文章,然后获取文章的各级标题并生成目录,并添加点击跳转到相应标题的功能。完整代码在https://github.com/lxmghct/lxmghct.github.io/blob/master/_includes/table_of_contents.html

2. 实现过程

2.1 获取标题并生成目录

先获取所有的标题h1-h6,然后遍历生成目录。生成目录时考虑到有些文章并不是从h1开始的,有可能h2或者h3才是第一级小标题,所以要先获取所有的标题,然后才能判断目录的层级。

为了方便后面的点击跳转,需要给每个标题添加一个id,如果标题没有id,则根据标题内容生成一个id。这样直接把#id添加到链接的href属性上就可以实现点击跳转。

// 获取文章内容的标题
const headings = document.querySelectorAll(".post-content h1, post-content h2, .post-content h3, .post-content h4, .post-contenth5, .post-content h6");
if (headings.length === 0) {
    document.querySelector(".toc-container").classList.add("hidden");
} else {
    document.querySelector(".toc-container").classList.remove("hidden");
}
// 目录容器
const tocList = document.getElementById("toc-list");
const tempTocData = []
// 遍历标题,生成目录
headings.forEach((heading) => {
    const level = parseInt(heading.tagName.charAt(1), 10); // 获取标题级别
    const listItem = document.createElement("li");
    const link = document.createElement("a");
    link.textContent = heading.textContent;
    if (!heading.id) {
        heading.id = heading.textContent.replace(/\s+/g, "-").toLowerCase();
    }
    link.href = `#${heading.id}`;
    listItem.appendChild(link);
    tocList.appendChild(listItem);
    tempTocData.push({
        level: level,
        dom: listItem
    })
});

2.2 调整目录格式

根据上一步获取的目录层级,设置左边距,使目录结构看起来更加清晰。

const minLevel = Math.min(...tempTocData.map(item => item.level))
tempTocData.forEach(item => {
    let diff = item.level - minLevel
    item.dom.style.marginLeft = diff * 20 + "px"
    if (diff === 0) {
        item.dom.classList.add("root-toc")
    }
})

2.3 判断文章页并使用目录

将该目录组件封装成了table_of_content.html组件,然后在文章页引入该组件。判断当前页面是否是文章的方式是判断page.idpage.date是否存在。

{% if page.id or page.date %}
    {% include table_of_content.html %}
{% endif %}

3. 样式调整

3.1 平滑滚动

点击目录跳转到相应标题时,添加平滑滚动效果。

html, body {
    scroll-behavior: smooth;
}

3.2 目录定位方式

我想把目录固定放在页面最左侧,不随滚动条滚动,本来想设置position: fixed,但是由于我页面的整体布局是有一个header导航栏,而且导航栏已经设置成了会随滚动条滚动,并非一直固定在页面顶部。所以如果此时目录是fixed定位,会出现目录和导航栏重叠的情况,而且整体观感也不好。所以我选择了sticky定位,目的是当导航栏滚动出屏幕前,目录也会随着滚动,当导航栏滚动到屏幕外时,目录会固定在页面最上方。这样既不会和导航栏重叠,浏览起来也比较舒服。

<div class="toc-container">
  <div class="toc-header">
    <h3>目录导航</h3>
  </div>
  <ul id="toc-list"></ul>
</div>
.toc-container {
    position: sticky;
    position: -webkit-sticky; /* 兼容 Safari */
    top: 30px;
    padding-top: 10px;
}

#toc-list {
    height: 70vh;
    overflow-y: auto;
    list-style: none;
    margin: 0;
    padding: 0;
}

4. 效果展示

效果展示