JavaScript 实现文章目录索引导航 (TOC) 详解

作者:异步漫步时间:2026-10-23 18:14:51浏览:125

本文详解如何使用 JavaScript 实现文章目录索引(TOC)功能。核心包含两点:利用 HTML 锚点实现点击跳转,以及通过监听滚动事件动态计算当前阅读段落并高亮对应目录项。文章提供了基于 jQuery 的完整源码、在线演示及 GitHub 仓库地址,并探讨了后端生成目录以优化 SEO 的方案。

JavaScript 实现文章目录索引导航 (TOC) 详解

TOC 即 Table of Content,意为文章目录索引。

具体效果可参考 Hexo 博客中的实际演示,例如访问示例站点即可体验。

实现该功能主要包含两个核心要点:

第一,点击目录跳转至对应段落,这通过 HTML <a> 标签的锚点机制实现,其具体原理可查阅相关文档。第二,滚动时目录项自动变换高亮状态,这需要 JavaScript 监听页面的滚动事件,实时判断用户当前所处的阅读段落,并更新目录中对应项的高亮样式。为此,我编写了一个简单的 Demo 来直观展示这一效果。

完整源码已开源,地址为:https://github.com/owenliang/js-toc

您可以访问以下链接在线体验实际效果:http://owenliang.github.io/js-toc

实现逻辑分析

接下来对实现逻辑进行具体分析。

页面结构上,#toc 元素负责展示左侧目录列表,#content 元素则承载右侧的文章正文内容。


 <div id="toc">
  <ul>

  </ul>
 </div>
 <div id="content">
  <a name="seg-1" class="seg-begin"><h1>第1章节</h1></a>
  <div class="seg-content"></div>
  <a name="seg-2" class="seg-begin"><h1>第2章节</h1></a>
  <div class="seg-content"></div>
  <a name="seg-3" class="seg-begin"><h1>第3章节</h1></a>
  <div class="seg-content"></div>
  <a name="seg-4" class="seg-begin"><h1>第4章节</h1></a>
  <div class="seg-content"></div>
 </div>

通过 CSS 样式控制,使 #toc 固定在左侧显示,并将当前激活的目录项文字设为红色;同时设置 #content 靠左偏移以留出目录空间,并填满屏幕剩余宽度。


  #toc {
   width: 200px;
   position: fixed;
   left: 0;
   top: 0;
  }
  #toc a.active {
   color: red;
  }
  #content {
   margin-left: 200px;
  }

在上述静态页面结构中,目录区域初始为空,因为具体的目录列表需要通过 JavaScript 动态生成。

在正文内容中,需要人工预先标记各段落的起始位置,即添加 class 为 seg-begin 的 <a> 标签作为锚点。每个段落的锚点 name 属性必须唯一,且该锚点之后紧跟着对应的段落内容。

在 JavaScript 逻辑中,首先按照锚点在文档中出现的顺序,收集所有 class 为 seg-begin 的元素并保存到 segs 数组中,该数组的顺序即对应文章自上而下的阅读顺序。随后,根据这些锚点关联的 <h1> 标签文本,构建出 #toc 区域内的 <ul> 列表结构。


    var segs = [];
    $(".seg-begin").each(function (idx, node) {
     segs.push(node)
     var link = $("<a></a>").attr("href", "#" + $(node).attr("name")).html($(node).children("h1").html())
     if (!idx) {
      link.addClass("active")
     }
     var row = $("<li></li>").append(link)
     $("#toc ul").append(row)
    })

接着,为浏览器窗口绑定 scroll 滚动事件进行监听。每次发生滚动时,程序会判断最近一个滚出屏幕顶部的 seg-begin 节点,该节点即代表用户当前正在阅读的段落。


$(window).bind("scroll", function() {
     var scrollTop = $(this).scrollTop()
     var topSeg = null
     for (var idx in segs) {
      var seg = segs[idx]
      if (seg.offsetTop > scrollTop) {
       continue
      }
      if (!topSeg) {
       topSeg = seg
      } else if (seg.offsetTop >= topSeg.offsetTop) {
       topSeg = seg
      }
     }
     if (topSeg) {
      $("#toc a").removeClass("active")
      var link = "#" + $(topSeg).attr("name")
      console.log('#toc a[href="' + link + '" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ]')
      $('#toc a[href="' + link + '" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ]').addClass("active")
      // console.log($(topSeg).children("h1").text())
     }
    })

后续优化方向

后续优化方向。

目前目录的生成完全依赖前端 JavaScript 根据正文锚点动态构建。为了提升搜索引擎优化(SEO)效果,建议在文章发布时由后端直接匹配正文中的锚点信息,并将生成的目录结构直接保存为静态 HTML,从而避免客户端渲染开销。

以下是完整的实现代码。


<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <style>
  * {
   margin: 0;
   padding: 0;
   word-break: break-all;
  }
  #toc {
   width: 200px;
   position: fixed;
   left: 0;
   top: 0;
  }
  #toc a.active {
   color: red;
  }
  #content {
   margin-left: 200px;
  }
 </style>
 <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
 <script>
  $(document).ready(function () {
   for (var i = 0; i < 50; ++i) {
    $(".seg-content").append("<p>一个段落而已</p>")
   }

   (function () {
    var segs = [];
    $(".seg-begin").each(function (idx, node) {
     segs.push(node)

     var link = $("<a></a>").attr("href", "#" + $(node).attr("name")).html($(node).children("h1").html())
     if (!idx) {
      link.addClass("active")
     }
     var row = $("<li></li>").append(link)
     $("#toc ul").append(row)
    })

    $(window).bind("scroll", function() {
     var scrollTop = $(this).scrollTop()

     var topSeg = null
     for (var idx in segs) {
      var seg = segs[idx]
      if (seg.offsetTop > scrollTop) {
       continue
      }
      if (!topSeg) {
       topSeg = seg
      } else if (seg.offsetTop >= topSeg.offsetTop) {
       topSeg = seg
      }
     }
     if (topSeg) {
      $("#toc a").removeClass("active")

      var link = "#" + $(topSeg).attr("name")
      console.log('#toc a[href="' + link + '" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ]')
      $('#toc a[href="' + link + '" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" ]').addClass("active")
      // console.log($(topSeg).children("h1").text())
     }
    })
   })()
  })
 </script>
</head>
<body>
 <div id="toc">
  <ul>

  </ul>
 </div>
 <div id="content">
  <a name="seg-1" class="seg-begin"><h1>第1章节</h1></a>
  <div class="seg-content"></div>
  <a name="seg-2" class="seg-begin"><h1>第2章节</h1></a>
  <div class="seg-content"></div>
  <a name="seg-3" class="seg-begin"><h1>第3章节</h1></a>
  <div class="seg-content"></div>
  <a name="seg-4" class="seg-begin"><h1>第4章节</h1></a>
  <div class="seg-content"></div>
 </div>
</body>
</html>

此外,当前示例未实现嵌套层级的目录结构。经过观察 Hexo 的实现方式,发现可以通过 h1、h2、h3 等标题标签的层级关系来表达嵌套结构。在遍历生成目录时,利用这些标题标签的信息即可完成嵌套层级的标记,从而解决该问题。

作者最新文章