和 比 更适合动态索引,因为二者具有明确语义: 表示逻辑分块(需含 –), 表示独立可复用内容单元(须含 或 ),索引脚本能据此精准提取标题锚点与内容范围,避免依赖易变的 class 名匹配。

为什么 <section></section> 和 <article></article> 比 <div> 更适合动态索引
<p>因为浏览器和索引脚本能直接识别语义标签的层级与作用域,<code><section></section> 表示逻辑分块,<article></article> 表示独立可复用内容单元——索引生成器(如基于 MutationObserver 的实时目录)可据此跳过样式容器,精准提取标题锚点和内容范围。
常见错误是把所有区块都套 <div class="section">,结果脚本只能靠 class 名硬匹配,一旦 class 改名或复用规则变化,索引就断掉。
<ul>
<li>
<code><section></section> 应包裹带 <h2></h2>–<h6></h6> 的完整子结构,不能嵌套同级 <section></section> 而不设标题
<article></article> 内部必须有明确的 <h1></h1> 或 <header></header>,否则索引可能漏掉该条目<nav></nav> 或 <aside></aside> 里放主内容标题——索引脚本通常会忽略这些区域如何用 document.querySelectorAll() 安全提取索引节点
直接写 document.querySelectorAll('section > h2, article > h1') 看似简单,但实际容易漏掉多级标题或被 Shadow DOM 阻断。关键不是“找什么”,而是“在哪找”和“怎么判重”。
真实场景中,用户滚动时动态插入新 <section></section>,旧索引不更新;或者 CMS 输出的 HTML 混用 <h3></h3> 作二级标题却没包在 <section></section> 里。
- 优先用
document.body.querySelectorAll(':is(section, article) > :is(h1, h2, h3)'),兼容标题深度浮动 - 对每个匹配节点检查
closest('section, article')是否存在且未被aria-hidden="true"屏蔽 - 用
node.id || node.textContent.trim().toLowerCase().replace(/\s+/g, '-')生成锚点,但必须去重——相同文本的<h2></h2>可能出现在不同<section></section>中
IntersectionObserver 监听滚动时,语义标签如何影响性能
监听 <section></section> 元素比监听所有 <h2></h2> 更高效:前者数量少、生命周期稳定;后者易受富文本编辑器插入临时标题干扰,触发频繁回调。
一个典型坑是给每个 <section></section> 都配独立 observer 实例——内存占用翻倍且难以管理。应该只用一个 observer,目标设为所有 <section></section>,再通过 entries[i].target 区分当前活跃区块。
- 初始化时用
new IntersectionObserver(cb, { rootMargin: '0px 0px -50% 0px' }),让标题刚进入视口顶部就触发,避免用户已看到内容才高亮索引项 - 如果页面含
<iframe></iframe>或 Web Component,需显式遍历其 shadowRoot 并调用observer.observe(),语义标签不会自动穿透 -
<section hidden></section>会被 observer 忽略,但display: none不会——语义隐藏应优先用hidden属性
服务端预生成索引时,为什么不能只依赖 innerHTML 解析
Node.js 里用 jsdom 解析 HTML,若原始文档混用 <div role="region"> 和 <code><section></section>,仅靠标签名会误判结构层级。语义完整性必须结合 ARIA role 和隐含含义交叉验证。
比如 <section aria-labelledby="foo"><h2 id="foo">标题</h2></section> 是标准写法;但若写成 <section aria-labelledby="foo"><p id="foo">标题</p></section>,索引脚本会取到 <p></p> 文本,却无法识别其标题语义。
- 预处理阶段应校验:每个
<section></section>至少含一个<h2></h2>–<h6></h6>,或明确设置了aria-labelledby且对应元素存在 - 用
cheerio时,$('section').filter((i, el) => $(el).find('h2, h3').length)比正则匹配更可靠 - 生成的索引 JSON 中,保留
level字段(来自标签名h2→2),别靠 class 名推断——class="heading-3"可能实际是<h4></h4>
contenteditable 动态编辑时,用户删掉 <h2></h2> 却没删 <section></section>,这时索引项还在,但指向空内容——得监听 input 事件做实时校验,而不是只靠初始解析。











