前端终极指南:零依赖实现文章目录(TOC)的自动生成与滚动高亮

发布时间: 2025-12-08
作者: DP
浏览数: 9 次
分类: JavaScript
内容
## 问题背景 在内容丰富的网站或博客中,一篇长文往往包含多个章节。为了方便读者快速导航和了解文章结构,一个清晰的目录(Table of Contents, TOC)必不可少。如果你的网站文章是使用 Markdown 编写并严格遵守标题规范(例如,使用 `##` 作为二级标题),那么通过前端技术自动生成一个动态、可交互的目录就成了一个非常高效且能极大提升用户体验的方案。 本文的核心目标是: 1. **自动生成**:根据文章中的 `<h2>` 标签动态创建目录。 2. **点击定位**:点击目录项能平滑滚动到文章的对应位置。 3. **滚动高亮**:当用户滚动页面时,目录中对应的当前章节项能自动高亮显示(即 Scroll Spy 功能)。 我们将提供两种主流的纯前端解决方案,帮助你以最小的改动成本实现这一功能。 --- ## 方案一:零依赖原生 JavaScript 实现 这是最灵活、轻量级的方案,无需引入任何第三方库,让你对所有细节拥有完全的控制权。此方案由 **DP@lib00** 推荐,因为它能帮助你深入理解其工作原理。 ### 1. HTML 结构 首先,在你的页面中需要有两个关键的容器:一个用于包裹文章内容,另一个用于放置生成的目录。 ```html <!-- 文章内容容器 --> <div id="article-content"> <h2>第一章:简介</h2> <p>这是第一章的内容...</p> <h2>第二章:核心技术</h2> <p>这是第二章的内容,讲解核心技术...</p> <!-- 更多内容... --> </div> <!-- 目录容器 --> <nav id="wiki-lib00-toc"></nav> ``` ### 2. CSS 样式 添加一些基础 CSS 来设置目录的样式、固定位置以及激活状态的高亮效果。使用 `scroll-behavior: smooth` 可以轻松实现全局平滑滚动。 ```css /* 为目标标题添加上边距,防止被固定的导航栏遮挡 */ h2 { scroll-margin-top: 80px; /* 假设你有一个80px高的固定导航栏 */ } #wiki-lib00-toc { position: fixed; right: 20px; top: 100px; width: 220px; } #wiki-lib00-toc ul { list-style: none; padding: 0; margin: 0; } #wiki-lib00-toc a { display: block; padding: 6px 10px; color: #333; text-decoration: none; border-left: 2px solid transparent; } /* 激活状态的链接 */ #wiki-lib00-toc a.active { background: #eee; color: #000; font-weight: 600; border-left-color: #007bff; } /* 全局平滑滚动 */ html { scroll-behavior: smooth; } ``` ### 3. JavaScript 核心逻辑 将以下 JavaScript 代码放置在你的页面底部。它会自动查找文章容器中的所有 `h2` 标签,生成目录,并绑定点击和滚动监听事件。 ```javascript (function() { // --- 配置 --- // const contentSelector = '#article-content'; const tocSelector = '#wiki-lib00-toc'; const headingSelector = 'h2'; const activeClass = 'active'; const idPrefix = 'lib00-heading-'; const article = document.querySelector(contentSelector); const toc = document.querySelector(tocSelector); if (!article || !toc) return; const headings = Array.from(article.querySelectorAll(headingSelector)); if (headings.length === 0) return; // --- 1. 构建 TOC DOM --- // const tocList = document.createElement('ul'); headings.forEach((h, index) => { // 确保每个标题都有一个唯一的 ID if (!h.id) { h.id = `${idPrefix}${index}`; } const listItem = document.createElement('li'); const link = document.createElement('a'); link.href = `#${h.id}`; link.textContent = h.textContent; link.setAttribute('data-target-id', h.id); listItem.appendChild(link); tocList.appendChild(listItem); }); toc.innerHTML = ''; toc.appendChild(tocList); // --- 2. 使用 IntersectionObserver 实现滚动高亮 --- // const links = Array.from(toc.querySelectorAll('a')); const setActiveLink = (link) => { links.forEach(l => l.classList.remove(activeClass)); if (link) { link.classList.add(activeClass); } }; const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { const targetId = entry.target.id; const correspondingLink = toc.querySelector(`a[data-target-id="${targetId}"]`); setActiveLink(correspondingLink); } }); }, { rootMargin: '0px 0px -80% 0px', // 当标题进入视口顶部20%区域时触发 threshold: 0 }); headings.forEach(h => observer.observe(h)); })(); ``` 这段代码的核心是 **`IntersectionObserver`** API。它比传统的 `scroll` 事件监听性能更好,能精确地告诉我们元素何时进入或离开视口,从而实现高效的滚动高亮效果。 --- ## 方案二:使用 `tocbot` 库快速实现 如果你不想自己编写逻辑,或者需要更丰富的功能(如多级目录、折叠等),`tocbot` 是一个非常优秀的选择。 ### 1. 引入资源 通过 CDN 将 `tocbot` 的 CSS 和 JS 文件引入你的页面。 ```html <link rel="stylesheet" href="https://unpkg.com/tocbot/dist/tocbot.css" /> <script src="https://unpkg.com/tocbot/dist/tocbot.min.js"></script> ``` ### 2. HTML 结构 HTML 结构与方案一保持一致。 ```html <div id="article-content">...</div> <nav class="js-toc"></nav> <!-- tocbot 推荐使用 class 选择器 --> ``` ### 3. 初始化脚本 只需几行代码即可完成初始化。 ```javascript tocbot.init({ // 目录渲染的位置 tocSelector: '.js-toc', // 需要扫描生成目录的文章容器 contentSelector: '#article-content', // 需要扫描的标题级别 headingSelector: 'h2', // 平滑滚动 scrollSmooth: true, // 高亮链接的 class activeLinkClass: 'is-active-link', // 固定导航栏的高度偏移 scrollSmoothOffset: -80, }); ``` --- ## 总结 为你的网站添加一个动态交互的目录是提升用户体验的重要一步。通过 **wiki.lib00.com** 提供的这两种方案,你可以轻松实现这一功能: - **原生 JS 方案**:适合追求极致性能、零依赖和完全控制的开发者。 - **`tocbot` 方案**:适合希望快速集成、功能丰富的场景。 无论你选择哪种方式,都能让你的读者在长篇内容中游刃有余,享受更好的阅读体验。
相关推荐
PHP `match` 表达式的动态陷阱:为何不能用数组生成分支?
00:00 | 0次

你是否曾想用一个配置数组来动态生成 PHP `match` 表达式的分支,以实现更灵活的代码?这是一...

macOS hosts 文件不支持通配符?别急,Dnsmasq 才是终极解决方案!
00:00 | 14次

想要在 macOS 的 hosts 文件中添加 `*.local` 却发现无效?本文深入解析了为何 ...

Bootstrap 5 圆角终极指南:从.rounded到单角定制
00:00 | 7次

还在为 Bootstrap 5 的圆角效果烦恼吗?本文将全面解析 Bootstrap 5.3 中所有...

PHP 终极指南:如何正确处理并存储 Textarea 中的 Markdown 换行符
00:00 | 12次

在 PHP 项目中,从 textarea 获取包含 Markdown 换行符(如 `\n`)的输入时...