侧边目录自动滚动应使用intersectionobserver监听章节容器而非标题,设threshold为0.6并结合getboundingclientrect校验可见性,统一坐标系;目录动态更新需mutationobserver监听正文变动;侧边栏滚动失效需用子元素padding替代父容器padding。

侧边目录自动滚动不是靠“监听 scrollTop 然后算哪个标题离顶部最近”就能稳的——那样会高频误判、跳变、卡顿,尤其在动态加载或标题密集时。真正能落地的方案,是用 IntersectionObserver 观察章节容器,配合 getBoundingClientRect() 做边界校验,再节流更新高亮状态。
为什么 IntersectionObserver 观察 <h2></h2> 会出问题
直接让 IntersectionObserver 监听所有 <h2></h2> 元素,看似简单,实则失效率高:标题本身高度小(常只有 20–30px),刚滚进视口就触发 isIntersecting: true,但用户实际还没看到正文;如果页面有吸顶导航栏,rootMargin 很难统一适配;更麻烦的是,Markdown 渲染后标题可能被包裹在 <div class="markdown"> 里,DOM 结构不干净,观察目标错位。<p>✅ 正确做法是:把每个章节内容包裹进 <code><section></section>(或带唯一 id 的 <div>),然后观察这些容器。判断“当前章节”的依据是:该容器在视口内可见区域是否 ≥ 60%(不是像素值,是比例)。<ul>
<li>给每个 <code><section id="sec-intro"></section> 添加 data-toc-id="sec-intro",方便和目录项关联
IntersectionObserver 配置 { threshold: [0.6] },只在可见比例达 60% 时回调entries,取第一个 isIntersecting === true 的 entry,它的 target.dataset.tocId 就是当前高亮项滚动时目录项高亮不同步?检查 top 和 offsetTop 的基准差异
目录点击跳转用 element.scrollIntoView({ block: 'start', behavior: 'smooth' }),但滚动后高亮没变,大概率是“当前章节”判定逻辑和跳转逻辑用了两套坐标系:前者基于 IntersectionObserver(相对视口),后者基于 offsetTop(相对文档)。两者在有固定头部、缩放、字体加载延迟等场景下会偏差几十像素。
✅ 解决办法是统一用 getBoundingClientRect() 校验:
- 在滚动监听回调里,不直接信
entry.isIntersecting,而是补一句:const rect = entry.target.getBoundingClientRect(); if (rect.top window.innerHeight * 0.2) - 这个范围(顶部在视口前 40% 内、底部在视口后 20% 内)比单纯看
isIntersecting更抗干扰 - 避免用
document.documentElement.scrollTop做计算——它在 iOS Safari 中可能不准,window.pageYOffset更可靠
目录生成后,正文增删标题,目录不更新?MutationObserver 没配对
很多脚本只在 DOMContentLoaded 时跑一次 document.querySelectorAll('h2, h3'),之后 Markdown 异步渲染新标题、或者用户用 JS 插入内容,目录就彻底失联。这不是功能缺陷,是设计遗漏。
将调研报告、方案对比、数据分析、需求梳理、会议纪要、设计说明、代码评审、学习笔记等任务产物输出为可在浏览器直接打开的单文件 HTML,替代 Markdown 作为最终交付物。Markdown 在信息密度、视觉清晰度和可分享性上已跟不上 AI 产物的丰富度,HTML 能同时承载排版、表格、SVG 等。
✅ 必须用 MutationObserver 监听正文容器(比如 <article class="content"></article>)的子节点变动:
- 观察选项设为
{ childList: true, subtree: true },确保新增/删除/移动标题都能捕获 - 回调里只处理
addedNodes,过滤出含<h2></h2>~<h4></h4>的节点,再调用目录重建函数 - 重建前先清空旧目录项,但保留容器 DOM 节点,避免重绘闪烁
- 提取标题文本必须用
node.textContent,不能用innerHTML——否则<code>fetch()会原样塞进目录项里
侧边栏本身滚动失效?别在 .sidenav 上写 padding-top
常见写法:.sidenav { height: 496px; padding-top: 60px; overflow-y: auto; }。看起来没问题,但 padding-top 不参与滚动区域计算,真实可用高度只剩 436px;当目录项展开(比如点击“性能优化”展开子项),内容高度超限,overflow-y: auto 却不显示滚动条——因为浏览器认为“没溢出”,只是 padding 把内容往下挤了。
✅ 正解是分层:让 .sidenav 只管滚动容器职责,内边距交给子元素:
.sidenav {
position: fixed;
top: 0;
left: 0;
height: 100%;
overflow-y: auto;
}
.sidenav > .toc-inner {
padding-top: 60px;
}
这样 .sidenav 的滚动上下文完全由内容撑开,.toc-inner 的 padding 只影响内部布局,不干扰溢出判定。移动端也一样适用——iOS Safari 对 height: 100vh 的解析不稳定,100% 更可靠。










