将 HTML 文本节点与标记节点精准分离为结构化对象数组

7次阅读

将 HTML 文本节点与标记节点精准分离为结构化对象数组

本文介绍如何通过递归遍历 dom 树,将任意 html 片段解析为严格按渲染顺序排列的对象数组,每个对象明确标识为纯文本(text)或 HTML 标记(markup),避免正则误匹配与节点顺序错乱问题。

本文介绍如何通过递归遍历 dom 树,将任意 html 片段解析为严格按渲染顺序排列的对象数组,每个对象明确标识为纯文本(`text`)或 html 标记(`markup`),避免正则误匹配与节点顺序错乱问题。

在处理富文本内容、实现自定义 HTML 解析器、构建可视化编辑器或进行语义化内容提取时,常需将原始 HTML 拆解为“可编程操作的原子单元”——即区分哪些是用户可见的纯文本内容,哪些是控制结构的 HTML 标签。关键挑战在于:必须严格保持 DOM 渲染时的节点顺序,尤其当嵌套元素交错出现(如

文本链接另一链接

)时,简单线性遍历 TreeWalker 容易导致闭合标签提前插入、子节点内容丢失或父子层级错位。

此时,递归深度优先遍历(DFS)是最自然且健壮的解决方案。它天然契合 DOM 的树形结构:对每个元素节点,先推入其开始标签,再递归处理全部子节点,最后推入其结束标签;对文本节点,则直接提取 textContent 并封装为 {text: “…”} 对象。该策略完全规避了手动追踪父级、预判闭合时机、拼接 outerHTML 等复杂逻辑,代码简洁、语义清晰、结果可靠。

以下为完整实现:

/**  * 将 DOM 节点树解析为扁平化的标记-文本混合数组  * @param {Node} root - 起始 DOM 节点(如 document.body 或任意 HTMLElement)  * @returns {Array<{text: string} | {markup: string}>}  */ function parseHtmlToTokens(root) {     const tokens = [];      function walk(node) {         for (const child of node.childNodes) {             if (child.nodeType === Node.TEXT_NODE) {                 // 过滤空白文本节点(可选优化)                 const text = child.textContent.trim();                 if (text.length > 0) {                     tokens.push({ text });                 }             } else if (child.nodeType === Node.ELEMENT_NODE) {                 // 推入开始标签(小写化保证一致性)                 tokens.push({                      markup: `<${child.tagName.toLowerCase()}>`                  });                 // 递归处理子树                 if (child.hasChildNodes()) {                     walk(child);                 }                 // 推入结束标签                 tokens.push({                      markup: `</${child.tagName.toLowerCase()}>`                  });             }             // 忽略注释节点(Node.COMMENT_NODE)、文档类型等非渲染节点         }     }      walk(root);     return tokens; }  // 使用示例 const htmlString = ` <h2 id="mcetoc_1h1m1ll27l">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</h2> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris at tincidunt lectus.   <a href="https://www.sadasdas.es" aria-invalid="true">tr</a>   <a title="titulo" href="https://www.sadasdas.es" aria-invalid="true">adsf afjdasi k</a>   <a title="titlee" href="https://www.sadasdas.es" aria-invalid="true">asdsssssssssssss</a>   <a href="https://www.sadasdas.es" aria-invalid="true">s</a> </p> <p><a href="https://www.sadasdas.es" aria-invalid="true">Lorem Ipsum</a></p> `;  // 创建临时容器解析 HTML 字符串 const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlString; const result = parseHtmlToTokens(tempDiv); console.log(result);

输出效果(节选):

[   {"markup": "<h2>"},   {"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."},   {"markup": "</h2>"},   {"markup": "<p>"},   {"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris at tincidunt lectus."},   {"markup": "<a>"},   {"text": "tr"},   {"markup": "</a>"},   {"markup": "<a>"},   {"text": "adsf afjdasi k"},   {"markup": "</a>"},   // ... 后续节点依序排列 ]

⚠️ 注意事项与最佳实践:

  • 空白文本处理:textContent 会包含换行与缩进空格。如需纯净语义文本,建议 .trim() 后过滤空字符串(如示例所示);若需保留格式(如
    内容),则跳过此步。
  • 属性与自闭合标签:当前实现仅生成基础标签名(如 将 HTML 文本节点与标记节点精准分离为结构化对象数组 会被简化为 将 HTML 文本节点与标记节点精准分离为结构化对象数组,而非带属性的完整形式)。如需完整 outerHTML,请将 markup 字段改为 child.outerHTML,但需注意:将 HTML 文本节点与标记节点精准分离为结构化对象数组 等自闭合标签无 ,此时应跳过结束标签推入逻辑。
  • 性能考量:对超大 DOM(>10k 节点),递归可能触发溢出。生产环境可改用显式栈的迭代 DFS,但绝大多数编辑器场景下递归足够高效。
  • 安全边界:本方案运行于已解析的 DOM 上,不涉及 innerHTML 动态执行,无 xss 风险;但若输入 HTML 来源不可信,务必先通过 DOMPurify 等库净化再解析。

总结而言,递归遍历是解决 HTML 结构化分词问题的范式级方案——它以最小的认知成本,换取最高的正确性与可维护性。放弃手工模拟浏览器渲染逻辑,转而信任 DOM API 与树的天然递归特性,是构建稳健前端解析工具的关键思维跃迁。

text=ZqhQzanResources