
本文介绍如何通过递归遍历 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><div class="aritcle_card flexRow">
<div class="artcardd flexRow">
<a class="aritcle_card_img" href="/ai/1470" title="Restorephoto"><img
src="https://img.php.cn/upload/ai_manual/000/000/000/175680377220952.jpg" alt="Restorephoto" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a href="/ai/1470" title="Restorephoto">Restorephoto</a>
<p>用AI修复旧的人像照片</p>
</div>
<a href="/ai/1470" title="Restorephoto" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span> </a>
</div>
</div><p><span>立即学习</span>“<a href="https://pan.quark.cn/s/cb6835dc7db1" style="text-decoration: underline !important; color: blue; font-weight: bolder;" rel="nofollow" target="_blank">前端免费学习笔记(深入)</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() 后过滤空字符串(如示例所示);若需保留格式(如
内容),则跳过此步。
-
属性与自闭合标签:当前实现仅生成基础标签名(如
会被简化为
,而非带属性的完整形式)。如需完整 outerHTML,请将 markup 字段改为 child.outerHTML,但需注意:
等自闭合标签无 ,此时应跳过结束标签推入逻辑。
- 性能考量:对超大 DOM(>10k 节点),递归可能触发栈溢出。生产环境可改用显式栈的迭代 DFS,但绝大多数编辑器场景下递归足够高效。
- 安全边界:本方案运行于已解析的 DOM 上,不涉及 innerHTML 动态执行,无 XSS 风险;但若输入 HTML 来源不可信,务必先通过 DOMPurify 等库净化再解析。
总结而言,递归遍历是解决 HTML 结构化分词问题的范式级方案——它以最小的认知成本,换取最高的正确性与可维护性。放弃手工模拟浏览器渲染逻辑,转而信任 DOM API 与树的天然递归特性,是构建稳健前端解析工具的关键思维跃迁。










